left join + where 右表关联字段 is null 是查询左表有而右表无记录的最直接方法,利用 left join 保留左表全部行、右表无匹配时关联字段为 null 的特性,通过 where 筛出该字段为 null 的行;须确保使用右表被 join 的字段(如主键或外键)判 null,避免误用非空字段或漏写条件。

LEFT JOIN + WHERE IS NULL 是最直接的写法
要找出左表有、右表没有的记录,核心就是利用 LEFT JOIN 保留左表全部行,再用 WHERE 右表主键 IS NULL 筛出那些没匹配上的行。注意必须是右表的**被 JOIN 字段(通常是主键或关联字段)为 NULL**,而不是左表字段。
常见错误是写成 WHERE 右表某非空字段 IS NULL——如果该字段本身允许 NULL,就会误判;或者漏掉 WHERE 条件,只用 LEFT JOIN,结果包含所有匹配行,达不到“仅找缺失”的目的。
-
SELECT a.id, a.name FROM users a LEFT JOIN orders b ON a.id = b.user_id WHERE b.user_id IS NULL;—— 正确:用右表关联字段判 NULL - 别用
b.order_id IS NULL替代,除非你确定它非空且能唯一标识关联关系 - 如果右表关联字段是复合主键,需全部判 NULL,例如
WHERE b.user_id IS NULL AND b.status IS NULL(极少用,通常说明设计有问题)
ON 条件里不能加右表的过滤逻辑
LEFT JOIN 的 ON 子句只控制连接行为,不是筛选条件。如果在 ON 里写 AND b.status = 'paid',会导致右表本应不匹配的行“看起来像匹配了”,从而让 WHERE b.user_id IS NULL 失效。
比如想查“从未下过已支付订单的用户”,错误写法:LEFT JOIN orders b ON a.id = b.user_id AND b.status = 'paid' —— 这会让未支付订单的用户也被纳入左连接结果,b.user_id 不为 NULL,漏掉真实目标。
- 正确做法:先
LEFT JOIN orders b ON a.id = b.user_id,再在WHERE加b.user_id IS NULL,需要额外过滤时用子查询或 CTE - 若右表有大量数据,把过滤条件放在
WHERE而非ON中,还能让优化器更早剪枝
EXISTS 和 NOT EXISTS 往往比 LEFT JOIN 更高效
当只关心“是否存在”而非右表字段时,NOT EXISTS 通常执行更快,尤其右表很大、左表较小时。数据库能在找到第一个匹配就停止搜索,而 LEFT JOIN 需构建完整中间结果集。
语义等价但性能差异明显:
SELECT id, name FROM users a WHERE NOT EXISTS (SELECT 1 FROM orders b WHERE b.user_id = a.id);- 确保
orders.user_id有索引,否则NOT EXISTS也会变慢 - 某些旧版 MySQL 对
NOT EXISTS优化不佳,可改用NOT IN,但要注意右表字段含 NULL 会导致整个结果为空(NOT IN (NULL, 1)永远不成立)
NULL 值和空字符串容易混淆,务必确认字段定义
如果右表关联字段允许 NULL,但业务上“未下单”应该存为 NULL 而不是空字符串或默认值(如 0),那 IS NULL 才可靠。否则可能因数据脏乱导致漏查。
检查方式:SELECT COUNT(*) FROM orders WHERE user_id IS NULL; 如果结果大于 0,得先确认这些 NULL 是“无关联”还是“数据异常”。
- 建表时尽量让外键字段
NOT NULL,避免歧义 - 如果右表用
0或-1表示“无用户”,就不能用IS NULL,得改成WHERE b.user_id = 0或类似逻辑 - MySQL 8.0+ 支持函数索引,可用
CREATE INDEX idx_user_null ON orders ((CASE WHEN user_id IS NULL THEN 1 END));加速 NULL 查找,但多数场景没必要
LEFT JOIN 不加 WHERE,看几条结果,确认右表对应字段真为 NULL 再加筛选。不然容易对着一堆非 NULL 数据调试半天。











