慢查询问题的排查路径
线上MySQL出现查询耗时飙升时,盲目加索引往往是无效甚至有害的。系统化的排查路径是:确认慢查询范围→获取执行计划→分析扫描行数和索引使用情况→针对性优化。MySQL 8.0提供了更丰富的EXPLAIN信息、Performance Schema指标和sys库视图,完整诊断链路比5.7方便得多。本文从一条真实慢查询的排查出发,覆盖执行计划分析、索引策略、SQL改写和实例级调优。
慢查询日志配置与捕获
-- my.cnf 核心配置
[mysqld]
slow_query_log = ON
long_query_time = 0.5 # 超过0.5秒记录
slow_query_log_file = /var/log/mysql/slow.log
log_queries_not_using_indexes = ON
min_examined_row_limit = 100 # 扫描行少于100不记录,减少噪音
-- 动态调整(无需重启)
SET GLOBAL long_query_time = 0.5;
SET GLOBAL slow_query_log = ON;
通过sys库快速查看Top 10慢查询:
-- 按平均执行时间排序的Top 10慢查询
SELECT *
FROM sys.statements_with_runtimes_in_95th_percentile
ORDER BY avg_latency DESC
LIMIT 10;
-- 查看当前未完成的长事务
SELECT trx_id, trx_state, trx_started,
trx_query, trx_tables_locked, trx_rows_locked
FROM information_schema.innodb_trx
ORDER BY trx_started;
EXPLAIN执行计划深度解读
以一条分页查询为例,拆解EXPLAIN各字段的含义:
EXPLAIN ANALYZE
SELECT o.order_id, o.amount, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'PAID'
AND o.created_at >= '2026-07-01'
ORDER BY o.created_at DESC
LIMIT 20;
MySQL 8.0的EXPLAIN ANALYZE会输出实际执行耗时,比传统EXPLAIN更有参考价值。关注以下字段:
| 字段 | 含义 | 优化方向 |
|---|---|---|
| type | 访问类型 | 目标:ref/eq_ref/range,避免ALL和index |
| key | 实际使用的索引 | NULL表示未走索引 |
| rows | 预估扫描行数 | 越小越好,对比实际行数判断统计信息准确性 |
| filtered | 过滤比例 | 低于10%说明索引选择性差 |
| Extra | 额外信息 | Using filesort/Using temporary需要重点关注 |
常见问题解读:
- Using filesort:排序未走索引,需要额外排序操作。解决方案:创建覆盖排序字段的复合索引
- Using temporary:使用了临时表,常见于GROUP BY无索引场景。解决方案:为GROUP BY字段创建索引
- Using index:覆盖索引,查询所有字段都在索引中,无需回表,性能最优
索引优化策略与常见陷阱
策略1:复合索引遵循最左前缀原则
-- 创建复合索引
CREATE INDEX idx_status_created ON orders(status, created_at);
-- 能走索引的查询
SELECT * FROM orders WHERE status = 'PAID'; -- ✅ 走索引
SELECT * FROM orders WHERE status = 'PAID' AND created_at >= '2026-07-01'; -- ✅ 走索引
-- 不能走索引的查询
SELECT * FROM orders WHERE created_at >= '2026-07-01'; -- ❌ 跳过了最左列status
策略2:覆盖索引消除回表
-- 只需要order_id和amount,创建覆盖索引
CREATE INDEX idx_status_created_amount ON orders(status, created_at, order_id, amount);
-- EXPLAIN显示Using index,无需回表查聚簇索引
SELECT order_id, amount
FROM orders
WHERE status = 'PAID' AND created_at >= '2026-07-01'
ORDER BY created_at DESC
LIMIT 20;
策略3:避免索引失效的常见写法
-- ❌ 索引列上使用函数
SELECT * FROM orders WHERE YEAR(created_at) = 2026;
-- ✅ 改写为范围查询
SELECT * FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';
-- ❌ 隐式类型转换(user_id是varchar,传入整型)
SELECT * FROM orders WHERE user_id = 12345;
-- ✅ 类型匹配
SELECT * FROM orders WHERE user_id = '12345';
-- ❌ OR条件导致索引失效
SELECT * FROM orders WHERE status = 'PAID' OR amount > 10000;
-- ✅ UNION改写
SELECT * FROM orders WHERE status = 'PAID'
UNION
SELECT * FROM orders WHERE amount > 10000;
InnoDB缓冲池调优
缓冲池(Buffer Pool)是InnoDB性能的核心,合理的配置直接影响查询响应时间:
-- 查看当前Buffer Pool配置
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
SHOW VARIABLES LIKE 'innodb_buffer_pool_instances';
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
-- 推荐配置(独占服务器)
-- Buffer Pool占物理内存的70%-80%
SET GLOBAL innodb_buffer_pool_size = 12884901888; -- 12GB(16GB服务器)
SET GLOBAL innodb_buffer_pool_instances = 8; -- 每1.5GB一个实例
-- 多实例减少锁竞争(8.x默认已优化)
-- 监控命中率
SELECT
(1 - (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests)) * 100
AS buffer_pool_hit_rate;
-- 命中率应 > 99%,低于95%需要增大Buffer Pool或优化查询
MySQL 8.0的Buffer Pool预热策略:
-- 关闭时转储Buffer Pool状态,启动时自动预热
SET GLOBAL innodb_buffer_pool_dump_at_shutdown = ON;
SET GLOBAL innodb_buffer_pool_load_at_startup = ON;
-- 手动触发转储/加载
SET GLOBAL innodb_buffer_pool_dump_now = ON;
SET GLOBAL innodb_buffer_pool_load_now = ON;
-- 查看转储状态
SHOW STATUS LIKE 'Innodb_buffer_pool_dump_status';
预热可以在实例重启后快速恢复到停机前的缓存状态,将冷启动的查询延迟从分钟级降到秒级。
SQL改写与分页优化
深度分页(LIMIT 100000, 20)的性能问题在数据量大时尤为严重。传统写法扫描前100020行后丢弃前100000行,优化方案:
-- ❌ 传统深度分页
SELECT * FROM orders WHERE status = 'PAID' ORDER BY id LIMIT 100000, 20;
-- 扫描100020行,回表100020次
-- ✅ 延迟关联优化
SELECT o.* FROM orders o
INNER JOIN (
SELECT id FROM orders WHERE status = 'PAID' ORDER BY id LIMIT 100000, 20
) tmp ON o.id = tmp.id;
-- 子查询走覆盖索引只扫描id,外层只回表20次
延迟关联的核心是利用覆盖索引在子查询中快速定位分页offset的id值,再通过主键回表获取完整数据。在大数据量场景下,性能提升可达10-50倍。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql80-man-cha-xun-zhen-duan-yu-suo-yin-you-hua-shi-zhan/