Oracle性能调优最佳实践:AWR报告分析与执行计划优化实战
大家好,我是数据库小学妹 👋
客户电商平台的核心报表跑了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_history(ASH)里挖。
生成AWR报告,两条SQL:
-- 查看可用的快照ID
SELECT snap_id, begin_interval_time, end_interval_time
FROM dba_hist_snapshot
ORDER BY snap_id DESC
FETCH FIRST 10 ROWS ONLY;
-- 生成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 * FROM TABLE(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 LOOP?HASH JOIN适合大表对大表的连接,把小表(驱动表)的Join Key做成Hash表,然后扫描大表逐行匹配。NESTED LOOP适合驱动表返回少量数据,逐行去被驱动表查索引。
优化器认为两个表都会返回500K行,所以选了HASH JOIN。理论没问题。但500K这个估值错了。
看Predicate Information:
Predicate 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)都依赖统计信息做决策。统计信息不准,决策就不准。
根源是统计信息过期。查一下:
-- 检查表统计信息是否过期
SELECT table_name, num_rows, last_analyzed, stale_stats
FROM dba_tab_statistics
WHERE table_name IN ('ORDERS', 'CUSTOMERS');
ORDERS表的last_analyzed是三个月前。这期间订单量翻了一倍。优化器用旧统计信息做决策,估出来的Rows自然不准。
还有一个容易忽略的点:倾斜列。STATUS字段只有4个值(待支付、已支付、已发货、已完成),分布极不均匀。已完成的占80%,待支付的只有5%。没有直方图的话,优化器用num_rows/ndv做均匀分布假设,估出来的基数偏差很大。
补直方图:
EXEC DBMS_STATS.GATHER_TABLE_STATS('SCHEMA_NAME', 'ORDERS',
METHOD_OPT => 'FOR COLUMNS STATUS SIZE AUTO');
STATISTIC_LEVEL参数也会影响统计信息收集。设成BASIC的话,很多扩展统计信息不会自动收集。生产环境建议设成TYPICAL。
制定优化方案
第一个动作:给ORDERS表建复合索引。按查询条件排序,区分度高的放前面。
-- 分析索引选择性
SELECT COUNT(DISTINCT create_date) / COUNT(*) AS date_selectivity,
COUNT(DISTINCT status) / COUNT(*) AS status_selectivity
FROM orders;
跑完选择性分析,CREATE_DATE的选择性是0.8,STATUS只有0.05。区分度高的放前面,索引过滤效率更高。
-- 创建复合索引
CREATE INDEX idx_orders_date_status ON orders(create_date, status);
-- 创建REGION索引
CREATE INDEX idx_customers_region ON customers(region);
-- 收集统计信息,让优化器知道索引的存在
EXEC DBMS_STATS.GATHER_TABLE_STATS('SCHEMA_NAME', 'ORDERS');
EXEC DBMS_STATS.GATHER_TABLE_STATS('SCHEMA_NAME', 'CUSTOMERS');
生产环境加索引要用ONLINE关键字,否则建索引期间表会被锁,业务中断。
CREATE INDEX idx_orders_date_status ON orders(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.21
db file scattered read从42%降到3%。log file sync升到了38%。等待事件转移了。
消除一个瓶颈,另一个就浮出来。这是调优的常态。
log file sync变高,说明提交频率成了问题。每次COMMIT,LGWR进程要把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数据。
-- 检查采样窗口内是否有统计信息收集任务
SELECT operation, start_time, end_time
FROM dba_optstat_operations
WHERE start_time BETWEEN TO_DATE('2026-01-15 10:00', 'YYYY-MM-DD HH24:MI')
AND TO_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报告里发现过的最意想不到的性能瓶颈是什么?索引失效?还是业务代码的提交频率?欢迎聊聊。
我是数据库小学妹,咱们下篇见 👋
- 点赞
- 收藏
- 关注作者
评论(0)