MongoDB复合分片键必须基于已创建的、字段顺序和方向完全匹配的索引,且分片键前缀查询才能定向路由,否则广播;一旦设定不可修改,唯一性约束需覆盖全部分片键字段。

可以,MongoDB 分片键支持多个字段,即复合分片键({ field1: 1, field2: -1 }),但必须是**已存在的索引前缀**,且该索引需在分片前创建;否则 sh.shardCollection() 会直接报错。
复合分片键必须基于已建索引
分片操作不会自动创建索引。如果集合非空,sh.shardCollection("db.coll", { a: 1, b: -1 }) 会失败,并提示 "cannot shard collection unless it has an index on the shard key"。
- 必须先手动创建匹配的索引:
db.coll.createIndex({ a: 1, b: -1 }) - 字段顺序、方向(
1或-1)必须与分片键定义完全一致;{ a: 1, b: 1 }不能用于{ a: 1, b: -1 }的分片键 - 索引方向影响数据分布:虽然 MongoDB 允许
-1,但分片键值本身按 BSON 比较规则排序,-1不改变底层块划分逻辑,仅影响索引扫描方向
复合分片键的排序与数据分布规则
分片键值是将整个文档中对应字段拼接后形成的“合成键值”,MongoDB 按 BSON 顺序对它做全局排序,再切分为连续的块(chunk)。例如:
索引定义:{ region: 1, user_id: "hashed" }
文档1:{ region: "CN", user_id: 1001 } → 键值:"CN" + hash(1001)
文档2:{ region: "US", user_id: 1002 } → 键值:"US" + hash(1002)
此时所有 region: "CN" 的文档会被尽量聚在相邻块中,但若 user_id 哈希后分布不均,仍可能导致单 region 下的块倾斜。
- 前导字段(如
region)决定粗粒度分布,基数低(如只有 3 个 region)会导致最多只有 3 个数据热点 - 后续字段(尤其哈希字段)用于打散,但哈希字段**不能单独用于范围查询**,否则无法路由到具体分片
- 不支持在非前缀字段上做等值查询来触发定向路由,例如分片键为
{ a: 1, b: 1 },查询{ b: 5 }仍是广播查询
查询时如何命中复合分片键提升效率
只有查询条件包含**分片键前缀**(prefix)才能触发定向查询(targeted query);否则 mongos 必须向所有分片发请求,性能断崖式下降。
- 分片键
{ tenant_id: 1, log_time: -1 }:- ✅
find({ tenant_id: "t1" })→ 定向 - ✅
find({ tenant_id: "t1", log_time: { $gt: ISODate("...") } })→ 定向 - ❌
find({ log_time: { $gt: ... } })→ 广播
- ✅
- 即使你建了
{ log_time: -1, tenant_id: 1 }索引,只要分片键不是这个顺序,就无法用log_time做路由 - 注意:分片键前缀匹配是严格左对齐的,不支持跳过中间字段(即没有“覆盖索引”式的分片路由优化)
容易被忽略的兼容性陷阱
复合分片键一旦设定,**不能修改或删除**;想换分片策略只能导出重建集合。更隐蔽的问题是唯一性约束失效:
- 若唯一索引字段不包含全部分片键(如分片键是
{ a: 1, b: 1 },唯一索引只建在{ c: 1 }),则c的唯一性只在单分片内生效 - 哈希字段参与复合分片键时(如
{ a: 1, b: "hashed" }),b字段本身不能用于$in或等值查询路由,因为哈希值不可预测,mongos 无法计算目标分片 - 时间戳类字段慎作第二字段:若
tenant_id基数高但log_time单调递增,新写入仍会集中在少数分片(因哈希未覆盖时间维度)
真正难处理的不是怎么设,而是设完之后——查询模式稍有偏移,就从定向退化成广播,而这种退化在测试环境极难暴露,往往上线后流量一上来才显现。











