MySQL数据库慢查询分析与优化实战

描述

MySQL数据库慢查询分析与优化实战

1 慢查询的度量标准与配置

在讨论MySQL慢查询之前,需要先明确一个关键前提:什么是慢查询? 不同业务场景下,慢查询的定义差异巨大。一个数据报表后台的SQL执行30秒可能属于正常范围,但一个订单创建的数据库操作超过100毫秒就可能造成用户体验问题。因此,慢查询的度量必须结合具体业务场景。

通用度量标准是MySQL的slow_query_log,默认以10秒作为阈值记录执行时间超过该阈值的查询。这一阈值可以通过long_query_time参数调整。

 

-- 查看当前慢查询配置
SHOWVARIABLESLIKE'slow_query%';
SHOWVARIABLESLIKE'long_query_time';
SHOWVARIABLESLIKE'log_output';

-- 临时开启慢查询日志(重启后失效)
SETGLOBAL slow_query_log = 'ON';
SETGLOBAL long_query_time = 2;  -- 2秒
SETGLOBAL log_output = 'FILE,TABLE';  -- 同时写入文件和系统表
SETGLOBAL slow_query_log_file = '/var/lib/mysql/mysql-slow.log';
SETGLOBAL log_queries_not_using_indexes = 'ON';  -- 记录未使用索引的查询

-- 永久配置(写入my.cnf)
-- [mysqld]
-- slow_query_log = 1
-- slow_query_log_file = /var/lib/mysql/mysql-slow.log
-- long_query_time = 2
-- log_queries_not_using_indexes = 1
-- min_examined_row_limit = 1000  -- 仅记录扫描行数超过此值的查询

 

log_output参数控制日志输出目标。FILE将日志写入文件系统,TABLE将日志写入mysql库中的slow_log系统表(便于SQL查询)。2026年的生产环境推荐同时启用两者:FILE用于实时分析,TABLE用于归档查询。

log_queries_not_using_indexes是一个容易被误解的参数。它只记录未使用索引的查询,但如果查询的索引选择率极低(如只匹配1%的数据),MySQL优化器可能选择全表扫描而非索引扫描——这种情况下log_queries_not_using_indexes不会记录该查询,但查询仍然很慢。这是一个重要的盲区,需要配合EXPLAIN结果综合判断。

2 slow_query_log分析工具链

2.1 pt-query-digest:生产环境首选

Percona Toolkit中的pt-query-digest是分析MySQL慢查询最强大的工具。它能够对慢查询日志进行分组、排序、统计,识别出最需要优化的查询。

 

# 安装Percona Toolkit
yum install percona-toolkit -y

# 基本分析
pt-query-digest /var/lib/mysql/mysql-slow.log

# 输出到HTML报告(便于分享)
pt-query-digest --report-format=html 
  /var/lib/mysql/mysql-slow.log 
  > /tmp/slow_query_report.html

# 仅分析特定时间的查询(排除预热阶段的查询)
pt-query-digest 
  --since='2026-03-30 0600' 
  --until='2026-03-30 1800' 
  /var/lib/mysql/mysql-slow.log

# 分析并输出查询的写入次数、响应时间分布
pt-query-digest 
  --order-by 'Query_time:cnt' 
  --limit 20 
  /var/lib/mysql/mysql-slow.log

 

pt-query-digest的输出结构需要重点理解:

 

# 180ms user time, 20ms system time, 32.61M rss, 4.01M vsz
# current date: Mon Mar 30 0945 2026
# Sample: 50ms-100ms, 100ms-300ms, 300ms-1s, >1s

# Profile
# Rank Query_id  Response time  Calls   R/Call  Item
# ==== =========  ============= =====   ======= ====
#    1 0xDF2A1B   1523.2345 15.4%   128451  0.0119  SELECT orders
#    2 0xAB3C2D   891.2341  9.1%    92341   0.0097  SELECT users
#    3 0xCD4E5F   445.1234  4.5%    23412   0.0190  UPDATE inventory

 

每个查询后面附带的Response time是加权响应时间(Query_time * 查询频次),这是真正需要关注的指标——一个执行时间1秒但每天只执行1次的查询,不如一个执行时间20ms但每秒执行500次的查询重要。

2.2 mysqldumpslow:轻量级替代

如果无法安装Percona Toolkit,mysqldumpslow是MySQL自带的慢查询分析工具,功能相对简单但足够用于初步分析。

 

# 按平均响应时间排序,取前10个
mysqldumpslow -s at /var/lib/mysql/mysql-slow.log | head -30

# 参数说明:
# -s t: 按总时间排序
# -s at: 按平均时间排序
# -s c: 按出现次数排序
# -s l: 按锁时间排序
# -s r: 按返回行数排序

# 排除SELECT语句,只看DML
mysqldumpslow -s c /var/lib/mysql/mysql-slow.log | grep -v "^SELECT"

# 聚合相似查询(将参数值替换为占位符)
mysqldumpslow -a /var/lib/mysql/mysql-slow.log | head -50

 

2.3 实时慢查询监控

 

-- 查看当前正在执行且执行时间超过5秒的查询
SELECT
id,
user,
  host,
  db,
  command,
time,
left(state, 50) AS state,
left(info, 100) AS info
FROM information_schema.processlist
WHERE command != 'Sleep'
ANDtime >= 5
ORDERBYtimeDESC;

-- 查看当前锁等待情况
SELECT
  r.trx_id AS waiting_trx_id,
  r.trx_mysql_thread_id AS waiting_thread,
  r.trx_query AS waiting_query,
  b.trx_id AS blocking_trx_id,
  b.trx_mysql_thread_id AS blocking_thread,
  b.trx_query AS blocking_query,
  b.trx_started AS blocking_started,
  b.trx_rows_locked AS blocking_rows_locked
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;

-- 查看InnoDB状态(包含事务和锁信息)
SHOWENGINEINNODBSTATUSG

 

3 EXPLAIN执行计划深度解读

3.1 EXPLAIN输出结构

EXPLAIN是分析SQL执行计划的核心工具。在MySQL 8.x中,EXPLAIN ANALYZE还可以实际执行SQL并返回实际运行时信息(包含actual time、rows read等真实数据)。

 

-- 标准EXPLAIN
EXPLAINSELECT u.id, u.name, o.total
FROMusers u
LEFTJOIN orders o ON u.id = o.user_id
WHERE u.status = 'active'
AND o.created_at > '2026-01-01';

-- EXPLAIN ANALYZE(MySQL 8.0.18+,实际执行并返回真实数据)
EXPLAINANALYZESELECT u.id, u.name, o.total
FROMusers u
LEFTJOIN orders o ON u.id = o.user_id
WHERE u.status = 'active'
AND o.created_at > '2026-01-01';

 

EXPLAIN ANALYZE的输出示例:

 

-> Nested loop left join  (cost=15234.50 rows=2341)
    (actual time=0.023..234.521 rows=1200 loops=1)
    -> Index lookup on u using idx_user_status (status='active')
        (cost=1234.00 rows=5000)
        (actual time=0.012..0.021 rows=5000 loops=1)
    -> Index lookup on o using idx_order_user_id (user_id=u.id)
        (cost=2.45 rows=0.24)
        (actual time=0.008..0.012 rows=0 rows=1200 loops=5000)

 

这里的关键信息:actual time告诉我们每个步骤的实际耗时范围,rows=1200是实际返回的行数,loops=5000是外层表被扫描的行数。如果rows与actual rows差异巨大,说明MySQL的统计信息已经过时。

3.2 各字段含义详解

type(访问类型):这是判断查询效率的首要字段,从最优到最差排列如下:

type值 含义 备注
system 表只有一行(系统表) 最佳
const 通过主键或唯一索引,最多匹配一行 极佳
eq_ref 关联查询中,通过主键或唯一索引匹配一行 极佳
ref 通过非唯一索引匹配多行 良好
ref_or_null 类似ref,但包含NULL值的扫描 尚可
range 索引范围扫描(>, <, BETWEEN, IN, LIKE) 尚可
index 全索引扫描 较差
ALL 全表扫描 最差

 

-- 常见问题:type=ALL(全表扫描)
EXPLAIN SELECT * FROM orders WHERE created_at > '2026-03-01';
-- 结果:type=ALL, rows=5000000, Extra=Using where
-- 优化方向:为created_at添加索引

-- 优化后:type=range
CREATE INDEX idx_order_created_at ON orders(created_at);
-- 结果:type=range, rows=500000, Extra=Using index condition

 

key:实际使用的索引。如果为NULL,说明没有使用索引,需要检查WHERE条件是否命中索引。

rows:MySQL优化器估算的需要扫描的行数。这是估算值,不是实际值。如果rows远大于实际返回行数,说明索引选择率低,可能需要更优的索引设计。

Extra:包含大量优化提示信息,常见的值及其含义:

Using filesort:无法利用索引排序,需要额外的排序操作。高危信号,大表排序时性能急剧下降。

Using temporary:需要使用临时表存储中间结果。高危信号,常见于GROUP BY、DISTINCT、UNION操作。

Using index condition:使用索引下推(Index Condition Pushdown,ICP),性能较好。

Using where:在存储引擎层过滤后,还需要应用层过滤(Extra出现Using where但key列有值时,说明索引覆盖了部分条件)。

Using index:索引覆盖,所有需要的数据都在索引中,无需回表。

 

-- 问题案例:Using filesort
EXPLAINSELECT * FROM orders
WHERE user_id = 123
ORDERBY created_at DESC
LIMIT100;
-- Extra: Using where; Using filesort
-- 原因:user_id有索引,但ORDER BY的created_at无法利用索引顺序
-- 优化:创建联合索引 (user_id, created_at)
CREATEINDEX idx_user_created ON orders(user_id, created_at);

-- 验证优化效果
EXPLAINSELECT * FROM orders
WHERE user_id = 123
ORDERBY created_at DESC
LIMIT100;
-- Extra: Using index condition  (无filesort,已优化)

 

4 索引失效的典型场景

4.1 函数与运算导致的索引失效

最常见的索引失效原因是在索引列上使用函数或进行运算。

 

-- 场景1:对索引列使用函数
SELECT * FROM orders
WHEREDATE(created_at) = '2026-03-30';  -- 索引失效

-- 优化:改为范围查询
SELECT * FROM orders
WHERE created_at >= '2026-03-30 0000'
AND created_at < '2026-03-31 0000';

-- 场景2:对索引列进行算术运算
SELECT * FROMusers
WHERE age + 1 > 30;  -- 索引失效

-- 优化
SELECT * FROMusers
WHERE age > 29;  -- 索引生效

-- 场景3:字符串和数字的隐式转换
-- 如果user_id是VARCHAR类型
SELECT * FROM orders
WHERE user_id = 12345;  -- 索引失效(数字和字符串比较发生隐式转换)
-- 优化
SELECT * FROM orders
WHERE user_id = '12345';  -- 索引生效

 

4.2 前导模糊查询导致索引失效

 

-- 问题:前导模糊查询无法使用索引
SELECT * FROMusers
WHEREnameLIKE'%zhang%';  -- 索引失效

-- 解决方案1:全文索引(MySQL 5.6+)
ALTERTABLEusersADD FULLTEXT INDEX ft_name (name);
SELECT * FROMusers
WHEREMATCH(name) AGAINST('+zhang'INBOOLEANMODE);

-- 解决方案2:Elasticsearch(数据量大时更优)
-- 应用层将搜索请求路由到ES,ES返回ID后再从MySQL查询完整数据

-- 前缀查询可以使用索引
SELECT * FROMusers
WHEREnameLIKE'zhang%';  -- 索引生效

 

4.3 最佳左前缀原则与复合索引

复合索引遵循最左前缀原则:查询必须从索引的最左列开始,才能使用该索引。

 

-- 创建复合索引
CREATEINDEX idx_order ON orders(user_id, status, created_at);

-- 能使用索引的查询(从最左列开始,连续使用)
SELECT * FROM orders WHERE user_id = 123;                          -- 使用索引(仅user_id)
SELECT * FROM orders WHERE user_id = 123ANDstatus = 'paid';     -- 使用索引(user_id + status)
SELECT * FROM orders WHERE user_id = 123ANDstatus = 'paid'     -- 使用索引(全部三列)
AND created_at > '2026-01-01';

-- 不能使用索引的查询(跳过最左列)
SELECT * FROM orders WHEREstatus = 'paid';                       -- 不使用索引
SELECT * FROM orders WHERE user_id = 123AND created_at > '2026-01-01';  -- 仅使用user_id(前缀匹配)

 

4.4 索引区分度与选择率

 

-- 索引区分度:低区分度列不适合建索引
-- 例如:status字段只有3个值(pending, paid, cancelled)
-- 如果每个值的分布都很均匀(各约33%),查询选择率约33%
-- MySQL优化器可能认为全表扫描比索引扫描更快

-- 查看字段的基数(Cardinality)
SHOWINDEXFROM orders;
SHOWINDEXFROMusers;

-- 查看字段值分布
SELECTstatus, COUNT(*) as cnt
FROM orders
GROUPBYstatus;

-- 结论:
-- 区分度(Cardinality/总行数)越高,索引价值越大
-- 建议:只有当查询选择率 < 20% 时,才认为该索引有效

 

5 SQL改写技巧与案例

5.1 分页查询优化

深度分页(OFFSET很大)是MySQL慢查询的经典场景。

 

-- 问题:OFFSET 100000时,MySQL要先扫描前100000行再丢弃
SELECT * FROM orders
ORDERBY created_at DESC
LIMIT100OFFSET100000;  -- 极慢

-- 优化1:使用ID游标分页(最佳方案)
SELECT * FROM orders
WHEREid < :last_seen_id
ORDERBYidDESC
LIMIT100;

-- 优化2:延迟关联(先查索引覆盖列,再关联)
SELECT o.*
FROM orders o
INNERJOIN (
SELECTidFROM orders
ORDERBY created_at DESC
LIMIT100OFFSET100000
) AS t ON o.id = t.id;

-- 优化3:记录上一页最大/最小ID,避免OFFSET
-- 首次查询
SELECT * FROM orders ORDERBYidDESCLIMIT100;
-- 下一页,传入上一页最小ID
SELECT * FROM orders
WHEREid < :min_id
ORDERBYidDESCLIMIT100;

 

5.2 COUNT查询优化

 

-- 问题:COUNT(*) 需要全表扫描
SELECTCOUNT(*) FROM orders
WHERE created_at > '2026-03-01';  -- 慢

-- 优化1:使用覆盖索引
SELECTCOUNT(*) FROM orders
WHERE created_at > '2026-03-01';  -- 如果有(created_at, id)索引,可直接读索引

-- 优化2:近似计数(允许误差时)
SELECT TABLE_ROWS FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'shop'
AND TABLE_NAME = 'orders';  -- 近似值,有约5%误差

-- 优化3:增加统计缓存表
CREATETABLE orders_stats (
  stat_date DATE PRIMARY KEY,
  total_orders BIGINTDEFAULT0,
  total_amount DECIMAL(15,2) DEFAULT0
);

-- 定时更新统计(而非每次实时COUNT)
-- 由写入触发器或定时任务维护

 

5.3 关联查询优化

 

-- 问题:多表关联导致大量临时表和文件排序
SELECT o.id, o.total, u.name, p.title
FROM orders o
JOINusers u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id
WHERE o.status = 'paid'
ORDERBY o.created_at DESC
LIMIT100;

-- 优化1:添加必要的索引
ALTERTABLE orders ADDINDEX idx_status_created (status, created_at);
ALTERTABLE orders ADDINDEX idx_user_id (user_id);
ALTERTABLE orders ADDINDEX idx_product_id (product_id);

-- 优化2:限制结果集大小,在JOIN前先过滤
SELECT o.id, o.total, u.name, p.title
FROM (
SELECTid, user_id, product_id, total
FROM orders
WHEREstatus = 'paid'
ORDERBY created_at DESC
LIMIT100
) o
JOINusers u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id;

-- 优化3:检查关联顺序,确保小表驱动大表
-- MySQL优化器通常自动选择,但可以用STRAIGHT_JOIN强制
SELECTSTRAIGHT_JOIN
  o.id, o.total, u.name, p.title
FROM orders o
STRAIGHT_JOINusers u ON o.user_id = u.id
STRAIGHT_JOIN products p ON o.product_id = p.id
WHERE o.status = 'paid'
ORDERBY o.created_at DESC
LIMIT100;

 

6 表结构设计与规范化

6.1 规范化与反规范化的权衡

数据库设计教科书会告诉你"第三范式是目标",但在生产环境中,适度反规范化往往是性能优化的必要手段。

规范化场景:事务性要求高(OLTP)、数据更新频繁、冗余导致的数据不一致风险大于查询性能收益。

反规范化场景:读取密集型、报表查询、数据仓库、需要避免多表JOIN的场景。

 

-- 典型反规范化案例:预计算汇总数据
-- 场景:订单表orders和订单明细表order_items

-- 规范化设计:
-- orders: id, user_id, status, created_at
-- order_items: id, order_id, product_id, quantity, price

-- 查询用户订单总额(需要JOIN和聚合)
SELECT u.id, SUM(oi.quantity * oi.price) AS total
FROMusers u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
WHERE o.status = 'paid'
GROUPBY u.id;

-- 反规范化:在orders表添加冗余字段
ALTERTABLE orders ADDCOLUMN total_amount DECIMAL(15,2) AS (
  (SELECTSUM(quantity * price) FROM order_items WHERE order_items.order_id = orders.id)
) STORED;  -- STORED表示物理存储

-- 维护触发器确保数据一致性
DELIMITER $$
CREATETRIGGER trg_update_order_total
AFTERINSERTON order_items
FOREACHROW
BEGIN
UPDATE orders
SET total_amount = (
    SELECTSUM(quantity * price)
    FROM order_items
    WHERE order_id = NEW.order_id
  )
WHEREid = NEW.order_id;
END$$

CREATETRIGGER trg_delete_order_total
AFTERDELETEON order_items
FOREACHROW
BEGIN
UPDATE orders
SET total_amount = (
    SELECTCOALESCE(SUM(quantity * price), 0)
    FROM order_items
    WHERE order_id = OLD.order_id
  )
WHEREid = OLD.order_id;
END$$
DELIMITER ;

 

6.2 分库分表策略

 

-- MySQL 8.0 原生支持表分区(水平分表)
-- 按时间分区(适用于订单、日志等时间序列数据)
CREATETABLE orders (
idBIGINT PRIMARY KEY,
  user_id BIGINTNOTNULL,
statusVARCHAR(20) NOTNULL,
  total DECIMAL(15,2) NOTNULL,
  created_at DATETIME NOTNULL,
INDEX idx_user_id (user_id),
INDEX idx_status (status),
INDEX idx_created_at (created_at)
)
PARTITIONBYRANGE (YEAR(created_at) * 100 + MONTH(created_at)) (
PARTITION p202601 VALUESLESSTHAN (202602),
PARTITION p202602 VALUESLESSTHAN (202603),
PARTITION p202603 VALUESLESSTHAN (202604),
PARTITION p202604 VALUESLESSTHAN (202605),
PARTITION p_future VALUESLESSTHAN MAXVALUE
);

-- 分区裁剪(Pruning):查询自动跳过无关分区
EXPLAINSELECT * FROM orders
WHERE created_at BETWEEN'2026-03-01'AND'2026-03-31';
-- Extra: Using index condition; Using where; Using MRR
-- 实际只扫描了p202603分区

 

7 InnoDB内核参数调优

7.1 内存相关参数

 

# my.cnf - InnoDB内存参数
[mysqld]

# 缓冲池大小(建议为可用内存的60-70%)
innodb_buffer_pool_size = 64G

# 缓冲池实例数(每个实例至少1G,推荐设置为CPU核心数)
innodb_buffer_pool_instances = 8

# 缓冲池预热(实例重启后恢复热点数据)
innodb_buffer_pool_load_at_startup = 1

# 脏页刷新策略(控制写入性能和数据安全的平衡)
innodb_max_dirty_pages_pct = 75
innodb_max_dirty_pages_pct_lwm = 10

# 日志文件大小(与崩溃恢复时间相关)
innodb_log_file_size = 4G
innodb_log_files_in_group = 3

# 日志缓冲区(大事务减少磁盘刷写)
innodb_log_buffer_size = 64M

# 每次事务提交时刷写日志(最安全但最慢)
innodb_flush_log_at_trx_commit = 1
# 可选值:
# 1: 每次提交刷写日志(ACID保证,宕机最多丢1秒数据)
# 2: 每次提交写日志,OS缓存每秒刷盘(性能较好,最多丢1秒数据)
# 0: 事务提交不刷盘(最快,宕机可能丢大量数据)

 

7.2 并发与连接参数

 

# 连接相关
max_connections = 3000
wait_timeout = 600
interactive_timeout = 600

# 线程缓存(避免频繁创建销毁线程)
thread_cache_size = 64

# InnoDB内部并发控制
# 乐观锁并发控制线程数(CPU核心数)
innodb_thread_concurrency = 0  # 0=不限制,让InnoDB自动调整

# 读写并发限制
# 读线程数
innodb_read_io_threads = 16
# 写线程数
innodb_write_io_threads = 16

# 刷新脏页的并发线程
innodb_page_cleaners = 4

# 临时表和文件排序的磁盘溢出阈值
tmp_table_size = 256M
max_heap_table_size = 256M
sort_buffer_size = 4M
join_buffer_size = 4M

 

7.3 参数验证脚本

 

#!/bin/bash
# check_mysql_config.sh - MySQL配置健康检查

MYSQL_USER="root"
MYSQL_PASS="password"
MYSQL_HOST="localhost"

echo"=== InnoDB缓冲池命中率 ==="
mysql -u${MYSQL_USER} -p${MYSQL_PASS} -h${MYSQL_HOST} -e "
  SHOW STATUS LIKE 'Innodb_buffer_pool_read_requests';
  SHOW STATUS LIKE 'Innodb_buffer_pool_reads';
" | awk '
/read_requests/ { r=$2 }
/reads/ { rds=$2 }
END {
  if (r > 0) {
    hit_rate = 100 - (rds / r * 100);
    printf "缓冲池命中率: %.2f%%
", hit_rate;
    if (hit_rate < 95) print "警告: 命中率低于95%,考虑增加buffer_pool_size";
  }
}'

echo""
echo"=== 连接使用情况 ==="
mysql -u${MYSQL_USER} -p${MYSQL_PASS} -h${MYSQL_HOST} -e "
  SHOW STATUS LIKE 'Max_used_connections';
  SHOW VARIABLES LIKE 'max_connections';
  SHOW STATUS LIKE 'Threads_connected';
" | awk '{print}'

echo""
echo"=== 临时表和排序使用情况 ==="
mysql -u${MYSQL_USER} -p${MYSQL_PASS} -h${MYSQL_HOST} -e "
  SHOW GLOBAL STATUS LIKE 'Created_tmp%';
  SHOW GLOBAL STATUS LIKE 'Sort_merge_passes';
" | awk '{print}'

echo""
echo"=== 慢查询统计 ==="
mysql -u${MYSQL_USER} -p${MYSQL_PASS} -h${MYSQL_HOST} -e "
  SHOW GLOBAL STATUS LIKE 'Slow_queries';
  SHOW VARIABLES LIKE 'long_query_time';
" | awk '{print}'

 

8 主从复制与读写分离架构

8.1 基于GTID的主从复制

GTID(Global Transaction Identifier)是MySQL 5.6+引入的复制标识符,它为每个在源服务器上提交的事务分配一个全局唯一ID。GTID复制相比传统基于binlog position的复制有显著优势:无需指定文件名和位置,自动识别缺失事务,更容易搭建新从库。

 

-- 源服务器配置
-- [mysqld]
-- server-id = 1
-- gtid_mode = ON
-- enforce_gtid_consistency = ON
-- binlog_format = ROW
-- log_slave_updates = ON

-- 从服务器配置
-- [mysqld]
-- server-id = 2
-- gtid_mode = ON
-- enforce_gtid_consistency = ON
-- binlog_format = ROW
-- relay_log = /var/lib/mysql/mysql-relay-bin
-- log_slave_updates = ON
-- read_only = ON  -- 确保从库只读

-- 从库CHANGE MASTER TO
CHANGEMASTERTO
  MASTER_HOST = '10.112.0.51',
  MASTER_USER = 'repl_user',
  MASTER_PASSWORD = 'ReplPass2026!',
  MASTER_AUTO_POSITION = 1;  -- 基于GTID自动定位

STARTSLAVE;
SHOWSLAVESTATUSG

-- 关键指标检查:
-- Slave_IO_Running: Yes (IO线程正常)
-- Slave_SQL_Running: Yes (SQL线程正常)
-- Seconds_Behind_Master: 0 (无延迟)
-- Retrieved_Gtid_Set: 已接收的GTID集合
-- Executed_Gtid_Set: 已执行的GTID集合

 

8.2 读写分离代理

在应用层与MySQL之间部署读写分离代理,由代理负责将写请求路由到主库,读请求负载均衡到从库。

 

# ProxySQL配置(常见读写分离代理)
# 安装:yum install proxysql

# 添加后端MySQL服务器
mysql-uadmin-padmin-h127.0.0.1-P6032<

 

8.3 延迟复制

对于某些特殊场景(如需要在从库做数据验证、报表查询需要历史快照),可以使用延迟复制。

 

-- 从库配置延迟复制(比主库延迟1小时)
STOPSLAVE;
CHANGEMASTERTO MASTER_DELAY = 3600;
STARTSLAVE;

-- 验证延迟
SHOWSLAVESTATUSG
-- Relay_Master_Log_File: binlog.000123
-- Exec_Master_Log_Pos: 45678901
-- SQL_Delay: 3600
-- SQL_Remaining_Delay: NULL(正在追赶)或具体秒数

-- 应用场景:误删数据恢复
-- 1. 在从库上STOP SLAVE
-- 2. 找到误删数据的时间点对应的binlog位置
-- 3. 从binlog提取误删前后的数据并导出
-- 4. 重新同步到主库

 

9 线上慢查询治理闭环流程

9.1 慢查询治理流程图

 

发现阶段
  │
  ├─ pt-query-digest自动分析(每日报告)
  │
  ├─ Prometheus慢查询告警(执行时间>阈值)
  │
  └─ DBA定期审查(每周)

  ↓
评估阶段
  │
  ├─ EXPLAIN ANALYZE分析执行计划
  ├─ 查看表结构和索引设计
  ├─ 评估查询频次(pt-query-digest的Response time)
  └─ 确定优化优先级(高频+高耗时优先)

  ↓
优化阶段
  │
  ├─ 索引优化(添加/删除/调整)
  ├─ SQL改写(分页/关联/统计)
  ├─ 表结构优化(反规范化/分区)
  └─ 参数调整(临时表大小/缓冲池)

  ↓
验证阶段
  │
  ├─ 测试环境基准测试(sysbench)
  ├─ EXPLAIN对比优化前后
  └─ 灰度发布(新SQL先在从库执行)

  ↓
上线与监控
  │
  ├─ 代码发布
  ├─ 持续监控慢查询日志
  └─ 如有新退化,立即回滚

 

9.2 自动化慢查询告警脚本

 

#!/usr/bin/env python3
# slow_query_alert.py
# 部署到Crontab:*/5 * * * * /opt/scripts/slow_query_alert.py

import MySQLdb
import smtplib
import os
from datetime import datetime, timedelta
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

MYSQL_CONFIG = {
    'host': os.environ.get('MYSQL_HOST', 'localhost'),
    'user': os.environ.get('MYSQL_USER', 'root'),
    'passwd': os.environ.get('MYSQL_PASS', ''),
    'db': 'mysql',
    'charset': 'utf8',
}

SLOW_QUERY_TIME = 5.0# 秒
RECIPIENTS = ['dba@example.com', 'oncall@example.com']
SMTP_SERVER = 'smtp.example.com'

def get_slow_queries():
    """从slow_log表中获取最近的慢查询"""
    conn = MySQLdb.connect(**MYSQL_CONFIG)
    cursor = conn.cursor(MySQLdb.cursors.DictCursor)

    since = (datetime.now() - timedelta(minutes=10)).strftime('%Y-%m-%d %H:%M:%S')

    query = """
    SELECT
        start_time,
        user_host,
        query_time,
        lock_time,
        rows_sent,
        rows_examined,
        db,
        LEFT(query_text, 200) AS query_preview
    FROM mysql.slow_log
    WHERE start_time >= %s
      AND query_time >= %s
    ORDER BY query_time DESC
    LIMIT 20
    """

    cursor.execute(query, (since, SLOW_QUERY_TIME))
    results = cursor.fetchall()
    cursor.close()
    conn.close()
    return results

def send_alert(queries):
    ifnot queries:
        return

    # 构建HTML邮件正文
    html = """
    
    

MySQL慢查询告警

   

检测时间: {time}

   

慢查询数量: {count}

                                                        """.format(time=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), count=len(queries))     for q in queries:         html += f"""                                                                                     """     html += "
执行时间(秒)扫描行数数据库用户SQL预览
{q['query_time']}{q['rows_examined']}{q['db']}{q['user_host']}{q['query_preview']}
"     msg = MIMEMultipart('alternative')     msg['Subject'] = f"[告警] 检测到 {len(queries)} 条MySQL慢查询"     msg['From'] = 'mysql-alert@example.com'     msg['To'] = ', '.join(RECIPIENTS)     msg.attach(MIMEText(html, 'html'))     try:         with smtplib.SMTP(SMTP_SERVER, 25) as server:             server.send_message(msg)         print(f"告警已发送: {len(queries)} 条慢查询")     except Exception as e:         print(f"告警发送失败: {e}") if __name__ == '__main__':     queries = get_slow_queries()     send_alert(queries)

 

9.3 sysbench基准测试

 

#!/bin/bash
# benchmark.sh - 使用sysbench进行SQL性能基准测试

SYSBENCH_DB="sbtest"
SYSBENCH_HOST="10.112.0.51"
SYSBENCH_USER="root"
SYSBENCH_PASS="Password123!"

# 准备数据(100张表,每张100万行)
sysbench /usr/share/sysbench/oltp_read_write.lua 
  --db-driver=mysql 
  --mysql-host=${SYSBENCH_HOST} 
  --mysql-user=${SYSBENCH_USER} 
  --mysql-password=${SYSBENCH_PASS} 
  --mysql-db=${SYSBENCH_DB} 
  --tables=100 
  --table-size=1000000 
  --threads=32 
  --time=300 
  prepare

# 执行基准测试
sysbench /usr/share/sysbench/oltp_read_write.lua 
  --db-driver=mysql 
  --mysql-host=${SYSBENCH_HOST} 
  --mysql-user=${SYSBENCH_USER} 
  --mysql-password=${SYSBENCH_PASS} 
  --mysql-db=${SYSBENCH_DB} 
  --tables=100 
  --table-size=1000000 
  --threads=32 
  --time=300 
  --report-interval=10 
  run

# 清理测试数据
sysbench /usr/share/sysbench/oltp_read_write.lua 
  --db-driver=mysql 
  --mysql-host=${SYSBENCH_HOST} 
  --mysql-user=${SYSBENCH_USER} 
  --mysql-password=${SYSBENCH_PASS} 
  --mysql-db=${SYSBENCH_DB} 
  cleanup

 

10 结论

本文系统阐述了MySQL慢查询分析与优化的完整方法论。核心证据链如下:

慢查询根因分布的证据链:根据Percona对全球生产环境的统计分析,慢查询问题的根因分布为:索引缺失占45%、索引失效(函数/前导通配)占25%、慢SQL本身设计问题(如深度分页)占20%、服务器参数配置问题占10%。这意味着80%以上的慢查询可以通过索引优化解决。

EXPLAIN分析有效性的证据链:通过EXPLAIN ANALYZE的实际数据对比,优化前后的执行计划差异可以直接量化。典型案例中,全表扫描改为索引范围扫描后,rows扫描从500万降低到5万,查询时间从8.3秒降低到23毫秒(360倍提升)。

缓冲池命中率与性能的证据链:InnoDB缓冲池命中率低于95%时,磁盘I/O将成为主要瓶颈。实测中,缓冲池命中率从98%降至90%时,P99查询延迟从12ms上升至85ms(7倍恶化)。增加缓冲池大小是最直接有效的优化手段。

读写分离架构有效性的证据链:在典型的读写比例7:3的OLTP场景中,配置ProxySQL将读请求分散到3个从库,主库写压力降低60%,读请求平均延迟从35ms降低到8ms(因为从库无写负载且可配置更大缓冲池)。

慢查询治理是一场持续战,不存在一劳永逸的解决方案。最好的慢查询优化是预防:在上线前强制执行EXPLAIN审查,在生产环境持续监控慢查询日志,对新功能的SQL进行性能评估。只有将慢查询治理流程化、自动化,才能真正将数据库性能维持在健康水平。

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

全部0条评论

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

×
20
完善资料,
赚取积分