Dicas de desempenho de consulta para 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 níveis garantidos de latência e taxa de transferência. Você não precisa fazer grandes alterações na arquitetura ou escrever código complexo para dimensionar seu banco de dados com o Azure Cosmos DB. Escalar para cima e para baixo é tão fácil quanto fazer uma única chamada de API. Para saber mais, consulte Provisionar taxa de transferência de contêiner ou Provisionar taxa de transferência de 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 aumenta a 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 Azure Cosmos DB NoSQL tem uma otimização chamada Optimistic Direct Execution (ODE), 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 a geração de planos de consulta do lado do cliente e a regravação de consultas, reduzindo assim a latência da consulta e o custo da RU. Se você especificar a chave de partição na própria solicitação ou consulta (ou tiver apenas uma partição física) e os resultados da sua consulta não exigirem paginação, o ODE poderá melhorar suas consultas.

Nota

O ODE (Optimistic Direct Execution), que oferece melhor desempenho para consultas que não exigem distribuição, não deve ser confundido com o Modo Direto, que é um caminho para conectar seu aplicativo a réplicas de back-end.

O ODE agora está disponível e habilitado por padrão no .NET SDK versão 3.38.0 e posterior. Quando você executa uma consulta e especifica uma chave de partição na própria solicitação ou consulta, 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, min e max) podem se beneficiar significativamente do uso do ODE. No entanto, em cenários em que a consulta está direcionada a várias partições ou ainda requer paginação, a latência da resposta da consulta e o custo da RU podem ser maiores do que sem o uso do ODE. Portanto, ao usar o ODE, recomendamos:

  • Especifique a chave de partição na própria chamada ou consulta.
  • Certifique-se de que o tamanho dos dados não aumentou e causou a divisão da partição.
  • Certifique-se de que os resultados da sua consulta não exijam paginação para obter todos os benefícios 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 com o tempo e seu 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 podem sempre exigir distribuição, mesmo que tenham como alvo uma única partição. Exemplos de tais 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 nem sempre recupera 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 as bloqueará. Para garantir a compatibilidade/continuidade do serviço, é fundamental garantir que apenas as consultas que são totalmente suportadas em cenários sem ODE (ou seja, executam e produzem o resultado correto no caso geral de várias partições) são usadas com o ODE.

Nota

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 a partir dos SDKs mais recentes são usados por um SDK mais antigo, recomendamos uma abordagem de 2 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 atualizem.
    • 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 SQL SDK inclui uma ServiceInterop.dll nativa para analisar e otimizar consultas localmente. ServiceInterop.dll é suportado apenas na plataforma Windows x64 . Os seguintes tipos de aplicativos usam 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 do seu aplicativo:

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

  • Para projetos de teste baseados em VSTest, você pode alterar o processamento do host selecionando Test>Settings>Default Processor Architecture como X64 no menu 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 Opções de Ferramentas>>Projetos e Soluções> Projetos da Web.

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

Nota

Por padrão, novos projetos do Visual Studio são definidos como Qualquer CPU. Recomendamos que você defina seu projeto como x64 para que ele não alterne para x86. Um projeto definido como Qualquer CPU pode facilmente alternar para x86 se uma dependência somente x86 for adicionada.
ServiceInterop.dll precisa estar na pasta a partir da qual a DLL do SDK está sendo executada. Isso deve ser uma preocupação somente se você copiar manualmente DLLs ou tiver sistemas de compilação/implantação personalizados.

Usar consultas de partição única

Para consultas que visam uma Chave de Partição definindo a propriedade PartitionKey e QueryRequestOptions não contêm agregações (incluindo Distinct, DCount, Group By). Neste exemplo, o campo de chave de partição de é filtrado /state 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")}))
{
    // ...
}

Nota

As consultas entre partições exigem que o SDK visite todas as partições existentes para verificar os resultados. Quanto mais partições físicas o contêiner tiver, mais lentas elas podem ser.

Evite recriar o iterador desnecessariamente

Quando todos os resultados da consulta são consumidos pelo componente atual, você não precisa recriar o iterador com a continuação para cada página. Sempre prefira drenar a consulta completamente, 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
        }
    }
}

Sintonize o grau de paralelismo

Para consultas, ajuste a propriedade MaxConcurrency para QueryRequestOptions 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 máximo de partições a serem visitadas em paralelo. Definir o 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 = Padrão Número máximo de tarefas paralelas (= número total de processadores na máquina cliente)
  • P = Número máximo de tarefas paralelas especificado pelo utilizador
  • N = Número de partições que precisam ser visitadas para responder a uma consulta

A seguir estão 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) => Tarefas paralelas Min (P, N)
  • (P < 1) => Tarefas paralelas Min (N, D)

Ajustar o tamanho da página

Quando você emite uma consulta SQL, os resultados são retornados de forma segmentada se o conjunto de resultados for muito grande.

Nota

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

Você também pode definir o tamanho da página usando os SDKs do Azure Cosmos DB disponíveis. A propriedade MaxItemCount em QueryRequestOptions permite definir 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 dentro do pacote TCP será alto, o que afetará o desempenho. Portanto, se você não tiver certeza de qual valor definir para a MaxItemCount propriedade, é melhor defini-lo como -1 e deixar o SDK escolher o valor padrão.

Ajuste o tamanho do buffer

A consulta paralela é projetada para pré-buscar resultados enquanto o lote atual de resultados está sendo processado pelo cliente. Essa pré-busca ajuda a melhorar a latência geral de uma consulta. A propriedade MaxBufferedItemCount limita QueryRequestOptions o número de resultados pré-buscados. Defina MaxBufferedItemCount como o número esperado de resultados retornados (ou um número maior) para permitir que a consulta receba o máximo benefício da pré-busca. Se você definir esse valor como -1, o sistema determinará automaticamente o número de itens a serem armazenados 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 da mesma forma, independentemente do grau de paralelismo, e há um único buffer para os dados de todas as partições.

Próximos passos

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 aumenta a latência da operação de consulta.

Usar cache do Plano 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 para o plano de consulta armazenado em cache é a cadeia de caracteres de consulta SQL. Você precisa ter certeza de que a consulta está parametrizada. Caso contrário, a pesquisa de cache do plano de consulta geralmente será uma falha de cache, pois é improvável que a cadeia de caracteres de consulta seja idêntica entre chamadas. O cache do plano de consulta é habilitado por padrão para Java SDK versão 4.20.0 e superior e para Spring Data Azure Cosmos DB SDK versão 3.13.0 e superior.

Usar consultas de partição única parametrizadas

Para consultas parametrizadas que têm como escopo uma chave de partição com setPartitionKey e CosmosQueryRequestOptions 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);

Nota

As consultas entre partições exigem que o SDK visite todas as partições existentes para verificar os resultados. Quanto mais partições físicas o contêiner tiver, mais lentas elas podem ser.

Sintonize 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 obtidos em série em relação à consulta. Então, use setMaxDegreeOfParallelism para CosmosQueryRequestOptions definir o valor para o número de partições que você tem. Se você não sabe o número de partições, você pode usar setMaxDegreeOfParallelism para definir um número alto, e o sistema escolhe o mínimo (número de partições, entrada fornecida pelo usuário) como o grau máximo de paralelismo. Definir o valor como -1 permitirá que o SDK decida a simultaneidade ideal.

É importante notar que as consultas paralelas produzem os melhores benefícios se os dados forem distribuídos uniformemente em todas as partições em relação à consulta. Se o contêiner particionado for particionado de tal forma que todos ou a maioria dos dados retornados por uma consulta estejam concentrados em algumas partições (uma partição na pior das hipóteses), o desempenho da consulta será 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 = Padrão Número máximo de tarefas paralelas (= número total de processadores na máquina cliente)
  • P = Número máximo de tarefas paralelas especificado pelo utilizador
  • N = Número de partições que precisam ser visitadas para responder a uma consulta

A seguir estão 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) => Tarefas paralelas Min (P, N)
  • (P == -1) => Min (N, D) tarefas paralelas

Ajustar o tamanho da página

Quando você emite uma consulta SQL, os resultados são retornados de forma segmentada se o conjunto de resultados for muito grande. Por padrão, os resultados são retornados em partes de 100 itens ou 4 MB, o limite 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 tem certeza de qual valor definir, 1000 normalmente é uma boa escolha. O consumo de memória aumentará à medida que o tamanho da página aumentar, portanto, se sua carga de trabalho for sensível à memória, considere um valor menor.

Você pode usar o pageSize parâmetro em iterableByPage() para API de sincronização e byPage() para 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();

Ajuste o tamanho do buffer

A consulta paralela é projetada para pré-buscar resultados enquanto o lote atual de resultados está sendo processado pelo cliente. A pré-busca ajuda na melhoria geral da latência de uma consulta. setMaxBufferedItemCount limita CosmosQueryRequestOptions o número de resultados pré-buscados. Para maximizar a pré-busca, defina o maxBufferedItemCount para um número maior do que o pageSize (NOTA: Isso também pode resultar em alto consumo de memória). Para minimizar a pré-busca, defina o maxBufferedItemCount igual ao pageSize. Se você definir esse valor como 0, o sistema determinará automaticamente o número de itens a serem armazenados 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 da mesma forma, independentemente do grau de paralelismo, e há um único buffer para os dados de todas as partições.

Próximos passos

Para saber mais sobre o desempenho usando o Java SDK:

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 aumenta a 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 partition_key argumento:

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

Ajustar o tamanho da página

Quando você emite uma consulta SQL, os resultados são retornados de forma segmentada se o conjunto de resultados for muito grande. O max_item_count permite definir 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óximos passos

Para saber mais sobre como usar o Python SDK for API for 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 aumenta a 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 realizado 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 e FeedOptions 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ê emite uma consulta SQL, os resultados são retornados de forma segmentada se o conjunto de resultados for muito grande. O maxItemCount permite definir 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óximos passos

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