在写 SQL JOIN 时,ON 和 WHERE 经常一起出现,看起来有点像可以互换。但它们的作用并不相同,尤其在使用外连接(LEFT / RIGHT / FULL JOIN)时,放错地方甚至会让结果完全变样。
本文从以下几个方面解释:
JOIN、ON、WHERE各自负责什么- 它们在功能上的差异(特别是
LEFT / RIGHT JOIN) - 对性能的影响
- 可读性、可维护性及常见坑
示例使用标准 SQL,大部分数据库(MySQL、PostgreSQL、SQL Server、Oracle 等)都适用。
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,把某些条件写在 ON 或 WHERE 中,很多时候得到的结果是一样的。
示例 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 条件等,ON 和 WHERE 的组合可能影响逻辑。这里说的是简单场景下的常见等价性。
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_id、status为NULL
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.status为NULL,不等于'complete')
结果:这个查询实质上变成了"只保留有已完成订单的客户"的内连接风格。
核心规则:
- 对于 可选数据(不一定存在) ,如果希望"主表行一定要保留",就要把该可选表的限制条件放在
ON中。 - 对于 必须存在的数据 ,用
WHERE来过滤,把不符合条件的整行去掉。
2.3 RIGHT JOIN 和 FULL OUTER JOIN
同样的逻辑可以推广:
- 对
RIGHT JOIN,如果在WHERE中对右表列做过滤,会把右表为NULL的行去掉,从而可能变成"类似内连接"的效果。 - 对
FULL OUTER JOIN,WHERE中的过滤会把任何一边为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 中的性能
对于内连接,如果在 ON 和 WHERE 中放的条件在语义上是等价的,那么在多数数据库中,它们的执行计划和性能通常差不多,因为:
- 优化器会重写这些谓词
- 真正影响性能的是:
-
- 连接键是否有索引
- 过滤条件的选择性(过滤掉多少数据)
- 是否有合适的索引
- 统计信息是否准确
例如:
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'通常会让索引失效)
很多时候,与其纠结 ON 和 WHERE 的性能差异,不如更关注:
-
正确选择 JOIN 类型
-
正确建立索引
-
写出优化器更容易理解和重写的 SQL
4. 其他角度:可读性与常见模式
4.1 可读性和可维护性
为了让 SQL 更容易读懂和维护,通常建议:
ON里写 表之间的关系:-
A.id = B.a_idA.user_id = B.user_id
WHERE里写 整体结果的过滤条件:-
A.is_deleted = 0A.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 而言,
ON和WHERE常常结果一样,但请保持一致风格以提升可读性。 - 遇到
LEFT JOIN却在WHERE中过滤右表字段时,要特别警惕。 -
- 如果
WHERE条件要求右表字段非空或等于某值,往往在逻辑上已经不再是"左连接"了 - 这是非常常见的 bug 来源
- 如果