简介:MySQL多表联合查询效率分析及优化是一份面向数据库开发者与运维人员的PDF技术文档,旨在解决多表关联查询中结果集膨胀、响应缓慢等常见问题。资源系统梳理了笛卡尔积、内连接、左/右外连接等连接类型的工作原理与适用场景,并结合实际案例说明索引使用、EXPLAIN执行计划分析、LIMIT限制结果集、子查询替代连接、临时表与UNION优化等多项策略,同时涵盖通过USE INDEX/IGNORE INDEX提示索引、避免OR条件导致索引失效等实用技巧。文档共1个PDF文件,压缩包仅81KB,内容紧凑、可直接对照实践。目前已有5456人学习下载,适合正在排查慢查询、优化复杂报表SQL或准备面试的中高级MySQL使用者。通过阅读这份笔记,读者可以快速建立多表查询的优化框架,掌握从连接类型选择到执行计划调优的完整思路,避免全表扫描并提升查询性能。
1. 为什么多表联合查询会拖垮 MySQL:先看这两类慢 SQL
你在处理 mysql 慢查询日志时,十有八九会遇到多表联合查询:一条看似正常的 left join,线上跑了几秒才出结果;一个嵌套子查询,测试环境数据量小没感觉,复制到生产直接打满连接池。多表联合查询效率分析的核心,不是背 join 语法,而是搞清楚 MySQL 在连接两张表时到底选了哪条路、每步估算多少行、临时表落在内存还是磁盘。这里会从执行计划讲起,把驱动表选择、索引设计、SQL 改写、深度分页这几个关键点逐个拆开,最后给一套可以在本地复现的基准测试方法。适合刚开始做慢 SQL 优化的研发,也适合被 DBA 追着要 explain 结果的业务开发。
2. 多表联合查询的执行计划入门:EXPLAIN 输出里该看哪几列
2.1 驱动表和被驱动表:小表驱动大表不是玄学
拿到一条慢的多表查询,先不要猜,直接执行 EXPLAIN。执行计划会输出多行,每一行对应一张表,其中排在第一行的表通常就是驱动表。驱动表决定外层循环次数,被驱动表决定每次嵌套查询的成本,这两者的角色直接决定了最终耗时。
EXPLAIN SELECT u.id, u.name, o.order_no FROM user u JOIN orders o ON o.user_id = u.id WHERE u.phone LIKE '138%';执行后一般能看到第一行 table=u,第二行 table=o。第一行是驱动表,第二行是被驱动表。如果 u 表按 phone 过滤后剩 2000 行,MySQL 会用这 2000 行去 orders 表按 user_id 逐次查找;orders 表行数再多,只要 user_id 有索引,每次查找成本都近似一次 B+ 树搜索。外层循环次数越少,总成本越低,这就是“小表驱动大表”的本质。
EXPLAIN FORMAT=JSON SELECT u.id, u.name, o.order_no FROM user u JOIN orders o ON o.user_id = u.id WHERE u.phone LIKE '138%';FORMAT=JSON 会输出更细的 cost_info 和 used_key_parts,适合在两张表看起来都有索引、但优化器仍然选错驱动表时用。参数方面,如果 Extra 列出现 Using join buffer,说明被驱动表匹配时用到了连接缓冲区,先看一下当前 join_buffer_size:
SHOW VARIABLES LIKE 'join_buffer_size';调大 join_buffer_size 可以减少批量匹配次数,但不要盲目调到几个 G,内存压力会转嫁到排序和临时表。对于 inner join,优化器会自动选择成本更小的驱动顺序;对于 left join,左表通常固定为驱动表,如果右表 join 字段没有索引,慢得非常直观。
2.2 type 与 rows:全表扫描和估算行数的坑
EXPLAIN 的 type 字段是最快判断一个 JOIN 有没有走索引的依据。常见类型从好到差大致是:const/eq_ref、ref、range、index、ALL。直接看表:
| type | 含义 | 多表 JOIN 场景 |
|---|---|---|
| const / eq_ref | 通过主键或唯一索引精确匹配 | 被驱动表按主键关联时常见 |
| ref | 普通二级索引等值匹配 | 被驱动表 join 列有非唯一索引 |
| range | 索引范围扫描 | where 里有 between、> 等条件 |
| index | 全索引扫描 | 覆盖索引可用,但无法利用索引过滤 |
| ALL | 全表扫描 | join 列没索引,或者索引失效 |
rows 列是优化器估算的行数,不是实际执行后返回的行数。很多新手看到 rows 很小就以为没救了,其实如果 type 是 ALL,rows 估算可能完全不准确。想拿真实执行行数,可以改用 EXPLAIN ANALYZE,这是 8.0.18 之后才有的功能:
EXPLAIN ANALYZE SELECT u.name, o.order_no FROM user u JOIN orders o ON o.user_id = u.id WHERE u.id = 12345;输出里会带 actual time、rows、loops 三类信息,比如actual time=0.123..0.456 rows=3 loops=1。rows 表示这一步实际生产了多少行,loops 表示这个节点被循环执行了多少次,两者乘起来才是真实数据量。至于 rows 估算偏了,最常见的修法是对表重新做统计信息:
ANALYZE TABLE user, orders;执行完再跑 EXPLAIN,rows 通常会回归到接近实际值的水平。注意 EXPLAIN ANALYZE 会真正执行这条 SQL,生产流量高峰时不要直接往主库上打,最好在从库或测试环境跑。
2.3 用 EXPLAIN 定位第一个需要优化的 JOIN
慢 SQL 优化第一步不是改 SQL,而是先定位慢在哪一张表。我一般会走这么几步:开启慢查询日志收集慢 SQL,到测试库上还原,再 EXPLAIN,最后根据 type 和 Extra 判断优先级。
SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;开启后,执行时间超过 1 秒的查询会进慢日志。生产环境用完记得关闭,日志落盘本身也有开销。拿到慢 SQL 后,在测试库前面加 EXPLAIN,重点看三件事:哪行 type 最差,哪行 rows 最大,Extra 里有没有 Using temporary、Using filesort、Using join buffer。
以常见的两表 JOIN 为例,如果第二行 orders 显示 type=ALL,说明被驱动表在按 user_id 关联时没有索引可用,这往往是第一个要修的;如果第一行 user 显示 ALL,但它是驱动表,优化器可能已经默认会扫全表来做批量匹配,此时给 user 加索引未必能解决问题,反而要先看 where 条件能不能收窄。真正定位时,我习惯把 EXPLAIN 结果和业务 SQL 一起贴进文档:表名、type、key、rows、Extra 各占一列,这样优化前后对比时一眼就能看到变化。
3. 三种常见多表查询优化手段:从索引到改写
3.1 索引设计:JOIN 的 ON 字段和 WHERE 字段要分开看
多表联合查询的索引设计和单表不一样。单表通常只看 where 条件,多表还要多考虑一层:驱动表靠 where 条件缩小结果集,被驱动表靠 on 字段快速匹配。两者经常是不同字段,所以不能只给一个字段建索引。
举一个实际场景:查询手机号以 138 开头、并且下过单的用户,订单状态还需要是 PAID。
SELECT u.id, u.name, o.order_no, o.status FROM user u JOIN orders o ON o.user_id = u.id WHERE u.phone LIKE '138%' AND o.status = 'PAID';这种查询的索引设计可以分成两步。第一步,在 user 表上给 phone 建索引,让驱动表过滤从全表扫描变成 range 扫描:
ALTER TABLE user ADD INDEX idx_phone(phone);第二步,在 orders 表上建联合索引,让被驱动表既能按 user_id 快速匹配,又能顺便过滤 status:
ALTER TABLE orders ADD INDEX idx_user_status(user_id, status);这样 join 时 orders 表不会再全表扫描,status 过滤也能直接在索引层完成,不需要每条记录回表判断。联合索引的字段顺序有讲究:等值条件放前面,范围条件放后面。user_id 在 on 里是等值条件,status 在 where 里也是等值条件,所以 (user_id, status) 比 (status, user_id) 更合适;如果还有 order_no 精确匹配,可以把 order_no 也加进去,比如 (user_id, status, order_no)。
建完索引先看SHOW INDEX FROM orders的 cardinality,这个值表示索引去重后的估算值。如果 user_id 的基数很低,比如一张表只有三五个用户,那加索引的作用也有限,反而白白增加写入成本。常见误用是给驱动表的 where 字段建了索引,却忘了被驱动表的 on 字段,结果是驱动表确实变小了,但每次关联仍在扫大表,整体毫无进步。
3.2 让查询走覆盖索引:减少回表的血泪经验
覆盖索引优化是慢 SQL 优化里见效最快、也最容易被忽略的一招。所谓覆盖索引,就是一条 SQL 需要读的所有列都落在同一个二级索引里,InnoDB 不需要回主键聚簇索引就能拿到结果。
SELECT u.id, u.name, o.order_no, o.status FROM user u JOIN orders o ON o.user_id = u.id WHERE u.phone = '13800001111';这查询里需要返回 user 表的 id、name,orders 表的 order_no、status,还涉及 on 字段 user_id。相应的覆盖索引可以这样建:
ALTER TABLE user ADD INDEX idx_phone_name(phone, name); ALTER TABLE orders ADD INDEX idx_user_status_order(user_id, status, order_no);user 表的主键 id 会自动出现在二级索引叶子节点里,所以 idx_phone_name 能覆盖 id、phone、name 三个字段;orders 表的联合索引覆盖 user_id、status、order_no 以及主键 id。执行计划里 Extra 如果出现 Using index,说明这个查询在索引里就把数据拿齐了。
这里有一个血泪经验:覆盖索引不是越宽越好。每多一个字段,索引页就越宽,B+ 树叶子节点能存放的条目就越少,写入和更新时索引维护成本也越高。我一般只给 qps 排名靠前的两三类高频 JOIN 做覆盖索引,不会把一张表所有查询都试图覆盖。另一个常见坑是 SELECT *:一旦把不在索引里的字段带进去,覆盖索引立刻失效,MySQL 只能回表取剩余列。跑多表 JOIN 时,先把你真正要用的列列出来,别让 SELECT * 把优化成果直接冲掉。
3.3 SQL 改写:子查询、IN 和 JOIN 的取舍
索引加到位之后,如果 SQL 本身结构很差,还是快不了。最常见的慢结构是相关子查询:外层每取一行,内层就执行一次子查询。比如统计下单超过 10 次的用户,写成子查询是这个样子:
SELECT id, name FROM user u WHERE (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) > 10;user 表有 10 万行,这个子查询理论上会被执行 10 万次。即使 orders 有索引,单次查询再快,乘以 10 万也慢得吓人。常见做法是改写成 JOIN 加 GROUP BY:
SELECT u.id, u.name FROM user u JOIN orders o ON o.user_id = u.id GROUP BY u.id, u.name HAVING COUNT(o.id) > 10;改写后的逻辑是先把 user 和 orders 按 user_id 关联,再对 user 分组统计,orders 表整体只扫描一次。GROUP BY 可能会产生临时表,但如果 user 表已经用 phone 或 status 过滤得很小,临时表开销远小于原来的 10 万次子查询。
改写时要注意语义等价。原相关子查询对没有订单的用户,COUNT 为 0,不会进入 HAVING 条件;改写后的 INNER JOIN 本身也只保留有订单的用户,两者结果一致。如果业务想保留没有订单的用户,就要用 LEFT JOIN LEFT JOIN 后 COUNT 为 0 的行同样被 HAVING 过滤掉,最终还是等价于 INNER JOIN,改成 LEFT JOIN 只是多消耗临时表容量。
MySQL 5.7 和 8.0 的优化器会把大部分 IN 子查询改写成 semi-join,效果不一定差,所以不要一看到子查询就改。正确的流程是先 EXPLAIN,看子查询有没有被物化、有没有走索引,再决定是否改写。多表查询常常会出现在事务处理链路里,一条慢 SQL 拉长了整个事务,导致行锁和间隙锁的持有时间变长,这也是多表查询慢会连带引发锁等待的原因之一。
4. 数据量增长后的多表联合查询:从嵌套循环到临时表
4.1 数据量增长后,为什么 inner join 也会翻车
很多人在测试环境只有几千行数据,JOIN 怎么跑都快,一到生产就原形毕露。原因在于嵌套循环 join 的复杂度是驱动表行数乘以被驱动表单次查询成本。两表都走索引时,总成本近似等于 M 次索引查找;驱动表行数从 1 万涨到 100 万,总耗时也会跟着涨近百倍,这是躲不掉的。
最坏情况是被驱动表 join 字段没有索引,MySQL 只能把两张表做笛卡尔式匹配,成本接近 M×N。1 万行的表和 1 万行的表关联,就是 1 亿次比较;涨到 100 万,就是万亿级别,基本等于把数据库拖死。这也是为什么被驱动表的 join 字段索引优先级最高。
MySQL 8.0.18 之后引入了 hash join,在两张表做等值 inner join、且被驱动表没有可用索引时,优化器会尝试用哈希连接代替嵌套循环:
EXPLAIN SELECT s.id, b.name FROM small_table s JOIN big_table b ON s.id = b.small_id;如果 Extra 里出现 Using join buffer (hash join),说明这一步走的是 hash join。它对无索引场景确实有明显改善,但哈希表要占内存,内存不够时还要落磁盘,反而可能更慢。在实际业务里,不要指望 hash join 来救大表 JOIN,核心仍然是把驱动表结果集缩小,或者在被驱动表上建立索引。MySQL 的并行能力很弱,单条 SQL 很难像其他数据库那样靠并行线程跑,所以多表联合查询的优化重点应该放在减少扫描行数和避免大临时表上。
4.2 group by 和 order by 在多表联查中的临时表问题
多表 JOIN 之后再做 group by 或 order by,是慢 SQL 的重灾区。MySQL 可能需要先把 join 后的中间结果放进临时表,再对临时表排序或分组,如果临时表超过内存阈值,就会落到磁盘,性能会断崖式下跌。
EXPLAIN SELECT o.user_id, COUNT(*) FROM user u JOIN orders o ON o.user_id = u.id WHERE u.status = 1 GROUP BY o.user_id ORDER BY o.user_id;执行计划里如果出现 Using temporary 和 Using filesort,说明中间结果已经进了临时表,排序也没有直接走索引。这条 SQL 的问题在于:过滤条件在 user 表,分组和排序字段在 orders 表,单索引很难同时满足两边的路径,MySQL 只能先用 user 表过滤,再把 join 结果整体缓存起来。
EXPLAIN ANALYZE SELECT o.user_id, COUNT(*) FROM user u JOIN orders o ON o.user_id = u.id WHERE u.status = 1 GROUP BY o.user_id ORDER BY o.user_id;用 EXPLAIN ANALYZE 可以看到临时表这一步的真实耗时,以及 rows 和 loops 的组合情况。常见的参数调整方向是先看 tmp_table_size 和 max_heap_table_size,内存临时表超过这两个值就会转成磁盘临时表,再看 sort_buffer_size,它决定排序缓冲区。但调参只能缓解症状,真正稳妥的做法是让 join 两侧的数据先各自收敛。
比如先对 orders 表单独做分组统计,再把结果和 user 表关联:
SELECT u.name, t.cnt FROM user u JOIN ( SELECT user_id, COUNT(*) cnt FROM orders GROUP BY user_id ) t ON u.id = t.user_id ORDER BY t.cnt DESC;这样 orders 的分组发生在单表内,索引可以直接支持 GROUP BY,不会产生跨表的超大临时表;user 表也只是在最后通过主键关联一次。ORDER BY 字段来自 t.cnt,此时排序的数据量等于用户数,而不是订单数。实践里这个改写对报表类查询提升非常明显。
4.3 分页查询配合多表联查的深度翻页优化
多表 JOIN 加 LIMIT 深度分页,是另一个容易被忽视的性能黑洞。LIMIT 100000, 20 并不是只取 20 行,而是先取前 100020 行,再丢掉前面 10 万行,如果前面这个排序还涉及多表 join,代价就翻倍了。
-- 优化前:offset 10 万 SELECT u.id, u.name, o.order_no FROM user u JOIN orders o ON o.user_id = u.id ORDER BY o.create_time DESC LIMIT 100000, 20;这个写法会把 user 和 orders 全部 join 完,再按 create_time 排序,最后丢弃前 10 万行。orders 表增长后,join 和排序的开销都会被放大。常见做法是延迟关联:先在子查询里只取主键,再回表补列。
-- 优化后:子查询先拿主键,再回表 SELECT u.id, u.name, o.order_no FROM ( SELECT id FROM orders ORDER BY create_time DESC LIMIT 100000, 20 ) tmp JOIN orders o ON tmp.id = o.id JOIN user u ON u.id = o.user_id ORDER BY o.create_time DESC;子查询里只 SELECT orders 主键 id,可以走 orders 表上的二级索引 (create_time, id),在索引层面完成排序和跳过,不需要把 user 表和 orders 表全量 join 一遍。拿到 20 个主键后,再回表取 order_no,并通过 user_id 关联 user 表,最后只处理 20 行数据。
这个写法对 mysql 排序场景尤其有效,但有一个细节不能漏:外部查询的 ORDER BY 字段必须和子查询里的排序字段一致,否则 join 后行序可能被打乱。如果业务允许,更好的方案是彻底放弃 offset,改用游标分页:每次带上上一次最后一条的 create_time,用 WHERE create_time < ? 加 LIMIT 20。对深度分页来说,游标分页几乎是最优解,缺点是用户不能直接跳页。
5. 多表联合查询避坑指南:5 个踩过的现场与排查方法
5.1 字符集不一致导致 JOIN 全表扫描
现象:一条联表查询在测试环境很快,到生产就慢得离谱,EXPLAIN 里被驱动表 type=ALL,Extra 还出现 Using where。
原因:两张表的关联字段字符集不一致,比如 user 表是 utf8mb4,orders 表是 utf8,MySQL 在比较时需要先做字符集转换,导致索引无法直接使用。这个问题从 SHOW CREATE TABLE 里一眼就能看出来,关联字段一个带 CHARACTER SET utf8mb4,另一个带 CHARACTER SET utf8。
解决:统一两边字段的字符集和排序规则。比如把 orders 表关联字段改成 utf8mb4:
ALTER TABLE orders MODIFY user_id VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;大表上的字符集修改会重建表,务必放在低峰期。改完之后再跑 EXPLAIN,type 一般会从 ALL 回到 ref 或 eq_ref。
5.2 隐式类型转换让索引失效
现象:WHERE 条件里传了一个数字给字符串字段,SQL 跑得很慢,EXPLAIN 显示 type=ALL。
原因:字段是 VARCHAR,但传入的是 INT 型参数,MySQL 会自动把字段做类型转换,转换后再比较,索引就失效了。反过来也一样,INT 字段传入字符串也可能触发同样的问题。
解决:先看表结构,再改参数类型。比如 user_id 是 VARCHAR,就写成字符串形式:
SELECT * FROM orders WHERE user_id = '123456';不要在 SQL 里依赖 MySQL 的自动转换。写代码时也尽量让 ORM 参数和字段类型对齐,排查时最有效的方法是直接对比 SQL 里的参数类型和表结构的字段类型。
5.3 LEFT JOIN 的 NULL 值让结果集悄悄变大
现象:LEFT JOIN 返回的行数比左表还多,或者在统计订单数量时,数据对不上。
原因:LEFT JOIN 会保留左表所有行,如果右表匹配到多行,左表的一行会被复制成多份。另一个常见问题是右表字段是 NULL 时,COUNT(右表字段) 不会把它算进去,统计结果和预期不一致。
解决:先确认两表关联关系是一对一还是一对多。如果右表可能有多条记录,先做聚合再去 JOIN。比如用户和订单是一对多,统计用户订单数时要这样处理:
SELECT u.id, u.name, t.cnt FROM user u LEFT JOIN ( SELECT user_id, COUNT(*) cnt FROM orders GROUP BY user_id ) t ON u.id = t.user_id;这样既保留没有订单的用户,又不会让行数膨胀。业务上如果只关心有订单的用户,直接用 INNER JOIN 更干净。
5.4 SELECT * 让覆盖索引失效
现象:查询里只改了 SELECT *,执行计划里本来应该有的 Using index 消失了,响应时间明显变长。
原因:SELECT * 会把表里所有列都带出来,覆盖索引覆盖不了那么多字段,InnoDB 只能一条条回表补数据。多表 JOIN 中这个影响会被放大,因为回表次数会乘以驱动表行数。
解决:只查询业务需要的列,不要图一时方便用星号。比如只需要用户 id、姓名和订单号,就明确列出:
SELECT u.id, u.name, o.order_no FROM user u JOIN orders o ON o.user_id = u.id;优化完成后,用 EXPLAIN 再确认一次 Extra 中是否出现 Using index,如果没有,说明覆盖索引还没完全生效,继续检查有没有不必要的字段被带了出来。
5.5 大表 COUNT JOIN 是个无底洞
现象:统计用户订单数量时,直接用 LEFT JOIN 再 COUNT,SQL 执行了几分钟都没回来。
原因:两张大表先做 JOIN 生成中间结果,再对这个庞大的中间结果做 COUNT,数据库扫描的行数可能达到千万甚至上亿级别。
解决:先聚合小表,再关联大表。上面的用户订单统计写法已经演示过:先对 orders 表 GROUP BY user_id,得到每个用户订单数,再和 user 表 LEFT JOIN。这本质上把“大表 JOIN 大表”变成了“大表先聚合再 JOIN 小表”,扫描次数从笛卡尔级别降成一次全表分组加一次主键关联。对于这种统计类查询,如果实时性要求不高,更彻底的做法是维护一张汇总表,订单写入时实时更新计数,查询直接读汇总表。
6. 最后一公里:用 EXPLAIN ANALYZE 和基准数据,把多表 JOIN 优化从经验变成结论
6.1 造一份和你线上同分布的数据
优化多表查询,最怕在只有几百行数据的测试表上自我感动。我一般会先造一份和线上同分布的数据,至少让驱动表有 10 万行以上,被驱动表有几百万行,并且字段之间的相关性也要模拟,比如老用户订单多、新用户订单少,而不是纯随机生成。
DELIMITER $$ CREATE PROCEDURE gen_orders_test_data() BEGIN DECLARE i INT DEFAULT 1; SET autocommit=0; WHILE i <= 500000 DO INSERT INTO orders(user_id, status, order_no, create_time) VALUES ( FLOOR(RAND()*100000)+1, IF(RAND()>0.5, 'PAID', 'CREATED'), CONCAT('NO', i), NOW() - INTERVAL FLOOR(RAND()*365) DAY ); SET i = i + 1; END WHILE; COMMIT; END$$ DELIMITER ; CALL gen_orders_test_data();这里关闭 autocommit 按批插入,500 万行能在可控时间内灌完。参数说明:user_id 是 1 到 10 万的随机分布,status 靠 RAND 制造 50% 的 PAID 比例,create_time 散布在一年内。如果你的线上数据是偏斜的,比如 top 10 用户占了 70% 订单,就要把分布改成偏斜分布,否则测出来的优化结论上生产会翻车。
6.2 优化前后对比:EXPLAIN ANALYZE 怎么看实际开销
数据造好后,把原始慢 SQL 和优化后的 SQL 都跑一遍 EXPLAIN ANALYZE,重点记录三条信息:actual time、rows、loops。actual time 是实际执行耗时,rows 是每一步实际行数,loops 表示这个节点循环了多少次。用一张表记录最直观:
| 方案 | type / key | actual time | rows | loops |
|---|---|---|---|---|
| 优化前 | ALL / NULL | 约 1.2s | 500000 | 1 |
| 优化后 | ref / idx_user_status | 约 0.08s | 20 | 1 |
EXPLAIN ANALYZE SELECT u.id, u.name, o.order_no FROM user u JOIN orders o ON o.user_id = u.id WHERE u.phone = '13800001111';注意 EXPLAIN ANALYZE 会真正执行 SQL,建议在测试库跑,不要在线上主库压测。
6.3 还需要看并发:mysqlslap 随手压一压
单条 SQL 从 1 秒降到 80 毫秒,不等于并发场景下经得住打。多表 JOIN 在并发下容易出现锁等待和连接积压,我一般会用 mysqlslap 做一个快速冒烟测试:
mysqlslap -u root -p -h 127.0.0.1 \ --create-schema=testdb \ --query="SELECT ..." \ --concurrency=20 --iterations=3concurrency 表示模拟 20 个并发连接,iterations 表示跑 3 轮。如果发现平均耗时随并发明显上升,就要回头检查事务隔离级别和索引锁范围。此时不能只盯着 EXPLAIN 里的 rows,还要结合慢查询日志和连接数曲线判断。
我最早优化多表查询,习惯只盯着 rows 看,觉得 rows 变小就完事了,结果有几次临时表和排序才是真凶。现在我每次改动前都会先保存一份 EXPLAIN ANALYZE 的基线,改完再对照一遍,慢 SQL 有没有变快、扫描行数减没减、临时表消没消失,全部一目了然。希望帮到你。
本文还有配套的精品资源,点击获取