Performance

Query lente: misurarle prima di aggiungere un indice

Una query lenta si misura con EXPLAIN QUERY PLAN e cinque esecuzioni. Su 200.000 righe SQLite la lettura completa costa 13,2 ms, l'indice 0,29 ms.

In sintesi

  • Prima di creare un indice leggi EXPLAIN QUERY PLAN: SCAN è lettura completa, SEARCH è indice.
  • Il numero utile è la mediana di cinque esecuzioni, non la prima che hai visto.
  • Su 200.000 righe SQLite l'indice su una colonna porta la query da 13,2 ms a 0,29 ms.
  • Un LIKE con wildcard iniziale non usa l'indice: 52,49 ms anche con l'indice presente.

Una query lenta si misura prima di toccarla: EXPLAIN QUERY PLAN dice se il database legge tutta la tabella, e cinque esecuzioni ripetute danno un tempo mediano su cui ragionare. Su 200.000 righe SQLite la stessa condizione passa da 13,2 ms a 0,29 ms quando l'indice giusto esiste.

Tre numeri prima di un indice

Cosa misuriDove lo leggiCosa ti dice
Piano di esecuzioneEXPLAIN QUERY PLANSCAN è lettura completa, SEARCH è indice
Tempo medianocinque esecuzioni, medianail numero che l'utente sente
Righe letteEXPLAIN QUERY PLAN, campo detailse il filtro è davvero selettivo

Il comando

sqlite3 catalogue.db <<'SQL'
.timer on
EXPLAIN QUERY PLAN SELECT id FROM components WHERE library = 'lib-1000';
SELECT count(*) FROM components WHERE library = 'lib-1000';
CREATE INDEX idx_library ON components (library);
SELECT count(*) FROM components WHERE library = 'lib-1000';
SQL

Il comando richiede il client sqlite3 e un file di prova con la tabella components. .timer on stampa la durata di ogni istruzione: la prima SELECT legge tutta la tabella, la seconda usa l'indice creato nel frattempo. La nostra misura ha usato lo stesso schema con node e sqlite3, perché serviva la mediana di più esecuzioni, non una sola.

Quando l'indice non aiuta

Un indice non è una bacchetta: risolve l'uguaglianza e gli intervalli su una colonna, non un LIKE '%chart%'. La wildcard iniziale obbliga il motore a leggere ogni riga, ed è il motivo dei 52,49 ms misurati. Non aiuta nemmeno su una colonna con due valori distinti: metà tabella resta metà tabella. In quei casi si cambia il modello, si aggiunge una tabella di ricerca o si accetta il costo.

Il limite da dichiarare

La tabella era in memoria, senza I/O su disco e senza concorrenza: i millisecondi assoluti sono più bassi di quelli di un server vero. Il rapporto tra lettura completa e indice è la parte da portare via, non il valore assoluto. Un database in produzione aggiunge cache, lock e query concorrenti, e un indice in più rallenta ogni scrittura.

Dove continuare

Il catalogo EneUI indicizza 111.985 componenti, 1.556 blocchi e il record spotlight card, una scheda che costa una query per ogni richiesta. Per capire quanto pesa il tempo di risposta del server leggi TTFB: cosa misura, per la cache che evita la query Cache-Control spiegato, e per il confronto tra architetture framework frontend a confronto.

Domande frequenti

Come capisco se una query usa un indice?

Leggi EXPLAIN QUERY PLAN: SEARCH con USING INDEX significa indice usato, SCAN significa lettura completa della tabella. Il piano non mostra i tempi, solo il percorso.

Quante esecuzioni servono per un tempo affidabile?

Cinque, con la mediana come risultato. La prima esecuzione paga la cache fredda e il caricamento del motore, quindi è sempre la più lenta e la più fuorviante.

Perché LIKE '%chart%' resta lento con l'indice?

Perché la wildcard iniziale impedisce di usare l'ordine dell'indice: il motore deve leggere ogni riga e confrontare il testo. Un indice funziona quando il prefisso della stringa è noto.

Un indice rallenta le scritture?

Sì. Ogni INSERT, UPDATE e DELETE aggiorna anche l'indice, quindi il costo si sposta sulle scritture. Vale la pena quando le letture sono molte di più e il filtro è selettivo.

Il tempo misurato in locale vale in produzione?

No. Cache, disco, concorrenza e dimensione reale dei dati cambiano il numero. Misura il piano in locale e il tempo sul database di produzione, con gli stessi parametri.