Il lavoro sulla performance del database comincia quasi sempre con qualcuno che propone un’istanza più grande, e finisce quasi sempre con la scoperta che una singola query eseguiva una scansione sequenziale su 4 milioni di righe a ogni caricamento di pagina. L’hardware non è mai stato il vincolo. Lo era il piano di esecuzione.
Lo schema si ripete con una regolarità tale da meritare il rango di ipotesi predefinita. Quando un’applicazione è lenta e il database è occupato, la causa sta quasi sempre in un numero ristretto di query specifiche e non in una carenza generale di capacità. Salire di taglia nasconde il problema esattamente per il tempo che serve alla tabella per tornare a crescere.
Misura prima di cambiare qualsiasi cosa. Ottimizzare una query indovinata a occhio è il modo in cui i team passano una settimana ad aggiungere indici che rallentano le scritture senza rendere le letture più veloci. Ogni database sa dirti quali istruzioni consumano più tempo totale. Parti da lì, correggi la prima, misura di nuovo. Due o tre iterazioni di questo ciclo di solito chiudono l’incidente.
La performance del database inizia dal trovare la query
Il tempo totale conta più del caso peggiore. Una query che impiega due secondi e viene eseguita due volte al giorno è irrilevante. Una query che impiega quaranta millisecondi e viene eseguita 8.000 volte al minuto è il tuo problema, e non comparirà mai in uno slow query log con soglia di un secondo.
In Postgres l’estensione pg_stat_statements aggrega esattamente questo: chiamate, tempo totale e tempo medio per istruzione normalizzata. Ordina per tempo totale e il colpevole di solito sta nelle prime tre righe. MySQL offre un’aggregazione analoga attraverso il performance schema.
Due cose vanno verificate prima di concludere che la colpa sia della query stessa. È lenta ogni volta, oppure solo in certe ore, il che indica contesa di risorse più che un problema di piano? Ed è lenta anche isolata, oppure solo sotto concorrenza, il che indica lock o limiti di connessione?
Leggi il piano invece di tirare a indovinare
Una volta individuata l’istruzione, chiedi al database come intende eseguirla. Postgres lo espone tramite EXPLAIN
, e la variante importante è EXPLAIN ANALYZE, che esegue davvero la query e riporta tempi reali invece di stime.
Tre elementi di quell’output portano quasi tutto il segnale.
Scansione sequenziale su una tabella grande. Il database sta leggendo ogni riga. Su una tabella piccola è corretto e veloce. Su una grande significa che non esiste un indice utilizzabile per la condizione che hai scritto, oppure che il planner ha deciso che l’indice non valeva la pena.
Un divario ampio fra righe stimate e righe effettive. Il planner sceglie la strategia in base alle statistiche, quindi quando la sua stima sbaglia di ordini di grandezza sceglie male per motivi che non hanno nulla a che fare con la tua query. Le statistiche obsolete sono una causa frequente e facile da correggere.
Tempo concentrato in un solo nodo. I piani sono alberi, e la correzione appartiene al nodo che ha consumato il tempo. Ottimizzare qualsiasi altra cosa non cambia niente.
L’istinto di aggiungere un indice appena si vede una scansione sequenziale è spesso giusto, e vale comunque trenta secondi di attesa, perché il motivo per cui l’indice non viene usato a volte conta più della sua assenza.
Perché gli indici non aiutano
Un indice che esiste non è ancora un indice che viene usato.
La condizione non è sargable. Avvolgere una colonna in una funzione, o applicarle un calcolo, in genere impedisce l’uso dell’indice su quella colonna, perché l’indice memorizza i valori della colonna e non quelli trasformati. Riscrivere la condizione lasciando la colonna nuda di solito ripristina l’uso dell’indice.
L’ordine delle colonne in un indice composito è sbagliato. Un indice composito serve le query che usano le sue colonne iniziali. Un indice su una colonna e poi su un’altra non aiuta una query che filtra solo sulla seconda, ed è un inganno in cui si cade di continuo.
Il planner ritiene che una scansione costi meno. Se una query restituisce una frazione ampia della tabella, leggerla in sequenza è davvero più veloce che saltare attraverso un indice. Questo è comportamento corretto, e la correzione consiste nel restituire meno righe.
Le statistiche sono obsolete. Dopo un caricamento massivo o una cancellazione importante, l’immagine dei dati che il planner possiede può essere gravemente sbagliata finché le statistiche non vengono aggiornate.
E ogni indice ha un costo. Le scritture devono mantenerlo, e occupa memoria che altrimenti terrebbe i dati in cache. Una tabella con quindici indici di solito ne ha diversi che non servono a nessuno, e ciascuno rende ogni inserimento più lento.
Il problema N+1 resta la singola causa più frequente
La lentezza applicativa nasce da qui più che da qualsiasi problema di piano, e non si presenta mai come query lenta perché ogni singola query è veloce.
La forma è familiare. Recuperi un elenco di 100 record, poi ci iteri sopra e per ognuno recuperi i dati collegati. Il risultato sono 101 andate e ritorni dove ne sarebbero bastati uno o due. Ogni query torna in tre millisecondi e la pagina impiega comunque mezzo secondo, perché il costo sta nei viaggi di rete e non nel lavoro.
Gli object relational mapper rendono facile scrivere questo per sbaglio, dato che l’accesso ai dati collegati sembra la lettura di una proprietà e non una chiamata al database. La correzione consiste nel caricare i dati collegati in un’unica query insieme all’insieme padre, cosa che ogni ORM maturo supporta e che la maggior parte non fa in modo predefinito.
Rilevarlo è semplice: conta le query per richiesta. Se una pagina emette un numero di query proporzionale al numero di elementi mostrati, l’hai trovato. È anche la verifica di maggior valore quando un’applicazione è lenta sull’edge, come spiega la nostra guida a Cloudflare Hyperdrive , perché i viaggi di rete costano molto di più quando la distanza aumenta.
Connessioni e contesa
Due problemi che sembrano lentezza e non lo sono.
Esaurimento delle connessioni. Ogni database ha un tetto di connessioni simultanee, e ognuna costa memoria. Quando un’applicazione ne apre più di quante il pool ne consenta, le richieste si mettono in coda in attesa di una connessione e l’applicazione appare lenta mentre il database resta inattivo. Il sintomo è latenza applicativa alta con CPU del database bassa, e la correzione si chiama pooling, non macchina più grande.
Contesa sui lock. Una transazione lunga che tiene un lock blocca tutto ciò che sta dietro. La causa abituale è una transazione lasciata aperta durante lavoro che del database non ha bisogno, per esempio una chiamata HTTP verso un altro servizio. Tieni le transazioni corte e limitate al lavoro sul database vero e proprio.
Entrambi i casi vanno esclusi presto, perché entrambi si leggono facilmente come un problema di query e nessuno dei due si risolve con un indice.
Cosa fare, e in quale ordine
Trova le istruzioni che consumano più tempo totale. Esegui EXPLAIN ANALYZE sulla peggiore e leggi dove il tempo se ne va davvero. Controlla il numero di query per richiesta per escludere l’N+1 prima di ottimizzare qualsiasi cosa. Aggiorna le statistiche prima di aggiungere un indice, perché a volte la correzione è tutta lì. Poi aggiungi l’indice più stretto che serva la condizione, e misura di nuovo.
Mecanik lo fa come parte del nostro lavoro di sviluppo software , e l’esito è quasi sempre lo stesso: due o tre query erano responsabili, la correzione era piccola, e l’istanza più grande che nessuno aveva comprato non è mai servita.
Post correlati: Versionamento delle API: quando rompere e come non farlo , Come sviluppare una web app nel 2026 - Guida UK , Archiviazione delle password: cosa usare nel 2026 , Sviluppo software su misura nel Regno Unito .
Domande frequenti
Come trovo la query che sta rallentando la mia applicazione? Ordina per tempo totale invece che per caso peggiore. Una query da quaranta millisecondi eseguita 8.000 volte al minuto costa molto più di una query da due secondi eseguita due volte al giorno, e non comparirà mai in uno slow query log con soglia di un secondo. In Postgres pg_stat_statements aggrega chiamate e tempo totale per istruzione; MySQL offre lo stesso attraverso il performance schema.
Cosa devo guardare nell’output di EXPLAIN ANALYZE? Tre elementi portano quasi tutto il segnale: una scansione sequenziale su una tabella grande, cioè nessun indice utilizzabile; un divario ampio fra righe stimate e righe effettive, cioè un planner che lavora su statistiche sbagliate; e tempo concentrato in un solo nodo dell’albero del piano, che è il punto a cui appartiene la correzione. Ottimizzare qualsiasi altro nodo non cambia niente.
Perché il mio indice non viene usato? Di solito per uno di quattro motivi. La condizione avvolge la colonna in una funzione o in un calcolo, quindi l’indice non corrisponde più. L’indice composito ha le colonne in un ordine che non serve la query. La query restituisce una frazione della tabella abbastanza ampia da rendere davvero meno costosa una scansione. Oppure le statistiche sono obsolete dopo un caricamento massivo o una cancellazione.
Che cos’è un problema di query N+1? Recuperare un elenco di record e poi emettere una query separata per i dati collegati di ciascuno, producendo 101 andate e ritorni dove ne sarebbero bastati uno o due. Non compare in nessuno slow query log perché ogni query è veloce; il costo sta nei viaggi di rete. Lo rilevi contando le query per richiesta e cercando un numero proporzionale agli elementi mostrati.
Un server di database più grande risolve le query lente? Raramente, e solo per un po’. Quando un’applicazione è lenta e il database è occupato, la causa sta quasi sempre in un numero ristretto di query specifiche più che in una carenza di capacità, quindi salire di taglia nasconde il problema finché la tabella non torna a crescere. Latenza applicativa alta con CPU del database bassa di solito indica invece un pool di connessioni esaurito, e più hardware non lo risolve.
Commenti