MySQL慢查询诊断与索引优化实战指南

慢查询是数据库性能问题的根源

MySQL性能调优的第一步是定位慢查询。线上数据库80%以上的性能问题由不到5%的SQL语句造成——全表扫描、缺失索引、低效JOIN、子查询改写不当是四大元凶。数据库运维的核心工作不是调参数,而是找出这些慢SQL并从根本上优化执行计划。

慢查询日志配置与分析

1. 开启慢查询日志

-- my.cnf动态配置
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;      -- 超过1秒记录
SET GLOBAL log_queries_not_using_indexes = ON;  -- 记录未用索引的查询
SET GLOBAL min_examined_row_limit = 100;  -- 扫描行低于100不记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

生产环境建议long_query_time设0.5-1秒,避免日志量过大。min_examined_row_limit过滤掉扫描行数很少的慢查询。

2. pt-query-digest分析Top N慢查询

pt-query-digest /var/log/mysql/slow.log --limit 95% --order-by Query_time:sum

# 输出示例
# Rank Query ID           Response time  Calls  R/Call  V/M
# ==== ================== ============== ====== ======= ====
#    1 0x7A3B2C1D4E5F6...  2543.1234 45%  1234   2.0639  0.12
#    2 0x8B9C0D1E2F3A4...  1876.5678 33%   567   3.3012  0.08
#    3 0x9C0D1E2A3B4C5...   432.9012  8%  3456   0.1253  0.01

Response time占比最高的Query ID就是优化目标。查看具体SQL:

pt-query-digest slow.log --filter '$event->{arg} =~ /0x7A3B2C1D4E5F6/' --print

EXPLAIN执行计划深度解读

拿到目标SQL后用EXPLAIN分析执行计划:

EXPLAIN FORMAT=JSON
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-01-01'
ORDER BY o.amount DESC
LIMIT 20;

重点关注以下字段:

– **type**:access类型,从优到劣:system > const > eq_ref > ref > range > index > ALL。出现ALL(全表扫描)必须优化
– **key**:实际使用的索引,NULL表示未用索引
– **rows**:预估扫描行数,越大越危险
– **Extra**:附加信息,出现Using filesort或Using temporary需要重点优化
– **Filtered**:过滤比例,低于10%说明索引选择性差

JSON格式输出更详细,attached_condition字段展示MySQL在存储引擎层还是服务层做过滤。过滤在存储引擎层(ICP)效率更高。

索引设计原则与实战案例

案例1:联合索引最左前缀匹配

-- 原始查询
SELECT * FROM orders 
WHERE user_id = 1001 AND status = 'PAID' 
ORDER BY created_at DESC 
LIMIT 10;

-- 错误索引:where条件和排序分属不同索引
ALTER TABLE orders ADD INDEX idx_user (user_id);
ALTER TABLE orders ADD INDEX idx_status (status);
ALTER TABLE orders ADD INDEX idx_created (created_at);

-- 正确索引:联合索引覆盖where + order
ALTER TABLE orders ADD INDEX idx_user_status_created (user_id, status, created_at);

-- EXPLAIN验证
type: ref
key: idx_user_status_created
rows: 45
Extra: Backward index scan  -- MySQL 8.0+降序索引优化

联合索引(user_id, status, created_at)同时满足等值过滤和排序,避免了filesort。

案例2:覆盖索引消除回表

-- 查询只需要少量列
SELECT order_id, amount FROM orders 
WHERE user_id = 1001 AND status = 'PAID';

-- 覆盖索引:查询列全部包含在索引中
ALTER TABLE orders ADD INDEX idx_cover (user_id, status, order_id, amount);

-- EXPLAIN输出
Extra: Using index  -- 无回表,直接从索引树返回数据

覆盖索引将查询从5万次回表降到0次,IO减少90%以上。代价是索引占用更多磁盘空间,适合高频查询场景。

案例3:索引下推ICP优化

-- MySQL 5.6+支持Index Condition Pushdown
SELECT * FROM orders 
WHERE user_id = 1001 AND amount > 1000;

-- 索引
ALTER TABLE orders ADD INDEX idx_user_amount (user_id, amount);

-- ICP开启时:存储引擎层先用user_id定位,再用amount > 1000过滤
-- ICP关闭时:存储引擎只按user_id过滤,amount在Server层判断
-- ICP减少回表次数

SET optimizer_switch = 'index_condition_pushdown=on';  -- 默认开启

JOIN优化与子查询改写

1. 小表驱动大表

-- 慢写法:大表做驱动表
SELECT o.* FROM orders o 
JOIN order_items i ON o.id = i.order_id 
WHERE o.status = 'PAID';

-- 优化:确保小结果集做驱动
-- EXPLAIN中rows少的是驱动表
-- 添加合适的索引让优化器选对驱动顺序
ALTER TABLE order_items ADD INDEX idx_order_id (order_id);

2. 子查询改写JOIN

-- 慢:相关子查询,每行执行一次子查询
SELECT * FROM orders o 
WHERE o.amount > (
  SELECT AVG(amount) FROM orders WHERE user_id = o.user_id
);

-- 快:改写为JOIN + 派生表
SELECT o.* FROM orders o
JOIN (
  SELECT user_id, AVG(amount) as avg_amount 
  FROM orders GROUP BY user_id
) avg_t ON o.user_id = avg_t.user_id
WHERE o.amount > avg_t.avg_amount;

MySQL 8.0对子查询有大量优化(semijoin、materialization),但复杂相关子查询的性能仍不如JOIN。

分页查询优化方案

深分页问题

-- 慢:LIMIT 100000, 20扫描100020行丢弃100000行
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;

-- 优化方案1:游标分页(推荐)
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20;

-- 优化方案2:延迟关联
SELECT o.* FROM orders o
JOIN (
  SELECT id FROM orders ORDER BY id LIMIT 100000, 20
) tmp ON o.id = tmp.id;

延迟关联的核心思路:子查询走覆盖索引只取ID(无回表),再关联主表取完整数据。100万行表的深分页从3秒降到50毫秒。

在线诊断工具与监控

1. sys库快速诊断

-- 当前正在执行的SQL
SELECT * FROM sys.session 
WHERE command != 'Sleep' AND time > 1\G

-- 等待锁的会话
SELECT * FROM sys.innodb_lock_waits\G

-- 索引使用统计(发现冗余索引)
SELECT * FROM sys.schema_unused_indexes;
SELECT * FROM sys.schema_redundant_indexes;

2. Performance Schema持续监控

-- 开启statements_digest
UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' 
WHERE NAME = 'statements_digest';

-- Top 10慢SQL
SELECT DIGEST_TEXT, COUNT_STAR, SUM_TIMER_WAIT/1000000000 as total_sec,
  AVG_TIMER_WAIT/1000000 as avg_ms
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

3. Prometheus + mysqld_exporter监控

核心告警指标:

– mysql_global_status_slow_queries增长速率 > 10/min
– mysql_global_status_innodb_row_lock_waits > 5/min
– mysql_global_status_threads_running接近max_connections的80%

索引优化没有银弹,核心方法论是:慢查询日志定位 → EXPLAIN分析执行计划 → 针对性建索引 → 验证优化效果 → 持续监控。定期用pt-query-digest做周度回顾,防止新上线的SQL引入性能回退。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql-man-cha-xun-zhen-duan-yu-suo-yin-you-hua-shi-zhan-zhi-2/

(0)
小编小编
上一篇 9小时前
下一篇 9小时前

相关推荐