← Alle SQL tips

Executieplannen lezen: waar kijk je het eerst naar?

-- performance tuning · Ronald de Groot

Een executieplan van een flinke query kan tientallen operators bevatten. Waar begin je? Na dertig jaar performance tuning is mijn ervaring: in negen van de tien plannen vind je de boosdoener met de vijf checks hieronder — meestal binnen een paar minuten.

Gebruik bij voorkeur het actual execution plan (of Query Store / het plan uit de plan cache met runtime-statistieken). Het estimated plan laat alleen zien wat de optimizer dacht, niet wat er echt gebeurde.

1. Volg de dikke pijlen

De dikte van een pijl tussen twee operators staat voor het aantal rijen dat erdoorheen stroomt. Begin rechtsboven (daar begint SQL Server met lezen) en volg de dikste pijlen. Vaak zie je dat er miljoenen rijen worden gelezen terwijl de query er maar een paar honderd teruggeeft: al die rijen zijn dan te laat weggefilterd. Dat is bijna altijd een teken dat een index op de filterkolom ontbreekt, of dat een predicaat niet sargable is.

-- Niet sargable: functie over de kolom, index onbruikbaar
WHERE YEAR(OrderDate) = 2026

-- Wel sargable: de index op OrderDate kan een seek doen
WHERE OrderDate >= '2026-01-01' AND OrderDate < '2027-01-01'

2. Scan waar je een seek verwacht

Een Clustered Index Scan of Index Scan op een grote tabel, terwijl je query maar een handvol rijen nodig heeft, is check nummer twee. Veelvoorkomende oorzaken:

Let op: een scan is niet per definitie fout. Bij een kleine tabel, of als je écht bijna alle rijen nodig hebt, is een scan juist de goedkoopste keuze.

3. Key Lookups met hoge aantallen

Een Key Lookup betekent: de nonclustered index leverde de rij op, maar er ontbraken kolommen, dus moet SQL Server per rij terug naar de clustered index. Bij tien rijen geen probleem; bij honderdduizend rijen is dat honderdduizend keer extra I/O. De oplossing is meestal een covering index:

CREATE INDEX IX_Orders_CustomerID
ON dbo.Orders (CustomerID)
INCLUDE (OrderDate, TotalAmount);   -- lookup-kolommen mee in de index

In mijn artikel over index tuning ga ik hier dieper op in.

4. Estimated vs. actual rows: klopt de schatting?

Vergelijk per operator het geschatte aantal rijen met het werkelijke aantal. Een afwijking van een factor 10, 100 of meer betekent dat de optimizer zijn keuzes (join-type, geheugen, volgorde) op verkeerde informatie baseerde. De meest voorkomende oorzaken:

5. Warnings: de uitroeptekens

Operators met een geel uitroepteken verdienen altijd een blik. De belangrijkste warnings:

WarningBetekenis
Spill to tempdbTe weinig geheugen toegekend; sort of hash join valt terug op tempdb — vaak gevolg van slechte schattingen (punt 4).
Implicit conversionDatatype-mismatch; kan index seeks onmogelijk maken (punt 2).
No join predicateCartesisch product — bijna altijd een vergeten JOIN-conditie.
Excessive memory grantQuery vroeg veel meer geheugen dan gebruikt; drukt andere queries weg.

Tot slot: meet ook buiten het plan

Het plan vertelt niet alles. Zet bij het tunen ook dit aan:

SET STATISTICS IO, TIME ON;

De logical reads per tabel zijn de eerlijkste maatstaf om een voor- en na-situatie te vergelijken: ze zijn onafhankelijk van cache en serverbelasting. Daalt het aantal logical reads dramatisch, dan is je fix echt een verbetering — wat de kostenpercentages in het plan ook beweren.

Hulp nodig bij een hardnekkig traag plan? Neem gerust contact op — een second opinion op een executieplan is zo gegeven.