前段时间遇到的一个问题,在这里记录下来。
需求:批量的将表A中的status 创建 到表B中 主键为id
例:
表offers id status --> 表offer_scores id offer_id status
1 online 1
2 offline 2
想要通过表offers得到表offer_scores,也就是批量创建。
看起来没有什么难度,尝试写一下试试看
# offers数据迁移至offer_scores
Offer.find_each(batch_size: 1000) do |offer|
OfferScore.create(offer_id: offer.id)
end
好了 运行起来也没问题。
不过当我的数据有6万条的时候 (我的实际情况),性能问题就出现了,它跑得太慢了(大约跑了一个半小时还在跑,最后我不得不中断了它)
思考了一下之后,我决定优化一下这段语句
我查找了一下 发现了一个 好用的方法 叫做 find_in_batches
并且配合了一个gem 叫做 activerecord-import
去文档查看了一下用法, 用法很简单 和 find_in_batches 搭配很不错
这里边记录一下 find_in_batches的用法
通过查询文档 我们可以看出
find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil)
Person.where("age > 21").find_in_batches do |group|
sleep(50) # Make sure it doesn't get too crowded in there!
group.each { |person| person.party_all_night! }
end
Person.find_in_batches.with_index do |group, batch|
puts "Processing group ##{batch}"
group.each(&:recover_from_last_night!)
end
它的查询是可以附带参数的batch_size 这个可以看出 默认的参数是一次查询1000条,恩符合我们要求,
另外 查询之后他会将我们查询的1000条数据放入 group中,好了,现在我们就可以对第一个创建方法进行改良了
Offer.find_in_batches do |group|
scores = []
scores = group.map { |offer| OfferScore.new(offer_id: offer.id)}
OfferScore.import scores
end
首先我们按照一次查询1000的条件 把1000条查询出来,然后新建了 数组,将group 进行遍历 并new了一个 目标对象 存入了 details 中,然后执行 OfferScoreDetail.import details, 这是gem提供的方法,后边是附带参数,这个方法会将我们提供的details集合中的所有对象一次性创建出来。
然后我将上面的代码重运行一次,6w条数据大约执行了100秒左右,相对于第种还是有很大提升的
优化的思路:
如果逐条执行,将会产生大量的数据库查询和创建语句,并且rails每次和数据库交互都会产生链接语句
这些语句如果过多都会产生数据库性能问题,所以类似此类问题,可以优先尝试降低数据库连接次数,查询 次数来提高速度。
上边说了批量创建的问题,下边来说一下批量更新的问题。
假设还是上边的两张表
例:
表offers id status --> 表offer_scores id offer_id status
1 online 1 1
2 offline 2 2
我们的目标是把表offers中的 status更新到表offer_scores中
Offer.find_each(batch_size: 1000) do |offer|
OfferScore.find_by(offer_id: offer.id).update_columns(status: offer.status)
end
这句已经可以完成我们想要的情况了(我们此次更新使用了update_columns 不触发回调),如果只是简单的更新数据到另一个表 这样子虽然可行,但是显而易见的 性能还是很差,试着跑一下6W条数据,还是非常慢的。
如果不触发回调机制,也不更新索引只是单纯的更新数据,利用SQL语句是不是要更好呢?来试试看另一种方法。
ActiveRecord::Base.connection.exec 利用这个的方法我们就可以直接写SQL 语句了
ActiveRecord::Base.connection.execute("UPDATE offers JOIN offer_scores ON offers.id = offer_scores.offer_id SET offers.status = offer_scores.score")
利用上边这条SQL 6w条数据大约不到2秒就能执行完毕,速度是非常快了,但是有时候我们的业务很复杂并不是简简单单的更新数据就可以,有时候我们需要更新索引的时候就不能采用这种方式,我们要触发更新索引的回调。
例:
表offers id status --> 表offer_scores id offer_id status
1 online 1 1
2 offline 2 2
我们的目标是把表offers中的 status更新到表offer_scores中
同样的任务,但是这次我们需要触发回调
Offer.find_each(batch_size: 1000) do |offer|
OfferScore.find_by(offer_id: offer.id).update_attributes(status: offer.status)
end
利用 update_attributes就可以触发回调了,当然问题还是性能,首先我们考虑到是不是可以采用事务将更新语句打包一起提交而不是逐条更新呢?尝试一下。
Offer.find_each(batch: 1000) do |offer|
ActiveRecord::Base::transaction do
OfferScore.find_by(offer_id: offer.id).update_attributes(status: offer.status)
end
end
这样我们将所有的更新语句都放在事务中,不过这样子虽然所有的更新请求都是一起提交,减少了链接数据库的次数,但是如果一个事务中条目过多恐怕也会处理不过来,不如把每1000条作为一个事务发起请求。
Offer.find_in_batches do |offer_group|
ActiveRecord::Base::transaction do
offer_group.each do |offer|
OfferScore.find_by(offer_id: offer.id).update_attributes(status: offer.status)
end
end
end
这样 我们每次查询1000条打包扔到事务中,事务中将会产生1000条更新请求,我们作为一个请求,链接数据库进行更新。
优化数据库连接方法还有很多,以上方法还有很多可优化之处,以后再来添加。