分片集群中唯一索引必须包含分片键作为前缀,否则创建失败;根本原因在于唯一性需由路由逻辑保障,而非跨分片协调索引。

分片集群唯一索引必须带分片键前缀
在分片集群里,createIndex({email: 1}, {unique: true}) 这种写法会直接失败——MongoDB 不允许创建不包含分片键的唯一索引。错误信息通常是:cannot create unique index over { email: 1 } with shards present。
根本原因是:唯一性必须由路由逻辑保障,而不是靠索引本身跨分片协调。所以你只能建形如 {region_id: 1, email: 1} 这样的复合索引,且分片键字段(如 region_id)必须是前缀。
- 如果已分片,再想加新唯一索引,必须确保该索引字段组合以当前分片键开头
- 若分片键是
{user_id: "hashed"},则唯一索引也得用{user_id: "hashed", email: 1},不能只用{email: 1} - 哈希分片键上不能单独建唯一索引,因为哈希值不具备顺序性,无法做全局去重保证
把去重字段设为分片键是最简方案
如果你的核心去重需求是 url 或 email,最直接的做法就是把它设为分片键:sh.shardCollection("db.users", {"email": 1})。这样所有相同 email 的文档必然落到同一分片,createIndex({email: 1}, {unique: true}) 才真正生效。
但要注意副作用:
- 分片键不可修改,选错会导致后续无法调整数据分布
- 高基数字段(如
email)做范围分片可能引发 chunk 拆分不均;建议搭配哈希分片:{"email": "hashed"} - 哈希分片后,
find({email: "a@b.c"})可以路由到单一分片,但find({email: {$gt: "a"}})就无法高效执行
复合分片键 + 应用层预计算是折中解法
当业务不允许把 email 单独作分片键(比如已有按 tenant_id 分片),可用复合分片键兜底:sh.shardCollection("db.users", {"tenant_id": 1, "email": 1}),再建唯一索引 createIndex({"tenant_id": 1, "email": 1}, {unique: true})。
这个组合能保证「同一租户内邮箱唯一」,但跨租户仍可能重复。如果需要全局唯一,就得靠应用层补位:
- 写入前先
find({tenant_id: X, email: "a@b.c"}),注意必须带上分片键字段,否则查询可能不命中正确分片 - 用
upsert时同样要确保查询条件含完整分片键,否则可能在不同分片各插一条 - 竞态条件无法完全避免,高并发场景建议加分布式锁或用事务(需 4.2+ 版本、副本集配置)
聚合查重必须加 allowDiskUse: true
即使索引设计合理,排查历史重复数据时仍大概率遇到:Exceeded memory limit for $group, but didn't allow external sort。这是因为在分片集群上,$group 阶段要把所有匹配文档拉到 mongos 归并。
安全写法只有三条:
- 聚合命令末尾显式传
{ allowDiskUse: true } -
$match尽量前置,过滤掉无关数据(比如加时间范围、状态字段) - 避免对高基数字段(如
createdAt、_id)做$group,优先选业务语义明确的低基数字段(如email、phone)
真正麻烦的不是怎么删重复,而是怎么让重复压根不出现——路由规则定下来那一刻,就决定了唯一性约束能不能落地。











