做Oracle DBA的,十有八九都遇到过这种场景:业务方火急火燎地打电话说系统卡死了,登录数据库一看,会话却老老实实挤在v$session里,什么动作都没往外报。这时候第一件事就是先把正在执行的SQL捞出来,看看是谁在跑、跑了多久。等系统缓过来,还要把刚才执行过的SQL翻出来分析到底哪一条是罪魁祸首。标题里说的“查看执行过的SQL及当前正在执行的SQL”,其实就是Oracle运维排障里最基础也最高频的两个动作。
这两个需求,看着简单,实际写SQL的时候会发现里面门道不少。一来Oracle里的SQL文本存在好几个不同的视图里,字段含义有重叠也有差异;二来12c开始引入多租户架构,CDB和PDB视角下看到的执行历史还不一样;三来很多人在v$sql里翻了半天找不到想要的记录,其实是因为没分清“当前还在内存缓存中的SQL”和“归档到AWR历史里的SQL”这两回事。
这篇文章我把这两类查询从原理到实操完整拆开讲。你会看到Oracle到底把“正在执行的SQL”和“执行过的SQL”分别存在哪里,每一类需求对应哪种查法,以及我在实际运维中踩过的坑和总结的排查思路。内容不绕弯子,直接上SQL,适合刚接手Oracle运维的DBA,也适合开发同学在排查慢SQL时自己动手查。
1. 先弄明白:Oracle到底把SQL“记”在哪儿
1.1 会话、游标和共享池的关系
要说清“正在执行”和“执行过”的区别,得先从Oracle执行一条SQL的过程说起。当你往数据库发一条SQL,服务进程会先在共享池(Shared Pool)的库缓存(Library Cache)里找有没有相同文本的SQL。如果找到了,就走软解析,直接复用已有的游标;找不到就得做硬解析,生成执行计划,并把这条SQL的文本、解析信息、执行计划一起缓存在库缓存里。
这个“缓存下来的SQL游标”,对外暴露出来的就是动态性能视图v$sql。每个SQL_ID对应的是一条SQL文本经过哈希计算后的唯一标识,只要SQL文本完全相同,SQL_ID就相同。所以理论上,只要这条SQL还在共享池里,你在v$sql里就能查到它,包括它被解析时的用户、执行次数、消耗的资源这些信息。
这里有个特别容易误解的点:v$sql里的记录,并不等于“所有执行过的SQL的历史全集”。它只是“当前还待在内存里的SQL”。一旦共享池空间紧张,或者SQL长时间不用,这些游标就会被淘汰掉。所以你今天执行过的SQL,明天可能还在,也可能已经被清出去了。要看更长周期的历史,就得靠AWR快照里的dba_hist_sqltext。
1.2 当前执行和历史执行是两条查询路线
明确了存储位置,查询思路就清楚了。“当前正在执行的SQL”这条线,核心是v$session,因为每个活动会话都会带着当前正在跑的SQL_ID,把它和v$sqltext、v$sql关联,就能拿到正在执行的完整SQL文本。
“执行过的SQL”这条线,核心是v$sql、v$sqlarea这两个内存视图,再加上AWR的dba_hist_sqltext、dba_hist_sqlstat两个历史视图。内存视图查的是“最近执行过且还在缓存里的”,AWR视图查的是“跨时间、跨重启都能保留的历史记录”。两条线查出来的数据时效性完全不同,用之前先想清楚你到底要哪一种。
12c开始有了多租户选项,情况稍微复杂了一点。CDB根容器里能看到所有PDB的SQL信息,v$sql这类视图多了一个CON_ID列来区分容器;但直接登录某个PDB查询,默认只看得到当前PDB的数据。这个细节后面实操部分我会专门演示。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实时活捉:正在执行的SQL怎么查
2.1 用v$session定位正在跑的会话
查“当前正在执行”的SQL,最直接的方式就是查v$session。这个视图实时记录着每个会话的状态,其中最关键的是两个字段:STATUS和SQL_ID。
STATUS为ACTIVE,表示这个会话正在干活——要么在跑SQL,要么在等待某个事件;STATUS为INACTIVE,说明会话是空闲的,这时候SQL_ID通常也没有值。所以最基础的查询就是过滤STATUS='ACTIVE',然后取出SQL_ID和会话基本信息:
sql复制SELECT s.sid, s.serial#, s.username, s.status, s.machine, s.program, s.sql_id
FROM v$session s
WHERE s.status = 'ACTIVE'
AND s.username IS NOT NULL
ORDER BY s.sid;
这一步的作用是快速锁定嫌疑对象。执行结果里你会看到一堆会话,有些是应用连接池的心跳查询,有些是报表任务,有些可能是真正卡住业务的元凶。根据USERNAME、MACHINE、PROGRAM这些字段,一般能先判断出是哪个业务模块发起的。
但v$session里只带了SQL_ID,没带SQL文本。如果你用工具看这条SQL的执行计划、绑定变量还好说,要直接看文本,就得再关联v$sqltext或者v$sql。
2.2 关联v$sqltext拼出完整SQL文本
v$sqltext这个视图比较有意思,它把一条完整的SQL按行拆成了多段存储,每个片段有一个PIECE序号。之所以拆开存,是因为内存里对SQL文本的长度有限制,超长SQL总不能一直占着连续内存。查询时按PIECE升序排列,就能拼回完整的SQL:
sql复制SELECT s.sid, s.serial#, s.username, q.sql_id, q.piece, q.sql_text
FROM v$session s, v$sqltext q
WHERE s.sql_id = q.sql_id
AND s.status = 'ACTIVE'
AND s.username IS NOT NULL
ORDER BY s.sid, q.piece;
这样查出来的结果是一行一个片段,看起来比较碎。实际工作中我更喜欢直接关联v$sql的SQL_FULLTEXT字段,这个字段是CLOB类型,存的就是完整的SQL文本,一行一条,看起来清爽得多:
sql复制SELECT s.sid, s.serial#, s.username, q.sql_id, q.sql_fulltext
FROM v$session s, v$sql q
WHERE s.sql_id = q.sql_id
AND s.status = 'ACTIVE'
AND s.username IS NOT NULL;
不过要注意,SQL_FULLTEXT在SQL*Plus里显示时会受Linesize和Long参数限制,CLOB默认只会显示前面一部分。要取完整文本,可以用DBMS_LOB.SUBSTR截取,或者配合spool导出到文件里。我习惯直接看前面4000个字符,绝大多数SQL语句这个长度都够了:
sql复制SELECT s.sid, s.serial#, s.username, q.sql_id, DBMS_LOB.SUBSTR(q.sql_fulltext, 4000, 1) sql_text
FROM v$session s, v$sql q
WHERE s.sql_id = q.sql_id
AND s.status = 'ACTIVE'
AND s.username IS NOT NULL;
查询结果里如果SQL_TEXT不为空,说明确实有一个正在执行的SQL;如果SQL_ID为NULL或者查不到对应文本,通常这个会话正在执行PL/SQL块、等待某种资源,或者纯粹处于空闲状态。这个判断在排障时非常关键,别看到ACTIVE就以为是SQL在跑,也可能是卡在锁上。
2.3 阻塞会话、长事务和操作系统进程的关联
排障时经常遇到的情况根本不是“没有SQL”,而是SQL发出去之后被别的会话堵住了。这时候光看当前会话的SQL没用,还得看它到底在等什么。最简单的关联查询是看阻塞链:
sql复制SELECT sid, serial#, username, sql_id, blocking_session, event, wait_class
FROM v$session
WHERE blocking_session IS NOT NULL
ORDER BY blocking_session;
BLOCKING_SESSION不为空,说明这个会话正在等待某个其他会话持有的锁。把BLOCKING_SESSION对应的会话也查出来,看看它当时在执行什么SQL,基本就能定位到是谁堵了谁。
如果是那种特别耗时的长事务,可能不需要实时盯着,而是想确认某个操作系统进程(SPID)到底在执行哪条SQL。这种情况可以用v$process关联v$session再关联v$sqltext:
sql复制SELECT p.spid, s.sid, s.serial#, s.username, q.sql_id, q.sql_text
FROM v$process p, v$session s, v$sqltext q
WHERE p.addr = s.paddr
AND s.sql_id = q.sql_id
AND p.spid = '12345';
把12345换成你在操作系统层用top或者ps看到的线程号,就能把数据库会话和操作系统进程对应起来。实际排障的时候,如果开发同事只给了你“服务器上某个进程CPU飙到百分之两百”,这一条SQL能帮你快速定位到数据库内部是哪个会话、哪条SQL在作妖。
2.4 12c多租户环境下的实时视图差异
12c以后如果你用的是多租户架构,v$session默认在PDB里查询时,只能看到当前PDB的会话;在CDB根容器里查询,能看到所有PDB的会话,并且多了一个CON_ID列用来区分容器。
有些DBA在PDB里执行上面的查询,发现结果比自己预期少很多,或者压根查不到数据,第一反应是SQL写错了。其实不是,而是容器隔离在起作用。如果你确实需要整库统一排查,建议登录CDB$ROOT去查,并且在结果里把CON_ID显示出来:
sql复制SELECT s.con_id, s.sid, s.serial#, s.username, s.sql_id
FROM v$session s
WHERE s.status = 'ACTIVE'
AND s.username IS NOT NULL
ORDER BY s.con_id, s.sid;
拿到CON_ID之后,可以用v$pdbs视图确认是哪个PDB:
sql复制SELECT con_id, name FROM v$pdbs;
这一点算是12c独有的坑,11g没有。实际上很多从11g转到12c的运维同学,第一天排查问题就会在这里卡一下。我的建议是:日常写脚本建议默认带CON_ID,甭管当前环境是不是多租户,多一个字段不影响什么,真要切换环境时就不会漏。
注意:v$session里的SQL_ID只代表会话“最后被执行的SQL”,如果会话正处于INACTIVE状态,这个SQL_ID保存的是上一次执行的信息,不代表当前正在跑。判断“正在执行”一定以STATUS='ACTIVE'为第一条件。
3. 翻旧账:执行过的SQL怎么查
3.1 v$sql和v$sqlarea,别搞混了
v$sql和v$sqlarea这两个视图都可以查“执行过的SQL”,但粒度不同。
v$sql是游标级别,一条SQL文本如果在不同会话里被多次解析,可能会产生多个子游标,每个子游标一行记录,它们的SQL_ID相同,但是CHILD_NUMBER不同,EXECUTIONS等统计也是分开累计的。v$sqlarea则是按SQL文本聚合,同一SQL_ID的多条子游标记录会汇总成一行,EXECUTIONS是所有子游标执行次数的总和。
日常查历史SQL,我的选择顺序是:要看单条SQL的细节,用v$sql;要做Top N排序,用v$sqlarea更准确。
拿最典型的“最近执行过哪些SQL”来说,用v$sql按LAST_LOAD_TIME倒序排列:
sql复制SELECT sql_id, DBMS_LOB.SUBSTR(sql_fulltext, 4000, 1) sql_text, executions,
first_load_time, last_load_time, parsing_schema_name
FROM v$sql
WHERE parsing_schema_name = 'SCOTT'
ORDER BY last_load_time DESC;
这里把解析用户限定成SCOTT,抓出来的结果就是SCOTT这个用户下最近执行过的SQL。FIRST_LOAD_TIME是这条SQL第一次被加载进缓存的时间,LAST_LOAD_TIME是最后一次被加载的时间。你可能会问,既然有LAST_LOAD_TIME,为什么不直接查“今天上午10点到11点执行的SQL”?理论上可以,但这里有个陷阱——LAST_LOAD_TIME是“游标被加载进共享池”的时间,不是“执行”的时间。如果一条SQL一直在共享池里,你在11点又执行了一次,LAST_LOAD_TIME并不会更新。所以在v$sql里查“某条SQL今天执行了多少次”是可以的,但查“某条SQL今天几点几分执行过”是查不准的。
3.2 按用户、时间、资源消耗排序查询
如果目标是做性能分析,通常更关心哪些SQL消耗资源最多。三个最常用的排序指标是ELAPSED_TIME(总耗时,单位微秒)、BUFFER_GETS(逻辑读次数)、DISK_READS(物理读次数)。
查当前库里Top 10最耗时的SQL:
sql复制SELECT * FROM (
SELECT sql_id, DBMS_LOB.SUBSTR(sql_fulltext, 4000, 1) sql_text,
executions, elapsed_time/1000000 elapsed_sec,
buffer_gets, disk_reads, parsing_schema_name
FROM v$sqlarea
ORDER BY elapsed_time DESC
)
WHERE ROWNUM <= 10;
结果里的ELAPSED_SEC是累计执行时间,单位已经换算成秒。注意这个数据是“从这条SQL进入共享池开始统计的累计值”,不是单次执行耗时。要看单次平均耗时,可以用ELAPSED_TIME/1000000/EXECUTIONS去算,但前提是EXECUTIONS不为零。
排在最上面的SQL,往往是那种跑一次就要几十秒、执行次数又贼多的“惯犯”。顺手再看一下它的BUFFER_GETS和DISK_READS,能初步判断是逻辑读密集还是物理读密集。逻辑读高说明可能缺索引,走了全表扫描;物理读高则可能是缓存命中率太低。
按时间维度翻记录,另一种常见需求是:我大概知道SQL里有某个关键字,想把它找出来。比如业务方说“昨天有个报表好像执行出错,字段可能跟订单有关系”,你又不知道具体的SQL_ID,可以先在共享池里模糊搜索:
sql复制SELECT sql_id, DBMS_LOB.SUBSTR(sql_fulltext, 4000, 1) sql_text, first_load_time
FROM v$sql
WHERE DBMS_LOB.INSTR(sql_fulltext, 'ORDER') > 0
AND parsing_schema_name = 'REPORT'
ORDER BY first_load_time DESC;
这里的DBMS_LOB.INSTR是对CLOB做模糊匹配,效率虽然不高,但作为定位手段非常好用。
3.3 AWR历史中的SQL:dba_hist_sqltext和dba_hist_sqlstat
v$sql和v$sqlarea有个共同的硬伤:数据库实例一重启,共享池清空,里头的记录全没了。想查一周前执行过的SQL,只能靠AWR。
AWR默认每60分钟采集一次快照,保留8天。每次快照会把当时的SQL统计信息存到dba_hist_sqlstat,把SQL文本存到dba_hist_sqltext。注意dba_hist_sqltext存的文本是“首次被采集时从共享池捞出来的”,如果这条SQL早就不在共享池里了,它在这个表里依然能查到文本,前提是它曾经被快照采集过。
先查看有哪些快照:
sql复制SELECT snap_id, begin_interval_time, end_interval_time
FROM dba_hist_snapshot
ORDER BY snap_id DESC FETCH FIRST 10 ROWS ONLY;
拿到快照ID范围之后,比如查第120到第130个快照之间的Top SQL:
sql复制SELECT s.sql_id, DBMS_LOB.SUBSTR(t.sql_text, 4000, 1) sql_text,
s.executions_delta, s.elapsed_time_delta/1000000 elapsed_sec
FROM dba_hist_sqlstat s, dba_hist_sqltext t
WHERE s.snap_id BETWEEN 120 AND 130
AND s.sql_id = t.sql_id
ORDER BY elapsed_time_delta DESC;
这个查询结果展现的是“该时间段内新增的执行次数和耗时”。EXECUTIONS_DELTA是快照区间内的执行次数增量,ELAPSED_TIME_DELTA是快照区间内的耗时增量。如果你发现某条SQL在快照区间内执行次数不多但耗时巨大,那基本就是这条SQL拖慢了整体性能。
dba_hist_sqltext的SQL_TEXT字段同样是CLOB,查询时需要做SUBSTR处理。如果SQL文本超过4000字符,建议用DBMS_LOB.SUBSTR分批截取,或者直接用DBMS_LOB.GETLENGTH先看长度再决定怎么取。实际遇到超长SQL的概率不高,但真遇到拼接了几百个字段的INSERT语句,取不全文本会误导分析方向。
3.4 12c中dba_hist视图的容器隔离
12c的AWR视图也引入了容器概念。在CDB根容器里查询dba_hist_sqlstat,能看到所有PDB的数据,每行记录带了CON_ID;在某个PDB里查询,只能看到当前PDB的数据。
有些环境下,你在PDB里查dba_hist_sqltext却发现返回为空,第一反应是“历史记录丢了”,其实没有。有可能是因为这条SQL的文本是首次在CDB级别采集的,而当前PDB的视角下没有单独记录。解决办法很简单:登录CDB$ROOT查询,或者显式加上CON_ID条件去过滤:
sql复制SELECT sql_id, con_id, DBMS_LOB.SUBSTR(sql_text, 4000, 1) sql_text
FROM dba_hist_sqltext
WHERE con_id = 3;
这个细节,生产环境只要是多租户,基本都会碰到。建议AWR相关的排查脚本都带上CON_ID,省得来回切换环境。
另外,12c的AWR数据默认保留8天,这个参数在dba_hist_wr_control里:
sql复制SELECT * FROM dba_hist_wr_control;
RETENTION字段就是保留时间,单位是分钟。需要保留更长时间可以调整这个参数,但会占用更多的SYSAUX表空间,这个权衡得根据实际环境决定。
4. 实操案例:一次典型的慢SQL排查
4.1 场景描述
一个真实的场景:某天下午业务忽然反馈订单查询页面响应极慢,点击一次要等十几秒。我登录数据库,先用最基础的活跃会话查询看了一眼,结果发现有一个来自应用服务器A的会话,STATUS是ACTIVE,SQL_ID是7s9h2k3j4d5f,USERNAME是APP_USER,但看不到它具体在跑什么。这个会话已经存活了两个多小时,从常理判断,一个订单查询不可能跑这么久,基本可以断定它有问题。
4.2 逐步定位过程
第一步,先锁定这个会话正在执行的SQL文本:
sql复制SELECT sid, serial#, username, sql_id, DBMS_LOB.SUBSTR(sql_fulltext, 4000, 1) sql_text
FROM v$session s, v$sql q
WHERE s.sql_id = q.sql_id
AND s.sid = 143;
结果出来一看,是一条UPDATE语句,内容是对订单状态做批量更新,WHERE条件里用了子查询。单看SQL文本,确实有慢查询的潜质,但还看不出真正卡在哪。
第二步,看这个会话在等待什么事件:
sql复制SELECT sid, event, wait_class, seconds_in_wait, blocking_session
FROM v$session
WHERE sid = 143;
结果BLOCKING_SESSION是392,说明它不是在慢执行,而是被另一个会话阻塞了。于是继续倒查会话392:
sql复制SELECT sid, serial#, username, sql_id, status, event, SQL_FULLTEXT
FROM v$session s, v$sql q
WHERE s.sql_id = q.sql_id
AND s.sid = 392;
会话392的SQL是一条INSERT语句,STATUS是INACTIVE,说明它早就执行完了但是没提交事务。这里很容易掉进一个误区:你以为阻塞者是正在跑SQL的会话,实际上它可能已经空闲,只是事务一直没提交,锁还捏在手里。392就是这种情况,UPDATE等它提交,它却一直不提交,后面的请求全被堵住。
第三步,把392对应的事务找出来,确认它锁了哪些对象:
sql复制SELECT sid, type, id1, id2, lmode, request, block
FROM v$lock
WHERE sid = 392 AND type = 'TX';
到这里,问题就清楚了。392会话里有一个未提交的事务,持有行锁,143里面的UPDATE被它堵住。处理方式就需要走业务侧确认:要么等392提交或回滚,要么DBA介入杀会话。杀会话的操作要谨慎,先跟业务确认392对应的事务能否直接回滚,再走alter system kill session 'serial#'这条命令。
4.3 案例复盘:从这个场景能学到什么
这个案例能很好地把“正在执行的SQL”和“执行过的SQL”串起来。定位问题发生时,核心是查v$session里当前正在执行的SQL;但要想知道392到底改了什么数据、为什么没提交,通常还要翻它“执行过的SQL”来确认。如果392的事务已经提交了,锁释放了,你会看到阻塞消失,此时再想把刚才那两条SQL捞出来复盘,就得去v$sql里查它们的执行计划和统计信息。
现实里这类问题一天能出好几次,掌握排查路径比记住单个SQL更重要。我的经验是,遇到数据库卡顿,先看v$session的ACTIVE会话和BLOCKING_SESSION,再补查对应SQL文本,最后查等待事件和锁。顺序不要反:先定位会话,再定位SQL,最后才分析SQL性能本身。
5. 常见问题与排查技巧实录
5.1 问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| v$sql里查不到某条SQL | 共享池被淘汰、实例重启过、SQL执行后未产生硬解析 | 查dba_hist_sqltext,或换SQL文本完全一致的写法重查 |
| v$session显示SQL_ID为NULL | 会话空闲、正在执行PL/SQL、或执行的是递归SQL | 查看v$session_wait的等待事件,或查该会话的SQL_TRACE |
| SQL文本显示不全 | SQL_FULLTEXT是CLOB,SQL*Plus显示截断 | 用DBMS_LOB.SUBSTR截取,或SPOOL到文件 |
| 在PDB里查不到历史SQL | 12c容器隔离,PDB视角数据不全 | 登录CDB$ROOT查询,或用CON_ID过滤 |
| AWR查不到SQL文本 | 快照未采集到该SQL、SQL在采集前已被淘汰 | 调整AWR保留期或提高采集频率,排查前先确认快照范围 |
| dba_hist视图提示权限不足 | 缺少SELECT ANY DICTIONARY权限 | 授权:GRANT SELECT ANY DICTIONARY TO USER |
5.2 几个值得留意的实操心得
第一,SQL_ID匹配是按文本精确匹配的。你在PL/SQL Developer里写“select * from emp”和“SELECT * FROM EMP”,Oracle解析后认为是两条SQL,SQL_ID完全不一样。所以拿网上抄来的SQL去v$sql里精确匹配,经常查不到东西。排查时尽量从v$sql里直接复制文本,或者用条件模糊搜索。
第二,结合v$sql_bind_capture看绑定变量,比只看文本更有用。同一条SQL,绑定变量不同,执行计划可能天差地别。要分析慢SQL到底是不是绑定变量引起的,可以把v$sql_bind_capture和v$sql关联:
sql复制SELECT sql_id, name, datatype_string, value_string
FROM v$sql_bind_capture
WHERE sql_id = '7s9h2k3j4d5f';
第三,v$sql里查不到不代表这条SQL从没执行过。Oracle对SQL文本的缓存是有淘汰机制的,尤其是12c里共享池的自动管理策略和压力大时差异很明显。完全依赖内存视图做长周期分析不现实,该用AWR的时候别犹豫。
第四,如果只是想知道某条SQL“当前正在执行到哪一步”,可以用v$session_longops监控长时间运行的操作:
sql复制SELECT sid, serial#, opname, target, sofar, totalwork, elapsed_seconds
FROM v$session_longops
WHERE totalwork > 0 AND sofar < totalwork;
这条视图适合跟踪大表扫描、索引重建这类耗时操作,能告诉你进度百分比。平时查实时SQL时,顺手看一眼这个视图,往往能多拿到一条重要线索。
第五,脚本里建议统一用DBMS_LOB.SUBSTR处理所有SQL_TEXT、SQL_FULLTEXT字段。别贪图省事直接select sql_fulltext,不同客户端工具的显示行为差异很大,SQL*Plus里你看到的可能只有几十个字符,白忙活一场。
这套查询技巧,我在10g、11g、12c、19c环境里都验证过,基本通用。12c多租户环境下注意加CON_ID字段就好。碰到紧急性能问题时,最快的路径永远是:v$session找到ACTIVE会话,关联v$sql看文本,再看等待事件和锁。先把这三步走完,再决定是优化SQL还是处理锁阻塞,基本不会跑偏。
