尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MySQL多表查询:自连接与子查询的实战解析与性能优化

MySQL多表查询:自连接与子查询的实战解析与性能优化 一、MySQL多表查询的底层逻辑与整体设计1. 多表查询到底在解决什么问题面试的时候十有八九会碰到这种题一张员工表里面有员工编号和员工姓名还有一列叫经理编号现在让你查出每个员工对应的经理姓名。刚入行的同学第一反应是把表复制一份先查员工再查经理然后拿代码去比对。但SQL本身就有能力在一句话里做完这件事核心就三个词多表查询、自连接、子查询。多表查询这个事本质上是在回答一个问题如何把分散在不同表里的信息按照某种业务关系重新组装起来。比如订单表里只存用户ID用户表里存用户姓名和手机号你要给运营导一份订单用户姓名手机号的清单就必须把两张表连起来查。MySQL内部的做法是先把两张或多张表做笛卡尔积——也就是所有行两两组合一遍再根据你给的ON条件把不相关的组合过滤掉最后投影出你要的列。这个流程听着简单但实际写起来有好几种写法性能差距能到几十倍。这篇内容我会把多表查询里最容易被问倒的两个重点——自连接和子查询——彻底拆开讲明白包括它们解决什么场景、怎么写、为什么这么写、以及实际工作中最容易踩的坑。有SQL基础但没系统整理过这两块的朋友看完应该能形成一套清晰的判断标准什么情况用自连接什么情况用子查询什么情况这俩一起用。2. 连接查询的全景分类多表查询在MySQL里分成两大类连接查询和子查询。连接查询又可以细分为内连接、外连接左外、右外、全外、交叉连接、自然连接其中自连接是一种特殊的内连接或外连接——连接的表是它自己。子查询则是把一条SELECT语句嵌套在另一条SQL里作为条件、作为临时表、甚至作为一列输出。很多教程一上来就扔一堆语法糖什么INNER JOIN、LEFT JOIN、RIGHT JOIN、FULL OUTER JOIN把初学者直接绕晕。我自己带新人的时候习惯先讲一个比喻连接查询就是把两张表当成人你要么只留两边都对得上的人内连接要么以左边的人为准、右边对不上的就补空值左连接要么以右边为准右连接。自连接不过是一个人同时出现在表格的两个角色里——比如员工表里他既是员工角色又是经理角色。子查询的比喻也好理解先查一个小结果集再拿这个结果集去参与外层查询。比如查出分数高于平均分的所有学生你没法一步到位因为平均分本身要先算出来。这时候先写一个子查询算平均分再把每个学生的分数拿去和这个平均值比较两步合在一句话里。下面这张表帮你快速对号入座查询类型作用典型场景内连接只保留两表匹配的行订单关联用户查有效交易左外连接左表全部保留右表不匹配补NULL查出所有用户及其订单没下过单的也要列出右外连接右表全部保留左表不匹配补NULL反向版本用得少自连接同一张表和自己匹配员工-经理、好友关系、连续签到子查询嵌套SELECT结果作为外层条件/数据源比平均值、查最高分、过滤在某集合中大家先把这个框架装进脑子里接下来我们逐个击破。3. 连接查询的启动条件别忽略笛卡尔积写连接查询有个底层机制很多人忽略了——MySQL先做笛卡尔积再过滤。假设A表有100行B表有200行两表连接时MySQL会生成100×20020000行中间结果然后再按照ON条件挑走需要的行。这个中间结果叫做笛卡尔积。如果不写ON条件或者ON条件写得不对就会拿到全部20000行这个叫做交叉连接。实际业务里几乎不会故意用交叉连接但新手写连接时忘了ON或者ON条件写反了就会莫名其妙查出大量重复数据。我见过最经典的翻车现场两张表各有一万多行新人写LEFT JOIN没带ON条件跑出来一亿多行直接把测试库搞卡了。所以写任何连接查询之前心里要先过一遍数据量级并习惯性用EXPLAIN验证一下这个习惯后面我会单独讲。二、自连接一张表和自己的对话1. 为什么需要自连接——树形结构只能用这招自连接这个名字第一次听很玄幻表还能和自己连接 能而且非常常用。凡是一张表内部存在层级、从属、上下游关系的场景都绕不开自连接。最典型的例子就是员工表CREATE TABLE emp ( empno INT PRIMARY KEY COMMENT 员工编号, ename VARCHAR(50) COMMENT 员工姓名, mgr INT COMMENT 经理编号指向empno );在这张表里张三的mgr是李四的empno李四的mgr是王五的empno一个字段既指向本表其他记录又会被本表其他记录指向。这就形成了一种树状结构。要查每个人的直接经理是谁必须把这张表当作两张独立的表来用一张扮演员工表一张扮演经理表然后通过员工.mgr 经理.empno把它们连接起来。这种写法在MySQL里就是标准的自连接SELECT e.ename AS 员工姓名, m.ename AS 经理姓名 FROM emp AS e LEFT JOIN emp AS m ON e.mgr m.empno;注意给表起别名是自连接的关键。如果不写别名MySQL根本分不清e和m分别代表哪一份emp语法直接报错。这里的e和m并不产生数据的复制它们只是同一张表在逻辑上的两个角色。你可以想象成同一个演员在一部电影里分饰两个角色自连接就是让这两个角色同框对话。2. 自连接的两种写法和一个关键差别自连接不一定非要用JOIN关键字老式写法也支持。下面两条SQL效果等价-- 写法一JOIN ON SELECT e.ename, m.ename FROM emp e INNER JOIN emp m ON e.mgr m.empno; -- 写法二逗号WHERE SELECT e.ename, m.ename FROM emp e, emp m WHERE e.mgr m.empno;两种写法的执行计划在MySQL 5.7以上基本没差别优化器会把它们转成同样的执行路径。但从团队协作角度看我建议统一用JOIN ... ON的写法理由是语义更清晰连接条件和过滤条件分开后来维护的人不容易把WHERE里混着多个条件时看懵。老式写法在左连接场景下很容易写出逻辑错误比如漏了WHERE导致出现笛卡尔积。自连接和普通连接最关键的差别在于左外自连接可以保留没有匹配行的数据。还是员工表的例子。假设老板CEO的mgr是NULL因为他没有经理。如果你用内连接老板这一行会被过滤掉因为NULL 任何值都不成立。但老板也是员工业务上查出所有员工和他们的经理就应该把他列出来经理列显示NULL。这个时候必须用LEFT JOIN把左表员工角色的全部行保留下来。实际报表场景里保留全部往往比只保留能对上号的更符合业务预期这也是我推荐默认用LEFT JOIN的原因。3. 自连接实战连续签到和好友关系员工-经理是自连接最经典的案例但如果你以为自连接只用来查上级下级那就低估它了。给你分享两个我在实际项目里用过的场景。第一个是连续签到天数。有一张打卡记录表每个用户每天一条记录。要计算每个用户连续签到的天数SQL写起来很绕但自连接可以帮你解决一个核心问题判断两条记录是否相邻。比如某用户5月1号、5月2号、5月3号都有记录5月4号断签那么5月1号和2号之间只差1天就说明是连续的。自连接把自己和日期偏移1天的记录配对能连续配上的就是一个连续序列。SELECT a.user_id, MIN(a.sign_date) AS start_date, MAX(a.sign_date) AS end_date, COUNT(*) AS continuous_days FROM sign_log a LEFT JOIN sign_log b ON a.user_id b.user_id AND a.sign_date DATE_ADD(b.sign_date, INTERVAL -1 DAY) WHERE b.user_id IS NULL -- 取序列的起点 GROUP BY a.user_id;这里只是展示自连接在相邻关系判断上的思路真正的连续天数还要配合窗口函数或临时表做进一步处理但核心思想就是自连接擅长表达行与行之间的关系而普通查询只能表达行与列的关系。第二个是好友关系。社交应用里通常用一张关系表存好友对为了去重一般约定小的ID放前面大的放后面。要查某个人的所有好友自连接同样能用——把一行记录里的两个ID看成两个角色一个当自己一个当对方。SELECT CASE WHEN f.user_id 1001 THEN f.friend_id ELSE f.user_id END AS friend_id FROM friend_relation f WHERE f.user_id 1001 OR f.friend_id 1001;这个例子不太需要自连接但它背后的思路和自连接完全一致一张表里的多个字段其实代表不同的角色查询时要分别对待。4. 自连接的性能陷阱和索引建议自连接最大的性能问题出在连接条件没走索引。还是员工表如果你对mgr字段没建索引MySQL每取一个员工的mgr值都要去扫描整张表找对应的empno这就是经典的嵌套循环连接Nested Loop Join里最慢的那种情况——外表每行都要全表扫一遍内表。假设员工表有10万行最坏情况下要做10万次全表扫描这个数量级绝对卡死。解决办法非常简单给自连接的关联字段建立索引。上述例子中mgr是外键性质字段虽然不强制建索引但在自连接场景下必须建ALTER TABLE emp ADD INDEX idx_mgr (mgr);同理连接条件里的empno是主键自带索引不用额外处理。还有一个细节经验自连接时尽量让驱动表是小表。MySQL优化器一般会自动选择但在复杂情况下可能选错。你可以通过STRAIGHT_JOIN强制指定连接顺序或者改写SQL让优化器有更准确的统计信息定期ANALYZE TABLE。我个人的经验是先建好索引再看执行计划实在不行再手动干预别一上来就用STRAIGHT_JOIN这种霸王硬上弓的办法。三、子查询把SQL写成分步思考1. 子查询的四种类型一句话记住子查询说得直白点就是嵌套在SELECT/INSERT/UPDATE/DELETE里的另一个SELECT。根据返回结果的样子子查询分成四种类型返回内容使用位置示例标量子查询单个值一行一列SELECT、WHERE、HAVING查平均分和每行比较列子查询一列多行WHERE IN / ANY / ALL查某个部门的员工ID集合行子查询一行多列WHERE 行构造器查部门工资匹配的行表子查询多行多列FROM派生表把子查询结果当临时表再查记的时候不需要死记概念只需要记住一个判断方法你需要在SQL的哪个位置塞进去一个小查询。WHERE后面需要一个值就放标量子查询WHERE后面需要和一堆值比较就放列子查询FROM后面需要一张临时表就放表子查询。举三个最常见的例子。标量子查询最经典的就是查分数高于平均分的学生SELECT student_name, score FROM exam WHERE score (SELECT AVG(score) FROM exam);这里子查询只返回一个数值和外层每一行的score做比较。这个子查询只执行一次效率很高属于非关联子查询。列子查询典型的是查所有在技术部任职的员工SELECT emp_name, dept_id FROM employee WHERE dept_id IN (SELECT dept_id FROM department WHERE dept_name 技术部);子查询返回的是多个dept_id值外层拿自己的dept_id去和这组值逐个比对。只要匹配上任何一个就纳入结果。表子查询典型的是查每个部门工资最高的员工SELECT d.dept_name, t.emp_name, t.max_salary FROM ( SELECT dept_id, MAX(salary) AS max_salary FROM employee GROUP BY dept_id ) t INNER JOIN department d ON t.dept_id d.dept_id INNER JOIN employee e ON e.dept_id t.dept_id AND e.salary t.max_salary;FROM后面的(...) t就是派生表相当于先算一个分组汇总结果再拿这个临时结果去和其他表连接。注意派生表必须起别名这里的t就是它的别名不写直接报错。2. 关联子查询和非关联子查询性能差距很大学完四种类型还得分清楚关联子查询和非关联子查询。这个区分直接决定SQL跑得快还是慢。非关联子查询子查询不依赖外层查询的任何列可以独立执行。上面三个例子都属于非关联子查询MySQL只执行一次子查询拿到结果后再给外层用。这种写法通常很快优化器还能做各种改写。关联子查询子查询里引用了外层表的字段比如SELECT emp_name, salary FROM employee e WHERE salary ( SELECT AVG(salary) FROM employee WHERE dept_id e.dept_id );这个子查询里的e.dept_id是外层表的字段所以子查询必须针对外层每一行都重新计算一次。如果employee表有10万行那个AVG就算10万次。虽然MySQL优化器有时会把关联子查询改写成连接查询但并非所有情况都能改写一旦改写不了性能就非常难看。判断关联还是非关联的技巧很简单看子查询里有没有飘出来的外层表别名。有就是关联子查询没有就是非关联子查询。实际工作中我有一条经验法则能用连接查询JOIN解决的事优先用连接不得不用子查询时优先用非关联子查询。连接查询的执行计划通常更透明、更可控而关联子查询在数据量大时容易成为性能黑洞。后面我们专门用一个章节详细对比。3. 子查询的三个藏身之处SELECT、FROM、WHERE子查询不只是出现在WHERE后面。把它放在不同的位置解决的问题也不同很多教程只讲WHERE导致读者以为子查询只能做条件过滤。放在SELECT后面当作一列输出。比如查员工的同时把每个部门的平均工资也顺带展示出来SELECT emp_name, dept_id, salary, (SELECT AVG(salary) FROM employee WHERE dept_id e.dept_id) AS dept_avg_salary FROM employee e;这里的子查询是关联子查询每行都要执行一次。数据量小的时候没问题数据量大就慎重优先用连接GROUP BY替代。放在FROM后面当作一张临时表。前面查每个部门工资最高的员工已经演示过了。这种用法适合先过滤、汇总、去重再和其他表做关联的场景。要注意的是派生表在MySQL里可能物化成临时表如果数据量大尽量让派生表里的数据先缩小范围避免临时表太大。放在WHERE后面当过滤条件。这是最常见的用法前面也举过例子。WHERE后面可以接着IN、EXISTS、ANY、ALL、比较运算符等组合起来非常灵活。放的位置不同性能特性也不同。放在SELECT后面的一般是最差的——如果一列子查询是关联的外层每行都要跑一次。放在FROM后面则取决于派生表的数据量以及MySQL是否能把派生表合并到外层查询里。放在WHERE后面的情况最复杂要结合具体的操作符分析。4. EXISTS和IN别再用错更慢的那个WHERE后面用子查询时IN和EXISTS是两大主力很多人以为它们等价实际大有讲究。先记住结论当子查询结果是大量数据时IN可能更快当子查询结果很小、外层表很大时EXISTS可能更快。但在MySQL 5.6以后的版本里优化器对IN子查询会做半连接semi-join优化把它转换成类似JOIN的执行方式所以两者在很多场景下差距没那么大了。不过有一个场景必须用EXISTS判断是否存在。比如找出所有下过单的用户用EXISTS的写法是SELECT u.user_id, u.user_name FROM users u WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id u.user_id );子查询里写SELECT 1而不是SELECT *是因为EXISTS只关心有没有记录不关心具体列写SELECT 1更省事也表达得更清楚。这个子查询是关联子查询对每个用户检查一遍他是否有订单记录一旦找到一条就短路不再继续找。IN的等价写法是SELECT u.user_id, u.user_name FROM users u WHERE u.user_id IN (SELECT DISTINCT user_id FROM orders);IN的缺点是如果子查询结果里有大量重复值DISTINCT的去重也要花时间。在orders表很大、user_id重复度极高的情况下EXISTS通常更省。我个人的选择逻辑是这样想要关联判断就用EXISTS想要集合匹配就用IN。EXISTS偏重有没有IN偏重在不在集合里。两者在MySQL优化器面前经常殊途同归但写清楚意图不仅维护的人看得懂优化器也能给出更符合预期的计划。四、自连接和子查询的正面交锋场景对决与性能实测1. 同一个业务两种写法先看一道经典题。有一张课程表course字段包括课程号、课程名、学分、先修课号。要查出每门课程及其先修课程的名称。这道题既可以用自连接也可以用子查询。自连接写法SELECT c1.cname AS 课程名, c2.cname AS 先修课名 FROM course c1 LEFT JOIN course c2 ON c1.pre_course c2.course_id;子查询写法这里更准确地说是相关子查询用来当列输出SELECT c1.cname AS 课程名, (SELECT c2.cname FROM course c2 WHERE c2.course_id c1.pre_course) AS 先修课名 FROM course c1;两条SQL的结果一模一样但执行方式完全不同。自连接只在FROM阶段做了一次连接匹配MySQL先做一次索引查找把所有匹配结果一次性算出来。子查询则是外层每扫到一行课程就跑到course表里按course_id找一次先修课名。数据少时感受不到差异课程表到几万行时自连接的优势就非常明显了。所以我的建议是查同表关联信息优先自连接。子查询在需要一次计算再比的场景里更合适比如查出学分高于平均学分的课程因为平均学分必须先算出来子查询天然表达这个先算后比的流程。2. 优化器的改写规则别你以为你以为的就是你以为MySQL的优化器很聪明它不一定按照你写的SQL字面意思去执行。最典型的就是子查询会被改写成连接查询。比如下面这个子查询写法SELECT * FROM employee WHERE dept_id IN (SELECT dept_id FROM department WHERE dept_name 技术部);MySQL 8.0会把IN子查询改写成半连接semi-join执行计划和直接写JOIN非常接近SELECT e.* FROM employee e INNER JOIN department d ON e.dept_id d.dept_id WHERE d.dept_name 技术部;注意半连接和普通连接有个重要区别半连接不会重复匹配。如果department表中存在多个技术部记录IN子查询的结果集不会让employee里的某个人出现两次而普通INNER JOIN可能会出现重复行需要加DISTINCT。这也是为什么优化器要做半连接而不是直接转成完整JOIN的原因——要保持语义一致。但是关联子查询的改写路径就少得多。尤其是那种外层每行算一次的关联子查询如果无法转换为连接查询就只能老老实实一行一行跑。我在大数据量场景下遇到过好几次这种性能问题最后都是手动改成JOIN才救回来。所以要学会用EXPLAIN看执行计划。看到DEPENDENT SUBQUERY依赖子查询或者UNCACHEABLE SUBQUERY时就要警惕这可能是个性能隐患。看到MATERIALIZED物化或SEMI JOIN说明优化器已经帮你改写成更优的方案了。3. 实际选择先业务语义再性能优化到底用自连接还是子查询我给的判断标准分两步。第一步看业务语义。如果你要表达的本来就是同一张表内部的关系员工-经理、课程-先修课、节点-父节点自连接是天然语义匹配。如果你要表达的是先算一个中间值再拿它做比较/过滤子查询更符合直觉。比如查工资高于公司平均工资的员工——平均工资就是中间值用子查询自然就写出来了。第二步看性能特征。当两种写法都能表达时默认先试自连接跑EXPLAIN看行数如果子查询的写法可读性明显更好而且子查询结果很小也比较安全。经验数据给我的是小表几百到几千行随便写大表几十万行以上要谨慎。给大家一个实操决策表场景推荐方案原因员工经理、父子节点、好友关系自连接语义自然一次匹配索引友好查高于平均值/最大值的记录标量子查询需要先聚合再比JOIN反而绕判断是否有子记录存在EXISTS子查询短路判断效率最优查某集合里的成员IN子查询或半连接优化器支持好写法直观多表汇总后再关联FROM派生表先缩小范围再连接五、实操中的高频坑和排查技巧1. WHERE IN (子查询) 遇到NULL的幽灵数据这个坑我自己踩过也见无数新人踩过。先看例子SELECT emp_name FROM employee WHERE dept_id NOT IN ( SELECT dept_id FROM department WHERE manager IS NULL );如果子查询的结果集里包含NULLNOT IN会一个结果都查不出来。原因不复杂SQL里x NOT IN (a, b, NULL)等价于x ! a AND x ! b AND x ! NULL而x ! NULL的结果是UNKNOWN不是TRUE整条AND链全部变成UNKNOWNWHERE就过滤掉了所有行。处理办法有两个一是子查询里显式排除NULLSELECT dept_id FROM department WHERE manager IS NULL AND dept_id IS NOT NULL二是改用NOT EXISTS它对NULL天然免疫SELECT emp_name FROM employee e WHERE NOT EXISTS ( SELECT 1 FROM department d WHERE d.dept_id e.dept_id AND d.manager IS NULL );规则总结成一句话看到NOT IN马上检查子查询结果可能不可能有NULL有NULL立刻换NOT EXISTS。IN本身遇到NULL不会出问题只是匹配不到NULL因为x IN (a, NULL)只要x等于a就能匹配上只有两边都是NULL时才匹配不到因为等号判断NULL需要IS NULL而不是。2. 派生表不写别名直接报错FROM后面的子查询必须要有别名这是MySQL的硬性语法要求。新手写这种SQL最容易漏SELECT * FROM (SELECT dept_id, AVG(salary) AS avg_sal FROM employee GROUP BY dept_id);直接报错Every derived table must have its own alias。解决办法很简单加个别名SELECT * FROM ( SELECT dept_id, AVG(salary) AS avg_sal FROM employee GROUP BY dept_id ) AS t;顺便说一个派生表的隐藏知识点派生表的列名可以被外层引用因此别名既是语法要求也是后续引用的入口。上面例子里的t.avg_sal就是拿子查询算出来的平均值去参与外层计算。3. 排序和分页遇上子查询坑更多分页是业务系统里绕不开的。多表查询分页最容易出现的问题是先连表再分页或者先分页再连表顺序反了会导致数据错乱或性能爆炸。举一个具体场景。查每个部门最近入职的5个员工。很多人会写成SELECT e.*, d.dept_name FROM employee e INNER JOIN department d ON e.dept_id d.dept_id GROUP BY e.dept_id ORDER BY e.hire_date DESC LIMIT 5;这个SQL逻辑是错的LIMIT 5是对整个结果集取前5行不是每个部门取5个。正确做法是用窗口函数或关联子查询把每个部门的前5名这个逻辑先算出来SELECT e.*, d.dept_name FROM ( SELECT e.*, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY hire_date DESC) AS rn FROM employee e ) e INNER JOIN department d ON e.dept_id d.dept_id WHERE e.rn 5;MySQL 8.0以上才支持窗口函数。如果还在用5.7那只能靠关联子查询或临时表实现代码会麻烦很多。再提醒一个分页排序的隐藏坑ORDER BY的列如果不在SELECT列表里且使用了DISTINCTMySQL会报错。这个在多表查询中非常常见因为SELECT里只保留了业务需要的列排序却按别的列排。报错信息大概长这样Expression #1 of ORDER BY clause is not in SELECT list。解决办法是把排序字段也加进SELECT列表或者去掉DISTINCT。4. 连接和子查询混用时的执行计划解读复杂业务往往不是简单的自连接或者子查询二选一而是混合使用。比如查出工资高于本部门平均工资的员工并显示部门名称你既要连接部门表又要用子查询算部门平均工资SELECT e.emp_name, e.salary, d.dept_name FROM employee e INNER JOIN department d ON e.dept_id d.dept_id INNER JOIN ( SELECT dept_id, AVG(salary) AS avg_sal FROM employee GROUP BY dept_id ) t ON e.dept_id t.dept_id WHERE e.salary t.avg_sal;这种SQL的排查重点就两个派生表t有没有被物化MATERIALIZED连接顺序是不是先小表后大表。经常用EXPLAIN一看t被物化成临时表里面有上万行再和employee连接效率很差。优化手段是把GROUP BY子查询改成窗口函数8.0或者把关联条件改成等值连接让优化器有更多选择。我一般建议团队里定一条规矩所有多表查询上线前必须EXPLAIN一次重点看type列有没有ALL全表扫描、key列用没用上索引、Extra列有没有Using temporary / Using filesort。只要看到Using temporary就说明中间结果被物化成临时表了这通常是性能恶化的预警信号。5. MySQL 8.0的优化新特性窗口函数和LATERAL最后补充一个8.0新特性和子查询、自连接都有关系。MySQL 8.0引入了窗口函数和LATERAL派生表这让很多原本要靠子查询或自连接才能写出来的逻辑变得简洁许多。前面的每个部门工资前5用窗口函数已经演示过了。LATERAL则允许派生表引用同级别FROM子句中前面表的列相当于横向关联的子查询。比如SELECT d.dept_name, t.emp_name, t.max_salary FROM department d LEFT JOIN LATERAL ( SELECT emp_name, salary AS max_salary FROM employee e WHERE e.dept_id d.dept_id ORDER BY salary DESC LIMIT 1 ) t ON TRUE;这个SQL查出每个部门工资最高的员工。LATERAL子查询里直接引用了外层d.dept_id不需要额外的临时表或复杂的GROUP BY子查询逻辑表达非常直接。但要注意LATERAL也不是万能的。它本质上是关联子查询的变体执行时对外层表的每一行都会执行一次外层表数据量大时性能同样堪忧。它最大的价值是让代码可读性大幅提升在数据量可控的报表场景里非常好用。六、写在最后多表查询、自连接和子查询其实是一套完整的SQL思维训练。我自己带团队时常说一句话写SQL最难的不是语法而是把业务问题翻译成数据之间的关系。自连接教会你一张表内部也有关系子查询教会你查询可以分步思考两者配合使用才能写出既正确又高效的SQL。说几个我个人的实操习惯供大家参考。第一每条多表SQL写完先跑EXPLAIN。看连接顺序、看索引使用、看有没有临时表。这不是形式主义是真能救命的习惯。我至少三次靠EXPLAIN在测试环境发现了生产级别的问题。第二优先保证语义正确再谈性能。很多人一上来就纠结IN和EXISTS哪个快、自连接还是子查询哪个好结果SQL写错了都不知道。先把结果跑对然后才去优化。数据量小的场景两种写法差距就是几十毫秒没必要为了这点时间牺牲可读性。第三给关联字段建索引是性价比最高的一步。不管是自连接的mgr字段还是子查询里被引用的外键字段加个索引性能立刻天壤之别。一个小索引往往比折腾半天SQL改写还管用。如果你正准备面试把自连接的员工-经理案例、子查询的四种类型和IN/EXISTS区别练熟基本够应付大部分多表查询题目。如果在实际项目中遇到更复杂的场景比如多级嵌套的自连接查员工的经理的经理或者子查询和GROUP BY混合使用可以在评论区留言我看到会尽量回复。SQL这条路上踩坑是常态但每踩一个坑你对数据的理解就深一层。
返回列表