Un indice parziale copre solo alcune righe
Un indice normale ha una voce per ogni riga della tabella. Un indice parziale ha voci solo per le righe che soddisfano una clausola WHERE che indichi quando lo crei. Indice più piccolo, meno pagine da percorrere, meno lavoro a ogni insert e update che non tocca la porzione indicizzata.
La sintassi è una normale CREATE INDEX con un WHERE in coda:
idx_orders_pending contiene voci solo per le righe in cui status = 'pending'. Gli ordini spediti, annullati e rimborsati non ci sono proprio. Se il 95% della tua tabella orders è storico e interroghi soprattutto quelli aperti, hai un indice 20 volte più piccolo con la stessa velocità di query.
Quando il planner lo usa davvero
Un indice parziale è utilizzabile solo quando SQLite riesce a dimostrare che la tua query si limita alle stesse righe coperte dall'indice. Il modo più pulito è ripetere nella query la clausola WHERE dell'indice:
Il piano dovrebbe citare USING INDEX idx_orders_pending. Togli status = 'pending' dalla query e il planner torna a una scansione completa della tabella: non ha modo di sapere che la query resta dentro il sottoinsieme indicizzato.
La regola pratica: il WHERE della query deve implicare il WHERE dell'indice. L'uguaglianza sulla stessa colonna e sullo stesso valore è il caso sicuro e ovvio. Disuguaglianze e OR complicano le cose; verifica con EXPLAIN QUERY PLAN.
Perché ne vale la pena: i tre vantaggi
Tre motivi concreti per cui gli indici parziali ripagano:
- Più piccoli su disco. Vengono salvate solo le righe che corrispondono. Con un carico in cui "l'1% della tabella è caldo", l'indice è circa l'1% di uno completo.
- Scritture più economiche. Insert e update toccano l'indice solo quando la riga soddisfa il filtro. Un insert con
status = 'shipped'nella tabella qui sopra non tocca affattoidx_orders_pending. - Stessa velocità di ricerca. Una ricerca in un B-tree è logaritmica rispetto alla dimensione dell'indice. Indice più piccolo, ricerche un po' più veloci, ma il guadagno maggiore sta in tutto il resto: meno cache miss, meno I/O.
Se una colonna è molto sbilanciata, con la maggior parte delle righe su un valore e un interesse solo per i valori rari, sei davanti al caso da manuale per un indice parziale.
Indici unique parziali (la funzione vincente)
I normali vincoli UNIQUE valgono per ogni riga. È un problema non appena introduci il soft delete:
-- Fallisce: ci sono due righe con email = 'a@x.com', anche se una è eliminata.
CREATE UNIQUE INDEX idx_users_email ON users(email);
Un indice unique parziale ti permette di imporre l'unicità solo sulle righe che contano:
Tre righe, stessa email, nessuna violazione del vincolo: solo la riga con deleted_at IS NULL partecipa al controllo di unicità. Prova a inserire una seconda riga attiva con la stessa email e SQLite solleva UNIQUE constraint failed.
Questo schema si trova ovunque: un solo abbonamento attivo per cliente, un solo indirizzo principale per utente, una sola fattura aperta per ordine. Gli indici unique parziali lo esprimono direttamente.
Indicizzare tenendo conto dei NULL
NULL ha un rapporto strano con gli indici. Un obiettivo comune è "ignorare del tutto i NULL": per esempio hai una colonna external_id poco popolata, in cui la maggior parte delle righe è NULL ma quelle valorizzate devono essere uniche:
I due NULL convivono senza problemi; le righe EXT-001 e EXT-002 sono garantite uniche. L'indice è anche più piccolo, dato che le righe NULL non vengono salvate affatto, quindi le ricerche per external_id restano veloci anche quando la tabella cresce.
Cosa può usare il filtro
La clausola WHERE di un indice parziale è restrittiva. Può fare riferimento a:
- Colonne della tabella indicizzata.
- Costanti letterali.
- Un piccolo insieme di funzioni integrate deterministiche.
Non può fare riferimento a:
- Altre tabelle.
- Subquery.
- Funzioni non deterministiche come
random()oCURRENT_TIMESTAMP. - Parametri o variabili.
Ha senso: SQLite deve valutare il filtro a ogni insert e update di una riga, e il risultato deve essere stabile. Quindi questo funziona:
Ma WHERE created_at > date('now') no: date('now') cambia nel tempo, quindi l'insieme delle righe indicizzate si sposterebbe sotto i piedi di SQLite.
Un controllo di verifica
Quando aggiungi un indice parziale, fai tre controlli:
La query 1 dovrebbe usare idx_jobs_runnable. Le query 2 e 3 dovrebbero ripiegare su una scansione (o su un altro indice, se ne hai uno). Se il planner sceglie l'indice parziale per una query in cui non te lo aspettavi, rileggi il filtro: potrebbe essere più ampio di quanto pensi.
Quando non usarlo
Gli indici parziali sono uno strumento affilato. Motivi per lasciar perdere:
- Il filtro corrisponde a gran parte della tabella. Se "attivo" vale per il 90% delle righe, un indice parziale è solo un indice normale con qualche passaggio in più. Indicizza semplicemente la colonna.
- Le tue query non includono il filtro alla lettera. Se il tuo codice usa un ORM che costruisce
WHERE status IN (?, ?, ?)o calcola il filtro in modo dinamico, spesso il planner non riconosce la corrispondenza. Verifica conEXPLAIN QUERY PLAN, non darlo per scontato. - Il sottoinsieme caldo cambia nel tempo. Un indice parziale sugli "ordini degli ultimi 30 giorni" sembra allettante ma non si può esprimere: il filtro deve essere deterministico. Dovresti ricostruire l'indice o scegliere uno schema diverso (una tabella
recent_ordersseparata o un booleanoarchivedda aggiornare ogni notte).
Quando il filtro è stabile e corrisponde a una piccola porzione di una tabella grande, gli indici parziali sono tra le ottimizzazioni più redditizie che puoi fare in SQLite.
Prossimo passo: leggere i piani di query
Gran parte di questa pagina si è appoggiata a EXPLAIN QUERY PLAN per confermare che un indice venisse davvero usato. Questo strumento merita una pagina tutta sua: come leggerne l'output, cosa significano le parole chiave e come distinguere una bella ricerca su indice da una scansione completa nascosta. È il prossimo argomento.
Domande frequenti
Cos'è un indice parziale in SQLite?
Un indice parziale indicizza solo le righe che soddisfano una clausola WHERE indicata al momento della creazione. Scrivi CREATE INDEX name ON table(col) WHERE condition e SQLite salva voci solo per le righe in cui la condizione è vera. Indice più piccolo, scritture più veloci e la stessa velocità di ricerca per le query che rientrano nel filtro.
Quando conviene un indice parziale invece di uno completo?
Quando interroghi di continuo una piccola porzione di una tabella grande: ordini in attesa, utenti attivi, job non ancora elaborati. Indicizzare solo quella porzione mantiene l'indice minuscolo e permette alle scritture sulle altre righe di saltarlo del tutto. Se le tue query non includono la stessa condizione WHERE dell'indice, il planner non può usarlo.
Un indice parziale può garantire l'unicità?
Sì. CREATE UNIQUE INDEX ... WHERE ... impone l'unicità solo sulle righe che soddisfano il filtro. L'uso classico è 'un solo record attivo per utente': le righe eliminate in modo logico sono escluse, quindi puoi avere più voci eliminate con la stessa chiave ma una sola attiva.