Oracle SQL的HASH_VALUE理解:从sql_id到执行计划,一次讲透
1. 从一次执行计划漂移说起HASH_VALUE 到底在查什么如果你在 Oracle 里排查过「同一条 SQL 一会儿快一会儿慢」大概率绕不开v$sqlarea、v$sql和HASH_VALUE这几个东西。HASH_VALUE是 Oracle 对 SQL 文本做哈希运算后得到的一个数值它和ADDRESS一起在 10g 之前是定位 library cache 中游标的唯一组合10g 之后SQL_ID逐渐成为更稳定的标识但HASH_VALUE依然大量出现在 AWR、ASH、v$sqlarea以及各种老脚本里。简单说SQL_ID是「身份证号」HASH_VALUE是「指纹」ADDRESS是「当前住址」——住址会变指纹理论上不变但哈希碰撞和版本差异会让它偶尔骗你。这篇面向的是 DBA 排查共享池中 SQL 执行计划漂移、游标冲突cursor conflict的场景。你会拿到可直接复制的v$sqlarea/v$sql查询语句、HASH_VALUE与SQL_ID互查脚本以及通过HASH_VALUE定位执行计划异常的验证步骤。适合已经会连数据库、能跑sqlplus或 SQL Developer但对 library cache 内部定位逻辑还不太熟的人。先说结论不要只用 HASH_VALUE 定位 SQL。它适合做「快速过滤」真正定位到具体子游标、具体执行计划必须落到SQL_ID CHILD_NUMBER再配合PLAN_HASH_VALUE判断计划是否真的变了。下面按这个思路一步步来。2. 前置准备TaoToken 环境与连接方式我平时做 Oracle 排查习惯把常用脚本和模型辅助分析放在一起。TaoToken 在这里的作用是提供一个统一的模型对话入口方便把 SQL 文本、执行计划、报错信息丢进去做辅助解读而不是替代 SQLPlus 或任何 Oracle 客户端。它本身不碰你的数据库只是帮你把「这段执行计划为什么走全表扫描」这类问题问清楚。如果你还没配好可以先去官网了解整体能力https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。需要拿 Key 做接口调用的话直接进控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite API Key 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。接口基址是 https://taotoken.net/api 注意这个地址不带 UTM 参数配置时别画蛇添足。如果你只是想快速验证一段 SQL 的语义或让模型帮你解释执行计划用模型对话页就够了https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 。长期做编码、写巡检脚本、维护 Agent 的可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。接入细节和参数说明统一看文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。注意TaoToken 是辅助分析工具不是数据库客户端也不做任何「直连生产库」的事。所有 SQL 仍然在你的 sqlplus / SQL Developer / DBeaver 里执行。3. 可复制配置HASH_VALUE 与 SQL_ID 互查脚本3.1 用 HASH_VALUE 反查 SQL_ID 和 SQL 文本最常用的入口是v$sqlarea它按父游标聚合一行代表一条 SQL 文本。假设你从 AWR 报告里拿到一个HASH_VALUE 1234567890想找到对应的 SQLSELECT sql_id, hash_value, address, plan_hash_value, executions, parse_calls, first_load_time, last_active_time, sql_text FROM v$sqlarea WHERE hash_value hash_val;这里ADDRESS是父游标句柄地址PLAN_HASH_VALUE是该父游标下某个子游标的计划哈希。注意v$sqlarea里的PLAN_HASH_VALUE只反映其中一个子游标不能代表全部。3.2 用 SQL_ID 反查 HASH_VALUE反过来如果你手上是SQL_ID想拿到HASH_VALUE去匹配老脚本SELECT sql_id, hash_value, address, plan_hash_value, child_number, is_obsolete, is_shareable, last_load_time FROM v$sql WHERE sql_id sql_id ORDER BY child_number;v$sql是子游标级别一行一个 child cursor。IS_OBSOLETE Y说明这个父游标因为子游标数量达到 1024 被标记为废弃这是执行计划漂移和游标冲突的典型信号。3.3 通过 ADDRESS HASH_VALUE 组合定位在 10g 之前ADDRESS HASH_VALUE是唯一标识。现在虽然SQL_ID更稳但很多老视图和脚本仍然用这个组合。比如查v$sqltext拼完整 SQLSELECT s.address, s.hash_value, s.sql_id, t.piece, t.sql_text FROM v$sqltext t JOIN v$sqlarea s ON t.address s.address AND t.hash_value s.hash_value WHERE s.hash_value hash_val ORDER BY t.piece;v$sqltext的SQL_TEXT是VARCHAR2(64)一条长 SQL 会被切成多行PIECE从 0 开始排序。拼的时候按PIECE顺序拼接即可。3.4 定位执行计划漂移的核心查询真正判断「计划是否漂移」要看同一个SQL_ID下不同CHILD_NUMBER的PLAN_HASH_VALUESELECT sql_id, child_number, plan_hash_value, hash_value, address, child_address, executions, buffer_gets, disk_reads, rows_processed, is_obsolete, is_shareable, is_bind_sensitive, is_bind_aware, last_active_time FROM v$sql WHERE sql_id sql_id ORDER BY child_number;如果同一个SQL_ID出现多个不同的PLAN_HASH_VALUE说明计划确实漂移了。IS_BIND_SENSITIVE Y且IS_BIND_AWARE N时往往是绑定变量窥探导致计划不稳定。4. 验证请求从 HASH_VALUE 到执行计划的完整链路4.1 构造一个可复现的测试场景先建一张测试表插入不均匀数据制造计划漂移的条件CREATE TABLE t_hash_demo AS SELECT LEVEL AS id, CASE WHEN LEVEL 990000 THEN COMMON ELSE RARE END AS flag, RPAD(X, 100, X) AS pad FROM dual CONNECT BY LEVEL 1000000; CREATE INDEX idx_t_hash_flag ON t_hash_demo(flag); BEGIN DBMS_STATS.GATHER_TABLE_STATS(USER, T_HASH_DEMO); END; /flag COMMON占 99%flag RARE只占 1%。用绑定变量执行两次观察是否产生不同子游标VARIABLE b VARCHAR2(10); EXEC :b : COMMON; SELECT COUNT(*) FROM t_hash_demo WHERE flag :b; EXEC :b : RARE; SELECT COUNT(*) FROM t_hash_demo WHERE flag :b;4.2 用 HASH_VALUE 找到这条 SQLSELECT sql_id, hash_value, address, plan_hash_value, executions FROM v$sqlarea WHERE sql_text LIKE %t_hash_demo% AND sql_text NOT LIKE %v$%;拿到HASH_VALUE后用 3.4 的查询看子游标SELECT child_number, plan_hash_value, executions, buffer_gets, disk_reads, is_bind_sensitive, is_bind_aware FROM v$sql WHERE sql_id sql_id ORDER BY child_number;正常情况下你会看到两个子游标PLAN_HASH_VALUE不同一个走索引一个走全表扫描。这就是典型的 bind peeking 导致的计划漂移。4.3 查看具体执行计划SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(sql_id, child_no, ALLSTATS LAST));把child_no分别填 0 和 1对比两次输出。你会看到Rows、Buffers、Cost的差异以及Peeked Binds那一段显示的绑定值。这一步是验证「HASH_VALUE 定位到的 SQL 是否真的计划异常」的关键。4.4 用 TaoToken 辅助解读执行计划把DBMS_XPLAN的输出贴到模型对话里让它帮你解释「为什么 child 1 走了全表扫描」https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 。我试过把带Peeked Binds的计划丢进去它能比较清楚地指出绑定变量窥探和直方图的交互。注意别把生产库的真实表名、敏感字段贴进去脱敏后再问。5. 本篇常见错排查5.1 HASH_VALUE 相同但 SQL 不同哈希碰撞虽然概率低但确实存在。如果你用HASH_VALUE过滤出多条SQL_ID不要惊讶SELECT sql_id, hash_value, address, sql_text FROM v$sqlarea WHERE hash_value hash_val;如果返回多行说明发生了碰撞必须用SQL_ID或ADDRESS HASH_VALUE进一步区分。这也是为什么 10g 之后 Oracle 引入SQL_ID13 位 base32作为更可靠的标识。5.2 ADDRESS 变了但 HASH_VALUE 没变ADDRESS是父游标在 library cache 中的句柄地址游标被换出、重新加载后地址会变但HASH_VALUE不变。所以用ADDRESS HASH_VALUE做关联时必须保证两个视图查的是同一时刻的快照。跨时间点对比会得到错误结果。5.3 IS_OBSOLETE Y 的父游标当一个父游标的子游标数量达到 1024且无法共享时Oracle 会把它标记为IS_OBSOLETE Y并创建新的父游标。这时你会看到同一个SQL_ID对应多个ADDRESSSELECT sql_id, address, hash_value, is_obsolete, child_number FROM v$sql WHERE sql_id sql_id ORDER BY address, child_number;排查时要过滤掉IS_OBSOLETE Y的行否则统计会重复。5.4 PLAN_HASH_VALUE 为 0 或 NULLPLAN_HASH_VALUE 0通常出现在游标还没执行完、或者计划还没完全生成的时候。等几秒再查或者用DBMS_XPLAN.DISPLAY_CURSOR直接看。如果一直是 0检查OPTIMIZER_MODE和是否有SQL_PATCH/SQL_PROFILE干扰。5.5 用 HASH_VALUE 查不到任何行最常见的原因是 SQL 已经被换出共享池。v$sqlarea只保留当前在 library cache 中的游标。如果 SQL 很久没执行或者共享池压力大被 aged out就查不到。这时候只能从 AWR 的DBA_HIST_SQLSTAT里找SELECT sql_id, plan_hash_value, executions_delta, buffer_gets_delta FROM dba_hist_sqlstat WHERE sql_id sql_id ORDER BY snap_id DESC;注意 AWR 里没有HASH_VALUE列需要用SQL_ID关联DBA_HIST_SQLTEXT。6. 把 HASH_VALUE 用对接入与长期维护建议HASH_VALUE的价值在于「快速过滤」和「兼容老脚本」但它不是精确定位工具。我的习惯是先用HASH_VALUE在v$sqlarea里缩小范围拿到SQL_ID后所有后续分析都基于SQL_ID CHILD_NUMBER PLAN_HASH_VALUE。这样既利用了老脚本的便利又避免了哈希碰撞和地址漂移带来的误判。如果你要把这套排查逻辑做成巡检脚本或 Agent 定期跑建议用 Coding Plan 来维护代码和提示词https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。接口调用和参数细节看文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite Key 在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 管理。基址仍然是 https://taotoken.net/api 不带 UTM。最后留一个我常用的「一键定位」脚本把hash_val换成你手上的值就能跑SELECT a.sql_id, a.hash_value, a.address, s.child_number, s.plan_hash_value, s.executions, s.buffer_gets, s.disk_reads, s.is_obsolete, s.is_bind_sensitive, s.is_bind_aware, s.last_active_time FROM v$sqlarea a JOIN v$sql s ON a.sql_id s.sql_id WHERE a.hash_value hash_val ORDER BY s.child_number;跑完这一步你基本就能判断这条 SQL 有没有多个子游标、计划有没有漂移、是不是 bind sensitive 但还没 bind aware。剩下的就是拿SQL_ID和CHILD_NUMBER去DBMS_XPLAN.DISPLAY_CURSOR看细节了。