如何基于“/_explain”端点判断 Mango 是否退化为全表扫描?

解读

国内一线互联网公司在面试 P6/P7 级别 CouchDB 岗位时,常把 Mango 查询优化作为“必答题”。
“/_explain”端点返回的 JSON 里藏着 CouchDB 的查询执行计划,面试官真正想考察的是:

  1. 你是否能快速定位 index_used 字段;
  2. 当索引缺失或选择器无法命中索引时,能否从 mrargscovering 字段推断出“全表扫描”风险;
  3. 能否给出线上止血方案(建索引、改写选择器、强制索引提示)。
    一句话:用“/_explain”把“慢查询”扼杀在上线前。

知识点

  1. “/_explain”端点
    只对 Mango 查询(POST /{db}/_find)有效,返回 JSON 包含:

    • index_used:实际选用的索引名,若值为 null“_all_docs”,即退化为全表扫描;
    • mrargs:当 start_keyend_key 均为 {} 或缺失,且 index_used“_all_docs”,可确认扫描整个 B+Tree;
    • covering:布尔值,false 表示需要回表取文档,若再叠加 index_used=null,则必为全表扫描;
    • selector_rewrite:CouchDB 改写后的选择器,若出现 or拆不开、or** 拆不开、**regex 前缀无常量,也会导致无法命中索引。
  2. 国内常见误区

    • 以为建了索引就一定走索引:CouchDB 的 B+Tree 索引对 nene、regex 非前缀、$not 不友好;
    • 只看 “total_docs_examined” 不看 “index_used”:测试库数据量小,total_docs_examined 可能等于结果集,误导判断;
    • 忽略 “partitioned” 场景:分区库下跨分区查询会强制回退 “_all_docs”,即使索引存在。

答案

步骤一:构造待验证的选择器,向“/_explain”发 POST

POST /production/_explain
Content-Type: application/json
{
  "selector": {"status": "RUNNING", "created_at": {"$gte": "2024-01-01"}},
  "use_index": "idx_status_created"
}

步骤二:解读返回 JSON

{
  "index_used": "_all_docs",
  "mrargs": {"start_key": {}, "end_key": {}},
  "covering": false,
  "selector_rewrite": {"status": "RUNNING", "created_at": {"$gte": "2024-01-01"}}
}
  1. index_used“_all_docs” → 未命中任何索引;
  2. mrargsstart_keyend_key 均为空 → 将遍历整个主 B+Tree;
  3. covering=false → 每条文档都需回表;
    结论:该查询已退化为全表扫描,必须立即建复合索引或改写选择器。

步骤三:线上止血

{
  "index": {
    "fields": ["status", "created_at"]
  },
  "name": "idx_status_created",
  "type": "json"
}

建完后再次调用“/_explain”,确认 index_used 变为 “idx_status_created”mrargs 出现具体起止键,即解除全表扫描风险。

拓展思考

  1. 灰度索引验证
    国内大厂通常有“影子集群”,可先用 “w=0” 在影子库建索引,再对同一份数据调用“/_explain”,对比执行计划差异,避免直接在生产库建索引引发 IO 抖动。

  2. 复合索引顺序与选择性
    当选择器为 {"department": "A", "score": {"$gte": 90}} 时,若 department 基数低而 score 基数高,应把 score 放在复合索引左侧;否则“/_explain”会显示 index_used 虽不为空,但 mrargsend_key 仍很大,实际扫描行数爆炸。可通过 “bookmark” 分页采样,验证 “total_docs_examined / limit” 比值。

  3. 与 Redis 缓存联动
    国内高并发场景下,即使“/_explain”证明已走索引,仍可能因 covering=false 导致磁盘 IO 高。可在业务层加 Redis 热缓存,Key 设计为 “couch:{selector_hash}”,TTL 30 s,把 covering=false 的查询结果缓存,降低 CouchDB 读压力。