MySQL唯一索引对NULL值跳过比较,不参与唯一性校验,因此允许多个NULL;主键索引则禁止NULL。联合唯一索引中任一列为NULL时整行绕过判重,这是SQL标准三值逻辑的实现,并非bug。

UNIQUE 索引对 NULL 的判断根本不用 “=”,而是跳过
MySQL 的 UNIQUE 约束不是靠“值相等”来判重,而是靠索引查找 + 唯一性检查。关键点在于:当某列值为 NULL 时,InnoDB 直接跳过该列的比较逻辑——既不认为它等于别的 NULL,也不拿它跟非 NULL 值比。这不是 bug,是 SQL 标准明确规定的三值逻辑(UNKNOWN)在存储引擎层的落地。
所以:INSERT INTO t (email) VALUES (NULL) 执行十次都不会报错;但只要出现两条 email = 'a@b.com',立刻触发 ERROR 1062 (23000): Duplicate entry 'a@b.com' for key 'uk_email'。
联合唯一索引里一个 NULL 就让整行“隐身”
比如建了 UNIQUE KEY (user_id, order_id),以下插入全部合法:
-
(1, NULL)和(1, NULL)—— 因为order_id是NULL,第二列不参与判重,只看user_id是否重复,而user_id是1,但单列不构成唯一约束 -
(NULL, 100)和(NULL, 100)—— 同理,user_id为NULL,第一列跳过,只剩order_id = 100,但单列也不受约束 -
(NULL, NULL)和(NULL, NULL)—— 两列全NULL,整行完全绕过唯一性检查
这和你直觉里的“组合去重”完全相反——不是“只要组合相同就拒绝”,而是“只要任一字段为 NULL,就不拿这行去比”。容易误以为索引失效,其实是它按设计本就如此。
想让 NULL 也参与去重?只能堵,不能疏
MySQL 没有配置项、没有 hint、没有开关能让 UNIQUE 把 NULL 当普通值对待。所有“让 NULL 变唯一”的方案,本质都是把 NULL 消掉或转成非空值:
- 建表时加
NOT NULL DEFAULT ''或NOT NULL DEFAULT -1,从源头禁绝NULL - 用生成列兜底:比如原始字段
mobile VARCHAR(20) DEFAULT NULL,再加mobile_key VARCHAR(20) STORED AS (COALESCE(mobile, '__NULL__')),然后在mobile_key上建UNIQUE - 应用层统一转换:ORM 或服务代码里把空值映射为哨兵字符串(如
'__MISSING__'),数据库永远收不到NULL
别试图用 WHERE col IS NULL 查询后人工去重——这个条件本身就会让索引失效,EXPLAIN 显示 type: ALL 或弱效的 ref_or_null,性能不可控。
已有重复数据时 ADD UNIQUE KEY 必然失败
执行 ALTER TABLE users ADD UNIQUE KEY uk_email (email) 前,如果表里已存在多条 email = 'test@example.com',命令会直接中断并报错。注意:NULL 不算重复,所以即使有 100 行 email IS NULL,也不会导致失败;但任意两个非 NULL 相同值都会卡住。
排查方式固定:
- 先跑
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1,找出所有非NULL的重复组 - 根据业务决定删、合并,或加条件过滤(比如只对
status = 'active'建约束——但这需要函数索引或生成列,普通UNIQUE KEY不支持WHERE) - 别信
ALTER IGNORE TABLE,该语法自 MySQL 5.7.4 起已被移除,强行用会报错
真正容易被忽略的是:你看到的“NULL 允许多个”,不是索引松懈,而是它严格遵守 SQL 标准中“NULL ≠ NULL”这一前提;而你在联合索引里漏掉的那一个 NULL,可能正悄悄绕过你精心设计的业务唯一性保障。


















