SQL 性能调优:EXPLAIN 详解与慢查询优化案例

各位架构师、数据库的"老中医",大家好!今天我们来聊聊数据库的"体检报告"------EXPLAIN。

  • 当你的接口响应慢得像蜗牛,CPU 飙升得像火箭时,千万别急着重启数据库,也别盲目地加索引。这时候,你需要的是给 SQL 拍一张"X 光片",看看它到底是在"跑步"(高效索引扫描),还是在"散步"(全表扫描)。

今天,我们用硬核的方式,"彻底"拆解 SQL 性能调优。

第一步:捕捉"嫌疑人"------慢查询日志

在优化之前,你得先知道是谁在拖后腿。MySQL 有个自带的"监控摄像头",叫慢查询日志

开启方式(临时生效):

复制代码
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
-- 设置阈值:超过 2 秒的 SQL 才会被记录(生产环境建议设为 1 或 0.5)
SET GLOBAL long_query_time = 2;

分析工具:

日志文件通常位于 /var/lib/mysql/slow-query.log。别用记事本一行行看,太累!用 MySQL 自带的工具 mysqldumpslow

复制代码
# 按查询时间排序,取前 10 条"最慢"的 SQL
mysqldumpslow -s t -t 10 /var/lib/mysql/slow-query.log

第二步:拍"X 光片"------EXPLAIN 详解

拿到慢 SQL 后,在它前面加上 EXPLAIN,就能得到它的执行计划。

  • EXPLAIN SELECT * FROM users WHERE name = 'Alice';

结果里字段很多,别慌。作为架构师,你只需要重点关注三个"命门":typekeyExtra

1. type:扫描类型(性能的生命线)

这是最重要的指标,它决定了 MySQL 是怎么找数据的。性能从优到差,等级森严:

  • const / eq_ref:这是"特快专递"。通过主键或唯一索引查询,直接定位,只读一行。
  • ref:这是"普通快递"。通过普通索引查询,找到匹配的行。
  • range :这是"区间扫描"。比如 WHERE id > 10,只扫描一部分索引。
  • index:这是"全索引扫描"。虽然也是扫描,但只扫索引树,不扫数据页,比全表快一点。
  • ALL :这是"地毯式搜索"。全表扫描! 看到 ALL,就像看到医生在体检报告上写了个"危",必须优化!

洞察

一般要求 SQL 至少达到 range 级别,最好是 ref。如果是 ALL,说明你的索引在"罢工"。

2. key:实际使用的索引
  • key :MySQL 实际用了哪个索引。如果是 NULL,说明没走索引。
  • possible_keys:MySQL 觉得可以用哪些索引。
  • 注意 :如果 possible_keys 有一堆索引,但 keyNULL,说明 MySQL 的优化器"犯傻"了,或者索引失效了。
3. Extra:额外信息(隐藏的性能杀手)

这里的信息量最大,也是"坑"最多的地方:

  • Using index完美! 覆盖索引。MySQL 直接在索引里就找到了所有需要的数据,连表都不用回(不用查数据页)。
  • Using where普通。 需要在存储引擎层根据条件过滤。
  • Using filesort警告! 文件排序。说明 MySQL 无法利用索引来完成排序,必须把数据取出来放到内存或磁盘上单独排序。这是性能杀手!
  • Using temporary严重警告! 使用了临时表。常见于 GROUP BYORDER BY 字段不一致时。先把数据塞进临时表,处理完再返回,效率极低。

第三步:对症下药------常见优化套路

1. 索引为什么会"迷路"(失效)?

明明建了索引,为什么 type 还是 ALL?通常是因为你触犯了"索引禁忌":

  • 对索引列"动刀"

    -- 错误:在索引列上做计算,索引直接报废
    SELECT * FROM orders WHERE YEAR(create_time) = 2026;
    -- 正确:把计算移到右边
    SELECT * FROM orders WHERE create_time >= '2026-01-01';

  • 模糊查询的前导通配符

    -- 错误:LIKE '%abc',因为索引是从左到右的,前面模糊相当于大海捞针
    -- 正确:LIKE 'abc%',走范围扫描

  • 类型隐式转换

    -- 错误:phone 字段是字符串,你却传了数字
    SELECT * FROM users WHERE phone = 13800000000;
    -- 正确:加引号
    SELECT * FROM users WHERE phone = '13800000000';

2. 分页优化:LIMIT 1000000, 10 的痛

当用户翻到第 100 万页时,你的 SQL 可能会慢死:

  • SELECT * FROM products LIMIT 1000000, 10;

原理:MySQL 会扫描前 1000010 条记录,然后丢弃前 1000000 条,只返回最后 10 条。这简直是浪费生命!

优化方案(延迟关联)

利用覆盖索引,先只查主键,再回表。

复制代码
SELECT p.* 
FROM products p
JOIN (SELECT id FROM products LIMIT 1000000, 10) AS tmp
ON p.id = tmp.id;

解释 :子查询 tmp 利用了覆盖索引(只查 id),速度极快。拿到 10 个 id 后,再跟原表关联,瞬间完成。

索引绝对不是越多越好

索引是一把双刃剑 :它在加速查询(读操作)的同时,会显著拖慢数据的写入和更新(写操作),并消耗宝贵的存储和内存资源。索引是用空间写入性能 换取读取性能的工具。优秀的程序员不会盲目堆砌索引,而是像狙击手一样,精准地只为最关键的查询路径提供支援。

写入性能的"多米诺骨牌"效应

这是索引过多最直接的代价。在 InnoDB 引擎中,数据的增删改(INSERT/UPDATE/DELETE)不仅仅是修改数据页,还必须同步维护所有相关的二级索引。

  • 底层原理
    当你插入一行数据时,数据库不仅要写入聚簇索引(主键索引),还要找到该行数据在所有二级索引树中的位置并插入。
    如果一张表有 10 个索引,一次 INSERT 操作实际上变成了 11 次 磁盘 I/O 操作(1次数据页 + 10次索引页)。
    更糟糕的是,这会导致频繁的页分裂(Page Split)。为了保持 B+ 树的有序性,插入新数据可能导致索引页满了,需要分裂出新页,这会极大地消耗 CPU 和 I/O 资源。
内存(Buffer Pool)的"挤兑"

数据库的性能很大程度上依赖于内存缓存(如 MySQL 的 Buffer Pool)。内存是有限的资源。

  • 底层原理
    • 索引也是要加载到内存中的。如果你建立了大量低频使用的索引,这些索引页会挤占 Buffer Pool 的空间。
    • 结果就是:真正热点的数据页(Data Page)被置换出内存,导致核心业务查询时发生大量的磁盘 I/O(缺页中断),反而降低了整体系统的吞吐量。
查询优化器的"选择困难症"

你可能认为索引多了,优化器(Optimizer)的选择就多了,查询会更快。其实恰恰相反。

  • 底层原理
    • 当一张表上有几十个索引时,MySQL 的查询优化器在生成执行计划时,需要计算和评估每一条路径的成本。
    • 这不仅增加了 SQL 解析阶段的 CPU 开销,还可能导致优化器"眼花",错误地选择了一个次优索引(比如选了区分度很低的索引),导致查询性能不升反降。
冗余与维护成本

很多索引其实是重复的,或者根本用不上。

  • 最左前缀原则的冗余
    如果你已经建立了一个联合索引 (a, b),那么单独给 a 再建一个索引就是完全多余的。因为 (a, b) 索引的最左前缀已经覆盖了 a 的查询需求。
  • 维护噩梦
    随着数据量的增长,索引会产生碎片 (Fragmentation)。索引越多,碎片整理(OPTIMIZE TABLERebuild Index)的时间就越长,线上运维的风险也越大。
什么时候索引会"失效"?

即使你建了很多索引,如果写法不对,它们也会全部失效,变成摆设。以下情况索引会"迷路":

  • 对索引列做运算WHERE YEAR(create_time) = 2026(索引失效,全表扫描)。
  • 模糊查询前导通配符LIKE '%abc'(索引失效)。
  • 类型隐式转换 :字符串字段没加引号 phone = 1380000(索引失效)。
"索引设计法则"

为了平衡读写性能,建议遵循以下原则:

  1. 按需创建,少而精
    只给查询频率高、区分度大(基数大)的字段建索引。不要给"性别"、"状态"这种只有几个值的字段单独建索引。
  2. 利用联合索引(覆盖索引)
    尽量使用联合索引(如 (a, b, c))来覆盖多个查询场景,减少回表操作。
  3. 定期清理
    利用 sys.schema_unused_indexes (MySQL 5.7+) 或 Performance Schema 定期排查从未使用的索引,果断删除。
  4. 写入优先场景
    对于日志表、流水表这种写多读少的表,尽量少建索引,甚至只保留主键索引,以保证写入吞吐量。

总结

SQL 调优不是靠猜,是靠数据。

  • EXPLAIN 是你的听诊器,通过 type 听心跳,通过 Extra 找病灶。
  • 索引 是你的高速公路,别让 ALL 把你拉回泥泞土路。
  • 覆盖索引 是你的VIP通道,能不走回头路(回表)就不走。

最后,送上金句

"调优不是靠猜,是靠数据。EXPLAIN 是你的听诊器,通过分析执行计划,让每一条 SQL 都走在最短的路径上。"

相关推荐
AI人工智能+电脑小能手8 小时前
【大白话说Java面试题 第87题】【Mysql篇】第17题:分布式事务的实现原理?
java·数据库·分布式·mysql·面试
yyuuuzz8 小时前
独立站的技术基础与常见运维问题
大数据·运维·服务器·网络·数据库·aws
Cosolar10 小时前
从零写一个 Attention Is All You Need
人工智能·面试·架构
键盘上的猫头鹰11 小时前
【MySQL 教程(八)】索引、事务、用户管理、导入导出与分页查询
数据库·python·mysql
Royzst11 小时前
数据库知识点
数据库
雪的季节12 小时前
企业级 Qt 全功能项目
开发语言·数据库·qt
宋浮檀s12 小时前
应急响应——Web漏洞:命令执行+SSRF+弱口令
运维·数据库·sql·网络安全·oracle·应急响应
jiayong2313 小时前
AI架构师面试题库 - 完整汇总文档
人工智能·面试·职场和发展
yurenpai(27届找实习中)13 小时前
redis_点评(21.好友关注——关注、取关功能实现;共同关注功能实现)
数据库·redis·缓存
Rick199313 小时前
索引的排序和分组
数据库·mysql