news 2026/10/23 2:50:05

订单里多了一行NULL,未下单客户为什么全查不出来?从NOT IN到NOT EXISTS

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
订单里多了一行NULL,未下单客户为什么全查不出来?从NOT IN到NOT EXISTS

我以前整理SQL学习清单时,把“没有下过订单的客户”记成了一个很直观的办法:客户ID不在订单的客户ID集合里,就选出来。

这个入口没有问题,但它省略了一个条件:订单表里的客户ID能不能为NULL?

下面只用三名客户。客户1已经下单,客户2、3没有下单。再放入一条没有关联客户的导入记录,你会发现:原本应该返回2、3的查询,突然一行也不返回。

我们不把这解释成“SQL坏了”,而是跟着每个比较看清它到底回答了什么。本文是从旧学习清单补出的独立实验,不是线上事故,也不声称复现了某道平台题的全部约束。

1. 三名客户,两条订单

用MySQL测试库里的同一个连接执行下面各段。两张表都是临时表,只在当前连接中存在,不创建或删除业务表。若当前连接已有同名临时表,换一组名字,不要直接删除未知表。

CREATE TEMPORARY TABLE blog_null_customers ( id INT PRIMARY KEY ); CREATE TEMPORARY TABLE blog_null_orders ( id INT PRIMARY KEY, customer_id INT NULL, memo VARCHAR(32) NULL, INDEX (customer_id) );
INSERT INTO blog_null_customers VALUES (1), (2), (3); INSERT INTO blog_null_orders VALUES (10, 1, NULL), (11, NULL, 'imported');
客户ID是否存在客户ID等于它的订单应不应该输出
1是,订单10否
2否是
3否是

订单11的customer_id未知,不意味着它同时属于所有客户。在本实验的业务定义里,只把ID相等的订单视作该客户的订单。

2. 看起来正确的NOT IN,为什么输出为空?

SELECT c.id FROM blog_null_customers AS c WHERE c.id NOT IN ( SELECT o.customer_id FROM blog_null_orders AS o ) ORDER BY c.id;

子查询从订单10取出1,从订单11取出NULL,得到1、NULL。客户2要“不在里面”,就要同时满足:与第一项不同,也与第二项不同。先把这两个比较分开:

2 <> 1 AND 2 <> NULL TRUE AND UNKNOWN -> UNKNOWN

第一项可以确认:2确实不是1。但第二项没有一个已知客户ID可比。问“这个未知ID是不是2”,比较所需的信息不够,普通不等号不能给出TRUE;SQL把这个结果记为UNKNOWN,MySQL用NULL表示它。NULL不是另一个普通编号,也不是与所有编号都不相等的特殊客户。

再看AND:它要求两项都为TRUE。一项确认不同,另一项还未知,不能说“两项都已确认不同”,所以整体仍是UNKNOWN。WHERE只收下条件确定为TRUE的行,客户2就没有被留下。

客户1又不一样:第一项1 <> 1已经是FALSE,不管另一项怎样,都不可能“两项都为TRUE”。因此整体是FALSE,不是UNKNOWN。把三个客户放在一起核对:

客户与1不相等与NULL不相等合起来WHERE保留
1FALSE未知FALSE否
2TRUE未知未知否
3TRUE未知未知否

这张表也解释了一个容易说过头的结论:不是“遇到NULL,NOT IN的结果全部变成NULL”。客户1已经确定匹配,结果仍是FALSE。在本例的过滤查询中,所有行都不满足TRUE,才得到空结果。MySQL比较运算文档

图只跟客户2这一行走,不提前拿另一种写法的结果代替解释。这里不是没执行查询,而是执行后没有任何行满足TRUE。

3. 第一种修正:先说明这个集合不包含NULL

SELECT c.id FROM blog_null_customers AS c WHERE c.id NOT IN ( SELECT o.customer_id FROM blog_null_orders AS o WHERE o.customer_id IS NOT NULL ) ORDER BY c.id;

现在参与比较的集合只有1,输出2、3。

这里有两个前提:客户id是主键,因此不为空;业务允许忽略客户ID未知的订单。不是随手加一个IS NOT NULL,就能把所有查询都修好。如果未知订单本身就是数据质量问题,应另外统计或处理,不能靠查询把异常藏起来。

实际题目若明确保证订单客户ID非空,原NOT IN写法也可以正确;先读约束,不需要为了展示技巧强行改写。

4. 第二种写法:直接问有没有匹配订单

SELECT c.id FROM blog_null_customers AS c WHERE NOT EXISTS ( SELECT 1 FROM blog_null_orders AS o WHERE o.customer_id = c.id ) ORDER BY c.id;

对每个客户,把相等的订单筛出来:有匹配行就不输出,没有匹配行才输出。比如检查客户2时,订单11产生的比较是NULL = 2,结果仍是UNKNOWN,不是FALSE。子查询的WHERE也只留下TRUE,因此这条未知客户的订单不会构成匹配;不是把NULL当成了一个确定不同的编号。

EXISTS关注的是子查询有没有返回行,不是SELECT列表里的值是不是NULL。SELECT 1表达“不需要订单的内容”,不是魔法常量。也不要漏掉o.customer_id = c.id:若完全去掉相关条件,只要订单表非空,就会让所有客户都被排除。MySQL EXISTS文档

5. LEFT JOIN也可以,但检查哪一列很重要

SELECT c.id FROM blog_null_customers AS c LEFT JOIN blog_null_orders AS o ON o.customer_id = c.id WHERE o.id IS NULL ORDER BY c.id;

左连接让没有匹配订单的客户也保留下来,右侧列补成NULL。订单id是非空主键:真实匹配行的id不可能为空,因此o.id IS NULL能识别“右侧没有匹配”。

不能把判断随便改成下面这样:

SELECT c.id FROM blog_null_customers AS c LEFT JOIN blog_null_orders AS o ON o.customer_id = c.id WHERE o.memo IS NULL ORDER BY c.id;

它会错误输出1、2、3。客户1有订单,但订单10的备注恰好为空。“没有匹配行”和“匹配行里的某个字段为空”不是同一个事实。

这里用o.id是因为它的约束明确。对这个特定等值连接,检查o.customer_id IS NULL也能成立:左侧id非空,匹配成功的右侧customer_id必然等于它;但这种推理依赖连接条件,不能外推成任何可空列都能当匹配标记。

6. 两个边界,避免只记一句口诀

可以在前面建好表的连接中执行:

SELECT 1 NOT IN (1, NULL) AS matched_value, 2 NOT IN (1, NULL) AS unmatched_value, NULL NOT IN (1) AS null_left_value, NULL NOT IN ( SELECT customer_id FROM blog_null_orders WHERE 1 = 0 ) AS null_left_empty_set;

本次输出依次为0、NULL、NULL、1。

最后一列提醒我们:子查询为空与子查询含一行NULL不同,不能都叫“里面没有值”。即使左侧是NULL,对空子查询的NOT IN也能为TRUE。

另外,若外层键允许NULL,过滤掉子查询NULL的NOT IN与普通等值相关的NOT EXISTS也不总等价:右侧非空时,前者可能得到未知,后者因找不到相等行而为真。本文客户主键非空,才避免了这个差异。是否应该输出未知客户,必须先定业务语义,不能先选语法再补解释。

7. 回到原来的“没下单客户”

原来的直觉可以保留:把已经下单的客户排除。但实现前需要补三个问题:

  • 被比较的两侧,是否可能为NULL?
  • 未知客户ID的订单,在业务上应该如何处理?
  • 如果用连接判断“没有匹配”,被检查的列是否真的能标识匹配?

真正值得带走的不是“以后不用NOT IN”,而是一个排除查询是否正确,取决于空值约束和匹配语义,不只取决于它看起来像不像集合运算。

附录:怎样验证这些结论

本地验证从这篇正文提取SQL,运行隔离MySQL实例,只监听本地Unix socket,不连业务数据库。客户固定为1、2、3,订单客户ID从1、2、3、4、NULL中选,枚举长度0到3的有序列表,共1+5+25+125=156组。

这些样本包含空表、只有NULL、重复订单、部分客户匹配、全部客户匹配以及不存在的客户4。验证程序用独立的集合判断计算预期:收集已知订单客户ID,再列出不在集合里的客户。

对每组数据,过滤NULL的NOT IN、NOT EXISTS、正确LEFT JOIN都必须与预期一致。原NOT IN也被检查,但它的预期来自三值语义:已经匹配为假,没有匹配且存在NULL为未知,否则为真。我们不是把错误写法偶尔输出正确当成它已经安全。

还做两个故意破坏的对照:漏掉NOT EXISTS的相关条件,以及把LEFT JOIN的非空主键检查改为可空备注检查。固定样例必须拒绝这两个错误。最后单独检查上一节四个表达式的结果。

156组只是这个明确小范围的穷举,不是所有类型转换、字符排序规则、多列比较和数据库版本的兼容性证明。没有测性能,不给NOT EXISTS或JOIN贴“永远更快”的标签;性能要在实际数据、索引和执行计划下另行核对。MySQL子查询优化

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

Joybuy ERP 哪个好用?跨境电商卖家选型避坑指南

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

作者头像 李华
网站建设 2026/10/23 2:49:45

工业托盘为什么越做越“讲究”:一块塑料垫板背后的力学取舍

在很多人的直觉里&#xff0c;仓库地面上垫着的那块托盘&#xff0c;大概是现代工业供应链里最没有技术含量的物件之一。它既没有精密芯片&#xff0c;也不用插电联网&#xff0c;看起来不过就是一大块注塑成型的塑料网格或者平板。过去用人工叉车在地库里倒腾几箱普货&#xf…

作者头像 李华
网站建设 2026/10/23 2:48:17

YimMenu:能防公开崩溃攻击的 GTA5 增强菜单,10 分钟上手

YimMenu&#xff1a;能防公开崩溃攻击的 GTA5 增强菜单&#xff0c;10 分钟上手 【免费下载链接】YimMenu YimMenu, a GTA V menu protecting against a wide ranges of the public crashes and improving the overall experience. 项目地址: https://gitcode.com/GitHub_Tre…

作者头像 李华