bigywater.tech
MongoDB · 32

唯一索引遇到 null 和缺失字段

唯一索引上第二个没有这个字段的文档能不能插入?显式 null 呢?这篇用同一个 email 字段在四种唯一索引设置下逐个试,再看复合索引和已有重复数据的情形。

E11000
唯一索引上第二个缺失 email 的文档就报这个错,dup key: { email: null }
3 个缺失 + 1 个 null
唯一 + sparse 的索引上能同时存在的缺失与显式 null 文档数
5 个
唯一部分索引(只管字符串 email)的集合最后的文档数,缺失与 null 不限

缺失就是 null

实测 · 本机执行输出
  test> db.a.createIndex({email: 1}, {unique: true})
  email_1
  test> db.a.insertOne({_id: 1, name: "a"})    // 没有 email
  true
  test> db.a.insertOne({_id: 2, name: "b"})    // 又一个没有 email 的文档
  E11000 duplicate key error collection: test.a index: email_1 dup key: { email: null }
  test> db.a.insertOne({_id: 3, email: null})    // 显式 null,与缺失的冲突
  E11000 duplicate key error collection: test.a index: email_1 dup key: { email: null }
  test> 两个不同的非空 email
  ok

普通唯一索引里,字段缺失与显式 null 都按 null 进索引,所以只能有一个。

唯一 + sparse

实测 · 本机执行输出
  test> db.b.createIndex({email: 1}, {unique: true, sparse: true})
  email_1
  test> db.b.insertMany([{_id: 1}, {_id: 2}, {_id: 3}])    // 三个缺失 email
  ok
  test> db.b.insertOne({_id: 4, email: null})    // 显式 null:第一个
  ok
  test> db.b.insertOne({_id: 5, email: null})    // 显式 null:第二个
  E11000 duplicate key error collection: test.b index: email_1 dup key: { email: null }

缺失字段的文档不进索引,可以有任意多个;显式 null 仍是一个值,只能有一个。

唯一 + 部分索引

实测 · 本机执行输出
  test> db.c.createIndex({email: 1}, {unique: true, partialFilterExpression: {email: {$type: "string"}}})
  email_1
  test> 缺失 ×2、null ×2
  ok
  test> 两个相同的字符串 email
  E11000 duplicate key error collection: test.c index: email_1 dup key: { email: "a@x.com" }
  test> db.c.countDocuments({})
  5

用 partialFilterExpression: {email: {$type: "string"}} 让只有字符串参与唯一性,缺失和 null 都不限制。

复合索引

实测 · 本机执行输出
  test> db.d.createIndex({a: 1, b: 1}, {unique: true})
  a_1_b_1
  test> db.d.insertOne({a: 1})    // b 缺失
  ok
  test> db.d.insertOne({a: 1, b: null})    // 和上一个相同的 (1, null)
  E11000 duplicate key error collection: test.d index: a_1_b_1 dup key: { a: 1, b: null }
  test> db.d.insertOne({a: 1, b: 2})
  ok
  test> db.d.insertOne({})    // a、b 都缺失
  ok
  test> 再来一个 a、b 都缺失的
  E11000 duplicate key error collection: test.d index: a_1_b_1 dup key: { a: null, b: null }

复合唯一索引逐个字段把缺失当 null,整个组合相同才冲突;{a: 1} 与 {a: 1, b: null} 冲突。

已有重复数据

实测 · 本机执行输出
  test> try { db.e.createIndex({k: 1}, {unique: true}) } catch (e) { ({code: e.code, message: e.message.slice(0, 200)}) }
  {
    code: 11000,
    message: 'Index build failed: 53e5d09f-245f-4067-b988-5190b0df7272: Collection test.e ( 3f36c4cf-fa04-42cb-abc0-0888acf89909 ) :: caused by :: E11000 duplicate key erro
  }
  test> 找出重复:$group 后 $match n > 1
  [
    { _id: null, ids: [ 3, 4 ], n: 2 },
    { _id: 'x', ids: [ 1, 2 ], n: 2 }
  ]

集合里已有重复值时建唯一索引会失败;用 $group 加 $match 找出重复的值和 _id,缺失的文档会被分在 null 一组。

错误信息

实测 · 本机执行输出
  test> try { db.a.insertOne({_id: 9}) } catch (e) { ({code: e.code, keyPattern: e.keyPattern, keyValue: e.keyValue, message: e.message}) }
  {
    code: 11000,
    keyPattern: { email: 1 },
    keyValue: { email: null },
    message: 'E11000 duplicate key error collection: test.a index: email_1 dup key: { email: null }'
  }
  test> try { db.d.insertOne({_id: 10, a: 1, b: 2}) } catch (e) { ({code: e.code, keyPattern: e.keyPattern, keyValue: e.keyValue}) }
  { code: 11000, keyPattern: { a: 1, b: 1 }, keyValue: { a: 1, b: 2 } }

E11000 的 keyPattern、keyValue 指出是哪个索引、哪个值冲突;缺失字段的冲突值显示为 null。

分步演示

唯一索引与 null
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MongoDB 9.0.2(Homebrew 的 mongod、mongos)/ mongosh 2.12.0 / pymongo 4.18.3;每个实验在临时目录里起全新的 mongod(独立 dbpath,端口 9900 以上,只监听 127.0.0.1,WiredTiger 缓存 0.25 GB),需要副本集或分片集群时在本机起多个进程;命令输出来自真实的 mongosh,批量写入、计时与多线程用 pymongo;计时用 time.perf_counter,数字每次运行会有小幅抖动,ObjectId、时间戳等每次运行都不同。每个设置都在新建的集合上验证,错误信息来自 mongosh
    • 没有验证:分片集合上的唯一索引限制(后面单独一篇);唯一索引对 upsert 并发的影响(下面几篇);collation 唯一索引;多键唯一索引(同一文档内重复元素不冲突);写关注对唯一性检查的影响
    京ICP备2026067502号-1京公网安备11010502063487号