1. 为什么盯 DB time 比盯 CPU 占用率更实在
头一回接触 Oracle 性能分析的人,最喜欢问的一个问题是:数据库到底忙不忙?很多人张口就是 CPU 用了多少、磁盘 IO 高不高、内存够不够。但真到了排查问题的时候你会发现,CPU、IO、内存这些都是资源侧的指标,能反映“环境紧不紧张”,却不能直接回答“数据库到底花了多少时间在处理用户请求”。真正能回答这个问题的,是 DB time。
DB time 全称 Database Time,统计的是所有前台会话在数据库内部花费的总时间。注意几个关键词:前台会话、数据库内部、总时间。前台会话指的是应用连进来的会话,后台进程比如 LGWR、DBWn 是不算在 DB time 里的;数据库内部则代表它不计入网络传输时间、应用端处理时间和客户端等待时间。所以 DB time 是一个相当纯正的“数据库工作量”指标。举个例子,如果某个时段墙钟时间是 10 分钟,而 DB time 显示 30 分钟,说明这 10 分钟里平均有 3 个前台会话同时在数据库里干活或者排队。DB time 高,意味着数据库层面负载高,不是某一两条 SQL 偶尔慢,而是整体处于繁忙状态。
在 10g 以后,Oracle 引入了 time model 体系,DB time 的累计值就存在 v$sys_time_model 里。你随时可以执行这条 SQL 查看:
sql复制SELECT stat_name, value
FROM v$sys_time_model
WHERE stat_name IN ('DB time', 'DB CPU');
value 是从实例启动以来累计的微秒数,除以 1000000 才是秒。但这里有一个让很多新手困惑的点:这个视图只能看累计值,不能直接回答“今天上午 9 点到 10 点 DB time 是多少”。因为它是从启动到现在一直累加的,不是按时间段分段的。要想拿到某一时间段的 DB time,必须依靠历史快照数据或者手工打点。
这篇文章我会把我自己常用的几种取数方式完整写出来,包括 AWR 报表、dba_hist_sys_time_model 手工 SQL、以及 ASH 近似还原,顺便把 RAC、PDB、实例重启这些场景里容易踩的坑一起讲清楚。不管你是做日常巡检、故障复盘还是容量评估,应该都能直接参考。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 时间段 DB time 的三种主流取数思路
2.1 AWR 快照与时间模型的底层逻辑
要理解“某一时间段的 DB time”,首先得知道 Oracle 是怎么把时间切成一段一段的。AWR(Automatic Workload Repository)会默认每小时采集一次快照,每个快照记录下那一刻的累计统计值,包括 DB time 的累计值。这么一来,相邻两个快照之间的 DB time 增量,就代表了那个时间段内数据库实际消耗的时间。
打个比方,v$sys_time_model 就好比汽车的里程表,只显示总里程。AWR 快照则是在不同时间点拍的仪表盘照片,两张照片上的里程数相减,就是这段时间里跑的距离。dba_hist_sys_time_model 就是存放这些历史“仪表盘读数”的仓库。
2.2 三条取数路径分别适合什么场景
在实际项目里,我一般会根据需求精度和工具条件选路径:
| 需求场景 | 推荐方式 | 精度 | 依赖条件 |
|---|---|---|---|
| 快速看某小时总体情况,写报告 | AWR 报表(awrrpt.sql) | 分钟级,随快照粒度 | 目标时段存在 AWR 快照 |
| 自定义时间段、跨多个快照、按小时统计趋势 | dba_hist_sys_time_model 手工 SQL |
分钟级,可精确到快照边界 | 快照保留期内 |
| 快照粒度太粗,或没有对应 AWR 快照 | ASH 历史会话近似 | 秒级,统计近似 | 有 ASH 数据,通常默认保留 1 小时到数天 |
| 正在发生的故障,实时观测 | v$sys_time_model 前后两次取值做差 |
任意精度 | 会话还连着,实例未重启 |
第三条路径 ASH 是很多人忽略的。AWR 快照默认 1 小时一个,如果业务高峰只有 10 分钟,又恰好落在两个快照中间,快照粒度根本切不出这 10 分钟的 DB time。这时候 ASH(Active Session History)就派上用场了,它是秒级采样的活动会话历史,能把分钟级的问题看得更细。
2.3 无论哪条路,单位换算都不能马虎
DB time 在历史表里存的单位同样是微秒。很多 DBA 第一次写 SQL 取数,直接拿 value 相减,以为结果就是秒,结果差了好几个数量级。正确换算方式如下:
- 微秒 → 秒:
value / 1000000 - 秒 → 分钟:
value / 1000000 / 60
AWR 报表里显示的时间通常已经换算过了,比如报表头部的 DB Time: 345.67 (mins)。但手工查询 dba_hist_sys_time_model 时,字段 value 依然是微秒,这种一眼看上去的“坑”在实际操作里最常见。
3. 手工查询 dba_hist_sys_time_model 的完整路线
3.1 基础版 SQL:相邻快照间 DB time 增量
我最常用、也最推荐大家掌握的是这条 SQL。它利用 dba_hist_snapshot 关联 dba_hist_sys_time_model,然后把每个快照的 DB time 累计值减去上一个快照的累计值,得到这一段区间内的 DB time。
sql复制SELECT
snap_id,
instance_number,
TO_CHAR(end_interval_time, 'YYYY-MM-DD HH24:MI:SS') AS snap_time,
ROUND((curr_value - prev_value) / 1000000 / 60, 2) AS db_time_mins
FROM (
SELECT
m.snap_id,
m.instance_number,
m.value AS curr_value,
LAG(m.value) OVER (
PARTITION BY m.dbid, m.instance_number
ORDER BY m.snap_id
) AS prev_value,
s.end_interval_time
FROM dba_hist_sys_time_model m
JOIN dba_hist_snapshot s
ON s.snap_id = m.snap_id
AND s.dbid = m.dbid
AND s.instance_number = m.instance_number
WHERE m.stat_name = 'DB time'
)
WHERE prev_value IS NOT NULL
AND end_interval_time >= TO_TIMESTAMP('2024-06-01 08:00:00', 'YYYY-MM-DD HH24:MI:SS')
AND end_interval_time <= TO_TIMESTAMP('2024-06-01 18:00:00', 'YYYY-MM-DD HH24:MI:SS')
ORDER BY instance_number, snap_id;
看到 LAG 别觉得复杂,它其实就是在同一实例、同一 DBID 下,按 snap_id 排序后把上一行的 value 挪到当前行来。这样即使两个快照之间隔了不止一小时,比如有人手工删过快照,只要前一个快照还在,就能正确计算增量。相比之下,使用 s.snap_id - 1 这样硬减的方式,一旦中间缺失快照就会出错,所以我更推荐 LAG 版本。
3.2 按小时统计趋势:一段时间的负载走势一目了然
如果不想只看一个总时间段,而是想看某个业务高峰期的 DB time 是怎么变化的,可以把相邻快照的增量按小时聚合。默认 AWR 快照是每小时一次,所以直接按 end_interval_time 截断到小时再汇总即可:
sql复制SELECT
instance_number,
TO_CHAR(end_interval_time, 'YYYY-MM-DD HH24:00') AS hour_bucket,
ROUND(SUM((curr_value - prev_value) / 1000000 / 60), 2) AS db_time_mins
FROM (
SELECT
m.snap_id,
m.instance_number,
m.value AS curr_value,
LAG(m.value) OVER (
PARTITION BY m.dbid, m.instance_number
ORDER BY m.snap_id
) AS prev_value,
s.end_interval_time
FROM dba_hist_sys_time_model m
JOIN dba_hist_snapshot s
ON s.snap_id = m.snap_id
AND s.dbid = m.dbid
AND s.instance_number = m.instance_number
WHERE m.stat_name = 'DB time'
)
WHERE prev_value IS NOT NULL
AND end_interval_time >= TO_TIMESTAMP('2024-06-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
AND end_interval_time <= TO_TIMESTAMP('2024-06-02 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
GROUP BY instance_number, TO_CHAR(end_interval_time, 'YYYY-MM-DD HH24:00')
ORDER BY hour_bucket, instance_number;
跑出来的结果会是一个小时一行,每个小时对应一个 DB time 分钟数。如果 9 点到 10 点那行特别高,比如 200 分钟,而这个小时实际只有 60 分钟,说明那个小时平均有 3.3 个会话同时在消耗数据库资源。这样的数据贴到日报或者复盘文档里,比干巴巴说“CPU 飙到 90%”要有说服力得多。
3.3 RAC 与多租户 PDB 的特殊处理
RAC 环境下每个实例都有自己的时间模型统计,instance_number 会区分不同实例。查询时记得不要遗漏 instance_number 分区条件,否则会把多个实例的累计值混在一起算,结果肯定不对。上面 SQL 里我已经用 PARTITION BY m.dbid, m.instance_number 做了隔离,汇总时也要保留 instance_number。
Oracle 12c 之后,如果你是在多租户环境里管理 PDB,还需要注意一个点:dba_hist_sys_time_model 反映的是整个 CDB 层面的 DB time,不是某个 PDB 的。想看某个 PDB 的时间段 DB time,需要查 dba_hist_pdb_sys_time_model。它的结构和 dba_hist_sys_time_model 类似,多了一个 con_id 或者 pdb_id 字段,关联 dba_hist_pdb_snapshot 时要注意口径。
4. 用 AWR 报表快速定位区间 DB time 的实操
4.1 生成 AWR 报告的完整步骤
手工 SQL 灵活,但如果只是被业务方问一句“昨天早上 10 点到 11 点数据库负载高不高”,直接出一份 AWR 报告反而更快。AWR 报告是 Oracle 自带功能,不需要额外安装任何工具。
在服务器上用 sqlplus 登录数据库,然后调用脚本:
bash复制sqlplus / as sysdba
SQL> @?/rdbms/admin/awrrpt.sql
脚本会先问你要生成 HTML 还是文本格式,一般给业务方看选 HTML,自己快速阅读取文本也够。接着会问你选择天数,比如输入 1 代表最近一天,然后列出可用的快照 ID 和时间范围。你只要挑出目标时段的首尾快照 ID 输入进去,报告就生成了。
还有一种更省事的方式,直接用 awrrpt.sql 加参数:
sql复制-- 生成指定开始快照和结束快照的 HTML 报告
SELECT * FROM TABLE(DBMS_WORKLOAD_REPOSITORY.AWR_REPORT_HTML(
dbid => 1234567890,
inst_num => 1,
dbid => 1234567890,
begin_snap => 45678,
end_snap => 45679
));
4.2 报表里 DB time 相关的位置在哪里
拿到 AWR 报告后,不要急着往下翻,先看报表头部。标准 AWR 报告中会有一段类似这样的信息:
code复制Elapsed: 60.24 (mins)
DB Time: 180.65 (mins)
这里 DB Time 就是这段快照区间内的数据库时间总和,单位是分钟。如果 DB Time 接近甚至超过 Elapsed 的三倍,说明这个时间段数据库相当繁忙。
除了头部,还要关注两个区域。第一是 “Time Model Statistics” 部分,里面会列出 DB time、DB CPU、background elapsed time 等明细,并且标注了 % DB time。比如 DB CPU 占 DB time 的 35%,那剩下的 65% 就是各种等待事件消耗的时间。第二是 “Top 5 Timed Events” 部分,可以直接看到 DB time 主要花在了哪些等待上。定位性能问题的时候,这两个区域基本上是必看的。
4.3 AWR 边界快照不够精准时的处理策略
AWR 报告最大的局限是:它只能以快照为边界,不能精确到任意时间点。假设你发现业务高峰期是 10:20 到 10:50,但快照是 10:00 和 11:00,AWR 报告会把整个 10:00 到 11:00 的时间段都算进去,其中包含着不相关的低峰时间。
这种情况下,我的做法是先看 AWR 报告判断大致方向,再回到 dba_hist_sys_time_model 或者 ASH 去细化。如果为了复盘一个严重故障,可以在问题发生前手工创建一个快照,结束后再手工创建一个快照,这样就能框出精确区间:
sql复制EXEC DBMS_WORKLOAD_REPOSITORY.CREATE_SNAPSHOT();
不过这是“事后补救”的办法,已经发生的事情没法回到过去打快照,所以 ASH 才显得格外重要。
5. 在故障窗口或亚分钟粒度场景下用 ASH 近似还原
5.1 活动会话数如何折算出 DB time
AWR 快照再密,也做不到秒级。如果业务方告诉你“10:42 到 10:46 这四分钟数据库卡死了”,你拿着小时级 AWR 快照根本没法精确验证。这时候就要上 ASH。
ASH 的全称是 Active Session History,Oracle 每秒会为数据库中的活动会话采集一个样本,记录在内存中,并周期性刷到 dba_hist_active_sess_history。它的逻辑可以这样理解:每个样本代表“某个会话在这一秒内处于活动状态”。所以,如果一个时段有 600 个样本,就相当于这段期间活动会话累计占用了 600 秒的数据库时间,数值上正好对应 DB time 的近似值。
5.2 ASH 聚合 SQL 示例
比如我想查 2024 年 6 月 1 日 10:42 到 10:46 这 4 分钟内的近似 DB time,可以执行:
sql复制SELECT
ROUND(COUNT(*) / 240, 2) AS avg_active_sessions,
COUNT(*) AS approx_db_time_seconds
FROM dba_hist_active_sess_history
WHERE sample_time >= TO_TIMESTAMP('2024-06-01 10:42:00', 'YYYY-MM-DD HH24:MI:SS')
AND sample_time < TO_TIMESTAMP('2024-06-01 10:46:00', 'YYYY-MM-DD HH24:MI:SS');
COUNT(*) 是这 240 秒内所有活动会话的样本总数,数值上等同于“活动会话数 × 时间秒”的总和,也就是近似的 DB time。比如结果是 720,那就说明这 4 分钟里平均有 3 个活动会话在忙(720 / 240 = 3)。
如果只想看具体是哪些 SQL 或等待事件占用了这些时间,可以按 sql_id、session_state、event 分组:
sql复制SELECT
sql_id,
session_state,
event,
COUNT(*) AS ses_seconds
FROM dba_hist_active_sess_history
WHERE sample_time >= TO_TIMESTAMP('2024-06-01 10:42:00', 'YYYY-MM-DD HH24:MI:SS')
AND sample_time < TO_TIMESTAMP('2024-06-01 10:46:00', 'YYYY-MM-DD HH24:MI:SS')
GROUP BY sql_id, session_state, event
ORDER BY ses_seconds DESC;
5.3 ASH 近似法的精度边界
必须承认,ASH 不是精确的 DB time 统计。它每秒采样一次,如果会话在两次采样之间短暂激活又结束,可能会被漏掉;反过来说,一个会话在同一秒内多次切换等待事件,也可能只保留其中一个样本。因此 ASH 更适合用来排查趋势、相对比较、定位热点,不适合作为严格精确的计费依据。
在故障复盘类场景里,我通常会把 ASH 结果和 AWR 报告的 DB time 对照一下。如果两者数量级一致,说明采样可信;如果差别特别大,那就优先以 AWR 和 dba_hist_sys_time_model 为准,ASH 只做参考。
6. 真实场景中的避坑清单与判断经验
6.1 实例重启会让增量统计变成负数
这是最容易踩的坑。dba_hist_sys_time_model 里的累计值是从实例启动开始算的,一旦实例重启,累计值会归零重新开始。此时如果直接拿重启前最后一个快照和重启后第一个快照相减,会得到一个负数,而且毫无意义。
我的处理方式是在 SQL 里增加一层过滤,把 curr_value - prev_value < 0 的行过滤掉,或者关联 dba_hist_snapshot 中的 startup_time,确保两个快照属于同一次实例运行周期。如果确实需要包含重启后那一段,应当从重启后第一个有效快照重新开始累计,不要跨重启边界做减法。
6.2 快照缺失与 AWR 保留期
默认 AWR 保留 8 天,快照间隔 60 分钟。有些系统维护窗口会关闭快照采集,或者因为磁盘问题删过部分历史快照,导致目标时间段根本没有快照数据。遇到这种情况,可以先查一下 dba_hist_snapshot 在目标时段内是否连续:
sql复制SELECT snap_id, TO_CHAR(begin_interval_time, 'YYYY-MM-DD HH24:MI') AS begin_time,
TO_CHAR(end_interval_time, 'YYYY-MM-DD HH24:MI') AS end_time
FROM dba_hist_snapshot
WHERE begin_interval_time >= TO_TIMESTAMP('2024-06-01 08:00:00', 'YYYY-MM-DD HH24:MI:SS')
AND begin_interval_time <= TO_TIMESTAMP('2024-06-01 18:00:00', 'YYYY-MM-DD HH24:MI:SS')
ORDER BY snap_id;
如果发现中间缺了一段,要么用 ASH 补,要么接受“只能取可用快照之间最大连续区间”的局限。
6.3 DB time 高不代表数据库一定有性能问题
这一点我反复跟团队强调:DB time 高只是说明数据库负载高,不代表一定有严重问题。比如一个批量任务在跑,大量会话都在正常使用 CPU,DB time 自然会很高,但这是预期中的负载。真正需要警惕的是 DB time 很高,同时 DB CPU 占比很低、等待事件占比很高,比如大量时间花在 enq: TX - row lock contention、buffer busy waits、log file sync 上。这说明系统不是忙于计算,而是在“等”,等锁、等内存、等 IO,这种时候才需要介入优化。
判断方式非常简单:DB time 减去 DB CPU,剩下的基本就是等待时间。等待占比越高,系统越“虚忙”,排查优先级也越高。
6.4 我的几条实操心得
第一,取数之前先搞清楚数据库是全球还是 RAC、是不是多租户 PDB,这决定了你要查哪个视图、要不要按实例汇总。第二,写 SQL 时尽量用 LAG 而不是 snap_id - 1,因为快照 ID 连续不等于时间连续,缺失快照的坑我踩过很多次。第三,单位换算不可省,把微秒当秒用的人,最后都会被数据狠狠教育一遍。
如果你只是临时想确认当前实例启动以来的 DB time,v$sys_time_model 是最快的路径;但如果要复盘一个过去的时间段,我的首选永远是 dba_hist_sys_time_model 结合 LAG 窗口函数做增量计算,需要秒级精度再切到 ASH。这套方法我在多个生产环境里反复用过,不管是 Oracle 11g 还是 19c,取数逻辑基本一致,只要注意 PDB 和 RAC 的差异,就能稳定拿到准确结果。
