Un indice, più colonne
Un indice composito, a volte chiamato indice multicolonna, è un unico indice costruito su due o più colonne. Lo crei elencando le colonne in ordine:
L'indice idx_orders_customer_status salva le voci ordinate prima per customer_id, poi per status all'interno di ogni cliente. Tutto sta in questo ordinamento: ogni altra cosa sugli indici compositi deriva da qui.
Il modello mentale: un elenco telefonico ordinato
Pensa a un vecchio elenco telefonico. Le voci sono ordinate per cognome e, a parità di cognome, per nome. È esattamente l'aspetto di un indice su (last_name, first_name).
Alcune ricerche costano poco, altre no:
- "Trova tutti quelli che si chiamano Patel": facile, i Patel stanno tutti vicini.
- "Trova Priya Patel": facile, salti ai Patel, poi scorri fino a Priya.
- "Trova tutte le persone di nome Priya": lento, devi scorrere ogni pagina. Le Priya sono sparse tra tutti i cognomi.
Un indice composito di SQLite funziona allo stesso modo. La prima colonna è la chiave di ordinamento principale; la seconda ordina solo le voci che condividono lo stesso valore nella prima colonna.
La regola del prefisso più a sinistra
SQLite può usare un indice composito per una query solo quando la clausola WHERE vincola un prefisso sinistro delle sue colonne. Per un indice su (a, b, c):
- Filtro su
a: usa l'indice. - Filtro su
aeb: usa l'indice. - Filtro su
a,bec: usa l'indice. - Filtro solo su
b, solo suc, oppure subec: l'indice non viene usato.
Puoi verificarlo direttamente con EXPLAIN QUERY PLAN:
Il primo piano riporta SEARCH events USING INDEX idx_events_user_kind_time. Il secondo ripiega su SCAN events: filtrare solo su kind salta la colonna iniziale user_id, quindi l'indice è inutile per quella query.
L'ordine delle colonne è una scelta di progetto
Visto che il prefisso sinistro conta, l'ordine in cui elenchi le colonne in CREATE INDEX è una scelta vera, non una questione di stile. Due regole pratiche:
- Metti per prima la colonna su cui filtri più spesso. È quella che rende l'indice utile per la gamma più ampia di query.
- Metti le colonne di uguaglianza prima di quelle di intervallo. SQLite può scendere nell'indice usando
=, poi scorrere un intervallo contiguo usando<,>oBETWEEN, ma solo sull'ultima colonna utilizzata.
Il piano mostra SEARCH sales USING INDEX idx_sales_region_time (region=? AND sold_at>?). SQLite salta direttamente a region = 'EU', poi avanza lungo l'intervallo di date. Inverti l'ordine delle colonne in (sold_at, region) e la stessa query deve scorrere tutte le righe dell'intervallo di date e ricontrollare region per ciascuna.
Composito contro diversi indici su singole colonne
Una domanda frequente: meglio creare un indice su (a, b) o due indici separati su a e b?
Per il filtro combinato l'indice composito è più veloce: SQLite va dritto alle voci (project_id, state) corrispondenti. Con due indici su singole colonne, SQLite di solito ne sceglie uno, lo usa per restringere le righe, poi ricontrolla l'altra colonna su ogni riga trovata. A volte riesce a intersecarli, ma l'indice composito è la risposta più pulita quando le colonne vengono interrogate insieme.
Se project_id e state vengono interrogate anche separatamente, potresti volerli entrambi: il composito per il filtro combinato, più un indice su state per le query che filtrano solo su quella colonna.
Covering index
Quando un indice include tutte le colonne di cui una query ha bisogno, sia quelle di filtro sia quelle selezionate, SQLite può rispondere alla query senza toccare affatto la tabella. È un covering index, e una query non può essere più veloce di così.
Il piano mostra USING COVERING INDEX idx_invoices_cover. La query legge issued_at e total direttamente dall'indice: notes e id non servono, quindi la tabella non viene mai aperta. Aggiungere una colonna a un indice composito solo per coprire una query frequente è uno scambio che conviene quando quella query viene eseguita di continuo.
Vincoli UNIQUE compositi
Gli indici compositi impongono anche l'unicità su combinazioni di colonne. Utile quando nessuna colonna è unica da sola, ma la combinazione deve esserlo:
Il terzo insert solleva UNIQUE constraint failed: enrollments.student_id, enrollments.course_id. La stessa coppia esiste già nell'indice, quindi SQLite rifiuta il duplicato.
Insidie da conoscere
- Un
ORtra colonne non iniziali blocca l'indice.WHERE a = 1 OR b = 2su un indice(a, b)di solito non può usare affatto l'indice: SQLite deve considerare i due rami separatamente. - Le funzioni sulle colonne indicizzate disattivano l'indice.
WHERE lower(email) = 'x'non userà un indice suemail. Indicizza l'espressione, oppure normalizza i dati in fase di insert. - Gli indici non sono gratis. Ogni indice viene aggiornato a ogni
INSERT,UPDATE(delle colonne indicizzate) eDELETE. Tre indici compositi su una tabella con molte scritture possono dominare il costo delle scritture. - Esegui
ANALYZEdopo aver creato gli indici. Il planner di SQLite usa le statistiche raccolte daANALYZEper scegliere tra gli indici candidati. Senza quelle statistiche ripiega su euristiche non sempre ottimali.
Un flusso di lavoro pratico
Quando ottimizzi una query lenta, il ciclo di solito è questo:
- Esegui
EXPLAIN QUERY PLANsulla query per vedere cosa fa SQLite oggi. - Se sta scansionando, guarda la clausola
WHERE: qual è la colonna di uguaglianza? Qual è quella di intervallo? Cosa viene selezionato? - Crea un indice composito ordinato prima per uguaglianza e poi per intervallo, aggiungendo in fondo le colonne selezionate se un covering index aiuta.
- Esegui
ANALYZE. - Esegui di nuovo
EXPLAIN QUERY PLAN. Conferma che il piano è cambiato e che l'indice viene usato. - Misura il tempo della query prima e dopo, su dati rappresentativi.
Salta il passo 6 a tuo rischio. Un indice che nel piano sembra corretto può comunque essere più lento nella pratica se la tabella è piccola o se il planner sceglie un altro percorso.
Prossimo passo: indici parziali
Gli indici compositi coprono tutte le righe della tabella. Ma spesso conta solo un piccolo sottoinsieme di righe: ticket aperti, job non ancora elaborati, record non eliminati. Un indice parziale ti permette di indicizzare solo quelle righe, con una clausola WHERE incorporata nell'indice stesso. È l'argomento della prossima pagina.
Domande frequenti
Cos'è un indice composito in SQLite?
Un indice composito è un unico indice che copre due o più colonne. Lo crei con CREATE INDEX idx_name ON table(col_a, col_b). SQLite salva le voci ordinate prima per col_a, poi per col_b all'interno di ogni valore di col_a: come un elenco telefonico ordinato per cognome e poi per nome.
L'ordine delle colonne conta in un indice composito SQLite?
Sì, moltissimo. SQLite può usare un indice composito per una query solo se la clausola WHERE filtra su un prefisso sinistro delle colonne indicizzate. Un indice su (a, b, c) aiuta le query che filtrano su a, su a e b, o su tutte e tre, ma non una query che filtra solo su b o solo su c.
Quando conviene un indice composito invece di indici separati su singole colonne?
Usa un indice composito quando le query filtrano od ordinano regolarmente sulla stessa combinazione di colonne. Gli indici separati su singole colonne funzionano quando ogni colonna viene interrogata in modo indipendente. Esegui EXPLAIN QUERY PLAN per vedere quale indice sceglie davvero SQLite: è l'unico riscontro affidabile.
Cos'è un covering index in SQLite?
Un covering index include tutte le colonne di cui la query ha bisogno, così SQLite può rispondere direttamente dall'indice senza toccare la tabella. EXPLAIN QUERY PLAN mostra USING COVERING INDEX quando succede. Aggiungere colonne extra a un indice composito solo per coprire una query molto frequente è un'ottimizzazione comune.