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
と同様の動作をする方法がありますが、合計最大制限があり、並べ替え基準を尊重しますか?
ドキュメント は、find_eachとfind_in_batchesがソート順と制限を保持しないことを示しています。
@rorraのように、この関数の独自のバージョンを作成できます。ただし、オブジェクトを変更すると問題が発生する可能性があります。たとえば、created_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
このソリューションのトレードオフは次のとおりです。
お役に立てれば!
find_each 使用 find_in_batches ボンネットの下。
find_in_batchesで説明されているように、レコードの順序を選択することはできません。主キー(「id ASC」)の昇順に自動的に設定されます。バッチ注文を機能させます。
ただし、基準は適用されます。できることは次のとおりです。
Thing.active.find_each(batch_size: 50000) { |t| puts t.id }
制限に関しては、まだ実装されていませんでした: https://github.com/Rails/rails/pull/5696
2番目の質問に答えて、自分でロジックを作成できます。
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, false).each do |photo_ids|
photos = Photo.order(likes_count: :desc).where(id: photo_ids)
# ...
end
ORDER BY
クエリを内部呼び出しに追加することも重要です。
1つのオプションは、特定のモデルに合わせて調整された実装をモデル自体に配置することです(そういえば、通常、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"
追伸 this answer に従ってコードをファイルに入れています。
標準の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には両方の境界が含まれますが、1つだけを含める必要があるためです。
確かに、このアプローチでは、一部のレコードが既に削除されているため、バッチで1000レコード未満をフェッチできますが、私の場合はこれで問題ありません。
試すことができますar-as-batches (Gem。
ドキュメント から、次のようなことができます
Users.where(country_id: 44).order(:joined_at).offset(200).as_batches do |user|
user.party_all_night!
end
私は同じ行動を探していて、この解決策を考えました。これは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がスキップされた場合は少なくなります。
雷 または他の何かを使用すると簡単です。
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
repo = ThingRepository.new
repo.batch_changes(5000).each do |g|
g.each do |t|
#...
end
end