背景与适用场景
MySQL 是业务系统最常用的数据库,跑着跑着突然接口超时、数据库 CPU 飙升、连接数打满,这些问题排查思路是什么?本文从运维角度出发,讲清楚 MySQL 慢查询的分析方法、索引优化思路、配置参数调优、以及常见性能问题的排查路径。让你遇到数据库性能问题时知道该查什么、怎么查、怎么改、改完怎么验证。
适合阅读本文的场景:
-
数据库 CPU 或内存突然打满,不知道哪些 SQL 在作妖 -
接口响应时间变长,怀疑是数据库查询慢 -
慢查询日志里有一堆 SQL,不知道该优化哪些 -
新上线的 SQL 有没有性能问题,怎么评估 -
数据库配置参数不知道怎么调 -
主从复制延迟突然变大 -
数据库连接池不够用还是浪费资源
阅读本文需要有一定的 Linux 操作基础,了解基本的 SQL 语句,知道什么是数据库连接和索引。
MySQL 架构先搞清楚
MySQL 线程模型
MySQL 是多线程架构,主要线程:
InnoDB 主线程(负责刷新脏页、合并缓冲池)
InnoDB 读线程(负责预读)
InnoDB 写线程(负责刷脏页)
InnoDB 脏页刷新线程(page cleaner)
MySQL 连接管理线程(负责接收连接)
查询缓存(MySQL 8.0 已移除)
连接模型:客户端连接 -> 连接池 -> 查询解析 -> 优化器 -> 执行器 -> 存储引擎(InnoDB/MyISAM)
# 查看当前 MySQL 版本
mysql --version
mysql -u root -p -e "SELECT VERSION();"
# 查看 MySQL 进程
ps aux | grep mysqld
systemctl status mysql
systemctl status mysqld
# 查看 MySQL 配置
mysql -u root -p -e "SHOW VARIABLES;" | head -20
# 查看最大连接数
mysql -u root -p -e "SHOW VARIABLES LIKE 'max_connections';"
InnoDB 存储引擎核心概念
InnoDB 是 MySQL 默认存储引擎,需要搞清楚几个核心概念:
Buffer Pool:InnoDB 的缓存池,缓存表数据和索引,减小磁盘 IO
redo log:事务日志,保证宕机后数据不丢失
undo log:回滚日志,支持事务回滚
change buffer:二级索引变更缓存,减少随机 IO
自适应哈希索引:InnoDB 自动优化的哈希索引
# 查看 InnoDB 状态
mysql -u root -p -e "SHOW ENGINE INNODB STATUS\G"
# 查看 Buffer Pool 大小(通常设置为机器内存的 60-80%)
mysql -u root -p -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
# 查看 Buffer Pool 使用情况(MySQL 5.6+)
mysql -u root -p -e "
SELECT
pool_id,
pool_size,
free_buffers,
database_pages,
modified_db_pages
FROM information_schema.INNODB_BUFFER_POOL_STATS;
"
# 注意:MySQL 8.0 的 INNODB_BUFFER_POOL_STATS 表结构略有差异
# 某些版本显示的字段名可能不同,实际查询时以 SHOW VARIABLES 为准
# 查看 redo log 大小和状态
mysql -u root -p -e "SHOW VARIABLES LIKE 'innodb_log_file%';"
mysql -u root -p -e "SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';"
慢查询日志配置与开启
开启慢查询日志
# 临时开启(MySQL 重启后失效)
mysql -u root -p -e "SET GLOBAL slow_query_log = 'ON';"
mysql -u root -p -e "SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';"
mysql -u root -p -e "SET GLOBAL long_query_time = 1;"# 超过 1 秒记录
# 永久开启(修改配置文件)
# /etc/mysql/my.cnf 或 /etc/mysql/mysql.conf.d/mysqld.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 # 记录未使用索引的查询
log_slow_admin_statements = 1 # 记录管理操作
# 创建日志目录并授权
mkdir -p /var/log/mysql
chown mysql:mysql /var/log/mysql
touch /var/log/mysql/slow.log
# 重启 MySQL 使配置生效
systemctl restart mysql
# 验证配置
mysql -u root -p -e "SHOW VARIABLES LIKE 'slow_query%';"
mysql -u root -p -e "SHOW VARIABLES LIKE 'long_query_time';"
mysqldumpslow 工具分析慢查询日志
# 分析慢查询日志,输出最慢的 10 条
mysqldumpslow -t 10 /var/log/mysql/slow.log
# 按平均查询时间排序
mysqldumpslow -s at /var/log/mysql/slow.log
# 按总查询时间排序
mysqldumpslow -s t /var/log/mysql/slow.log
# 按锁时间排序
mysqldumpslow -s l /var/log/mysql/slow.log
# 忽略锁时间的排序
mysqldumpslow -s ar /var/log/mysql/slow.log
# 输出示例:
# Reading mysql slow query log from /var/log/mysql/slow.log
# Count: 1 Time=5.23s (5s) Lock=0.00s (0s) Rows=1000000 (1000000) SELECT * FROM orders
# Count: 1 Time=3.21s (3s) Lock=0.00s (0s) Rows=50000 (50000) SELECT * FROM products
pt-query-digest 更强大的分析工具
Percona Toolkit 的 pt-query-digest 是生产环境分析慢查询的首选工具。
# 安装(CentOS)
yum install percona-toolkit -y
# 安装(Ubuntu/Debian)
apt-get install percona-toolkit -y
# 分析慢查询日志
pt-query-digest /var/log/mysql/slow.log
# 只分析最近 24 小时的慢查询
pt-query-digest --since=24h /var/log/mysql/slow.log
# 只分析某个时间段的慢查询
pt-query-digest --since='2026-05-29 00:00:00' --until='2026-05-29 12:00:00' /var/log/mysql/slow.log
# 分析远程 MySQL 的慢查询
pt-query-digest --max-review 100 h=192.168.1.100,u=root,p=password --since=24h
# 输出到文件
pt-query-digest /var/log/mysql/slow.log > /tmp/query_analysis.txt
# 统计查询类型分布
pt-query-digest --filter '$event->{fingerprint} =~ m/^SELECT/i' /var/log/mysql/slow.log
pt-query-digest 输出解读:
# 400ms user=root host=localhost db=mydb
# Query_time: 5.234 Lock_time: 0.001 Rows_sent: 100 Rows_examined: 500000
select * from orders where status = 'completed' and created_at > '2026-01-01'
# 解读:
# Query_time: 5.234 秒 - 这条 SQL 执行了 5.234 秒
# Lock_time: 0.001 秒 - 表锁等了 0.001 秒
# Rows_sent: 100 - 返回了 100 行
# Rows_examined: 500000 - 扫描了 50 万行
# 问题:扫描 50 万行只返回 100 行,典型的需要加索引场景
EXPLAIN 执行计划分析
EXPLAIN 基本用法
-- 语法分析(不执行)
EXPLAINSELECT * FROMusersWHEREname = '张三';
-- 详细执行计划(MySQL 5.6+)
EXPLAINANALYZESELECT * FROMusersWHEREname = '张三';
-- 查看所有字段(MySQL 5.7+)
EXPLAINFORMAT=JSONSELECT * FROMusersWHEREname = '张三';
执行计划字段解读
-- 创建测试表
CREATETABLE orders (
idBIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOTNULL,
user_id BIGINTNOTNULL,
statusVARCHAR(32) NOTNULL,
amount DECIMAL(10,2) NOTNULL,
created_at DATETIME NOTNULL,
updated_at DATETIME,
INDEX idx_user_id (user_id),
INDEX idx_status (status),
INDEX idx_created_at (created_at),
UNIQUEINDEX idx_order_no (order_no)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;
-- 分析这条 SQL
EXPLAINSELECT * FROM orders
WHERE user_id = 1000
ANDstatus = 'completed'
AND created_at > '2026-01-01'
ORDERBY created_at DESC
LIMIT20;
EXPLAIN 输出字段详解:
id: 1
select_type: SIMPLE
table: orders
partitions: NULL
type: ref
possible_keys: idx_user_id,idx_status,idx_created_at
key: idx_user_id
key_len: 8
ref: const
rows: 1234
filtered: 10.00
Extra: Using index condition; Using where; Using filesort
字段详解:
- id:执行顺序编号,相同数字表示一起执行
- select_type:查询类型,SIMPLE 最简单,SUBQUERY/UNION 等更复杂
- table:涉及的表名
- partitions:匹配的分区(分区表相关)
- type:连接类型,system > const > eq_ref > ref > range > index > ALL
ALL 是全表扫描,最差;ref 是索引查找,最好
- possible_keys:可能使用的索引
- key:实际使用的索引
- key_len:索引长度,越短越好
- ref:与索引比较的列
- rows:预计扫描的行数,越少越好
- filtered:符合条件行数占总行数的百分比
- Extra:额外信息,Using where 表示需要回表检查;Using filesort 表示需要额外排序
type 字段详解(性能从好到差)
system:表只有一行(系统表),最好
const:最多一行匹配,通常是主键或唯一索引查询
eq_ref:连接时用主键或唯一索引,只返回一行
ref:用普通索引查询,返回匹配的多行
ref_or_null:类似 ref,但包含 NULL 查询
index_merge:使用多个索引合并
unique_subquery:子查询返回唯一值
index_subquery:子查询返回非唯一值
range:索引范围查询(BETWEEN、IN、>、<)
index:全索引扫描(只遍历索引,不回表)
ALL:全表扫描(最差,需要优化)
Extra 字段详解
Using filesort:无法用索引排序,需要额外排序操作,常见于 ORDER BY 非索引字段
Using temporary:需要临时表,常见于 DISTINCT、GROUP BY、UNION
Using index:覆盖索引,直接用索引返回数据,不需要回表
Using index condition:索引下推,用索引过滤部分条件
Using where:需要回表检查 WHERE 条件
Using join buffer:连接时使用 join buffer(通常意味着连接字段没索引)
Impossible WHERE:WHERE 条件永远为 FALSE
Select tables optimized away:优化器直接返回结果
常见问题 SQL 分析
-- 1. 全表扫描(type=ALL)
EXPLAINSELECT * FROM orders WHERE amount > 100;
-- 问题:amount 字段没有索引
-- 解决:ALTER TABLE orders ADD INDEX idx_amount (amount);
-- 2. Using filesort(需要排序)
EXPLAINSELECT * FROM orders ORDERBY created_at DESCLIMIT10;
-- 问题:created_at 有索引但查询条件导致索引失效,或 ORDER BY 字段没有索引
-- 解决:SELECT * FROM orders WHERE status='completed' ORDER BY created_at DESC LIMIT 10;
-- 3. Using temporary(需要临时表)
EXPLAINSELECTDISTINCT user_id FROM orders WHEREstatus = 'completed';
-- 问题:user_id 没有索引或优化器选择不当
-- 解决:ALTER TABLE orders ADD INDEX idx_user_status (user_id, status);
-- 4. 索引失效(明明有索引但没用上)
EXPLAINSELECT * FROM orders WHEREYEAR(created_at) = 2026;
-- 问题:对索引字段使用函数导致索引失效
-- 解决:改写为 SELECT * FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';
-- 5. 多表连接顺序不当
EXPLAINSELECT a.*, b.name
FROM orders a
JOINusers b ON a.user_id = b.id
WHERE a.status = 'pending';
-- 注意看 id 字段,数字大的先执行
索引优化
索引设计原则
1. 频繁出现在 WHERE 条件的字段加索引
2. 频繁出现在 ORDER BY、GROUP BY 的字段加索引
3. 区分度高的字段适合加索引(性别这类字段区分度低,不适合)
4. 复合索引遵循最左前缀原则
5. 索引不是越多越好,维护索引有开销
6. 存储空间有限时,优先在高频查询表上建索引
复合索引设计
-- 场景:经常查询某个用户的订单,按状态和时间排序
-- 这样的 SQL:
SELECT * FROM orders
WHERE user_id = 1000ANDstatus = 'completed'
ORDERBY created_at DESC;
-- 复合索引设计(遵循最左前缀)
-- 正确:user_id 在前,status 在中,created_at 在后
ALTERTABLE orders ADDINDEX idx_user_status_date (user_id, status, created_at);
-- 验证索引是否被使用
EXPLAINSELECT * FROM orders
WHERE user_id = 1000ANDstatus = 'completed'
ORDERBY created_at DESC;
-- 最左前缀原则验证:
-- 1. user_id = 1000 -- 使用索引
-- 2. user_id = 1000 AND status = 'completed' -- 使用索引
-- 3. user_id = 1000 AND status = 'completed' AND created_at > '2026-01-01' -- 使用索引
-- 4. status = 'completed' -- 不使用索引(没从最左开始)
-- 5. user_id > 1000 -- 使用索引,但范围查询后不走索引
查看现有索引
-- 查看表的所有索引
SHOWINDEXFROM orders;
-- 格式化输出(MySQL 5.6+)
SHOWINDEXFROM orders\G
-- 从 information_schema 查看
SELECT
TABLE_NAME,
INDEX_NAME,
COLUMN_NAME,
SEQ_IN_INDEX,
NON_UNIQUE,
CARDINALITY
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'your_database'
AND TABLE_NAME = 'orders'
ORDERBY INDEX_NAME, SEQ_IN_INDEX;
索引分析工具
# 使用 EXPLAIN 分析慢查询
# 1. 先从慢查询日志或 pt-query-digest 找到需要优化的 SQL
# 2. 用 EXPLAIN 分析
mysql -u root -p -e "EXPLAIN SELECT ..." database
# 使用 optimizer_trace 分析优化器决策(MySQL 5.6+)
mysql -u root -p -e "SET optimizer_trace='enabled=on';"
mysql -u root -p -e "SELECT ..." database
mysql -u root -p -e "SELECT * FROM information_schema.OPTIMIZER_TRACE\G"
mysql -u root -p -e "SET optimizer_trace='enabled=off';"
# 使用 performance_schema 分析(MySQL 5.6+)
-- 开启监控
UPDATE performance_schema.setup_consumers
SET enabled = 'YES'
WHERE name IN ('statements_digest');
-- 查看哪些 SQL 最耗时
SELECT
DIGEST_TEXT,
COUNT_STAR,
SUM_TIMER_WAIT/1000000000000 AS sum_seconds,
AVG_TIMER_WAIT/1000000000000 AS avg_seconds,
SUM_ROWS_EXAMINED,
SUM_ROWS_SENT
FROM performance_schema.events_statements_summary_by_digest
WHERE DIGEST_TEXT IS NOT NULL
ORDER BY sum_seconds DESC
LIMIT 10;
常见慢查询优化案例
案例 1:SELECT * 危害
-- 问题 SQL
SELECT * FROM orders WHERE user_id = 1000;
-- EXPLAIN 分析
EXPLAINSELECT * FROM orders WHERE user_id = 1000;
-- type: ref
-- key: idx_user_id
-- rows: 50
-- Extra: Using index condition
-- 问题:虽然走了索引,但 SELECT * 会回表查询所有字段
-- 如果 idx_user_id 是覆盖索引(包含查询的所有字段),则不回表
-- 优化:只查需要的字段
SELECTid, order_no, status, amount, created_at
FROM orders WHERE user_id = 1000;
-- 更好:利用覆盖索引,避免回表
ALTERTABLE orders ADDINDEX idx_user_cover (user_id, id, order_no, status, amount, created_at);
EXPLAINSELECTid, order_no, status, amount, created_at
FROM orders WHERE user_id = 1000;
-- Extra: Using index(覆盖索引,不需要回表)
案例 2:分页查询优化
-- 问题 SQL:深分页查询
SELECT * FROM orders
WHEREstatus = 'completed'
ORDERBYidDESC
LIMIT1000000, 20;
-- 问题:MySQL 需要扫描 1000020 行,返回最后 20 行,极慢
-- EXPLAIN 分析
EXPLAINSELECT * FROM orders
WHEREstatus = 'completed'
ORDERBYidDESC
LIMIT1000000, 20;
-- type: ALL(全表扫描)
-- rows: 5000000(扫描 500 万行)
-- Extra: Using filesort
-- 优化 1:利用主键索引
SELECT * FROM orders
WHEREid > 1000000ANDstatus = 'completed'
ORDERBYidDESC
LIMIT20;
-- 优化 2:延迟关联
SELECT o.* FROM orders o
INNERJOIN (
SELECTidFROM orders
WHEREstatus = 'completed'
ORDERBYidDESC
LIMIT1000000, 20
) t ON o.id = t.id;
-- 优化 3:游标分页(最适合大数据量)
-- 上一页最后一条的 id 是 1000020
SELECT * FROM orders
WHEREstatus = 'completed'ANDid < 1000020
ORDERBYidDESC
LIMIT20;
案例 3:COUNT(*) 优化
-- 问题 SQL
SELECTCOUNT(*) FROM orders WHEREstatus = 'pending';
-- 问题:全表扫描统计行数,大表极慢
-- EXPLAIN 分析
EXPLAINSELECTCOUNT(*) FROM orders WHEREstatus = 'pending';
-- type: ALL
-- rows: 5000000
-- 优化 1:利用索引
ALTERTABLE orders ADDINDEX idx_status (status);
EXPLAINSELECTCOUNT(*) FROM orders WHEREstatus = 'pending';
-- type: ref
-- key: idx_status
-- rows: 500
-- 优化 2:使用 EXPLAIN 辅助(MySQL 8.0+ 支持)
EXPLAINSELECTCOUNT(*) FROM orders WHEREstatus = 'pending';
-- 优化 3:近似值(允许误差时)
SELECT TABLE_ROWS FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_database'AND TABLE_NAME = 'orders';
-- 优化 4:汇总表(实时性要求不高时)
CREATETABLE orders_status_count (
statusVARCHAR(32) PRIMARY KEY,
cnt BIGINTNOTNULLDEFAULT0
);
-- 定时更新汇总表
UPDATE orders_status_count, (
SELECTstatus, COUNT(*) as cnt FROM orders GROUPBYstatus
) t SET orders_status_count.cnt = t.cnt
WHERE orders_status_count.status = t.status;
案例 4:JOIN 优化
-- 问题 SQL:多表 JOIN
SELECT o.id, o.order_no, u.name, u.email
FROM orders o
JOINusers u ON o.user_id = u.id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.status = 'completed'
AND p.category = 'electronics'
LIMIT100;
-- EXPLAIN 分析
EXPLAINSELECT o.id, o.order_no, u.name, u.email
FROM orders o
JOINusers u ON o.user_id = u.id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.status = 'completed'
AND p.category = 'electronics'
LIMIT100;
-- 优化原则:
-- 1. 确保连接字段有索引
-- 2. 优先过滤数据量大的表
-- 3. 小表驱动大表(MySQL 优化器通常自动选择)
-- 先看各表数据量
SELECTCOUNT(*) FROM orders; -- 500万
SELECTCOUNT(*) FROMusers; -- 100万
SELECTCOUNT(*) FROM order_items; -- 2000万
SELECTCOUNT(*) FROM products; -- 10万
-- 优化:让数据量小的表先 JOIN
SELECT o.id, o.order_no, u.name, u.email
FROM products p
JOIN order_items oi ON p.id = oi.product_id
JOIN orders o ON oi.order_id = o.id
JOINusers u ON o.user_id = u.id
WHERE p.category = 'electronics'
AND o.status = 'completed'
LIMIT100;
-- 确保索引完整
ALTERTABLE order_items ADDINDEX idx_order_product (order_id, product_id);
ALTERTABLE orders ADDINDEX idx_status (status);
案例 5:OR 条件优化
-- 问题 SQL
SELECT * FROMusers
WHEREname = '张三'OR email = 'zhangsan@example.com'
OR phone = '13800138000';
-- 问题:OR 条件可能导致索引失效
-- EXPLAIN 分析
EXPLAINSELECT * FROMusers
WHEREname = '张三'OR email = 'zhangsan@example.com'
OR phone = '13800138000';
-- type: ALL(可能全表扫描)
-- 优化 1:改写为 UNION
SELECT * FROMusersWHEREname = '张三'
UNION
SELECT * FROMusersWHERE email = 'zhangsan@example.com'
UNION
SELECT * FROMusersWHERE phone = '13800138000';
-- 优化 2:利用索引(每个 OR 条件都走索引时)
EXPLAINSELECT * FROMusers
WHEREname = '张三'
UNION
SELECT * FROMusers
WHERE email = 'zhangsan@example.com'
UNION
SELECT * FROMusers
WHERE phone = '13800138000';
-- 优化 3:使用 IN 代替 OR
SELECT * FROMusers
WHEREnameIN ('张三')
OR email IN ('zhangsan@example.com')
OR phone IN ('13800138000');
-- 效果同 OR,但语义更清晰
-- 建议:为每个字段单独建索引,UNION 效率更高
ALTERTABLEusersADDINDEX idx_name (name);
ALTERTABLEusersADDINDEX idx_email (email);
ALTERTABLEusersADDINDEX idx_phone (phone);
案例 6:IN + 子查询优化
-- 问题 SQL:IN 里面的子查询
SELECT * FROM orders
WHERE user_id IN (
SELECTidFROMusers
WHERE created_at > '2026-01-01'
);
-- EXPLAIN 分析
EXPLAINSELECT * FROM orders
WHERE user_id IN (
SELECTidFROMusers
WHERE created_at > '2026-01-01'
);
-- 执行计划可能先执行子查询,再执行外层
-- 优化 1:使用 EXISTS 改写(子查询数据量大时)
SELECT * FROM orders o
WHEREEXISTS (
SELECT1FROMusers u
WHERE u.id = o.user_id
AND u.created_at > '2026-01-01'
);
-- 优化 2:使用 JOIN 改写
SELECT o.* FROM orders o
INNERJOINusers u ON o.user_id = u.id
WHERE u.created_at > '2026-01-01';
-- 优化 3:半连接优化(MySQL 8.0+ 自动优化)
EXPLAINSELECT * FROM orders
WHERE user_id IN (
SELECTidFROMusers
WHERE created_at > '2026-01-01'
);
-- MySQL 8.0 可能自动选择 MaterializeLookup 策略
MySQL 配置参数调优
核心参数说明
# 连接到 MySQL
mysql -u root -p
# 查看当前参数值
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
SHOW VARIABLES LIKE 'max_connections';
SHOW VARIABLES LIKE 'innodb_log_file_size';
常用调参配置(在 my.cnf 中设置):
[mysqld]
# 内存配置
# Buffer Pool 大小,通常设置为机器内存的 60-80%
innodb_buffer_pool_size = 8G
# 连接配置
# 最大连接数,根据业务并发需求调整
max_connections = 2000
# 每个连接可用内存(防止单个查询耗尽内存)
max_connections = 2000
max_allowed_packet = 64M
# 日志配置
# redo log 文件大小,建议 1GB-4GB
innodb_log_file_size = 1G
# redo log 组数量
innodb_log_files_in_group = 3
# 事务提交后刷盘策略(1 最安全,2 性能最好,0 折中)
innodb_flush_log_at_trx_commit = 1
# 临时表和排序缓冲
tmp_table_size = 256M
max_heap_table_size = 256M
sort_buffer_size = 4M
join_buffer_size = 4M
# 注意:MySQL 5.7 和 MySQL 8.0 已经移除了查询缓存(query_cache)
# 5.7 的 query_cache_size 和 query_cache_type 已废弃
# 8.0 完全移除,配置这些参数会报错
# 如果看到配置文件中有这些参数,应该删除
# 慢查询日志
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
# 线程缓存
thread_cache_size = 64
# 表缓存
table_open_cache = 4000
# InnoDB 配置
# 刷新脏页的线程数(建议 CPU 核心数的 1/4)
innodb_page_cleaners = 4
# 开启独立表空间(建议开启,便于管理)
innodb_file_per_table = 1
# 并发线程数(0 表示自动设置,等于 CPU 核心数)
innodb_thread_concurrency = 0
# 开启慢 SQL 记录(记录未使用索引的查询)
log_queries_not_using_indexes = 1
参数调整验证
# 修改参数后验证
mysql -u root -p -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
# 查看 Buffer Pool 使用情况
mysql -u root -p -e "
SELECT
ROUND(POOL_SIZE * PAGE_SIZE / 1024 / 1024, 2) AS pool_size_mb,
ROUND(FREE_BUFFERS * PAGE_SIZE / 1024 / 1024, 2) AS free_mb,
ROUND(DATABASE_PAGES * PAGE_SIZE / 1024 / 1024, 2) AS used_mb
FROM information_schema.INNODB_BUFFER_POOL_STATS
WHERE pool_id = 0;
"
# 查看连接数使用情况
mysql -u root -p -e "
SELECT
MAX_USED_CONNECTIONS,
MAX_USED_CONNECTIONS / MAX_CONNECTIONS * 100 AS usage_pct,
THREADS_CONNECTED,
THREADS_RUNNING
FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME IN ('Max_used_connections', 'Max_used_connections', 'Threads_connected', 'Threads_running');
"
# 查看当前连接
mysql -u root -p -e "SHOW PROCESSLIST;"
mysql -u root -p -e "SHOW FULL PROCESSLIST;"
配置变更操作步骤
# 1. 备份原配置文件
cp /etc/mysql/my.cnf /etc/mysql/my.cnf.bak.$(date +%Y%m%d)
# 2. 修改配置
vim /etc/mysql/my.cnf
# 3. 检查配置语法
mysqld --validate-config
# 4. 优雅重启 MySQL(不断开现有连接)
mysql -u root -p -e "SET GLOBAL innodb_buffer_pool_size = 8589934592;"
# 注:某些参数需要重启才能生效
# 5. 重启 MySQL(会短暂断开连接)
systemctl restart mysql
# 6. 验证配置生效
mysql -u root -p -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
# 7. 监控错误日志
tail -f /var/log/mysql/error.log
主从复制与延迟监控
查看主从复制状态
-- MySQL 5.7 查看主从状态
SHOW SLAVE STATUS\G
-- MySQL 8.0+ 查看主从状态(8.0.22+ 推荐使用 REPLICA 关键字)
SHOW REPLICA STATUS\G
-- 关键字段解读(两种版本通用):
-- Slave_IO_Running / Replica_IO_Running: IO 线程是否运行
-- Slave_SQL_Running / Replica_SQL_Running: SQL 线程是否运行
-- Seconds_Behind_Master / Seconds_Behind_Source: 延迟秒数
-- Relay_Log_Pos / Replica_Log_Pos: Relay Log 位置
-- Exec_Master_Log_Pos / Exec_Master_Log_Pos: 执行到主库日志位置
-- Last_Error: 最后一次错误
常见延迟原因与排查
-- 1. 大事务导致延迟
-- 监控:Seconds_Behind_Master 突然变大
-- 排查:
SHOWPROCESSLIST;
-- 找到 SQL 线程状态,查看执行的 SQL
-- 2. 从库压力大导致延迟
-- 监控:CPU 和 IO 使用率
-- 排查:在从库执行
SHOWSTATUSLIKE'Handler_read%';
-- 3. 网络延迟导致
-- 排查:监控主从之间的网络带宽和延迟
-- 解决:使用压缩传输
STOPSLAVE;
SETGLOBAL slave_compressed_protocol = 1;
STARTSLAVE;
-- 4. 大表 DDL 导致延迟
-- 解决:使用 pt-online-schema-change 或 gh-ost
-- pt-online-schema-change 示例:
pt-online-schema-change \
--alter "ADD INDEX idx_name (name)" \
--user=root \
--password=xxx \
D=mydb,t=orders \
--execute
-- 5. 错误跳过导致延迟
-- 如果某个 SQL 执行报错被跳过
SHOWSLAVESTATUS\G
-- 查看 Last_Errno 和 Last_Error
主从延迟监控 SQL
-- MySQL 5.7 查询当前延迟
SHOWSLAVESTATUS\G
-- 查看 Seconds_Behind_Master 字段
-- MySQL 8.0 查询当前延迟
SHOW REPLICA STATUS\G
-- MySQL 8.0 performance_schema 查询(更详细)
SELECT
slave_server_id,
source_uuid,
service_state,
last_heartbeat_timestamp,
received_heartbeat_set,
last_error_number,
last_error_message
FROM performance_schema.replication_connection_status\G
SELECT
slave_server_id,
service_state,
count_transactions_retries
FROM performance_schema.replication_applier_status\G
-- 注意:performance_schema.replication_* 表是 MySQL 8.0+ 才有的
-- MySQL 5.7 使用 SHOW SLAVE STATUS 获取同样信息
性能问题排查流程
数据库突然变慢的排查思路
1. 查看当前连接和查询
SHOW PROCESSLIST;
SHOW FULL PROCESSLIST;
2. 查看慢查询
mysqldumpslow -t 10 /var/log/mysql/slow.log
3. 查看锁等待
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
SELECT * FROM information_schema.INNODB_TRX;
4. 查看资源使用
SHOW STATUS LIKE 'Threads%';
SHOW STATUS LIKE 'Connections';
5. 查看表锁情况
SHOW STATUS LIKE 'Table_locks%';
实时排查命令
# 1. 查看当前执行的 SQL
mysql -u root -p -e "SHOW PROCESSLIST\G"
mysql -u root -p -e "SHOW FULL PROCESSLIST\G"
# 2. 查看锁等待
mysql -u root -p -e "
SELECT
r.trx_id,
r.trx_mysql_thread_id,
r.trx_query,
b.blocking_query,
b.blocking_id
FROM information_schema.INNODB_TRX r
LEFT JOIN (
SELECT
trx_id,
trx_mysql_thread_id AS blocking_id,
trx_query AS blocking_query
FROM information_schema.INNODB_TRX
) b ON b.blocking_id = r.trx_mysql_thread_id
WHERE r.trx_state = 'LOCK WAIT';
"
# 3. 查看连接数
mysql -u root -p -e "
SELECT
VARIABLE_NAME,
VARIABLE_VALUE
FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME IN ('MAX_USED_CONNECTIONS', 'THREADS_CONNECTED');
"
# 4. 查看 Buffer Pool 命中率
mysql -u root -p -e "
SELECT
POOL_ID,
PAGES_DATA,
PAGES_DIRTY,
PAGES_FLUSH,
PAGES_MADE_NOT_YONG,
PAGES_MADE_YOUNG,
NUMBER_PAGES_GET,
HASH_SCANDES,
HASH_ROWS_GET,
HASH_ROWS_UPDATED,
HASH_ROWS_DELETED
FROM information_schema.INNODB_BUFFER_PAGE;
" | less
# 5. 查看临时表使用情况
mysql -u root -p -e "
SELECT
SUM(CREATED_TMP_TABLES) AS created_tmp_tables,
SUM(CREATED_TMP_DISK_TABLES) AS created_tmp_disk_tables,
SUM(CREATED_TMP_FILES) AS created_tmp_files
FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME LIKE 'Created_tmp%';
"
常见问题速查
-- 问题 1:连接数打满
-- 原因:连接泄漏、连接池配置不当、慢查询阻塞
-- 解决:
-- 1. 查看连接
SHOWPROCESSLIST;
-- 2. 杀掉阻塞的连接
KILL <thread_id>;
-- 3. 调大 max_connections
SETGLOBAL max_connections = 3000;
-- 问题 2:CPU 打满
-- 原因:慢查询、缺少索引、表结构不合理
-- 解决:
-- 1. 找慢 SQL
SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDERBY SUM_TIMER_WAIT DESCLIMIT10;
-- 2. EXPLAIN 分析
EXPLAIN <slow_sql>;
-- 3. 加索引
ALTERTABLE orders ADDINDEX idx_xxx (xxx);
-- 问题 3:磁盘 IO 打满
-- 原因:频繁刷脏、大事务、大排序
-- 解决:
-- 1. 调大 Buffer Pool
SETGLOBAL innodb_buffer_pool_size = 16 * 1024 * 1024 * 1024;
-- 2. 合并小 IO
SETGLOBAL innodb_flush_log_at_trx_commit = 2;
-- 3. 优化 SQL,减少扫描行数
-- 问题 4:内存使用过高
-- 原因:Buffer Pool 设置过大、临时表过多
-- 解决:
-- 1. 检查 Buffer Pool 大小
SHOWVARIABLESLIKE'innodb_buffer_pool_size';
-- 2. 检查临时表
SHOWSTATUSLIKE'Created_tmp%';
-- 3. 调整 tmp_table_size
SETGLOBAL tmp_table_size = 128 * 1024 * 1024;
风险提醒与回滚方案
生产环境操作规范
# 1. DDL 操作前必须备份
mysqldump -u root -p --single-transaction mydb orders > orders_backup_$(date +%Y%m%d).sql
# 2. 大表加索引用 pt 工具(在线加索引,不锁表)
pt-online-schema-change \
--alter "ADD INDEX idx_name (name)" \
--user=root \
--password=xxx \
D=mydb,t=orders \
--execute \
--print
# 3. 禁止在业务高峰期执行 DDL
# 选择凌晨低峰期,并通知相关人员
# 4. 配置变更前备份原配置
cp /etc/mysql/my.cnf /etc/mysql/my.cnf.bak.$(date +%Y%m%d)
# 5. 重大变更前在测试环境验证
# 测试环境完全模拟生产配置
# 执行同样的 SQL,观察执行计划
# 6. 回滚方案准备
# DDL 回滚:备份原表结构,用备份恢复
# 配置回滚:保留原配置文件
# 索引回滚:DROP INDEX 即可(风险较小)
危险操作清单
# 1. DELETE 和 UPDATE 必须带 WHERE 条件
-- 危险:DELETE FROM orders; -- 删除整表
-- 安全:DELETE FROM orders WHERE id > 1000000 AND created_at < '2025-01-01';
# 2. TRUNCATE 和 DROP TABLE 极度危险
# TRUNCATE TABLE orders; -- 立即清空表,无法回滚(除非有备份)
# DROP TABLE orders; -- 删除表结构,无法回滚
# 3. ALTER TABLE 可能长时间锁表
# 大表 ALTER 在 MySQL 5.6 之前会锁表
# 使用 pt-online-schema-change 或 gh-ost
# 4. 禁止在生产环境执行无限制的 JOIN
# SELECT * FROM a JOIN b JOIN c JOIN d ... -- 可能导致内存爆炸
# 5. 禁止在生产环境执行无 LIMIT 的查询
# SELECT * FROM orders WHERE status = 'pending'; -- 可能返回百万行
# 6. 连接数调整需要谨慎
# max_connections 设置过大会导致 MySQL 启动失败(内存不足)
# 预估:max_connections * max_allowed_packet = 最大内存使用
备份策略
# 1. 全量备份
mysqldump -u root -p \
--single-transaction \
--routines \
--triggers \
--events \
--master-data=2 \
mydb > mydb_full_backup_$(date +%Y%m%d).sql
# 2. 备份压缩
mysqldump -u root -p mydb | gzip > mydb_backup_$(date +%Y%m%d).sql.gz
# 3. 只备份表结构
mysqldump -u root -p --no-data mydb > mydb_schema.sql
# 4. 只备份数据
mysqldump -u root -p --no-create-info mydb > mydb_data.sql
# 5. xtrabackup 热备(生产环境推荐)
innobackupex --user=root --password=xxx /backup/
innobackupex --user=root --password=xxx --apply-log /backup/2026-05-29_xx-xx-xx/
# 6. 恢复
mysql -u root -p mydb < mydb_full_backup_20260529.sql
# 7. 定时备份脚本示例
cat > /opt/scripts/mysql_backup.sh << 'EOF'
#!/bin/bash
DATE=$(date +%Y%m%d)
BACKUP_DIR="/backup/mysql"
MYSQL_USER="root"
MYSQL_PASS="xxx"
MYSQL_HOST="localhost"
# 创建备份目录
mkdir -p ${BACKUP_DIR}
# 全量备份
mysqldump -h ${MYSQL_HOST} -u ${MYSQL_USER} -p${MYSQL_PASS} \
--single-transaction \
--routines \
--triggers \
--events \
mydb | gzip > ${BACKUP_DIR}/mydb_${DATE}.sql.gz
# 清理 7 天前的备份
find ${BACKUP_DIR} -name "*.sql.gz" -mtime +7 -delete
echo"Backup completed: ${DATE}"
EOF
chmod +x /opt/scripts/mysql_backup.sh
# 加入 crontab
# 0 3 * * * /opt/scripts/mysql_backup.sh
总结
MySQL 慢查询优化是运维的必备技能,核心知识点:
慢查询分析:
-
开启慢查询日志: slow_query_log = 1、long_query_time = 1 -
用 mysqldumpslow和pt-query-digest分析日志 -
重点关注 Rows_examined和Rows_sent的比值
执行计划分析:
-
EXPLAIN查看 SQL 执行计划 -
type字段从好到差:const>eq_ref>ref>range>index>ALL -
Extra关注Using filesort、Using temporary、Using index
索引优化:
-
遵循最左前缀原则 -
覆盖索引避免回表 -
不要在索引列上使用函数 -
OR 条件改写为 UNION
配置调优:
-
innodb_buffer_pool_size:机器内存的 60-80% -
max_connections:根据业务并发需求 -
innodb_log_file_size:1-4GB
排查流程:
-
SHOW PROCESSLIST查看当前查询 -
information_schema查看锁等待 -
performance_schema查看性能统计
遇到数据库性能问题不要慌,按顺序排查:先看连接和慢 SQL,再分析执行计划,最后调整配置和索引。改动前一定备份,改动后一定验证。
转自:马哥Linux运维
版权申明:内容来源网络,版权归原创者所有,如有侵权请联系删除
想了解更多干货,可通过下方扫码关注

可扫码添加上智启元官方客服微信👇

17认证网








