4.2.2 慢查询案例

下面通过一个完整的实战案例,从发现问题、配置日志、分析原因、索引优化到效果验证,完整展示慢查询的处理流程。


🛒 1. 场景与准备

电商系统,订单表 orders 数据量约 500万行,用户频繁查询自己的订单并按创建时间倒序展示。

表结构
sql 复制代码
CREATE TABLE orders (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NOT NULL,
    status ENUM('pending','paid','shipped','completed','cancelled') NOT NULL,
    create_time DATETIME NOT NULL,
    amount DECIMAL(10,2) NOT NULL,
    product_name VARCHAR(200) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
初始索引(仅有主键)
sql 复制代码
-- 无任何二级索引
数据分布
  • 用户数:10万
  • 每个用户平均 50 笔订单
  • 高频查询:某用户按时间倒序取最近 20 笔

⚙️ 2. 慢查询日志配置

为捕获执行超过 0.3秒 的查询,开启慢查询日志并写入文件,同时记录详细字段。

sql 复制代码
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.3;
SET GLOBAL log_output = 'FILE';
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
SET GLOBAL log_slow_extra = ON;
SET GLOBAL min_examined_row_limit = 500;
SET GLOBAL log_queries_not_using_indexes = OFF; -- 本例先关闭,专注慢查询

配置文件 /etc/my.cnf 中持久化:

ini 复制代码
[mysqld]
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.3
log_output = FILE
log_slow_extra = ON
min_examined_row_limit = 500
log_queries_not_using_indexes = OFF

🐢 3. 慢查询复现

应用程序频繁执行以下 SQL(分页查询用户订单):

sql 复制代码
SELECT id, status, create_time, amount, product_name
FROM orders
WHERE user_id = 10086
ORDER BY create_time DESC
LIMIT 20;

由于表上只有主键索引,user_id 无索引,且需要按 create_time 倒序排序,MySQL 只能全表扫描。

在 MySQL 客户端模拟执行(多次运行以产生足够慢日志):

sql 复制代码
SELECT ... FROM orders WHERE user_id = 10086 ORDER BY create_time DESC LIMIT 20;

执行时间通常在 2~8秒,远超 0.3秒。


📄 4. 慢查询日志内容

查看慢日志尾部:

bash 复制代码
tail -n 20 /var/log/mysql/slow.log

得到类似输出:

text 复制代码
# Time: 2025-06-27T10:20:33.123456Z
# User@Host: app_user[app_user] @ db1 [10.0.0.5]
# Thread_id: 128  Schema: ecommerce  QC_hit: No
# Query_time: 5.234567  Lock_time: 0.000112  Rows_sent: 20  Rows_examined: 5000000
# Rows_affected: 0  Bytes_sent: 2048
SET timestamp=1719476433;
SELECT id, status, create_time, amount, product_name
FROM orders
WHERE user_id = 10086
ORDER BY create_time DESC
LIMIT 20;

关键发现

  • Query_time: 5.23秒 严重超标。
  • Rows_examined: 5000000 扫描全表所有行,而 Rows_sent: 20 只返回20行,比例极差。
  • 没有合适的索引,导致全表扫描 + 文件排序。

🔍 5. 分析与诊断

使用 EXPLAIN 查看执行计划
sql 复制代码
EXPLAIN
SELECT id, status, create_time, amount, product_name
FROM orders
WHERE user_id = 10086
ORDER BY create_time DESC
LIMIT 20;

输出:

id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE orders ALL NULL NULL NULL NULL 4980000 Using where; Using filesort

解读:

  • type: ALL 全表扫描。
  • key: NULL 未使用任何索引。
  • Extra: Using filesort 需要额外排序,因为未使用索引顺序。
  • rows 估计扫描近500万行。

问题根因 :缺少包含 user_idcreate_time 的联合索引。


🔧 6. 优化措施

创建联合索引

索引设计需满足:

  • user_id 等值过滤。
  • create_time DESC 排序,以消除 filesort。
  • 查询列 id, status, create_time, amount, product_name 尽量覆盖,避免回表。

权衡磁盘空间和写入负载后,创建覆盖索引(读远大于写,可接受稍大索引):

sql 复制代码
ALTER TABLE orders ADD INDEX idx_usr_time_cover 
    (user_id, create_time DESC, status, amount, product_name);

列顺序解释

  • user_id 等值条件,放在最前。
  • create_time DESC 范围排序,紧接其后,利用降序索引特性(MySQL 8.0)。
  • status, amount, product_name 为覆盖列,避免回表。

如果 MySQL 版本低于 8.0,降序索引不支持,可创建 (user_id, create_time) 即可,查询中的 DESC 仍可利用索引反向扫描(8.0 之前虽不支持真正降序索引,但升序索引可反向扫描用于 ORDER BY ... DESC)。

执行创建语句
sql 复制代码
ALTER TABLE orders ADD INDEX idx_usr_time_cover 
    (user_id, create_time DESC, status, amount, product_name);

在线操作可能耗时几分钟,根据表大小评估窗口期。


🧪 7. 验证优化效果

再次执行原查询
sql 复制代码
SELECT id, status, create_time, amount, product_name
FROM orders
WHERE user_id = 10086
ORDER BY create_time DESC
LIMIT 20;

执行时间降至 0.01秒 左右。

使用 EXPLAIN 验证新执行计划
sql 复制代码
EXPLAIN
SELECT id, status, create_time, amount, product_name
FROM orders
WHERE user_id = 10086
ORDER BY create_time DESC
LIMIT 20;

输出:

id table type key key_len ref rows Extra
1 orders ref idx_usr_time_cover 8 const 50 Using where; Using index
  • type: ref 索引等值查找。
  • key: idx_usr_time_cover 使用了新索引。
  • Extra: Using index 覆盖索引,无需回表。
  • rows 估算仅扫描该用户的50行,实际直接定位并顺序读取。

再无 Using filesort,排序直接利用索引顺序。


📈 8. 效果对比与总结

指标 优化前 优化后
执行时间 5.23 秒 0.01 秒
扫描行数 500 万 (全表) ~50 (仅用户订单)
Extra Using where; Using filesort Using where; Using index
索引使用 idx_usr_time_cover (覆盖)
排序 文件排序 索引顺序,无额外排序
回表 --- 无(覆盖索引)

通过开启慢查询日志并正确配置,成功捕获了这条典型的全表扫描慢 SQL。借助 EXPLAIN 分析执行计划,找到了问题根因------缺少合适的联合索引。最终通过设计覆盖索引,将查询性能提升了 500倍以上,从数秒降至毫秒级,极大提升了用户体验并降低数据库负载。


💎 9. 慢查询优化的通用闭环

  1. 配置 :合理设置 long_query_timelog_slow_extra、轮转策略。
  2. 捕获:收集慢查询日志。
  3. 分析 :使用 mysqldumpslow / pt-query-digest 汇总,EXPLAIN 逐条诊断。
  4. 优化:添加/调整索引、改写 SQL、调整库表结构。
  5. 验证 :再次 EXPLAIN 确认计划,并观察执行时间。
  6. 监控:持续关注慢查询数量,形成预防性优化文化。