哈希分片键必须搭配非哈希前缀才能支持高效范围查询;单独使用"_id": "hashed"会导致$gt、$lt等操作广播到所有分片,而复合键如{city: 1, uid: "hashed"}可实现局部扫描与写入均衡。

哈希分片键必须搭配非哈希前缀才能支持范围查询
单纯用 "_id": "hashed" 作为分片键,聚合里所有带 $gt、$lt、$regex 或 $sort 的阶段都会触发广播——mongos 得发请求到每个分片,再合并结果。这不是性能问题,是架构限制。真正能兼顾均匀写入和局部范围扫描的,是「复合分片键」:前缀字段用于范围/区域定位,哈希字段负责打散写入热点。
比如业务按城市 + 用户ID 分布,{ city: 1, uid: "hashed" } 就比单独 { uid: "hashed" } 更实用:查 city: "shanghai" 能命中单一分片,再加 uid 等值条件还能进一步收敛;但若只查 uid: ObjectId(...),就只能靠哈希定位,不支持范围。
- 前缀字段(如
city、tenant_id、region)必须建在哈希字段之前,顺序不能颠倒 - 前缀字段值分布要尽量均匀,避免某几个值占 80% 数据(否则仍会形成热点分片)
- 哈希字段本身不能是数组、浮点数(尤其 > 253)、或含嵌套空值的字段,否则建索引失败或查询错漏
聚合管道里哪些阶段会被哈希分片键“拦腰截断”
只要 $match 阶段没覆盖完整分片键前缀,后续所有阶段都可能降级为广播执行。常见踩坑点:
-
$match只写了哈希字段(如{ uid: ObjectId(...) })→ 可定位单分片,但无法利用前缀做范围裁剪 -
$match写了前缀但用了$in查 3 个以上离散值(如{ city: { $in: ["bj", "sh", "gz", "sz"] } })→ mongos 会向这 4 个对应分片发请求,不算全广播,但已损失局部性 -
$sort字段不是分片键前缀 → 排序必须在 mongos 层合并后做,内存压力陡增;若加了$limit却没配$hint,很可能各分片各自 limit 再合并,结果不准确 - 用
$lookup关联另一个分片集合时,如果被关联集合的分片键与本地不一致,99% 概率触发全广播
createIndex 时别漏掉前缀字段的排序方向
复合哈希索引本身不要求指定哈希字段的排序("hashed" 已隐含),但前缀字段的升序/降序会影响聚合中 $sort 是否能命中索引。例如分片键是 { region: 1, ts: "hashed" },而你常做 .sort({ region: 1, created_at: -1 }),那就得额外建一个 db.coll.createIndex({ region: 1, created_at: -1 }) —— 哈希分片键 ≠ 查询索引,二者职责不同。
- 分片键索引(含哈希)只管数据怎么分到哪个分片,不管查询怎么加速
- 聚合中涉及排序、范围过滤、group key 的字段,需单独建对应方向的普通复合索引
- 哈希字段上不能建唯一索引,如果业务要求
uid全局唯一,得额外加db.coll.createIndex({ uid: 1 }, { unique: true })
convertShardKeyToHashed() 是调试哈希值的唯一可靠手段
别猜哈希结果,也别用第三方 hash 库模拟。MongoDB 的哈希函数对 ObjectId、字符串、嵌套文档有特定折叠逻辑,且浮点数会先截断为 int64。实际部署前,必须用 convertShardKeyToHashed() 验证典型值的哈希分布:
mongosh
> convertShardKeyToHashed({ city: "shanghai", uid: ObjectId("65a1b2c3d4e5f67890123456") })
{ city: "shanghai", uid: NumberLong("-4567890123456789012") }
- 检查哈希值是否大致均匀(用不同
uid测 100 个,看 NumberLong 正负分布和量级) - 若大量哈希值集中在同一千位区间(如全为 -4567...),说明前缀字段
city基数太低,或uid实际重复率高 - 该函数返回的是 NumberLong,不是字符串,代码里拼接分片键时别误转成字符串再哈希
哈希分片真正的复杂点不在语法,而在前缀字段的业务语义是否稳定——今天按 tenant_id 分,明天租户合并,前缀就失效;今天 region 有 50 个值,明天缩成 5 个,数据块立刻倾斜。这些没法靠命令修复,只能靠设计时留余量。


















