用v$open_cursor关联v$sql、v$session查当前正在跑的 SQL,脚本没几行,真正耗时间的是排障:结果为空、会话状态对不上、ACTIVE里混着一堆历史游标,分不清该信哪一条。这篇的思路不是再写一版更花哨的 SQL,而是让 Codex 把这段关联逻辑逐行拆开、把每个过滤条件单独验证一遍。模型通道走 TaoToken:先去 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 创建 Key,再把 Codex 的 Base URL 填成 https://taotoken.net/api。TaoToken 在这里只负责让 Codex 能稳定发起多轮分析,真正解释v$open_cursor和v$sql关系的是 Codex 本身。
1. 先把原文那条三视图关联 SQL 拆开看
1.1 v$open_cursor、v$sql、v$session 各自装了什么
这三个视图名字都带 v$,但回答的问题完全不一样,混在一起就容易误读。v$session是会话维度的,一个连接一行,你关心的是SID、SERIAL#、USERNAME、STATUS、SQL_ID、LAST_CALL_ET这几列。它的STATUS只有三个取值:ACTIVE、INACTIVE、KILLED。ACTIVE的含义是"会话此刻正在执行 SQL 或处于非空闲等待",不是"这条 SQL 属于业务主流程"。
v$open_cursor是游标维度的,每个会话手里打开的游标一行,列里已经有SID、USER_NAME、SQL_ID、SQL_TEXT、HASH_VALUE、LAST_SQL_ACTIVE_TIME。注意它自带SQL_TEXT,很多人还是习惯绕一圈去 joinv$sql,这是因为 10g 时代这个视图的列没现在全。
v$sql是子游标维度的,SQL_ID+CHILD_NUMBER唯一确定一行,里面存执行次数、缓冲区读、解析次数这些统计。它的SQL_TEXT是VARCHAR2(1000),超长语句会被截断,完整文本在SQL_FULLTEXT这个 CLOB 里。搞清这层维度差异,后面所有"为什么查出来重复"的问题都会自动有答案。
1.2 HASH_VALUE 和 SID 两个连接条件为什么不能省
原文用oc.HASH_VALUE = sq.HASH_VALUE把游标表和 SQL 统计表接起来,再用s.SID = oc.SID把会话接进来。第一个条件解决的是"这个游标对应的 SQL 文本是什么",第二个条件解决的是"这个游标属于谁"。
SID这个连接条件一旦省掉,查询语义就彻底变了:它不再是"某个会话打开的游标",而是"全库游标和全库会话的笛卡尔积再过滤"。表面上可能还能出结果,但那些结果跟你关心的会话已经没关系了。如果你只想看某个会话当前在执行什么,直接v$session.sql_id关联v$sql就够了,v$open_cursor的独特价值在于告诉你"这个会话手里还攥着哪些游标",哪怕它此刻是INACTIVE。
HASH_VALUE这个条件在 11g 之后建议换成SQL_ID。原因是同一个HASH_VALUE在v$sql里对应多行 child cursor 时,join 会把结果行数放大;而SQL_ID配合CHILD_NUMBER才是稳定的定位方式。老脚本不改也能跑,只是排障时行数对不上就容易怀疑人生。
1.3 重写一版带 SID、SQL_ID 的查询
原文那版只 select 了SQL_TEXT,set lines 10000 set pages 0是为了宽行不折行、不分页,适合 spool 到文件。但排障时你需要的定位信息更多,下面这版把会话和游标标识都带出来,方便跟v$session里看到的SID对号入座:
set lines 32767 set pages 0 set trimspool on select s.sid, s.serial#, s.username, s.status, s.sql_id as session_sql_id, oc.sql_id as cursor_sql_id, oc.hash_value, oc.last_sql_active_time, substr(sq.sql_text, 1, 300) as sql_text from v$open_cursor oc, v$sql sq, v$session s where oc.sql_id = sq.sql_id and s.sid = oc.sid and s.status = 'ACTIVE' and s.username = 'VIDS' and sq.sql_text like 'select%' order by sq.sql_text;改动就两处:连接条件从HASH_VALUE换成SQL_ID,select 列表补上SID、SERIAL#、SQL_ID、LAST_SQL_ACTIVE_TIME。LAST_SQL_ACTIVE_TIME特别有用,它能告诉你这个游标上一次真正活跃是什么时候,值离当前时间很远的基本就是缓存游标。
1.4 这个脚本天生会出重复行和空行
先说重复。v$open_cursor是同会话可以有多行同一个SQL_ID的,因为不同游标(不同绑定变量、不同子游标)都算独立游标。join 到v$sql之后,一个SQL_ID下面如果还有多个 child cursor,行数会再翻。想快速看清楚"到底有哪几条不同 SQL",加个distinct substr(sq.sql_text,1,300)比盯着重复行数有用。
再说空行。原文脚本里set pages 0是彻底关掉分页输出,所以在 SQL*Plus 里看到的结果是一行接一行的纯文本,最后还有一行exit;直接退出。如果你把这段整体粘进 SQL Developer 之类的图形工具,set命令会报错,exit会把连接直接掐掉,结果自然"什么都没查出来"。这不是 SQL 的问题,是执行环境的问题,排障时先确认这一点能省下不少时间。
2. 结果为空的四种典型情况,先别急着改 SQL
2.1 USERNAME='VIDS' 的大小写和权限
v$session.USERNAME存的是 Oracle 用户名的原始大小写。普通create user vids建出来会存成大写VIDS,但如果建库时写成create user "vids"带双引号,那存进去就是小写,USERNAME='VIDS'永远匹配不上。先用select username, count(*) from v$session group by username看一眼实际值,比反复改查询快得多。
另一个坑是权限。用VIDS自己登录去查v$session、v$open_cursor这类动态性能视图,需要有SELECT_CATALOG_ROLE或SELECT ANY DICTIONARY权限,否则要么报 ORA-00942 表或视图不存在,要么视图能打开但内容被过滤。这种情况换 SYS 或者其他有权限的账号登录查一次,对照结果就能确认是不是权限在作怪。
2.2 STATUS='ACTIVE' 抓的是瞬间状态
ACTIVE是个瞬时值。你十点整跑这条查询,它反映的是十点整那一瞬间哪些会话在活跃执行,十点零一分再跑,名单就变了。所以"第一次查有结果、第二次查是空的"经常不是脚本坏了,而是那条 SQL 已经跑完了。
排障时更实用的做法是放宽条件再看一遍:把s.status = 'ACTIVE'去掉,换成按s.last_call_et排序,或者加上and s.last_call_et > 5只看跑了超过 5 秒的会话。这样既能抓到正在跑的,也能抓到刚跑完不久、还留着痕迹的,信息量比死盯ACTIVE大得多。
2.3 SQL_TEXT like 'select%' 会漏掉带空格的语句
like 'select%'要求文本第一个字符就是小写s。但v$sql.sql_text里存的是原始 SQL 文本,前面有换行、空格、Tab 的情况非常常见,尤其是从代码里拼出来的多行 SQL 或者带注释的语句。更常见的写法是select前面有空格,或者大小写混用成SELECT、Select。
稳妥一点改成where lower(ltrim(sq.sql_text)) like 'select%',或者干脆用regexp_like(sq.sql_text, '^[[:space:]]*select', 'i')。如果你本来就想看全部语句,别加这个条件,直接靠SID和USERNAME收敛范围更准确。用like过滤本质上是在做文本匹配,跟"这条 SQL 是不是查询语句"不是一回事。
2.4 v$open_cursor 里的游标大部分是缓存不是正在跑
这一点最容易被忽略:v$open_cursor里的行,绝大多数是会话缓存的游标,不是"正在执行的 SQL"。一个连接跑过几十条 SQL,这几十条游标都会挂在v$open_cursor里,直到会话关闭或者游标被换出。这就是为什么ACTIVE过滤之后结果里还是混着一堆历史语句——ACTIVE是会话的状态,而结果里每一行是游标,两者粒度不同。
要真正只保留"正在执行"的,可以把v$session.sql_id和oc.sql_id对上:and s.sql_id = oc.sql_id。这样查出来的就是会话当前正在执行的那条语句对应的游标,历史缓存会被自然排除掉。代价是结果通常只剩一两行,但那一两行才是你要的答案。
3. 把 Codex 的模型通道切到 TaoToken 之后再问 SQL
3.1 在 TaoToken 控制台创建 Key、确认模型 ID
Codex 能不能反复问同一段 SQL,取决于模型通道稳不稳。打开 TaoToken 注册登录,进控制台创建一个 API Key,这个值只显示一次,复制下来存好,后面统一用YOUR_API_KEY代称。
模型 ID 不要自己猜,也不要从别处抄带日期后缀的名字。去模型广场看当前可用列表,挑一个你打算用来分析 SQL 的模型,把它的 ID 原样记下来。这一步看着琐碎,但多轮对话里模型 ID 填错是最常见的失败原因,先把这一步做扎实,后面能少走很多弯路。
3.2 ~/.codex/config.toml 里把 base_url 指向 TaoToken
Codex 走的是config.toml,不要往里面塞ANTHROPIC_*那套变量,那是 Claude Code 的字段,混着填只会让 Codex 读不到配置。在~/.codex/config.toml里加一个自定义 provider:
model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"model填你在模型广场看到的那个 ID。base_url就写https://taotoken.net/api,末尾不要加/v1,也不要带任何查询参数——UTM 是给浏览器点的,填进接口地址会让请求直接打到错误路径。然后设好环境变量,让env_key能取到值:
export TAOTOKEN_API_KEY=YOUR_API_KEY如果 Codex 版本支持 profile,也可以把上面这段塞进[profiles.taotoken],用codex --profile taotoken启动。跑起来之前先用一句最简单的"读一下这段 SQL 干了什么"试探一下通道是否通了,通了再问正经问题。
3.3 一个能让 Codex 逐条核对的提问模板
不要只丢一句"这条 SQL 为什么查不出数据",信息太少,模型只能泛泛而谈。把 SQL、现象、你已经排除过的项一起给它,效果差很多。可以照这个结构问:
下面这段 SQL 是查 Oracle 活动会话正在执行的 select 语句: [粘贴你改写后的 SQL] 现象:在 SQL*Plus 里执行返回 0 行,但用 SYS 登录能看到 VIDS 用户有 ACTIVE 会话。 请按顺序做三件事: 1. 逐个说明 v$open_cursor、v$sql、v$session 在这个查询里各自的过滤点; 2. 列出可能导致结果为空的过滤条件,按排查优先级排序; 3. 对每个条件给我一条独立的验证 SQL,我自己在 SQL*Plus 里跑。 不要假设你能连上数据库,只输出 SQL 和解释。最后那句约束很重要。Codex 拿不到你的库,它只能生成和解释 SQL;诊断语句由你在本地 SQL*Plus 或 SQLcl 里执行,把输出贴回对话,让它继续分析。这样每一轮都有真实数据支撑,而不是在猜。
4. 让 Codex 出清单,执行永远放你本地
4.1 诊断 SQL 由你在本地跑,Codex 只负责生成和解释
把期望写成"让 Codex 连上库执行诊断 SQL",这条路走不通,也不该走。AI 编程工具默认不能直连你的生产库或生产机器去执行业务操作,Codex 能做的三件事是:生成 SQL、解释 SQL、对照你贴回来的结果做判断。
所以正确的循环是:Codex 给一条验证语句,你复制到 SQL*Plus 里跑,把结果或报错原文贴回去,它再给下一条。多轮下来,空结果到底是权限、是大小写、还是ACTIVE的瞬时性,会一项项被排除掉。这个循环里 TaoToken 承担的角色只是让这些多轮请求稳定发出去,分析逻辑全部在 Codex 那一侧。
4.2 把 SQL_TEXT 排序结果贴回去,让它标注可疑行
原文最后用order by sq.SQL_TEXT排序,这个习惯挺好,因为同一条 SQL 的不同游标会聚在一起,肉眼扫的时候命中率高。当你拿到按SQL_TEXT排好序的清单,把它整段贴给 Codex,附上一句:"帮我标出哪些行属于当前正在执行的,哪些是缓存游标,依据是哪一列。"
它通常会让你补LAST_SQL_ACTIVE_TIME和v$session.sql_id这两列,然后按时间差和SQL_ID是否等于session_sql_id来分组。这一步的产出不是一句结论,而是一张可核对的分组表——哪几行是活跃语句、哪几行是历史游标,各自依据是哪一列的值。这比直接问"哪些是正在跑的"要可靠得多。
4.3 一次对话里把三种过滤条件分别试一遍
与其反复修改同一条大 SQL,不如让 Codex 帮你把三个过滤条件拆成三条查询,逐条验证。第一条只留s.username = 'VIDS',看这个用户到底有多少会话;第二条去掉status,改成按last_call_et排序,看哪些会话刚跑过;第三条加上s.sql_id = oc.sql_id,看真正正在执行的游标。
三条跑完,问题基本就定位了。这种拆法特别适合v$open_cursor这类多视图关联的场景,因为每个过滤条件背后对应的是完全不同的语义层,混在一起查,出错时根本分不清是哪一层的问题。分开查虽然要多跑几次,但每次结论都是确定的。
5. 配置上的三个坑:401、模型名、多写的 /v1
5.1 401 基本都出在 env_key 没生效
Codex 报 401 的时候,先别怀疑 Key 本身。config.toml里的env_key = "TAOTOKEN_API_KEY"只是个变量名,它要求你的运行环境里真的存在这个环境变量。如果你是在一个终端里export的,换一个终端窗口或者换到 IDE 内置终端跑 Codex,变量就没了,于是请求不带认证头,服务端只能返回 401。
排查方法很简单:在准备启动 Codex 的那个终端里执行echo $TAOTOKEN_API_KEY,能看到值就说明生效了。看不到就重新 export,或者把它写进 shell 的启动文件里。Key 本身有问题也会 401,但先确认变量这一层,能排除掉大部分误判。
5.2 模型 ID 写错的表现和确认方式
模型 ID 不存在时,返回的通常不是参数错误,而是"模型不可用"或者直接 404 一类的结果,看起来很像通道不通,实际上只是名字写错了。也可能是你写了一个带后缀或随手加的日期尾巴的 ID,而模型广场里根本没有这一项。
确认方式就一句话:以模型广场当时列表为准,复制粘贴,不要手敲。如果你在多个模型之间切换对比分析效果,改config.toml里的model字段就行,其他字段不用动。改完重启 Codex,别指望正在跑的会话会自动重载配置。
5.3 base_url 末尾不要加 /v1,也不要带 UTM
代码里最容易犯的一个错,是习惯性把base_url写成https://taotoken.net/api/v1。这个地址在你的工具配置里就是错的,路径对不上会直接导致请求失败,而且报错信息通常不会明确告诉你"路径多了一段"。
另一个更隐蔽的错,是把浏览器地址栏里的东西整段复制过来,包括?utm_source=...这类查询参数。UTM 是给落地页做归因用的,只能出现在浏览器里,不能出现在base_url、环境变量、curl 命令或者 CLI 参数里。base_url就老老实实写https://taotoken.net/api,一个字符都不多。
6. 排障收尾:去控制台对一下这次调用
6.1 用同一把 Key 在 Codex 之外验证一次
排障跑通之后,建议做一次交叉验证,确认问题真的在 SQL 而不在通道上。打开 TaoToken 模型对话 用同一把 Key 发一条测试消息,看响应是否正常。如果对话页正常而 Codex 报错,那问题一定在config.toml或环境变量上,跟 Key 和通道无关。
如果你打算把这种"贴 SQL、读结果、继续追问"的流程长期用下去,去 Coding Plan 看下套餐额度是否够用。Key 本身可以在 控制台 API Keys 创建和轮换,不用每次重新注册账号。
6.2 下次再遇到空结果,从哪里开始查
回过头看,这段v$open_cursor、v$sql、v$session的关联查询本身没有 bug,它的复杂度全部来自视图粒度不一致:会话是会话,游标是游标,子游标是子游标,三个STATUS、USERNAME、like过滤条件各自作用在不同粒度上。Codex 帮你做的,是把这三层拆开逐层验证,而不是替你改 SQL。
下次再遇到结果为空,按这个顺序走一遍就够:先确认执行环境是不是 SQL*Plus、再看USERNAME实际大小写、然后放开STATUS看last_call_et、最后用s.sql_id = oc.sql_id把缓存游标剔掉。这四步走完还不够,就把每步的输出贴回 Codex,让它接着往下排。所有查询都在你自己的客户端里执行,Codex 只负责读结果和给下一步,这个分工从头到尾别搞混。