当“docs_examined”远大于“docs_returned”时,如何优化?

解读

在国内 CouchDB 生产环境中,运维同学常在 Fauxton 的“Active Tasks”或日志里发现 docs_examineddocs_returned 的几十倍甚至上千倍。面试官抛出该问题,核心想验证两点:

  1. 你是否真正理解 Mango/视图的工作机制;
  2. 能否用最小代价把“全表扫描”变成“精准命中”,并兼顾国内常见的云主机 IO 瓶颈移动断网同步场景

知识点

  1. B+Tree 索引结构:CouchDB 的视图索引是 B+Tree,只能按“前缀有序”快速定位;一旦查询字段顺序与索引不一致就会退化成顺序扫描。
  2. 覆盖索引(Covering Index):如果索引条目中已包含 select-list 里的全部字段,CouchDB 可直接从索引返回结果,不再回读文档,显著减少 docs_examined
  3. Partial Index:国内很多业务把“软删除”字段(isDelete=0)当过滤条件,建立部分索引可把无效文档直接排除在索引外,降低索引体积与扫描行数
  4. 组合索引最左前缀原则: Mango 的“$and”查询里,等值字段放最左、范围字段放最右才能命中索引;一旦顺序错位就会触发全量扫描。
  5. 视图函数副作用:map 里如果 emit 了数组 key,但查询时只传了前缀,会退化成 range-scan;同理,reduce 为 _sum 时若 rereduce 失败也会重扫。
  6. 国内云盘 IO 抖动:docs_examined 高往往伴随 disk_read_count 飙升,导致同步端阻塞;优化后不仅 QPS 提升,还能减少移动网络下的流量费用

答案

  1. 确认查询计划:在 Fauxton → Run Query Explain 里查看 mrargs 的“start_key/end_key”是否收窄;若 start_key=[]、end_key={} 即全表扫描,需立刻建索引。
  2. 建立高筛选率组合索引
    • 把“等值过滤 + 排序”字段放最左,例如 {“selector”:{“orgId”:“10086”,“status”:“active”,“updateTime”:{“$gte”:“2024-01-01”}}},建索引 ["orgId", "status", "updateTime"]
    • 若查询里含 $or,拆成多条独立查询再合并,避免 CouchDB 无法复用单一索引。
  3. 使用 Partial Index 排除冷数据
    {
      "index": {
        "partial_filter_selector": {
          "isDelete": 0,
          "orgId": {"$exists": true}
        },
        "fields": ["orgId", "status", "updateTime"]
      },
      "ddoc": "idx_active",
      "type": "json"
    }
    
    这样已删除或测试租户的数据根本不进索引,docs_examined ≈ docs_returned
  4. 覆盖索引减少回表:把需要返回的字段全部塞进索引值或 emit 的 value,例如
    emit([doc.orgId, doc.status], {“_id”: doc._id, “name”: doc.name});
    查询带 ?include_docs=falsereduce=false,CouchDB 直接吐索引内容,磁盘 IO 降到 0
  5. 定期压缩与视图清理:国内很多业务写入频繁,旧视图索引碎片会导致范围扫描放大;设置 db_fragmentation=30% 自动压缩,并在低峰期执行 view_compaction,保证 B+Tree 扇出系数稳定。
  6. 监控验证:通过 /_stats/couchdb/httpd/requests_count/_node/_local/_systemdisk_read_count 对比,确认优化后 docs_examined 下降一个数量级,同时 query_latency_p99 从 800 ms 降到 80 ms 以内即达标。

拓展思考

  1. 移动边缘节点场景:国内高铁、矿区经常 4G/5G 断续,若 docs_examined 过高会直接把同步流量打爆;可结合 Partial Index + 增量复制过滤器(filter function 里直接 return doc.orgId===“10086”),让边缘 SQLite 只拉取索引命中的 doc,节省 70% 流量费
  2. 多租户+权限前缀:对于 SaaS 平台,把 tenantId+userRole 作为索引前缀,查询时必带,可天然隔离数据;同时利用 CouchDB 4.x 的内存映射索引缓存,把热点 tenant 的索引常驻内存,QPS 可再翻 3 倍
  3. 视图 vs Mango 取舍:如果业务需要复杂聚合,优先用视图 + reduce=_sum,但要在 map 里 提前裁剪字段,避免 emit 整个 doc;若只是点查,用 Mango 部分索引更轻量,升级 4.x 后可直接走原生 NIF,延迟更低
  4. 国产化信创适配:在鲲鹏 ARM 或麒麟 OS 上编译 CouchDB 时,打开 snappy 加速与 NEON 指令集,磁盘扫描的 CPU 耗时下降 15%,可把省下来的算力留给更多并发连接,同等硬件多跑 20% 业务实例