J'essaie d'exécuter une requête d'environ 50 000 enregistrements à l'aide de la méthode find_each
d'ActiveRecord, mais elle semble ignorer mes autres paramètres, comme ceci:
Thing.active.order("created_at DESC").limit(50000).find_each {|t| puts t.id }
Au lieu de vous arrêter à 50 000 et de trier par created_at
, voici la requête résultante qui est exécutée sur l'ensemble de données whole :
Thing Load (198.8ms) SELECT "things".* FROM "things" WHERE "things"."active" = 't' AND ("things"."id" > 373343) ORDER BY "things"."id" ASC LIMIT 1000
Existe-t-il un moyen d'obtenir un comportement similaire à find_each
mais avec une limite maximale totale et en respectant mes critères de tri?
La documentation indique que find_each et find_in_batches ne conservent pas l'ordre de tri ni la limite, car:
Vous pouvez écrire votre propre version de cette fonction, comme l’a fait @rorra. Mais vous pouvez avoir des problèmes lorsque vous mute les objets. Si, par exemple, vous triez par created_at et enregistrez l’objet, il pourra être récupéré lors de l’un des prochains lots. De même, vous pouvez ignorer des objets car l'ordre des résultats a changé lors de l'exécution de la requête pour obtenir le prochain lot. Utilisez cette solution uniquement avec des objets en lecture seule.
Maintenant, ma principale préoccupation était que je ne souhaitais pas charger plus de 30000 objets en mémoire à la fois. Ma préoccupation n'était pas le temps d'exécution de la requête elle-même. Par conséquent, j'ai utilisé une solution qui exécute la requête d'origine mais met uniquement en cache l'ID. Il divise ensuite le tableau d'identifiants en morceaux et interroge/crée les objets par morceau. De cette façon, vous pouvez muter les objets en toute sécurité car l'ordre de tri est conservé en mémoire.
Voici un exemple minimal similaire à ce que j'ai fait:
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
Les compromis à cette solution sont:
J'espère que cela t'aides!
find_each utilise find_in_batches sous le capot.
Il n'est pas possible de sélectionner l'ordre des enregistrements, comme décrit dans find_in_batches, est automatiquement réglé sur croissant sur la clé primaire («id ASC») pour que la commande par lots fonctionne.
Cependant, les critères sont appliqués, vous pouvez faire ce qui suit:
Thing.active.find_each(batch_size: 50000) { |t| puts t.id }
En ce qui concerne la limite, elle n’a pas encore été mise en œuvre: https://github.com/Rails/rails/pull/5696
En répondant à votre deuxième question, vous pouvez créer la logique vous-même:
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
Récupération de la ids
en premier et traitement du 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
Il est important d'ajouter également la requête ORDER BY
à l'appel interne.
Une option consiste à insérer une implémentation adaptée à votre modèle particulier dans le modèle lui-même (à savoir, id
est généralement le meilleur choix pour commander des enregistrements, created_at
peut avoir des doublons):
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
Sinon, vous pouvez généraliser un peu les choses et les faire fonctionner pour tous les modèles:
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. Je mets le code dans les fichiers en fonction de cette réponse .
Vous pouvez itérer en arrière par les itérateurs Ruby standard:
Thing.last.id.step(0,-1000) do |i|
Thing.where(id: (i-1000+1)..i).order('id DESC').each do |thing|
#...
end
end
Note: +1
est parce que BETWEEN qui sera dans la requête inclut les deux bornes mais nous n’en avons besoin que d’une.
Bien sûr, avec cette approche, moins de 1000 enregistrements par lot pourraient être récupérés, car certains d’entre eux sont déjà supprimés, mais c’est acceptable dans mon cas.
Comme l'a remarqué @Kirk dans l'un des commentaires, find_each
supporte limit
à partir de la version 5.1.0 .
Exemple du changelog:
Post.limit(10_000).find_each do |post|
# ...
end
La documentation dit:
Les limites sont respectées et, le cas échéant, la taille du lot n'est pas requise: elle peut être inférieure, égale ou supérieure à la limite.
(la définition d'une commande personnalisée n'est toujours pas prise en charge)
Vous pouvez essayer ar-as-batchs Gem.
De leur documentation vous pouvez faire quelque chose comme ceci
Users.where(country_id: 44).order(:joined_at).offset(200).as_batches do |user|
user.party_all_night!
end
Je recherchais le même comportement et pensais à cette solution. This NE PAS commander par created_at mais je pensais que je posterais de toute façon.
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
Inconvénients de cette approche.
En utilisant Kaminari ou autre chose, ce sera facile.
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