一次对账SQL“灵异事件”排查:NOT IN返回空集的坑

问题背景

在一次数据对账场景中,遇到了一个非常"诡异"的现象。

首先,执行以下两条统计SQL,结果分别是1164条和1165条:

复制代码
SELECT COUNT(*) FROM t_payment_bill 
WHERE created_source = 'TSF' AND new_payment_type = 6 
  AND payment_channel = 3 AND date_end > '2026-09-02';
-- 结果:1164

SELECT COUNT(*) FROM t_yibao_bill 
WHERE created_source = 'TSF' AND new_payment_type = 6 
  AND out_trade_no NOT LIKE '%CS%';
-- 结果:1165

既然两边数量接近,按理说用 NOT IN 应该能查出差异数据。于是写了第三条SQL来查找"在医保账单表中存在,但在支付账单表中不存在"的记录:

复制代码
SELECT * FROM t_yibao_bill 
WHERE created_source = 'TSF' AND new_payment_type = 6 
  AND out_trade_no NOT IN (
    SELECT out_trade_no FROM t_payment_bill 
    WHERE created_source = 'TSF' AND new_payment_type = 6 
      AND payment_channel = 3 AND date_end >= '2026-09-02'
  );

结果竟然是:空集(0条)。

更奇怪的是,单独检查了医保账单表,确认没有重复的 out_trade_no

复制代码
SELECT out_trade_no FROM t_yibao_bill 
WHERE created_source = 'TSF' AND new_payment_type = 6 
GROUP BY out_trade_no HAVING COUNT(out_trade_no) > 1;
-- 结果:空

这到底是怎么回事?

排查过程

遇到 NOT IN 返回空集,第一反应通常是怀疑子查询中包含了 NULL 值。因为根据SQL的三值逻辑,NOT IN 遇到 NULL 会导致整个条件失效。

于是先验证子查询中是否有 NULL

复制代码
SELECT COUNT(*) FROM t_payment_bill 
WHERE created_source = 'TSF' AND new_payment_type = 6 
  AND payment_channel = 3 AND date_end >= '2026-09-02'
  AND out_trade_no IS NULL;

结果发现,out_trade_no 字段本身并不为 NULL。那问题出在哪?

进一步检查子查询的返回结果:

复制代码
SELECT out_trade_no FROM t_payment_bill 
WHERE created_source = 'TSF' AND new_payment_type = 6 
  AND payment_channel = 3 AND date_end >= '2026-09-02' 
LIMIT 5;

发现返回的全是 NULL

原来,t_payment_bill 表中根本没有 out_trade_no 这个字段

根本原因

在MySQL中,如果 SELECT 后面跟的列名在当前表中不存在,MySQL不会直接报错,而是会将其视为一个常量表达式 (类似 SELECT 1SELECT NULL),对每一行返回 NULL

所以子查询实际返回的结果是:

复制代码
NULL
NULL
NULL
...(共1164个NULL)

这就触发了经典的 NOT IN 遇到 NULL 全军覆没 的陷阱。

原理: NOT IN 的本质是多个 !=AND 连接。假设子查询返回了 'A', 'B', NULL,那么条件等价于:

复制代码
out_trade_no != 'A' AND out_trade_no != 'B' AND out_trade_no != NULL

任何值与 NULL 比较的结果都是 UNKNOWN,而 TRUE AND UNKNOWN = UNKNOWN。WHERE子句只接受 TRUE,因此所有行都被过滤掉,最终返回空集

解决方案

方案一(推荐):改用 NOT EXISTS

NOT EXISTS 不受 NULL 影响,且能利用索引做反半连接(Anti-Semi-Join),性能更优:

复制代码
SELECT * FROM t_yibao_bill y
WHERE y.created_source = 'TSF' AND y.new_payment_type = 6 
  AND y.out_trade_no NOT LIKE '%CS%'
  AND NOT EXISTS (
    SELECT 1 FROM t_payment_bill p
    WHERE p.created_source = 'TSF' AND p.new_payment_type = 6 
      AND p.payment_channel = 3 AND p.date_end >= '2026-09-02'
      AND p.out_trade_no = y.out_trade_no
  );

方案二:子查询中排除 NULL

如果坚持用 NOT IN,必须在子查询中显式排除 NULL

复制代码
SELECT * FROM t_yibao_bill 
WHERE ... AND out_trade_no NOT IN (
    SELECT out_trade_no FROM t_payment_bill 
    WHERE ... AND out_trade_no IS NOT NULL
);

方案三:使用正确的字段名

DESC t_payment_bill; 查看表结构,找到正确的字段名(可能是 trade_noorder_no 等),替换后重新执行。

最佳实践建议
  1. 写SQL时尽量使用 表名.列名 的写法 (如 p.out_trade_no)。这样如果字段不存在,MySQL会直接报 Unknown column 错误,能提前发现问题,避免这种隐式的 NULL 陷阱。
  2. 在涉及"排除"逻辑时,优先使用 NOT EXISTS 替代 NOT IN 。这是线上对账、数据校验场景的最佳实践,能彻底规避 NULL 陷阱,且性能通常更好。
  3. 对账前先确认两表的关联字段名是否一致,避免因字段名拼写错误或字段不存在导致的数据偏差。
总结

表格

现象 原因
NOT IN 子查询返回空集 子查询结果全为 NULL(因字段不存在),触发三值逻辑陷阱
SELECT 不存在的列不报错 MySQL将其视为常量表达式,返回 NULL
两个 COUNT 差1 两表数据本身存在差异,需单独排查

这次排查经历再次提醒我们:SQL中的 NULL 处理需要格外小心,尤其是在使用 NOT ININJOIN 等操作时,一定要确认字段是否存在、是否包含 NULL 值。

相关推荐
Linux-lucky1 小时前
32-38-Linux学习之旅之MySQL综合
linux·运维·学习·mysql·ubuntu
&不羁之风&4 小时前
从 MySQL 到 MongoDB:80 万系统日志(sys_log)平滑迁移完整实战(含 Windows 环境 mongoimport 安装与避坑)
mysql·mongodb
这个DBA有点耶5 小时前
InnoDB页结构深入:页分裂、页合并、填充因子——B+树底层机制全解析
数据库·mysql·架构
山岚的运维笔记5 小时前
mysql 专业笔记 -- 第 36 章:MySQL 管理
运维·数据库·笔记·后端·mysql·oracle·dba
霸道流氓气质7 小时前
SQL 里的 AND / OR 优先级陷阱:一个括号引发的“数据越界“
mysql
爱奥尼欧9 小时前
【MySQL】建库就是建目录?字符集校验规则、备份恢复与alter改表实操
mysql
大虾别跑10 小时前
mysql5.7升级
mysql
geovindu10 小时前
sql: Transaction & Concurrency Patterns using mysql 9.0
mysql·数据库开发·数据库架构
敲代码的瓦龙11 小时前
Jetpack?DataBinding!!!
android·java·开发语言·mysql·android-studio