post

Ruby on Rails | Massenverarbeitung mit ActiveRecord

Bei der produktiven Nutzung eines von mir entwickelten Tools hat es zugeschlagen: Es ist halt doch etwas anderes mit ein paar Testdatensätzen oder mit ein paar Hunderttausend Datensätzen zu arbeiten. Die Laufzeiten für INSERTs und UPDATEs gingen gar nicht. Also was tun? Das Projekt ist mit ActiveRecord realisiert.

Etwas Recherche hat gezeigt: Da geht einiges!

Abrufen von Datensätzen

Das Abrufen von Datensätzen via ActiveRecord kann über verschiedene Techniken beschleunigt werden. Erster Ansatzpunkt sind die Funktionen zum Abruf von Datensätzen in Bündeln (Batches). Hierzu bietet ActiveRecord bereits Funktionen an, so dass eine große Menge an Datensätzen nicht auf einmal, sondern stapelweise und somit schonend für Arbeitsspeicher und Swap Bereich verarbeitet werden kann (Retrieving Multiple Objects in Batches).

Generell sollte man die Menge an Datensätzen aus der Datenbank bereits bei der Abfrage weitestgehend reduzieren um im Programmcode möglichst nur noch mit den relevanten Datensätzen zu arbeiten. Es sollten also nicht die Ruby Methoden find bzw. find_all zum Filtern von Datensätzen genutzt werden, sondern besser bereits bei der Abfrage entsprechende Einschränkungen gemacht werden.

Also so eher nicht:

class Client < ActiveRecord::Base
  has_one :address
  has_many :orders
  has_and_belongs_to_many :roles
end

top_clients=Client.all.find_all do |client|
  client.orders.count > 5
end

Besser:

class Client < ActiveRecord::Base
  has_one :address
  has_many :orders
  has_and_belongs_to_many :roles
end

top_clients=Client.where("orders_count > 5")

Bei n:m Beziehungen sollte man mit der ‚through‘ Anweisung arbeiten um die Beziehungen zwischen Objekten darzustellen (Siehe http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association). ActiveRecord generiert dann SQL mit entsprechenden JOIN Anweisungen, anstatt über mehrere SELECTs die Anfragen zusammenzubauen.

UPDATEs auf vielen Datensätzen

Updates auf vielen Datensätzen ist ineffizient, wenn für jeden Datensatz ein eigenes UPDATE Statement generiert wird. Hier schafft update_all Abhilfe. Man selektiert vorab alle Datensätze die aktualisiert werden sollen und kann dann diese in einem Rutsch bearbeiten:

# Alle Clients mit mehr als 5 Bestellungen sind VIPs
Client.where("orders_count > 5").update_all(vip: true)

Somit wird ein UPDATE Statement für alle Datensätze erzeugt.

Massen-INSERT

Das massenhafte Anlegen neuer Datensätze frisst ebenso sehr viel Performance, da für jeden INSERT eine eigene Transaktion gestartet wird. Hier kann man manuell eingreifen und ActiveRecord beibringen alle INSERTs ein einer Transaktion zu verarbeiten:

ActiveRecord::Base.transaction do
  1000.times do 
    Client.create(options)
  end
end

Schreibe einen Kommentar