Frage

Die „N + 1 wählt Problem“ wird im Allgemeinen als ein Problem in Object-Relational Mapping (ORM) Diskussionen erwähnt, und ich verstehe, dass es eine Menge von Datenbankabfragen für etwas etwas tun, um mit mit machen hat, die einfach zu sein scheint in der Objektwelt.

Hat jemand eine ausführlichere Erklärung des Problems?

War es hilfreich?

Lösung

Angenommen, Sie haben eine Sammlung von Car Objekte (Datenbankzeilen) und jede Car hat eine Sammlung von Wheel Objekte (auch Zeilen). Mit anderen Worten, Car -> Wheel ist eine 1-zu-viele-Beziehung

.

Nun lassen Sie uns sagen, dass Sie alle Autos zu durchlaufen, und für jeden einzelnen, um eine Liste der Räder ausdrucken. Die naive O / R-Implementierung würde wie folgt vor:

SELECT * FROM Cars;

Und dann für jede Car:

SELECT * FROM Wheel WHERE CarId = ?

Mit anderen Worten, Sie haben ein select für die Autos, und dann N zusätzlichen wählt, wobei N die Gesamtzahl der Autos ist.

Alternativ könnte man alle Räder bekommen und die Lookups im Speicher ausgeführt werden:

SELECT * FROM Wheel

Dies reduziert die Anzahl der Roundtrips in die Datenbank von N + 1 bis 2. Die meisten ORM-Tools geben Ihnen mehrere Möglichkeiten, N + 1 wählt zu verhindern.

Referenz: Java Persistence mit Hibernate , Kapitel 13.

Andere Tipps

SELECT 
table1.*
, table2.*
INNER JOIN table2 ON table2.SomeFkId = table1.SomeId

Das bringt Sie eine Ergebnismenge in denen Kinder Reihen in table2 Ursache Vervielfältigung durch die tabelle1 Ergebnisse für jedes Kind Reihe in table2 zurück. O / R-Mapper sollte tabelle1 Instanzen basiert auf einem einzigartigen Schlüsselfeld unterscheiden, dann alle Spalten table2 verwenden, um Kind Instanzen zu füllen.

SELECT table1.*

SELECT table2.* WHERE SomeFkId = #

Die N + 1 ist, wo die erste Abfrage des primären Objekt auffüllt und die zweite Abfrage füllt die alle untergeordneten Objekte für jeden der eindeutigen primären Objekte zurückgegeben.

Bedenken Sie:

class House
{
    int Id { get; set; }
    string Address { get; set; }
    Person[] Inhabitants { get; set; }
}

class Person
{
    string Name { get; set; }
    int HouseId { get; set; }
}

und Tabellen mit einer ähnlichen Struktur. Eine einzelne Abfrage für die Adresse „22-Tal St“ kann zurück:

Id Address      Name HouseId
1  22 Valley St Dave 1
1  22 Valley St John 1
1  22 Valley St Mike 1

Die O / RM sollte mit ID = 1, Adresse = „22-Tal St“ eine Instanz von Hause füllen und dann bevölkert die Einwohner Array mit Menschen Instanzen für Dave, John und Mike mit nur einer Abfrage.

A N + 1 Abfrage für die gleiche Adresse verwendet oben in ergäbe:

Id Address
1  22 Valley St

mit einer separaten Abfrage wie

SELECT * FROM Person WHERE HouseId = 1

und was zu einem gesonderten Daten wie

Name    HouseId
Dave    1
John    1
Mike    1

und das Endergebnis ist das gleiche wie oben mit der einzigen Abfrage.

Die Vorteile für einzelne wählen ist, dass Sie alle Daten vorne bekommen, die sein kann, was Sie schließlich wünschen. Die Vorteile auf N + 1 Abfrage Komplexität reduziert wird, und können Sie verzögertes Laden verwenden, wo das Kind Ergebnismengen werden nur auf erste Anforderung geladen.

Lieferant mit einer Eins-zu-viele-Beziehung mit Produkt. Ein Lieferant hat (Lieferungen) viele Produkte.

***** Table: Supplier *****
+-----+-------------------+
| ID  |       NAME        |
+-----+-------------------+
|  1  |  Supplier Name 1  |
|  2  |  Supplier Name 2  |
|  3  |  Supplier Name 3  |
|  4  |  Supplier Name 4  |
+-----+-------------------+

***** Table: Product *****
+-----+-----------+--------------------+-------+------------+
| ID  |   NAME    |     DESCRIPTION    | PRICE | SUPPLIERID |
+-----+-----------+--------------------+-------+------------+
|1    | Product 1 | Name for Product 1 |  2.0  |     1      |
|2    | Product 2 | Name for Product 2 | 22.0  |     1      |
|3    | Product 3 | Name for Product 3 | 30.0  |     2      |
|4    | Product 4 | Name for Product 4 |  7.0  |     3      |
+-----+-----------+--------------------+-------+------------+

Faktoren:

  • Faule Modus für Lieferanten auf „true“ (default)

  • Modus für die Abfrage auf Produkt verwendet Fetch ist Wählen Sie

  • Fetch-Modus (Standard): Lieferanteninformationen zugegriffen

  • Caching keine Rolle zum ersten Mal spielt die

  • Lieferant zugegriffen

Fetch-Modus Wählen Sie Fetch (Standard)

// It takes Select fetch mode as a default
Query query = session.createQuery( "from Product p");
List list = query.list();
// Supplier is being accessed
displayProductsListWithSupplierName(results);

select ... various field names ... from PRODUCT
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
select ... various field names ... from SUPPLIER where SUPPLIER.id=?

Ergebnis:

  • 1 select-Anweisung für Produkt
  • N wählen Anweisungen für Lieferanten

Dies ist N + 1 und wählen Sie Problem!

Ich kann nicht direkt auf anderen Antworten kommentieren, weil ich nicht genug Ruf. Aber es ist erwähnenswert, dass das Problem im Wesentlichen nur entsteht, weil, historisch gesehen, hat viele dbms gewesen ziemlich schlecht, wenn es um Handhabung kommt verbindet (MySQL ein besonders bemerkenswertes Beispiel ist). So n + 1 hat, oft gewesen deutlich schneller als ein beizutreten. Und dann gibt es Möglichkeiten, auf n + 1, aber immer noch zu verbessern, ohne dass eine Verknüpfung, das ist, was das ursprüngliche Problem betrifft.

Allerdings MySQL ist jetzt viel besser als früher, wenn es darum geht, verbindet. Als ich das erste MySQL gelernt, habe ich viel verbindet. Dann entdeckte ich, wie langsam sie sind, und wechselte zu n + 1 in dem Code statt. Aber in letzter Zeit, ich habe schließt sich wieder zu bewegen, weil MySQL ist nun ein verdammt viel besser Umgang mit ihnen, als es war, als ich es zuerst gestartet werden.

In diesen Tagen, ein einfache auf einem korrekt indiziert Satz von Tabellen Join ist selten ein Problem, in anwendungstechnischer Sicht hat. Und wenn es einen Leistungseinbruch nicht geben, dann ist die Verwendung von Indexhinweisen oft löst sie.

Dies wird hier besprochen von einem des MySQL-Entwicklungsteam:

http://jorgenloland.blogspot.co.uk/2013/02/dbt-3-q3-6-x-performance-in-mysql-5610.html

So ist die Zusammenfassung: Wenn Sie bereits haben verbindet sich mit ihnen wegen MySQL abgründigen Performance in der Vergangenheit zu vermeiden, versuchen Sie dann erneut auf den neuesten Versionen. Sie werden wahrscheinlich angenehm überrascht sein.

Wir zogen von der ORM in Django weg wegen dieses Problems. Grundsätzlich, wenn Sie versuchen und tun

for p in person:
    print p.car.colour

Die ORM wird gerne alle Menschen zurückkehren (in der Regel als Instanzen einer Person-Objekt), aber dann wird es brauchen, um das Auto Tabelle für jede Person abzufragen.

Ein einfacher und sehr effektiver Weg, um dies ist etwas, was ich als „ Leporello- Faltung “, das die unsinnige Idee vermeidet, dass die Ergebnisse aus einer relationalen Datenbank abfragen sollten die ursprünglichen Tabellen, aus denen Karte zurück die Abfrage zusammengesetzt ist.

Schritt 1: Breiter wählen

  select * from people_car_colour; # this is a view or sql function

Das wird wieder so etwas wie

  p.id | p.name | p.telno | car.id | car.type | car.colour
  -----+--------+---------+--------+----------+-----------
  2    | jones  | 2145    | 77     | ford     | red
  2    | jones  | 2145    | 1012   | toyota   | blue
  16   | ashby  | 124     | 99     | bmw      | yellow

Schritt 2: Objectify

saugen die Ergebnisse in ein generisches Objekt Schöpfer mit einem Argument, nach dem dritten Element zu spalten. Das bedeutet, dass „jones“ Objekt wird nicht mehr als einmal gemacht werden.

Schritt 3: Übertragen

for p in people:
    print p.car.colour # no more car queries

Siehe diese Webseite für eine Implementierung von Leporello- Faltung für python.

Angenommen, Sie Unternehmens- und Mitarbeiter haben. Unternehmen verfügt über viele Mitarbeiter (das heißt EMPLOYEE hat ein Feld company_id).

In einigen O / R-Konfigurationen, wenn Sie ein zugeordnetes Unternehmen bezwecken und gehen seine Mitarbeiter Objekte zuzugreifen, die O / R Werkzeug tut man für jeden Mitarbeiter auswählen, wheras wenn Sie nur Dinge in gerade SQL taten, Sie könnte select * from employees where company_id = XX. So N (Anzahl der Mitarbeiter) plus 1 (Firma)

Dies ist, wie die ersten Versionen von EJB Entity Beans gearbeitet. Ich glaube, dass Dinge wie Hibernate getan haben, weg mit diesem, aber ich bin mir nicht sicher. Die meisten Werkzeuge sind in der Regel Informationen über ihre Strategie für die Zuordnung.

Hier ist eine gute Beschreibung des Problems - https://web.archive.org/web/20160310145416/http://www.realsolve.co.uk/site/tech/hib-tip -pitfall.php? name = why-faul

Nun, da Sie das Problem verstehen es in der Regel, indem Sie eine Verknüpfung holen in der Abfrage vermieden werden kann. Dies zwingt grundsätzlich die das lazy loaded Objekts zu holen, so dass die Daten in einer Abfrage abgerufen werden anstelle von n + 1 Abfrage. Hoffe, das hilft.

Meiner Meinung nach dem Artikel geschrieben in Hibernate Pitfall: Warum Beziehungen sollten faul ist genau das Gegenteil von dem realen N + 1 Ausgabe ist

.

Wenn Sie richtige Erklärung benötigen siehe Hibernate - Kapitel 19: Verbesserung der Leistung - Fetching-Strategien

  

Wählen Sie Abrufen (Standardeinstellung) ist   extrem anfällig für N + 1 wählt   Probleme, wollen wir vielleicht so ermöglichen,   beitreten Abrufen

Überprüfen Sie Ayende Beitrag zum Thema: Bekämpfung das auswählen N + 1 Problem In NHibernate

Grundsätzlich, wenn ein ORM wie NHibernate oder EntityFramework verwenden, wenn Sie eine Eins-zu-viele (Master-Detail) Beziehung und wollen für jeden Stammsatz alle Details aufzulisten, müssen Sie N + 1 Abfrage machen der Datenbank ruft, „N“ die Anzahl der Datensätze Master: 1 Abfrage alle Stammsätze zu erhalten, und N-Abfragen, eine pro Stammsatz, alle Details pro Stammsatz zu erhalten

.

Weitere Datenbankabfrage ruft -> mehr Latenzzeit -.> Verringerte Anwendung / Datenbank-Performance

Allerdings ORM haben Optionen, dieses Problem zu vermeiden, vor allem mit "verbindet".

Die N + 1 Abfrage Problem tritt auf, wenn Sie vergessen, einen Verein zu holen und dann müssen Sie es zugreifen:

List<PostComment> comments = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();

LOGGER.info("Loaded {} comments", comments.size());

for(PostComment comment : comments) {
    LOGGER.info("The post title is '{}'", comment.getPost().getTitle());
}

Welche generiert die folgenden SQL-Anweisungen:

SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_
FROM   post_comment pc
WHERE  pc.review = 'Excellent!'

INFO - Loaded 3 comments

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 1

INFO - The post title is 'Post nr. 1'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 2

INFO - The post title is 'Post nr. 2'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 3

INFO - The post title is 'Post nr. 3'

Als erstes Hibernate führt die JPQL Abfrage und eine Liste von PostComment Einheiten abgerufen.

Dann wird für jedes PostComment der zugehörige post Eigenschaft wird verwendet, um eine Log-Nachricht zu erzeugen, um den Post Titel enthält.

Da der post Verband nicht initialisiert wird, Hibernate muss die Post Entität mit einer sekundären Abfrage holen und für N PostComment Einheiten, N mehr Anfragen gehen ausgeführt werden (daher der N + 1 Abfrage Problem) sind.

Zuerst müssen Sie richtige SQL-Protokollierung und Überwachung so dass Sie dieses Problem erkennen.

Zweitens ist diese Art von Problem besser durch Integrationstests gefangen werden. Sie können eine zu validieren die erwartete Anzahl der generierten SQL-Anweisungen . Die db-Einheit Projekt bereits diese Funktionalität bereitstellt, und es ist Open Source.

Wenn Sie die N + 1 Abfrage Problem identifiziert, Sie müssen verwendet eine JOIN FETCH so dass Kind Verbände in einer Abfrage abgerufen werden, anstelle von N . Wenn Sie mehr Kinder Verbände holen müssen, ist es besser, eine Sammlung in der ersten Abfrage und die zweiten mit einer sekundären SQL-Abfrage zu holen.

Die mitgelieferte Link hat ein sehr einfach Beispiel für das n + 1 Problem. Wenn Sie sich bewerben sie es im Grunde über die gleiche Sache sprechen Hibernate. Wenn Sie für ein Objekt abzufragen, ist die Einheit geladen, aber alle Verbände (sofern nicht anders konfiguriert) werden faul geladen werden. Daher eine Abfrage für die Stammobjekte und andere Abfrage der Zuordnungen für jede diesen zu laden. 100 Objekte zurückgegeben Mitteln eine erste Abfrage und dann werden 100 zusätzliche Abfragen den Verein für jeden zu bekommen, n + 1.

http://pramatr.com/2009/02 / 05 / SQL-n-1-Selects erläuterte /

Ein Millionär hat N Autos. Sie möchten alle (4) Räder erhalten.

Ein (1) Abfrage lädt alle Autos, sondern für jede (N) Auto eine separate Abfrage wird für das Laden Räder gestellt.

Kosten:

Angenommen Indizes in ram passen.

1 + N query-Parsing und Hobel + Indexsuche und 1 + N + (N * 4) -Platte Zugang zum Laden Nutzlast.

Angenommen Indizes passen nicht in den Arbeitsspeicher.

Nebenkosten im schlimmsten Fall 1 + N Platte greift zum Laden Index.

Zusammenfassung

Flaschenhals ist Platte Zugang (ca. 70 mal pro Sekunde mit wahlfreiem Zugriff auf HDD) Ein eifriger verbinden wählen würde auch die Platte + 1 N + Zugriff (N * 4) Zeiten für Nutzlast. Also, wenn die Indizes passen in ram -. Kein Problem, seine schnell genug, da nur Operationen rammen beteiligt

Es ist viel schneller 1 Abfrage ausgeben, die 100 Ergebnisse liefert als 100 Anfragen zu erteilen, die jeweils 1 Ergebnis zurück.

N + 1 und wählen Sie Ausgabe ist ein Schmerz, und es macht Sinn, solche Fälle in Unit-Tests zu erfassen. Ich habe für die Überprüfung der Anzahl der Abfragen über eine kleine Bibliothek, entwickelt von einem bestimmten Testverfahren ausgeführt oder nur einen beliebigen Code-Block - JDBC Sniffer

Nur eine spezielle JUnit Regel auf Ihre Testklasse und Ort Annotation mit dem erwarteten Anzahl der Abfragen auf Ihrer Testmethode hinzufügen:

@Rule
public final QueryCounter queryCounter = new QueryCounter();

@Expectation(atMost = 3)
@Test
public void testInvokingDatabase() {
    // your JDBC or JPA code
}

Die Frage, wie andere elegante gesagt hat, ist, dass Sie entweder ein Kartesisches Produkt der OneToMany Spalten haben oder Sie tun N + 1 auswählt. Entweder möglich gigantische resultset oder gesprächig mit der Datenbank verbunden.

Ich bin überrascht dies nicht erwähnt, aber diese, wie ich, um dieses Problem bekommen habe ... Ich mache eine semi-temporäre IDs Tabelle . ich dies auch tun, wenn Sie die IN () Klausel Begrenzung haben.

Das funktioniert nicht für alle Fälle (wahrscheinlich nicht einmal eine Mehrheit), aber es funktioniert besonders gut, wenn Sie eine Menge von Kind-Objekten, so dass das kartesische Produkt aus der Hand wird (dh einer Menge OneToMany Spalten der Anzahl der Ergebnisse werden eine Multiplikation der Spalten sein) und seine mehr eine Charge wie Job.

Zuerst fügen Sie Ihren übergeordneten Objekt-IDs als Batch in eine ids Tabelle. Dieses batch_id ist etwas, das wir in unserer App generieren und halten an.

INSERT INTO temp_ids 
    (product_id, batch_id)
    (SELECT p.product_id, ? 
    FROM product p ORDER BY p.product_id
    LIMIT ? OFFSET ?);

Jetzt für jede OneToMany Spalte Sie einfach eine SELECT auf der ids Tabelle INNER JOINing die untergeordnete Tabelle mit einem WHERE batch_id= (oder umgekehrt). Sie wollen nur sicherstellen, dass Sie durch die Spalte id bestellen, wie es Ergebnisspalten machen Verschmelzung leichter (sonst müssen Sie eine HashMap / Tabelle für die gesamte Ergebnismenge, die nicht so schlecht sein kann).

Dann reinigen Sie nur in regelmäßigen Abständen die ids Tabelle.

Das funktioniert auch besonders gut, wenn der Benutzer beispielsweise 100 oder so unterschiedliche Produkte für irgendeine Art von Massenverarbeitung auswählt. Setzen Sie die 100 verschiedene IDs in der temporären Tabelle.

Nun ist die Anzahl der Abfragen, die Sie tun, ist durch die Anzahl der Spalten OneToMany.

Nehmen Matt Solnit Beispiel vorstellen, dass Sie eine Verbindung zwischen Auto und Räder als LAZY definieren und Sie müssen einige Räder Felder aus. Dies bedeutet, dass nach der ersten Auswahl, Hibernate wird "Select * from Wheels wo car_id =: id" tun. Für jedes Auto

Das macht die erste Auswahl und 1 wählen Sie von jedem Auto N, das ist, warum es n + 1 Problem genannt wird.

Um dies zu vermeiden, stellen Sie die Verbindung so eifrig holen, so dass Hibernate lädt Daten mit einem Join.

Aber Achtung, wenn viele Male Sie nicht zugeordneten Räder zugreifen, ist es besser, es zu halten LAZY oder ändern Typ mit Kriterien holen.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top