Verbessern Sie die Leistung Ihrer Rails-App mit Eager Loading

Benutzer mögen rasante schnelle Anwendungen, und dann verlieben sie sich in sie und machen sie zu einem Teil ihres Lebens. Langsame Anwendungen ärgern hingegen nur die Benutzer und verlieren Einnahmen. In diesem Lernprogramm stellen wir sicher, dass wir nicht mehr Geld oder Benutzer verlieren und die verschiedenen Möglichkeiten zur Verbesserung der Leistung verstehen.

Active Records und ORM sind sehr leistungsfähige Werkzeuge in Ruby on Rails, aber nur, wenn wir wissen, wie man diese Fähigkeit einsetzt und nutzt. Am Anfang finden Sie unzählige Möglichkeiten, eine ähnliche Aufgabe in RoR auszuführen,Aber erst wenn man etwas tiefer gräbt, lernt man tatsächlich die Kosten für die Verwendung eines Systems kennen. 

Bei ORM und Associations in Rails ist es die gleiche Geschichte. Sie machen unser Leben sicher einfacher, können aber in manchen Situationen auch als Overkill wirken.

Nehmen wir ein Beispiel

Aber vorher generieren wir schnell eine Dummy-Anwendung, um damit herumzuspielen.

Schritt 1 

Starten Sie Ihr Terminal und geben Sie diese Befehle ein, um eine neue Anwendung zu erstellen:

Schienen neues Blog CD-Blog

Schritt 2

Generieren Sie Ihre Bewerbung:

Schienen g Gerüst Autorenname: String Schienen g Gerüst Post Titel: String Körper: Text Autor: Referenzen

Schritt 3

Stellen Sie es auf Ihrem lokalen Server bereit:

rake db: schiene migrieren s

Und das war es! Jetzt sollte eine Dummy-Anwendung ausgeführt werden.

So sollten unsere beiden Modelle (Autor und Post) aussehen. Wir haben Beiträge, die zum Autor gehören, und wir haben Autoren, die viele Beiträge haben können. Dies ist die grundlegende Verbindung zwischen diesen beiden Modellen, mit der wir spielen werden.

# Post Model Klasse Post < ActiveRecord::Base belongs_to :author end # Author Model class Author < ActiveRecord::Base has_many :posts end

Schauen Sie sich Ihren "Posts Controller" an - so sollte es aussehen. Unser Hauptfokus wird nur auf der Indexmethode liegen.

# Controller-Klasse PostsController < ApplicationController def index @posts = Post.order(created_at: :desc) end end

Und last but not least, unsere Beiträge Indexansicht. Ihre scheinen einige zusätzliche Zeilen zu haben, aber auf diese sollten Sie sich konzentrieren, besonders auf die Zeile post.author.name.

 <% @posts.each do |post| %>  <%= post.title %> <%= post.body %> <%= post.author.name %>  <% end %>  

Lassen Sie uns einfach einige Dummy-Daten erstellen, bevor wir loslegen. Gehen Sie zu Ihrer Rails-Konsole und fügen Sie die folgenden Zeilen hinzu. Oder Sie können einfach zu gehen http: // localhost: 3000 / posts / new und  http: // localhost: 3000 / autoren / neu um einige Daten manuell hinzuzufügen. 

Autors = Author.create ([Name: 'John', Name: 'Doe', Name: 'Manish']]) Post.Create (Titel: 'I love Tuts +', Körper: ", Autor: hors.first) Post.create (Titel: 'Tuts + is Awesome', Körper: ", Autor: Autors.second) Post.create (Titel: 'Long Live Tuts +', Körper:", Autor: Authors.last) 

Nachdem Sie alle Einstellungen vorgenommen haben, starten Sie den Server mit Schienen s und schlagen localhost: 3000 / Beiträge.

Auf dem Bildschirm sehen Sie einige Ergebnisse wie diese.

Alles scheint also in Ordnung zu sein: keine Fehler, und es werden alle Datensätze zusammen mit den zugehörigen Autorennamen abgerufen. Wenn Sie sich jedoch Ihr Entwicklungsprotokoll ansehen, werden viele Abfragen wie unten ausgeführt.

Post Load (0.6ms) SELECT "posts". * FROM "posts" ORDER BY "posts". "Created_at" DESC Author Load (0.5ms) SELECT "Autoren". * FROM "Autoren" WHERE "Autoren". "Id" =? LIMIT 1 [["id", 3]] Author Load (0.1ms) SELECT "Autoren". * FROM "Autoren" WHERE "Autoren". "Id" =? LIMIT 1 [["id", 2]] Author Load (0.1ms) SELECT "Autoren". * FROM "Autoren" WHERE "Autoren". "Id" =? LIMIT 1 [["id", 1]]

Okay, ich bin damit einverstanden, dass dies nur vier Anfragen sind, aber stellen Sie sich vor, Sie haben 3.000 Einträge in Ihrer Datenbank anstatt nur drei. In diesem Fall wird unsere Datenbank mit 3.000 + 1-Abfragen überschwemmt, weshalb dieses Problem als bezeichnet wird N + 1 Problem.

Warum bekommen wir dieses Problem??

In Ruby on Rails ist das Lazy-Laden daher standardmäßig aktiviert, was das Laden der Daten bis zu dem Punkt verzögert, an dem wir sie tatsächlich benötigen.

In unserem Fall ist es zunächst der Controller, an dem alle Beiträge abgerufen werden sollen.

def index @posts = Post.order (created_at:: desc) Ende

Zweitens ist die Ansicht, wo wir die vom Controller abgerufenen Posts durchlaufen und eine Abfrage senden, um den Autorennamen für jeden Post separat zu erhalten. Daher die N + 1 Problem. 

<% @posts.each do |post| %> … <%= post.author.name %>  <% end %>

Wie lösen wir das Problem??

Um uns aus solchen Situationen zu retten, bietet uns Rails ein Feature namens eifriges Laden.

Beim Eifrigen Laden können Sie die zugehörigen Daten vorab laden (Autoren)für alle Beiträge verbessert die Gesamtleistung der Datenbank, indem die Anzahl der Abfragen reduziert wird, und stellt Ihnen die Daten zur Verfügung, die Sie in Ihren Ansichten anzeigen möchten. Der einzige Haken dabei ist jedoch, welche verwendet werden soll. Erwischt!

Ja, weil wir drei davon haben und alle dem gleichen Zweck dienen, aber je nach Fall kann sich herausstellen, dass einer von ihnen die Leistung erneut mindert oder übertrifft.

preload () eager_load () umfasst ()

Nun könnten Sie fragen, welche Sie in diesem Fall verwenden sollen? Nun, fangen wir mit dem ersten an.

def index @posts = Post.order (created_at:: desc) .preload (: author) Ende

Speichern Sie es. Klicken Sie erneut auf die URL localhost: 3000 / Beiträge.

Es gibt also keine Änderungen an den Ergebnissen: Alles wird genau gleich geladen, aber unter der Haube im Entwicklungsprotokoll wurden diese Tonnen von Abfragen auf die folgenden zwei geändert.

SELECT "posts". * FROM "posts" ORDER BY "posts". "Created_at" DESC SELECT "Autoren". * FROM "Autoren" WHERE "Autoren". "Id" IN (3, 2, 1)

Preload verwendet zwei separate Abfragen, um die Hauptdaten und die zugehörigen Daten zu laden. Dies ist tatsächlich viel besser als eine separate Abfrage für jeden Autorennamen (das N + 1-Problem), aber dies reicht uns nicht aus. Aufgrund des separaten Abfrageansatzes wird eine Ausnahme in Szenarien ausgelöst wie:

  1. Beiträge nach Autorennamen bestellen.
  2. Nur Beiträge des Autors "John" finden.

Lassen Sie uns alle Szenarien mit eager_load () eins nach dem anderen ausprobieren

1. Bestellen Sie die Beiträge nach dem Namen des Autors

# Beiträge nach Autorennamen bestellen. def index @posts = Post.order ("hors.name "). eager_load (: author) Ende

Ergebnisabfrage in den Entwicklungsprotokollen:

SELECT "posts". "Id" AS t0_r0, "posts". "Title" AS t0_r1, "posts". "Body" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "posts". "updated_at" AS t0_r5, "autoren". "id" AS t1_r0, "autoren". "name" AS t1_r1, "autoren". "created_at" AS t1_r2, "autoren". "updated_at" AS t1_r3 FROM "posts" LEFT OUTER JOIN "Autoren" ON "Autoren". "Id" = "posts". "Author_id" ORDER BY Autors.name 

2. Nur nach Beiträgen des Autors suchen "John"

# Nur Beiträge des Autors suchen "John". def index @posts = Post.order (created_at:: desc) .eager_load (: author) .where ("autors.name =?", "Manish") enden

Ergebnisabfrage in den Entwicklungsprotokollen:

SELECT "posts". "Id" AS t0_r0, "posts". "Title" AS t0_r1, "posts". "Body" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "posts". "updated_at" AS t0_r5, "autoren". "id" AS t1_r0, "autoren". "name" AS t1_r1, "autoren". "created_at" AS t1_r2, "autoren". "updated_at" AS t1_r3 FROM "posts" LEFT OUTER JOIN "Autoren" ON "Autoren". "Id" = "posts". "Author_id" WHERE (autors.name = 'Manish') ORDER BY "posts". "Created_at" DESC 

3. N + 1 Szenario

def index @posts = Post.order (created_at:: desc) .eager_load (: author) end 

Ergebnisabfrage in den Entwicklungsprotokollen:

SELECT "posts". "Id" AS t0_r0, "posts". "Title" AS t0_r1, "posts". "Body" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "posts". "updated_at" AS t0_r5, "autoren". "id" AS t1_r0, "autoren". "name" AS t1_r1, "autoren". "created_at" AS t1_r2, "autoren". "updated_at" AS t1_r3 FROM "posts" LEFT OUTER JOIN "Autoren" ON "Autoren". "Id" = "posts". "Author_id" ORDER BY "posts". "Created_at" DESC 

Wenn Sie sich also die resultierenden Abfragen aller drei Szenarien ansehen, gibt es zwei Gemeinsamkeiten. 

Zuerst, eager_load () benutzt immer die LINKE ÄUSSERE VERBINDUNG Wie auch immer der Fall sein mag. Zweitens ruft es alle zugehörigen Daten in einer einzigen Abfrage ab, wodurch der Wert sicher überschritten wird Vorspannung () Methode in Situationen, in denen wir die zugehörigen Daten für zusätzliche Aufgaben wie Sortieren und Filtern verwenden möchten. Aber eine einzelne Abfrage und LINKE ÄUSSERE VERBINDUNG kann auch in einfachen Szenarien wie oben sehr teuer sein, in denen Sie lediglich die benötigten Autoren filtern müssen. Es ist wie eine Bazooka, um eine winzige Fliege zu töten.

Ich verstehe, dass dies nur zwei einfache Beispiele sind, und in realen Szenarien kann es sehr schwierig sein, sich für dasjenige zu entscheiden, das für Ihre Situation am besten geeignet ist. Aus diesem Grund hat uns Rails das gegeben Includes () Methode.

Mit Includes (), Active Record kümmert sich um die schwierige Entscheidung. Es ist viel klüger als die beiden Vorspannung () und eager_load () Methoden und entscheidet, welche man alleine verwenden soll.

Lassen Sie uns alle Szenarien mit Includes () ausprobieren

1. Bestellen Sie die Beiträge nach dem Namen des Autors

# Beiträge nach Autorennamen bestellen. def index @posts = Post.order ("hors.name "). enthält (: author) end

Ergebnisabfrage in den Entwicklungsprotokollen:

SELECT "posts". "Id" AS t0_r0, "posts". "Title" AS t0_r1, "posts". "Body" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "posts". "updated_at" AS t0_r5, "autoren". "id" AS t1_r0, "autoren". "name" AS t1_r1, "autoren". "created_at" AS t1_r2, "autoren". "updated_at" AS t1_r3 FROM "posts" LEFT OUTER JOIN "Autoren" ON "Autoren". "Id" = "posts". "Author_id" ORDER BY Autors.name

2. Nur nach Beiträgen des Autors suchen "John"

# Nur Beiträge des Autors suchen "John". def index @posts = Post.order (created_at:: desc) .includes (: author) .where ("hors.name =? "," Manish ") # Für Schienen 4 Vergessen Sie nicht, .references (: author.) hinzuzufügen ) am Ende @posts = Post.order (created_at:: desc) .includes (: author) .where ("hors.name =? "," Manish "). referenzen (: author) ende

Ergebnisabfrage in den Entwicklungsprotokollen:

SELECT "posts". "Id" AS t0_r0, "posts". "Title" AS t0_r1, "posts". "Body" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "posts". "updated_at" AS t0_r5, "autoren". "id" AS t1_r0, "autoren". "name" AS t1_r1, "autoren". "created_at" AS t1_r2, "autoren". "updated_at" AS t1_r3 FROM "posts" LEFT OUTER JOIN "Autoren" ON "Autoren". "Id" = "posts". "Author_id" WHERE (autors.name = 'Manish') ORDER BY "posts". "Created_at" DESC 

3. N + 1 Szenario

def index @posts = Post.order (created_at:: desc) .includes (: author) ende 

Ergebnisabfrage in den Entwicklungsprotokollen:

SELECT "posts". * FROM "posts" ORDER BY "posts". "Created_at" DESC SELECT "Autoren". * FROM "Autoren" WHERE "Autoren". "Id" IN (3, 2, 1)

Wenn wir nun die Ergebnisse mit dem vergleichen eager_load () Englisch: emagazine.credit-suisse.com/app/art ... = 118 & lang = en Die ersten beiden Fälle haben ähnliche Ergebnisse, aber im letzten Fall hat es sich klug entschieden, auf die Vorspannung () Methode für eine bessere Leistung.

Ehrfürchtig, richtig?

Nein, denn in diesem Leistungswettlauf kann manchmal auch die eifrige Ladung nachlassen. Ich hoffe, einige von euch haben das schon bemerkt, wenn eifrige Lademethoden zum Einsatz kommen VERBINDUNGEN, Sie verwenden nur LINKE ÄUSSERE VERBINDUNG. Außerdem laden sie in jedem Fall zu viele unnötige Daten in den Speicher. Sie wählen jede einzelne Spalte aus der Tabelle aus, während wir nur den Namen des Autors benötigen.

Willkommen bei den Joins

Auch wenn Sie mit Active Record Bedingungen für die geladenen Assoziationen festlegen können Joins (), Die empfohlene Methode ist die Verwendung von Joins. ~ Rails-Dokumentation.

Wie in der Rail-Dokumentation empfohlen, ist die Joins () Methode ist in diesen Situationen einen Schritt voraus. Es fügt die zugehörige Tabelle hinzu, lädt jedoch nur die erforderlichen Modelldaten in den Speicher Beiträge in unserem Fall. Daher laden wir redundante Daten nicht unnötig in den Speicher. Wenn wir möchten, können wir dies auch tun.

Lassen Sie uns in einige Beispiele eintauchen

1. Bestellen Sie die Beiträge nach dem Namen des Autors

# Beiträge nach Autorennamen bestellen. def index @posts = Post.order ("hors.name "). joins (: author) ende

Ergebnisabfrage in den Entwicklungsprotokollen:

SELECT "posts". * FROM "posts" INNER JOIN "Autoren" ON "Autoren". "Id" = "posts". "Author_id" ORDER BY Writers.name SELECT "Autoren". * FROM "Autoren" WHERE "Autoren" . "id" =? LIMIT 1 [["id", 2]] SELECT "Autoren". * FROM "Autoren" WHERE "Autoren". "Id" =? LIMIT 1 [["id", 1]] SELECT "Autoren". * FROM "Autoren" WHERE "Autoren". "Id" =? LIMIT 1 [["id", 3]]

2. Nur nach Beiträgen des Autors suchen "John"

# Nur Beiträge des Autors suchen "John". def index @posts = Post.order (Published_at:: Desc) .joins (: Autor) .where ("Autors.name =?", "John") enden

Ergebnisabfrage in den Entwicklungsprotokollen:

SELECT "posts". * FROM "posts" INNER JOIN "Autoren" ON "Autoren". "Id" = "posts". "Author_id" WHERE (hors.name = 'Manish') ORDER BY "posts". "Created_at" DESC SELECT "autors". * FROM "autoren" WO "autoren". "Id" =? LIMIT 1 [["id", 3]] 

3. N + 1 Szenario

def index @posts = Post.order (Published_at:: desc) .joins (: author) Ende

Ergebnisabfrage in den Entwicklungsprotokollen:

SELECT "posts". * FROM "posts" INNER JOIN "Autoren" ON "Autoren". "Id" = "posts". "Author_id" ORDER BY "posts". "Created_at" DESC SELECT "Autoren". * FROM "Autoren "WO" Autoren "." Id "=? LIMIT 1 [["id", 3]] SELECT "Autoren". * FROM "Autoren" WHERE "Autoren". "Id" =? LIMIT 1 [["id", 2]] SELECT "Autoren". * FROM "Autoren" WHERE "Autoren". "Id" =? LIMIT 1 [["id", 1]]

Das erste, was Sie an den Ergebnissen oben feststellen können, ist, dass das N + 1 Das Problem ist zurück, aber konzentrieren wir uns zuerst auf den guten Teil. 

Schauen wir uns die erste Abfrage aus allen Ergebnissen an. Alle sehen mehr oder weniger so aus. 

SELECT "posts". * FROM "posts" INNER JOIN "Autoren" ON "Autoren". "Id" = "posts". "Author_id" ORDER BY Autors.name

Es holt alle Spalten von den Beiträgen. Er fügt beide Tabellen zusammen und sortiert oder filtert die Datensätze je nach Bedingung, ohne jedoch Daten aus der zugeordneten Tabelle abzurufen. Welches wollten wir überhaupt.

Aber nach den ersten Abfragen werden wir sehen 1 oder 3 oder N Anzahl der Abfragen in Abhängigkeit von den Daten in Ihrer Datenbank wie folgt:

SELECT "Autoren". * FROM "Autoren" WO "Autoren". "Id" =? LIMIT 1 [["id", 2]] SELECT "Autoren". * FROM "Autoren" WHERE "Autoren". "Id" =? LIMIT 1 [["id", 1]] SELECT "Autoren". * FROM "Autoren" WHERE "Autoren". "Id" =? LIMIT 1 [["id", 3]]

Nun fragen Sie vielleicht: Warum ist das so? N + 1 Problem zurück Es ist wegen dieser Linie aus unserer Sicht post.author.name.

 <% @posts.each do |post| %>  <%= post.title %> <%= post.body %> <%= post.author.name %>  <% end %>  

Diese Zeile löst alle diese Abfragen aus. In dem Beispiel, in dem wir nur unsere Beiträge bestellen mussten, müssen wir den Namen des Autors nicht in unseren Ansichten anzeigen. In diesem Fall können wir dieses Problem beheben, indem Sie die Zeile entfernen post.author.name aus der Sicht.

Aber dann könnten Sie fragen: "Hey MK, wie sieht es mit den Beispielen aus, bei denen wir den Namen des Autors in der Ansicht anzeigen möchten?" 

Nun, in diesem Fall die Joins () Methode wird es nicht von selbst beheben. Wir müssen es sagen Joins () um den Namen des Autors oder eine andere Spalte aus der Tabelle für diese Angelegenheit auszuwählen. Und wir können es tun, indem Sie eine hinzufügen wählen() Aussage am Ende wie folgt:

def index @posts = Post.order (Published_at:: desc) .joins (: author) .select ("posts. *, autors.name als author_name") enden

Ich habe einen Alias ​​"Autorname" für Autors.Name erstellt. Wir werden sehen warum gerade eine Sekunde.

Ergebnisabfrage in den Entwicklungsprotokollen:

SELECT posts. * ,Hors.name als author_name FROM "posts" INNER JOIN "Autoren" ON "Autoren". "Id" = "posts". "Author_id" ORDER BY "posts". "Created_at" DESC 

Los geht's: Endlich eine saubere SQL-Abfrage mit Nein N + 1 Problem, ohne unnötige Daten, nur mit den Dingen, die wir brauchen. Jetzt müssen Sie nur noch diesen Alias ​​in Ihrer Ansicht verwenden und ändern post.author.name zu post.author_name. Dies liegt daran, dass der Autorenname jetzt ein Attribut unseres Post-Modells ist. Nach dieser Änderung sieht die Seite so aus:

Alles genau gleich, aber unter der Haube wurde vieles verändert. Wenn ich alles auf den Punkt bringe, um das zu lösen N + 1 du solltest gehen eifriges Laden, Aber manchmal sollten Sie, abhängig von der Situation, die Dinge unter Ihre Kontrolle bringen und verwenden schließt sich an für bessere Optionen. Sie können auch unformatierte SQL-Abfragen für die bereitstellen Joins () Methode für mehr Anpassung.

Verknüpfungen und eifriges Laden ermöglichen auch das Laden mehrerer Verknüpfungen, aber am Anfang können die Dinge sehr kompliziert werden und es ist schwierig, die beste Option zu wählen. In solchen Situationen empfehle ich Ihnen, diese beiden sehr schönen Envato Tuts + -Tutorials zu lesen, um Joins besser zu verstehen und in der Lage zu sein, den kostengünstigsten Ansatz hinsichtlich der Leistung zu bestimmen:

  • Ein tiefer gehender Blick auf erweiterte Auswahlabfragen 
  • Mit MySQL und INNER JOIN arbeiten

Nicht zuletzt kann es kompliziert sein, Bereiche in Ihrer Pre-Build-Anwendung herauszufinden, in denen Sie die Leistung generell verbessern oder die Leistung verbessern möchten N + 1 Probleme. In diesen Fällen empfehle ich einen schönen Edelstein Kugel. Es kann Sie darüber informieren, wann Sie das Ladenlust hinzufügen möchten N + 1 Abfragen, und wenn Sie eifriges Laden unnötig verwenden.