当前位置: 首页 > news >正文

NULL不是空——数据库里最反直觉的设计,90%新人踩过的坑

关键词: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(不是TRUENULL!=NULL→ UNKNOWN(不是TRUE

NULL = NULL的结果是UNKNOWN,而WHERE只返回TRUE的行,所以查出来是0行。必须用IS NULLIS 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!=NULL

id != 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 NULLIS 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 BYNULL值被分到同一组

避免NULL的设计思路

如果业务上某个字段"必须有值",在建表时就加上NOT NULL约束,并设置默认值:

CREATETABLEusers(idINTPRIMARYKEY,nameVARCHAR(50)NOTNULL,statusTINYINTNOTNULLDEFAULT0,phoneVARCHAR(20)-- 允许NULL,因为确实可能没登记);

NOT NULL的字段就别留NULL。NULL存在的每一处,都是未来查询时可能踩的坑。


总结

NULL不是空值,是"未知"。记住三句话:

  1. 判断NULL用IS NULL,别用=
  2. NULL参与运算结果是NULL,用COALESCE处理
  3. NOT IN遇到NULL会吞掉所有结果,改用NOT EXISTS

建表时能NOT NULL就别留NULL,少一个NULL,少十个bug。

小耶在手,SQL不愁。

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~


参考文献

  1. SQL-92标准 - 三值逻辑规范
  2. 《SQL反模式》第12章:NULL的处理陷阱
  3. MySQL 8.0 Reference Manual - Working with NULL:https://dev.mysql.com/doc/refman/8.0/en/working-with-null.html
  4. PostgreSQL Documentation - Null Values:https://www.postgresql.org/docs/current/functions-comparison.html
http://www.jsqmd.com/news/1118040/

相关文章:

  • WVP-GB28181-Pro:企业级视频监控平台的现代化互联互通解决方案
  • STM32F767ZI与IS31FL3731 LED驱动芯片的完美结合
  • LiteLLM代理配置优化:解决DeepSeek API Token异常消耗问题
  • STM32F417ZG与MC6470 IMU的高精度运动控制系统设计
  • 你的数字记忆管家:用WeChatMsg将微信对话变为永恒珍藏
  • Blazor WebAssembly性能优化实战与技巧
  • 如何在Windows电脑上直接安装Android应用:APK Installer终极指南
  • 工业4-20mA电流环设计与PIC微控制器应用
  • Windows 11系统优化神器:3分钟让你的电脑更快更私密
  • WzComparerR2:深入解析冒险岛WZ文件资源的专业提取器
  • Windows平台PDF处理新选择:Poppler预编译包完全指南
  • Python Tkinter实现SM4国密文件加解密桌面工具开发指南
  • 2021年人工智能十大工程级突破:可复现、可部署、已验证
  • Windows 11终极优化指南:用开源工具Win11Debloat让你的电脑更快更安全
  • 终极SSDTTime硬件优化指南:跨平台系统调校完整教程
  • DeepChem分子指纹:3种核心方法对比与实战选择指南
  • Manus AI深度评测:本地优先的AI编程助手实战账本
  • WeChatPad:解锁微信多设备同时登录的实用方案
  • 德州扑克GTO求解器Desktop Postflop:免费开源的高性能策略分析工具
  • 物联网网关(IoT Gateway)
  • Java毕业设计-基于前后端分离的医疗设备资产管理系统的设计与实现 医院器械领用归还与库存管理系统(源码+LW+部署文档+全bao+远程调试+代码讲解等)
  • STM32F429ZI与13DOF传感器融合的嵌入式导航方案
  • 最受欢迎的5种数据科学工具
  • 浅谈QString的性能话题:隐式转换、零拷贝与 Qt6 SSO
  • 基于TB9051FTG与PIC32的静音电机控制方案
  • 明日方舟桌宠Ark-Pets终极指南:3分钟让你的游戏角色“活“在桌面上
  • Nginx IP访问控制实战:从白名单黑名单到动态封禁
  • RevTorch:PyTorch可逆神经网络内存优化实战
  • 3分钟掌握llama-cpp-python:解锁本地大模型开发的终极Python集成方案
  • WinDiskWriter终极指南:5分钟在Mac上制作Windows启动U盘完整教程