MySQL 中 NULL 导致唯一键失效

本文探讨了在MySQL中,NULL值如何影响唯一键约束。当字段包含NULL时,唯一约束可能失效,允许重复记录。解决办法是避免在唯一约束字段上使用NULL,或者将NULL转换为空字符串。同时,文章解释了MySQL中NULL的特殊性质,两个NULL值被视为不相等,因此在唯一索引中可以共存。此外,还介绍了主键索引与唯一索引的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近遇到一个问题,我为一张表中创建了一个唯一键,并且键中字段为NULL,最终导致了唯一约束失效。这里做下分析:

首先新建一张表,包含 work_no,name,age 三个字段:

DROP TABLE IF EXISTS t_emp;
CREATE TABLE t_emp(
  id int(8) not null auto_increment,
  work_no varchar(8) comment  '工号',
  name varchar(255) comment '姓名',
  age int(3) comment '年龄',
  primary key(id),
  unique key(work_no,name,age)
)engine=InnoDB,charset=UTF8mb4,comment="员工表";

从创建语句中我们可以看到,我们为工号,姓名,年龄上添加了唯一约束,现在我们尝试插入数据:

insert into t_emp(work_no,name,age) values ('9527','张三',23);
insert into t_emp(work_no,name,age) values ('9527','张三',23);

我们在尝试插入第二条数据时会报错:

(1062, “Duplicate entry ‘9527-张三-23’ for key ‘work_no’”)

可见唯一键是生效了的,现在我们尝试将 age 为 null 再次尝试下:

insert into t_emp(work_no,name,age) values ('9527','张三',NULL);
insert into t_emp(work_no,name,age) values ('9527','张三',NULL);

可以看到当 age 为空时附加在该字段上的唯一约束失效了。

首先我们可以确认 null 值的出现破坏了唯一性,原本应该被唯一约束限制到的记录被插入到了表中。

如何确保 NULL 不破坏唯一性?

  • 不允许列出现 NULL 值,即NOT NULL
  • 给列增加 DEFUALT 值,注意 DEFAULT 不能是 NULL

所以需要调整建表语句:

DROP TABLE IF EXISTS t_emp;
CREATE TABLE t_emp(
  id int(8) not null auto_increment,
  work_no varchar(8) NOT NULL DEFAULT '' comment  '工号',
  name varchar(255) NOT NULL DEFAULT '' comment '姓名',
  age int(3) NOT NULL DEFAULT 0 comment '年龄',
  primary key(id),
  unique key(work_no,name,age)
)engine=InnoDB,charset=UTF8mb4,comment="员工表";

简单来说就是不要在唯一约束上使用 NULL,但是空字符串是可以的。

如果我们再继续寻找其根本原因的话,就要探究 NULL 在 MySQL 中的实现原理了。

深度探讨

关于NULL 值是否应该被唯一约束限制到的问题,早年有人向 MySQL 开发提出过问题。具体可以参考文档,可以看到开发者给出的解释是:
在这里插入图片描述
感兴趣的童鞋可以点进去看下,关于这个问题是否是一个BUG 的问题探讨的还是很激烈的。最终结果还是显而易见的,MySQL 保留了空值不受唯一约束的限制的这个特性。开发者通过调整空值为空字符串来处理该类异常问题。

NULL 与 NULL 不相等

还记得在 MySQL 中如何以字段值为 NULL 作为条件查询吗?

SELECT * FROM t_emp WHERE age IS NULL;

NULL 在 MySQL 中作为一个特殊的存在,我们无法使用平常使用的等值查询进行查询:

MySQL root@localhost:test> select null = null;
|| null = null ||
| <null> |
1 row in set
Time: 0.001s
MySQL root@localhost:test> SELECT NULL IS NULL;
|| NULL IS NULL ||
| 1 |
1 row in set
Time: 0.001s

比如我们有一个单列的唯一索引,既然实际会有空值的情况,如果唯一约束对空值也有起作用,就会导致仅有一行数据可以为空,这可能会和实际的业务需求相冲突的。所以通常MySQL的存储引擎的唯一索引对NULL值是不适用的。 这也就倒是联合唯一索引的情况下,只要某一列为空,就不会报唯一索引冲突。

或者说,我们一般认为在 MySQL 中 NULL 是一个没有被赋值的值,既然没有被赋值,那么它就有可能被赋值为任意值。这样就可以理解为两个任意值不相等,也就是两个 NULL 值是不相等的,因此也就可以在 唯一索引中单独存在了。

面试题:主键索引与唯一索引的区别

  • 唯一索引的索引列允许空值,而主键索引的列不允许空值
    • 唯一索引列存在空值时,唯一约束对空值时不生效的
  • 主键索引可以被引用为外键,而唯一索引不能
  • 一个表中最多只能创建一个主键索引,单可以创建多个唯一索引

主键索引 = 唯一索引 + 唯一约束 + 非空约束
唯一索引 = 唯一索引 + 唯一约束

参考资料

MySQL: 唯一索引与NULL
Mysql 唯一索引的字段值
Bug #8173 unique index allows duplicates with null values

<think>好的,用户现在问的是MySQL中索引失效的情景。之前他问过JVM类加载机制如何保证线程安全,现在转向了数据库索引的问题。这说明用户可能在学习后端开发,或者正在准备面试,需要了解常见的性能优化点。 首先,我需要回忆一下常见的索引失效的情况。比如,使用函数、类型转换、OR条件、模糊查询以通配符开头、最左前缀原则等等。这些都是比较典型的情况,但需要确认是否正确,有没有遗漏。 用户可能想知道为什么索引会失效,以及如何避免。所以不仅要列出情景,还要简单说明原因,这样他们能更好地理解。比如,当在WHERE子句中对字段使用函数时,索引为什么会失效?因为索引存储的是原始值,应用函数后需要计算,导致无法直接使用索引结构进行查找。 还要注意用户可能的误区,比如组合索引的最左前缀原则,可能用户会错误地认为只要用了组合索引中的某些字段就能生效,但实际上必须从左开始连续使用。比如索引是(a,b,c),查询条件用了b和c,这时候索引可能用不上。 另外,用户可能遇到隐式类型转换的问题,比如字段是字符串类型,但查询时用了数字,导致MySQL进行类型转换,从而无法使用索引。这也是常见的陷阱,特别是当表设计不够严谨的时候。 还要考虑范围查询后的索引失效,比如WHERE a > 1 AND b = 2,如果索引是(a,b),那么范围查询之后的字段b可能无法使用索引。这时候需要调整索引的顺序或者分开查询条件。 OR条件导致索引失效的情况也需要提到,尤其是当OR两边的字段有任何一个没有索引时,整个查询可能无法使用索引。这时候可以考虑用UNION代替OR,或者为相关字段添加索引。 另外,索引列参与计算,比如col + 1 = 10,这样的表达式会导致无法使用索引,因为索引存储的是col的值,而不是col+1的结果。需要重写为col = 9才能有效利用索引。 还有使用不等于操作符(!=或<>)的时候,大部分情况下索引会失效,因为需要扫描大部分数据,这时候优化器可能选择全表扫描而不是索引。 对于IS NULL和IS NOT NULL的情况,如果列允许NULL,并且查询中使用了这些条件,是否能用索引取决于数据分布和MySQL的版本,需要具体分析,但通常IS NULL可能使用索引,而IS NOT NULL可能不会,尤其是当大部分数据不为NULL时。 还有全表扫描比索引快的情况,当数据量很小或者需要返回大部分行时,优化器可能跳过索引,直接全表扫描。这也是用户需要注意的,索引并不是万能的,有时候反而会降低性能。 最后,可能需要提到如何诊断索引失效,比如使用EXPLAIN命令查看执行计划,确认是否使用了预期的索引,以及索引的使用情况。 总结一下,需要将这些情况分门别类,用清晰的例子和解释,让用户能够理解每种情况的原因和解决方法。同时要注意结构清晰,便于用户查阅和记忆。</think>MySQL 中索引失效的常见情景通常与 **查询写法、索引设计、数据类型** 等因素相关。以下是典型场景及原因分析: --- ### **1. 违反最左前缀原则** - **组合索引未从最左列开始使用** 例如,索引为 `(a, b, c)`,但查询条件未包含 `a`,仅使用 `b` 或 `c`,则索引无法生效。 ```sql -- 失效:未使用 a SELECT * FROM table WHERE b = 1 AND c = 2; -- 生效:使用 a SELECT * FROM table WHERE a = 1 AND b = 2; ``` --- ### **2. 对索引列进行运算或函数操作** - **索引列参与计算或函数调用** 索引存储的是列的原始值,运算或函数会破坏其直接可检索性。 ```sql -- 失效:对列进行运算 SELECT * FROM table WHERE YEAR(create_time) = 2023; -- 生效:避免运算 SELECT * FROM table WHERE create_time BETWEEN '2023-01-01' AND '2023-31'; ``` --- ### **3. 使用 `OR` 条件且部分列无索引** - **`OR` 连接的条件中存在非索引列** 若 `OR` 两侧的列不全有索引,优化器可能放弃使用索引。 ```sql -- 失效:address 列无索引 SELECT * FROM table WHERE id = 1 OR address = 'Beijing'; -- 改进:拆分查询或为 address 添加索引 ``` -- ### **4. 模糊查询以通配符开头** - **`LIKE '%value'` 或 `LIKE '%value%'`** 前导通配符会导致索引无法匹配前缀。 ```sql -- 失效:以 % 开头 SELECT * FROM table WHERE name LIKE '%John'; -- 生效:以确定值开头 SELECT * FROM table WHERE name LIKE 'John%'; ``` --- ### **5. 隐式类型转换** - **查询条件与索引列类型不匹配** 例如,索引列为字符串类型,但查询时使用数字,导致隐式转换。 ```sql -- 失效:id 是 VARCHAR,但查询用数字 SELECT * FROM table WHERE id = 100; -- 生效:显式使用字符串 SELECT * FROM table WHERE id = '100'; ``` --- ### **6. 范围查询后的索引列失效** - **范围查询(`>`、`<`、`BETWEEN`)后的列无法使用索引** 索引中范围查询之后的列会失效。 ```sql -- 索引 (a, b, c) SELECT * FROM table WHERE a > 1 AND b = 2; -- b 无法使用索引 ``` --- ### **7. 使用 `NOT IN` 或 `!=`** - **非等值查询可能导致全表扫描** 当数据分布中不满足条件的比例较高时,优化器可能放弃索引。 ```sql -- 可能失效 SELECT * FROM table WHERE status != 'deleted'; ``` --- ### **8. 全表扫描更高效** - **数据量小或高比例查询** 当表数据量极小,或查询需要返回大部分数据时,优化器可能直接全表扫描。 --- ### **9. 索引列允许 `NULL` 且条件为 `IS NULL`/`IS NOT NULL`** - **`IS NULL` 可能使用索引,但 `IS NOT NULL` 可能失效** 取决于数据分布和索引统计信息。 --- ### **10. 统计信息不准确** - **索引统计信息过期** InnoDB 的索引统计信息可能未及时更新,导致优化器误判索引效率(可通过 `ANALYZE TABLE` 更新)。 --- ### **如何验证索引是否失效?** 使用 `EXPLAIN` 查看执行计划,关注 `key`(使用的索引)和 `type`(访问类型): - `type` 为 `ALL` 表示全表扫描。 - `type` 为 `ref`、`range` 等表示索引有效。 --- ### **避免索引失效的建议** 1. 合理设计索引(优先高频查询字段,控制索引数量)。 2. 避免对索引列进行运算或函数操作。 3. 注意查询条件的顺序和类型匹配。 4. 定期更新统计信息(`ANALYZE TABLE`)。 5. 使用覆盖索引(查询字段均在索引中)减少回表。 掌握这些规则可有效优化查询性能!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值