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!

Marco Puccio

🔍 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