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
undRoot.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.