一次对账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 1 或 SELECT 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_no、order_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 IN、IN、JOIN 等操作时,一定要确认字段是否存在、是否包含 NULL 值。

相关推荐
天天喝旺仔3 小时前
MySQL 索引原理与慢查询优化实战:从 B+ 树到执行计划调优
数据库·sql·mysql·性能优化
m0_646429974 小时前
MySQL 初始化 SQL 中文乱码问题总结
数据库·sql·mysql
实战派K8S&DB4 小时前
TDSQL 核心模块与进程体系
运维·数据库·分布式·sql·mysql
Lost_the_wind4 小时前
MySQL 学习笔记三
笔记·学习·mysql
腾科IT教育5 小时前
Oracle OCP和MySQL OCP怎么选?方向对比指南
mysql·oracle·ocp·oracle ocp·ocp怎么选·mysql ocp
用户460381291946 小时前
别人都 commit 了,为什么我的 select 还是旧值?图解 MySQL MVCC
mysql
毕业设计7039 小时前
(免费领源码)django空气净化器销售系统09218- java、PHP、python、C#、小程序、大数据、单片机、网络工程等)
python·scrapy·mysql·pycharm·django·flask·pandas
CJi0NG10 小时前
【自用】MySQL-事务
数据库·mysql
VX_bysjlw98510 小时前
数码设备销售网站设计与实现39138-计算机毕设原创(免费领源码+带部署教程)
java·vue.js·spring boot·mysql·tomcat·mybatis·idea
PHP实战开发录11 小时前
MySQL数字排序为什么乱
数据库·mysql·php