MySQL 实战精通系列 · 第8篇:慢查询治理与性能优化实战
本篇目标:线上 MySQL 变慢了,能系统化定位问题、给出优化方案、验证效果。
文章目录
- [MySQL 实战精通系列 · 第8篇:慢查询治理与性能优化实战](#MySQL 实战精通系列 · 第8篇:慢查询治理与性能优化实战)
-
- 一、性能优化分层思路
-
- [1.1 优化的五个层次](#1.1 优化的五个层次)
- [1.2 一张图看懂排查顺序](#1.2 一张图看懂排查顺序)
- [二、慢查询日志:找到慢 SQL](#二、慢查询日志:找到慢 SQL)
-
- [2.1 开启慢查询日志](#2.1 开启慢查询日志)
- [2.2 查看慢查询日志](#2.2 查看慢查询日志)
- [2.3 用 performance_schema 分析](#2.3 用 performance_schema 分析)
- 三、关键性能指标
-
- [3.1 必须监控的指标](#3.1 必须监控的指标)
- [3.2 查看当前状态](#3.2 查看当前状态)
- [3.3 计算 QPS / TPS](#3.3 计算 QPS / TPS)
- [四、SQL 优化实战](#四、SQL 优化实战)
-
- [4.1 优化流程](#4.1 优化流程)
- [4.2 案例1:深分页优化](#4.2 案例1:深分页优化)
- [4.3 案例2:JOIN 优化](#4.3 案例2:JOIN 优化)
- [4.4 案例3:子查询优化](#4.4 案例3:子查询优化)
- [4.5 案例4:COUNT 优化](#4.5 案例4:COUNT 优化)
- [4.6 案例5:ORDER BY 优化](#4.6 案例5:ORDER BY 优化)
- [五、大表 DDL 在线变更](#五、大表 DDL 在线变更)
-
- [5.1 为什么大表 DDL 危险?](#5.1 为什么大表 DDL 危险?)
- [5.2 Online DDL](#5.2 Online DDL)
- [5.3 gh-ost 实战](#5.3 gh-ost 实战)
- [5.4 pt-online-schema-change](#5.4 pt-online-schema-change)
- [六、sysbench 压测](#六、sysbench 压测)
-
- [6.1 安装 sysbench](#6.1 安装 sysbench)
- [6.2 准备测试数据](#6.2 准备测试数据)
- [6.3 运行压测](#6.3 运行压测)
- [6.4 输出解读](#6.4 输出解读)
- [6.5 清理](#6.5 清理)
- 七、参数调优
-
- [7.1 关键参数](#7.1 关键参数)
- [7.2 调优原则](#7.2 调优原则)
- 八、实战任务
- 九、本篇小结
一、性能优化分层思路
1.1 优化的五个层次
性能优化
│
├── ① SQL 与索引 ← 收益最大,成本最低
├── ② 表结构与数据类型 ← 建模阶段决定
├── ③ 事务与锁 ← 并发问题
├── ④ 连接池与参数 ← 配置调优
└── ⑤ 硬件与架构 ← 最后手段
原则:从上往下查,先查 SQL,再查配置,最后才考虑加机器。
1.2 一张图看懂排查顺序
MySQL 变慢
│
↓
① 是不是某条 SQL 慢?
│ 是 → EXPLAIN 分析 → 加/改索引 → 重写 SQL
│
↓ 否
② 是不是锁等待?
│ 是 → 查看 innodb_trx → 找长事务 → 优化事务
│
↓ 否
③ 是不是连接数爆了?
│ 是 → 查看 processlist → 优化连接池
│
↓ 否
④ 是不是磁盘 IO 高?
│ 是 → 查看 Buffer Pool 命中率 → 加内存
│
↓ 否
⑤ 是不是数据量太大?
是 → 分区 / 分表 / 归档
二、慢查询日志:找到慢 SQL
2.1 开启慢查询日志
sql
-- 查看当前配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过1秒记录
SET GLOBAL log_queries_not_using_indexes = ON; -- 记录未用索引的
-- 永久生效:改 my.cnf
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
2.2 查看慢查询日志
bash
# 查看日志文件
docker exec mysql8 cat /var/log/mysql/slow.log
# 用 mysqldumpslow 汇总
docker exec mysql8 mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
mysqldumpslow 参数:
| 参数 | 含义 |
|---|---|
| -s t | 按总时间排序 |
| -s c | 按次数排序 |
| -s l | 按锁时间排序 |
| -t 10 | 取前10条 |
2.3 用 performance_schema 分析
sql
-- 开启
UPDATE performance_schema.setup_consumers
SET ENABLED = 'YES'
WHERE NAME LIKE '%statements%';
-- 查看 Top 10 慢 SQL
SELECT
DIGEST_TEXT AS sql_text,
COUNT_STAR AS exec_count,
ROUND(SUM_TIMER_WAIT/1000000000, 2) AS total_ms,
ROUND(AVG_TIMER_WAIT/1000000000, 2) AS avg_ms,
ROUND(MAX_TIMER_WAIT/1000000000, 2) AS max_ms,
SUM_ROWS_EXAMINED AS rows_examined,
SUM_ROWS_SENT AS rows_sent
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
三、关键性能指标
3.1 必须监控的指标
| 指标 | 含义 | 健康值 |
|---|---|---|
| QPS | 每秒查询数 | 业务决定 |
| TPS | 每秒事务数 | 业务决定 |
| RT | 响应时间 | < 100ms |
| 连接数 | 当前连接 | < max_connections 80% |
| Buffer Pool 命中率 | 内存命中 | > 99% |
| 锁等待 | 等待锁的事务 | 越少越好 |
| 慢查询数 | 慢 SQL 数量 | 越少越好 |
| 主从延迟 | 从库落后 | < 1s |
3.2 查看当前状态
sql
-- 当前连接
SHOW PROCESSLIST;
-- 连接数
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Max_used_connections';
-- QPS / TPS
SHOW STATUS LIKE 'Queries';
SHOW STATUS LIKE 'Com_commit';
SHOW STATUS LIKE 'Com_rollback';
-- Buffer Pool 命中率
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
-- 锁等待
SELECT * FROM information_schema.innodb_trx;
SELECT * FROM performance_schema.data_lock_waits;
3.3 计算 QPS / TPS
sql
-- QPS = Queries / 秒
-- 两次采样差值
-- 查看
SHOW GLOBAL STATUS LIKE 'Queries';
-- 等 10 秒
SHOW GLOBAL STATUS LIKE 'Queries';
-- QPS = (第二次 - 第一次) / 10
四、SQL 优化实战
4.1 优化流程
慢 SQL
│
↓
① EXPLAIN 看执行计划
│
↓
② 判断问题类型
│
├── type = ALL → 没走索引
├── key = NULL → 没走索引
├── rows 很大 → 扫描行数多
├── Extra = Using filesort → 文件排序
└── Extra = Using temporary → 临时表
│
↓
③ 对应优化
│
├── 加索引
├── 改 SQL
├── 改表结构
└── 分页优化
4.2 案例1:深分页优化
sql
-- 慢:扫描 100 万行,只返回 20 行
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
-- 优化1:用主键游标
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;
-- 优化2:延迟关联
SELECT o.* FROM orders o
INNER JOIN (
SELECT id FROM orders ORDER BY id LIMIT 1000000, 20
) t ON o.id = t.id;
对比:
LIMIT 1000000, 20
│
↓ 扫描 1000020 行
↓ 丢弃前 1000000 行
↓ 返回 20 行
主键游标
│
↓ 直接定位到 id > 1000000
↓ 返回 20 行
4.3 案例2:JOIN 优化
sql
-- 慢:小表驱动大表,但驱动表没索引
SELECT * FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE u.phone = '13800000001';
-- 优化:先查小表,再关联
SELECT o.* FROM orders o
INNER JOIN (
SELECT id FROM users WHERE phone = '13800000001'
) u ON o.user_id = u.id;
JOIN 优化原则:
① 小表驱动大表
② 被驱动表的关联字段要有索引
③ 能用 INNER JOIN 就别用 LEFT JOIN
④ 减少 JOIN 的表数量
4.4 案例3:子查询优化
sql
-- 慢:相关子查询,每行都执行一次
SELECT * FROM users u
WHERE (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) > 5;
-- 优化:JOIN + GROUP BY
SELECT u.* FROM users u
INNER JOIN (
SELECT user_id FROM orders GROUP BY user_id HAVING COUNT(*) > 5
) t ON u.id = t.user_id;
4.5 案例4:COUNT 优化
sql
-- 慢:COUNT(*)
SELECT COUNT(*) FROM orders WHERE status = 1;
-- 优化1:加索引
CREATE INDEX idx_status ON orders (status);
-- 优化2:用近似值(业务允许时)
SHOW TABLE STATUS LIKE 'orders'; -- 看 Rows 列
-- 优化3:维护计数表
CREATE TABLE order_stats (
status TINYINT PRIMARY KEY,
cnt BIGINT NOT NULL DEFAULT 0
);
4.6 案例5:ORDER BY 优化
sql
-- 慢:Using filesort
SELECT * FROM orders ORDER BY total_amount DESC LIMIT 10;
-- 优化:加索引
CREATE INDEX idx_total ON orders (total_amount);
-- 联合索引优化
-- 查询:WHERE status = 1 ORDER BY created_at DESC
CREATE INDEX idx_status_created ON orders (status, created_at);
ORDER BY 优化原则:
① 排序字段加索引
② 联合索引:等值在前,排序在后
③ 避免 SELECT *,减少排序数据量
④ 排序方向一致(都 ASC 或都 DESC)
五、大表 DDL 在线变更
5.1 为什么大表 DDL 危险?
ALTER TABLE orders ADD COLUMN remark VARCHAR(255);
MySQL 5.6 之前:
① 创建新表
② 复制数据
③ 删除旧表
④ 重命名
→ 锁表,1000万行可能几小时
MySQL 5.6+:
Online DDL,大部分操作不锁表
但仍有风险
5.2 Online DDL
sql
-- 加索引(不锁表)
ALTER TABLE orders ADD INDEX idx_status (status), ALGORITHM=INPLACE, LOCK=NONE;
| 参数 | 含义 |
|---|---|
| ALGORITHM=INPLACE | 原地修改 |
| ALGORITHM=COPY | 复制表 |
| LOCK=NONE | 不锁表 |
| LOCK=SHARED | 共享锁 |
| LOCK=EXCLUSIVE | 排他锁 |
5.3 gh-ost 实战
bash
# 安装 gh-ost
# 在线加列
gh-ost \
--host=127.0.0.1 \
--port=3306 \
--user=root \
--password=root123 \
--database=shop \
--table=orders \
--alter="ADD COLUMN remark VARCHAR(255) DEFAULT ''" \
--execute
gh-ost 原理:
① 创建影子表(_orders_gho)
② 在影子表上执行 DDL
③ 从 binlog 同步增量数据
④ 分批复制存量数据
⑤ 原子重命名
5.4 pt-online-schema-change
bash
pt-online-schema-change \
--host=127.0.0.1 \
--user=root \
--password=root123 \
--alter="ADD COLUMN remark VARCHAR(255)" \
D=shop,t=orders \
--execute
六、sysbench 压测
6.1 安装 sysbench
bash
# Ubuntu
apt install sysbench
# 或 Docker
docker run --rm -it --network host severalnines/sysbench
6.2 准备测试数据
bash
sysbench oltp_read_write \
--mysql-host=127.0.0.1 \
--mysql-port=3306 \
--mysql-user=root \
--mysql-password=root123 \
--mysql-db=shop \
--tables=10 \
--table-size=100000 \
prepare
6.3 运行压测
bash
sysbench oltp_read_write \
--mysql-host=127.0.0.1 \
--mysql-port=3306 \
--mysql-user=root \
--mysql-password=root123 \
--mysql-db=shop \
--tables=10 \
--table-size=100000 \
--threads=16 \
--time=60 \
--report-interval=10 \
run
6.4 输出解读
SQL statistics:
queries performed:
read: 1000000
write: 300000
other: 200000
total: 1500000
transactions: 100000 (1666.67 per sec.)
queries: 1500000 (25000.00 per sec.)
ignored errors: 0 (0.00 per sec.)
reconnects: 0 (0.00 per sec.)
General statistics:
total time: 60.0000s
total number of events: 100000
Latency (ms):
min: 5.00
avg: 10.00
max: 100.00
95th percentile: 20.00
sum: 1000000.00
Threads fairness:
events (avg/stddev): 6250.0000/50.00
execution time (avg/stddev): 60.0000/0.00
关键指标:
| 指标 | 含义 | 关注点 |
|---|---|---|
| transactions per sec | TPS | 越高越好 |
| queries per sec | QPS | 越高越好 |
| avg latency | 平均延迟 | 越低越好 |
| 95th percentile | 95% 请求延迟 | 越低越好 |
6.5 清理
bash
sysbench oltp_read_write \
--mysql-host=127.0.0.1 \
--mysql-user=root \
--mysql-password=root123 \
--mysql-db=shop \
cleanup
七、参数调优
7.1 关键参数
ini
[mysqld]
# 缓冲池,一般设为物理内存的 60%-80%
innodb_buffer_pool_size = 4G
# redo log 大小
innodb_log_file_size = 1G
# 刷盘策略,1 最安全
innodb_flush_log_at_trx_commit = 1
# binlog 刷盘策略
sync_binlog = 1
# 连接数
max_connections = 500
# 每连接缓冲
sort_buffer_size = 2M
join_buffer_size = 2M
read_buffer_size = 2M
# 临时表
tmp_table_size = 64M
max_heap_table_size = 64M
# 慢查询
slow_query_log = 1
long_query_time = 1
7.2 调优原则
参数调优
│
├── Buffer Pool 最大,其他按需
├── 不要盲目调大所有参数
├── 先监控,再调优
├── 每次只改一个参数
└── 改完必须压测验证
八、实战任务
任务清单
- 开启慢查询日志
- 用 mysqldumpslow 分析 Top 10
- 用 performance_schema 查 Top 10 慢 SQL
- 优化深分页查询
- 优化一个 JOIN 查询
- 优化一个子查询
- 用 gh-ost 在线加列
- 用 sysbench 压测并记录结果
- 调整一个参数并验证效果
自检问题
- 性能优化的五个层次是什么?
- 慢查询日志怎么开启和分析?
- 深分页怎么优化?
- JOIN 优化原则是什么?
- 大表 DDL 为什么危险?gh-ost 怎么解决?
- sysbench 关键指标有哪些?
- Buffer Pool 应该设多大?
九、本篇小结
第8篇 核心收获
│
├── 优化分层
│ ├── SQL 与索引(收益最大)
│ ├── 表结构
│ ├── 事务与锁
│ ├── 连接池与参数
│ └── 硬件与架构
│
├── 慢查询日志
│ ├── slow_query_log
│ ├── mysqldumpslow
│ └── performance_schema
│
├── 关键指标
│ ├── QPS / TPS / RT
│ ├── 连接数
│ ├── Buffer Pool 命中率
│ └── 锁等待
│
├── SQL 优化
│ ├── 深分页:主键游标 / 延迟关联
│ ├── JOIN:小表驱动大表
│ ├── 子查询:改 JOIN
│ ├── COUNT:加索引 / 计数表
│ └── ORDER BY:加索引
│
├── 大表 DDL
│ ├── Online DDL
│ ├── gh-ost
│ └── pt-online-schema-change
│
├── sysbench
│ ├── prepare / run / cleanup
│ └── TPS / QPS / 95% 延迟
│
└── 参数调优
├── Buffer Pool 最大
├── 先监控再调
└── 每次只改一个
下一篇:第9篇《备份恢复、主从复制与高可用实战》