Советы по повышению производительности запросов для пакетов SDK Azure Cosmos DB

ОБЛАСТЬ ПРИМЕНЕНИЯ: NoSQL

Azure Cosmos DB — быстрая и гибкая распределенная база данных, которая легко масштабируется с гарантированными уровнями задержки и пропускной способности. Для масштабирования базы данных с помощью Azure Cosmos DB не нужно вносить в архитектуру существенные изменения или писать сложный код. Для увеличения или уменьшения масштаба достаточно выполнить один вызов API. Дополнительные сведения см. в разделах о подготовке пропускной способности контейнера и о подготовке пропускной способности базы данных.

Уменьшение числа вызовов для плана запроса

Чтобы выполнить запрос, необходимо создать план запроса. В общем случае, план запроса представляет собой сетевой запрос к шлюзу Azure Cosmos DB, который увеличивает задержку для операции запроса. Существует два способа, позволяющих удалить этот запрос и уменьшить задержку операции запроса:

Оптимизация запросов с одним разделом с помощью оптимистического прямого выполнения

Azure Cosmos DB NoSQL имеет оптимизацию под названием Оптимистическое прямое выполнение (ODE), что может повысить эффективность определенных запросов NoSQL. В частности, запросы, которые не требуют распределения, включают те, которые могут выполняться в одной физической секции или имеют ответы, которые не требуют разбиения на страницы. Запросы, которые не требуют распространения, могут уверенно пропускать некоторые процессы, такие как создание плана запросов на стороне клиента и перезапись запросов, тем самым уменьшая задержку запросов и затраты на ЕЗ. Если в запросе или запросе указан ключ секции (или только одна физическая секция), а результаты запроса не требуют разбиения на страницы, ODE может улучшить запросы.

Примечание.

Оптимистическое прямое выполнение (ODE), которое обеспечивает улучшенную производительность запросов, которые не требуют распределения, не следует путать с прямым режимом, который является путем подключения приложения к внутренним репликам.

Теперь ODE доступен в пакете SDK для .NET версии 3.38.0 и более поздних версий. При выполнении запроса и указан ключ секции в самом запросе или запросе, или база данных имеет только одну физическую секцию, выполнение запроса может использовать преимущества ODE. Чтобы включить ODE, задайте для параметра EnableOptimisticDirectExecution значение true в QueryRequestOptions.

Запросы с одним разделом, которые поддерживают функции GROUP BY, ORDER BY, DISTINCT и агрегирования (например, суммы, среднее, минимальное и максимальное) могут значительно воспользоваться ODE. Однако в сценариях, когда запрос предназначен для нескольких секций или по-прежнему требует разбиения на страницы, задержка ответа запроса и затраты ЕЗ могут быть выше, чем без использования ODE. Поэтому при использовании ODE рекомендуется:

  • Укажите ключ секции в самом вызове или запросе.
  • Убедитесь, что размер данных не вырос и вызвал разделение секции.
  • Убедитесь, что результаты запроса не требуют разбиения на страницы, чтобы получить полное преимущество ODE.

Ниже приведены несколько примеров простых запросов с одним разделом, которые могут воспользоваться 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 

В случаях, когда запросы на одну секцию могут по-прежнему требовать распределения, если количество элементов данных увеличивается со временем, а база данных Azure Cosmos DB разделяет секцию. Примеры запросов, в которых это может произойти, включают:

- 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 

Некоторые сложные запросы всегда могут требовать распределения, даже если оно предназначено для одной секции. Примеры таких запросов:

- 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 

Важно отметить, что ODE может не всегда извлекать план запроса и, в результате, не может запретить или отключить неподдерживаемые запросы. Например, после разделения секций такие запросы больше не имеют права на ODE и, следовательно, не будут выполняться, так как оценка плана запросов на стороне клиента блокирует эти запросы. Чтобы обеспечить непрерывность совместимости и обслуживания, важно убедиться, что с ODE используются только запросы, которые полностью поддерживаются в сценариях без ODE (то есть выполняются и создают правильный результат в общем случае с несколькими секциями).

Примечание.

Использование ODE может привести к созданию нового типа маркера продолжения. Такой маркер не распознается старыми пакетами SDK по проектированию, и это может привести к исключению маркера неправильного продолжения. Если у вас есть сценарий, в котором маркеры, созданные из новых пакетов SDK, используются старым пакетом SDK, рекомендуется выполнить 2 шага для обновления:

  • Обновите новый пакет SDK и отключите ODE как часть одного развертывания. Дождитесь обновления всех узлов.
    • Чтобы отключить ODE, установите для параметра EnableOptimisticDirectExecution значение false в QueryRequestOptions.
  • Включите ODE в рамках второго развертывания для всех узлов.

Создавайте план запроса локально

Пакет SDK для SQL содержит собственный файл ServiceInterop.dll для локального анализа и оптимизации запросов. Файл ServiceInterop.dll поддерживается только на 64-разрядной платформе Windows. В следующих типах приложений по умолчанию используется 32-разрядная обработка узлов. Чтобы изменить обработку узлов на 64-разрядную, выполните следующие действия в зависимости от типа приложения.

  • Для исполняемых приложений можно изменить обработку узла, задав для параметра Целевая платформа значение x64 в окне Свойства проекта на вкладке Сборка.

  • Для тестовых проектов на основе VSTest можно изменить обработку узла, выбрав Тест>Параметры тестирования>Архитектура процессора по умолчанию — X64 в пункте меню Тест в Visual Studio.

  • Для локально развернутых веб-приложений ASP .NET можно изменить обработку узла, установив флажок Использовать 64-разрядную версию IIS Express для веб-сайтов и проектов в меню Сервис>Параметры>Проекты и решения>Веб-проекты.

  • Для веб-приложений ASP .NET, развернутых в Azure, можно изменить обработку узлов, выбрав 64-разрядную платформу в окне Параметры приложения на портале Azure.

Примечание.

По умолчанию для новых проектов Visual Studio задано значение Любой ЦП. Рекомендуется задать для проекта значение x64, чтобы он не переключился на x86. Проект, для которого задано значение Любой ЦП, может легко переключиться на x86, если добавляется зависимость только для x86.
Файл ServiceInterop.dll должен находиться в папке, из которой выполняется библиотека DLL пакета SDK. Это может вызывать проблемы только в том случае, если вы вручную копируете библиотеки DLL или используете пользовательские сборки или системы развертывания.

Используйте односекционные запросы

Для запросов, предназначенных для ключа секции, задав свойство PartitionKey в QueryRequestOptions и не содержат агрегирования (включая Distinct, DCount, Group By). В этом примере поле /state ключа секции фильтруется по значению Washington.

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

При необходимости ключ секции можно указать как часть объекта параметров запроса.

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

Внимание

На клиентах, работающих под управлением ОС, отличных от Windows, таких как Linux и MacOS, ключ секции всегда должен быть указан в объекте параметров запроса.

Примечание.

Для запросов между секциями пакет SDK должен обработать все существующие секции для проверки результатов. Чем больше физических секций содержит контейнер, тем медленнее он может обрабатываться.

Не создавайте итератор повторно без необходимости

Если все результаты запроса используются текущим компонентом, вам не нужно повторно создавать итератор с продолжением для каждой страницы. Всегда обрабатывайте запрос полностью, если разбиение на страницы не контролируется другим компонентом вызова:

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

Настройка степени параллелизма

Настройте свойство MaxConcurrency в QueryRequestOptions для запросов, чтобы определить оптимальные конфигурации для приложения, особенно если выполняются запросы между секциями (без фильтрации по значению ключа секции). Параметр MaxConcurrency определяет максимальное число параллельных задач, т. е. максимальное количество секций, обрабатываемых одновременно. При установке значения -1 для этого параметра пакет SDK будет самостоятельно определять оптимальную степень параллелизма.

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

Предположим следующее:

  • D — максимальное число параллельных задач, используемое по умолчанию (т. е. общее количество процессоров на компьютере клиента);
  • P — определенное пользователем максимальное число параллельных задач;
  • N — количество секций, которые необходимо посетить для получения ответов на запросы.

Возможны следующие сценарии поведения параллельных запросов при различных значениях P:

  • (P == 0) => последовательный режим;
  • (P == 1) => не более одной задачи;
  • (P > 1) => минимум (P, N) параллельных задач;
  • (P < 1) => минимум (N, D) параллельных задач.

Настройка размера страницы

При выполнении SQL-запроса если результирующий набор имеет слишком большой размер, результаты возвращаются в сегментированном виде.

Примечание.

Свойство MaxItemCount не должно использоваться только для разбиения на страницы. Его основное применение — повышение производительности запросов за счет уменьшения максимального числа элементов, возвращаемых на одной странице.

Размер страницы также можно изменить с помощью доступных пакетов SDK для Azure Cosmos DB. Свойство MaxItemCount в QueryRequestOptions позволяет задать максимальное число элементов, возвращаемых операцией перечисления. Если параметр MaxItemCount имеет значение –1, пакет SDK автоматически находит оптимальное значение в зависимости от размера документа. Например:

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

При выполнении запроса полученные данные отправляются в пакете TCP. Если указано слишком маленькое значение для MaxItemCount, то число круговых путей, необходимых для отправки данных в пакете TCP, будет больше, что повлияет на производительность. Поэтому, если вы не уверены, какое значение следует задать для свойства MaxItemCount, лучше установить его в значение –1 и позволить пакету SDK выбрать значения по умолчанию.

Настройка размера буфера

Параллельный запрос предназначен для предварительного получения результатов, пока текущий пакет результатов обрабатывается клиентом. Такая предварительная выборка способствует общему уменьшению задержки при обработке запроса. Значение свойства MaxBufferedItemCount в QueryRequestOptions ограничивает количество предварительно выбираемых результатов. Если настроить для параметра MaxBufferedItemCount ожидаемое (или максимальное) количество возвращаемых результатов, запрос получит максимальную выгоду от предварительной выборки. Если установить это значение на –1, система автоматически определит количество элементов для буферизации.

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

Предварительная выборка работает одинаково при любом значении степени параметра. Для данных из всех секций применяется один буфер.

Следующие шаги

Дополнительные сведения о повышении производительности с помощью пакета SDK для .NET см. в следующих разделах:

Уменьшение числа вызовов для плана запроса

Чтобы выполнить запрос, необходимо создать план запроса. В общем случае, план запроса представляет собой сетевой запрос к шлюзу Azure Cosmos DB, который увеличивает задержку для операции запроса.

Использование кэширования плана запроса

План запроса для запроса, ограниченного одной секцией, кэшируется на клиенте. Это избавляет от необходимости выполнять вызов шлюза для получения плана запроса после первого вызова. Ключом для кэшированного плана запроса является строка запроса SQL. Необходимо убедиться в том, что запрос является параметризованным. В противном случае в кэше планов запросов будут часто возникать промахи, так как строка запроса, скорее всего, не будет одинаковой для всех вызовов. Кэширование плана запросов включено по умолчанию для пакета SDK для Java версии 4.20.0 и выше , а также для пакета SDK для Spring Data Azure Cosmos DB версии 3.13.0 и более поздней.

Используйте параметризованные односекционные запросы

Для параметризованных запросов, область действия которых ограничена ключом запроса с помощью параметра setPartitionKey в CosmosQueryRequestOptions и которые не содержат агрегатов (включая Distinct, DCount и Group By), план запроса можно не использовать:

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

Примечание.

Для запросов между секциями пакет SDK должен обработать все существующие секции для проверки результатов. Чем больше физических секций содержит контейнер, тем медленнее он может обрабатываться.

Настройка степени параллелизма

Параллельные запросы позволяют одновременно обращаться к нескольким секциям. Однако данные из отдельного секционированного контейнера извлекаются последовательно в соответствии с запросом. Поэтому используйте параметр setMaxDegreeOfParallelism в CosmosQueryRequestOptions для установки значения, соответствующего числу секций. Если вы не знаете количество секций, просто укажите достаточно большое значение параметра setMaxDegreeOfParallelism. Система автоматически выберет минимальное из двух значений (количество секций или число, указанное пользователем) в качестве максимальной степени параллелизма. При установке значения -1 для этого параметра пакет SDK будет самостоятельно определять оптимальную степень параллелизма.

Следует отметить, что параллельные запросы обеспечивают больше преимуществ, если данные равномерно распределены во всех секциях по отношению к запросу. Если контейнер секционируется таким образом, что все или большинство данных, возвращаемых запросом, содержатся в нескольких секциях (в наихудшем случае в одной секции), то это негативно скажется на производительности запроса.

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

Предположим следующее:

  • D — максимальное число параллельных задач, используемое по умолчанию (т. е. общее количество процессоров на компьютере клиента);
  • P — определенное пользователем максимальное число параллельных задач;
  • N — количество секций, которые необходимо посетить для получения ответов на запросы.

Возможны следующие сценарии поведения параллельных запросов при различных значениях P:

  • (P == 0) => последовательный режим;
  • (P == 1) => не более одной задачи;
  • (P > 1) => минимум (P, N) параллельных задач;
  • (P == -1) => минимум (N, D) параллельных задач.

Настройка размера страницы

При выполнении SQL-запроса если результирующий набор имеет слишком большой размер, результаты возвращаются в сегментированном виде. По умолчанию результаты возвращаются в пакетах (не более 100 элементов и не более 4 МБ в каждом пакете). Увеличение размера страницы приведет к уменьшению количества необходимых обходов и повышения производительности запросов, возвращающих более 100 элементов. Если вы не уверены, какое значение нужно задать, 1000 обычно является хорошим выбором. Потребление памяти увеличивается по мере увеличения размера страницы, поэтому если рабочая нагрузка учитывает память, учитывайте более низкое значение.

Чтобы задать размер страницы, можно использовать параметр pageSize в iterableByPage() для синхронного API и параметр byPage() для асинхронного API:

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

Настройка размера буфера

Параллельный запрос предназначен для предварительного получения результатов, пока текущий пакет результатов обрабатывается клиентом. Предварительная выборка способствует общему уменьшению задержки при обработке запроса. Значение параметра setMaxBufferedItemCount в CosmosQueryRequestOptions ограничивает количество предварительно выбираемых результатов. Чтобы максимально увеличить предварительную выборку, задайте maxBufferedItemCount для параметра более высокое число, чем pageSize (ПРИМЕЧАНИЕ. Это также может привести к большому потреблению памяти). Чтобы свести к минимуму предварительную выборку, задайте равный maxBufferedItemCount pageSizeзначению . Если установить это значение 0, система определит количество элементов для буферизации автоматически.

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

Предварительная выборка работает одинаково при любом значении степени параметра. Для данных из всех секций применяется один буфер.

Следующие шаги

Дополнительные сведения о повышении производительности с помощью пакета SDK для Java см. в следующих разделах:

Уменьшение числа вызовов для плана запроса

Чтобы выполнить запрос, необходимо создать план запроса. В общем случае, план запроса представляет собой сетевой запрос к шлюзу Azure Cosmos DB, который увеличивает задержку для операции запроса. Существует способ удалить этот запрос и уменьшить задержку операции запроса одной секции. Для запросов с одним разделом укажите значение ключа секции для элемента и передайте его в качестве аргумента partition_key :

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

Настройка размера страницы

При выполнении SQL-запроса если результирующий набор имеет слишком большой размер, результаты возвращаются в сегментированном виде. Max_item_count позволяет задать максимальное количество элементов, возвращаемых в операции перечисления.

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

Следующие шаги

Дополнительные сведения об использовании пакета SDK Python для API для NoSQL:

Уменьшение числа вызовов для плана запроса

Чтобы выполнить запрос, необходимо создать план запроса. В общем случае, план запроса представляет собой сетевой запрос к шлюзу Azure Cosmos DB, который увеличивает задержку для операции запроса. Существует способ удалить этот запрос и уменьшить задержку операции запроса одной секции. Для запросов отдельных секций можно выполнить запрос к одной секции двумя способами.

Использование параметризованного выражения запроса и указание ключа секции в инструкции запроса. Запрос создается программным способом: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}`);
}

Или укажите partitionKey и передайте его в FeedOptions качестве аргумента:

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

Настройка размера страницы

При выполнении SQL-запроса если результирующий набор имеет слишком большой размер, результаты возвращаются в сегментированном виде. MaxItemCount позволяет задать максимальное количество элементов, возвращаемых в операции перечисления.

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

Следующие шаги

Дополнительные сведения об использовании пакета SDK Node.js для API для NoSQL: