ARTICLE DETAIL

资讯详情

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

Oracle、MySQL、PostgreSQL 慢 SQL 排查与执行计划优化实战指南

Oracle、MySQL、PostgreSQL 慢 SQL 排查与执行计划优化实战指南 做后台开发这些年我打交道最多的数据库就是 Oracle、MySQL 和 PostgreSQL。这三家构成了绝大多数业务系统的数据底座而“慢 SQL 排查”可以说是每个后端和 DBA 都绕不开的日常噩梦——尤其是当你管着不止一套库的时候。同一个慢 SQL在 Oracle 里可能是一句 AWR 报告里的 SQL_ID在 MySQL 里是慢查询日志里的一条 long_time在 PostgreSQL 里又变成了 pg_stat_statements 里的一行统计。很多人一上来就翻日志翻完却不知道下一步该干啥还有人只知道看执行计划却不知道不同数据库的执行计划根本不能拿同一套逻辑去套。这篇文章我就从自己实际排查过的问题出发把三种数据库各自的定位手段、执行计划解读、常见病根以及排查思路完整捋一遍尽量让每个环节都能直接照着做。1. 先把慢 SQL“捞”出来三种数据库的定位手段差异1.1 Oracle——AWR、ASH 与 v$SQLOracle 的慢 SQL 排查入口其实很多但最常用的还是 AWR、ASH 和 v$SQL 这老几样。AWR 报告是 Oracle 自动负载信息库的快照分析结果它会把某个时间段内的 Top SQL、等待事件、表空间 IO 都汇总出来。生产环境出了慢 SQL我一般先找人帮我生成一份最近 1 小时的 AWR 报告看 Top SQL 里有没有执行时间突然暴涨的语句。如果 Statistic Level 是 TYPICAL 或者 ALLAWR 默认每小时拍一次快照你连手工快照都不用拍直接用脚本生成报告就行。手工生成也很简单sqlplus / as sysdba ?/rdbms/admin/awrrpt.sql跑完之后会让你选天数、起始快照 ID 和结束快照 ID按提示输入即可。生成的 HTML 报告我优先看两个部分SQL Statistics 里的“SQL ordered by Elapsed Time”和“SQL ordered by CPU Time”。前者能帮你抓住总耗时最高的 SQL后者能帮你区分到底是 CPU 消耗型还是 IO 等待型。AWR 是宏观快照适合找“过去一段时间哪条 SQL 最慢”。但如果你要处理的是“现在正在发生”的慢 SQL那更合适的是查 v$SQL / v$SQLAREA。它记录的是 SQL 在共享池里的累计执行情况内容实时更新。我常用的定位语句长这样SELECT SQL_ID, ROUND(ELAPSED_TIME / 1000000, 2) AS elapsed_secs, EXECUTIONS, ROUND(ELAPSED_TIME / 1000000 / NULLIF(EXECUTIONS, 0), 4) AS avg_secs, BUFFER_GETS, DISK_READS, SUBSTR(SQL_TEXT, 1, 100) AS sql_text FROM v$SQL WHERE EXECUTIONS 0 ORDER BY avg_secs DESC FETCH FIRST 20 ROWS ONLY;注意FETCH FIRST 是 12c 之后的语法如果是 11g 或者更早的老库就把 ORDER BY 那个子查询包一层再用 ROWNUM 截前 20 条。这里我一般按单次平均耗时排序而不是按总耗时排序。理由很简单总耗时高可能是被高频执行堆出来的单次平均耗时高才说明这条 SQL 本身有问题。如果一条 SQL 每天执行几万次每次 10 毫秒总耗时也很吓人但它未必需要你操心反过来一条 SQL 每次 3 秒一天只跑 20 次虽然总量不大但只要触发了就是故障。两者关注点不一样排查思路也不一样。ASHActive Session History数据在 v$ACTIVE_SESSION_HISTORY 视图里它按秒采样能还原出某个时间点有哪些会话在跑什么 SQL、在等什么事件。排查“某天 14:30 突然数据库整体变慢”这种问题时ASH 是比 AWR 更细粒度的证据来源。1.2 MySQL——慢查询日志与 performance_schemaMySQL 的慢 SQL 定位比 Oracle 直观得多核心就是慢查询日志。只要在配置里打开开关超过阈值的 SQL 都会被记录下来。建议线上至少把 long_query_time 设成 1 秒如果业务还有余量甚至可以设 0.5 秒。我之前遇到过不少团队把 long_query_time 默认的 10 秒留着不管结果 2 秒的 SQL 跑了好几个月数据库负载上来了还没人知道这就属于“日志开了等于没开”。相关配置如下slow_query_log 1 slow_query_log_file /data/mysql/log/slow.log long_query_time 1 log_queries_not_using_indexes 1log_queries_not_using_indexes 建议顺手开上它会把没有走索引的查询也记进慢日志初筛阶段特别有用。改完配置记得重启 MySQL 或者用 SET GLOBAL 动态打开SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;日志文件会越攒越大建议直接搭一个分析流程。工具没有特殊要求MySQL 自带的 mysqldumpslow 够用第三方生态里 Percona Toolkit 的 pt-query-digest 更强大。pt-query-digest 出来的报告会按总执行时间、平均执行时间、响应时间占比给你排好还能看到每条 SQL 的平均行数扫描、锁等待时间、临时表使用情况。它的输出长这样# Profile # Rank Query ID Response time Calls R/Call V/M Item # # 1 0x1A2B3C4D... 3124.1234s 1024 3.0512s 0.10 SELECT t_order ...R/Call 是平均响应时间V/M 是响应时间的变异系数和命中率的综合指标。我一般只关心 Rank 前 10 的条目后面的基本没优化价值。如果 MySQL 版本是 5.7慢日志还可以写进 mysql.slow_log 表用 SQL 查询比 grep 文件方便不过要注意表引擎默认是 CSV查询性能一般日志量大时建议转成 MyISAM 或者定期清理。除了慢日志performance_schema 里的 events_statements_summary_by_digest 也很有用。它是按归一化后的 SQL 文本聚合的统计表也就是把参数值帮你去掉只保留 SQL 模板。查它最大的好处是能把成千上万条参数不同的相似 SQL 汇总成一条模板直接看 TOP N。SELECT SCHEMA_NAME, DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT / 1e12, 2) AS total_secs, ROUND(AVG_TIMER_WAIT / 1e9, 2) AS avg_ms, ROUND(SUM_ROWS_EXAMINED / SUM_ROWS_AFFECTED, 2) AS rows_examined_ratio FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 20;DIGEST_TEXT 就是归一化后的 SQL 文本命中率极高。但要注意这张表的统计数据从实例启动开始累积实例运行太久之后 TOP N 会被历史包袱占据需要 FLUSH 重置或配合时间窗口对比使用。1.3 PostgreSQL——pg_stat_statements 与 auto_explainPostgreSQL 这两年的热度上来很快但很多人是从 MySQL 转过来的在慢 SQL 排查上经常习惯性地找“慢日志”却忽略了更核心的 pg_stat_statements。PostgreSQL 的查询日志也能记录慢 SQL配置项是 log_min_duration_statement比如设置成 1000ms超过 1 秒的语句就会带耗时记录打到日志里。但它有个明显短板——只能记录执行时间超过阈值的 SQL而 pg_stat_statements 能对全部 SQL 做统计和聚合。这个扩展需要预先安装并加载到共享库# postgresql.conf shared_preload_libraries pg_stat_statements pg_stat_statements.max 10000 pg_stat_statements.track all改完必须重启 PostgreSQL因为扩展要在启动时预加载。重启后执行一次CREATE EXTENSION IF NOT EXISTS pg_stat_statements;然后就可以查询了SELECT query, calls, ROUND(total_exec_time::numeric / 1000, 2) AS total_secs, ROUND(mean_exec_time::numeric, 2) AS avg_ms, ROUND(max_exec_time::numeric, 2) AS max_ms, rows FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;注意 PG 13 之前的时间字段叫 total_timePG 13 之后改成了 total_exec_time / mean_exec_time / max_exec_time。如果直接拿网上旧教程的 SQL 跑到新版本上会因为字段名对不上直接报错。另一个杀手锏是 auto_explain。这个模块可以不改变业务 SQL的前提下自动把超阈值 SQL 的执行计划打入日志。开启方式是修改 postgresql.confshared_preload_libraries auto_explain auto_explain.log_min_duration 1000ms auto_explain.log_analyze on auto_explain.log_buffers on重启后在 PostgreSQL 日志里就能看到类似下面这种内容duration: 2312.348 ms plan: Query Text: SELECT * FROM t_order WHERE status $1 AND create_time $2 Seq Scan on t_order (cost0.00..48312.39 rows118 width92) Filter: ((status PAID::text) AND (create_time 2025-04-01::timestamp))这对复现生产环境偶发慢 SQL 特别有用——因为它捕获的是真实执行时的计划而不是你事后手工 EXPLAIN 的计划。很多偶发慢 SQL 只要换个参数值、换了数据分布EXPLAIN 可能就复现不出来了auto_explain 直接帮你把案发现场固定下来了。2. 执行计划——判断 SQL 快慢的根本依据2.1 Oracle 执行计划EXPLAIN PLAN 与 DBMS_XPLANOracle 获取执行计划的方式很多我实际工作中最常用的是 DBMS_XPLAN 家族。如果你手里只有一个 SQL_ID比如从 AWR 报告或者 v$SQL 里拿到的可以直接用 DISPLAY_CURSOR 函数把它的真实执行计划查出来SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(你的SQL_ID, 0, ALLSTATS LAST));ALLSTATS LAST 参数的作用是附带执行时的 A-STATS也就是真实行数和代价估算的对比。这是我最看重的一列信息——如果估算行数和实际行数差了好几个数量级那你基本可以先把 CBO 的基数估算骂一遍然后去查统计信息是不是过期了。SQL 在开发环境跑太慢需要手工推演时用 EXPLAIN PLAN FOR 更顺手EXPLAIN PLAN FOR SELECT * FROM orders WHERE cust_id 12345; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);在 SQLPlus 里也可以直接 SET AUTOTRACE ON执行完 SQL 后自动带出执行计划。不过要注意AUTOTRACE 实际会执行这条 SQL如果是大查询别在生产环境直接开否则慢 SQL 还没查出来你自己先制造了一个慢 SQL。我曾经见到有人排查线上问题直接在 SQLPlus 里 AUTOTRACE 跑了一条 20 分钟的大查询成功把生产库拖得更慢了。正确做法是先 EXPLAIN PLAN FOR确认逻辑没问题再用实际数据量比较小的等值条件去验证。读 Oracle 执行计划时核心是看两步第一步从缩进最深的操作开始读里层先执行数据往上抛给外层第二步关注 TABLE ACCESS FULL、INDEX RANGE SCAN、INDEX FULL SCAN 这些访问路径以及 NESTED LOOPS、HASH JOIN、SORT ORDER BY 这些连接和排序方式。如果出现大表全表扫描检查它的 Filter 谓词看看是没索引、索引没被用、还是用了隐式转换把索引搞失效了。我见过太多 CASE WHEN 把字段包住后导致全表扫描的例子后面会详细展开。2.2 MySQL 执行计划EXPLAIN 的核心列MySQL 的 EXPLAIN 被很多人用过但绝大多数人只会看 type 是不是 ALL。实际上一个完整的 MySQL EXPLAIN 里我一般固定看这几列type、key、key_len、rows、Extra。type 列表示访问类型性能排序大概是 system const eq_ref ref range index ALL。看到 ALL 基本就是全表扫描index 表面上是用了索引但可能扫描的是整个索引树未必比 ALL 好多少。中间这几档里range 说明用了索引范围扫描ref 说明是等值匹配这都属于正常状态。key_len 列代表用到索引的长度——如果联合索引用到了一部分key_len 会小于索引定义的总长度这时你要判断到底用上了哪几列是否漏掉了关键过滤条件。Extra 里出现 Using filesort 或 Using temporary基本就是性能炸弹。filesort 不是文件里的排序而是需要额外排序的统称一旦出现意味着 MySQL 没法利用索引顺序直接输出只能自己另起炉灶排一遍。举个例子。假设有表 t_order建立了联合索引 (user_id, status, create_time)EXPLAIN SELECT * FROM t_order WHERE user_id 123 AND status PAID ORDER BY create_time DESC LIMIT 10;如果执行计划里的 key 显示为 idx_user_status_timekey_len 正好是 4 各种字段长度之和Extra 没有 filesort说明索引的最左前缀全用上了排序也走索引了。但如果你把查询条件改成 status PAID 和 create_time xxx只留了联合索引的中间列和第三列而少了第一列 user_id那 type 大概率掉到 ALL因为索引最左前缀原则决定了你没法跳过 user_id 直接使用后面的列——除非你重新建索引或者调换条件顺序。MySQL 还有个大坑是 EXPLAIN 默认不显示实际执行时间和实际行访问量。你看到的 rows 是基于估算统计信息的不是真实值。想知道真实值得用 EXPLAIN ANALYZE这是 MySQL 8.0.18 之后的功能它会真实执行 SQL 并在执行计划里附上实际耗时和循环行数。EXPLAIN ANALYZE SELECT * FROM t_order WHERE user_id 123;实测输出里会多出来 actual time 和 actual rows我是靠这个来判断估算值是否靠谱。MySQL 8.0 之前想拿到真实执行信息比较麻烦需要靠慢日志和 SHOW PROFILE 兜底这也是我在不少老项目里一直建议尽早升级 8.0 的一个原因。2.3 PostgreSQL 执行计划EXPLAIN ANALYZE 的细节PostgreSQL 让我最舒服的一点是EXPLAIN ANALYZE 默认就把真实执行时间和真实行数展示出来。执行计划不是干巴巴的估算树而是“估算 vs 实际”的对照组EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE create_time 2025-01-01 AND create_time 2025-02-01;输出大概是Seq Scan on orders (cost0.00..13412.90 rows1410 width64) (actual time0.021..12.312 rows198839 loops1) Filter: ((create_time 2025-01-01 00:00:00::timestamp) AND (create_time 2025-02-01 00:00:00::timestamp)) Rows Removed by Filter: 801161 Buffers: shared hit5421 read248 dirtied11这里我要先看 actual rows 和估算 rows 的差距再看 Buffers 里的 shared read 和 shared hit。shared read 是从磁盘或者操作系统缓存中读的块shared hit 是直接从 PostgreSQL 共享缓冲区命中的块。如果 read 占比高说明这个查询正在把大量数据从磁盘拉进来这就是全表扫描或索引回表风暴的信号。如果 rows 估算偏差特别大下一步九成是统计信息和 vacuum 的问题而不是 SQL 本身的问题。PG 执行计划解读还有个容易忽略的细节如果 Sort Method 显示 external merge或者提示 temporary file说明 work_mem 不够排序溢出了到磁盘。这个优化不是改 SQL 能解决的得调高单个会话的 work_mem或者优化 SQL 让排序量变小。另外 PG 14 之后支持了更直观的 EXPLAIN (ANALYZE, MEMORY)可以把内存占用更细致地列出来遇到隐式内存问题可以优先用这个。2.4 三家执行计划的差异点三种数据库执行计划的基础概念差不多但细节上差异非常大我习惯记几个关键点对比项OracleMySQLPostgreSQL计划获取EXPLAIN PLAN DBMS_XPLANEXPLAIN / EXPLAIN ANALYZEEXPLAIN / EXPLAIN (ANALYZE, BUFFERS)核心估算对象基数 Cardinality、选择性type 列、key_len、rowscost、actual rows/loops真实执行数据需 A-STATS默认不显示8.0 之后 EXPLAIN ANALYZE 支持默认就显示 actual time/rows统计信息更新默认自动收集也可手工 DBMS_STATSanalyze tableautovacuum analyze大坑预警基数估算严重偏差、绑定变量窥探索引失效最左原则、filesortwork_mem 不足、真空滞后、膨胀拿着这样一张对照表去排查能省很多弯路。同一句 SQL 在 Oracle 里可能是索引跳过的错在 MySQL 里可能是隐式转换的错在 PostgreSQL 里可能是膨胀导致的估算失效。定位的手段可以复制但根因分析一定要回到它自己的执行计划逻辑里去。3. 典型慢 SQL 病根——从真实案例里拆出来的高频问题3.1 条件写错隐式转换、函数包裹、NULL 判断很多人以为慢 SQL 都是因为没有索引其实我见过的大部分慢 SQL 是索引“在而不用”。最常见的罪魁祸首就是隐式类型转换。MySQL 里如果被索引列是 varchar查询条件传了数字优化器会先把索引列转成数字再比较索引直接失效-- phone 列是 varchar传入数字导致隐式转换 SELECT * FROM users WHERE phone 13800138000;改成字符串才能稳定走索引SELECT * FROM users WHERE phone 13800138000;Oracle 中同样存在Oracle 12c 之后对隐式转换的容忍度比旧版好一点但 varchar2 和 number 比较、date 和 timestamp 比较时仍然可能因为类型不匹配而放弃索引范围扫描。SQL 层面解决就是把类型写对不要让数据库猜。函数包裹列也很常见。比如按天统计订单-- 不走 create_time 索引 SELECT * FROM orders WHERE TRUNC(create_time) DATE 2025-04-01; -- 可以走 create_time 索引如果存在 SELECT * FROM orders WHERE create_time DATE 2025-04-01 AND create_time DATE 2025-04-02;Oracle 和 PostgreSQL 对这类函数表达式支持函数索引MySQL 8.0 也支持函数索引了但性能开销和索引维护成本都在能改 SQL 就优先改 SQL。NULL 判断也是重灾区尤其 MySQL 里 is null 能不能走索引取决于版本、count 统计方式的差异很大查询模型拿不准时最好用 EXPLAIN 实测。经验值频繁用来过滤的列干脆在表设计阶段加 NOT NULL 约束或设置默认值业务上兼容一下比后期用 SQL 技巧去绕靠谱得多。3.2 分页深挖大偏移量 LIMIT 与 Oracle 的 ROWNUM分页慢是另一种高频病根。MySQL 典型慢分页是这样SELECT * FROM t_order ORDER BY create_time DESC LIMIT 1000000, 20;MySQL 得先把前 1000020 行全部排完、丢掉前 1000000 行才能给你最后 20 行。数据量小没事上了百万级就是灾难。缓解思路是延迟关联或游标式翻页。游标式翻页本质上利用上一页最后一条数据的锚点SELECT * FROM t_order WHERE create_time 2025-04-01 10:00:00 -- 上一页最后一条的时间 ORDER BY create_time DESC LIMIT 20;前提是排序字段唯一且单调否则可能有漏页。业务允许的条件下这种方案性能提升非常直观。Oracle 12c 之前的分页是 ROWNUM 带派生表SELECT * FROM ( SELECT t.*, ROWNUM rn FROM (SELECT * FROM t_order ORDER BY create_time DESC) t WHERE ROWNUM 1000020 ) WHERE rn 1000000;深分页同样绕不开全排序。Oracle 12c 提供了 OFFSET FETCH写法是:SELECT * FROM t_order ORDER BY create_time DESC OFFSET 1000000 ROWS FETCH NEXT 20 ROWS ONLY;语义上更清晰但本质上也没法避免“把所有行扫完再跳过”。PostgreSQL 原生支持 LIMIT OFFSET深分页一样慢优化思路跟 MySQL 相同keyset 分页在 PG 里用起来我觉得最顺手SELECT * FROM orders WHERE (create_time, id) (2025-04-01 10:00:00, 123456) -- 上一页最后一条数据的位置 ORDER BY create_time DESC, id DESC LIMIT 20;这种查询完全走索引每页扫描行数几乎恒定不会随着页码增大而变慢。分页这个坑的本质是“不告诉数据库你要从哪条开始只告诉它你要跳过多少条”让数据库去白干那些无意义的扫描业务有翻页需求时尽量改成基于索引定位的keyset方案。3.3 JOIN 写飞了连接顺序与驱动表JOIN 查得慢也是常见投诉。三种数据库对 JOIN 的处理方式有差异Oracle 和 PostgreSQL 的成本优化器一般会自己决定连接顺序和连接算法MySQL 的优化器相对更依赖驱动表的统计和索引情况。MySQL 5.7/8.0 里最典型的慢 JOIN 是关联字段没有索引。比如 A 表 JOIN B 表B 表关联列没有索引那就只能启动 block nested loop逐行去 B 表里扫。解决方法很粗暴给关联列建索引同时尽量保证关联列的数据类型完全一致。关联列 varchar 和 char 混用、utf8mb4 和 utf8 混用都会导致索引不可用业务不崩但性能奇差。Oracle 里我见过最多的是 NESTED LOOPS 被错误选中的情况而正确的应该是 HASH JOIN或者反过来两个大表关联时优化器选了 HASH JOIN但 PGA 的 workarea 太小hash 区只能部分在内存导致大量写盘临时段。遇到这类问题执行计划里能看到 temp 落盘操作。简单说就是小表驱动大表时嵌套循环没问题大表关联大表时无脑选 hash join 反而比 nested loop 更快——除非关联条件极其精准能过滤掉大部分数据。PostgreSQL 的 JOIN 也有独特脾气。15 之前如果 work_mem 不够大hash join 可能会临时落盘16 开始默认有 32MB 的 work_mem 和 4MB 的维护内存整体好很多。但这方面不同版本差异挺大建议一手 EXPLAIN ANALYZE一手查数据库日志别只凭经验猜测。子查询下推也是一个常见的优化点。很多 ORM 生成的 SQL 会在外层套一层过滤优化器如果没能把谓词下推到子查询里就会把子查询全部结果物化出来再过滤。这时手工把过滤条件写到子查询内部执行计划往往立刻好很多。MySQL 8.0 的派生表合并derived merge解决了部分问题但生成列、聚合函数、LIMIT 的子查询仍然不能直接合并需要注意。3.4 统计信息过期与数据膨胀同一个 SQL 三天后突然变慢还有一类慢 SQL 最磨人SQL 一个字没改三天前还很快今天突然从 300 毫秒变成 30 秒。这种“非代码因素”的慢十有八九是统计信息过期或者数据膨胀。Oracle 11g 之后默认开启了自动统计信息收集每天固定晚上窗口执行但如果业务表数据变化特别剧烈比如今日新增 20% 数据或者分区表只改了某个分区自动收集没跑到就非常容易让执行计划选错。处理方式是手工对目标表收集一次统计信息EXEC DBMS_STATS.GATHER_TABLE_STATS(ownname APP, tabname ORDERS, cascade TRUE);如果要查表分区级别的统计信息建议加 granularity PARTITION 参数。MySQL 里对应的是 ANALYZE TABLEANALYZE TABLE orders;MySQL 8.0 中 innodb_stats_auto_recalc 默认开启但不是实时更新即时性不够数据量剧烈变化后建议手工跑一次。PostgreSQL 里和统计信息、数据膨胀绑定的概念是 autovacuum。很多 PG 慢 SQL 其实是在读表本身的死元组和膨胀页vacuum 没跟上导致扫描块数远超实际数据量。解决手段是VACUUM (ANALYZE, VERBOSE) orders;定期巡检时我还会检查膨胀表SELECT schemaname, relname, n_dead_tup, n_live_tup FROM pg_stat_user_tables WHERE n_dead_tup 10000 ORDER BY n_dead_tup DESC;如果死元组长期居高不下说明 autovacuum 设置可能需要调参而不是单单这一次手动 vacuum。统计信息和数据膨胀这类问题是最容易被研发忽略的——因为代码层看不出任何变化但 DBA 一看数据分布就能定位排查时一定要有“数据状态变化”这个怀疑方向。4. 一次跨库慢 SQL 排查实战复盘4.1 现象与最初判断有一次周五晚上一个零售系统的报表平台被用户投诉销售日报从原来的 30 秒变成 2 分半钟连带着 Oracle 库存查询、MySQL 订单查询都出现了超时告警。团队第一反应是网络抖动或者数据库被爬虫拖死了但我进到每套库分别看了一眼连接数发现真实情况完全不同。首先是 Oracle 侧AWR 里排在榜首的一条 SQL 之前没进过 Top 榜单它负责查询最近 7 天的订单汇总单次平均执行时间从 400 毫秒飙到 4 秒。其次是 MySQL 侧慢查询日志里频繁出现大分页的 SELECTLIMIT 偏移量已经到 80 万行。最后是 PostgreSQL 侧的报表查询pg_stat_statements 里总耗时第一的查询模板单次平均耗时从 120 毫秒涨到了 2 秒。三个库分别都有问题而且看起来毫无关联但时间点几乎一致这就让我怀疑是不是有共同的“上游诱因”在起作用。4.2 逐库排查的完整路线我先处理 Oracle。用 DBMS_XPLAN.DISPLAY_CURSOR 把榜首 SQL 的真实执行计划和实际执行统计拉出来发现关键的一行TABLE ACCESS FULL ORDERS但实际返回行数只有 284 行。一个 800 万行的订单表过滤条件命中 284 行却走了全表扫描明显是执行计划选错了。再看估算行数优化器估算为 9 万行实际 284 行差了 3 个数量级。这说明统计信息严重失真ORDER 表和 CUSTOMER 表因为当天凌晨大批量清理历史数据数据分布发生剧变自动收集任务由于时间窗口刚好错过没来得及更新。解决方法就是对订单表重新收集统计信息。MySQL 这边的问题就更“传统”。慢日志里那条 80 万偏移量的分页 SQL其实是报表前端点到了第 40000 页后端用的是 LIMIT 800000, 20。这种写法就算把所有索引都建齐也没用InnoDB 必须扫描 800020 行才能返回结果。优化方式是用最前面的排序列加自增 ID 做 keyset 分页我改完后从 6.8 秒降到 80 毫秒。PostgreSQL 的报表查询问题出在膨胀。我执行 EXPLAIN (ANALYZE, BUFFERS) 发现orders 表实际扫描了 128 万个块但真正返回的数据只有 19 万行Shared Read 占比极高。再看 pg_stat_user_tablesorders 表的 n_dead_tup 已经 300 多万n_live_tup 才 50 万。这说明 autovacuum 在凌晨运行时被另一个长事务卡住没把死元组清干净于是每次查询都扫了大量已删除数据占住的页。处理办法是调大 autovacuum 的 worker 数量并且对订单表做一次 VACUUM ANALYZE。4.3 根因与处理排查下来三套库的问题其实是同一个业务链路触发的报表平台在当天凌晨从 MySQL 批量搬运 500 万条历史订单到 Oracle 的归档表搬完后又清理了 PG 测试环境的临时表这些操作分别导致Oracle 的订单表分布变化统计信息过期MySQL 的前端分页在数据量翻倍后继续沿用大偏移量翻页PostgreSQL 的清理长事务阻塞了 autovacuum死元组积累。处理动作总结成了一张清单数据库直接原因处理方案优化效果Oracle统计信息过期执行计划选错全表扫描手工收集统计信息DBMS_STATS.GATHER_TABLE_STATS单次 SQL 从 4 秒回落到 420 毫秒MySQLLIMIT 大偏移量导致回表扫描 80 万行改成 keyset 游标分页并给 create_time,id 建联合索引从 6.8 秒降到 80 毫秒PostgreSQL死元组膨胀全表扫描块数暴增VACUUM ANALYZE调整 autovacuum 参数报表查询从 2 秒恢复到 150 毫秒那一次复盘让我印象很深三套库的时间点一致、业务动作一致如果只盯着一套库的日志查很可能查半天都找不到主因。多数据库环境里的慢 SQL永远要先看“最近到底发生了什么业务操作”再看“每个库各自的证据链”。这两步不能反过来。5. 慢 SQL 之外的坑——连接池、锁与资源竞争5.1 连接池耗尽才是幕后黑手慢 SQL 排查过程中经常出现一种假象数据库确实有一条 SQL 跑了 5 秒但真正导致用户超时的是应用侧连接池被打满所有线程都在排队等连接报错一堆 timeout数据库本身 CPU 和 IO 却只有 20%。遇到这种情况先在应用侧的监控里看连接池的 active 数、wait 数、pool size 上限。然后分别查一下三套库的连接数视图。Oracle 查 v$SESSION、v$PROCESSMySQL 查 information_schema.processlist特别关注 State 列为 Statistics、Sending data、Waiting for table metadata lock 的长期会话PostgreSQL 查 pg_stat_activity重点看 state 是 active 还是 idle in transaction。-- PostgreSQL 查看长时间运行事务 SELECT pid, state, now() - xact_start AS xact_age, now() - query_start AS query_age, wait_event_type, wait_event, left(query, 80) AS query FROM pg_stat_activity WHERE state idle ORDER BY xact_age DESC;连接池问题常常是慢 SQL 的放大器一条慢 SQL 占着连接不放后续请求全部堵在池里超时重试又把新请求补进来形成雪崩。所以每次排查慢 SQL都应该以分钟级粒度把数据库连接数、活跃进程数和慢 SQL 执行时间放在同一时间轴上对比先分清谁是因谁是果。我见过有人在慢 SQL 上耗了一下午结果把连接数一放开问题直接缓解了六成。5.2 锁等待和阻塞Lock 等待也是典型的“SQL 变慢但跟优化无关”的原因。Oracle 的锁一般在 v$LOCKED_OBJECT、DBA_BLOCKERS 这些视图里可见MySQL 可以查SELECT trx_id, trx_state, trx_started, trx_wait_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx;PostgreSQL 则是 pg_locks 配合 pg_classSELECT pid, relation::regclass, locktype, mode, granted, now() - query_start AS wait_age FROM pg_locks l LEFT JOIN pg_stat_activity a USING (pid) WHERE NOT granted ORDER BY wait_age DESC;锁等待出现时单纯看执行计划没有任何意义因为 SQL 本身并不慢它是在等别的会话释放锁。定位到阻塞源头后需要业务侧配合确认是否可以终止会话或事务行动前要谨慎评估是否有未提交的数据变更不要贸然 kill 可能导致事务回滚导致业务中断。5.3 参数配置与硬件资源除了 SQL 本身数据库参数也会制造慢 SQL。比较常见的有 Oracle 的 SGA/PGA 内存设置过小、MySQL 的 innodb_buffer_pool_size 偏小、sort_buffer_size 和 join_buffer_size 偏小、PostgreSQL 的 shared_buffers 和 work_mem 偏小等状况。比较直观的定位方法看执行计划和数据库日志里有没有磁盘排序、临时文件、物理读比例过高的线索。Oracle 的 v$SQL_WORKAREA 可以查每个 SQL 的 workarea 使用情况MySQL 打开 profiling 之后能看到 Sending data、Sorting result、Creating sort index 等阶段的耗时PG 的 EXPLAIN ANALYZE 里如果出现 external merge、temporary file 基本上就是 work_mem 不够。参数调整没有万能答案要结合你的业务负载特征。比如你查询大部分是小的排序就不用把 work_mem 调到 1G否则可能会导致每个会话预分配大量内存并浪费。比较务实的做法是先确定瓶颈类型IO、CPU、内存再针对性地调并且每次只调一个参数调整后观察 1-2 天再做下一轮。5.4 慢 SQL 治理的长效手段排查完一条不代表以后不会再犯。我处理完问题后通常会在团队里补三样东西慢 SQL 日志归档和看板、执行计划基线、变更流程里的“SQL 预检”。慢 SQL 日志归档比较好落地无论哪套库把慢日志统一采集到 ELK 或 ClickHouse 里配置告警平均耗时超过阈值的 SQL 模板出现时自动发通知。执行计划基线我主要用在重要查询上记录优化后的执行计划关键字段比如 MySQL 的 type 级别、key 和 rows 区间后续如果执行计划发生变化告警出来再人工分析。变更流程里的预检则要求研发上线的每个涉及 SQL 的版本至少对数据库做一次 EXPLAIN并在代码评审里附带执行计划截图或者文本养成习惯之后很多索引失效的问题在测试阶段就被拦下来了。除此之外我还会做压测回归。不为所有 SQL 做只针对核心链路和日活访问量最高的几十条 SQL。压测数据量最好按生产库容量的 1.2 倍准备这样能提前撞见统计信息过期、数据膨胀等非代码问题。最后一点个人体会三类数据库各有一套完整的慢 SQL 排查体系工具和命令不同但思路底层是共通的先拿到“最慢的那批 SQL”再看执行计划里的访问路径和估算误差最后回头检查数据状态、连接资源和锁等待这些 SQL 之外的因素。我踩过很多次只看日志、只优化 SQL 的弯路最后发现慢 SQL 大多是“病在数据库、根在系统”。如果你现在处于刚入门阶段我建议先把自己的主力业务数据库吃透再横向迁移到另两套应对多库环境时心里会踏实很多。排查慢 SQL 没有一劳永逸的银弹但把这套“捞日志、读计划、查资源、验结果”的闭环跑熟大部分问题都能在半小时内收敛。
返回列表