복붙노트

[RUBY-ON-RAILS] 액티브의 find_each 제한 및 주문과 함께

RUBY-ON-RAILS

액티브의 find_each 제한 및 주문과 함께

나는 액티브의 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에하지만 내 정렬 기준을 존중 총 최대 한계와 비슷한 동작을 얻을 수있는 방법이 있나요?

해결법

  1. ==============================

    1.문서는 find_each 및 find_in_batches가 정렬 순서 및 제한 때문에 유지되지 않는 것을 말한다 :

    문서는 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
    

    이 솔루션에 대한 장단점은 다음과 같습니다 :

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

  2. ==============================

    2.후드 find_each 용도의 find_in_batches.

    후드 find_each 용도의 find_in_batches.

    find_in_batches에 설명 된대로 레코드의 순서를 선택하는 것이 그것의 불가능은 자동으로 일괄 주문 작품을 만들기 위해 기본 키 ( "아이디 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
    
  3. ==============================

    3.제 ID를 검색하고 처리 in_groups_of

    제 ID를 검색하고 처리 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
    

    또한 내부 호출에 질의하여 추가 주문하는 것이 중요합니다.

  4. ==============================

    4.하나의 옵션 (ID 보통, created_at 중복이있을 수 있습니다 주문 레코드에 대한 더 나은 선택이다, 말하자면) 모델 자체에 특정 모델에 맞는 구현을 배치하는 것입니다 :

    하나의 옵션 (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
    

    설정 / 초기화 / extensions.rb :

    require "active_record_extensions"
    

    추신 나는이 대답에 따라 파일의 코드를 걸었습니다.

  5. ==============================

    5.당신은 표준 루비 반복자에 의해 뒤쪽을 반복 할 수 있습니다 :

    당신은 표준 루비 반복자에 의해 뒤쪽을 반복 할 수 있습니다 :

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

    참고 : 쿼리에있을 것입니다 것은 모두 경계를 포함하지만 우리가 하나를 포함 할 필요가있는 BETWEEN 때문에 하나입니다.

    물론, 그들 중 일부는 이미 삭제되기 때문에이 방법으로 일괄 이하 1,000 이상의 레코드를 가져올있을 수 있지만 내 경우에는 괜찮습니다.

  6. ==============================

    6.나는 같은 동작을 찾고이 액까지 생각되었다. 이 created_at에 의해 NOT 명령을합니까하지만 난 어쨌든 게시 할 것이라고 생각했다.

    나는 같은 동작을 찾고이 액까지 생각되었다. 이 created_at에 의해 NOT 명령을합니까하지만 난 어쨌든 게시 할 것이라고 생각했다.

    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
    

    이 방법의 단점 : - 당신은이 쿼리가 필요합니다 (첫 번째는 빨라야한다) -이 50K 기록의 최대 보장하지만 ID를 생략하는 경우 당신은 더 적은 얻을 것이다.

  7. ==============================

    7.당신은 AR-AS-일괄 보석을 시도 할 수 있습니다.

    당신은 AR-AS-일괄 보석을 시도 할 수 있습니다.

    자신의 문서에서 당신은 이런 식으로 뭔가를 할 수

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

    8.코멘트 중 하나에 @Kirk에 의해 언급 된 바와 같이, find_each 지원 버전 5.1.0의로 제한합니다.

    코멘트 중 하나에 @Kirk에 의해 언급 된 바와 같이, find_each 지원 버전 5.1.0의로 제한합니다.

    변경 로그의 예 :

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

    문서는 말합니다 :

    (사용자 지정 순서를 설정하는 것은 여전히 ​​있지만 지원되지 않음)

  9. ==============================

    9.미나리 또는 다른 뭔가를 사용하면 쉽게 될 것입니다.

    미나리 또는 다른 뭔가를 사용하면 쉽게 될 것입니다.

    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
    
  10. ==============================

    10.주문 find_in_batches를 추가하는 것은 이미 필요 일괄 처리 및 주문하지만,이 ID를 가진 한 경우, 내 유스 케이스를 해결했다. 그것은 @ 더크 - geurs 솔루션에 의해 영감을했다

    주문 find_in_batches를 추가하는 것은 이미 필요 일괄 처리 및 주문하지만,이 ID를 가진 한 경우, 내 유스 케이스를 해결했다. 그것은 @ 더크 - 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. ==============================

    11.하나의 쿼리와 피할 반복하는 그것을 수행

    하나의 쿼리와 피할 반복하는 그것을 수행

    User.offset (2) .order ( '이름 DESC'). 마지막으로 (3)

    이 같은 의지 제품 A를 쿼리

    SELECT "사용자". * "사용자"순서와 이름 ASC의 LIMIT BY $ 1 [ "OFFSET"[[ "LIMIT", 3] $ 2 OFFSET 2]

  12. from https://stackoverflow.com/questions/15189937/activerecord-find-each-combined-with-limit-and-order by cc-by-sa and MIT license