分片集群中唯一索引仅在单个分片内生效,无法保障全局唯一;相同值若路由至不同分片则插入成功,导致重复数据。根本解法是将去重字段设为分片键或复合分片键,确保同值文档落在同一分片。

分片集群中唯一索引无法跨分片保证全局唯一,仅在单个分片内生效——这是重复数据最常被忽略的根源。
唯一索引在分片集群中只作用于单个分片
当你在分片集合上对 url 字段创建唯一索引:db.collection.createIndex({url: 1}, {unique: true}),MongoDB 实际是在每个分片上分别建索引。如果两条相同 url 的文档被路由到不同分片(比如因分片键与 url 无关),插入会成功,不报错。
常见错误现象:
- 应用层反复调用
insertOne却没报错,但查出多条相同url记录 -
upsert在某些情况下也失效——因为查询时可能没命中正确分片(尤其分片键未参与查询条件)
所以:除非分片键包含你要去重的字段(如 {url: 1} 或复合分片键含 url),否则唯一索引形同虚设。
聚合任务排查重复必须加 allowDiskUse: true
在分片集群上运行去重聚合(如按 email 分组查重复),$group 阶段需把所有匹配文档拉到一个 mongos 节点做归并,极易触发内存超限:
错误信息:Exceeded memory limit for $group, but didn't allow external sort
实操建议:
- 聚合命令末尾必须显式加上
{ allowDiskUse: true } - 避免在
$group中使用高基数字段(如timestamp)作为分组依据 - 优先缩小输入范围:在
$match阶段就过滤掉明显不需要的数据(例如加时间范围、状态条件)
示例(安全写法):
db.users.aggregate([
{ $match: { email: { $exists: true, $ne: null }, createdAt: { $gte: ISODate("2025-01-01") } } },
{ $group: { _id: "$email", count: { $sum: 1 }, ids: { $addToSet: "$_id" } } },
{ $match: { count: { $gt: 1 } } }
], { allowDiskUse: true })
真正可靠的去重必须依赖分片键设计
想让唯一性约束在分片集群中真正起效,核心不是“怎么删”,而是“怎么路由”:
- 把去重字段设为分片键(如
sh.shardCollection("db.collection", {url: 1})),这样相同url必定落在同一分片,唯一索引才有效 - 若不能改分片键,可用复合分片键 + 哈希(如
{hash_url: "hashed"}),再配合应用层预计算哈希值确保路由一致 - 业务层写入前先
find查询(带分片键条件),确认不存在再insert——但要注意读写间存在竞态窗口
注意:upsert 在分片环境下仍受分片键限制:只有查询条件包含完整分片键时,才能精准定位到目标分片执行更新或插入。
批量删除重复文档要防误删和锁表
用聚合结果驱动 remove 或 deleteMany 时,分片集群下容易出问题:
- 不要用
forEach+remove(已废弃且不支持分片集群的原子性) - 改用
deleteMany并传入明确的_id数组,但数组长度别超 10 万,否则可能超命令大小限制 - 生产环境务必加
limit和分批处理(例如每次删 1000 条),避免长时间持有分片锁 - 删除前先备份:用
mongodump -d db -c collection --query '{_id: {$in: [...]}}'导出待删文档
最易被忽略的一点:分片集群中,deleteMany 的查询条件如果不含分片键,会被广播到所有分片执行——性能差,还可能误删其他分片上的非重复数据。











