☰
一条简单SQL从5ms变1000ms:MySQL慢查询排查实录
2026/10/2 3:36:23 网站建设 项目流程

前两周线上监控突然弹了一条告警:某条 SQL 平均耗时 1023ms,峰值到过 1.8 秒。我当时第一反应就是“纳尼?”——这条 SQL 是典型的单表查询,WHERE 条件里一个订单号、一个状态,索引都建得明明白白,执行计划之前看过无数遍都是 type=ref,明明应该 5ms 以内返回。结果它硬生生跑了一秒多,线上订单查询接口跟着抖,业务方直接甩了张超时截图过来。

这条 SQL 长这样:

SELECT id, order_no, status, user_id, amount FROM orders WHERE order_no = 'SO202501150001' AND status = 1;

orders 表大概 800 万行,order_no 有唯一索引 idx_order_no,status 上有个普通索引 idx_status。从任何角度看,它都不该慢。但数据库优化这行干久了就明白一件事:慢 SQL 最怕的不是那种一眼看过去就很复杂的查询,而是这种“看起来简单却突然变慢”的查询。它背后往往是隐式转换、统计信息失真、锁等待、环境抖动这些隐蔽因素在叠加。

这篇文章我就把这次排查过程完整复盘一遍,从慢查询日志、EXPLAIN、performance_schema,到索引重构和 SQL 改写,把我踩过的坑和最终验证结果都写出来。适合正在跟慢 SQL 缠斗的同学,尤其是刚接触数据库性能优化的新手——看完你能少走一大半弯路。

1. 从一条“简单SQL”说起:问题是怎么暴露的

1.1 现象描述:一条本以为不会出问题的查询

那天的告警信息其实很简短:orders表某条 SELECT 语句平均耗时 1023ms,最近 10 分钟出现次数 2000+。我打开慢查询日志一看,就是开头那条 SQL。用户侧的表现是订单列表页打开变慢,偶尔超时,但又不是完全不可用,属于那种“让人烦躁但还没到事故级别”的故障。

这种问题最坑的地方在于,第一次看代码和索引设计时,你找不到任何明显的毛病。order_no 是业务单号,有唯一索引;status 是订单状态,有普通索引;SQL 里没写SELECT *,只取了五个字段;连接池配置正常;数据库负载看着也不算高。按照常规经验,这种查询应该走idx_order_no,扫描 1 行,然后回表判断status = 1,整个过程最多几毫秒。

但实际表现就是慢。所以排查的第一步,不是去改 SQL,而是把所有“想当然”都清零,从现场数据开始看。

1.2 第一反应:看执行计划,排除“想当然”

我拉了一次 EXPLAIN:

EXPLAIN SELECT id, order_no, status, user_id, amount FROM orders WHERE order_no = 'SO202501150001' AND status = 1;

结果如下:

列名值
tableorders
typeref
possible_keysidx_order_no, idx_status
keyidx_order_no
rows1
ExtraUsing where

type=ref、key=idx_order_no、rows=1,这个执行计划堪称完美。可越是这样越说明问题不在“这条 SQL 本身的结构”,而在数据库没按我们以为的路径去走。

这里有个很容易被忽略的细节:EXPLAIN的结果是优化器根据当前统计信息估算出来的,它反映的是“优化器打算怎么做”,不一定等于“实际执行时真正做了什么”。如果统计信息过期,或者某个环节触发了索引失效,执行计划可能完全不同。我当时就在想,是不是线上实际的执行计划和这个“标准答案”不一样?

1.3 别急着动手,先把瓶颈分成三类

排查慢 SQL,我习惯先把问题归到三类里,避免像无头苍蝇一样乱试。

  • 第一类:存储引擎和 IO 层。比如数据量暴涨、索引碎片化严重、缓冲池命中率低、磁盘 IO 抖动、网络延迟变高。这类问题通常表现为“执行计划没问题,但耗时就是降不下来”。
  • 第二类:优化器选择层。比如统计信息过期、隐式转换导致索引失效、多列索引顺序不合适、参数嗅探导致执行计划不稳定。这类问题需要重点看 EXPLAIN 和实际耗时分布。
  • 第三类:锁与事务层。比如长事务持锁、批量更新阻塞、undo log 膨胀、死锁重试。这类问题用SHOW ENGINE INNODB STATUS和information_schema.innodb_trx能很快定位。

这个分类方法就像排查快递为什么送得慢:可能是仓库爆仓、可能是快递员路线规划得不对、也可能是门锁坏了根本送不进去。三者的处理方式完全不同。这条 SQL 最终被我归到了第二类,但中间也踩了第一类和第三类的坑,下面挨个说。

2. 排查过程复盘:从慢查询日志到执行计划

2.1 开启慢查询日志,找出现场

要看线上真实的慢 SQL,最直接的办法就是慢查询日志。我当时的命令很简单:

SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;

查询确认开启状态:

SHOW VARIABLES LIKE 'slow_query_log%'; SHOW VARIABLES LIKE 'long_query_time';

这里有一个经验:线上生产环境不要长时间开着全局慢查询日志,尤其是大并发业务,日志量会非常恐怖,本身也会拖累磁盘 IO。我一般是在定位窗口期临时开 1 到 2 个小时,把long_query_time设置成 1 秒,抓到现场后马上关掉。

除了慢日志,还可以用SHOW FULL PROCESSLIST看当前正在执行的会话。如果那条 SQL 在告警时刻还在跑,你直接能看到它的执行状态和耗时。我当时从慢日志里锁定了几个时间点,然后用SHOW FULL PROCESSLIST抓到一批同样 SQL 的会话,发现它们的State基本都是Sending data。这个状态其实不能说明太多,因为很多 SQL 在返回结果集的过程中都会停在这里,但它至少帮我把问题从“客户端到服务端网络”缩小到了“数据库内部执行”。

2.2 EXPLAIN 执行计划该怎么读:type、key、rows、Extra

很多同学看 EXPLAIN 只看有没有用到索引,这是远远不够的。我建议所有做数据库优化的人都把下面这张表背下来,遇到慢 SQL 时逐个字段过一遍。

type含义是否需要警惕
ALL全表扫描高。OLTP 场景下通常是大问题
index全索引扫描,扫了整个索引树一般,看扫描行数
range索引范围扫描通常可接受
ref等值匹配,返回多行通常可接受
eq_ref最多返回一行,常见于主键或唯一索引连接很好
const最多一行,主键/唯一索引等值匹配理想

真正要盯死的几列:

  • type:看访问路径。
  • key:实际用了哪个索引。如果这里显示 NULL,说明索引完全没生效。
  • rows:优化器预估要读多少行。注意是“预估”,不是实际值。
  • Extra:这里的信息量最大。出现Using temporary说明有临时表;Using filesort说明要额外排序;Using where说明存储引擎返回后又在 server 层过滤;Using index则是好事,表示覆盖索引,不用回表。

我当时第二次抓到的真实执行计划是这个:

typekeyrowsExtra
refidx_status3200000Using where

同一个 SQL,真实执行用了idx_status索引,预估扫描 320 万行,而 EXPLAIN 单独执行时却显示idx_order_no、rows=1。这就矛盾了:为什么同一个 SQL,优化器在不同时刻选择完全不同的执行路径?

实际上,线上真实的执行计划是慢查询日志里记录的、加了ANALYZE之后才看到的,或者通过 performance_schema 抓到的实际执行计划。单独执行EXPLAIN时由于传入参数的值不同、统计信息状态不同,都可能得到不同的结果。这正是慢 SQL 优化最核心的一个认知:执行计划是“动态”的,不是“静态”的。

2.3 用 profile 和 performance_schema 看真实耗时分布

EXPLAIN 只能告诉我们“打算怎么执行”,但执行过程中时间到底花在哪一步,还得靠更细的工具。

MySQL 5.7 可以用:

SET profiling = 1; SELECT * FROM orders WHERE order_no = 20250115001 AND status = 1; SHOW PROFILE FOR QUERY 1;

MySQL 8.0 中SHOW PROFILE已经废弃,但 performance_schema 完全可以顶上。我习惯查这个:

SELECT EVENT_NAME, TIMER_WAIT / 1000000000 AS time_ms FROM performance_schema.events_stages_history_long WHERE THREAD_ID = ...;

或者更直接一点,用 sys 库:

SELECT * FROM sys.statement_analysis WHERE query LIKE '%orders%'\G

当时从 profile 的结果看,耗时占比最高的是Sending data。这个阶段包括了扫描行、回表、组装结果集的所有动作,它最耗时恰恰说明扫描行数太多了。再结合真实执行计划里 rows=3200000,基本可以确认问题出在“优化器选择了一个扫描大量行的索引”。

同样的问题在 SQL Server 里可以用SET STATISTICS IO ON和SET STATISTICS TIME ON看逻辑读和 CPU 耗时;Oracle 可以用AWR或者v$SQL看执行计划和实际成本。跨数据库排查的思路完全一致:先确认访问路径,再确认耗时分布。

3. 为什么“简单SQL”会跑出 1000ms:五大隐藏元凶

3.1 索引设计坑:索引该有却没用上

先说最经典的一类原因:索引建了,但设计上存在缺陷,导致优化器用不上。

最常见的是多列索引的列顺序问题。比如很多业务表上有这样一个索引idx_status_created(status, created_at),但业务查询却是WHERE created_at >= ? AND status = ?。这种情况下,优化器只能拿created_at去做范围匹配,status的等值条件没法充分利用索引,导致扫描范围变大。

我当时检查 orders 表结构时发现,除了idx_order_no和idx_status,还有一个idx_user_status(user_id, status),表面上看没问题,但分析全部订单查询 SQL 后发现,大部分查询是WHERE user_id = ? ORDER BY created_at DESC,这个索引根本覆盖不了排序字段,所以还要filesort。索引不是你建了它就一定为你服务,设计时必须考虑查询条件和排序字段的组合。

此外,主键设计也容易出问题。如果主键用 UUID 或者随机字符串,写入时会产生大量页分裂和索引碎片。之前有个项目的主键默认值就是UUID(),数据量大之后,即使是主键查询也会因为碎片和页分裂问题从几毫秒变成几百毫秒。碎片率可以通过information_schema.tables的表数据空间和空余空间大致判断,或者用SHOW TABLE STATUS LIKE 'orders'\G查看Data_free字段。

这次排查里,idx_order_no本身没有失效,但碎片率偏高,导致回表效率打折。真正让问题爆发的,是下面这个原因。

3.2 隐式转换与函数包裹:索引如何被“跳过”

第二个大坑就是隐式转换。这个坑我几乎每年都要见几次,而且每次都伪装得很深。

那天的 SQL 看起来是:

SELECT id, order_no, status, user_id, amount FROM orders WHERE order_no = 20250115001 AND status = 1;

注意,order_no在表结构里是VARCHAR(50),但业务代码里传过来的却是整数类型。MySQL 在比较时会自动把字符串列转换成数字,也就是在order_no上执行了一个隐式的 CAST,导致idx_order_no索引完全失效。

这个规则一定要记牢:当索引列是字符串类型,而查询参数是数字类型时,MySQL 会自动把列值 CAST 成数字来比较,索引无法使用。反过来,如果索引列是数字类型,参数是字符串,通常也能被优化器处理,但也不保证百分百走索引。最稳妥的做法是让参数类型和列类型保持一致。

函数包裹也是一样的道理。比如WHERE DATE(created_at) = '2025-01-15',只要索引列被函数包住,MySQL 就不会走该字段的索引。这种场景应该改写为范围查询:

WHERE created_at >= '2025-01-15 00:00:00' AND created_at < '2025-01-16 00:00:00';

对隐式转换的判断方法很简单:把 SQL 里的参数去掉引号,如果原来的 SQL 带了引号,去引号后 EXPLAIN 变差了,就说明触发了类型转换。其实在 EXPLAIN 的Extra列里并不会直接显示隐式转换,但有些版本会把Convert相关的提示放在执行计划里,或者用explain format=tree看更详细的信息。

3.3 统计信息失真:优化器选错了执行路径

隐式转换让idx_order_no失效之后,优化器只能在idx_status和全表扫描之间做选择。正常情况下,如果status = 1只占全表的 1%,优化器肯定会选idx_status,扫描大约 8 万行,这个量级还不至于太慢。但现实是,那段时间订单状态分布发生了明显变化:大量历史订单被批量脚本刷成了status = 1,实际占比从 1% 涨到了 40% 以上。

而优化器用的统计信息还是早期采集的,它以为status = 1只有 1%。于是它选择了idx_status,实际扫描了 320 万行,再回表判断order_no = 20250115001,最终只有 1 行满足条件。这就相当于快递员按照一张一个月前的地图送件,地图上显示目的地只有几步路,实际跑过去发现目的地搬走了,来回折腾一大圈。

处理方式很直接,更新统计信息:

ANALYZE TABLE orders;

更新前耐心等等,大表的ANALYZE也可能执行几十秒。更新之后再跑一次 EXPLAIN,果然变回key=idx_order_no, rows=1。线上单条查询耗时从 1000ms 降到了 12ms 左右。

这里提醒一句:ANALYZE TABLE在 InnoDB 上通常不会阻塞读写,但会消耗一点 IO;在业务高峰期执行前最好评估一下。另外,MySQL 8.0 支持直方图,如果你有字段值分布极度不均匀的情况,可以考虑对关键列建立直方图,帮助优化器做得更准确。

3.4 行锁、间隙锁与事务等待:查询被“卡住”

第三类隐藏元凶是锁和事务。有些简单 SQL 本身执行只要几毫秒,但碰上了锁等待,就只能干等。

排查锁问题,先看当前活跃事务:

SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx ORDER BY trx_started;

再看是否有阻塞关系:

SELECT * FROM sys.innodb_lock_waits\G

在 MySQL 8.0 中,sys.innodb_lock_waits会把等待链展示得很清楚:谁持锁、谁在等、等了多久。如果发现有长事务一直没提交,并且它更新过 orders 表,那这条简单 SQL 就可能被它拖住。

还有一类容易被忽略的问题:长事务会导致 undo log 膨胀。即使没有直接的锁冲突,MVCC 在读已提交或可重复读隔离级别下,也需要根据 undo 构造旧版本数据。长事务不结束,undo log 越积越多,查询就会在回滚段扫描上浪费大量时间。我当时通过SHOW ENGINE INNODB STATUS看到了正常的锁信息,但顺道发现有一个跑了 40 分钟的报表事务,这个事务也解释了为什么那段时间整体查询性能都在下降。所以如果遇到“简单 SQL 忽然变慢”且执行计划正常,一定要去查事务列表,什么都可能藏在哪里。

3.5 环境级因素:缓冲池、网络、CPU抖动

最后一类原因是环境层面的。执行计划没问题、SQL 写得也没问题,但数据库所在实例的资源被别的东西抢走了。

我常看的几个指标:

SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';

前者是逻辑读请求,后者是物理读次数。如果物理读占比突然上升,说明 Buffer Pool 命中率下降,大量冷数据被读入内存,查询就会变慢。另一个经典场景是云上 RDS 磁盘被限流,或者 SSD 在跑日常备份和批量任务时 IOPS 被打满,这类问题看监控会更直观。

网络层也不能完全排除。如果慢查询日志记录的是数据库内部执行时间,而应用层看到的是整体超时,那就要对比应用监控和数据库监控。有一些看似慢 SQL 的告警,实际上是连接池满了、CPU 上下文切换过高,或者是应用和数据库之间的网络 RPC 延迟变大。遇到这种问题,先看同一时段 DB CPU、IO、连接数三条曲线,往往一眼就能找到真凶。

这次案例里,环境因素不是主因,但它确实把问题放大了。同一时间段有一个报表任务在跑大查询,把磁盘 IO 占掉了一部分,导致本来要 400ms 的查询被拖到了 1000ms。这让我意识到一个问题:优化慢 SQL 时不能只看单条 SQL 的局部,还要看整个实例的资源水位。

4. 优化实操:把 1000ms 压到 50ms 的具体改法

4.1 索引重构:从全表扫描到聚簇索引覆盖

这次问题的最直接修复,是让idx_order_no重新生效,并避免回表。隐式转换修复后,SQL 会走idx_order_no,但由于查询需要返回status、user_id、amount等字段,而idx_order_no是二级索引,MySQL 还得回聚簇索引拿这些字段。

如果要彻底压耗时,可以考虑覆盖索引。所谓覆盖索引,就是让 SELECT 要的字段全都包含在同一个索引里,查询时不需要回表。例如:

ALTER TABLE orders ADD INDEX idx_order_no_cover(order_no, status, user_id, amount);

变成覆盖索引后,WHERE order_no = 'SO20250115001' AND status = 1这个查询可以直接从索引里拿到所有字段。

不过要注意,索引不是越多越好。每增加一个索引,写入时都要额外维护 B+ 树,会增加写入延迟并占用磁盘空间。像这种高频查询但字段固定的 SQL,适合做覆盖索引;如果是低频报表查询,就要权衡收益和代价。我这次没有直接加覆盖索引,因为修复参数类型后,单条查询已经回到 10ms 左右,已经满足业务要求。覆盖索引只是一张“更好的牌”,但不一定每局都要打出去。

4.2 SQL 改写技巧:避免 SELECT *、统一类型、改写 OR

除了索引重构,SQL 本身的写法也值得打磨。这次问题的直接根因是隐式转换,所以第一步就是把参数类型和列类型统一:

-- 修复前(order_no 索引失效) SELECT id, order_no, status, user_id, amount FROM orders WHERE order_no = 20250115001 AND status = 1; -- 修复后 SELECT id, order_no, status, user_id, amount FROM orders WHERE order_no = 'SO20250115001' AND status = 1;

在代码层面,更应该用参数化查询,而不是手动拼字符串。这样既能从源头避免类型不一致,也能规避 SQL 注入风险。我见过太多因为字符串拼接导致的慢 SQL,也见过因为拼接引发安全问题的案例,这个问题永远不值得赌。

SQL 改写还有几个常见技巧:

  • 不要用SELECT *,只取需要的字段,减少回表和数据传输量,还能给覆盖索引创造机会。
  • WHERE status = 1 OR status = 2可以改写为WHERE status IN (1, 2)。尤其是 OR 的两侧字段不同时,优化器往往会把 path 搞得很差,甚至放弃索引合并。
  • 对数据量非常大的表,避免使用负向查询,比如WHERE status != 1,因为这种条件很难走索引,优化器经常选择全表扫。

4.3 统计信息与参数调优:给优化器“重新带路”

这次案例里,统计信息失真是放大伤害的核心因素之一。所以优化动作里必须包含统计信息更新。

MySQL 的统计信息更新命令:

ANALYZE TABLE orders;

SQL Server:

UPDATE STATISTICS orders;

Oracle 则常用:

BEGIN DBMS_STATS.GATHER_TABLE_STATS(ownname => 'APP', tabname => 'ORDERS'); END;

统计信息更新之后,执行计划已经正常。但如果你的场景里优化器仍然“固执己见”,可以用FORCE INDEX让它走指定索引,不过这是兜底方案,不是长期方案。强制索引一旦数据分布再次变化,可能让执行计划比优化器自己选的更差。正确做法是先搞清楚它为什么选错,是统计信息过期还是索引选择逻辑有问题,再对症下药。

参数调优这块要非常谨慎。比如:

SET SESSION sort_buffer_size = 8 * 1024 * 1024;

这个操作只对当前会话生效,而且sort_buffer_size配得过大,在并发场景下可能造成内存暴涨。我的建议是,除非你已经定位到明确的排序瓶颈,否则不要盲目调这些参数。数据库的性能问题多数情况下是执行路径和索引结构的问题,参数只是辅助手段。

4.4 分页、去重、窗口函数场景下的慢SQL优化

这次案例本身不是分页问题,但排查的过程中,我在慢查询日志里顺便发现了一批其他慢 SQL,它们的高频原因集中在两个模式:深分页和去重。顺手说一下,因为这些模式在电商报表场景里实在太常见。

深分页是指这种 SQL:

SELECT * FROM orders ORDER BY created_at DESC LIMIT 100000, 20;

LIMIT 的 OFFSET 越大,MySQL 需要扫描并丢弃的行就越多,100000 的偏移量可能意味着它要扫描几百万行。优化方式有两种:

  • 延迟关联:先查主键,再回表取数据。
  • 游标分页:用WHERE created_at < 上次最大值代替 OFFSET。

延迟关联示例:

SELECT o.id, o.order_no, o.status, o.amount FROM orders o JOIN ( SELECT id FROM orders ORDER BY created_at DESC LIMIT 100000, 20 ) t ON o.id = t.id ORDER BY o.created_at DESC;

去重场景也有类似问题。如果对一个大表的文本字段做DISTINCT,MySQL 可能需要生成临时表并排序,就会非常慢。可以尝试:

  • 只对必要的字段去重,而不是全字段DISTINCT。
  • 用GROUP BY配合MIN/MAX提取关键信息,减少排序范围。

窗口函数是解决“取分组内最新一条”的利器。比如每个用户的最新订单:

SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM orders ) t WHERE rn = 1;

但要注意,窗口函数通常需要全量排序,如果底层没有合适索引,数据量大时也很慢。可以在PARTITION BY user_id ORDER BY created_at DESC对应的字段上建组合索引,至少让扫描范围可控。

4.5 优化验证:before/after 对比与回归测试

所有优化动作做完之后,必须做严密验证。不能只看一两次的执行时间,因为数据库有缓存,连续执行同一条 SQL 第二次往往快很多。

正确的验证方式是同一查询条件执行 5 到 10 次,取 P95 或者取多次平均,避免缓存带来的假象。我当时验证数据大致如下:

阶段访问路径预估扫描行数平均耗时(5次)
优化前idx_status 回表32000001023ms
修复隐式转换后idx_order_no 回表138ms
更新统计信息后idx_order_no 回表112ms
加覆盖索引(测试)idx_order_no_cover15ms

还要做功能回归测试:查询结果集必须和修复前完全一致,包括字段内容、排序顺序、NULL 值处理等。数据量对比可以用COUNT(*)、金额汇总、最新订单号抽查等方式。

上线前一定要做变更评审。如果决定加索引,要评估对INSERT/UPDATE/DELETE的影响;如果在生产直接跑ANALYZE TABLE,要错开业务高峰。所有的 DDL 变更都建议先在一台从库或者低峰期执行,确认无误后再上主库。

5. 实用速查:慢SQL常见问题与排查清单

5.1 高频问题速查表

问题现象常见原因快速验证方法处理方案
有索引但 type=ALL隐式转换 / 函数包裹 / 统计信息过期去掉参数引号后 EXPLAIN 对比统一参数类型、禁止函数包裹、ANALYZE TABLE
走错索引,rows 异常大统计信息过期 / 数据分布不均查看实际执行计划和真实 rows更新统计信息或建直方图
执行计划好但耗时高锁等待 / undo 膨胀 / IO 抖动查 innodb_trx、innodb_lock_waits、IO 监控杀掉长事务、优化批量任务、排查实例资源
深分页变慢OFFSET 过大看 LIMIT 偏移量延迟关联或游标分页
SQL Server 同一条 SQL 时快时慢参数嗅探对比带固定值和带变量的执行计划使用 OPTION(RECOMPILE) 或强制参数化
DISTINCT/GROUP BY 慢临时表和文件排序EXPLAIN 显示 Using temporary/filesort减少去重字段,优化索引
网络延迟高导致应用层超时客户端和服务端 RPC 慢对比应用监控和慢查询日志排查网络链路,优化连接池

这里再加一条安全提醒:所有 SQL 优化都必须建立在参数化查询的基础上,不要为了改类型方便就在代码里拼接 SQL。拼接字符串既可能导致慢查询,也可能引入注入风险。慢 SQL 可以慢慢优化,安全问题不能等。

5.2 排查工具清单与建议阈值

不同数据库各有好用的排查工具,我整理了一份清单:

  • MySQL:慢查询日志、EXPLAIN、EXPLAIN FORMAT=tree、performance_schema、sys 库、pt-query-digest、Percona Toolkit。
  • SQL Server:SET STATISTICS IO ON/TIME ON、SSMS 执行计划图形化工具、sys.dm_exec_query_stats、Missing Index 动态管理视图。
  • Oracle:AWR 报告、v$SQL、SQL Tuning Advisor、10046 事件跟踪。

关于阈值,我给一个经验值但反对照搬。OLTP 业务里,单条简单查询 P95 超过 100ms 就应该警惕;复杂报表或批量任务超过 1 秒可以接受。慢查询日志的long_query_time默认建议 1s,如果业务对延迟敏感,可以先设置为 100ms,看一周再调回。核心是让监控系统能抓到增量问题,而不是把所有日志都存下来。

5.3 线上排障的安全注意事项

线上排查慢 SQL,最怕的是为了解决问题制造更大的问题。我给自己定了几条铁律:

  • 不在高峰期直接对超大表执行ANALYZE TABLE。如果必须做,先评估表大小,并用--sampling之类的方式控制开销。
  • 不轻易在生产加索引。先在一台从库或灰度环境验证,确认不影响写入再上。
  • 所有锁相关操作,先查清楚持锁事务的业务来源,不要盲目KILL。曾经有人杀掉了一个跑了两小时的导出任务,结果业务方那边直接断了一大片。
  • 保留好现场:慢查询日志、EXPLAIN 结果、监控截图、事务列表,这些是后续复盘和写故障报告的第一手资料。
  • 变更后准备好回滚方案。索引加错了可以 DROP,参数调大了要立刻恢复,无回滚计划的变更在线上等于裸奔。

复盘这件事,我自己的体会是,大多数“简单 SQL 突然变慢”的故障都不是单点原因,更像是一连串小问题叠加出来的结果:隐式转换让索引失效,统计信息失真让优化器选错索引,大查询抢占了 IO,最终把一个本应 10ms 的查询拖到 1000ms。排查时如果只盯着其中一个环节,很容易修完一个又一个,却始终看不到全貌。

最后分享一个我现在养成的习惯:每次上线一个新的 SQL,我都会顺手保存一份当时的执行计划和 rows 估算值,并在监控里对这几条关键 SQL 做耗时基线。这样等某天它突然变慢时,我能立刻对比“之前什么样、现在什么样、中间改了什么”。很多问题在十分钟内就能定位清楚,靠的就是这份提前准备的基线数据。

下次你再看到“纳尼,一条简单 SQL 居然超过 1000ms”的告警,先别急着怀疑数据库疯了,按从执行计划到统计信息到锁等待的顺序查一遍,大概率能在很短时间内找到那个藏在细节里的真凶。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询