_id字段可作分片键但默认不推荐;范围分片({_id: 1})因ObjectId时间戳单调递增导致写热点,哈希分片({_id: "hashed"})可均匀分布,需先建哈希索引且非空集合须预建索引。

直接回答:_id 字段可以当分片键,但默认不推荐——除非你明确用 hashed 模式
因为 MongoDB 默认的 _id 是 ObjectId,它本质是**单调递增时间戳 + 机器标识 + 进程号 + 自增计数器**。用它做范围分片({_id: 1})会导致所有新写入都打到同一个分片,形成严重写热点;而用哈希分片({_id: "hashed"})则能均匀分布,这是目前最稳妥的“开箱即用”方案。
为什么 {_id: 1} 做范围分片会卡死写入
范围分片按 _id 值大小切块,而 ObjectId 的前 4 字节是秒级时间戳,新文档的 _id 几乎总是比旧文档大。结果就是:
- 所有新插入都落在“最大值块”上,该块持续分裂、迁移,最终长期驻留在某一个分片
- 这个分片 CPU、磁盘 IO、网络带宽率先打满,其他分片空转
-
mongos执行insert时看似分布式,实际是单点写入,吞吐量上不去 - 即使开了多个
mongos实例,也无法缓解底层分片的写压力
用 {_id: "hashed"} 的实操要点和坑
哈希分片把 _id 哈希后映射到一个伪随机整数空间,彻底打破时间局部性。但要注意这些细节:
- 必须先建哈希索引:
db.collection.createIndex({_id: "hashed"}),再执行sh.shardCollection() - 如果集合非空,
sh.shardCollection("db.col", {_id: "hashed"})会失败,报错"cannot shard collection with non-empty data without existing hashed index" - 哈希后,
find({_id: ObjectId(...)})仍能精准路由到单一分片(targeted operation),但find({_id: {$gt: ...}})这类范围查询会退化为广播操作(broadcast operation),性能明显下降 - 浮点型
_id(极少见)不能哈希:MongoDB 会截断小数部分,2.1、2.9哈希结果一样,导致冲突
什么情况下该放弃 _id,换别的字段做分片键
如果你的应用有更自然的业务维度,且满足高基数、低频更新、非单调变化,就别硬扛 _id。典型场景包括:
- 用户系统:用
user_id(UUID 或数字 ID)+"hashed",比_id更贴近查询模式(如查某用户所有订单) - IoT 设备数据:用
device_id范围分片,配合zone标签按地域隔离冷热数据 - 日志类集合:用
log_type(字符串,基数有限)+timestamp(时间戳)复合分片,避免单一字段倾斜 - 注意:一旦
shardCollection执行成功,分片键就不可更改——选错就得导出重建,没有后悔药
最容易被忽略的是:哈希分片解决不了范围查询性能问题。如果你大量依赖 $gte/$lt 查时间窗口或数值区间,哪怕用了 {_id: "hashed"},也得接受广播开销。这时候该考虑复合键或区域分片,而不是反复调优哈希本身。

















