21_MySQL 优化案例集 —— 慢 SQL 优化、索引重构与参数调整收益对比

21_MySQL 优化案例集 ------ 慢 SQL 优化、索引重构与参数调整收益对比

系列定位 :《MySQL 数据库全栈运维实战系列》第 21 篇,与《Oracle 数据库全栈运维实战系列(VMware 版)》同构对标。适用版本 :MySQL 8.0(5.7 差异随文标注)。官方文档:MySQL 8.0 Reference Manual、Optimization / The InnoDB Buffer Pool / Indexes 章节。


一、优化方法论(总图)

MySQL 没有 Oracle 的 AWR,但 performance_schema.events_statements_summary_by_digest + sys schema + 慢日志三件套完全够用来定位 Top SQL。优化的铁律是"没有量化对比就不算优化"------每一次变更都要留下前后基线。

图 1

维度 MySQL 定位手段 类比 Oracle
Top SQL performance_schema.events_statements_summary_by_digest AWR Top SQL
执行计划 EXPLAIN / EXPLAIN ANALYZE(8.0.18+) DBMS_XPLAN
索引使用 sys.schema_unused_indexes / performance_schema.table_io_waits_summary_by_index_usage v$object_usage
等待事件 performance_schema.events_waits_summary_global_by_event_name v$system_event

VMware 虚机环境注意事项 :优化压测必须在与生产同规格的虚机 上做(同 vCPU、同内存、同磁盘置盘方式),否则收益数字不可信。虚机做基准测试前先固定 vmx 里的 cpuid.coresPerSocket、关闭宿主机上其他大负载 VM、确认磁盘是 SSD/thick,避免 noisy neighbor 把测试数据污染。每次压测前清 Buffer Pool:SET GLOBAL innodb_buffer_pool_dump_now=OFF; 冷/热数据要分开标注。


二、案例 1:全表扫描慢 SQL(订单列表查询)

图 2

2.1 问题定位

业务反馈:后台订单列表页打开要 8 秒。

复制代码
-- 慢日志抓到的 SQL(解释:典型的多条件查询没走索引)
SELECT * FROM t_order
WHERE status = 2
  AND create_time >= '2026-09-01'
  AND create_time <  '2026-10-01'
ORDER BY create_time DESC
LIMIT 20;

-- EXPLAIN 看执行计划(解释:type=ALL 就是全表扫描,rows 接近全表)
EXPLAIN SELECT * FROM t_order
WHERE status = 2
  AND create_time >= '2026-09-01'
  AND create_time <  '2026-10-01'
ORDER BY create_time DESC
LIMIT 20\G
-- 输出关键行:
-- type: ALL
-- key: NULL
-- rows: 8300000
-- Extra: Using where; Using filesort

-- 8.0.18+ 直接看真实执行成本(解释:EXPLAIN ANALYZE 实际跑一遍,给真实行数和耗时)
EXPLAIN ANALYZE
SELECT * FROM t_order WHERE status = 2 AND create_time >= '2026-09-01' LIMIT 20\G

2.2 优化与收益

复制代码
-- 优化:建复合索引(解释:等值列在前,范围列在后,顺序匹配 WHERE + ORDER BY)
CREATE INDEX idx_order_status_ctime ON t_order(status, create_time);

-- 验证(解释:type 变成 ref/range,rows 从 830 万降到几千,filesort 消失)
EXPLAIN SELECT * FROM t_order
WHERE status = 2 AND create_time >= '2026-09-01' AND create_time < '2026-10-01'
ORDER BY create_time DESC LIMIT 20\G
-- type: range
-- key: idx_order_status_ctime
-- rows: 4500
-- Extra: Using index condition
指标 优化前 优化后
SQL 耗时 8.2s 0.03s
扫描行数 830 万 4500
filesort 有 无
高峰期 DB CPU 95% 40%

最左前缀原则 :索引 (status, create_time) 能支撑 WHERE status=?、WHERE status=? AND create_time>=?,但不能 支撑 WHERE create_time>=?(跳过了 status 这个最左列)。这是 MySQL B+Tree 索引与 Oracle 索引共用的核心规则。


三、案例 2:索引失效(函数包裹列 / 隐式类型转换)

3.1 问题定位

复制代码
-- 现象:这 SQL 明明有 idx_user_id,却还是全表扫描
SELECT * FROM t_order WHERE DATE(create_time) = '2026-10-09';

EXPLAIN SELECT * FROM t_order WHERE DATE(create_time) = '2026-10-09'\G
-- type: ALL,key: NULL

第二个坑:

复制代码
-- phone 字段是 VARCHAR,SQL 里却传数字(解释:隐式类型转换导致索引失效)
SELECT * FROM t_user WHERE phone = 13800138000;     -- phone 是 varchar
EXPLAIN SELECT * FROM t_user WHERE phone = 13800138000\G
-- type: ALL(对每行做 CAST(phone AS SIGNED),索引用不上)

3.2 优化

复制代码
-- 优化 1:不要在索引列上包函数,改成范围查询
SELECT * FROM t_order
WHERE create_time >= '2026-10-09 00:00:00'
  AND create_time <  '2026-10-10 00:00:00';

-- 优化 2:类型对齐,SQL 传字符串
SELECT * FROM t_user WHERE phone = '13800138000';
指标 优化前 优化后
订单按天查询耗时 6s 0.02s
用户按手机号查询 2s(全表 500 万) 0.001s

四、案例 3:深分页优化(LIMIT 1000000, 20)

图 3

4.1 问题定位

后台报表翻到第 5 万页,SQL 跑 3 秒。

复制代码
-- 慢 SQL(解释:MySQL 要先扫描并丢弃前 100 万行,再取 20 行)
SELECT * FROM t_order
ORDER BY id DESC
LIMIT 1000000, 20;

4.2 优化

复制代码
-- 优化 1:游标/keyset 翻页(解释:记住上一页最后一个 id,避免 OFFSET)
SELECT * FROM t_order
WHERE id < 987654321       -- 上一页最后一条 id
ORDER BY id DESC
LIMIT 20;

-- 优化 2:延迟关联(解释:先用覆盖索引查出 20 个主键,再回表)
SELECT o.* FROM t_order o
INNER JOIN (
    SELECT id FROM t_order ORDER BY id DESC LIMIT 1000000, 20
) t ON o.id = t.id;
指标 优化前 优化后
第 5 万页耗时 3.2s 0.05s
回表次数 100 万次 20 次

五、案例 4:索引重构(碎片 + 冗余 + 未使用索引)

5.1 问题定位

DML 写入变慢,一张 2000 万行的表,INSERT ... SELECT 导入比上周慢了 40%。

复制代码
-- 找未使用的索引(解释:8.0 用 sys schema,跑一周后看)
SELECT * FROM sys.schema_unused_indexes
WHERE object_schema = 'app'\G

-- 看索引体积与冗余(解释:重复索引浪费写性能)
SELECT * FROM sys.schema_redundant_indexes\G

-- 看 InnoDB 表碎片(解释:information_schema.TABLES 的 DATA_FREE)
SELECT table_name,
       ROUND(data_length/1024/1024,1) AS data_mb,
       ROUND(index_length/1024/1024,1) AS index_mb,
       ROUND(data_free/1024/1024,1) AS free_mb
FROM information_schema.tables
WHERE table_schema='app' AND table_name='t_order';

5.2 优化

复制代码
-- 1. 删掉确认无业务使用的冗余/未用索引(解释:每个索引都拖慢写)
DROP INDEX idx_order_status_old ON app.t_order;   -- 已被 idx_order_status_ctime 覆盖

-- 2. 碎片整理(解释:8.0 InnoDB 在线 DDL,不锁表;5.7 大表建议 pt-online-schema-change)
ALTER TABLE app.t_order ENGINE=InnoDB;
-- 或 OPTIMIZE TABLE app.t_order;   -- 本质也是重建,会锁表片刻,建议低峰
指标 优化前 优化后
t_order 索引数量 11 个(含 3 个冗余) 8 个
表碎片率 35% < 5%
批量导入速度 100%(基准) -40% 耗时
写放大(redo 量) 100% -25%

类比 Oracle :Oracle 重建索引用 ALTER INDEX ... REBUILD ONLINE,MySQL InnoDB 没有原生"重建索引"命令,用 ALTER TABLE ... ENGINE=InnoDB 或 OPTIMIZE TABLE 整表重建;大表线上操作推荐 Percona Toolkit 的 pt-online-schema-change,避免长时间锁表。


六、案例 5:JOIN 优化(被驱动表没走索引)

6.1 问题定位

复制代码
-- 慢 SQL(解释:订单 JOIN 用户表,用户表没走索引)
SELECT o.order_id, u.user_name, o.amount
FROM t_order o
JOIN t_user u ON o.user_id = u.id
WHERE o.status = 2;

EXPLAIN ... \G
-- o: type=ref(用了 idx_status),rows=50 万
-- u: type=ALL(每行订单都全表扫一次用户表),rows=500 万
-- 总扫描 = 50 万 × 500 万 = 灾难

6.2 优化

复制代码
-- 确保被驱动表 t_user.id 是主键(本来就该是),
-- 且小表驱动大表;MySQL 8.0 优化器一般自动处理,但要确认:
EXPLAIN FORMAT=JSON ...\G
-- 看 "query_cost" 和 nested_loop_loop 结构

-- 另一个常见场景:JOIN 字段类型不一致导致索引失效
-- 检查两表 JOIN 列类型是否完全一致(字符集、排序规则、int/bigint)
SHOW FULL COLUMNS FROM t_order LIKE 'user_id';
SHOW FULL COLUMNS FROM t_user LIKE 'id';
指标 优化前 优化后
JOIN 查询耗时 25s 0.2s
嵌套循环次数 50 万 × 500 万 50 万 × 主键命中

七、案例 6:参数调整收益对比(Buffer Pool / redo / 临时表)

7.1 背景

一台 32G 内存的 VMware 虚机跑 MySQL 8.0,业务上线半年,参数还是初始化默认值。用 sysbench(详见番外附-01)跑基线后做了三组调整,量化对比如下。

7.2 参数调整项

复制代码
# my.cnf 调整前后对比(32G 虚机)
[mysqld]
-- 调整前(默认/保守)
innodb_buffer_pool_size = 134217728     -- 128M!
innodb_log_file_size     = 50331648      -- 48M(5.7 默认)
innodb_flush_log_at_trx_commit = 1
sync_binlog              = 1
tmp_table_size           = 16777216      -- 16M
max_heap_table_size      = 16777216
innodb_io_capacity       = 200
innodb_io_capacity_max   = 400

-- 调整后(32G 虚机合理基线)
innodb_buffer_pool_size = 21474836480   -- 20G(物理内存 60~70%)
innodb_redo_log_capacity = 2147483648  -- 2G(8.0.30+ 统一参数,取代 innodb_log_file_size)
innodb_flush_log_at_trx_commit = 1     -- 不建议改,双 1 保数据安全
sync_binlog = 1                         -- 不建议改
tmp_table_size = 134217728             -- 128M
max_heap_table_size = 134217728
innodb_io_capacity = 2000               -- SSD/SSD 直通盘可调高
innodb_io_capacity_max = 4000
innodb_flush_method = O_DIRECT

-- 在线验证(解释:8.0 大部分参数可动态改,不用重启;buffer pool 也支持在线 resize)
SET GLOBAL innodb_buffer_pool_size = 21474836480;
SHOW STATUS LIKE 'Innodb_buffer_pool_resize_status';

7.3 收益对比(sysbench oltp_read_write,10 表 × 100 万行)

参数组 TPS QPS 平均延迟 备注
默认基线(BP=128M) 210 4200 480ms 数据全落盘,IO 打满
BP=20G 1850 37000 55ms 热数据进内存,IO 降 80%
redo=2G + IO_CAPACITY=2000 2350 47000 42ms 刷盘更顺,checkpoint 抖动消失
tmp_table_size=128M 2400 48000 40ms 排序不再落临时磁盘表
最终组合 2400 48000 40ms 综合收益约 11 倍

双 1 原则不动 :innodb_flush_log_at_trx_commit=1 + sync_binlog=1 是 MySQL 不丢数据的底线(类比 Oracle LGWR + ARCH 同步写)。很多文章教你改成 2/0 换性能------那是拿数据安全换 TPS,金融/交易业务禁止。


八、案例 7:慢日志捕获与常态化治理

图 4

复制代码
-- 开启慢日志(解释:线上长期开,long_query_time 别设 0,会写爆)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;          -- 超过 1s 记下来
SET GLOBAL log_queries_not_using_indexes = OFF;  -- 高峰别开,会刷大量全表扫描
SET GLOBAL slow_query_log_file = '/data/mysql-slow/slow.log';

-- 用 performance_schema 看 Top SQL(解释:比解析慢日志更实时)
SELECT DIGEST_TEXT,
       COUNT_STAR AS exec_count,
       ROUND(SUM_TIMER_WAIT/1e12,2) AS total_sec,
       ROUND(AVG_TIMER_WAIT/1e9,2)  AS avg_ms,
       SUM_ROWS_EXAMINED,
       SUM_ROWS_SEND
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10\G
治理动作 频率 产出
慢日志 Top 10 SQL 评审 每周 优化清单
schema_unused_indexes 巡检 每月 删冗余索引
关键 SQL 执行计划基线 上线前 防计划漂移

九、优化收益闭环(验证纪律)

图 5

图 6

纪律 说明
同口径对比 同一 SQL、同一时间窗、同一数据量
多轮取样 跑 3 次取中位数,排除缓存/抖动
留基线 变更前后都存 EXPLAIN 和耗时截图
测试库先行 大表加索引在从库/预发先验证耗时

十、踩坑记录(实测汇总)

# 坑 现象 解决
1 加了索引 SQL 没变快 优化器没选新索引 ANALYZE TABLE 或删旧索引,确认统计信息
2 索引建了反而写更慢 索引过多拖慢 DML 用 schema_unused_indexes 每月清冗余
3 WHERE 列包了函数 索引失效全表扫 改范围查询,别在索引列上做函数
4 字符串列传数字 隐式转换索引失效 应用侧类型严格对齐
5 深分页用大 OFFSET 越翻越慢 keyset 翻页 / 延迟关联
6 大表 OPTIMIZE TABLE 锁表 业务中断 低峰做或用 pt-online-schema-change
7 盲目改双 1 换 TPS 宕机丢数据 双 1 不动,从架构上扩展
8 Buffer Pool 开太大 OS OOM 60~70% 物理内存,留够连接和 OS
9 压测在 noisy neighbor 虚机上做 收益数字忽高忽低 同规格虚机、固定资源、清缓存
10 EXPLAIN 看 type=ref 就放心 没看 rows 估算偏差 用 EXPLAIN ANALYZE 看真实行数

十一、类比 Oracle 速查

MySQL 优化动作 类比 Oracle 说明
EXPLAIN / EXPLAIN ANALYZE DBMS_XPLAN.DISPLAY_CURSOR 8.0.18+ ANALYZE 等价真实执行
events_statements_summary_by_digest AWR Top SQL 按 digest 聚合,避免硬解析干扰
CREATE INDEX CREATE INDEX 都要注意 ONLINE 与锁
ALTER TABLE ENGINE=InnoDB MOVE TABLE / REBUILD MySQL 整表重建碎片
innodb_buffer_pool_size db_cache_size / SGA 都是数据缓存核心
双 1(flush_log_at_trx_commit+sync_binlog) LGWR+ARCH 同步写 不可为性能牺牲持久性

十二、本文要点速记

  1. 定位靠 performance_schema digest:比解析慢日志更实时,按总耗时排序找 Top SQL。
  2. EXPLAIN 看三样 :type(别 ALL)、key(真用上索引)、rows(估算行数)。
  3. 复合索引最左前缀:等值在前范围在后,排序列尽量走索引避免 filesort。
  4. 索引要定期瘦身:未用索引和冗余索引每个月清一次,写性能白捡。
  5. 参数调整要量化:Buffer Pool 是第一收益点,但双 1 底线不动。
  6. 优化必须前后对比:没有基线数字的优化都是玄学。

十三、参考资料(官方正式文档)

主题 官方链接
MySQL 8.0 Optimization 总览 https://dev.mysql.com/doc/refman/8.0/en/optimization.html
EXPLAIN 输出格式 https://dev.mysql.com/doc/refman/8.0/en/explain-output.html
EXPLAIN ANALYZE(8.0.18+) https://dev.mysql.com/doc/refman/8.0/en/explain-analyze.html
InnoDB Buffer Pool 调优 https://dev.mysql.com/doc/refman/8.0/en/innodb-buffer-pool.html
优化语句(慢查询日志) https://dev.mysql.com/doc/refman/8.0/en/slow-query-log.html
performance_schema 语句摘要表 https://dev.mysql.com/doc/refman/8.0/en/performance-schema-statement-summary-tables.html
sys schema(未用索引/冗余索引) https://dev.mysql.com/doc/refman/8.0/en/sys-schema.html
Percona Toolkit(pt-online-schema-change) https://www.percona.com/doc/percona-toolkit/LATEST/pt-online-schema-change.html

本文章属于《MySQL 数据库全栈运维实战系列》实战与方案篇。欢迎收藏、交流。

相关推荐
此生决int41 分钟前
Linux系列(08)——进程控制
linux·运维·驱动开发
vipxieliang1 小时前
事件驱动架构实战:解耦、异步与可扩展性
架构
BioTender1 小时前
Xaira 发布 X-Automate:将物理 AI 与智能体 AI 引入实验室自动化
运维·人工智能·自动化
挖掘狂人1 小时前
MySQL 深分页优化:limit 100000, 10 慢在哪,怎么救
后端·sql·mysql
codigger1 小时前
MySQL 深分页优化:limit 100000, 10 慢在哪,怎么救
数据库·sql·mysql·程序员
Francek Chen1 小时前
【大数据处理与分析】数据仓库Hive:09 Hive 编程实践
大数据·数据仓库·hive·hadoop·sql
麦壳饼1 小时前
SonnetDB 5.0 正式发布:增强 SQL 与事务,开放九模型 MCP 读写
数据库·人工智能·sql·sonnetdb
mldong2 小时前
同一个节点接力办几轮,流程才往下走:jeeflow 的后加签
后端·架构