In questo articolo analizziamo vediamo un esempio di utilizzo delle tabelle temporanee per migliorare le performance di una query su SQL Server. Le tabelle temporanee sono salvate all’interno del TempDB, questo Database utilizza uno spazio di memoria molto delicato per almeno due motivi:
- è condiviso dall’intera istanza di SQL Server e non solo da un singolo Database;
- è utilizzato continuamente da SQL Server sia per eseguire task di sistema e sia per risolvere parte delle comuni query SQL lanciate dagli sviluppatori o utenti.
Di conseguenza ogni volta che vogliamo usare le tabelle temporanee, dobbiamo accertarci che l’output che la tabella che andremo a creare abbiamo un modesto numero di righe (o comunque ragionevolmente piccolo in base a quella che è la configurazione del TempDb e del disco su cui è salvato).
Relazione tra piano d’esecuzione e statistiche
Supponiamo di avere una tabella con una distribuzione molto particolare di dati in una colonna:
- un unico valore è presente più del 99% delle volte;
- qualche altro valore si divide il restante 1% delle righe;
- sulla colonna è presente un indice non clustered.
Per facilità supponiamo che si tratta della tabella dei Clienti, che la colonna in questione sia il Nome e che il valore ripetuto più volte sia Nicola. Ci aspettiamo per queste due query molto simili dei piani d’esecuzione differenti:
SELECT *
FROM dbo.Clienti
WHERE Nome = 'Nicola';
SELECT *
FROM dbo.Clienti
WHERE Nome = 'Alberto';
Infatti nel primo caso SQL Server stima la presenza di 9802 righe che rispettano il filtro, di conseguenza sceglierà di eseguire una semplice scansione dell’intera tabella (l’indice clustered). Nel secondo caso invece stima la presenza di 1 sola riga, di conseguenza userà un indice (non covered) per poi ricavare tutte le altre informazioni con una key lookup (se non conosci questo lessico puoi guardare il mio video corso gratuito sui piani d’esecuzione).

In entrambi i casi le stime sono corrette e il piano d’esecuzione è quello ottimale. Ma cosa succede se il valore con cui eseguire il filtro non è esplicitato direttamente, ma è calcolato tramite una subquery?
Consideriamo ad esempio la query
SELECT *
FROM dbo.Clienti
WHERE Nome = (SELECT TOP 1 Nome
FROM dbo.Clienti
ORDER BY Cognome DESC);
SQL Server crea il piano d’esecuzione prima di eseguire la subquery, di conseguenza non conosce a priori quale sia il relativo risultato. Di conseguenza è possibile che nella costruzione del piano utilizzi una stima non precisa del numero di righe che rispettano la condizione.

Ho racchiuso in blu la parte relativa alla subquery e in rosso la stima utilizzata per la parte finale della query. Come vedete in questo caso risulta sbagliata: 4901 contro 1 sola riga reale. Personalmente non conosco con quale regola sia stato scelto il valore 4901, noto però che in questo caso è circa pari alla metà delle righe complessive della tabella.
Utilizzo delle tabelle temporanee per il tuning
L’idea di ottimizzazione è quella di materializzare il risultato della sub-query in una tabella temporanea e di creare dunque due query distinte. In questo caso siamo sicuri di poter utilizzare tranquillamente una tabella temporanea senza creare problemi al TempDB, in quanto l’output della sub-query sarà per costruzione sempre costituito da una sola riga. Speriamo infatti che in questo modo SQL Server utilizzi il valore presente all’interno della tabella temporanea per eseguire una stima più corretta.
Tuttavia, con un po’ di delusione, la parte relativa al filtro sul nome non cambia né per quanto riguarda il piano d’esecuzione, né per quanto riguarda le stime.
SELECT TOP 1 Nome
INTO #TopNome
FROM Clienti
ORDER BY Cognome DESC;
SELECT *
FROM dbo.Clienti
WHERE Nome = (SELECT Nome
FROM #TopNome);
La situazione invece cambia sostituendo l’operatore uguale (“=”) con l’operatore IN. Nel caso esaminato le due query sono completamente equivalenti perché la tabella temporanea sarà sempre costituita da una sola riga (in generale, se fossero presenti più righe la query con l’uguale solleverebbe un errore). Riscriviamo dunque la seconda query in questo modo
SELECT *
FROM dbo.Clienti
WHERE Nome IN (SELECT Nome
FROM #TopNome);
e otteniamo un piano d’esecuzione ottimizzato, creato stimando correttamente una sola riga di ouput.

Osserviamo in particolare che il valore nella tabella temporanea viene prima cercato in maniera ottimizzata all’interno dell’indice IX_clienti_nome e poi la query è “completata” tramite una Key Lookup per estrarre anche le colonne non presenti nell’indice.
Continua a imparare
- Segui la lezione successiva per imparare a individuare le query più lente
- Torna all’indice delle lezioni
- Visita la pagina del mio videocorso avanzato su T-SQL e performance