Negli scorsi script abbiamo introdotto l'utilizzo di MongoDB in ASP.NET Core e abbiamo visto come installare il driver ed eseguire le operazioni CRUD basilari. Uno dei grandi vantaggi del client per .NET Core è l'eccellente supporto a LINQ, che ci permette di eseguire query con una naturalezza estrema e senza dover conoscere praticamente nulla della sintassi di MongoDB.
Usare LINQ per eseguire query
Riprendiamo per esempio il controller che abbiamo già usato negli script precedenti, e modifichiamo la action Index in questo modo:
private readonly IMongoCollection<Person> _people; // GET: People public async Task<IActionResult> Index(string filter) { var query = _people.AsQueryable(); if (!string.IsNullOrWhiteSpace(filter)) query = query.Where(x => x.Name.ToLower().StartsWith(filter)); return View(await query.ToListAsync()); }
Nell'esempio in alto abbiamo aggiunto un parametro filter che, quando specificato, vogliamo usare per effettuare ricerche. Tutto ciò che dobbiamo fare è invocare l'extension method AsQueryable per poter iniziare a usare tutti i metodi LINQ che già conosciamo. Nel nostro caso, vogliamo cercare gli elementi che iniziano per la stringa di filtro, effettuando un ToLower per far sì che la ricerca sia case insensitive.
Indici in MongoDB
Tutto sembra estremamente facile e naturale. Il rovescio della medaglia, però, è che un utilizzo inconsapevole di questa tecnica possa portare a problemi prestazionali in produzione. Cerchiamo di capire questo concetto analizzando l'execution plan del server.
Per prima cosa, se facciamo il ToString della nostra variabile query, possiamo recuperare il comando che il driver eseguirà sul database. Nel nostro caso, sarà qualcosa di simile al seguente:
aggregate([{ "$match" : { "Name" : /^m/i } }]
che nel linguaggio di MongoDB si traduce in "recupera tutti i documenti la cui proprietà Name inizia per 'm', case insensitive".
Per determinare l'execution plan dobbiamo sfruttare la console di MongoDB, che possiamo richiamare con il comando
mongo.exe
o, se stiamo usando Docker, con
docker exec -it ..nomecontainer.. mongo
A questo punto, possiamo analizzare il comportamento della query digitando:
use MyTestDb db.people.aggregate([{ "$match" : { "Name" : /^m/i } }], { explain:true })
La prima riga seleziona il database su cui vogliamo operare, ossia MyTestDb. La seconda, invece, esegue l'operazione aggregate vista in precedenza, sulla collection people, passando il parametro opzionale explain a true.
Quest'ultimo farà sì che, invece dei risultati della query, ne venga stampato l'execution plan. Si tratta di un testo non troppo difficile da interpretare, il cui punto di maggiore interesse è il seguente:
"winningPlan" : { "stage" : "COLLSCAN", "filter" : { "Name" : { "$regex" : "^m", "$options" : "i" } }, "direction" : "forward" },
Come possiamo notare, la strategia scelta da MongoDB è COLLSCAN. Questo perchè, in assenza di un opportuno indice, l'unica opzione che il server ha è quella di scorrersi tutta la collection di documenti e valutarli uno per uno. Con pochi dati il problema non si nota, ma il rischio è che questa query diventi lentissima una volta che siamo in produzione.
La soluzione, allora, è quella di creare un indice, in questo caso sulla proprietà Name, che possa essere sfruttato per migliorare sensibilmente le prestazioni. Possiamo farlo con l'istruzione seguente:
db.people.createIndex({ Name:1 })
Il parametro "1" indica che l'indice è ordinato in senso crescente, cosa che non ha molto peso nel nostro esempio specifico.
Se a questo punto estraiamo di nuovo l'exeuction plan, il risultato ottenuto sarà molto diverso:
"winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "indexName" : "Name_1", "filter" : { "Name" : { "$regex" : "^m", "$options" : "i" } }, ...
Ora MongoDB sta effettivamente eseguendo una IXSCAN, ossia una scansione dell'indice Name_1, con ovvi vantaggi dal punto di vista delle prestazioni anche con grandi moli di dati.
Conclusioni
In questo script abbiamo visto come il driver .NET Core di MongoDB ci permetta di sfruttare LINQ per eseguire query sulla base dati. Tuttavia, in assenza di un opportuno indice, il query engine si troverà costretto a effettuare uno scan della intera collection, con un ovvio decadimento prestazionale.
Tramite la console, possiamo analizzare l'execution plan della query e creare indici che ne migliorino le performance.
Commenti
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
Approfondimenti
Paginare i risultati con QuickGrid in Blazor
Ottimizzazione dei block template in Angular 17
Miglioramenti nelle performance di Angular 16
Migliorare i tempi di risposta di GPT tramite lo streaming endpoint in ASP.NET Core
Registrare servizi multipli tramite chiavi in ASP.NET Core 8
Change tracking e composition in Entity Framework
Generare la software bill of material (SBOM) in GitHub
Gestione degli stili CSS con le regole @layer
Usare lo spread operator con i collection initializer in C#
Sfruttare lo stream rendering per le pagine statiche di Blazor 8
Estrarre dati randomici da una lista di oggetti in C#
Utilizzare database e servizi con gli add-on di Container App