Suggerimenti sulle prestazioni delle query per gli SDK di Azure Cosmos DB

SI APPLICA A: NoSQL

Azure Cosmos DB è un database distribuito veloce, flessibile, facilmente scalabile e con livelli di latenza e velocità effettiva garantiti. Non è necessario apportare modifiche significative all'architettura o scrivere codice complesso per ridimensionare il database con Azure Cosmos DB. Aumentare o ridurre le prestazioni è semplice come eseguire una singola chiamata API. Per altre informazioni, vedere Effettuare il provisioning della velocità effettiva per un contenitore oppure Effettuare il provisioning della velocità effettiva per un database.

Ridurre le chiamate al piano di query

Per eseguire una query, è necessario compilare un piano di query. Questo in generale rappresenta una richiesta di rete al gateway Azure Cosmos DB, che aggiunge alla latenza dell'operazione di query. Esistono due modi per rimuovere questa richiesta e ridurre la latenza dell'operazione di query:

Ottimizzazione delle query a partizione singola con l'esecuzione diretta ottimistica

NoSQL di Azure Cosmos DB offre un'ottimizzazione denominata ODE (Optimistic Direct Execution), che può migliorare l'efficienza di determinate query NoSQL. In particolare, le query che non richiedono la distribuzione includono quelle che possono essere eseguite in una singola partizione fisica o con risposte che non richiedono paginazione. Le query che non richiedono la distribuzione possono ignorare in modo sicuro alcuni processi, ad esempio la generazione del piano di query sul lato client e la riscrittura delle query, riducendo così la latenza delle query e il costo delle UR. Se si specifica la chiave di partizione nella richiesta o nella query stessa (o si dispone di una sola partizione fisica) e i risultati della query non richiedono la paginazione, ODE può migliorare le query.

Nota

Optimistic Direct Execution (ODE), che offre prestazioni migliorate per le query che non richiedono la distribuzione, non deve essere confusa con la modalità diretta, ovvero un percorso per la connessione dell'applicazione alle repliche back-end.

ODE è ora disponibile in .NET SDK versione 3.38.0 e successive. Quando si esegue una query e si specifica una chiave di partizione nella richiesta o nella query stessa oppure il database ha una sola partizione fisica, l'esecuzione della query può sfruttare i vantaggi di ODE. Per abilitare ODE, impostare EnableOptimisticDirectExecution su true in QueryRequestOptions.

Le query a partizione singola che dispongono di funzioni GROUP BY, ORDER BY, DISTINCT e di aggregazione (ad esempio sum, mean, min e max) possono trarre vantaggio significativamente dall'uso di ODE. Tuttavia, negli scenari in cui la query è destinata a più partizioni o richiede ancora la paginazione, la latenza della risposta della query e il costo delle UR potrebbe essere superiore a quello senza usare ODE. Pertanto, quando si usa ODE, è consigliabile:

  • Specificare la chiave di partizione nella chiamata o nella query stessa.
  • Assicurarsi che le dimensioni dei dati non siano aumentate e che la partizione sia suddivisa.
  • Assicurarsi che i risultati della query non richiedano l'impaginazione per ottenere il massimo vantaggio di ODE.

Ecco alcuni esempi di semplici query a partizione singola che possono trarre vantaggio da ODE:

- SELECT * FROM r
- SELECT * FROM r WHERE r.pk == "value"
- SELECT * FROM r WHERE r.id > 5
- SELECT r.id FROM r JOIN id IN r.id
- SELECT TOP 5 r.id FROM r ORDER BY r.id
- SELECT * FROM r WHERE r.id > 5 OFFSET 5 LIMIT 3 

In alcuni casi le query a partizione singola possono comunque richiedere la distribuzione se il numero di elementi di dati aumenta nel tempo e il database di Azure Cosmos DB suddivide la partizione. Esempi di query in cui può verificarsi questo problema includono:

- SELECT Count(r.id) AS count_a FROM r
- SELECT DISTINCT r.id FROM r
- SELECT Max(r.a) as min_a FROM r
- SELECT Avg(r.a) as min_a FROM r
- SELECT Sum(r.a) as sum_a FROM r WHERE r.a > 0 

Alcune query complesse possono sempre richiedere la distribuzione, anche se la destinazione è una singola partizione. Esempi di tali query includono:

- SELECT Sum(id) as sum_id FROM r JOIN id IN r.id
- SELECT DISTINCT r.id FROM r GROUP BY r.id
- SELECT DISTINCT r.id, Sum(r.id) as sum_a FROM r GROUP BY r.id
- SELECT Count(1) FROM (SELECT DISTINCT r.id FROM root r)
- SELECT Avg(1) AS avg FROM root r 

È importante notare che ODE potrebbe non sempre recuperare il piano di query e, di conseguenza, non è in grado di impedire o disattivare le query non supportate. Ad esempio, dopo la suddivisione della partizione, tali query non sono più idonee per ODE e, pertanto, non verranno eseguite perché la valutazione del piano di query sul lato client bloccherà tali query. Per garantire la continuità di compatibilità/servizio, è fondamentale assicurarsi che solo le query completamente supportate in scenari senza ODE (ovvero, eseguono e producono il risultato corretto nel caso di più partizioni generale) vengono usate con ODE.

Nota

L'uso di ODE può potenzialmente causare la generazione di un nuovo tipo di token di continuazione. Un token di questo tipo non viene riconosciuto dagli SDK precedenti per impostazione predefinita e ciò potrebbe comportare un'eccezione del token di continuazione in formato non valido. Se si ha uno scenario in cui i token generati dagli SDK più recenti vengono usati da un SDK precedente, è consigliabile adottare un approccio in 2 passaggi per l'aggiornamento:

  • Eseguire l'aggiornamento al nuovo SDK e disabilitare ODE, entrambi insieme come parte di una singola distribuzione. Attendere l'aggiornamento di tutti i nodi.
    • Per disabilitare ODE, impostare EnableOptimisticDirectExecution su false in QueryRequestOptions.
  • Abilitare ODE come parte della seconda distribuzione per tutti i nodi.

Usare la generazione del piano di query locale

L'SDK SQL include un file ServiceInterop.dll nativo che consente di analizzare e ottimizzare le query in locale. Il file ServiceInterop.dll è supportato solo nella piattaforma Windows x64. Per impostazione predefinita, i tipi di applicazioni seguenti usano l'elaborazione host a 32 bit. Per modificare l'elaborazione dell'host a 64 bit, seguire questa procedura in base al tipo di applicazione:

  • Per le applicazioni eseguibili, è possibile modificare l'elaborazione host impostando la destinazione della piattaforma su x64 nella finestra Proprietà progetto, nella scheda Build.

  • Per i progetti di test basati su VSTest, è possibile modificare l'elaborazione host selezionando Test>Impostazioni di test>Architettura del processore predefinita come X64 nel menu Test di Visual Studio.

  • Per le applicazioni Web ASP.NET distribuite in locale è possibile modificare il processo host selezionando Usare la versione a 64 bit di IIS Express per progetti e siti Web in Strumenti>Opzioni>Progetti e soluzioni>Progetti Web.

  • Per le applicazioni Web ASP.NET distribuite in Azure, è possibile modificare l'elaborazione host selezionando la piattaforma a 64 bit in Impostazioni applicazione nel portale di Azure.

Nota

Per impostazione predefinita, i nuovi progetti di Visual Studio sono impostati su Qualsiasi CPU. È consigliabile impostare il progetto su x64 in modo da non passare a x86. Un progetto impostato su Qualsiasi CPU può passare facilmente a x86 se viene aggiunta una dipendenza solo x86.
ServiceInterop.dll deve trovarsi nella cartella da cui viene eseguita la DLL dell'SDK. Questo dovrebbe essere un problema solo se si copiano manualmente DLL o si dispone di sistemi di compilazione/distribuzione personalizzati.

Usare query a partizione singola

Per le query destinate a una chiave di partizione impostando la proprietà PartitionKey in QueryRequestOptions e che non contengono aggregazioni (inclusi Distinct, DCount, Group By). In questo esempio il campo della chiave di partizione /state viene filtrato in base al valore Washington.

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle' AND c.state = 'Washington'"
{
    // ...
}

Facoltativamente, è possibile specificare la chiave di partizione come parte dell'oggetto opzioni della richiesta.

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey("Washington")}))
{
    // ...
}

Importante

Nei client che eseguono un sistema operativo non Windows, ad esempio Linux e MacOS, la chiave di partizione deve essere sempre specificata nell'oggetto opzioni della richiesta.

Nota

Le query tra partizioni richiedono all'SDK di visitare tutte le partizioni esistenti per verificare la presenza di risultati. Più partizioni fisiche contiene il contenitore, più può essere lento.

Evitare di ricreare l'iteratore inutilmente

Quando tutti i risultati della query vengono utilizzati dal componente corrente, non è necessario ricreare l'iteratore con la continuazione per ogni pagina. Preferisce sempre svuotare completamente la query, a meno che l'impaginazione non sia controllata da un altro componente chiamante:

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey("Washington")}))
{
    while (feedIterator.HasMoreResults) 
    {
        foreach(MyItem document in await feedIterator.ReadNextAsync())
        {
            // Iterate through documents
        }
    }
}

Ottimizzare il grado di parallelismo

Per le query, ottimizzare la proprietà MaxConcurrency in QueryRequestOptions per identificare le configurazioni migliori per l'applicazione, soprattutto se si eseguono query tra partizioni (senza un filtro sul valore della chiave di partizione). MaxConcurrency controlla il numero massimo di attività in parallelo, ovvero il numero massimo di partizioni accessibili in parallelo. L'impostazione del valore su -1 consentirà all'SDK di decidere la concorrenza ottimale.

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { 
        PartitionKey = new PartitionKey("Washington"),
        MaxConcurrency = -1 }))
{
    // ...
}

Si supponga che

  • D = numero massimo predefinito di attività in parallelo (= numero totale di processori nel computer client)
  • P = Numero massimo di attività in parallelo specificato dall'utente
  • N = Numero di partizioni cui è necessario accedere per rispondere a una query

Di seguito sono indicate le implicazioni sul comportamento delle query in parallelo per diversi valori di P.

  • (P == 0) => Modalità seriale
  • (P == 1) => Massimo un'attività
  • (P > 1) => Attività in parallelo minime (P, N)
  • (P < 1) => Attività in parallelo minime (N, D)

Ottimizzare le dimensioni della pagina

Quando si esegue una query SQL, i risultati vengono restituiti in modo segmentato se il set di risultati è troppo grande.

Nota

La proprietà MaxItemCount non deve essere usata solo per la paginazione. L'uso principale consiste nel migliorare le prestazioni delle query riducendo il numero massimo di elementi restituiti in una singola pagina.

È anche possibile impostare le dimensioni della pagina usando gli SDK di Azure Cosmos DB disponibili. La proprietà MaxItemCount in QueryRequestOptions consente di impostare il numero massimo di elementi da restituire nell'operazione di enumerazione. Quando MaxItemCount è impostato su -1, l'SDK trova automaticamente il valore ottimale, a seconda delle dimensioni del documento. Ad esempio:

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { 
        PartitionKey = new PartitionKey("Washington"),
        MaxItemCount = 1000}))
{
    // ...
}

Quando viene eseguita una query, i dati risultanti vengono inviati all'interno di un pacchetto TCP. Se si specifica un valore troppo basso per MaxItemCount, il numero di corse necessarie per inviare i dati all'interno del pacchetto TCP è elevato, che influisce sulle prestazioni. Pertanto, se non si è certi del valore da impostare per la proprietà MaxItemCount, è consigliabile impostarlo su -1 e lasciare che l'SDK scelga il valore predefinito.

Ottimizzare le dimensioni del buffer

La query parallela è progettata per la prelettura dei risultati mentre il client elabora il batch di risultati corrente. Questa prelettura consente di migliorare la latenza complessiva di una query. La proprietà MaxBufferedItemCount in QueryRequestOptions limita il numero di risultati prelettura. Impostare MaxBufferedItemCount sul numero previsto di risultati restituiti (o un numero superiore) per consentire alla query di ricevere il massimo vantaggio dal prelettura. Se si imposta questo valore su -1, il sistema determina automaticamente il numero di elementi da memorizzare nel buffer.

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { 
        PartitionKey = new PartitionKey("Washington"),
        MaxBufferedItemCount = -1}))
{
    // ...
}

La prelettura funziona allo stesso modo indipendentemente dal grado di parallelismo ed è presente un singolo buffer per i dati di tutte le partizioni.

Passaggi successivi

Per altre informazioni sulle prestazioni con .NET SDK:

Ridurre le chiamate al piano di query

Per eseguire una query, è necessario compilare un piano di query. Questo in generale rappresenta una richiesta di rete al gateway Azure Cosmos DB, che aggiunge alla latenza dell'operazione di query.

Usare la memorizzazione nella cache del piano di query

Il piano di query, per una query con ambito a una singola partizione, viene memorizzato nella cache nel client. In questo modo si elimina la necessità di effettuare una chiamata al gateway per recuperare il piano di query dopo la prima chiamata. La chiave per il piano di query memorizzato nella cache è la stringa di query SQL. È necessario assicurarsi che la query sia parametrizzata. In caso contrario, la ricerca nella cache del piano di query spesso sarà un mancato riscontro nella cache perché è improbabile che la stringa di query sia identica tra le chiamate. La memorizzazione nella cache del piano di query è abilitata per impostazione predefinita per Java SDK versione 4.20.0 e successive e per Spring Data Azure Cosmos DB SDK versione 3.13.0 e successive.

Usare query a partizione singola parametrizzate

Per le query con parametrizzate con ambito a una chiave di partizione con setPartitionKey in CosmosQueryRequestOptions che non contengono aggregazioni (inclusi Distinct, DCount, Group By), è possibile evitare il piano di query:

CosmosQueryRequestOptions options = new CosmosQueryRequestOptions();
options.setPartitionKey(new PartitionKey("Washington"));

ArrayList<SqlParameter> paramList = new ArrayList<SqlParameter>();
paramList.add(new SqlParameter("@city", "Seattle"));
SqlQuerySpec querySpec = new SqlQuerySpec(
        "SELECT * FROM c WHERE c.city = @city",
        paramList);

//  Sync API
CosmosPagedIterable<MyItem> filteredItems = 
    container.queryItems(querySpec, options, MyItem.class);

//  Async API
CosmosPagedFlux<MyItem> filteredItems = 
    asyncContainer.queryItems(querySpec, options, MyItem.class);

Nota

Le query tra partizioni richiedono all'SDK di visitare tutte le partizioni esistenti per verificare la presenza di risultati. Più partizioni fisiche contiene il contenitore, più può essere lento.

Ottimizzare il grado di parallelismo

Le query parallele funzionano eseguendo in parallelo le query su più partizioni. Tuttavia, i dati di un singolo contenitore partizionato vengono recuperati in modo seriale rispetto alla query. Usare quindi setMaxDegreeOfParallelism su CosmosQueryRequestOptions per impostare il valore sul numero di partizioni disponibili. Se non si conosce il numero di partizioni, è possibile impostare il valore setMaxDegreeOfParallelism su un numero elevato. Il sistema sceglie il numero minimo (numero di partizioni, input specificato dall'utente) come livello di parallelismo massimo. L'impostazione del valore su -1 consentirà all'SDK di decidere la concorrenza ottimale.

È importante notare che le query parallele producono i vantaggi migliori se i dati sono distribuiti uniformemente tra tutte le partizioni per quanto riguarda la query. Se il contenitore è partizionato in modo che tutti o la maggior parte dei dati restituiti da una query vengano concentrati in poche partizioni (una sola partizione nel peggiore dei casi), le prestazioni della query risulteranno ridotte.

CosmosQueryRequestOptions options = new CosmosQueryRequestOptions();
options.setPartitionKey(new PartitionKey("Washington"));
options.setMaxDegreeOfParallelism(-1);

// Define the query

//  Sync API
CosmosPagedIterable<MyItem> filteredItems = 
    container.queryItems(querySpec, options, MyItem.class);

//  Async API
CosmosPagedFlux<MyItem> filteredItems = 
    asyncContainer.queryItems(querySpec, options, MyItem.class);

Si supponga che

  • D = numero massimo predefinito di attività in parallelo (= numero totale di processori nel computer client)
  • P = Numero massimo di attività in parallelo specificato dall'utente
  • N = Numero di partizioni cui è necessario accedere per rispondere a una query

Di seguito sono indicate le implicazioni sul comportamento delle query in parallelo per diversi valori di P.

  • (P == 0) => Modalità seriale
  • (P == 1) => Massimo un'attività
  • (P > 1) => Attività in parallelo minime (P, N)
  • (P == 1) => Attività in parallelo minime (N, D)

Ottimizzare le dimensioni della pagina

Quando si esegue una query SQL, i risultati vengono restituiti in modo segmentato se il set di risultati è troppo grande. Per impostazione predefinita, i risultati vengono restituiti in blocchi di 100 elementi o 4 MB, a seconda del limite che viene raggiunto prima. L'aumento delle dimensioni della pagina ridurrà il numero di round trip necessari e aumenterà le prestazioni per le query che restituiscono più di 100 elementi. Se non si è certi del valore da impostare, 1000 è in genere una scelta ottimale. L'utilizzo della memoria aumenterà man mano che aumentano le dimensioni della pagina, quindi se il carico di lavoro è sensibile alla memoria, considerare un valore inferiore.

È possibile usare il parametro pageSize in iterableByPage() per l'API di sincronizzazione e byPage() per l'API asincrona, per definire le dimensioni della pagina:

//  Sync API
Iterable<FeedResponse<MyItem>> filteredItemsAsPages =
    container.queryItems(querySpec, options, MyItem.class).iterableByPage(continuationToken,pageSize);

for (FeedResponse<MyItem> page : filteredItemsAsPages) {
    for (MyItem item : page.getResults()) {
        //...
    }
}

//  Async API
Flux<FeedResponse<MyItem>> filteredItemsAsPages =
    asyncContainer.queryItems(querySpec, options, MyItem.class).byPage(continuationToken,pageSize);

filteredItemsAsPages.map(page -> {
    for (MyItem item : page.getResults()) {
        //...
    }
}).subscribe();

Ottimizzare le dimensioni del buffer

La query parallela è progettata per la prelettura dei risultati mentre il client elabora il batch di risultati corrente. La prelettura consente il miglioramento complessivo della latenza di una query. setMaxBufferedItemCount in CosmosQueryRequestOptions consente di limitare il numero di risultati di prelettura. Per ottimizzare il prelettura, impostare maxBufferedItemCount su un numero superiore rispetto a pageSize (NOTA: questo può comportare un utilizzo elevato della memoria). Per ridurre al minimo il prelettura, impostare maxBufferedItemCount uguale a pageSize. Se si imposta questo valore su -0, il sistema determina automaticamente il numero di elementi da memorizzare nel buffer.

CosmosQueryRequestOptions options = new CosmosQueryRequestOptions();
options.setPartitionKey(new PartitionKey("Washington"));
options.setMaxBufferedItemCount(-1);

// Define the query

//  Sync API
CosmosPagedIterable<MyItem> filteredItems = 
    container.queryItems(querySpec, options, MyItem.class);

//  Async API
CosmosPagedFlux<MyItem> filteredItems = 
    asyncContainer.queryItems(querySpec, options, MyItem.class);

La prelettura funziona allo stesso modo indipendentemente dal grado di parallelismo ed è presente un singolo buffer per i dati di tutte le partizioni.

Passaggi successivi

Per altre informazioni sulle prestazioni con l'SDK Java:

Ridurre le chiamate al piano di query

Per eseguire una query, è necessario compilare un piano di query. Questo in generale rappresenta una richiesta di rete al gateway Azure Cosmos DB, che aggiunge alla latenza dell'operazione di query. È possibile rimuovere questa richiesta e ridurre la latenza dell'operazione di query a partizione singola. Per le query a partizione singola specificare il valore della chiave di partizione per l'elemento e passarlo come argomento partition_key:

items = container.query_items(
        query="SELECT * FROM r where r.city = 'Seattle'",
        partition_key="Washington"
    )

Ottimizzare le dimensioni della pagina

Quando si esegue una query SQL, i risultati vengono restituiti in modo segmentato se il set di risultati è troppo grande. Il parametro max_item_count consente di impostare il numero massimo di elementi da restituire nell'operazione di enumerazione.

items = container.query_items(
        query="SELECT * FROM r where r.city = 'Seattle'",
        partition_key="Washington",
        max_item_count=1000
    )

Passaggi successivi

Per altre informazioni sull'uso di Python SDK per API per NoSQL:

Ridurre le chiamate al piano di query

Per eseguire una query, è necessario compilare un piano di query. Questo in generale rappresenta una richiesta di rete al gateway Azure Cosmos DB, che aggiunge alla latenza dell'operazione di query. È possibile rimuovere questa richiesta e ridurre la latenza dell'operazione di query a partizione singola. Per le query a partizione singola, la definizione dell'ambito di una query su una singola partizione può essere eseguita in due modi.

Uso di un'espressione di query con parametri e specifica della chiave di partizione nell'istruzione di query. La query è composta a livello di codice per SELECT * FROM todo t WHERE t.partitionKey = 'Bikes, Touring Bikes':

// find all items with same categoryId (partitionKey)
const querySpec = {
    query: "select * from products p where p.categoryId=@categoryId",
    parameters: [
        {
            name: "@categoryId",
            value: "Bikes, Touring Bikes"
        }
    ]
};

// Get items 
const { resources } = await container.items.query(querySpec).fetchAll();

for (const item of resources) {
    console.log(`${item.id}: ${item.name}, ${item.sku}`);
}

In alternativa, specificare partitionKey in FeedOptions e passarlo come argomento:

const querySpec = {
    query: "select * from products p"
};

const { resources } = await container.items.query(querySpec, { partitionKey: "Bikes, Touring Bikes" }).fetchAll();

for (const item of resources) {
    console.log(`${item.id}: ${item.name}, ${item.sku}`);
}

Ottimizzare le dimensioni della pagina

Quando si esegue una query SQL, i risultati vengono restituiti in modo segmentato se il set di risultati è troppo grande. Il parametro maxItemCount consente di impostare il numero massimo di elementi da restituire nell'operazione di enumerazione.

const querySpec = {
    query: "select * from products p where p.categoryId=@categoryId",
    parameters: [
        {
            name: "@categoryId",
            value: items[2].categoryId
        }
    ]
};

const { resources } = await container.items.query(querySpec, { maxItemCount: 1000 }).fetchAll();

for (const item of resources) {
    console.log(`${item.id}: ${item.name}, ${item.sku}`);
}

Passaggi successivi

Per altre informazioni sull'uso di Node.js SDK per l'API per NoSQL: