我正在尝试使用ActiveRecord的find_each
方法运行大约50,000条记录的查询,但它似乎忽略了我的其他参数:
Thing.active.order("created_at DESC").limit(50000).find_each {|t| puts t.id }
而不是停留在50,000我喜欢并按created_at
排序,这是通过整个数据集执行的结果查询:
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
获得类似的行为,但总的最大限额并尊重我的排序标准?
答案 0 :(得分:57)
The documentation表示find_each和find_in_batches不会保留排序顺序和限制,因为:
你可以像@rorra那样编写自己的这个函数版本。但是在改变对象时你会遇到麻烦。例如,如果按create_at排序并保存对象,则可能会在下一批次中再次出现。同样,您可能会跳过对象,因为在执行查询以获取下一批时,结果的顺序已更改。仅将该解决方案与只读对象一起使用。
现在我主要担心的是我不想一次将30000+个对象加载到内存中。我担心的不是查询本身的执行时间。因此,我使用了一个执行原始查询但只缓存ID的解决方案。然后它将ID数组分成块,并按块查询/创建对象。这样您就可以安全地改变对象,因为排序顺序保存在内存中。
这是一个类似于我所做的最小例子:
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
此解决方案的权衡取决于:
希望这有帮助!
答案 1 :(得分:24)
无法选择记录的顺序,如 find_in_batches 中所述,会自动设置为在主键(“id ASC”)上升序,以使批量订购工作。
但是,应用了标准,您可以做的是:
Thing.active.find_each(batch_size: 50000) { |t| puts t.id }
关于限制,尚未实施:https://github.com/rails/rails/pull/5696
回答第二个问题,您可以自己创建逻辑:
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
答案 2 :(得分:15)
首先检索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
查询添加到内部调用中非常重要。
答案 3 :(得分:4)
一种选择是将针对您的特定模型定制的实现放入模型本身(说到哪个,id
通常是订购记录的更好选择,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"
P.S。我根据this answer将代码放入文件中。
答案 4 :(得分:3)
您可以通过标准的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包含两个边界但我们只需要包含一个边界。
当然,使用这种方法可以批量获取少于1000条记录,因为其中一些已被删除但在我的情况下这是可以的。
答案 5 :(得分:2)
我一直在寻找相同的行为,并想到了这个解决方案。这不是由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个查询(第一个应该很快) - 这可以保证最多50K的记录,但是如果跳过id,你会得到更少的记录。
答案 6 :(得分:2)
您可以尝试 ar-as-batches 宝石。
从他们的documentation你可以做这样的事情
Users.where(country_id: 44).order(:joined_at).offset(200).as_batches do |user|
user.party_all_night!
end
答案 7 :(得分:2)
如@Kirk在评论之一中所述,<WebView>
从5.1.0版本开始支持find_each
。
变更日志中的示例:
limit
我们会遵守限制,如果存在限制,则不需要批量大小:可以小于,等于或大于限制。
(尽管仍不支持设置自定义订单)
答案 8 :(得分:1)
Rails 6.1为find_each
,find_in_batches
和in_batches
降序添加support。
答案 9 :(得分:0)
使用Kaminari或其他方法会很容易。
plot_data <- your_data[!your_data$Response %in% "Not sure",]
boxplot(Crops~Response, data=plot_data)
module BatchLoader
extend ActiveSupport::Concern
def batch_by_page(options = {})
options = init_batch_options!(options)
next_page = 1
loop do
next_page = yield(next_page, options[:batch_size])
break next_page if next_page.nil?
end
end
private
def default_batch_options
{
batch_size: 50
}
end
def init_batch_options!(options)
options ||= {}
default_batch_options.merge!(options)
end
end
class ThingRepository
include BatchLoader
# @param [Integer] per_page
# @param [Proc] block
def batch_changes(per_page=100, &block)
relation = Thing.active.order("created_at DESC")
batch_by_page do |next_page|
query = relation.page(next_page).per(per_page)
yield query if block_given?
query.next_page
end
end
end
答案 10 :(得分:0)
添加find_in_batches_with_order确实解决了我的用例,在该用例中我已经有ID,但是需要进行批处理和排序。它的灵感来自@ dirk-geurs解决方案
# Create file config/initializers/find_in_batches_with_order.rb with follwing code.
ActiveRecord::Batches.class_eval do
## Only flat order structure is supported now
## example: [:forename, :surname] is supported but [:forename, {surname: :asc}] is not supported
def find_in_batches_with_order(ids: nil, order: [], batch_size: 1000)
relation = self
arrangement = order.dup
index = order.find_index(:id)
unless index
arrangement.push(:id)
index = arrangement.length - 1
end
ids ||= relation.order(*arrangement).pluck(*arrangement).map{ |tupple| tupple[index] }
ids.each_slice(batch_size) do |chunk_ids|
chunk_relation = relation.where(id: chunk_ids).order(*order)
yield(chunk_relation)
end
end
end
将要旨留在这里https://gist.github.com/the-spectator/28b1176f98cc2f66e870755bb2334545
答案 11 :(得分:0)
我在使用DISTINCT ON
进行查询时遇到了同样的问题,您需要在该字段中使用ORDER BY
,因此这是我使用Postgres的方法:
def filtered_model_ids
Model.joins(:father_model)
.select('DISTINCT ON (model.field) model.id')
.order(:field)
.map(&:id)
end
def processor
filtered_model_ids.each_slice(BATCH_SIZE).lazy.each do |batch|
Model.find(batch).each do |record|
# Code
end
end
end
答案 12 :(得分:-1)
在一个查询中执行此操作并避免迭代:
User.offset(2).order('name DESC').last(3)
将产生这样的查询
SELECT "users".* FROM "users" ORDER BY name ASC LIMIT $1 OFFSET $2 [["LIMIT", 3], ["OFFSET", 2]