DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

MongoDB 复合唯一索引报 E11000,冲突的却是两个 null

复合唯一索引的缺失字段也会形成 null 键。按完整组合区分重复,核对 sparse 与 partial 的纳入范围,并在受控写入窗口替换索引、预先准备约束回退。

给 company 与 userName 创建复合唯一索引,MongoDB 却报 E11000 duplicate key error,dup key 里两个值都是 null。检查文档时,发现有些记录根本没有这两个字段。缺失字段仍可能产生空值索引键,唯一约束检查的是索引键,不是字段有没有出现在原 JSON 中。

本文针对普通升序或降序字段组成的复合唯一索引,并假定业务字段是标量。数组、多键索引和分片集合另有规则;不要把单字段“只容许一个 null”的解释直接套到所有复合索引。

复合索引约束的是整个组合

复合唯一索引比较 company、userName 组成的完整键。两个字段都缺失时,它们在键中对应 null,两条这样的记录就可能产生相同组合。某个字段单独缺失,并不必然冲突;另一个字段的不同值仍可能让组合不同。

先确认报错中的数据库、集合、索引名和 dup key,检查实际索引。下面是只读示例,集合与字段需替换:

db.accounts.getIndexes()
db.accounts.find(
  { company: null, userName: null },
  { company: 1, userName: 1 }
)

普通查询里的 field: null 同时匹配显式 null 与缺失字段,因此这个结果用来找到候选记录,不能证明它们全都显式存了 null。继续检查具体文档和真实 BSON 类型,并核对字段名、模型映射及大小写。

如果目标集合已有重复组合,建立唯一索引也会失败。需要先按业务规则处理重复,不能把创建索引失败当成索引已经生效。应用声明 unique,也不替代数据库里实际的索引定义。

先决定缺失值在业务上是否合法

如果 company 与 userName 必须存在,修复写入校验和模型映射,补齐经过确认的真实值,再建立需要的唯一约束。不要给所有缺失字段统一填一个空字符串:它仍可能形成重复组合,也未必代表有效业务身份。

如果某些文档本来就不属于账号身份记录,可以让唯一约束只覆盖符合条件的那部分文档。过滤条件应表达明确的业务范围,不能只是为了让失败的数据绕开约束。

例如,只有两个字段都为有效字符串的标量账号才参与唯一性检查,可以评估以下局部索引示例。这里要求应用或集合校验已经保证这两个字段不会是数组;$type 查询可能匹配数组元素,不能靠这段过滤器保证字段是标量:

db.accounts.createIndex(
  { company: 1, userName: 1 },
  {
    name: "company_userName_partial_unique",
    unique: true,
    partialFilterExpression: {
      company: { $type: "string" },
      userName: { $type: "string" }
    }
  }
)

两个条件都要满足,文档才进入这个部分索引。空字符串仍是字符串,也会参加约束;如果业务不允许空字符串,要另行校验,不能声称示例自动过滤了所有无效值。过滤器之外的文档不受这项唯一约束限制,这一变化必须符合数据模型。

sparse 不会自动排除显式 null

普通稀疏索引会跳过缺失相应索引字段的文档,但仍可索引显式 null。由普通升序或降序字段组成的复合 sparse 索引,只要文档含至少一个索引字段,就可能进入索引。只剩另一个字段缺失,并不意味着整条文档都会被跳过。

因此 sparse:true 与“两个字段都必须是字符串才检查唯一性”不同。先区分缺失、显式 null、空字符串和其他类型,再选择过滤规则。部分索引能更明确表达纳入条件,但同一个索引不能同时指定 sparse 和 partialFilterExpression。

已有普通唯一索引仍在时,新建一个部分索引不会解除旧索引的限制。不同 MongoDB 版本对相同键模式、不同选项的索引共存有具体限制,变更前应核对当前版本与所有现存定义,不能反复调用 createIndex 就假定它修改了原索引。

索引替换必须保留约束和恢复办法

保存数据备份、全部索引定义、字段校验与应用写入版本。先在隔离副本检查符合新过滤器的文档是否有重复组合,并估算索引构建的资源影响。正式变更时,安排能够控制写入的窗口与明确顺序,确保不在旧索引移除、新索引尚未成功之间接受违反目标规则的数据。

如果新索引已构建成功、版本又允许其与旧索引共存,仍要按审核后的流程切换约束;旧索引不移除,原错误可能继续存在。若版本不允许共存,更需要受控写入与备份方案,而不是先删除唯一索引再在线尝试。

回退恢复旧唯一索引时,期间新增的文档可能已经不满足旧规则,重建会失败。应事先定义这些记录怎样保留、迁移或恢复,不能把回退当成只恢复一行配置。

验收要检查哪些文档仍受唯一限制

查看实际索引及完整选项,在获准的测试环境核对:符合条件的不同组合可以写入,相同组合按设计被拒绝,允许缺失字段的文档也符合新模型。验证实际应用入口,避免只在手工构造的文档里成功,应用仍把字段写到了另一个名称。

还应检查依赖该索引的查询。部分索引只覆盖一部分文档,查询需要符合相应条件才能安全利用它;不能强制 hint 后漏掉其他文档,再把少返回的结果当成性能改善。修复的目标是正确的唯一性范围,不是让所有 E11000 都消失。