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 字段的排序,从好到差大概是:

  1. system/const:最多匹配一行,极快
  2. eq_ref:主键或唯一索引等值匹配
  3. ref:普通索引等值匹配
  4. range:索引范围扫描
  5. index:索引全扫描
  6. 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% 都是“男”,你查“男”的时候优化器觉得全表扫描更快,就不走索引了。这不是索引失效,是优化器认为走索引成本更高。

核心原则:索引失效的本质,是数据库无法利用索引的有序性和快速定位能力。你写的 SQL 破坏了这种能力,索引就形同虚设。

3.4 知识体系图:性能瓶颈分析全景

下面这张图,把慢查询的根因、分析工具、索引失效场景串在了一起。你可以把它当作排查时的路线图。

查询性能瓶颈分析全景图 慢查询根因分析 慢查询常见原因 执行计划解读 索引失效场景 全表扫描 数据量过大 锁竞争 SQL写法问题 type: ALL → 全表扫描 Extra: Using filesort key: NULL → 无索引 隐式类型转换 函数包裹索引列 LIKE '%关键字' 违反最左前缀 OR 连接条件 优化器放弃索引 排查口诀 先看 type 和 key,再查 Extra 和 rows;索引失效六场景,逐一对照别放过

3.5 避坑指南与实战建议

最后,分享几个我自己的实战经验:

  • 养成习惯:写完 SQL 先跑 EXPLAIN,别等上线了再查。我在团队里推行了“SQL Review 清单”,第一条就是“是否已检查执行计划”。
  • 监控先行:在数据库层面开启慢查询日志,设置阈值 1 秒。每周复盘一次,把 TOP 10 慢查询拿出来分析。
  • 索引不是越多越好:我曾经见过一个表建了 20 多个索引,写入性能惨不忍睹。索引是读的加速器,写的减速器。
  • 联合索引设计:把区分度高的列放前面,等值条件放前面,范围条件放后面。这个顺序直接影响索引效率。
我曾经踩过的坑:有一次线上订单查询突然变慢,排查了半天,发现是开发在代码里用了 WHERE order_id IN (?),传入的列表有 5000 个值。IN 列表太长,优化器直接放弃索引走了全表扫描。后来我们限制了 IN 列表长度不超过 200,问题解决。