Eseguire query LINQ ottimizzate con MongoDB in ASP.NET Core

di Marco De Sanctis, in ASP.NET Core,

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

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi