UNION ALL + GROUP BY + HAVING COUNT(*) = 1 是最直接的全量差异定位方式:先拼接两表数据,再按业务定义的唯一标识字段分组,最后筛选仅出现1次的行;它不依赖主键,但要求SELECT列严格对齐、GROUP BY字段完整覆盖非聚合列,并需谨慎处理NULL、类型隐式转换及索引优化。

能,但必须严格对齐字段、显式写出所有比对列、并处理 NULL 和类型隐式转换——否则结果不可信。
GROUP BY + HAVING COUNT(*) = 1 的核心逻辑
本质是把两张表用 UNION ALL 拼成一张“宽表”,再按业务定义的“唯一行”字段分组;若某组只出现 1 次,说明该行仅存在于其中一张表。
它不依赖主键,也不要求字段名一致,只要 SELECT 列能一一对应即可。
- 必须显式写出所有参与比对的列,不能写
*(除非两表字段完全一致且顺序相同) -
GROUP BY字段必须和SELECT中的非聚合列完全一致,MySQL 8.0+ 会直接报错 - 字符串字段注意
CHAR和VARCHAR对末尾空格的处理差异,可能让本该相同的行被拆成两组 -
NULL在GROUP BY中被视为相同值,但多条NULL行会导致COUNT(*) > 1,从而漏掉真实差异
为什么 GROUP BY 后少一个字段就出错?
比如对比配置表 config_standard_quality_control_item,业务上靠 defect_id、defect_name、type_id 三者联合唯一——漏掉任意一个,就会把逻辑不同的两条记录强行归为一组,HAVING COUNT(*) = 1 就失效了。
- 先确认“一行数据”在业务上的定义,再反推需要进
GROUP BY的字段 - 有主键或唯一索引时,优先用其全部字段做分组,最稳妥
- 避免把
update_time或created_at这类时间戳字段加入分组,同步延迟极易造成假差异 - 常见错误现象:
Column count doesn't match或结果为空但实际有差异——多半是SELECT列没对齐,或漏写了GROUP BY字段
如何保留重复行并区分哪边多哪边少?
原生 GROUP BY + HAVING 只能告诉你“存在差异”,但无法回答“是 A 表多出 3 条,还是 B 表少了 2 条”。这时得引入标记列:
SELECT a1, a2, a3,
COUNT(src1) AS cnt_a,
COUNT(src2) AS cnt_b
FROM (
SELECT a1, a2, a3, 1 AS src1, NULL AS src2 FROM table_a
UNION ALL
SELECT b1, b2, b3, NULL AS src1, 2 AS src2 FROM table_b
) t
GROUP BY a1, a2, a3
HAVING COUNT(src1) != COUNT(src2);
-
COUNT(src1)统计table_a中该行出现次数(NULL不计入) -
COUNT(src2)同理统计table_b出现次数 - 结果里能看到具体是哪边缺失、哪边冗余,甚至能发现重复数据问题
- 这个模式可扩展到 3 张表以上,只需增加
src3、src4等标记列
性能与兼容性要注意什么?
全表扫描 + GROUP BY 对大数据量压力不小,尤其当比对字段没有索引时。
- 确保参与
GROUP BY的字段上有合适索引,否则执行计划可能走全表排序 - MySQL 不支持
EXCEPT,所以这个方案在 MySQL 下仍是主流选择 - 如果表太大,可按逻辑分区(如按日期范围、状态码)分批比对,避免单次内存溢出
- 字段类型隐式转换(比如
INT和DECIMAL比较)可能触发全表类型转换,拖慢速度
真正容易被忽略的是:比对前没确认业务上“一行”的定义,就直接拿所有字段往 GROUP BY 里塞——结果看似跑通,实则漏掉了关键差异。










