SQL-Abfragen optimieren mit :select und :include
Um bei Rails mit der Datenbank zu kommunizieren, muss man sich dank ActiveRecord-Pattern nicht mehr um SQL-Befehle kümmern. Diese Kapselung ist sehr praktisch und verhindert auch Fehler, die sich bei komplizierten Abfragen schnell einschleichen.
Dieser Vorteil wird allerdings teuer erkauft, da ActiveRecord nicht weiss, wie die Daten von der Anwendung verwendet werden sollen. So benötigt man beispielsweise nicht immer alle Felder (oder Spalten) einer Tabelle- oder aber man weiss schon im Vorfeld, dass man auch Datensätze aus verbundenen Tabellen benötigt (belongs_to).
Nur benötigte Felder laden mit :select
Wird ein Datensatz über find() geladen, so führt ActiveRecord ein “SELECT * FROM …” aus. Das ist schlecht, da es eine Menge speicher verschwenden kann.
Beispiel: Eine Blog-Anwendung soll die 100 am meisten gelesenen Blog-Beiträge in einer Liste ausgeben, wobei nur die Überschriften angezeigt werden. Die Blog-Einträge sind in einer Tabelle gespeichert, welche Spalten für Id, Datum, Anzahl der Leser, Titel und Inhalt des Blogbeitrags hat. Zum erstellen dieser Liste benötigt man nur den Titel und die Id (um den Link zu erstellen).
Die Abfrage könnte daher lauten:
@most_viewed = BlogPost.find(:all, :order => 'views DESC', :limit => 100, :select => 'id, title')
Angenommen, jeder Beitrag (Inhalts-Feld) ist im Schnitt 10 KB groß dann fallen wärend dieser Abfrage mindestens 1 MB weniger Daten aus Datenbank an (die sowieso nicht verwendet werden). Das hört sich vielleicht zunächst nicht nach einem Problem an, allerdings geht es hier nicht nur um den Arbeitsspeicher des Webservers. Läuft der Datenbank-Server nämlich auf einem anderen Rechner als der Webserver, zum Beispiel bei einem größeren Projekt mit mehreren Webservern, dann müssen für die optimierte Abfrage mindestens 1 MB weniger an Daten übers Netzwerk übertragen werden. Bedenkt man jetzt noch, dass eine solche auch von vielen Besuchern gleichzeitig ausgelöst werden kann, summieren sich schnell mehrere MB unnützer Daten deren Übertragung für die Performance nicht folgenlos bleiben kann.
Mögliche Komplikationen: Bei einem Datensatz der mit :select-Anweisung geladen wurde sind die nicht gewählten Felder nil. Beim Aufruf von save werden diese Felder daher auch in der Datenbank mit NULL überschrieben! Mit reload können die fehlenden Felder nachgeladen werden. (Selbstverständlich bringt es nichts mit :select und reload zu arbeiten, wenn save auf JEDEN Datensatz angewendet werden soll).
belongs_to-Models laden mit :include
Angenommen beim BlogPost-Beispiel gibt es noch ein zugehöriges “Author”-Model, welches über belongs_to/has_many mit BlogPost verbunden ist und wir den Benutzernamen mit anzeigen wollen, dann würden wir bei der Ausgabe <blogpost-instanz-variable>.author.nickname aufrufen.
Da ActiveRecord nicht weiss, dass das Author-Model verwendet werden soll, wird es mit einer zweiten SQL-Abfrage in dem Moment geladen, wo auf das Feld nickname zugegriffen wird. Das ist insb. in Schleifen ungünstig, da n+1 SQL-Abfragen dazu notwendig sind (Eine Abfrage um die Liste der BlogPosts zu erstellen, dann n Abfragen um für jeden BlogPost den Author zu laden).
Per SQL kann mit der JOIN-Anweidungs der zugehörige Datensatz direkt (in einer Abfrage) geladen werden. ActiveRecord erzeugt Joins mit der :include-Option:
@posts = BlogPost.find(:all, :order => 'created_at', :limit => 100, :include => 'Author')
Mögliche Komplikationen: Im Beispiel wird nach der Spalte created_at sortiert, welche möglicherweise in beiden Models/Tabellen vorhanden ist. Man erhält dann eine ActiveRecord::StatementInvalid-Exception und folgende Fehlermeldung:
Mysql::Error: Column 'created_at' in order clause is ambiguous: SELECT ...
created_at gibt es in beiden Tabellen, daher ist es unklar nach welchem der Felder sortiert werden soll. Da der Inhalt von :order direkt an die SQL-Abfrage angehängt wird, kann man dort die Tabelle direkt mit angeben, z.B. so:
@posts = BlogPost.find(:all, :order => 'blog_posts.created_at', :limit => 100, :include => 'Author')
Tags: datenbank, include, optimierung, select