Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Problémy s výkonem při jednotlivých dotazech
Při práci s relačními databázemi EF načte související entity zaváděním JOINů do jediného dotazu. I když jsou joiny při používání SQL poměrně standardní, můžou při nesprávném použití způsobit významné problémy s výkonem. Tato stránka popisuje tyto problémy s výkonem a ukazuje alternativní způsob, jak načíst související entity, které s nimi fungují.
Kartézský výbuch
Pojďme se podívat na následující dotaz LINQ a jeho přeložený ekvivalent SQL:
var blogs = await ctx.Blogs
.Include(b => b.Posts)
.Include(b => b.Contributors)
.ToListAsync();
SELECT [b].[Id], [b].[Name], [p].[Id], [p].[BlogId], [p].[Title], [c].[Id], [c].[BlogId], [c].[FirstName], [c].[LastName]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Contributors] AS [c] ON [b].[Id] = [c].[BlogId]
ORDER BY [b].[Id], [p].[Id]
V tomto příkladu, protože obě Posts i kolekce navigace Contributors - jsou na stejné úrovni - relační databáze vrací křížový Blog: každý řádek z je spojený s každým řádkem z PostsContributors . To znamená, že pokud má daný blog 10 příspěvků a 10 přispěvatelů, databáze vrátí pro tento jeden blog 100 řádků. Tento jev – někdy označovaný jako kartézská exploze – může způsobit neúmyslný přenos velkého množství dat do klienta, zejména když do dotazu přidáte více paralelních JOINů, což může být zásadní problém s výkonem v databázových aplikacích.
Všimněte si, že k kartézské explozi nedojde, pokud tyto dva joiny nejsou na stejné úrovni:
var blogs = await ctx.Blogs
.Include(b => b.Posts)
.ThenInclude(p => p.Comments)
.ToListAsync();
SELECT [b].[Id], [b].[Name], [t].[Id], [t].[BlogId], [t].[Title], [t].[Id0], [t].[Content], [t].[PostId]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Comment] AS [c] ON [p].[Id] = [c].[PostId]
ORDER BY [b].[Id], [t].[Id]
V tomto dotazu je Comments navigace kolekce Post, na rozdíl od Contributors v předchozím dotazu, což byla navigace kolekce Blog. V tomto případě se vrátí jeden řádek pro každý komentář, který má blog (prostřednictvím svých příspěvků), a křížový součin nenastane.
Duplikace dat
Operace JOIN mohou způsobit jiný typ potíže s výkonem. Pojďme se podívat na následující dotaz, který načte pouze jednu navigaci v kolekci:
var blogs = await ctx.Blogs
.Include(b => b.Posts)
.ToListAsync();
SELECT [b].[Id], [b].[Name], [b].[HugeColumn], [p].[Id], [p].[BlogId], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
ORDER BY [b].[Id]
Při zkoumání projektovaných sloupců obsahuje každý řádek vrácený tímto dotazem vlastnosti z obou tabulek Blogs a Posts; to znamená, že vlastnosti blogu jsou duplikovány pro každý příspěvek, který blog má. I když je to obvykle normální a nezpůsobuje žádné problémy, pokud Blogs se stane, že tabulka obsahuje velmi velký sloupec (např. binární data nebo obrovský text), tento sloupec se několikrát duplikuje a odešle zpět klientovi. To může výrazně zvýšit síťový provoz a nepříznivě ovlivnit výkon vaší aplikace.
Pokud ve skutečnosti nepotřebujete obrovský sloupec, jednoduše se na něj nebudete dotazovat:
var blogs = await ctx.Blogs
.Select(b => new
{
b.Id,
b.Name,
b.Posts
})
.ToListAsync();
Pomocí projekce explicitně zvolíte, které sloupce chcete, můžete vynechat velké a nepotřebné sloupce a zlepšit tím výkon. Mějte na paměti, že toto je dobrý nápad bez ohledu na duplikaci dat, proto zvažte tuto možnost i tehdy, pokud nenačítáte kolekci. Vzhledem k tomu, že tento projekt převádí blog na anonymní typ, blog není sledován EF a změny v něm nelze uložit zpět jako obvykle.
Stojí za zmínku, že na rozdíl od kartézské exploze není duplicita dat způsobená příkazem JOIN obvykle významná, protože velikost duplicitních dat je zanedbatelná; obvykle to je něco, čím byste si měli dělat starosti pouze pokud máte velké sloupce v hlavní tabulce.
Rozdělené dotazy
Pokud chcete vyřešit problémy s výkonem popsanými výše, ef umožňuje určit, že daný dotaz LINQ by se měl rozdělit na několik dotazů SQL. Místo použití JOINs vygenerují rozdělené dotazy další SQL dotaz pro každou zahrnutou kolekci navigace.
using (var context = new BloggingContext())
{
var blogs = await context.Blogs
.Include(blog => blog.Posts)
.AsSplitQuery()
.ToListAsync();
}
Vytvoří následující SQL:
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]
ORDER BY [b].[BlogId]
SELECT [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title], [b].[BlogId]
FROM [Blogs] AS [b]
INNER JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]
ORDER BY [b].[BlogId]
Upozornění
Při použití rozdělených dotazů s použitím Skip/Take na verzích EF před 10 věnujte zvláštní pozornost tomu, aby řazení dotazů bylo zcela jedinečné; jinak by to mohlo způsobit vrácení nesprávných dat. Pokud jsou například výsledky seřazené pouze podle data, ale může existovat více výsledků se stejným datem, každý z rozdělených dotazů může z databáze získat různé výsledky. Řazení podle data i ID (nebo jakékoli jiné jedinečné vlastnosti nebo kombinace vlastností) způsobí, že řazení je plně jedinečné a zabrání tomuto problému. Mějte na paměti, že relační databáze ve výchozím nastavení nepoužívají žádné řazení, a to ani u primárního klíče.
Poznámka:
Entity související v poměru 1:1 se vždy načítají prostřednictvím JOINů ve stejném dotazu, protože to nemá žádný dopad na výkon.
Povolení rozdělených dotazů na globální úrovni
Rozdělené dotazy můžete také nakonfigurovat jako výchozí pro kontext vaší aplikace:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;ConnectRetryCount=0",
o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
}
Pokud jsou rozdělené dotazy nakonfigurované jako výchozí, je stále možné nakonfigurovat konkrétní dotazy tak, aby se spouštěly jako jednotlivé dotazy:
using (var context = new SplitQueriesBloggingContext())
{
var blogs = await context.Blogs
.Include(blog => blog.Posts)
.AsSingleQuery()
.ToListAsync();
}
EF Core ve výchozím nastavení používá režim jednoho dotazu bez jakékoli konfigurace. Vzhledem k tomu, že může dojít k problémům s výkonem, EF Core vygeneruje upozornění při splnění následujících podmínek:
- EF Core zjistí, že dotaz načte více kolekcí.
- Uživatel nenakonfiguroval globálně režim dělení dotazů.
- Uživatel v dotazu nepoužil
AsSingleQuery/AsSplitQueryoperátor.
Pokud chcete upozornění vypnout, nakonfigurujte režim dělení dotazů globálně nebo na úrovni dotazu na odpovídající hodnotu.
Charakteristiky rozdělených dotazů
Rozdělený dotaz sice zabraňuje problémům s výkonem souvisejících s JOINy a kartézskou explozí, ale má také některé nevýhody:
- I když většina databází zaručuje konzistenci dat pro jednotlivé dotazy, neexistují žádné takové záruky pro více dotazů. Pokud se databáze aktualizuje souběžně při provádění dotazů, nemusí být výsledná data konzistentní. Můžete to zmírnit zabalením dotazů do serializovatelné nebo snímkové transakce, i když to může vytvořit vlastní problémy s výkonem. Další informace najdete v dokumentaci k databázi.
- Každý dotaz v současné době znamená další přenos přes síť do vaší databáze. Několik síťových cyklů může snížit výkon, zejména pokud je latence databáze vysoká (například cloudové služby).
- I když některé databáze umožňují využívat výsledky více dotazů najednou (SQL Server s MARS, Sqlite), většina z nich umožňuje, aby byl v každém daném okamžiku aktivní jenom jeden dotaz. Takže všechny výsledky z dřívějších dotazů musí být do vyrovnávací paměti v paměti aplikace před spuštěním pozdějších dotazů, což vede ke zvýšení požadavků na paměť.
- Pokud zahrnete referenční navigace i navigace kolekce, bude každý z rozdělených dotazů zahrnovat spojení s referenčními navigacemi. To může snížit výkon, zejména pokud existuje mnoho referenčních navigací. Pokud byste chtěli vidět, že se to spraví, dejte prosím hlas #29182.
Bohužel neexistuje jedna strategie pro načítání souvisejících entit, které odpovídají všem scénářům. Pečlivě zvažte výhody a nevýhody jednoduchých a rozdělených dotazů a vyberte ten, který vyhovuje vašim potřebám.