Come ho migliorato una query da 3 minuti a 2 secondi in SQL Server!

Come ho migliorato una query da 3 minuti a 2 secondi in SQL Server!

πŸ” Diagnosi: cosa succede davvero nel motore SQL?

Il primo passo è stato attivare SQL Server Profiler per intercettare la query esatta eseguita dal frontend. Usavano già una SqlQuery() diretta, quindi niente Entity Framework a rallentare.

Con SET STATISTICS TIME, IO ON ho raccolto le prime informazioni:

  • Più di 1.8 milioni di letture logiche,

  • CPU costantemente alta durante l'esecuzione,

  • Una vista complessa con JOIN annidati e funzioni scalari nella SELECT,

  • Uso di YEAR() e ISNULL() nelle clausole WHERE.

Il piano di esecuzione mostrava:
❌ Table scan completo su tabelle enormi
❌ Nested loop inefficienti
❌ Join su campi non indicizzati


πŸ› οΈ Analisi della query originale

Ecco una versione semplificata:

SELECTΒ  u.Nome, ISNULL(f.Codice, '') AS CodiceFiscale, YEAR(u.DataCreazione) AS Anno, (SELECT COUNT(*) FROM Ordini o WHERE o.IdUtente = u.IdUtente) AS TotaleOrdini FROM Utenti u LEFT JOIN Fiscali f ON dbo.MySafeTrim(f.IdUtente) = u.IdUtente WHERE YEAR(u.DataCreazione) = 2023

Problemi principali:

  • Funzioni come YEAR() = non sargable

  • Funzione MySafeTrim personalizzata = non indicizzabile

  • Subquery in SELECT = costosa per ogni riga

  • Nessun indice su DataCreazione o IdUtente


🧠 Strategia di intervento: ottimizzare senza cambiare il risultato

L'obiettivo era mantenere lo stesso output ma ottimizzare tutto al massimo.

1. Query riscritta in forma sargable

SELECT u.Nome, f.Codice AS CodiceFiscale, o.TotaleOrdini FROM Utenti u LEFT JOIN Fiscali f ON f.IdUtente = u.IdUtente LEFT JOIN ( SELECT IdUtente, COUNT(*) AS TotaleOrdini FROM Ordini GROUP BY IdUtente ) o ON o.IdUtente = u.IdUtente WHERE u.DataCreazione >= '2023-01-01' AND u.DataCreazione < '2024-01-01'

βœ… Tolte le funzioni dalla WHERE
βœ… Sostituita la subquery correlata con un JOIN preaggregato
βœ… Evitato ISNULL() se non indispensabile nel filtro
βœ… Rimossa funzione MySafeTrim


🧱 Indici creati ad hoc

Utilizzando sys.dm_db_missing_index_details ho individuato suggerimenti su tabelle usate senza indice.

πŸ“Œ Indice su Utenti per data:

CREATE NONCLUSTERED INDEX IX_Utenti_Data ON Utenti (DataCreazione) INCLUDE (Nome, IdUtente)

πŸ“Œ Indice su Fiscali per join rapida:

CREATE NONCLUSTERED INDEX IX_Fiscali_IdUtente ON Fiscali (IdUtente)

πŸ“Œ Indice su Ordini:

CREATE NONCLUSTERED INDEX IX_Ordini_IdUtente ON Ordini (IdUtente)

Inoltre ho suggerito all’azienda di ricostruire periodicamente gli indici per evitare la frammentazione, tramite job SQL Server Agent.


⏱️ Confronto dei risultati

VersioneTempo esecuzioneLetture logicheCosti CPU
Prima3 min e 12 sec1.870.000Alta
Dopo2.1 secondi32.000Bassa

La query ora risponde in tempo reale anche con una base dati in crescita.


βœ… Conclusioni e lezioni apprese

Questa esperienza dimostra alcune regole d’oro:

βœ… Non serve hardware se la query è scritta male

βœ… Le funzioni scalari sono nemiche delle performance

βœ… Le subquery correlate sono da evitare nei report

βœ… Gli indici vanno costruiti in funzione delle query, non a caso

βœ… Il profiler SQL Server è essenziale, sempre

βœ… A volte basta riscrivere, non rifattorizzare tutto