NOT EXISTS比NOT IN更可靠,因为NULL会使NOT IN恒为UNKNOWN导致结果为空,而NOT EXISTS只判断是否存在匹配行、不受NULL影响,但必须写成正确关联的相关子查询。

NOT EXISTS比NOT IN更可靠,因为NULL会让NOT IN永远不返回结果
只要id列里存在NULL,WHERE id NOT IN (SELECT id FROM t)整个条件就恒为UNKNOWN,所有行被过滤——结果永远为空。这不是bug,是SQL三值逻辑的必然行为。实操中第一件事就是跑SELECT COUNT(*), COUNT(id) FROM t,如果两个数不等,说明有NULL混在里面。
用NOT EXISTS就没这问题,它只关心“是否存在匹配行”,不依赖值比较。但必须写成相关子查询:WHERE NOT EXISTS (SELECT 1 FROM t t2 WHERE t2.id = t1.id + 1),漏掉t2.id = t1.id + 1这个关联条件,就会变成全表扫描+恒真判断。
LEFT JOIN + IS NULL适合查“主表有、右表无”的明确缺失
比如查orders里哪些user_id在users表里不存在,直接写LEFT JOIN users u ON o.user_id = u.id WHERE u.id IS NULL最直观。注意两点:
-
IS NULL不能写成= NULL,后者永远不成立 - 连接字段类型要一致,比如
orders.user_id是VARCHAR而users.id是INT,隐式转换会让索引失效甚至匹配失败
生成全量序列再LEFT JOIN,比反向推导更可控
当知道ID范围(比如1–1000),优先走“生成序列→左连原表→找NULL”这条路,而不是靠id+1自连接或NOT EXISTS去猜。原因很实在:
- 递归CTE(如
WITH RECURSIVE seq(n) AS (...))能一次生成万级数字,毫秒级完成 - 避免
NOT EXISTS在稀疏数据(比如只存了1、100、200)下扫描大量无效id - 生成上限别无脑设1000000,否则CTE构建本身成瓶颈;设成
(SELECT MAX(id) FROM t)更稳妥
MySQL低版本没递归CTE?用变量或自连接模拟连续数
MySQL 5.7及更早不支持WITH RECURSIVE,硬写UNION ALL十几层太脆弱。可行替代方案:
- 用用户变量生成序列:
SELECT @row := @row + 1 AS n FROM (SELECT 1 UNION ALL SELECT 2) t, (SELECT @row := 0) r LIMIT 1000,但需确保sql_mode不含ONLY_FULL_GROUP_BY干扰 - 用
information_schema.tables这类系统表做笛卡尔积凑数(慎用,不同MySQL版本行为不一) - 真正稳定的做法:建一张
nums辅助表,预存1–10000的整数,CREATE TABLE nums (n INT PRIMARY KEY),之后反复LEFT JOIN即可
id字段没索引、或者JOIN两边类型不一致,查询可能从毫秒变分钟。

















