前段时间,给大家做Oracle 培训时,有同学提出了几个很有价值的问题:为什么直方图和绑定变量一起使用时,
有时会引发性能问题。
同学们希望好好学习一下借助直方图来解决绑定变量窥视(bind peeking)带来的问题。
我对直方图和绑定变量做了一下具体问题的总结,总结如下:
1. 当 SQL 使用绑定变量时,直方图会对执行计划产生怎样的影响?
2. 为什么直方图和绑定变量无法很好地协同工作?
3. 使用绑定变量时不要建直方图,背后的原因是什么?
如果大家对以上有疑问,我们分几次对这3个问题进行详细讲述。
当 WHERE 条件里和绑定变量做对比的列上存在直方图时,数据库内部会发生什么。
直方图并非完全不能和绑定变量一起工作,但结果通常不是我们想要的。
使用绑定变量的初衷, 是为了减少执行计划的数量,实现游标共享和复用。
而直方图的作用, 则是帮助优化器针对传入的谓词条件选出理论最优的执行计划;
在绑定变量场景下,这个谓词就是**被窥视到的绑定变量值**。
所以,如果某一列建有直方图:
假如这条 SQL 第一次硬解析时,传入的绑定变量是该列中**高频主流值**,优化器就会生成一份适配这个主流值的、
它认为是 “最优” 的执行计划。
反过来,如果第一次硬解析传入的是该列里**极少出现的小众值**,优化器会为同一条 SQL 生成一套完全不一样的执行计划,
这套计划是专门针对小众值优化的(比如会选择索引范围扫描,而不走全表扫描)。
在后续执行这条 SQL 时,即便绑定变量传入的值发生变化,**执行计划却不会随之改变**。
举个场景:表中存在一个占绝大多数的热门值,同时还有大量冷门值。
如果这条 SQL 第一次硬解析时传入的是那个热门值,那么后续所有执行都会沿用硬解析生成的全表扫描计划 ——
哪怕后续查询只需要返回寥寥几行数据,也依然会走全表扫描。
我搭建了测试用例,用于演示上述观点:演示环境使用的是oracle 19c
[oracle@zbyl ~]$ sqlplus / as sysdba
SQL*Plus: Release 19.0.0.0.0 - Production on Tue Oct 20 10:10:24 2026
Version 19.22.0.0.0
Copyright (c) 1982, 2023, Oracle. All rights reserved.
Connected to:
Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production
Version 19.22.0.0.0
SQL>
--创建测试表
SQL> CREATE TABLE TEST (
C1 NUMBER,
C2 NUMBER,
C3 VARCHAR2(300)); 2 3 4
Table created.
--构建测试数据,分热门值,冷门值,两个值倾斜严重。
SQL> INSERT INTO
TEST
SELECT
*
FROM
(SELECT
ROWNUM C1,
DECODE(MOD(ROWNUM,100),99,99,1) C2,
RPAD('A',300,'A') C3
FROM
DUAL
CONNECT BY
LEVEL <= 1000000)
ORDER BY
C2; 2 3 4 5 6 7 8 9 10 11 12 13 14 15
1000000 rows created.
--C2为99的值,有10000条,值为1的有990000条。
SQL> select c2,count(*) from test group by c2;
C2 COUNT(*)
---------- ----------
99 10000
1 990000
--创建索引
SQL> CREATE INDEX IND_T1_C2 ON TEST(C2);
Index created.
--收集直方图信息。
SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(OWNNAME=>USER,TABNAME=>'TEST',CASCADE=>TRUE,METHOD_OPT=>'FOR ALL INDEXED COLUMNS SIZE 254')
PL/SQL procedure successfully completed.
上面的语句创建了一张包含 100 万行的TEST表:其中 99% 的行,C2 列值为 1;1% 的行值为 99。
因为插入时带了 `ORDER BY`,数据的**聚簇因子(clustering factor)非常理想**。
我们在这个建有索引的列上收集了直方图。
我们来做个测试:这条查询**第一次硬解析**时,给绑定变量传入一个冷门值 2。其实值2的没有行,查询不到值的。
SQL> VARIABLE N1 NUMBER
SQL> EXEC :N1:=2
PL/SQL procedure successfully completed.
SQL> SELECT /*+ GATHER_PLAN_STATISTICS */
C1,
C2
FROM
TEST
WHERE
C2 = :N1; 2 3 4 5 6 7
no rows selected
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL,NULL,'ALLSTATS LAST'));
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID fgsshbz6rhybs, child number 0
-------------------------------------
SELECT /*+ GATHER_PLAN_STATISTICS */ C1, C2 FROM TEST WHERE C2
= :N1
Plan hash value: 3318186407
-----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 0 |00:00:00.01 | 3 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| TEST | 1 | 5000 | 0 |00:00:00.01 | 3 |
|* 2 | INDEX RANGE SCAN | IND_T1_C2 | 1 | 5000 | 0 |00:00:00.01 | 3 |
-----------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("C2"=:N1)
20 rows selected.
因此,本次查询没有返回任何行;优化器预估会返回 5000 行,并且选择了索引访问路径来获取数据,使用的索引范围扫INDEX RANGE SCAN。
那这个索引访问路径,对于绑定变量值 1 来说还合适吗?我们继续测试,这次绑定变量传入值 99:
SQL> EXEC :N1:=99
PL/SQL procedure successfully completed.
SQL> SET TIMING ON
SQL> SELECT /*+ GATHER_PLAN_STATISTICS */
C1,
C2
FROM
TEST
WHERE
C2 = :N1; 2 3 4 5 6 7
......
908799 99
896099 99
896199 99
896299 99
896399 99
896499 99
10000 rows selected.
Elapsed: 00:00:00.11
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL,NULL,'ALLSTATS LAST'));
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID fgsshbz6rhybs, child number 0
-------------------------------------
SELECT /*+ GATHER_PLAN_STATISTICS */ C1, C2 FROM TEST WHERE C2
= :N1
Plan hash value: 3318186407
-----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 10000 |00:00:00.01 | 1781 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| TEST | 1 | 5000 | 10000 |00:00:00.01 | 1781 |
|* 2 | INDEX RANGE SCAN | IND_T1_C2 | 1 | 5000 | 10000 |00:00:00.01 | 689 |
-----------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("C2"=:N1)
20 rows selected.
Elapsed: 00:00:00.02
SQL>
次执行,执行计划显示:优化器依旧预估返回 5000 行,而实际返回了 10000 行。同时留意到子游标编号仍然是 0,
说明没有发生硬解析。
我们继续测试,这次绑定变量传入值 1:
SQL> EXEC :N1:=1
PL/SQL procedure successfully completed.
Elapsed: 00:00:00.00
SQL> SELECT /*+ GATHER_PLAN_STATISTICS */
C1,
C2
FROM
TEST
WHERE
C2 = :N1; 2 3 4 5 6 7
......
999922 1
999923 1
999924 1
990000 rows selected.
Elapsed: 00:00:11.62
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL,NULL,'ALLSTATS LAST'));
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID fgsshbz6rhybs, child number 0
-------------------------------------
SELECT /*+ GATHER_PLAN_STATISTICS */ C1, C2 FROM TEST WHERE C2
= :N1
Plan hash value: 3318186407
-----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 10000 |00:00:00.01 | 1781 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| TEST | 1 | 5000 | 10000 |00:00:00.01 | 1781 |
|* 2 | INDEX RANGE SCAN | IND_T1_C2 | 1 | 5000 | 10000 |00:00:00.01 | 689 |
-----------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("C2"=:N1)
20 rows selected.
Elapsed: 00:00:00.02
SQL>
这并不是实际使用的执行计划,因为它仍然显示上一次执行返回 10000 行,使用是上次的执行计划。
而 执行C2=1值时表明实际返回了 990000 行。我们再试执行一次。
SQL> EXEC :N1:=1
PL/SQL procedure successfully completed.
Elapsed: 00:00:00.00
SQL> SELECT /*+ GATHER_PLAN_STATISTICS */
C1,
C2
FROM
TEST
WHERE
C2 = :N1; 2 3 4 5 6 7
......
999922 1
999923 1
999924 1
990000 rows selected.
Elapsed: 00:00:11.62
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL,NULL,'ALLSTATS LAST'));
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID fgsshbz6rhybs, child number 1
-------------------------------------
SELECT /*+ GATHER_PLAN_STATISTICS */ C1, C2 FROM TEST WHERE C2
= :N1
Plan hash value: 1357081020
------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 990K|00:00:00.16 | 108K|
|* 1 | TABLE ACCESS FULL| TEST | 1 | 990K| 990K|00:00:00.16 | 108K|
------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("C2"=:N1)
19 rows selected.
Elapsed: 00:00:00.03
上面展示的才是真正执行时用到的执行计划。
**自适应游标共享(ACS,Oracle 11.1 版本引入)**在这里生效了:它触发了执行计划的重新评估,
避免继续走索引导致查询性能极差。
在 11.1 之前的版本不会有这个重新评估机制,(在旧版本 Oracle 中,如果 SQL 里用的是字面值,设置 `CURSOR_SHARING='SIMILAR'` 有可能达到类似效果)。
使用低版本的数据库进行测试:
SQL> ALTER SESSION SET OPTIMIZER_FEATURES_ENABLE='10.2.0.4';
Session altered.
Elapsed: 00:00:00.00
SQL> VARIABLE N1 NUMBER
SQL> EXEC :N1:=2
PL/SQL procedure successfully completed.
Elapsed: 00:00:00.00
SQL> SELECT /*+ GATHER_PLAN_STATISTICS */
C1,
C2
FROM
TEST
WHERE
C2 = :N1; 2 3 4 5 6 7
no rows selected
Elapsed: 00:00:00.00
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL,NULL,'ALLSTATS LAST'));
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID fgsshbz6rhybs, child number 2
-------------------------------------
SELECT /*+ GATHER_PLAN_STATISTICS */ C1, C2 FROM TEST WHERE C2
= :N1
Plan hash value: 4126779481
---------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
---------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 0 |00:00:00.01 | 3 |
| 1 | TABLE ACCESS BY INDEX ROWID| TEST | 1 | 5000 | 0 |00:00:00.01 | 3 |
|* 2 | INDEX RANGE SCAN | IND_T1_C2 | 1 | 5000 | 0 |00:00:00.01 | 3 |
---------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("C2"=:N1)
20 rows selected.
Elapsed: 00:00:00.09
注意,上面示例中 **CHILD_NUMBER(子游标编号)变成了 2**,原因是我们修改了优化器的执行环境
(大家可以思考一下修改数据库的哪些参数,哪些因素会触发优化器执行环境发生变化):
SQL> EXEC :N1:=99
PL/SQL procedure successfully completed.
Elapsed: 00:00:00.01
SQL> SELECT /*+ GATHER_PLAN_STATISTICS */
C1,
C2
FROM
TEST
WHERE
C2 = :N1; 2 3 4 5 6 7
.......
896199 99
896299 99
896399 99
896499 99
10000 rows selected.
Elapsed: 00:00:00.11
SQL>
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL,NULL,'ALLSTATS LAST'));
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID fgsshbz6rhybs, child number 2
-------------------------------------
SELECT /*+ GATHER_PLAN_STATISTICS */ C1, C2 FROM TEST WHERE C2
= :N1
Plan hash value: 4126779481
---------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
---------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 10000 |00:00:00.01 | 1781 |
| 1 | TABLE ACCESS BY INDEX ROWID| TEST | 1 | 5000 | 10000 |00:00:00.01 | 1781 |
|* 2 | INDEX RANGE SCAN | IND_T1_C2 | 1 | 5000 | 10000 |00:00:00.03 | 689 |
---------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("C2"=:N1)
20 rows selected.
Elapsed: 00:00:00.02
子游标编号仍然是 2,因此没有发生硬解析。
我们继续测试
SQL> EXEC :N1:=1
PL/SQL procedure successfully completed.
Elapsed: 00:00:00.00
SQL> SELECT /*+ GATHER_PLAN_STATISTICS */
C1,
C2
FROM
TEST
WHERE
C2 = :N1; 2 3 4 5 6 7
.......
909255 1
909256 1
909257 1
990000 rows selected.
Elapsed: 00:00:11.98
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL,NULL,'ALLSTATS LAST'));
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID fgsshbz6rhybs, child number 2
-------------------------------------
SELECT /*+ GATHER_PLAN_STATISTICS */ C1, C2 FROM TEST WHERE C2
= :N1
Plan hash value: 4126779481
---------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
---------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 990K|00:00:01.04 | 175K|
| 1 | TABLE ACCESS BY INDEX ROWID| TEST | 1 | 5000 | 990K|00:00:01.04 | 175K|
|* 2 | INDEX RANGE SCAN | IND_T1_C2 | 1 | 5000 | 990K|00:00:00.65 | 67807 |
---------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("C2"=:N1)
20 rows selected.
Elapsed: 00:00:00.03
以上就是子游标编号 2 对应的执行计划。可以看到,这次它实际执行返回行数是 990000 行,
说明**这正是针对表中占比 99% 的那个绑定变量值所使用的执行计划**。
自适应游标共享(ACS)并没有生效,没有触发执行计划的重新评估,执行计划也**没有切换成全表扫描**。
这就是你要承担的风险:如果在数据分布不均匀的列上保留直方图,WHERE 条件中使用绑定变量引用该列,
并且开启了绑定变量窥视(Oracle 9i 及以上版本默认开启;绑定变量窥视由隐藏参数 `_OPTIM_PEEK_USER_BINDS` 控制,
默认值为 TRUE)。 很有可能它会选错执行计划。高版本已经避免了这个 自适应游标共享(ACS),如果你还在使用oracle 11.1 之前的版本就要注意了。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/12798004/viewspace-3186063/ ,如需转载,请注明出处,否则将追究法律责任。