Blazor och .NET

Vi mätte de fem vanligaste EF Core-felen: så mycket kostar vart och ett på riktigt

N+1, hela entiteter, tracking, frågor i loopar och filtrering i minnet: vi mätte varje EF Core 10-fel mot lösningen. Upp till 9x långsammare och 700x mer minne.

Vlado Pandžić

Vlado Pandžić · Grundare · Senior .NET-arkitekt
Publicerad · 8 min läsning

Innehåll
  1. Så mätte vi
  2. Resultaten
  3. 1. Filtrering efter ToList(): 9× långsammare, 700× så mycket minne
  4. 2. Hela entiteter för en lista: 2,4× långsammare, 12× så mycket minne
  5. 3. N+1: 1,9× långsammare här, mycket värre över ett nätverk
  6. 4. En fråga i en loop: 1,8× långsammare, och samma nätverksproblem
  7. 5. Tracking på en lista som ingen ändrar: 1,5× långsammare
  8. Därför är siffrorna en undre gräns
  9. Så hittar du dem i din applikation
  10. Frågor vi ofta får
  11. Så arbetar vi

Alla .NET-utvecklare har hört råden: undvik N+1, använd AsNoTracking, ladda inte hela entiteter. Det som oftast saknas är siffran. Hur mycket långsammare är felet egentligen? Tillräckligt för att spela roll, eller något att fixa någon gång?

Så vi mätte. Fem fel som vi ser om och om igen i EF Core-applikationer, vart och ett bredvid sin lösning, på samma data och med standardverktyget för benchmarks.

Det korta svaret: det värsta, filtrering efter ToList(), var 9 gånger långsammare och använde 700 gånger så mycket minne. Det mildaste, tracking på en lista som bara läses, kostade ändå 46 % mer tid. Och de två fel som ser mest oskyldiga ut, N+1 och frågor i en loop, blir mycket värre på en riktig databasserver än vad våra siffror visar.

Så mätte vi

  • EF Core 10 på .NET 10, med SQLite som databasfil på disk.
  • Datan: 1 000 kunder, 10 000 order med ett anteckningsfält på 2 KB och 50 000 orderrader. En fjärdedel av ordrarna är öppna.
  • Verktyget: BenchmarkDotNet 0.15.8, med 3 uppvärmningsvarv och 12 uppmätta iterationer per fall, och minnesallokeringar registrerade.
  • Varje fall öppnar en ny DbContext, precis som ett webbanrop skulle göra.
  • Maskinen: en bärbar dator med Intel Core Ultra 7 255H.

En viktig begränsning: SQLite körs inuti applikationens process, så varje fråga är ett funktionsanrop och inte en resa över nätverket. På SQL Server eller Azure SQL kostar varje extra fråga också en tur och retur över nätverket. Vi återkommer till det nedan, eftersom det ändrar bilden för två av de fem.

Resultaten

Hur många gånger långsammare varje vanligt EF Core-fel var än lösningen

Fel Tid Lösningen Tid Långsammare Minne
Filtrering efter ToList(), i minnet 91,4 ms Filtrering i databasen 10,0 ms 9,1× 43,6 MB mot 0,06 MB
Hela entiteter för en lista 22,5 ms Projektion till de kolumner som behövs 9,6 ms 2,4× 12,7 MB mot 1,0 MB
N+1: rader laddade per order 3,5 ms En fråga med Include 1,9 ms 1,9× 1,8 MB mot 0,8 MB
En fråga per kund i en loop 19,3 ms En fråga för alla kunder 10,9 ms 1,8× 2,6 MB mot 0,3 MB
Skrivskyddad lista med tracking 27,1 ms AsNoTracking 18,6 ms 1,5× 13,3 MB mot 10,9 MB

Nu ett i taget, med koden.

1. Filtrering efter ToList(): 9× långsammare, 700× så mycket minne

Klassikern: en ToList() på fel ställe, och filtret körs i C# i stället för i SQL.

var open = db.Orders.ToList()
    .Where(o => o.Status == "Open" && o.Total > 500)
    .Count();

Databasen skickar alla 10 000 order, med sina anteckningar på 2 KB, och applikationen slänger det mesta. Lösningen låter databasen göra jobbet:

var open = db.Orders.Count(o => o.Status == "Open" && o.Total > 500);

Felet allokerade 43,6 MB för ett enda anrop; lösningen 62 KB. På en webbserver innebär den skillnaden att skräpsamlaren får jobba hårt vid varje anrop, och den växer med tabellen. Oftast smyger det sig in via en metod som returnerar List<T> eller IEnumerable<T> i stället för IQueryable<T>, så att filtret läggs till först när datan redan har kommit fram.

2. Hela entiteter för en lista: 2,4× långsammare, 12× så mycket minne

En listskärm behöver fyra kolumner. Koden laddar kompletta entiteter, inklusive ett stort textfält som ingen visar:

var rows = db.Orders.AsNoTracking()
    .Include(o => o.Customer)
    .Where(o => o.Status == "Open")
    .ToList()
    .Select(o => new OrderRow(o.Id, o.Customer.Name, o.Date, o.Total));

En projektion ber databasen bara om det som skärmen behöver:

var rows = db.Orders
    .Where(o => o.Status == "Open")
    .Select(o => new OrderRow(o.Id, o.Customer.Name, o.Date, o.Total))
    .ToList();

12,7 MB blev 1 MB. Ju bredare tabellen är, och alla tabeller blir bredare med åren, desto större blir skillnaden. Projektioner spåras dessutom aldrig, så de får fördelen från den sista punkten på köpet.

3. N+1: 1,9× långsammare här, mycket värre över ett nätverk

En fråga för ordrarna, och sedan en till för raderna i varje order:

var orders = db.Orders.OrderBy(o => o.Id).Take(100).ToList();
foreach (var order in orders)
    total += db.OrderLines.Count(l => l.OrderId == order.Id);

Det blir 101 frågor. Med Include blir det en:

var orders = db.Orders.OrderBy(o => o.Id).Take(100)
    .Include(o => o.Lines)
    .ToList();

I vårt test var skillnaden 1,6 ms. Det låter ofarligt, men SQLite hade inget nätverk. På en SQL Server i samma datacenter tar en tur och retur normalt någonstans mellan en halv millisekund och ett par millisekunder. Hundra extra turer lägger då till ungefär 50 till 200 ms på varje sida som gör så här, och sidan som har 100 rader i dag har 1 000 nästa år.

4. En fråga i en loop: 1,8× långsammare, och samma nätverksproblem

Samma mönster i en annan form: en loop över kunder, med en fråga per kund.

foreach (var id in customerIds)
    count += db.Orders.Count(o => o.CustomerId == id && o.Status == "Open");

En enda fråga gör samma jobb:

var count = db.Orders.Count(o => customerIds.Contains(o.CustomerId) && o.Status == "Open");

Lokalt kostade 200 frågor mot en 1,8× så mycket tid och 7,7× så mycket minne. Över ett nätverk lägger de 200 turerna till samma 0,1 till 0,4 sekunder som vid N+1. Sådana loopar gömmer sig i importer, bakgrundsjobb och rapporter, och det är därför nattkörningar växer från minuter till timmar.

5. Tracking på en lista som ingen ändrar: 1,5× långsammare

Som standard sparar EF Core en kopia av varje laddad entitet för att upptäcka ändringar. För en skärm som bara visar data är det bortkastat arbete:

var orders = db.Orders.Where(o => o.Status == "Open").ToList();
var orders = db.Orders.AsNoTracking().Where(o => o.Status == "Open").ToList();

46 % långsammare och 2,4 MB mer minne, varje gång en lista laddas. Det är det mildaste av de fem, och det enklaste att åtgärda: ett anrop, eller QueryTrackingBehavior.NoTracking som standard för en kontext som bara läser.

Därför är siffrorna en undre gräns

Två av de fem felen, N+1 och frågor i en loop, handlar främst om antalet turer till databasen. Vår uppställning tog bort nätverket helt, så den visar bara kostnaden inuti applikationen. En grov tumregel för en riktig server:

Extra frågor per sida Vid 0,5 ms per tur och retur Vid 2 ms per tur och retur
10 +5 ms +20 ms
100 +50 ms +200 ms
1 000 +0,5 s +2 s

De andra tre felen, filtrering i minnet, hela entiteter och tracking, handlar om hur mycket data som skickas och hur mycket minne den tar. Ett nätverk gör även dem värre, eftersom varje onödig megabyte måste ta sig över det.

Så hittar du dem i din applikation

  1. Titta på SQL:en. Slå på EF Core-loggning med LogTo(Console.WriteLine) i utvecklingsmiljön, eller anropa ToQueryString() på en fråga, och räkna antalet frågor per sida.
  2. Leta efter ToList() och AsEnumerable() följt av Where, Count eller First, och efter repository-metoder som returnerar List<T>.
  3. Leta efter loopar som innehåller en fråga, även de dolda i lazy-loadade navigeringsegenskaper.
  4. Mät de långsammaste skärmarna före och efter en åtgärd, som vi gjorde här. Den åtgärd som spelar roll är den som flyttar siffran.

Hela listan med orsaker, inklusive dem vi inte mätte här, som saknade index och paginering som blir allt långsammare, finns i vår artikel om varför EF Core är långsamt.

Frågor vi ofta får

Är EF Core i sig långsamt?

Inte enligt vår erfarenhet. I alla fem fallen var lösningen fortfarande EF Core, bara använt på ett annat sätt. De flesta långsamma EF Core-applikationer är långsamma på grund av hur frågorna är skrivna, inte på grund av biblioteket.

Borde vi byta till Dapper eller ren SQL?

För en handfull mycket belastade frågor kan det vara vettigt. För resten ger lösningarna ovan det mesta av vinsten, samtidigt som du behåller ett enda sätt att komma åt data. Att skriva om dataåtkomstlagret är sällan första steget.

Gäller resultaten på SQL Server?

Riktningen gör det, och för N+1 och loopar blir skillnaderna större på grund av nätverket. De exakta millisekunderna beror på din server, din data och dina index. Därför lönar det sig att mäta dina egna långsammaste skärmar.

Vi kör fortfarande EF6. Gäller det här oss?

Samma fel finns i EF6, och lösningarna är liknande. Att gå över till EF Core är ett separat projekt, som vi går igenom i vår artikel om migrering från EF6 till EF Core.

För applikationer som fortfarande kör EF6 har vi mätt samma typ av frågor på EF6 och EF Core 10 sida vid sida: EF6 mot EF Core 10.

Så arbetar vi

Det här är precis vad vi gör. ProCoding är en .NET-studio i Split i Kroatien, och EF Core-prestanda är något vi lägger mycket av vår tid på: hitta de frågor som kostar mest, åtgärda dem med mätningar före och efter, och se till att dataåtkomstlagret hänger med när datan växer. På egen hand, tillsammans med ditt team, eller med en .NET-konsult i ditt team. Första steget: Boka ett kostnadsfritt samtal på 30 minuter.

Källor

Den här artikeln är endast allmän information och inte juridisk, skattemässig, ekonomisk eller annan professionell rådgivning. Scenarier, exempel och beräkningar är illustrativa. Användarvillkor och ansvarsfriskrivning.

Relaterade artiklar

© 2026 ProCoding — Alla rättigheter förbehållna.Företagsinformation och integritetAnvändarvillkorSplit, Kroatien