EF Core traag? 8 veelvoorkomende oorzaken en hoe u ze oplost

EF Core-applicatie wordt trager? De acht meest voorkomende oorzaken, van N+1-queries tot paginering, met code voor en na, en hoe u de SQL ziet.

Vlado Pandžić

Vlado Pandžić · Oprichter · Senior .NET-architect
Gepubliceerd · 5 min lezen

In het begin is alles snel. De database heeft een paar duizend rijen, schermen openen meteen, en niemand denkt na over welke SQL Entity Framework verstuurt. Twee jaar en een paar miljoen rijen later duren dezelfde schermen seconden, en begint het team over overstappen op “gewone SQL”.

EF Core is zelden alleen de schuldige. Het doet bijna altijd precies wat het is opgedragen, alleen is het iets duurs opgedragen. Dit zijn de acht oorzaken die we het vaakst vinden, elk met code voor en na.

Eerst: bekijk de SQL die EF verstuurt

Zolang u de SQL niet ziet, gokt u. EF Core laat hem op twee manieren zien:

options.UseSqlServer(connectionString)
       .LogTo(Console.WriteLine, LogLevel.Information);          // elke query naar de console, alleen tijdens ontwikkeling

var sql = db.Orders.Where(o => o.Total > 1000).ToQueryString();  // de SQL van één query, zonder hem uit te voeren

In productie laat Application Insights hetzelfde zien: elke aanroep naar de database, hoe lang die duurde en uit welk verzoek die kwam.

1. N+1-queries

Eén query voor de lijst, en dan nog één voor elke rij. Honderd orders betekenen honderdeen keer naar de database.

var orders = await db.Orders.ToListAsync();
foreach (var order in orders)
{
    var customer = await db.Customers.FindAsync(order.CustomerId);  // één query per order
}

De oplossing is alles in één keer ophalen, met Include of, nog beter, met de projectie uit het volgende punt.

2. Hele entiteiten terwijl het scherm drie velden nodig heeft

De orderlijst toont het nummer, de klant en het bedrag, en de applicatie laadt hele orders met al hun regels.

var rows = await db.Orders
    .Select(o => new OrderRow(o.Number, o.Customer.Name, o.Total))  // alleen wat het scherm toont
    .ToListAsync();

Een projectie met Select lost N+1 en de overbodige data in één keer op: één query, alleen de kolommen die u nodig hebt.

3. Wijzigingen bijhouden voor data die alleen wordt gelezen

EF houdt standaard elke geladen entiteit bij, voor het geval u hem wijzigt. Voor schermen die alleen data tonen is dat onnodig werk en geheugen.

var products = await db.Products
    .AsNoTracking()                                                 // alleen lezen, zonder bijhouden
    .Where(p => p.IsActive)
    .ToListAsync();

4. Filteren in het geheugen in plaats van in de database

Eén ToList() op de verkeerde plek, en de hele tabel reist naar de applicatie om daar te worden gefilterd.

// voor: de hele tabel in het geheugen, gefilterd in de applicatie
var open = (await db.Orders.ToListAsync()).Where(o => o.Status == OrderStatus.Open);

// na: de database filtert
var open = await db.Orders.Where(o => o.Status == OrderStatus.Open).ToListAsync();

Op een ontwikkelmachine met duizend rijen ziet u het verschil niet. In productie met een miljoen rijen wel.

5. Cartesiaanse explosie bij meerdere Includes

Als twee collecties tegelijk worden geladen, stuurt EF standaard één query met JOINs. Een order met 20 regels en 5 betalingen geeft dan 100 rijen terug in plaats van 26.

var orders = await db.Orders
    .Include(o => o.Lines)
    .Include(o => o.Payments)
    .AsSplitQuery()                                                 // een aparte query per collectie
    .ToListAsync();

6. Bulkwijzigingen rij voor rij

Duizenden rijen laden, ze één voor één wijzigen en opslaan betekent duizenden rijen over het netwerk en duizenden updates. EF Core doet het met één opdracht in de database:

await db.Orders
    .Where(o => o.Status == OrderStatus.Open && o.CreatedAt < cutoff)
    .ExecuteUpdateAsync(s => s.SetProperty(o => o.Status, OrderStatus.Expired));

Hetzelfde geldt voor verwijderen, met ExecuteDeleteAsync.

7. Paginering die steeds trager wordt

Met Skip moet de database elke keer alle vorige rijen doorlopen, dus pagina honderdeen is veel trager dan pagina één. Sneller is verdergaan vanaf de laatst geziene rij:

// voor: de database slaat alle vorige rijen over
var page = await db.Orders.OrderBy(o => o.Id).Skip(pageNumber * 20).Take(20).ToListAsync();

// na: hij gaat verder vanaf de laatst geziene rij
var page = await db.Orders.OrderBy(o => o.Id).Where(o => o.Id > lastId).Take(20).ToListAsync();

Dit werkt voor “volgende” en “vorige”, niet om naar een willekeurige pagina te springen. Voor de meeste lijsten is dat ruim voldoende.

8. Ontbrekende indexen

De best geschreven query is nog steeds traag als de database de hele tabel moet lezen om tien rijen te vinden. In EF Core worden indexen bij het model gedefinieerd, dus ze horen bij de migraties en de code:

modelBuilder.Entity<Order>()
    .HasIndex(o => new { o.CustomerId, o.CreatedAt });              // voor "orders van een klant, nieuwste eerst"

Welke indexen er ontbreken, laat het uitvoeringsplan van de traagste queries zien, niet het onderbuikgevoel.

In welke volgorde

  1. SQL zienLogTo, ToQueryString, Application Insights
  2. N+1Queries in lussen en te veel aanroepen
  3. ProjectiesSelect en AsNoTracking
  4. IndexenOp basis van het uitvoeringsplan

De volgorde doet ertoe. Een index redt geen scherm dat tweehonderd queries stuurt, en een projectie helpt geen query die de hele tabel leest. Daarom begint u bij wat de SQL laat zien. Het grotere plaatje, van de database tot de server, staat in het artikel over trage applicaties.

Hoe wij werken

ProCoding is een .NET-studio uit Split, Kroatië, en de performance van EF Core en SQL Server is een van de dingen die we het vaakst doen. We lopen uw traagste schermen door, laten zien welke SQL ze sturen en waarom, en lossen het op, zelf of samen met uw team. De eerste stap is een gratis gesprek van 30 minuten.

Bronnen

Gerelateerde artikelen

© 2026 ProCoding — Alle rechten voorbehouden.Colofon en privacySplit, Kroatië