当选择度低于 5% 时,如何创建复合索引避免回表?

解读

国内互联网与金融场景下,CouchDB 常被用来做离线-在线数据同步或微服务日志归档。面试官抛出“选择度低于 5%”这一极端过滤条件,核心想验证两点:

  1. 你是否理解 CouchDB 无回表的真正含义——B+Tree 索引即文档,只要索引里包含查询与排序所需的全部字段,就无需二次 fetch;
  2. 你是否能在 Mango 索引语法视图设计 之间做权衡,把高过滤列放在最左,同时把返回列打包进索引,实现 Covering Index
    答不到“把返回字段塞进索引”就基本掉分,答不到“选择度低字段放最左”会被追问代价。

知识点

  1. 选择度(Selectivity):distinct 值 ÷ 总行数;低于 5% 说明该列极稀疏,是过滤黄金列,必须放在复合索引最左侧。
  2. CouchDB 回表本质:Mango 查询若所需字段不在同一索引内,内部会触发 _find 二次读 doc,造成回表 IO;视图同理,若 emit 值不含后续使用字段,需 include_docs=true 回表。
  3. Covering Index:索引片里直接携带查询投影与排序字段,CouchDB 可直接从 .view.idx 文件返回结果,无需再打开文档本体。
  4. Mango 复合索引语法{"index": {"fields": [{"high_selectivity:asc"}, {"low_selectivity:asc"}, {"return_fieldA:asc"}, {"return_fieldB:asc"}]}};字段顺序决定前缀匹配,低选择度列不能放最左。
  5. 视图方案emit([highSelectivity, lowSelectivity], {fieldA: doc.fieldA, fieldB: doc.fieldB});value 用 JSON 对象一次性携带返回字段,查询时 startkey=[v1,v2]&endkey=[v1,v2,{}]&include_docs=false 即可覆盖。
  6. 国内部署注意
    • 磁盘多使用 SSD,但 mobile 边缘节点 IO 弱,covering 收益更大;
    • 复合索引字段数建议 ≤ 5,避免写入放大;
    • 生产环境务必在 test 库/_explain 验证是否 covering: true

答案

步骤一:确认最高选择度列
假设订单表 1 亿条,status="PENDING" 仅 300 万条(3%),则 status 为最高选择度列。

步骤二:把查询与排序所需的所有字段按“过滤列 → 排序列 → 返回列”顺序打入复合索引
Mango 示例:

{
  "index": {
    "fields": [
      {"status": "asc"},
      {"createTime": "desc"},
      {"orderId": "asc"},
      {"userName": "asc"}
    ]
  },
  "ddoc": "idx_cover_order",
  "type": "json"
}

查询语句:

{
  "selector": {
    "status": "PENDING"
  },
  "sort": [{"createTime": "desc"}],
  "fields": ["orderId", "userName", "createTime"],
  "limit": 100,
  "use_index": "idx_cover_order"
}

此时 /_explain 返回 "covering": true,证明无回表。

视图方案:

function (doc) {
  if (doc.type === 'order' && doc.status === 'PENDING') {
    emit([doc.status, doc.createTime], {orderId: doc.orderId, userName: doc.userName});
  }
}

查询:
GET /db/_design/order/_view/cover?startkey=["PENDING",{}]&endkey=["PENDING"]&descending=true&limit=100&include_docs=false
value 已含返回字段,无需 include_docs=true,实现零回表。

拓展思考

  1. 若返回字段很大(如商品详情 HTML),covering 索引会膨胀,需在 IO 与空间 之间权衡;可只 cover 主键,再用 bulk fetch 批量拿 doc,减少往返。
  2. 当查询条件出现 oror** 或 **regex 时,Mango 可能退化为全表扫描;此时应拆成多条视图或利用 分区数据库(db-per-user) 把数据量先降一个量级。
  3. 国内合规要求 敏感字段脱敏,即使做 covering 也要在索引构建阶段调用 sha256 哈希,避免明文落地。