如何写回聚合结果到 CouchDB?

解读

国内面试官问“写回聚合结果”并不是想听 Map/Reduce 的入门概念,而是考察候选人是否真正在生产环境里做过离线汇总、增量统计、移动端同步这类场景。CouchDB 本身没有“UPDATE … GROUP BY”语法,也没有内置的物化视图自动回写机制,因此必须自己设计幂等、可重入、并发安全的写回方案。能否把聚合结果安全地落回同库或分库,并保证后续同步不冲突,是区分“会用”与“实战”的关键分水岭。

知识点

  1. Map/Reduce 视图仅产生索引,不落地文档;要持久化必须显式写回。
  2. _bulk_docs 批量接口支持 new_edits=false 模式,可绕过版本冲突,实现幂等覆盖
  3. 设计文档(_design/*)里可放 update 函数,在 HTTP PUT 一个聚合结果时由服务器端脚本完成“读-改-写”,避免客户端并发竞争。
  4. *_local/ 文档**不参与复制,可用来存“上次聚合游标”实现增量统计,降低重复计算。
  5. 多主复制场景下,同一聚合键可能出现版本分支,需用业务层合并策略(max、sum、last-write-wins)并在写回时把 _rev 置为最新。
  6. 国内阿里云、腾讯云托管 CouchDB 均关闭 reduce_limit = false,但默认单次查询仍限 200 条分组,需分页或增量聚合。
  7. Fauxton 控制台不能直接回写,必须通过代码或脚本,面试时要强调“自动化脚本 + CI 定时任务”而不是手工点按钮。

答案

线上环境推荐“增量聚合 + 幂等写回”两步走:
第一步,用 Mango 或视图按时间戳过滤出增量文档,在业务服务内存里做 reduce,得到聚合值。
第二步,调用 _bulk_docs 把结果写回专用聚合库(或同一库下的 agg_* 前缀文档),请求体里带上 new_edits=false 与固定 _id,保证重复跑任务不会生成新版本;若需覆盖旧值,则先 HEAD 取最新 _rev,再带 _rev 写回。
如果并发量高,可在设计文档里写一个 update 函数

function(doc, req) {
  if (!doc) {
    doc = {_id: req.id, count: 0, sum: 0};
  }
  var delta = JSON.parse(req.body);
  doc.count += delta.count;
  doc.sum   += delta.sum;
  return [doc, 'updated'];
}

客户端只需 PUT /db/agg_key 把增量差值发过去,服务器端完成原子累加,避免“读-改-写”竞争。
最后,用 _local/lastAggSeq 记录已处理过的 update_seq,下次从该序号继续扫描,实现断点续传小时级离线窗口统计。

拓展思考

  1. 如果聚合结果需要按租户分库,可让租户 DB 名带 agg_ 前缀,利用 CouchDB 的多库复制过滤器把结果同步到中心 BI 库,再二次汇总。
  2. 移动断网场景,可在 PouchDB 端先本地 reduce,再把结果文档同步到云端;此时要用业务时间戳 + 设备 ID 作为 _id 的一部分,防止不同终端写回同一 key 造成冲突。
  3. 国内金融类项目要求审计溯源,可在写回时同时生成一条 _id=audit/{UUID} 的审计文档,记录来源 update_seq、计算脚本版本、操作人,方便监管核查。