Efektivní dotazování

Efektivní dotazování je obrovské téma, které pokrývá předměty jako indexy, související strategie načítání entit a mnoho dalších. Tato část podrobně popisuje některé běžné motivy pro rychlejší dotazování a uživatelé obvykle narazí na nástrahy.

Správné používání indexů

Hlavním rozhodovacím faktorem, zda dotaz běží rychle nebo ne, je to, jestli bude správně využívat indexy tam, kde je to vhodné: databáze se obvykle používají k ukládání velkých objemů dat a dotazy, které procházejí celými tabulkami, jsou obvykle zdrojem závažných problémů s výkonem. Problémy s indexováním nejsou snadné zjistit, protože není zřejmé, jestli daný dotaz použije index, nebo ne. Příklad:

// Matches on start, so uses an index (on SQL Server)
var posts1 = context.Posts.Where(p => p.Title.StartsWith("A")).ToList();
// Matches on end, so does not use the index
var posts2 = context.Posts.Where(p => p.Title.EndsWith("A")).ToList();

Dobrým způsobem, jak odhalit problémy s indexováním, je nejprve určit pomalý dotaz a pak prozkoumat jeho plán dotazů prostřednictvím oblíbeného nástroje vaší databáze; Další informace o tom, jak to udělat, najdete na stránce s diagnostikou výkonu . Plán dotazu zobrazuje, jestli dotaz prochází celou tabulkou, nebo používá index.

Obecně platí, že neexistují žádné speciální znalosti EF pro používání indexů ani diagnostiku problémů s výkonem souvisejících s nimi; obecné znalosti databáze související s indexy jsou stejně relevantní pro aplikace EF, jako pro aplikace, které nepoužívají EF. Následující seznam obsahuje některé obecné pokyny, které je potřeba při používání indexů vzít v úvahu:

  • Indexy sice urychlují dotazy, ale také zpomalují aktualizace, protože je potřeba udržovat aktuální. Vyhněte se definování indexů, které nejsou potřeba, a zvažte použití filtrů indexů k omezení indexu na podmnožinu řádků, čímž se sníží tato režie.
  • Složené indexy můžou urychlit dotazy, které filtrují více sloupců, ale můžou také urychlit dotazy, které nefiltrují všechny sloupce indexu – v závislosti na řazení. Například index sloupců A a B urychlí filtrování dotazů podle A a B a také dotazy filtrující pouze podle A, ale nezrychlí pouze filtrování dotazů přes B.
  • Pokud dotaz filtruje výraz přes sloupec (např. price / 2), nelze použít jednoduchý index. Můžete však definovat uložený trvalý sloupec pro výraz a vytvořit index nad tímto výrazem. Některé databáze také podporují indexy výrazů, které je možné přímo použít k urychlení filtrování dotazů libovolným výrazem.
  • Různé databáze umožňují konfiguraci indexů různými způsoby a v mnoha případech je poskytovatelé EF Core zpřístupňují prostřednictvím rozhraní Fluent API. Poskytovatel SQL Serveru například umožňuje nakonfigurovat, jestli je index clusterovaný, nebo nastavit jeho výplňový faktor. Další informace najdete v dokumentaci poskytovatele.

Project only properties you need

EF Core usnadňuje dotazování instancí entit a následné použití těchto instancí v kódu. Dotazování instancí entit ale může často načíst více dat, než je potřeba z databáze. Vezměte v úvahu následující skutečnosti:

foreach (var blog in context.Blogs)
{
    Console.WriteLine("Blog: " + blog.Url);
}

I když tento kód potřebuje jenom vlastnost každého blogu Url , celá entita blogu se načte a nepotřebné sloupce se přenesou z databáze:

SELECT [b].[BlogId], [b].[CreationDate], [b].[Name], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]

To je možné optimalizovat pomocí ef Select , které sloupce se mají promítnout:

foreach (var blogName in context.Blogs.Select(b => b.Url))
{
    Console.WriteLine("Blog: " + blogName);
}

Výsledný SQL stáhne jenom potřebné sloupce:

SELECT [b].[Url]
FROM [Blogs] AS [b]

Pokud potřebujete promítnout více než jeden sloupec, projektujte na anonymní typ jazyka C# s požadovanými vlastnostmi.

Všimněte si, že tato technika je velmi užitečná pro dotazy jen pro čtení, ale věci jsou složitější, pokud potřebujete aktualizovat načtené blogy, protože sledování změn EF funguje jenom s instancemi entit. Aktualizace je možné provádět bez načtení celých entit připojením upravené instance blogu a oznámením EF, které vlastnosti se změnily, ale to je pokročilejší technika, která nemusí být za to.

Omezení velikosti sady výsledků

Ve výchozím nastavení dotaz vrátí všechny řádky, které odpovídají jeho filtrům:

var blogsAll = context.Posts
    .Where(p => p.Title.StartsWith("A"))
    .ToList();

Vzhledem k tomu, že počet vrácených řádků závisí na skutečných datech v databázi, není možné zjistit, kolik dat se z databáze načte, kolik paměti budou výsledky zachytávat a kolik dalšího zatížení se vygeneruje při zpracování těchto výsledků (např. odesláním do prohlížeče uživatelů přes síť). Testovací databáze často obsahují málo dat, takže všechno funguje dobře při testování, ale při spuštění dotazu na data z reálného světa se náhle objeví problémy s výkonem a vrátí se mnoho řádků.

V důsledku toho je obvykle vhodné uvažovat o omezení počtuvýsledkůch

var blogs25 = context.Posts
    .Where(p => p.Title.StartsWith("A"))
    .Take(25)
    .ToList();

Uživatelské rozhraní může minimálně zobrazit zprávu, že v databázi může existovat více řádků (a umožnit jejich načtení jiným způsobem). Plnohodnotné řešení by implementovalo stránkování, kde uživatelské rozhraní zobrazuje jenom určitý počet řádků najednou a umožnilo uživatelům podle potřeby přejít na další stránku. Další podrobnosti o tom, jak tuto efektivní implementaci provést, najdete v další části.

Efektivní stránkování

Stránkování odkazuje na načítání výsledků na stránkách, nikoli najednou; to se obvykle provádí u velkých sad výsledků, kde se zobrazí uživatelské rozhraní, které uživateli umožňuje přejít na další nebo předchozí stránku výsledků. Běžným způsobem implementace stránkování s databázemi je použití Skip a Take operátorů (OFFSET a LIMIT v SQL), i když jde o intuitivní implementaci, je to také poměrně neefektivní. U stránkování, které umožňuje přesunout jednu stránku najednou (na rozdíl od přechodu na libovolné stránky), zvažte místo toho použití stránkování sady klíčů.

Další informace najdete na stránce dokumentace o stránkování.

V relačních databázích se všechny související entity načítají zavedením JOIN v jednom dotazu.

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Post] AS [p] ON [b].[BlogId] = [p].[BlogId]
ORDER BY [b].[BlogId], [p].[PostId]

Pokud má typický blog více souvisejících příspěvků, řádky pro tyto příspěvky duplikují informace blogu. Tato duplicita vede k problému tzv. kartézského exploze. S načtením více relací 1:N se může zvýšit množství duplicitních dat a nepříznivě ovlivnit výkon vaší aplikace.

EF umožňuje vyhnout se tomuto efektu pomocí "rozdělených dotazů", které načítají související entity prostřednictvím samostatných dotazů. Další informace najdete v dokumentaci o rozdělení a jednotlivých dotazech.

Poznámka

Aktuální implementace rozdělených dotazů provede pro každý dotaz zaokrouhlování. Plánujeme to v budoucnu vylepšit a provádět všechny dotazy v jednom zpátečním čase.

Než budete pokračovat v této části, doporučujeme přečíst si vyhrazenou stránku souvisejících entit .

Při práci se souvisejícími entitami obvykle předem víme, co potřebujeme načíst: typickým příkladem by bylo načtení určité sady blogů spolu se všemi jejich příspěvky. V těchto scénářích je vždy lepší používat dychtivé načítání, aby EF mohl načíst všechna požadovaná data v jednom kruhovém kole. Filtrovaná funkce zahrnutí také umožňuje omezit, které související entity chcete načíst, a zároveň udržet proces načítání dychtivý a proto je možné provést v jednom zpátečním čase:

using (var context = new BloggingContext())
{
    var filteredBlogs = context.Blogs
        .Include(
            blog => blog.Posts
                .Where(post => post.BlogId == 1)
                .OrderByDescending(post => post.Title)
                .Take(5))
        .ToList();
}

V jiných scénářích nemusíme vědět, kterou související entitu budeme potřebovat, než získáme její hlavní entitu. Například při načítání nějakého blogu možná budeme muset zkonzultovat s nějakým jiným zdrojem dat , případně webovou službou, abychom věděli, jestli nás zajímají příspěvky na blogu. V těchto případech lze explicitní nebo opožděné načítání použít k načtení souvisejících entit samostatně a naplnění navigace příspěvky blogu. Všimněte si, že vzhledem k tomu, že tyto metody nejsou dychtivé, vyžadují do databáze další zaokrouhlování, což je zdroj zpomalení; v závislosti na konkrétním scénáři může být efektivnější vždy načíst všechny příspěvky, nikoli provést další zaokrouhlení a selektivně získat pouze příspěvky, které potřebujete.

Pozor na opožděné načítání

Opožděné načítání často vypadá jako velmi užitečný způsob zápisu logiky databáze, protože EF Core automaticky načte související entity z databáze, protože k nim přistupuje váš kód. Tím se zabrání načítání souvisejících entit, které nejsou potřeba (například explicitní načítání), a zdánlivě uvolní programátora v tom, aby se musel zabývat souvisejícími entitami úplně. Opožděné načítání je však obzvláště náchylné k výrobě nepotřebných dodatečných zaokrouhlování, které mohou aplikaci zpomalit.

Vezměte v úvahu následující skutečnosti:

foreach (var blog in context.Blogs.ToList())
{
    foreach (var post in blog.Posts)
    {
        Console.WriteLine($"Blog {blog.Url}, Post: {post.Title}");
    }
}

Tento zdánlivě nevinný kus kódu iteruje přes všechny blogy a jejich příspěvky, vytisknout je. Zapnutí protokolování příkazů EF Core odhalí následující:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [b].[BlogId], [b].[Rating], [b].[Url]
      FROM [Blogs] AS [b]
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (5ms) [Parameters=[@__p_0='1'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[@__p_0='2'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[@__p_0='3'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0

... and so on

Co se tu děje? Proč se všechny tyto dotazy odesílají pro výše uvedené jednoduché smyčky? Při opožděném načítání jsou příspěvky blogu načteny pouze (lazily) při přístupu jeho Příspěvky vlastnost; V důsledku toho každá iterace ve vnitřním foreachu aktivuje další databázový dotaz ve svém vlastním zaokrouhlování. Výsledkem je, že po počátečním načtení všech blogů máme další dotaz na blog, načítáme všechny jeho příspěvky. Někdy se tomu říká problém N+1 a může způsobit velmi významné problémy s výkonem.

Za předpokladu, že budeme potřebovat všechny blogové příspěvky, dává smysl místo toho použít dychtivé načítání sem. K načítání můžeme použít operátor Include , ale protože potřebujeme jenom adresy URL blogů (a měli bychom načíst jenom to, co je potřeba). Místo toho použijeme projekci:

foreach (var blog in context.Blogs.Select(b => new { b.Url, b.Posts }).ToList())
{
    foreach (var post in blog.Posts)
    {
        Console.WriteLine($"Blog {blog.Url}, Post: {post.Title}");
    }
}

Tím se načte všechny blogy EF Core společně s jejich příspěvky v jednom dotazu. V některýchpřípadechch

Upozorňující

Vzhledem k tomu, že opožděné načítání usnadňuje neúmyslné aktivaci problému N+1, doporučuje se ho vyhnout. Když dojde k zaokrouhlování databáze, je v zdrojovém kódu velmi jasné nebo explicitní načtení.

Ukládání do vyrovnávací paměti a streamování

Ukládání do vyrovnávací paměti odkazuje na načtení všech výsledků dotazu do paměti, zatímco streamování znamená, že EF pokaždé aplikaci předává jeden výsledek, který nikdy neobsahuje celou sadu výsledků v paměti. V zásadě jsou pevné požadavky na paměť streamovaného dotazu – jsou stejné, jestli dotaz vrátí 1 řádek nebo 1000; Dotaz do vyrovnávací paměti na druhou stranu vyžaduje více paměti, aby se vrátilo více řádků. U dotazů, které vedou k velkým sadám výsledků, může to být důležitý faktor výkonu.

Jestli dotaz uloží do vyrovnávací paměti nebo datové proudy, závisí na tom, jak se vyhodnotí:

// ToList and ToArray cause the entire resultset to be buffered:
var blogsList = context.Posts.Where(p => p.Title.StartsWith("A")).ToList();
var blogsArray = context.Posts.Where(p => p.Title.StartsWith("A")).ToArray();

// Foreach streams, processing one row at a time:
foreach (var blog in context.Posts.Where(p => p.Title.StartsWith("A")))
{
    // ...
}

// AsEnumerable also streams, allowing you to execute LINQ operators on the client-side:
var doubleFilteredBlogs = context.Posts
    .Where(p => p.Title.StartsWith("A")) // Translated to SQL and executed in the database
    .AsEnumerable()
    .Where(p => SomeDotNetMethod(p)); // Executed at the client on all database results

Pokud vaše dotazy vrátí jenom několik výsledků, pravděpodobně se o to nemusíte starat. Pokud ale dotaz může vracet velký počet řádků, je vhodné místo ukládání do vyrovnávací paměti uvažovat o streamování.

Poznámka

Vyhněte se použití ToList nebo ToArray pokud chcete ve výsledku použít jiný operátor LINQ – to bude zbytečně ukládat všechny výsledky do paměti. Místo toho použijte AsEnumerable.

Interní ukládání do vyrovnávací paměti ef

V některých situacích ef sám uloží sadu výsledků interně do vyrovnávací paměti bez ohledu na to, jak dotaz vyhodnotíte. K tomuto problému dochází ve dvou případech:

  • Když je spuštěná strategie opakování provádění. Tím zajistíte, aby se při pozdějším opakování dotazu vrátily stejné výsledky.
  • Při použití rozděleného dotazu se sady výsledků všech kromě posledního dotazu ukládají do vyrovnávací paměti – pokud není na SQL Serveru povolená funkce MARS (více aktivních sad výsledků). Důvodem je to, že obvykle není možné mít současně aktivní více sad výsledků dotazu.

Všimněte si, že k tomuto internímu ukládání do vyrovnávací paměti dochází kromě jakéhokoli ukládání do vyrovnávací paměti, které způsobíte prostřednictvím operátorů LINQ. Pokud například použijete ToList dotaz a použijete strategii opakovaného spuštění, sada výsledků se načte do paměti dvakrát: jednou interně ef a jednou .ToList

Sledování, bez sledování a řešení identit

Než budete pokračovat v této části, doporučujeme si přečíst vyhrazenou stránku o sledování a sledování bez sledování .

EF ve výchozím nastavení sleduje instance entit, aby byly zjištěny a trvalé změny při SaveChanges jejich zavolání. Dalším účinkem sledovacích dotazů je, že EF zjistí, jestli už je instance načtená pro vaše data, a automaticky vrátí tuto sledovaný instanci místo vrácení nové instance; tomu se říká řešení identity. Sledování změn z hlediska výkonu znamená následující:

  • EF interně udržuje slovník sledovaných instancí. Když se načtou nová data, EF zkontroluje slovník a zjistí, jestli je pro klíč dané entity už sledována instance (překlad identity). Údržba slovníku a vyhledávání zabírají nějakou dobu při načítání výsledků dotazu.
  • Než aplikaci předáte načtenou instanci, ef snímky této instance a snímek se zachová interně. Při SaveChanges zavolání se instance aplikace porovná se snímkem, aby se zjistily změny, které se mají zachovat. Snímek zabírá více paměti a samotný proces vytváření snímků trvá dlouho; Někdy je možné určit jiné, možná efektivnější chování snímků prostřednictvím porovnávačů hodnot nebo použít proxy servery pro sledování změn k úplnému obejití procesu vytváření snímků (i když je součástí vlastní sady nevýhod).

Ve scénářích jenproch Vzhledem k tomu, že dotazy bez sledování neprovádějí překlad identit, bude řádek databáze, na který odkazuje více dalších načtených řádků, materializován jako různé instance.

Předpokládejme, že načítáme velký počet příspěvků z databáze a také blog, na který odkazuje každý příspěvek. Pokud na stejný blog odkazuje 100 příspěvků, zjistí to sledovací dotaz prostřednictvím překladu identity a všechny instance Post budou odkazovat na stejnou instanci blogu s odstraněným duplicitním kódem. Dotaz bez sledování naproti tomu duplikuje stejný blog 100krát – a kód aplikace musí být napsán odpovídajícím způsobem.

Tady jsou výsledky srovnávacího testu, který porovnává sledování vs. chování bez sledování pro dotaz načítá 10 blogů s 20 příspěvky. Zdrojový kód je zde k dispozici, můžete ho použít jako základ pro vlastní měření.

metoda NumBlogs NumPostsPerBlog Střední hodnota Chyba Směrodatná odchylka Medián Koeficient RatioSD Gen 0 Gen 1 Gen 2 Přiděleno
AsTracking 10 20 1 414,7 us 27.20 us 45.44 nás 1 405,5 nás 1,00 0,00 60.5469 13.6719 - 380.11 KB
AsNoTracking 10 20 993.3 nás 24.04 nás 65.40 nás 966.2 us 0.71 0.05 37.1094 6.8359 - 232.89 KB

Nakonec je možné provádět aktualizace bez režie sledování změn tím, že využijete dotaz bez sledování a pak připojíte vrácenou instanci k kontextu a určíte, které změny se mají provést. To přenese zátěž sledování změn ze systému souborů EF na uživatele a mělo by se o to pokoušet pouze tehdy, pokud se režie sledování změn ukázala jako nepřijatelná prostřednictvím profilace nebo srovnávacího testu.

Použití dotazů SQL

V některých případech existuje optimalizovanější SQL pro váš dotaz, který EF negeneruje. K tomu může dojít v případě, že konstruktor SQL je rozšíření specifické pro vaši databázi, která není podporována, nebo jednoduše proto, že se na ni ef ještě nepřeloží. V těchto případech může psaní SQL ručně přinést výrazné zvýšení výkonu a EF podporuje několik způsobů, jak to udělat.

  • Používejte dotazy SQL přímo v dotazu, například prostřednictvím FromSqlRaw. EF vám dokonce umožňuje vytvářet přes SQL běžné dotazy LINQ, což umožňuje vyjádřit pouze část dotazu v SQL. To je dobrá technika, když SQL stačí použít pouze v jednom dotazu v základu kódu.
  • Definujte uživatelem definovanou funkci (UDF) a pak ji volejte z vašich dotazů. Systém SOUBORŮ EF umožňuje vrátit úplné sady výsledků – označují se jako funkce s hodnotami tabulky (TVF) a také umožňují mapování DbSet na funkci, aby vypadala stejně jako jiná tabulka.
  • Definujte zobrazení databáze a dotazování z ní v dotazech. Všimněte si, že na rozdíl od funkcí nemohou zobrazení přijímat parametry.

Poznámka

Nezpracovaný SQL by se měl obecně používat jako poslední možnost, jakmile se ujistěte, že EF nemůže vygenerovat požadovaný SQL a kdy je výkon pro daný dotaz dostatečně důležitý, aby ho odůvodnil. Použití nezpracovaných SQL přináší značné nevýhody údržby.

Asynchronní programování

Obecně platí, že aby vaše aplikace byla škálovatelná, je důležité vždy používat asynchronní rozhraní API, nikoli synchronní (např. SaveChangesAsync nesynchronní SaveChanges). Synchronní rozhraní API blokují vlákno po dobu trvání vstupně-výstupních operací databáze, což zvyšuje potřebu vláken a počtu přepínačů kontextu vlákna, ke kterým musí dojít.

Další informace najdete na stránce o asynchronním programování.

Upozorňující

Vyhněte se kombinování synchronního a asynchronního kódu ve stejné aplikaci – je velmi snadné neúmyslně aktivovat drobné problémy s hladovým fondem vláken.

Upozorňující

Asynchronní implementace Microsoft.Data.SqlClient bohužel obsahuje některé známé problémy (např. #593, #601 a další). Pokud dochází k neočekávaným problémům s výkonem, zkuste místo toho použít spuštění příkazu synchronizace, zejména při práci s velkými textovými nebo binárními hodnotami.

Další prostředky

  • Další témata související s efektivním dotazováním najdete na stránce s pokročilými tématy týkajícími se výkonu.
  • Některé osvědčené postupy při porovnávání hodnot s možnou hodnotou null najdete na stránce s informacemi o výkonu stránky s porovnáním hodnot null.