问题背景
在一次数据对账场景中,遇到了一个非常"诡异"的现象。
首先,执行以下两条统计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 等),替换后重新执行。
最佳实践建议
- 写SQL时尽量使用
表名.列名的写法 (如p.out_trade_no)。这样如果字段不存在,MySQL会直接报Unknown column错误,能提前发现问题,避免这种隐式的NULL陷阱。 - 在涉及"排除"逻辑时,优先使用
NOT EXISTS替代NOT IN。这是线上对账、数据校验场景的最佳实践,能彻底规避NULL陷阱,且性能通常更好。 - 对账前先确认两表的关联字段名是否一致,避免因字段名拼写错误或字段不存在导致的数据偏差。
总结
表格
| 现象 | 原因 |
|---|---|
NOT IN 子查询返回空集 |
子查询结果全为 NULL(因字段不存在),触发三值逻辑陷阱 |
SELECT 不存在的列不报错 |
MySQL将其视为常量表达式,返回 NULL |
两个 COUNT 差1 |
两表数据本身存在差异,需单独排查 |
这次排查经历再次提醒我们:SQL中的 NULL 处理需要格外小心,尤其是在使用 NOT IN、IN、JOIN 等操作时,一定要确认字段是否存在、是否包含 NULL 值。