web-dev-qa-db-de.com

ActiveRecord find_each in Kombination mit Limit und Order

Ich versuche, eine Abfrage von etwa 50.000 Datensätzen mit der find_each-Methode von ActiveRecord auszuführen, aber es scheint, dass meine anderen Parameter ignoriert werden.

Thing.active.order("created_at DESC").limit(50000).find_each {|t| puts t.id }

Anstatt bei 50.000 anzuhalten und nach created_at zu sortieren, wird hier die resultierende Abfrage angezeigt, die über das gesamte - Dataset ausgeführt wird:

Thing Load (198.8ms)  SELECT "things".* FROM "things" WHERE "things"."active" = 't' AND ("things"."id" > 373343) ORDER BY "things"."id" ASC LIMIT 1000

Gibt es eine Möglichkeit, ein ähnliches Verhalten wie find_each zu erhalten, jedoch mit einer maximalen Höchstgrenze und unter Berücksichtigung meiner Sortierkriterien?

58
Avishai

Die Dokumentation besagt, dass find_each und find_in_batches die Sortierreihenfolge und das Limit nicht beibehalten, weil:

  • Das Sortieren von ASC auf der PK wird verwendet, um die Stapelbestellung zu erledigen.
  • Limit wird zur Kontrolle der Chargengrößen verwendet.

Sie können Ihre eigene Version dieser Funktion wie @rorra schreiben. Sie können jedoch in Schwierigkeiten geraten, wenn Sie die Objekte mutieren. Wenn Sie beispielsweise nach create_at sortieren und das Objekt speichern, wird es möglicherweise in einem der nächsten Stapel erneut angezeigt. Ebenso können Sie Objekte überspringen, da sich die Reihenfolge der Ergebnisse beim Ausführen der Abfrage zum Abrufen des nächsten Stapels geändert hat. Verwenden Sie diese Lösung nur für schreibgeschützte Objekte.

Jetzt ging es mir vor allem darum, dass ich nicht mehr als 30000 Objekte gleichzeitig in den Speicher laden wollte. Mein Anliegen war nicht die Ausführungszeit der Abfrage selbst. Daher habe ich eine Lösung verwendet, die die ursprüngliche Abfrage ausführt, aber nur die IDs im Cache speichert. Es teilt dann das Array von IDs in Stücke auf und fragt/erstellt die Objekte pro Block. Auf diese Weise können Sie die Objekte sicher mutieren, da die Sortierreihenfolge gespeichert wird.

Hier ist ein minimales Beispiel ähnlich dem, was ich getan habe:

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

Die Kompromisse bei dieser Lösung sind:

  • Die vollständige Abfrage wird ausgeführt, um die IDs abzurufen
  • Ein Array aller IDs wird im Speicher gehalten
  • Verwendet die MySQL-spezifische Funktion FIELD ()

Hoffe das hilft!

57
Dirk Geurs

find_each use find_in_batches unter der Haube.

Es ist nicht möglich, die Reihenfolge der Datensätze auszuwählen, wie in find_in_batches beschrieben. Der Primärschlüssel ("id ASC") wird automatisch aufsteigend gesetzt, damit die Stapelbestellung funktioniert.

Die Kriterien werden jedoch angewendet. Sie können Folgendes tun:

Thing.active.find_each(batch_size: 50000) { |t| puts t.id }

In Bezug auf das Limit wurde es noch nicht implementiert: https://github.com/Rails/rails/pull/5696


Bei der Beantwortung Ihrer zweiten Frage können Sie die Logik selbst erstellen:

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
24
rorra

Zuerst das ids abrufen und den in_groups_of bearbeiten

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

Es ist wichtig, dem inneren Aufruf auch die ORDER BY-Abfrage hinzuzufügen.

13
Thomas Klemm

Eine Möglichkeit besteht darin, eine auf Ihr Modell zugeschnittene Implementierung in das Modell selbst aufzunehmen (wobei id normalerweise die bessere Wahl für das Bestellen von Datensätzen ist, created_at möglicherweise Duplikate hat):

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

Oder Sie können die Dinge ein wenig verallgemeinern und für alle Modelle funktionieren lassen:

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. Ich füge den Code in Dateien gemäß dieser Antwort ein.

4
x-yuri

Sie können von Standard-Ruby-Iteratoren rückwärts iterieren:

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

Hinweis: +1 ist darauf zurückzuführen, dass BETWEEN in der Abfrage beide Begrenzungen enthält, wir müssen jedoch nur einen angeben.

Sicher, bei diesem Ansatz könnten weniger als 1000 Datensätze im Stapel abgerufen werden, da einige von ihnen bereits gelöscht wurden, aber dies ist in meinem Fall in Ordnung.

3
Lev Lukomsky

Wie @Kirk in einem Kommentar bemerkt, unterstützt find_eachlimit ab Version 5.1.0 .

Beispiel aus dem Änderungsprotokoll:

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

Die Dokumentation sagt:

Grenzwerte werden beachtet, und falls vorhanden, ist keine Chargengröße erforderlich: Sie kann kleiner als, gleich oder größer als der Grenzwert sein.

(Das Festlegen einer benutzerdefinierten Reihenfolge wird jedoch immer noch nicht unterstützt.)

2
tsauerwein

Sie können ar-as-batches Gem ausprobieren.

Aus ihrer Dokumentation kannst du so etwas machen

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

Ich suchte nach dem gleichen Verhalten und überlegte mir diese Lösung. Dies bestellt NICHT von created_at, aber ich dachte, ich würde sowieso posten.

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

Nachteile dieses Ansatzes: - Sie benötigen 2 Abfragen (die erste sollte schnell sein) - Dies garantiert eine maximale Anzahl von 50.000 Datensätzen. Wenn jedoch IDs übersprungen werden, erhalten Sie weniger.

2
Moemars

Mit Kaminari oder etwas anderem wird es einfach. 

Batch Loader-Klasse erstellen.

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

Repository erstellen

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

Verwenden Sie das Repository

repo = ThingRepository.new
repo.batch_changes(5000).each do |g|
  g.each do |t|
    #...
  end
end
0
merqlove