ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

MySQL慢查询排查与索引优化实战:从日志到EXPLAIN的完整指南

MySQL慢查询排查与索引优化实战:从日志到EXPLAIN的完整指南 做后端开发这些年几乎每个团队都会遇到同一个场景白天正忙着监控群突然弹出数据库CPU告警或者上线新功能后接口从原来的 200ms 直接飙到 3s再一查一条慢查询 SQL 正躺在慢查询日志里轻则拖垮单个接口重则把整个库的连接池打满。MySQL 的慢查询排查和优化可能是后端开发者最该掌握、也最常被问到的基本功之一。这篇文章我想从一线实战的角度把慢查询的发现、分析、定位、优化这一整条链路完整走一遍不堆概念只讲我实际用过的排查方法、EXPLAIN 各字段的实战解读以及几个真正让我“少熬夜”的索引优化经验适合刚接触性能调优的初中级开发也适合想系统梳理慢查询排查思路的同学参考。1. MySQL怎么判断一条SQL“慢”了慢查询日志的开启与调参1.1 慢查询日志的工作机制慢查询日志是 MySQL 提供的一种记录“执行时间超过阈值”SQL 的日志能力。它的核心逻辑很简单每条 SQL 执行完之后MySQL 会计算这条 SQL 实际执行所消耗的时间如果超过设定的阈值就把完整的 SQL 文本、执行时间、锁等待时间、扫描行数等信息写入日志文件。这里容易产生一个误解有人以为慢查询日志记录的只是“执行慢的 SELECT”。实际上UPDATE、DELETE、INSERT 同样会被记录。只要一条写操作执行时间超过 long_query_time它一样会进慢查询日志。我遇到过不少同学排查慢查询时只看 SELECT结果真正拖垮数据库的是一条没带 WHERE 条件的 UPDATE这种偏见非常容易导致漏判。另外要记住一点慢查询日志记录的时间只包含“执行阶段”不包含等待获取锁的时间。MySQL 里有一对很容易混淆的参数——long_query_time负责“执行时间”阈值log_slow_admin_statements控制是否记录 ALTER TABLE 等管理语句而锁等待时间则在日志的Lock_time字段中单独体现。所以如果一条 SQL 的Query_time并不高但Lock_time奇高无比那问题往往不在 SQL 本身而在锁竞争。1.2 如何开启并验证慢查询日志绝大多数 MySQL 版本默认关闭慢查询日志因为日志写入本身也有 IO 开销。在生产环境开启时我建议先查一下当前配置再决定使用动态参数还是修改配置文件。mysql SHOW VARIABLES LIKE slow_query_log%; ------------------------------------------------------- | Variable_name | Value | ------------------------------------------------------- | slow_query_log | OFF | | slow_query_log_file | /var/lib/mysql/mysql-slow.log | | long_query_time | 10.000000 | -------------------------------------------------------临时开启可以用 SET 语句这种方式在 MySQL 重启后会失效适合排查问题时使用SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SET GLOBAL log_queries_not_using_indexes ON;注意long_query_time的生效范围。设完全局变量后已经存在的连接不会立刻生效必须新建立的连接才使用新阈值。这一点我在一次线上排查时踩过坑SET GLOBAL之后查当前连接仍然显示旧值容易误以为配置没生效。如果想精确验证可以用SHOW VARIABLES和SHOW GLOBAL VARIABLES分别查看会话级和全局级参数。而永久生效的方案是在my.cnf或my.ini的[mysqld]段下配置[mysqld] slow_query_log 1 slow_query_log_file /var/log/mysql/mysql-slow.log long_query_time 1 log_queries_not_using_indexes 1配置完成后重启 MySQL 服务。long_query_time到底设置多少合适默认 10 秒在生产环境基本等于形同虚设10 秒才叫慢用户的体验早就崩了。我一般建议 OLTP 业务从 1 秒起步如果日志量过大再逐步上调到 2 秒对于核心交易链路甚至可以设置到 0.1 秒把微慢查询也暴露出来。log_queries_not_using_indexes建议一并开启它会把所有没走索引的 SQL 都记下来——哪怕执行时间不慢。这条配置的价值在于很多“现在不慢”的 SQL在全表数据量翻倍后就会变成灾难提前发现它们比事后救火更重要。1.3 另一个容易被忽略的参数min_examined_row_limit还有一个参数我强烈建议你了解一下min_examined_row_limit。它表示检查行数超过多少才记录日志。默认是 0和log_queries_not_using_indexes结合时有个副作用——只要 SQL 没走索引哪怕只查几行数据也会被记入日志导致日志文件疯狂膨胀。我遇到过最典型的场景某张只有几百行的字典表每次查询都触发“未使用索引”告警慢查询日志一个小时涨了几百 MB。这种情况下把min_examined_row_limit设置成 1000 或 10000就可以过滤掉那些“因为表太小而没走索引”的误报。毕竟扫描 1000 行对 MySQL 来说是瞬间的事不值得占用你的排查精力。所以生产环境一个比较合理的组合是参数推荐值理由slow_query_logON开启核心日志能力long_query_time11秒以上即视为慢查询log_queries_not_using_indexesON暴露潜在的全表扫描min_examined_row_limit1000过滤小表误报这套组合让我在多个项目里都能比较精准地捕捉到真正的性能问题而不是被日志噪声淹没。2. 翻慢查询日志的三种姿势从官方工具到手工提炼2.1 日志原始格式里藏着的关键信息慢查询日志开启之后如果运气不好或者说运气好你会在日志文件里看到类似这样的内容# Time: 2024-06-18T14:32:11.882345Z # UserHost: app_user[app_user] [10.10.20.33] Id: 281734 # Query_time: 2.531845 Lock_time: 0.000128 Rows_sent: 100 Rows_examined: 486512 SET timestamp1718721131; SELECT id, order_no, user_id, amount FROM t_order WHERE status PAID AND create_time BETWEEN 2024-06-01 AND 2024-06-18 ORDER BY create_time DESC LIMIT 100;这里面四行注释信息非常关键依次是执行时间、用户来源和连接 ID、执行耗时与锁等待耗时与返回行数和扫描行数、以及这条 SQL 实际执行的时间戳。第四行的SET timestamp很重要因为日志里记录的 SQL 文本不含具体时间参数有了这个时间戳你才能还原 SQL 执行时的时间上下文尤其是在排查某段时间内的异常波动时。Rows_examined和Rows_sent的对比是判断 SQL 是否“吃力不讨好”的核心指标。上面这个例子里扫描了 48 万行最终只返回 100 行这种 SQL 有着巨大的优化空间。而如果Rows_examined只有几百行但Query_time却很高那问题更可能出在锁等待、网络延迟或大事务上这时候继续优化 SQL 本身方向就错了。2.2 mysqldumpslow官方自带的分析利器当慢查询日志积累到一定量直接打开日志文件逐条看是不现实的。MySQL 官方提供了一个简单好用的统计工具——mysqldumpslow。它能把日志里的 SQL 按照执行时间、锁等待时间、扫描行数等维度做聚合统计还会自动把具体数字替换成N这样结构相同、参数不同的 SQL 就能合并归类。# 按平均查询时间倒序显示前10条 mysqldumpslow -s at -t 10 /var/log/mysql/mysql-slow.log # 按扫描行数倒序显示前20条 mysqldumpslow -s r -t 20 /var/log/mysql/mysql-slow.log参数含义-s指定排序方式c是计数、l是锁等待时间、r是返回行数、at是平均查询时间-t表示只取前 N 条。输出结果大致长这样Count: 38 Time1.82s (69s) Lock0.00s (0s) Rows99.0 (3762), app_user[app_user][10.10.20.33] SELECT id, order_no, user_id, amount FROM t_order WHERE status N AND create_time BETWEEN N AND N ORDER BY create_time DESC LIMIT N;注意Time1.82s (69s)里的两个数字前者是平均耗时括号里是总耗时。Count: 38表示同样结构的 SQL 出现了 38 次。用mysqldumpslow结合grep、sort等 shell 命令你可以在几分钟内定位出“最频繁出现的慢 SQL”和“累计耗时最长的 SQL 模式”这两类 SQL 通常是优化的第一优先级。2.3 真正强大的 pt-query-digest如果慢查询日志量大、SQL 模式多mysqldumpslow的聚合能力就显得单薄了。这时候我推荐用 Percona Toolkit 里的pt-query-digest它是我用过最顺手的慢查询分析工具。# 分析慢查询日志输出到指定文件 pt-query-digest /var/log/mysql/mysql-slow.log /tmp/slow_report.txt # 只分析最近1小时的日志 pt-query-digest --since1h /var/log/mysql/mysql-slow.log # 直接连 MySQL 从 information_schema 获取数据 pt-query-digest --typepercona /var/log/mysql/mysql-slow.logpt-query-digest的分析报告分成几大块最前面是“总体统计”包含总查询数、总耗时、最慢 SQL 的耗时占比接着是“查询模式排行”每种模式的执行次数、占比、平均耗时、响应时间占比一目了然最后还有针对每种模式的详细样本。它最厉害的一点是会自动识别同构 SQL把只有具体值不同的 SQL 归为一类并且把总耗时、平均耗时、占比计算的清清楚楚。# Profile # Rank Query ID Response time Calls R/Call V/M Item # # 1 0x1A2B3C4D5E6F7A8B 473.2112 83.2% 128 3.6970 0.01 SELECT t_order # 2 0x9E8F7A6B5C4D3E2F 45.9821 8.1% 256 0.1796 0.00 SELECT t_user看到这种输出你基本不用犹豫直接按响应时间占比从上往下处理就行。第一名的 SQL 占了 83.2% 的响应时间优化它就是在优化整个数据库的负载。成本是 Percona Toolkit 需要额外安装但在能安装的情况下我建议每个团队都备一个。2.4 没有工具时的手工排查方法有些内网环境不方便装工具或者日志量还没大到需要上工具的程度这时候用手工加 SQL 也能完成排查-- 从日志表或按时间过滤日志文件 grep Query_time mysql-slow.log | awk {print $3} | sort -rn | head -20 -- 统计慢查询频率 grep -c Query_time mysql-slow.log如果你开启了performance_schema还可以直接查相关事件表但这种场景相对少见。实际手工操作时我通常配合tail -f实时观察新产生的慢查询定位问题是否在持续发生。比如上线后新功能有问题tail日志能实时看到新慢 SQL 进来比事后分析更快定位到是哪个接口引起的。3. EXPLAIN执行计划里我最先看哪四列3.1 从一次“加个索引就好”到“EXPLAIN看哪里”的转变很多同学在优化慢 SQL 时的第一反应是“加索引”然后不管三七二十一把相关字段全建上索引。这种做法的成功率完全靠运气。正确的姿势是先用EXPLAIN查看这条 SQL 的执行计划搞清楚 MySQL 到底是怎么执行这条 SQL 的再决定优化方案。EXPLAIN SELECT id, order_no, user_id, amount FROM t_order WHERE status PAID AND create_time BETWEEN 2024-06-01 AND 2024-06-18 ORDER BY create_time DESC LIMIT 100;EXPLAIN 的输出是一张表每列代表一种执行信息。列很多但我在实际排查时最先看的四列是type、key、rows、Extra。这四列基本能讲清楚MySQL 是怎么扫描数据的、用了哪个索引、预计扫描多少行、还有没有额外的排序或临时表操作。3.2 type列数据访问方式的“速度阶梯”type是执行计划中最重要的一列它描述了 MySQL 如何访问表中的数据。从快到慢大致是system const eq_ref ref range index ALLconst按主键或唯一索引查询最多返回一行是性能最好的访问方式。eq_ref被驱动表通过主键或唯一索引等值匹配常见于 JOIN 操作中。ref通过非唯一索引等值匹配能找到多行但比全表扫描高效得多。range索引范围扫描比如BETWEEN、、、IN等。index全索引扫描遍历整棵索引树比全表扫描稍快但仍然是扫描。ALL全表扫描最差的情况通常意味着 SQL 需要重写或加索引。我在实际优化时如果看到typeALL基本就确定了“必须加索引或改写 SQL”看到typeindex说明至少索引上有覆盖的可能但要结合Extra进一步判断range和ref是多数线上查询的正常状态可接受但不代表没有优化空间比如还能加一个联合索引让rows进一步下降。3.3 rows列与Extra列成本预估和隐藏操作rows是 MySQL 估算的需要扫描的行数这个数字直接决定了一条 SQL 的“体力消耗”。优化前后对比rows的变化是最直观的效果验证方式。比如一条 SQL 的rows从 486512 降到 3120执行时间通常会从秒级降到毫秒级。Extra列里的信息同样关键。常见的危险信号有三个Using filesortMySQL 需要额外的排序操作并不一定是在磁盘上排序但肯定比用索引顺序直接读取要慢。如果排序字段没有索引ORDER BY就会触发这个操作。Using temporary执行过程中创建了临时表常见于GROUP BY、DISTINCT、UNION或子查询临时表可能在内存也可能落盘性能损失很大。Using where表示在存储引擎层拿到数据后还要在 Server 层再做一次条件过滤通常意味着索引没有完全覆盖查询条件但不一定是坏信号。一个相对理想的执行计划长这样typeref、keyidx_status_create、rows3120、Extra里没有Using filesort也没有Using temporary。3.4 用EXPLAIN对比优化前后一个直观例子拿开头那条订单查询为例。优化前它的执行计划大概是----------------------------------------------------------------------------------------------------------- | type | key | rows | Extra | ---------------------------------------------------------------------------------- | ALL | NULL | 486512 | Using where; Using filesort | ----------------------------------------------------------------------------------typeALL全表扫描keyNULL没走任何索引预计扫描 48 万行还要 filesort 排序。这种 SQL 不慢才是怪事。优化方案是在(status, create_time)上建立联合索引优化后的执行计划变成---------------------------------------------------------------------------------- | type | key | rows | Extra | ---------------------------------------------------------------------------------- | ref | idx_status_create | 3120 | Using index condition; Using filesort | ----------------------------------------------------------------------------------type从 ALL 变成 refrows从 48 万降到 3120查询快了不止一个量级。虽然Extra里仍然有Using filesort但因为扫描行数大幅减少排序代价已经可以接受。如果业务上对排序性能有更高要求可以考虑让create_time在索引里的顺序满足ORDER BY的需求进一步消除 filesort这就是索引设计的高级玩法了我在下一章仔细讲。4. 索引设计是慢SQL的“特效药”如何建一个真正有效的索引4.1 为什么索引能加速查询从B树说起索引就像书的目录没有目录时你得一页页翻完整本书才能找到目标内容这就是全表扫描有了目录你直接翻到对应章节效率自然高。MySQL InnoDB 引擎的索引底层是 B 树结构它把数据按顺序组织起来查询时通过树的高度跳跃式地找到目标位置。B 树有多快对于几百万行的表走主键索引查询通常只需要 3 到 4 层树的高度也就是 3 到 4 次磁盘 IO 就能定位到数据而全表扫描可能要读几万个数据页。理解 B 树之后你就能明白为什么“索引不是万能的”了。索引树需要额外存储空间写入数据时要同步维护索引结构所以索引建的太多太乱会把查询变快的收益吃掉一部分写性能反而下降。这也是“不要给每个字段都建索引”的根本原因。4.2 联合索引设计最左前缀原则多数慢查询优化都要用到联合索引。联合索引的匹配规则是最左前缀原则MySQL 会从联合索引的最左列开始匹配查询条件如果查询条件里没有最左列那这个联合索引基本用不上。举个例子订单表上有三个常用查询字段status订单状态、create_time创建时间、user_id用户ID。创建一个联合索引ALTER TABLE t_order ADD INDEX idx_status_create_user (status, create_time, user_id);这个索引对以下查询有效WHERE status PAID WHERE status PAID AND create_time BETWEEN ... WHERE status PAID AND create_time BETWEEN ... AND user_id 1001但对下面这个查询无效WHERE create_time BETWEEN ... -- 没带 status无法使用左前缀 WHERE user_id 1001 -- 没带 status 和 create_time列的顺序不是随便排的。我把status放最前面是因为它通常是等值条件能够直接定位到一个小的数据子集create_time排第二是因为它是范围条件放在中间能用上范围扫描user_id排最后是因为它往往用于进一步过滤。这里有个细节如果查询条件里有范围查询BETWEEN、、范围列后面的索引列无法用于等值定位只能用于覆盖或排序设计联合索引时要尽量把等值条件的列放在范围条件之前。联合索引设计的另一个关键是“尽量让查询条件覆盖更多列让 WHERE 条件里的列都能在索引树中被用于过滤”。如果你从执行计划里看到Using where而且key用的是联合索引但索引列没有包含 WHERE 里的某个条件列说明那个条件是在 Server 层做的二次过滤索引结构还有优化空间。4.3 覆盖索引和回表隐藏的性能分水岭InnoDB 的二级索引非主键索引叶子节点存储的是索引列的值加上主键值。当你通过二级索引查数据时如果查询需要的字段恰好都在索引树里就不用再根据主键回表查聚簇索引这叫覆盖索引性能非常优秀如果查询需要的字段不在索引里就必须回表每行数据都要根据主键再去聚簇索引里取一次完整行行数一多性能就会明显下降。这就是为什么我经常建议不要写SELECT *尽量只查需要的字段。字段越少越容易构建覆盖索引回表成本越低。比如t_order常用查询只需要id, order_no, status, create_time, user_id那建一个包含这些字段的联合索引查询就走覆盖索引Extra列里会出现Using index这是一个非常理想的信号。判断一段查询能否用上覆盖索引其实很直接在 EXPLAIN 的Extra列里看到Using index就表示已经覆盖只看到Using where但key列有值通常意味着回表了。回表本身不是错误但当rows很大时回表带来的随机 IO 就是性能瓶颈。此时要么精简查询字段要么把常用查询字段加入索引。4.4 索引失效的常见场景我踩过的坑下面这些场景都是我实际在线上见到过的索引失效情况。写出来给你提个醒避免踩同样的坑。函数操作导致索引失效。对索引列使用函数MySQL 很可能放弃索引。典型写法-- 即使 create_time 有索引也会失效 WHERE DATE(create_time) 2024-06-18改成范围查询就能用上索引WHERE create_time 2024-06-18 00:00:00 AND create_time 2024-06-19 00:00:00隐式类型转换导致索引失效。表里的order_no是 varchar 类型查询时却传了数值WHERE order_no 202406180001MySQL 会把字符串列转换为数值去比较索引就废了。正确写法是加引号WHERE order_no 202406180001。前导模糊查询导致索引失效。LIKE %abc这种写法因为不知道字符串开头是什么索引树无法定位起点只能全表扫描。而LIKE abc%是可以用上索引的前缀匹配能直接定位范围。条件里有关联表的隐式类型不匹配。JOIN 关联时两个表的关联字段如果一个是 varchar 一个是 bigint也可能导致索引失效。设计表结构时尽量保证关联字段类型一致。优化器判断走索引不如全表扫描。这是最容易让人困惑的情况明明建了索引EXPLAIN 却显示typeALL。原因可能是你查的数据量占表比例太高比如一张 1 万行的表要查 5000 行MySQL 优化器认为全表扫描更划算就放弃了索引。这种场景不是索引失效而是索引“不该用”。解决方向通常是从 SQL 逻辑本身着手比如分页、缩小范围条件而不是继续加索引。5. 一次真实慢查询的完整排查复盘从告警到上线5.1 线上现象告警与第一反应某天午后监控群突然弹出告警一条订单查询接口的 TP99 耗时从 300ms 飙升到 4.5s数据库 CPU 使用率同步拉高。接口对应的 SQL 大体是SELECT order_id, order_no, status, settle_time, pay_amount FROM t_order WHERE buyer_id ? AND status PAID ORDER BY settle_time DESC LIMIT 20;这个 SQL 单独看非常普通条件字段也都有索引为什么会慢这里我想强调一个排查原则不要凭直觉加索引先看数据先用 EXPLAIN 说话。我见过太多人一上来就“给 buyer_id 加索引”“给 status 加索引”结果问题没解决反而把表上的冗余索引越堆越多。正确的第一步是去数据库上真实执行一次 EXPLAIN。5.2 排查过程EXPLAIN揭示了真相实际执行 EXPLAIN 之后执行计划显示------------------------------------------------------------------------------------ | type | key | rows | Extra | ------------------------------------------------------------------------------------ | ref | idx_buyer_status | 87219 | Using index condition; Using filesort | ------------------------------------------------------------------------------------typeref索引idx_buyer_status确实用上了但rows87219一个买家竟然扫描了 8.7 万行而且Extra里有Using filesort。显然这个买家的订单量非常大通过(buyer_id, status)定位到 8.7 万行再对这 8.7 万行做settle_time排序最终只取 20 行。Using filesort在这种数据量下就是性能杀手。8.7 万行数据需要逐行读取、排序再丢弃大部分整个过程 CPU 和 IO 消耗都很大。索引虽然有两列但buyer_id和status都是等值过滤ORDER BY settle_time用不上索引顺序只能额外排序。怎么让排序也走索引答案是调整联合索引的列顺序让settle_time直接参与索引的有序性构建。5.3 优化方案联合索引列顺序的调整我把索引从(buyer_id, status)调整为(buyer_id, status, settle_time)-- 先删除旧索引 ALTER TABLE t_order DROP INDEX idx_buyer_status; -- 创建新的联合索引 ALTER TABLE t_order ADD INDEX idx_buyer_status_settle (buyer_id, status, settle_time);优化后的执行计划------------------------------------------------------------------- | type | key | rows | Extra | ------------------------------------------------------------------- | ref | idx_buyer_status_settle | 354 | Using index condition | -------------------------------------------------------------------rows从 87219 降到 354Using filesort消失只保留了Using index condition。因为settle_time已经作为索引的第三列排序字段的顺序恰好与索引顺序一致MySQL 可以直接按索引顺序读取数据取前 20 条返回完全不需要额外排序。接口 TP99 从 4.5s 降到 120ms数据库 CPU 下降明显。这个案例的价值在于慢查询优化的核心不是“有没有索引”而是“索引是否精确匹配了查询的过滤条件和排序需求”。旧的索引能过滤到 8.7 万行方向正确但不够彻底。加上settle_time后过滤和排序一次搞定效果立竿见影。5.4 复盘与举一反三这次排查给我最大的启发是一条慢 SQL 的优化不要只盯着WHERE条件还要关注ORDER BY、GROUP BY、LIMIT这些“不显眼”的语句。排序字段如果能放进联合索引消除 filesort通常会带来倍数的性能提升。之后我总结出一套通用的排查顺序先看type全表扫描直接加索引或改写 SQL再看rows行数大说明过滤性差考虑联合索引优化条件组合最后看Extra出现Using filesort或Using temporary时优先考虑通过索引调整消除它们。从那次之后我在团队里立了个规矩任何涉及慢查询的优化必须提交优化前后两份 EXPLAIN 截图。没有执行计划的“加索引”请求一律打回。这一条规则帮助我们避免了很多无效优化。6. 慢查询治理不是一次性工作如何把它变成日常机制6.1 日志的轮转与保留别让磁盘爆了慢查询日志启用后日志文件会不断增长如果不做轮转几个月下来一个日志文件铺满磁盘数据库直接没法写。我在生产环境一般用两种方案。第一种是 MySQL 自带的日志轮转机制结合log_output参数控制日志输出到 FILE 还是 TABLE。设置为 TABLE 时慢查询写入mysql.slow_log表可以方便地用 SQL 查询但表本身也会膨胀需要配合定期清理。第二种是用 Linux 的logrotate工具做文件轮转。配置示例/var/log/mysql/mysql-slow.log { daily rotate 14 compress missingok notifempty copytruncate }copytruncate很关键它会先复制日志文件再清空原文件避免因移动文件导致 MySQL 继续往旧 inode 写入的问题。轮转策略的核心就一句话日志必须能自动压缩、自动删除不能等着人去手动清。6.2 定期巡检与监控告警把问题拦在爆发之前慢查询的治理不能总靠上线后出了问题再排查。更成熟的做法是建立主动巡检机制。我自己常用的做法是每天凌晨用pt-query-digest分析前一天的慢查询日志生成日报发到团队群里重点看两件事——有没有新出现的 SQL 模式以及已知慢 SQL 的执行次数和平均耗时有没有恶化。日报的价值在于“趋势可见”很多隐患会在趋势里提前暴露。监控告警方面如果用的云数据库通常自带慢查询监控能力如果是自建库可以写一个定时脚本用pt-query-digest --since5m扫描最近 5 分钟的慢查询当某个 SQL 模式在短时间内出现频率超过阈值时触发告警。告警比日报更重要它能让你在用户还没感知到问题之前就介入处理。这里要特别提醒一点监控和告警是手段不是目的。不要为了“有监控”而把告警阈值调得过于敏感否则会变成告警轰炸团队很快会麻木。告警的精髓是少而准宁可漏掉几个不重要的也不要让重要告警被噪声淹没。6.3 开发侧的SQL规范新慢SQL的预防从源头开始等到慢查询出现了再优化永远是“救火”。真正的高效做法是在开发阶段就防止慢 SQL 产生。我参与制定过团队内部的 SQL 规范核心就几条禁止SELECT *只查询需要的字段方便构建覆盖索引。UPDATE/DELETE 必须带 WHERE或至少在事务前先 SELECT 确认影响行数。大表分页查询必须用游标或限时扫描LIMIT 100000, 20这种深分页在大表上是性能炸弹。重要的统计类查询避开高峰期比如报表统计、数据归档安排在凌晨执行。代码审查同步审查 SQL不是只看逻辑正确性还要看执行计划。条件允许的话在测试环境用真实数据量下的 EXPLAIN 检查一次。规范能不能落地主要看工具和流程。我在一个项目中推动过把“慢查询日志接入 CI/CD 流水线”作为上线检查项之一。每次代码发布前自动化跑一轮接口测试然后把生成的慢查询日志丢给pt-query-digest分析如果发现新引入的慢 SQL流水线直接失败。这个方案看起来有点重但一旦跑起来团队里新出现慢 SQL 的速度会肉眼可见地下降。6.4 一个高效的日常机制慢查询“周会”复盘最后分享一个我很推荐的做法如果团队的线上慢查询比较多建议每两周开一次 30 分钟的“慢查询复盘会”。会议流程很简单从pt-query-digest的报告中挑 TOP 5 的 SQL 模式逐个讨论这条 SQL 是哪个业务场景产生的它变慢的根本原因是什么索引数据量SQL 写法优化方案是什么谁负责什么时间上线优化后如何验证效果听起来有点像“开小会”但它带来的收益非常直接团队对线上数据量的感知会变得很敏锐哪些表在膨胀、哪些查询模式开始顶不住大家心里都有数。很多问题在形成事故之前就会在这种低压力、高信息量的复盘中提前暴露。慢查询排查优化说到底是个循环发现慢 SQL 是关键前提通过日志和工具准确地捞出来分析慢 SQL 是核心环节用 EXPLAIN 找到根因优化慢 SQL 是技术手段索引设计和 SQL 改写双管齐下而建机制才是长期方案让问题不反复、不扩大。这套链路我执行了几年最大的体会是优化一条慢 SQL 很容易难的是让团队每个人都养成“先看执行计划再谈加索引”的习惯。如果你现在正被慢查询困扰不妨先从打开慢查询日志开始让问题先暴露出来后面的一切才有针对性。
返回列表