ARTICLE DETAIL

资讯详情

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

自然连接与等值连接:SQL查询中两种JOIN方式的本质差异与选型

自然连接与等值连接:SQL查询中两种JOIN方式的本质差异与选型 1. 两种连接的前世今生先看懂问题在哪自然连接和等值连接这对名词在关系代数里是老邻居了但很多人第一次见到它们是在一门叫“数据库原理”的课上。上课时候老师会画两个大圆中间有个阴影区说“这就是连接”当时听懂了下来自己写SQL却开始迷糊。原因很简单概念层面它们长得非常像都是拿两个表里的某些列做匹配匹配结果放在一行里但一到执行层面自然连接多做的那些隐性操作经常让人在不该出问题的地方踩坑。在关系代数这套理论中自然连接的定义是先比较两个关系中所有同名列的值然后把值相等的行拼在一起最后把这些同名列合并成单列输出。等值连接的定义则宽松得多——它只是拿一个“等值条件”去做筛选比如A.x B.y条件成立就拼接条件不成立就丢弃。你要用SELECT *去看等值连接的结果时那两个用于匹配的列会同时出现在结果集里数据显得冗余但从执行器角度来看这反而是最自然的事因为你明确告诉它“我要用这两列相等来连接”它不会擅自合并任何列。这个差异在纸上写写很容易但实际到了项目里表现出的困惑却相当真实为什么NATURAL JOIN在某些表结构下不报错结果却明显不对为什么两个名字不同但业务意义相同的列就只能用ON来写等值连接为什么同一个查询改成自然连接后速度会变快或变慢这些问题的答案都指向同一件事——你并没有完全掌握这两种连接在语义上和实现上的边界。写代码这么多年我的经验是自然连接和等值连接的确是同一棵树上结出的两个果子但你连树的生长方式都不了解摘到烂果子的概率会非常高。这篇内容会把两者的定义、执行机制、SQL写法和工程踩坑逐一拆开讲并配合可以直接跑的实验来验证。适合正在学数据库理论的人、准备面试的开发者以及写了很多年SQL却从来没仔细抠过连接内部机制的人。2. 核心差异逐个拆开看定义、投影与去重2.1 自然连接在数学定义上的“隐藏操作”先看形式化表达。给定两个关系R和S自然连接记为R ⋈ S它的结果由三步操作组成笛卡尔积、筛选、投影。笛卡尔积就是把R的每一行和S的每一行两两组合组合之后一张表有多少行取决于两个表的行数乘积可能在数据量稍大时瞬间膨胀成天文数字。接着做筛选条件是“所有同名列的值都相等”这一步把绝大多数组合丢掉。最后做一个关键操作——投影把所有同名列只留一列因为既然值已经相等再出现两列就属于冗余信息。这个投影步骤就是自然连接和等值连接在数学层面最本质的区别。等值连接只做了前两步或者说得更准确些它只做了“带条件”的笛卡尔积不做同名列合并。所以你拿到SELECT *的结果时能看到两张表里各自的连接列都完整保留着。自然连接则不同你会在结果集里看到的是“合并后的列”它们通常位于结果集的最前面然后才是各自不重复的列。2.2 一张表看懂语法语义差异对比维度自然连接等值连接连接条件来源自动匹配所有同名列必须显式写条件连接条件形式隐含的等值条件可以是等值也可以是其他比较结果集投影同名列合并为一列所有列全部保留含重复列能否处理非同名列不能仅靠列名判断可以靠你指定的任意条件SQL标准写法NATURAL JOINJOIN ... ON R.x S.y或WHERE控制精细度低高自然连接的“自动匹配所有同名列”是它最吸引人的地方也是它最危险的地方。数据库不会管两个同名列在你的业务场景里是否真的代表同一个含义它只看列名。如果你在两张表里都有一个叫status的列但一个是订单状态一个是用户状态NATURAL JOIN会毫不犹豫地拿它们做等值匹配然后在结果里只保留一个status。轻则数据错乱重则整个查询结果完全不可用。等值连接则是把敏感问题都摆到台面上来。ON子句里写着什么执行器就干什么。即使两个列名相同你写USING (id)和写ON t1.id t2.id在MySQL里返回的列数都不一样前者会对id做合并后者会保留两个id列。这其实说明了一个更深刻的道理自然连接本质上是在“更高级的语法层面”帮你做了一个投影决策而这个决策很可能不是你想要的。2.3 为什么自然连接经常被开发者排斥在工业级代码里自然连接的使用频率远低于等值连接。原因不是它没用而是它的隐式行为会让代码的可读性和可维护性变差。写JOIN ... ON的时候条件一眼可见审查代码的人不用去推测这个查询想干什么写NATURAL JOIN时连接的是什么列取决于两个表的schemaschema一变查询语义就跟着变这种“隐性耦合”在大型项目里是大忌。但要说自然连接彻底没用也不对。数据仓库中有一种场景非常适合它两张已经被仔细设计过的维度表或中间表同名列恰好是业务主键且没有其他重复属性列时NATURAL JOIN写法最简洁输出的列也最干净。如果一个团队能保证表结构的规范性自然连接反而能减少大量重复的ON条件书写。所以更好的态度不是“永不使用”而是“知道它的行为在受控的环境里使用”。3. 执行机制与性能原理不比不知道3.1 执行器眼中的自然连接是什么样的从数据库优化器的视角看NATURAL JOIN在执行前会被改写成等值连接。你不会在最终的执行计划里看到“natural join”这个字眼看到的是inner join和一个由同名列组成的连接条件。也就是说自然连接只是“语法糖”最终执行时仍然等价于等值连接。既然执行方式相同那性能差异又是从哪来的来自两个层面。第一个层面是“结果的体积”。自然连接因为做了投影合并输出给上层算子的行宽可能更小后续如果有排序、分组或再次连接内存和IO的开销就会更低。第二个层面是优化器能拿到的信息。在特定数据库里如果同名列恰好有唯一索引或主键约束优化器可以推断出连接后的行数上界不超过大表行数从而选择更优的驱动表顺序。等值连接能否获得这个优势取决于你显式条件中使用的列是否带有同样的统计信息。3.2 连接算法选择会怎么变关系型数据库实现连接时最常用的有三种算法嵌套循环连接Nested Loop Join、哈希连接Hash Join、排序合并连接Sort-Merge Join。嵌套循环连接适合一个小表驱动一个大表并且大表上有索引的情况。比如订单表有十万行用户表有两千行用用户表做外表订单表上的user_id有索引那么每次外表的行去内表查找几乎都是索引点查总体耗时可控。哈希连接适合两个表都比较大且没有合适索引的等值连接。优化器会先建立一个哈希表然后用探测阶段去匹配。这种算法对等值条件有天然亲和力因为它就是在做键值查找。排序合并连接适合两个表都已经按连接列排好序的场景或者需要输出有序结果的情况。值得强调的是自然连接因为只可能产生等值条件所以哈希连接和排序合并连接都可以顺利参与优化而等值连接在语义上也可以扩展成不等值条件一旦出现不等值哈希连接直接失效优化器只能退回到嵌套循环或排序合并性能往往呈现量级差异。这算是在性能维度上等值连接“自由度过高”带来的一个反效果。3.3 用真实实验看差距我曾经在MySQL 8.0里做过一个简单测试。两张表各20万行字段结构故意设计成a(id, val, name)和b(id, val, extra)表a的id有主键表b的id有普通索引。查询两种写法自然连接SELECT * FROM a NATURAL JOIN b;等值连接显式保留双列SELECT * FROM a JOIN b ON a.id b.id;同一批数据同一种连接算法注意由于两张表的同名列不仅有id还有val所以NATURAL JOIN的真正连接条件是a.idb.id AND a.valb.val而等值连接只匹配a.idb.id。这直接导致结果行数完全不同——自然连接因为多了一个等值条件行数更少如果val没有索引还需要额外做一次基于哈希的等值匹配。于是自然连接的耗时反而比只匹配id的等值连接高了将近30%。这个实验结果很好地说明了工程里的一句老话自然连接虽然写法是简洁的但“自动匹配所有同名列”意味着执行器必须对所有同类名建立等值条件而不是只匹配你心里想的那一个。很多时候自然连接慢不是因为它运行机制特殊而是因为它默默帮你加了一条你没要求的连接条件。4. 实操四种写法的SQL对比与等价转换4.1 从建表到数据准备直接照着跑为了把四种写法放在同一语境下对比我们先建两张简单的表。CREATE TABLE emp ( emp_id INT PRIMARY KEY, emp_name VARCHAR(50), dept_id INT, mgr_id INT ); CREATE TABLE dept ( dept_id INT PRIMARY KEY, dept_name VARCHAR(50), mgr_id INT ); INSERT INTO emp VALUES (1, 张伟, 10, 100), (2, 李娜, 10, 101), (3, 王强, 20, 100), (4, 赵敏, 30, NULL); INSERT INTO dept VALUES (10, 研发部, 100), (20, 市场部, 100), (30, 财务部, 101);注意这里的表结构很微妙emp里有dept_id和mgr_iddept里也有dept_id和mgr_id。这意味着两张表有两个同名列如果直接写NATURAL JOIN连接条件会同时包含dept_id和mgr_id两个等值约束。这在实际业务中很少是你要的语义——一个员工在哪个部门和员工对应的主管编号这两个维度不应该同时作为连接条件。4.2 四种写法和它们的结果差异第一种最规范的等值连接SELECT e.emp_id, e.emp_name, d.dept_name FROM emp e JOIN dept d ON e.dept_id d.dept_id;结果会得到三行员工数据emp_id为1、2的员工属于研发部emp_id为3的属于市场部emp_id为4的因为dept_id是30在部门表里有对应行所以也能连接上。第二种使用USING子句SELECT e.emp_id, e.emp_name, d.dept_name FROM emp e JOIN dept d USING (dept_id);USING的语义是按指定列名做等值连接并且结果集里只保留一列dept_id。它有点像是自然连接和等值连接的折中方案——比NATURAL JOIN可控比ON少写一点条件。在实际工程中当两个表的确存在同名且同义的列时USING是很好的选择。第三种自然连接SELECT * FROM emp NATURAL JOIN dept;上面的查询等价于SELECT * FROM emp JOIN dept ON emp.dept_id dept.dept_id AND emp.mgr_id dept.mgr_id;由于两张表中只有dept_id10且mgr_id100的部门存在研发部最终只剩一行结果员工张伟匹配上研发部。这个结果对很多人来说是“反直觉”的因为看起来像“员工能匹配到部门”但实际却因为mgr_id的额外条件把行数砍掉了。第四种用WHERE做等值连接老式写法SELECT * FROM emp e, dept d WHERE e.dept_id d.dept_id;这个写法在ANSI SQL标准里属于旧的连接语法但它做的事和第一种完全一样。唯一需要注意的是如果哪天忘了写WHERE条件执行的就是笛卡尔积数据量爆炸时很容易把数据库压垮。老式写法在复杂的查询中可读性很差不建议在运维、开发环境中继续使用。4.3 结果集投影的实操对照把四种查询的SELECT *结果放在一起看差异就非常直观写法是否合并同名列输出列清单ON e.dept_id d.dept_id否emp_id, emp_name, dept_id, mgr_id, dept_id, dept_name, mgr_idUSING (dept_id)只合并指定列emp_id, emp_name, dept_id, mgr_id, dept_name, mgr_idNATURAL JOIN合并所有同名列emp_id, emp_name, dept_id, mgr_id, dept_nameWHERE老式写法否同ON写法在真实场景里若没有指定明确的输出列而直接用SELECT *等值连接的结果集会包含两个dept_id数据抽取到应用层后如果用列名去取值往往只能取到第一个或最后一个非常容易出bug。这也是为什么很多数据同步工具在抽取时都建议明确列清单而不是依赖SELECT *。4.4 一个必会的转换技巧把自然连接改写成等值连接如果你的系统里已经有一段用了自然连接的老SQL现在要改造成等值连接千万不能只保留一个同名等值条件。正确做法是先查两个表的元数据把所有同名列全部列出来然后一条条转成ON ... AND ...的形式。如果你的数据库是MySQL可以用下面的语句快速拿到同名列SELECT column_name FROM information_schema.columns WHERE table_schema your_db AND table_name IN (emp, dept) GROUP BY column_name HAVING COUNT(DISTINCT table_name) 2;拿到同名列清单后再结合具体业务决定哪些应该作为连接条件哪些应该通过USING或投影来消除歧义。这个步骤看似机械但它是规避“改写后语义漂移”最有效的方法。5. 高频翻车现场与排查心得5.1 自然连接自动扩大了连接条件导致行数骤减这是最常见也是最隐蔽的问题。我记得有一次在数据仓库里帮同事排查一个报表的数据量对不上的问题。源SQL用的就是NATURAL JOIN。当时两张宽表有十几个同名列我随便看了下明面上那两三个主键列觉得没什么问题。但结果就是行数只有预期的一半排查了很久才发现其中一张表在某个分区上有一个叫source_type的列在另一张表里也有而且值域恰好能重叠。自然连接把它也作为连接条件后大量本应保留的行被过滤掉了。这个问题的排查思路非常明确只要你知道该看什么一眼就能定位。把自然连接改写等价条件后EXPLAIN一下再看Extra里的Using where、Using join buffer等信息通常就能看到额外条件。也可以用下面这条SQL直接查同名列再从业务视角逐个判断这些列是否真的应该参与连接SELECT table_name, column_name FROM information_schema.columns WHERE table_schema your_db AND (table_name emp OR table_name dept) ORDER BY column_name, table_name;5.2 等值连接里两个dept_id造成的列名冲突等值连接不合并同名列的结果往往会在应用层引发“字段覆盖”问题。比如上面的查询里SELECT *同时返回了emp.dept_id和dept.dept_id如果你用的ORM框架按列名自动映射后面的值会把前面的值覆盖掉而员工所属部门的编号恰恰存在于被覆盖的那一列中。解决办法是显式定义输出列表给可能冲突的列起别名SELECT e.emp_id, e.emp_name, e.dept_id AS emp_dept_id, d.dept_name, d.mgr_id AS dept_mgr_id FROM emp e JOIN dept d ON e.dept_id d.dept_id;这个经验在接口开发和数据导出时尤其重要。不要嫌写列名麻烦列名清单本身就是一种文档能有效防止数据和字段错位。5.3 NULL与连接条件自然连接永远匹配不到的坑等值连接和自然连接在NULL处理上有一个共同点NULL NULL的结果是UNKNOWN也就是说NULL永远不会等于NULL。因此如果两张表的连接列中存在NULL值这些行永远不会出现在结果中。这个行为在INNER JOIN中如此在自然连接中同样如此。比如员工赵敏的mgr_id是NULL部门表财务部的mgr_id是101虽然这个值不是NULL但其余两个部门研发部、市场部的mgr_id都是100如果员工和部门都按mgr_id来匹配赵敏还是匹配不到。假如把条件换成USING (mgr_id)同样如此。如果你业务上确实需要保留未匹配的行就应该使用LEFT JOIN并且显式写清条件更不要试图用自然连接去实现外连接——绝大多数数据库对NATURAL LEFT JOIN的支持都很有限行为也容易让人困惑倒不如老老实实用ON。5.4 解释执行计划时如何看连接代价遇到性能问题我建议先执行一遍EXPLAIN看看优化器是怎么选的。在MySQL中优化器选择的驱动表和连接算法都会体现在执行计划里。判断一个等值连接是否高效关键看三点看什么怎么判断驱动表第一行应尽量是小表或过滤后行数少的表被驱动表访问方式命中索引type为ref或eq_ref最优扫描行数与最终结果行数的比例扫描行数远大于结果行数时说明条件过滤性差自然连接在这里有个坑由于它自动带上了所有同名列条件优化器评估时会把多条件的综合选择性纳入成本计算有概率选出一个和你预想完全不同的驱动表顺序。如果你看到一个查询的驱动表和预期不符不要急着手工加STRAIGHT_JOIN或hint先确认自然连接被改写成的条件集是不是你真正想要的。6. 应用场景选型什么情况下才真正需要自然连接6.1 数据分析中的一时爽与长期坑数据分析和报表开发中经常要临时探索数据。这时候自然连接确实“一时爽”因为你不用关心两个表里到底有哪些同名列一句NATURAL JOIN就能把两张表最容易匹配的部分拼起来快速看数据形态。但一旦这个探索型查询要固化成定时任务或报表逻辑自然连接就成了定时炸弹。表结构在数据仓库中变化是常态一个上游表加了新列下游所有自然连接的行为就可能跟着变而且这种变化完全静默直到某天报表数据对不上才会被注意到。所以我的个人习惯是探索阶段随便用固化阶段必须改成显式等值连接。很多团队的数据开发规范也明确禁止在正式任务中使用NATURAL JOIN理由无非如此。6.2 数据清洗与同名异义列的处理数据清洗任务中两个表经常存在“同名异义”的列。比如一张表里的id是自增主键另一张表里的id是业务编号NATURAL JOIN会把它们当作同一个值域来匹配结果几乎必然错乱。处理同名异义列时通常要配套列重命名或投影剥离让列在语义上恢复一致后再用显式等值连接聚合所需字段。一个常见做法是在清洗阶段就重写列名SELECT e.emp_id, e.emp_name, d.dept_name FROM emp e LEFT JOIN dept d ON e.dept_id d.dept_id;如果需要保留原始表的信息可以使用USING或显式投影把歧义列摘干净保证下游不会出现列冲突。6.3 面试考点与理论落地知道为什么比知道是什么更重要面试中经常会有这么一道题在什么情况下自然连接和等值连接的结果是一样的这个问题比“两者区别是什么”更深一层它考察的是你有没有真正理解投影和筛选的关系。只有当两个表没有任何同名列时自然连接会退化为笛卡尔积这就不是等值连接了如果两表只有一个同名列且该列具有唯一性约束同时你并不关心重复列是否保留那么两者的结果在逻辑上基本一致只在列数量上有差异。遇到这类问题我建议用例子来做现场推导不要死记结论。把两张表画出来先做笛卡尔积再写筛选条件再做投影自然连接的结果可以一步一步推导出来。这比你背诵十条区别都管用因为推导本身已经把“为什么”完整走了一遍。7. 最后分享两个小经验第一个经验是关于代码评审的。在评审别人SQL时只要看到NATURAL JOIN我一定会让作者解释清楚两个表的全部同名列分别是什么含义。如果对方说不清楚就改成显式等值连接。这不是教条而是因为“隐式行为”是代码维护中最大的敌人。数据库这种工具不同版本、不同优化器对同一个写法的处理都可能存在差异让一切条件显式化至少能保证你在排查问题时有一个确定的起点。第二个经验是设计表结构时尽量规避同名异义列。如果你在设计表的时候就刻意把业务语义编进列名比如订单表用order_status、用户表用user_status那么即使哪天真有人写了NATURAL JOIN也不会因为重名而静默产生多余条件。反过来说如果表结构一团乱麻到处都是status、type、flag这种含混列名那么自然连接这种工具迟早会给你上一课。底层的防御永远比上层的补救来得可靠。
返回列表