3、查询性能瓶颈分析:慢查询的常见原因、数据库执行计划解读、索引失效场景
性能问题,说白了就是查询跑得太慢。用户点个报表要等十几秒,后台接口超时报警,这时候你第一反应是什么?我个人的习惯是——先别急着加索引,也别去调数据库参数。先搞清楚一个问题:到底慢在哪?
这一章我们就来拆解慢查询的根因。我会结合自己踩过的坑,带你读懂执行计划,顺便把那些让你索引失效的“骚操作”一个个揪出来。
3.1 慢查询的常见原因
先列个清单。你遇到的大部分慢查询,跑不出下面这几类:
| 原因分类 | 典型场景 | 影响程度 |
|---|---|---|
| 全表扫描 | 没走索引,或者索引失效 | 极高 |
| 数据量过大 | 单表千万级,未做分区 | 高 |
| 锁竞争 | 行锁升级、死锁重试 | 中 |
| 网络延迟 | 跨机房查询、返回数据量过大 | 中 |
| SQL写法问题 | 隐式类型转换、函数包裹索引列 | 高 |
| 硬件瓶颈 | 磁盘IOPS打满、内存不足 | 极高 |
我在项目中遇到过最离谱的一次,是开发同事在WHERE条件里写了 WHERE DATE(create_time) = '2024-01-01'。看起来没问题对吧?但这一下就让索引失效了,直接全表扫描。后面我们会细讲为什么。
slow_query_log,把执行时间超过 1 秒的 SQL 捞出来。别凭感觉猜,数据说话。
3.2 数据库执行计划解读
执行计划,就是数据库告诉你“我打算怎么执行这条SQL”。读懂它,你才能对症下药。
3.2.1 怎么看执行计划
以 MySQL 为例,最常用的命令是:
EXPLAIN SELECT * FROM orders WHERE order_id = 10086;
输出结果里,有几个关键字段你必须盯住:
| 字段 | 含义 | 重点关注 |
|---|---|---|
| type | 访问类型 | ALL 要警惕,const/ref 是好的 |
| key | 实际使用的索引 | NULL 表示没走索引 |
| rows | 预估扫描行数 | 越大越危险 |
| Extra | 额外信息 | Using filesort、Using temporary 是红灯 |
type 字段的排序,从好到差大概是:
- system/const:最多匹配一行,极快
- eq_ref:主键或唯一索引等值匹配
- ref:普通索引等值匹配
- range:索引范围扫描
- index:索引全扫描
- ALL:全表扫描,性能杀手
type = ALL 且 rows 超过万级,基本可以断定这条SQL需要优化。我曾经接手过一个报表系统,每天凌晨跑半小时,一查全是ALL,加完索引直接降到3秒。
3.2.2 执行计划中的“红灯”信号
Extra 字段里出现下面这些词,你要打起精神:
- Using filesort:排序没走索引,数据量大了会非常慢
- Using temporary:用了临时表,通常是 GROUP BY 或 DISTINCT 没走索引
- Using where; Using index:这个反而是好的,表示覆盖索引
- Impossible WHERE:条件永远为假,检查逻辑
嗯,这里要注意:Using filesort 不是真的用了文件,而是说排序无法通过索引完成。我见过一个分页查询,ORDER BY 字段没在索引里,结果每页都要全排序,用户翻到第100页直接超时。
3.3 索引失效场景
这是面试高频题,也是实际开发中最容易踩的坑。我总结了最常见的 6 种场景,你对照着自查。
3.3.1 隐式类型转换
-- 假设 order_id 是 varchar 类型
SELECT * FROM orders WHERE order_id = 10086; -- 失效!
SELECT * FROM orders WHERE order_id = '10086'; -- 正常
数据库会把 order_id 隐式转为数字,导致索引列上发生函数操作。说白了,就是索引白建了。
3.3.2 对索引列使用函数
-- 失效写法
SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01';
-- 正确写法(范围查询)
SELECT * FROM orders
WHERE create_time >= '2024-01-01 00:00:00'
AND create_time < '2024-01-02 00:00:00';
我曾经在代码评审里反复强调这个点,但还是有人写。后来我直接在规范里加了一条:索引列禁止包裹函数。
3.3.3 模糊查询以通配符开头
-- 失效
SELECT * FROM products WHERE product_name LIKE '%手机%';
-- 有效(但只能匹配前缀)
SELECT * FROM products WHERE product_name LIKE '手机%';
为什么?因为 B+Tree 索引是按左前缀匹配的。你从中间开始模糊,索引就不知道怎么走了。
3.3.4 联合索引未遵循最左前缀
假设有联合索引 (a, b, c):
| 查询条件 | 是否走索引 | 说明 |
|---|---|---|
WHERE a=1 AND b=2 |
✅ 走 | 完全匹配最左前缀 |
WHERE b=2 |
❌ 不走 | 跳过了 a |
WHERE a=1 AND c=3 |
⚠️ 部分走 | a 走索引,c 不能走 |
3.3.5 使用 OR 连接条件
-- 如果 status 和 type 都有索引,但 OR 可能导致索引合并或全表扫描
SELECT * FROM orders WHERE status = 'completed' OR type = 'refund';
我的建议是:能用 UNION 代替 OR 就尽量用,或者确保每个条件都能独立走索引。
3.3.6 数据分布导致优化器放弃索引
这个比较隐蔽。比如性别字段,90% 都是“男”,你查“男”的时候优化器觉得全表扫描更快,就不走索引了。这不是索引失效,是优化器认为走索引成本更高。
3.4 知识体系图:性能瓶颈分析全景
下面这张图,把慢查询的根因、分析工具、索引失效场景串在了一起。你可以把它当作排查时的路线图。
3.5 避坑指南与实战建议
最后,分享几个我自己的实战经验:
- 养成习惯:写完 SQL 先跑
EXPLAIN,别等上线了再查。我在团队里推行了“SQL Review 清单”,第一条就是“是否已检查执行计划”。 - 监控先行:在数据库层面开启慢查询日志,设置阈值 1 秒。每周复盘一次,把 TOP 10 慢查询拿出来分析。
- 索引不是越多越好:我曾经见过一个表建了 20 多个索引,写入性能惨不忍睹。索引是读的加速器,写的减速器。
- 联合索引设计:把区分度高的列放前面,等值条件放前面,范围条件放后面。这个顺序直接影响索引效率。
WHERE order_id IN (?),传入的列表有 5000 个值。IN 列表太长,优化器直接放弃索引走了全表扫描。后来我们限制了 IN 列表长度不超过 200,问题解决。