Ottimizzazione delle query SQL: Leggere i piani di esecuzione e correggere query lente, con tempistiche
Smetti di indovinare perché una query è lenta. Impara a leggere un piano di esecuzione, quindi applica le correzioni più importanti, con misurazioni prima e dopo da una tabella reale di un milione di righe.
- Inizia con il piano di esecuzione. Ti dice cosa fa effettivamente il database.
- Cerca scansioni complete su tabelle di grandi dimensioni, quindi aggiungi indici che corrispondono ai tuoi filtri e join.
- Evitare di racchiudere una colonna indicizzata in una funzione, poiché ciò potrebbe impedire un utilizzo efficiente dell’indice.
- Misura prima e dopo, perché ogni database e set di dati si comporta in modo diverso.
Una query che l’anno scorso richiedeva millisecondi può richiedere minuti oggi, semplicemente perché la tabella è cresciuta. L’ottimizzazione delle query SQL riguarda meno trucchi intelligenti e più un processo ripetibile: scoprire cosa sta facendo il database, rimuovere il lavoro sprecato e verificare che sia stato d’aiuto.
Abbiamo testato ciascuna tecnica di seguito su un milione di righe generate in SQLite 3.50. I tempi sono i migliori tra diverse esecuzioni da un laptop, utilizzando un database in memoria. Non corrisponderanno al tuo sistema e PostgreSQL, MySQL e SQL Server hanno ciascuno i propri ottimizzatori e formati di piano. Concentrarsi sulla portata di ogni miglioramento e sui principi che ne stanno alla base.
Risultati in breve
| Tecnica | Prima | Dopo |
|---|---|---|
| Indice sulla colonna del filtro | 73 ms | 0,01 ms |
| Intervallo anziché una funzione su una colonna | 96 ms | 33 ms |
| Indice composito per due filtri | 19 ms | 2,2 ms |
| Keyset anziché OFFSET profondo | 8,1 ms | 0,01 ms |
L’impostazione della prova
CREATE TABLE customers (
id INTEGER PRIMARY KEY, name TEXT, city TEXT
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY, customer_id INT, status TEXT,
created TEXT, total INT, note TEXT
);
-- 50,000 customers and 1,000,000 orders with random values
Un semplice processo di ottimizzazione
- Trova le query che contano. Utilizza registri o monitoraggio delle query lenti. Una query di 200 ms eseguita mille volte al minuto può avere più importanza di un report mensile di 10 secondi.
- Leggi il piano. Scopri cosa sta realmente facendo il database.
- Cambia una cosa. Aggiungi un indice o riscrivi parte della query.
- Misurare nuovamente, su dati realistici. Un piano su 100 righe di prova dice molto poco su un milione.
- Guardalo nel tempo. I piani possono cambiare man mano che i dati crescono e le statistiche cambiano.
Leggere un piano di esecuzione
Un piano di esecuzione è la strategia passo passo del database per l’esecuzione di una query, inclusi gli indici utilizzati e l’ordine in cui unisce le tabelle. Prefisso una query per vederla:
| Banca dati | Comando |
|---|---|
| SQLite | SPIEGARE QUERY PIANO SELEZIONA... |
| PostgreSQL | SPIEGARE ANALIZZA SELEZIONA... |
| MySQL | EXPLAIN SELECT ... o EXPLAIN ANALYZE |
| SQLServer | Piano di esecuzione effettivo in SSMS |
Tieni presente che EXPLAIN ANALYZE in PostgreSQL esegue effettivamente la query, quindi fai attenzione quando lo usi con UPDATE o DELETE. Ecco cosa cercare per primo:
| Vedi | Significato | Di solito |
|---|---|---|
| Scansione o Scansione sequenziale | Legge l’intera tabella | Una preoccupazione sui grandi tavoli |
| Cerca o Cerca nell’indice | Passa alle righe corrispondenti | Bene |
| Indice di copertura | Risposta solo dall’indice | Molto buono |
| Temp B-tree o Ordina | Lavoro di smistamento extra | Vale la pena controllare |
Una scansione completa non è automaticamente negativa. Se una query richiede gran parte di una tabella piccola, la scansione può essere la scelta più rapida. In PostgreSQL EXPLAIN ANALYZE, confronta anche le righe stimate con le righe effettive. Una grande differenza spesso significa che le statistiche della tabella non sono aggiornate.
Aggiungi l’indice giusto
Questa query trova gli ordini di un cliente:
SELECT id, total FROM orders WHERE customer_id = 123;
Prima di aggiungere un indice, il piano prevedeva una scansione completa:
SCAN orders
-- 20 rows in 73 ms
Quindi abbiamo aggiunto un indice nella colonna del filtro:
CREATE INDEX idx_orders_customer ON orders(customer_id);
SEARCH orders USING INDEX idx_orders_customer (customer_id=?)
-- 20 rows in 0.01 ms
Le stesse 20 righe tornavano migliaia di volte più velocemente, perché il database non leggeva più un milione di righe per trovarle. Nessun’altra tecnica in questo articolo ci ha dato una vittoria più grande. Buoni primi candidati sono le colonne utilizzate in WHERE, JOIN e ORDER BY, soprattutto quando restringono i risultati a poche righe. La nostra guida agli indici SQL spiega come funzionano.
Non racchiudere le colonne indicizzate nelle funzioni
Ecco due modi per trovare gli ordini di marzo, con un indice su created:
-- Function on the column
SELECT id FROM orders WHERE substr(created, 1, 7) = '2026-03';
-- SCAN orders USING COVERING INDEX idx_orders_created
-- 84,961 rows in 96 ms
-- Plain range
SELECT id FROM orders
WHERE created >= '2026-03-01' AND created < '2026-04-01';
-- SEARCH orders USING COVERING INDEX idx_orders_created (created>? AND created<?)
-- 84,961 rows in 33 ms
Entrambi restituiscono righe identiche, ma la prima ha impiegato circa tre volte il tempo. Il piano mostra il motivo: con una funzione sulla colonna, il database non può saltare a una posizione nell’indice ordinato, quindi deve SCAN attraverso ogni voce. La versione della gamma può SEARCH direttamente al posto giusto. Una condizione che può utilizzare un indice in questo modo è chiamata sargable.
Lo stesso problema si presenta con questi modelli comuni:
| Evitare | Preferisco |
|---|---|
DOVE ANNO(creato) = 2026 |
DOVE creato >= '2026-01-01' E creato < '2027-01-01' |
DOVE prezzo * 2 > 100 |
DOVE prezzo > 50 |
DOVE LOWER(e-mail) = 'a@b.com' |
Un indice basato su funzioni o regole di confronto senza distinzione tra maiuscole e minuscole |
DOVE nome COME '%99' |
Una ricerca del prefisso o una ricerca del testo completo |
Abbiamo controllato anche quest’ultimo. Viene prodotto un carattere jolly iniziale SCAN customers, poiché un indice ordinato a partire dalla prima lettera non può aiutare con una corrispondenza alla fine.
Indici compositi
Quando una query viene filtrata su due colonne, un indice a colonna singola svolge solo parte del lavoro:
SELECT id FROM orders
WHERE status = 'paid'
AND created >= '2026-03-01' AND created < '2026-03-08';
-- Using only the created index: 6,198 rows in 19 ms
CREATE INDEX idx_status_created ON orders(status, created);
-- SEARCH orders USING COVERING INDEX idx_status_created
-- (status=? AND created>? AND created<?)
-- 6,198 rows in 2.2 ms
L’ordine delle colonne è molto importante. Un indice su (status, created) funziona come un elenco telefonico ordinato per cognome e poi per nome. Puoi cercare solo il cognome, ma non solo il nome. La nostra query su status da sola potrebbe utilizzare questo indice. Il filtraggio solo su created in genere non è in grado di utilizzarlo in modo efficiente. Come regola generale, metti prima le colonne testate con l’uguaglianza e dopo di esse le colonne con intervallo.
Quel piano dice anche indice di copertura, il che significa che l’indice conteneva tutto ciò di cui la query aveva bisogno, quindi non era necessario leggere la tabella stessa. La selezione solo delle colonne necessarie rende ciò più probabile, il che è un altro motivo per evitarlo SELECT *.
Impaginazione più veloce
L’utilizzo di OFFSET per le pagine profonde è un costo nascosto, poiché il database deve ancora superare tutte le righe saltate:
-- Page deep into the results
SELECT id FROM orders ORDER BY id LIMIT 20 OFFSET 500000;
-- 8.1 ms
-- Keyset pagination: continue from the last id seen
SELECT id FROM orders WHERE id > 500000 ORDER BY id LIMIT 20;
-- 0.01 ms
L’impaginazione dei keyset rimane veloce, non importa quanto vai in profondità. Inoltre, evita che le righe si spostino tra le pagine quando vengono aggiunti nuovi dati. Il compromesso è che gli utenti non possono passare direttamente alla pagina 5.000, il che raramente è una reale necessità. Nelle tabelle con righe larghe o con più righe da saltare, il divario può essere molto maggiore rispetto al nostro test.
Suggerimento: I nostri tempi misurano una query alla volta su un computer inattivo. In condizioni di carico reale, una query inefficiente provoca più danni, perché mantiene anche i blocchi e compete con tutto il resto per memoria, disco e CPU.
Scrivi query più snelle
- Seleziona solo le colonne che ti servono.
SELECT *invia più dati e può impedire a un indice di coprire la query. - Filtra in anticipo. Utilizza
WHEREper rimuovere le righe prima del raggruppamento eHAVINGsolo per le condizioni sugli aggregati. Vedere SQL GROUP BY e HAVING. - Utilizza LIMIT quando ti servono solo poche righe.
- Preferisci UNION ALL quando non è necessario rimuovere i duplicati. Il nostro test UNION vs UNION ALL mostra cosa si risparmia.
- Evita DISTINCT non necessari. Spesso nasconde un join che produce duplicati.
- Utilizza EXISTS per i controlli di esistenza. Può arrestarsi alla prima corrispondenza, come spiega la nostra guida alle sottoquery SQL.
- Cerca N+1 query. Una query per riga in un ciclo, solitamente causata dal codice dell’applicazione, è spesso il vero problema. Una singola query di unione o batch può sostituire centinaia di viaggi di andata e ritorno.
Si unisce
Indicizza le colonne a cui ti iscrivi. Abbiamo eseguito un report sulla spesa totale per i clienti in una città e il piano era esattamente quello che speravi:
SELECT c.name, SUM(o.total)
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE c.city = 'Goa'
GROUP BY c.id;
-- SCAN c
-- SEARCH o USING COVERING INDEX idx_cover (customer_id=?)
-- 9,900 rows in 34 ms
Esegue la scansione dei clienti, quindi utilizza un indice per trovare gli ordini di ciascuno. Senza un indice su orders.customer_id, il database dovrebbe svolgere molto più lavoro per ciascun cliente. Un indice sulla chiave esterna è una delle aggiunte più comuni e preziose che puoi apportare. Per rinfrescare la tua comprensione delle nozioni di base, consulta Spiegazione dei join SQL.
Quando gli indici ti costano
Gli indici non sono gratuiti, quindi non aggiungerne uno per ogni colonna:
- Scritture più lente. Ogni inserimento, aggiornamento ed eliminazione deve aggiornare anche ogni indice.
- Più spazio di archiviazione e memoria. Gli indici possono diventare grandi su tabelle di grandi dimensioni.
- Poco vantaggio sulle colonne non selettive. Un indice su una colonna true o false potrebbe non essere d’aiuto se la maggior parte delle righe corrisponde.
- Ridondanza. Un indice su
(a)spesso non è necessario se ne hai già uno su(a, b).
Controlla periodicamente le statistiche di utilizzo e rimuovi gli indici che non vengono utilizzati. E mantieni aggiornate le statistiche con ANALYZE o l’equivalente del tuo database, perché l’ottimizzatore fa affidamento su di loro per scegliere un buon piano. A volte la parte lenta è la contesa piuttosto che la domanda. Le transazioni di lunga durata che mantengono i blocchi possono far sembrare lenta una query veloce.
Errori comuni
- Ottimizzare senza misurare. Molte ipotesi attendibili si rivelano errate.
- Test con dati di piccole dimensioni. I piani e le tempistiche cambiano drasticamente man mano che le tabelle crescono.
- Affidarsi a un suggerimento per un altro database. I consigli per un motore potrebbero non essere applicabili a un altro.
- Aggiunta di indici alla cieca. Ognuno di essi rallenta le scritture.
- Ignorando l’effetto della prima esecuzione. Le cache rendono la seconda esecuzione più veloce, quindi esegui più volte.
Considerazioni finali
La maggior parte delle query lente ha cause ordinarie: un indice mancante, una funzione su una colonna o troppi dati recuperati. Prendi la tua query reale più lenta questa settimana, esegui EXPLAIN su di essa e cerca una scansione completa su una tabella di grandi dimensioni. Correggi una cosa, misura di nuovo e prendi nota di ciò che hai modificato.
Domande frequenti
Cos’è l’ottimizzazione delle query SQL?
È il processo che fa sì che le query utilizzino meno tempo e meno risorse, in genere esaminando i piani di esecuzione, aggiungendo indici adeguati e riscrivendo query inefficienti.
Cos’è un piano di esecuzione?
È la strategia scelta dal database per eseguire una query, incluso il modo in cui legge le tabelle, quali indici utilizza e in quale ordine si unisce.
Come trovo le query lente?
Abilita il registro delle query lente del tuo database o utilizza strumenti come pg_stat_statements in PostgreSQL. Quindi concentrati sulle query con il tempo totale più elevato.
L’aggiunta di un indice rende sempre le query più veloci?
No. Aiuta le letture selettive ma rallenta le scritture e utilizza lo spazio di archiviazione. L’ottimizzatore potrebbe anche ignorarlo se la maggior parte delle righe soddisfa la condizione.
SELECT * è dannoso per le prestazioni?
Può essere. Trasferisce dati non necessari e può impedire a un indice di coprire la query. Di solito è meglio elencare solo le colonne che ti servono.
Continua a imparare
Hai bisogno di aiuto con test, WordPress o SEO?
Aiutiamo i team a fornire software migliore e a far crescere i loro siti. Raccontaci a cosa stai lavorando e ti suggeriremo chiaramente il prossimo passo.
Altre guide su test, intelligenza artificiale e sviluppo moderno
Ti è piaciuto questo articolo? Scegli un argomento e passa direttamente ai nostri tutorial più recenti.
Spiegazione delle allucinazioni dell'intelligenza artificiale: perché i modelli linguistici inventano le cose e come ridurle
Spiegazione delle allucinazioni dell'intelligenza artificiale: cosa sono, perché i modelli linguistici le producono, tipi comuni, come ridurle con b…
Leggi l'articolo →DevOpsSpiegazione delle variabili d'ambiente: file .env, segreti e configurazione eseguiti correttamente
Variabili d'ambiente spiegate con esempi Python che abbiamo eseguito: lettura e impostazione, perché ogni valore è una stringa, file .env, ereditari…
Leggi l'articolo →Sviluppo softwareSpiegazione della limitazione della velocità API: algoritmi, intestazioni e gestione degli errori 429
La limitazione della velocità dell'API spiegata con una simulazione che abbiamo eseguito: bucket di token, finestra fissa, finestra scorrevole, risp…
Leggi l'articolo →Automazione dei testDerisione in Python: unittest.mock, patch e gli errori che fanno mentire i test
Mocking in Python con unittest.mock, testato: Mock e return_value, side_effect, patch dove viene utilizzato un nome, specifiche e autospec, date moc…
Leggi l'articolo →Test del softwareSpiegazione della copertura del codice: copertura di linea o di filiale e cosa significa realmente il 100%.
Copertura del codice spiegata con una vera esecuzione di cover.py: copertura di linea, ramo e condizione, lettura di un rapporto, perché la copertur…
Leggi l'articolo →Database e SQLDELETE vs TRUNCATE vs DROP in SQL: differenze, rollback e quando utilizzarli
DELETE vs TRUNCATE vs DROP spiegato con SQL testato: cosa rimuove, rollback, trigger, reimpostazione dell'identità, velocità e come differiscono Pos…
Leggi l'articolo →Database e SQLSpiegazione delle visualizzazioni SQL: crea, utilizza ed evita le insidie, con esempi
Viste SQL spiegate con le query che abbiamo eseguito: creazione di una vista, risultati in tempo reale, nascondere colonne, prestazioni, viste aggio…
Leggi l'articolo →Sviluppo softwareInterfaccia TypeScript e tipo: differenze reali, testate con il compilatore
Interfaccia TypeScript vs tipo, verificata con il vero compilatore: cosa possono fare entrambi, cosa può fare solo uno, fusione delle dichiarazioni,…
Leggi l'articolo →Sviluppo softwareSpiegazione di Python *args e **kwargs: funzioni flessibili con esempi chiari
Python *args e **kwargs spiegati con il codice che abbiamo eseguito: compressione e decompressione, ordine dei parametri, argomenti solo per parole …
Leggi l'articolo →Sviluppo softwareFunzioni Lambda Python: sintassi, usi reali e quando non usarli
Funzioni lambda Python spiegate con il codice che abbiamo eseguito: sintassi, ordinamento con chiave, mappa e filtro, chiusure, trap dell'associazio…
Leggi l'articolo →Intelligenza artificialeCosa sono gli agenti AI? Come funzionano, con un semplice ciclo di agenti che puoi eseguire
Cosa sono gli agenti IA? Una semplice guida al ciclo dell'agente, agli strumenti, alla memoria e ai guardrail, con un piccolo ciclo Python che abbia…
Leggi l'articolo →DevOpsMonitoraggio vs osservabilità: la vera differenza, con log, metriche e tracce
Spiegazione del monitoraggio e dell'osservabilità: errori noti e sconosciuti, log, metriche e tracce, una procedura dettagliata sugli incidenti di p…
Leggi l'articolo →DevOpsVolumi Docker e montaggi di associazione: mantenere i dati del contenitore nel modo giusto
Volumi Docker e montaggi di collegamento spiegati: perché i dati del contenitore scompaiono, volumi denominati, montaggi di collegamento, tmpfs, ese…
Leggi l'articolo →Automazione dei testTDD vs BDD: differenze chiave, una caratteristica costruita in entrambi i modi e quando utilizzarli ciascuno
TDD vs BDD spiegato con una funzione di sconto costruita in entrambi i modi: refactoring rosso-verde con il codice che abbiamo eseguito, scenari "Da…
Leggi l'articolo →Test del softwareTest di accettazione da parte dell'utente (UAT): processo, lista di controllo ed esempi reali
Spiegazione del test di accettazione dell'utente: chi lo esegue, in cosa differisce dal QA, un processo in sette fasi, un modello di test case UAT, …
Leggi l'articolo →Database e SQLSpiegazione dei valori SQL NULL: IS NULL, COALESCE, NULLIF e le trappole da evitare
Valori SQL NULL spiegati con le query che abbiamo eseguito: logica a tre valori, IS NULL, COALESCE, NULLIF, come aggregazioni, join e ordinamento tr…
Leggi l'articolo →Database e SQLIstruzione SQL CASE WHEN: sintassi, esempi reali ed errori comuni
SQL CASE WHEN spiegato con le query che abbiamo eseguito: moduli semplici e cercati, ordine delle condizioni, pivot con SUM, trucchi COUNT, ordiname…
Leggi l'articolo →Sviluppo softwareCos'è il JWT? Token Web JSON spiegati con un esempio funzionante
Cos'è il JWT? Spiegazione dei token Web JSON: le tre parti, un token creato e manomesso nel codice che abbiamo eseguito, JWT e sessioni, archiviazio…
Leggi l'articolo →Sviluppo softwareSpiegazione del loop di eventi JavaScript: stack di chiamate, microtask e timer con esempi
Il ciclo di eventi JavaScript spiegato con il codice che abbiamo eseguito: stack di chiamate, code di attività e microtask, perché setTimeout 0 non …
Leggi l'articolo →Sviluppo softwareSpiegazione dei generatori Python e del rendimento: risparmia memoria con esempi reali
Generatori Python e rendimento spiegati con il codice che abbiamo eseguito: come vengono messi in pausa e ripresi, misurato il risparmio di memoria,…
Leggi l'articolo →Intelligenza artificialeCosa sono gli incorporamenti? I vettori e la somiglianza del coseno spiegati semplicemente
Cosa sono gli incorporamenti? Una guida in linguaggio semplice ai vettori, alla similarità del coseno e alla ricerca semantica, con un piccolo esemp…
Leggi l'articolo →DevOpsDistribuzione blu-verde rispetto a quella canaria: differenze, compromessi e come scegliere
Spiegazione della distribuzione blu-verde e canary: come funziona ciascuna, costi, rischi e rollback, bucket utente stabile che abbiamo testato, mig…
Leggi l'articolo →Automazione dei testTest instabili: quali sono le cause e come risolverli definitivamente
Test instabili spiegati con demo che abbiamo eseguito: le cause principali, come confermarne e diagnosticarne uno, soluzioni adeguate per tempistich…
Leggi l'articolo →Test del softwareTest di carico e test di stress: differenze, metriche ed esempi k6
Spiegazione del test di carico e dello stress test: cosa dimostra ciascuno, test di picco e di assorbimento, le metriche che contano, perché le medi…
Leggi l'articolo →Test del softwareCaso di test e scenario di test: differenze chiave, esempi e quando utilizzarli
Spiegazione del caso di test e dello scenario di test: definizioni chiare, una tabella affiancata, una funzione di reimpostazione della password pre…
Leggi l'articolo →Database e SQLOttimizzazione delle query SQL: leggi i piani di esecuzione e correggi query lente, con tempistiche
Ottimizzazione delle query SQL testata su un milione di righe: leggi i piani di esecuzione, aggiungi gli indici corretti, correggi filtri non aggior…
Leggi l'articolo →Database e SQLSQL UNION vs UNION ALL: differenze, esempi e prestazioni testate
SQL UNION vs UNION ALL spiegati con le query che abbiamo eseguito: duplicati, regole seguite da entrambi, un vero bug di dati, prestazioni misurate,…
Leggi l'articolo →Sviluppo softwareTutorial sull'API di recupero JavaScript: GET, POST, gestione degli errori e timeout
Un tutorial sull'API JavaScript Fetch testato su un server locale: GET e POST, perché 404 non genera eccezioni, un helper riutilizzabile, stringhe d…
Leggi l'articolo →Sviluppo softwareGestione dei file Python: lettura, scrittura, CSV, JSON e pathlib con esempi
Gestione dei file Python spiegata con il codice che abbiamo eseguito: modalità aperte, con, lettura di file di grandi dimensioni, CSV, JSON, pathlib…
Leggi l'articolo →Sviluppo softwareGit merge vs rebase: differenze, esempi e quando usare ciascuno
Git merge vs rebase spiegato con output reale del log, una tabella di confronto, la regola d'oro del rebase e un flusso di lavoro sicuro per i team.
Leggi l'articolo →Sviluppo softwareList comprehension in Python: sintassi, esempi e quando evitarla
List comprehension in Python spiegata: sintassi, filtri, if-else, cicli annidati, tempi misurati e i casi in cui un normale ciclo for è meglio.
Leggi l'articolo →Database e SQLSQL JOIN spiegati con esempi (Inner, Left, Right, Full)
Capisci i JOIN SQL passo dopo passo con tabelle e query di esempio: INNER, LEFT, RIGHT, FULL, CROSS e SELF, con errori comuni e consigli sulle prest…
Leggi l'articolo →