{company_id: "hashed", create_time: 1}是危险组合,因company_id区分度低导致哈希后仍数据倾斜,且create_time在哈希字段后使范围查询无法路由而广播全分片;哈希字段须高区分度、单值、非浮点,前置字段须覆盖高频查询条件。

为什么{company_id: "hashed", create_time: 1}是危险组合
这个键在 SaaS 多租户场景下极易引发数据倾斜和广播查询。company_id 区分度低(比如 1000 家客户,但 80% 文档集中在 top 5),哈希后 chunk 仍扎堆;而 create_time 放在哈希字段之后,所有 {$gt: ISODate("2026-01-01")} 查询都会广播到全部分片——因为哈希打乱了时间顺序,mongos 无法判断哪些 chunk 可能包含该时间范围。
关键问题不在“用了哈希”,而在字段顺序和业务语义错配:
- 哈希字段必须是高区分度、单值、非浮点(或 ≤253)的字段,如
user_id或order_id - 前置字段必须支撑高频路由条件,且本身不能单调递增(否则产生写热点)
- 不能同时存在
{"company_id": "hashed"}和{"company_id": 1}两个索引,mongos 可能误选后者触发全量扫描
怎样用analyzeShardKey验证键是否合理
analyzeShardKey 不是拍脑袋工具,它基于真实采样做分布模拟。执行时务必加 readWriteDistribution: true,否则会漏掉写热点问题:
db.orders.analyzeShardKey(
{region: 1, user_id: "hashed"},
{sampleSize: 200000, readWriteDistribution: true}
)重点盯三项输出:
-
imbalancedChunks> 5%:说明即使加了哈希,数据仍倾斜——可能是region值分布极不均,或user_id实际重复率高 -
broadcastOperations高:说明前置字段没覆盖常见查询条件,比如漏了region过滤,导致所有{create_time: {$gte: ...}}查询都广播 -
estimatedChunkCount超过 1000:前缀粒度太细(如把region细分到region + city + district),chunk 数爆炸,迁移压力大
复合分片键里哈希字段必须满足什么硬约束
哈希字段不是随便选的。MongoDB 对 "hashed" 的底层实现有严格限制:
- 必须是单值字段:不能是数组(
[1,2,3])、不能是嵌套文档({a:1,b:2}) - 浮点数必须可无损转为 64 位整数:像
3.1415926会被截断成3,多个不同小数值哈希后撞车;超2^53的浮点数直接报错 - 不能是
_id字段本身(除非你明确用{_id: "hashed"}单字段分片) - 哈希值不可预测:别指望用
convertShardKeyToHashed()提前算好路由——mongos 内部哈希逻辑可能随版本微调
实操建议:优先选业务主键类字段(user_id, order_id),避开时间戳、自增 ID、经纬度等易单调或精度失控的字段。
refineCollectionShardKey 能否安全加字段后缀
可以,但仅限追加后缀字段,且新字段必须已存在于所有文档中(或允许缺失,默认归入 null chunk)。例如原键是 {region: 1},想加 user_id 哈希补足区分度:
sh.refineCollectionShardKey("mydb.orders", {region: 1, user_id: "hashed"})注意三个现实限制:
- 操作期间集合写入会阻塞约 2 秒,应用需容忍短暂延迟
- 每个分片需预留至少
(collection_size + index_size) * 2 / shard_count的空闲空间,否则失败 - 如果原集合有唯一索引,新分片键必须能保证该唯一性(比如原索引是
{order_id: 1},新键含order_id才合法)
真正容易被忽略的是:refine 操作不会重分布已有数据,只影响后续插入和迁移中的 chunk 拆分逻辑。若旧数据已严重倾斜,refine 后仍需靠均衡器慢慢搬——这不是一劳永逸的“修复”,而是模型演进的起点。


















