Gem ActiveRecord 用 bulk_insert 来批量插入数据,提高效率

huacnlee · 2016年04月06日 · 最后由 rails_taotao 回复于 2018年01月08日 · 7351 次阅读

实际的项目环境中,我们时常会有一次动作写入多条数据的场景,比如导数据,创建通知、Event 给多个人...

简单做法可能大家会直接循环插入

receiver_ids.each do |user_id|
  Event.create(user_id: user_id, event_type: 'test')
end

或者你可能会在外面包个 transaction 来减少 Commit 次数,从而微微的提速,但还是有 N 次数据库的调用

Event.transaction do
 receiver_ids.each do |user_id|
   Event.create(user_id: user_id, event_type: 'test')
 end
end

或者你还知道 SQL 批量插入的方法:

INSERT INTO events (event_type, user_id) VALUES("test", 2),("test", 5),("test", 8) ...;

ref: MySQL, PostgreSQL

关于第三种方式,有 Gem 帮助我们简单来实现:

https://github.com/jamis/bulk_insert

# Gemfile
gem 'bulk_insert'

然后我们就可以用了:

Event.bulk_insert(set_size: 100) do |worker|
  receiver_ids.each do |user_id|
    worker.add({ user_id: user_id, event_type: 'test' })
  end
end

相关链接

不想用 gem 的话以下代码可实现该功能

class ActiveRecord::Base
  def self.import!(record_list)
    raise ArgumentError "record_list not an Array of Hashes" unless record_list.is_a?(Array) && record_list.all? {|rec| rec.is_a? Hash }
    key_list, value_list = convert_record_list(record_list)
    sql = "INSERT INTO #{self.table_name} (#{key_list.join(", ")}) VALUES #{value_list.map {|rec| "(#{rec.join(", ")})" }.join(" ,")}"
    self.connection.insert_sql(sql)
  end

  def self.convert_record_list(record_list)
    key_list = record_list.map(&:keys).flatten.uniq.sort

    value_list = record_list.map do |rec|
      list = []
      key_list.each {|key| list <<  ActiveRecord::Base.connection.quote(rec[key]) }
      list
    end

    return [key_list, value_list]
  end
end

#5 楼 @ywjno 插入数据有很多细节场景,这段代码在你的项目里面实际跑过么?

#6 楼 @huacnlee 跑过的,用在 定时任务里面 创建通知消息里面

#8 楼 @hooopo

大概 130w 数据用时 16 秒,平均每秒插入 8w 条记录左右。

... 暴快啊!

postgresql 直接 copy 暴快

#11 楼 @gihnius pg 的 copy 不支持 replace 选项啊,在做增量插入的时候好麻烦。pg 9.5 才支持 UPSERT,但 copy 还不行。

正好碰到这个需求了。刚改成Event.transaction do,准备改成第三种,真是有用👍

测试了下效果挺好的,但是如果 Model 层有 validation 的话,并不会触发。

canonpd 展示一下自己的第一个 Rails 作品 - clwy.cn 提及了此话题。 03月22日 18:30
需要 登录 后方可回复, 如果你还没有账号请 注册新账号