Conoscere e utilizzare gli indici su MongoDB (e in generale nella maggioranza dei database) è fondamentale, in quanto essi hanno un impatto notevole sulle prestazioni delle interrogazioni.
Consideriamo la query seguente:
db.books.find({ isbn: '1783287756' })
Se non utilizziamo gli indici, MongoDB deve esaminare ogni documento presente nella collezione e confrontare il campo isbn con il valore indicato. Le prestazioni dell’interrogazione degradano, quindi, man mano che il nostro database si consolida acquisendo sempre più dati. Indicizzare una collezione consente di ridurre drasticamente il numero di documenti esaminati da MongoDB durante una query, con una conseguente riduzione dei tempi di esecuzione. D’altra parte, la decisione di indicizzare uno o più campi deve essere ponderata, perché l’indicizzazione ha comunque l’effetto di occupare spazio su disco e rallenta l’inserimento e la modifica dei documenti.
MongoDB supporta diversi tipi di indici, ma prima di esaminarli vediamo come reperire informazioni utili sulla velocità delle nostre query.
Profiling con explain
Per prima cosa generiamo un database sufficientemente ricco. Dalla console di mongo possiamo generare un po’ di dati per i nostri test:
for(var i=1; i<=20000; i++) { db.books.insert( { isbn: 223456 + I + '', title : "book " + i } ); }
A questo punto vorremmo sapere qualcosa in più sulle performance della query di ricerca tramite ISBN che abbiamo visto prima. In particolare vogliamo sapere il piano d’esecuzione dell’interrogazione, cioè in quante e quail operazioni MongoDB tradurrà la nostra query. Le query, infatti, specificano il risultato che vogliamo ottenere, senza nulla a che fare con le modalità in cui le operazioni vengono effettivamente eseguite. In altri termini, si dice che le query sono scritte in un linguaggio dichiarativo, viceversa le operazioni e i comandi hanno un approccio più imperativo.
Il metodo explain
è ciò di cui abbiamo bisogno per ottenere queste informazioni più tecniche. Possiamo invocarlo su un cursore, quindi esattamente in coda ad una chiamata di find
:
db.books.find({ isbn: '223458' }).explain()
La risposta del server sarà simile alla seguente (sarà diversa a seconda delle caratteristiche del server e dell’installazione di MongoDB):
{
"cursor" : "BasicCursor",
"isMultiKey" : false,
"n" : 1,
"nscannedObjects" : 20000,
"nscanned" : 20000,
"nscannedObjectsAllPlans" : 20000,
"nscannedAllPlans" : 20000,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 156,
"nChunkSkips" : 0,
"millis" : 6,
"server" : "MongoDbServer:27017",
"filterSet" : false
}
Le informazioni che adesso sono di nostro interesse sono nei seguenti campi:
-
cursor: specifica il tipo di cursore usato nella query:
-
BasicCursor
indica che c’è stata una scansione di tutta la collezione, cioè non è stato usato nessun indice. -
BtreeCursor
indica che si è usato un indice, di cui viene anche riportato il nome. Se viene indicatoGeoSearchCursor
, significa che l’indice usato è di tipo geografico. -
ComplexPlan
indica che MongoDB ha usato diversi indici, e ha intersecato i risultati fra loro.
-
- nscannedObjects: specifica il numero totale di documenti esaminati. Più basso teniamo questo valore, migliori sono prestazioni della query.
Come si vede dal nostro piano di esecuzione, è stata esaminata tutta la collezione, il che ovviamente non è un buon segno.
Indici su campo singolo
Quello che ci serve è un indice sul campo isbn
. Quello che useremo è il più semplice tipo di indice, e viene usato quando il tipo di ricerca più ricorrente in una collezione è basata su un certo campo.
Prima di continuare, è bene specificare che, per default, MongoDB crea un indice singolo sul campo _id
di ogni collezione
Per creare l’indice è sufficiente invocare il comando:
db.books.ensureIndex({ isbn: 1}, {unique: true})
Il primo parametro del comando specifica quali campi si vogliono inserire nell’indice. Il secondo campo (opzionale) fornisce alcune informazioni sull’indice voluto. Specificando unique
si intende un indice dove può esistere un solo documento con un dato valore. E questo è tutto: d’ora in poi, le query basate sul campo isbn
faranno uso dell’indice creato. Non dovremo preoccuparci di dire a MongoDB di usare l’indice.
Oltre a unique
si può usare l’opzione sparse
per indicare che l’indice non deve contenere riferimenti ai documenti che non hanno il campo indicizzato.
Come funzionano gli indici
Se per noi la presenza o meno di un indice è del tutto indifferente quando scriviamo una query, internamente, un indice è una struttura dati parallela alla collezione, cioè un’altra collezione di riferimenti ai documenti, ordinati in base al campo o ai campi che abbiamo deciso di indicizzare: per ogni indice che creiamo in una collezione, viene generata la lista ordinata di documenti, che MongoDB tiene aggiornati ad ogni modifica o inserimento di documenti nella collezione. Questo significa che parallelamente alla collezione books
, MongoDB manterrà sincronizzato un elenco di riferimenti ai libri presenti nella collezione, ordinati in base al valore del campo isbn
.
La maggiore velocità di esecuzione delle ricerche è data dal fatto che, invece di scorrere tutta la collezione, essendo l’indice ordinato, si può trovare immediatamente il riferimento al documento con l’isbn
indicato, perché la lista è ordinata.
Ora se eseguiamo nuovamente explain
sulla stessa query, avremo un risultato con le seguenti differenze rispetto a prima (riportiamo solo quelle significative):
"cursor" : "BtreeCursor isbn_1",
"nscannedObjects" : 1,
"nscanned" : 1,
"indexBounds" : {
"isbn" : [
[
"223458",
"223458"
]
]
},
La prima differenza è che stavolta il campo cursor
riporta BtreeCursor
, e a fianco il nome dell’indice usato: isbn_1
(il primo indice sul campo isbn
). Poi vediamo che il numero di documenti esaminati è sceso ad 1 (il documento restituito dall’indice), il che ovviamente è un gran vantaggio in termini di prestazioni.
Indici composti
Quando un campo è unico in una collezione (ad esempio l’ISBN o il codice fiscale) e viene usato nelle ricerche, è sicuramente un ottimo candidato per essere indicizzato singolarmente. Ma può capitare che vengano eseguite query su collezioni con molti valori di campi ripetuti, e magari esso venga usato insieme con altri campi. Ad esempio:
db.books.find({ category: 'Programming', published: 2014 })
In questo caso, avere un indice sul campo category
o sul campo published
potrebbe dare pochi vantaggi prestazionali, perché magari potrebbero esserci sia moltissimi documenti con la categoria indicata sia con l’anno di pubblicazione passato. In tal caso, quindi, è più utile avere un indice composto (Compound Index), costituito da due campi:
db.books.ensureIndex({ category: 1, published: 1 })
Indicizzazione di Array
Se un campo indicizzato è un array in almeno un documento, MongoDB crea un particolare tipo di indice (Multikey) che permette di velocizzare le ricerche all’interno di un array. Tutto ciò è del tutto trasparente all'utente, quindi non dobbiamo preoccuparci di specificare questo tipo di indice.
Indici testuali
La ricerca di testo, in genere, consiste nel trovare una o poche parole all'interno di un testo. Per abilitare la ricerca testuale bisogna creare degli indici testuali, ossia bisogna istruire MongoDB a considerare uno o più campi nell’indice testuale. Ad esempio:
db.books.ensureIndex( { title: "text" } )
A questo punto la sintassi per fare una ricerca nell’indice testuale è indicata nel prossimo esempio. Verranno restituiti i documenti che contengono una delle parole passate (gli spazi vengono ignorati) oppure entrambe.
db.books.find({ $text: { $search: 'NoSql MongoDB' } })
Un fatto interessante è che si può escludere un termine utilizzando il simbolo meno (-), come si vede nel prossimo esempio.
db.books.find({ $text: { $search: 'NoSql MongoDB -Cassandra' } })
Altri tipi di indici
Gli altri tipi di indici, che approfondiremo nelle prossime lezioni, sono:
- indici basati su hash (Hashed index) che vengono usati per effettuare lo sharding;
- indici georeferenziali (Geospatial index) usati per indicizzare coordinate geografiche.