ActiveRecord найти каждый в сочетании с лимитом и порядком
Я пытаюсь выполнить запрос около 50 000 записей, используя у ActiveRecord find_each
метод, но он, кажется, игнорирует мои другие параметры, такие как:
Thing.active.order("created_at DESC").limit(50000).find_each {|t| puts t.id }
вместо того, чтобы останавливаться на 50,000 я хотел бы и сортировка по created_at
, вот результат запроса, который выполняется над весь dataset:
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
но с полным максимальным пределом и соблюдением моих критериев сортировки?
8 ответов:
документация говорит, что find_each и find_in_batches не сохраняют порядок сортировки и ограничения, потому что:
- сортировка ASC на ПК используется для выполнения пакетного заказа.
- предел используется для управления размерами партии.
вы можете написать свою собственную версию этой функции, как это сделал @rorra. Но вы можете попасть в беду при мутации объектов. Если, например, вы сортируете по created_at и сохраняете объект, он может появиться снова в одной из следующих партий. Точно так же вы можете пропустить объекты, потому что порядок результатов изменился при выполнении запроса для получения следующего пакета. Используйте это решение только с объектами только для чтения.
теперь моя главная забота заключалась в том, что я не хотел загружать 30000+ объектов в память сразу. Меня беспокоило не время выполнения самого запроса. Поэтому я использовал решение, которое выполняет исходный запрос, но только схроны идентификатора. Затем он делит массив идентификаторов в куски и запросы / создает объекты для каждого куска. Таким образом, вы можете безопасно мутировать объекты, потому что порядок сортировки хранится в памяти.
вот минимальный пример, похожий на то, что я сделал:
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
компромиссы для этого решения являются:
- полный запрос выполняется, чтобы получить идентификатор
- массив всех идентификаторов хранится в
- использует функцию MySQL specific 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).each do |photo_ids| photos = Photo.order(likes_count: :desc).where(id: photo_ids) # ... end
важно также добавить
ORDER BY
запрос к внутреннему вызову.
вы можете выполнить итерацию назад с помощью стандартных итераторов 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
это потому, что между которыми будет в запросе включает в себя обе границы, но нам нужно включить только один.конечно, при таком подходе может быть извлечено менее 1000 записей в пакете, потому что некоторые из них уже удалены, но в моем случае это нормально.
Я искал такое же поведение и придумал это решение. Это не заказывает 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
- обычно лучший выбор для упорядочения записей,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"
P. S. Я ввожу код в файлы в соответствии с ответ.
вы можете попробовать ar-as-batches камень.
из них документация вы можете сделать что-то подобное
Users.where(country_id: 44).order(:joined_at).offset(200).as_batches do |user| user.party_all_night! end