Oracle多表查询从入门到精通:JOIN、子查询与SQL优化实战
发布时间:2026/9/28 22:51:56来源:尧图网络
1. 多表查询的底层逻辑为什么不能只靠单表接触Oracle数据库的同学十有八九是从单表查询起步的SELECT * FROM emp、WHERE ename SMITH一切都挺美好。但真正上了生产环境面对动辄几十个字段、关联七八张业务表的报表需求时单表查询瞬间就变成了一场灾难。为什么因为关系型数据库的核心思想就是把数据拆分存储减少冗余而拆完之后要还原出业务需要的完整信息就必须靠多表查询把它们重新缝起来。拿最经典的emp员工表和dept部门表来说员工表里存的只是deptno这个部门编号你查员工信息时自然想知道部门名称和所在位置但这两个字段只存在于部门表中。如果你只会单表查询要么查完员工再循环补查部门表要么把两张表的数据全部捞出来在应用层做匹配——前者会产生灾难性的循环查询性能问题后者更是资源浪费到姥姥家。其实Oracle早就给你准备好了JOIN机制一次SQL就能把两张表按关联条件拼成一张宽表这就是多表查询存在的核心意义。从底层原理来看Oracle执行多表查询时通常有三个基本步骤解析SQL、生成执行计划、按计划访问表数据。这里最关键的是执行计划中的连接顺序和连接方式。Oracle的优化器CBO会根据表的数据量、索引情况、统计信息决定先访问哪张表驱动表再用什么方式去匹配另一张表被驱动表。你写的表顺序、连接条件写法都会影响优化器最终的选择。很多人以为多表查询不过是把表写在FROM后面拿WHERE和JOIN ON把它串起来这只能说对了一半真正的门槛在于理解连接类型、NULL值处理、笛卡尔积陷阱、子查询去重以及如何让优化器走一条高效路径。这篇文章我会把Oracle多表查询从入门到精通的完整脉络拆开讲。适合正在学习Oracle的初学者也适合那些写了好几个月SQL但对性能优化一知半解的开发。以emp、dept、salgrade这组经典表结构为主线穿插面试题和真实工作里的坑尽量把每个知识点讲透而不是扔一堆语法让你死记。1.1 两张表关联时到底发生了什么我们先从一次最简单的两表连接说起。假设执行SELECT e.ename, d.dname FROM emp e, dept d WHERE e.deptno d.deptno;这是Oracle老式的等值连接写法也是很多人接触多表查询的第一个SQL。它的执行逻辑可以理解为Oracle先取出emp表的一行拿这一行的deptno去dept表里找匹配的行找到就把两行的字段合并输出找不到就继续看下一行。当然这只是嵌套循环连接的简化模型实际执行时Oracle可以有三种主要的连接方式我在后面专门讲。这里必须强调一个新手常犯的认知误区多表连接不是你想象中那种纯手工一行行比的低效操作。Oracle优化器会先看deptno上有没有索引、两张表各有多少行、有没有统计信息然后决定到底是先用emp做驱动表还是先用dept做驱动表。如果你这两个表都没收集统计信息优化器只能用动态采样猜测很可能搞出一个全表扫描再哈希连接——数据量大了SQL就慢了。所以学习多表查询其实有一半的功夫要花在让Oracle做出聪明选择上。1.2 连接条件的执行细节与NULL值的特殊命运连接条件怎么写直接决定了结果集的大小也决定了很多业务上莫名其妙少数据的问题根源。就拿e.deptno d.deptno来说如果emp表里恰好有个员工的deptno是NULL——比如他还没分部门——那么这个员工的这行数据在等值连接里会直接消失因为NULL和任何值做等值比较结果都是未知既不是TRUE也不是FALSE因此不会被纳入结果集。这个细节在做数据核对时经常坑人。你写了一个JOIN发现结果比预想的少了几行花了大半天排查最后发现是某几条记录的关联字段是NULL。解决办法也很简单如果需要保留那些NULL值方记录就用后面的LEFT JOIN如果业务上确实不该出现NULL那就当成脏数据先去清洗。这里顺带说一个经验日常写多表查询之前最好先对关联字段做一次非空检查SELECT COUNT(*) FROM emp WHERE deptno IS NULL;一条语句花不了几秒却能省下后面数小时的排查时间。1.3 多表查询的学习路径怎么排才合理很多自学Oracle的朋友上来就背语法INNER JOIN、LEFT JOIN、RIGHT JOIN、FULL JOIN、CROSS JOIN……背得滚瓜烂熟但一扔到真实业务里就懵因为不知道什么时候该用哪种。我的建议是换一条学习路径先理解表与表之间的业务关系一对多、多对多、一对一关系决定了连接结果的形态。再弄清Oracle老式连接写法FROM a, b WHERE a.x b.y和ANSI连接JOIN ... ON之间的区别至少在生产代码里两种都见过。然后把内连接、外连接、自连接、非等值连接这几类核心连接方式吃透能用经典案例跑通。接着学习子查询、集合操作UNION、MINUS、INTERSECT知道哪些场景用JOIN哪些场景用集合操作哪些场景用子查询效率更高。最后回到执行计划上用EXPLAIN PLAN验证自己的SQL写法是否高效。这条路走完你对多表查询的理解就不会只停留在语法层面。下面我会按这条路径把每个环节都过一遍。2. 连接查询的类型选择与实战拆解从内连接到自连接连接查询是多表查询的核心但Oracle里的连接类型非常丰富等值连接、非等值连接、自连接、内连接、外连接左外、右外、全外、笛卡尔积、交叉连接……名字听起来多实际归归类就那么几大类。本节我用真实场景一个个拆解重点讲怎么选和为什么这么选。2.1 内连接与外连接业务含义上的根本分野内连接INNER JOIN返回的是两张表中都满足连接条件的行也就是两张表的交集。外连接OUTER JOIN则会把某一侧表中不满足条件的行也保留下来缺失的另一侧字段用NULL填充。这个差别不是语法游戏而是业务语义的不同。举个例子报表要求列出所有部门及其员工人数那没问题只要在职员工都归属某个部门用内连接就够。但如果报表要求列出所有部门即使当前没有任何员工也要显示该部门并让员工数字为0那内连接就会把空部门丢掉必须用左外连接SELECT d.deptno, d.dname, COUNT(e.empno) AS emp_cnt FROM dept d LEFT JOIN emp e ON e.deptno d.deptno GROUP BY d.deptno, d.dname ORDER BY d.deptno;这里注意一个细节COUNT(e.empno)而不是COUNT(*)。如果左表dept的某一行在右表emp没有匹配那么e.empno是NULLCOUNT(字段)会忽略NULL结果就是0但如果写成COUNT(*)Oracle会把左表那一行也算进去结果变成1这个错误在报表里非常隐蔽。再说右外连接和全外连接。右外连接RIGHT JOIN和左外连接是对称的只是保留右侧全部记录。全外连接FULL JOIN则把左右两侧所有记录都保留两侧都不匹配的部分都补NULL。在Oracle老语法里外连接是用()号实现的放在连接条件中缺失的那一侧比如SELECT d.deptno, d.dname, e.ename FROM dept d, emp e WHERE e.deptno() d.deptno;这个()表示emp是补NULL的那一侧效果等价于LEFT JOIN dept on ...。新开发建议直接写ANSI风格的LEFT JOIN语义清晰、可移植性好但如果是维护老项目()的写法你必须看得懂。2.2 非等值连接连接条件不只是等号很多人的直觉是连接嘛就是用等号把主键外键串起来所以遇到工资等级表这种没有公共字段、只有范围区间匹配的场景就卡壳了。实际上Oracle完全支持非等值连接连接条件可以用、、BETWEEN等操作符。salgrade表就是经典例子它有losal和hisal两个字段表示每个等级的最低工资和最高工资。现在要查每个员工所在工资等级SELECT e.ename, e.sal, s.grade FROM emp e, salgrade s WHERE e.sal BETWEEN s.losal AND s.hisal;这种连接方式在业务里并不少见按分数区间评级、按时间范围匹配费率、按IP段匹配归属地都属于非等值连接。它的特点是没有等值条件下那么好的索引利用空间BETWEEN条件下如果两个字段都有索引优化器可能会做范围扫描所以数据量大时尤其要注意执行计划。2.3 自连接一张表自己和自己拼自连接是最容易绕晕的一种多表查询但理解之后非常实用。什么叫自连接其实就是同一张表在FROM子句中出现两次取两个不同的别名然后通过某种关联条件把它们自己和自己连接起来本质上是查同一张表但行与行之间存在关系。最典型的就是emp表里有个mgr字段保存的是该员工上级的empno。要查每个员工的姓名以及他的上级姓名就必须让emp表跟自己连接SELECT worker.ename AS employee_name, manager.ename AS manager_name FROM emp worker LEFT JOIN emp manager ON worker.mgr manager.empno ORDER BY worker.ename;这里的关键点有两个必须给两个别名worker和manager否则Oracle分不清哪行是员工、哪行是上级。建议用LEFT JOIN而不建议直接INNER JOIN。因为老板KING的mgr是NULL如果用内连接老板这一行会被丢掉报表少了顶头上司从业务上说不通。自连接除了查上下级关系之外还可以用来做同一张表内找重复数据、同一张表内比较不同行值等操作。比如找出工资高于同部门平均工资的员工用自连接配合分组视图也能实现。很多面试官喜欢拿自连接出题因为你单纯背语法是写不出来的必须真正理解表与自身建立关联的含义。2.4 笛卡尔积与CROSS JOIN危险的爆炸连接笛卡尔积是指两张表的所有行两两组合。比如emp表14行、dept表4行做笛卡尔积就是56行。在FROM子句里写多张表却忘了写连接条件Oracle就会产生笛卡尔积在数据量大的生产环境下这会造成查询结果疯狂膨胀把数据库资源瞬间打满。SELECT e.ename, d.dname FROM emp e, dept d;这种写法在很多新手手里是无意中写出来的。排查方法也很直接如果执行计划里出现了NESTED LOOPS但没有join predicate或者结果行数等于两张表行数乘积那基本就是笛卡尔积。当然某些特殊场景下我们会故意使用笛卡尔积比如要生成一个每个员工 × 每个月份的排班基础数据集这时用CROSS JOIN语义更清晰SELECT e.empno, m.month_id FROM emp e CROSS JOIN month_dim m;但请记住日常业务里除了生成维度组合之外笛卡尔积几乎都是性能事故的代名词写的时候千万要小心。3. 子查询与集合操作多表查询的进阶玩法连接是把表横向拼起来而子查询和集合操作则是用另一种维度处理多表数据。子查询本质上是嵌套在另一个查询里的查询可以放在WHERE、FROM、SELECT、HAVING等位置集合操作则是把多个查询的结果纵向合并、比较。两者和JOIN各有不同的适用场景用对了能极大提升SQL表达能力。3.1 WHERE子句里的经典子查询IN、EXISTS与标量子查询最常见的场景是查工资大于部门平均工资的员工如果你只用连接和分组也许能写出来但读起来很别扭。用子查询可以这样组织思路先算出每个部门的平均工资再拿员工表和这个结果比对。单行子查询就是子查询只返回一行一列可以用、这类普通比较符。比如查工资最高的员工SELECT ename, sal FROM emp WHERE sal (SELECT MAX(sal) FROM emp);多行子查询再用就会报错除非配合INOracle这里最常用的是INSELECT ename, sal FROM emp WHERE deptno IN (SELECT deptno FROM dept WHERE loc NEW YORK);但有一个经典争论到底该用IN还是EXISTS我直接说结论数据量小的时候差别不大数据量大时EXISTS往往更优特别是子查询表很大而外表较小时。原因在于EXISTS是半连接逻辑找到第一条匹配就停止当前行的判断不需要把子查询结果集全部物化出来而IN在某些版本里会先执行子查询把结果集做去重后再去外表匹配一旦子查询结果集是几十万行内存和临时表开销都很可观。不过这不是绝对的Oracle优化器有子查询展开subquery unnesting机制可能把IN改写成semi join所以最终还是要看执行计划。至于标量子查询Scalar Subquery通常用在SELECT列表里。比如查每个部门的人数可以这么写SELECT d.dname, (SELECT COUNT(*) FROM emp e WHERE e.deptno d.deptno) AS emp_cnt FROM dept d;这种写法在行数少时很清晰但如果外层表大标量子查询会被反复执行性能容易出问题。干部门表就4行没问题换成几万行的客户表就危险了。替代方案是改用LEFT JOIN GROUP BY整体更稳妥。3.2 FROM子句的子查询内联视图子查询还可以放在FROM后面Oracle管这叫内联视图Inline View。它的作用相当于先构造一个临时的逻辑表再让外层查询跟它做关联。当你要基于聚合结果再做进一步计算时内联视图比反复嵌套WHERE子查询要清晰得多。比如查每个部门工资最高的员工名称如果只查部门号和最高工资用GROUP BY就够了但要带着员工姓名一起查就得先把部门最高工资聚合出来再和员工表连接SELECT e.ename, e.deptno, e.sal FROM emp e, (SELECT deptno, MAX(sal) AS max_sal FROM emp GROUP BY deptno) d WHERE e.deptno d.deptno AND e.sal d.max_sal;这里的内联视图d就相当于一张部门最高工资维度表。需要注意的是Oracle对内联视图的优化空间相对有限。如果内联视图里做了大范围聚合外层还要跟它做连接可能产生临时表开销。所以写内联视图的时候能提前过滤的条件尽量推进子查询内部让子查询返回尽量少的行数再参与外层连接这是我一直强调的优化习惯。3.3 集合操作UNION、INTERSECT、MINUS的适用边界连接是把两张表的列拼在一起集合操作则是把两个查询的结果按行合并或比较。Oracle里主要就是UNION、UNION ALL、INTERSECT、MINUS。UNION合并两个结果集并去重相当于给结果整体做了一个DISTINCT。UNION ALL合并两个结果集但不去重速度快因为少了一步排序或哈希去重。INTERSECT返回两个结果集的交集。MINUS返回第一个结果集有、但第二个结果集没有的记录相当于差集。举个例子查在部门10工作过的员工的empno和做过销售岗位的员工的empno分别有哪些SELECT empno FROM emp WHERE deptno 10 UNION SELECT empno FROM emp WHERE job SALESMAN;这里UNION会把可能在两段查询中都出现的员工编号去重而如果用UNION ALL这种重复就会被保留。业务上如果是把不同类别的明细数据合并展示且能确认没有重复直接用UNION ALL性能更好如果必须去重才用UNION。再看一个面试常见题怎么判断两张表的数据是否完全一致很多人的第一反应是写一堆MINUS。其实Oracle有快捷办法比如MINUS方向对比再加上UNION。具体来说(SELECT * FROM table_a MINUS SELECT * FROM table_b) UNION ALL (SELECT * FROM table_b MINUS SELECT * FROM table_a);如果结果集为空说明两张表数据完全一致。如果非空多出来的就是差异数据。这个思路在数据迁移验证、同步校验阶段非常实用。3.4 集合操作和JOIN如何选择很多同学遇到两表都有某个维度想比较差异时不知道到底该写JOIN还是MINUS。我的经验总结如下需要横向扩展字段时比如员工表关联部门表把部门名称带出来那就用JOIN。只需要纵向比较行是否出现时比如对比源表和目标表的ID集合用集合操作更直观且高效。需要找出在A表但不在B表的记录还要携带A表详细字段时优先考虑LEFT JOIN ... WHERE B.PK IS NULL这种写法通常比NOT IN子查询更容易让优化器走anti join性能也更稳定。拿出查询没有员工的部门来对比三种写法-- 写法1LEFT JOIN SELECT d.deptno, d.dname FROM dept d LEFT JOIN emp e ON e.deptno d.deptno WHERE e.empno IS NULL; -- 写法2NOT IN子查询 SELECT deptno, dname FROM dept WHERE deptno NOT IN (SELECT deptno FROM emp WHERE deptno IS NOT NULL); -- 写法3MINUS SELECT deptno, dname FROM dept MINUS SELECT deptno, NULL FROM emp;写法2有个致命坑点如果子查询deptno列里出现NULLNOT IN永远不会返回任何行因为deptno NOT IN (NULL)相当于NOT (deptno NULL)结果全是UNKNOWN。所以用NOT IN必须显式过滤NULL或者改写成WHERE deptno NOT IN (SELECT deptno FROM emp WHERE deptno IS NOT NULL)。而LEFT JOIN ... IS NULL写法和MINUS就没有这个困扰这也是我在生产环境里更推荐它们的原因。4. 多表查询的性能优化与常见坑让执行计划告诉你真相多表查询写起来容易真正难的是一张SQL跑得快。很多开发抱怨Oracle太慢实际排查下来往往是SQL写得有问题该走索引没走、驱动表选反、子查询被反复执行、连接条件用函数包裹导致索引失效……本节我把这些坑一个个摊开并给出可复现的优化思路。4.1 三种连接方式嵌套循环、哈希连接、排序合并Oracle执行多表连接时最核心的三种方式是连接方式适用场景特征与注意点NESTED LOOPS嵌套循环驱动表小、被驱动表有索引先遍历外表每一行再去内表通过索引找匹配行连接条件必须能利用索引否则极慢HASH JOIN哈希连接两表数据量大尤其一张表远大于另一张先对驱动表建哈希表再扫描被驱动表进行探测无索引也能高效工作SORT MERGE JOIN排序合并连接条件是非等值、或者数据已排序两表按连接键排序然后合并匹配排序本身很耗资源我在医院系统里优化过一条SQL两表各几十万行原来走的嵌套循环内表又没有索引跑了40多秒。后来把统计信息更新后让优化器改成哈希连接直接降到2秒以内。这说明连接方式的选择对性能影响是数量级的而不是微调。怎么让Oracle选择正确的连接方式最重要的前提是统计信息准确。平时用DBMS_STATS.GATHER_TABLE_STATS定期收集表统计信息优化器才能估算出行数和选择率。其次才是你调整/* LEADING(e) USE_HASH(d) */这类提示hint但hint只应在完全确认执行计划时使用不要在业务SQL里乱加否则统计信息变化后可能会让执行计划更糟。4.2 驱动表选择的秘密小表驱动大表很多Oracle教材都说过小表驱动大表但知其然不知其所以然的人很多。其实不同连接方式对驱动表的依赖程度不同嵌套循环中驱动表决定了循环次数所以必须选小表哈希连接中驱动表决定了哈希表的构建成本所以也应该选小表。排序合并连接对驱动表依赖不那么强因为两边都要排序。在一个两表等值连接中优化器原则上会估算哪种顺序的代价更低。但如果你发现执行计划里驱动表明显选错比如大表当驱动表可以通过hint纠正SELECT /* LEADING(d) USE_NL(e) */ e.ename, d.dname FROM dept d, emp e WHERE e.deptno d.deptno;不过我还是要提醒hint是最后手段优先检查统计信息、索引、连接条件类型。如果统计信息新鲜、关联字段有索引Oracle CBO的默认选择往往不会太离谱。4.3 OR条件、函数包裹、隐式类型转换让索引失效的三座大山多表查询性能问题的第二大类是索引明明建了却用不上。最常见的三类情况在连接字段上套函数比如WHERE TRUNC(e.hiredate) TRUNC(d.somedate)。左边套了TRUNC索引就失效了应该改成范围条件e.hiredate ... AND e.hiredate ...。隐式类型转换比如字段是VARCHAR2类型但你拿数字去比较Oracle会隐式把字段转换成数字从而触发全表扫描。连接条件也一样两侧字段类型不相同索引极可能失效。OR条件连接比如WHERE e.deptno d.deptno OR e.job MANAGER很可能导致无法直接做索引连接优化器会把OR改写成UNION或者走全表扫描。排查这类问题最直观的方式是用EXPLAIN PLAN看执行计划。如果计划里出现TABLE ACCESS FULL而你确信索引存在基本就属于上面某一种情况。再提醒一下生产库里TRUNC(日期列)这种写法非常常见我见过无数人栽在上面正确做法是写成日期范围条件。4.4 多表连接顺序的排列与连接条件的放置位置多表连接时FROM子句里表的顺序以及ON条件和WHERE条件的放置会直接影响优化器的选择空间。老写法里所有连接条件和过滤条件都挤在WHERE里ANSI写法把连接条件放ON、过滤条件放WHERE语义更清晰。但这两者的执行计划可能不同。比如SELECT e.ename, d.dname FROM dept d LEFT JOIN emp e ON e.deptno d.deptno WHERE e.sal 2000;如果写成这样WHERE e.sal 2000实际上等于把左外连接变成了内连接的效果因为e.sal为NULL的行会被过滤掉。如果你本意是所有部门都保留只统计工资大于2000的员工那过滤条件必须放在ON子句里SELECT d.dname, e.ename FROM dept d LEFT JOIN emp e ON e.deptno d.deptno AND e.sal 2000;这两种写法的结果集完全不同。前者只返回有高薪员工的部门后者返回所有部门部门下没有高薪员工就用NULL填充。这是很多开发在生产环境里踩过的坑数据报表里少了好几行却死活找不到原因。所以我建议只要涉及外连接就养成习惯连接条件一律进ON过滤条件能进ON就进ON除非你明确知道需要先过滤后连接。4.5 WITH子句与临时结果集复用多表查询里如果有多个子查询都要用到同一个聚合结果与其反复嵌套子查询不如用WITH公共表表达式CTE把中间结果抽出来先物化再复用。这既是写法上的优化有时也是性能上的优化。比如统计各部门人数并且找出人数超过部门平均的部门如果不用WITH你得在一大段SQL里重复写聚合子查询。用WITH可以这样组织WITH dept_stats AS ( SELECT deptno, COUNT(*) AS emp_cnt FROM emp GROUP BY deptno ) SELECT d.deptno, d.dname, ds.emp_cnt FROM dept d LEFT JOIN dept_stats ds ON ds.deptno d.deptno WHERE ds.emp_cnt (SELECT AVG(emp_cnt) FROM dept_stats);这里dept_stats只写了一次既清晰又好维护。Oracle的优化器通常会把WITH结果当作内联视图或临时表来处理如果你在WITH里做了大聚合并且后续引用多次物化效果能明显减少重复计算。不过如果WITH子查询非常大而且只引用一次优化器也可能选择直接内联展开所以还是那句老话用EXPLAIN PLAN验证。5. 基于经典表结构的全攻略实战从需求到SQL的完整推演前面讲的是知识点和原理这一节我直接用一组从简单到复杂的业务需求把多表查询从需求分析到写出SQL再到验证结果的完整流程走一遍。表结构沿用Oracle经典教学三件套emp员工表、dept部门表、salgrade工资等级表。字段含义就不重复赘述了。5.1 需求一查所有员工及其部门名称、工资等级这个需求涉及emp、dept、salgrade三张表算是三表连接的入门题。思路是先把emp和dept做等值连接再把结果和salgrade做非等值连接SELECT e.ename, e.sal, d.dname, s.grade FROM emp e JOIN dept d ON e.deptno d.deptno JOIN salgrade s ON e.sal BETWEEN s.losal AND s.hisal ORDER BY s.grade, e.sal DESC;这里有几层细节值得注意三张表的连接顺序不是随便写的。Oracle会评估先连哪两张代价最小但这道题数据量小优化器通常无所谓。如果某个员工工资低于最低工资等级BETWEEN匹配不上这个员工会被内连接丢掉。业务上如果要求所有员工都出来可以考虑把salgrade连接改成LEFT JOIN。排序建议放在SQL最后ORDER BY的字段最好也出现在SELECT列表里这样逻辑清晰。exact运行结果形如ENAME SAL DNAME GRADE ---------- ---- -------------- ----- SMITH 800 RESEARCH 1 JAMES 950 RESEARCH 1 ADAMS 1100 RESEARCH 1 WARD 1250 SALES 2 ...5.2 需求二列出所有部门统计部门人数并标出最高工资员工这个需求有所有部门意味着要用左外连接。统计人数需要用GROUP BY找出最高工资员工又需要额外操作。我拆成两步SQL来演示第一段实现人数统计SELECT d.deptno, d.dname, COUNT(e.empno) AS emp_cnt FROM dept d LEFT JOIN emp e ON e.deptno d.deptno GROUP BY d.deptno, d.dname ORDER BY d.deptno;注意这里用了COUNT(e.empno)而不是COUNT(*)原因前面已经讲过。接着如果要标出每个部门工资最高的员工姓名就不能只GROUP BY了得回到内联视图的思路SELECT e.ename, e.deptno, e.sal FROM emp e, (SELECT deptno, MAX(sal) AS max_sal FROM emp GROUP BY deptno) t WHERE e.deptno t.deptno AND e.sal t.max_sal;这样查出来的是每个部门工资最高的人。如果部门内存在工资相同的多个员工这个查询会返回多行这是业务上需要接受的现实——除非再附加empno做去重条件。5.3 需求三查每位员工的上级姓名找出没有上级的老板自连接场景前面讲过我们直接给出完整写法。这里再加一个进阶需求同时显示员工所在部门名称和上级所在部门名称。这意味着emp表自连接之后还要再关联dept表两次SELECT worker.ename AS emp_name, worker.deptno AS emp_dept, d1.dname AS emp_dept_name, manager.ename AS manager_name, d2.dname AS manager_dept_name FROM emp worker LEFT JOIN emp manager ON worker.mgr manager.empno LEFT JOIN dept d1 ON worker.deptno d1.deptno LEFT JOIN dept d2 ON manager.deptno d2.deptno ORDER BY worker.empno;这里第五行以后是重点manager.deptno可能为NULL老板的上级不存在所以d2.dname也会为NULL这没问题。但如果用内连接去关联dept d2老板那行的manager.deptno是NULL会导致整行消失连员工自己都查不出来了。这就是为什么外连接在多表关联中一旦涉及越级字段必须沿着可能为NULL的链路一路LEFT JOIN下去。5.4 需求四跨表数据核对找出两表差异假设你有两张结构相同的表emp和emp_bak现在要快速核对哪些员工数据发生了变化。如果字段很多逐列比对不现实。最简单的兜底方案就是MINUS方向对比前面已经讲过更进一步你可以用外部表或全字段拼接的方式来做。Oracle里有一个常用技巧把一行数据的所有字段用||拼接成一个大字符串再做MINUS。虽然不优雅但在字段数量不多、数据量中等的时候非常实用SELECT empno || | || ename || | || sal AS record FROM emp MINUS SELECT empno || | || ename || | || sal AS record FROM emp_bak;如果查出来的记录不是空的就说明emp表里有emp_bak没有的行。两个方向都查完之后就能定位到新增、删除、修改。注意拼接符要选字段里不会出现的字符比如|、#避免歧义。这也是我做数据迁移校验时最常用的快速核对手段。5.5 需求五带分页的多表查询Oracle分页和多表查询结合是实际开发里最常见的组合。Oracle没有MySQL那种LIMIT语法经典分页是借助ROWNUM或FETCH FIRST子句。ROWNUM分页的关键在于先分页后排序还是先排序后分页很容易搞错。正确思路是先通过子查询把数据排序再在外层套ROWNUM限制行数然后再次封装取页码区间。三表连接的分页示例SELECT * FROM ( SELECT t.*, ROWNUM AS rn FROM ( SELECT e.ename, d.dname, s.grade, e.sal FROM emp e JOIN dept d ON e.deptno d.deptno JOIN salgrade s ON e.sal BETWEEN s.losal AND s.hisal ORDER BY e.sal DESC ) t WHERE ROWNUM 20 ) WHERE rn 11;这段SQL是取第11到第20行对应第二页。内层ROWNUM 20先截出前20行外层WHERE rn 11再丢弃前10行。如果直接在外层写ROWNUM BETWEEN 11 AND 20Oracle的ROWNUM机制会导致永远查不出结果这是新手的经典错误。从Oracle 12c开始可以直接用OFFSET和FETCH语法SELECT e.ename, d.dname FROM emp e JOIN dept d ON e.deptno d.deptno ORDER BY e.empno OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;语义上清晰很多但要注意FETCH FIRST的写法在12c以下版本不兼容。如果公司还在用11g老老实实记牢ROWNUM三明治写法。5.6 需求六多表UPDATE和DELETE中的关联更新多表查询不只是SELECT的专利Oracle的UPDATE和DELETE也同样支持多表关联。比如把emp表中属于NEW YORK部门的所有员工工资上调10%UPDATE emp e SET e.sal e.sal * 1.1 WHERE e.deptno IN (SELECT deptno FROM dept WHERE loc NEW YORK);这是一种通过子查询关联更新。如果要在UPDATE里直接关联多张表Oracle还支持可更新视图方式和MERGE语法。最容易踩坑的是更新前最好先SELECT一遍同样的关联条件确认影响行数符合预期这是所有DML操作的铁律。我曾经见过有人在生产库上写了一条UPDATEWHERE子查询漏了过滤条件导致全表工资被调整还好有备份否则后果不敢想。DELETE多表关联也有类似写法比如删除没有订单记录的客户通常写DELETE FROM customers c WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id c.customer_id);这里用NOT EXISTS比NOT IN更稳妥因为orders.customer_id如果存在NULLNOT IN会导致一条都删不掉而NOT EXISTS不会。6. 写在最后的调试习惯统计信息、执行计划与代码规范多表查询说要精通语法只是基础真正的杀手锏是让SQL在复杂业务和海量数据下依然跑得快、跑得稳。我最后分享几个实际工作中养成的调试习惯希望对你有帮助。第一多表查询写完之后养成看一眼执行计划的习惯。特别是一种SQL有多种写法的时候用EXPLAIN PLAN FOR或者直接在PL/SQL Developer、DBeaver里执行EXPLAIN PLAN查看确认没有全表扫描、没有笛卡尔积、没有明显异常的大排序/大哈希。如果一时看不懂执行计划里的关键字至少重点关注TABLE ACCESS FULL、NESTED LOOPS、HASH JOIN、SORT这几个词。第二保持统计信息新鲜。多表查询的优化依赖统计信息。定期给大表做DBMS_STATS.GATHER_TABLE_STATS生产库建议放在业务低峰期能让优化器的基数估计更准确。很多时候SQL突然变慢不是代码变了而是统计信息过期了更新之后执行计划自动恢复正常。第三SQL写法要可读、可控、可维护。老式FROM a, b WHERE a.xb.y写法在维护老项目时总能看到但新代码建议用ANSI风格JOIN。字段必须有表别名前缀SELECT子句里别用SELECT *多表连接时尤其致命分组和排序字段要和查询语义一致。这些规范不是做给别人看而是三个月后你自己回来看代码时不用对着屏幕发呆猜逻辑。第四重要SQL上线前先在测试环境用生产数据量做一次压测。多表查询在1000行和100万行时执行计划可能是完全不同的。一个在测试环境跑得飞快的小表SQL到了大数据量的生产环境可能会变成噩梦。所以别只看功能正确要看它在目标数据量下是否站得住。我在实际使用Oracle的过程中深刻感受到多表查询的很多坑都不是语法层面的而是你以为你懂了但数据让你翻了车。NULL值的吞噬、外连接的过滤条件位置、NOT IN遇到NULL的静默失败、索引被函数隐藏——这些都是几十行数据完全测不出来、几百万行数据立刻爆雷的问题。多表查询的学习一定要带着结果集长什么样执行计划怎么走的疑问去验证而不仅仅是把SQL跑出结果就完事。希望这篇攻略能帮你少走一些弯路。如果后续工作中遇到更刁钻的多表查询场景比如多层嵌套、树形结构、分区表连接、并行查询都值得单独写一篇文章展开聊。这里先把基础打得扎实后面的路自然会顺很多。
网站建设高端定制企业官网