慢查询异常排查案例(一):基于执行计划分析
做数据库审计日志分析,"慢查询" 是绕不开的老大难。说白了,系统突然变慢、CPU 飙高、连接数打满,十有八九都是慢查询惹的祸。我个人习惯第一反应就是去翻 慢查询日志——这个操作基本上成了肌肉记忆。
这章咱们就从一个真实场景出发,聊怎么利用慢查询日志配合执行计划,精准定位全表扫描的问题。
慢查询日志 在哪里?长什么样?
先确认一件事:你的数据库有没有开启慢查询日志?
我遇到过不少同事,系统都卡死了,一查发现慢查询日志根本就没开。嗯,这个坑踩过之后就记住了。
登录 MySQL,跑一句命令看一眼:
mysql> SHOW VARIABLES LIKE 'slow_query%';
+---------------------+-------------------------------+
| Variable_name | Value |
+---------------------+-------------------------------+
| slow_query_log | ON |
| slow_query_log_file | /var/log/mysql/mysql-slow.log |
+---------------------+-------------------------------+
mysql> SHOW VARIABLES LIKE 'long_query_time';
+-----------------+----------+
| Variable_name | Value |
+-----------------+----------+
| long_query_time | 2.000000 |
+-----------------+----------+
- slow_query_log:是否开启。OFF 的话你想查也查不到。
- slow_query_log_file:日志文件路径,这是第一现场。
- long_query_time:超过这个秒数的 SQL 才算"慢"。默认 10 秒,实际我建议压到 2 秒甚至 1 秒。
一个真实的慢查询案例
前阵子公司线上一个订单查询接口,某几个时间段疯狂超时。我看了一眼慢查询日志,翻到这么一条:
# Time: 2025-03-15T14:23:11.123456Z
# User@Host: app_user[app_user] @ [10.0.1.100]
# Query_time: 8.723456 Lock_time: 0.000102
# Rows_sent: 15 Rows_examined: 987654
use order_db;
SELECT order_id, user_id, amount, status
FROM orders
WHERE status = 'PENDING'
AND create_time > DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY create_time DESC;
你看那个 Rows_examined: 987654,接近 100 万行。但结果只返回了 15 行。这说明什么?数据库把近百万行数据翻了个遍,才挑出那 15 条。要是表再大点,这系统不挂才怪。
执行计划:一眼看穿全表扫描
拿到慢 SQL,我的下一步动作就是 EXPLAIN。这玩意儿能帮你看到 MySQL 到底是怎么执行这条语句的。
mysql> EXPLAIN SELECT order_id, user_id, amount, status
-> FROM orders
-> WHERE status = 'PENDING'
-> AND create_time > DATE_SUB(NOW(), INTERVAL 7 DAY)
-> ORDER BY create_time DESC\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: orders
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 987654
Extra: Using where; Using filesort
这里有几个要命的信号:
- type: ALL —— 这你就直接告诉我:全表扫描。最差的情况,没有之一。
- possible_keys: NULL —— 连一个可选索引都没有。表上根本没建相关的索引。
- rows: 987654 —— MySQL 估算要扫 98 万行,基本对应全表数据。
- Extra: Using filesort —— 排序也没用上索引,临时文件排序。
全表扫描的常见诱因
为什么这条 SQL 走了全表扫描?你想想看,status 字段没索引,create_time 也没索引。MySQL 别无选择,只能全表撸一遍,再把结果丢到排序缓冲区去处理。
| 问题 | 表现 | 后果 |
|---|---|---|
| 缺少过滤索引 | type=ALL, possible_keys=NULL | 全表扫描,随着数据量增长线性变慢 |
| 索引选择性差 | type=index、rows 依然很大 | MySQL 觉得走索引还不如全表扫 |
| 隐式类型转换 | Extra: Using index 但实际全扫描 | 索引失效,相当于没用 |
| 函数包裹索引列 | WHERE 条件中使用函数 | 索引列被函数包装后无法使用 |
解决方案:索引优化
发现问题之后,我们得动手解决。对于这个 orders 表的查询,我建议建一个 复合索引:
mysql> ALTER TABLE orders
-> ADD INDEX idx_status_create_time (status, create_time);
你看这个索引的顺序:status 在前,create_time 在后。因为 SQL 里先等值查询 status = 'PENDING',接着是范围查询 create_time > ...,最后还用 create_time 排序。复合索引刚好覆盖这三个环节。
建完索引再来看看执行计划:
mysql> EXPLAIN SELECT order_id, user_id, amount, status
-> FROM orders
-> WHERE status = 'PENDING'
-> AND create_time > DATE_SUB(NOW(), INTERVAL 7 DAY)
-> ORDER BY create_time DESC\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: orders
type: range
possible_keys: idx_status_create_time
key: idx_status_create_time
key_len: 14
ref: NULL
rows: 12500
Extra: Using index condition
- type:ALL → range,直接从最差跳到良好级别。
- rows:98 万 → 1.25 万,扫描量收缩了两个数量级。
- Extra:Using filesort 消失了,变成 Using index condition,排序也走索引了。
实际效果呢?查询时间从 8.7 秒降到了 0.02 秒。接口响应时间从超时变成毫秒级,问题解决。
排查流程总图
这张图画的就是我实际排查时的流程图。从收到告警开始,到确认日志落地,再到抓关键字段做分析,最后给出索引优化方案。每一步都对应着日志或者执行计划里的某个特征,你可以照着这个流程走一遍,基本能搞定 90% 的慢查询问题。