下面通过一个完整的实战案例,从发现问题、配置日志、分析原因、索引优化到效果验证,完整展示慢查询的处理流程。
🛒 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_id 和 create_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. 慢查询优化的通用闭环
- 配置 :合理设置
long_query_time、log_slow_extra、轮转策略。 - 捕获:收集慢查询日志。
- 分析 :使用
mysqldumpslow/pt-query-digest汇总,EXPLAIN逐条诊断。 - 优化:添加/调整索引、改写 SQL、调整库表结构。
- 验证 :再次
EXPLAIN确认计划,并观察执行时间。 - 监控:持续关注慢查询数量,形成预防性优化文化。