MySQL、Oracle、PostgreSQL慢SQL排查,我的三板斧
干数据库运维这些年,遇到最多的一个问题就是:业务方跑过来说“系统慢了”“接口超时了”,然后一查,十有八九是慢SQL在作祟。我自己是从Oracle入的行,后来公司业务转向互联网化,又主用了好几年MySQL,这两年团队里PostgreSQL的实例也越来越多。三套数据库系统,慢SQL的排查思路既有共通之处,底层原理也完全不一样。市面上的资料要么只讲单库,要么写得过于理论,真正能落地到生产环境里解决问题的方法论反而很少。这篇文章就把我在oracle、mysql、pgsql三类数据库上排查慢SQL的实操经验做一个系统梳理,从采集、定位、分析到治理,把我踩过的坑和觉得好用的方法都摊开讲,希望能给正在被慢SQL折磨的同行一些参考。
不管你是在传统企业维护Oracle仓库,还是在互联网公司跟MySQL死磕,又或者刚接触PostgreSQL还不清楚它的排查工具链,这篇文章都适合你。开篇先把结论放在这里:慢SQL排查的本质就是回答三个问题——慢在哪一步,为什么慢,怎么让它恢复正常;而不同数据库只是回答问题的工具和语言不同。
1. 慢SQL排查:先搞清楚它为什么是个“系统性工程”
很多人把慢SQL排查理解成“打开慢查询日志,找超过阈值的SQL,然后加个索引”。这个思路不能算错,但放到生产环境里往往不够用。因为慢SQL的杀伤力不是单条查询慢,而是它会连带走慢整个实例。一条SQL如果长时间占着连接不释放,连接池会被耗尽,后续所有正常SQL都进不来;如果它做了全表扫描,会把buffer pool或者shared buffer里的热数据挤出去,其他SQL的命中率跟着下降;如果它持有锁很长时间,其他事务只能干等。所以排查慢SQL,第一件要做的事其实是理解它的连锁反应,这样你才知道为什么要争分夺秒去定位问题。
另外一个容易被忽视的点是:Oracle、MySQL、PostgreSQL三者在排查慢SQL时的思路框架虽然类似,但具体工具和底层原因差异很大。
- MySQL属于轻量级数据库,慢查询日志配合performance_schema基本是标配,由于它没有Oracle那么强大的自动诊断引擎,很多时候要依赖DBA的经验去分析执行计划。
- Oracle有完整的AWR、ASH、SQL Trace体系,还有自带的SQL调优顾问,定位问题相对容易,但它的等待事件体系非常复杂,把SQL给到执行引擎后,到底是CPU消耗高还是I/O等待多,需要进一步拆解。
- PostgreSQL比较特殊,它的优化器和MySQL、Oracle都不太一样,默认配置下对复杂查询处理得不错,但参数调整空间很大,统计信息过期或者work_mem太小,都会造成看似莫名其妙的慢查询。
所以,如果你问我排查慢SQL最重要的一步是什么,我的答案是:先搞清楚当前数据库是什么类型、版本多少、有什么可用工具,然后对症下药。别拿着一套MySQL的习惯去硬套Oracle,也别把PostgreSQL的配置项往Oracle上套,那是会出问题的。
1.1 排查前必须建立的三个基线认知
第一,业务基线。你要知道当前系统的正常水位是什么样。正常的QPS、TPS大概是多少,平均响应时间多少,慢查询数量平时有没有。没有这个基线,告警来了你分不清是突发问题还是本来就慢。我习惯在每个实例上做一份周维度的慢查询统计,把各时间段慢查询数量、平均耗时、TOP SQL记录下来。这样遇到业务方反馈“最近系统变得很慢”,翻一翻历史数据,能直接判断出波动是从什么时间开始的,幅度有多大,是不是伴随发布或定时任务流转。
第二,性能工具基线。不同版本的数据库,排查工具可能在细微处有差异。比如MySQL 5.7和8.0的performance_schema表结构不完全一样,8.0里EXPLAIN ANALYZE是实测执行,5.7只能用普通的EXPLAIN看估算行数。PostgreSQL也类似,从11到15,pg_stat_statements的字段发生过几次变化,老的查询语句里如果还写着total_time,到了新版本就会报错,因为已经改成了total_exec_time。所以平时就要把每个实例的版本、可用扩展、采集开关摸排清楚,不要等故障发生了再临时查文档。
第三,权限基线。排查慢SQL有时候需要查看其他用户的执行计划、追踪会话、甚至调整系统参数。我遇到过好几次这样的情况:告警平台推送慢SQL,结果账号权限不够,查询动态视图时没权限,还得临时走审批去申请权限,整个过程多花了大半个小时,火急火燎的。所以给负责排查的账号提前授予必要的只读权限(比如MySQL的PROCESS和SELECT权限、Oracle的SELECT_CATALOG_ROLE、PG的pg_monitor角色),并且把这些权限清单纳入账号初始化流程,这是我踩过几次坑之后总结下来的经验。
1.2 先分清“真慢”还是“假慢”,提防锁和资源瓶颈
接警后不要急着去捞SQL日志,先花一两分钟判断慢SQL是“自己慢”还是“被拖慢”。所谓“自己慢”,是SQL本身执行计划差、扫描行数多、I/O消耗大;所谓“被拖慢”,是SQL本来很快,但赶上了锁等待或者资源争抢。比如MySQL里一个简单的UPDATE突然从1毫秒变成5秒,很可能是有其他事务拿着行锁不释放,它在等锁而不是在执行。Oracle里常见的是等待enq: TX - row lock contention,PG里则是pg_locks上能看到waiting=true的会话。
这个区分非常重要,因为处理方式完全不一样。如果是SQL自身执行计划的问题,要优化索引和SQL写法;如果是锁等待,要去查谁占着锁、为什么长时间不提交,甚至可能要终止阻塞会话。我见过不少初级DBA,看到慢SQL就往执行计划上做文章,结果改了一通索引,锁的问题依旧,业务还是卡,最后才发现是上游一个未提交的事务把整张表的行锁都占住了。
这里顺便分享一个我的习惯:在排查慢SQL的开头,我会先把数据库当前的会话状态、锁等待情况、CPU和IO指标一次性捞出来。MySQL用SHOW ENGINE INNODB STATUS和performance_schema.data_lock_waits,Oracle查v$session和v$session_wait,PG查pg_stat_activity和pg_locks。把这几项过一遍,基本能判断出慢SQL的类别归属,后续排查才不至于跑偏方向。
2. 第一步:开启慢查询采集,把问题“引出来”
排查慢SQL的前提是能找到它。很多生产事故之所以发生,是因为慢查询日志压根没开,或者阈值设置得太高,等发现的时候已经积累了很久的隐患。数据库默认配置一般比较保守,不会主动记录慢SQL,需要DBA自己手动开启和调整。
不同类型数据库的采集方式差异很大,下面分三块详细讲,都是我实际使用过的配置和命令。
2.1 MySQL:从慢查询日志到performance_schema
MySQL采集慢SQL的第一入口是慢查询日志,相关核心参数是slow_query_log、slow_query_log_file和long_query_time。默认情况下slow_query_log是关闭的,long_query_time是10秒。10秒这个阈值对于绝大多数业务来说太宽松了,一个接口如果出现2秒以上的查询,用户体感已经很差了,等到10秒才记日志,黄花菜都凉了。我一般建议线上OLTP业务把long_query_time设置成1秒,如果业务对性能要求特别高,可以设置成0.5秒甚至更低。
配置方法以MySQL 8.0为例:
-- 全局开启 SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; SET GLOBAL log_queries_not_using_indexes = ON; -- 8.0中持久化参数 SET PERSIST slow_query_log = ON; SET PERSIST long_query_time = 1; SET PERSIST log_queries_not_using_indexes = ON;这里有一个坑需要提醒:long_query_time的修改对已经存在的连接不生效,新建立的连接才使用新值。如果你用同一个会话执行一条慢查询,发现日志里没记录,先别慌,多半是这个原因,重新连接一下再测试。
另外,log_queries_not_using_indexes要慎开。它的本意是记录所有没用索引的查询,但生产环境里偶尔会有一些故意做全表扫描的定时统计任务,一旦开启这个参数,会把慢查询日志刷得非常快,磁盘占用暴涨。我建议在测试环境先开启观察一段时间,确认没有大查询会被误记录,再决定要不要在生产环境长期开启。
只开日志还不够,日志文件是文本,查询起来不方便。我的做法是搭配performance_schema一起用,它可以把SQL按模板聚合,直接统计每种SQL模式的总执行次数、总耗时、平均耗时:
SELECT SCHEMA_NAME, DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT / 1000000000, 2) AS total_sec, ROUND(AVG_TIMER_WAIT / 1000000000, 2) AS avg_sec, ROUND(MAX_TIMER_WAIT / 1000000000, 2) AS max_sec FROM performance_schema.events_statements_summary_by_digest WHERE DIGEST_TEXT LIKE 'SELECT%' AND SCHEMA_NAME IS NOT NULL ORDER BY SUM_TIMER_WAIT DESC LIMIT 20;这样按总耗时排序,很容易找出累计消耗最大的SQL,而不只是单次最慢的SQL。很多慢SQL单次看起来不算慢(比如平均500毫秒),但架不住执行频率高,累计耗时才惊人。这类SQL往往才是性能瓶颈的大头。
2.2 Oracle:AWR与ASH是两条腿
Oracle排查慢SQL,最经典的入口是AWR报告。AWR(Automatic Workload Repository)会自动快照数据库的性能数据,默认每60分钟一次,保留8天。生成一份AWR报告的方式很简单,在命令行里执行:
@?/rdbms/admin/awrrpt.sql按提示选择报告类型(文本还是HTML)、天数、起止快照ID,一份报告就生成了。报告里直接看“SQL Ordered by Elapsed Time”这个模块,里面列出区间内累计消耗时间最多的SQL,点击SQL ID还能看到它的执行计划。
AWR是区间性的,适合看“历史平均趋势”,但如果是正在发生的故障,AWR的时效性就赶不上了。这时候要用ASH(Active Session History),它按秒记录活跃会话的活动。有一种比较典型的场景:某条SQL每过几分钟就卡住一次,AWR里只能看到它消耗时间很多,但看不出具体是哪个时间段卡住的。用ASH去查,能定位到具体时间点、当时的等待事件和正在执行的SQL:
SELECT sample_time, session_id, sql_id, event, wait_class, time_waited FROM v$active_session_history WHERE sample_time > SYSDATE - INTERVAL '1' HOUR AND sql_id IN (SELECT sql_id FROM v$sqlarea WHERE elapsed_time / executions > 1000000) ORDER BY sample_time;Oracle还有一个容易被忽略的入口是v$sql和v$sqlarea视图,可以直接查当前共享池里的SQL的累计执行次数、物理读、逻辑读、CPU时间和等待时间占比。如果某个SQL的逻辑读远超物理读,说明数据在内存里反复被读取而过滤条件没生效,典型的问题比如没有合适的索引导致需要把大量数据块加载进内存再过滤。
2.3 PostgreSQL:pg_stat_statements和auto_explain组合拳
PostgreSQL的慢SQL排查思路和MySQL、Oracle都有所不同。首先要明确一点:PostgreSQL默认不记录慢SQL,需要手动开启两个关键组件——pg_stat_statements扩展和auto_explain模块。
pg_stat_statements负责统计SQL的执行频率和耗时,它需要提前配置到shared_preload_libraries中,然后重启数据库实例。很多新手在这个地方容易踩坑:只执行了CREATE EXTENSION pg_stat_statements,但没改shared_preload_libraries,导致视图里没有数据或者只统计了当前会话的内容。正确做法是:
# 修改 postgresql.conf shared_preload_libraries = 'pg_stat_statements' track_io_timing = on track_functions = 'all' # 重启数据库后创建扩展 CREATE EXTENSION pg_stat_statements;然后查询就是:
SELECT queryid, query, calls, total_exec_time::numeric(10,1) AS total_ms, mean_exec_time::numeric(10,1) AS avg_ms, max_exec_time::numeric(10,1) AS max_ms, rows, shared_blks_hit, shared_blks_read FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;注意PG 13之前的版本,时间字段叫total_time和mean_time,PG 13及之后改名成total_exec_time和mean_exec_time。碰到版本迁移或者从网上抄脚本时,这是个非常容易踩的坑。
auto_explain更厉害,它不但能记录慢SQL,还能顺带记录SQL的执行计划,这是MySQL和Oracle里默认没有的便利能力。开启后,凡是超过阈值的SQL,日志里会自动带上完整的执行计划,对于事后分析特别有用:
# postgresql.conf 中追加 shared_preload_libraries = 'pg_stat_statements,auto_explain' auto_explain.log_min_duration = '1s' auto_explain.log_analyze = on auto_explain.log_buffers = on auto_explain.log_nested_statements = on我自己的习惯是auto_explain.log_min_duration设置成和慢SQL业务告警阈值一致(比如1秒),这样只要业务侧报慢,数据库日志里一定有针对这条SQL的执行计划快照,省去了事后模拟或者手工捕获的麻烦。
3. 第二步:拿到SQL之后,如何快速定位根因
慢SQL被采集到之后,真正的重头戏才开始。我的定位思路分两步走:先看执行计划,再看等待事件。执行计划决定SQL“怎么执行”,等待事件揭示SQL“卡在哪”。两者结合,才能给出准确的优化方向。
3.1 执行计划:三类数据库必须关注的重点
拿到一条慢SQL,我第一步永远是看它有没有全表扫描,以及预估行数与实际返回行数的差异。这个思路对三个数据库都一样,但看执行计划的入口和关键字有差异。
MySQL里用EXPLAIN,关键看type列:ALL是全表扫描,index是索引全扫描,range是范围扫描,ref是等值匹配,const是唯一匹配。ALL基本等于灾难片,但也不是见ALL就一定要优化,如果表只有几百行,全表扫也无所谓。还需要看rows列,它估算扫描的行数。MySQL 8.0还提供了EXPLAIN ANALYZE,可以实际执行这条SQL,给出真实的耗时、真实扫描行数和各步骤耗时占比,比普通EXPLAIN准确得多:
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = '123456' ORDER BY created_at DESC LIMIT 20;Oracle的执行计划稍微复杂一些。在生产环境里我一般用DBMS_XPLAN.DISPLAY_AWR配合AWR报告里的SQL_ID直接看历史执行计划,或者对当前正在执行的SQL用DBMS_XPLAN.DISPLAY_CURSOR。Oracle执行计划里需要重点关注的操作有:TABLE ACCESS FULL、INDEX RANGE SCAN、NESTED LOOPS、HASH JOIN、SORT ORDER BY。特别注意Rows列(基数估算)和A-Rows(实际行数)的偏差,如果估算行数和实际行数相差几十倍甚至上万倍,说明统计信息不准确或者有绑定变量窥视问题,优化器走了错误的计划。
PostgreSQL用EXPLAIN (ANALYZE, BUFFERS):
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE customer_id = '123456' ORDER BY created_at DESC LIMIT 20;PG的执行计划是树状的,从下往上读。关键看每个节点的actual time和rows,以及有没有Seq Scan。但PG和MySQL有一个显著差异:PG的Seq Scan不一定是坏事,如果表比较小,顺序扫描因为预读效率高,可能比走索引还快。所以判断PG的扫描方式是否合理,要结合表大小和limit条件来看,不能一看到Seq Scan就认为有问题。
3.2 从执行计划反推:高频慢SQL模式与优化手法
看多了慢SQL之后,你会发现绝大多数慢SQL都能归入几个固定的模式,优化手法也相对固定。
第一类是大表全表扫描。典型场景是查询条件用到了函数包裹的字段,比如WHERE DATE(created_at) = CURRENT_DATE,或者字段类型不匹配导致隐式转换。MySQL和PG都在字段上叠了函数之后,索引直接失效。解决办法是改写为范围条件,比如:
-- 慢 SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01'; -- 改成 SELECT * FROM orders WHERE created_at >= '2024-01-01 00:00:00' AND created_at < '2024-01-02 00:00:00';第二类是深分页问题。LIMIT 100000, 20这种写法,前面10万行全都要扫描然后丢弃。这个在三个数据库里都有类似问题,但处理手法不完全一样,细节我在下一个模块用一个实际案例完整展开。
第三类是多表连接顺序不合理。明明小表驱动大表会很快,优化器却选了大表驱动小表,常见原因有统计信息过期、连接条件上缺少索引、OR条件导致无法用到索引等。Oracle里可以加LEADING提示手工指定驱动表顺序,MySQL和PG也有各自的连接顺序控制方法,但核心是把统计信息弄准确,让优化器自己做对选择,而不是频繁去干预。
另外,我见过很多团队习惯“一慢就加索引”,这其实是治标不治本。索引不是万能的,写入频繁的表索引多了反而会拖慢插入和更新,而且一条SQL如果本身写法问题导致索引失效,加再多索引也白搭。我的建议是:先改写SQL,再考虑加索引,加索引之前务必用EXPLAIN验证新索引能被用上。
3.3 别忘了锁等待:SQL慢可能是“等”出来的
执行计划看起来一切正常,索引也走得很好,但SQL就是慢,这种情况十有八九是锁的问题。很多人在排查慢SQL时容易忽略锁等待,因为EXPLAIN根本看不出来。每条SQL在执行前,都要先尝试获取相关行或表的锁,一旦拿不到,就进入等待状态。等待时间一长,从应用侧看就是超时、就是慢SQL。
三大数据库查锁等待的入口各不相同,我把它们整理成一张速查表:
| 数据库 | 查询锁等待的核心视图 | 常用排查SQL |
|---|---|---|
| MySQL | performance_schema.data_lock_waits、sys.innodb_lock_waits | SELECT * FROM sys.innodb_lock_waits\G |
| Oracle | v$session、v$session_wait、v$locked_object | 查看event字段是否出现enq: TX - row lock contention、enq: TM - contention |
| PostgreSQL | pg_locks、pg_stat_activity | SELECT * FROM pg_stat_activity WHERE wait_event_type = 'Lock' |
排查锁问题的核心是先找到阻塞链的源头。MySQL里sys.innodb_lock_waits已经帮我们把阻塞方和被阻塞方关联起来了,直接看blocking_pid,定位到源头后联系业务方决定是否终止该会话。Oracle里从v$session的BLOCKING_SESSION字段找,PG里在pg_stat_activity中看blocked_by数组字段。
这里有一个非常容易忽略的情况:事务长时间不提交,锁越积越多。应用侧开启了事务,但逻辑异常导致事务没有正常提交或回滚,一直空转持有锁。所以碰到锁等待类慢SQL,除了处理眼前这条SQL,还要排查上游应用的事务管理是否存在问题,否则容易反复出现同类告警。
4. 实战复盘:一个订单翻页查询的完整优化过程
理论讲再多,不如一个完整案例来得直接。我之前负责的一个电商系统就出过一起典型的深分页慢查询问题,数据库是MySQL 8.0,业务表是一张订单主表,数据量接近2000万行。最初的几天,接口响应还正常,随着订单量上涨,列表页每翻一页,响应时间就肉眼可见地变长,OMG,到第100页之后基本就超过3秒了,用户侧开始出现大量超时反馈。
4.1 现象:页数越翻越慢,甚至超时
先从慢查询日志里捞出了这个接口对应的SQL,简化后的写法是:
SELECT * FROM orders WHERE status = 1 ORDER BY created_at DESC LIMIT 100000, 20;我第一反应是执行计划肯定有问题。果然,用EXPLAIN一看,type是ALL,全表扫描,预估扫描行数1600多万行。表上虽然有idx_status(status字段)和idx_created_at(created_at字段)两个独立索引,但优化器认为即使走索引选出所有符合条件的记录,再排序后从第100001条开始取20条,综合成本还不如全表扫描之后加文件排序来得快。于是它选择了最笨的方案:把整张表扫一遍,然后排序,再丢弃前面10万行。
这种慢的根源在于:LIMIT 100000, 20虽然最终只返回20行,但数据库必须把前10万行全部处理完才能知道从哪里开始取。越到后面的页,偏移量越大,处理的行数越多,耗时自然线性增长。
4.2 根因定位:走了全表回表,扫描行数爆炸
为了进一步确认,我又用EXPLAIN ANALYZE真实执行了一遍。输出显示:扫描行数约1653万行,全部走完花了1.9秒,其中大部分时间消耗在filesort排序上。这里还能看到一个隐藏问题:SELECT *意味着即使走索引,也还要回表取完整行数据,回表次数等于扫描行数,这个放大效应会让I/O压力倍增。
当时我把这个排查过程演示给团队里的开发同事看,大家第一反应都是“加索引”。但我和他们说,这个问题单纯加索引解决不了。因为即使加了(status, created_at)的联合索引,SQL写法不变的话,优化器要满足LIMIT 100000, 20,依然需要扫到索引的第10万条后继续取20条。索引可以避免全表扫描和回表,但不能避免“深偏移”的计算开销,翻到很深页面的时候依然会很慢。
这里有必要解释一下为什么深分页如此“顽固”:MySQL的索引组织和LIMIT语义决定了,它必须“遍历”到偏移位置才能取数,没有跳跃式访问的能力。这就像翻一本厚书,无论目录多好,你从第1页翻到第10万页总是要时间,只不过索引把每页翻动的动作变得快了一点,但次数没变少。
4.3 两套优化方案:延迟关联与游标分页
当时我们提供了两套方案,最终都验证有效,只是适用场景不同。
方案一:延迟关联(Deferred Join)。核心思路是先只查主键,因为主键在索引里,不需要回表,然后再用主键关联回原表取完整行。改写后的SQL长这样:
SELECT o.* FROM orders o INNER JOIN ( SELECT id FROM orders WHERE status = 1 ORDER BY created_at DESC LIMIT 100000, 20 ) t ON o.id = t.id ORDER BY o.created_at DESC;执行计划里比之前少了1690万行的回表操作。子查询先在索引上做排序和偏移,索引大小远小于全表,扫描速度快很多;拿到20个主键后再回原表查询,最多回表20次。实测单次查询从1.9秒降到约60毫秒,效果立竿见影。
方案二:游标分页(Keyset Pagination)。这种方式更彻底,思路是不用LIMIT偏移,而是用上次查询的最后一条记录作为游标:
-- 第一页 SELECT * FROM orders WHERE status = 1 ORDER BY created_at DESC LIMIT 20; -- 应用记录下最后一条的 created_at 和 id -- 第二页及以后 SELECT * FROM orders WHERE status = 1 AND (created_at, id) < ('2024-06-01 10:30:00', '987654') ORDER BY created_at DESC LIMIT 20;这种方法无论翻到多深,永远都是走(status, created_at, id)联合索引的范围扫描,扫描的行数恒定在“目标行数”附近,时间复杂度是O(1)。但是有一个前提:必须给(status, created_at, id)建联合索引。它和方案一的取舍在于代码改写成本较高,而且要求业务里必须有稳定且唯一的排序字段组合,如果用户在翻页期间新插入了一条符合条件的数据,游标分页可能出现“漏数据”的情况。
最终考虑到业务场景是后台管理系统的订单列表,用户翻页后看到的数据允许保持快照一致性,我们采用了方案二,并在代码里做了一个无侵入的改造。上线后翻到第200页的查询耗时稳定在30毫秒以内,慢查询告警也随之消失。
5. 慢SQL治理经验:从“救火”到“防火”
排查慢SQL、优化单条SQL当然很重要,但长期来看,真正有价值的其实是一套完整的慢SQL治理体系。我一直跟团队强调:慢SQL优化做得好,不是看你能解决多少紧急故障,而是看你能不能把问题前置,让慢SQL压根不发生或者一冒头就被发现和消灭。
5.1 我用过的几个实用排查工具和技巧
工具层面,我常用的组合是:mysqldumpslow和pt-query-digest做MySQL日志聚合分析。mysqldumpslow是MySQL自带的工具,简单但够用:
# 按平均耗时排序,取前10条 mysqldumpslow -s at -t 10 /var/log/mysql/mysql-slow.logpt-query-digest是Percona Toolkit里的利器,能自动把慢日志里结构相似的SQL归组,统计每组的执行次数、总耗时、平均耗时、响应时间分布,还能生成一份HTML报告,非常详尽。
pt-query-digest /var/log/mysql/mysql-slow.log > slow_report.txtOracle这边,除了AWR和ASH,我强烈建议配合SQL Monitor使用。Oracle 11g之后自带DBMS_SQLTUNE,可以对单条SQL做自动调优分析,给出索引建议和改写建议。它的输出虽然不是万能,但能提供一个很好的切入视角。我之前遇到过一个Oracle上非常隐蔽的慢SQL,执行计划看起来完全正常,但DBMS_SQLTUNE给出的建议是“收集一下指定表的统计信息”,照做之后SQL直接从2秒跑到20毫秒。
PostgreSQL这边,pgBadger是一个好用的日志分析工具,能自动解析PG的日志文件,生成带图表和SQL统计的HTML报告:
pgbadger /var/log/postgresql/postgresql.log -o report.html不过要注意,pgBadger依赖日志里有足够的结构化条目,所以PG的log_min_duration_statement要提前设置好,并且建议把log_line_prefix设置成包含时间、会话、数据库等信息的格式,否则很多分析维度会缺失。
实战技巧方面,我总结一个自己用得很顺手的“三板斧”流程:
- 先用聚合视图(
performance_schema.events_statements_summary_by_digest、v$sqlarea、pg_stat_statements)把所有慢SQL按总耗时排序,找出TOP N。 - 对每个TOP SQL,查看它的执行计划(分别用
EXPLAIN ANALYZE、DBMS_XPLAN、EXPLAIN (ANALYZE, BUFFERS)),找到最耗时的节点。 - 最后结合等待事件(
sys.innodb_lock_waits、v$session_wait、pg_stat_activity)确认慢的根因是计算还是锁等待,再决定优化手段。
5.2 建立慢SQL的日常巡检与告警闭环
经验证明,靠人肉定期去数据库上查慢SQL靠不住。现代运维必须建立自动化的巡检和告警。我建议做这样几件事:
第一,把慢SQL日志集中采集到日志平台,比如用Filebeat/Logstash加上Elasticsearch这样的组合,或者接入公司自有的日志服务。在日志平台上设置两个维度的告警:单条慢SQL耗时超过阈值(比如3秒),以及某个SQL模板总执行次数和平均耗时异常上升。有了历史数据和趋势图,做周报和分析会便利很多。
第二,定期对performance_schema、pg_stat_statements或Oracle的AWR历史数据做快照对比。比较典型的变化是:某条SQL过去一周日均执行1000次、平均耗时50毫秒,本周突然平均耗时达到2秒,这说明它的执行计划可能变了(统计信息更新、数据量变化、参数调整都有可能),需要立刻检查。
第三,把慢SQL治理接入到研发流程里。上线前的SQL评审、代码评审中强制要求带上EXPLAIN截图(数据量大的表必须走索引),并且建立慢SQL责任分配机制:数据库告警推送时,同时@到数据表和接口的负责人。我在团队里推行这种机制后,慢SQL的发现到解决时长从平均3天降到了4小时以内。
还有一个细节值得提醒:建索引要有规划,建完要验证。很多时候开发着急,直接在表上扔一个索引,结果执行计划压根没走,或者走到了另一个更差的路径。我要求团队每次上线索引变更,都要附带优化前后的执行计划对比和执行耗时对比,用数据说话,避免拍脑袋。
从Oracle到MySQL再到PostgreSQL,我最大的体感是:慢SQL排查的技术工具迭代了很多,但核心是培养一套思维方式——凡是慢,必有原因;凡是原因,必可定位;凡是定位,必有手段。数据库的优化器越来越智能,但也时不时出些“轴”的时候,最终还是要靠人对数据和业务的深入理解来做判断。希望这篇文章里那些踩过的坑和验证过的方法,能帮你少走一些弯路。