大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!
数据库里有一个设计,让无数新手怀疑人生------NULL。
你以为NULL代表"空"、"什么都没有"、"等于空白"。但数据库告诉你:WHERE column = NULL查不出任何数据。你以为COUNT(*)能统计所有行,但COUNT(column)却漏掉了一些。你写了一个if (value == null)的判断,结果数据死活对不上。
这一切的根源在于------NULL在数据库里,根本不是空值,而是一个特殊标记,表示"未知"或"不适用"。
今天把NULL这件事讲清楚,帮你避开那些反直觉的坑。
几个先搞明白的概念
NULL的本质:NULL不是空字符串'',不是0,不是false。它是一个独立的特殊值,表示"这个字段当前没有确定的值"。你可以把它理解成一张问卷上的"未作答"------它不是"否",也不是"空白",而是"我不知道"。
三值逻辑:大多数编程语言只有TRUE和FALSE两种结果。但数据库引入了NULL之后,逻辑运算变成了三值:TRUE、FALSE、UNKNOWN。当运算中涉及NULL时,结果可能就是UNKNOWN------而WHERE条件只返回TRUE的行,UNKNOWN被当作FALSE处理,这就是为什么很多查询"查不出数据"的根本原因。
空字符串 vs NULL:空字符串''是一个确定的值------它就是一个长度为0的字符串。NULL表示"没有值"。''和NULL在数据库中是完全不同的两个概念。
NULL的五大反直觉陷阱
陷阱一:WHERE column = NULL 查不出任何数据
这是新手必踩的坑。
sql
-- 你以为这样能查出所有phone为空的记录
SELECT * FROM users WHERE phone = NULL;
-- 结果:0行
-- 正确写法
SELECT * FROM users WHERE phone IS NULL;
为什么?因为NULL不等于任何东西,包括它自己。在数据库中:
sql
NULL = NULL → UNKNOWN(不是TRUE)
NULL != NULL → UNKNOWN(不是TRUE)
NULL = NULL的结果是UNKNOWN,而WHERE只返回TRUE的行,所以查出来是0行。必须用IS NULL或IS NOT NULL来判断。
陷阱二:COUNT(column) 忽略NULL值
sql
-- 假设users表有100行,其中10行phone为NULL
SELECT COUNT(*) FROM users; -- 100(统计所有行)
SELECT COUNT(phone) FROM users; -- 90(忽略NULL值)
COUNT()统计行数,不管字段是什么。COUNT(column)只统计该字段非NULL的行数。如果你想知道"有多少人有手机号",用COUNT(phone);如果你想知道"总共有多少人",用COUNT()。
陷阱三:NULL参与运算,结果还是NULL
sql
SELECT 1 + NULL; -- NULL
SELECT 'hello' || NULL; -- NULL
SELECT NULL = 0; -- UNKNOWN(不是FALSE)
SELECT NULL = ''; -- UNKNOWN(不是FALSE)
SELECT NULL AND TRUE; -- UNKNOWN(不是FALSE)
任何值和NULL运算,结果都是NULL。这在实际业务中会造成很多bug:
sql
-- 计算员工总薪资
SELECT salary + bonus FROM employees;
-- 如果某个员工bonus是NULL,整条记录的总薪资就是NULL
正确做法是用COALESCE把NULL替换为默认值:
sql
SELECT salary + COALESCE(bonus, 0) FROM employees;
-- NULL变成0,计算正常
陷阱四:NOT IN 遇到NULL,整个查询结果为空
这是最隐蔽的一个。
sql
-- 假设子查询返回了 (1, 2, NULL)
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM orders);
-- 如果orders表里有user_id为NULL的记录,整个查询返回0行
为什么?因为NOT IN在底层展开成:
sql
WHERE id != 1 AND id != 2 AND id != NULL
id != NULL的结果是UNKNOWN,整个AND表达式变成UNKNOWN,WHERE不返回任何行。
解决办法:用NOT EXISTS替代NOT IN,或者在子查询中排除NULL:
sql
-- 方案一:NOT EXISTS
SELECT * FROM users u WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);
-- 方案二:子查询排除NULL
SELECT * FROM users WHERE id NOT IN (
SELECT user_id FROM orders WHERE user_id IS NOT NULL
);
陷阱五:排序时NULL的位置
不同数据库对NULL的排序处理不同:
sql
SELECT * FROM users ORDER BY phone ASC;
-- MySQL:NULL排在最前面
-- Oracle/PostgreSQL:NULL排在最后面
如果你不确定NULL的排序行为,最好显式指定:
sql
-- PostgreSQL
SELECT * FROM users ORDER BY phone ASC NULLS FIRST;
SELECT * FROM users ORDER BY phone DESC NULLS LAST;
-- MySQL(用IF/CASE处理)
SELECT * FROM users ORDER BY IF(phone IS NULL, 1, 0), phone ASC;
NULL的正确打开方式
判断NULL :用IS NULL或IS NOT NULL,别用=或!=。
处理NULL参与计算 :用COALESCE提供默认值。
sql
SELECT COALESCE(phone, '未登记') FROM users;
SELECT salary + COALESCE(bonus, 0) FROM employees;
聚合函数对NULL的态度:
| 函数 | 对NULL的处理 |
|---|---|
| COUNT(*) | 统计所有行,不忽略NULL |
| COUNT(column) | 忽略该列的NULL值 |
| SUM(column) | 忽略NULL值 |
| AVG(column) | 忽略NULL值,且分母也不计NULL行 |
| MAX/MIN | 忽略NULL值 |
| GROUP BY | NULL值被分到同一组 |
避免NULL的设计思路:
如果业务上某个字段"必须有值",在建表时就加上NOT NULL约束,并设置默认值:
sql
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
phone VARCHAR(20) -- 允许NULL,因为确实可能没登记
);
能NOT NULL的字段就别留NULL。NULL存在的每一处,都是未来查询时可能踩的坑。
总结
NULL不是空值,是"未知"。记住三句话:
- 判断NULL用IS NULL,别用=
- NULL参与运算结果是NULL,用COALESCE处理
- NOT IN遇到NULL会吞掉所有结果,改用NOT EXISTS
建表时能NOT NULL就别留NULL,少一个NULL,少十个bug。
小耶在手,SQL不愁。
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽......我们下次见~