理解 SQL JOIN: ON 与 WHERE 的区别

在写 SQL JOIN 时,ONWHERE 经常一起出现,看起来有点像可以互换。但它们的作用并不相同,尤其在使用外连接(LEFT / RIGHT / FULL JOIN)时,放错地方甚至会让结果完全变样。

本文从以下几个方面解释:

  • JOINONWHERE 各自负责什么
  • 它们在功能上的差异(特别是 LEFT / RIGHT JOIN
  • 对性能的影响
  • 可读性、可维护性及常见坑

示例使用标准 SQL,大部分数据库(MySQLPostgreSQLSQL ServerOracle 等)都适用。


1. 基本概念

1.1 JOIN 是做什么的?

JOIN 用来根据某种关联条件,将两个(或多个)表的行组合在一起。

常见的 JOIN 类型:

  • INNER JOIN:只保留两边都匹配上的行

  • LEFT JOIN(或 LEFT OUTER JOIN):保留左表的所有行,右表只保留匹配到的

  • RIGHT JOIN:与 LEFT 相反,保留右表所有行

  • FULL OUTER JOIN:保留两边所有行(某些数据库不支持)

1.2 ON 是做什么的?

ON 用来定义 连接条件(join condition)

  • 告诉数据库两个表的行如何匹配
  • 在 JOIN 操作时被使用

示例:

vbnet 复制代码
SELECT *
FROM orders o
JOIN customers c
  ON o.customer_id = c.id;

ON 告诉数据库:"当 orders.customer_id 等于 customers.id 时,这两行属于同一组。"

1.3 WHERE 是做什么的?

WHERE 用来在 所有 JOIN 形成的结果集之后,对行进行过滤。

示例:

ini 复制代码
SELECT *
FROM orders o
JOIN customers c
  ON o.customer_id = c.id
WHERE c.country = 'US';
  • ON:定义两表如何连接

  • WHERE:在连接后的结果中,决定哪些行要保留

可以简单理解:

  • ON = "表之间的关系"

  • WHERE = "最终结果要满足的过滤条件"


2. 功能上的差异:ON vs WHERE

2.1 在 INNER JOIN 中:通常结果相同

对于 INNER JOIN,把某些条件写在 ONWHERE 中,很多时候得到的结果是一样的。

示例 1:过滤条件写在 WHERE

ini 复制代码
SELECT *
FROM orders o
INNER JOIN customers c
  ON o.customer_id = c.id
WHERE c.status = 'active';

示例 2:过滤条件写在 ON

ini 复制代码
SELECT *
FROM orders o
INNER JOIN customers c
  ON o.customer_id = c.id
 AND c.status = 'active';

对于 INNER JOIN,这两种写法在逻辑上通常是 等价的

  • 内连接只保留满足 ON 条件的行
  • 再用 WHERE 做进一步过滤
  • 把过滤条件合并进 ON,对内连接来说效果一样(仅限简单场景)

注意: 如果涉及多个表、复杂的 OR 条件等,ONWHERE 的组合可能影响逻辑。这里说的是简单场景下的常见等价性。


2.2 在 LEFT JOIN 中:差别非常关键

一旦使用外连接(LEFT / RIGHT / FULL OUTER JOIN),把条件放在 ON 还是 WHERE 就会 影响结果集结构

假设:

  • customers(左表):所有客户

  • orders(右表):客户的订单,status 可能是 'pending''complete''canceled'

场景:返回所有客户,以及他们的"已完成订单"(如果有)

A) 条件写在 ON 中(保留所有左表行)

ini 复制代码
SELECT c.id, c.name, o.id AS order_id, o.status
FROM customers c
LEFT JOIN orders o
  ON c.id = o.customer_id
 AND o.status = 'complete';

行为:

  • 每个客户 至少出现一次
  • 有已完成订单的客户:显示这些订单
  • 没有已完成订单的客户:照样显示,但 order_idstatusNULL

B) 条件写在 WHERE 中(连接后再过滤)

vbnet 复制代码
SELECT c.id, c.name, o.id AS order_id, o.status
FROM customers c
LEFT JOIN orders o
  ON c.id = o.customer_id
WHERE o.status = 'complete';

行为:

  • 先做左连接:所有客户都会出现,没订单的客户订单列为 NULL
  • 然后 WHERE o.status = 'complete' 会:
    • 去掉所有订单状态不是 complete 的行
    • 也会去掉 完全没有订单的客户 (因为他们的 o.statusNULL,不等于 'complete'

结果:这个查询实质上变成了"只保留有已完成订单的客户"的内连接风格。

核心规则:

  • 对于 可选数据(不一定存在) ,如果希望"主表行一定要保留",就要把该可选表的限制条件放在 ON 中。
  • 对于 必须存在的数据 ,用 WHERE 来过滤,把不符合条件的整行去掉。

2.3 RIGHT JOIN 和 FULL OUTER JOIN

同样的逻辑可以推广:

  • RIGHT JOIN,如果在 WHERE 中对右表列做过滤,会把右表为 NULL 的行去掉,从而可能变成"类似内连接"的效果。
  • FULL OUTER JOINWHERE 中的过滤会把任何一边为 NULL 的行去掉,破坏"全保留"的语义。

FULL OUTER JOIN 的典型写法(需注意不要随便直接在 WHERE 中过滤右表或左表为 NULL 的列):

sql 复制代码
SELECT ...
FROM A
FULL OUTER JOIN B
  ON A.id = B.id
WHERE (A.status = 'active' OR A.status IS NULL)
  AND (B.status = 'active' OR B.status IS NULL);

更常见的做法是:将只影响匹配关系的条件写在 ON 中,真正要"删行"的过滤写在 WHERE 中,避免破坏外连接的语义。


3. 性能方面的考虑

3.1 逻辑顺序 vs 实际执行顺序

SQL 是声明式语言:你写的是"要什么结果",而不是"怎么做"。数据库优化器可以在不改变语义的前提下重写你的 SQL。

关键点:

  • 对于 INNER JOIN
    • 优化器通常会把一些 WHERE 条件"推入"到 JOIN 里,或者把某些 ON 条件当作过滤来处理
  • 对于 OUTER JOIN
    • 优化器必须小心,不能随意把 ON/WHERE 条件互换,否则会改变结果

3.2 在 INNER JOIN 中的性能

对于内连接,如果在 ONWHERE 中放的条件在语义上是等价的,那么在多数数据库中,它们的执行计划和性能通常差不多,因为:

  • 优化器会重写这些谓词
  • 真正影响性能的是:
    • 连接键是否有索引
    • 过滤条件的选择性(过滤掉多少数据)
    • 是否有合适的索引
    • 统计信息是否准确

例如:

ini 复制代码
-- 写法1
SELECT *
FROM orders o
JOIN customers c
  ON o.customer_id = c.id
WHERE c.status = 'active';

-- 写法2
SELECT *
FROM orders o
JOIN customers c
  ON o.customer_id = c.id
 AND c.status = 'active';

在很多数据库中,这两条语句会生成同样的执行计划。

3.3 在 LEFT / RIGHT / FULL JOIN 中的性能

在外连接中,首先要保证 语义正确,然后再谈优化。

一般经验:

  • 想保留外连接的行为(主表行必须保留),又想减少右表的匹配行数时:
    • 应将右表的过滤条件放在 ON 中,这样既保留主表所有行,又能限制匹配的右表行数
  • 如果你本来就不需要未匹配的行(逻辑上就是内连接),那最好直接用 INNER JOIN,而不是 LEFT JOIN + WHERE 把行删掉:
    • 内连接通常更容易被优化器优化

3.4 索引与谓词位置

不管条件写在 ON 还是 WHERE 里,只要语义等价,影响性能的主要因素都是:

  • 连接字段、过滤字段是否有合适的索引

  • 筛选条件是否具有高选择性

  • 是否对列使用了函数(如 WHERE LOWER(name) = 'bob' 通常会让索引失效)

很多时候,与其纠结 ONWHERE 的性能差异,不如更关注:

  • 正确选择 JOIN 类型

  • 正确建立索引

  • 写出优化器更容易理解和重写的 SQL


4. 其他角度:可读性与常见模式

4.1 可读性和可维护性

为了让 SQL 更容易读懂和维护,通常建议:

  • ON 里写 表之间的关系
    • A.id = B.a_id
    • A.user_id = B.user_id
  • WHERE 里写 整体结果的过滤条件
    • A.is_deleted = 0
    • A.created_at > '2024-01-01'
    • A.country = 'US'

在使用外连接时,增加一个重要规则:

  • 限制"可选表(右表)能否构成匹配"的条件放在 ON 中:
    • AND B.status = 'active'
  • 限制"最终结果整行是否保留"的条件放在 WHERE 中:
    • WHERE A.country = 'US'

这样写,能清晰表达你的意图:

  • ON = "关联关系 + 可选表的匹配约束"

  • WHERE = "最终结果必须满足的条件"

也更不容易不小心把外连接"写成了"内连接。

4.2 常见模式示例

模式 1:所有主记录 +(可选的)已过滤从记录

需求:

"列出所有商品,以及它们正在生效的折扣(如有)。没有折扣的商品也要显示。"

正确写法:

css 复制代码
SELECT p.id, p.name, d.amount
FROM products p
LEFT JOIN discounts d
  ON d.product_id = p.id
 AND d.active = 1;
  • 限制 discounts 表中 active = 1 的条件写在 ON
  • 这样即使没有任何 active 折扣,商品也会被显示(折扣为 NULL)

错误写法(在本需求下):

css 复制代码
SELECT p.id, p.name, d.amount
FROM products p
LEFT JOIN discounts d
  ON d.product_id = p.id
WHERE d.active = 1;  -- 会去掉所有没有折扣的商品

这相当于"必须有一个 active 折扣才能显示商品",逻辑变了。

模式 2:只要那些拥有某种关联数据的主记录

需求:

"列出所有至少有一张已完成订单的客户。"

可以这样写:

vbnet 复制代码
SELECT DISTINCT c.id, c.name
FROM customers c
JOIN orders o
  ON o.customer_id = c.id
WHERE o.status = 'complete';

或者这样写(对 INNER JOIN 逻辑等价):

ini 复制代码
SELECT DISTINCT c.id, c.name
FROM customers c
JOIN orders o
  ON o.customer_id = c.id
 AND o.status = 'complete';

因为本来就只要"有完成订单的客户",所以内连接是合理的,放在 ON 还是 WHERE 差别不大。


5. ON vs WHERE 对比小结

角度 ON 子句 WHERE 子句
主要作用 定义表之间如何连接(匹配关系) 在连接完成后,过滤最终结果行
逻辑评估阶段 JOIN 过程中 JOIN 之后
在 INNER JOIN 中 对过滤条件来说,通常可以和 WHERE 互换,结果相同 对过滤条件来说,通常可以和 ON 互换,结果相同
在 OUTER JOIN 中 能限制匹配记录但保留外表行(不会轻易"变成内连接") 对右表(或外表)的过滤可能把 NULL 行删掉,从而变成内连接行为
典型内容 连接键关系、匹配逻辑、可选表的限制条件 整体结果的过滤条件、全局逻辑限制
性能 对内连接与 WHERE 大多等价;外连接需先保证语义正确 对内连接与 ON 大多等价;外连接滥用会破坏语义
可读性建议 表达"表之间怎么连、连哪些行" 表达"最终结果要留下哪些行"

6. 实战建议

  • 先考虑正确性,再考虑性能。
    弄清楚自己要的是内连接还是外连接:
    • 如果不在乎未匹配行 → 用 INNER JOIN
    • 如果必须保留某一边所有记录 → 用 LEFT / RIGHT / FULL JOIN
  • ON 表达表之间的关系以及可选表的匹配限制。
    尤其在外连接中:
    • 右表的条件如果写在 WHERE,很容易把未匹配的行删掉
  • WHERE 表达最终结果的过滤条件。
    比如业务约束:国家、时间、状态等全局条件。
  • 对 INNER JOIN 而言,ONWHERE 常常结果一样,但请保持一致风格以提升可读性。
  • 遇到 LEFT JOIN 却在 WHERE 中过滤右表字段时,要特别警惕。
    • 如果 WHERE 条件要求右表字段非空或等于某值,往往在逻辑上已经不再是"左连接"了
    • 这是非常常见的 bug 来源
相关推荐
绝无仅有1 小时前
PHP与Java项目在服务器上的对接准备与过程
后端·面试·架构
四七伵1 小时前
数据库必修课:MySQL金额字段用decimal还是bigint?
数据库·后端
彭于晏Yan2 小时前
LangChain4j实战三:图像模型
java·spring boot·后端·langchain
SimonKing2 小时前
跨越数据孤岛!SpringBoot使用JDBC调用Calcite联邦查询实战
java·后端·程序员
Java编程爱好者2 小时前
金融级数据库架构实战:MySQL Router + MGR 深度指南
后端
Java编程爱好者2 小时前
Java后端开发面试题总结(全网最全、最细、附答案)
后端
Java水解2 小时前
Spring应用事件机制实践
后端·spring
feathered-feathered2 小时前
测试实战【用例设计】自己写的项目+功能测试(1)
java·服务器·后端·功能测试·jmeter·单元测试·压力测试
Sincerelyplz3 小时前
【WebSocket】消息丢失的补偿/补发机制
后端·websocket