系统讲解MySQL数据库慢查询优化思路

描述

背景与适用场景

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 STATUSG"

# 查看 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 0000' --until='2026-05-29 1200' /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 ordersG

-- 从 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_TRACEG"
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 STATUSG

-- MySQL 8.0+ 查看主从状态(8.0.22+ 推荐使用 REPLICA 关键字)
SHOW REPLICA STATUSG

-- 关键字段解读(两种版本通用):
-- 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 执行报错被跳过
SHOWSLAVESTATUSG
-- 查看 Last_Errno 和 Last_Error

 

主从延迟监控 SQL

 

-- MySQL 5.7 查询当前延迟
SHOWSLAVESTATUSG
-- 查看 Seconds_Behind_Master 字段

-- MySQL 8.0 查询当前延迟
SHOW REPLICA STATUSG

-- 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_statusG

SELECT
    slave_server_id,
    service_state,
    count_transactions_retries
FROM performance_schema.replication_applier_statusG

-- 注意: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 PROCESSLISTG"
mysql -u root -p -e "SHOW FULL PROCESSLISTG"

# 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 ;
-- 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 ;
-- 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,再分析执行计划,最后调整配置和索引。改动前一定备份,改动后一定验证。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分