在日常我们对OracleSQL优化过程中,能够准确获取SQL的执行计划中真实的绑定变量值是很多分析工作的基石,这样描述,真的不过分。

90% 以上的SQL性能问题,差不多都需要结合实际参数值来判断这个执行计划是否合理,同样一条SQL,如果传的绑定变量值不同,有可能会产生完全不同的执行路径并表现出不同的性性能来。

不少DBA甚至是一些老炮DBA和开发一起排查慢SQL时,经常会遇到“执行计划看起来没啥问题,但咋就是实际执行的时候特别慢呢”。这种情况,很有可能是因为没能拿到真实的绑定变量值,导致分析的方向出现了问题。

接下来,我将以oracle内核的捕获机制作为出发点,和大家聊聊生产环境中获取真实绑定变量值的实用方法,也希望和大家交流,希望大家能给出更好的建议。

一、Oracle 绑定变量捕获的核心机制

Oracle 对绑定变量值的捕获实际上是并非实时、无差别进行,而是有其严苛的内部规则,也是很多人容易忽略的关键点,只有了解了这些规则,才有可能避免查询时走弯路。

1.1 触发捕获的两个必要条件

Oracle 只会在以下两种场景会将绑定变量实际值写入系统视图:

  1. 硬解析阶段:当含有绑定变量的 SQL 第一次执行,或因为其它的各种原因(如统计信息变更、DDL 操作等)导致需要重新硬解析时,Oracle 会自动捕获本次执行的所有绑定变量值。
  2. 软解析 / 软软解析阶段:当 SQL 已经存在共享池中,以软解析或软软解析方式重复执行时,Oracle 也有可能捕获绑定变量值,但默认情况下,两次捕获的最小时间间隔为 15 分钟。

这个 15 分钟的间隔其实也是 Oracle 为了平衡性能和诊断需求而设计的 —— 如果每次执行都捕获绑定变量值,这样也会给一些高并发系统带来巨大CPU和内存开销。

1.2 一个极其重要的捕获限制

Oracle 对绑定变量的捕获有一个非常特殊的限制,这也是生产环境中很多DBA老炮容易踩坑的地方:

Oracle 永远不会捕获 INSERT 语句 VALUES 子句中的绑定变量值,无论这条 INSERT 语句是硬解析还是软解析执行。

这个限制是从 Oracle 10g 版本开始并延续到后面的很多版本。很多 DBA 在排查批量插入性能问题时,花费大量时间查询 v$sql_bind_capture 视图,却始终得不到任何结果,正是这个原因。

二、实时绑定变量值查询方法

当目标 SQL 的游标还存在于共享池中时,咱们可以通过v$sql_bind_capture视图查询最近捕获的绑定变量值。这也是是生产环境中最常用的实时查询方法。

2.1 基础查询语句

--- 查询指定SQL_ID的实时绑定变量值

SELECT 
    sql_id,
    name AS bind_name,
    position AS bind_position,
    datatype_string AS data_type,
    value_string AS bind_value,
    TO_CHAR(last_captured, 'yyyy-mm-dd hh24:mi:ss') AS capture_time
FROM 
    v$sql_bind_capture
WHERE 
    sql_id = '&target_sql_id'
ORDER BY 
    position;

2.2 查询结果解读

  • sql_id:目标 SQL 的唯一标识
  • name:绑定变量的名称(如:1、:2 或自定义名称)
  • position:绑定变量在 SQL 中的位置
  • datatype_string:绑定变量的数据类型
  • value_string:捕获到的绑定变量值(字符串形式)
  • last_captured:最后一次捕获的时间戳

2.3 常见问题排查

如果查询v$sql_bind_capture没有返回结果,通常可能有以下三种原因:

1)目标 SQL 的游标已经被老化(age out)出共享池

2)目标 SQL 是 INSERT VALUES 语句(受限于前面提到的捕获限制)

3)距离上一次捕获的时间间隔不足 15 分钟,且期间没有发生硬解析

三、历史绑定变量值查询方法

当目标 SQL 的游标已经被老化出共享池时,我们需要查询 AWR中的历史数据。Oracle 会将捕获到的绑定变量值定期写入 AWR中,保留时间由 AWR 保留策略决定(通常默认是 8 天)。

3.1 通过 dba_hist_sqlbind 查询

dba_hist_sqlbind视图存储了历史上所有被捕获的绑定变量信息,也是查询历史数据的首选:

--- 查询指定SQL_ID的历史绑定变量值
SELECT 
    snap_id,
    name AS bind_name,
    position AS bind_position,
    value_string AS bind_value,
    TO_CHAR(last_captured, 'yyyy-mm-dd hh24:mi:ss') AS capture_time,
    was_captured
FROM 
    dba_hist_sqlbind
WHERE 
    sql_id = '&target_sql_id'
ORDER BY 
    snap_id, position;

3.2 通过 dba_hist_sqlstat 查询

dba_hist_sqlstat视图的bind_data列以二进制格式存储了绑定变量数据,可以使用dbms_sqltune.extract_bind函数来进行解析:

-- 解析dba_hist_sqlstat中的绑定变量数据
SELECT 
    snap_id,
    dbms_sqltune.extract_bind(bind_data, 1).value_string AS bind_1,
    dbms_sqltune.extract_bind(bind_data, 2).value_string AS bind_2,
    dbms_sqltune.extract_bind(bind_data, 3).value_string AS bind_3
FROM 
    dba_hist_sqlstat
WHERE 
    sql_id = '&target_sql_id'
    AND bind_data IS NOT NULL
ORDER BY 
    snap_id;

注意:AWR 数据默认每小时生成一次快照,因此历史绑定变量值的时间精度为小时级。如果需要更精确的时间信息,可以结合dba_hist_active_sess_history视图一起进行分析。

四、INSERT 语句绑定变量值获取解决方案

前面也提到了,Oracle 不会捕获 INSERT VALUES 语句的绑定变量值,这给批量插入性能问题的排查带来了很大困难。下面介绍一个小技巧可以绕过这个限制。

4.1 核心技巧:改写 INSERT 语句

将标准的INSERT INTO table VALUES(...)语句,改写为INSERT INTO table SELECT ... FROM dual WHERE ...的形式。这样,绑定变量就出现在了 WHERE 子句中,Oracle 就会正常捕获它们的值。

改写示例

--- 原语句(无法捕获绑定变量)
INSERT INTO t (id, name) VALUES (:n, :v);

-- 改写后(可以正常捕获绑定变量)
INSERT INTO t (id, name)
SELECT :n, :v FROM dual WHERE 1=1;

4.2 效果验证

通常可以通过以下步骤验证这个技巧的有效性:

1)执行改写后的 INSERT 语句

2)查询 v$sqlarea 确认 SQL 已经执行

3)查询 v$sql_bind_capture 查看绑定变量值

--- 执行改写后的INSERT语句
DECLARE
    n NUMBER(10);
    v VARCHAR2(128);
BEGIN
    n := 1001;
    v := 'test_user_001';
    EXECUTE IMMEDIATE 'INSERT INTO t (id, name) SELECT :n, :v FROM dual WHERE 1=1' USING n, v;
    COMMIT;
END;
/

-- 查看绑定变量捕获结果
SELECT 
    sql_id,
    name,
    position,
    value_string,
    TO_CHAR(last_captured, 'yyyy-mm-dd hh24:mi:ss') AS capture_time
FROM 
    v$sql_bind_capture
WHERE 
    sql_id IN (SELECT sql_id FROM v$sqlarea WHERE sql_text LIKE 'INSERT INTO t (id, name) SELECT%');

执行结果会显示,Oracle 已经成功捕获到了:n=1001 和:v='test_user_001' 这两个绑定变量值。

4.3 性能影响评估

很多人会担心这种改写会带来性能开销。实际上,SELECT ... FROM dual WHERE 1=1是一个非常轻量级的操作,Oracle 优化器会自动将其优化为常量表达式,不会产生任何实际的表扫描或 IO 操作。通常在生产环境的高并发批量插入场景中,这样改写的性能损耗可以忽略不计。

五、高级技巧与特殊场景处理

5.1 修改绑定变量捕获间隔

如果需要更频繁地捕获绑定变量值,可以通过修改隐藏参数_cursor_bind_capture_interval来调整捕获间隔(单位为秒)。默认值是 900 秒。

--- 查看当前捕获间隔
SELECT 
    ksppinm AS parameter_name,
    ksppstvl AS parameter_value
FROM 
    x$ksppi
    JOIN x$ksppcv ON x$ksppi.indx = x$ksppcv.indx
WHERE 
    ksppinm = '_cursor_bind_capture_interval';

-- 修改捕获间隔为5分钟(300秒)
ALTER SYSTEM SET "_cursor_bind_capture_interval" = 300 SCOPE=BOTH;

风险提示:减小捕获间隔会增加系统的 CPU 和内存开销。在高并发系统中,建议不要将间隔设置得小于 300 秒。修改前务必在测试环境进行充分验证。

5.2 使用 10046 事件获取绑定变量值

当上述方法都无法获取到绑定变量值时,还可以使用 Oracle 的 10046 事件进行 SQL 跟踪。这是底层可靠的方法,可以捕获到 SQL 执行时的所有绑定变量值,不受任何捕获规则的限制。

--- 开启当前会话的10046跟踪(级别12,包含绑定变量和等待事件)
ALTER SESSION SET EVENTS '10046 trace name context forever, level 12';

-- 执行需要跟踪的SQL语句
...

--- 关闭跟踪
ALTER SESSION SET EVENTS '10046 trace name context off';

--- 查找跟踪文件
SELECT value FROM v$diag_info WHERE name = 'Default Trace File';

生成的跟踪文件中会包含完整的绑定变量值信息,可以使用 tkprof 或其他工具进行解析。

六、常见误区与最佳实践

6.1 常见误区

1)误区一:认为 v$sql_bind_capture 会捕获所有执行的绑定变量值

      实际上,它只捕获硬解析时和每隔 15 分钟软解析时的变量值

2)误区二:花费大量时间查询 INSERT VALUES 语句的绑定变量值

      记住 Oracle 永远不会捕获这类语句的 VALUES 子句绑定变量

3)误区三:随意修改_cursor_bind_capture_interval 参数

      过小的间隔会给系统带来不必要的性能压力

6.2 最佳实践

1)优先使用 v$sql_bind_capture:对于实时问题,这是最快、最方便的方法

2)合理利用 AWR 历史数据:对于已经发生的问题,AWR 是最主要的数据源

3)规范 SQL 编写:对于需要排查性能问题的 INSERT 语句,尽量使用 INSERT SELECT 的形式

4)谨慎修改隐藏参数:除非万不得已,否则不要修改_cursor_bind_capture_interval 参数

5)结合多种方法:当一种方法无法获取数据时,尝试使用其他方法(如 10046 事件)

七、总结

想要获取 Oracle SQL 的绑定变量值是性能优化工作中不可或缺的一环。上面也讲解了 Oracle 绑定变量的捕获机制,以及实时查询、历史查询、INSERT 语句特殊处理等多种实用方法。

但是在实际工作中,咱们还是需要根据不同的场景选择合适的方法:

  • 对于实时问题,优先使用v$sql_bind_capture视图
  • 对于历史问题,使用dba_hist_sqlbinddba_hist_sqlstat视图
  • 对于 INSERT 语句,使用改写为 INSERT SELECT 的技巧
  • 对于特殊场景,使用 10046 事件进行跟踪

掌握这些方法,能够帮助我们快速准确地定位 SQL 性能问题,能够提升我们的运维效率。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐