如何强制指定“partition”参数以利用覆盖索引避免跨分片扫描?

解读

在国内中大型互联网或金融场景里,CouchDB 常被用来做离线优先的移动端数据同步,或作为微服务中的高可用文档库
面试官抛出此题,核心想验证两点:

  1. 你是否真的理解 CouchDB 3.x 的 Partitioned Database 机制(国内很多候选人只把它当“MongoDB 替代品”,忽略了 partition 概念);
  2. 你是否能在覆盖索引(covering index)层面把 partition 键作为最左前缀,从而把查询范围锁在单分片内,避免触发代价高昂的 global 索引扫描。
    如果答不到“强制指定 partition 参数”这一动作,基本会被判定为“只用过 CouchDB,没调优过”。

知识点

  1. Partitioned Database:建库时 ?partitioned=true,每个 doc 必须带 _id=partition_key:doc_key,CouchDB 按 partition_key 做哈希分片。
  2. Partition 参数:在查询字符串里显式加 ?partition=partition_key,相当于告诉 Coordinator 节点“我只查这一个分片”,否则默认走 global 索引。
  3. 覆盖索引:CouchDB 的 Mango 索引若包含查询条件+排序字段+返回字段全部字段,就无需回表;把 partition_key 放在索引最左,可让分片级索引直接覆盖。
  4. 强制指定:在代码层绝不省略 partition=xxx,即使 SDK 有“自动推导”也手动写死,防止因版本差异或配置漂移退化为跨分片。
  5. 国内踩坑点
    • 阿里云 CouchDB 托管版对 global 查询有 QPS 限流,跨分片直接触发 429;
    • 移动端网络抖动时,若一次同步跨越多个 partition,易造成冲突风暴
    • 金融审计场景要求秒级点对点修复,跨分片扫描会把 RT 从 20 ms 拉到 2 s,无法通过 SLA。

答案

步骤一:建库时开启分区
PUT /bank_app {"partitioned":true}

步骤二:写入时把分区键放在 _id 最前面
PUT /bank_app/sh_001:acct_1001
{"_id":"sh_001:acct_1001","name":"张三","balance":100}

步骤三:创建覆盖索引,把分区键放最左
POST /bank_app/_index
{"index":{"fields":["**_id.partition**","balance","name"]},"name":"idx_cover_balance","partitioned":true}

步骤四:查询时强制指定 partition 参数
GET /bank_app/_partition/sh_001/_find
{"selector":{"balance":{"$gte":50}},"fields":["name","balance"],"use_index":"idx_cover_balance"}

解释:

  • _partition/sh_001 把扫描范围锁在单个分片;
  • 索引字段顺序与查询字段完全对齐,实现覆盖
  • 执行计划里 partitioned_index=truecovering=true,无 global 回表,避免跨分片扫描

拓展思考

  1. 如果业务需要按城市+日期双维分区,而 CouchDB 只支持单级 partition,如何设计复合分区键?
    答:用 city_yyyyMM 作为 partition_key,牺牲部分写入热点换取查询维度收敛;同时建partial index过滤历史冷分区,降低索引体积。
  2. 当 SDK(如 Spring-data-couchdb)未暴露 partition 参数时,如何不改源码强制指定?
    答:在拦截器层面对 /db/_find 请求做 URI 重写,把 POST /db/_find 改写成 POST /db/_partition/{pk}/_find,并校验 selector 里包含对应分区值,防止误路由。
  3. 国内信创环境要求国密算法加密字段,若加密后字段失去顺序性,覆盖索引会失效,如何平衡安全与性能?
    答:采用前缀保留加密(FPE) 只加密敏感后缀,把分区键与范围前缀留明文,确保索引最左匹配仍有效,同时满足等保测评对字段加密的要求。