tl;dr Více Include
s vyhodit do povětří sadu výsledků SQL. Brzy bude levnější načítat data pomocí více databázových volání namísto spouštění jednoho mega příkazu. Pokuste se najít nejlepší kombinaci Include
a Load
prohlášení.
zdá se, že při použití Include
dochází k omezení výkonu
To je podcenění! Více Include
s rychle vyhodí do vzduchu výsledek SQL dotazu jak na šířku, tak na délku. Proč?
Růstový faktor Include
s
(Tato část platí Entity Framework classic, v6 a dřívější)
Řekněme, že máme
- kořenová entita
Root
- nadřazená entita
Root.Parent
- podřízené entity
Root.Children1
aRoot.Children2
- příkaz LINQ
Root.Include("Parent").Include("Children1").Include("Children2")
Tím se vytvoří příkaz SQL, který má následující strukturu:
SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children1
UNION
SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children2
Tyto <PseudoColumns>
sestávají z výrazů jako CAST(NULL AS int) AS [C2],
a slouží ke stejnému počtu sloupců ve všech UNION
-ed dotazy. První část přidává pseudo sloupce pro Child2
, druhá část přidává pseudo sloupce pro Child1
.
Toto znamená pro velikost sady výsledků SQL:
- Počet sloupců v
SELECT
klauzule je součet všech sloupců ve čtyřech tabulkách - Počet řádků je součet záznamů v zahrnutých podřízených kolekcích
Protože celkový počet datových bodů je columns * rows
, každé další Include
exponenciálně zvyšuje celkový počet datových bodů v sadě výsledků. Dovolte mi to demonstrovat pomocí Root
znovu, nyní s dalším Children3
sbírka. Pokud mají všechny tabulky 5 sloupců a 100 řádků, dostaneme:
Jeden Include
(Root
+ 1 podřízená kolekce):10 sloupců * 100 řádků =1000 datových bodů.
Dva Include
s (Root
+ 2 podřízené kolekce):15 sloupců * 200 řádků =3000 datových bodů.
Tři Include
s (Root
+ 3 podřízené kolekce):20 sloupců * 300 řádků =6000 datových bodů.
S 12 Includes
to by znamenalo 78 000 datových bodů!
A naopak, pokud získáte všechny záznamy pro každou tabulku samostatně namísto 12 Includes
, máte 13 * 5 * 100
datové body:6500, méně než 10 %!
Nyní jsou tato čísla poněkud přehnaná, protože mnoho z těchto datových bodů bude null
, takže ke skutečné velikosti sady výsledků, která je odeslána klientovi, příliš nepřispívají. Ale velikost dotazu a úkol pro optimalizátor dotazů jsou jistě negativně ovlivněny zvyšujícím se počtem Include
s.
Zůstatek
Takže pomocí Includes
je křehká rovnováha mezi cenou databázových volání a objemem dat. Těžko říct nějaké pravidlo, ale nyní si dokážete představit, že objem dat obecně rychle převýší náklady na další hovory, pokud je jich více než ~3 Includes
pro podřízené kolekce (ale o něco více pro nadřazené Includes
, které pouze rozšiřují sadu výsledků).
Alternativa
Alternativa k Include
je načíst data v samostatných dotazech:
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);
Tím se načtou všechna požadovaná data do mezipaměti kontextu. Během tohoto procesu EF provede opravu vztahu pomocí kterého automaticky vyplní navigační vlastnosti (Root.Children
atd.) načtenými entitami. Konečný výsledek je totožný s příkazem Include
s, až na jeden důležitý rozdíl:podřízené kolekce nejsou ve správci stavu entity označeny jako načtené, takže se EF pokusí spustit líné načítání, pokud k nim přistoupíte. Proto je důležité vypnout líné načítání.
Ve skutečnosti budete muset zjistit, jakou kombinaci Include
a Load
výroky pro vás fungují nejlépe.
Další aspekty ke zvážení
Každý Include
také zvyšuje složitost dotazů, takže optimalizátor dotazů databáze bude muset vynaložit stále více úsilí na nalezení nejlepšího plánu dotazů. V určitém okamžiku se to již nemusí podařit. Pokud také chybí některé životně důležité indexy (zejména na cizích klíčích), výkon může utrpět přidáním Include
s, dokonce i s nejlepším plánem dotazů.
Jádro Entity Framework
Kartézská exploze
Z nějakého důvodu bylo výše popsané chování, UNIONed dotazy, opuštěno od EF core 3. Nyní vytváří jeden dotaz s připojeními. Když je dotaz ve tvaru "hvězdy", vede to ke karteziánské explozi (v sadě výsledků SQL). Mohu najít pouze poznámku oznamující tuto zásadní změnu, ale neříká proč.
Rozdělit dotazy
Aby se čelilo této karteziánské explozi, zavedlo jádro Entity Framework 5 koncept rozdělených dotazů, které umožňují načítání souvisejících dat ve více dotazech. Zabraňuje vytvoření jedné masivní, znásobené sady výsledků SQL. Kvůli nižší složitosti dotazu může také zkrátit dobu potřebnou k načtení dat i při více zpátečních cestách. Při souběžných aktualizacích to však může vést k nekonzistentním datům.
Více vztahů 1:n mimo kořen dotazu.