tl;dr Meerdere Include
s blaas de SQL-resultatenset op. Al snel wordt het goedkoper om gegevens te laden door meerdere database-aanroepen in plaats van één mega-statement uit te voeren. Probeer de beste combinatie van Include
. te vinden en Load
verklaringen.
het lijkt erop dat er een prestatiestraf is bij het gebruik van Include
Dat is een understatement! Meerdere Include
s blaast snel het SQL-queryresultaat op, zowel in de breedte als in de lengte. Waarom is dat?
Groeifactor van Include
s
(Dit deel is van toepassing op Entity Framework classic, v6 en eerder)
Laten we zeggen dat we hebben
- root entiteit
Root
- bovenliggende entiteit
Root.Parent
- onderliggende entiteiten
Root.Children1
enRoot.Children2
- een LINQ-statement
Root.Include("Parent").Include("Children1").Include("Children2")
Dit bouwt een SQL-statement op met de volgende structuur:
SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children1
UNION
SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children2
Deze <PseudoColumns>
bestaan uit uitdrukkingen zoals CAST(NULL AS int) AS [C2],
en ze dienen om hetzelfde aantal kolommen te hebben in alle UNION
-ed vragen. Het eerste deel voegt pseudokolommen toe voor Child2
, het tweede deel voegt pseudo-kolommen toe voor Child1
.
Dit is wat het betekent voor de grootte van de SQL-resultatenset:
- Aantal kolommen in de
SELECT
clausule is de som van alle kolommen in de vier tabellen - Het aantal rijen is de som van records in opgenomen onderliggende collecties
Aangezien het totale aantal gegevenspunten columns * rows
. is , elke extra Include
neemt het totale aantal gegevenspunten in de resultatenset exponentieel toe. Laat me dat aantonen door Root
. te nemen nogmaals, nu met een extra Children3
verzameling. Als alle tabellen 5 kolommen en 100 rijen hebben, krijgen we:
Eén Include
(Root
+ 1 onderliggende verzameling):10 kolommen * 100 rijen =1000 gegevenspunten.
Twee Include
s (Root
+ 2 onderliggende collecties):15 kolommen * 200 rijen =3000 gegevenspunten.
Drie Include
s (Root
+ 3 onderliggende verzamelingen):20 kolommen * 300 rijen =6000 gegevenspunten.
Met 12 Includes
dit zou neerkomen op 78000 datapunten!
Omgekeerd, als u alle records voor elke tabel afzonderlijk krijgt in plaats van 12 Includes
, je hebt 13 * 5 * 100
datapunten:6500, minder dan 10%!
Nu zijn deze getallen enigszins overdreven omdat veel van deze gegevenspunten null
zullen zijn , dus ze dragen niet veel bij aan de werkelijke grootte van de resultatenset die naar de klant wordt verzonden. Maar de querygrootte en de taak voor de query-optimizer worden zeker negatief beïnvloed door het toenemende aantal Include
v.
Saldo
Dus gebruik Includes
is een delicaat evenwicht tussen de kosten van database-oproepen en datavolume. Het is moeilijk om een vuistregel te geven, maar je kunt je inmiddels voorstellen dat het datavolume over het algemeen snel groter is dan de kosten van extra oproepen als er meer dan ~3 Includes
zijn. voor onderliggende collecties (maar nog veel meer voor bovenliggende Includes
, die de resultatenset alleen maar verbreden).
Alternatief
Het alternatief voor Include
is om gegevens in afzonderlijke zoekopdrachten te 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);
Dit laadt alle vereiste gegevens in de cache van de context. Tijdens dit proces voert EF relatieherstel uit waarmee het automatisch navigatie-eigenschappen invult (Root.Children
enz.) door geladen entiteiten. Het eindresultaat is identiek aan het statement met Include
s, met uitzondering van een belangrijk verschil:de onderliggende collecties zijn niet gemarkeerd als geladen in de entiteitsstatusmanager, dus EF zal proberen lui laden te activeren als je ze opent. Daarom is het belangrijk om lui laden uit te schakelen.
In werkelijkheid zult u moeten uitzoeken welke combinatie van Include
en Load
uitspraken werken het beste voor u.
Andere aspecten om te overwegen
Elke Include
verhoogt ook de complexiteit van query's, zodat de query-optimizer van de database steeds meer moeite zal moeten doen om het beste queryplan te vinden. Op een gegeven moment lukt dit misschien niet meer. Ook als enkele vitale indexen ontbreken (in het bijzonder op externe sleutels), kunnen de prestaties eronder lijden door Include
toe te voegen. s, zelfs met het beste zoekplan.
Entity Framework-kern
Cartesiaanse explosie
Om de een of andere reden werd het hierboven beschreven gedrag, UNIONed-query's, opgegeven vanaf EF core 3. Het bouwt nu één query met joins. Wanneer de query de vorm van een "ster" heeft, leidt dit tot een cartesiaanse explosie (in de SQL-resultatenset). Ik kan alleen een briefje vinden waarin deze belangrijke wijziging wordt aangekondigd, maar er staat niet in waarom.
Zoekopdrachten splitsen
Om deze Cartesiaanse explosie tegen te gaan, introduceerde Entity Framework core 5 het concept van gesplitste query's waarmee gerelateerde gegevens in meerdere query's kunnen worden geladen. Het voorkomt het bouwen van één enorme, vermenigvuldigde SQL-resultatenset. Vanwege de lagere querycomplexiteit kan het ook de tijd verminderen die nodig is om gegevens op te halen, zelfs bij meerdere roundtrips. Het kan echter leiden tot inconsistente gegevens wanneer gelijktijdige updates plaatsvinden.
Meerdere 1:n-relaties buiten de query-root.