USING / NATURAL JOIN 与 ON 的区别?为什么大多数生产环境不推荐 NATURAL JOIN?
What is the difference between USING / NATURAL JOIN and ON? Why is NATURAL JOIN not recommended in most
考察要点
这道题面试官想看你是否深入理解了 SQL 中不同 JOIN 写法的语义差异、边界条件和工程实践中的取舍。核心考察点包括:
- 语法精确性:你是否清楚
ON,USING,NATURAL JOIN在指定连接条件上的显式与隐式差异。 - 结果集结构:你是否知道这三种写法对连接列(Join Key)在最终输出结果中的处理方式是不同的。
- 代码健壮性与可维护性:你是否能从软件工程角度,分析出为什么隐式约定(
NATURAL JOIN)在多人协作和需求变更频繁的生产环境中是高风险的。
核心概念
ON, USING, NATURAL JOIN 都是用于定义两个表如何连接的子句,但它们的显式程度、灵活性和对结果集的影响各不相同。
我们使用下面两张表作为示例:
employees 表
1CREATE TABLE employees (2 emp_id INT PRIMARY KEY,3 emp_name VARCHAR(50),4 dept_id INT,5 location_id INT6);7INSERT INTO employees VALUES8(101, 'Alice', 10, 1),9(102, 'Bob', 20, 2),10(103, 'Charlie', 10, 1);
departments 表
1CREATE TABLE departments (2 dept_id INT PRIMARY KEY,3 dept_name VARCHAR(50),4 location_id INT5);6INSERT INTO departments VALUES7(10, 'Engineering', 1),8(20, 'Human Resources', 2),9(30, 'Sales', 3);
1. ON 子句
ON 是最通用、最显式、功能最强大的连接条件。
-
语法:
... JOIN table2 ON table1.column_name = table2.column_name -
特点:
- 显式指定:必须明确写出完整的连接条件,包括表名和列名。
- 高度灵活:连接列可以有不同名称(
ON e.dept_id = d.department_number),可以使用非等值连接(ON e.salary > s.min_salary),也可以包含复杂的AND/OR逻辑。 - 结果集:连接列会分别出现在结果集中。如果列名相同,需要使用表别名来区分。
-
示例:
1SELECT *2FROM employees e3JOIN departments d ON e.dept_id = d.dept_id;
- 输出:注意
dept_id和location_id列都出现了两次。 | emp_id | emp_name | e.dept_id | e.location_id | d.dept_id | dept_name | d.location_id | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | 101 | Alice | 10 | 1 | 10 | Engineering | 1 | | 102 | Bob | 20 | 2 | 20 | Human Resources | 2 | | 103 | Charlie | 10 | 1 | 10 | Engineering | 1 |
2. USING 子句
USING 是当连接列在两个表中名称完全相同时,ON 的一种简写形式。
-
语法:
... JOIN table2 USING (column_name1, column_name2) -
特点:
- 前提:连接列在两个表中必须同名。
- 简洁:只需列出同名的连接列即可,比
ON更简洁。 - 结果集:连接列在结果集中只出现一次,自动合并,无需使用别名区分。
-
示例:
1SELECT *2FROM employees3JOIN departments USING (dept_id);
- 输出:注意
dept_id只出现一次。location_id因为没有在USING中指定,所以仍然是两列。 | dept_id | emp_id | emp_name | location_id | dept_name | location_id | | :--- | :--- | :--- | :--- | :--- | :--- | | 10 | 101 | Alice | 1 | Engineering | 1 | | 20 | 102 | Bob | 2 | Human Resources | 2 | | 10 | 103 | Charlie | 1 | Engineering | 1 |
3. NATURAL JOIN
NATURAL JOIN 是一种更彻底的简写,它会自动连接两个表中所有同名的列。
-
语法:
... NATURAL JOIN table2 -
特点:
- 完全隐式:无需指定任何连接列,数据库会自动寻找两个表中所有名称相同的列作为连接条件。
- 极度简洁:写法最简单。
- 结果集:所有同名的连接列在结果集中都只出现一次。
- 高风险:这是其在生产环境中不被推荐的核心原因。
-
示例:
1SELECT *2FROM employees3NATURAL JOIN departments;
- 分析:
employees和departments表有dept_id和location_id两个同名列。因此,NATURAL JOIN等价于... ON e.dept_id = d.dept_id AND e.location_id = d.location_id。 - 输出: | dept_id | location_id | emp_id | emp_name | dept_name | | :--- | :--- | :--- | :--- | :--- | | 10 | 1 | 101 | Alice | Engineering | | 20 | 2 | 102 | Bob | Human Resources | | 10 | 1 | 103 | Charlie | Engineering |
为什么不推荐 NATURAL JOIN?
核心原因:隐式约定导致代码脆弱,极易在未来维护中引入不易察觉的 BUG。
假设 DBA 为了记录修改时间,给 employees 和 departments 两张表都增加了一个名为 last_updated 的字段。
1ALTER TABLE employees ADD COLUMN last_updated TIMESTAMP;2ALTER TABLE departments ADD COLUMN last_updated TIMESTAMP;
此时,原有的 NATURAL JOIN 查询的逻辑就被隐式地改变了。它的连接条件会从原来的:
ON e.dept_id = d.dept_id AND e.location_id = d.location_id
悄悄地变成:
ON e.dept_id = d.dept_id AND e.location_id = d.location_id AND e.last_updated = d.last_updated
这个新增的 last_updated 条件几乎不可能满足,将导致查询返回空集或错误的结果,而代码本身一行未改,这使得问题排查变得异常困难。ON 和 USING 则完全不受此影响,因为它们的连接条件是显式声明的。
常见低分回答
- 混淆
USING和NATURAL JOIN:“NATURAL JOIN就是USING的简写,差不多。” —— 错误。USING让你显式选择哪些同名列用于连接,而NATURAL JOIN是自动使用所有同名列,这是本质区别。 - 不清楚结果集列的差异:“它们都能连表,功能一样。” —— 忽略了
ON会保留两个表的连接列,而USING/NATURAL JOIN会合并它们。这个差异在SELECT *或后续操作中至关重要。 - 对
NATURAL JOIN的风险解释不清:“NATURAL JOIN不好,因为不清晰。” —— 太笼统。高分回答必须能举出类似“增加updated_at或version公共字段导致连接条件隐式改变”的具体工程灾难场景。 - 误认为有性能差异:“
ON更灵活,所以性能可能更好/更差。” —— 错误。对于等值连接,JOIN ON t1.id = t2.id和JOIN USING(id)在现代数据库优化器下生成的执行计划通常是完全相同的。它们的区别在于SQL语义和可维护性,而非性能。 - 忽略
SELECT *陷阱:当使用ON时,SELECT *会产生重复的列名,这可能导致依赖列名的应用层代码(如 ORM 框架)出错或产生歧义。虽然USING能避免,但在生产代码中应始终避免SELECT *,明确列出所需字段。
工程实践与优化
- 编码规范:强制使用
ON。这是最明确、最无歧义、最健壮的写法。代码的读者无需检查表结构就能理解连接逻辑。 - 可维护性:
ON将连接逻辑和业务逻辑(WHERE过滤)清晰地分离。NATURAL JOIN将连接逻辑隐藏在表结构中,是高耦合的坏味道。 - 调试效率:当
JOIN结果不符合预期时,使用ON的查询,问题几乎总是在ON子句本身。而NATURAL JOIN则需要你去DESC table1,DESC table2,然后人肉比对所有列名,才能找出实际生效的连接条件,效率低下。 - 执行计划分析:虽然语法不同,但
EXPLAIN一个... JOIN ... ON t1.id = t2.id和... JOIN ... USING(id)的查询,你会发现它们的type,key,rows,Extra等核心指标通常完全一致。优化器在解析完 SQL 后,会生成相同的内部连接逻辑。这证明了它们的区别不在于执行性能。 USING的有限场景:在一些设计极度规范的数据仓库(DW)环境中,事实表和维度表的连接键(Surrogate Key)命名完全一致,USING可以作为一种提升代码简洁度的选择。但即便如此,ON的明确性依然更具优势。
常见追问与延伸题目
-
追问:既然
NATURAL JOIN这么危险,那 SQL 标准为什么要设计它?- 回答要点:它的设计初衷是为了极致的简洁,特别是在交互式查询和某些理论模型中。当时的设计者可能低估了在大型、长期演进的软件工程项目中,这种隐式约定会带来的巨大风险。可以把它看作一个理想化设计与工程现实冲突的典型例子。在个人做数据探索、一次性分析时,如果对表结构了如指掌,用它来少打字也未尝不可,但绝不能进入生产代码库。
-
追问:
SELECT *在JOIN查询中有什么问题?USING在这方面比ON有优势吗?- 回答要点:
- 问题:在生产代码中
SELECT *是反模式。1)它会查询不必要的列,增加网络IO和内存消耗。2)当表结构变更(如增删列)时,依赖列顺序或列数量的客户端代码可能会崩溃。3)对于JOIN ... ON,SELECT *会导出重名列,导致应用层(如Java的ResultSet.getString("id"))产生歧义或直接报错。 USING的优势:JOIN ... USING(id)通过合并连接列,避免了SELECT *时的列名重复问题,输出结果更干净。这是一个微小的优势,但远不足以抵消ON在明确性和灵活性上的巨大好处。最佳实践永远是:使用ON进行连接,并明确指定SELECT的列清单。
- 问题:在生产代码中
- 回答要点:
-
追问:在
LEFT JOIN中,把过滤条件放在ON子句和WHERE子句有什么本质区别?- 回答要点:这是一个非常经典且重要的区分点,能体现你对
OUTER JOIN的理解深度。- 条件在
ON子句:... LEFT JOIN t2 ON t1.id = t2.id AND t2.status = 'active'。这里的t2.status = 'active'是作为连接条件的一部分。它在匹配t2表时生效。如果t2的某行满足id匹配但status不匹配,这行t2记录被视为“不存在”,但t1的记录依然会保留,t2对应的列填充为NULL。 - 条件在
WHERE子句:... LEFT JOIN t2 ON t1.id = t2.id WHERE t2.status = 'active'。这里的WHERE子句在LEFT JOIN完成后对整个结果集进行过滤。对于那些在t2中没有匹配项的t1行,t2.status的值是NULL。WHERE t2.status = 'active'会将这些NULL的行过滤掉,这导致LEFT JOIN的“保留左表所有行”的特性失效,实质上等价于一个INNER JOIN。这是新手极易犯的错误。
- 条件在
- 回答要点:这是一个非常经典且重要的区分点,能体现你对