1. 一次线上抖动,把我拉回 VERSION_COUNT 这个老话题
CURSOR_SHARING、VERSION_COUNT和绑定变量这三者的关系,是 Oracle SQL 调优里最容易被忽略、又最容易在半夜把你叫起来的一类问题。简单说:VERSION_COUNT是V$SQLAREA里同一条 SQL 文本对应的子游标(child cursor)数量,也就是这条 SQL 在共享池里攒了多少个执行计划版本;CURSOR_SHARING决定 Oracle 允许多相似的 SQL 共享同一个父游标;绑定变量则决定这条 SQL 到底是"一条模板复用多次"还是"每条字面量各占一个坑"。这三者一旦配合不好,VERSION_COUNT就会膨胀,共享池被大量几乎一样的子游标塞满,硬解析变多,library cache latch等待飙升,业务响应时间跟着抖。
这篇面向 DBA 和后端开发,不讲概念堆砌,直接给可复制的查询语句、V$SQL_SHARED_CURSOR诊断脚本和验证动作,让你能在真实调优场景里确认:版本膨胀到底是不是绑定变量和游标共享策略引起的。适合正在排查共享池压力、latch 争用、或者刚接手一套"SQL 写法很随意"的老系统的同学。
2. 先把 TaoToken 的接入前置准备好
我平时做这类诊断,习惯把模型对话和 API 调用放在手边,遇到不熟的等待事件或参数含义,直接问一句比翻文档快。TaoToken 这边接入很直接,官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api (这个不加 UTM)。你需要先去控制台拿 Key,地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,Key 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。
拿 Key 的步骤不复杂:注册登录后进控制台,在 API Keys 页面新建一个 Key,复制出来存好。如果你只是想先验证模型能不能用、问几个 Oracle 参数问题,直接用模型对话页 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 就行,不用写代码。要是你打算长期做 SQL 调优、写诊断脚本、跑 Agent 自动分析 AWR,那更适合上 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ,按套餐走比单次调用省心。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,Claude Code 相关的配置看 https://taotoken.net/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite 。
注意:TaoToken 只是帮你把模型能力接进来的通道,真正定位
VERSION_COUNT膨胀还得靠下面这些 Oracle 字典视图和脚本。
3. 可复制的诊断配置与脚本
3.1 先确认当前 CURSOR_SHARING 和共享池状态
第一步永远是看现场。别急着改参数,先确认当前CURSOR_SHARING是什么值,共享池里有没有明显的版本堆积。
-- 查看当前 cursor_sharing 设置 show parameter cursor_sharing; -- 或者用字典视图,方便脚本化 SELECT name, value, isdefault FROM v$parameter WHERE name = 'cursor_sharing'; -- 看共享池整体情况 SELECT pool, name, bytes/1024/1024 AS mb FROM v$sgastat WHERE pool = 'shared pool' AND name IN ('free memory', 'miscellaneous');cursor_sharing有三个值:EXACT(默认,精确匹配,字面量不同就是不同 SQL)、FORCE(强制把字面量替换成绑定变量)、SIMILAR(有柱状图时按绑定变量处理,没柱状图时等同 FORCE)。这个参数在 11g 之后SIMILAR已经被标记为废弃,12c 起官方建议只用EXACT或FORCE,这点后面排障会再提。
3.2 定位 VERSION_COUNT 偏高的 SQL
V$SQLAREA里VERSION_COUNT就是子游标数量。直接按它倒序排,谁高谁可疑。
-- 找出 VERSION_COUNT 最高的 SQL SELECT sql_id, version_count, executions, parse_calls, loaded_versions, sql_text FROM v$sqlarea WHERE version_count > 1 ORDER BY version_count DESC FETCH FIRST 20 ROWS ONLY;如果你在 11g 环境,FETCH FIRST不支持,换成SELECT * FROM ( ... ORDER BY version_count DESC ) WHERE ROWNUM <= 20。
3.3 用 V$SQL_SHARED_CURSOR 看"为什么不共享"
这是最关键的一步。V$SQL_SHARED_CURSOR会告诉你每个子游标为什么没能和父游标共享,每一列对应一个原因,值是Y就表示"因为这个原因不共享"。
-- 针对某条 sql_id,看每个子游标的不可共享原因 SELECT child_number, reason, address, hash_value FROM v$sql_shared_cursor WHERE sql_id = '&sql_id';reason列是 Oracle 把多个原因拼在一起的字符串,常见的有BIND_LENGTH_UPGRADE(绑定变量长度变化)、BIND_MISMATCH(绑定变量类型或数量不一致)、OPTIMIZER_MISMATCH(优化器环境不同)、HASH_MATCH_FAILED、PURGED_CURSOR等。如果你看到BIND_LENGTH_UPGRADE反复出现,基本可以锁定是绑定变量长度变化导致的版本膨胀。
更细的版本可以逐列查:
SELECT sql_id, child_number, bind_length_upgrade, bind_mismatch, optimizer_mismatch, stats_row_mismatch, language_mismatch, auth_check_mismatch FROM v$sql_shared_cursor WHERE sql_id = '&sql_id' ORDER BY child_number;3.4 判断 SQL 是否真的用了绑定变量
有个很实用的小窍门:如果sql_text里出现:"SYS_B_0"这种形式,说明这条 SQL 原本没写绑定变量,是CURSOR_SHARING帮你替换的。如果看到的是:1、:name这种,才是应用自己写的绑定变量。
-- 看 SQL 文本里是 SYS_B 还是应用自己的绑定变量 SELECT sql_id, version_count, sql_text FROM v$sqlarea WHERE sql_text LIKE '%SYS_B_%' AND version_count > 1 ORDER BY version_count DESC;3.5 复现实验:柱状图 + SIMILAR 如何把 VERSION_COUNT 顶上去
想亲手验证,可以按下面这套走。建一张小表,插几行数据,然后分别在不同CURSOR_SHARING和统计信息状态下跑同样的字面量 SQL。
-- 建测试表 CREATE TABLE test ( name VARCHAR2(50), address VARCHAR2(50) ); INSERT INTO test VALUES ('Robinson', 'ChongQing'); INSERT INTO test VALUES ('luoluo', 'China'); INSERT INTO test VALUES ('luobingsen', 'Earth'); INSERT INTO test VALUES ('ROBINSON', 'YuBei'); INSERT INTO test VALUES ('bingbing', 'Tiananmen'); INSERT INTO test VALUES ('aaaaaa', 'bbbbbbbbbb'); INSERT INTO test VALUES ('aaaaaa', 'bbbbbbbbbbb'); COMMIT; -- 清空共享池,保证干净起点 ALTER SYSTEM FLUSH SHARED_POOL;然后在CURSOR_SHARING=EXACT下跑七条字面量不同的 SQL:
SELECT name FROM test WHERE address = 'ChongQing'; SELECT name FROM test WHERE address = 'Tiananmen'; SELECT name FROM test WHERE address = 'China'; SELECT name FROM test WHERE address = 'Earth'; SELECT name FROM test WHERE address = 'YuBei'; SELECT name FROM test WHERE address = 'bbbbbbbbbb'; SELECT name FROM test WHERE address = 'bbbbbbbbbbb';查一下结果:
SELECT sql_text, version_count FROM v$sqlarea WHERE sql_text LIKE 'SELECT NAME FROM TEST%';你会看到七条 SQL,每条VERSION_COUNT都是 1,但它们是七个不同的父游标,各自硬解析了一次。这就是EXACT的代价:字面量不同,完全不共享。
接着切到SIMILAR,先删掉统计信息再跑:
ALTER SYSTEM SET cursor_sharing = SIMILAR; -- 需要重启或至少让参数生效 EXEC dbms_stats.delete_schema_stats('你的schema名');再跑那七条 SQL,查V$SQL,你会发现它们还是七条独立 SQL,VERSION_COUNT还是 1。原因是没有柱状图,SIMILAR此时等同FORCE但没触发替换逻辑,Oracle 没有强制绑定。
然后收集柱状图:
EXEC dbms_stats.gather_table_stats( ownname => '你的schema名', tabname => 'TEST', cascade => FALSE, method_opt => 'for columns address size 2' );再跑那七条 SQL,这次查V$SQL:
SELECT sql_text, hash_value, child_address FROM v$sql WHERE sql_text LIKE 'SELECT NAME FROM TEST%';你会看到七行sql_text全变成SELECT NAME FROM TEST WHERE ADDRESS=:"SYS_B_0",hash_value相同,但child_address各不相同。再查V$SQLAREA:
SELECT sql_text, version_count FROM v$sqlarea WHERE sql_text LIKE 'SELECT NAME FROM TEST%';VERSION_COUNT变成 7。这就是版本膨胀的现场:一条父游标下挂了七个子游标,每个子游标一个执行计划。原因是柱状图让 Oracle 认为不同字面量对应的数据分布不同,需要各自独立的执行计划,于是即使强制绑定了变量,还是给每个值生成了独立子游标。
最后切到FORCE:
ALTER SYSTEM SET cursor_sharing = FORCE; ALTER SYSTEM FLUSH SHARED_POOL;再跑那七条 SQL,查V$SQLAREA,VERSION_COUNT降回 1。因为FORCE不看柱状图,直接按绑定变量共享。
4. 验证请求与成功结果
诊断做完,怎么确认问题真的解决了?看三个指标。
第一,VERSION_COUNT是否回落。针对目标sql_id反复查:
SELECT sql_id, version_count, executions, parse_calls FROM v$sqlarea WHERE sql_id = '&sql_id';如果version_count从几十降到个位数甚至 1,说明子游标不再堆积。
第二,library cache latch等待是否下降。查 AWR 或实时视图:
SELECT event, total_waits, time_waited_micro/1000000 AS seconds FROM v$system_event WHERE event LIKE 'latch: library cache%' ORDER BY time_waited_micro DESC;对比调整前后的time_waited,如果明显下降,说明共享池争用缓解了。
第三,硬解析比例是否降低。看parse_calls和executions的比值:
SELECT sql_id, executions, parse_calls, ROUND(parse_calls / GREATEST(executions,1), 4) AS parse_ratio FROM v$sqlarea WHERE sql_id = '&sql_id';parse_ratio越接近 0 越好,说明大部分执行都走了软解析。
如果你用 TaoToken 的模型对话来辅助分析,可以把V$SQL_SHARED_CURSOR的输出贴进去,让它帮你归类哪些reason是绑定变量问题、哪些是优化器环境问题。模型对话入口在 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,适合快速判断。要长期跑这类分析脚本,Coding Plan 更合适: https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。
5. 本篇常见错排查
5.1 改了 cursor_sharing 没生效
cursor_sharing是静态参数,改完必须重启实例或者至少让参数在会话级别生效。ALTER SYSTEM SET之后如果没重启,当前会话可能还在用旧值。用show parameter cursor_sharing确认,别只看ALTER SYSTEM返回成功。
5.2 SIMILAR 在 12c 之后行为变了
SIMILAR在 11g 就已经不推荐,12c 起官方文档明确说它被废弃,行为可能和 11g 不一致。如果你在 12c 以上环境看到SIMILAR相关的怪异版本膨胀,先确认版本,别照搬 11g 的经验。生产环境建议只用EXACT或FORCE。
5.3 用了绑定变量但 VERSION_COUNT 还是高
这种情况不一定是CURSOR_SHARING的锅。常见原因有:绑定变量长度变化触发BIND_LENGTH_UPGRADE;不同 schema 下同名 SQL 各自解析;优化器环境(optimizer_mode、optimizer_features_enable)不同;数据库 Bug。用V$SQL_SHARED_CURSOR逐列看,别只盯着cursor_sharing。
5.4 看到 SYS_B 就以为是应用问题
SYS_B_0是CURSOR_SHARING替换字面量时生成的系统绑定变量名。看到它说明 SQL 原本没写绑定变量,是参数帮你替换的。这时候要回到应用层,找那些拼接字面量的 SQL,改成真正的绑定变量,而不是继续依赖FORCE。
5.5 直接上 FORCE 的副作用
FORCE能压VERSION_COUNT,但它会让所有字面量 SQL 共享同一个执行计划。如果数据分布倾斜严重,某些值用同一个计划会走错索引,反而更慢。所以FORCE是万不得已的兜底,根本解法还是应用层重写 SQL、正确使用绑定变量。
6. 接下来怎么走
如果你已经用上面的脚本定位到VERSION_COUNT膨胀,下一步分两条路。应用能改的,去代码里找拼接 SQL 的地方,改成绑定变量,这是最干净的解法。应用改不动的,短期用FORCE压住,同时用V$SQL_SHARED_CURSOR持续监控,确认没有新的BIND_LENGTH_UPGRADE冒出来。
需要把诊断脚本接进自动化流程、或者让模型帮你批量分析 AWR 和V$SQL_SHARED_CURSOR输出,可以走 API: https://taotoken.net/api ,Key 在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 拿,接入细节看文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。长期做 SQL 调优和 Agent 自动巡检,Coding Plan 更划算: https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。