父游标、子游标与共享游标:一次讲清数据库游标嵌套与复用机制
1. 从一次线上告警说起父游标、子游标与共享游标到底是什么如果你在后端开发或 DBA 岗位上待过一段时间大概率遇到过这样的场景数据库 CPU 突然飙高AWR 报告里硬解析hard parse占比异常library cache相关等待事件排在前列。你打开v$sqlarea一看明明是同一条 SQL却出现了好几行记录executions各自为战。这时候问题往往就出在游标体系的理解上——父游标、子游标、共享游标这三者的层级关系没理清调优就无从下手。游标这个概念之所以让人头疼是因为它在不同语境下指代不同的东西。站在 SQL 引擎的角度游标是library cache里那份可被多个会话复用的解析产物也就是共享游标shared cursor站在会话的角度游标是 PGA/UGA 里那块逐行取数的内存结构也就是 session cursor。前者关心的是能不能复用、复用了几次后者关心的是这一行取完了没有、什么时候关闭。本文聚焦的是前者父游标parent cursor、子游标child cursor以及共享游标shared cursor的层级关系与复用逻辑。我会用可复制的 SQL 示例带你看清一条语句从文本哈希到执行计划落地的完整链路并给出通过v$sqlarea、v$sql、v$sql_shared_cursor验证嵌套层级、复用命中与资源释放的具体动作。适合后端开发、数据库调优工程师以及正在准备面试、想把游标机制讲清楚的同学。先给一个最直观的类比父游标像一本书的书名 作者子游标像这本书的不同版本。同一个书名可以对应精装版、平装版、电子版它们共享书名这个索引但内容排版执行计划可能不同。共享游标则是图书馆里这本书被借阅的次数——只要书名和版本都对得上就能直接复用不用重新印刷。理解了这个类比后面的sql_id、hash_value、child_number就都好理解了sql_id定位父游标sql_id child_number定位子游标而address hash_value child_number是确定一个游标的三个主要字段。2. 前置准备TaoToken 环境与游标实验库搭建在动手做实验之前先把环境准备好。我平时做这类数据库机制验证习惯用一个统一的模型/API 网关来管理实验脚本和文档TaoToken 就是我在用的一个平台。它的官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。需要说明的是TaoToken 在这里扮演的是实验辅助工具的角色——我用它来跑一些 SQL 解析辅助脚本、生成测试数据、整理执行计划对比文档而不是让它替代数据库本身。数据库的游标机制最终还是要落到真实的 Oracle 或兼容实例上去验证。为什么要在游标实验里引入这样一个工具因为游标调优往往需要反复对比不同绑定变量、不同优化器参数下的子游标生成情况手工敲 SQL 效率很低。用脚本批量生成测试语句、批量查询v$sql视图能省下大量时间。TaoToken 的模型对话入口https://taotoken.net/api 下的对话能力可以帮我快速生成 PL/SQL 块和查询模板Coding Planhttps://taotoken.net/api 的 coding-plan 路径则适合长期维护这套实验脚本。具体到实验库的搭建你需要一个可以创建表、查询动态性能视图的账号。下面是我用的建表语句和 excerpt 里的演示保持同一思路但补充了更多可复现的细节-- 创建实验表 t数据来源可以是任意业务表 CREATE TABLE t AS SELECT empno, ename, sal FROM emp WHERE deptno 10; -- 确认数据 SELECT COUNT(*) FROM t;接着准备查询动态性能视图的权限。普通开发账号通常没有SELECT ANY DICTIONARY需要 DBA 授权或者直接用 DBA 账号做实验GRANT SELECT ON v_$sqlarea TO your_user; GRANT SELECT ON v_$sql TO your_user; GRANT SELECT ON v_$sql_shared_cursor TO your_user;这里有个容易踩的坑v$sqlarea和v$sql是公共同义词但授权时要写v_$sqlarea这种带下划线的底层视图名。我第一次做实验时就卡在这里报ORA-00942: table or view does not exist排查了半天才发现是授权对象写错了。环境准备好之后先跑一条最简单的查询确认动态视图能正常访问SELECT sql_id, sql_text, executions FROM v$sqlarea WHERE sql_text LIKE %empno7782% AND sql_text NOT LIKE %v$sqlarea%;如果这条能返回结果说明前置条件齐了。接下来我们进入核心部分父游标和子游标是怎么在解析过程中产生的。3. 可复制配置父游标、子游标与共享游标的声明与嵌套调用这一节是全文的技术核心。我会先给出游标解析的完整流程再用可复制的 SQL 演示父游标、子游标的产生最后给出共享游标的复用配置。所有片段都可以直接粘贴执行。先看解析流程。一条 SQL 进来数据库会依次做这几件事第一步如果表上有行级安全控制VPD把安全策略生成的约束条件追加到WHERE子句。第二步做语法、语义、访问权限检查——SQL 写得对不对、对象存不存在、用户有没有权限。第三步父游标缓存查找把 SQL 文本做哈希拿哈希值去library cache里找。找不到就生成父游标继续走后面的逻辑优化和物理优化找到了就进一步判断有没有可共享的子游标有就直接调用其执行计划没有就继续走优化。第四步逻辑优化用转换技巧生成语义等价的新 SQL扩大搜索空间。第五步物理优化读数据字典统计信息和动态采样算开销选最低的那个执行计划。第六步子游标缓存分配内存、生成子游标、和父游标关联。只走完前两步的叫软解析走完全程的叫硬解析。这个区分很关键因为硬解析是 CPU 消耗大户。下面用一组实验把父游标和子游标逼出来。注意 SQL 文本的大小写和空格差异-- 语句 1小写 select小写 from SELECT * FROM t WHERE empno 7782; -- 语句 2小写 select小写 from与语句 1 文本完全一致 SELECT * FROM t WHERE empno 7782; -- 语句 3大写 SELECT大写 FROM SELECT * FROM t WHERE empno 7782; -- 语句 4小写 select小写 from与语句 1、2 一致 SELECT * FROM t WHERE empno 7782;执行完这四条去查v$sqlareaCOL sql_text FORMAT a40 SELECT sql_id, sql_text, executions FROM v$sqlarea WHERE sql_text LIKE %empno7782% AND sql_text NOT LIKE %v$sqlarea%;你会看到三行父游标文本完全一致的两条语句 1、2、4共享同一个父游标executions累计为 3大小写不同的语句 3 单独一个父游标。这就是父游标共享的直接证据——SQL 文本完全一致时多个相同语句共享一个父游标。再查v$sql看子游标SELECT sql_id, hash_value, child_number, plan_hash_value, sql_text, executions FROM v$sql WHERE sql_text LIKE %empno7782% AND sql_text NOT LIKE %v$sql%;每个父游标下目前都只有一个child_number 0的子游标因为执行环境一致。现在制造执行环境不一致的场景逼出第二个子游标ALTER SESSION SET optimizer_index_caching 40; SELECT SUM(sal) FROM t; ALTER SESSION SET optimizer_index_caching 100; SELECT SUM(sal) FROM t;同样的SELECT SUM(sal) FROM t因为optimizer_index_caching不同优化器环境哈希值不同于是产生了两个子游标SELECT sql_id, child_number, sql_text, optimizer_env_hash_value oehv, child_address FROM v$sql WHERE sql_text LIKE %sum(sal)% AND sql_text NOT LIKE %from v$sql%;结果里child_number分别是 0 和 1optimizer_env_hash_value不同child_address也不同。这就是子游标不能共享的典型原因。要定位到底是哪个环境差异导致子游标不能共享查v$sql_shared_cursorSELECT child_number, optimizer_mismatch FROM v$sql_shared_cursor WHERE sql_id gu68ka2qzx3hh;optimizer_mismatch为Y的那一行就是新增子游标的原因。这个视图是排查子游标膨胀的利器后面排障章节还会用到。关于共享游标的复用配置核心是控制什么条件下允许复用。几个关键动作一是统一 SQL 文本避免大小写、空格、注释差异。二是尽量使用绑定变量避免字面量导致父游标爆炸。三是控制优化器环境参数的一致性比如optimizer_index_caching、optimizer_mode这类会话级参数不要在同一批 SQL 执行过程中随意切换。四是关注cursor_sharing参数EXACT是默认值FORCE会强制把字面量替换为绑定变量但要谨慎使用因为它可能改变执行计划。如果你用 TaoToken 的 Coding Plan 来管理这套实验脚本可以把上面的 SQL 片段整理成配置文件方便反复执行。配置里需要写全三件套Base URL 用 https://taotoken.net/api Key 在控制台生成Model ID 按你实际使用的模型填写。这样脚本调用和数据库实验就能串起来。4. 验证请求与成功结果用执行计划和游标状态视图确认嵌套层级配置写完了怎么确认父游标、子游标的嵌套层级真的按预期建立怎么确认共享游标真的命中了这一节给出具体的验证动作和预期结果。第一个动作确认父游标与子游标的关联关系。从 Oracle 10g 之后sql_id可以唯一确定一个父游标sql_id child_number唯一确定一个子游标。查询父游标SELECT address, hash_value, sql_id FROM v$sqlarea WHERE sql_id gu68ka2qzx3hh;查询子游标SELECT address, hash_value, child_number, sql_id, child_address FROM v$sql WHERE sql_id gu68ka2qzx3hh;预期结果是v$sqlarea返回一行v$sql返回两行child_number为 0 和 1两行的address和hash_value与父游标一致但child_address不同。这就验证了一个父游标对应多个子游标的层级关系。第二个动作确认共享游标的复用命中。执行同一条 SQL 多次观察executions是否累加而不是新增父游标SELECT sql_id, sql_text, executions, parse_calls FROM v$sqlarea WHERE sql_text LIKE %empno7782% AND sql_text NOT LIKE %v$sqlarea%;如果executions从 1 变成 3而结果集行数没变说明共享游标命中了没有产生新的硬解析。parse_calls如果远小于executions也是复用良好的信号。第三个动作用执行计划确认子游标对应的计划确实不同SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(gu68ka2qzx3hh, 0)); SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(gu68ka2qzx3hh, 1));两个子游标的执行计划如果不同说明它们确实是因为环境差异而分开的如果相同那新增子游标就是浪费需要排查共享失败的原因。第四个动作观察游标资源释放。session cursor 关闭后UGA 里的内存会释放但library cache里的共享游标按 LRU 原则清除这为后续复用提供了可能。查询v$open_cursor可以看到当前会话打开的游标SELECT sql_id, child_address, sql_text FROM v$open_cursor WHERE sid SYS_CONTEXT(USERENV, SID);关闭游标后对应的行应该消失。如果一直不消失可能是游标泄漏需要检查应用代码里有没有忘记CLOSE。我实测下来最有效的验证组合是v$sqlarea看父游标和executionsv$sql看子游标和child_numberv$sql_shared_cursor看共享失败原因DBMS_XPLAN.DISPLAY_CURSOR看计划差异。四个视图配合嵌套层级和复用命中就一目了然了。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth 报错做游标实验和接入辅助工具的过程中报错是难免的。这一节把几类高频错误和排查路径列清楚都是真实遇到过的。第一类ORA-00942: table or view does not exist。前面提过查动态性能视图时授权对象要写v_$sqlarea而不是v$sqlarea。另外如果用的是普通用户可能还需要SELECT_CATALOG_ROLE角色。排查顺序先确认视图名拼写再确认授权最后确认当前用户。第二类ORA-01031: insufficient privileges。执行ALTER SESSION SET optimizer_index_caching需要ALTER SESSION权限普通账号可能没有。换成 DBA 账号或者让 DBA 授权。第三类接入辅助工具时的401 Unauthorized。这通常是 Key 没配对或者 Key 过期了。检查三件套Base URL 是不是 https://taotoken.net/api Key 是不是在控制台重新生成过Model ID 是不是写对了。三者缺一不可。如果用的是 Claude Code 这类工具还要确认 OAuth 流程走完了token 有没有正确写入本地配置。第四类local proxy failed。这个报错一般出现在本地代理配置和实际网络环境不匹配的时候。排查思路是先确认本地代理进程是否在运行再确认配置里的地址和端口是否和代理进程一致最后确认目标 API 地址是否可达。如果代理配置里写了http://127.0.0.1:xxxx但代理没启动就会报这个错。第五类reading choices相关报错。这类错误通常出现在解析模型返回结果时返回体格式和预期不符。排查方法是先把原始返回打印出来看结构是不是标准的 JSON再看choices字段是否存在、数组是否为空。如果是流式返回还要确认有没有正确处理data:前缀和[DONE]结束标记。第六类OAuth 回调失败。常见原因是回调地址和注册地址不一致或者本地端口被占用。排查时先确认回调 URL 完全一致包括末尾斜杠再换一个端口重试。这里要特别提醒如果你在配置里同时用了 CC Switch、Cline MCP 或 Codex 的auth.json一定要把三件套写全——Base URL、Key、Model ID。少任何一个都会在请求阶段报错而且报错信息往往不直观。我踩过的坑就是只填了 Key 没填 Model ID结果一直报模型不存在查了半天才发现是配置缺项。排查这类问题的通用思路是先看报错原文定位是网络层、认证层还是解析层再逐层缩小范围最后用最小可复现的请求验证。不要一上来就改配置先确认问题出在哪一层。6. 语义一致 CTA把游标机制落到日常调优动作里游标机制讲到这里父游标、子游标、共享游标的层级关系和复用逻辑应该比较清楚了。回到开头的线上告警场景硬解析高本质是父游标或子游标没能有效共享。解决路径无非三条——统一 SQL 文本、使用绑定变量、控制执行环境一致性。这三条对应到具体动作就是查v$sqlarea找重复父游标、查v$sql看子游标数量、查v$sql_shared_cursor定位共享失败原因。如果你想把这套排查流程固化成日常动作可以把它整理成脚本配合 TaoToken 的 API Keyshttps://taotoken.net/api 下的 api-keys 路径和接入文档https://taotoken.net/api 下的 doc 路径来管理。需要验证模型对 SQL 解析结果的理解时用模型对话入口需要长期维护这套调优脚本和 Agent 时用 Coding Plan。Claude Code 相关的接入配置参考 https://taotoken.net/api 下的 ClaudeCodeAnthropic 路径。最后留一个实用技巧每次做完游标实验记得清理测试数据并检查v$open_cursor里有没有残留的未关闭游标。游标泄漏在长连接应用里很隐蔽但积累起来会拖垮 UGA 内存。养成打开即关闭的习惯比事后排查省事得多。