news 2026/10/2 3:23:36

SQL中NULL的“逻辑黑洞”:从NOT IN失效到三值逻辑的实战避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL中NULL的“逻辑黑洞”:从NOT IN失效到三值逻辑的实战避坑指南

NULL这个坑,我在数据库这行踩了快十年,每次碰到都还是会心里一紧。印象最深的一次是帮业务部门排查一个报表数据缺失的故障:两张表都有上万条记录,关联字段看着也正常,可结果集硬是凭空少了几万条。折腾了两个小时,最后发现罪魁祸首就是子查询里带了一个NULL值,把整个NOT IN的逻辑直接“吸”进了黑洞。今天这篇,我就从NOT IN失效这件事谈起,把SQL里NULL引发的那些逻辑黑洞一次讲透——它不讲道理、不报错、不提示,只会让你的查询结果“悄悄变少”或“直接为空”。内容覆盖三值逻辑原理、实战改法和排错技巧,不管你是刚入行的数据分析师,还是天天写SQL的后端开发,这篇都值得看完再收藏。

1. 先认识NULL:它不是一个“值”,而是一种“状态”

1.1 三值逻辑:为什么SQL里会有“UNKNOWN”这种鬼东西

刚接触SQL的时候,很多人以为NULL就是“没有值”或者“空值”,最多再加一句“和0或者空字符串不一样”。这个理解方向是对的,但远远不够。真正要搞懂NULL,必须先接受一个颠覆直觉的事实:SQL里的逻辑判断不是二值的,而是三值的——除了TRUE和FALSE,还有UNKNOWN。

为什么要搞出第三种状态?因为现实世界里的“不知道”和“不存在”是两码事。比如一张用户表里有个“手机号”字段,张三的手机号是空字符串,代表他注册时填了空;李四的手机号是NULL,代表这个信息压根没采集到。这两种情况在业务含义上完全不同,SQL为了能表达这种“缺失且未知”的语义,就引入了NULL。但是代价也随之而来:任何涉及到NULL的比较运算,结果都变成UNKNOWN,而不是TRUE或FALSE。

这里有一个关键点必须刻在脑子里:WHERE子句只保留判断结果为TRUE的行,FALSE和UNKNOWN都会被过滤掉。

这就有意思了。你可能会写一个查询想找到“不是程序员”的用户,写WHERE job <> '程序员'。结果发现,那些job字段是NULL的人,根本不会出现在结果里。为什么?因为NULL <> '程序员' 这个比较的结果是UNKNOWN,就是字面上的“我不知道他是不是程序员”。数据库很老实,它不知道,就不给你。

1.2 三值逻辑演算:从布尔代数到NULL复合表达式的真值表

三值逻辑不只是单条件判断的问题,更可怕的是它会通过逻辑运算层层传染。在普通布尔代数里,TRUE OR FALSE是TRUE,FALSE AND TRUE是FALSE。但是在SQL的三值逻辑里,UNKNOWN一旦参与运算,整个表达式的结果都可能被带偏。

我用一张简化版真值表来说明:

ABA AND BA OR B
TRUEUNKNOWNUNKNOWNTRUE
FALSEUNKNOWNFALSEUNKNOWN
UNIONKNOWNUNKNOWNUNKNOWNUNKNOWN

这张表透露了两个很吓人的信息。第一,UNKNOWN AND FALSE等于FALSE,这是三值逻辑里少数的“能救回来”的情况;第二,只要有一个UNKNOWN参与AND运算,而另一个条件不是FALSE,最终结果几乎都是UNKNOWN。放到实际查询里,就是你的WHERE条件明明写了好几个,某个字段一旦有NULL,整行数据就可能莫名其妙地从结果里消失。

更麻烦的是NOT运算。普通逻辑里,NOT FALSE等于TRUE,NOT TRUE等于FALSE。但在三值逻辑里,NOT UNKNOWN还是UNKNOWN。这就为后面要讲的NOT IN失效埋下了伏笔——你以为是取反操作,实际上数据库取反之后还是一团“不知道”。

2. 逻辑黑洞的五大常见现场:不只是NOT IN

2.1 场景一:NOT IN子查询里藏着NULL,直接全军覆没

现在正式回到NOT IN这个话题。先看一个非常典型的例子。假设有一个订单表orders和一个黑名单表blacklist,业务上想找出所有“不在黑名单里”的订单:

SELECT * FROM orders WHERE customer_id NOT IN (SELECT customer_id FROM blacklist);

这段SQL看起来人畜无害。如果blacklist里所有customer_id都有值,查询完全正常。可只要blacklist中哪怕有一条记录的customer_id是NULL,结果就变成了“空表”。

这是为什么?拆开来看。NOT IN的本质是“不等于子查询结果里的任何一个值”。当你拿到一组值,比如(101, 102, NULL),真正执行的判断是:

customer_id <> 101 AND customer_id <> 102 AND customer_id <> NULL

前面两个条件都正常,但最后一个customer_id <> NULL的返回值是UNKNOWN。再套用前面讲的真值表,一个TRUE AND TRUE AND UNKNOWN,结果还是UNKNOWN。除非customer_id本身也是NULL——那一行的判断结果同样是UNKNOWN。最终整个WHERE条件对所有行都不成立,查询结果就只能是空集。

我当年排查那个报表故障时,就是吃了这个亏。黑名单表里人工录入了一条没有客户ID的记录,整个报表数据直接清零,没有任何报错,业务方还以为系统宕机了。这就是NULL的可怕之处:它不给你任何提示,只是安静地把结果变成空。

2.2 场景二:字符串拼接遇到NULL,整个字段集体“失踪”

除了条件判断,NULL在做字符串拼接时同样像个黑洞。以SQL Server为例,常见写法是直接用加号:

SELECT first_name + ' ' + last_name AS full_name FROM users;

如果某条记录的last_name是NULL,那么整个full_name的值就是NULL。因为在SQL Server的默认行为里,NULL参与字符串连接,结果还是NULL。这和编程语言里“null + '字符串' = 'null字符串'”的处理方式完全不同。很多从Java、Python转过来的开发,第一次看到这种结果都会愣住。

MySQL则更特殊,它有函数CONCAT和运算符||。CONCAT里一旦有NULL,返回的结果也是NULL;但在某些SQL模式下,||被当作逻辑或,行为又不一样。跨数据库平台写代码,这一块最容易踩坑。

那怎么破?方案是使用COALESCE或ISNULL在拼接前把NULL转成空字符串:

SELECT COALESCE(first_name, '') + ' ' + COALESCE(last_name, '') AS full_name FROM users;

有人说,那我直接在建表时把所有字符串字段都设成NOT NULL DEFAULT '',彻底断绝NULL不就行了?这个思路对了一半。后面我会专门讲,NULL和空字符串在业务语义上是两回事,一刀切会引入新的数据质量问题。

2.3 场景三:聚合函数与NULL的爱恨情仇:COUNT、SUM、AVG的隐蔽行为

聚合函数里NULL的表现也是一堆暗坑。先说最经典的COUNT:

写法行为
COUNT(*)统计所有行数,包括NULL字段的行
COUNT(column)只统计该列非NULL的行数
COUNT(DISTINCT column)只统计非NULL的不同值个数

很多人在做报表时会写COUNT(remark)统计有备注的记录数,以为和COUNT(*)结果一样。一旦表里有多条记录remark为NULL,数字就悄悄变少。做日报数量的同学如果没注意这一点,非常容易报错数据。

SUM和AVG也有一处反直觉的地方。SUM(amount)如果这一列全是NULL,结果是NULL,不是0;AVG(amount)计算平均值时,分母是“非NULL的行数”,而不是所有行数。这会导致一个经典错误:某天没有产生任何销售额,AVG却算出某个诡异值,因为空的那天压根没参与计算。

稳妥的做法是聚合前先确认业务口径:如果SUM希望缺省算0,用COALESCE(SUM(amount), 0);如果AVG希望空值也作为0参与平均,先COALESCE列再聚合:AVG(COALESCE(amount, 0))。这里没有标准化答案,一切取决于你想要的业务含义。

2.4 场景四:CASE WHEN里的NULL判断,顺序错了全盘皆输

CASE WHEN是SQL里写逻辑最灵活的工具,但它对NULL的处理也有自己的规则。很多人习惯写CASE WHEN column = NULL THEN '空',这是最典型的错误写法,因为column = NULL返回UNKNOWN,根本不会进入THEN分支。正确写法是IS NULL:

CASE WHEN column IS NULL THEN '空' WHEN column = 'A' THEN 'A类' ELSE '其他' END

还有个更隐蔽的坑是CASE的“短路顺序”问题。SQL标准里的CASE会按顺序逐个判断WHEN,一旦某个分支成立,后续分支不再执行。但这个特性在某些数据库里表现得激进,某些数据库里又显得保守。如果你的CASE里既有NULL判断又有范围判断,建议把IS NULL分支放在最前面,避免被其他条件“抢先吞掉”。

我就处理过一次事故:一个员工绩效分类SQL,CASE里先写了WHEN score >= 90 THEN '优秀',后面才写WHEN score IS NULL THEN '无数据'。结果所有人的NULL成绩都被分到了“优秀”里——因为NULL >= 90同样返回UNKNOWN,按道理不该进这个分支,可当时的逻辑嵌套里问题要复杂得多。排查到最后,发现是外层还有一个COALESCE把NULL默认成了100。所以记住:排查NULL问题,一定要顺着SQL的执行链路看每一层的处理,光看那一次判断是不够的。

2.5 场景五:空字符串与NULL的边界混淆:一场业务层面的数据暗战

热搜词里有一条“kettle 局部修改空字符串不转换为null”,这正好戳中了数据处理的一个痛点。很多ETL工具(比如Kettle)在同步数据时,默认会把源端空字符串转成空值NULL,或者反过来,把NULL转成空字符串。这个看起来人畜无害的转换,会导致下游SQL行为和预期完全不符。

在业务上,“空字符串”和“NULL”经常代表完全不同的状态:空字符串可能是用户提交了空表单,NULL可能是系统压根没收到这个字段。如果你在SQL里用WHERE phone = ''过滤,你只排除了空字符串,NULL的手机号还在结果里;如果你用WHERE phone IS NULL,你只排除了NULL,空字符串的又漏了出来。

推荐的处理方式是在接数据时,就明确一个统一口径,并且在SQL里主动防御,比如对所有这种字段做标准化:

UPDATE users SET phone = NULL WHERE phone = '';

这种写法把空字符串统一清洗成NULL,后续查询只需要记一种判断方式。但要谨慎操作,先确认业务上“空字符串”和“NULL”是否真的可以视为同一含义,否则会造成不可逆的数据污染。

3. 从实战中找对策:NOT IN改NOT EXISTS,以及更多防坑写法

3.1 NOT IN的可靠替代方案:NOT EXISTS和LEFT JOIN + IS NULL

与其在NULL的雷区里小心翼翼,不如换一种写法,彻底避开UNKNOWN。处理“不在某个集合里”的需求,业界最经典的两种替代方案是NOT EXISTS和LEFT JOIN + WHERE IS NULL。

先看NOT EXISTS写法:

SELECT o.* FROM orders o WHERE NOT EXISTS ( SELECT 1 FROM blacklist b WHERE b.customer_id = o.customer_id );

这种写法为什么安全?因为EXISTS子查询只关心“有没有匹配的记录”,根本不关心子查询里选出的字段是不是NULL,也不关心关联字段之间是怎么比较的。只要关联条件不成立,EXISTS就是FALSE,NOT EXISTS就是TRUE,整个判断回到二值逻辑,不再被UNKNOWN牵着走。

再看LEFT JOIN写法:

SELECT o.* FROM orders o LEFT JOIN blacklist b ON b.customer_id = o.customer_id WHERE b.customer_id IS NULL;

这个方案的逻辑是:先把orders和blacklist做外连接,凡是blacklist里匹配不上的,连接后的b.customer_id都是NULL。最后通过IS NULL这个显式判断,精准选出“没有匹配上的订单”。这两个方案语义等价于NOT IN,但都绕开了与子查询结果集中的NULL值做比较的问题。

从性能角度看,如果子查询表有合适的索引,NOT EXISTS通常不会太差;LEFT JOIN则更容易让优化器走hash join。实际场景里,数据量大时就重点看执行计划,哪个快用哪个。

3.2 显式处理NULL的三大函数:COALESCE、ISNULL、NULLIF的适用边界

处理NULL当然不只是躲避,也可以主动出击。SQL标准提供了一组函数,先明确它们的区别:

函数适用数据库行为
COALESCE几乎所有数据库返回参数列表里第一个非NULL值,参数个数不限
ISNULLSQL Server只有两个参数,若第一个为NULL则返回第二个
IFNULLMySQL两个参数,类似ISNULL
NULLIF几乎所有数据库如果两个参数相等,返回NULL,否则返回第一个参数

我重点说两个容易被用错的函数。COALESCE非常强大,比如COALESCE(a, b, c, 0)会依次取a、b、c中第一个非NULL值,全为NULL就返回0。但很多人容易忽略参数类型的一致性。如果a是字符串,b是整数,某些数据库会直接报类型转换错误;另一些数据库则会悄悄做隐式转换,带来精度损失。所以用COALESCE前,尽量保证参数类型统一。

NULLIF则适合处理“除数为零”这类问题。假设要计算增长率,分母可能为0,可以写:

SELECT amount / NULLIF(denominator, 0) FROM sales;

NULLIF(denominator, 0)在分母为0时返回NULL,而任何数除以NULL得到NULL,从而避免了除以零的报错。对比一下,如果用CASE WHEN,代码会长一截,但也更直观。NULLIF的优点是简洁,缺点是不熟悉这种写法的人第一眼会看不懂,团队协作时要在注释里写清楚。

还有一种主动出击的思路是“反向使用NULLIF”,它在ETL清洗时特别好用。例如把字符串中的空字符串统一变成NULL:

SELECT NULLIF(column, '') AS column FROM source_table;

NULLIF的语义和场景二里的UPDATE写法等价,但它是查询层面的,不动表里的真实数据,更适合做临时分析和报表。

3.3 建表时防患于未然:NOT NULL约束与默认值的正确姿势

如果你还在和存量数据打持久战,那下面这套“源头治理”的思路要尽早用上。建表时给字段加上NOT NULL约束,是最直接的防御。但你不可能所有字段都不允许NULL,该允许NULL的字段,就要想清楚默认值。

一个常见误区是:把所有可空字段都设成DEFAULT ''或DEFAULT 0,以为就没有NULL问题了。前面已经强调过,空字符串和NULL的业务语义不同,强行统一会给下游埋雷。正确的姿势是分情况讨论:

  • 业务上“一定会有值”的字段(比如创建时间、主键),设NOT NULL。
  • 业务上“暂时不知道但将来会补齐”的字段(比如用户昵称、手机号),允许NULL,但要约定NULL表示“未填写”。
  • 业务上“可能确实没有”的字段(比如备注、删除时间),允许NULL,NULL表示“确实没有”。

做这个设计时,最好把每种空值的业务含义写进数据字典里,否则半年后你自己写SQL都会犯嘀咕。

如果要对已有表加约束,可以用类似下面的ALTER语句:

ALTER TABLE users ALTER COLUMN phone SET NOT NULL;

执行前先确认当前表里没有NULL值。有的话先用UPDATE补齐,否则加约束会直接失败。更稳妥的流程是:先查一遍NULL分布,再决定是补默认值还是清数据,最后才动表结构。

3.4 快速定位NULL问题的排查方法论

遇到“SQL结果为空”或“结果少了几行”的诡异情况,我习惯按下面这套顺序排查,效率极高。

第一步,检查子查询或关联字段里是否存在NULL。把SQL拆开,单独跑子查询的结果集,用WHERE column IS NULL查一下,看看是不是藏着看不见的NULL值。

第二步,去掉WHERE条件中所有涉及该字段的判断,看结果是否恢复。如果恢复,说明就是该判断里的UNKNOWN在作祟。

第三步,检查COALESCE、CASE WHEN等表达式里有没有可能把NULL“传染”到别的字段。记住NULL的传染性:一个NULL参与运算,结果往往是NULL。

第四步,使用EXPLAIN查看执行计划。有时候不是逻辑问题,而是索引失效或统计信息陈旧导致优化器选了错误的执行路径,这种物理层面的查询也要纳入排查范围。

这套方法我在多个数据库上都验证过——SQL Server、MySQL、PostgreSQL、Oracle,逻辑相通,只是函数语法略有差异。排查时养成“子在川上曰,NULL果然坑人”的习惯,心态会稳很多。

4. 复杂业务数据模型中的NULL:从单表查询到多表关联的连锁反应

4.1 多表外连接时NULL扩散的实景模拟

讲完了单表、单表达式的坑,再看一个更接近生产环境的场景。假设有三张表:用户表users、订单表orders、退款表refunds。业务要统计每个用户的订单数和退款数,常规写法是用两个LEFT JOIN:

SELECT u.user_id, COUNT(o.order_id) AS order_cnt, COUNT(r.refund_id) AS refund_cnt FROM users u LEFT JOIN orders o ON o.user_id = u.user_id LEFT JOIN refunds r ON r.order_id = o.order_id GROUP BY u.user_id;

这段SQL有两个问题。第一,如果一个用户有多个订单,每个订单又有自己的退款记录,那么LEFT JOIN会让订单数和退款数交叉相乘,COUNT出来的是一个“笛卡尔爆炸”的数字。这个虽然严格来说不是NULL的问题,但一旦与NULL混合在一起,排查难度会成倍上升。

第二,LEFT JOIN时,没有订单的用户,o.order_id是NULL;没有退款的订单,r.refund_id也是NULL。COUNT(列)会自动忽略NULL,所以看起来结果“似乎正确”,但如果你中间加了任何计算,比如SUM(o.amount) - SUM(r.amount),NULL会直接把某个用户的数据算成NULL。

更复杂的情况是,当orders表中o.amount本身就有NULL时,SUM(amount)会把那部分直接跳过,而你以为是“订单金额缺失”,实际是“整列NULL”。这种数据模型下,报表的每一个数字都可能藏着若干个NULL黑洞。

实战里我更推荐分步聚合:先按用户统计订单数,再按用户统计退款数,最后再JOIN一次,而不堆叠多个LEFT JOIN。虽然多写了几行SQL,但每一步的中间结果都清晰可控,出了问题也容易定位。

4.2 慢SQL优化与NULL的交互效应:索引失效的隐形帮凶

热搜词里有“慢sql优化”“并行sql优化”,正好和NULL问题交汇在一起。一个隐藏很深的坑是:在可空列上建了索引,但执行计划并不一定走索引。为什么?因为NULL值通常在索引里也有特殊存储方式,查询WHERE column IS NULL时,有些数据库可以走索引,有些数据库则只能扫全表。更严重的是,如果你在WHERE里写了WHERE column <> '某个值',该列的NULL行无法匹配,优化器发现要过滤大量行,也可能放弃索引。

另一个相关场景是排序。ORDER BY column遇到NULL时,不同数据库的默认排序位置不同:比如SQL Server里NULL默认排最前,Oracle里NULL默认排最后,PostgreSQL里NULL默认排最后。如果你没意识到这一点,看排序结果时可能误以为数据顺序有问题。

这类问题的本质是:NULL的存在改变了数据的分布形态,而查询优化器对分布形态很敏感。优化手段通常是下面几招:

  • 尽量减少可空列上的“不等于”类筛选,改写为IS NULL或IS NOT NULL的显式表达。
  • 如果业务允许,把可空列拆成一个单独的表,主表字段设NOT NULL。
  • 对经常需要过滤NULL的查询,使用带IS NULL条件的索引策略(不同数据库能力不同,此处不展开)。

4.3 连接服务器与ORM框架场景中的NULL传递问题

搜热词里有一长串报错,比如“链接服务器 "(null)" 的 OLE DB 访问接口”以及各种编程语言报“xxx is null”的异常。这提醒我们,NULL的坑不只存在于纯SQL里,在连接服务器、ORM框架、API接口层同样存在。

举一个分布式系统里很典型的场景:主库通过链接服务器访问外部数据库,外部返回的结果集中,某些字段是NULL。主库这边接到NULL后,再作为参数传给存储过程,存储过程里如果直接用这个参数做条件判断,NULL就会一路传染到底。最麻烦的是,链接服务器执行远程查询时,可能会把本地NULL“映射”成某种特殊值,导致你调试时看到的NULL和远程实际的NULL根本不是同一个。

ORM框架(比如Entity Framework、Hibernate、MyBatis)也有自己的NULL处理策略。以MyBatis为例,如果你传入的参数是NULL,动态SQL里判断<if test="name != null">会跳过该条件,拼接出来的SQL可能就和预期不符。这时候你就要格外注意动态标签里的NULL判断顺序,尽量在Mapper层就把NULL语义处理清楚。

跨系统传参时,我个人的铁律是:在系统边界对NULL做显式封装。从接口拿到的值,先做空值标准化,统一转换为业务层自定义的默认值,或者干脆报错拒绝。这样做也许会让代码多几行,但能大幅度减少下游的隐性故障。

5. 常见问题与排错实录:从SQL写错到执行计划异常

5.1 案例一:查询总是莫名少几行,排查半小时发现是NOT IN里藏了NULL

真实场景复盘:某天用户运营找到我,说“这批用户里有多少人没有下过单”的报表,数据比前一天突然少了近一半。我查了任务日志,发现SQL没有报错,数据也更新了,唯独结果集不对。

我做的第一件事,就是把子查询单独跑出来,看了下customer_id字段有没有NULL。果然,前一天运营手动导入了一份客户名单,其中有一个单元格是空的,ETL程序把空单元格转成了NULL。就这么一个NULL,让整个NOT IN子查询瞬间失效。

修复方案是我前面写过的NOT EXISTS改写。改完以后,数据立刻恢复了正常。这次事故之后,我在团队里立了一个规矩:凡是写NOT IN,必须检查子查询返回列是否可能为NULL;如果不确定,一律改成NOT EXISTS。成本几乎为零,收益却非常大。

5.2 案例二:字符串拼接结果全为空,源头是数据库默认行为

当时有个前端页面展示“用户全名”,需要从数据库读first_name和last_name拼起来。测试环境一切正常,一到生产环境,很多行的全名就变成空白。前端同事跑来问我:“是不是数据没同步过来?”

实际上数据都在,问题出在SQL Server的拼接默认行为上。生产环境里部分老用户的last_name是NULL,NULL + 字符串等于NULL,再赋值给前端,页面就显示空白。修复方案就是COALESCE提前处理:

SELECT COALESCE(first_name, '') + ' ' + COALESCE(last_name, '') AS full_name FROM users;

这个案例说明,环境不同、数据质量不同,同样一段SQL的表现可以天差地别。最好在写SQL的初期就把NULL处理当成默认操作,不要等出了问题再补。

5.3 案例三:存储过程入参为NULL,导致整批数据处理错误

存储过程里有个典型写法:

CREATE PROCEDURE p_update_order @order_id INT AS BEGIN UPDATE orders SET status = '已完成' WHERE order_id = @order_id; END;

如果调用方不小心传入了NULL,这段SQL的WHERE就变成order_id = NULL,结果自然是什么也不更新。但问题是调用方并不知情,以为更新成功了,继续推进后续业务流程,最终导致整个审批流程“卡在空气中”。这不是逻辑写错,而是参数传错,但NULL不报错、不提示,让这个错误彻底隐形。

处理办法有两种:一种是在存储过程开头加参数校验,如果@order_id IS NULL,直接输出错误信息并RETURN;另一种是在UPDATE语句里显式处理:WHERE order_id = ISNULL(@order_id, order_id)——但这样会让“传NULL更新全部”成为隐式逻辑,很危险,我更推荐第一种。

5.4 NULL问题排查速查表

症状可能原因快速定位方法修复方案
查询结果为空NOT IN子查询有NULL单独跑子查询查NULL改NOT EXISTS或LEFT JOIN
查询结果少行WHERE比较返回UNKNOWN给该字段加上IS NULL条件对比显式处理NULL或改写逻辑
拼接字段为空白NULL参与字符串拼接查看原始字段是否有NULLCOALESCE转空串
聚合数字异常COUNT/SUM/AVG忽略或产生NULL使用COUNT(*)对比COUNT(列)使用COALESCE包裹聚合结果
ORDER BY顺序诡异数据库对NULL排序不同查看执行计划或排序结果用CASE WHEN显式指定NULL排序位置
接口报空指针数据边界未处理NULL检查接口日志入参系统边界做空值标准化
条件判断不生效参数传入NULL打印参数值存储过程/程序里显式校验

这个速查表是实际操作中积累下来的,遇到NULL问题可以直接对着排查,至少能省下大半的定位时间。

5.5 几个值得养成的SQL防NULL习惯

我个人在团队培训里经常讲,写SQL防NULL不能靠某一个技巧,而是要靠一组习惯。第一个习惯是,任何WHERE条件里出现“不等于”,都要反问一句:这里的NULL怎么办?不等于运算符(<>、!=)对NULL天然不友好,很多时候需要额外加OR xxx IS NULL。

第二个习惯是,不要用“任何值 = NULL”来判断空值。这个错误新老手都会犯,但每次犯都让人很尴尬。判断NULL只有IS NULL和IS NOT NULL,没有别的手段。

第三个习惯是,在使用聚合函数前,先想清楚业务上对缺失值的口径。COUNT(*)和COUNT(列)结果不同不是Bug,而是语义不同;如果你的是业务报表,要专门确认缺失值参与不参与计算。

第四个习惯是,写SQL时把中间结果物化,分步验证。先看子查询,再看外层,最后看表达式转换。许多人习惯一口气写完再跑,遇到NULL问题时,错误藏在哪一层根本不知道。

这些习惯看着简单,真正落实到日常开发里,带来的回报是减少大量隐性Bug和深夜加班。NULL问题从来不是“高端技术”,而是一种基础严谨性的体现。

就我个人经验来说,十次NULL引发的故障,有八次在写SQL的那一刻如果能多想一秒钟,就能完全避免。做数据库这行,写正确SQL靠的不只是对语法的熟悉,更是对数据状态的理解。NULL代表缺失、未知、未定义,它在现实世界里到处都是,所以你根本躲不开。与其害怕它,不如把上面几套改写法练成肌肉记忆。下一次再看到查询结果“莫名其妙”为空,先别慌,按顺序查一遍子查询里的NULL,大概率三分钟就能破案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 3:23:19

Flutter鸿蒙化适配:ANSI日志染色与终端输出策略解析

做 Flutter 鸿蒙化适配这一年多&#xff0c;我经手过不少三方库的移植&#xff0c;yaansi 是其中印象很深的一个。它不是那种几十万行的大库&#xff0c;核心逻辑可能连一千行都不到&#xff0c;但它恰好踩中了鸿蒙适配里最难解释的一类问题&#xff1a;纯 Dart 逻辑库&#xf…

作者头像 李华
网站建设 2026/10/2 3:22:59

个人量化交易系统落地指南:从数据回测到风控闭环

简介&#xff1a;一套基于Python的个人量化交易系统源码&#xff0c;面向个人投资者和量化爱好者&#xff0c;覆盖从行情数据采集、因子计算、策略生成到回测、模拟交易与风险监控的完整流程。压缩包大小约457KB&#xff0c;共91个文件&#xff0c;其中包括79个Python源文件、C…

作者头像 李华
网站建设 2026/10/2 3:22:56

YOLOV5电动车头盔检测数据集:从标注训练到部署的实战指南

简介&#xff1a;面向目标检测与电动车安全治理场景的YOLOv5数据集&#xff0c;聚焦道路上电动车骑行者头盔佩戴识别&#xff0c;共3个类别&#xff1a;戴头盔、未戴头盔、整体标注&#xff0c;适合目标检测入门练习及校园、园区等场景的安全监测项目。数据集按训练/验证划分&a…

作者头像 李华
网站建设 2026/10/2 3:22:51

Android记账本毕设全攻略:从SQLite到RecyclerView实战

如果你正在准备Android方向的毕业设计&#xff0c;想在几个月内拿下一个既写得出深度、又能经得住答辩追问、还可以直接拿到源码参考完整方案的题目&#xff0c;记账本这个方向我建议你认真考虑。我自己当年就是靠一个记账本App拿下的优秀毕设&#xff0c;后来工作里也带过不少…

作者头像 李华
网站建设 2026/10/2 3:22:42

月加班20小时却被嫌少?揭秘大厂工时与绩效的底层逻辑

“月加班20小时还被嫌少”&#xff0c;这句话放在多数行业里&#xff0c;怎么听都像段子。但发帖的是大厂员工&#xff0c;语气里满是“被警告”的无奈&#xff0c;评论区跟着一排“感同身受”。当你发现自己拼了命凑出来的加班时长&#xff0c;在上级眼里只是一笔不合格的账时…

作者头像 李华
网站建设 2026/10/2 3:22:19

TrustZone开发环境搭建实战:OP-TEE与BoostKit配置指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华