☰
慢SQL优化实战:从索引失效到执行计划的查询调优指南
2026/10/9 12:50:47 网站建设 项目流程

1. 慢SQL拖垮业务的那天晚上:问题到底出在哪儿

先讲一个我亲历的场景。凌晨两点,值班手机突然被告警刷屏,一个核心订单查询接口的P99延迟从80毫秒直接飙到4.8秒,数据库CPU瞬间打满,连带周边三个服务跟着雪崩。登录RDS控制台一看,慢日志里躺着一条三年前写的SQL,一个关联了六张表的统计查询,跑一次要扫3700万行,执行计划里赫然写着Using join buffer和Using temporary。

把这条SQL拎出来单独跑,EXPLAIN结果是:type=ALL,rows=3700万,Extra列写满了"Using where; Using temporary; Using filesort"。这种执行计划我闭着眼睛都能背出来——全表扫描加临时表加文件排序,它就是来杀数据库的。

当时我做的第一件事不是优化这条SQL,而是先把它从业务链路里摘掉。因为这种级别的慢查询,任何在线DDL、加索引操作都会被它拖死,你得先止血,再治病。第二天上班后,我才开始正经做SQL优化的完整流程:从慢日志采集、执行计划解读、索引策略设计,到查询改写、参数调优,最后再回头看这条SQL到底该怎么重写。

这篇文章我想把我这几年在生产环境里做的SQL优化工作完整梳理一遍。内容不只针对某个数据库,而是以MySQL为主,同时会带上Oracle里常见的并行SQL优化思路做横向对比。适合的读者是:后端开发、DBA、架构师,以及所有被慢SQL折磨过、想系统性掌握索引策略和查询调优的人。

2. 索引策略的设计:从B+树到真正会建索引

2.1 为什么索引能快三个数量级:B+树的高度与扇出

很多人背过"索引是B+树",但真问到"为什么B+树查得快"就卡住了。我习惯用这样一组数字讲清楚:

MySQL InnoDB默认页大小是16KB。假设你表里主键是BIGINT,占用8字节,再加6字节的行指针,这样一个索引节点大约能存16KB / 14B ≈ 1170个键值对。B+树叶子节点存数据,假设一行数据平均1KB,一个叶子页能存16行。

那么一棵高度为3的B+树能存多少行?计算方式是:1170 × 1170 × 16 ≈ 2190万行。也就是说,一张2000万行的表,走主键索引查任意一行,最多只需要3次磁盘I/O。如果走全表扫描呢?2000万行除以每页16行,大约是125万次磁盘I/O。这中间就是几十万倍的差距。

这个底层原理直接决定了索引设计的几个基本判断:

  • 主键最好用BIGINT自增或雪花算法生成的定长整数,不要用UUID字符串。字符串键值更长,单个节点能存的键值数更少,树更高,I/O次数更多。
  • 索引列值越短越好,因为索引节点扇出越大,树越矮。
  • 索引不是越多越好,每个索引都是一棵独立的B+树,写入时要同步维护,索引过多会拖垮写性能。

2.2 主键索引和唯一索引的区别:一个查得快,一个约束得严

这是面试里高频出现、但很多人答不到点子上的问题。主键索引和唯一索引的区别要从三个维度看:

约束层面:主键索引隐含了唯一约束和NOT NULL约束,一张表只能有一个主键;唯一索引只保证唯一性,允许存在NULL值(MySQL里多个NULL是不算冲突的),一张表可以有多个唯一索引。

存储层面:这才是核心区别。InnoDB是聚集索引组织表,主键索引就是整张表本身——叶子节点直接存储完整行数据,表数据按照主键顺序物理排列。二级索引(包括唯一索引)的叶子节点存储的是主键值,不是行数据。

查询层面:通过主键索引查询,一次B+树查找直接拿到完整行;通过唯一索引查询,需要先在二级索引B+树里找到主键值,再回主键索引做第二次查找才能拿到完整行数据,这个过程叫回表。

因此主键索引的查询效率一定高于唯一索引的查询效率。但如果你有一列在业务上天然具备唯一性(比如用户表的身份证号),该加唯一索引还是要加,因为这是数据约束层面的需求,不能为了查询快而牺牲正确性。

2.3 存储引擎:InnoDB和MyISAM的账,得算清楚

在MySQL语境下聊SQL优化,绕不开存储引擎的选择。虽然现在新项目基本默认InnoDB,但存量系统里MyISAM仍然存在,两者的优化思路截然不同。

InnoDB和MyISAM的核心差异在四个层面:

  • 事务:InnoDB支持ACID事务,有redo log和undo log;MyISAM不支持事务,写入崩溃后表可能损坏,需要repair。
  • 锁粒度:InnoDB支持行级锁(配合索引实现),高并发下写入冲突少;MyISAM只支持表级锁,任何写入都会锁全表,写并发极差。
  • 索引结构:前面说了,InnoDB主键索引和数据一起存储,二级索引存主键值;MyISAM索引和数据分离,索引叶子节点存的是数据行的物理地址,所有索引都是非聚集的,查询无论走哪个索引都需要额外一次地址定位。
  • 并发读写:InnoDB靠MVCC实现读写不互斥;MyISAM读和写之间完全互斥,读多写少还可以,一旦写入频繁,延迟会急剧恶化。

有一个很容易被忽略的坑:MyISAM即使走索引也可能会因为表级锁产生阻塞。你在MyISAM表上做任何INSERT、UPDATE、DELETE,都会锁住整张表,所有SELECT都要等锁释放。所以如果你的系统并发写入超过每秒几百次,别再纠结索引优化了,先把表迁移到InnoDB,这个收益远大于任何索引调整。

2.4 联合索引的字段顺序:为什么最左前缀不是玄学

联合索引是最容易被用坏的东西。很多开发人员建了一个idx_a_b_c(a, b, c),就以为所有含a、b、c的查询都能走索引,这是误解。

联合索引本质上是一棵排序规则为"先按a排序,再按b排序,再按c排序"的B+树。最左前缀原则说的是:查询条件里必须包含索引最左边的列,并且不能跳过中间列,索引才有可能被用到。

但更深入的细节是:联合索引的字段顺序设计,要同时考虑等值查询和排序场景。举个例子,业务上有两个查询:

  • WHERE user_id = ? AND status = ? AND create_time > ?
  • WHERE user_id = ? ORDER BY create_time DESC LIMIT 20

第一个查询有三个等值条件+一个范围条件,第二个查询有一个等值条件+一个排序字段。那么适合的联合索引顺序是(user_id, status, create_time)还是(user_id, create_time, status)?

先看查询一:如果是(user_id, status, create_time),三个等值条件下索引精确定位到一个区间后,create_time范围扫描很舒服。如果是(user_id, create_time, status),create_time是范围条件,status就无法继续走索引了,只能在create_time范围内做过滤,效率差一些。

再看查询二:ORDER BY create_time DESC要能走索引,必须满足最左前缀且排序字段是索引的最后一个有序字段。如果你把status放中间,(user_id, create_time, status)里create_time是索引第二列,查询二里缺少status条件,那么create_time的排序仍然可以利用(因为跳过索引中间的列,前面的等值条件下create_time依然是有序的)。

综合两个查询,(user_id, status, create_time)和(user_id, create_time, status)各有取舍。真实生产中我会用(user_id, status, create_time),因为第一个查询是高频统计接口,第二个查询可以通过覆盖索引规避排序(后文会展开讲)。索引设计的好与坏,本质是拿你业务的真实查询模式来投票。

3. 索引失效的14个场景:每个坑我都用生产事故换过教训

3.1 对索引列做函数运算:最隐蔽的性能杀手

很多人以为索引失效只有LIKE '%xxx'和!=,其实最阴险的是对索引列做函数操作。比如:

SELECT * FROM orders WHERE DATE(create_time) = '2024-06-01';

这条SQL看起来人畜无害,但DATE()函数套在create_time上之后,优化器无法直接利用create_time上的B+树有序性做范围查找,MySQL 5.7及以下版本会直接放弃索引。正确的写法是:

SELECT * FROM orders WHERE create_time >= '2024-06-01 00:00:00' AND create_time < '2024-06-02 00:00:00';

把函数作用从索引列上移除,换成范围条件,优化器就能直接走索引区间扫描。这里面的原理其实不复杂:B+树里索引键的排序是基于原始列值的,你一旦套了函数,比较的基准就变成了函数结果,这个结果在B+树里根本没有对应的排序关系,优化器只能扫码所有键值算一遍函数结果再过滤。

3.2 隐式类型转换:你以为在查字符串,MySQL以为在查数字

这是一个容易在代码评审里漏掉的场景:

SELECT * FROM users WHERE phone = 13800138000;

phone列是VARCHAR类型,但等号右边传了一个整数。MySQL会先把列值转成数字再比较,相当于对phone列做了一个隐式的CAST(phone AS SIGNED),和函数操作一样破坏索引。

更离谱的是这个问题在代码里很难被发现,因为结果往往是对的——MySQL的隐式转换规则是字符串转数字,'13800138000'转成数字后确实等于13800138000,返回的数据没问题,但执行计划已经从索引扫描退化成全表扫描了。排查方式很简单:对线上SQL做EXPLAIN时重点看type列是不是从ref变成了ALL,同时用SHOW WARNINGS查看优化器改写后的SQL,能看到它加上的隐式转换。

3.3 范围查询之后的列:联合索引的连续性断裂

这是联合索引里最高频的失效模式。假设索引是idx_status_create_time(status, create_time):

SELECT * FROM orders WHERE status = 1 AND create_time > '2024-06-01' AND amount > 100;

status是等值,create_time是范围,amount上没有索引字段。执行时status等值定位后,create_time范围内amount无法使用索引。这个其实不是"失效",而是联合索引的结构决定了范围查询之后的列根本无法继续利用索引的有序性。

有一种典型的优化思路是把amount这种范围之后的过滤条件,通过冗余字段、或者重新设计联合索引顺序来覆盖。但如果amount本身是范围查询,你无论怎么排顺序,它后面的列都没法走索引了。所以遇到这种情况,通常是两种解法:一是把高频使用的过滤列放在范围列之前;二是用覆盖索引直接把要查的字段兜住,让amount不参与索引查找、只参与索引覆盖(如果它被包含在索引里)。

3.4 不等于、NOT IN、IS NOT NULL:走索引也白走

!=、<>、NOT IN、IS NOT NULL这些条件,MySQL优化器在绝大多数情况下会放弃索引扫描。为什么?因为它们对应的是索引里除了目标值之外的所有区间,优化器估算扫描范围和直接全表扫描差不多,甚至因为要频繁回表而更慢。

我见过一个极端案例:线上一个千万级表,开发人员为了排除极少量脏数据写了WHERE status != 99,这条SQL把索引完全废掉,全表扫描跑了两分多钟。改成WHERE status IN (0,1,2,3)之后,走回索引查找并加上范围扫描,耗时降到80毫秒。解决方案往往是把否定条件改写为肯定条件,因为B+树的强项是精确命中,不是否定意义上的区间扫描。

3.5 LIKE前置通配符:覆盖索引可以救你一次

LIKE '%keyword%'导致索引失效是常识,但有一个生产环境里经常被漏掉的例外:如果查询列和条件列都在同一个索引里,即使LIKE '%keyword%'不能用于索引查找,MySQL也可以使用索引全扫描(index full scan)来避免全表扫描。因为索引通常比表小很多,扫描索引的代价远小于扫描整张表。

SELECT id, title FROM articles WHERE title LIKE '%SQL优化%';

假设有联合索引idx_id_title(id, title),这条SQL可以走Using index,即全索引扫描,不需要回表。这比全表扫描快一到两个数量级。但要注意,一旦查询列超出了索引范围(比如加了content字段),这个优化就失效了,会退化成全表扫描。这是覆盖索引在LIKE查询里的特殊用法,生产环境里很实用。

3.6 OR条件混用非索引列:粉碎机来了

WHERE a = 1 OR b = 2这种写法是索引粉碎机。MySQL优化器对OR的处理逻辑是:除非每个分支都能使用索引,否则它只能选择全表扫描。也就是说,idx_a和idx_b都存在并且都能走索引的情况下,索引合并(index merge)可以让OR条件走索引;但只要有一个分支列上没有索引,整个OR就直接全表扫描。

最保险的做法是把OR改写成UNION ALL:

SELECT * FROM users WHERE name = '张三' UNION ALL SELECT * FROM users WHERE age = 30;

这里的前提是两个分支的查询结果集合没有重叠。如果存在重叠,则要用UNION去重。改写后每个分支都能独立走索引,性能往往提升明显。

4. 查询调优的主战场:执行计划、覆盖索引与SQL改写

4.1 EXPLAIN里的每列怎么看:我读执行计划的口诀

执行计划是查询调优的CT机,看不懂EXPLAIN,一切优化都是抓瞎。我不讲教科书,只讲我实际看执行计划的优先级顺序:

先看type列,从好到差依次是:system>const>eq_ref>ref>range>index>ALL。生产环境里,ALL意味着全表扫描,必须警惕;index意味着索引全扫描,如果表很大且有回表,也可能很慢。理想情况下,点查应该达到ref或const级别,范围查询应该达到range级别。

再看key列,确认实际用到的索引是哪一个。如果key为NULL,说明这条SQL没走索引,直接跳回上一步检查为什么。

再往下看rows列,这是优化器估算的需要扫描的行数。有一个实用技巧:用rows乘以filtered(MySQL 5.7+的EXPLAIN输出里有filtered列,表示经过条件过滤后剩余行的百分比),可以估算实际返回的数据量级。如果rows很大但filtered只有1%,说明索引选择得不好,筛选了很多无效行。

最后看Extra列,出现Using filesort意味着排序没用到索引,需要优化排序或加对应索引;出现Using temporary意味着用了临时表,多半是GROUP BY或DISTINCT导致的;出现Using index说明覆盖索引生效,是好事;Using index condition说明索引条件下推(ICP)被启用,部分过滤条件下推到存储引擎层执行。

4.2 覆盖索引:让SQL连回表都省了

覆盖索引是查询调优里性价比最高的手段之一。它的核心思想是:查询需要的所有字段都包含在同一个二级索引里,那么InnoDB可以直接从索引B+树里取到全部数据,不需要回表。

举个例子,一个订单列表页高频查询是:

SELECT order_id, status, amount, create_time FROM orders WHERE user_id = 123 ORDER BY create_time DESC LIMIT 20;

这个查询如果只有idx_user_id(user_id),那么走索引找到所有符合条件的user_id后,每个都需要回表拿status、amount、create_time,然后在内存里做排序。当某个用户有几千条订单时,回表加排序的代价就显现出来了。

但如果你把索引改成idx_user_cover(user_id, create_time, status, amount),这个查询的所有列都被索引覆盖了,而且(user_id, create_time)正好构成联合索引的有序前缀,连排序都不需要,直接索引倒序扫20条就返回了。Extra列会显示Using index,性能提升非常显著。

覆盖索引的一个使用前提是:不能为了覆盖而无限扩大索引字段,导致索引过大。比如TEXT、BLOB这类大字段就不适合放进覆盖索引,一个页能存的键值会急剧变少,反而拖慢扫描。

4.3 子查询改JOIN:别让MySQL生成临时表

很多SQL性能问题来自于复杂的子查询,尤其是IN后面跟着一个子查询的时候。拿经典的"查下过单的用户"举例:

SELECT * FROM users WHERE user_id IN (SELECT user_id FROM orders WHERE order_status = 1);

MySQL 5.5之前对这类SQL的优化很糟糕,会为每个外部行的user_id重复执行子查询(相关子查询)。5.6之后有了物化优化(materialization),会把子查询结果先物化成临时表再执行半连接(semi join),但物化临时表本身也是开销。

更可控的做法是改写为JOIN:

SELECT DISTINCT u.* FROM users u INNER JOIN orders o ON u.user_id = o.user_id WHERE o.order_status = 1;

这种改写有一点要注意:JOIN会把一个用户对应多条订单的记录全部展开,所以如果业务上想返回的是用户列表而不是订单列表,必须加DISTINCT去重。把子查询改JOIN的好处是,可以被优化器直接使用嵌套循环连接加索引查找,不需要额外物化中间结果。

4.4 ORDER BY和GROUP BY:排序的代价从哪儿来

排序往往是查询性能的隐形杀手。ORDER BY触发Using filesort时,MySQL会先把结果集拷贝到sort buffer里做排序,如果sort buffer不够,会写磁盘临时文件做多路归并排序,这个代价在数据量大时非常恐怖。

解决办法是让排序字段和索引的排序方向一致。比如订单列表按create_time DESC排序,那么索引设计成(user_id, create_time DESC),MySQL就能直接倒序扫描,不走filesort。这里有一个MySQL 8.0的细节:从8.0开始支持索引的降序存储,之前版本虽然也能通过倒序扫描完成ORDER BY DESC优化,但联合索引里混合升降序的需求除了8.0之外都很难被满足。

GROUP BY的优化思路和ORDER BY类似——如果GROUP BY字段正好是联合索引的最左前缀,那么MySQL可以从索引有序性里直接分组,避免建立临时表分组。一个非常典型的错误写法是:

SELECT user_id, COUNT(*) FROM orders WHERE create_time > '2024-01-01' GROUP BY user_id;

这里user_id不是联合索引的第一列,分组只能靠临时表。如果把索引设计成(user_id, create_time),并且把查询改成先按user_id等值过滤再分组,情况就会好很多。

4.5 SQL改写里没有银弹:一个3000万行表的调优实战过程

我把之前提到的那个3000万行慢SQL完整的优化过程放这里,你能看到每一步的取舍。

原始SQL大致是:

SELECT u.user_name, COUNT(*) AS order_cnt, SUM(o.amount) AS total_amount FROM users u LEFT JOIN orders o ON u.user_id = o.user_id LEFT JOIN order_items i ON o.order_id = i.order_id WHERE o.status IN (1,2,3) AND o.create_time >= '2024-01-01' GROUP BY u.user_id, u.user_name HAVING total_amount > 1000 ORDER BY total_amount DESC LIMIT 50;

第一步是改连接顺序。LEFT JOIN里把大表放前面会扩大中间结果集。我把主表改成orders作为驱动表(因为status和create_time都可以走索引定位到较小的数据集合),然后跟order_items、users做连接,把LEFT JOIN改为INNER JOIN,因为HAVING total_amount > 1000已经隐式排除了没有订单的用户,LEFT JOIN语义上等价于INNER JOIN。

第二步是加联合索引。orders表加idx_status_create(user_id, status, create_time),让WHERE和GROUP BY都在索引里完成。

第三步是为order_items表的连接键加索引idx_order_id(order_id),同时让SUM(o.amount)涉及的字段通过覆盖索引带出来。

第四步是把GROUP BY user_id, user_name里的user_name去掉——它是不必要分组键,只要user_id唯一对应一个user_name,保留user_id分组就够了,避免user_name参与分组导致更大的排序和临时表。

优化后这条SQL从4.8秒降到了120毫秒。没有删掉任何一个业务功能,只靠索引重设计和SQL改写,量级提升接近40倍。

5. 并行SQL优化与Oracle/MySQL的差异化调优逻辑

5.1 Oracle并行SQL:什么时候开并行,什么时候别开

很多人听到"并行SQL优化"就以为是无脑给SQL加并行度,其实这一步里面门道很深。Oracle的并行执行(Parallel Execution)适合的场景是:大表的全表扫描、大表关联、大表排序、大表的DML操作,这些操作可以通过多个并行服务进程分片处理数据,再把结果汇总。

我举一个真实改造过的场景。一张流水表有8亿行,业务方要按月统计各渠道的交易量:

SELECT channel, SUM(amount) FROM trade_flow WHERE biz_date BETWEEN '2024-01-01' AND '2024-03-31' GROUP BY channel;

单进程跑要35分钟。我加了并行Hint:

SELECT /*+ PARALLEL(8) */ channel, SUM(amount) FROM trade_flow WHERE biz_date BETWEEN '2024-01-01' AND '2024-03-31' GROUP BY channel;

跑完只要4分半,提升了接近8倍。这个SQL适合并行的原因是:它对大表做了全范围扫描,并行扫描可以把分区或数据块分给不同进程做,同时还需要做GROUP BY聚合,聚合本身可以分阶段做(每个分片先聚合成部分结果,再汇总)。

但Oracle并行SQL的坑同样不少。在OLTP系统上,并发几十上百个并行SQL一起来,每个都开8个并行进程,数据库瞬间被并行进程打垮,系统整体吞吐量反而骤降。并行度的选择要结合系统CPU核数、IO能力、当前并发量来动态决策,而不是固定写死。生产经验上,OLTP环境里几乎不给并行Hint,只有跑批、报表这类长任务才会考虑。另外,并行DML要谨慎,并行更新如果做了一半后回滚,那回滚的代价也会被并行放大。

5.2 MySQL和Oracle在优化器策略上的几个关键差异

如果你同时管过MySQL和Oracle,会发现它们对同样的SQL产生的执行计划思路差异非常大。我把工作中碰到的主要差异点列成了一张表:

差异维度MySQL InnoDBOracle
驱动表选择优化器基于索引和估算行数,首选小表驱动大表,但5.7及以下优化器能力有限,经常需要人肉调连接顺序基于系统统计信息和代价模型的高效CBO,可以通过leadingHint强行指定驱动顺序
排序优化主要依赖索引避免filesort;没有索引时sort buffer溢出会落盘,性能骤降有专门的排序区(SORT_AREA_SIZE/PGA),大排序可以自动并行
分区剪裁支持分区表,但对分区裁剪的优化相对保守,SELECT * FROM分区表时仍需仔细验证裁剪范围分区裁剪非常成熟,结合并行,数仓场景下几亿行的表也能秒级响应
物化视图不支持原生物化视图,需要应用层做汇总表或使用flexviews这类工具原生支持物化视图和查询重写,汇总查询可以直接命中预计算的物化结果,极大降低查询时间
索引结构主键即聚集索引,二级索引存主键值,回表是常态堆表结构,索引叶子节点存物理ROWID,走索引访问直接定位数据块,不需要额外回表
优化器Hint支持FORCE INDEX、USE INDEX等,但hint功能有限,尤其是复杂连接顺序Hint体系极其丰富,LEADING控制驱动表、PARALLEL控制并行、MATERIALIZE强制物化,优化空间大

说着重一点,MySQL的FORCE INDEX用得时候要小心——它只是建议,不保证一定走你指定的索引。很多时候你FORCE INDEX(idx_a),优化器还是选择ALL,这是MySQL优化器在特定估算下的行为。更好的做法是先把表统计信息更新一下(ANALYZE TABLE),让优化器拿到更准确的基数估算,很多"优化器选错索引"的问题,更新统计信息之后自然就解决了。

5.3 从Oracle并行SQL里反向借鉴MySQL的调优思路

Oracle的并行SQL给MySQL调优提供了一个很好的反向借鉴:MySQL没有原生的并行执行能力(MySQL 8.0的并行查询仍在有限场景下支持,比如innodb_parallel_read_threads影响的是单表扫描的预读线程数,并不是真正的查询并行执行)。所以MySQL里遇到大查询,你不能指望靠并行度提升来解决,只能从两个方向用力:一是减少扫描数据量,二是让每个操作更便宜。

减少扫描数据量可以做的事情包括:分区裁剪、索引条件下推、覆盖索引、物化汇总表(相当于手动实现Oracle的物化视图)。让操作更便宜包括:避免filesort、避免临时表、用JOIN替代子查询、把多个小查询合并成一个大查询减少网络往返。

这个思路说起来简单,但做起来需要你在执行计划里反复验证每个操作符的代价。我在线上遇到MySQL大表统计查询时,常用的组合拳是:先看是否可以走分区裁剪,不行就加覆盖索引,再不行就建汇总表做定期增量刷新。这套路和Oracle并行SQL最后达到的效果是一致的——用户拿到结果的时间短了,代价是我们要在后台额外维护聚合数据的一致性。

6. 慢SQL优化的一线自查清单:先照做,再理解

到这里,我把索引策略和查询调优的核心内容都过了一遍。最后分享一个我自己在每次处理慢SQL问题时都会过的自查清单,你可以直接照着做:

  • 打开慢日志,找rows_examined最大的几条SQL,先处理它们,不要被query_time最高的SQL迷惑——有大量回表的查询单次可能只要几十毫秒,但它被调用10万次,整体代价远大于一条单次10秒的SQL。
  • 对目标SQL执行EXPLAIN,按type -> key -> rows -> Extra的顺序检查。
  • 如果type是ALL或index,先确认WHERE条件里的列有没有可用的索引;如果没有,针对WHERE里的等值列+排序字段建联合索引。
  • 检查是否存在函数操作、隐式类型转换、前置通配符、OR条件这些索引杀手。
  • 确认SELECT的字段能不能被覆盖索引完全覆盖。
  • 检查GROUP BY / ORDER BY的字段和索引顺序是否一致。
  • 如果SQL关联了多张表,用小表驱动大表,用INNER JOIN替代LEFT JOIN(业务语义允许的前提下)。
  • 验证优化后的EXPLAIN和实际线上执行时间,特别要注意索引使用是否稳定,会不会因为数据分布变化导致执行计划漂移。

在实际操作中,我的经验是每一条慢SQL都值得追到底,而不是加个索引就完事。一条SQL慢只是表象,它身后可能藏着错误的数据模型、缺失的业务约束、错误的调用方式。索引是优化手段,但它解决不了根本的架构问题。当你发现一条SQL无论怎么优化都得全表扫描时,也许是时候重新审视表结构设计了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询