MySQL性能调优实战:慢查询定位、索引策略与执行计划深度解读

慢查询日志配置与自动化分析

性能调优的第一步是找到瓶颈。MySQL慢查询日志是最直接的性能诊断工具,但默认未开启。

-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.5;  -- 超过500ms记录
SET GLOBAL log_queries_not_using_indexes = ON;  -- 未用索引的查询也记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

-- 确认生效
SHOW VARIABLES LIKE 'slow_query%';

生产环境不建议长期开启全量慢查询日志,会影响IO。建议在排查窗口期临时开启,用pt-query-digest分析。

# 用Percona Toolkit分析慢查询
pt-query-digest /var/log/mysql/slow.log --limit 95% --outliers F=3,S=1M

# 输出样例:
# Rank Query ID       Response time  Calls  R/Call  V/M
# ==== ============= ============== ====== ======= ====
#    1 0x5A1B2C3D...  1254.5 62.3%   3451   0.364  0.12
#    2 0x7E8F9A0B...   432.1 21.5%    892   0.484  0.05

Rank 1的查询贡献了62.3%的响应时间,这就是优化重点。拿到Query ID后回slow.log找原始SQL。

EXPLAIN执行计划深度解读

拿到慢SQL后,用EXPLAIN分析执行计划。很多人只看type列,这不够,每一列都有含义。

EXPLAIN FORMAT=JSON SELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.created_at BETWEEN '2026-07-01' AND '2026-07-31'
  AND o.status = 'PAID'
ORDER BY o.amount DESC
LIMIT 20;

关键列解读:

type:访问类型,从优到差:system > const > eq_ref > ref > range > index > ALL。出现ALL意味着全表扫描,必须优化。

key:实际使用的索引。为NULL说明没走索引。

rows:预估扫描行数。这个值×filtered百分比才是实际返回行数的参考。

Extra:重点关注以下几种:
– Using filesort:排序未走索引,需要额外排序操作
– Using temporary:使用了临时表,常见于GROUP BY无索引场景
– Using index condition:ICP下推,是好事
– Using where; Using join buffer:关联查询没有索引,依赖缓冲区处理

索引设计策略:避免无效索引

不是加了索引就能加速。以下几种情况索引无效:

1. 对索引列使用函数或表达式

-- 无效:对created_at使用函数
SELECT * FROM orders WHERE DATE(created_at) = '2026-07-23';
-- 有效:范围查询
SELECT * FROM orders WHERE created_at >= '2026-07-23' AND created_at < '2026-07-24';

2. 联合索引的最左前缀原则

-- 索引:idx_user_status_created (user_id, status, created_at)
-- 能走索引
WHERE user_id = 1
WHERE user_id = 1 AND status = 'PAID'
WHERE user_id = 1 AND status = 'PAID' AND created_at > '2026-07-01'

-- 不能走索引(跳过了user_id)
WHERE status = 'PAID'
WHERE status = 'PAID' AND created_at > '2026-07-01'

3. 隐式类型转换导致索引失效

-- 如果user_id是varchar类型
-- 无效:传入整数,MySQL做隐式转换
SELECT * FROM users WHERE user_id = 12345;
-- 有效:传入字符串
SELECT * FROM users WHERE user_id = '12345';

分页查询优化:告别OFFSET

深度分页(OFFSET 100000 LIMIT 20)的问题是MySQL要扫描前100020行然后丢弃前100000行。

-- 慢:传统OFFSET分页
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;

-- 快:游标分页(适合连续翻页场景)
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20;

-- 折中方案:子查询延迟关联
SELECT o.* FROM orders o
INNER JOIN (
    SELECT id FROM orders ORDER BY id LIMIT 100000, 20
) t ON o.id = t.id;

子查询方案只走覆盖索引查id,再用id回表取完整数据,IO量大幅减少。

线上索引变更的安全生产流程

线上加索引会锁表(MySQL 5.6之前)或消耗大量IO(Online DDL期间)。安全做法:

# 1. 使用pt-online-schema-change无锁加索引
pt-online-schema-change   --alter "ADD INDEX idx_status_created (status, created_at)"   --execute   --max-load=Threads_running=100   --critical-load=Threads_running=200   --chunk-size=1000   D=production,t=orders

# 2. 或使用gh-ost(GitHub的方案)
gh-ost   --user=root --password=xxx   --host=127.0.0.1   --database=production   --table=orders   --alter="ADD INDEX idx_status_created (status, created_at)"   --allow-on-master   --chunk-size=1000   --execute

两个工具的核心原理相同:创建影子表 → 增量同步数据 → 同步完成后原子切换。全程不锁原表。建议在低峰期执行,同时设置max-load阈值——如果主库压力超过阈值自动暂停。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql-xing-neng-diao-you-shi-zhan-man-cha-xun-ding-wei-suo/

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

相关推荐

MySQL性能调优实战:慢查询定位、索引策略与执行计划深度解读

慢查询日志配置与自动化分析

性能调优的第一步是找到瓶颈。MySQL慢查询日志是最直接的性能诊断工具,但默认未开启。

-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.5;  -- 超过500ms记录
SET GLOBAL log_queries_not_using_indexes = ON;  -- 未用索引的查询也记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

-- 确认生效
SHOW VARIABLES LIKE 'slow_query%';

生产环境不建议长期开启全量慢查询日志,会影响IO。建议在排查窗口期临时开启,用pt-query-digest分析。

# 用Percona Toolkit分析慢查询
pt-query-digest /var/log/mysql/slow.log --limit 95% --outliers F=3,S=1M

# 输出样例:
# Rank Query ID       Response time  Calls  R/Call  V/M
# ==== ============= ============== ====== ======= ====
#    1 0x5A1B2C3D...  1254.5 62.3%   3451   0.364  0.12
#    2 0x7E8F9A0B...   432.1 21.5%    892   0.484  0.05

Rank 1的查询贡献了62.3%的响应时间,这就是优化重点。拿到Query ID后回slow.log找原始SQL。

EXPLAIN执行计划深度解读

拿到慢SQL后,用EXPLAIN分析执行计划。很多人只看type列,这不够,每一列都有含义。

EXPLAIN FORMAT=JSON SELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.created_at BETWEEN '2026-07-01' AND '2026-07-31'
  AND o.status = 'PAID'
ORDER BY o.amount DESC
LIMIT 20;

关键列解读:

type:访问类型,从优到差:system > const > eq_ref > ref > range > index > ALL。出现ALL意味着全表扫描,必须优化。

key:实际使用的索引。为NULL说明没走索引。

rows:预估扫描行数。这个值×filtered百分比才是实际返回行数的参考。

Extra:重点关注以下几种:
– Using filesort:排序未走索引,需要额外排序操作
– Using temporary:使用了临时表,常见于GROUP BY无索引场景
– Using index condition:ICP下推,是好事
– Using where; Using join buffer:关联查询没有索引,依赖缓冲区处理

索引设计策略:避免无效索引

不是加了索引就能加速。以下几种情况索引无效:

1. 对索引列使用函数或表达式

-- 无效:对created_at使用函数
SELECT * FROM orders WHERE DATE(created_at) = '2026-07-23';
-- 有效:范围查询
SELECT * FROM orders WHERE created_at >= '2026-07-23' AND created_at < '2026-07-24';

2. 联合索引的最左前缀原则

-- 索引:idx_user_status_created (user_id, status, created_at)
-- 能走索引
WHERE user_id = 1
WHERE user_id = 1 AND status = 'PAID'
WHERE user_id = 1 AND status = 'PAID' AND created_at > '2026-07-01'

-- 不能走索引(跳过了user_id)
WHERE status = 'PAID'
WHERE status = 'PAID' AND created_at > '2026-07-01'

3. 隐式类型转换导致索引失效

-- 如果user_id是varchar类型
-- 无效:传入整数,MySQL做隐式转换
SELECT * FROM users WHERE user_id = 12345;
-- 有效:传入字符串
SELECT * FROM users WHERE user_id = '12345';

分页查询优化:告别OFFSET

深度分页(OFFSET 100000 LIMIT 20)的问题是MySQL要扫描前100020行然后丢弃前100000行。

-- 慢:传统OFFSET分页
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;

-- 快:游标分页(适合连续翻页场景)
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20;

-- 折中方案:子查询延迟关联
SELECT o.* FROM orders o
INNER JOIN (
    SELECT id FROM orders ORDER BY id LIMIT 100000, 20
) t ON o.id = t.id;

子查询方案只走覆盖索引查id,再用id回表取完整数据,IO量大幅减少。

线上索引变更的安全生产流程

线上加索引会锁表(MySQL 5.6之前)或消耗大量IO(Online DDL期间)。安全做法:

# 1. 使用pt-online-schema-change无锁加索引
pt-online-schema-change   --alter "ADD INDEX idx_status_created (status, created_at)"   --execute   --max-load=Threads_running=100   --critical-load=Threads_running=200   --chunk-size=1000   D=production,t=orders

# 2. 或使用gh-ost(GitHub的方案)
gh-ost   --user=root --password=xxx   --host=127.0.0.1   --database=production   --table=orders   --alter="ADD INDEX idx_status_created (status, created_at)"   --allow-on-master   --chunk-size=1000   --execute

两个工具的核心原理相同:创建影子表 → 增量同步数据 → 同步完成后原子切换。全程不锁原表。建议在低峰期执行,同时设置max-load阈值——如果主库压力超过阈值自动暂停。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql-xing-neng-diao-you-shi-zhan-man-cha-xun-ding-wei-suo/

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

相关推荐