订阅更新

SQL LEFT JOIN 条件放在 ON 和 WHERE 有什么区别?

面试问 068 约 3 分钟读完 1 次阅读

本文目录
  1. 面试怎么回答
  2. 条件放在 ON:限制右表如何匹配
  3. 条件放在 WHERE:过滤 Join 后的结果
  4. 不只是右表条件,左表条件也要小心
  5. 实际写 SQL 时怎么判断?
  6. 一个典型数仓场景

面试怎么回答

ON 条件在 Join 匹配阶段生效,决定左右表哪些记录能够匹配;WHERE 在 Join 完成后过滤结果。对 LEFT JOIN 来说,右表条件放 ON 会保留未匹配的左表行并补 NULL,放 WHERE 则可能把这些行过滤掉,使结果接近 INNER JOIN。实际使用时我会先确认条件是在定义匹配范围还是最终保留范围,同时再检查右表粒度是否会放大数据。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

同样是给右表加一个 status = 'paid' 条件,两条 LEFT JOIN SQL,一条返回 3 个用户,另一条只剩 1 个用户。

条件只是从 ON 挪到了 WHERE,结果为什么会变?

先记住结论:ON 决定左右表哪些行能够匹配,WHERE 决定 Join 完成后的哪些结果行能够留下。对于外连接,这两个阶段不能随便互换。


条件放在 ON:限制右表如何匹配

假设用户表有 3 个用户,订单表只有用户 1 存在已支付订单:

SELECT
  u.user_id,
  o.order_id
FROM dim_user u
LEFT JOIN dwd_order o
  ON u.user_id = o.user_id
 AND o.status = 'paid';

执行时,status = 'paid' 是 Join 匹配条件的一部分。

用户 2 和用户 3 找不到符合条件的订单,但 LEFT JOIN 仍会保留左表用户,只是右表字段补成 NULL。最终仍然返回 3 个用户。

条件放在 WHERE:过滤 Join 后的结果

SELECT
  u.user_id,
  o.order_id
FROM dim_user u
LEFT JOIN dwd_order o
  ON u.user_id = o.user_id
WHERE o.status = 'paid';

LEFT JOIN 先为未匹配用户补出右表 NULL,然后 WHERE 再过滤结果。

对于用户 2 和用户 3,o.status = 'paid' 的判断结果不是 TRUE,因此都会被过滤掉。结果只剩下真正匹配已支付订单的用户,语义已经接近 INNER JOIN。

LEFT JOIN 中 ON 与 WHERE 的结果差异


不只是右表条件,左表条件也要小心

很多人以为 LEFT JOIN 里所有条件都可以放 ON。

比如:

FROM dim_user u
LEFT JOIN dwd_order o
  ON u.user_id = o.user_id
 AND u.user_status = 'active'

这里的 u.user_status = 'active' 只决定用户能不能与订单匹配,并不会删除非活跃用户。非活跃用户仍会作为左表记录保留,只是订单字段为 NULL。

如果需求是只查询活跃用户,条件应该放在 WHERE,或者先在子查询中筛选左表。

实际写 SQL 时怎么判断?

我通常会先问一句:这个条件是在定义“谁和谁匹配”,还是在定义“最终要保留谁”?

  • 限制右表参与匹配的记录:放 ON,或先过滤右表子查询
  • 过滤最终结果:放 WHERE
  • 保留全部左表主体:不要在 WHERE 中直接要求右表字段满足条件
  • 只保留未匹配记录:使用 WHERE 右表主键 IS NULL

一个典型数仓场景

需求是统计所有渠道的注册用户数,以及其中产生付费的用户数。

如果把付费条件放 WHERE,没有付费用户的渠道可能整行消失,最终报表看不到“注册很多但付费为 0”的渠道。

更清楚的做法,是保留用户或渠道作为左侧全集,在 ON 中限制付费订单,再通过条件聚合计算付费人数。

当然,条件位置正确也不代表结果一定正确。右表同一个用户存在多笔订单时,Join 仍可能放大行数,需要先确认统计粒度,必要时先把右表聚合到用户粒度。

条件位置解决的是保留语义,Join 粒度解决的是数据是否被放大,这是两个不同问题。


刷题进度保存在当前浏览器