有关 Azure Cosmos DB SDK 的查询性能提示

适用范围: NoSQL

Azure Cosmos DB 是一个快速、弹性的分布式数据库,可以在提供有保证的延迟与吞吐量级别的情况下无缝缩放。 凭借 Azure Cosmos DB,无需对体系结构进行重大更改或编写复杂的代码即可缩放数据库。 扩展和缩减操作就像执行单个 API 调用一样简单。 若要了解详细信息,请参阅预配容器吞吐量预配数据库吞吐量

减少查询计划调用

若要执行查询,需要生成一个查询计划。 此计划通常表示对 Azure Cosmos DB 网关的网络请求,这会增大查询操作的延迟。 可通过两种方式消除此请求并降低查询操作的延迟:

使用 Optimistic Direct Execution 优化单分区查询

Azure Cosmos DB NoSQL 具有一种叫做 Optimistic Direct Execution (ODE) 的优化,可用来提高某些 NoSQL 查询的效率。 具体而言,不需要分发的查询包括可在单个物理分区上执行的查询或具有不需要分页的响应的查询。 不需要分发的查询可以放心地跳过某些过程,例如客户端查询计划生成和查询重写,从而减少查询延迟和 RU 成本。 如果你在请求或查询本身中指定分区键(或只有一个物理分区),并且查询结果不需要分页,则 ODE 可以改进查询。

注意

乐观直接执行 (ODE) 为不需要分发的查询提供了改进的性能,不应与直接模式混淆,后者是将应用程序连接到后端副本的路径。

ODE 现已可用,并且默认在 .NET SDK 版本 3.38.0 及更高版本中启用。 当你执行查询并在请求或查询本身中指定分区键时,或你的数据库只有一个物理分区时,你的查询执行可以利用 ODE 的优势。 若要禁用 ODE,请在 QueryRequestOptions 中将 EnableOptimisticDirectExecution 设置为 false。

具有 GROUP BY、ORDER BY、DISTINCT 和聚合函数(如 sum、mean、min 和 max)的单分区查询可以显著受益于 ODE 的使用。 但是,如果查询面向多个分区或仍然需要分页,则查询响应的延迟和 RU 成本可能高于不使用 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,请在 QueryRequestOptions 中将 EnableOptimisticDirectExecution 设置为 false。
  • 在所有节点的第二个部署过程中启用 ODE。

使用本地查询计划生成

SQL SDK 包含一个本机 ServiceInterop.dll,用于在本地分析和优化查询。 仅 Windows x64 平台支持 ServiceInterop.dll。 以下类型的应用程序默认使用 32 位主机处理。 若要将主机处理更改为 64 位处理,请根据应用程序的类型执行以下步骤:

  • 对于可执行应用程序,可以在“项目属性”窗口中的“版本”选项卡上,通过将平台目标设置为“x64”来更改主机处理。

  • 对于基于 VSTest 的测试项目,可以通过在 Visual Studio“测试”菜单中选择“测试”>“测试设置”>“默认处理器体系结构为 X64”,来更改主机处理。

  • 对于本地部署的 ASP.NET Web 应用程序,可以通过在“工具”>“选项”>“项目和解决方案”>“Web 项目”下选择“对网站和项目使用 IIS Express 的 64 位版”,来更改主机处理。

  • 对于部署在 Azure 上的 ASP.NET Web 应用程序,可以通过在 Azure 门户上的“应用程序设置”中选择“64 位”平台,来更改主机处理。

注意

新的 Visual Studio 项目默认设置为“任何 CPU”。 我们建议将项目设置为“x64”,使其不会切换到“x86”。 如果添加了仅限 x86 的依赖项,则设置为“任何 CPU”的项目可以轻松切换到“x86”。
ServiceInterop.dll 所处的文件夹必须是在其中执行 SDK DLL 的文件夹。 仅当你手动复制 DLL 或使用自定义的生成/部署系统时,此位置才是一个考虑因素。

使用单分区查询

对于通过在 QueryRequestOptions 中设置 PartitionKey 属性来定位分区键且不包含聚合(包括 Distinct、DCount、Group By)的查询。 在此示例中,/state 的分区键字段已根据值 Shanghai 进行筛选。

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

(可选)可以将分区键作为请求选项对象的一部分提供。

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

注意

跨分区查询需要 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
        }
    }
}

优化并行度

对于查询,优化 QueryRequestOptions 中的 MaxConcurrency 属性来确定适合你的应用程序的最佳配置,尤其是当执行跨分区查询时(没有针对分区键值的筛选器)。 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) => Min (P, N) 并行任务数
  • (P < 1) => Min (N, D) 并行任务数

优化页面大小

发出 SQL 查询时,如果结果集太大,则结果将以分段形式返回。

注意

MaxItemCount 属性不应仅用于分页目的。 它的主要用途是通过减少单个页面中返回的最大项数来提高查询性能。

也可以使用提供的 Azure Cosmos DB SDK 设置页面大小。 QueryRequestOptions 中的 MaxItemCount 属性允许你设置要在枚举操作中返回的最大项数。 当 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 选择默认值。

优化缓冲区大小

并行查询设计为当客户端正在处理当前结果批时预提取结果。 这种预提取可帮助改善查询的总体延迟。 QueryRequestOptions 中的 MaxBufferedItemCount 属性限制预先提取的结果数。 将 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}))
{
    // ...
}

预提取的工作方式与并行度无关,使用一个单独的缓冲区来存储所有分区的数据。

后续步骤

若要详细了解如何使用 .NET SDK 提高性能,请参阅:

减少查询计划调用

若要执行查询,需要生成一个查询计划。 此计划通常表示对 Azure Cosmos DB 网关的网络请求,这会增大查询操作的延迟。

使用查询计划缓存

对于范围限定为单个分区的查询,查询计划将在客户端上缓存。 这样,在首次调用后,就不需要调用网关来检索查询计划。 缓存的查询计划的键是 SQL 查询字符串。 需要确保查询已参数化。 否则,查询计划缓存查找通常会发生缓存未命中,因为查询字符串在不同的调用中不太可能相同。 对于 Java SDK 4.20.0 和更高版本以及 Spring Data Azure Cosmos DB SDK 3.13.0 和更高版本,默认启用查询计划缓存。

使用参数化的单分区查询

对于通过 CosmosQueryRequestOptions 中的 setPartitionKey 将范围限定为某个分区键且不包含聚合(包括 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 访问所有现有分区以检查结果。 容器的物理分区越多,查询速度就可能越慢。

优化并行度

并行查询的方式是并行查询多个分区。 但对于查询,将按顺序提取单个已分区容器中的数据。 因此,请使用 CosmosQueryRequestOptions 上的 setMaxDegreeOfParallelism 将值设置为你的分区数。 如果你不知道分区数,可以使用 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) => Min (P, N) 并行任务数
  • (P == -1) => Min (N, D) 并行任务数

优化页面大小

发出 SQL 查询时,如果结果集太大,则结果将以分段形式返回。 默认情况下,以包括 100 个项的块或 4 MB 大小的块返回结果(以先达到的限制为准)。 增大页面大小将减少所需的往返次数,并提高返回结果超过 100 项的查询的性能。 如果你不确定要设置多大的值,则 1000 通常是一个不错的选择。 内存消耗量会随着页面大小的增大而增大,因此,如果工作负载对内存敏感,请考虑使用一个较低的值。

可将 iterableByPage() 中的 pageSize 参数用于同步 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();

优化缓冲区大小

并行查询设计为当客户端正在处理当前结果批时预提取结果。 预提取帮助改进查询中的的总体延迟。 CosmosQueryRequestOptions 中的 setMaxBufferedItemCount 限制预提取结果的数量。 若要最大程度地增加预提取量,请将 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);

预提取的工作方式与并行度无关,使用一个单独的缓冲区来存储所有分区的数据。

后续步骤

若要详细了解如何使用 Java SDK 提高性能,请参阅:

减少查询计划调用

若要执行查询,需要生成一个查询计划。 此计划通常表示对 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
    )

后续步骤

若要详细了解如何使用 Python SDK for API for 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}`);
}

或者,在 FeedOptions 中指定 partitionKey 并将其作为参数传递:

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 查询时,如果结果集太大,则结果将以分段形式返回。 max_item_count 允许你设置要在枚举操作中返回的最大项数。

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

后续步骤

若要详细了解如何使用 Node.js SDK for API for NoSQL,请执行以下操作: