慢查询异常排查案例(一):基于执行计划分析

做数据库审计日志分析,"慢查询" 是绕不开的老大难。说白了,系统突然变慢、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 —— 排序也没用上索引,临时文件排序。
避坑指南: 我曾经被 "Using filesort" 误导过,以为它在磁盘上排序。其实 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 秒。接口响应时间从超时变成毫秒级,问题解决。

小提醒: 慢查询日志定位 + EXPLAIN 确认 + 索引优化修复,这三步是 DBA 的黄金流程,我每次排查都按这个顺序走,从没失手过。

排查流程总图

慢查询异常排查流程图 应用告警/响应变慢 开启/检查慢查询日志 提取关键字段:Query_time / Rows_examined EXPLAIN 分析执行计划 定位全表扫描 → 索引优化 关键指标 Query_time > long_query_time Rows_examined 远大于 Rows_sent type=ALL、Extra=Using filesort

这张图画的就是我实际排查时的流程图。从收到告警开始,到确认日志落地,再到抓关键字段做分析,最后给出索引优化方案。每一步都对应着日志或者执行计划里的某个特征,你可以照着这个流程走一遍,基本能搞定 90% 的慢查询问题。