优先用NOT EXISTS校验主键差异,如SELECT id FROM t1 WHERE NOT EXISTS (SELECT 1 FROM t2 WHERE t2.id = t1.id),需确保主键有索引;再通过哈希比对非主键字段,注意NULL处理、类型转换及时区对齐。

用 EXISTS + NOT EXISTS 找出不一致的主键记录
直接对比两张大表全字段太慢,也容易 OOM;优先校验主键(或业务唯一键)是否存在“一方有、另一方无”的情况。核心是避免 LEFT JOIN 扫全表,改用半连接语义更高效。
假设表 t1 和 t2 都有主键 id,想查 t1 里存在但 t2 里缺失的记录:
SELECT id FROM t1 WHERE NOT EXISTS (SELECT 1 FROM t2 WHERE t2.id = t1.id);
同理查 t2 多出的记录:
SELECT id FROM t2 WHERE NOT EXISTS (SELECT 1 FROM t1 WHERE t1.id = t2.id);
- 必须确保
t1.id和t2.id上都有索引,否则子查询会触发全表扫描 - 不要写成
NOT IN (SELECT id FROM t2)—— 如果t2.id含NULL,整个结果为空,这是经典陷阱 - MySQL 8.0+、PostgreSQL、SQL Server 对这类
NOT EXISTS优化较好;Oracle 需注意绑定变量窥探可能影响执行计划
用子查询 + HASH 比对非主键字段差异
主键一致后,再验证关键业务字段是否完全相同。逐行比对性能差,推荐用聚合哈希:对每行拼接关键字段后取 MD5 或 SHA2,再比对分组哈希值。
例如校验 t1 和 t2 的 name, status, updated_at 是否一致(仅限主键已对齐的记录):
SELECT id, MD5(CONCAT(COALESCE(name,''), '|', COALESCE(status,''), '|', COALESCE(updated_at,''))) AS h FROM t1 WHERE id IN (SELECT id FROM t2)
把上面结果存为临时表 h1,同理生成 h2,再用 NOT EXISTS 找哈希不等的 id:
SELECT id FROM h1 WHERE NOT EXISTS (SELECT 1 FROM h2 WHERE h2.id = h1.id AND h2.h = h1.h)
-
COALESCE必须加,否则任意字段为NULL会导致CONCAT返回NULL,哈希失效 - 分隔符(如
'|')不能省——否则name='ab', status='c'和name='a', status='bc'哈希会撞车 - PostgreSQL 可用
MD5(t1.*::text)简化,但跨版本兼容性差;MySQL 不支持直接行转文本,老老实实拼字段
处理 NULL 值和类型隐式转换导致的假差异
子查询中字段类型不一致(比如 t1.status 是 TINYINT,t2.status 是 VARCHAR),或 NULL 处理逻辑不同,会让本该一致的记录被误判为差异。
- 在哈希拼接前统一显式转换:用
CAST(status AS CHAR)或IFNULL(status, '')替代裸字段 - 避免在
WHERE子句中写t1.status = t2.status直接比较——如果一边是NULL,结果恒为UNKNOWN,整行被过滤掉 - 用
(t1.status = t2.status) OR (t1.status IS NULL AND t2.status IS NULL)才能正确处理 NULL 相等语义 - 时间字段注意时区和精度:MySQL 中
DATETIME(3)和DATETIME比较会自动补零,但 PostgreSQL 默认不补,建议先TRUNC或ROUND
大表校验时控制资源消耗和中断恢复
一次性跑完可能锁表、占满内存或超时,得拆解+留痕。
- 用主键范围分片:比如每次查
id BETWEEN 100000 AND 200000,配合AND NOT EXISTS子查询 - 把中间结果(如哈希表)建在
MEMORY引擎或临时表里,避免反复计算 - 记录已校验的最大
id到一张控制表,失败后从断点继续,别重头再来 - 在从库上做校验,禁止在生产主库高峰时段执行;若必须在主库,加
SET SESSION max_execution_time = 30000防卡死
真正难的不是写出子查询,而是确认哪些字段该参与比对、NULL 怎么算相等、以及如何让几亿行的校验不拖垮数据库。这些细节没对齐,结果就是“查出来不一致”,但你不知道是数据真错了,还是你查错了。

















