从入门到实战:SQL多表查询与子查询,一次讲透关联逻辑与性能陷阱
干这行久了,你会发现一个特别有意思的现象:很多人写单表查询特别溜,一遇到多表关联就抓瞎,要么表连接把数据搞出好几倍,要么子查询嵌套三层直接跑挂数据库。而面试的时候,SQL多表查询和子查询几乎又是必考项,从初级到高级,总能问出点东西来。说白了,多表查询和子查询不只是语法问题,它背后考验的是你对数据关系的理解,对关联逻辑的把握,以及对性能的敏感度。这篇内容,我就把这两块硬骨头从头到尾掰开揉碎了讲一遍,所有案例都是我在实际开发中真实用过的,你直接拿去套用都行。
不管你是刚入行的新人,还是写了两年SQL想系统梳理一下的开发者,这篇文章都适用。我会先讲清楚多表查询的底层逻辑,再逐个拆解JOIN类型和子查询形态,最后用完整的业务案例带你走一遍从需求到SQL的全过程,顺手把那些经常让人翻车的性能坑也一并交代了。
1. 多表查询的设计思路与底层原理
1.1 为什么数据要拆成多张表
很多初学者会有个疑问:数据明明可以放一张表里,为什么非要拆开?这个问题的答案,实际上就是多表查询存在的根本原因。
以电商系统为例,如果所有数据放一张表,一个订单可能包含三个商品,那么这张表的每一行就得重复写三遍订单信息——用户名称、收货地址、下单时间、商品名称、商品价格,全得跟着重复。这带来的直接后果有三个:第一是存储浪费,大量冗余数据白白占空间;第二是更新困难,如果用户改了收货地址,所有历史订单行都要同步改,漏掉一行就产生脏数据,这就是典型的更新异常;第三是产生一致性问题,同一个订单的数据散落在多行,统计时很容易被重复计数。
所以正规的数据库设计会遵循范式理论,把不同粒度的数据拆到不同的表里。订单基本信息放一张表,订单明细放一张表,用户信息放一张表,商品信息放一张表。每张表只关注自己这个维度的数据。查询的时候,再通过关联条件把它们重新组合起来。拆是设计手段,查是应用需求,而多表查询,就是连接这两者的那座桥。
1.2 JOIN的本质:笛卡尔积加过滤条件
多表查询的核心操作是JOIN。很多教材喜欢直接扔语法,不说原理,导致很多人只知道“JOIN就是连表”,但不知道为什么连完是这个结果。
JOIN的底层逻辑其实可以分解成两步。第一步,把参与连接的两张表做笛卡尔积,也就是说,左表的每一行都会和右表的每一行组合一次。比如左表有100行,右表有50行,笛卡尔积就是100乘50,总共5000行。第二步,根据你写的连接条件,从这5000行里把满足条件的行筛选出来。
这样理解就能明白很多现象了。为什么连接条件写错了会出现数据爆炸?因为笛卡尔积本身就是全组合,连接条件只是用来过滤的。如果连接条件写得不严谨,或者压根没写,那数据库无法过滤,只能把5000行全部返回。这就是所谓的笛卡尔积灾难。我在排障时见过很多次,一条本来是关联查询的SQL,因为漏写了JOIN的ON条件,或者把条件误写到了WHERE里,结果返回了百万行,直接把应用端搞挂了。
打个比方,两张表就像社团的两个成员名单,笛卡尔积相当于把所有成员两两配对,每对组合都写出来。你想要的只是“同一个部门的人”配对,那连接条件就是“部门编号相等”这个筛选规则。
1.3 驱动表与被驱动表的执行逻辑
聊到多表查询,就绕不开驱动表这个概念。很多性能优化文章都会提到“小表驱动大表”,但不少人只是记住了这句话,并不理解背后的执行机制。
MySQL里,多表JOIN最常见的执行方式是嵌套循环连接(Nested-Loop Join)。拿两表连接举例,数据库会先选一张表作为驱动表,遍历它的每一行,然后拿着这一行的关联字段值,去另一张表(被驱动表)里查找匹配的行。
这里的性能关键点在于:被驱动表的关联字段有没有索引,直接影响查找速度。有索引,相当于查字典,直接翻到对应页;没索引,相当于把整本书从头到尾翻一遍找某个字。举个例子,A表join B表,A表1万行,B表10万行。如果B表的关联字段有索引,循环1万次,每次都是索引查找,速度很快。如果没索引,每次都要全表扫描10万行,总扫描量就是1万乘以10万,等于1亿行次的比较,这可不是闹着玩的。
所以在实际工作中,我优化JOIN的优先级排序是:先看被驱动表的关联字段有没有索引,再看驱动表是否足够小,最后才是检查SQL语句本身的写法。顺序反了,优化效果会大打折扣。有人一上来就改SQL逻辑,把子查询改成JOIN,结果索引没建,照样慢,等于白折腾。
2. 四种JOIN类型:适用场景与避坑细节
2.1 INNER JOIN:只取交集,日常使用最频繁
INNER JOIN(内连接)是三表查询里用得最多的类型。它只返回两个表中连接条件匹配成功的行。也就是说,左边有但右边没有的,右边有但左边没有的,统统不显示。
什么时候用INNER JOIN?当你要查的数据要求“两边都存在”时。比如查“有过订单的用户信息”,那用户没有订单就不需要出现在结果里。用INNER JOIN,连接条件就是用户表的用户ID等于订单表的用户ID,结果里只有那些下过单的用户。
语法上,INNER JOIN有两种写法。一种是显式的JOIN写法:
SELECT u.user_name, o.order_no, o.amount FROM user_info u INNER JOIN order_info o ON u.user_id = o.user_id;另一种是隐式的逗号写法:
SELECT u.user_name, o.order_no, o.amount FROM user_info u, order_info o WHERE u.user_id = o.user_id;两种写法结果一样,但我强烈建议用显式JOIN写法。原因很简单,显式写法把连接条件和过滤条件分开了,ON后面管连接,WHERE后面管过滤,逻辑边界清晰。隐式写法全混在WHERE里,语句一长,哪些是连接条件哪些是过滤条件根本分不清,后期维护非常痛苦。还有个更现实的理由:隐式写法一旦忘了WHERE条件,直接就变成了笛卡尔积,而显式写法如果忘写ON,数据库也会报错提醒。
2.2 LEFT JOIN:左表全保留,注意NULL陷阱
LEFT JOIN(左连接)的语义是:返回左表的全部行,右表只返回匹配成功的行,如果右表没有匹配上,就用NULL填充。
什么时候用LEFT JOIN?场景很典型:比如查“所有用户及其订单信息”,注意这里的核心是“所有用户”,哪怕用户没下过单,也要出现在结果里,订单信息显示为NULL。这类“以某张表为主体,其他表信息可有可无”的需求,就是LEFT JOIN的主场。
但LEFT JOIN有个经典陷阱,必须注意:连接条件应该写在ON里,而不是WHERE里。很多新手喜欢把所有条件一股脑丢进WHERE,这在INNER JOIN里无所谓,但在LEFT JOIN里会引起严重逻辑错误。
举个例子,查所有用户以及他们有效订单的金额:
SELECT u.user_name, o.amount FROM user_info u LEFT JOIN order_info o ON u.user_id = o.user_id AND o.status = '有效';这样写,用户A即使订单全是无效的,也会出现在结果里,ORDER字段为NULL,因为ON里的条件是“匹配”的规则。但如果把订单状态条件挪到WHERE里:
SELECT u.user_name, o.amount FROM user_info u LEFT JOIN order_info o ON u.user_id = o.user_id WHERE o.status = '有效';结果就完全变了,用户A没有有效订单,那一行会被WHERE过滤掉,整个查询实际上就等同于INNER JOIN的结果了。这就是所谓的“LEFT JOIN被WHERE变成INNER JOIN”,是实际开发中非常隐蔽的坑。要判断自己有没有踩坑,可以查一下结果行数和左表行数是否一致,不一致多半是WHERE把NULL行给过滤掉了。
2.3 RIGHT JOIN与FULL JOIN:冷门但要知道
RIGHT JOIN(右连接)的语义跟LEFT JOIN完全对称,返回右表的全部行,左表没匹配上就用NULL填充。从逻辑上讲,把两张表的顺序换一下,用LEFT JOIN就能实现RIGHT JOIN的效果。所以我个人建议统一使用LEFT JOIN,因为人阅读SQL的习惯是从左往右的,把主体表放前面更符合直觉,两张表以上的连接语句尤其如此,全用LEFT JOIN的代码总体读起来也更顺畅。
FULL JOIN(全外连接)返回两表的全部行,匹配不上的一侧填NULL。它在MySQL 5.x及之前的版本里并不支持,所以想用FULL JOIN只能另想思路,把LEFT JOIN和RIGHT JOIN的结果用UNION合并起来。实际业务里,完全外连接的场景不算多,比如对账场景需要把两边的差异数据全部找出来,这时候才比较有用。
还有一类特殊的连接叫CROSS JOIN,也就是交叉连接,不带任何条件。它返回的是纯粹的笛卡尔积,日常开发里一般用不到,但在需要生成测试数据、做笛卡尔累积计算时,可以派上用场。
2.4 ON与WHERE的边界:一张表说清楚
多表查询里,ON和WHERE的区别是面试高频问题,也是实际写错的高发地带。我整理了一下它们的职责边界:
| 类型 | ON子句的定位 | WHERE子句的定位 | 错误后果 |
|---|---|---|---|
| INNER JOIN | 连接匹配规则 | 结果集过滤 | 逻辑上等价,但语义混乱 |
| LEFT JOIN | 连接匹配规则 | 连接完成后的最终过滤 | WHERE误过滤NULL行,拉平为INNER JOIN |
| RIGHT JOIN | 连接匹配规则 | 连接完成后的最终过滤 | 同LEFT JOIN,方向相反 |
| FULL JOIN | 连接匹配规则 | 连接完成后的最终过滤 | WHERE会把未匹配行全部杀掉 |
原则就一条:连接条件放ON,结果过滤放WHERE。LEFT JOIN场景下,如果想要右表某个字段的过滤条件不影响左表全保留,就必须把这个条件写在ON里,否则就等着结果悄悄变少吧。
3. 子查询的四种形态与实战用法
3.1 标量子查询:返回单个值的便携计算器
子查询就是嵌套在另一个查询里的查询。如果子查询返回的结果是单个值,也就是一行一列,那它就是标量子查询,可以在SELECT列表、WHERE条件、甚至HAVING条件里直接使用。
实际业务里最常见的就是在SELECT列表里算“附带字段”。比如,我想查每个用户的名称,以及他们的最近一次下单时间:
SELECT u.user_name, (SELECT MAX(o.order_time) FROM order_info o WHERE o.user_id = u.user_id) AS last_order_time FROM user_info u;这个查询里,每一行用户数据产生后,数据库都会执行一次这个标量子查询,找到该用户的最大下单时间。标量子查询有个必须遵守的约束:子查询必须返回且只能返回一个值。如果某个用户存在多条匹配记录导致子查询返回多行,数据库会直接报错。所以,子查询里常用MIN、MAX、COUNT这样的聚合函数来确保返回单值,或者用LIMIT 1限制行数。
3.2 列子查询:IN与NOT IN的经典搭档
列子查询返回的是单列多行,最典型的用法就是和IN、NOT IN搭配。
需求:查“下过订单的用户姓名”。可以先在订单表里找出所有有订单的用户ID,再拿着这批ID去用户表里筛人:
SELECT user_name FROM user_info WHERE user_id IN (SELECT user_id FROM order_info);这里子查询返回的就是一列user_id,可能有多个值,外层查询根据这些值做匹配。
列子查询里有几个容易踩的雷。第一个是IN列表里有NULL值。如果子查询结果中包含NULL,用NOT IN的时候要特别小心,因为NULL参与比较的结果是UNKNOWN,最终会导致整个查询返回空结果。比如NOT IN (1, 2, NULL),对所有行来说,判断结果都是UNKNOWN,一条都不会返回。这是个非常经典的坑,遇到NOT IN查不出数据,优先检查子查询里有没有NULL。
第二个是性能问题。当右表的数据量很大时,IN子查询的优化可能不如EXISTS。但在数据量小的时候,IN写法更简洁直观。我会在后面专门讲IN和EXISTS怎么选。
3.3 行子查询与表子查询:更高级的组合拳
行子查询返回一行多列,可以和元组比较一起用。比如查“和某用户同城市且同龄的其他用户”:
SELECT user_name FROM user_info WHERE (city, age) = (SELECT city, age FROM user_info WHERE user_name = '张三');这种写法用一个比较符同时比较多个字段,逻辑很紧凑,但实际工作中用得不多。想一下就能明白,一旦字段多一点,或子查询结果为空,问题就会变得复杂。
表子查询则是在FROM子句里嵌一个查询结果作为临时表。比如先按月份汇总订单金额,再从中筛选月份总额超过十万的月份:
SELECT month_id, total_amount FROM ( SELECT DATE_FORMAT(order_time, '%Y-%m') AS month_id, SUM(amount) AS total_amount FROM order_info GROUP BY DATE_FORMAT(order_time, '%Y-%m') ) t WHERE total_amount > 100000;表子查询的精髓在于逻辑分层:先做内层计算,得到中间结果,再基于这个中间结果做外层处理。这在处理多级聚合、排名、同比环比这类复杂需求时极其好用。有一点要赘述:FROM子句里的表子查询必须起别名,这是SQL语法的硬性要求。有人常问为什么要别名,因为子查询结果本身没有表名,起别名的目的就是让外层能引用它。
3.4 EXISTS与IN:性能分水岭与语义差异
EXISTS和IN都能实现“存在性判断”,但执行逻辑完全不同。IN是先执行子查询,把结果集生成一张临时表,然后外层查询逐一匹配。EXISTS则不同,它对外层查询的每一行,去执行一次子查询,只要子查询能返回至少一行,就判定为真,根本不管返回的是什么内容。
性能上怎么选?取决于内外表的数据量。传统的经验法则是:外表小、内表大时,用EXISTS;外表大、内表小时,用IN。原因在于,EXISTS是“外部驱动,逐行探测”,适合内表数据量大的场景;IN是“内部驱动,生成结果再匹配”,适合内表结果集较小的场景。
但是,这里必须强调一点,现在的优化器越来越智能,MySQL 5.6之后对IN和EXISTS都有改写优化,很多场景下两者的执行计划已经在内部被等价转换了。所以实际工作中不要盲信“EXISTS永远比IN快”的说法。真正靠谱的做法是看执行计划,EXPLAIN一下,看谁用了索引、谁在走全表扫描、谁被物化成临时表,这一眼就能看出来。
另外,语义上EXISTS还有个优势,它不关心子查询里返回了什么字段,只关心有没有行。所以子查询里写SELECT 1就行,不必真的把某个字段查出来,这样既能表达意图也省去无谓的取值开销。
4. 实战案例:从业务需求到SQL的完整拆解
4.1 场景设定:电商订单报表
理论说再多,不如跑一个实际案例。我设计一个电商场景,四张表,覆盖多表查询和子查询的大多数典型用法。
第一张是用户表user_info,字段有user_id、user_name、city、reg_date。第二张是商品表product_info,字段有product_id、product_name、price、category。第三张是订单表order_info,字段有order_id、user_id、order_time、status,状态分为有效和取消。第四张是订单明细表order_item,字段有item_id、order_id、product_id、quantity。
四张表的关系是:用户一对多订单,订单一对多明细,商品通过明细和订单发生关联。
4.2 需求一:查询每个用户最近一笔有效订单的商品清单
这个需求要把四张表全用上。思路是先把有效订单按用户排序,找每个人的最近订单,再去关联明细和商品。
第一步,查每个用户的最近有效订单。这一步用关联子查询实现:
SELECT o1.user_id, o1.order_id, o1.order_time FROM order_info o1 WHERE o1.status = '有效' AND o1.order_time = ( SELECT MAX(o2.order_time) FROM order_info o2 WHERE o2.user_id = o1.user_id AND o2.status = '有效' );这里有个细节:为什么不用GROUP BY + MAX直接做?因为GROUP BY只能得到每个用户的最近下单时间,拿不到对应的order_id。通过关联子查询,可以精确锁定“时间等于该用户最大值”的那些订单。如果同一用户在同一秒下了两单,这个写法会把两单都查出来,结果更准确。
第二步,把上面查询的结果当作临时表,与商品明细、商品表连接:
SELECT t.user_id, p.product_name, oi.quantity, p.price FROM ( SELECT o1.user_id, o1.order_id, o1.order_time FROM order_info o1 WHERE o1.status = '有效' AND o1.order_time = ( SELECT MAX(o2.order_time) FROM order_info o2 WHERE o2.user_id = o1.user_id AND o2.status = '有效' ) ) t INNER JOIN order_item oi ON t.order_id = oi.order_id INNER JOIN product_info p ON oi.product_id = p.product_id ORDER BY t.user_id;这种“子查询先圈定主表范围,再用JOIN拉取明细字段”的组合方式,是我在多表查询中最常用的套路。子查询负责精确筛选主体,JOIN负责扩展信息广度,各司其职,SQL理解起来非常顺。
4.3 需求二:统计每个品类的销售总额并排出前三
这个需求涉及聚合、多表连接、还有排序取前N。先说统计每个品类的销售总额。销售额的算法是单价乘以数量求和。因为价格存在商品表里,数量在明细表里,所以要先连接两张表再聚合:
SELECT p.category, SUM(p.price * oi.quantity) AS total_sales FROM order_item oi INNER JOIN product_info p ON oi.product_id = p.product_id INNER JOIN order_info o ON oi.order_id = o.order_id WHERE o.status = '有效' GROUP BY p.category;为什么要多连接一次订单表?因为要过滤掉取消的订单。如果直接把状态条件挂在商品或者明细表上,语义会出错。
接下来要取前三。SQL Server可以直接用SELECT TOP,MySQL用LIMIT,Oracle用ROWNUM。但这里有个隐藏需求:如果两个品类销售额一样,排名怎么算?简单LIMIT 3会漏掉并列的品类。严谨一点的写法是:
SELECT p.category, SUM(p.price * oi.quantity) AS total_sales FROM order_item oi INNER JOIN product_info p ON oi.product_id = p.product_id INNER JOIN order_info o ON oi.order_id = o.order_id WHERE o.status = '有效' GROUP BY p.category ORDER BY total_sales DESC LIMIT 3;如果业务上接受并列,那是另一个更复杂的窗口函数场景。这里我只说一句:LIMIT取TopN适合并列无所谓的报表,如果要处理并列,请用DENSE_RANK窗口函数。窗口函数虽然不属于本次主题,但既然热搜词里出现了SQL窗口函数,后面有机会我会单独写一篇,这里不展开。LIMIT 3这个分页截断虽然简单,但要记得:ORDER BY字段如果不唯一,分页结果是不可预期的。
4.4 需求三:查找从未下过单的用户
这个需求可用的写法很多,我列三种,每种都有不同的适用场景。
第一种,LEFT JOIN + IS NULL,先连接,再找右表为空的行:
SELECT u.user_id, u.user_name FROM user_info u LEFT JOIN order_info o ON u.user_id = o.user_id WHERE o.order_id IS NULL;LEFT JOIN把没下过单的用户也保留下来,订单表字段为NULL,然后用IS NULL过滤。这是最常用的写法,但要注意,如果用户表非常大,LEFT JOIN会产生大量中间行,性能上可能不如NOT EXISTS。
第二种,NOT EXISTS:
SELECT u.user_id, u.user_name FROM user_info u WHERE NOT EXISTS ( SELECT 1 FROM order_info o WHERE o.user_id = u.user_id );这种写法逻辑很直白:“不存在任何一个他的订单”。加上关联字段有索引的话,性能通常是三种方案里最好的,因为一旦找到匹配行,子查询就会立刻停止扫描。
第三种,NOT IN,也就是前面提到有NULL陷阱的写法:
SELECT user_id, user_name FROM user_info WHERE user_id NOT IN (SELECT user_id FROM order_info);只要order_info里user_id列没有NULL,这条语句在逻辑上就没有问题。但如果有NULL,这条查询会返回空结果。所以我的建议是,生产环境里保守一点,优先使用NOT EXISTS。这三种写法语义相同,但细节差异能决定成败,这也是多表查询有意思的地方。
5. 多表查询的典型故障与性能优化实录
5.1 数据翻倍排查:连接条件缺失和一对多连接
多表查询里最让人头疼的就是结果行数不对。你要查100个用户的订单,结果返回了280行,怎么回事?
最常见的原因是连接字段有重复值。比如user_info表的user_id是唯一的,但如果你拿user_name做连接条件,而两个用户重名,就会产生一对多的匹配,每个匹配都会生成一行结果,行数翻倍。更隐蔽的情况是,多张表层层JOIN时,某张表的关联字段不是唯一的,中间某个环节放大了行数,后续连接全部跟着放大。
排查的方法很简单:把SELECT后面的字段改成只查主表的唯一键,然后看结果有没有重复。如果主表的user_id出现了多次,说明连接层级里有数据膨胀。逐步去掉参与连接的中间表,二分查找膨胀点。
另外,建议在做一对多连接之前,先用GROUP BY或DISTINCT确认关联字段的唯一性。如果你已经预判到明细表会对主表造成行数放大,可以在聚合场景中把JOIN放到子查询之后再进行,或者先把明细表汇总成一行。
5.2 关联字段类型不一致引发的隐式转换
这个坑我掉进去过不止一次。表A的user_id是字符型,表B的user_id是整数型,直接用等号关联:
SELECT ... FROM A INNER JOIN B ON A.user_id = B.user_id;数据库会对其中一个字段做隐式类型转换。坏消息是,一旦字段上套了函数,索引就可能失效,原本能走索引的关联变成全表扫描。经验是:设计表时,所有跨表关联字段必须保持类型一致。如果是已经存在的历史表,无法修改类型,那就把转换明确写出来,并用CAST函数统一类型,虽然仍然可能影响索引,但至少语义清晰可维护。
顺带提一个相关的问题:字符集和排序规则不一致,也会导致JOIN无法使用索引,甚至直接报错。比如一张表的字段是utf8mb4_general_ci,另一张表是utf8mb4_unicode_ci,关联时MySQL会判定两个字段不兼容而无法走索引。排查这类问题,用EXPLAIN看一下key列是不是NULL,就能快速发现。
5.3 依赖子查询的慢SQL优化实战
有一次接手一个报表任务,查询跑了整整四分钟。简化后的SQL长这样:
SELECT ... FROM order_info o WHERE o.user_id IN ( SELECT user_id FROM user_info WHERE city = '某市' );乍一看没毛病,子查询先筛出某市的用户ID,再查订单。但EXPLAIN一看,user_info全表扫描,order_info全表扫描,两个表都没有走索引。原因是user_info表的数据量太大,而city字段没有索引,子查询的结果集也大,外层IN匹配效率极低。
优化思路分两步。第一步,给user_info.city加上索引,让子查询快速圈定一批用户;第二步,把IN改写为EXISTS:
SELECT ... FROM order_info o WHERE EXISTS ( SELECT 1 FROM user_info u WHERE u.user_id = o.user_id AND u.city = '某市' );改写后,外层订单表走了user_id索引,每次探测用户表时也通过主键关联加city过滤,更快。同样的业务结果,从四分钟降到了十几秒。
这个案例告诉我们一个道理:慢SQL优化不能只盯着SQL本身,要结合索引设计、数据分布和执行计划综合判断。先看执行计划,再动手改SQL,顺序不要颠倒。
5.4 常见问题速查表
我把这些年遇到过的高频问题整理成一张速查表,方便你排查时对照:
| 现象 | 可能原因 | 快速验证方式 | 解决方案 |
|---|---|---|---|
| 结果行数远超预期 | 连接字段有重复或漏写条件 | 只查主表唯一键,看是否重复 | 检查关联字段唯一性,补连接条件 |
| LEFT JOIN查出少行 | WHERE过滤了NULL行 | 把右表过滤条件移到ON里测试 | 将条件从WHERE迁移到ON |
| NOT IN查不出数据 | 子查询结果里包含NULL | 用SELECT COUNT(*)查NULL行数 | 改用NOT EXISTS |
| 关联查询走全表扫描 | 字段类型不一致触发隐式转换 | EXPLAIN查看type列 | 统一字段类型或显式CAST |
| 加索引后仍慢 | 字符集或排序规则不一致 | EXPLAIN查看key列 | 统一字符集与排序规则 |
| 子查询嵌套太深 | 性能与可读性双双失控 | 从最内层单独运行验证 | 把子查询抽成临时表或改写JOIN |
5.5 关于EXPLAIN的执行计划观察要点
会看EXPLAIN,才算真正入了SQL优化的门。我习惯先看三个关键列:type、key、rows。
type列展示了访问类型,优化目标是从all(全表扫描)到index(全索引扫描)再到range/ref/eq_ref/const,访问效率逐步变优。看到all和index,就要警觉。key列显示实际用到的索引,如果是NULL,说明索引没生效。rows列是预估扫描行数,数值越大越危险,优化后对比这个数字,能直观看到改写的效果。
关于多表查询,还有一个额外提醒。多表JOIN的EXPLAIN结果会显示多行,每行对应一张表的执行计划,执行顺序从上到下。第一行是驱动表,后续是被驱动表。检查的时候尽量看看每行是否都走索引了,如果某一张被驱动表的rows特别大,就优先优化它。
多表查询和子查询这块内容,能写的东西实在太多。我在实际项目里踩过的坑和总结的经验,远不止这些。最后再给大家几个实用锦囊:第一,写复杂的多表关联之前,先在草稿纸上画一下表间关系和关联层级,理清哪个是主表哪个是明细表,避免在SQL里来回折腾;第二,凡是涉及LEFT JOIN,写完以后立刻查一下结果集行数是否等于主表行数,这是个一秒就能发现的逻辑自检;第三,子查询能少嵌套就少嵌套,嵌套超过两层建议用WITH公共表表达式或临时表代替,不然性能难控,改起来更是想骂人。SQL是一项经验学科,写得多了,踩坑踩得多了,自然会形成肌肉记忆。
希望这篇关于SQL多表查询与子查询的实战分享,能帮你在日常开发和面试中少走几步弯路。如果有具体的业务场景不知道怎么改写,欢迎留言交流,我们评论区见。