IT TIP

제한 및 주문과 결합 된 ActiveRecord find_each

itqueen 2020. 11. 21. 08:28
반응형

제한 및 주문과 결합 된 ActiveRecord find_each


ActiveRecord의 find_each방법을 사용하여 약 50,000 개의 레코드에 대한 쿼리를 실행하려고하는데 다음 과 같은 다른 매개 변수를 무시하는 것 같습니다.

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

50,000 I 'd like and sorting by 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는 다음과 같은 이유로 정렬 순서와 제한을 유지하지 않습니다.

  • PK의 정렬 ASC는 배치 주문 작업을 수행하는 데 사용됩니다.
  • 제한은 배치 크기를 제어하는 ​​데 사용됩니다.

@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 () 함수를 사용합니다.

도움이 되었기를 바랍니다!


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


두 번째 질문에 답하면 로직을 직접 만들 수 있습니다.

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내부 호출에 쿼리를 추가하는 것도 중요합니다 .


한 가지 옵션은 특정 모델에 맞는 구현을 모델 자체에 넣는 것입니다 ( 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"

추신 : 이 답변 에 따라 파일에 코드를 넣습니다 .


표준 루비 반복기로 역방향으로 반복 할 수 있습니다.

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 개 미만의 레코드를 일괄 적으로 가져올 수 있지만 제 경우에는 괜찮습니다.


I was looking for the same behaviour and thought up of this solution. This DOES NOT order by created_at but I thought I would post anyways.

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

Drawbacks of this approach: - You need 2 queries (first one should be fast) - This guarantees a max of 50K records but if ids are skipped you will get less.


You can try ar-as-batches Gem.

From their documentation you can do something like this

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

As remarked by @Kirk in one of the comments, find_each supports limit as of version 5.1.0.

Example from the changelog:

Post.limit(10_000).find_each do |post|
  # ...
end

The documentation says:

Limits are honored, and if present there is no requirement for the batch size: it can be less than, equal to, or greater than the limit.

(setting a custom order is still not supported though)


Using Kaminari or something other it will be easy.

Create batch loader class.

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

Create Repository

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

Use the repository

repo = ThingRepository.new
repo.batch_changes(5000).each do |g|
  g.each do |t|
    #...
  end
end

Do it in one query and avoid iterating:

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

will product a query like this

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

참고URL : https://stackoverflow.com/questions/15189937/activerecord-find-each-combined-with-limit-and-order

반응형