MySQL 性能调优实战:索引优化与慢查询分析

MySQL 性能调优是每一位后端工程师的必修课。当接口响应时间从 50ms 突然飙到 5s,第一现场往往不是业务代码,而是慢 SQL 与缺失的索引。本文从慢查询日志、EXPLAIN 执行计划入手,系统讲解索引优化、常见慢 SQL 改写与关键参数调优,给出一套可落地的 MySQL 性能调优路径,帮助你把”凭感觉优化”变成”看数据优化”。

一、慢查询日志:定位性能瓶颈的第一现场

任何 MySQL 性能调优都该从”看见慢查询”开始。开启慢查询日志后,所有执行超过阈值的 SQL 都会被记录,是排查线上性能问题最可靠的入口。建议把阈值设为 1 秒(long_query_time=1),并对未走索引的查询也单独记录。

# my.cnf 开启慢查询日志
[mysqld]
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = ON
min_examined_row_limit = 1000   # 扫描行数过少的不记录,避免噪声

重启生效后,用官方工具 mysqldumpslow 对日志做聚合,快速找出”出现次数最多、总耗时最高”的 SQL,而不是被单条偶发慢查询带偏。

# 按总耗时排序,取前 10 条最慢的 SQL
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

# 按执行次数排序,找出高频查询
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log
参数作用建议值
slow_query_log是否开启慢日志ON
long_query_time慢查询阈值(秒)1(高并发可 0.5)
log_queries_not_using_indexes记录全表扫描ON
min_examined_row_limit最小扫描行数过滤1000

二、EXPLAIN:读懂执行计划

拿到慢 SQL 后,在查询前加 EXPLAIN 即可看到 MySQL 优化器选择的访问路径。MySQL 性能调优的核心,就是让 EXPLAIN 输出里的 type 尽量靠前、rows 尽量小、Extra 里没有糟糕的提示。

EXPLAIN SELECT id, user_id, amount
FROM orders
WHERE user_id = 10086 AND status = 'PAID'
ORDER BY created_at DESC
LIMIT 20;
type 等级含义调优方向
const / eq_ref唯一索引命中,最优保持
ref非唯一索引命中可接受
range索引范围扫描可用
index全索引扫描考虑覆盖索引
ALL全表扫描必须优化

重点看三个字段:type 表示访问类型,越靠左越好;key 显示实际用到的索引,为空说明没走索引;Extra 若出现 Using filesort 或 Using temporary,往往意味着排序或分组没用到索引,需要重点优化。关于排序与分组导致的性能问题,可参考 PostgreSQL 慢查询分析与优化 的排查思路,两者内核原理相通。

三、索引优化实战

3.1 最左前缀匹配

联合索引 (a, b, c) 只能按 a、a+b、a+b+c 的顺序命中。如果查询跳过 a 直接查 b,索引就会失效。这是 MySQL 性能调优里最容易踩的坑。

-- 建立联合索引
CREATE INDEX idx_user_status_time ON orders(user_id, status, created_at);

-- 能命中索引:满足最左前缀
SELECT * FROM orders WHERE user_id = 10086 AND status = 'PAID';

-- 无法命中:缺失 user_id 这一最左列
SELECT * FROM orders WHERE status = 'PAID';

3.2 覆盖索引避免回表

如果索引本身就包含了查询所需的全部列,MySQL 无需回表读取数据行,性能可提升数倍。EXPLAIN 的 Extra 列会显示 Using index,这就是覆盖索引生效的标志。

-- 只查索引列,触发覆盖索引
SELECT user_id, status, created_at
FROM orders
WHERE user_id = 10086
ORDER BY created_at DESC;

-- Extra 显示 "Using index",无需回表

3.3 索引下推(ICP)

MySQL 5.6 引入的索引条件下推(Index Condition Pushdown),允许在存储引擎层就过滤掉不满足条件的记录,减少回表次数。EXPLAIN 的 Extra 会显示 Using index condition。这是默认开启的优化,但只有在合理设计索引时才能发挥作用。

3.4 避免索引失效的写法

-- 错误:对列做函数运算,索引失效
SELECT * FROM orders WHERE DATE(created_at) = '2026-10-04';

-- 正确:改成范围查询,索引可命中
SELECT * FROM orders
WHERE created_at >= '2026-10-04' AND created_at < '2026-10-05';

-- 错误:隐式类型转换(user_id 是 int,却传字符串)
SELECT * FROM orders WHERE user_id = '10086';

-- 错误:前导模糊查询,索引失效
SELECT * FROM orders WHERE user_name LIKE '%明%';

四、常见慢 SQL 场景与改写

很多慢查询并非缺索引,而是写法本身低效。下面两个场景在业务代码里极为常见。

-- 深分页:LIMIT 100000, 20 要扫描前 10 万行再丢弃
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;

-- 优化:用游标(上一页最大 id)代替偏移量
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20;
-- 关联查询:用小表驱动大表,并在关联字段建索引
SELECT o.id, u.name
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.city = 'Hangzhou';
-- 确保 orders.user_id 有索引,否则退化为大表全扫描
反面写法优化写法收益
对列套函数改写为范围比较恢复索引
LIMIT 大偏移游标分页避免扫描丢弃
SELECT *只取所需列覆盖索引
前导 % 模糊后缀/全文索引走索引

五、关键参数调优

索引之外,MySQL 性能调优还依赖内存与 I/O 参数。最核心的是 InnoDB 缓冲池 innodb_buffer_pool_size,它缓存索引和数据页,通常设为物理内存的 60%–70%(专用数据库服务器)。缓冲池越大,磁盘 I/O 越少。

# my.cnf 生产环境参考(以 16G 内存专用库为例)
[mysqld]
innodb_buffer_pool_size = 10G
innodb_buffer_pool_instances = 8
innodb_log_file_size = 1G
innodb_flush_log_at_trx_commit = 2   # 性能优先,可接受秒级丢失
max_connections = 500
sort_buffer_size = 4M
join_buffer_size = 4M
参数作用注意
innodb_buffer_pool_size缓存数据与索引专用机 60–70% 内存
innodb_log_file_sizeredo 日志大小越大写吞吐越高
innodb_flush_log_at_trx_commit刷盘策略2 兼顾性能与安全
max_connections最大连接数配合连接池使用

调优参数时务必配合连接池,避免短连接风暴拖垮数据库。关于主从架构下读写分离的连接管理,可延伸阅读 MySQL 主从复制与读写分离实战。如果业务对缓存层有需求,Redis 缓存设计 能进一步降低数据库压力。

六、生产落地检查清单

检查项达标标准
慢查询日志已开启,阈值 ≤ 1s
核心查询EXPLAIN type 至少 ref
高频查询尽量覆盖索引
避免失效写法无函数/隐式转换/前导模糊
缓冲池≥ 60% 物理内存
深分页已改用游标
变更流程加索引走 Online DDL / PT-OSC

最后提醒:生产环境加索引会锁表,大表务必用 ALGORITHM=INPLACE 的 Online DDL 或 pt-online-schema-change 工具,避免写入被长时间阻塞。完整的数据库变更最好纳入 GitHub Actions CI 流水线 做评审与灰度。若需要跨数据库对比方案,PostgreSQL 与 SQL Server 选型 也值得一读。

七、小结

MySQL 性能调优不是玄学:先用慢查询日志定位问题 SQL,再用 EXPLAIN 看清执行计划,然后围绕最左前缀、覆盖索引、避免失效写法做索引优化,配合参数与连接池调优,最后用检查清单固化成果。把这套方法变成团队习惯,绝大多数"数据库变慢"的故障都能在分钟级定位。把它和容器化部署(如 Docker 实战)结合,性能与稳定性会更上一层楼。

上一篇 esbuild 实战:极速 JS/TS 打包器与工程化落地
下一篇 Flowise 实战:低代码搭建 LLM 应用工作流
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]