LEFT JOIN + IS NULL 是查找主表有而从表无记录的最直接方式,核心是以主表驱动左连接并筛选从表关联字段为 NULL 的行,如查无明细的订单。

用 LEFT JOIN + IS NULL 找主表有、从表无的记录
这是最常见也最直接的差异对比方式,适用于检查主表中哪些记录在从表里完全缺失。核心逻辑是:以主表为驱动,左连接从表,再筛选从表关联字段为 NULL 的行。
典型场景:订单主表 orders 和订单明细表 order_items,想查哪些订单连一条明细都没有。
SELECT o.order_id, o.created_at FROM orders o LEFT JOIN order_items oi ON o.order_id = oi.order_id WHERE oi.order_id IS NULL;
- 必须用
LEFT JOIN(不是INNER JOIN或RIGHT JOIN),否则无法保留主表空匹配行 -
WHERE条件必须写在oi.order_id IS NULL,不能写成oi.* IS NULL—— 多列判空不可靠,且部分数据库不支持 - 如果连接条件含多个字段(如
(o.id, o.version) = (oi.ref_id, oi.ref_version)),IS NULL要选一个确定非空的从表字段(比如主键或外键列)
用 FULL OUTER JOIN 检查双向差异(需注意数据库兼容性)
当既要找主表独有、也要找从表独有记录时,FULL OUTER JOIN 是语义最清晰的方案,但不是所有数据库都原生支持——PostgreSQL、SQL Server 支持;MySQL 不支持,需用 UNION 模拟。
示例(PostgreSQL):
SELECT 'only_in_orders' AS source, o.order_id FROM orders o FULL OUTER JOIN order_items oi ON o.order_id = oi.order_id WHERE oi.order_id IS NULL <p>UNION ALL</p><p>SELECT 'only_in_items' AS source, oi.order_id FROM orders o FULL OUTER JOIN order_items oi ON o.order_id = oi.order_id WHERE o.order_id IS NULL;</p>
- MySQL 用户必须拆成两个
LEFT JOIN查询再UNION ALL,否则报错 -
FULL OUTER JOIN的ON条件一旦写错(比如漏了时间范围过滤),会导致笛卡尔积式膨胀,务必先加索引并小数据验证 - 结果中
NULL值来自哪一侧取决于连接方向,别把o.order_id和oi.order_id混着选,容易误读
用 NOT EXISTS 替代 LEFT JOIN 提升大表性能
当主表很大、从表有高效索引时,NOT EXISTS 通常比 LEFT JOIN ... IS NULL 更快,因为能提前终止子查询,避免构建临时连接结果集。
SELECT o.order_id FROM orders o WHERE NOT EXISTS ( SELECT 1 FROM order_items oi WHERE oi.order_id = o.order_id );
-
NOT EXISTS子查询里必须关联外部表(即oi.order_id = o.order_id),否则变成恒真/恒假,结果全错 - 从表的连接字段(如
order_items.order_id)必须有索引,否则子查询退化为全表扫描,性能反而更差 - 不能用
NOT IN替代:如果从表该字段存在NULL,整个NOT IN表达式返回空结果集,静默出错
对比内容差异(不只是存在性)要用行级哈希或逐字段比较
前面方法只解决“有没有”,但实际常需判断“内容是否一致”——比如主表和从表都有某条记录,但金额、状态等字段值不同。这时不能只靠连接,得引入值比对逻辑。
推荐做法:对关键字段拼接后计算哈希(如 MD5(CONCAT(col1, col2, col3))),再连接比对哈希值。
SELECT o.order_id FROM orders o JOIN order_items oi ON o.order_id = oi.order_id WHERE MD5(CONCAT(o.status, o.total_amount)) != MD5(CONCAT(oi.status, oi.amount));
- 字符串拼接前建议用
COALESCE(col, '')统一处理NULL,否则CONCAT(NULL, 'x')得到NULL,哈希恒等 - 哈希法快但有极低碰撞风险;要求绝对精确可用逐字段
AND (o.a != oi.a OR o.b != oi.b),但 SQL 冗长且NULL判等需额外写IS DISTINCT FROM(PostgreSQL)或三元逻辑(MySQL) - 字段类型不一致(如主表是
DECIMAL(10,2),从表是FLOAT)会导致隐式转换误差,比对前先显式CAST
实际做主从差异比对时,最容易被忽略的是 NULL 的语义陷阱——它既不是值,也不等于自身。所有判等、拼接、聚合操作都得预先约定如何归一化,否则白天跑通的 SQL,半夜数据进 NULL 就翻车。










