关键词:NULL、数据库空值、三值逻辑、NULL陷阱、SQL空值处理
大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!
数据库里有一个设计,让无数新手怀疑人生——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 查不出任何数据
这是新手必踩的坑。
-- 你以为这样能查出所有phone为空的记录SELECT*FROMusersWHEREphone=NULL;-- 结果:0行-- 正确写法SELECT*FROMusersWHEREphoneISNULL;为什么?因为NULL不等于任何东西,包括它自己。在数据库中:
NULL=NULL→ UNKNOWN(不是TRUE)NULL!=NULL→ UNKNOWN(不是TRUE)NULL = NULL的结果是UNKNOWN,而WHERE只返回TRUE的行,所以查出来是0行。必须用IS NULL或IS NOT NULL来判断。
陷阱二:COUNT(column) 忽略NULL值
-- 假设users表有100行,其中10行phone为NULLSELECTCOUNT(*)FROMusers;-- 100(统计所有行)SELECTCOUNT(phone)FROMusers;-- 90(忽略NULL值)COUNT()统计行数,不管字段是什么。COUNT(column)只统计该字段非NULL的行数。如果你想知道"有多少人有手机号",用COUNT(phone);如果你想知道"总共有多少人",用COUNT()。
陷阱三:NULL参与运算,结果还是NULL
SELECT1+NULL;-- NULLSELECT'hello'||NULL;-- NULLSELECTNULL=0;-- UNKNOWN(不是FALSE)SELECTNULL='';-- UNKNOWN(不是FALSE)SELECTNULLANDTRUE;-- UNKNOWN(不是FALSE)任何值和NULL运算,结果都是NULL。这在实际业务中会造成很多bug:
-- 计算员工总薪资SELECTsalary+bonusFROMemployees;-- 如果某个员工bonus是NULL,整条记录的总薪资就是NULL正确做法是用COALESCE把NULL替换为默认值:
SELECTsalary+COALESCE(bonus,0)FROMemployees;-- NULL变成0,计算正常陷阱四:NOT IN 遇到NULL,整个查询结果为空
这是最隐蔽的一个。
-- 假设子查询返回了 (1, 2, NULL)SELECT*FROMusersWHEREidNOTIN(SELECTuser_idFROMorders);-- 如果orders表里有user_id为NULL的记录,整个查询返回0行为什么?因为NOT IN在底层展开成:
WHEREid!=1ANDid!=2ANDid!=NULLid != NULL的结果是UNKNOWN,整个AND表达式变成UNKNOWN,WHERE不返回任何行。
解决办法:用NOT EXISTS替代NOT IN,或者在子查询中排除NULL:
-- 方案一:NOT EXISTSSELECT*FROMusers uWHERENOTEXISTS(SELECT1FROMorders oWHEREo.user_id=u.id);-- 方案二:子查询排除NULLSELECT*FROMusersWHEREidNOTIN(SELECTuser_idFROMordersWHEREuser_idISNOTNULL);陷阱五:排序时NULL的位置
不同数据库对NULL的排序处理不同:
SELECT*FROMusersORDERBYphoneASC;-- MySQL:NULL排在最前面-- Oracle/PostgreSQL:NULL排在最后面如果你不确定NULL的排序行为,最好显式指定:
-- PostgreSQLSELECT*FROMusersORDERBYphoneASCNULLSFIRST;SELECT*FROMusersORDERBYphoneDESCNULLSLAST;-- MySQL(用IF/CASE处理)SELECT*FROMusersORDERBYIF(phoneISNULL,1,0),phoneASC;NULL的正确打开方式
判断NULL:用IS NULL或IS NOT NULL,别用=或!=。
处理NULL参与计算:用COALESCE提供默认值。
SELECTCOALESCE(phone,'未登记')FROMusers;SELECTsalary+COALESCE(bonus,0)FROMemployees;聚合函数对NULL的态度:
| 函数 | 对NULL的处理 |
|---|---|
| COUNT(*) | 统计所有行,不忽略NULL |
| COUNT(column) | 忽略该列的NULL值 |
| SUM(column) | 忽略NULL值 |
| AVG(column) | 忽略NULL值,且分母也不计NULL行 |
| MAX/MIN | 忽略NULL值 |
| GROUP BY | NULL值被分到同一组 |
避免NULL的设计思路:
如果业务上某个字段"必须有值",在建表时就加上NOT NULL约束,并设置默认值:
CREATETABLEusers(idINTPRIMARYKEY,nameVARCHAR(50)NOTNULL,statusTINYINTNOTNULLDEFAULT0,phoneVARCHAR(20)-- 允许NULL,因为确实可能没登记);能NOT NULL的字段就别留NULL。NULL存在的每一处,都是未来查询时可能踩的坑。
总结
NULL不是空值,是"未知"。记住三句话:
- 判断NULL用IS NULL,别用=
- NULL参与运算结果是NULL,用COALESCE处理
- NOT IN遇到NULL会吞掉所有结果,改用NOT EXISTS
建表时能NOT NULL就别留NULL,少一个NULL,少十个bug。
小耶在手,SQL不愁。
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~
参考文献:
- SQL-92标准 - 三值逻辑规范
- 《SQL反模式》第12章:NULL的处理陷阱
- MySQL 8.0 Reference Manual - Working with NULL:https://dev.mysql.com/doc/refman/8.0/en/working-with-null.html
- PostgreSQL Documentation - Null Values:https://www.postgresql.org/docs/current/functions-comparison.html