제한 및 주문과 결합 된 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
'IT TIP' 카테고리의 다른 글
컬로 쿠키 보내기 (0) | 2020.11.21 |
---|---|
자바 스크립트에서 nl2br () 해당 (0) | 2020.11.21 |
별도의 어셈블리에서 컨텍스트로 마이그레이션을 사용 하시겠습니까? (0) | 2020.11.21 |
CSS로 직사각형 이미지를 원형으로 만드는 방법 (0) | 2020.11.21 |
텍스트가 허용 된 것보다 큰 경우 CSS로 오버플로시 텍스트 페이드 아웃 (0) | 2020.11.21 |