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.
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. Například:
// Matches on start, so uses an index (on SQL Server)
var posts1 = await context.Posts.Where(p => p.Title.StartsWith("A")).ToListAsync();
// Matches on end, so does not use the index
var posts2 = await context.Posts.Where(p => p.Title.EndsWith("A")).ToListAsync();
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 up-to-date. 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.
Projektujte pouze vlastnosti, které potřebujete
EF Core usnadňuje dotazování instancí entit a následné použití těchto instancí v kódu. Při vykonávání dotazů na instance entit se ale může často načíst z databáze více dat, než je potřeba. Vezměte v úvahu následující skutečnosti:
await foreach (var blog in context.Blogs.AsAsyncEnumerable())
{
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 použitím Select k určení, které sloupce má EF promítnout.
await foreach (var blogName in context.Blogs.Select(b => b.Url).AsAsyncEnumerable())
{
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 = await context.Posts
.Where(p => p.Title.StartsWith("A"))
.ToListAsync();
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čtu výsledků.
var blogs25 = await context.Posts
.Where(p => p.Title.StartsWith("A"))
.Take(25)
.ToListAsync();
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 vždy jen určitý počet řádků a podle potřeby umožnilo uživatelům přejít na další stránku; Další podrobnosti o tom, jak tuto implementaci efektivně implementovat, 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í.
Vyhněte se kartézskému výbuchu při načítání souvisejících entit.
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 duplicitní struktura vede k problému tzv. kartézské exploze. Jak se načítá více relací typu 1:N, objem duplicitních dat se může zvýšit 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 tématu rozdělených a jednoduchých dotazů.
Poznámka:
Aktuální implementace dělených dotazů provádí pro každý dotaz jednu cestu. Plánujeme to v budoucnu zvýšit a provádět všechny dotazy v jednom cyklu.
Pokud je to možné, načtěte přednostně související entity.
Než budete pokračovat v této části, doporučujeme přečíst speciální stránku o souvisejících entitách.
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 eager loading, aby EF mohl načíst všechna požadovaná data v jediném požadavku. Funkce filtrovaného zahrnutí vám také umožňuje omezit, které související entity chcete načíst, a zároveň udržet načítání aktivní a tudíž provést v jednom načtení.
using (var context = new BloggingContext())
{
var filteredBlogs = await context.Blogs
.Include(
blog => blog.Posts
.Where(post => post.BlogId == 1)
.OrderByDescending(post => post.Title)
.Take(5))
.ToListAsync();
}
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 nenačítají data předem, vyžadují další přístupy do databáze, což je zdroj zpomalení. V závislosti na vašem konkrétním scénáři může být efektivnější vždy načíst všechny Příspěvky, než provádět další přístupy a selektivně získávat pouze ty 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ě. Líné načítání je však obzvláště náchylné k vytváření nepotřebných dodatečných požadavků, které mohou aplikaci zpomalit.
Vezměte v úvahu následující skutečnosti:
foreach (var blog in await context.Blogs.ToListAsync())
{
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 a je vytiskne. Zapnutí protokolování příkazů v 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í se příspěvky blogu načítají pouze při přístupu k jeho vlastnosti příspěvky. V důsledku toho každá iterace ve vnitřním "foreach" aktivuje další databázový dotaz, který probíhá v samostatném dotazu. Výsledkem je, že po počátečním načtení dotazu na všechny blogy pak máme další dotaz na blog, načítá se 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 příspěvky blogu, dává smysl místo toho použít eager loading. 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:
await foreach (var blog in context.Blogs.Select(b => new { b.Url, b.Posts }).AsAsyncEnumerable())
{
foreach (var post in blog.Posts)
{
Console.WriteLine($"Blog {blog.Url}, Post: {post.Title}");
}
}
Tím EF Core načte všechny blogy společně s jejich příspěvky v jednom dotazu. V některých případech může být užitečné se vyhnout efektům kartézského explozí použitím rozštěpených dotazů.
Výstraha
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 dotazu na databázi, je ve zdrojovém kódu velmi jasné, zda se jedná o eager nebo explicitní načtení.
Ukládání do vyrovnávací paměti a přenášení
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é předá aplikaci jeden výsledek, takže paměť nikdy neobsahuje celou sadu výsledků najednou. 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.
Zda dotaz bufferuje nebo streamuje, závisí na způsobu jeho vyhodnocení:
// ToList and ToArray cause the entire resultset to be buffered:
var blogsList = await context.Posts.Where(p => p.Title.StartsWith("A")).ToListAsync();
var blogsArray = await context.Posts.Where(p => p.Title.StartsWith("A")).ToArrayAsync();
// Foreach streams, processing one row at a time:
await foreach (var blog in context.Posts.Where(p => p.Title.StartsWith("A")).AsAsyncEnumerable())
{
// ...
}
// AsAsyncEnumerable 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
.AsAsyncEnumerable()
.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í vyrovnávání paměti EF
V některých situacích EF uloží výsledek interně do vlastní vyrovnávací paměti bez ohledu na to, jak dotaz zpracovává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 dotaz s ToList a je uplatněna strategie opakovaného spuštění, sada výsledků se načte do paměti dvakrát: jednou interně pomocí EF a jednou pomocí ToList.
Sledování, nesledování a rozpoznávání 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 mohly být změny na nich detekovány a uloženy při zavolání SaveChanges. Dalším účinkem sledovacích dotazů je, že EF rozpozná, jestli už je pro vaše data načtena instance, a automaticky vrátí tuto sledovanou instanci místo vrácení nové; to se nazývá ř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, aby zjistil, zda je instance již sledována pro klíč dané entity (rozpoznání 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 si pořídí snímek této instance a uchovává jej 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 pouze pro čtení, kde se změny neukládají zpět do databáze, se zmíněné režijní náklady dají vyhnout použitím dotazů bez sledování. Vzhledem k tomu, že dotazy bez sledování neprovádějí řešení totožnosti, bude řádek databáze, který odkazuje na několik 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 100 příspěvků odkazuje na stejný blog, zjistí to sledovací dotaz prostřednictvím řešení identity a všechny instance Příspěvku budou odkazovat na stejnou odduplikovanou instanci blogu. 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á chování se sledováním a bez sledování u dotazu načítajícího 10 blogů s 20 příspěvky každý. Zdrojový kód je zde k dispozici, můžete ho použít jako základ pro vlastní měření.
| Metoda | NumBlogs | PočetPříspěvkůNaBlog | Znamenat | Chyba | Směrodatná odchylka | Medián | Poměr | RatioSD | Gen 0 | Gen 1 | Gen 2 | Přidělený |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| AsTracking | 10 | 20 | 1 414,7 us | 27.20 us | 45.44 amerických dolarů | 1 405,5 us | 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 z Entity Framework na uživatele a mělo by se o to pokoušet pouze tehdy, pokud se náročnost 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 komponovat nad SQL pomocí dotazů LINQ, což vám umožní vyjádřit čá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 EF umožňuje, aby uživatelské definované funkce vracely úplné sady výsledků – tyto funkce jsou známé jako funkce vracející tabulky (TVF) – a také umožňuje namapování
DbSetna funkci, takže vypadá jako další 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í.
Výstraha
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 vyčerpáním vláknového fondu.
Výstraha
Asynchronní implementace Microsoft.Data.SqlClient bohužel obsahuje některé známé problémy (např. #593, #601a 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.
Dodatečné zdroje
- 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 .
- Podívejte se na sekci výkonu na dokumentační stránce o porovnání hodnot null, kde najdete některé osvědčené postupy pro porovnávání hodnot null.