MySQL 性能问题在生产环境中的表现通常是渐进式的:业务量增长、数据量膨胀,某天突然发现 P99 响应时间从 50ms 涨到 2s。慢查询是最常见的根因,而索引设计不合理又是慢查询的主要来源。 MySQL 8.4 LTS 在查询优化器、直方图统计、索引跳跃扫描等方面有明显改进,但核心的分析方法论没有变化:先定位慢查询,再用 EXPLAIN 分析执行计划,最后针对性地调整索引或 SQL。 动态修改(无需重启): pt-query-digest 输出解读: 关注指标: 输出示例: type 字段(从好到差排序): Extra 字段关键信息: 覆盖索引的代价是索引体积增大,写入时维护成本上升。对于写多读少的表,不要滥用。 电商订单表,数据量 5000 万行,某天下午业务反馈订单列表页响应时间从 200ms 涨到 8s。 输出发现最慢的 SQL: 结果: ⚠️ 警告:以下操作在生产环境中可能造成严重性能问题。MySQL性能优化实战:慢查询分析与索引调优全流程
一、概述
1.1 背景介绍
1.2 技术特点
1.3 适用场景
1.4 环境要求
二、详细步骤
2.1 慢查询日志配置
2.1.1 开启慢查询日志
# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1 # 超过 1 秒记录,生产初期可设 0.5
log_queries_not_using_indexes = OFF # 不建议开,会产生大量噪音
log_slow_extra = ON # 8.0.14+ 支持,记录更多上下文信息
min_examined_row_limit = 100 # 扫描行数少于 100 的不记录,过滤简单查询SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
SET GLOBAL log_slow_extra = ON;2.1.2 pt-query-digest 分析
# 安装 Percona Toolkit
apt install percona-toolkit # Ubuntu
# 或
yum install percona-toolkit # CentOS
# 基础分析:按总执行时间排序,输出 Top 10 慢查询
pt-query-digest /var/log/mysql/slow.log \
--limit 10 \
--order-by Query_time:sum \
> /tmp/slow_report.txt
# 只分析最近 1 小时的慢查询
pt-query-digest /var/log/mysql/slow.log \
--since "1h" \
--limit 20
# 过滤特定数据库
pt-query-digest /var/log/mysql/slow.log \
--filter '$event->{db} eq "production_db"'
# 输出到 MySQL 表,便于历史对比
pt-query-digest /var/log/mysql/slow.log \
--review h=127.0.0.1,D=percona,t=query_review \
--history h=127.0.0.1,D=percona,t=query_history \
--no-report# Query 1: 0.50 QPS, 2.50x concurrency, ID 0xABC123 at byte 12345
# This item is included in the report because it matches --limit.
# Scores: V/M = 1.23
# Time range: 2024-01-15 10:00:00 to 2024-01-15 11:00:00
# Attribute pct total min max avg 95% stddev median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count 15 1800
# Exec time 42 1800s 0.5s 5s 1s 2.1s 0.8s 0.9s
# Lock time 2 90s 0s 0.1s 0.05s 0.08s 0.02s 0.04s
# Rows sent 8 14400 1 20 8 15 4 8
# Rows examine 65 585000 100 1000 325 800 200 300Rows examine / Rows sent 比值,超过 100 说明索引效率低。2.2 EXPLAIN 执行计划解读
2.2.1 核心字段含义
EXPLAIN SELECT o.id, o.amount, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
AND o.created_at > '2024-01-01'
ORDER BY o.created_at DESC
LIMIT 20\Gid: 1
select_type: SIMPLE
table: o
partitions: NULL
type: range ← 关键字段
possible_keys: idx_status_created,idx_created_at
key: idx_status_created ← 实际使用的索引
key_len: 10
ref: NULL
rows: 1250 ← 预估扫描行数
filtered: 100.00
Extra: Using index condition; Using filesort ← 注意 filesortUsing index:覆盖索引,无需回表,性能最优Using index condition:索引下推(ICP),在索引层过滤,减少回表次数Using filesort:需要额外排序,如果数据量大会很慢Using temporary:使用临时表,GROUP BY 或 ORDER BY 时出现,需要重点关注Using where:在 Server 层过滤,索引没有完全覆盖 WHERE 条件2.2.2 EXPLAIN ANALYZE(8.0.18+)
-- EXPLAIN ANALYZE 实际执行查询并返回真实耗时
EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 12345 AND status = 'paid'\G
-- 输出包含实际执行时间和行数
-- -> Filter: (orders.status = 'paid') (cost=5.25 rows=3) (actual time=0.045..0.089 rows=2 loops=1)
-- -> Index lookup on orders using idx_user_id (user_id=12345)
-- (cost=3.50 rows=15) (actual time=0.038..0.075 rows=15 loops=1)actual rows 与 rows(预估)差距大时,说明统计信息过期,需要 ANALYZE TABLE。2.3 索引设计原则
2.3.1 联合索引最左前缀
-- 假设有联合索引 idx_user_status_created (user_id, status, created_at)
-- 能用索引(最左前缀匹配)
SELECT * FROM orders WHERE user_id = 1;
SELECT * FROM orders WHERE user_id = 1ANDstatus = 'paid';
SELECT * FROM orders WHERE user_id = 1ANDstatus = 'paid'AND created_at > '2024-01-01';
-- 不能用索引(跳过了 user_id)
SELECT * FROM orders WHEREstatus = 'paid';
SELECT * FROM orders WHEREstatus = 'paid'AND created_at > '2024-01-01';
-- 范围查询后的列无法用索引过滤
-- 以下查询中 status 列无法通过索引过滤
SELECT * FROM orders WHERE user_id = 1AND created_at > '2024-01-01'ANDstatus = 'paid';
-- 建议改为:WHERE user_id = 1 AND status = 'paid' AND created_at > '2024-01-01'
-- 把等值条件放前面,范围条件放最后2.3.2 覆盖索引
-- 原始查询,需要回表
SELECT id, amount, created_at FROM orders WHERE user_id = 1 AND status = 'paid';
-- 创建覆盖索引,包含查询所需的所有列
ALTER TABLE orders ADD INDEX idx_covering (user_id, status, amount, created_at);
-- EXPLAIN 中 Extra 显示 "Using index",无需回表
-- 对于高频查询,覆盖索引能将响应时间从毫秒级降到微秒级2.3.3 索引下推(ICP)
-- 联合索引 idx_age_name (age, name)
-- 查询:WHERE age > 20 AND name LIKE 'Zhang%'
-- 没有 ICP 时:
-- 1. 存储引擎用 age > 20 找到所有记录
-- 2. 回表取完整行
-- 3. Server 层用 name LIKE 'Zhang%' 过滤
-- 有 ICP 时(MySQL 5.6+ 默认开启):
-- 1. 存储引擎用 age > 20 找到索引记录
-- 2. 在索引层直接检查 name LIKE 'Zhang%'
-- 3. 只有满足条件的记录才回表
-- EXPLAIN Extra 显示 "Using index condition"
-- 验证 ICP 是否生效
SET optimizer_switch = 'index_condition_pushdown=on'; -- 默认开启2.4 InnoDB Buffer Pool 调优
# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
# Buffer Pool 大小:物理内存的 50-75%
# 16GB 内存的服务器,专用 MySQL 实例设 10-12GB
innodb_buffer_pool_size = 10G
# Buffer Pool 实例数:每个实例独立锁,减少竞争
# 建议每个实例 1-2GB,10GB 设 8 个实例
innodb_buffer_pool_instances = 8
# 预热:重启后自动加载上次的热数据
innodb_buffer_pool_dump_at_shutdown = ON
innodb_buffer_pool_load_at_startup = ON
innodb_buffer_pool_dump_pct = 25 # 只 dump 最热的 25%
# 监控 Buffer Pool 命中率
# 命中率 = 1 - (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests)-- 查看 Buffer Pool 命中率
SELECT
FORMAT(
(1 - (
variable_value / (
SELECT variable_value
FROM performance_schema.global_status
WHERE variable_name = 'Innodb_buffer_pool_read_requests'
)
)) * 100, 2
) AS hit_rate_pct
FROM performance_schema.global_status
WHERE variable_name = 'Innodb_buffer_pool_reads';
-- 命中率低于 95% 时需要考虑增大 Buffer Pool
-- 查看 Buffer Pool 使用详情
SELECT pool_id, pool_size, free_buffers, database_pages, hit_rate
FROM information_schema.INNODB_BUFFER_POOL_STATS;2.5 连接池配置
[mysqld]
max_connections = 500 # 根据业务并发量设置,不要无限调大
thread_cache_size = 50 # 缓存线程数,减少线程创建开销
wait_timeout = 600 # 空闲连接超时(秒)
interactive_timeout = 600
max_connect_errors = 100 # 连接错误次数上限,超过则封锁 IP
# 连接队列
back_log = 128 # TCP 连接队列长度,高并发时适当增大-- 监控连接状态
SHOWSTATUSLIKE'Threads_%';
-- Threads_connected: 当前连接数
-- Threads_running: 活跃线程数(真正在执行 SQL)
-- Threads_cached: 缓存中的线程数
-- 如果 Threads_running 持续接近 max_connections,说明有连接积压
-- 查看当前连接详情
SELECTuser, host, db, command, time, state, info
FROM information_schema.PROCESSLIST
WHERE command != 'Sleep'
ORDERBYtimeDESC
LIMIT20;三、示例代码和配置
3.1 生产案例:从慢查询到索引优化完整流程
案例背景
3.1.1 定位慢查询
# 分析最近 30 分钟的慢查询
pt-query-digest /var/log/mysql/slow.log \
--since "30m" \
--order-by Query_time:sum \
--limit 5SELECT o.id, o.order_no, o.amount, o.status, o.created_at,
u.name, u.phone
FROM orders o
JOINusers u ON o.user_id = u.id
WHERE o.merchant_id = 1001
AND o.status IN ('pending', 'paid')
AND o.created_at BETWEEN'2024-01-01'AND'2024-01-31'
ORDERBY o.created_at DESC
LIMIT20OFFSET0;
-- 平均执行时间 6.8s,扫描行数 320 万3.1.2 分析执行计划
EXPLAIN SELECT o.id, o.order_no, o.amount, o.status, o.created_at,
u.name, u.phone
FROM orders o
JOINusers u ON o.user_id = u.id
WHERE o.merchant_id = 1001
AND o.status IN ('pending', 'paid')
AND o.created_at BETWEEN'2024-01-01'AND'2024-01-31'
ORDERBY o.created_at DESC
LIMIT20\Gtype: ALL,rows: 50000000,Extra: Using where; Using filesort。全表扫描 + 文件排序,问题明确。3.1.3 索引设计决策
-- 分析列的选择性
SELECT
COUNT(DISTINCT merchant_id) / COUNT(*) AS merchant_selectivity,
COUNT(DISTINCTstatus) / COUNT(*) AS status_selectivity,
COUNT(DISTINCTDATE(created_at)) / COUNT(*) AS date_selectivity
FROM orders;
-- merchant_selectivity: 0.0002(低,1万个商户/5000万行)
-- status_selectivity: 0.00000012(极低,只有几个状态值)
-- date_selectivity: 0.0006(低)
-- 设计联合索引:等值条件在前,范围条件在后
-- merchant_id(等值)+ status(IN,等值)+ created_at(范围+排序)
ALTERTABLE orders
ADDINDEX idx_merchant_status_created (merchant_id, status, created_at);
-- 如果需要覆盖索引(避免回表),加上 SELECT 的列
-- 但 name、phone 在 users 表,JOIN 无法避免
-- 只能覆盖 orders 表的列
ALTERTABLE orders
ADDINDEX idx_merchant_status_created_cover
(merchant_id, status, created_at, id, order_no, amount, user_id);3.1.4 验证优化效果
-- 强制使用新索引验证
EXPLAINSELECT o.id, o.order_no, o.amount, o.status, o.created_at,
u.name, u.phone
FROM orders o FORCEINDEX (idx_merchant_status_created)
JOINusers u ON o.user_id = u.id
WHERE o.merchant_id = 1001
AND o.status IN ('pending', 'paid')
AND o.created_at BETWEEN'2024-01-01'AND'2024-01-31'
ORDERBY o.created_at DESC
LIMIT20\G
-- type: range, rows: 1250, Extra: Using index condition
-- 扫描行数从 5000 万降到 1250,响应时间降到 15ms3.2 直方图统计(MySQL 8.0+)
-- 对低选择性列创建直方图,帮助优化器做更准确的行数估算
ANALYZE TABLE orders UPDATE HISTOGRAM ON status, merchant_id WITH 256 BUCKETS;
-- 查看直方图信息
SELECT * FROM information_schema.COLUMN_STATISTICS
WHERE table_name = 'orders'\G
-- 直方图适合:不适合建索引但需要准确统计的列
-- 不适合:高选择性列(直接建索引更好)、频繁更新的列(直方图不自动更新)四、最佳实践和注意事项
4.1 最佳实践
4.1.1 索引设计原则
-- 查找从未使用的索引(需要运行足够长时间后查询)
SELECT object_schema, object_name, index_name
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL
AND count_star = 0
AND object_schema NOT IN ('mysql', 'performance_schema', 'information_schema')
ORDER BY object_schema, object_name;4.1.2 SQL 编写规范
-- 避免在索引列上使用函数
-- 错误:无法使用 created_at 上的索引
SELECT * FROM orders WHEREYEAR(created_at) = 2024;
-- 正确:
SELECT * FROM orders WHERE created_at >= '2024-01-01'AND created_at < '2025-01-01';
-- 避免隐式类型转换
-- 错误:phone 是 VARCHAR,传入整数会导致全表扫描
SELECT * FROMusersWHERE phone = 13800138000;
-- 正确:
SELECT * FROMusersWHERE phone = '13800138000';
-- 大分页问题:OFFSET 越大越慢
-- 错误:OFFSET 100000 需要扫描 100020 行
SELECT * FROM orders ORDERBYidLIMIT20OFFSET100000;
-- 正确:游标分页
SELECT * FROM orders WHEREid > 100000ORDERBYidLIMIT20;4.1.3 定期维护
-- 更新统计信息(数据变化超过 10% 后执行)
ANALYZETABLE orders;
-- 重建索引(索引碎片率高时)
-- 查看碎片率
SELECT table_name,
ROUND(data_free / (data_length + index_length) * 100, 2) AS frag_pct
FROM information_schema.TABLES
WHERE table_schema = 'production_db'
AND data_free > 0
ORDERBY frag_pct DESC;
-- 在线重建(8.0+ 支持,不锁表)
ALTERTABLE orders ENGINE=InnoDB, ALGORITHM=INPLACE, LOCK=NONE;4.2 注意事项
4.2.1 常见误区
SELECT * 会导致覆盖索引失效,始终明确列出需要的字段ALTER TABLE ADD INDEX 会锁表,使用 pt-online-schema-change 或 gh-ostFORCE INDEX 只用于临时调试,不要提交到生产代码4.2.2 常见错误
五、故障排查和监控
5.1 实时性能诊断
-- 查看当前正在执行的慢 SQL(超过 5 秒)
SELECTid, user, host, db, time, state, LEFT(info, 200) AS sql_snippet
FROM information_schema.PROCESSLIST
WHERE command = 'Query'
ANDtime > 5
ORDERBYtimeDESC;
-- 查看锁等待情况
SELECT
r.trx_id AS waiting_trx_id,
r.trx_mysql_thread_id AS waiting_thread,
r.trx_query AS waiting_query,
b.trx_id AS blocking_trx_id,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_query AS blocking_query
FROM information_schema.INNODB_TRX r
JOIN information_schema.INNODB_TRX b
ON r.trx_wait_started ISNOTNULL
AND b.trx_id = (
SELECT blocking_trx_id
FROM performance_schema.data_lock_waits
WHERE requesting_engine_transaction_id = r.trx_id
LIMIT1
);5.2 性能监控指标
5.2.1 关键指标
# 实时监控 MySQL 状态
mysqladmin -u root -p extended-status -i 1 | grep -E "Questions|Slow|Threads_running|InnoDB_buffer"5.2.2 监控指标说明
5.3 备份与恢复
5.3.1 慢查询日志轮转
# /etc/logrotate.d/mysql-slow
/var/log/mysql/slow.log {
daily
rotate 7
compress
missingok
notifempty
postrotate
# 通知 MySQL 重新打开日志文件
mysql -u root -p"${MYSQL_ROOT_PASS}" -e "FLUSH SLOW LOGS;"
endscript
}六、总结
6.1 技术要点回顾
Rows examine / Rows sent > 100 是优化信号6.2 进阶学习方向
6.3 参考资料
附录
A. 命令速查表
# 开启慢查询日志
mysql -e "SET GLOBAL slow_query_log=ON; SET GLOBAL long_query_time=1;"
# 分析慢查询
pt-query-digest /var/log/mysql/slow.log --limit 10
# 查看表索引
SHOW INDEX FROM orders\G
# 更新统计信息
ANALYZE TABLE orders;
# 查看 Buffer Pool 命中率
mysql -e "SHOW STATUS LIKE 'Innodb_buffer_pool_read%';"
# 在线加索引(大表使用)
pt-online-schema-change --alter "ADD INDEX idx_name (col)" D=db,t=table --executeB. 配置参数详解
C. 术语表
THE END
喜欢就支持以下吧
天天下载Ttzip
2026-01-24
非常全,感谢分享
匿名
2025-12-13
大大可以找下哈狗的1030吗,太想要那首歌了~谢谢!
匿名
2025-11-09
https://collaigo.com 免费在线拼图工具
匿名
2025-10-22
盖楼盖楼!
匿名
2025-08-11
沙发沙发
匿名
2025-08-10
https://at.oiik.cn/bing.html