Guida Completa agli Indici Composti in MongoDB: Ottimizzare le Performance delle Query

Principiante
Database e SQL MongoDB

Scopri come funzionano gli indici composti in MongoDB, come progettarli correttamente e come evitare i colli di bottiglia nelle tue applicazioni web.

Pubblicato
Tag
database Performance backend MongoDB NoSQL Indexing

Introduzione agli Indici in MongoDB

Quando iniziamo a lavorare con MongoDB, è facile pensare che, essendo un database NoSQL orientato ai documenti, la velocità sia garantita per default. Tuttavia, man mano che la nostra collezione cresce da poche centinaia a milioni di documenti, noteremo un rallentamento significativo nelle risposte del server. Questo accade perché, senza un indice, MongoDB deve eseguire una "Collection Scan" (COLLSCAN), ovvero leggere ogni singolo documento nella collezione per trovare quelli che corrispondono ai criteri di ricerca.

Un indice è essenzialmente una struttura dati speciale (solitamente un B-tree) che memorizza una piccola parte del set di dati in un formato ordinato. Invece di scansionare l'intera collezione, MongoDB consulta l'indice per trovare rapidamente la posizione dei documenti richiesti.

Mentre gli indici singoli (su un solo campo) sono utili per query semplici, le applicazioni del mondo reale richiedono spesso filtri su più campi contemporaneamente. È qui che entrano in gioco gli indici composti.

Cos'è un Indice Composto?

Un indice composto è un indice che include riferimenti a più campi all'interno del suo documento di indice. Invece di avere un indice per il campo nome e uno per il campo cognome, creiamo un unico indice che contiene entrambi: { nome: 1, cognome: 1 }.

Perché non usare semplicemente due indici singoli? Potrebbe sembrare intuitivo, ma MongoDB può utilizzare efficacemente un solo indice per query (salvo l'uso dell'Index Intersection, che è meno efficiente). Se hai un indice su nome e uno su cognome, MongoDB dovrà sceglierne uno, filtrare i risultati e poi scansionare manualmente i documenti rimasti per verificare l'altro campo. Un indice composto, invece, permette al database di restringere il campo di ricerca su entrambi i criteri simultaneamente.

L'importanza dell'ordine dei campi

La caratteristica più critica di un indice composto è l'ordine dei campi. Un indice su { città: 1, età: 1 } non è la stessa cosa di un indice su { età: 1, città: 1 }.

Immaginate l'indice come un elenco telefonico: i nomi sono ordinati per cognome e poi per nome. Potete trovare rapidamente tutti i "Rossi", e tra i Rossi potete trovare rapidamente "Mario". Ma non potete usare l'elenco per trovare tutte le persone che si chiamano "Mario" senza scorrere l'intero libro, perché l'ordinamento primario è il cognome.

Come Creare Indici Composti

In MongoDB, gli indici vengono creati utilizzando il metodo createIndex(). Il valore 1 indica l'ordinamento ascendente, mentre -1 indica l'ordinamento discendente.

Ecco un esempio pratico. Immaginiamo di avere una collezione di utenti e di voler ottimizzare le ricerche che filtrano per stato e data_registrazione.

// Connessione al database e selezione della collezione
const db = connectToMongo();
const users = db.collection('users');

// Creazione di un indice composto
// 1 = Ascendente, -1 = Discendente
db.users.createIndex({
  "stato": 1, 
  "data_registrazione": -1
});

console.log("Indice composto creato con successo!");

In questo esempio, abbiamo creato un indice che ordina i documenti prima per stato (in ordine alfabetico) e poi, per ogni stato, ordina i documenti per data_registrazione dalla più recente alla più vecchia. Questo è estremamente utile per query del tipo: "Mostrami tutti gli utenti attivi, ordinati dal più recente".

La Regola d'Oro: ESR (Equality, Sort, Range)

Progettare l'ordine dei campi in un indice composto può sembrare complicato, ma esiste una formula collaudata chiamata Regola ESR. Per ottenere le massime prestazioni, i campi dell'indice dovrebbero seguire questo ordine:

  1. Equality (Uguaglianza): Campi su cui effettui ricerche esatte (es. stato: 'attivo', categoria: 'elettronica').
  2. Sort (Ordinamento): Campi utilizzati per ordinare i risultati (es. .sort({ data: -1 })).
  3. Range (Intervallo): Campi su cui effettui ricerche di intervallo (es. prezzo: { $gt: 10 }, età: { $lte: 30 }).

Perché seguire l'ESR?

Se metti un campo di "intervallo" prima di un campo di "ordinamento", MongoDB dovrà scansionare tutti i documenti che rientrano nell'intervallo e poi ordinarli in memoria (operazione chiamata Blocking Sort), il che è lentissimo e può causare errori se il set di dati supera i 32MB di RAM.

Se invece segui l'ESR, MongoDB usa l'indice per filtrare l'uguaglianza, poi usa l'indice stesso per restituire i dati già ordinati, e infine applica il filtro di intervallo, rendendo la query quasi istantanea.

Esempi Pratici e Casi d'Uso

Vediamo come applicare questi concetti in tre scenari comuni.

Scenario 1: E-commerce (Ricerca Prodotti)

Supponiamo di voler cercare prodotti di una specifica categoria che abbiano un prezzo tra 10 e 50 euro, ordinati per popolarità.

Query:

db.products.find({
  categoria: "smartphone",
  prezzo: { $gte: 10, $lte: 50 }
}).sort({ popolarità: -1 });

Indice Ottimale (ESR):

  • Equality: categoria
  • Sort: popolarità
  • Range: prezzo
db.products.createIndex({ categoria: 1, popolarità: -1, prezzo: 1 });

Scenario 2: Blog (Articoli Recenti per Tag)

Vogliamo trovare articoli con il tag "JavaScript", pubblicati negli ultimi 30 giorni, ordinati per data di pubblicazione.

Query:

db.posts.find({
  tags: "JavaScript",
  data_pubblicazione: { $gte: ISODate("2023-01-01") }
}).sort({ data_pubblicazione: -1 });

Indice Ottimale: In questo caso, l'ordinamento e l'intervallo avvengono sullo stesso campo (data_pubblicazione).

db.posts.createIndex({ tags: 1, data_pubblicazione: -1 });

Scenario 3: Gestione Utenti (Filtro e Paginazione)

Ricerca di utenti in una determinata città, con un ruolo specifico, ordinati per nome.

Query:

db.users.find({
  citta: "Milano",
  ruolo: "admin"
}).sort({ nome: 1 });

Indice Ottimale:

db.users.createIndex({ citta: 1, ruolo: 1, nome: 1 });

Errori Comuni e FAQ

1. Creare troppi indici

Un errore tipico dei principianti è creare un indice per ogni possibile combinazione di campi. Ricordate che ogni volta che inserite, aggiornate o eliminate un documento, MongoDB deve aggiornare anche tutti i relativi indici. Troppi indici rallentano le operazioni di scrittura (INSERT, UPDATE, DELETE) e consumano molta RAM.

2. Dimenticare i prefissi

Se avete un indice su { a: 1, b: 1, c: 1 }, non avete bisogno di creare un indice separato su { a: 1 } o { a: 1, b: 1 }. MongoDB può utilizzare l'indice composto per qualsiasi query che filtri per i campi di "prefisso". Tuttavia, non può usarlo per una query che filtra solo per { b: 1 } o { c: 1 }.

3. Confondere l'ordine di Sort

Se l'indice è { a: 1, b: 1 }, potete ordinare per { a: 1, b: 1 } o per { a: -1, b: -1 } (l'inverso esatto). Ma se provate a ordinare per { a: 1, b: -1 }, l'indice non sarà efficiente per l'ordinamento e MongoDB dovrà eseguire un sort in memoria.

Come Verificare se l'Indice Funziona: .explain()

Per sapere se MongoDB sta effettivamente usando il vostro indice composto, non indovinate: usate il metodo .explain("executionStats").

db.users.find({ stato: "attivo", eta: { $gt: 18 } }).explain("executionStats");

Nel risultato JSON, cercate queste chiavi:

  • stage: "IXSCAN": Ottimo! Significa che MongoDB sta usando un indice (Index Scan).
  • stage: "COLLSCAN": Allarme! MongoDB sta scansionando l'intera collezione.
  • totalDocsExamined: Se questo numero è molto più alto del numero di documenti restituiti, l'indice non è ottimizzato.

Prossimi Passi

Ora che avete compreso le basi degli indici composti e la regola ESR, potete approfondire ulteriormente l'ottimizzazione di MongoDB esplorando i seguenti argomenti:

  1. Indici Parziali: Creare indici solo su una parte dei documenti (es. solo sugli utenti attivi) per risparmiare spazio.
  2. Indici TTL (Time To Live): Utili per eliminare automaticamente i documenti dopo un certo tempo (es. sessioni utente o log).
  3. Indici Multikey: Come gestire gli indici su campi che contengono array.
  4. Profiling: Utilizzare il database profiler di MongoDB per identificare le query lente in produzione.

La chiave per un database performante non è aggiungere indici a caso, ma analizzare i pattern di lettura della vostra applicazione e progettare gli indici in modo chirurgico.