IBM Cloudant 查询性能指南

本指南从最基本的优化开始,逐步介绍优化 IBM Cloudant 查询性能的基本技术。 通过本指南,您可以将缓慢、昂贵的查询转化为快速、高效的查询,并随着数据的扩展而扩展。

指南内容包括

  • 概念:
    • 索引的选择性
  • 基础主题
    • 避免大型扫描
    • 复合索引中的字段排序
    • 使用允许使用索引的运算符
    • 避免在 JSON 索引中丢失文件
    • 监视性能
  • 高级主题
    • 部分索引
    • 封面索引
    • 指数整合战略

指南最后附有一份生产准备清单。

关键概念:选择性

性能和可扩展性查询取决于将查询需求与能有效服务于这些查询的索引相匹配。 建立此类索引的一个关键概念是_选择性_ --索引帮助缩小结果集的程度。

_高选择性_索引(或称_高选择性_索引)能显著减少查询所匹配的文档数量。 相比之下,_低选择性_索引会匹配许多文档,性能优势较小。

  • 对于订单表,_高选择性_指数的例子包括
    • order_id 注意:每个订单都有一个唯一的 ID,因此查询最多只能匹配一个文档。
    • username 用户数量:每个用户的订单数量远远少于订单总数。
  • _低选择性_指数的例子包括
    • status 状态:许多订单可能共享相同的状态。
    • country 在这个广泛的领域,可能会匹配很多订单。

了解选择性有助于设计索引,使查询更快、更高效。

基础部分:避免大型数据库扫描

影响:1000x+ 性能改进

IBM Cloudant Query 中最具破坏性的性能问题是触发大型数据库扫描。 造成这一问题的原因有两个:

  • 该查询使用了_低选择性_索引,因此需要扫描许多文档,以生成更小的结果集。
  • 该查询无法使用用户定义的索引,因此只能使用内置的 _all_docs 索引,结果是要扫描数据库中的所有文档。

如何识别大型扫描

使用 _explain 端点检查查询将使用哪个索引:

POST /orders/_explain
{
  "selector": {
    "username": "bob@email.com",
    "status": "cancelled",
    "date": {"$gte": "2018-01-01", "$lt": "2019-01-01"}
  }
}

如果答复显示了特定指数,则评估该指数是_高_选择性还是_低_选择性。 在本例中,status 字段上的索引可能是低选择性索引,因此需要对该查询进行处理:

{
  "index": {
    "ddoc": "_design/status-index",
    "def": {
      "fields": [
        {
          "status": "asc"
        }
      ]
    },
    "name": "d479bdddf50865e520a0193704c8b93a3bd48f77",
    "type": "json"
  }
}

如果响应显示 "name": "_all_docs",则查询将扫描整个数据库:

{
  "index": {
    "ddoc": null,
    "name": "_all_docs",
    "type": "special"
  }
}

解决方案:创建适当的索引

对于上述查询,最佳索引是涵盖查询字段的复合(多字段)JSON 索引:

POST /orders/_index
{
  "index": {
    "fields": ["username", "status", "date"]
  },
  "name": "username-status-date-index",
  "type": "json"
}

性能影响:在 100,000 个文档数据库中,这一变化可将查询时间从 20 多秒缩短到 10 毫秒以下。

生产安全:防止意外大扫描

如果知道某个索引对查询有很高的选择性,可将 use_indexallow_fallback: false 结合使用,以强制使用该索引:

{
  "selector": {"username": "bob@email.com", "status": "cancelled"},
  "use_index": ["my_design_doc", "username-index"],
  "allow_fallback": false
}

如果 my_design_doc/username-index 无法用于查询,则会返回错误,而不是默默地执行昂贵的扫描。 allow_fallback 的默认值是 true,这意味着 IBM Cloudant 将使用不同的索引来响应查询,可能会导致大量扫描。

如果不提供 use_index,那么 allow_fallback: false 仍将阻止使用 _all_docs 索引进行查询:

{
  "selector": {"username": "bob@email.com"},
  "allow_fallback": false
}

如果不存在合适的用户定义索引,将返回错误,从而避免昂贵的扫描。

有关这些查询参数的更多详情,请参阅查询 API 文档

对于关键查询,建议的做法是始终将 use_indexallow_fallback 相结合。

基础:复合索引中的字段排序

影响:100x 性能改进

一旦有了索引,最常见的错误就是复合索引中的字段排序不正确。 这会将高效查询变成扫描 40% 以上数据库的查询。

关键规则 1:化合物指数选择性

按选择性排列您的相等字段(非范围字段) - 选择性最强的优先:

  • user 字段:每个用户 50 份文档(高选择性)--将其放在首位。
  • status 字段:每个状态 10,000 份文档(中等选择性)。

最佳索引排序["username", "status", "date"],其中 date 是范围字段。

关键规则 2:范围字段必须放在最后

创建复合索引时,任何与范围运算符一起使用的字段 ($gt, $gte, $lt, $lte) 必须在字段列表中最后出现。

错误 - 导致部分数据库扫描:

{
  "index": {
    "fields": ["date", "username", "status"]  // Bad - range field first
  }
}

正确--实现高效搜索:

{
  "index": {
    "fields": ["username", "status", "date"]  // Good - range field last
  }
}

关键规则 3:排序字段必须放在最后

索引还可用于对查询结果进行排序。 当您的查询对 usernamestatus 等字段进行筛选,并对 date 等字段进行排序时,索引必须包括所有三个字段,排序字段放在最后

例如,支持以下查询:

{
  "selector": { "username": "bob@email.com", "status": "cancelled" },
  "sort": [ { "date": "asc" } ]
}

您需要 ["username", "status", "date"] 上的索引。 在这种结构中,排序字段 (date) 必须放在最后。 这种排序方式可确保 IBM Cloudant 能够有效地使用索引进行筛选和排序。

为什么实地排序很重要

IBM Cloudant 索引的工作原理与排序表类似。 ["username", "status", "date"] 上的索引创建的条目首先按用户排序,然后按每个用户中的状态排序,最后按每个用户-状态组合中的日期排序:

["alice", "paid", "2018-01-15"]
["alice", "paid", "2018-02-20"]
["bob", "cancelled", "2018-01-10"]
["bob", "cancelled", "2018-01-15"]
["bob", "paid", "2018-01-12"]

当对 user: "bob", status: "cancelled", date: {$gte: "2018-01-01"} 这样的查询使用高效索引时,IBM Cloudant:

  1. 直接跳转到鲍勃的记录。
  2. 在鲍勃的记录中查找已取消的订单。
  3. 有效扫描日期范围。

如果把日期放在首位,IBM Cloudant 就需要检查所有用户和状态的日期范围内的每份文档。

基础:使用允许使用索引的运算符

影响:10-100x 性能差异

必须将高选择性索引与允许使用这些索引的查询选择器结合起来。 选择器条件中使用的运算符可以阻止索引的使用。

在执行查询时,只有某些运算符允许 IBM Cloudant 使用索引来有效地回答查询。如果在某个字段上有一个高选择性索引,但该字段条件中使用的操作符不支持使用索引,那么查询将无法使用该索引。 因此,效率会很低。

JSON 和文本索引支持在条件中使用不同的操作符。 如果 JSON 索引不支持您需要的操作符,那么文本索引也可以。

下表总结了索引的主要属性及其支持的操作符:

特性 JSON 索引 文本索引
基础技术 CouchDB MapReduce 查看(B 树) Apache Lucene
主要用途 对延迟敏感、可预测的查询 灵活的临时查询
性能 精确匹配和范围更快 速度更慢,但功能更全面
文件要求 必须包含所有索引字段 灵活 - 字段缺失 OK
强类型
组合操作员
$and ✔️ 支持 ✔️ 支持
$or 不支持 ✔️ 支持
$nor 不支持 ✔️ 支持
$not 不支持 ✔️ 支持
条件操作员
$eq (平等) ✔️ 最佳 ✔️ 支持
$gt, $gte, $lt, $lte ✔️ 最佳范围 ✔️ 支持
$exists ✔️ 支持 ✔️ 支持
$beginsWith ✔️ 支持 ✔️ 支持
$ne (不等同) 不支持 ✔️ 支持
$in 不支持 ✔️ 支持
$size 不支持 ✔️ 支持
$type 不支持 ✔️ 支持
$text 不支持 ✔️ 全文搜索
$regex 不支持 不支持
现场处理
排序 通过索引字段排序固定 更灵活
数组字段 不支持 ✔️ 完全支持 [] 符号
嵌套对象(点符号) ✔️ 支持 ✔️ 支持
缺少字段 不包括文件 包括文件
查询页面大小限制
全球查询 ??? 200
分区范围查询 ??? 200

索引类型选择指南

在以下情况下选择 JSON 索引

  • 查询模式具有可预测性和一致性。
  • 您需要最大限度地提高精确匹配的性能。
  • 文件结构一致。
  • 您主要使用 $and$eq 和范围运算符。

在下列情况下选择文本索引

  • 您需要 $or 跨多个字段进行操作。
  • 文件的结构各不相同。
  • 您需要全文搜索功能。
  • 查询模式是临时的或用户驱动的。
  • 您正在使用 $ne$exists 或复杂运算符。

决策矩阵示例:

// Predictable e-commerce queries → JSON index
{
  "selector": {
    "username": "bob@email.com",
    "status": "paid",
    "date": {"$gte": "2018-01-01"}
  }
}

// Flexible search interface → Text index  
{
  "selector": {
    "$or": [
      "status": {"$in": ["paid", "in-progress"]}
      "category": "electronics"
    ]
  }
}

基础:查询性能监控

影响:持续优化和问题检测

高效查询需要持续监控。IBM Cloudant 提供了随时间推移测量和跟踪查询性能的工具。

IBM Cloudant 查询在响应中提供 execution_stats 字段,以帮助评估您的查询。 请参阅 监控低效的 IBM IBM Cloudant 查询, 以获取 IBM Cloudant 提供的用于分析查询执行的指标的详细指南。

警告:JSON 索引的文档结构要求

影响:查询可能会返回不完整的结果或意外失败

IBM Cloudant 中的 JSON 索引有一个与 SQL 数据库不同的关键约束条件:文档必须包含所有索引字段才能被纳入索引

问题

如果在 ["username", "status", "date"] 上创建索引,任何缺少其中一个字段的文档都会被完全排除在索引之外。 这意味着:

// This document WON'T be indexed
{
  "_id": "order123",
  "username": "bob@email.com",
  "status": "paid"
  // Missing "date" field
}

解决方案

方案 1:确保文件结构一致

// Always include all indexed fields
{
  "_id": "order123",
  "username": "bob@email.com",
  "status": "paid",
  "date": null  // Use null for missing values
}

方案 2:为不同的文档结构创建多个 JSON 索引

// Index for documents with all fields
{"fields": ["username", "status", "date"]}

// Index for documents without date
{"fields": ["username", "status"]}

方案 3:使用文本索引满足灵活的字段要求

{
  "index": {
    "fields": [
      {"name": "username", "type": "string"},
      {"name": "status", "type": "string"},
      {"name": "date", "type": "string"}
    ]
  },
  "type": "text"
}

文本索引将对缺少 date (或其他)字段的文件进行索引。

高级:部分索引

影响:索引大小和查询延迟减少 50-90

部分索引是一种强大的优化技术,可在编制索引前对文档进行预过滤,从而创建更小、更快的索引,同时降低存储成本。

何时使用部分索引

在下列情况下,部分索引是理想的选择

  • 您可以持续查询文档子集(例如,只查询活动记录)。
  • 您的数据库包含大量与大多数查询无关的数据。
  • 您希望降低 IBM Cloudant 存储成本(按 GB 计费)。

创建部分索引

使用 partial_filter_selector 指定要包含的文件:

{
  "index": {
    "fields": ["date", "category"]
  },
  "partial_filter_selector": {
    "status": "published"
  },
  "name": "published-articles-index"
}

该索引只包含 status: "published" 的文档,因此在查询已发布内容时,索引更小,速度更快。

partial_filter_selector 可以包含任何有效的选择器,而不仅仅是示例中简单的单字段选择器。

真实案例:博客系统

情景:一个拥有 100,000 篇文章的博客数据库,其中 80,000 篇为草稿,20,000 篇为已发表文章。

传统索引 (涵盖所有文件):

{"fields": ["category", "publishDate"]}

部分索引 (仅发表文章):

{
  "index": {"fields": ["category", "publishDate"]},
  "partial_filter_selector": {"status": "published"}
}

优点:

  • 索引大小:缩小 80
  • 查询速度:2-3x
  • 存储成本:大幅降低
  • 缓存效率:更多相关数据留在内存中

确保查询与部分索引兼容

要使用该索引,您的查询选择器必须与部分过滤器兼容

这招管用:

// Compatible - query includes the partial filter condition
{
  "selector": {
    "status": "published",    // Matches partial filter
    "category": "technology"
  }
}

这行不通:

// Incompatible - query doesn't match partial filter
{
  "selector": {
    "status": "draft",        // Contradicts partial filter
    "category": "technology"
  }
}

高级:涵盖索引

影响:2-5x 提高重读取工作负载的性能

覆盖索引是一种强大的优化技术--它可以完全通过索引回答查询,而无需检索原始文档。

了解覆盖索引

当您的索引包含满足查询选择器返回字段所需的所有字段时,就产生了覆盖索引:

// Index covers these fields
{
  "index": {
    "fields": ["username", "status", "date", "amount"]
  }
}

// Query only needs indexed fields
{
  "selector": {"username": "bob@email.com", "status": "paid"},
  "fields": ["username", "date", "amount"]  // All fields in index
}

IBM Cloudant 可以直接从索引中返回结果,而无需读取原始文档,从而大大提高了性能。

设计覆盖范围

围绕常见查询模式规划索引:

// Analyze your query patterns
const commonQueries = [
  {selector: {username: "...", status: "..."}, fields: ["date", "amount"]},
  {selector: {status: "...", date: {...}}, fields: ["username", "amount"]},
  {selector: {username: "..."}, fields: ["status", "date", "total"]}
];

// Design covering index
{
  "index": {
    "fields": ["username", "status", "date", "amount", "total"]
  }
}

当找到能覆盖查询的索引时,_explain 端点 将包含 "covering": true

高级:指数整合战略

与其创建许多狭窄的索引,不如考虑创建较少的更宽泛的复合索引,以覆盖多种查询模式。 使用更少的索引可以减少建立索引所需的时间。

替代:

{"fields": ["username"]}
{"fields": ["status"]}
{"fields": ["date"]}
{"fields": ["username", "date"]}

用途:

{"fields": ["username", "status", "date", "amount"]}

这种单一索引可以有效地为筛选查询提供服务:

  • username
  • usernamestatus
  • username, status,和 date 范围
  • 以上所有组合,在申请 amount 时可享受保障福利

生产部署清单

在将查询部署到生产环境之前,请进行验证:

指数战略审查

  • 所有生产查询都有适当的索引
  • 复合索引遵循“范围-前置-后置”规则
  • 字段排序反映数据的实际选择性
  • 查询不会意外返回到 _all_docs

性能验证

  • 使用生产规模的数据量测试查询
  • 监测效率比率(目标是 >50)
  • 验证执行时间是否符合 SLA 要求
  • 考虑部分索引以优化成本

监视设置

  • 启用 execution_stats 日志记录
  • 设置不良效率比率警报
  • 监控索引使用模式
  • 查询性能回归测试计划

错误处理

  • 使用 allow_fallback: false 防止代价高昂的事故
  • 优雅地处理索引选择错误
  • 规划部署期间的索引构建时间

进一步阅读

IBM Cloudant 博客有更多关于探索和提高查询性能的深入文章:

结论

优化 IBM Cloudant 查询性能是一个系统化的过程,首先要避免最昂贵的错误(大量数据库扫描),然后通过越来越复杂的技术逐步实现。 按照本指南进行操作:

  1. 基础知识创建适当的索引并避免大扫描
  2. 结构:正确排列复合索引字段,将范围排在最后
  3. 运算符选择高效的查询操作符
  4. 兼容性:了解文档结构要求
  5. 监测:持续测量和优化性能
  6. 先进:利用部分索引和覆盖模式

索引设计中的微小变化都能带来显著的性能提升。 一个经过适当优化的查询只需检查 2 个文档,就能返回 2 个结果,其结果总是优于一个未经过优化的查询,后者需要检查 50,000 个文档,才能得到同样的 2 个结果。

请记住:优化良好的查询与优化不佳的查询之间的区别不仅仅是性能,它还是可扩展应用程序与在负载下崩溃的应用程序之间的区别。