复合分片键必须对应前缀顺序一致的复合索引,哈希字段须为最后一个且类型严格匹配,region+hashed组合最稳,空集合分片需指定numInitialChunks避免热点。

复合分片键必须满足索引前置要求
复合分片键不是任意字段组合,它必须对应一个已存在的**复合索引**,且该索引的前缀字段顺序必须与分片键定义完全一致。MongoDB 不会自动为你创建这个索引——如果集合非空,sh.shardCollection() 会直接报错 IndexNotFound。
常见错误现象:执行 sh.shardCollection("db.col", { region: 1, user_id: "hashed" }) 时失败,提示“no suitable index found”。这是因为缺少 { region: 1, user_id: "hashed" } 这个确切顺序和类型的索引。
实操建议:
- 先用
db.col.createIndex({ region: 1, user_id: "hashed" })显式建索引(注意:哈希只能出现在**单个字段**上,不能对多个字段都标"hashed") - 确保索引字段类型与分片键声明严格匹配(例如
"hashed"不能写成"hash") - 若已有其他复合索引如
{ user_id: "hashed", region: 1 },它无法复用——顺序错,MongoDB 不认
region + hashed 组合是生产中最稳的模式
当业务有地理隔离、合规或低延迟写入需求时,{ region: 1, _id: "hashed" } 或 { region: 1, user_id: "hashed" } 是经过验证的可靠组合。前缀字段 region 支持范围查询和区域分片(zone sharding),后缀哈希字段破除单调性,避免写入热点。
为什么不用 { region: "hashed", user_id: 1 }?因为哈希前缀无法支持 region: "cn" 这类等值路由,mongos 无法定位目标分片,会退化为广播查询。
使用场景举例:
- 用户消息表需按
country隔离存储,同时防止按时间递增的created_at导致写入集中 - SaaS 多租户系统中,
tenant_id作前缀保证租户数据物理局部性,_id哈希保证单租户内写入分散
别把高基数字段塞在哈希位置后面
复合分片键中,哈希字段**必须是最后一个**,否则无法发挥负载均衡作用。比如 { user_id: "hashed", status: 1 } 看似合理,但 status 只有有限几个值("active"、"inactive"),实际形成的分片键空间被压缩成 hash(user_id) × N,N 很小,整体关联基数仍受限。
更糟的是,如果 status 查询频繁,而 user_id 又是哈希的,那么 { status: "active" } 这种查询无法命中索引前缀,mongos 只能全分片扫描。
正确做法:
- 把高频过滤、低基数字段(如
status、type)放在**前缀位置**,前提是它们能配合区域或业务逻辑形成自然分区 - 把真正高基数、单调增长的字段(如
_id、order_no)放最后并哈希 - 避免在哈希字段之后再加任何字段——MongoDB 不允许,
sh.shardCollection()会拒绝
空集合分片时,复合键的初始 chunk 分布不可控
对空集合执行 sh.shardCollection("db.log", { service: 1, ts: "hashed" }),MongoDB 默认只创建 **1 个初始 chunk**,覆盖整个键空间 MinKey → MaxKey。即使你后续添加了 zone range,这个 chunk 也不会自动拆分,得等写入触发均衡或手动 sh.splitAt()。
容易踩的坑:误以为“指定了复合键就自动均匀分布”,结果上线后所有写请求打到同一个 shard,直到 chunk 开始分裂才缓解。
缓解方式:
- 提前预估
service的取值数量(如 5 个微服务名),用numInitialChunks参数强制初始化更多 chunk:sh.shardCollection("db.log", { service: 1, ts: "hashed" }, { numInitialChunks: 20 }) - 若已启用 zone,务必在
shardCollection()前用sh.addShardToZone()和sh.updateZoneKeyRange()定义好范围,否则初始 chunk 不按 zone 分配


















