分片键选错会导致单点写入瓶颈或全集群扫描;基数须大于分片数×3,低基数字段单独作片键将引发jumbo chunk卡死;哈希分片键使范围查询变广播扫描,等值查询多用哈希、范围/排序查询多用范围分片;复合片键中哈希字段必须置末尾;片键不可更改,需兼顾写入均匀性、查询路由性和chunk可分裂性。

分片键选错,集群很快会退化成“单点写入瓶颈”或“全集群扫描”,不是某个分片磁盘爆满,就是查询慢得查不出数据。
分片键基数必须 > 分片数 × 3
基数低的字段(比如 status 只有 "active"/"inactive"、region 只有 3 个值)单独做片键,会导致 chunk 极少、无法拆分,所有写入挤在少数几个 chunk 上。MongoDB 会不断尝试迁移,但 jumbo chunk 无法分割,最终卡死。
快速验证命令:db.collection.aggregate([{$group: {_id: "$field"}}, {$count: "distinct_count"}])
- 若结果
-
_id不是万能解:ObjectID 前 4 字节是时间戳,天然写热点;UUID v4 看似随机,但若应用层生成不均,也可能分布倾斜 - 复合索引可救急:比如
{region: 1, user_id: 1},前提是user_id在每个region内足够分散
单调递增字段(如 created_at、seq_id)必须打散
直接用 {created_at: 1} 当片键,新数据永远写到最后一个 chunk,对应分片 CPU、IO、内存持续飙升,其他分片闲置。sh.status() 一眼可见 chunk 数严重失衡。
- 补救不是加索引,而是改策略:要么用
{created_at: "hashed"}(但范围查询失效) - 要么组合高基数前缀字段,例如
{tenant_id: 1, created_at: "hashed"},其中tenant_id必须满足基数 > 分片数 × 3 且写入稳定 - 切记:
sh.shardCollection()前必须已建对应索引,否则报错cannot shard collection without an index on the shard key
哈希分片键会让范围查询变广播式扫描
如果你大量执行 find({user_id: {$gt: 1000, $lt: 2000}}),而片键是 {user_id: "hashed"},mongos 就得把请求发给所有分片——本质是全集群扫表,延迟翻倍,QPS 断崖下跌。
- 等值查询多(如
{user_id: 12345})→ 哈希合适 - 范围/排序查询多(如日志按时间查、订单按 ID 段查)→ 必须用范围分片,且前缀字段要高频参与查询
- 折中方案:
{user_type: 1, user_id: "hashed"},但前提是user_type几乎每次查询都带等值条件(如{user_type: "vip"}),否则还是广播
复合片键中哈希字段必须放在末尾
顺序不可逆。写成 {user_id: "hashed", region: 1} 是无效的——MongoDB 不支持哈希字段后接非哈希字段。它只允许哈希作为最后一个组件。
- 正确写法只有两种:
{a: 1, b: "hashed"}或{a: "hashed"} - 前缀字段(如
a)决定数据分布主维度,也决定哪些查询能路由到单一分片;后缀哈希字段只负责在该前缀下进一步打散 - 一旦
sh.shardCollection()执行成功,片键不可更改,重分片代价极高,务必在数据量小、流量低时验证好再上线
最常被忽略的一点:片键不是“选一个高基数字段就行”,而是要同时扛住你的 write pattern 和 query pattern ——写入是否均匀、查询是否能命中单分片、chunk 是否能持续分裂,三者缺一不可。











