0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

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

马哥Linux运维 来源:马哥Linux运维 2026-04-02 09:38 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

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

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

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

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

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

-- 临时开启慢查询日志(重启后失效)
SETGLOBALslow_query_log ='ON';
SETGLOBALlong_query_time =2; -- 2秒
SETGLOBALlog_output ='FILE,TABLE'; -- 同时写入文件和系统表
SETGLOBALslow_query_log_file ='/var/lib/mysql/mysql-slow.log';
SETGLOBALlog_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'
 --limit20 
 /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)ASstate,
left(info,100)ASinfo
FROMinformation_schema.processlist
WHEREcommand !='Sleep'
ANDtime>=5
ORDERBYtimeDESC;

-- 查看当前锁等待情况
SELECT
 r.trx_idASwaiting_trx_id,
 r.trx_mysql_thread_idASwaiting_thread,
 r.trx_queryASwaiting_query,
 b.trx_idASblocking_trx_id,
 b.trx_mysql_thread_idASblocking_thread,
 b.trx_queryASblocking_query,
 b.trx_startedASblocking_started,
 b.trx_rows_lockedASblocking_rows_locked
FROMinformation_schema.innodb_lock_waits w
JOINinformation_schema.innodb_trx bONb.trx_id = w.blocking_trx_id
JOINinformation_schema.innodb_trx rONr.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
EXPLAINSELECTu.id, u.name, o.total
FROMusersu
LEFTJOINorders oONu.id = o.user_id
WHEREu.status ='active'
ANDo.created_at >'2026-01-01';

-- EXPLAIN ANALYZE(MySQL 8.0.18+,实际执行并返回真实数据)
EXPLAINANALYZESELECTu.id, u.name, o.total
FROMusersu
LEFTJOINorders oONu.id = o.user_id
WHEREu.status ='active'
ANDo.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(全表扫描)
EXPLAINSELECT*FROMordersWHEREcreated_at >'2026-03-01';
-- 结果:type=ALL, rows=5000000, Extra=Using where
-- 优化方向:为created_at添加索引

-- 优化后:type=range
CREATEINDEXidx_order_created_atONorders(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*FROMorders
WHEREuser_id =123
ORDERBYcreated_atDESC
LIMIT100;
-- Extra: Using where; Using filesort
-- 原因:user_id有索引,但ORDER BY的created_at无法利用索引顺序
-- 优化:创建联合索引 (user_id, created_at)
CREATEINDEXidx_user_createdONorders(user_id, created_at);

-- 验证优化效果
EXPLAINSELECT*FROMorders
WHEREuser_id =123
ORDERBYcreated_atDESC
LIMIT100;
-- Extra: Using index condition (无filesort,已优化)

4 索引失效的典型场景

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

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

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

-- 优化:改为范围查询
SELECT*FROMorders
WHEREcreated_at >='2026-03-30 0000'
ANDcreated_at < '2026-03-31 0000';

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

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

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

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

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

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

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

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

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

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

-- 创建复合索引
CREATEINDEXidx_orderONorders(user_id,status, created_at);

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

-- 不能使用索引的查询(跳过最左列)
SELECT*FROMordersWHEREstatus='paid';           -- 不使用索引
SELECT*FROMordersWHEREuser_id =123ANDcreated_at >'2026-01-01'; -- 仅使用user_id(前缀匹配)

4.4 索引区分度与选择率

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

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

-- 查看字段值分布
SELECTstatus,COUNT(*)ascnt
FROMorders
GROUPBYstatus;

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

5 SQL改写技巧与案例

5.1 分页查询优化

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

-- 问题:OFFSET 100000时,MySQL要先扫描前100000行再丢弃
SELECT*FROMorders
ORDERBYcreated_atDESC
LIMIT100OFFSET100000; -- 极慢

-- 优化1:使用ID游标分页(最佳方案)
SELECT*FROMorders
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(*)FROMorders
WHEREcreated_at >'2026-03-01'; -- 慢

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

-- 优化2:近似计数(允许误差时)
SELECTTABLE_ROWSFROMinformation_schema.TABLES
WHERETABLE_SCHEMA ='shop'
ANDTABLE_NAME ='orders'; -- 近似值,有约5%误差

-- 优化3:增加统计缓存表
CREATETABLEorders_stats (
 stat_dateDATEPRIMARYKEY,
 total_ordersBIGINTDEFAULT0,
 total_amountDECIMAL(15,2)DEFAULT0
);

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

5.3 关联查询优化

-- 问题:多表关联导致大量临时表和文件排序
SELECTo.id, o.total, u.name, p.title
FROMorders o
JOINusersuONo.user_id = u.id
JOINproducts pONo.product_id = p.id
WHEREo.status ='paid'
ORDERBYo.created_atDESC
LIMIT100;

-- 优化1:添加必要的索引
ALTERTABLEordersADDINDEXidx_status_created (status, created_at);
ALTERTABLEordersADDINDEXidx_user_id (user_id);
ALTERTABLEordersADDINDEXidx_product_id (product_id);

-- 优化2:限制结果集大小,在JOIN前先过滤
SELECTo.id, o.total, u.name, p.title
FROM(
SELECTid, user_id, product_id, total
FROMorders
WHEREstatus='paid'
ORDERBYcreated_atDESC
LIMIT100
) o
JOINusersuONo.user_id = u.id
JOINproducts pONo.product_id = p.id;

-- 优化3:检查关联顺序,确保小表驱动大表
-- MySQL优化器通常自动选择,但可以用STRAIGHT_JOIN强制
SELECTSTRAIGHT_JOIN
 o.id, o.total, u.name, p.title
FROMorders o
STRAIGHT_JOINusersuONo.user_id = u.id
STRAIGHT_JOINproducts pONo.product_id = p.id
WHEREo.status ='paid'
ORDERBYo.created_atDESC
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和聚合)
SELECTu.id,SUM(oi.quantity * oi.price)AStotal
FROMusersu
JOINorders oONu.id = o.user_id
JOINorder_items oiONo.id = oi.order_id
WHEREo.status ='paid'
GROUPBYu.id;

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

-- 维护触发器确保数据一致性
DELIMITER $$
CREATETRIGGERtrg_update_order_total
AFTERINSERTONorder_items
FOREACHROW
BEGIN
UPDATEorders
SETtotal_amount = (
 SELECTSUM(quantity * price)
 FROMorder_items
 WHEREorder_id = NEW.order_id
 )
WHEREid= NEW.order_id;
END$$

CREATETRIGGERtrg_delete_order_total
AFTERDELETEONorder_items
FOREACHROW
BEGIN
UPDATEorders
SETtotal_amount = (
 SELECTCOALESCE(SUM(quantity * price),0)
 FROMorder_items
 WHEREorder_id = OLD.order_id
 )
WHEREid= OLD.order_id;
END$$
DELIMITER ;

6.2 分库分表策略

-- MySQL 8.0 原生支持表分区(水平分表)
-- 按时间分区(适用于订单、日志等时间序列数据)
CREATETABLEorders (
idBIGINTPRIMARYKEY,
 user_idBIGINTNOTNULL,
statusVARCHAR(20)NOTNULL,
 totalDECIMAL(15,2)NOTNULL,
 created_at DATETIMENOTNULL,
INDEXidx_user_id (user_id),
INDEXidx_status (status),
INDEXidx_created_at (created_at)
)
PARTITIONBYRANGE(YEAR(created_at) *100+MONTH(created_at)) (
PARTITIONp202601VALUESLESSTHAN(202602),
PARTITIONp202602VALUESLESSTHAN(202603),
PARTITIONp202603VALUESLESSTHAN(202604),
PARTITIONp202604VALUESLESSTHAN(202605),
PARTITIONp_futureVALUESLESSTHANMAXVALUE
);

-- 分区裁剪(Pruning):查询自动跳过无关分区
EXPLAINSELECT*FROMorders
WHEREcreated_atBETWEEN'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;
CHANGEMASTERTOMASTER_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

importMySQLdb
importsmtplib
importos
fromdatetimeimportdatetime, timedelta
fromemail.mime.textimportMIMEText
fromemail.mime.multipartimportMIMEMultipart

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'

defget_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()
 returnresults

defsend_alert(queries):
 ifnotqueries:
   return

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

MySQL慢查询告警

检测时间: {time}

慢查询数量: {count}

""".format(time=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), count=len(queries)) forqinqueries: 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: withsmtplib.SMTP(SMTP_SERVER,25)asserver: server.send_message(msg) print(f"告警已发送:{len(queries)}条慢查询") exceptExceptionase: 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进行性能评估。只有将慢查询治理流程化、自动化,才能真正将数据库性能维持在健康水平。

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

    关注

    7

    文章

    4078

    浏览量

    68524
  • MySQL
    +关注

    关注

    1

    文章

    928

    浏览量

    29738

原文标题:MySQL数据库慢查询分析与优化实战

文章出处:【微信号:magedu-Linux,微信公众号:马哥Linux运维】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    分析一下MySQL数据库与ElasticSearch的实际应用

    就要根据用户的查找内容去构建对应检索数据的语句,这条语句往往是多表查询的,模糊查询的操作,十分耗费系统资源,更何况是在一些并发情况下,系统的性能就很低了,流程可见下图:此外,数据库处理
    发表于 06-15 17:15

    基于数据库查询过程优化设计

    在大型关系数据库管理与开发中,优化设计极大地提高数据库的性能。通过对一大型数据库查询语句执行过程的讨论,提出了对同一表格进行多个选择运算的
    发表于 02-27 16:05 18次下载

    基于语义指向性分析数据库访问查询优化设计

    基于语义指向性分析数据库访问查询优化设计_马晓珺
    发表于 01-03 17:41 0次下载

    基于Greenplum数据库查询优化

    针对分布式数据库查询效率随着数据规模的增大而降低的问题,以Greenplum分布式数据库为研究对象,从优化
    发表于 03-29 17:46 0次下载

    数据库系统概论之如何进行关系查询处理和查询优化

    本文档的主要内容详细介绍的是数据库系统概论之如何进行关系查询处理和查询优化主要内容包括了:1、关系数据库系统的
    发表于 11-15 15:12 11次下载
    <b class='flag-5'>数据库</b>系统概论之如何进行关系<b class='flag-5'>查询</b>处理和<b class='flag-5'>查询</b><b class='flag-5'>优化</b>

    MySQL数据库:理解MySQL的性能优化优化查询

    最近一直在为大家更新MySQL相关学习内容,可能有朋友不懂MySQL的重要性。在程序,语言,架构更新换代频繁的今天,MySQL 恐怕是大家使用最多的存储数据库了。由于
    的头像 发表于 07-02 17:18 3771次阅读
    <b class='flag-5'>MySQL</b><b class='flag-5'>数据库</b>:理解<b class='flag-5'>MySQL</b>的性能<b class='flag-5'>优化</b>、<b class='flag-5'>优化</b><b class='flag-5'>查询</b>

    MySQL数据库管理与应用

    MySQL数据库管理与应用 MySQL是一种广泛使用的关系型数据库管理系统,被认为是最流行和最常见的开源数据库之一。它可以被用于多种不同的应
    的头像 发表于 08-28 17:15 1925次阅读

    数据库数据恢复—MYSQL数据库ibdata1文件损坏的数据恢复案例

    mysql数据库故障: mysql数据库文件ibdata1、MYI、MYD损坏。 故障表现:1、数据库无法进行
    的头像 发表于 12-09 11:05 1378次阅读

    数据库数据恢复—Mysql数据库表记录丢失的数据恢复流程

    Mysql数据库故障: Mysql数据库表记录丢失。 Mysql数据库故障表现: 1、
    的头像 发表于 12-16 11:05 1338次阅读
    <b class='flag-5'>数据库</b><b class='flag-5'>数据</b>恢复—<b class='flag-5'>Mysql</b><b class='flag-5'>数据库</b>表记录丢失的<b class='flag-5'>数据</b>恢复流程

    MySQL数据库的安装

    MySQL数据库的安装 【一】各种数据库的端口 MySQL :3306 Redis :6379 MongoDB :27017 Django :8000 flask :5000 【二】
    的头像 发表于 01-14 11:25 1217次阅读
    <b class='flag-5'>MySQL</b><b class='flag-5'>数据库</b>的安装

    MySQL数据库是什么

    MySQL数据库是一种 开源的关系型数据库管理系统(RDBMS) ,由瑞典MySQL AB公司开发,后被Oracle公司收购。它通过结构化查询
    的头像 发表于 05-23 09:18 1419次阅读

    企业级MySQL数据库管理指南

    在当今数字化时代,MySQL作为全球最受欢迎的开源关系型数据库,承载着企业核心业务数据的存储与处理。作为数据库管理员(DBA),掌握MySQL
    的头像 发表于 07-09 09:50 864次阅读

    MySQL查询终极优化指南

    作为一名在生产环境摸爬滚打多年的运维工程师,我见过太多因为查询导致的线上故障。今天分享一套经过实战检验的MySQL
    的头像 发表于 08-13 15:55 940次阅读

    数据库查询分析与SQL优化实战技巧

    今天,我将分享我在处理数千次数据库性能问题中积累的实战经验,帮助你系统掌握查询分析与SQL优化
    的头像 发表于 09-08 09:34 1213次阅读

    MySQL查询调优指南

    MySQL查询数据库性能问题的最常见原因。当一条SQL语句执行超过1秒时,就可能影响用户体验;超过10秒时,通常会收到用户投诉;而超过30秒的
    的头像 发表于 04-09 10:01 156次阅读