Sqlserver
 sql >> Datenbank >  >> RDS >> Sqlserver

Entity-Framework-Code ist langsam, wenn Include() oft verwendet wird

tl;dr Mehrere Include ■ Vergrößern Sie die SQL-Ergebnismenge. Bald wird es billiger, Daten durch mehrere Datenbankaufrufe zu laden, anstatt eine Mega-Anweisung auszuführen. Versuchen Sie, die beste Mischung aus Include zu finden und Load Aussagen.

Es scheint, dass es bei der Verwendung von Include

zu Leistungseinbußen kommt

Das ist eine Untertreibung! Mehrere Include s vergrößern Sie schnell das Ergebnis der SQL-Abfrage sowohl in der Breite als auch in der Länge. Warum ist das so?

Wachstumsfaktor von Include s

(Dieser Teil gilt für Entity Framework Classic, v6 und früher)

Nehmen wir an, wir haben

  • Root-Entität Root
  • übergeordnete Entität Root.Parent
  • untergeordnete Entitäten Root.Children1 und Root.Children2
  • eine LINQ-Anweisung Root.Include("Parent").Include("Children1").Include("Children2")

Dadurch wird eine SQL-Anweisung erstellt, die die folgende Struktur hat:

SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children1

UNION

SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children2

Diese <PseudoColumns> bestehen aus Ausdrücken wie CAST(NULL AS int) AS [C2], und sie dienen dazu, in allen UNION die gleiche Anzahl an Spalten zu haben -ed Abfragen. Der erste Teil fügt Pseudospalten für Child2 hinzu , fügt der zweite Teil Pseudospalten für Child1 hinzu .

Dies bedeutet für die Größe der SQL-Ergebnismenge:

  • Anzahl der Spalten im SELECT -Klausel ist die Summe aller Spalten in den vier Tabellen
  • Die Anzahl der Zeilen ist die Summe der Datensätze in eingeschlossenen untergeordneten Sammlungen

Da die Gesamtzahl der Datenpunkte columns * rows ist , jedes weitere Include erhöht die Gesamtzahl der Datenpunkte in der Ergebnismenge exponentiell. Lassen Sie mich das demonstrieren, indem ich Root nehme wieder, jetzt mit einem zusätzlichen Children3 Sammlung. Wenn alle Tabellen 5 Spalten und 100 Zeilen haben, erhalten wir:

Ein Include (Root + 1 untergeordnete Sammlung):10 Spalten * 100 Zeilen =1000 Datenpunkte.
Zwei Include s (Root + 2 untergeordnete Sammlungen):15 Spalten * 200 Zeilen =3000 Datenpunkte.
Drei Include s (Root + 3 untergeordnete Sammlungen):20 Spalten * 300 Zeilen =6000 Datenpunkte.

Mit 12 Includes das wären 78000 Datenpunkte!

Umgekehrt, wenn Sie statt 12 Includes alle Datensätze für jede Tabelle separat erhalten , haben Sie 13 * 5 * 100 Datenpunkte:6500, weniger als 10 %!

Nun sind diese Zahlen etwas übertrieben, da viele dieser Datenpunkte null sein werden , sodass sie nicht viel zur tatsächlichen Größe der Ergebnismenge beitragen, die an den Client gesendet wird. Aber die Abfragegröße und die Aufgabe für den Abfrageoptimierer werden sicherlich negativ beeinflusst durch eine zunehmende Anzahl von Include s.

Guthaben

Verwenden Sie also Includes ist ein empfindliches Gleichgewicht zwischen den Kosten für Datenbankaufrufe und dem Datenvolumen. Es ist schwer, eine Faustregel zu geben, aber Sie können sich mittlerweile vorstellen, dass das Datenvolumen bei mehr als ~3 Includes die Kosten für zusätzliche Anrufe in der Regel schnell übersteigt für untergeordnete Sammlungen (aber einiges mehr für übergeordnete Includes , die nur die Ergebnismenge erweitern).

Alternative

Die Alternative zu Include ist, Daten in separate Abfragen zu laden:

context.Configuration.LazyLoadingEnabled = false;
var rootId = 1;
context.Children1.Where(c => c.RootId == rootId).Load();
context.Children2.Where(c => c.RootId == rootId).Load();
return context.Roots.Find(rootId);

Dadurch werden alle erforderlichen Daten in den Cache des Kontexts geladen. Während dieses Vorgangs führt EF eine Beziehungskorrektur aus wodurch Navigationseigenschaften automatisch ausgefüllt werden (Root.Children usw.) durch geladene Entitäten. Das Endergebnis ist identisch mit der Anweisung mit Include s, mit Ausnahme eines wichtigen Unterschieds:Die untergeordneten Sammlungen sind im Entity State Manager nicht als geladen markiert, sodass EF versucht, verzögertes Laden auszulösen, wenn Sie darauf zugreifen. Deshalb ist es wichtig, Lazy Loading zu deaktivieren.

In Wirklichkeit müssen Sie herausfinden, welche Kombination von Include und Load Anweisungen funktionieren am besten für Sie.

Andere zu berücksichtigende Aspekte

Jedes Include erhöht auch die Abfragekomplexität, sodass der Abfrageoptimierer der Datenbank immer mehr Aufwand betreiben muss, um den besten Abfrageplan zu finden. Irgendwann gelingt das vielleicht nicht mehr. Wenn einige wichtige Indizes fehlen (insbesondere bei Fremdschlüsseln), kann die Leistung durch Hinzufügen von Include beeinträchtigt werden s, selbst mit dem besten Abfrageplan.

Entity Framework-Kern

Kartesische Explosion

Aus irgendeinem Grund wurde das oben beschriebene Verhalten, UNIONed-Abfragen, ab EF Core 3 aufgegeben. Es erstellt jetzt eine Abfrage mit Verknüpfungen. Wenn die Abfrage "sternförmig" ist, führt dies zu einer kartesischen Explosion (in der SQL-Ergebnismenge). Ich kann nur eine Notiz finden, die diese bahnbrechende Änderung ankündigt, aber sie sagt nicht warum.

Abfragen aufteilen

Um dieser kartesischen Explosion entgegenzuwirken, wurde in Entity Framework Core 5 das Konzept geteilter Abfragen eingeführt, das das Laden verwandter Daten in mehreren Abfragen ermöglicht. Es verhindert den Aufbau einer massiven, multiplizierten SQL-Ergebnismenge. Aufgrund der geringeren Abfragekomplexität kann es auch bei mehreren Roundtrips die Zeit reduzieren, die zum Abrufen von Daten benötigt wird. Es kann jedoch zu inkonsistenten Daten führen, wenn gleichzeitige Aktualisierungen auftreten.

Mehrere 1:n-Beziehungen aus dem Abfragestamm.