Scegliere l'indice giusto in PostgreSQL

PostgreSQL include sei metodi di accesso agli indici, e CREATE INDEX senza una clausola USING ti dà sempre un B-tree. Quel default è corretto la maggior parte delle volte — questa guida parla di riconoscere i casi in cui non lo è, e delle tre funzionalità degli indici (parziale, su espressione, covering) che contano più del metodo di accesso nel lavoro di tutti i giorni.

B-tree: il default per un buon motivo

Un indice B-tree supporta confronti di uguaglianza e di intervallo (=, <, >, BETWEEN, IN), IS NULL, LIKE 'abc%' con prefisso (con la giusta collation o text_pattern_ops), e — spesso dimenticato — l'ordinamento: un indice su (created_at) può soddisfare ORDER BY created_at DESC LIMIT 20 senza ordinare nulla.

Per i B-tree multicolonna, l'ordine delle colonne è tutto. Un indice su (customer_id, created_at) è eccellente per WHERE customer_id = ? ORDER BY created_at, ma pressoché inutile per una query che filtra solo su created_at. Regola pratica: prima le colonne di uguaglianza, poi la colonna di intervallo o di ordinamento.

Quando il B-tree è lo strumento sbagliato

Tipo di indiceRicorri a esso quando…Attenzione a
GIN Il valore è un contenitore e cerchi al suo interno: jsonb @> '{"status":"active"}', sovrapposizione di array &&, full-text tsvector @@ tsquery, ricerca trigram con pg_trgm per LIKE '%term%'. Più lento del B-tree in aggiornamento; la dimensione dell'indice può essere grande. Va bene per dati con molte letture.
GiST Dati geometrici, range type (sovrapposizioni di tstzrange), nearest-neighbor ORDER BY location <-> point, e vincoli di esclusione (es. "nessuna prenotazione sovrapposta per la stessa stanza"). Lossy per alcune classi di operatori; di solito più grande e più lento di un B-tree per la semplice uguaglianza.
BRIN Tabelle enormi append-only (log, eventi) in cui la colonna è correlata all'ordine fisico delle righe, tipicamente i timestamp. Minuscolo — megabyte dove un B-tree occuperebbe gigabyte. Inutile se i dati non sono fisicamente ordinati per quella colonna; restringe la scansione solo a intervalli di blocchi.
Hash Pura uguaglianza su valori lunghi dove un B-tree si ingrossa. Raramente necessario; crash-safe e replicato dalla versione 10 di PostgreSQL. Solo uguaglianza: niente intervalli, niente ordinamento, niente vincoli di unicità.
SP-GiST Strutture partizionate nello spazio: ricerche per prefisso su testo, intervalli IP (inet), quadtree per punti. Di nicchia; fai un benchmark contro GiST/B-tree sui tuoi dati reali prima di adottarlo.

Indici parziali: indicizza solo ciò che interroghi

CREATE INDEX orders_pending_idx
    ON orders (created_at)
    WHERE status = 'pending';

Se il 99% dei tuoi ordini è completato ma ogni polling chiede quelli pending, un indice parziale resta piccolo, resta caldo in cache ed è economico da mantenere — le righe completate non lo toccano mai. La query deve includere la condizione WHERE (o qualcosa da cui il planner possa dimostrarne l'implicazione) affinché l'indice venga preso in considerazione.

Indici su espressione: corrispondi a ciò che la query calcola

CREATE INDEX users_email_lower_idx ON users (lower(email));

Una query che filtra su lower(email) = lower($1) non può usare un semplice indice su email — l'indice memorizza i valori grezzi, non quelli in minuscolo. L'espressione nell'indice deve corrispondere all'espressione nella query. La stessa tecnica funziona per date_trunc('day', created_at), l'estrazione di campi JSON e qualsiasi funzione immutable.

Indici covering e index-only scan

CREATE INDEX orders_cust_idx
    ON orders (customer_id) INCLUDE (total, created_at);

Se l'indice contiene ogni colonna di cui la query ha bisogno, PostgreSQL può saltare del tutto la tabella — un index-only scan. INCLUDE aggiunge colonne di payload alle foglie dell'indice senza renderle parte della chiave. Due avvertenze: l'indice diventa più grande, e gli index-only scan richiedono anche che la visibility map sia aggiornata — su una tabella molto aggiornata e raramente sottoposta a vacuum vedrai Heap Fetches salire e gran parte del beneficio svanire.

Regole pratiche

Il Visualizzatore EXPLAIN segnala i due segni classici di un indice mancante o sbagliato: scansioni sequenziali che leggono molto più di quanto restituiscono e conteggi elevati di Rows Removed by Filter.
🧯 Errori correlati: duplicate key value violates unique constraint, no constraint matching ON CONFLICT, e violazioni di chiave esterna — delete lente sulla tabella padre spesso significano una colonna FK non indicizzata.