Sugerencias de rendimiento de consultas para los SDK de Azure Cosmos DB

SE APLICA A: NoSQL

Azure Cosmos DB es una base de datos distribuida, rápida y flexible que se escala sin problemas con niveles de latencia y rendimiento garantizados. No es necesario realizar cambios de arquitectura importantes ni escribir el código complejo para escalar la base de datos con Azure Cosmos DB. Escalar o reducir verticalmente es tan sencillo como realizar una única llamada API. Para más información, consulte Aprovisionamiento del rendimiento del contenedor o Aprovisionamiento del rendimiento de la base de datos.

Reducción de las llamadas al plan de consulta

Para ejecutar una consulta, es necesario crear un plan de consulta. En general, esto representa una solicitud de red a la puerta de enlace de Azure Cosmos DB, que se agrega a la latencia de la operación de consulta. Hay dos maneras de quitar esta solicitud y reducir la latencia de la operación de consulta:

Optimización de consultas de partición única con ejecución directa optimista

Azure Cosmos DB NoSQL tiene una optimización denominada Ejecución directa optimista (ODE), que puede mejorar la eficacia de determinadas consultas NoSQL. En concreto, las consultas que no requieren distribución incluyen aquellas que pueden ejecutarse en una única partición física o que tienen respuestas que no requieren paginación. Las consultas que no requieren distribución pueden omitir con confianza algunos procesos, como la generación del plan de consulta del lado cliente y la reescritura de consultas, lo que reduce la latencia de las consultas y el costo de RU. Si especifica la clave de partición en la propia solicitud o consulta (o solo tiene una partición física), y los resultados de su consulta no requieren paginación, entonces ODE puede mejorar sus consultas.

Nota:

Optimistic Direct Execution (ODE), que ofrece un rendimiento mejorado para las consultas que no requieren distribución, no debe confundirse con el Modo directo, que es una ruta de acceso para conectar la aplicación a las réplicas de back-end.

ODE ya está disponible y habilitado de forma predeterminada en el SDK de .NET, versión 3.38.0 y posteriores. Al ejecutar una consulta y especificar una clave de partición en la propia solicitud o consulta, o la base de datos solo tiene una partición física, la ejecución de la consulta puede aprovechar las ventajas de ODE. Para deshabilitar ODE, establezca EnableOptimisticDirectExecution en false en QueryRequestOptions.

Las consultas de partición única que incluyen funciones GROUP BY, ORDER BY, DISTINCT y agregación (como sum, mean, min y max) pueden beneficiarse significativamente del uso de ODE. Sin embargo, en escenarios en los que la consulta tiene como destino varias particiones o todavía requiere paginación, la latencia de la respuesta de consulta y el costo de RU podría ser mayor que sin usar ODE. Por lo tanto, al usar ODE, se recomienda:

  • Especifique la clave de partición en la propia llamada o consulta.
  • Asegúrese de que el tamaño de sus datos no ha crecido y ha provocado la división de la partición.
  • Asegúrese de que los resultados de la consulta no requieren paginación para obtener la ventaja completa del ODE.

Estos son algunos ejemplos de consultas sencillas de partición única que pueden beneficiarse de 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 

Puede haber casos en los que las consultas de partición única pueden seguir necesitando distribución si el número de elementos de datos aumenta con el tiempo y la base de datos de Azure Cosmos DB divide la partición. Entre los ejemplos de consultas en las que se podría producir esto se incluyen:

- 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 

Algunas consultas complejas siempre pueden requerir distribución, incluso si tienen como destino una sola partición. Algunos ejemplos de consultas simples son:

- 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 

Es importante tener en cuenta que es posible que ODE no siempre recupere el plan de consulta y, como resultado, no pueda permitir ni desactivar las consultas no admitidas. Por ejemplo, después de la división de particiones, estas consultas ya no son aptas para ODE y, por lo tanto, no se ejecutarán porque la evaluación del plan de consulta del lado cliente bloqueará esas. Para garantizar la compatibilidad o la continuidad del servicio, es fundamental asegurarse de que solo se usan consultas totalmente compatibles en escenarios sin ODE (es decir, ejecutan y generan el resultado correcto en el caso general de varias particiones) con ODE.

Nota:

El uso de ODE puede provocar que se genere un nuevo tipo de token de continuación. Los SDK más antiguos no reconocen este token por diseño y esto podría dar lugar a una excepción de token de continuación con formato incorrecto. Si tiene un escenario en el que un SDK más antiguo usa tokens generados a partir de SDK más recientes, se recomienda un enfoque de 2 pasos para la actualización:

  • Actualice al nuevo SDK y deshabilite ODE, ambos juntos como parte de una sola implementación. Espere a que todos los nodos se actualicen.
    • Para deshabilitar ODE, establezca EnableOptimisticDirectExecution en false en QueryRequestOptions.
  • Habilite ODE como parte de la segunda implementación para todos los nodos.

Uso de la generación del plan de consulta local

El SDK de SQL incluye un archivo ServiceInterop.dll nativo para analizar y optimizar consultas localmente. ServiceInterop.dll solo se admite en la plataforma Windows x64. Los siguientes tipos de aplicaciones utilizan el procesamiento de host de 32 bits de forma predeterminada. Para cambiar el procesamiento de host al procesamiento de 64 bits, siga estos pasos según el tipo de la aplicación:

  • En el caso de las aplicaciones ejecutables, puede cambiar el procesamiento de host estableciendo el destino de la plataforma en x64 en la ventana Propiedades del proyecto, en la pestaña Compilar.

  • Para proyectos de prueba basados en VSTest, puede cambiar el procesamiento del host seleccionando Prueba>Configuración de prueba>Arquitectura de procesador predeterminada como X64 en el menú de Prueba de Visual Studio.

  • En el caso de las aplicaciones Web de ASP.NET implementadas de forma local, puede cambiar el procesamiento de host seleccionando Usar la versión de 64 bits de IIS Express para proyectos y sitios web en Herramientas>Opciones>Proyectos y Proyectos>de Soluciones Web.

  • En el caso de las aplicaciones Web de ASP.NET implementadas en Azure, puede cambiar el procesamiento de host seleccionando la plataforma de 64 bits en Configuración de la aplicación en el Azure Portal.

Nota

De forma predeterminada, los nuevos proyectos de Visual Studio se establecen en cualquier CPU. Se recomienda establecer el proyecto en x64 para que no cambie a x86. Un proyecto establecido para cualquier CPU puede cambiar fácilmente a x86 si se agrega una dependencia de solo x86.
ServiceInterop.dll debe estar en la carpeta desde la que se ejecuta la DLL del SDK. Esto solo debe ser un problema si se copian manualmente los archivos DLL o los sistemas de compilación o implementación personalizados.

Uso de consultas de partición única

Para las consultas que tienen como destino una clave de partición estableciendo la propiedad PartitionKey en QueryRequestOptions y no contienen agregaciones (incluidas Distinct, DCount, Group By). En este ejemplo, el campo de clave de partición de /state se filtra en el valor Washington.

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

Opcionalmente, puede proporcionar la clave de partición como parte del objeto de opciones de solicitud.

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

Nota

Las consultas entre particiones requieren que el SDK visite todas las particiones existentes para comprobar los resultados. Cuantas más particiones físicas tenga el contenedor, más lentas pueden ser potencialmente.

Evite volver a crear el enumerador innecesariamente

Cuando el componente actual consume todos los resultados de la consulta, no es necesario volver a crear el enumerador con la continuación de cada página. Siempre es preferible purgar la consulta por completo a menos que otro componente de llamada controle la paginación:

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

Ajuste del grado de paralelismo

Para las consultas, ajuste la propiedad MaxConcurrency en QueryRequestOptions para identificar las mejores configuraciones para su aplicación, en especial si realiza consultas entre particiones (sin un filtro en el valor de clave de partición). MaxConcurrency controla el número máximo de tareas en paralelo, es decir, el número máximo de particiones que se van a visitar en paralelo. Establecer el valor en -1 permitirá al SDK decidir la simultaneidad óptima.

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

Supongamos que

  • D = número máximo predeterminado de tareas en paralelo (= número total de procesadores en el equipo cliente)
  • P = número máximo especificado por el usuario de tareas en paralelo
  • N = número de particiones que se necesita visitar para responder a una consulta

A continuación se muestran las distintas implicaciones de cómo se comportarán las consultas en paralelo para valores diferentes de P.

  • (P == 0) => modo serie
  • (P == 1) => un máximo de una tarea
  • (P > 1) => mínimo de tareas en paralelo (P, N)
  • (P < 1) => mínimo de tareas en paralelo (N, D)

Ajuste del tamaño de página

Cuando emite una consulta SQL, los resultados se devuelven de forma segmentada si el conjunto de resultados es demasiado grande.

Nota

La propiedad MaxItemCount no se debe utilizar solo para la paginación. Su uso principal es mejorar el rendimiento de las consultas reduciendo el número máximo de elementos devueltos en una sola página.

También puede establecer el tamaño de página mediante los SDK de Azure Cosmos DB disponibles. La propiedad MaxItemCount de QueryRequestOptions permite establecer el número máximo de elementos que se devolverán en la operación de enumeración. Cuando MaxItemCount se establece en-1, el SDK busca automáticamente el valor óptimo, en función del tamaño del documento. Por ejemplo:

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

Cuando se ejecuta una consulta, se envían los datos resultantes en un paquete TCP. Si especifica un valor demasiado bajo para MaxItemCount, el número de viajes necesarios para enviar los datos dentro del paquete TCP es alto, lo que afecta al rendimiento. Por lo tanto, si no está seguro del valor que se va a establecer para la propiedad MaxItemCount, es mejor establecerlo en -1 y dejar que el SDK elija el valor predeterminado.

Ajuste del tamaño de búfer

Las consultas en paralelo están diseñadas para capturar previamente los resultados mientras el cliente procesa el lote actual de resultados. Esta captura previa ayuda a mejorar la latencia general de una consulta. La propiedad MaxBufferedItemCount en QueryRequestOptions limita el número de resultados capturados previamente. Establezca MaxBufferedItemCount en el número esperado de resultados devueltos (o un número más alto) para permitir que la consulta reciba el máximo beneficio de la captura previa. Si establece este valor en -1, el sistema determinará automáticamente el número de elementos que se van a almacenar en búfer.

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

La captura previa funciona de la misma manera, independientemente del grado de paralelismo y hay un único búfer para los datos de todas las particiones.

Pasos siguientes

Para obtener más información sobre el rendimiento mediante el SDK de .NET:

Reducción de las llamadas al plan de consulta

Para ejecutar una consulta, es necesario crear un plan de consulta. En general, esto representa una solicitud de red a la puerta de enlace de Azure Cosmos DB, que se agrega a la latencia de la operación de consulta.

Uso del almacenamiento en caché del plan de consulta

Para una consulta en el ámbito de una partición única, el plan de consulta se almacena en caché en el cliente. Esto elimina la necesidad de realizar una llamada a la puerta de enlace para recuperar el plan de consulta después de la primera llamada. La clave del plan de consulta almacenado en caché es la cadena de la consulta SQL. Debe asegurarse de que la consulta esté parametrizada. Si no es así, la búsqueda de caché del plan de consulta a menudo será una línea no ejecutada de caché, ya que es poco probable que la cadena de consulta sea idéntica en todas las llamadas. El almacenamiento en caché del plan de consulta está habilitado de forma predeterminada para el SDK de Java versión 4.20.0 y posteriores y para el SDK de Spring Data Azure Cosmos DB versión 3.13.0 y posteriores.

Uso de consultas de partición única parametrizadas

Para las consultas parametrizadas que tienen como ámbito una clave de partición con setPartitionKey en CosmosQueryRequestOptions y no contienen agregaciones (incluido Distinct, DCount, Group By), se puede evitar el plan de consulta:

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

Las consultas entre particiones requieren que el SDK visite todas las particiones existentes para comprobar los resultados. Cuantas más particiones físicas tenga el contenedor, más lentas pueden ser potencialmente.

Ajuste del grado de paralelismo

Las consultas paralelas funcionan creando consultas en varias particiones en paralelo. Sin embargo, los datos de un contenedor con particiones individual se capturan en serie con respecto a la consulta. Por lo tanto, use setMaxDegreeOfParallelism en CosmosQueryRequestOptions para establecer el valor en el número de particiones que tiene. Si no conoce el número de particiones, puede usar setMaxDegreeOfParallelism para establecer un número alto y el sistema elegirá el mínimo (número de particiones, entrada proporcionada por el usuario) como el grado máximo de paralelismo. Establecer el valor en -1 permitirá al SDK decidir la simultaneidad óptima.

Es importante tener en cuenta que las consultas en paralelo producen los mejores beneficios si los datos se distribuyen uniformemente entre todas las particiones con respecto a la consulta. Si el contenedor con particiones está dividido de tal forma que todos o la mayoría de los datos devueltos por una consulta se concentran en algunas particiones (una partición en el peor de los casos), entonces el rendimiento de la consulta se vería 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);

Supongamos que

  • D = número máximo predeterminado de tareas en paralelo (= número total de procesadores en el equipo cliente)
  • P = número máximo especificado por el usuario de tareas en paralelo
  • N = número de particiones que se necesita visitar para responder a una consulta

A continuación se muestran las distintas implicaciones de cómo se comportarán las consultas en paralelo para valores diferentes de P.

  • (P == 0) => modo serie
  • (P == 1) => un máximo de una tarea
  • (P > 1) => mínimo de tareas en paralelo (P, N)
  • (P == -1) =>> mínimo de tareas en paralelo (N, D)

Ajuste del tamaño de página

Cuando emite una consulta SQL, los resultados se devuelven de forma segmentada si el conjunto de resultados es demasiado grande. De forma predeterminada, se devuelven resultados en fragmentos de 4 MB o de 100 artículos, el límite que se alcance primero. Aumentar el tamaño de página reduce el número de recorridos de ida y vuelta necesarios y mejora el rendimiento de las consultas que devuelven más de 100 elementos. Si no está seguro de qué valor establecer, 1000 suele ser una buena opción. El consumo de memoria aumentará a medida que aumente el tamaño de página. Por tanto, si la carga de trabajo es sensible a la memoria, considere el uso de un valor inferior.

Puede usar el parámetro pageSize en iterableByPage() para la API de sincronización y byPage() para la API asincrónica, para definir un tamaño 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 del tamaño de búfer

Las consultas en paralelo están diseñadas para capturar previamente los resultados mientras el cliente procesa el lote actual de resultados. La captura previa ayuda a mejorar la latencia general de una consulta. setMaxBufferedItemCount en CosmosQueryRequestOptions limita el número de resultados capturados previamente. Para maximizar la captura previa, establezca maxBufferedItemCount en un número mayor que pageSize (NOTA: Esto también puede dar como resultado un consumo elevado de memoria). Para minimizar la captura previa, establezca maxBufferedItemCount en un valor igual a pageSize. Si establece este valor en 0, el sistema determinará automáticamente el número de elementos que se van a almacenar en búfer.

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 captura previa funciona de la misma manera, independientemente del grado de paralelismo y hay un único búfer para los datos de todas las particiones.

Pasos siguientes

Para obtener más información sobre el rendimiento mediante el SDK de Java:

Reducción de las llamadas al plan de consulta

Para ejecutar una consulta, es necesario crear un plan de consulta. En general, esto representa una solicitud de red a la puerta de enlace de Azure Cosmos DB, que se agrega a la latencia de la operación de consulta. Existe una forma de eliminar esta petición y reducir la latencia de la operación de consulta de partición única. Para consultas de partición única, especifique el valor de la clave de partición para el elemento y páselo como argumento partition_key:

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

Ajuste del tamaño de página

Cuando emite una consulta SQL, los resultados se devuelven de forma segmentada si el conjunto de resultados es demasiado grande. El max_item_countpermite establecer el número máximo de elementos que se devolverán en la operación de enumeración.

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

Pasos siguientes

Para obtener más información sobre el uso del SDK de Python para API para NoSQL:

Reducción de las llamadas al plan de consulta

Para ejecutar una consulta, es necesario crear un plan de consulta. En general, esto representa una solicitud de red a la puerta de enlace de Azure Cosmos DB, que se agrega a la latencia de la operación de consulta. Existe una forma de eliminar esta petición y reducir la latencia de la operación de consulta de partición única. Para las consultas de una sola partición, el alcance de una consulta a una sola partición puede realizarse de dos formas.

Utilización de una expresión de consulta parametrizada y especificación de la clave de partición en la sentencia de consulta. La consulta se compone mediante programación 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}`);
}

O especificar partitionKey en FeedOptions y pasarlo 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}`);
}

Ajuste del tamaño de página

Cuando emite una consulta SQL, los resultados se devuelven de forma segmentada si el conjunto de resultados es demasiado grande. El maxItemCount permite establecer el número máximo de elementos que se devolverán en la operación de enumeración.

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

Pasos siguientes

Para obtener más información sobre el uso del SDK de Node.js para API para NoSQL: