[RUBY] [Schienen] N + 1 ist böse! Wenn es doch passiert, lösen Sie es vorerst! !! Ist gefährlich

N+1

Das ist die Wurzel allen Übels! Leistungsfeind! !! Es ist eine schlechte Existenz, die Sie abwehren sollten, sobald Sie sie finden! !! !!

Ich denke, viele Leute denken das.

In der Tat ist das richtig, und selbst wenn Sie es online nachschlagen, finden Sie viel Know-how, um N + 1 zu eliminieren.

Grundsätzlich können Sie es nach dem Know-how korrigieren, aber in seltenen Fällen ist es besser, N + 1 nicht zu eliminieren, deshalb werde ich es mit einem konkreten Beispiel vorstellen.

Worüber ich in diesem Artikel sprechen möchte

Worüber ich in diesem Artikel sprechen möchte, ist, warum N + 1 behoben werden sollte? darüber. N + 1 ist so berühmt, dass ich viel Know-how habe, um es zu reparieren, aber ich habe das Gefühl, dass ich vergessen habe, warum ich es reparieren sollte. Eine Sache, die ich erwähnen möchte, ist, dass das Reparieren von N + 1 nicht darauf zurückzuführen ist, dass wir die Anzahl der ausgegebenen Abfragen reduzieren möchten. Weil ich die Leistung verbessern will! Mit anderen Worten, wenn sich die Leistung nicht verbessert, selbst wenn die Anzahl der Abfragen abnimmt, muss N + 1 nicht behoben werden.

Was ist N + 1?

Lassen Sie uns zuerst das typische N + 1 überprüfen.

Ich werde anhand des folgenden Modells erklären.

class User
  has_many :articles
end

class Article
  belongs_to :user
  has_many :images
end

class Image
  belongs_to :article
end

Stellen Sie sich eine API vor, die dieses Modell verwendet, um eine Liste von Artikeln für einen bestimmten Benutzer abzurufen. Die Antwort ist wie folgt

{
  articles: [
    id: 1
    body: "hogehoge"
    images: [
      {
        id: 1
        alt: "alt"
        src: "https://example.com/hoge1.img"
      }
    ]
  ]
}

Jeder liebt dies (?) Wenn Sie Jbuilder verwenden, erhalten Sie die folgende Implementierung.

class ArticlesController
  def index
    #Erhalten Sie 10 Elemente in absteigender Reihenfolge des Aktualisierungsdatums(using kaminari)
    @articles = Articles.where(user_id: params[:user_id]).order(updated_at: :desc).page(params[:page]).per(10)
  end
end

# articles/index.json.jbuilder
json.articles do
  json.array!(@articles) do |article|
    json.id article.id
    json.body article.body
    json.images do
      json.array!(article.images) do |image|
        json.id image.id
        json.alt image.alt
        json.src image.src
    end
  end
end

Was ist, wenn ich das oben genannte mache? Wie unten gezeigt, wird SQL zum Abrufen der Bildtabelle für die Anzahl der Artikel ausgegeben.

--1 Abfrage, um Artikel zu erhalten
SELECT `articles` FROM `articles` WHERE `articles`.`user_id` = 1 ORDER BY `articles`.`updated_at` DESC LIMIT 10

--Die Abfrage zur Bilderfassung wird für die Anzahl der Artikel ausgegeben
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 1
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 2
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 3
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 4
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 5
...
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 10

Rails hat eine Funktion, um dieses Problem zu lösen. Das ist "Preload", "eifriges Laden", "enthält". Ich werde in diesem Artikel nicht auf diese Details eingehen, aber dieses Mal werde ich "Preload" verwenden, um N + 1 zu eliminieren.

class ArticlesController
  def index
    # preload(:images)hinzufügen
    @articles = Articles.where(user_id: params[:user_id]).preload(:images).order(updated_at: :desc, id: :desc).page(params[:page]).per(10)
  end
end
--1 Abfrage, um Artikel zu erhalten
SELECT `articles` FROM `articles` WHERE `articles`.`user_id` = 1 ORDER BY `articles`.`updated_at` DESC LIMIT 10

--1 Abfrage zur Bilderfassung
SELECT `images`.* FROM `images` WHERE `images`.`article_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Die Ausgabe von Abfragen ist teuer, daher kann die Reduzierung der Anzahl der Abfragen die Leistung erheblich verbessern. Auch in diesem Beispiel wurden 11 Abfragen erheblich auf 2 Abfragen reduziert.

N + 1 Abstoßung abgeschlossen! !!

Muster, die N + 1 nicht eliminieren sollen

Im obigen Beispiel wurde N + 1 erfolgreich aufgelöst. Wenn es jedoch viele Benutzer gibt, die viele Bilder haben, war diese Korrespondenz wirklich gut? Betrachten wir als extremes Beispiel den Fall, in dem durchschnittlich 1.000 Bilder pro Artikel verlinkt sind.

In diesem Fall wird ein Bild von 1.000 * 10 = 10.000 zurückgegeben, wenn die obige Antwort unverändert bleibt, und die Antwort wird zu groß und die Leistung verschlechtert sich. Daher wird in den meisten Fällen die Artikelliste in eine Spezifikation geändert, die einen Teil des Bildes zurückgibt (z. B. nur die ersten 5).

Lassen Sie uns die Spezifikationen ändern. Ich habe den jbuilder-Teil geändert, ohne den Controller zu ändern.

# articles/index.json.jbuilder
json.articles do
  json.array!(@articles) do |article|
    json.id article.id
    json.body article.body
    json.images do
      #Holen Sie sich nur die ersten 5
      json.array!(article.images.first(5)) do |image|
        json.id image.id
        json.alt image.alt
        json.src image.src
    end
  end
end

Wenn dies ausgeführt wird, beträgt die Anzahl der Bilder der Antwort bis zu 5 für jeden Artikel. Herzlichen Glückwunsch ... es wird nicht sein! !! !! !!

Was ist los? Bei Betrachtung der Abfragen wurden nur die gleichen zwei Abfragen wie im vorherigen Beispiel ausgegeben.

--1 Abfrage, um Artikel zu erhalten
SELECT `articles` FROM `articles` WHERE `articles`.`user_id` = 1 ORDER BY `articles`.`updated_at` DESC LIMIT 10

--1 Abfrage zur Bilderfassung
SELECT `images`.* FROM `images` WHERE `images`.`article_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Hast du das Problem verstanden? In diesem Beispiel ist das Problem die Bildabfrage, die N + 1 beim letzten Mal gelöst hat. Nach dieser Voraussetzung sind 1.000 Bilder mit einem Artikel verknüpft. Dies bedeutet, dass diese Bildabfrage 10.000 Bildobjekte abruft. Während ActiveRecord praktisch ist, ist das Objekt sehr groß. Das Erstellen von 10.000 dieser Objekte verbraucht viel Speicher. Außerdem werde ich diesmal nur die ersten 5 Karten verwenden.

Was passiert also, wenn Sie "Preload" entfernen und in den Zustand zurückkehren, bevor N + 1 aufgelöst wurde?

--1 Abfrage, um Artikel zu erhalten
SELECT `articles` FROM `articles` WHERE `articles`.`user_id` = 1 ORDER BY `articles`.`updated_at` DESC LIMIT 10

--Die Abfrage zur Bilderfassung wird für die Anzahl der Artikel ausgegeben
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 1 limit 5
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 2 limit 5
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 3 limit 5
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 4 limit 5
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 5 limit 5
...
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 10 limit 5

Die Bildabfrage wird so oft wie die Anzahl der Artikel ausgegeben, aber da wir jeweils nur 5 erhalten, wird die Anzahl der generierten ActiveRecord-Objekte erheblich von 10.000 auf 50 reduziert.

Es ist nicht absolut, da es von der Leistung der Ausführungsumgebung abhängt, aber es ist oft besser, Speicher zu sparen, als N + 1 zu eliminieren. (* Welche Antwort tatsächlich angemessen ist, kann ohne Leistungsüberprüfung in einer Umgebung, die der Ausführungsumgebung entspricht, nicht bekannt sein.) Wenn N + 1 schlecht ist! Wenn Sie der Meinung sind, dass Sie das Problem unbedingt beheben müssen, fügen Sie bei der oben beschriebenen Bildabfrage eine Vorlast hinzu, die die Speichernutzung erheblich erhöhen und zu Leistungseinbußen führen kann. Vielleicht.

Schließlich

N + 1. Wenn Sie jedoch wie in diesem Beispiel auf "has_many" blicken, sollten Sie berücksichtigen, wie viele Fälle Sie erwarten können, bevor Sie es implementieren. Wenn Sie alle erhalten, ohne die Anzahl der Fälle zu berücksichtigen, kann dies zu Speichermangel und Leistungseinbußen führen und im schlimmsten Fall den Server einfrieren.

Außerdem habe ich am Anfang ein wenig geschrieben, aber lassen Sie uns den Zweck der Korrektur von N + 1 erkennen. Möchten Sie bestätigen, dass die Anzahl der Abfragen abgenommen hat, wenn Sie N + 1 korrigieren und die Antwort vervollständigen? Das Eliminieren von N + 1 dient nicht dazu, die Ausgabe von Abfragen zu reduzieren, sondern die Leistung zu verbessern. Überprüfen Sie nicht nur, ob die Anzahl der Abfragen abgenommen hat, sondern stellen Sie sicher, dass sich die Leistung ordnungsgemäß verbessert.

Unter dem Gesichtspunkt der Leistungsverbesserung gibt es neben N + 1 verschiedene Gesichtspunkte wie die Verringerung der Anzahl von Prozessen wie die Speichernutzung und Schleifen. Denken Sie daran, dass N + 1 nur eine Möglichkeit ist, die Leistung zu verbessern, und berücksichtigen Sie andere Perspektiven.

Recommended Posts

[Schienen] N + 1 ist böse! Wenn es doch passiert, lösen Sie es vorerst! !! Ist gefährlich
Installieren Sie vorerst Amazon Corretto (Vorschau)
Verwenden Sie vorerst eine externe Java-Bibliothek
Führen Sie jetzt Dataflow, Java, Streaming aus
Befehl, um Docker vorerst zu verwenden