not exists 是查找 a 表有而 b 表无记录最稳妥的方式,不受 null 影响,需显式比对所有关键字段并建立联合索引;not in 遇 null 会失效,left join … is null 可提升可读性但判断条件须置于 where。

用 NOT EXISTS 找出 A 表有但 B 表没有的增量记录
这是最常用也最稳妥的方式,尤其当 B 表有复合主键或存在 NULL 值时,NOT IN 会意外失效,而 NOT EXISTS 不受 NULL 影响。
假设你有两个结构相同的表:orders_today(新数据)和 orders_yesterday(旧快照),想查今天新增的订单:
SELECT * FROM orders_today t
WHERE NOT EXISTS (
SELECT 1 FROM orders_yesterday y
WHERE y.order_id = t.order_id
AND y.customer_id = t.customer_id
);
- 必须显式写出所有用于比对的字段,不能只靠
order_id—— 如果业务上“同一订单号+不同客户”算新记录,就得一起比 -
SELECT 1是惯例写法,不查实际列,性能更轻量 - 确保
orders_yesterday(order_id, customer_id)有联合索引,否则子查询可能全表扫描
用 LEFT JOIN ... IS NULL 替代子查询提升可读性
当字段较多、逻辑稍复杂时,LEFT JOIN 比嵌套子查询更直观,执行计划也常被优化器处理得更好。
同样找增量订单:
SELECT t.* FROM orders_today t LEFT JOIN orders_yesterday y ON t.order_id = y.order_id AND t.customer_id = y.customer_id WHERE y.order_id IS NULL;
- 注意判断条件必须写在
WHERE中(y.order_id IS NULL),而不是ON里加AND y.order_id IS NULL—— 后者会变成无效连接条件,结果错乱 - 如果
orders_yesterday的order_id允许为 NULL,就别用它做 IS NULL 判断,换一个非空字段如y.id(自增主键)更安全 - 这个写法在 PostgreSQL 和 SQL Server 上表现稳定;MySQL 8.0+ 也没问题,但老版本要注意驱动表选择
警惕 NOT IN 遇到 NULL 导致结果为空
这是线上最容易踩的坑:只要 orders_yesterday.order_id 里有一个 NULL,NOT IN (SELECT order_id FROM orders_yesterday) 整个条件就恒为 UNKNOWN,最终返回空结果集 —— 看起来“没增量”,其实是逻辑崩了。
- 错误示例:
SELECT * FROM orders_today WHERE order_id NOT IN ( SELECT order_id FROM orders_yesterday );
- 哪怕你确定“昨天没 NULL”,只要表结构允许 NULL 或某次 ETL 出错插入了 NULL,这个查询就不可信
- 真要用
NOT IN,必须显式过滤:SELECT order_id FROM orders_yesterday WHERE order_id IS NOT NULL - 但多一层过滤不如直接换
NOT EXISTS或LEFT JOIN,省心且语义清晰
大数据量下避免全表扫描的关键点
当两个表都超百万行,子查询慢不是因为语法,而是缺索引或字段类型不一致。
- 比对字段的类型必须严格一致:比如
t.order_id是BIGINT,y.order_id却是VARCHAR,会导致索引失效 + 隐式转换 - 联合比对时,索引顺序要和
ON或WHERE中的字段顺序一致,例如(order_id, customer_id)能加速ON a.order_id = b.order_id AND a.customer_id = b.customer_id,但对customer_id = ? AND order_id = ?效果打折 - 如果只是校验是否存在差异(不需要明细),用
COUNT(*)包一层比查所有字段快得多,尤其配合EXISTS提前终止
实际跑的时候,先 EXPLAIN 看一眼执行计划里有没有 DEPENDENT SUBQUERY 或 Using join buffer —— 前者说明子查询被反复执行,后者往往意味着内存不足被迫落盘。这些细节比语法本身更容易决定快慢。










