ActiveRecord find_each结合限制和顺序

我试图使用ActiveRecord的find_each方法运行约50,000条logging的查询,但似乎忽略了我的其他参数,如下所示:

 Thing.active.order("created_at DESC").limit(50000).find_each {|t| puts t.id } 

而不是停在50,000我希望和created_atsorting,这是得到的查询得到执行整个数据集:

 Thing Load (198.8ms) SELECT "things".* FROM "things" WHERE "things"."active" = 't' AND ("things"."id" > 373343) ORDER BY "things"."id" ASC LIMIT 1000 

有没有办法得到类似的行为find_each但总的最大限制,并尊重我的sorting标准?

该文档说find_each和find_in_batches不保留sorting顺序和限制,因为:

  • 在PK上对ASC进行sorting用于批量订购工作。
  • 限制用于控制批量大小。

你可以像@rorra一样编写你自己的这个函数的版本。 但是当你改变对象的时候你可能会遇到麻烦。 例如,如果按照created_atsorting并保存该对象,则可能会在下一批中再次出现该对象。 同样,您可能会跳过对象,因为在执行查询以获取下一个批次时,结果顺序已更改。 只能将该解决scheme与只读对象一起使用。

现在我主要关心的是,我不想一次加载30000多个对象到内存中。 我关心的不是查询本身的执行时间。 因此,我使用了一个解决scheme来执行原始查询,但只cachingID。 然后它将ID数组划分成块并查询/创build每个块的对象。 这样,您可以安全地变异对象,因为sorting顺序保存在内存中。

这是一个与我所做的相似的简单例子:

 batch_size = 512 ids = Thing.order('created_at DESC').pluck(:id) # Replace .order(:created_at) with your own scope ids.each_slice(batch_size) do |chunk| Thing.find(chunk, :order => "field(id, #{chunk.join(',')})").each do |thing| # Do things with thing end end 

这个解决scheme的权衡是:

  • 完整的查询被执行以获取ID
  • 所有ID的数组都保存在内存中
  • 使用MySQL特定的FIELD()函数

希望这可以帮助!

find_each底层使用了find_in_batches

find_in_batches中所述,无法selectlogging的顺序将自动设置为在主键(“id ASC”)上升序以使批次sorting工作。

但是,标准是应用的,你可以做的是:

 Thing.active.find_each(batch_size: 50000) { |t| puts t.id } 

关于限制,它还没有实现: https : //github.com/rails/rails/pull/5696


回答第二个问题,你可以自己创build逻辑:

 total_records = 50000 batch = 1000 (0..(total_records - batch)).step(batch) do |i| puts Thing.active.order("created_at DESC").offset(i).limit(batch).to_sql end 

首先检索ids并处理in_groups_of

 ordered_photo_ids = Photo.order(likes_count: :desc).pluck(:id) ordered_photo_ids.in_groups_of(1000).each do |photo_ids| photos = Photo.order(likes_count: :desc).where(id: photo_ids) # ... end 

还要将ORDER BY查询添加到内部调用中,这一点很重要。

我正在寻找相同的行为,并想到这个解决scheme。 这不是由created_at命令,但我想我会张贴反正。

 max_records_to_retrieve = 50000 last_index = Thing.count start_index = [(last_index - max_records_to_retrieve), 0].max Thing.active.find_each(:start => start_index) do |u| # do stuff end 

这种方法的缺点: – 你需要2个查询(第一个应该是快) – 这保证了最大的50Klogging,但如果ids被跳过,你会得到更less。

一种select是将针对您的特定模型量身定制的实现放入模型本身(对此, id通常是订购logging的更好select, created_at可能有重复):

 class Thing < ActiveRecord::Base def self.find_each_desc limit batch_size = 1000 i = 1 records = self.order(created_at: :desc).limit(batch_size) while records.any? records.each do |task| yield task, i i += 1 return if i > limit end records = self.order(created_at: :desc).where('id < ?', records.last.id).limit(batch_size) end end end 

否则,你可以概括一些东西,并使其适用于所有模型:

lib/active_record_extensions.rb

 ActiveRecord::Batches.module_eval do def find_each_desc limit batch_size = 1000 i = 1 records = self.order(id: :desc).limit(batch_size) while records.any? records.each do |task| yield task, i i += 1 return if i > limit end records = self.order(id: :desc).where('id < ?', records.last.id).limit(batch_size) end end end ActiveRecord::Querying.module_eval do delegate :find_each_desc, :to => :all end 

config/initializers/extensions.rb

 require "active_record_extensions" 

PS我把代码放在文件根据这个答案 。

你可以通过标准的ruby迭代器向后迭代:

 Thing.last.id.step(0,-1000) do |i| Thing.where(id: (i-1000+1)..i).order('id DESC').each do |thing| #... end end 

注意: +1是因为BETWEEN将在查询中包含两个边界,但我们只需要包含一个边界。

当然,使用这种方法,可以批量取得less于1000条logging,因为其中一些已经被删除了,但在我的情况下这是可以的。

你可以尝试一批批gem。

从他们的文档你可以做这样的事情

 Users.where(country_id: 44).order(:joined_at).offset(200).as_batches do |user| user.party_all_night! end 

做一个查询,避免迭代:

User.offset(2).order('name DESC').last(3)

会产生这样的查询

SELECT "users".* FROM "users" ORDER BY name ASC LIMIT $1 OFFSET $2 [["LIMIT", 3], ["OFFSET", 2]