Dicas de desempenho de consulta para os SDKs do Azure Cosmos DB

APLICA-SE A: NoSQL

O Azure Cosmos DB é um banco de dados distribuído, rápido e flexível que pode ser dimensionado perfeitamente com garantia de níveis de latência e taxa de transferência. Você não precisa fazer grandes alterações de arquitetura nem escrever códigos complexos para dimensionar seu banco de dados com o Azure Cosmos DB. Aumentar e diminuir a escala é tão fácil quanto fazer uma única chamada de API. Para saber mais, consulte Provisionar a taxa de transferência do contêiner ou Provisionar a taxa de transferência do banco de dados.

Reduzir chamadas do Plano de Consulta

Para executar uma consulta, um plano de consulta precisa ser criado. Em geral, isso representa uma solicitação de rede para o Gateway do Azure Cosmos DB, que adiciona à latência da operação de consulta. Há duas maneiras de remover essa solicitação e reduzir a latência da operação de consulta:

Otimizando consultas de partição única com Execução Direta Otimista

O NoSQL do Azure Cosmos DB tem uma otimização chamada ODE (Execução Direta Otimista), que pode melhorar a eficiência de determinadas consultas NoSQL. Especificamente, as consultas que não exigem distribuição incluem aquelas que podem ser executadas em uma única partição física ou que têm respostas que não exigem paginação. As consultas que não exigem distribuição podem ignorar com confiança alguns processos, como geração de plano de consulta do lado do cliente e reescrita de consulta, reduzindo assim a latência de consulta e o custo de RU. Se você especificar a chave de partição na solicitação ou consulta em si (ou tiver apenas uma partição física) e os resultados da consulta não exigirem paginação, o ODE poderá melhorar suas consultas.

Observação

A ODE (Execução Direta Otimista), que oferece melhor desempenho para consultas que não exigem distribuição, não deve ser confundida com o Modo Direto, que é um caminho para conectar seu aplicativo às réplicas de back-end.

A ODE já está disponível e habilitada por padrão na versão 3.38.0 e posterior do SDK do .NET. Quando você executa uma consulta e especifica uma chave de partição na solicitação ou consulta em si, ou seu banco de dados tem apenas uma partição física, sua execução de consulta pode aproveitar os benefícios do ODE. Para desabilitar o ODE, defina EnableOptimisticDirectExecution como false em QueryRequestOptions.

Consultas de partição única que apresentam funções GROUP BY, ORDER BY, DISTINCT e agregação (como soma, média, mín. e máx) podem se beneficiar significativamente do uso do ODE. No entanto, em cenários em que a consulta está direcionando várias partições ou ainda requer paginação, a latência da resposta da consulta e do custo de RU pode ser maior do que sem usar o ODE. Portanto, ao usar o ODE, recomendamos que você:

  • Especifique a chave de partição na chamada ou na própria consulta.
  • Verifique se o tamanho dos dados não cresceu e fez com que a partição se dividisse.
  • Verifique se os resultados da consulta não exigem paginação para obter o benefício completo do ODE.

Aqui estão alguns exemplos de consultas simples de partição única que podem se beneficiar do 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 

Pode haver casos em que consultas de partição única ainda podem exigir distribuição se o número de itens de dados aumentar ao longo do tempo e o banco de dados do Azure Cosmos DB dividir a partição. Exemplos de consultas em que isso pode ocorrer incluem:

- 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 

Algumas consultas complexas sempre podem exigir distribuição, mesmo se forem direcionadas a uma única partição. Exemplos dessas consultas incluem:

- 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 observar que o ODE pode nem sempre recuperar o plano de consulta e, como resultado, não é capaz de não permitir ou desativar consultas sem suporte. Por exemplo, após a divisão da partição, essas consultas não são mais qualificadas para ODE e, portanto, não serão executadas porque a avaliação do plano de consulta do lado do cliente bloqueará essas consultas. Para garantir a compatibilidade/continuidade do serviço, é essencial garantir que apenas as consultas que têm suporte total em cenários sem ODE (ou seja, elas executem e produzam o resultado correto no caso geral de várias partições) sejam usadas com o ODE.

Observação

O uso do ODE pode potencialmente fazer com que um novo tipo de token de continuação seja gerado. Esse token não é reconhecido pelos SDKs mais antigos por design e isso pode resultar em uma exceção de token de continuação malformada. Se você tiver um cenário em que os tokens gerados dos SDKs mais recentes são usados por um SDK mais antigo, recomendamos uma abordagem de duas etapas para atualizar:

  • Atualize para o novo SDK e desabilite o ODE, ambos juntos como parte de uma única implantação. Aguarde até que todos os nós sejam atualizados.
    • Para desabilitar o ODE, defina EnableOptimisticDirectExecution como false em QueryRequestOptions.
  • Habilite o ODE como parte da segunda implantação para todos os nós.

Usar a geração do Plano de Consulta local

O SDK do SQL inclui um ServiceInterop.dll nativo para analisar e otimizar as consultas localmente. O ServiceInterop.dll tem suporte apenas na plataforma Windows x64. Os tipos de aplicativos a seguir usam o processamento de host de 32 bits por padrão. Para alterar o processamento do host para o processamento de 64 bits, siga estas etapas, com base no tipo de seu aplicativo:

  • Para aplicativos executáveis, você pode alterar o processamento do host definindo o destino da plataforma como x64 janela Propriedades do Projeto na guia Build.

  • Para projetos de teste baseados em VSTest, você pode alterar o processamento do host selecionando Teste>Configurações de Teste>Arquitetura Padrão do Processador X64 no menu de Teste do Visual Studio.

  • Para aplicativos Web ASP.NET implantados localmente, você pode alterar o processamento do host selecionando Usar a versão de 64 bits do IIS Express para sites e projetos em Ferramentas>Opções>Projetos e soluções>Projetos da Web.

  • Para aplicativos Web ASP.NET implantados no Azure, você pode alterar o processamento do host selecionando a plataforma de 64 bits em Configurações de aplicativo no portal do Azure.

Observação

Por padrão, novos projetos do Visual Studio são definidos para Qualquer CPU. Recomenda-se definir o projeto como x64 para que ele não mude para x86. Um projeto definido como Qualquer CPU pode mudar facilmente para x86 quando uma dependência somente x86 é adicionada.
ServiceInterop.dll precisa estar na pasta em que a DLL do SDK está sendo executada. Isso é importante apenas quando você copia DLLs manualmente ou tem sistemas personalizados de compilação/implantação.

Usar consultas de partição única

Para consultas direcionadas a uma chave de partição pela definição da propriedade PartitionKey em QueryRequestOptions e sem agregação (incluindo Distinct, DCount e Group By). Neste exemplo, o campo de chave de partição de /state é filtrado no valor Washington.

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

Opcionalmente, você pode fornecer a chave de partição como parte do objeto de opções de solicitação.

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

Observação

Consultas entre partições exigem que o SDK visite todas as partições existentes para verificar se há resultados. Quanto mais partições físicas o contêiner tiver, mais lentas elas poderão ficar.

Evitar recriar o iterador desnecessariamente

Quando todos os resultados da consulta forem consumidos pelo componente atual, não é necessário criar novamente o iterador com a continuação para cada página. Sempre prefira esvaziar totalmente a consulta, a menos que a paginação seja controlada por outro componente de chamada:

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
        }
    }
}

Ajustar o grau de paralelismo

Em consultas, ajuste a propriedade MaxConcurrencyQueryRequestOptions para identificar as melhores configurações para seu aplicativo, especialmente se você executar consultas entre partições (sem um filtro no valor da chave de partição). MaxConcurrency controla o número máximo de tarefas paralelas, ou seja, o número máximo de partições a serem visitadas paralelamente. A definição do valor como -1 permitirá que o SDK decida a simultaneidade ideal.

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

Vamos supor que

  • D = número máximo padrão de tarefas paralelas (= número total dos processadores no computador cliente)
  • P = número máximo especificado pelo usuário de tarefas paralelas
  • N = número de partições que precisam ser visitadas para responder a uma consulta

Veja a seguir as implicações de como as consultas paralelas se comportariam para diferentes valores de P.

  • (P == 0) => Modo serial
  • (P == 1) => Máximo de uma tarefa
  • (P > 1) => Mín. (P, N) de tarefas paralelas
  • (P < 1) => Mín. (N, D) de tarefas paralelas

Ajustar o tamanho da página

Quando você envia uma consulta SQL, os resultados serão retornados de maneira segmentada se o conjunto de resultados for muito grande.

Observação

A propriedade MaxItemCount não deve ser usada apenas para paginação. Seu uso principal é aprimorar o desempenho das consultas reduzindo o número máximo de itens retornados em uma só página.

Você também pode definir o tamanho da página usando os SDKs do Azure Cosmos DB disponíveis. A propriedade MaxItemCount no QueryRequestOptions permite que você defina o número máximo de itens a serem retornados na operação de enumeração. Quando MaxItemCount é definido como -1, o SDK localiza automaticamente o valor ideal, dependendo do tamanho do documento. Por exemplo:

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

Quando uma consulta é executada, os dados resultantes são enviados dentro de um pacote TCP. Se você especificar um valor muito baixo para MaxItemCount, o número de viagens necessárias para enviar os dados no pacote TCP será alto, o que afetará o desempenho. Sendo assim, se você não tem certeza de qual valor definir para a propriedade MaxItemCount, é melhor defini-lo como -1 e permitir que o SDK escolha o valor padrão.

Ajustar o tamanho do buffer

A consulta paralela destina-se a buscar previamente resultados enquanto o lote atual de resultados está sendo processado pelo cliente. A pré-busca ajuda a aprimorar a latência geral de uma consulta. A propriedade MaxBufferedItemCount em QueryRequestOptions limita o número de resultados com busca prévia. Configurar MaxBufferedItemCount para o número esperado de resultados retornados (ou um número mais alto) permite que a consulta aproveite ao máximo a pré-busca. Se você definir esse valor como -1, o sistema determinará automaticamente o número de itens para armazenar em buffer.

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

A pré-busca funciona sempre da mesma forma, independentemente do paralelismo, e há um único buffer para os dados de todas as partições.

Próximas etapas

Para saber mais sobre o desempenho usando o SDK do .NET:

Reduzir chamadas do Plano de Consulta

Para executar uma consulta, um plano de consulta precisa ser criado. Em geral, isso representa uma solicitação de rede para o Gateway do Azure Cosmos DB, que adiciona à latência da operação de consulta.

Usar o plano caching de consulta

O plano de consulta, para uma consulta com escopo para uma única partição, é armazenado em cache no cliente. Isso elimina a necessidade de fazer uma chamada para o gateway para recuperar o plano de consulta após a primeira chamada. A chave do plano de consulta em cache é a cadeia de caracteres da consulta SQL. Você precisa verificar se a consulta é parametrizada. Caso contrário, a pesquisa de cache do plano de consulta geralmente será um erro de cache, pois a cadeia de caracteres de consulta provavelmente será idêntica em todas as chamadas. O cache do plano de consulta é habilitado por padrão para o Java SDK versão 4.20.0 e superior e para o SDK Spring Data Azure Cosmos DB versão 3.13.0 e superior.

Usar consultas de partição única parametrizadas

Para consultas parametrizadas que têm o escopo de uma chave de partição com setPartitionKey no CosmosQueryRequestOptions e não contêm agregações (incluindo Distinct, DCount, Group By), o plano de consulta pode ser evitado:

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);

Observação

Consultas entre partições exigem que o SDK visite todas as partições existentes para verificar se há resultados. Quanto mais partições físicas o contêiner tiver, mais lentas elas poderão ficar.

Ajustar o grau de paralelismo

As consultas paralelas funcionam consultando várias partições em paralelo. No entanto, os dados de um contêiner particionado individual são buscados em série com relação à consulta. Portanto, use setMaxDegreeOfParallelism no CosmosQueryRequestOptions para definir o valor do número de partições que você possui. Se você não souber o número de partições, poderá usar setMaxDegreeOfParallelism para definir um número alto, e o sistema escolherá o mínimo (número de partições, entrada fornecida pelo usuário) como o grau máximo de paralelismo. A definição do valor como -1 permitirá que o SDK decida a simultaneidade ideal.

É importante observar que as consultas paralelas produzirão os melhores benefícios se os dados forem distribuídos uniformemente em todas as partições com relação à consulta. Se o contêiner particionado for particionado de uma forma que todos ou a maioria dos dados retornados por uma consulta ficarem concentrados em algumas partições (uma partição, na pior das hipóteses), o desempenho da consulta seria degradado.

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);

Vamos supor que

  • D = número máximo padrão de tarefas paralelas (= número total dos processadores no computador cliente)
  • P = número máximo especificado pelo usuário de tarefas paralelas
  • N = número de partições que precisam ser visitadas para responder a uma consulta

Veja a seguir as implicações de como as consultas paralelas se comportariam para diferentes valores de P.

  • (P == 0) => Modo serial
  • (P == 1) => Máximo de uma tarefa
  • (P > 1) => Mín. (P, N) de tarefas paralelas
  • (P == -1) => Min (N, D) de tarefas paralelas

Ajustar o tamanho da página

Quando você envia uma consulta SQL, os resultados serão retornados de maneira segmentada se o conjunto de resultados for muito grande. Por padrão, os resultados são retornados em blocos de 100 itens ou 4 MB, o limite que for atingido primeiro. Aumentar o tamanho da página reduzirá o número de viagens de ida e volta necessárias e aumentará o desempenho de consultas que retornam mais de 100 itens. Se você não tiver certeza de qual valor definir, 1000 normalmente é uma boa opção. O consumo de memória aumentará à medida que o tamanho da página aumentar, portanto, se a carga de trabalho diferenciar memória, considere um valor menor.

Você pode usar o parâmetro pageSize no iterableByPage() para API síncrona e byPage() para a API assíncrona, para definir um tamanho de página:

//  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();

Ajustar o tamanho do buffer

A consulta paralela destina-se a buscar previamente resultados enquanto o lote atual de resultados está sendo processado pelo cliente. A busca prévia ajuda a melhorar a latência geral de uma consulta. setMaxBufferedItemCount em CosmosQueryRequestOptions limita o número de resultados pré-buscados. Para maximizar a pré-busca, defina a maxBufferedItemCount com um número maior do que o pageSize (OBSERVAÇÃO: isso também pode resultar em um alto consumo de memória). Para minimizar a pré-busca, defina a maxBufferedItemCount como igual ao pageSize. Se você definir esse valor como 0, o sistema determinará automaticamente o número de itens para armazenar em 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);

A pré-busca funciona sempre da mesma forma, independentemente do paralelismo, e há um único buffer para os dados de todas as partições.

Próximas etapas

Para saber mais sobre o desempenho usando o SDK do Java:

Reduzir chamadas do Plano de Consulta

Para executar uma consulta, um plano de consulta precisa ser criado. Em geral, isso representa uma solicitação de rede para o Gateway do Azure Cosmos DB, que adiciona à latência da operação de consulta. Há uma maneira de remover essa solicitação e reduzir a latência da operação de consulta de partição única. Para consultas de partição única, especifique o valor da chave de partição para o item e passe-o como argumento partition_key:

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

Ajustar o tamanho da página

Quando você envia uma consulta SQL, os resultados serão retornados de maneira segmentada se o conjunto de resultados for muito grande. A propriedade max_item_count permite que você defina o número máximo de itens a serem retornados na operação de enumeração.

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

Próximas etapas

Para saber mais sobre como usar o SDK do Python para API para NoSQL:

Reduzir chamadas do Plano de Consulta

Para executar uma consulta, um plano de consulta precisa ser criado. Em geral, isso representa uma solicitação de rede para o Gateway do Azure Cosmos DB, que adiciona à latência da operação de consulta. Há uma maneira de remover essa solicitação e reduzir a latência da operação de consulta de partição única. Para consultas de partição única, o escopo de uma consulta para uma única partição pode ser feito de duas maneiras.

Usando uma expressão de consulta parametrizada e especificando a chave de partição na instrução de consulta. A consulta é composta programaticamente para 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}`);
}

Ou especifique partitionKey em FeedOptions e passe-o como argumento:

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}`);
}

Ajustar o tamanho da página

Quando você envia uma consulta SQL, os resultados serão retornados de maneira segmentada se o conjunto de resultados for muito grande. A propriedade MaxItemCount no permite que você defina o número máximo de itens a serem retornados na operação de enumeração.

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}`);
}

Próximas etapas

Para saber mais sobre como usar o SDK do Node.js para API para NoSQL: