Das "N + 1-Auswahlproblem" wird in ORM-Diskussionen (Object-Relational Mapping) allgemein als Problem angegeben, und ich verstehe, dass es etwas damit zu tun hat, dass viele Datenbankabfragen für etwas durchgeführt werden müssen, das im Objekt einfach erscheint Welt.
Hat jemand eine detailliertere Erklärung des Problems?
orm
select-n-plus-1
Lars A. Brekken
quelle
quelle
Antworten:
Angenommen, Sie haben eine Sammlung von
Car
Objekten (Datenbankzeilen) und jedeCar
hat eine Sammlung vonWheel
Objekten (auch Zeilen). Mit anderen Worten,Car
→Wheel
ist eine 1-zu-viele-Beziehung.Angenommen, Sie müssen alle Autos durchlaufen und für jedes eine Liste der Räder ausdrucken. Die naive O / R-Implementierung würde Folgendes bewirken:
Und dann für jeden
Car
:Mit anderen Worten, Sie haben eine Auswahl für die Autos und dann N zusätzliche Auswahlen, wobei N die Gesamtzahl der Autos ist.
Alternativ könnte man alle Räder bekommen und die Suchvorgänge im Speicher durchführen:
Dies reduziert die Anzahl der Roundtrips zur Datenbank von N + 1 auf 2. Die meisten ORM-Tools bieten verschiedene Möglichkeiten, um die Auswahl von N + 1 zu verhindern.
Referenz: Java-Persistenz im Ruhezustand , Kapitel 13.
quelle
SELECT * from Wheel;
) anstelle von N + 1 erhalten. Mit einem großen N kann der Leistungstreffer sehr signifikant sein.Dadurch erhalten Sie eine Ergebnismenge, in der untergeordnete Zeilen in Tabelle2 zu Duplikaten führen, indem die Ergebnisse von Tabelle1 für jede untergeordnete Zeile in Tabelle2 zurückgegeben werden. O / R-Mapper sollten Tabellen1-Instanzen anhand eines eindeutigen Schlüsselfelds unterscheiden und dann alle Spalten der Tabelle2 verwenden, um untergeordnete Instanzen zu füllen.
Bei N + 1 füllt die erste Abfrage das primäre Objekt und die zweite Abfrage alle untergeordneten Objekte für jedes der zurückgegebenen eindeutigen primären Objekte.
Erwägen:
und Tabellen mit einer ähnlichen Struktur. Eine einzelne Abfrage für die Adresse "22 Valley St" kann Folgendes zurückgeben:
Das O / RM sollte eine Instanz von Home mit ID = 1, Address = "22 Valley St" füllen und dann das Array "Inhabitants" mit People-Instanzen für Dave, John und Mike mit nur einer Abfrage füllen.
Eine N + 1-Abfrage für dieselbe oben verwendete Adresse würde Folgendes ergeben:
mit einer separaten Abfrage wie
und was zu einem separaten Datensatz wie
und das Endergebnis ist das gleiche wie oben mit der einzelnen Abfrage.
Die Vorteile von Single Select bestehen darin, dass Sie alle Daten im Voraus erhalten, was letztendlich das sein kann, was Sie sich wünschen. Der Vorteil von N + 1 besteht darin, dass die Komplexität der Abfragen verringert wird und Sie das verzögerte Laden verwenden können, wenn die untergeordneten Ergebnismengen nur bei der ersten Anforderung geladen werden.
quelle
Lieferant mit einer Eins-zu-Viele-Beziehung zum Produkt. Ein Lieferant hat (liefert) viele Produkte.
Faktoren:
Lazy-Modus für Lieferanten auf "true" gesetzt (Standard)
Der für die Abfrage des Produkts verwendete Abrufmodus ist Auswählen
Abrufmodus (Standard): Auf Lieferanteninformationen wird zugegriffen
Caching spielt zum ersten Mal keine Rolle
Auf den Lieferanten wird zugegriffen
Der Abrufmodus ist Select Fetch (Standard).
Ergebnis:
Dies ist N + 1 Auswahlproblem!
quelle
Ich kann andere Antworten nicht direkt kommentieren, weil ich nicht genug Ruf habe. Es ist jedoch anzumerken, dass das Problem im Wesentlichen nur auftritt, weil in der Vergangenheit viele DBMS beim Umgang mit Joins ziemlich schlecht waren (MySQL ist ein besonders bemerkenswertes Beispiel). Daher war n + 1 oft deutlich schneller als ein Join. Und dann gibt es Möglichkeiten, n + 1 zu verbessern, aber immer noch ohne Join, worauf sich das ursprüngliche Problem bezieht.
Allerdings ist MySQL jetzt viel besser als früher, wenn es um Joins geht. Als ich MySQL zum ersten Mal lernte, habe ich viel Joins verwendet. Dann entdeckte ich, wie langsam sie sind, und wechselte stattdessen im Code zu n + 1. Aber in letzter Zeit bin ich wieder zu Joins zurückgekehrt, weil MySQL jetzt viel besser damit umgehen kann als zu Beginn meiner Verwendung.
Heutzutage ist ein einfacher Join für einen ordnungsgemäß indizierten Satz von Tabellen in Bezug auf die Leistung selten ein Problem. Und wenn es einen Leistungseinbruch gibt, werden sie durch die Verwendung von Indexhinweisen häufig gelöst.
Dies wird hier von einem der MySQL-Entwicklungsteams besprochen:
http://jorgenloland.blogspot.co.uk/2013/02/dbt-3-q3-6-x-performance-in-mysql-5610.html
Die Zusammenfassung lautet also: Wenn Sie in der Vergangenheit Joins aufgrund der miserablen Leistung von MySQL vermieden haben, versuchen Sie es erneut mit den neuesten Versionen. Sie werden wahrscheinlich angenehm überrascht sein.
quelle
JOIN
in RDBMS verwendeten Algorithmen wird als verschachtelte Schleifen bezeichnet. Grundsätzlich handelt es sich um eine N + 1-Auswahl unter der Haube. Der einzige Unterschied besteht darin, dass die Datenbank eine intelligente Entscheidung getroffen hat, sie basierend auf Statistiken und Indizes zu verwenden, anstatt dass Client-Code sie kategorisch auf diesen Pfad zwingt.Wir sind wegen dieses Problems vom ORM in Django weggezogen. Grundsätzlich, wenn Sie versuchen und tun
Das ORM gibt gerne alle Personen zurück (normalerweise als Instanzen eines Personenobjekts), muss dann jedoch die Autotabelle für jede Person abfragen.
Ein einfacher und sehr effektiver Ansatz ist etwas, das ich " Fanfolding " nenne , wodurch die unsinnige Idee vermieden wird, dass Abfrageergebnisse aus einer relationalen Datenbank wieder den ursprünglichen Tabellen zugeordnet werden sollten, aus denen die Abfrage besteht.
Schritt 1: Große Auswahl
Dies wird so etwas wie zurückgeben
Schritt 2: Objektivieren
Saugen Sie die Ergebnisse in einen generischen Objektersteller mit einem Argument, das nach dem dritten Element aufgeteilt werden soll. Dies bedeutet, dass das Objekt "jones" nicht mehr als einmal erstellt wird.
Schritt 3: Rendern
Auf dieser Webseite finden Sie eine Implementierung von Fanfolding für Python.
quelle
select_related
, was dieses Problem lösen soll - tatsächlich beginnen seine Dokumente mit einem Beispiel, das Ihremp.car.colour
Beispiel ähnlich ist .select_related()
undprefetch_related()
in Django jetzt.select_related()
und Freund scheinen keine der offensichtlich nützlichen Extrapolationen eines Joins wie zLEFT OUTER JOIN
. Das Problem ist kein Schnittstellenproblem, sondern ein Problem, das mit der seltsamen Idee zu tun hat, dass Objekte und relationale Daten meiner Ansicht nach abgebildet werden können.Was ist das N + 1-Abfrageproblem?
Das N + 1-Abfrageproblem tritt auf, wenn das Datenzugriffsframework N zusätzliche SQL-Anweisungen ausführt, um dieselben Daten abzurufen, die beim Ausführen der primären SQL-Abfrage abgerufen werden könnten.
Je größer der Wert von N ist, desto mehr Abfragen werden ausgeführt, desto größer ist die Auswirkung auf die Leistung. Und im Gegensatz zum langsamen Abfrageprotokoll , mit dem Sie langsam laufende Abfragen finden können, ist das N + 1-Problem nicht erkennbar, da jede einzelne zusätzliche Abfrage so schnell ausgeführt wird, dass das langsame Abfrageprotokoll nicht ausgelöst wird.
Das Problem besteht darin, eine große Anzahl zusätzlicher Abfragen auszuführen, die insgesamt ausreichend Zeit benötigen, um die Antwortzeit zu verlangsamen.
Nehmen wir an, wir haben die folgenden Post- und Post_comments-Datenbanktabellen, die eine Eins-zu-Viele-Tabellenbeziehung bilden :
Wir werden die folgenden 4
post
Zeilen erstellen :Außerdem erstellen wir 4
post_comment
untergeordnete Datensätze:N + 1-Abfrageproblem mit einfachem SQL
Wenn Sie die
post_comments
Verwendung dieser SQL-Abfrage auswählen :Und später entscheiden Sie sich, die zugehörigen
post
title
für jeden abzurufenpost_comment
:Sie werden das Problem mit der N + 1-Abfrage auslösen, da Sie anstelle einer SQL-Abfrage 5 (1 + 4) ausgeführt haben:
Das Beheben des N + 1-Abfrageproblems ist sehr einfach. Sie müssen lediglich alle Daten extrahieren, die Sie in der ursprünglichen SQL-Abfrage benötigen:
Dieses Mal wird nur eine SQL-Abfrage ausgeführt, um alle Daten abzurufen, an deren Verwendung wir weiter interessiert sind.
N + 1-Abfrageproblem mit JPA und Ruhezustand
Bei Verwendung von JPA und Hibernate gibt es verschiedene Möglichkeiten, das N + 1-Abfrageproblem auszulösen. Daher ist es sehr wichtig zu wissen, wie Sie diese Situationen vermeiden können.
Betrachten Sie für die nächsten Beispiele, dass wir die
post
undpost_comments
-Tabellen den folgenden Entitäten zuordnen:Die JPA-Zuordnungen sehen folgendermaßen aus:
FetchType.EAGER
Die
FetchType.EAGER
implizite oder explizite Verwendung für Ihre JPA-Zuordnungen ist eine schlechte Idee, da Sie viel mehr Daten abrufen werden, als Sie benötigen. Darüber hinaus ist dieFetchType.EAGER
Strategie auch anfällig für N + 1-Abfrageprobleme.Leider werden die
@ManyToOne
und@OneToOne
AssoziationenFetchType.EAGER
standardmäßig verwendet. Wenn Ihre Zuordnungen also folgendermaßen aussehen:Sie verwenden die
FetchType.EAGER
Strategie und jedes Mal,JOIN FETCH
wenn Sie vergessen, sie beim Laden einigerPostComment
Entitäten mit einer JPQL- oder Kriterien-API-Abfrage zu verwenden:Sie werden das N + 1-Abfrageproblem auslösen:
Beachten Sie die zusätzlichen SELECT - Anweisungen , die ausgeführt werden , weil die
post
Vereinigung der auf die Rückkehr geholt , bevor sein mussList
vonPostComment
Einheiten.Im Gegensatz zum Standardabrufplan, den Sie beim Aufrufen der
find
Methode von verwendenEnrityManager
, definiert eine JPQL- oder Kriterien-API-Abfrage einen expliziten Plan, den Hibernate nicht durch automatisches Einfügen eines JOIN FETCH ändern kann. Sie müssen dies also manuell tun.Wenn Sie die
post
Zuordnung überhaupt nicht benötigt haben , haben Sie bei der VerwendungFetchType.EAGER
kein Glück, da es nicht möglich ist, das Abrufen zu vermeiden. Deshalb ist es besser,FetchType.LAZY
standardmäßig zu verwenden.Wenn Sie jedoch die
post
Zuordnung verwenden möchten, können SieJOIN FETCH
das N + 1-Abfrageproblem umgehen:Dieses Mal führt Hibernate eine einzelne SQL-Anweisung aus:
FetchType.LAZY
Selbst wenn Sie
FetchType.LAZY
explizit für alle Zuordnungen verwenden, können Sie dennoch auf das Problem N + 1 stoßen.Diesmal wird die
post
Zuordnung folgendermaßen zugeordnet:Wenn Sie nun die
PostComment
Entitäten abrufen:Der Ruhezustand führt eine einzelne SQL-Anweisung aus:
Wenn Sie sich danach darauf beziehen, verweisen Sie auf die faul geladene
post
Zuordnung:Sie erhalten das Problem mit der N + 1-Abfrage:
Da die
post
Zuordnung träge abgerufen wird, wird beim Zugriff auf die verzögerte Zuordnung eine sekundäre SQL-Anweisung ausgeführt, um die Protokollnachricht zu erstellen.Das Update besteht wiederum darin
JOIN FETCH
, der JPQL-Abfrage eine Klausel hinzuzufügen :Und genau wie im
FetchType.EAGER
Beispiel generiert diese JPQL-Abfrage eine einzelne SQL-Anweisung.So erkennen Sie das N + 1-Abfrageproblem automatisch
Wenn Sie ein N + 1-Abfrageproblem in Ihrer Datenzugriffsebene automatisch erkennen möchten, wird in diesem Artikel erläutert, wie Sie dies mithilfe des
db-util
Open-Source-Projekts tun können .Zunächst müssen Sie die folgende Maven-Abhängigkeit hinzufügen:
Danach müssen Sie nur noch das
SQLStatementCountValidator
Dienstprogramm verwenden, um die zugrunde liegenden SQL-Anweisungen zu aktivieren, die generiert werden:Wenn Sie
FetchType.EAGER
den obigen Testfall verwenden und ausführen, wird der folgende Testfallfehler angezeigt:quelle
SELECT cars, wheels FROM cars JOIN wheels LIMIT 0, 5
. Sie erhalten jedoch 2 Autos mit 5 Rädern (erstes Auto mit allen 4 Rädern und zweites Auto mit nur 1 Rad), da LIMIT die gesamte Ergebnismenge und nicht nur die Root-Klausel einschränkt.Angenommen, Sie haben UNTERNEHMEN und MITARBEITER. UNTERNEHMEN hat viele MITARBEITER (dh MITARBEITER hat ein Feld UNTERNEHMEN_ID).
In einigen O / R-Konfigurationen führt das O / R-Tool eine Auswahl für jeden Mitarbeiter durch, wenn Sie ein zugeordnetes Unternehmensobjekt haben und auf dessen Mitarbeiterobjekte zugreifen. Wenn Sie nur in reinem SQL arbeiten, können Sie dies tun
select * from employees where company_id = XX
. Also N (Anzahl der Mitarbeiter) plus 1 (Unternehmen)So funktionierten die ersten Versionen von EJB Entity Beans. Ich glaube, Dinge wie Hibernate haben dies beseitigt, aber ich bin mir nicht sicher. Die meisten Tools enthalten normalerweise Informationen zu ihrer Strategie für die Zuordnung.
quelle
Hier ist eine gute Beschreibung des Problems
Nachdem Sie das Problem verstanden haben, können Sie es normalerweise vermeiden, indem Sie in Ihrer Abfrage einen Join-Abruf durchführen. Dies erzwingt im Wesentlichen das Abrufen des verzögert geladenen Objekts, sodass die Daten in einer Abfrage anstelle von n + 1 Abfragen abgerufen werden. Hoffe das hilft.
quelle
Überprüfen Sie den Ayende-Beitrag zum Thema: Bekämpfung des Select N + 1-Problems in NHibernate .
Wenn Sie ein ORM wie NHibernate oder EntityFramework verwenden und eine Eins-zu-Viele-Beziehung (Master-Detail) haben und alle Details für jeden Master-Datensatz auflisten möchten, müssen Sie grundsätzlich N + 1 Abfrageaufrufe an die Datenbank, wobei "N" die Anzahl der Stammsätze ist: 1 Abfrage zum Abrufen aller Stammsätze und N Abfragen, eine pro Stammsatz, um alle Details pro Stammsatz abzurufen.
Mehr Datenbankabfrageaufrufe → mehr Latenzzeit → verminderte Anwendungs- / Datenbankleistung.
ORMs haben jedoch Optionen, um dieses Problem zu vermeiden, hauptsächlich mithilfe von JOINs.
quelle
Es ist viel schneller, 1 Abfrage auszugeben, die 100 Ergebnisse zurückgibt, als 100 Abfragen auszugeben, die jeweils 1 Ergebnis zurückgeben.
quelle
Meiner Meinung nach ist der Artikel in Hibernate Pitfall: Warum Beziehungen faul sein sollten, genau das Gegenteil von echtem N + 1-Problem.
Wenn Sie eine korrekte Erklärung benötigen, lesen Sie bitte Ruhezustand - Kapitel 19: Verbessern der Leistung - Abrufen von Strategien
quelle
Der mitgelieferte Link enthält ein sehr einfaches Beispiel für das n + 1-Problem. Wenn Sie es auf den Ruhezustand anwenden, handelt es sich im Grunde genommen um dasselbe. Wenn Sie nach einem Objekt fragen, wird die Entität geladen, aber alle Zuordnungen (sofern nicht anders konfiguriert) werden verzögert geladen. Daher eine Abfrage für die Stammobjekte und eine andere Abfrage zum Laden der Zuordnungen für jedes dieser Objekte. 100 zurückgegebene Objekte bedeuten eine anfängliche Abfrage und dann 100 zusätzliche Abfragen, um die Zuordnung für jede n + 1 zu erhalten.
http://pramatr.com/2009/02/05/sql-n-1-selects-explained/
quelle
Ein Millionär hat N Autos. Sie möchten alle (4) Räder bekommen.
Eine (1) Abfrage lädt alle Autos, aber für jedes (N) Auto wird eine separate Abfrage zum Laden von Rädern gesendet.
Kosten:
Angenommen, die Indizes passen in den RAM.
Parsen und Planen von 1 + N-Abfragen + Indexsuche UND 1 + N + (N * 4) Plattenzugriff zum Laden von Nutzdaten.
Angenommen, die Indizes passen nicht in den RAM.
Zusätzliche Kosten im schlimmsten Fall 1 + N Plattenzugriffe für den Ladeindex.
Zusammenfassung
Der Flaschenhals ist Plattenzugriff (ca. 70-mal pro Sekunde zufälliger Zugriff auf Festplatte). Ein eifriger Join-Select würde auch 1 + N + (N * 4) Mal für die Nutzlast auf die Platte zugreifen. Wenn also die Indizes in den RAM passen - kein Problem, ist dies schnell genug, da nur RAM-Operationen erforderlich sind.
quelle
N + 1-Auswahlproblem ist ein Schmerz, und es ist sinnvoll, solche Fälle in Unit-Tests zu erkennen. Ich habe eine kleine Bibliothek entwickelt, um die Anzahl der Abfragen zu überprüfen, die von einer bestimmten Testmethode oder nur einem beliebigen Codeblock ausgeführt werden - JDBC Sniffer
Fügen Sie Ihrer Testklasse einfach eine spezielle JUnit-Regel hinzu und platzieren Sie Anmerkungen mit der erwarteten Anzahl von Abfragen zu Ihren Testmethoden:
quelle
Wie andere eleganter festgestellt haben, besteht das Problem darin, dass Sie entweder ein kartesisches Produkt der OneToMany-Spalten haben oder N + 1 Selects ausführen. Entweder mögliche gigantische Ergebnismenge oder Chat mit der Datenbank.
Ich bin überrascht, dass dies nicht erwähnt wird, aber so bin ich um dieses Problem herumgekommen ... Ich erstelle eine semi-temporäre ID-Tabelle . Ich mache das auch, wenn Sie die
IN ()
Klauselbeschränkung haben .Dies funktioniert nicht in allen Fällen (wahrscheinlich nicht einmal in der Mehrheit), aber es funktioniert besonders gut, wenn Sie viele untergeordnete Objekte haben, sodass das kartesische Produkt außer Kontrolle gerät (dh viele
OneToMany
Spalten, die Anzahl der Ergebnisse ist a Multiplikation der Spalten) und es ist eher ein Batch-ähnlicher Job.Zuerst fügen Sie Ihre übergeordneten Objekt-IDs als Stapel in eine ID-Tabelle ein. Diese batch_id generieren wir in unserer App und halten daran fest.
Jetzt machen Sie für jede
OneToMany
Spalte einfach eineSELECT
ID-Tabelle in der untergeordnetenINNER JOIN
Tabelle mit einemWHERE batch_id=
(oder umgekehrt). Sie möchten nur sicherstellen, dass Sie nach der ID-Spalte sortieren, da dies das Zusammenführen von Ergebnisspalten erleichtert (andernfalls benötigen Sie eine HashMap / Tabelle für die gesamte Ergebnismenge, die möglicherweise nicht so schlecht ist).Dann bereinigen Sie einfach regelmäßig die ID-Tabelle.
Dies funktioniert auch besonders gut, wenn der Benutzer etwa 100 verschiedene Elemente für eine Massenverarbeitung auswählt. Fügen Sie die 100 unterschiedlichen IDs in die temporäre Tabelle ein.
Die Anzahl der Abfragen, die Sie ausführen, richtet sich nach der Anzahl der OneToMany-Spalten.
quelle
Nehmen Sie das Beispiel von Matt Solnit und stellen Sie sich vor, Sie definieren eine Zuordnung zwischen Auto und Rädern als LAZY und benötigen einige Felder für Räder. Dies bedeutet, dass der Ruhezustand nach der ersten Auswahl "Select * from Wheels where car_id =: id" für jedes Auto ausführt.
Dies macht die erste Auswahl und mehr 1 Auswahl von jedem N Auto, deshalb heißt es n + 1 Problem.
Um dies zu vermeiden, lassen Sie die Zuordnung als eifrig abrufen, damit der Ruhezustand Daten mit einem Join lädt.
Aber Vorsicht, wenn Sie oft nicht auf zugehörige Räder zugreifen, ist es besser, sie faul zu halten oder den Abruftyp mit Kriterien zu ändern.
quelle