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 55

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

сделайте это в одном запросе и избегайте повторения:

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

будет продукт запрос, как это

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