ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

自然连接与等值连接的区别:SQL JOIN隐式条件与生产环境避坑指南

自然连接与等值连接的区别:SQL JOIN隐式条件与生产环境避坑指南 有一次做代码评审看到一条NATURAL JOIN的 SQL同事得意地说这写法够简洁。我盯着那行代码问了一句那你知道这两张表现在有几个同名公共列吗他一下子愣住了。后来这条 SQL 在测试环境查出来的数据少了一大批排查了很久才发现多出来的那个公共列把本该匹配上的行全过滤掉了。自然连接和等值连接这两个概念在教科书里占的篇幅不大但很多人从入门写 SQL 到工作好几年始终没完全分清。有人把自然连接当成等值连接的别名有人以为NATURAL JOIN一定安全还有人压根没见过这个关键字。这篇就把两者的定义、SQL 写法、结果集差异、执行计划特征以及生产环境里的坑一次说透适合正在学数据库原理的学生、准备面试的开发者以及每天和数据打交道的数仓工程师。1. 先理清概念连接运算的谱系与自然连接的准确定义1.1 从笛卡尔积到 theta 连接再到等值连接关系代数里的一切连接操作起点都是笛卡尔积。两个表做笛卡尔积就是把左边每一行和右边每一行全部配对结果是 |R| × |S| 行。这个结果绝大多数行没有业务意义所以要用条件去过滤。theta 连接是上层概念先做笛卡尔积再按照给定的条件 θ 挑出满足条件的元组。θ 可以是任意比较表达式比如R.a S.b、R.a S.b AND R.c S.c它是连接操作最一般的形态。等值连接就是 θ 条件里全部使用的连接。这里有个关键点很容易被忽略等值连接只要求条件里是等号并不要求连接列同名。R.dept_id S.dept_id是等值连接R.user_id S.owner_id同样是等值连接。列名是否一致和等值连接没有必然关系。自然连接比等值连接更进一步。它连连接条件都省了系统自动找出左右两张表所有同名且类型兼容的列让它们全部相等然后在结果里把同名列合并成一个。关系代数中写作 R ⋈ S。1.2 自然连接的完整定义拆成三步才看得透自然连接的数学定义看起来只有一行但理解它必须拆开找出公共属性集合 C attrs(R) ∩ attrs(S)。如果 C 为空自然连接就退化为笛卡尔积。做 R 与 S 的笛卡尔积然后选择所有公共属性值相等的元组也就是同时对 C 里的每个列做等值匹配。去掉右边重复的公共属性列把同名列合并为一列也就是投影去重。第三步是自然连接和普通等值连接最本质的差异。等值连接的结果会把左右两边的连接列都保留下来比如R.dept_id和S.dept_id各留一份自然连接则合并成一份dept_id。所以只看结果列数自然连接等于「左表列数 右表列数 - 公共列数」。这四类操作的关系可以用表格概括操作是否做笛卡尔积是否有显式条件条件是否必须为等号是否合并同名列笛卡尔积是无不适用否theta 连接是有否任意比较否等值连接是有是否自然连接是无自动推导是针对所有公共列是1.3 为什么「自然连接一定是等值连接」这话对但不完全对自然连接在语义上确实是标准的等值连接而且是多列等值连接但它和用户手写的等值连接有两点重要区别连接条件不是显式写出来的而是靠列名自动推导结果集中公共列做了合并。这两点看起来只是表达方式不同实际却会导致完全不同的查询结果和使用风险。举个例子能很快明白。假设employees表和departments表同时拥有dept_id和join_date两个同名列如果用户心里的连接意图只是「员工属于哪个部门」也就是只按dept_id关联那么自然连接会自动把join_date也拉进连接条件变成双条件等值连接。这个额外的条件是用户没想到的数据因此减少都浑然不觉。这就是自然连接在实践中容易被诟病的根源。2. SQL 实操ON、USING、NATURAL JOIN 三种写法的结果差异2.1 构造一个能看出问题的员工-部门例子光讲理论容易晕直接建两张表实测。我设计一个刻意带「坑」的模型两张表除了业务上的外键列dept_id之外还都有一个join_date列但语义完全不同——员工表里是入职日期部门表里是部门成立日期。CREATE TABLE employees ( emp_id INT PRIMARY KEY, dept_id INT, name VARCHAR(50), join_date DATE ); CREATE TABLE departments ( dept_id INT PRIMARY KEY, dept_name VARCHAR(50), join_date DATE ); INSERT INTO employees VALUES (1, 101, 张三, 2020-01-15), (2, 102, 李四, 2019-03-01), (3, 101, 王五, 2021-07-20); INSERT INTO departments VALUES (101, 研发部, 2020-01-15), (102, 市场部, 2019-03-01);这个例子里的数据特意让张三、李四的入职日期和部门成立日期一致王五不一致。这样三种写法的差异会立刻暴露出来。2.2 三种写法的结果集逐列对比等值连接最常见的就是JOIN ... ON ...连接条件完全显式SELECT e.emp_id, e.dept_id, e.name, e.join_date, d.dept_name, d.join_date AS dept_join_date FROM employees e JOIN departments d ON e.dept_id d.dept_id;按dept_id连接王五确实属于研发部所以返回 3 行。结果中dept_id本质上有两份一份来自 employees一份来自 departments只是因为投影时选择了一个别名覆盖。如果不做投影直接SELECT *两个dept_id和两个join_date都会出现在结果集里共 7 列。USING是自然连接和等值连接之间的折中方案连接列靠用户指定但结果会合并该列SELECT * FROM employees JOIN departments USING (dept_id);这里同样只按dept_id连接返回 3 行。dept_id被合并成一份但由于join_date没有在 USING 里指定它仍然是两份。ASSELECT *的结果列数为 6emp_id, dept_id, name, join_date, dept_name, join_date。很多人以为 USING 会把所有同名列都合并这是错的它只合并明确指定的列。自然连接写起来最省事SELECT * FROM employees NATURAL JOIN departments;结果只有 2 行——张三和李四因为入职日期碰巧和部门成立日期一致而匹配上王五明明在研发部却因为join_date不同被过滤掉。结果列数为 5emp_id, dept_id, name, join_date, dept_name两个同名列都合并了。三种写法差异汇总写法连接条件结果行数结果列数dept_id 是否合并join_date 是否合并JOIN ... ON e.dept_id d.dept_id显式只按 dept_id37否否JOIN ... USING (dept_id)显式只按 USing 指定列36是否NATURAL JOIN隐式按所有公共列25是是同一个业务问题三种写法三个结果这就是自然连接最需要警惕的地方。它确实简洁但简洁的代价是把连接条件交给了数据库的列名匹配规则。2.3 各数据库对 NATURAL JOIN 的支持情况NATURAL JOIN是 SQL 标准里的语法但实际生产环境差异不小。根据我接触过的数据库整理如下数据库NATURAL JOINJOIN USING备注Oracle支持支持老版本就很稳定PostgreSQL支持支持EXPLAIN 能清晰看到连接条件MySQL支持支持8.0 仍保留该语法MariaDB支持支持语法兼容 MySQLSQLite支持支持轻量库也实现了SQL Server不支持不支持T-SQL 只能用 ON 显式表达SQL Server 完全不支持NATURAL JOIN和JOIN USING所以微软系技术栈的开发者日常几乎接触不到自然连接。这也解释了为什么不同公司出来的人对自然连接的熟悉程度差异极大。如果你在 SQL Server 里看到一个被注释掉的NATURAL JOIN大概率是从其他数据库迁移过来的代码需要人工改写成JOIN ... ON ...。3. 生产环境真正踩坑的点隐式公共列、笛卡尔积回退与模式演化3.1 多个公共列时自动推导的多重等值条件上面那个员工-部门例子已经展示了多公共列的危害。只要两张表有第二个同名同类型列自然连接就会自动把它变成连接条件的一部分。问题在于数据库不知道这两个列语义是否一致它只看列名和类型。现实业务模型里两张表同时拥有created_at、updated_at、status这类公共列几乎是常态。审计字段和状态字段是建模时最容易被复制粘贴的列而它们在语义上完全不应该参与表关联。一旦有人用了NATURAL JOIN这些无辜的列全部变成连接条件结果集急剧缩水有时候甚至查出来 0 行。我接手过一个数据修复任务线上报表某天起突然少了几十万行最后定位到原因就是有人在公共层视图里改用了NATURAL JOIN而源表刚好新增了一个source_system公共列两边取值规则还不完全一致。这种错误极其隐蔽因为 SQL 没有报错结果看起来也是正常的关联查询结果只是行数变少了。3.2 没有公共列时自然连接直接退化为笛卡尔积自然连接的另一面同样危险当两张表没有任何同名列时公共属性集 C 为空系统没有可比对的条件结果就是纯粹做笛卡尔积。左表 10 万行右表 5 万行一条NATURAL JOIN下去直接产生 50 亿行中间结果应用基本卡死数据库负载飙升。这种情况在列名设计不规范的系统里特别容易发生。比如 employees 表里外键叫emp_dept_iddepartments 表里主键叫dept_id语义明明关联列名却对不上。写NATURAL JOIN的人压根没意识到两张表没有公共列于是得到一个莫名膨胀的结果集。代码评审时看到NATURAL JOIN我第一反应永远是先去查两张表的列交集。3.3 加列不加 SQL 的「隐式改语义」问题这是自然连接最阴险的一个坑上游表结构发生变更下游 SQL 一行都不用改语义却悄悄变了。项目初期employees 和 departments 只有dept_id一个公共列NATURAL JOIN工作正常。半年后需求方要求两个表都记录创建时间DBA 给两张表各加了created_at列。此时旧的NATURAL JOINSQL 自动从单条件变成双条件连接代码没有任何变更运行结果却变了。而且由于历史数据导入时created_at往往是同一天的批量时间戳部分行碰巧匹配部分行不匹配结果时对时错比稳定报错更难排查。显式写JOIN ... ON e.dept_id d.dept_id则完全不同上游加列不会影响连接条件SQL 行为保持稳定。这也是很多大厂把NATURAL JOIN列入禁用语法清单的核心原因——它破坏了 SQL 的可预测性。查询结果不应该依赖两张表的列名交集发生变化而自然连接恰好把正确性建立在了这种脆弱的基础上。3.4 外连接场景下更隐蔽的 NULL 故障内连接丢行还能通过行数对比发现外连接下的自然连接问题更隐蔽。把前面的查询改成左外连接SELECT * FROM employees e NATURAL LEFT JOIN departments d;王五因为join_date不匹配会被保留下来但dept_name和部门的join_date变成 NULL。用户看到的结果是这个员工确实出现了只是部门信息显示为空。乍一看像是数据缺失实际上问题出在连接条件多了join_date这一项。等值连接里 NULL 值本身也不会参与匹配自然连接里公共列上任何一方的 NULL 都会让该行在结果中失去另一半信息外连接只是把这种丢失从「丢行」变成了「丢列」而已。3.5 隐式连接的本质正确性绑定在列名语义上回过头来看自然连接设计初衷是好的在理想的范式化模型里同名同义的连接列是一种规范开发者的意图可以直接通过列名传达。但在真实系统里数据模型是长期演化的结果命名不统一、审计字段复制粘贴、模块间列名撞车这些情况远远多于理想的同名同义情形。自然连接把正确性押注在列名的唯一性上而列名恰恰是整个数据链路里最不稳定、最容易被复制的东西。所以我在代码评审时的标准很简单看到NATURAL JOIN先查两张表的全列再逐个核对公共列的语义。如果公共列超过一个或者存在宽表直接要求改成显式JOIN ON。这不是教条是踩过坑之后的应激反应。4. 执行计划视角同样的等值条件为什么自然连接更难掌控4.1 优化器眼中的自然连接和等值连接从数据库优化器的角度自然连接被解析后本质上就是多个等值条件的连接查询。PostgreSQL 里执行EXPLAIN查看一条NATURAL JOIN看到的节点和手写多条件JOIN ON几乎一模一样。也就是说自然连接并不会因为写法高级就获得性能优势也不会因为写法笨重就更慢。执行计划的差异完全由解析出来的连接条件和统计信息决定。理解了这一点就能明白「自然连接有性能问题」这个说法其实不太准确。真正的问题在于自然连接自动生成的那些额外连接条件可能落在低区分度、无索引、或者统计信息完全不可用的列上从而导致优化器选择了糟糕的关联策略。你无法通过改写条件去干预它因为条件根本不是你写的。4.2 看执行计划时重点盯住什么不管用 PostgreSQL 的EXPLAIN还是 MySQL 的EXPLAIN只要查询里存在连接我一般重点看三件事第一是关联方式。常见的 Nested Loop、Hash Join、Merge Join 各有适用场景。Nested Loop 适合小表驱动大表且连接列有索引Hash Join 适合无索引或等值连接大表Merge Join 依赖有序输入。自然连接解析出来的多条件如果是低区分度列优化器可能选 Hash Join对内存压力很大。第二是连接条件是否命中索引。等值连接是最适合走索引的场景前提是连接列上建立了合适的索引并且选择性足够好。自然连接自动把created_at这类列加进条件后这个条件往往既没有索引支持选择性也很差等于凭空给优化器增加了噪音。第三是估算行数是否合理。EXPLAIN 里的 rows 是成本优化的核心输入如果估算误差太大关联顺序和关联方式都可能跑偏。自然连接引入的隐式条件常常让估算变得更困难尤其是在复合条件里某列的数据分布极度不均匀时。我曾见过一张表 90% 的数据集中在同一个日期自然连接自动把这个日期列加入条件后优化器严重低估了行数选了大错特错的执行路径。给一个最简单的观察方法-- PostgreSQL EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM employees NATURAL JOIN departments; -- MySQL EXPLAIN SELECT * FROM employees NATURAL JOIN departments\G看到 join 条件里出现你没预期的列基本可以判定自然连接在搞鬼。4.3 行数膨胀的真实逻辑与控制手段连接列如果不唯一行数会成倍放大这一点和自然连接还是等值连接无关但自然连接会加剧这种失控。因为自动条件的列往往是两张表的公共审计字段这些字段的可区分度很低。假设某个公共列在右表的重复因子是 10那么理论上匹配后的行数会是左表匹配行数的 10 倍而不是 1 倍。控制行数膨胀的手段无非三种一是确保连接条件里有唯一键约束尤其是子表外键对应父表主键的场景二是在连接之前通过 WHERE 条件缩减一侧的表降低参与关联的基数三是检查连接列是否为低区分度必要时给相关列建索引或者干脆移除该连接条件。换成显式JOIN ON后这些控制手段才真正掌握在开发者手里因为你确确实实知道连接列是哪一个。5. 区分要点与答题模板从面试到日常评审5.1 一张表理清列数和行数的变化规律经常有人问自然连接和等值连接到底差在哪用下面这张表可以快速回答维度等值连接自然连接连接条件来源用户显式指定自动取所有同名同类型列连接条件是否清晰清晰可见隐式推导需要查表结构确认连接列是否必须同名否R.a S.b 也可以是必须公共列名一致结果列数左表列数 右表列数左表列数 右表列数 - 公共列数公共列合并不合并各保留一份合并为一列无公共列报错或需要显式条件退化为笛卡尔积上游加列后行为稳定不变可能自动改变连接条件行数方面没有绝对公式。如果公共列在某一侧是主键或唯一键自然连接和等值连接的行数通常一致如果公共列在两侧都不是唯一键两边都会出现行数膨胀。空值不会被等值匹配这也是两种连接共同的行为。5.2 面试常问五题与解析面试官问「自然连接和等值连接的区别」最优回答不是背诵定义而是分三层讲清楚第一层是关系代数定义自然连接是自动选取公共属性和合并列第二层是 SQL 语义差异自然连接把列名当作连接条件来源第三层是实践风险自然连接引入隐式条件可能因为 schema 变更导致结果变化。能把这三层讲完面试基本稳了。几个常见自测题题目 1表 A(a, b, c, d)表 B(b, c, e)A 与 B 做自然连接结果有几列答案5 列。A 有 4 列B 有 3 列公共列是 b、c 两个4 3 - 2 5。题目 2表 A(a, x)表 B(b, y)两表没有同名列A NATURAL JOIN B 的结果是什么答案等价于笛卡尔积。没有公共属性时自然连接无等值条件可选所有行之间两两配对。题目 3等值连接一定要求连接列同名吗答案不要求。等值连接只要条件是等号A.id B.owner_id完全合法且常见。自然连接则要求同名列。题目 4自然连接一定是等值连接吗答案是但它是特殊的等值连接自动指定所有公共列相等并在结果中合并这些列。和用户手写的多条件等值连接相比区别在于条件和列的显式性。题目 5如果公共列上存在 NULL自然连接会怎么处理答案NULL 与任何值比较结果都是 NULL 或 FALSE无法满足等值条件因此内连接会丢弃含 NULL 的行外连接则保留一侧并将另一侧置为 NULL。5.3 生产环境选型建议实践中的选择原则我总结为三条默认使用JOIN ... ON ...并在 SELECT 中显式列出所需列。这是最稳妥的做法连接条件一目了然任何 DBA 和同事都能看懂。需要避免SELECT *因为等值连接产生的同名列会造成读取混乱比如 JDBC 里按列名取值时不知道取的是哪一份。希望合并输出列时用JOIN ... USING (col)。USING 比自然连接多一个好处连接列是显式指定的不会被隐式公共列绑架。如果你确实希望结果里连接列只出现一次USING 是更可控的选择。NATURAL JOIN仅在完全受控的建模环境中使用。例如两张表由同一套规范管理公共列只有真正的主外键且 schema 变更流程严格禁止随意加列。即便如此大多数团队依然选择禁用因为它把正确性押注在列名管理上这不是一个值得冒的风险。我在实际项目里遇到老 SQL 中的NATURAL JOIN改造步骤一般是先查两张表的公共列明确自然连接到底生成了哪些连接条件然后原样改写成显式JOIN ON多条件形式最后用EXPLAIN对比改造前后的执行计划。这样既不改变原 SQL 的结果又把隐式逻辑变成了显式代码。查询公共列的方法很简单-- PostgreSQL SELECT attname FROM pg_attribute WHERE attrelid employees::regclass AND attnum 0 AND NOT attisdropped INTERSECT SELECT attname FROM pg_attribute WHERE attrelid departments::regclass AND attnum 0 AND NOT attisdropped; -- MySQL SELECT column_name FROM information_schema.columns WHERE table_schema your_db AND table_name employees INTERSECT SELECT column_name FROM information_schema.columns WHERE table_schema your_db AND table_name departments;我个人现在对NATURAL JOIN的态度很简单遇到就改。不是因为它不能用而是因为它把最重要的连接条件藏了起来等于把正确性交给运气和列名设计。作为工程师我更愿意把连接逻辑明明白白放在代码里让下一个读这条 SQL 的人不用靠猜一眼就知道它在关联什么以及为什么这么关联。
返回列表