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}
" 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)
""".format(time=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), count=len(queries)) for q in queries: html += f""" 执行时间(秒) 扫描行数 数据库 用户 SQL预览 """ html += " {q['query_time']} {q['rows_examined']} {q['db']} {q['user_host']} {q['query_preview']}
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进行性能评估。只有将慢查询治理流程化、自动化,才能真正将数据库性能维持在健康水平。
全部0条评论
快来发表一下你的评论吧 !