UNION ALL + GROUP BY + HAVING 是最直接的全量差异定位方式:先拼接两表数据,再按所有比对字段分组,最后用 HAVING COUNT(*) = 1 筛出仅存在于一张表的行;它不依赖主键,但要求 SELECT 列严格对齐、GROUP BY 字段完整覆盖非聚合列,且需注意 NULL 处理、类型隐式转换及索引优化。

UNION ALL + GROUP BY + HAVING 筛单边存在行
这是最直接的全量差异定位方式,适用于结构兼容的表(列数、类型、顺序可对齐)。把两张表用 UNION ALL 拼起来,按所有比对字段分组,再用 HAVING COUNT(*) = 1 找出只在其中一张表出现的行。
常见错误现象:Column count doesn't match 或结果为空但实际有差异——多半是 SELECT 的列没对齐,或漏写了 GROUP BY 后的关键字段。
- 必须显式写出所有参与比对的列,不能写
*(除非两张表字段完全一致且顺序相同) -
GROUP BY的字段必须和SELECT中的非聚合列完全一致,否则 MySQL 8.0+ 会报错 - 字符串字段注意隐式类型转换:比如
CHAR和VARCHAR末尾空格处理不同,可能让本该相同的行被当成差异 -
NULL值在GROUP BY中被视为相同,但多条NULL行会导致COUNT(*) > 1,从而漏掉真实差异
LEFT JOIN + IS NULL 定位单向差集
MySQL 不支持 EXCEPT,想取「表 A 有、表 B 没有」的数据,必须靠 LEFT JOIN + IS NULL 组合实现。核心是把待查差异行作为左表,右表按全字段关联,再筛出右表匹配为空的记录。
典型写法示例:
SELECT a.* FROM table_a a LEFT JOIN table_b b ON a.id = b.id AND a.name <=> b.name AND a.status <=> b.status WHERE b.id IS NULL;
注意: 是 MySQL 的 NULL 安全等于操作符,比 = 更可靠。
- 所有参与比对的字段都要出现在
ON子句中,否则WHERE b.xxx IS NULL判定会失效 - 字段允许
NULL时,a.col = b.col在任一端为NULL时结果恒为UNKNOWN,应改用a.col b.col - 连接字段最好都有索引;若无联合索引,多字段
ON条件可能触发全表扫描 - 字符集或排序规则(
COLLATE)不一致会导致比较失败,执行前务必用SHOW CREATE TABLE核对各字段的COLLATE是否一致
GROUP BY 后对比聚合结果差异
当你要比的是分组统计值(比如各城市订单数、各品类销售额),而不是原始明细行,就别直接拼原始数据。先各自 GROUP BY 出统计结果,再用 LEFT JOIN 对齐维度后比数值。
关键点在于:ON 必须包含全部分组键,漏一个就会错配。
- 例如按
category和region分组,ON就得写成ON a.category = b.category AND a.region = b.region - 想同时抓出“数量变化”和“完全消失”,
WHERE条件别只写b.cnt IS NULL,换成a.cnt != b.cnt OR b.cnt IS NULL - 别在
JOIN前加HAVING过滤——那会先筛掉数据,导致无法识别“从有到无”的变化 - 如果还要查“B 有但 A 没有”,换用
RIGHT JOIN或改写为FULL OUTER JOIN(MySQL 不原生支持,需用UNION模拟)
为什么 NOT IN 和子查询容易出问题
NOT IN 看似简洁,但实际风险高:只要子查询返回任意一个 NULL,整个 NOT IN 结果恒为 FALSE 或 UNKNOWN,导致零行返回(静默失败)。
- 无法利用索引加速,尤其子查询结果大时性能断崖式下降
- 语义上它检查的是单字段值是否不在集合中,不适用于整行比对场景
-
EXISTS虽比NOT IN更安全,但仍需确保子查询中所有比对字段都参与关联条件,否则逻辑不等价 - 真正可靠的单向差集写法仍是
LEFT JOIN ... WHERE right_table.pk IS NULL
比对前必须明确“一行数据”的业务定义——差异判断粒度由 GROUP BY 或 JOIN 字段决定。少一个字段,就可能把两条逻辑不同的记录当成相同,或者把一条合法变更当成异常差异。

















