必须用 left join + where right_table.id is null,因为 = null 永远返回 false,导致结果全空;on 条件须覆盖全部业务键、避免 or 和函数、右表字段需有索引且类型一致;null 值需显式处理,否则匹配失效。

必须用 LEFT JOIN + WHERE right_table.id IS NULL,否则结果不可靠、性能差、还容易全空——这不是优化建议,是底线。
为什么不能写 WHERE right_table.id = NULL
SQL 中 = NULL 永远返回 FALSE,不是 UNKNOWN,更不会匹配任何行。哪怕右表真没数据,这条 WHERE 也过滤掉全部左表记录,结果永远为空。
- 正确只能是
IS NULL,这是唯一能判定缺失匹配的语法 - 如果误写成
= NULL,EXPLAIN 看执行计划会发现没走索引,但你根本查不到数据,连调试入口都没有 - 某些 ORM 自动生成的 SQL 会偷偷把
IS NULL转成= NULL(尤其老版本 Django 或 MyBatis),要手动检查生成语句
ON 条件漏字段或混用 OR,差集就失效
多字段联合判断“是否存在”时,ON 必须显式列出全部业务键,少一个就可能把本该排除的行放进来。
- 比如用
(user_id, sku)做唯一标识,ON a.user_id = b.user_id单独成立,但a.sku和b.sku不参与关联 → 左表某user_id可能匹配右表任意sku行,IS NULL判定完全失真 - 避免在
ON里写OR(如ON a.id = b.id OR a.email = b.email),这会让 MySQL 放弃使用索引,甚至触发嵌套循环全表扫描 - 若右表字段允许
NULL,且你又拿它做ON条件(比如ON a.email = b.email),那所有email IS NULL的行都匹配不上 —— 因为NULL = NULL是UNKNOWN,不成立
右表没索引,LEFT JOIN 比 NOT IN 还慢
很多人以为换用 LEFT JOIN 就自动变快,其实不然:它的性能瓶颈几乎全压在右表上。没索引时,MySQL 只能对右表逐行扫描来尝试匹配。
- 必须确保右表用于
ON的字段有索引(比如returns.order_id),且类型与左表严格一致(INT对INT,不是VARCHAR对INT) - 避免在
ON或WHERE中对字段用函数(如ON UPPER(a.email) = UPPER(b.email)),索引直接失效 - 右表子查询(如
SELECT id FROM logs WHERE dt = '2026-10-13')务必提前加好过滤条件,别让它返回几百万行再 JOIN
NULL 值导致匹配失败,得手动兜底
当关联字段本身可为空(比如 phone、address),标准等值 ON 会跳过所有含 NULL 的行,造成差集漏数据。
- 显式补全逻辑:
ON (a.phone = b.phone OR (a.phone IS NULL AND b.phone IS NULL)),但注意不能只写OR部分,否则产生笛卡尔积 - 多个字段都可能为
NULL时(如phone,email,address),ON条件迅速膨胀,此时应优先改用NOT EXISTS,语义更干净 - PostgreSQL 用户可用
IS NOT DISTINCT FROM简化:ON a.phone IS NOT DISTINCT FROM b.phone,MySQL 不支持,别硬搬
真正难的不是写出第一版 LEFT JOIN,而是确认右表字段有没有索引、ON 条件是否覆盖全部业务键、以及那些藏在 NULL 里的匹配失效场景——这些地方一错,语句看着跑通,数据却悄悄不对。










