Frage Was ist das N + 1 SELECT-Abfrageproblem?


SELECT N + 1 wird allgemein als Problem in Object-Relational Mapping (ORM) -Diskussionen angegeben, und ich verstehe, dass es etwas damit zu tun hat, viele Datenbankabfragen für etwas zu machen, das in der Objektwelt einfach erscheint.

Hat jemand eine detailliertere Erklärung des Problems?


1297
2017-09-18 21:30


Ursprung


Antworten:


Nehmen wir an, Sie haben eine Sammlung von Car Objekte (Datenbankzeilen) und jeder Car hat eine Sammlung von Wheel Objekte (auch Zeilen). Mit anderen Worten, Car -> Wheel ist eine 1: n-Beziehung.

Nehmen wir an, Sie müssen alle Autos durchlaufen und für jede eine Liste der Räder ausdrucken. Die naive O / R-Implementierung würde Folgendes tun:

SELECT * FROM Cars;

Und dann für jede Car:

SELECT * FROM Wheel WHERE CarId = ?

Mit anderen Worten, Sie haben eine Auswahl für die Autos, und dann wählt N zusätzliche, wobei N die Gesamtzahl der Autos ist.

Alternativ könnte man alle Räder bekommen und die Lookups im Speicher durchführen:

SELECT * FROM Wheel

Dies reduziert die Anzahl der Hin- und Rückfahrten von N + 1 auf 2. Die meisten ORM-Tools bieten mehrere Möglichkeiten, um N + 1-Selektionen zu verhindern.

Referenz: Java-Persistenz mit Hibernate, Kapitel 13.


767
2017-09-18 21:36



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

Dadurch erhalten Sie eine Ergebnismenge, bei der untergeordnete Zeilen in Tabelle2 zu einer Duplizierung führen, indem die Ergebnisse von table1 für jede untergeordnete Zeile in Tabelle2 zurückgegeben werden. O / R-Mapper sollten table1-Instanzen basierend auf einem eindeutigen Schlüsselfeld unterscheiden und dann alle table2-Spalten verwenden, um untergeordnete Instanzen zu füllen.

SELECT table1.*

SELECT table2.* WHERE SomeFkId = #

Das N + 1 ist, wo die erste Abfrage das primäre Objekt auffüllt, und die zweite Abfrage füllt alle unterordneten Objekte für jedes der zurückgegebenen eindeutigen primären Objekte.

Erwägen:

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 Valley St" kann zurückkehren:

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

Das O / RM sollte eine Instanz von Home mit ID = 1, Adresse = "22 Valley St" füllen und dann das Feld Bewohner mit People-Instanzen für Dave, John und Mike mit nur einer Abfrage füllen.

Eine N + 1 Abfrage für dieselbe Adresse, die oben verwendet wurde, würde zu folgendem führen:

Id Address
1  22 Valley St

mit einer separaten Abfrage wie

SELECT * FROM Person WHERE HouseId = 1

und ergibt einen separaten Datensatz wie

Name    HouseId
Dave    1
John    1
Mike    1

und das Endergebnis ist dasselbe wie oben bei der einzelnen Abfrage.

Die Vorteile von Single Select besteht darin, dass Sie alle Daten im Voraus erhalten, was Sie letztendlich wünschen. Die Vorteile von N + 1 sind, dass die Komplexität der Abfrage verringert wird und Sie das Lazy-Laden verwenden können, bei dem die untergeordneten Ergebnismengen nur bei der ersten Anforderung geladen werden.


98
2017-09-18 21:43



Lieferant mit einer Eins-zu-Viele-Beziehung mit Produkt. Ein Lieferant hat (liefert) 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:

  • Lazy-Modus für Lieferanten auf "true" (Standard) eingestellt

  • Abrufmodus, der für die Abfrage von Produkt verwendet wird, ist Auswählen

  • Abrufmodus (Standard): Auf Lieferanteninformationen wird zugegriffen

  • Caching spielt zum ersten Mal keine Rolle

  • Der Lieferant wird aufgerufen

Abrufmodus ist Auswählen Abruf (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 Select-Anweisungen für den Lieferanten

Dies ist N + 1 Auswahlproblem!


58
2017-12-01 13:35



Ich kann andere Antworten nicht direkt kommentieren, weil ich nicht genug Reputation habe. Es ist jedoch erwähnenswert, dass das Problem im Wesentlichen nur deshalb auftritt, weil in der Vergangenheit viele dbms ziemlich schlecht waren, wenn es um den Umgang mit Joins ging (MySQL ist ein besonders bemerkenswertes Beispiel). So war n + 1 oft deutlich schneller als ein Join. Und dann gibt es Möglichkeiten, n + 1 zu verbessern, aber immer noch ohne einen Join zu benötigen, 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 gelernt habe, habe ich viel Joins benutzt. Dann entdeckte ich, wie langsam sie sind, und wechselte stattdessen zu n + 1 im Code. Aber in letzter Zeit bin ich wieder zu Joins zurückgekehrt, weil MySQL jetzt viel besser damit zurechtkommt als zu Beginn meiner Arbeit.

Heutzutage ist ein einfacher Join auf einem ordnungsgemäß indizierten Satz von Tabellen selten ein Problem in Bezug auf die Leistung. Und wenn es einen Performance-Hit gibt, dann löst die Verwendung von Index-Hinweisen oft sie.

Dies wird hier von einem der MySQL-Entwickler diskutiert:

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

Die Zusammenfassung lautet also so: Wenn Sie Joins in der Vergangenheit wegen der abgrundtiefen MySQL-Performance vermieden haben, versuchen Sie es erneut mit den neuesten Versionen. Sie werden wahrscheinlich angenehm überrascht sein.


33
2018-01-08 12:49



Wir sind wegen dieses Problems vom ORM in Django weggezogen. Grundsätzlich, wenn Sie es versuchen und tun

for p in person:
    print p.car.colour

Das ORM wird alle Personen (normalerweise als Instanzen eines Person-Objekts) zurückgeben, aber dann muss es die Autotabelle für jede Person abfragen.

Ein einfacher und sehr effektiver Ansatz ist das, was ich "Fanfaltung", was die unsinnige Idee vermeidet, dass Abfrageergebnisse von einer relationalen Datenbank zu den ursprünglichen Tabellen, aus denen die Abfrage besteht, zurückverknüpft werden sollten.

Schritt 1: Große Auswahl

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

Dies wird etwas zurückgeben

  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: Objektivieren

Ziehen Sie die Ergebnisse in einen generischen Objekt-Ersteller mit einem Argument, das nach dem dritten Element aufgeteilt werden soll. Dies bedeutet, dass "Jones" -Objekte nicht mehr als einmal erstellt werden.

Schritt 3: Rendern

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

Sehen diese Webseite für eine Implementierung von Fanfaltung für Python.


25
2018-06-09 21:18



Angenommen, Sie haben UNTERNEHMEN und MITARBEITER. UNTERNEHMEN hat viele MITARBEITER (d. H. MITARBEITER hat ein Feld COMPANY_ID).

Wenn Sie in einigen O / R-Konfigurationen ein zugeordnetes Company-Objekt haben und auf seine Employee-Objekte zugreifen, führt das O / R-Tool eine Auswahl für jeden Mitarbeiter durch, während Sie, wenn Sie nur Dinge in direktem SQL machen würden select * from employees where company_id = XX. Also N (Anzahl der Angestellten) plus 1 (Firma)

So funktionierten die ersten Versionen von EJB Entity Beans. Ich glaube, Dinge wie Hibernate haben damit aufgehört, aber ich bin mir nicht sicher. Die meisten Tools enthalten normalerweise Informationen zu ihrer Strategie für das Mapping.


16
2017-09-18 21:33



Hier ist eine gute Beschreibung des Problems - http://www.realsolve.co.uk/site/tech/hib-tip-pitfall.php?name=why-lazy

Jetzt, wo Sie das Problem verstehen, können Sie es in der Regel vermeiden, indem Sie in Ihrer Abfrage einen Join-Abruf durchführen. Dies erzwingt im Wesentlichen das Abrufen des Lazy Loaded-Objekts, so dass die Daten in einer Abfrage anstelle von n + 1 Abfragen abgerufen werden. Hoffe das hilft.


14
2017-09-18 21:43



Überprüfen Sie Ayende Beitrag zum Thema: Bekämpfen des Select N + 1 Problems in NHibernate

Wenn Sie eine ORM wie NHibernate oder EntityFramework verwenden, müssen Sie, wenn Sie eine 1: n-Beziehung (Master-Detail) haben und alle Details für jeden Stammdatensatz auflisten möchten, N + 1-Query-Aufrufe an den Datenbank, wobei "N" die Anzahl der Stammdatensätze ist: 1 Abfrage, um alle Stammdatensätze zu erhalten, und N Abfragen, eine pro Stammdatensatz, um alle Details pro Stammdatensatz zu erhalten.

Mehr Abfragen in der Datenbankabfrage -> mehr Latenzzeit -> geringere Anwendungs- / Datenbankleistung

ORMs haben jedoch Optionen, um dieses Problem zu vermeiden, hauptsächlich mit "Joins".


12
2018-06-05 22:21