ARTICLE DETAIL

资讯详情

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

一条简单SQL从5ms变1000ms:MySQL慢查询排查实录

一条简单SQL从5ms变1000ms:MySQL慢查询排查实录 前两周线上监控突然弹了一条告警某条 SQL 平均耗时 1023ms峰值到过 1.8 秒。我当时第一反应就是“纳尼”——这条 SQL 是典型的单表查询WHERE 条件里一个订单号、一个状态索引都建得明明白白执行计划之前看过无数遍都是 typeref明明应该 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_nostatus 上有个普通索引 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 第一反应看执行计划排除“想当然”我拉了一次 EXPLAINEXPLAIN SELECT id, order_no, status, user_id, amount FROM orders WHERE order_no SO202501150001 AND status 1;结果如下列名值tableorderstyperefpossible_keysidx_order_no, idx_statuskeyidx_order_norows1ExtraUsing wheretyperef、keyidx_order_no、rows1这个执行计划堪称完美。可越是这样越说明问题不在“这条 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则是好事表示覆盖索引不用回表。我当时第二次抓到的真实执行计划是这个typekeyrowsExtrarefidx_status3200000Using where同一个 SQL真实执行用了idx_status索引预估扫描 320 万行而 EXPLAIN 单独执行时却显示idx_order_no、rows1。这就矛盾了为什么同一个 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。这个阶段包括了扫描行、回表、组装结果集的所有动作它最耗时恰恰说明扫描行数太多了。再结合真实执行计划里 rows3200000基本可以确认问题出在“优化器选择了一个扫描大量行的索引”。同样的问题在 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 formattree看更详细的信息。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果然变回keyidx_order_no, rows1。线上单条查询耗时从 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 ServerUPDATE 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它们的高频原因集中在两个模式深分页和去重。顺手说一下因为这些模式在电商报表场景里实在太常见。深分页是指这种 SQLSELECT * 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;去重场景也有类似问题。如果对一个大表的文本字段做DISTINCTMySQL 可能需要生成临时表并排序就会非常慢。可以尝试只对必要的字段去重而不是全字段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 高频问题速查表问题现象常见原因快速验证方法处理方案有索引但 typeALL隐式转换 / 函数包裹 / 统计信息过期去掉参数引号后 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 FORMATtree、performance_schema、sys 库、pt-query-digest、Percona Toolkit。SQL ServerSET STATISTICS IO ON/TIME ON、SSMS 执行计划图形化工具、sys.dm_exec_query_stats、Missing Index 动态管理视图。OracleAWR 报告、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”的告警先别急着怀疑数据库疯了按从执行计划到统计信息到锁等待的顺序查一遍大概率能在很短时间内找到那个藏在细节里的真凶。
返回列表