Query-ul tău durează 45 de secunde. Versiunea colegului tău pentru aceeași logică durează 2 secunde. Al tău arată mai simplu. Ce se întâmplă?
Răspunsul e în planul de execuție — setul efectiv de pași pe care SQL Server a ales să-i execute pentru query-ul tău. Să înveți să-l citești e ca și cum ai învăța să citești o hartă: odată ce poți, nu te mai rătăcești.
Cum vezi planul
În SQL Server Management Studio (SSMS):
- Planul estimat: Apasă
Ctrl+Lsau click pe „Display Estimated Execution Plan”. Arată ce crede optimizatorul că va face, fără a rula query-ul. - Planul efectiv: Apasă
Ctrl+Mapoi rulează query-ul. Arată ce a făcut efectiv SQL Server, inclusiv numărul real de rânduri. Acesta e cel care contează pentru diagnostic.
În Azure Data Studio sau DBeaver, caută opțiunile „Explain” sau „Show Plan” — aceeași idee, interfață ușor diferită.
Citește-l de la dreapta la stânga, de jos în sus
Planurile de execuție sunt arbori. Datele curg de la dreapta la stânga. Operatorii din extrema dreaptă produc date (scanări de tabele, căutări pe index); operatorul din extrema stângă e setul final de rezultate care ajunge la tine.
Fiecare operator are:
- Cost % — costul relativ în cadrul query-ului. Operatorul la 85% e locul unde se duce timpul tău.
- Rânduri estimate vs rânduri efective — dacă diferă de 100×, optimizatorul a ghicit prost și probabil a ales o strategie proastă. Acesta e cel mai diagnostic număr din plan.
- Săgeți groase vs subțiri — grosimea săgeții dintre operatori arată volumul de date. O săgeată groasă care alimentează una subțire e un filtru care-și face treaba. O săgeată subțire care alimentează una groasă e un semnal de alarmă (nested loops care produc mai multe date decât era de așteptat).
Cei trei operatori care explică majoritatea problemelor
Nu trebuie să memorezi 50 de operatori. Trei dintre ei apar în aproape fiecare query lent:
1. Table Scan / Clustered Index Scan
Un scan citește fiecare rând din tabel. Pentru un tabel cu un milion de rânduri, asta înseamnă un milion de rânduri examinate chiar dacă ai nevoie doar de 10.
Scan-urile nu sunt mereu rele — dacă ai nevoie cu adevărat de majoritatea tabelului, un scan e cea mai rapidă opțiune. Dar dacă filtrezi până la câteva rânduri și planul arată un scan, probabil îți lipsește un index.
Soluție: Creează un index nonclustered pe coloana (coloanele) din clauza WHERE. Dacă nu ești familiarizat cu indexurile, citește indexuri clustered mai întâi.
2. Key Lookup (Bookmark Lookup)
Ai un index nonclustered care găsește rândurile, dar query-ul are nevoie și de coloane care nu sunt în acel index. SQL Server se întoarce la indexul clustered ca să aducă coloanele lipsă, un rând la un moment dat. Pentru câteva rânduri, e ok. Pentru mii, e dezastru.
Soluție: Adaugă coloanele lipsă ca și coloane INCLUDE pe indexul nonclustered:
CREATE NONCLUSTERED INDEX IX_Orders_Status
ON Orders (Status)
INCLUDE (CustomerName, OrderTotal);
Asta transformă lookup-ul într-un covering index — toate datele de care are nevoie query-ul sunt deja în index, fără drum dus-întors.
3. Hash Match / Sort (cost ridicat)
Apar când SQL Server face join sau agregare pe seturi mari de date. Nu sunt inerent rele, dar când rândurile estimate sunt complet greșite, operatorul alege un algoritm nepotrivit:
- Nested Loops — excelent pentru puține rânduri, catastrofal pentru multe
- Hash Match — excelent pentru date mari nesortate, costisitor ca memorie
- Merge Join — eficient când ambele input-uri sunt deja sortate pe cheia de join
Dacă vezi un Hash Match care mănâncă 70% din costul query-ului, verifică dacă estimarea de rânduri a optimizatorului e corectă. Statisticile învechite sunt vinovatul obișnuit:
UPDATE STATISTICS Orders;
Parameter sniffing: eroul negativ invizibil
SQL Server pune în cache planurile de execuție și le reutilizează. Prima dată când o procedură stocată rulează, construiește un plan optimizat pentru acel set specific de valori de parametri. Dacă următorul apel are valori foarte diferite, reutilizează un plan care ar putea fi teribil pentru noile valori.
Simptomul clasic: „E rapid când îl rulez în SSMS dar lent din aplicație.” Rularea din SSMS a compilat un plan proaspăt; aplicația reutilizează unul vechi.
Soluții rapide:
-- Opțiunea 1: Recompilare la fiecare apel (sigur dar puțin mai mult CPU)
EXEC sp_MyProc @id = 123 WITH RECOMPILE;
-- Opțiunea 2: Optimizare pentru necunoscut (plan generic, de obicei OK)
-- Adaugă asta în interiorul procedurii stocate:
OPTION (OPTIMIZE FOR UNKNOWN);
-- Opțiunea 3: Opțiunea nucleară — golește cache-ul de planuri pentru acest query
DBCC FREEPROCCACHE(plan_handle);
Strategia de citire 80/20
Nu trebuie să înțelegi fiecare operator ca să rezolvi majoritatea problemelor:
- Găsește cea mai groasă săgeată. Acolo curge cel mai mult volum de date.
- Găsește cel mai mare cost %. Acolo se duce cel mai mult timp.
- Compară rândurile estimate vs efective. O discrepanță înseamnă statistici proaste sau parameter sniffing.
- Caută scan-uri unde seek-urile ar avea sens. Index lipsă.
- Caută key lookup-uri. Coloane INCLUDE lipsă.
Aceste cinci verificări explică aproximativ 90% din query-urile lente. Restul de 10% e unde lucrurile devin interesante — dar rezolvi mai întâi cele 90%.