Padroneggiare l'Indicizzazione di MongoDB per Prestazioni Ottimali delle Query

Sblocca prestazioni ottimali delle query in MongoDB padroneggiando le tecniche di indicizzazione. Questa guida completa copre gli indici a campo singolo, composti e multichiave, spiega il potere delle query di copertura e ti guida nell'uso di `explain()` per l'analisi delle prestazioni. Impara le migliori pratiche per velocizzare le operazioni di lettura, ridurre il carico del database e costruire un'applicazione più reattiva. Lettura essenziale per qualsiasi sviluppatore MongoDB concentrato sull'ottimizzazione delle prestazioni.

Padroneggiare l'Indicizzazione di MongoDB per Prestazioni Ottimali delle Query

L'indicizzazione di MongoDB diventa interessante quando il database non è più abbastanza piccolo per tentativi fortunati. Una query che sembrava istantanea in sviluppo può diventare dolorosa in produzione una volta che una collezione ha milioni di documenti, una dashboard aggiunge ordinamenti o un endpoint API inizia a filtrare per diversi campi contemporaneamente.

L'obiettivo non è indicizzare ogni campo. Di solito questo rallenta le scritture, consuma memoria e disco, e lascia comunque importanti query scoperte. L'obiettivo è comprendere la manciata di forme di query da cui la tua applicazione dipende effettivamente, quindi costruire indici che corrispondano a quelle forme.

Comprendere gli Indici di MongoDB

In sostanza, un indice è come un indice in un libro. Invece di leggere l'intero libro per trovare un argomento, consulti un riferimento ordinato e salti vicino alla pagina giusta. Gli indici di MongoDB aiutano il pianificatore di query a localizzare i documenti corrispondenti senza scansionare l'intera collezione. Senza un indice utile, MongoDB può eseguire una scansione della collezione, esaminando i documenti uno per uno fino a trovare le corrispondenze.

Le scansioni della collezione non sono sempre malvagie. Scansionare una collezione piccola può andare bene. Eseguire un report amministrativo una volta al mese può andare bene. Ma una scansione della collezione all'interno di un percorso di richiesta ad alto traffico è diversa. Compet con letture e scritture normali, peggiora man mano che i dati crescono e spesso si manifesta come latenza imprevedibile.

Come Funzionano gli Indici

MongoDB utilizza comunemente indici di tipo B-tree per gli indici di campo normali. Il dettaglio pratico importante è che i valori indicizzati sono memorizzati in ordine. Questo ordinamento aiuta MongoDB con filtri di uguaglianza, filtri di intervallo e ordinamenti quando la forma della query si allinea con l'indice.

Ad esempio, un indice su { email: 1 } è perfetto per:

db.users.findOne({ email: "[email protected]" })

Non è utile per:

db.users.find({ lastLoginAt: { $lt: ISODate("2025-01-01") } })

Quella seconda query ha bisogno di un indice che inizi con lastLoginAt, o deve scansionare.

Quando Usare gli Indici

Gli indici sono più vantaggiosi per i campi che vengono utilizzati frequentemente in:

  • Criteri di query (find(), findOne()): Campi utilizzati nel documento filter delle tue query.
  • Criteri di ordinamento (sort()): Campi utilizzati per ordinare i risultati delle tue query.
  • Campo _id: Per impostazione predefinita, MongoDB crea un indice sul campo _id, garantendo unicità e ricerche rapide per ID.

Tuttavia, gli indici hanno anche un costo:

  • Spazio di archiviazione: Gli indici consumano spazio su disco.
  • Prestazioni di scrittura: Gli indici devono essere aggiornati ogni volta che i documenti vengono inseriti, aggiornati o eliminati, il che può rallentare le operazioni di scrittura.
  • Pressione sulla memoria: Le pagine degli indici utilizzate frequentemente competono per la cache. Troppi indici grandi possono rendere più difficile mantenere il working set in memoria.

Pertanto, è fondamentale creare indici strategicamente, concentrandosi sui campi che produrranno i maggiori guadagni di prestazioni per le tue comuni operazioni di lettura.

Creare e Gestire gli Indici

MongoDB fornisce il metodo createIndex() per creare indici e getIndexes() per visualizzare quelli esistenti. Il metodo dropIndex() viene utilizzato per rimuoverli.

Creazione di Base di un Indice

Per creare un indice a campo singolo, specifichi il nome del campo e il tipo di indice (di solito 1 per ordine ascendente o -1 per ordine discendente).

db.collection.createIndex( { fieldName: 1 } );

Esempio: Indicizzare un campo username in ordine ascendente:

db.users.createIndex( { username: 1 } );

Visualizzare gli Indici

Per vedere gli indici su una collezione:

db.collection.getIndexes();

Esempio: Visualizzare gli indici sulla collezione users:

db.users.getIndexes();

Questo restituirà un array di definizioni di indici, incluso l'indice predefinito _id.

Su una collezione trafficata, crea gli indici deliberatamente. Le versioni moderne di MongoDB supportano la creazione di indici online in molti casi comuni, ma la creazione di indici consuma comunque CPU, I/O del disco e memoria. Sui sistemi di produzione, pianifica la creazione di grandi indici durante i periodi più tranquilli e controlla il ritardo di replica se esegui un replica set.

Eliminare gli Indici

Per rimuovere un indice:

db.collection.dropIndex( "indexName" );

Puoi trovare indexName dall'output di getIndexes(). In alternativa, puoi eliminare un indice specificando i campi indicizzati nello stesso formato di createIndex():

db.collection.dropIndex( { fieldName: 1 } );

Esempio: Eliminare l'indice username:

db.users.dropIndex( "username_1" ); // Usando il nome dell'indice
// OPPURE
db.users.dropIndex( { username: 1 } ); // Usando la definizione dell'indice

Prima di eliminare un indice, controlla se qualcosa lo utilizza ancora:

db.users.aggregate([{ $indexStats: {} }])

Questo mostra i contatori di accesso da quando il server è stato avviato. Un contatore pari a zero è un indizio, non una prova assoluta. Il server potrebbe essere stato riavviato di recente, o la query potrebbe essere eseguita solo durante un lavoro settimanale. Per i sistemi importanti, combina $indexStats, la ricerca nel codice dell'applicazione, i log delle query e un breve periodo di osservazione.

Indici Composti

Gli indici composti coinvolgono più campi. L'ordine dei campi in un indice composto è critico. MongoDB utilizza indici composti per query che coinvolgono più campi nelle clausole filter o sort.

Quando Usare gli Indici Composti

Gli indici composti sono più efficaci quando le tue query filtrano o ordinano frequentemente per una combinazione di campi. L'indice può soddisfare query che corrispondono ai campi nello stesso ordine in cui sono definiti nell'indice o in un prefisso dell'indice.

Esempio: Considera una collezione di orders con campi come userId, orderDate e status. Se interroghi frequentemente gli ordini per un utente specifico e li ordini per data, un indice composto su { userId: 1, orderDate: 1 } sarebbe molto vantaggioso.

db.orders.createIndex( { userId: 1, orderDate: 1 } );

Questo indice può supportare efficientemente query come:

  • db.orders.find( { userId: "user123" } ).sort( { orderDate: -1 } )
  • db.orders.find( { userId: "user123", orderDate: { $lt: ISODate() } } )

Tuttavia, potrebbe non essere altrettanto efficace per query che filtrano solo per orderDate se userId non è anche specificato, o se i campi sono in un ordine diverso.

L'Ordine dei Campi è Importante

L'ordine dei campi in un indice composto determina quali pattern di query può supportare bene. Una regola pratica utile è mettere prima i campi di uguaglianza, poi i campi di ordinamento, poi i campi di intervallo. Questa è spesso chiamata la linea guida ESR: uguaglianza, ordinamento, intervallo. È una linea guida, non una legge, ma previene molti progetti di indici scadenti.

Supponiamo che la tua pagina degli ordini esegua questa query:

db.orders.find({
  tenantId: "t1",
  status: "paid",
  createdAt: { $gte: ISODate("2025-01-01") }
}).sort({ createdAt: -1 })

Un indice ragionevole potrebbe essere:

db.orders.createIndex({ tenantId: 1, status: 1, createdAt: -1 })

tenantId e status sono filtri di uguaglianza. createdAt supporta l'ordinamento e l'intervallo. Se invece crei { createdAt: -1, status: 1, tenantId: 1 }, MongoDB potrebbe comunque usarlo in alcuni casi, ma di solito è meno allineato con questa query.

Per le query che ordinano i risultati, l'ordine dei campi nell'indice dovrebbe corrispondere all'ordine dei campi nell'operazione sort() per prestazioni ottimali. Se una query include sia un filtro che un ordinamento, e l'indice corrisponde ai campi del filtro, può anche essere utilizzato per l'ordinamento senza una scansione separata della collezione per l'ordinamento.

Gli indici composti possono anche servire query con prefisso. Un indice su { tenantId: 1, status: 1, createdAt: -1 } può aiutare una query solo su tenantId, o tenantId più status. Generalmente non può aiutare molto con una query solo su status perché status non è il campo principale.

Query di Copertura

Una query di copertura è una query in cui MongoDB può soddisfare l'intera query utilizzando solo l'indice. Ciò significa che l'indice contiene tutti i campi che vengono interrogati e proiettati. Le query di copertura evitano di recuperare i documenti dalla collezione stessa, rendendole estremamente veloci.

Come Ottenere Query di Copertura

Per ottenere una query di copertura, assicurati che:

  1. Hai un indice che include tutti i campi utilizzati nel filtro della query.
  2. Includi solo quei campi indicizzati (o un loro sottoinsieme) nella tua proiezione.

Esempio: Considera una collezione employees con campi name, age e city. Se hai un indice { city: 1, age: 1 } e vuoi recuperare i nomi e le età dei dipendenti in una città specifica, puoi creare una query di copertura:

db.employees.find( { city: "New York" }, { name: 1, age: 1, _id: 0 } ).explain()

In questa query, city è nell'indice, e name e age sono inclusi nella proiezione. Se l'indice contenesse anche name e age, sarebbe una query di copertura.

Perfezioniamo l'indice e la query per una vera query di copertura:

// Crea un indice che includa tutti i campi necessari per la query e la proiezione
db.employees.createIndex( { city: 1, age: 1, name: 1 } );

// Ora, una query che filtra per città e proietta nome ed età può essere coperta
db.employees.find( { city: "New York" }, { name: 1, age: 1, _id: 0 } )

Quando esegui explain("executionStats") su questa query, un piano coperto dovrebbe esaminare le chiavi dell'indice senza recuperare i documenti completi dalla collezione. In molti piani di explain, ciò significa che vedrai uno stadio IXSCAN senza uno stadio FETCH, e totalDocsExamined dovrebbe essere 0. L'output di explain varia in base alla versione di MongoDB e alla forma della query, quindi concentrati sugli stadi effettivi del piano e sui conteggi esaminati piuttosto che cercare un'etichetta esatta.

Le query di copertura sono utili per percorsi di lettura caldi come il completamento automatico, piccole viste di elenco o controlli di autorizzazione. Sono meno utili se la proiezione include campi grandi, molti campi o campi che cambiano costantemente. Aggiungere troppi campi a un indice solo per coprire una query può creare un indice voluminoso che danneggia le prestazioni di scrittura.

Altri Tipi di Indice Importanti

MongoDB offre vari tipi di indice per casi d'uso specifici:

Indici Multichiave

Gli indici multichiave vengono creati automaticamente quando indicizzi un campo array. Ti permettono di interrogare elementi all'interno degli array.

Esempio: Se hai una collezione products con un campo array tags ["electronics", "gadgets"]:

db.products.createIndex( { tags: 1 } );

Questo indice supporterà query come db.products.find( { tags: "electronics" } ).

Gli array richiedono cure extra negli indici composti. Un indice multichiave memorizza voci per gli elementi dell'array, il che può aumentare rapidamente la dimensione dell'indice. MongoDB ha anche restrizioni sugli indici composti multichiave quando più di un campo indicizzato può contenere array nello stesso documento. Se il tuo modello di dati ha diversi array e filtri complessi, testa la query esatta con dati rappresentativi prima di presumere che un indice composto si comporterà come farebbe un indice a campo scalare.

Indici di Testo

Gli indici di testo supportano la ricerca efficiente di contenuti di stringa nei documenti. Sono utilizzati per query di ricerca testuale usando l'operatore $text.

db.articles.createIndex( { content: "text" } );

Ciò consente ricerche come: db.articles.find( { $text: { $search: "database performance" } } ).

Gli indici di testo sono utili per la ricerca testuale di base, ma non sono una piattaforma di ricerca completa. Se hai bisogno di ottimizzazione avanzata della rilevanza, tolleranza agli errori di battitura, facet, evidenziazione o comportamento di ricerca specifico per lingua, MongoDB Atlas Search o un motore di ricerca dedicato potrebbero essere una scelta migliore.

Indici Geospaziali

Gli indici geospaziali vengono utilizzati per interrogare efficientemente dati geografici usando gli operatori $near, $geoWithin e $geoIntersects.

db.locations.createIndex( { loc: "2dsphere" } ); // Per indice 2dsphere

Indici Unici

Gli indici unici impongono l'unicità per un campo o una combinazione di campi. Se viene inserito o aggiornato un valore duplicato, MongoDB restituirà un errore.

db.users.createIndex( { email: 1 }, { unique: true } );

Per le tabelle utente di produzione, normalizza prima di imporre l'unicità. Gli indirizzi email sono un esempio comune. Se la tua applicazione tratta [email protected] e [email protected] come lo stesso utente, memorizza un campo normalizzato come emailLower e metti lì l'indice unico. Non fare affidamento solo sul codice dell'applicazione per prevenire duplicati in concorrenza.

Indici Parziali

Gli indici parziali indicizzano solo i documenti che corrispondono a un'espressione di filtro. Sono utili quando una query si concentra su un sottoinsieme di una collezione.

db.orders.createIndex(
  { tenantId: 1, createdAt: -1 },
  { partialFilterExpression: { status: "open" } }
)

Questo può aiutare se la tua applicazione legge frequentemente ordini aperti e gli ordini chiusi costituiscono la maggior parte della collezione. L'indice è più piccolo perché esclude i documenti che non corrispondono al filtro parziale. La query deve includere una condizione compatibile affinché MongoDB lo utilizzi.

Indici TTL

Gli indici TTL rimuovono automaticamente i documenti dopo un tempo configurato. Sono comunemente usati per sessioni, token temporanei o eventi di breve durata.

db.sessions.createIndex(
  { expiresAt: 1 },
  { expireAfterSeconds: 0 }
)

L'eliminazione TTL non è istantanea al momento esatto della scadenza. MongoDB rimuove i documenti scaduti in background. Usalo per la pulizia, non per una tempistica di sicurezza precisa in cui un token deve diventare immediatamente non valido. La tua applicazione dovrebbe comunque controllare la scadenza durante le letture.

Analisi delle Prestazioni con explain()

Comprendere come MongoDB esegue le tue query è cruciale per ottimizzarle. Il metodo explain() fornisce informazioni sul piano di esecuzione della query, incluso se un indice è stato utilizzato e come.

db.collection.find( {...} ).explain( "executionStats" );

Campi chiave da cercare nell'output di explain():

  • winningPlan.stage: Indica lo stadio del piano di esecuzione (ad es., COLLSCAN per scansione della collezione, IXSCAN per scansione dell'indice).
  • executionStats.totalKeysExamined: Il numero di chiavi dell'indice esaminate.
  • executionStats.totalDocsExamined: Il numero di documenti esaminati.

Un buon piano di esecuzione avrà totalDocsExamined vicino o uguale al numero di documenti restituiti, e totalKeysExamined significativamente inferiore al numero totale di documenti nella collezione. Se totalDocsExamined è molto alto, o viene utilizzato COLLSCAN, suggerisce che manca un indice o non viene utilizzato efficacemente.

Ecco il modo rapido in cui leggo un piano explain:

  1. Cerca COLLSCAN. Se questo è un percorso caldo e la collezione è grande, di solito è il primo problema.
  2. Cerca IXSCAN seguito da FETCH. Un fetch è normale quando la query ha bisogno di campi al di fuori dell'indice, ma un esame eccessivo dei documenti significa che l'indice non è abbastanza selettivo.
  3. Confronta nReturned, totalKeysExamined e totalDocsExamined. Restituire 20 documenti dopo aver esaminato 25 chiavi è sano. Restituire 20 documenti dopo aver esaminato 500.000 chiavi non lo è.
  4. Fai attenzione agli ordinamenti in memoria. Se MongoDB deve ordinare un ampio set di risultati dopo il filtraggio, un indice composto che supporti l'ordinamento può aiutare.

Usa filtri realistici durante i test. Un piano explain per tenantId: "demo" potrebbe non corrispondere a un grande tenant con milioni di documenti. La distribuzione dei dati è importante.

Un Percorso Pratico di Progettazione degli Indici

Immagina un'applicazione con una collezione tickets. Gli agenti di supporto utilizzano una pagina di coda con questi filtri:

db.tickets.find({
  tenantId: "acme",
  status: "open",
  assigneeId: "u123"
}).sort({ updatedAt: -1 }).limit(50)

Inizia con la forma della query, non con l'elenco dei campi. La collezione è multi-tenant, gli agenti di solito filtrano per stato e assegnatario, e l'interfaccia utente ordina prima gli aggiornamenti più recenti. Un indice pratico è:

db.tickets.createIndex({
  tenantId: 1,
  status: 1,
  assigneeId: 1,
  updatedAt: -1
})

Ora considera un'altra pagina: i manager visualizzano tutti i ticket aperti, indipendentemente dall'assegnatario:

db.tickets.find({
  tenantId: "acme",
  status: "open"
}).sort({ updatedAt: -1 }).limit(100)

L'indice precedente può utilizzare il prefisso { tenantId, status }, ma assigneeId si trova prima di updatedAt, quindi potrebbe non supportare altrettanto bene l'ordinamento per questa query del manager. Potresti aver bisogno di un secondo indice:

db.tickets.createIndex({
  tenantId: 1,
  status: 1,
  updatedAt: -1
})

Questo è un compromesso normale. Un singolo indice raramente serve perfettamente ogni schermata. Il compito è supportare i percorsi importanti senza creare un mucchio di indici sovrapposti che costano tutti scritture.

Migliori Pratiche per l'Indicizzazione di MongoDB

  • Indicizza solo ciò di cui hai bisogno: Evita di creare indici su campi che vengono raramente interrogati o ordinati. Ogni indice aggiunge overhead.
  • Usa saggiamente gli indici composti: Ordina i campi correttamente in base ai pattern di query. Considera prima i campi più selettivi.
  • Punta a query di copertura: Se le prestazioni di lettura sono critiche, progetta indici per coprire le comuni operazioni di lettura.
  • Monitora l'uso degli indici: Rivedi regolarmente l'uso degli indici usando explain() e db.collection.aggregate([{ $indexStats: {} }]) per identificare indici inutilizzati o inefficienti.
  • Considera la selettività dell'indice: Gli indici su campi con bassa cardinalità (pochi valori distinti) potrebbero non essere efficaci quanto quelli su campi con alta cardinalità.
  • Mantieni gli indici piccoli: Evita di includere campi grandi o array negli indici a meno che non sia assolutamente necessario per le query di copertura.
  • Testa i tuoi indici: Testa sempre l'impatto dei nuovi indici sia sulle prestazioni di lettura che di scrittura in condizioni di carico realistiche.
  • Rimuovi gli indici ridondanti con attenzione: Se hai { a: 1, b: 1 }, un indice separato { a: 1 } potrebbe essere ridondante per molti carichi di lavoro. Conferma l'uso prima di eliminare.
  • Progetta intorno a schermi e lavori reali: Gli indici dovrebbero mappare il comportamento dell'applicazione: ricerca di login, pagina di coda, filtro di report, scansione di worker in background.
  • Rivedi dopo modifiche allo schema: Un nuovo campo, un nuovo ordine di ordinamento o un nuovo modello di tenant possono rendere un vecchio indice meno utile.

Come Ci Si Sente con una Buona Indicizzazione

Una buona indicizzazione di MongoDB è di solito silenziosa. Le query importanti esaminano all'incirca la quantità di dati che restituiscono. Gli ordinamenti non si riversano in lavoro costoso. Le scritture non sono appesantite da una dozzina di indici speculativi. Quando una nuova funzionalità aggiunge una nuova forma di query, la testi con explain("executionStats") prima che diventi un incidente di produzione.

L'abitudine pratica è semplice: raccogli la query reale, progetta il più piccolo indice utile per quella forma di query, testa con dati rappresentativi e continua a controllare l'uso dell'indice nel tempo. Quell'abitudine farà di più per le prestazioni di MongoDB che memorizzare ogni tipo di indice.