EF Core ist langsam? 8 häufige Ursachen und wie Sie sie beheben
EF-Core-Anwendung wird langsamer? Die acht häufigsten Ursachen, von N+1-Abfragen bis Paginierung, mit Code vorher und nachher, und wie man das SQL sieht.
Vlado Pandžić · Gründer · Senior .NET-Architekt
Veröffentlicht · 4 Min. Lesezeit
Am Anfang ist alles schnell. Die Datenbank hat ein paar tausend Zeilen, die Bildschirme öffnen sofort, und niemand denkt darüber nach, welches SQL Entity Framework schickt. Zwei Jahre und ein paar Millionen Zeilen später brauchen dieselben Bildschirme Sekunden, und das Team fängt an, über “reines SQL” zu reden.
EF Core ist selten allein schuld. Es tut fast immer genau das, was ihm gesagt wurde, nur wurde ihm etwas Teures gesagt. Das sind die acht Ursachen, die wir am häufigsten finden, jeweils mit Code vorher und nachher.
Zuerst: Schauen Sie sich das SQL an, das EF schickt
Solange Sie das SQL nicht sehen, raten Sie. EF Core zeigt es auf zwei Wegen:
options.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information); // jede Abfrage in die Konsole, nur in der Entwicklung
var sql = db.Orders.Where(o => o.Total > 1000).ToQueryString(); // das SQL einer Abfrage, ohne sie auszuführen
In der Produktion zeigt Application Insights dasselbe: jeden Aufruf an die Datenbank, wie lange er dauerte und aus welcher Anfrage er kam.
1. N+1-Abfragen
Eine Abfrage für die Liste, dann noch eine für jede Zeile. Hundert Bestellungen bedeuten hunderteins Wege zur Datenbank.
var orders = await db.Orders.ToListAsync();
foreach (var order in orders)
{
var customer = await db.Customers.FindAsync(order.CustomerId); // eine Abfrage pro Bestellung
}
Die Lösung ist, alles auf einmal zu holen, mit Include oder, noch besser, mit der Projektion aus dem nächsten Punkt.
2. Ganze Entitäten, wenn der Bildschirm drei Felder braucht
Die Bestellliste zeigt Nummer, Kunde und Betrag, und die Anwendung lädt ganze Bestellungen mit allen Positionen.
var rows = await db.Orders
.Select(o => new OrderRow(o.Number, o.Customer.Name, o.Total)) // nur das, was der Bildschirm zeigt
.ToListAsync();
Eine Projektion mit Select löst N+1 und die überflüssigen Daten in einem Zug: eine Abfrage, nur die benötigten Spalten.
3. Änderungsverfolgung für Daten, die nur gelesen werden
EF verfolgt standardmäßig jede geladene Entität, falls Sie sie ändern. Für Bildschirme, die Daten nur anzeigen, ist das unnötige Arbeit und unnötiger Speicher.
var products = await db.Products
.AsNoTracking() // nur lesen, ohne Verfolgung
.Where(p => p.IsActive)
.ToListAsync();
4. Filtern im Speicher statt in der Datenbank
Ein ToList() an der falschen Stelle, und die ganze Tabelle wandert in die Anwendung, um dort gefiltert zu werden.
// vorher: die ganze Tabelle in den Speicher, gefiltert in der Anwendung
var open = (await db.Orders.ToListAsync()).Where(o => o.Status == OrderStatus.Open);
// nachher: die Datenbank filtert
var open = await db.Orders.Where(o => o.Status == OrderStatus.Open).ToListAsync();
Auf einem Entwicklungsrechner mit tausend Zeilen sieht man den Unterschied nicht. In der Produktion mit einer Million Zeilen sehr wohl.
5. Kartesische Explosion bei mehreren Include
Werden zwei Collections auf einmal geladen, schickt EF standardmäßig eine Abfrage mit JOINs. Eine Bestellung mit 20 Positionen und 5 Zahlungen liefert dann 100 Zeilen statt 26.
var orders = await db.Orders
.Include(o => o.Lines)
.Include(o => o.Payments)
.AsSplitQuery() // eine eigene Abfrage pro Collection
.ToListAsync();
6. Massenänderungen Zeile für Zeile
Tausende Zeilen laden, eine nach der anderen ändern und speichern bedeutet tausende Zeilen über das Netzwerk und tausende Updates. EF Core schafft das mit einer einzigen Anweisung in der Datenbank:
await db.Orders
.Where(o => o.Status == OrderStatus.Open && o.CreatedAt < cutoff)
.ExecuteUpdateAsync(s => s.SetProperty(o => o.Status, OrderStatus.Expired));
Dasselbe gilt fürs Löschen, mit ExecuteDeleteAsync.
7. Paginierung, die immer langsamer wird
Mit Skip muss die Datenbank jedes Mal alle vorherigen Zeilen durchgehen, deshalb ist Seite hunderteins viel langsamer als Seite eins. Schneller ist es, ab der zuletzt gesehenen Zeile weiterzumachen:
// vorher: die Datenbank überspringt alle vorherigen Zeilen
var page = await db.Orders.OrderBy(o => o.Id).Skip(pageNumber * 20).Take(20).ToListAsync();
// nachher: sie macht ab der zuletzt gesehenen Zeile weiter
var page = await db.Orders.OrderBy(o => o.Id).Where(o => o.Id > lastId).Take(20).ToListAsync();
Das funktioniert für “weiter” und “zurück”, nicht für den Sprung auf eine beliebige Seite. Für die meisten Listen reicht das völlig.
8. Fehlende Indizes
Die am besten geschriebene Abfrage ist trotzdem langsam, wenn die Datenbank die ganze Tabelle lesen muss, um zehn Zeilen zu finden. In EF Core werden Indizes mit dem Modell definiert, sie sind also Teil der Migrationen und des Codes:
modelBuilder.Entity<Order>()
.HasIndex(o => new { o.CustomerId, o.CreatedAt }); // für "Bestellungen eines Kunden, neueste zuerst"
Welche Indizes fehlen, zeigt der Ausführungsplan der langsamsten Abfragen, nicht das Bauchgefühl.
In welcher Reihenfolge
- SQL sehenLogTo, ToQueryString, Application Insights
- N+1Abfragen in Schleifen, zu viele Aufrufe
- ProjektionenSelect und AsNoTracking
- IndizesNach dem Ausführungsplan
Die Reihenfolge ist wichtig. Ein Index rettet keinen Bildschirm, der zweihundert Abfragen schickt, und eine Projektion hilft keiner Abfrage, die die ganze Tabelle liest. Deshalb beginnt man mit dem, was das SQL zeigt. Das größere Bild, von der Datenbank bis zum Server, beschreibt der Artikel über langsame Anwendungen.
Wie wir arbeiten
ProCoding ist ein .NET-Studio aus Split, Kroatien, und die Performance von EF Core und SQL Server gehört zu den Dingen, die wir am häufigsten machen. Wir gehen Ihre langsamsten Bildschirme durch, zeigen, welches SQL sie schicken und warum, und beheben das, allein oder gemeinsam mit Ihrem Team. Der erste Schritt ist ein kostenloses 30-minütiges Gespräch.
Quellen
- Übersicht über die Leistung, EF Core, Microsoft Learn
- Effiziente Abfragen, EF Core, Microsoft Learn
- Einzelne und geteilte Abfragen, EF Core, Microsoft Learn
- ExecuteUpdate und ExecuteDelete, EF Core, Microsoft Learn
- Paginierung, EF Core, Microsoft Learn