Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Att köra frågor effektivt är ett omfattande ämne som täcker olika aspekter såsom index, strategier för att ladda entiteter och mycket mer. Det här avsnittet beskriver några vanliga teman för att göra dina frågor snabbare och fallgropar som användare vanligtvis stöter på.
Använd index korrekt
Den viktigaste avgörande faktorn för om en fråga körs snabbt eller inte är om den kommer att använda index korrekt när det är lämpligt: databaser används vanligtvis för att lagra stora mängder data och frågor som passerar hela tabeller är vanligtvis källor till allvarliga prestandaproblem. Indexeringsproblem är inte lätta att upptäcka, eftersom det inte är direkt uppenbart om en viss fråga använder ett index eller inte. Till exempel:
// 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();
Ett bra sätt att upptäcka indexeringsproblem är att först hitta en långsam fråga och sedan undersöka dess frågeplan via databasens favoritverktyg. Se sidan för prestandadiagnos för mer information om hur du gör det. Frågeplanen visar om frågan passerar hela tabellen eller använder ett index.
Som en allmän regel finns det ingen särskild EF-kunskap om att använda index eller diagnostisera prestandaproblem som är relaterade till dem. allmän databaskunskap som rör index är lika relevant för EF-program som för program som inte använder EF. Följande listar några allmänna riktlinjer att tänka på när du använder index:
- Medan index påskyndar frågor, saktar de också ner uppdateringar eftersom de måste hållas up-to-date. Undvik att definiera index som inte behövs och överväg att använda indexfilter för att begränsa indexet till en delmängd av raderna, vilket minskar kostnaderna.
- Sammansatta index kan påskynda frågor som filtrerar på flera kolumner, men de kan också påskynda frågor som inte filtrerar på alla indexkolumner – beroende på ordning. Ett index för kolumnerna A och B påskyndar till exempel filtrering av frågor efter A och B samt frågor som endast filtreras efter A, men det påskyndar inte frågor som bara filtrerar över B.
- Om en fråga filtreras efter ett uttryck över en kolumn (t.ex.
price / 2) kan ett enkelt index inte användas. Du kan dock definiera en lagrad bevarad kolumn för uttrycket och skapa ett index över det. Vissa databaser har också stöd för uttrycksindex, som kan användas direkt för att påskynda filtrering av frågor med valfritt uttryck. - Med olika databaser kan index konfigureras på olika sätt, och i många fall exponerar EF Core-leverantörer dessa via Fluent API. Med SQL Server-providern kan du till exempel konfigurera om ett index är klustrat eller ange dess fyllningsfaktor. Mer information finns i leverantörens dokumentation.
Visa endast egenskaper du behöver
EF Core gör det mycket enkelt att fråga ut entitetsinstanser och sedan använda dessa instanser i kod. Att fråga entitetsinstanser kan dock ofta hämta mer data än nödvändigt från databasen. Tänk på följande:
await foreach (var blog in context.Blogs.AsAsyncEnumerable())
{
Console.WriteLine("Blog: " + blog.Url);
}
Även om den här koden faktiskt bara behöver varje bloggs Url-egenskap, hämtas hela bloggentiteten, och onödiga kolumner överförs från databasen.
SELECT [b].[BlogId], [b].[CreationDate], [b].[Name], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]
Detta kan optimeras med hjälp Select av för att tala om för EF vilka kolumner som ska projiceras ut:
await foreach (var blogName in context.Blogs.Select(b => b.Url).AsAsyncEnumerable())
{
Console.WriteLine("Blog: " + blogName);
}
Den resulterande SQL-filen hämtar endast de kolumner som behövs:
SELECT [b].[Url]
FROM [Blogs] AS [b]
Om du behöver projicera ut mer än en kolumn projicerar du ut till en anonym C#-typ med de egenskaper du vill använda.
Observera att den här tekniken är mycket användbar för endast-läs frågor, men det blir mer komplicerat om du behöver uppdatera de hämtade bloggarna, eftersom EF:s ändringsspårningsmekanism endast fungerar med entitetsinstanser. Det går att utföra uppdateringar utan att läsa in hela entiteter genom att koppla en modifierad blogginstans och berätta för EF vilka egenskaper som har ändrats, men det är en mer avancerad teknik som kanske inte är värd det.
Begränsa antalet resultat
Som standard returnerar en fråga alla rader som matchar dess filter:
var blogsAll = await context.Posts
.Where(p => p.Title.StartsWith("A"))
.ToListAsync();
Eftersom antalet rader som returneras beror på faktiska data i databasen är det omöjligt att veta hur mycket data som läses in från databasen, hur mycket minne som tas upp av resultaten och hur mycket ytterligare belastning som genereras vid bearbetning av dessa resultat (t.ex. genom att skicka dem till en webbläsare via nätverket). Avgörande är att testdatabaser ofta innehåller lite data, så att allt fungerar bra vid testning, men prestandaproblem visas plötsligt när frågan börjar köras på verkliga data och många rader returneras.
Därför är det vanligtvis värt att tänka på att begränsa antalet resultat:
var blogs25 = await context.Posts
.Where(p => p.Title.StartsWith("A"))
.Take(25)
.ToListAsync();
Användargränssnittet kan åtminstone visa ett meddelande som anger att det kan finnas fler rader i databasen (och tillåta att de hämtas på något annat sätt). En fullständig lösning skulle implementera sidnumrering, där användargränssnittet endast visar ett visst antal rader i taget och tillåter användare att gå vidare till nästa sida efter behov. Mer information om hur du implementerar detta effektivt finns i nästa avsnitt.
Effektiv sidnumrering
Sidnumrering syftar på att hämta resultat på sidor i stället för alla samtidigt. Detta görs vanligtvis för stora resultatuppsättningar, där ett användargränssnitt visas som gör att användaren kan navigera till nästa eller föregående sida i resultatet. Ett vanligt sätt att implementera sidnumrering med databaser är att använda Skip operatorerna och Take (OFFSET och LIMIT i SQL); även om detta är en intuitiv implementering är det också ganska ineffektivt. För sidnumrering som gör det möjligt att flytta en sida i taget (i stället för att hoppa till godtyckliga sidor) bör du överväga att använda sidnumrering av nyckeluppsättningar i stället.
Mer information finns på dokumentationssidan om sidnumrering.
Undvik kartesisk explosion vid inläsning av relaterade entiteter
I relationsdatabaser läses alla relaterade entiteter in genom att JOIN introduceras i en enda fråga.
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]
Om en typisk blogg har flera relaterade inlägg duplicerar rader för dessa inlägg bloggens information. Den här dupliceringen leder till det så kallade "cartesian explosion"-problemet. När fler en-till-många-relationer läses in kan mängden duplicerade data växa och påverka programmets prestanda negativt.
MED EF kan du undvika den här effekten med hjälp av "delade frågor", som läser in relaterade entiteter via separata frågor. Mer information finns i dokumentationen om delade och enkla frågor.
Anmärkning
Den aktuella implementeringen av delade frågor kör en tur och retur för varje fråga. Vi planerar att förbättra detta i framtiden och köra alla frågor i en enda omgång.
Läs in relaterade entiteter proaktivt när det är möjligt
Vi rekommenderar att du läser den dedikerade sidan om relaterade entiteter innan du fortsätter med det här avsnittet.
När vi hanterar relaterade entiteter vet vi vanligtvis i förväg vad vi behöver läsa in: ett typiskt exempel skulle vara att läsa in en viss uppsättning bloggar, tillsammans med alla deras inlägg. I dessa scenarier är det alltid bättre att använda eager loading, så att EF kan hämta alla nödvändiga data på en gång. Med den filtrerade inkluderingsfunktionen kan du också begränsa vilka relaterade entiteter som du vill läsa in, samtidigt som inläsningsprocessen är ivrig och därför kan utföras på en enda tur och retur:
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();
}
I andra scenarier kanske vi inte vet vilken relaterad entitet vi behöver innan vi får dess huvudentitet. När vi till exempel läser in en blogg kan vi behöva konsultera någon annan datakälla , eventuellt en webbtjänst, för att veta om vi är intresserade av den bloggens inlägg. I dessa fall kan explicit eller fördröjd inläsning användas för att hämta relaterade entiteter separat och fylla i Bloggens inläggsnavigering. Observera att eftersom dessa metoder inte är snabba kräver de flertal resor till databasen, vilket leder till avmattning. Beroende på ditt specifika scenario kan det vara mer effektivt att alltid ladda upp alla inlägg, i stället för att utföra ytterligare resor och selektivt bara hämta de inlägg du behöver.
Akta dig för lat inläsning
Lat inläsning verkar ofta vara ett mycket användbart sätt att skriva databaslogik eftersom EF Core automatiskt läser in relaterade entiteter från databasen när de används av koden. Detta förhindrar inläsning av relaterade entiteter som inte behövs (till exempel explicit inläsning) och frigör till synes programmeraren från att behöva hantera relaterade entiteter helt och hållet. Dock har fördröjd inläsning en särskild tendens att producera onödiga extra rundresor som kan sakta ner programmet.
Tänk på följande:
foreach (var blog in await context.Blogs.ToListAsync())
{
foreach (var post in blog.Posts)
{
Console.WriteLine($"Blog {blog.Url}, Post: {post.Title}");
}
}
Detta till synes oskyldiga kodstycke itererar genom alla bloggar och deras inlägg, och skriver ut dem. Aktivering av EF Cores instruktionsloggning avslöjar följande:
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
Vad är det som händer här? Varför skickas alla dessa frågor för de enkla looparna ovan? Med lat inläsning laddas en bloggs inlägg först när egenskapen Posts nås; därför utlöser varje iteration i den inre foreach-loopen ytterligare en databasfråga vid varje körning. När den första frågan har läst in alla bloggar har vi därför en annan fråga per blogg som läser in alla dess inlägg. Detta kallas ibland för N+1-problemet och kan orsaka mycket betydande prestandaproblem.
Om vi antar att vi kommer att behöva alla bloggarnas inlägg, är det vettigt att använda eager loading här istället. Vi kan använda include-operatorn för att utföra inläsningen, men eftersom vi bara behöver bloggarnas URL:er (och vi bör bara läsa in det som behövs). Så vi använder en projektion i stället:
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}");
}
}
Detta gör att EF Core hämtar alla bloggar – tillsammans med sina inlägg – i en enda fråga. I vissa fall kan det också vara användbart att undvika kartesiska explosionseffekter med hjälp av delade frågor.
Varning
Eftersom lat inläsning gör det extremt enkelt att oavsiktligt utlösa N+1-problemet rekommenderar vi att du undviker det. Ivrig eller explicit inläsning gör det mycket tydligt i källkoden när en databasrundresa sker.
Buffring och direktuppspelning
Buffring syftar på att läsa in alla frågeresultat i minnet, medan strömning innebär att EF ger programmet ett enda resultat varje gång, utan att hela resultatuppsättningen finns i minnet. I princip är minneskraven för en strömmande fråga fasta – de är desamma oavsett om frågan returnerar 1 rad eller 1 000. en buffringsfråga kräver å andra sidan mer minne, desto fler rader returneras. För frågor som resulterar i stora resultatmängder kan detta vara en viktig prestandafaktor.
Om en fråga buffrar eller strömmar beror på hur den utvärderas:
// 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
Om dina frågor bara returnerar några få resultat behöver du förmodligen inte bekymra dig om detta. Men om frågan kan returnera ett stort antal rader är det värt att tänka på strömning i stället för buffring.
Anmärkning
Undvik att använda ToList eller ToArray om du tänker använda en annan LINQ-operator på resultatet – detta buffras i onödan alla resultat i minnet. Använd AsEnumerable i stället.
Intern buffring av EF
I vissa situationer buffrar EF själv resultatuppsättningarna internt, oavsett hur du utvärderar din frågeställning. De två fall där detta inträffar är:
- När en återförsöksstrategi används. Detta görs för att se till att samma resultat returneras om frågan görs på nytt senare.
- När en delad fråga används buffras resultatuppsättningarna för alla utom den sista frågan – såvida inte MARS (flera aktiva resultatuppsättningar) är aktiverat på SQL Server. Detta beror på att det vanligtvis är omöjligt att ha flera frågeresultatuppsättningar aktiva samtidigt.
Observera att den här interna buffring sker utöver eventuell buffring som du orsakar via LINQ-operatorer. Till exempel, om du använder ToList i en fråga och när en strategi för återförsökskörning används, läses resultatuppsättningen in i minnet två gånger: en gång internt av EF och en gång av ToList.
Spårning, ingen spårning och identitetsmatchning
Vi rekommenderar att du läser den dedikerade sidan om spårning och spårning utan spårning innan du fortsätter med det här avsnittet.
EF spårar entitetsinstanser som standard, så att ändringar på dem identifieras och sparas när SaveChanges anropas. En annan effekt av att spåra frågor är att EF identifierar om en instans redan har lästs in för dina data och automatiskt returnerar den spårade instansen i stället för att returnera en ny. Detta kallas identitetsmatchning. Ur ett prestandaperspektiv innebär ändringsspårning följande:
- EF hanterar internt ett register över spårade instanser. När nya data läses in kontrollerar EF ordlistan för att se om en instans redan har spårats för den entitetens nyckel (identitetsmatchning). Ordlisteunderhåll och uppslag tar lite tid när frågans resultat läses in.
- Innan du överlämnar en inläst instans till applikationen, tar EF en ögonblicksbild av den instansen och behåller ögonblicksbilden internt. När SaveChanges anropas jämförs programmets instans med ögonblicksbilden för att identifiera de ändringar som ska sparas. Ögonblicksbilden tar upp mer minne och själva ögonblicksbildsprocessen tar tid. Det går ibland att ange olika, möjligen effektivare beteende för ögonblicksbilder via värdejämförare, eller att använda proxyservrar för ändringsspårning för att kringgå ögonblicksbildprocessen helt och hållet (även om det kommer med en egen uppsättning nackdelar).
I skrivskyddade scenarier där ändringar inte sparas tillbaka till databasen kan ovanstående omkostnader undvikas med hjälp av frågor utan spårning. Men eftersom förfrågningar utan spårning inte gör identitetsmatchning, kommer en databasrad som refereras av flera andra laddade rader att materialiseras som skilda instanser.
För att illustrera antar vi att vi läser in ett stort antal inlägg från databasen samt den blogg som refereras av varje inlägg. Om 100 inlägg refererar till samma blogg identifierar en spårningsfråga detta via identitetsmatchning, och alla Post-instanser refererar till samma avduplicerade blogginstans. En fråga utan spårning duplicerar däremot samma blogg 100 gånger – och programkoden måste skrivas i enlighet med detta.
Här är resultaten för ett riktmärke som jämför spårning jämfört med beteende utan spårning för en fråga som läser in 10 bloggar med 20 inlägg vardera. Källkoden finns härkan du använda den som grund för dina egna mätningar.
| Metod | NumBlogs | AntalInläggPerBlogg | Medelvärde | Fel | StdDev | Median | Förhållande | RatioSD | Gen 0 | Gen 1 | Gen 2 | Tilldelades |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| AsTracking | 10 | 20 | 1 414,7 oss | 27.20 oss | 45.44 oss | 1 405,5 oss | 1,00 | 0,00 | 60,5469 | 13.6719 | - | 380,11 KB |
| AsNoTracking | 10 | 20 | 993.3 oss | 24.04 oss | 65.40 oss | 966.2 oss | 0.71 | 0.05 | 37.1094 | 6.8359 | - | 232,89 KB |
Slutligen är det möjligt att utföra uppdateringar utan kostnader för ändringsspårning genom att använda en fråga utan spårning och sedan koppla den returnerade instansen till kontexten och ange vilka ändringar som ska göras. Detta överför bördan av ändringsspårning från EF till användaren och bör endast försökas om ändringsspårningskostnaderna har visat sig vara oacceptabla via profilering eller benchmarking.
Använda SQL-frågor
I vissa fall finns mer optimerad SQL för din fråga, vilket EF inte genererar. Detta kan inträffa när SQL-konstruktionen är ett tillägg som är specifikt för din databas som inte stöds, eller bara för att EF inte översätter till den ännu. I dessa fall kan skrivning av SQL för hand ge en betydande prestandaökning, och EF har stöd för flera sätt att göra detta.
- Använd SQL-frågor direkt i din fråga, t.ex. via FromSqlRaw. MED EF kan du till och med skriva över SQL med vanliga LINQ-frågor, så att du bara kan uttrycka en del av frågan i SQL. Det här är en bra teknik när SQL bara behöver användas i en enda fråga i din kodbas.
- Definiera en användardefinierad funktion (UDF) och anropa sedan den från dina frågor. Observera att EF tillåter UDF:er att returnera fullständiga resultatuppsättningar – dessa kallas tabellvärdesfunktioner (TVF:er) – och tillåter även mappning av en
DbSettill en funktion, vilket gör att den ser ut precis som bara en annan tabell. - Definiera en databasvy och fråga från den i dina frågor. Observera att till skillnad från funktioner kan vyer inte acceptera parametrar.
Anmärkning
Raw SQL bör vanligtvis användas som en sista utväg, efter att ha kontrollerat att EF inte kan generera den SQL du vill ha, och när prestanda är tillräckligt viktigt för att den angivna frågan ska kunna motivera den. Användning av rå SQL medför avsevärda underhållsnackdelar.
Asynkron programmering
För att programmet ska vara skalbart är det som en allmän regel viktigt att alltid använda asynkrona API:er i stället för synkrona (t.ex. SaveChangesAsync i stället SaveChangesför ). Synkrona API:er blockerar tråden under databasens I/O-tid, vilket ökar behovet av trådar och antalet trådkontextväxlar som måste ske.
Mer information finns på sidan om asynkron programmering.
Varning
Undvik att blanda synkron och asynkron kod i samma program – det är mycket enkelt att oavsiktligt utlösa subtila problem med utsvulten trådpool.
Varning
Asynkron implementering av Microsoft.Data.SqlClient har tyvärr några kända problem (t.ex. #593, #601och andra). Om du får oväntade prestandaproblem kan du prova att använda körning av synkroniseringskommandon i stället, särskilt när du hanterar stora text- eller binära värden.
Ytterligare resurser
- För ytterligare ämnen relaterade till effektiv frågeställning, se sidan för avancerade prestandaämnen.
- Se prestandaavsnittet på dokumentationssidan för nulljämförelse för några metodtips när du jämför null-värden.