Ruby-on-Rails Datenbankoptimierung Teil 1

Anzahl der Datenbankabfragen optimieren

Eine typische Webanwendung stellt viele Datenbankanfragen, bevor sie die Antwortseite an den Webbrowser ausliefert. Bei jeder dieser Datenbankabfragen muss die Anwendung auf die Antwort warten, dazu kommen noch Prozessumschaltungen, die zusätzlich bremsen. Der Datenbankserver muss jede Anfrage analysieren, und auch die Kommunikation zwischen Datenbank und Anwendung braucht Zeit. Weniger Datenbankanfragen reduzieren die Gesamtbelastung des Systems, das System skaliert besser.

Containerschiffe und Ihre Flaggenstaaten

Als Beispiel implementieren wir ein Schiffsinformationssystem. Das Datenmodel enthält Reedereien (companies), Schiffe (container_vessels) und Flaggenstaaten (countries). Eine Reederei besitzt mehrere Schiffe, jedes Schiff ist in einem Flaggenstaat registriert.

class Company < ActiveRecord::Base
    has_many :container_vessels
end
 
class ContainerVessel < ActiveRecord::Base     
  belongs_to :company     
  belongs_to :legal_country, :class_name => 'Country'
end

Wir wollen jetzt für eine Reederei eine Tabelle anzeigen, die alle ihre Schiffe zusammen mit dem jeweiligen Flaggenstaat enthält. Die naheliegende Implementierung hangelt sich einfach durch die Assoziationen des Models.

Im Controller werden die Schiffe geholt:

@company = Company.find(params[:company_id])
@container_vessels = @company.container_vessels.order(:name)

Und in der View greifen wir einfach auf das jeweilige Land zu

<% @container_vessels.each do |vessel| %>
  <%= vessel.name %>
  <%= vessel.legal_country.name if vessel.legal_country.present? %>
<% end %>

Die Lösung funktioniert und ist einfach zu verstehen. Schauen wir uns an, welche Datenbankabfragen ausgelöst werden:

SELECT "companies".* FROM "companies" 
        WHERE "companies"."id" = ? LIMIT 1 [["id", "3"]]
SELECT "container_vessels".* FROM "container_vessels" 
        WHERE "container_vessels"."company_id" = 3 ORDER BY name
 
SELECT "countries".* FROM "countries" WHERE "countries"."id" = 4 LIMIT 1
SELECT "countries".* FROM "countries" WHERE "countries"."id" = 10 LIMIT 1
SELECT "countries".* FROM "countries" WHERE "countries"."id" = 9 LIMIT 1

Alle Schiffe einer Reederei werden in einem Schwung mit nur einer Datenbankabfrage geholt. Die Länderdatensätze werden jedoch einzeln geholt. Je mehr Länder, desto mehr Datenbankabfragen. Hier gibt es Optimierungspotential.

Eifriges Laden

Für solche Anwendungsfälle bietet ActiveRecord eine einfache Verbesserung: die includes-Methode. Bei der Abfrage der Schiffs-Objekte können wir so Rails mitteilen, das wir auch an den Ländern interessiert sind:

@container_vessels = @company.container_vessels.order(:name).
        includes(:legal_country)

Weitere Änderungen müssen wir nicht vornehmen! Im Ergebnis werden diese Datenbankabfragen gestellt:

SELECT "companies".* FROM "companies" 
        WHERE "companies"."id" = ? LIMIT 1 [["id", "2"]]
SELECT "container_vessels".* FROM "container_vessels" 
        WHERE "container_vessels"."company_id" = 2 ORDER BY name
SELECT "countries".* FROM "countries" 
        WHERE "countries"."id" IN (8, 7, 4)

Statt jeden Länderdatensatz einzeln zu holen werden jetzt alle vorkommenden Länder in einer Abfrage geladen (eager loading). Der Vorteil kann gerade auf tabellenlastigen Übersichtsseiten enorm sein.

Einige Probleme gibt es leider dennoch:

1. Die Abfrage der verknüpften Datensätze lässt sich nur sehr eingeschränkt steuern. Beispielsweise kann man nicht einfach die Datenfelder mit „select“ begrenzen.

2. Das Optimierungspotential wird nicht ausgeschöpft. Es werden erst alle Schiffsdatensätze geholt, danach erst die Länderdatensätze, die dazu jeweils einzeln identifiziert werden müssen. SQL würde es ermöglichen, die Datensätze gemeinsam zu holen.

Es bleibt also noch genug Stoff für den zweiten Teil des Artikels.

Schreibe einen Kommentar