71

ActiveRecordのメソッドを使用して約50,000レコードのクエリを実行しようとしていますがfind_each、次のような他のパラメータを無視しているようです。

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合計最大制限を使用し、並べ替え基準を尊重して、同様の動作を実現する方法はありますか?

4

13 に答える 13

72

ドキュメントによると、次の理由により、find_each と find_in_batches は並べ替え順序と制限を保持しません。

  • PK での ASC の並べ替えは、バッチの順序付けを機能させるために使用されます。
  • Limit は、バッチ サイズを制御するために使用されます。

@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

このソリューションのトレードオフは次のとおりです。

  • ID を取得するために完全なクエリが実行されます。
  • すべての ID の配列がメモリに保持されます
  • MySQL 固有の FIELD() 関数を使用します

お役に立てれば!

于 2013-11-06T17:21:12.457 に答える
27

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
于 2013-03-03T20:11:12.893 に答える
18

最初のものを取得して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内部呼び出しにクエリを追加することも重要です。

于 2014-07-23T17:58:23.420 に答える
4

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"

PS私はこの回答に従ってコードをファイルに入れています。

于 2015-02-25T07:28:42.847 に答える
4

Rails 6.1 では、 、およびで降順のサポートが追加されました。find_eachfind_in_batchesin_batches

于 2020-06-29T08:01:46.590 に答える
2

ar-as-batches Gemを試すことができます。

彼らのドキュメントから、このようなことができます

Users.where(country_id: 44).order(:joined_at).offset(200).as_batches do |user|
  user.party_all_night!
end
于 2016-08-31T21:11:13.397 に答える
0

カミナリか何かを使えば簡単だろう 。

バッチ ローダー クラスを作成します。

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
于 2018-10-17T13:35:50.430 に答える