EF Core je spor? 8 najčešćih uzroka i kako ih popraviti

Aplikacija na EF Coreu usporava kako raste? Osam najčešćih uzroka, od N+1 upita do straničenja, s kodom prije i poslije, i kako uopće vidjeti SQL koji EF šalje.

Vlado Pandžić

Vlado Pandžić · Osnivač · Senior .NET arhitekt
Objavljeno · 4 min čitanja

Na početku je sve brzo. Baza ima nekoliko tisuća redaka, ekrani se otvaraju trenutno i nitko ne razmišlja o tome kakav SQL Entity Framework šalje. Dvije godine i nekoliko milijuna redaka kasnije isti ekrani traju sekundama, a tim počinje pričati o prelasku na “čisti SQL”.

EF Core rijetko je sam kriv. Gotovo uvijek radi točno ono što mu je rečeno, samo što mu je rečeno nešto skupo. Ovo je osam uzroka koje najčešće nalazimo, svaki s kodom prije i poslije.

Prvo: pogledajte SQL koji EF šalje

Dok ne vidite SQL, nagađate. EF Core ga pokazuje na dva načina:

options.UseSqlServer(connectionString)
       .LogTo(Console.WriteLine, LogLevel.Information);          // svaki upit u konzolu, samo u razvoju

var sql = db.Orders.Where(o => o.Total > 1000).ToQueryString();  // SQL jednog upita, bez izvršavanja

U produkciji isto pokazuje Application Insights: svaki poziv prema bazi, koliko je trajao i iz kojeg zahtjeva je došao.

1. N+1 upiti

Jedan upit za popis, pa još po jedan za svaki redak. Sto narudžbi znači sto i jedan odlazak u bazu.

var orders = await db.Orders.ToListAsync();
foreach (var order in orders)
{
    var customer = await db.Customers.FindAsync(order.CustomerId);  // jedan upit po narudžbi
}

Rješenje je dohvatiti sve odjednom, s Include ili, još bolje, projekcijom iz sljedeće točke.

2. Cijeli entiteti kad ekranu trebaju tri polja

Popis narudžbi prikazuje broj, kupca i iznos, a aplikacija učita cijele narudžbe sa svim stavkama.

var rows = await db.Orders
    .Select(o => new OrderRow(o.Number, o.Customer.Name, o.Total))  // samo ono što ekran prikazuje
    .ToListAsync();

Projekcija sa Select rješava i N+1 i višak podataka u jednom potezu: jedan upit, samo potrebni stupci.

3. Praćenje promjena za podatke koji se samo čitaju

EF po defaultu prati svaki učitani entitet, za slučaj da ćete ga mijenjati. Za ekrane koji samo prikazuju podatke to je nepotreban posao i memorija.

var products = await db.Products
    .AsNoTracking()                                                 // samo čitanje, bez praćenja
    .Where(p => p.IsActive)
    .ToListAsync();

4. Filtriranje u memoriji umjesto u bazi

Jedan ToList() na krivom mjestu i cijela tablica putuje u aplikaciju da bi se tamo filtrirala.

// prije: cijela tablica u memoriju, filtrira se u aplikaciji
var open = (await db.Orders.ToListAsync()).Where(o => o.Status == OrderStatus.Open);

// poslije: filtrira baza
var open = await db.Orders.Where(o => o.Status == OrderStatus.Open).ToListAsync();

Na razvojnom računalu s tisuću redaka razlika se ne vidi. U produkciji s milijun redaka vidi se i te kako.

5. Kartezijska eksplozija kod više Include

Kad se učitavaju dvije kolekcije odjednom, EF po defaultu šalje jedan upit s JOIN-ovima. Narudžba s 20 stavki i 5 uplata tako vraća 100 redaka umjesto 26.

var orders = await db.Orders
    .Include(o => o.Lines)
    .Include(o => o.Payments)
    .AsSplitQuery()                                                 // zaseban upit za svaku kolekciju
    .ToListAsync();

6. Masovne izmjene red po red

Učitati tisuće redaka, promijeniti ih jedan po jedan i spremiti znači tisuće redaka kroz mrežu i tisuće izmjena. EF Core to može jednom naredbom u bazi:

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

Isto vrijedi i za brisanje, s ExecuteDeleteAsync.

7. Straničenje koje je sve sporije

S Skip baza svaki put mora proći sve prethodne retke, pa je stotinu i prva stranica puno sporija od prve. Brže je nastaviti od zadnjeg viđenog retka:

// prije: baza preskače sve prethodne retke
var page = await db.Orders.OrderBy(o => o.Id).Skip(pageNumber * 20).Take(20).ToListAsync();

// poslije: nastavlja od zadnjeg viđenog
var page = await db.Orders.OrderBy(o => o.Id).Where(o => o.Id > lastId).Take(20).ToListAsync();

Ovo radi za “sljedeća” i “prethodna”, a ne za skok na proizvoljnu stranicu. Za većinu popisa to je sasvim dovoljno.

8. Indeksi koji nedostaju

Najbolje napisan upit i dalje je spor ako baza mora pročitati cijelu tablicu da nađe deset redaka. Indeksi se u EF Coreu definiraju uz model, pa su dio migracija i koda:

modelBuilder.Entity<Order>()
    .HasIndex(o => new { o.CustomerId, o.CreatedAt });              // za "narudžbe kupca, najnovije prve"

Koje indekse dodati, pokaže plan izvršavanja najsporijih upita, a ne osjećaj.

Kojim redom

  1. Vidjeti SQLLogTo, ToQueryString, Application Insights
  2. N+1Upiti u petljama i previše poziva
  3. ProjekcijeSelect i AsNoTracking
  4. IndeksiPrema planu izvršavanja

Poredak je bitan. Indeks neće spasiti ekran koji šalje dvjesto upita, a projekcija neće pomoći upitu koji čita cijelu tablicu. Zato se kreće od onoga što se vidi u SQL-u. Širu sliku, od baze do servera, opisuje članak o sporim aplikacijama.

Kako mi radimo

ProCoding je .NET studio iz Splita, a performanse EF Corea i SQL Servera jedno su od onoga što radimo najčešće. Prolazimo najsporije ekrane, pokažemo koji SQL šalju i zašto, i popravimo ih, sami ili zajedno s vašim timom. Prvi korak je besplatni razgovor od 30 minuta.

Izvori

Povezani članci

© 2026 ProCoding — Sva prava pridržana.Impresum i privatnostSplit, Hrvatska