Blazor och .NET

Är EF Core långsamt? 8 vanliga orsaker och hur du åtgärdar dem

Blir din EF Core-applikation långsammare när den växer? De åtta vanligaste orsakerna, från N+1-frågor till paginering, med kod före och efter.

Vlado Pandžić

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

I början går allt snabbt. Databasen har några tusen rader, skärmarna öppnas direkt och ingen funderar på vilken SQL Entity Framework skickar. Två år och några miljoner rader senare tar samma skärmar flera sekunder, och teamet börjar prata om att gå över till ”ren SQL”.

EF Core är sällan ensam boven. Det gör nästan alltid exakt det som det blivit tillsagt, det har bara blivit tillsagt att göra något dyrt. Det här är de åtta orsaker vi hittar oftast, var och en med kod före och efter.

Först: titta på den SQL som EF skickar

Så länge du inte ser SQL:en gissar du. EF Core visar den på två sätt:

options.UseSqlServer(connectionString)
       .LogTo(Console.WriteLine, LogLevel.Information);          // every query to the console, development only

var sql = db.Orders.Where(o => o.Total > 1000).ToQueryString();  // the SQL of one query, without running it

I produktion visar Application Insights samma sak: varje anrop till databasen, hur lång tid det tog och vilken förfrågan det kom från.

1. N+1-frågor

En fråga för listan, sedan en till för varje rad. Hundra ordrar betyder hundraen turer till databasen.

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

Lösningen är att hämta allt på en gång, med Include eller, ännu bättre, med projektionen i nästa punkt.

2. Hela entiteter när skärmen behöver tre fält

Orderlistan visar nummer, kund och belopp, och applikationen läser in hela ordrar med alla orderrader.

var rows = await db.Orders
    .Select(o => new OrderRow(o.Number, o.Customer.Name, o.Total))  // only what the screen shows
    .ToListAsync();

En projektion med Select löser både N+1 och överflödig data på en gång: en fråga, bara de kolumner du behöver.

3. Ändringsspårning för data som bara läses

Som standard spårar EF varje entitet den läser in, ifall du skulle ändra den. För skärmar som bara visar data är det onödigt arbete och onödigt minne.

var products = await db.Products
    .AsNoTracking()                                                 // read only, no tracking
    .Where(p => p.IsActive)
    .ToListAsync();

4. Filtrering i minnet i stället för i databasen

Ett ToList() på fel ställe, och hela tabellen skickas till applikationen för att filtreras där.

// before: the whole table into memory, filtered in the application
var open = (await db.Orders.ToListAsync()).Where(o => o.Status == OrderStatus.Open);

// after: the database filters
var open = await db.Orders.Where(o => o.Status == OrderStatus.Open).ToListAsync();

På en utvecklardator med tusen rader ser du ingen skillnad. I produktion med en miljon rader gör du det.

5. Kartesisk explosion med flera Include

När två samlingar läses in samtidigt skickar EF som standard en enda fråga med JOIN. En order med 20 orderrader och 5 betalningar returnerar då 100 rader i stället för 26.

var orders = await db.Orders
    .Include(o => o.Lines)
    .Include(o => o.Payments)
    .AsSplitQuery()                                                 // a separate query for each collection
    .ToListAsync();

6. Massändringar rad för rad

Att läsa in tusentals rader, ändra dem en i taget och spara betyder tusentals rader över nätverket och tusentals uppdateringar. EF Core klarar det med en enda sats i databasen:

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

Detsamma gäller radering, med ExecuteDeleteAsync.

7. Paginering som blir allt långsammare

Med Skip måste databasen gå igenom alla tidigare rader varje gång, så sida hundraen är mycket långsammare än sida ett. Det går fortare att fortsätta från den senaste raden du såg:

// before: the database skips all the previous rows
var page = await db.Orders.OrderBy(o => o.Id).Skip(pageNumber * 20).Take(20).ToListAsync();

// after: it continues from the last row seen
var page = await db.Orders.OrderBy(o => o.Id).Where(o => o.Id > lastId).Take(20).ToListAsync();

Det fungerar för ”nästa” och ”föregående”, inte för att hoppa till valfri sida. För de flesta listor räcker det gott.

8. Index som saknas

Även den bäst skrivna frågan är långsam om databasen måste läsa hela tabellen för att hitta tio rader. I EF Core definieras index tillsammans med modellen, så de är en del av migreringarna och koden:

modelBuilder.Entity<Order>()
    .HasIndex(o => new { o.CustomerId, o.CreatedAt });              // for "a customer's orders, newest first"

Vilka index som ska läggas till visar exekveringsplanen för de långsammaste frågorna, inte magkänslan.

I vilken ordning

  1. Se SQL:enLogTo, ToQueryString, Application Insights
  2. N+1Frågor i loopar och för många anrop
  3. ProjektionerSelect och AsNoTracking
  4. IndexUtifrån exekveringsplanen

Ordningen spelar roll. Ett index räddar inte en skärm som skickar tvåhundra frågor, och en projektion hjälper inte en fråga som läser hela tabellen. Därför börjar du med det som SQL:en visar. Den större bilden, från databasen till servern, finns i artikeln om långsamma applikationer.

Så arbetar vi

ProCoding är en .NET-studio från Split i Kroatien, och prestanda i EF Core och SQL Server är något av det vi arbetar med oftast. Det här är precis vad vi gör: vi går igenom dina långsammaste skärmar, visar vilken SQL de skickar och varför, och åtgärdar det, på egen hand eller tillsammans med ditt team. Det första steget är 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