慢SQL优化实战:10个典型索引失效与查询性能提升案例
2026/9/17 3:06:11 网站建设 项目流程

开头

慢 SQL 这事儿,几乎每个做后端开发的人都会碰上。不管是刚工作一两年的新人,还是带团队的老手,只要系统流量一上来,数据库迟早会给你点颜色看看——接口突然从 50ms 变成 5s,监控告警刷屏,老板在旁边盯着你看,那一刻的心情,懂的都懂。

我这些年排查过的慢 SQL 没有一百条也有八十条,各种奇葩场景都见过:有的是一条 SQL 把数据库 CPU 打满,有的是索引没生效导致全表扫描,有的是数据量才几十万但查询慢得离谱,还有的是 SQL 本身写得就有问题,改写之后性能直接翻几十倍。这篇文章我挑了 10 个印象最深的案例,覆盖了索引失效、SQL 改写、表结构设计、优化器误判、分页深翻页、隐式转换、函数导致索引失效、范围查询、多表关联、子查询这 10 类高频问题。每个案例我都会把当时的 SQL、表结构、问题现象、排查过程和最终方案完整写出来,方便你对号入座。

不管你是后端开发、DBA,还是刚入门想系统学习 SQL 优化的同学,这篇文章都能给你一些能直接落地的思路。我的习惯是:先看执行计划,再定位瓶颈,最后再做针对性优化,而不是一上来就瞎加索引。

1. 索引失效的典型案例

1.1 隐式类型转换导致索引失效

第一个案例来自一个订单查询接口。业务反馈说订单列表页偶尔会卡,查了一下慢查询日志,发现有一条 SQL 平均执行时间在 2 秒以上:

SELECT * FROM t_order WHERE order_no = 'SO20240115001' LIMIT 10;

这条 SQL 看起来非常简单,order_no 字段上也建了唯一索引,按道理不应该慢。但我看了执行计划之后发现,type 是 ALL,也就是说走了全表扫描,索引完全没生效。

问题出在哪里?后面查了表结构才发现,order_no 字段的类型是 varchar(32),但当时的业务代码里传的参数是通过框架自动映射的,某些接口传过来的是数值类型。MySQL 在遇到字符串字段和数值比较时,会把字符串隐式转换为数值,然后在字段上做转换,导致索引失效。

注意:隐式类型转换是索引失效里最常见、也最坑的一种。字段是 varchar,传入参数是数字;字段是 datetime,传入参数是字符串。只要发生了隐式转换,MySQL 大概率不会走索引。

解决办法就是保证参数类型和字段类型一致。如果确实无法避免,可以在 SQL 里显式转换,让转换发生在参数侧而不是字段侧:

SELECT * FROM t_order WHERE order_no = CAST('SO20240115001' AS CHAR) LIMIT 10;

当然,最根本的做法还是在代码层面统一参数类型。比如在 Java 里不要用 Object 类型接收参数,接口定义是什么类型就是什么类型,避免框架帮你做隐式转换。

1.2 函数作用于字段导致索引失效

第二个案例是一个用户维度的统计查询。业务需要统计某个用户在最近 30 天内的下单金额:

SELECT user_id, SUM(amount) FROM t_order WHERE DATE(create_time) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY user_id;

t_order 表的数据量大概在 800 万行左右,create_time 字段上有普通索引。但这条 SQL 执行了将近 4 秒,原因很直接:DATE(create_time) 这个函数作用在了索引字段上,导致优化器无法使用 create_time 上的索引,只能全表扫描。

解决办法很简单,把函数从字段上移到参数侧。DATE_SUB(CURDATE(), INTERVAL 30 DAY) 的结果可以先算出来,然后用 create_time 直接跟一个具体的日期去比较,这样 create_time 上的索引就能正常走:

SELECT user_id, SUM(amount) FROM t_order WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY user_id;

改完之后执行时间从 4 秒降到了 300 毫秒左右,效果立竿见影。这类问题的通用规则就是:永远不要在索引字段上做任何运算,包括函数、算术运算和类型转换。

1.3 前缀模糊匹配导致索引失效

第三个案例是一个搜索接口慢的问题。业务是搜索商品名称包含关键词的商品,SQL 写法如下:

SELECT * FROM t_product WHERE product_name LIKE '%无线耳机%';

product_name 字段上有索引,但因为 LIKE 的模糊匹配是以 % 开头的,B+ Tree 索引无法利用前缀匹配的特性,优化器直接放弃索引选择全表扫描。这张表有 200 万行数据,查询结果集可能有几千行,每次都要全表扫一遍,耗时在 1.5 秒左右。

这种场景没有特别完美的解法。如果你的业务真的需要搜索包含某个关键词的商品,那更适合用全文索引或者搜索引擎来做。如果数据量不大,或者对实时性要求不高,也可以考虑用覆盖索引加全表扫描的折中方案——但本质上没法把前缀模糊查询优化成索引精确匹配。

我当时的做法是:先跟业务确认,搜索场景其实只关心前 10 个匹配结果,于是用了一条辅助 SQL 先做精准前缀匹配:

SELECT * FROM t_product WHERE product_name LIKE '无线耳机%' LIMIT 10;

这样能走索引,响应时间降到了几十毫秒。然后再在代码里做一次数据补充,把前缀匹配不到的结果用搜索引擎的召回结果填充。这个方案不是所有场景都适用,但思路可以参考——优先满足核心链路,把非核心链路放在后面补偿。

2. 分页查询深翻页的优化

2.1 深翻页为什么慢

第四个案例非常典型,是后台管理系统的列表页。运营同学反馈说,翻到第 100 页之后页面加载特别慢,甚至超时。查询 SQL 长这样:

SELECT id, order_no, user_id, amount FROM t_order ORDER BY create_time DESC LIMIT 100000, 20;

这条 SQL 慢的原因,一句话就能说清楚:MySQL 需要先扫描到第 100020 行,然后丢弃前面的 100000 行,只返回最后 20 行。这就像翻一本 10 万页的书,每翻一页都要从第 1 页开始翻起,翻到第 99980 页的时候,前面那一万页的阅读成本全都白花了。

在这个案例里,即使 create_time 上有索引,MySQL 也要遍历 10 万条索引记录,再回表拿到完整行数据,最终返回最后 20 条。整体耗时在 2.8 秒左右,数据量再大的话会更夸张。

2.2 延迟关联优化法

我用的方案是延迟关联。核心思路是:先用覆盖索引快速定位到需要的 20 个主键 ID,再通过主键回表去获取完整记录。这样即使偏移量很大,索引扫描阶段也只扫描了 100020 个主键,而不是 100020 个完整行,回表次数从 10 万次降到了 20 次。

SELECT t.id, t.order_no, t.user_id, t.amount FROM t_order t INNER JOIN ( SELECT id FROM t_order ORDER BY create_time DESC LIMIT 100000, 20 ) tmp ON t.id = tmp.id;

改完之后耗时降到了 400 毫秒左右。这个方案在很多场景下都适用,而且改动量不大,只是把原来的单表查询改成了子查询关联。

注意:如果业务允许,还有一种更彻底的方案叫基于游标的翻页。比如上一页最后一条记录是 create_time <= '2024-01-15 10:00:00',那下一页就查询 WHERE create_time < '2024-01-15 10:00:00' ORDER BY create_time DESC LIMIT 20。这种方案不管翻多少页,扫描的数据量都是固定的,性能最稳定,但需要业务端配合改造。

3. 多表关联查询的优化

3.1 关联字段没有索引

第五个案例是报表系统的多表关联查询。业务需要查订单表和用户表的关联数据,SQL 如下:

SELECT o.order_no, u.user_name, u.mobile, o.amount FROM t_order o INNER JOIN t_user u ON o.user_id = u.id WHERE o.create_time >= '2024-01-01' AND o.create_time < '2024-02-01';

t_order 表在 create_time 上有索引,所以订单表这一侧只扫了 1 月份的数据,大概 50 万行。但 t_user 表的 user_id 上没有索引!这就导致每一行订单数据都要去 t_user 表做一次全表扫描匹配,50 万次全表扫描,性能直接崩了。我的 DBA 同事告诉我这条 SQL 跑了 12 秒。

解决办法很直接,在 t_user 表的 id 字段上建主键索引。等等,id 本来就是主键啊?我看了执行计划才发现,问题不在 id,而在于驱动表选错了。优化器以 t_order 作为驱动表,对每一行订单记录去 t_user 表查找匹配的用户,如果 t_user 表是主键查找,单次查找是很快的,所以理论上不至于慢到这个程度。

实际排查后我发现,慢的根本原因不是关联字段没有索引,而是 t_order 表的 create_time 索引选择性太差。1 月份的 50 万行数据占了整张表的 60% 以上,优化器觉得走索引还不如全表扫描划算,就选择了全表扫描订单表,再做嵌套循环关联。全表扫描加上 50 万次关联匹配,就是这么慢的。

3.2 改写驱动表与关联条件

我的优化方案是两步走。第一步,优化 SQL 写法,明确指定驱动表和关联顺序:

SELECT o.order_no, u.user_name, u.mobile, o.amount FROM t_user u INNER JOIN t_order o ON o.user_id = u.id WHERE o.create_time >= '2024-01-01' AND o.create_time < '2024-02-01'

把 t_user 作为驱动表,先查出所有用户,再通过 user_id 去订单表里匹配。t_user 表大概有 100 万行,但通过嵌套循环连接,每次匹配 t_order 都能用上 user_id 上的索引,单次查找成本极低。

第二步,在 t_order 表上建一个联合索引(user_id, create_time),这样关联条件和过滤条件都能走索引。

ALTER TABLE t_order ADD INDEX idx_user_create (user_id, create_time);

改完之后 SQL 耗时从 12 秒降到了 800 毫秒。这个案例给我的启发是:多表关联查询的优化,关键在于搞清楚驱动表是谁、被驱动表的关联列有没有索引。很多情况下不是你 SQL 写错了,而是你没有给优化器提供足够的执行路径选择。

4. 子查询与 IN 的优化

4.1 IN 子查询过慢

第六个案例是一个库存扣减相关的查询。场景是查一批商品的库存信息,商品 ID 来自一个子查询:

SELECT * FROM t_inventory WHERE product_id IN ( SELECT product_id FROM t_product WHERE category_id = 1001 );

这个 SQL 慢得离谱,执行了 8 秒。我看了执行计划发现,MySQL 对 IN 子查询的处理方式是:先执行子查询,结果集有 10 万条,然后把这 10 万个外部参数拼装成一个很大的 IN 列表,再对 t_inventory 做全表扫描匹配。相当于遍历 10 万个值的列表,去和 t_inventory 的 500 万行数据做全表比较,不慢才怪。

4.2 改为 JOIN 关联

我的优化方案是改写成 JOIN:

SELECT i.* FROM t_inventory i INNER JOIN t_product p ON i.product_id = p.product_id WHERE p.category_id = 1001;

改完之后执行计划变成了先过滤 t_product 表(走 category_id 上的索引,只查出 10 万条),然后以这 10 万条作为驱动表,通过 product_id 关联到 t_inventory 表。t_inventory 表上的 product_id 有索引,每次匹配都是索引查找,总体耗时降到了 300 毫秒。

这个案例的核心教训是:IN 子查询并不一定慢,但它的性能取决于子查询结果集的大小和外部表的索引情况。如果你碰到 IN 子查询特别慢,第一反应应该是看执行计划——子查询是不是被物化成临时表了?外部表索引有没有生效?如果都正常还是慢,直接改成 JOIN 试试,通常会有惊喜。

4.3 关联字段字符集不一致导致索引失效

在这个案例中还顺手解决了一个隐藏问题。t_product 表的 product_id 是 varchar(20),t_inventory 表的 product_id 也是 varchar(20),但两张表的字符集不一样,一个是 utf8mb4,一个是 utf8。MySQL 在做 JOIN 时,会自动把 utf8 的字段转换为 utf8mb4 来比较,转换发生在了 t_inventory 表的 product_id 字段上,导致索引失效。

解决办法是统一两张表的字符集:

ALTER TABLE t_inventory MODIFY COLUMN product_id varchar(20) CHARACTER SET utf8mb4;

这个坑非常隐蔽,执行计划里可能显示关联类型是 ref,但实际扫描行数却异常高。如果你在建表时没有统一字符集规范,多表关联时很容易踩这个坑。我一般建议所有表默认都用 utf8mb4,可以省去大量这类问题。

5. 范围查询导致的索引失效

5.1 范围条件放在联合索引中间列

第七个案例来自一个订单查询接口。业务需要按时间范围和状态查询订单,SQL 如下:

SELECT * FROM t_order WHERE status = 1 AND create_time BETWEEN '2024-01-01' AND '2024-01-31' ORDER BY id DESC LIMIT 20;

我在 t_order 表上建了联合索引 idx_status_time(status, create_time)。理论上这条 SQL 应该能走这个索引,但实际执行计划显示,它只用了 status 这一个条件,create_time 的范围过滤是在回表之后做的,扫描行数比预期的多很多。

原因在于联合索引的匹配原则是:从左到右依次匹配,一旦遇到范围查询(BETWEEN、>、<),后面的列就无法继续使用索引。在这个索引里,status 是等值匹配,create_time 是范围匹配,用的没有问题。但仔细看会发现,SQL 里还有一个 ORDER BY id DESC。当 create_time 被用于范围过滤后,id 的排序就无法使用索引,MySQL 只能在回表后做一次文件排序(filesort),这会导致额外的性能开销。

5.2 调整联合索引列顺序

我的优化方案是换一个联合索引,让排序字段也能走索引:

ALTER TABLE t_order ADD INDEX idx_status_id_time (status, id, create_time);

这样优化器通过 idx_status_id_time 先按 status 等值过滤,然后按 id 进行索引排序,再在索引内部过滤 create_time 的范围条件。排序问题直接解决了,不需要文件排序,执行时间从 500 毫秒降到了 80 毫秒。

这里的核心思想是:联合索引设计时,不只要考虑 WHERE 条件,还要考虑 ORDER BY 和 GROUP BY。最常见的设计原则是:等值条件放最前面,其次是排序字段,最后才放范围条件。这样能让索引同时服务过滤和排序,效率最高。

注意:不要建太多联合索引。每建一个索引都会拖慢写入速度,占用更多磁盘空间。理想情况下,一个表的核心索引控制在 3-5 个,每个索引都要能覆盖到频繁执行的查询模式,而不是为了某一条慢 SQL 无脑加索引。

6. 并行 SQL 执行的优化思路

6.1 大查询拆分并行的适用场景

第八个案例比较特殊,来自一个离线数据统计任务。业务需要统计一年内所有用户的消费数据,SQL 如下:

SELECT user_id, SUM(amount) AS total_amount FROM t_order WHERE pay_time >= '2024-01-01' AND pay_time < '2025-01-01' GROUP BY user_id ORDER BY total_amount DESC LIMIT 100;

这个 SQL 的问题很典型:全表扫描一年的订单数据,大概 5000 万行,然后做 GROUP BY 聚合,再排序取前 100。即使 pay_time 上有索引,优化器也大概率会选择全表扫描,因为要扫的数据量太大。这条 SQL 跑一次要 45 秒,对于离线任务勉强能接受,但我们希望能更快一些。

对于这种场景,单纯靠索引优化已经不够了。我的方案是:把大查询拆成多个小查询,再用并行方式执行。具体思路是:按月份把一年的数据拆成 12 个区间,每个区间单独一个查询,同时并行执行。每个查询只处理一个月的数据,扫描量只有原来的 1/12,内存排序的压力也小得多。

-- 1月 SELECT user_id, SUM(amount) AS total_amount FROM t_order WHERE pay_time >= '2024-01-01' AND pay_time < '2024-02-01' GROUP BY user_id; -- 2月 SELECT user_id, SUM(amount) AS total_amount FROM t_order WHERE pay_time >= '2024-02-01' AND pay_time < '2024-03-01' GROUP BY user_id; -- ... 以此类推

6.2 多线程合并结果

如果应用层用 Java 的多线程来执行这 12 个查询,再对结果做合并和再排序,整个任务的耗时可以从 45 秒降到 5 秒左右。但要注意:并行不是银弹,盲目并行反而可能把数据库打爆。

提示:并行查询只适合大数据量、无事务依赖的只读场景。如果查询之间有依赖关系,或者你用的是小规格数据库实例,强行并行可能会导致连接数耗尽、锁竞争加剧,反而更慢。经验值上,并行度控制在 4-8 个比较稳妥,不要超过数据库 max_connections 的 20%。

另外,如果你们的数据库版本支持 MySQL 8.0 的并行查询特性(8.0.14 之后)或者用了暖气的分析型引擎,也可以直接通过设置 parallel_degree 参数来触发优化器并行执行。但这类特性在不同版本上表现差异很大,我建议还是先在测试环境验证效果再上生产。

7. 表结构设计引发的慢查询

7.1 SELECT 查询列过多

第九个案例来自一个用户中心的服务。查询用户基本信息和扩展字段的接口慢,SQL 如下:

SELECT * FROM t_user WHERE user_id = 123456;

t_user 表有 80 多个字段,包括用户头像地址、个性化配置、还包含几个大文本字段(TEXT 类型)。这张表有 800 万行数据,单行数据平均长度在 3KB 左右。虽然 user_id 上有唯一索引,这个查询走的是索引等值查询,按道理不该慢,但实际耗时仍然有 1.2 秒。

问题出在回表上。InnoDB 的聚簇索引叶子节点存的是整行数据,如果一行数据特别宽(3KB),回表时就要从磁盘读取更多的数据页。MySQL 的最小 IO 单位是 16KB 的页,如果每页只存了 5-6 行数据,为了拿到一行数据,可能就要多读好几个页。这个查询看起来走索引很快,但真正的瓶颈在随机 IO。

7.2 把宽表拆成窄表

我的方案不是去改 SQL,而是去改表结构。把 t_user 表中的大文本字段和不常用的扩展字段拆到独立的用户扩展表里,t_user 只保留核心高频字段,单行平均长度从 3KB 降到 500 字节左右。这样 16KB 的数据页可以容纳更多行,回表时随机 IO 次数大幅下降,查询耗时降到了 30 毫秒。

这个案例给我的启发是:SQL 优化不是只盯着 SQL 本身,表结构设计对查询性能的影响往往更大。如果你的表里有大文本字段、超多字段但大部分都用不上,这些都会拖慢主表的查询性能。宽表和窄表没有绝对的好坏,关键是区分高频和低频字段,把低频的大字段隔离出去。

注意:拆表之后,读写逻辑都要改。如果业务对实时性要求高,可以考虑引入缓存层来分摊读压力。拆表前一定要评估业务所有查询场景,避免把高频字段误拆到扩展表,结果每次查询都要 join 两张表,反而更慢。

8. 优化器误判与统计信息过期

8.1 统计信息不准确引发全表扫描

第十个案例,也是我印象最深的一个。一个订单查询接口在某次大促后突然变慢,SQL 非常简单:

SELECT * FROM t_order WHERE status = 2 AND create_time >= '2024-06-01' ORDER BY create_time DESC LIMIT 20;

status = 2 表示已支付订单,这个状态在订单表里占 90% 以上。但大促之后,表里涌入了大量新订单,status = 2 的占比其实更高了。MySQL 优化器依赖统计信息来决定执行计划,但统计信息没有及时更新,优化器低估了 status = 2 的过滤效果,选择了全表扫描,执行了 3 秒。

一开始我怀疑是索引失效,各种排查都没有发现问题。最后看了执行计划的预估行数和实际行数,发现差距巨大——预估返回 100 行,实际返回 80 万行。这时候我会优先确认是不是统计信息过期了。

8.2 使用 ANALYZE TABLE 触发统计信息更新

解决办法很简单,执行 ANALYZE TABLE 让优化器重新收集统计信息:

ANALYZE TABLE t_order;

执行完之后,再跑原来的 SQL,发现耗时降到了 50 毫秒。这个案例告诉我们:如果你的 SQL 本身没什么毛病,索引也都建对了,但执行计划就是很离谱,先看看统计信息是否过期。MySQL 的自动统计信息收集是有一定频率的,大表在高并发写入场景下容易出现统计信息落后于实际数据分布的情况。

日常运维中,我建议在每次大批量数据导入、或者大促活动结束后,手动执行一次 ANALYZE TABLE。这个操作的代价很小,但可能避免很多诡异的慢查询问题。

8.3 强制使用索引的兜底方案

如果统计信息更新之后优化器还是选择了不理想的执行计划,最后的兜底方案是用 FORCE INDEX 强制指定索引:

SELECT * FROM t_order FORCE INDEX (idx_status_time) WHERE status = 2 AND create_time >= '2024-06-01' ORDER BY create_time DESC LIMIT 20;

但我不推荐一上来就用 FORCE INDEX,因为这会把优化器的自主权拿走。一旦未来数据分布变化,这个强制索引可能反而不是最优的。正确的姿势是:先分析为什么优化器选错,能通过更新统计信息、重建索引解决的,优先选这些方式。只有确认优化器确实反复误判,且无法通过常规方式解决时,才考虑 FORCE INDEX,并要在代码里标注清楚原因,方便后续维护。

9. 常见慢 SQL 问题速查表

我把这些年最常见的慢 SQL 问题和对应解法整理成了一张表,方便大家排查时对照。这张表不是教科书,而是我从实际案例里提炼出来的经验汇总,按出现频率排序。

问题类型典型特征排查方向解决方案
隐式类型转换字段类型和参数类型不一致查看执行计划 type=ALL统一参数类型,避免字段侧转换
函数作用于索引字段WHERE 条件里有 DATE()、YEAR() 等查看索引是否被使用改写 SQL,函数移到参数侧
前缀模糊匹配LIKE '%keyword%'查看索引是否被使用改前缀匹配,或引入全文索引
深分页LIMIT 100000, 20扫描行数远大于返回行数延迟关联、基于游标翻页
多表关联无索引被驱动表关联列无索引查看执行计划 type=ALL 或 ref补充索引,或改写驱动表
IN 子查询过大子查询结果集很大查看是否物化临时表改 JOIN 或分批 IN
字符集不一致关联字段字符集不同查看 collation 不匹配统一字符集为 utf8mb4
范围查询导致排序失效联合索引同时有范围和排序字段查看 Extra 有 filesort调整联合索引列顺序
统计信息过期预估行数和实际行数差距大查看执行计划 rows 字段ANALYZE TABLE 手动更新
SELECT * 且行宽过大表字段多、大文本字段多查看单行平均长度拆宽表、只查询必要字段
数据倾斜某个值占比特别高查看过滤条件区分度改写 SQL,或拆分为多个查询

这张表适合在你接到一个慢 SQL 工单时,先对照特征快速定位问题方向。但记住,任何排查都从执行计划开始,纸上谈兵只会浪费时间。

10. 快慢 SQL 优化的完整排查流程优化

如果你拿到一条慢 SQL,不知道从哪儿下手,可以按下面这个顺序来。这套流程是我这几年踩过无数坑之后沉淀下来的,不说百分之百解决所有问题,但至少能保证你找到 90% 的问题根源。

第一步,从慢查询日志里把 SQL 捞出来,带上执行计划一起看。MySQL 的 EXPLAIN 可以告诉你访问类型、扫描行数、索引使用情况、是否有 filesort 或临时表。执行计划是你最重要的诊断工具,比任何人的经验都可靠。

第二步,分析 SQL 的访问模式。是单表查询还是多表关联?是等值查询还是范围查询?有没有 ORDER BY、GROUP BY、LIMIT?这些操作是否都能利用索引?把 SQL 拆成最小的条件单元,逐一判断是否走索引。

第三步,查看表结构和统计信息。字段类型是否合理?索引设计是否符合查询模式?统计信息是否过期?有时候问题不在 SQL 本身,而在表结构设计和索引规划上。

第四步,根据前两步的分析结果选择优化策略:

  • 如果索引没生效,先排查隐式转换、函数操作、字符集不一致这些坑。
  • 如果 SQL 本身写法有问题,考虑改写 SQL,比如 IN 改 JOIN、深翻页改延迟关联。
  • 如果索引已经到位但还慢,考虑是不是表结构设计不合理,比如宽表、大文本字段。
  • 如果数据量太大,任何优化都没法根治,就需要考虑数据归档、分库分表或者引入搜索引擎了。

第五步,验证优化效果。改完之后,用 EXPLAIN 和实际执行时间双重验证,然后观察一段时间,确认没有引入新的问题。如果是在生产环境操作,建议先在压测环境或低峰期进行,避免影响线上业务。

这条流程看似简单,但要真正做到位,需要你对执行计划的各种字段了如指掌,对不同优化手段的适用范围有清晰的认知。多积累案例,多分析执行计划,慢慢地你看到一条慢 SQL 就能本能地判断出问题在哪儿。

11. 写在最后的几个实操心得

最后分享几个我在实际排查过程中积累的小技巧,不算系统性的方法论,就是一些零碎但很实用的经验。

第一个,加索引之前先确认这个 SQL 是否需要索引。如果查询本身扫描的数据量占全表的比例超过 30%,优化器大概率不会走索引,就算你强制索引,性能也未必比全表扫描快。这种场景更适合从业务角度改造,比如限制查询范围、拆分查询维度,而不是硬怼索引。

第二个,能用覆盖索引的就别回表。我见过很多慢 SQL,明明 SELECT 只需要两三个字段,却用 SELECT * 把整行都捞出来,回表开销白白浪费。如果核心查询的字段能全部覆盖在索引里,MySQL 可以直接从索引返回结果,不回表,性能提升非常明显。

第三个,EXPLAIN 的 Extra 字段里出现 Using filesort 或 Using temporary,一定要优先处理。文件排序和临时表通常意味着查询做了额外的工作,这两个词一出现,基本就是性能瓶颈所在。

第四个,慢 SQL 优化要按 80/20 原则来,优先处理调用频率最高的那批 SQL,而不是单次最慢的那几条。一条每天跑一次、耗时 10 秒的离线任务,和一条每秒调用 100 次、耗时 200 毫秒的接口查询,后者的优化价值大得多。

第五个,如果你在做并行 SQL 优化,一定要做好结果合并的幂等性设计。并行查询的每个分片都要独立无依赖,合并且结果要能通过简单的方式去重或归并。不要搞到一半发现有两个分片查了相同的数据,结果还得重跑一遍,时间全浪费在这上面。

第六个,定期做慢查询巡检。不要等业务投诉了才去排查,我习惯每周花点时间看一次慢查询日志,把执行时间超过阈值的 SQL 拉出来分析一遍。很多问题在变成故障之前,其实早就已经在慢查询日志里躺了很久了。提前发现、提前优化,比事后救火要轻松得多。

这篇文章里说的 10 个案例,基本都是我在真实项目中碰到并解决的问题。SQL 优化这东西,理论知识看再多,不如自己亲手排查一次印象深。希望这些案例能帮你少走一些弯路,下次再碰到慢 SQL 的时候,能先深呼吸,打开执行计划,然后淡定地找到问题所在。

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

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

立即咨询