如何优化全表扫描的任务

672 查看

背景

有些daily job或者weekly job是需要对某些表做全表扫描的,尤其是内部系统的运维性质、统计汇总性质的报表。

一个简单需要扫表的例子:某业务有百万甚至更多的用户,运营团队希望掌握这个业务的用户的活跃情况和流失情况,可能要设置几个指标进行分析。比如这个账户的本周做A操作的次数,做A操作的次数的周环比,做A操作的平均时间间隔...。每个用户都需要去查其他几个表做一些可能比较复杂计算才能得到这个用户的所有指标。

现在任务就是将所有用户每个指标每周更新一次,也就是说需要User表全表扫描。

Ruby为例

1

假如User表并不大,将所有id取出来放到内存再去遍历是很快的。

User.all.only(:id).entries.map(&:id).each{|user_id|
  get_metric1(user_id)
  get_metric2(user_id)
  #...
}

2

User表巨大的情况,使用方法1,内存有压力,使用游标
如mongoid中使用游标是这样的,这里说游标不准确,但是原理是一样的

User.db.collection("users").find({some conditions}, {:timeout => false}){|cursor|
  get_metric1(cursor['id'])
  get_metric1(cursor['id'])
  #...
}

3

游标可以解决User表很大的情况,但是游标的效率是很低的,结合一下两者的优势,做batch query,比如使用了分页的包(gem kaminari),如果没有用分页的包,自己实现一个分页是一样的。

total = User.count
page_count = (total / 1000) + 1 # set batch size to 1000
(1..page_count).each{|page|
  User.all.only(:id).page(page).per(1000).entries.map(&:id).each{|userid|
    get_metric1(user_id)
    get_metric2(user_id)
    #...
  }
}

方法3实际就是平衡了时间和空间,用有限的资源尽快跑完job。