Sdílet prostřednictvím


Jednoduché a rozdělené dotazy

Problémy s výkonem s jedním dotazem

Při práci s relačními databázemi ef načte související entity tím, že do jednoho dotazu zavádějí joiny. 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 = ctx.Blogs
    .Include(b => b.Posts)
    .Include(b => b.Contributors)
    .ToList();
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 Blog - jsou na stejné úrovni - relační databáze vrací křížový produkt: každý řádek z Posts je spojený s každým řádkem z ContributorsContributors . 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 hodnot JOIN, což může být velký problém s výkonem v databázových aplikacích.

Všimněte si, že k kartézskému explozi nedojde, pokud tyto dva JOIN nejsou na stejné úrovni:

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .ThenInclude(p => p.Comments)
    .ToList();
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 Comments je kolekce navigace Post, na rozdíl od Contributors předchozího dotazu, což byla kolekce navigace Blog. V tomto případě se pro každý komentář, který má blog (prostřednictvím svých příspěvků), vrátí jeden řádek a křížový produkt se neprojeví.

Duplikace dat

Sítě JOI můžou vytvořit jiný typ problému s výkonem. Pojďme se podívat na následující dotaz, který načte pouze jednu navigaci v kolekci:

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .ToList();
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í v projektovaných sloupcích obsahuje každý řádek vrácený tímto dotazem vlastnosti z obou Blogs tabulek. Posts To znamená, že vlastnosti blogu jsou duplikovány pro každý příspěvek, který blog obsahuje. 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 = ctx.Blogs
    .Select(b => new
    {
        b.Id,
        b.Name,
        b.Posts
    })
    .ToList();

Pomocí projekce explicitně zvolit, které sloupce chcete, můžete vynechat velké sloupce a zlepšit výkon; Mějte na paměti, že to je dobrý nápad bez ohledu na duplikaci dat, proto zvažte, že i když nenačítáte navigaci kolekce. Vzhledem k tomu, že tento projekt projektuje blog na anonymní typ, není blog sledován ef a změny v něm nelze uložit jako obvykle.

Stojí za zmínku, že na rozdíl od kartézského exploze není duplicita dat způsobená JOIN obvykle významná, protože duplicitní velikost dat je zanedbatelná; obvykle je to něco, co byste si měli dělat starosti jenom v případě, že máte v hlavní tabulce velké sloupce.

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 hodnot JOIN vygenerují rozdělené dotazy další dotaz SQL pro každou zahrnutou navigaci v kolekci:

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
        .AsSplitQuery()
        .ToList();
}

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]

Upozorňující

Při použití rozdělených dotazů s funkcí Skip/Take věnujte zvláštní pozornost tomu, aby řazení dotazů bylo plně jedinečné; to by 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í s 1:1 se vždy načítají prostřednictvím JOIN ve stejném dotazu, protože nemají žádný dopad na výkon.

Globální povolení rozdělených dotazů

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 = context.Blogs
        .Include(blog => blog.Posts)
        .AsSingleQuery()
        .ToList();
}

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/AsSplitQuery operá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ím s JOIN a kartézským 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 ho zmírnit zabalením dotazů do serializovatelné nebo snímek transakce, i když to může způsobit problémy s výkonem své vlastní. Další informace najdete v dokumentaci k databázi.
  • Každý dotaz v současné době implikuje další odezvu sítě do 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 před spuštěním pozdějších dotazů v paměti vaší aplikace do vyrovnávací paměti, 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 navigačních panelů. Pokud byste chtěli vidět něco, co byste chtěli opravit, upvote prosím #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.