Leggere un piano di esecuzione senza piangere

Come SQL Server decide di eseguire la tua query, come leggere il piano che ti mostra, e i tre operatori che spiegano il 90% delle query lente.

Leggere un piano di esecuzione senza piangere

La tua query impiega 45 secondi. La versione del tuo collega della stessa logica ne impiega 2. La tua sembra più semplice. Cosa sta succedendo?

La risposta è nel piano di esecuzione — l’insieme concreto di passaggi che SQL Server ha scelto per eseguire la tua query. Imparare a leggerlo è come imparare a leggere una mappa: una volta che sai farlo, smetti di perderti.

Come visualizzare il piano

In SQL Server Management Studio (SSMS):

  • Piano stimato: premi Ctrl+L o clicca “Display Estimated Execution Plan”. Mostra cosa l’optimizer pensa di fare, senza eseguire la query.
  • Piano effettivo: premi Ctrl+M e poi esegui la query. Mostra cosa SQL Server ha effettivamente fatto, incluso il conteggio reale delle righe. È questo quello che conta per la diagnosi.

In Azure Data Studio o DBeaver, cerca le opzioni “Explain” o “Show Plan” — stesso concetto, interfaccia leggermente diversa.

Si legge da destra a sinistra, dal basso verso l’alto

I piani di esecuzione sono alberi. I dati fluiscono da destra a sinistra. Gli operatori più a destra producono i dati (table scan, index seek); l’operatore più a sinistra è il result set finale che torna a te.

Ogni operatore ha:

  • Costo % — costo relativo all’interno della query. L’operatore all’85% è dove se ne va il tuo tempo.
  • Righe stimate vs righe effettive — se differiscono di 100×, l’optimizer ha fatto una stima sbagliata e probabilmente ha scelto una strategia sbagliata. Questo è il numero più diagnostico dell’intero piano.
  • Frecce spesse vs sottili — lo spessore della freccia tra operatori indica il volume dei dati. Una freccia spessa che arriva a una sottile è un filtro che fa il suo lavoro. Una freccia sottile che arriva a una spessa è un segnale d’allarme (nested loop che producono più dati del previsto).

I tre operatori che spiegano la maggior parte dei problemi

Non serve memorizzare 50 operatori. Tre di essi compaiono in quasi ogni query lenta:

1. Table Scan / Clustered Index Scan

Uno scan legge ogni riga della tabella. Per una tabella da un milione di righe, significa un milione di righe esaminate anche se te ne servono solo 10.

Gli scan non sono sempre un male — se hai genuinamente bisogno della maggior parte della tabella, uno scan è l’opzione più veloce. Ma se stai filtrando fino a una manciata di righe e il piano mostra uno scan, probabilmente ti manca un indice.

Soluzione: crea un indice non clustered sulle colonne nella tua clausola WHERE. Se non hai familiarità con gli indici, leggi prima gli indici clustered.

2. Key Lookup (Bookmark Lookup)

Hai un indice non clustered che trova le righe, ma la query ha bisogno anche di colonne che non sono in quell’indice. SQL Server torna all’indice clustered per recuperare le colonne mancanti, una riga alla volta. Per poche righe, va bene. Per migliaia, è un massacro.

Soluzione: aggiungi le colonne mancanti come colonne INCLUDE sull’indice non clustered:

CREATE NONCLUSTERED INDEX IX_Orders_Status
ON Orders (Status)
INCLUDE (CustomerName, OrderTotal);

Questo trasforma il lookup in un covering index — tutti i dati di cui la query ha bisogno sono già nell’indice, nessun round-trip necessario.

3. Hash Match / Sort (costo elevato)

Compaiono quando SQL Server sta facendo join o aggregazioni su dataset grandi. Non sono intrinsecamente negativi, ma quando le righe stimate sono completamente sbagliate, l’operatore sceglie un algoritmo sbagliato:

  • Nested Loops — ottimo per poche righe, catastrofico per molte
  • Hash Match — ottimo per grandi moli di dati non ordinati, costoso in termini di memoria
  • Merge Join — efficiente quando entrambi gli input sono già ordinati sulla chiave di join

Se vedi un Hash Match che mangia il 70% del costo della tua query, controlla se la stima delle righe dell’optimizer è accurata. Le statistiche obsolete sono il colpevole di turno:

UPDATE STATISTICS Orders;

Parameter sniffing: il cattivo invisibile

SQL Server mette in cache i piani di esecuzione e li riutilizza. La prima volta che una stored procedure viene eseguita, costruisce un piano ottimizzato per quello specifico set di valori dei parametri. Se la chiamata successiva ha valori molto diversi, riutilizza un piano che potrebbe essere terribile per i nuovi valori.

Il sintomo classico: “È veloce quando la lancio in SSMS ma lenta dall’applicazione.” L’esecuzione in SSMS ha compilato un piano fresco; l’applicazione sta riutilizzando uno stantio.

Soluzioni rapide:

-- Opzione 1: Ricompila a ogni chiamata (sicuro ma leggermente più CPU)
EXEC sp_MyProc @id = 123 WITH RECOMPILE;

-- Opzione 2: Ottimizza per unknown (piano generico, di solito OK)
-- Aggiungi questo dentro la stored procedure:
OPTION (OPTIMIZE FOR UNKNOWN);

-- Opzione 3: Opzione nucleare — svuota la plan cache per questa query
DBCC FREEPROCCACHE(plan_handle);

La strategia di lettura 80/20

Non serve capire ogni operatore per risolvere la maggior parte dei problemi:

  1. Trova la freccia più grossa. Lì scorre il volume maggiore di dati.
  2. Trova il costo % più alto. Lì se ne va la maggior parte del tempo.
  3. Confronta righe stimate vs effettive. Una discrepanza significa statistiche obsolete o parameter sniffing.
  4. Cerca scan dove avrebbero senso dei seek. Indice mancante.
  5. Cerca key lookup. Colonne INCLUDE mancanti.

Questi cinque controlli spiegano circa il 90% delle query lente. Il restante 10% è dove le cose si fanno interessanti — ma prima risolvi il 90%.