做DBA或者Oracle运维的人,十有八九都遇到过这种场景:生产库突然告警,CPU飙到99%,你连上数据库第一件事就是想知道“现在到底在跑什么SQL”;又或者是开发跑过来问“昨天我那几条SQL到底执行了没有,执行了几次,跑了多久”,你却翻遍了工具都找不到痕迹。
在Oracle 12c里,查“正在执行的SQL”和“查执行过的SQL”其实是两个完全不同的命题,用的视图、思路、权限要求都不一样。很多人上来就只知道一个v$session和v$sql,结果要么查不全,要么查不到历史,要么被SQL_TEXT截断坑得怀疑人生。这篇文章我就把这套东西彻底捋一遍,从视图原理到实操SQL,到12c特有的新功能,再到我踩过的坑,一次讲清楚。
1. 先分清两个概念:正在执行和曾经执行
1.1 为什么DBA天天都在查这两个东西
先说“正在执行的SQL”。这个场景通常出现在故障排查的黄金窗口期——数据库hang住了,锁表了,某个会话把资源吃满了,你必须在几秒内定位到那个罪魁祸首。这时候查的是当前这一秒数据库在做什么,是动态的、瞬时的。
再说“执行过的SQL”。这个场景更多是事后分析——晚上跑批慢了,某条SQL走了全表扫描导致性能劣化,开发想确认某条语句到底有没有被调用过。这时候你要查的是过去一段时间内数据库执行了什么,是静态的、历史性的。
这两个需求在Oracle里的实现路径完全不同。当前正在执行的要靠v$session这种动态性能视图(也就是常说的固定视图),而历史SQL则要靠共享池缓存(v$sql)、AWR快照(dba_hist_sqltext)或者审计功能。很多新手栽跟头,就是因为把这两类混为一谈,拿着v$sql去查“昨天的SQL”,发现啥也没有,就开始怀疑数据库坏了——其实只是没搞懂数据生命周期。
1.2 12c里相关的几个核心视图
Oracle 12c里跟SQL查询相关的核心视图大概有这么几类,我列个表大家先有个整体印象:
| 视图/功能 | 能查到什么 | 数据保留时间 | 适用场景 |
|---|---|---|---|
v$session |
当前所有会话状态,包括正在执行的SQL_ID | 实时,会话结束即消失 | 定位正在执行的SQL、锁等待 |
v$sql / v$sqlarea |
共享池中缓存的SQL文本、执行次数、资源消耗 | 受共享池空间影响,可能随时被age out | 查最近执行过的SQL及其统计 |
v$sql_monitor |
正在执行或刚执行完的SQL的详细运行时统计 | 默认保留最近几分钟(受_sqlmon_max_plan等参数影响) |
实时监控长SQL、并行SQL |
dba_hist_sqltext |
AWR快照收集过的SQL全文 | 取决于AWR保留策略,默认8天 | 查历史SQL文本 |
dba_hist_sqlstat |
AWR快照中SQL的执行统计 | 同上 | 查历史SQL的性能指标 |
dba_audit_trail |
审计记录中的SQL | 取决于审计保留策略 | 合规审计、追责 |
unified_audit_trail |
12c统一审计记录 | 取决于保留策略 | 12c推荐使用的审计查询方式 |
这里有个关键点要记住:v$开头的视图(或12c中的v$)只在内存中,实例重启就清空;dba_hist_开头的视图来自AWR快照,存储在SYSAUX表空间里,能跨重启保留;审计记录则写在内核日志表中(也可能是OS文件,取决于审计模式)。理解了这一点,你查SQL的时候思路就清晰了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 当前正在执行的SQL:三招定位
2.1 第一招:V$SESSION加V$SQL,最直观的组合
这是我最常用的第一板斧。核心思路很简单:v$session里记录了每个会话当前正在执行的sql_id,拿这个sql_id去关联v$sql或v$sqlarea,就能把SQL文本捞出来。
sql复制SELECT a.sid,
a.serial#,
a.username,
a.status,
a.sql_id,
a.sql_child_number,
b.sql_text,
a.event,
a.wait_class,
a.machine,
a.program,
a.logon_time,
a.last_call_et
FROM v$session a,
v$sql b
WHERE a.sql_id = b.sql_id
AND a.status = 'ACTIVE'
AND a.type = 'USER'
ORDER BY a.last_call_et DESC;
last_call_et是会话最后一次调用到现在过去的秒数,对正在执行的SQL来说,基本就是这条SQL已经跑了多久。用last_call_et降序排,第一时间就能看到跑得最久的会话,这在处理锁等待和长事务时特别有用。
但这里有个要注意的地方:v$session.status = 'ACTIVE'并不完全等于“正在执行SQL”。一个处于ACTIVE状态的会话可能是在CPU上跑SQL,也可能是在等待某个事件(比如等锁、等I/O),或者是正在做PGA排序。所以我把wait_class和event也一起查出来,这样可以区分它到底是“真干活”还是“卡住了”。如果一个会话last_call_et很大,但event是enq: TX - row lock contention,那它就是在等锁而不是在算SQL,处理方向完全不一样。
2.2 第二招:V$SQLMONITOR,12c的实时监控利器
如果你查了v$session发现SQL在跑,但还想知道它的实时执行进度、消耗了多少内存、有没有走并行,那就得上v$sql_monitor。这是Oracle 11g引入、12c里非常好用的实时SQL监控功能,对DBA来说是查“正在执行SQL”的详细情报来源。
sql复制SELECT sql_id,
status,
elapsed_time / 1000000 AS elapsed_secs,
cpu_time / 1000000 AS cpu_secs,
buffer_gets,
disk_reads,
sql_text
FROM v$sql_monitor
WHERE status = 'EXECUTING'
ORDER BY elapsed_time DESC;
这里的时间单位是微秒,所以记得除以1000000换算成秒。v$sql_monitor里能看到SQL的实时执行计划和每一步的IO、行数、耗时,这比单纯看文本强太多了。你可以配合DBMS_SQLTUNE.REPORT_SQL_MONITOR函数生成一份可读性很强的文本报告:
sql复制SELECT DBMS_SQLTUNE.REPORT_SQL_MONITOR(sql_id => '你的SQL_ID') FROM dual;
输出就是一份完整的监控报告,包含执行计划、每个步骤的实际行数、耗时、内存使用等。我一般在处理“这个SQL到底慢在哪一步”的问题时,直接用它,比自己一行行去分析执行计划高效得多。
有一点要注意:v$sql_monitor默认只对执行时间超过5秒(或并行执行的SQL)启用监控,短SQL可能没有记录。这个阈值受隐含参数_sqlmon_max_time控制,一般不建议随便改。
2.3 第三招:通过操作系统进程反查
有时候你遇到极端情况:v$session查不到任何ACTIVE会话,但操作系统上top显示有个Oracle进程CPU爆满。这说明会话可能正处于一种“非数据库活动”的状态,比如在做PL/SQL递归调用、在解析SQL、或者在等待某个非空闲事件,status没有及时刷新。
这种情况下可以从操作系统层面反查。先在OS上找到那个耗CPU的进程PID,然后关联到v$process和v$session:
sql复制SELECT p.spid,
s.sid,
s.serial#,
s.username,
s.sql_id,
s.event,
s.wait_class,
s.last_call_et
FROM v$process p,
v$session s
WHERE p.addr = s.paddr
AND p.spid = '你的OS进程PID';
这个“从OS到数据库”的反查思路在处理CPU 100%问题时特别有效。我遇到过一个案例,top显示某个进程CPU占满,v$session里却只有一堆INACTIVE会话,后来用这个反查SQL定位到是一条正则表达式函数疯狂吃CPU,而session状态停留在INACTIVE是因为语句在极端CPU密集计算时不会更新状态位。
3. 执行过的SQL:缓存、历史、审计三条路
3.1 V$SQL/V$SQLAREA:共享池里的“最近执行”
先说最容易理解的v$sql和v$sqlarea。Oracle执行任何SQL时,如果共享池里已经有相同文本的SQL,就直接复用游标;如果没有,就硬解析并缓存到共享池。所以理论上,只要SQL还在共享池里,你就可以在v$sql中查到它。
v$sql是游标级别的,同一个SQL_ID如果有多个子游标(比如因为绑定变量长度不同、NLS参数不同导致的重新解析),会出现多行;v$sqlarea是SQL文本级别的聚合,同一文本合并为一行,执行次数、资源消耗都做了汇总。日常排查性能问题我更推荐v$sqlarea,看起来干净。
sql复制SELECT sql_id,
executions,
buffer_gets,
disk_reads,
cpu_time / 1000000 AS cpu_secs,
elapsed_time / 1000000 AS elap_secs,
sql_text
FROM v$sqlarea
WHERE sql_text NOT LIKE '%v$sqlarea%'
ORDER BY buffer_gets DESC
FETCH FIRST 20 ROWS ONLY;
注意,这里我用了FETCH FIRST 20 ROWS ONLY,这是12c引入的SQL行限制语法,比11g用ROWNUM方便多了。如果你的库里还有11g的脚本习惯,赶紧换成这个。
ORDER BY buffer_gets DESC是按逻辑读排序,能快速找出“吃掉最多内存的SQL”。如果你更关心CPU消耗,就把排序字段换成cpu_time;关心执行时长就换elapsed_time。这个查询的本质是按资源消耗给共享池里的SQL排个名次,是性能优化的入场券。
3.2 DBA_HIST_SQLTEXT:AWR快照里的历史档案
共享池是有限的内存空间,SQL多了、时间久了,那些老SQL就会被age out,从v$sql里消失。这时候想查“昨天跑过的SQL”,就要去AWR快照里捞了。
AWR默认每小时采集一次快照,快照时会把当时在共享池里的一批SQL连同执行统计一起存到dba_hist_sqlstat,SQL文本则存到dba_hist_sqltext。这两个表通过sql_id和dbid关联。
sql复制SELECT sql_id, sql_text
FROM dba_hist_sqltext
WHERE sql_id = '你的SQL_ID';
如果不知道SQL_ID,只知道大概文本片段,可以模糊查:
sql复制SELECT DISTINCT sql_id, sql_text
FROM dba_hist_sqltext
WHERE sql_text LIKE '%表名%'
AND sql_text NOT LIKE '%dba_hist_sqltext%';
要查某条SQL在某段时间内的执行情况,需要把dba_hist_sqlstat和dba_hist_snapshot关联起来:
sql复制SELECT s.begin_interval_time,
d.sql_id,
d.executions_delta,
d.buffer_gets_delta,
d.disk_reads_delta,
d.elapsed_time_delta / 1000000 AS elap_secs_delta,
t.sql_text
FROM dba_hist_sqlstat d,
dba_hist_snapshot s,
dba_hist_sqltext t
WHERE d.snap_id = s.snap_id
AND d.dbid = s.dbid
AND d.sql_id = t.sql_id
AND d.dbid = t.dbid
AND d.sql_id = '你的SQL_ID'
ORDER BY s.begin_interval_time DESC;
注意这些_delta列是“该快照周期内的增量值”,不是累计值。executions_delta就是这个小时内这条SQL被执行的次数,elapsed_time_delta就是这小时内累计执行时间。
3.3 审计与FGA:完整执行记录的最后防线
如果v$sql里查不到(被age out),AWR里也没有(执行时间太短,快照没抓到),但你又确实需要证明某条SQL“在某个时刻执行过”——那就只能靠审计了。
12c默认开启了统一审计(Unified Auditing),和传统审计相比查询更方便。你可以在数据库级别开启对指定类型的审计:
sql复制-- 开启对DML语句的审计(根据实际需求调整)
AUDIT SELECT TABLE, INSERT TABLE, UPDATE TABLE, DELETE TABLE BY ACCESS;
然后查询统一审计视图:
sql复制SELECT event_timestamp,
dbusername,
osusername,
userhost,
action_name,
sql_text,
sql_bind
FROM unified_audit_trail
WHERE event_timestamp > SYSDATE - 1
ORDER BY event_timestamp DESC;
要说明一下,传统审计默认只记录“执行了什么操作”,不保证记录完整的SQL文本。如果你需要把某张敏感表的完整SQL文本和绑定变量都留下来,就得用细粒度审计(FGA,Fine-Grained Auditing)。
sql复制BEGIN
DBMS_FGA.ADD_POLICY(
object_schema => 'SCOTT',
object_name => 'EMP',
policy_name => 'EMP_SEL_POLICY',
audit_condition => '1=1',
enable => TRUE,
statement_types => 'SELECT, INSERT, UPDATE, DELETE'
);
END;
/
策略建好之后,所有对SCOTT.EMP的DML和SELECT都会生成审计记录,而且默认带完整SQL文本和绑定变量值:
sql复制SELECT timestamp,
db_user,
os_user,
userhost,
sql_text,
sql_bind
FROM dba_fga_audit_trail
ORDER BY timestamp DESC;
FGA是把双刃剑,开启后会对目标表的访问产生额外开销,特别是高频访问的表影响尤其明显。我的建议是只在真正需要合规审计的敏感表上开启,不要全局开,否则生产环境的性能代价会让你后悔。
4. 实操组合:日常排查的命令模板
4.1 我常用的几个查询模板
直接给几个可以直接抄的模板,都是我一线上在用、打磨过的版本。
模板一:查所有正在执行的用户SQL(含执行时间)
sql复制SELECT s.sid,
s.serial#,
s.username,
s.sql_id,
s.event,
s.wait_class,
s.last_call_et AS 已执行秒数,
s.machine,
s.program,
SUBSTR(b.sql_text, 1, 200) AS sql_text
FROM v$session s,
v$sql b
WHERE s.sql_id = b.sql_id
AND s.type = 'USER'
AND s.status = 'ACTIVE'
AND s.last_call_et > 5
ORDER BY s.last_call_et DESC;
我把last_call_et > 5加进去了,过滤掉刚开始的短SQL,这样输出不会太吵。SUBSTR是为了防止SQL太长刷屏。生产上执行这条,几秒内就能锁定“谁在跑大SQL”。
模板二:查RAC所有节点正在执行的SQL
12c RAC环境中,单节点的v$session只能看本节点,要用gv$session:
sql复制SELECT inst_id,
sid,
serial#,
username,
sql_id,
event,
wait_class,
last_call_et AS 已执行秒数,
machine,
program
FROM gv$session
WHERE type = 'USER'
AND status = 'ACTIVE'
ORDER BY last_call_et DESC;
RAC环境下加了一列inst_id,能直接看到这条SQL跑在哪个节点上。这在排查节点间负载不均时特别好用。
模板三:根据SQL_ID查执行计划
查到SQL_ID之后,通常还要看执行计划是否正常。用DBMS_XPLAN.DISPLAY_CURSOR可以直接从共享池取执行计划:
sql复制SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('你的SQL_ID', NULL, 'ALLSTATS LAST'));
这里ALLSTATS LAST表示显示最后一次执行的运行时统计,能看到每一步实际返回的行数、耗时,比单纯的EXPLAIN PLAN实锤多了。
4.2 绑定变量值的查看
查历史SQL时,光有SQL文本还不够,你还得知道当时的绑定变量值是什么,才能复现问题。Oracle在v$sql_bind_capture里保留了绑定变量的捕获值(并非每个执行都捕获,只在硬解析或某些特殊时点捕获):
sql复制SELECT b.sql_id,
b.name AS bind_name,
b.value_string AS bind_value,
b.last_captured
FROM v$sql_bind_capture b
WHERE b.sql_id = '你的SQL_ID'
ORDER BY b.last_captured DESC;
如果你是找正在执行的SQL,直接通过v$session把会话的SID,SERIAL#拿过来,然后反查:
sql复制SELECT s.sid,
s.serial#,
s.sql_id,
b.name AS bind_name,
b.value_string AS bind_value
FROM v$session s,
v$sql_bind_capture b
WHERE s.sql_id = b.sql_id
AND s.sid = &SESSION_SID
AND s.serial# = &SESSION_SERIAL;
这个功能在做SQL性能分析时几乎是必需品。我之前遇到过一个经典场景:开发反馈同一个SQL一会快一会慢,查了绑定变量才发现,变量值分别是小范围主键和全表量级的选择率,执行计划当然会天差地别。不看绑定变量,这种“薛定谔的SQL性能”是永远解释不清的。
4.3 SQL全文获取与截断处理
还有一个高频坑:v$sql.sql_text和v$sqlarea.sql_text是VARCHAR2(1000),超过1000字符的SQL会被截断。当你处理的SQL是一大坨几百行的报表语句时,靠这个字段根本看不全。
12c里提供了sql_fulltext这个CLOB字段,配合DBMS_LOB.SUBSTR就能取到完整SQL的前几K字符:
sql复制SELECT sql_id,
DBMS_LOB.SUBSTR(sql_fulltext, 4000, 1) AS sql_text_excerpt
FROM v$sql
WHERE sql_id = '你的SQL_ID';
注意DBMS_LOB.SUBSTR的第三个参数是起始位置,单位是字符,不是字节。如果SQL长到离谱,可以分几次截:
sql复制SELECT DBMS_LOB.SUBSTR(sql_fulltext, 4000, 4001) AS sql_text_part2
FROM v$sql
WHERE sql_id = '你的SQL_ID';
DBA_HIST_SQLTEXT里的sql_text也是CLOB,同样可以用这个办法取。这个技巧能帮你省下不少“看着截断的SQL猜全貌”的冤枉时间。
5. 常见问题与避坑
5.1 为什么查不到SQL
这是被问得最多的问题:明明SQL执行过了,v$sql里却没有。原因通常有以下几种:
- 共享池太小或压力大,SQL刚执行完就被age out了。遇到这种情况,可以尝试扩大共享池(
SGA_TARGET或SHARED_POOL_SIZE),但这只是缓解,不能根治。 - 查询时间点距离执行时间太远,这是数据生命周期决定的,正常现象。
- SQL有非共享属性,比如使用了
CURSOR_SHARING=EXACT(默认)且文本因大小写、空格不同而不同,每次都是硬解析,缓存里会出现海量相似但不同的SQL,单条反而容易被挤出。 v$sql只记录普通SQL会话,如果是PL/SQL内部调用的SQL,文本可能被拆分成多个部分,而且有些递归SQL(如空间索引、延迟约束检查)默认不会出现在v$sql中。
遇到查不到的情况,先别急着怀疑环境,按上面四个方向排查,基本能定位。
5.2 已执行SQL被age out了怎么办
如果你的SQL已经不在共享池,但还在AWR保留期(默认8天)内,就去dba_hist_sqltext里查。AWR快照每小时采集一次,所以只要执行时点落在快照采样附近,就有机会被记录。为了确认快照有没有覆盖到你关心的时段,可以先查一下快照列表:
sql复制SELECT snap_id, begin_interval_time, end_interval_time
FROM dba_hist_snapshot
ORDER BY snap_id DESC
FETCH FIRST 20 ROWS ONLY;
如果快照里也没有,那基本只能靠审计了。所以对生产环境,我建议至少开启统一审计,否则真的出了“SQL执行责任争议”问题,你拿不出证据会非常被动。
5.3 权限不足与视图不显示
12c里v$sql_monitor、v$session等视图需要SELECT_CATALOG_ROLE或SELECT ANY DICTIONARY权限才能查全。普通开发账号查v$session可能只能看到自己的会话,看不到别人的。DBA账号没这个问题,但如果你用的是一个受限的运维账号,可能查出来的结果是“空的”,不是没有SQL,而是权限不够。
另外,12c的多租户架构(CDB/PDB)要特别注意:在PDB内查v$session默认只能看到当前PDB的会话;要想看全库,得在CDB$下查询或者使用gv$视图。很多从11g转到12c的DBA都在这上面栽过跟头,查半天以为数据库没活动,其实只是被容器隔离了。
5.4 查询本身反而拖垮数据库
最后说个我自己经历过的尴尬事:刚开始做DBA那会,遇到数据库卡顿就拼命执行各种v$session、v$sql_area查询,结果越查越卡。后来才意识到,大型的v$sqlarea排序查询本身需要大量的PGA内存和CPU,在系统已经繁忙的时候执行这种查询,等于雪上加霜。
现在我的习惯是:系统已经卡顿的时候,先跑最轻量的v$session查询(秒级返回),确认罪魁祸首后立刻终止会话;恢复平稳后,再去跑v$sqlarea、AWR这类重查询做深度分析。另外,那种ORDER BY buffer_gets DESC的排名查询,我会在非业务高峰跑,绝不趁火打劫。
还有一个细节:查询v$sql和v$sqlarea时别用SELECT *,你能用到的字段就那几个,取全字段反而增加了内存消耗。这就是为什么我上面的模板都只列了常用字段——不是为了省那几行字,是真的会影响性能。
最后再分享一个小技巧:如果你经常需要查这类信息,建议把这些模板存成SQL脚本文件(比如active_sql.sql、history_sql.sql),配合SQL*Plus的&1参数传SQL_ID,下次用的时候一条命令就搞定,不用每次重新敲一遍。我在本地维护了一套这样的脚本,从11g用到19c,除了12c新增的视图稍微补充了几条,核心查询基本没怎么变过。这套东西用熟了,排查SQL问题的效率能提升一倍不止。
