背景与适用场景
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,再分析执行计划,最后调整配置和索引。改动前一定备份,改动后一定验证。
全部0条评论
快来发表一下你的评论吧 !