大家好我是数据库小学妹 客户电商平台的核心报表跑了45分钟出不来客户投诉了让我去查。我打开AWR报告密密麻麻的数字铺满屏幕。说实话第一眼看懵了。但后来我发现AWR就像数据库的体检报告。只要找对指标答案就在那儿。今天我把从AWR分析到执行计划优化到最终落地的完整过程写出来。朋友如果你也面对过一张看不懂的AWR报告这篇能帮你找到入口。AWR是什么先搞清它记录了什么AWR全称Automatic Workload Repository。Oracle自动收集的性能数据快照。每隔一小时采一次样把这一小时里数据库干了什么、花了多久、时间都耗在哪记下来。你拿到AWR报告等于拿到了数据库过去一小时的行程单。它去哪儿了、干了什么、哪件事最耗时间一目了然。AWR不是实时监控工具。它记录的是采样窗口内的聚合统计量。如果某个SQL在9:05跑了30秒在9:50又跑了30秒AWR告诉你的是这60秒的总和不会告诉你这两次各自是什么时候发生的。想看具体执行时间线得去v$active_session_historyASH里挖。生成AWR报告两条SQL-- 查看可用的快照IDSELECTsnap_id,begin_interval_time,end_interval_timeFROMdba_hist_snapshotORDERBYsnap_idDESCFETCHFIRST10ROWSONLY;-- 生成AWR报告?/rdbms/admin/awrrpt.sql-- 交互式选择起始和结束SNAP_ID输出HTML或TEXT报告起止SNAP_ID的选择要注意。选一个完整的业务周期。报表跑在9点到10点之间就选9点前和10点后的两个快照。跨度太短数据没有代表性跨度太长高峰期的问题被平均掉看不出来。采样间隔默认1小时保留期限默认8天。性能问题发生在8天前AWR已经自动清理了。长期趋势分析可以用SQL Tuning Set留存历史数据。看AWR报告的正确顺序别一上来就找SQL我第一次看AWR直奔SQL Statistics部分。找了半天没头绪。后来前辈教了我一个顺序先看宏观数据再往细处挖。看报告头部的快照信息确认时间窗口对不对。我见过有人拿了备份时间段的AWR来分析业务慢的原因白忙活。多实例环境还要注意DB Name和Instance Number别拿错报告。然后看Load Profile。每秒事务数、每秒逻辑读、每秒物理读这几个数告诉你数据库当时有多忙。Load Profile Per Second Per Transaction Per Exec Per Call ~~~~~~~~~~~~~~~~ ---------- --------------- -------- -------- DB Time(s): 456.2 1,234.56 0.45 12.34 DB CPU(s): 89.3 241.78 0.09 2.42 Redo size (bytes): 1,234,567 3,340,890.12 Logical reads: 45,678 123,456.78 Block changes: 3,456 9,345.67 Physical reads: 5,678 15,345.67 Physical writes: 1,234 3,345.67 Execute Count: 1,012 2,734.56我重点看两个比值。DB Time和DB CPU的比值。DB Time是456秒DB CPU只有89秒。数据库前台进程大部分时间不在计算在等待。等待I/O、等待锁、等待网络回包都算等待。Physical reads和Logical reads的比例。逻辑读45678次物理读5678次比例约12%。88%的数据在Buffer Cache里命中了。OLTP系统这个数值正常。OLAP报表场景的话物理读比例偏高说明Buffer Cache不够大或者扫描范围太广。DB Time的计算方式我踩过坑。它等于会话在数据库中的总耗时除以采样窗口秒数。10个会话同时运行每个等了10秒DB Time就是100秒除以窗口秒数。所以DB Time可能大于1表示多个会话并行等待。接下来看Top 5 Timed Events。这是AWR报告最核心的部分。Top 5 Timed Foreground Events ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Avg wait % DB Event Waits time time -------------------------------- ----------- ------ ------ db file scattered read 1,234,567 12ms 42.31 log file sync 456,789 8ms 18.56 CPU time - - 15.23 enq: TX - row lock contention 23,456 45ms 12.67 db file sequential read 789,012 3ms 6.78等待事件的判断逻辑等待事件含义排查方向db file scattered read全表扫描读数据块查缺索引、SQL改写db file sequential read索引扫描读数据块索引是否有效率问题log file sync等redo日志写入磁盘提交太频繁、redo日志性能enq: TX行锁竞争多事务争同一行业务并发逻辑冲突latch: cache buffers chains争内存缓冲区链热点块、SQL效率低Avg wait time也值得留意。db file scattered read平均等待12ms磁盘I/O来看不算高但Waits达到123万次累积时间就大了。高频低延迟的等待通常是全表扫描反复读小表低频高延迟的等待更可能是存储层面的瓶颈。db file scattered read排第一占42%的DB time。大量全表扫描。方向有了。从等待事件到具体SQL定位慢查询看SQL Statistics。按Elapsed Time排序找到最耗时的SQL。Elapsed Time CPU Time Gets Executions Elap per Exec (s) SQL Id (s) (s) (s) --------------- ----------------- -------- ------------ ------------------- ------------- 2,456.78 456.78 5,678,901 1,234 1.99 3z4m5k6n7p 890.12 234.56 2,345,678 567 1.57 8x9y0z1a2b 567.89 123.45 1,234,567 234 2.43 3c4d5e6f7g排名第一的SQL执行了1234次每次1.99秒总耗时2456秒。注意一个细节CPU Time只有456秒说明这条SQL大部分时间也在等待。Gets是567万次逻辑读平均每次执行约4600次。对于报表查询这个数字偏高。我还会看Disk Reads列。Disk Reads很高但Gets很低说明SQL几乎没有利用缓存每次都要读磁盘。Gets很高但Disk Reads很低说明数据都在内存里但扫描量太大。两种情况优化方向完全不同。拿到SQL ID用DBMS_XPLAN看执行计划-- 通过SQL ID查看执行计划SELECT*FROMTABLE(DBMS_XPLAN.DISPLAY_AWR(3z4m5k6n7p));执行计划-------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost | Time | -------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 56789 | | | 1 | HASH JOIN | | 500K | 125M | 56789 | 00:45:23 | | 2 | TABLE ACCESS FULL| ORDERS | 2M | 200M | 23456 | 00:18:45 | | 3 | TABLE ACCESS FULL| CUSTOMERS | 500K | 50M | 12345 | 00:09:32 | | 4 | SORT GROUP BY | | 500K | 125M | 20988 | 00:17:06 | --------------------------------------------------------------------------------ORDERS表两百万行全表扫描CUSTOMERS表五十万行全表扫描然后做HASH JOIN。没有走索引。为什么优化器选了HASH JOIN而不是NESTED LOOPHASH JOIN适合大表对大表的连接把小表驱动表的Join Key做成Hash表然后扫描大表逐行匹配。NESTED LOOP适合驱动表返回少量数据逐行去被驱动表查索引。优化器认为两个表都会返回500K行所以选了HASH JOIN。理论没问题。但500K这个估值错了。看Predicate InformationPredicate Information (identified by operation id): --------------------------------------------------- 2 - filter(ORDERS.STATUS IS NOT NULL AND ORDERS.CREATE_DATE SYSDATE-365) 3 - filter(CUSTOMERS.REGION 华东)ORDERS表在STATUS和CREATE_DATE上没有复合索引。CUSTOMERS表的REGION字段也没有索引。全表扫描是必然的。执行计划里的Rows估值是500K实际过滤后的数据只有50K。优化器估错了10倍。这就是它选HASH JOIN的原因。如果Rows估对了优化器有可能选NESTED LOOP也可能仍然选HASH JOIN——50K行的连接量级两种策略都可能。不管怎样基数估算准了Cost计算才可靠。基数估算Cardinality Estimation不准优化器就会选错执行计划。任何基于成本的优化器CBO都依赖统计信息做决策。统计信息不准决策就不准。根源是统计信息过期。查一下-- 检查表统计信息是否过期SELECTtable_name,num_rows,last_analyzed,stale_statsFROMdba_tab_statisticsWHEREtable_nameIN(ORDERS,CUSTOMERS);ORDERS表的last_analyzed是三个月前。这期间订单量翻了一倍。优化器用旧统计信息做决策估出来的Rows自然不准。还有一个容易忽略的点倾斜列。STATUS字段只有4个值待支付、已支付、已发货、已完成分布极不均匀。已完成的占80%待支付的只有5%。没有直方图的话优化器用num_rows/ndv做均匀分布假设估出来的基数偏差很大。补直方图EXECDBMS_STATS.GATHER_TABLE_STATS(SCHEMA_NAME,ORDERS,METHOD_OPTFOR COLUMNS STATUS SIZE AUTO);STATISTIC_LEVEL参数也会影响统计信息收集。设成BASIC的话很多扩展统计信息不会自动收集。生产环境建议设成TYPICAL。制定优化方案第一个动作给ORDERS表建复合索引。按查询条件排序区分度高的放前面。-- 分析索引选择性SELECTCOUNT(DISTINCTcreate_date)/COUNT(*)ASdate_selectivity,COUNT(DISTINCTstatus)/COUNT(*)ASstatus_selectivityFROMorders;跑完选择性分析CREATE_DATE的选择性是0.8STATUS只有0.05。区分度高的放前面索引过滤效率更高。-- 创建复合索引CREATEINDEXidx_orders_date_statusONorders(create_date,status);-- 创建REGION索引CREATEINDEXidx_customers_regionONcustomers(region);-- 收集统计信息让优化器知道索引的存在EXECDBMS_STATS.GATHER_TABLE_STATS(SCHEMA_NAME,ORDERS);EXECDBMS_STATS.GATHER_TABLE_STATS(SCHEMA_NAME,CUSTOMERS);生产环境加索引要用ONLINE关键字否则建索引期间表会被锁业务中断。CREATEINDEXidx_orders_date_statusONorders(create_date,status)ONLINE;ONLINE方式有代价生成更多redo建索引时间更长。表特别大几十亿行的话我会选业务低峰期用普通方式建。优化后重新跑报表。执行计划变了-------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost | -------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | 3456 | | 1 | HASH JOIN | | 50K | 3456 | | 2 | TABLE ACCESS BY INDEX ROWID | ORDERS | 50K | 2345 | | 3 | INDEX RANGE SCAN | IDX_ORDERS_DATE_STAT | 50K | 1234 | | 4 | TABLE ACCESS BY INDEX ROWID | CUSTOMERS | 50K | 890 | | 5 | INDEX RANGE SCAN | IDX_CUSTOMERS_REGION | 50K | 456 | --------------------------------------------------------------------------------TABLE ACCESS FULL变成了INDEX RANGE SCAN。TABLE ACCESS BY INDEX ROWID说明先走索引拿到ROWID再回表取数据。Cost从56789降到3456。Rows估值从500K修正到50K。执行时间从45分钟降到8分钟。等待事件转移优化后的新问题但这还没完。重新抓了一份AWR报告Top 5事件变了优化后 Top 5 Timed Events ~~~~~~~~~~~~~~~~~~~~~~~~~ Event Waits Avg wait % DB time -------------------------------- ----------- ---------- ----------- log file sync 89,012 12ms 38.45 CPU time - - 32.10 db file sequential read 45,678 2ms 15.67 enq: TX - row lock contention 12,345 23ms 8.90 db file scattered read 5,678 5ms 3.21db file scattered read从42%降到3%。log file sync升到了38%。等待事件转移了。消除一个瓶颈另一个就浮出来。这是调优的常态。log file sync变高说明提交频率成了问题。每次COMMITLGWR进程要把redo buffer写入redo log文件前台进程要等写入完成。提交越频繁等待越多。查业务代码报表生成后每条结果都单独COMMIT。一个循环处理5万条数据COMMIT了5万次。改为批量提交后log file sync降到了8%。-- 优化前每处理一条就COMMIT FOR rec IN cursor LOOP INSERT INTO report_result VALUES (...); COMMIT; -- 5万次COMMIT END LOOP; -- 优化后批量提交 FOR rec IN cursor LOOP INSERT INTO report_result VALUES (...); IF MOD(counter, 1000) 0 THEN COMMIT; -- 变成50次COMMIT END IF; END LOOP; COMMIT;COMMIT次数从5万降到50。log file sync的Waits从89012降到3200。报表总时间从8分钟进一步降到5分半。注意别把等待事件当绝对真理这里我要坦白。刚开始做调优时看到高等待事件就急着加索引。结果SQL跑得更慢了。那个等待事件是统计信息收集任务造成的。AWR采样窗口刚好撞上了DBMS_STATS的自动执行。高等待不是业务SQL的问题是后台任务。看AWR报告前先确认采样窗口里有没有特殊任务。备份、统计信息收集、数据加载都会干扰AWR数据。-- 检查采样窗口内是否有统计信息收集任务SELECToperation,start_time,end_timeFROMdba_optstat_operationsWHEREstart_timeBETWEENTO_DATE(2026-01-15 10:00,YYYY-MM-DD HH24:MI)ANDTO_DATE(2026-01-15 11:00,YYYY-MM-DD HH24:MI);还有一次enq: TX行锁竞争很高我以为是并发冲突。后来发现是业务逻辑问题两个定时任务同时更新同一批订单的状态。不是数据库的问题是应用层的调度设计。看AWR时我养成了一个习惯先把等待事件列出来逐一对应到具体的SQL和业务场景。不急着动手先搞清因果关系。AWR调优避坑清单看到高等待事件别急着加索引。先确认等待是业务SQL造成的还是后台任务。我加错过一次索引执行计划变了但性能没改善查了半天发现等待事件的源头是业务代码里嵌套循环引发的全表扫描。索引加在了错误的字段上应该先读代码再动手。AWR采样窗口要避开备份和数据加载。性能问题恰好发生在凌晨备份期间AWR报告里的数据会被备份IO干扰。在业务高峰期用DBMS_WORKLOAD_REPOSITORY.CREATE_SNAPSHOT手动采样拿干净的快照。DB time不等于等待事件时间的简单相加。很多人会把Top 5事件的百分比加起来看是不是100%。DB time包含了CPU时间和等待时间等待事件之间有重叠。百分比加起来超过100%正常的。建索引不是性能优化的终点。一口气给表加十几个索引INSERT和UPDATE会变慢。索引的维护成本要考虑。加索引之前想清楚这个索引被哪些SQL用使用频率多高写入频率多高表的写远多于读加索引得不偿失。不同数据库的AWR报告格式各有差异。Oracle的最成熟国产数据库也在做自己的性能诊断报告。比如金仓数据库有类似的诊断工具等待事件的分类逻辑和Oracle类似具体指标呈现上做了简化。核心思路通用找到时间花在哪对症下药。调优总结AWR报告就是一面镜子照出数据库在某个时间段里做了什么、慢了哪一步。我的流程看Load Profile确认负载 → 看Top 5事件找方向 → 看SQL Statistics定位语句 → 看执行计划找原因 → 加索引或改SQL → 重新采样验证。这次优化的几个关键节点Load Profile确认DB Time远大于DB CPU等待问题而非计算瓶颈。Top 5指向db file scattered read全表扫描是根因。SQL Statistics找到最慢的SQL执行计划揭示两表全表扫描加HASH JOIN。统计信息过期和基数估算偏差补了直方图。建复合索引并收集新统计信息。优化后等待事件转移到log file sync业务代码频繁COMMIT。改为批量提交性能再提升。从45分钟到8分钟再到5分半。每一步都有数据支撑。你在AWR报告里发现过的最意想不到的性能瓶颈是什么索引失效还是业务代码的提交频率欢迎一起聊聊。我是数据库小学妹咱们下篇见