NOT EXISTS比NOT IN更可靠,因其不受NULL影响、支持短路执行且能更好利用索引;必须写成相关子查询(如t.id = s.id),并确保子表字段有索引。

NOT EXISTS 比 NOT IN 更可靠,尤其当字段含 NULL
直接写 WHERE id NOT IN (SELECT id FROM target) 看似简单,但只要 target.id 里有一行是 NULL,整条查询就返回空——这不是 bug,是 SQL 三值逻辑(TRUE/FALSE/UNKNOWN)的必然结果。MySQL 和 PostgreSQL 都如此,且 NOT IN 无法利用索引加速,大表下极易超时。
改用 NOT EXISTS:它只关心“是否存在匹配行”,绕过 NULL 干扰;支持半连接优化,遇到第一条匹配就停止扫描;配合索引效果显著。
- 必须写相关子查询:
WHERE NOT EXISTS (SELECT 1 FROM target t WHERE t.id = s.id),t.id = s.id是关键关联条件 - 子查询里始终用
SELECT 1或主键,别写SELECT *,避免冗余传输和优化器误判 - 确保
target(id)有索引;若比对多字段(如id, status, updated_at),建复合索引更稳妥
避免标量子查询引发 N+1 查询
像 SELECT u.id, (SELECT MAX(created_at) FROM orders o WHERE o.user_id = u.id) AS last_order FROM users u 这种写法,在 users 有 10 万行时,会触发 10 万次独立子查询。即使 orders.user_id 有索引,I/O 和连接开销也扛不住。
正确做法是先聚合再 JOIN:把子查询提前物化为派生表,一次计算、多次复用。
- 聚合侧必须
GROUP BY关联字段,例如:(SELECT user_id, MAX(created_at) AS max_time FROM orders GROUP BY user_id) - 外层用
LEFT JOIN衔接,显式指定ON u.id = o.user_id - 如果聚合结果为空(某用户无订单),
LEFT JOIN仍保留用户行,max_time为NULL,语义清晰
用 UNION ALL + GROUP BY 替代 EXCEPT 实现全量差异定位
MySQL 5.7 不支持 EXCEPT,8.0+ 虽支持但默认去重,可能掩盖重复数据问题。更可控的方式是拼接后分组计数:UNION ALL 合并两表数据,GROUP BY 所有比对字段,再筛出 HAVING COUNT(*) = 1 的行——这些就是单边独有的记录。
这个方法不依赖主键,也不要求字段名一致,只要列数、类型、顺序对齐即可。
-
SELECT必须显式列出所有参与比对的字段,不能用*(除非两张表结构完全一致) -
GROUP BY字段必须和SELECT中非聚合列完全一致,否则 MySQL 8.0+ 会报错 - 字符串字段注意
COLLATE是否一致,不一致会导致相同内容被当成差异;可用COLLATE utf8mb4_0900_as_cs显式对齐 -
NULL在GROUP BY中被视为相同值,但多个NULL行会让COUNT(*) > 1,从而漏掉真实差异——必要时先COALESCE(col, '##NULL##')标准化
跨库或字段多时,优先用哈希做初筛
当表有几十万行、字段多、类型杂(比如含 TEXT、TIMESTAMP、DECIMAL),逐字段比较性能差。可先算行级哈希再比对,大幅减少需精细比对的数据量。
MySQL 可用 SHA2(CONCAT_WS('|', COALESCE(col1,''), COALESCE(col2,''), ...), 256),SQL Server 用 BINARY_CHECKSUM(*)。
-
CONCAT_WS自动跳过NULL,但为防空串干扰,仍建议COALESCE(col, '') - 哈希碰撞概率极低,但生产关键比对不能只靠哈希——应先用哈希筛出候选差异集,再对小结果集做字段级确认
- 注意隐式差异:时区偏移、尾部空格、字符集转换(如
utf8vsutf8mb4)都会影响哈希值,比对前先标准化数据
EXPLAIN 看执行计划,重点确认是否走了索引、有没有临时表或文件排序。很多“慢”不是语法问题,而是索引缺失或字段类型不匹配导致的隐式转换。

















