NULL 在 WHERE 子句中的特殊行为?为什么 col = NULL 不能用,必须用 IS NULL?
The special behavior of NULL in a WHERE clause? Why doesn't `col = NULL`
考察要点
这道题面试官想看你是否深刻理解 SQL 的三值逻辑(Three-Valued Logic, 3VL),以及 NULL 并非一个“值”而是一个“标记”的本质。这直接决定了你在 WHERE 子句、JOIN 条件和聚合函数中能否写出逻辑严谨、没有隐藏 bug 的 SQL。
核心概念
SQL 的逻辑判断和大多数编程语言的二值逻辑(True/False)不同,它引入了第三个状态:UNKNOWN。NULL 是导致 UNKNOWN 状态产生的根源。
-
NULL的本质NULL代表“未知”、“不适用”或“缺失的信息”,它不是一个具体的值。因此,NULL不等于任何东西,甚至不等于它自己。5 = NULL的结果是UNKNOWN'hello' <> NULL的结果是UNKNOWNNULL = NULL的结果也是UNKNOWN
-
WHERE子句的工作原理WHERE子句只返回使其条件判断结果为TRUE的行。结果为FALSE或UNKNOWN的行都会被过滤掉。 因为col = NULL的结果永远是UNKNOWN,所以WHERE col = NULL这条语句永远不会返回任何数据。 -
专用判断谓词:
IS NULL和IS NOT NULL为了处理对NULL的判断,SQL 提供了专门的谓词:col IS NULL:如果col是NULL,结果为TRUE;否则为FALSE。col IS NOT NULL:如果col不是NULL,结果为TRUE;否则为FALSE。
这是在
WHERE子句中正确筛选NULL记录的唯一标准方式。 -
数据示例 假设我们有一张
employees表:id name manager_id 1 Alice 10 2 Bob NULL 3 Carol 10 - 错误查询:
sql1SELECT * FROM employees WHERE manager_id = NULL;2-- 结果:返回 0 行。因为 manager_id = NULL 的结果是 UNKNOWN。
- 正确查询:
sql1SELECT * FROM employees WHERE manager_id IS NULL;2-- 结果:返回 id=2 的 Bob 这一行。
- 另一个陷阱:查询所有有经理的员工
sql1-- 错误方式2SELECT * FROM employees WHERE manager_id <> NULL;3-- 结果:返回 0 行。因为 manager_id <> NULL 的结果也是 UNKNOWN。45-- 正确方式6SELECT * FROM employees WHERE manager_id IS NOT NULL;7-- 结果:返回 id=1 和 id=3 的行。
- 错误查询:
-
MySQL 特有的 NULL-safe 等价运算符:
<=>MySQL 提供了一个特殊的运算符<=>(NULL-safe equal to operator),它在行为上类似=,但视NULL <=> NULL为TRUE,并且col <=> NULL在col不为NULL时为FALSE。sql1-- 使用 <=>2SELECT * FROM employees WHERE manager_id <=> NULL;3-- 结果:返回 id=2 的 Bob 这一行,等价于 IS NULL。这个操作符在标准 SQL 中对应
IS NOT DISTINCT FROM。虽然方便,但<=>并非所有数据库都支持,可移植性较差。
常见低分回答
- 直接使用
col = NULL或col != NULL:这是最基础也是最致命的错误,直接表明候选人对 SQL 三值逻辑缺乏基本认识。 - 混淆
NULL与空字符串''或数字0:回答“NULL就是空字符串”,这是概念性错误。NULL是未知,''是一个已知的、长度为零的字符串。col = ''或col = 0是合法的、结果为TRUE/FALSE的判断。 - 认为
NOT (col = 'some_value')可以匹配出NULL值:这是一个逻辑陷阱。- 假设
col为NULL,则col = 'some_value'结果是UNKNOWN。 NOT (UNKNOWN)的结果依然是UNKNOWN。- 因此
WHERE NOT (col = 'some_value')同样会把NULL记录过滤掉。 - 正确的写法是
WHERE col <> 'some_value' OR col IS NULL。
- 假设
- 在
COUNT中误用NULL:认为COUNT(col)和COUNT(*)结果总是一样。实际上COUNT(col)会忽略col为NULL的行,而COUNT(*)或COUNT(1)会统计所有行。 - 在
NOT IN子查询中忽略NULL:写出SELECT ... FROM t1 WHERE id NOT IN (SELECT id FROM t2),但没有意识到如果t2的id列中包含任何NULL值,整个查询将永远不返回任何结果。这是因为NOT IN内部展开为一系列AND col <> value的组合,只要其中一个value是NULL,col <> NULL的UNKNOWN结果就会毁掉整个逻辑链。
工程实践与优化
-
索引对
NULL的处理:- 在现代存储引擎(如
InnoDB)中,B+Tree索引会存储NULL值的条目。 - 因此,
WHERE col IS NULL是可以有效利用col上的索引的,执行计划中type可能是ref,key为对应索引,Extra中显示Using where。 WHERE col IS NOT NULL的查询,如果col列的NULL值比例很低(大部分值都非NULL),优化器可能会认为走索引扫描大部分数据的成本高于全表扫描,从而放弃使用索引。
- 在现代存储引擎(如
-
避免在
WHERE子句中对列使用函数:- 反例:
WHERE IFNULL(col, 'default') = 'some_value' - 这会导致索引失效,因为优化器无法直接利用
col上的索引。 - 正解:改写为
SARGable(Search Argument Able) 的形式:WHERE (col = 'some_value' OR col IS NULL)。这样可以分别利用索引进行两次range scan或ref查询,然后合并结果,性能远高于全表扫描。
- 反例:
-
数据库 Schema 设计:
- 在设计表结构时,应尽可能为列设置
NOT NULL约束,除非业务逻辑明确要求允许“未知”或“不适用”的状态。 - 这不仅能保证数据完整性,还能从根本上避免
NULL带来的逻辑复杂性和潜在的查询错误。
- 在设计表结构时,应尽可能为列设置
-
执行计划分析:当遇到与
NULL相关的性能问题时,务必使用EXPLAIN查看执行计划。关注type,key,rows,Extra等字段,确认是否如预期般使用了索引。对于 MySQL 8.0+,可以使用EXPLAIN ANALYZE获取更详细的执行信息。
常见追问与延伸题目
-
追问:
NULL在JOIN条件中是如何表现的?比如ON t1.id = t2.id?- 回答要点:
JOIN的ON子句行为与WHERE子句类似,遵循三值逻辑。如果t1.id或t2.id中有NULL,那么t1.id = t2.id的判断结果是UNKNOWN。因此,在INNER JOIN中,id为NULL的行是永远不会互相匹配上的。这也是一个常见的数据丢失原因。 - 对于
LEFT JOIN,如果左表的id无法在右表中找到匹配(包括右表id为NULL的情况),则右表的所有列在结果集中都会显示为NULL。
- 回答要点:
-
追问:
WHERE col NOT IN (1, 2, NULL)这个查询会返回什么?为什么?- 回答要点:这个查询永远不会返回任何行。
col NOT IN (1, 2, NULL)在逻辑上等价于col <> 1 AND col <> 2 AND col <> NULL。- 对于任何
col的值(包括NULL本身),col <> NULL这个表达式的结果永远是UNKNOWN。 - 在
AND逻辑链中,TRUE AND TRUE AND UNKNOWN的最终结果是UNKNOWN。 - 由于
WHERE子句只保留结果为TRUE的行,所以没有任何行会被返回。这是一个非常经典的NOT IN陷阱。
-
追问:
AVG()函数如何处理NULL值?这对计算结果有什么影响?- 回答要点:所有标准的聚合函数(
SUM,AVG,MAX,MIN,STDDEV等),除了COUNT(*)和GROUPING,在计算前都会忽略NULL值。 AVG(col)的计算公式是SUM(col) / COUNT(col),而不是SUM(col) / COUNT(*)。- 举例:对于数据集
{1, 2, 3, NULL},AVG()的结果是(1+2+3) / 3 = 2,而不是(1+2+3) / 4 = 1.5。这在统计平均值时非常重要,面试官想确认你是否清楚这个区别,因为它会直接影响业务报表的准确性。
- 回答要点:所有标准的聚合函数(