SQL调优指南笔记20:Improving Real-World Performance Through Cursor Sharing

本文为SQL Tuning Guide第20章“Improving Real-World Performance Through Cursor Sharing”的笔记。

游标共享可以将数据库应用程序性能提高几个数量级。

重要基础概念

  • optimizer goal
    The prioritization of resource usage by the optimizer. Using the OPTIMIZER_MODE initialization parameter, you can set the optimizer goal best throughput or best response time.

  • bind variable
    A placeholder in a SQL statement that must be replaced with a valid value or value address for the statement to execute successfully. By using bind variables, you can write a SQL statement that accepts inputs or parameters at run time.

20.1 Overview of Cursor Sharing

Oracle 数据库可以共享游标,游标是指向共享池中私有 SQL 区域的指针。

20.1.1 About Cursors

私有 SQL 区域保存有关已解析 SQL 语句的信息和其他特定于会话的信息以供处理。

当服务器进程执行 SQL 或 PL/SQL 代码时,该进程使用私有 SQL 区域来存储绑定变量值、查询执行状态信息和查询执行工作区域。 每次执行语句的私有 SQL 区域不共享,并且可能包含不同的值和数据。

游标是特定私有 SQL 区域的名称或句柄。 游标包含特定于会话的状态信息,例如绑定变量值和结果集。

如下图所示,您可以将游标视为客户端的指针和服务器端的状态。 因为游标与私有 SQL 区域密切相关,所以这些术语有时可以互换使用。
在这里插入图片描述

20.1.1.1 Private and Shared SQL Areas

私有 SQL 区域中的游标指向库缓存中共享 SQL 区域

与包含会话状态信息的私有 SQL 区域不同,共享 SQL 区域包含语句的分析树和执行计划。 例如,SELECT * FROM employees 的执行具有存储在一个共享 SQL 区域中的计划和分析树。又例如, SELECT * FROM employees的执行在语法和语义上都不同,它另有一个存储在单独的共享 SQL 区域中的计划和分析树。

同一或不同会话中的多个私有 SQL 区域可以引用单个共享 SQL 区域,这种现象称为游标共享。 例如,在一个会话中执行 SELECT * FROM employees 和在不同会话中执行 SELECT * FROM employees(访问同一个表)可以使用相同的分析树和计划。 由多个语句访问的共享 SQL 区域称为共享游标。
在这里插入图片描述
Oracle 数据库使用以下步骤自动确定发出的 SQL 语句或 PL/SQL 块在文本上是否与当前在库缓存中的另一条语句相同:

  1. 计算语句文本的哈希值
  2. 数据库为共享池中的现有 SQL 语句查找匹配的哈希值。 以下选项是可能的:
    – 不存在匹配的哈希值。
    在这种情况下,共享池中当前不存在 SQL 语句,因此数据库执行硬解析。 这将结束共享池检查。
    – 存在匹配的哈希值。
    在这种情况下,数据库会进行下一步,即文本匹配。
  3. 数据库将匹配语句的文本与哈希语句的文本进行比较,以确定它们是否相同。 以下选项是可能的:
    – 文本匹配失败。
    在这种情况下,文本匹配过程停止,导致硬解析。
    – 文本匹配成功。
    在这种情况下,数据库进行下一步:确定 SQL 是否可以共享现有的父游标。
    要发生文本匹配,SQL 语句或 PL/SQL 块的文本必须逐个字符相同,包括空格、大小写和注释。 例如,以下语句不能使用相同的共享 SQL 区域:
    SELECT * FROM employees;
    SELECT * FROM Employees;
    SELECT *  FROM employees;
    
    通常,仅文字(literal)不同(本例为121和247)的 SQL 语句不能使用相同的共享 SQL 区域。 例如,以下语句不会解析到相同的 SQL 区域:
    SELECT count(1) FROM employees WHERE manager_id = 121;
    SELECT count(1) FROM employees WHERE manager_id = 247;
    
    此规则的唯一例外是当参数 CURSOR_SHARING 已设置为 FORCE 时,在这种情况下,类似的语句可以共享 SQL 区域。

20.1.1.2 Parent and Child Cursors

每个已解析的 SQL 语句都有一个父游标和一个或多个子游标。

父游标存储 SQL 语句的文本。 如果两个语句的文本相同,则这两个语句共享同一个父游标。 但是,如果文本不同,则数据库会创建一个单独的父游标。

在这个例子中,前两个语句在语法上是不同的(字母“c”在第一个语句中是小写,在第二个语句中是大写),但在语义上是相同的。 由于语法差异,这些语句具有不同的父游标。 第三个语句在语法上与第一个语句(小写“c”)相同,但在语义上不同,因为它引用了不同模式中的客户表。 由于语法相同,第三条语句可以与第一条语句共享一个父游标。

SQL> CONNECT oe@inst1
Enter password: *******
Connected.
SQL> SELECT COUNT(*) FROM customers;

  COUNT(*)
----------
       319

SQL> SELECT COUNT(*) FROM Customers;

  COUNT(*)
----------
       319

SQL> CONNECT sh@inst1
Enter password:  *******
Connected.
SQL> SELECT COUNT(*) FROM customers;

  COUNT(*)
----------
    55500

下面的 V$SQL 查询指示了两个父节点。 SQL ID 为 8h916vv2yw400 的语句,即该语句的小写“c”版本,有一个父游标和两个子游标:child 0 和 child 1。SQL ID 为 5rn2uxjtpz0wd 的语句,即大写的“ c” 版本的语句,具有不同的父游标和只有一个子游标:child 0。

SQL> CONNECT SYSTEM@inst1
Enter password:  *******
Connected.

COL SQL_TEXT FORMAT a30
COL CHILD# FORMAT 99999
COL EXEC FORMAT 9999
COL SCHEMA FORMAT a6
SELECT SQL_ID, PARSING_SCHEMA_NAME AS SCHEMA, SQL_TEXT, 
CHILD_NUMBER AS CHILD#, EXECUTIONS AS EXEC FROM V$SQL 
WHERE SQL_TEXT LIKE '%ustom%' AND SQL_TEXT NOT LIKE '%SQL_TEXT%' ORDER BY SQL_ID;

SQL_ID        SCHEMA SQL_TEXT                       CHILD#  EXEC
------------- ------ ------------------------------ ------ -----
5rn2uxjtpz0wd OE     SELECT COUNT(*) FROM Customers      0     1
8h916vv2yw400 OE     SELECT COUNT(*) FROM customers      0     1
8h916vv2yw400 SH     SELECT COUNT(*) FROM customers      1     1
20.1.1.2.1 Parent Cursors and V$SQLAREA

V$SQLAREA 视图为每个父游标包含一行。

在下面的示例中,V$SQLAREA 查询显示了两个父游标,每个游标都用不同的 SQL_ID 标识。 VERSION_COUNT 表示子游标的数量
此SQL原文有错

COL SQL_TEXT FORMAT a30
SELECT SQL_TEXT, SQL_ID, VERSION_COUNT, HASH_VALUE
FROM   V$SQLAREA
WHERE  SQL_TEXT LIKE '%ustomer%'
AND    SQL_TEXT NOT LIKE '%SQL_TEXT%';
 
SQL_TEXT                       SQL_ID        VERSION_COUNT HASH_VALUE
------------------------------ ------------- ------------- ----------
SELECT COUNT(*) FROM customers 8h916vv2yw400             2 3320713216
SELECT COUNT(*) FROM Customers 5rn2uxjtpz0wd             1 1935639437

在前面的输出中,SELECT * FROM customers 的 VERSION_COUNT 为 2 表示多个子游标,这是必要的,因为该语句是针对两个不同的对象执行的。 相反,语句 SELECT * FROM Customers(注意大写“E”)只执行一次,因此有一个父游标和一个子游标(VERSION_COUNT 为 1)。

20.1.1.2.2 Child Cursors and V$SQL

每个父游标都有一个或多个子游标。

子游标包含执行计划、绑定变量、有关查询中引用的对象的元数据、优化器环境和其他信息。 与父游标相比,子游标不存储 SQL 语句的文本

如果语句能够重用父游标,则数据库检查该语句是否可以重用现有的子游标。 数据库执行多项检查,包括以下内容:

  • 数据库将发出的语句中引用的对象与池中语句引用的对象进行比较,以确保它们都是相同的。
    在 SQL 语句或 PL/SQL 块中对模式对象的引用必须解析为相同模式中的相同对象。 例如,如果两个用户发出以下 SQL 语句,并且每个用户都有自己的雇员表,则以下语句不完全相同,因为该语句为每个用户引用不同的雇员表:
SELECT * FROM employees;

注意:数据库可以共享私有临时表的游标,但只能在同一个会话中。 数据库将会话标识符关联为游标上下文的一部分。 在软解析过程中,只有当前会话 ID 与游标上下文中的会话 ID 匹配时,数据库才能共享子游标。

  • 数据库确定优化器模式是否相同。
    例如,必须使用相同的优化器目标来优化 SQL 语句。

示例 20-2 多个子游标
V$SQL 描述当前驻留在库缓存中的语句。 它为每个子游标包含一行,如下例所示:

COL SQL_TEXT FORMAT A30
COL USR FORMAT A6
SELECT SQL_TEXT, SQL_ID, USERNAME AS USR, CHILD_NUMBER AS CHILD#, 
       HASH_VALUE, PLAN_HASH_VALUE AS PLAN_HASHV
FROM   V$SQL s, DBA_USERS d
WHERE  SQL_TEXT LIKE '%ustomer%'
AND    SQL_TEXT NOT LIKE '%SQL_TEXT%'
AND    d.USER_ID = s.PARSING_USER_ID;

SQL_TEXT                       SQL_ID        USR        CHILD# HASH_VALUE PLAN_HASHV
------------------------------ ------------- ------ ---------- ---------- ----------
SELECT COUNT(*) FROM customers 8h916vv2yw400 OE              0 3320713216 1140397121
SELECT COUNT(*) FROM Customers 5rn2uxjtpz0wd OE              0 1935639437 1140397121
SELECT COUNT(*) FROM customers 8h916vv2yw400 SH              1 3320713216  237477902
20.1.1.2.3 Cursor Mismatches and V$SQL_SHARED_CURSOR

如果父游标有多个子游标,则 V$SQL_SHARED_CURSOR 视图提供有关游标未共享的原因的信息。 对于几种类型的不兼容,TRANSLATION_MISMATCH 列指示与值 Y 或 N 不匹配。

示例 20-3 翻译不匹配
在此示例中,TRANSLATION_MISMATCH 列显示两个语句 (SELECT * FROM employees) 引用了不同的对象,导致最后一个语句的 TRANSLATION_MISMATCH 值为 Y。 因为无法共享,所以每个语句都有一个单独的子游标,如 CHILD_NUMBER 的 0 和 1 所示。

SELECT S.SQL_TEXT, S.CHILD_NUMBER, s.CHILD_ADDRESS, 
       C.TRANSLATION_MISMATCH
FROM   V$SQL S, V$SQL_SHARED_CURSOR C
WHERE  SQL_TEXT LIKE '%ustomer%'
AND    SQL_TEXT NOT LIKE '%SQL_TEXT%'
AND S.CHILD_ADDRESS = C.CHILD_ADDRESS;

SQL_TEXT                       CHILD_NUMBER CHILD_ADDRESS    T
------------------------------ ------------ ---------------- -
SELECT COUNT(*) FROM customers            0 00000000A7208390 N
SELECT COUNT(*) FROM customers            1 00000000A7192130 Y
SELECT COUNT(*) FROM Customers            0 00000000A71F3EF8 N

20.1.2 About Cursors and Parsing

如果应用程序发出一条语句,并且 Oracle 数据库无法重用游标,则它必须构建应用程序代码的新可执行版本。 此操作称为硬解析

软解析是不是硬解析的任何解析,并且在数据库可以重用现有代码时发生。 一些软解析比其他软解析占用更少的资源。 例如,如果该语句的父游标已经存在,那么Oracle数据库可以进行各种优化,然后将子游标存储在共享SQL区域中。 但是,如果父游标不存在,则 Oracle 数据库还必须将父游标存储在共享 SQL 区域中,这会产生额外的内存开销。

实际上,硬解析会在运行语句之前重新编译它。 在每次执行之前硬解析 SQL 语句类似于在每次执行之前重新编译 C 程序硬解析执行如下操作

  • 检查 SQL 语句的语法
  • 检查 SQL 语句的语义
  • 检查发出语句的用户的访问权限
  • 创建执行计划
  • 多次访问库缓存和数据字典缓存以检查数据字典

硬解析的一个特别耗费资源的方面是多次访问库缓存和数据字典缓存以检查数据字典。 当数据库访问这些区域时,它使用一个称为锁存器的序列化设备,以便在检查期间它们的定义不会改变。 锁存器争用会增加语句执行时间并降低并发性。

由于上述所有原因,硬解析的 CPU 和内存开销会造成严重的性能问题。 这些问题在从表单接受用户输入然后动态生成 SQL 语句的 Web 应用程序中尤为明显。 真实世界性能组强烈建议尽可能减少硬解析

https://youtu.be/WcRcmfSIajc

20.1.3 About Literals and Bind Variables

绑定变量对于 Oracle 数据库应用程序中的游标共享至关重要。

20.1.3.1 Literals and Cursors

在构造 SQL 语句时,某些 Oracle 应用程序使用文字而不是绑定变量。

例如,语句 SELECT SUM(salary) FROM hr.employees WHERE employee_id < 101 使用文字值 101 作为员工 ID。 默认情况下,当类似语句不使用绑定变量时,Oracle 数据库无法利用游标共享。 因此,Oracle 数据库将除了值 102 或任何其他随机值之外完全相同的语句视为一条全新的语句,需要进行硬解析。

Real-World Performance 小组已确定使用文字的应用程序是性能、可伸缩性和安全问题的常见原因。 在现实世界中,不考虑游标共享而快速编写应用程序的情况并不少见。 一个典型的例子是“屏幕抓取”应用程序,它从 Web 表单中复制内容,然后连接字符串以动态构造 SQL 语句。

使用文字值导致的主要问题包括:

  • 连接最终用户输入的文字的应用程序容易受到 SQL 注入攻击。 只有重写应用程序以使用绑定变量才能消除这种威胁。
  • 如果每个语句都被硬解析,则游标不共享,因此数据库必须消耗更多内存来创建游标。
  • Oracle 数据库在硬解析时必须锁定共享池和库缓存。 随着硬解析数量的增加,等待锁定共享池的进程数量也会增加。 这种情况会降低并发性并增加争用。

https://youtu.be/EBdZ-RE2HFs

示例 20-6 文字和游标共享

考虑一个执行以下语句的应用程序,它们仅在文字上有所不同:

SELECT SUM(salary) FROM hr.employees WHERE employee_id < 101;
SELECT SUM(salary) FROM hr.employees WHERE employee_id < 120;
SELECT SUM(salary) FROM hr.employees WHERE employee_id < 165;

下面的 V$SQLAREA 查询表明这三个语句需要三个不同的父游标。 如 VERSION_COUNT 所示,每个父游标都需要其自己的子游标。

COL SQL_TEXT FORMAT a30
SELECT SQL_TEXT, SQL_ID, VERSION_COUNT, HASH_VALUE
FROM   V$SQLAREA
WHERE  SQL_TEXT LIKE '%mployee%'
AND    SQL_TEXT NOT LIKE '%SQL_TEXT%';

SQL_TEXT                              SQL_ID VERSION_COUNT HASH_VALUE
------------------------------ ------------- ------------- ----------
SELECT SUM(salary) FROM hr.emp b1tvfcc5qnczb             1  191509483
loyees WHERE employee_id < 165
SELECT SUM(salary) FROM hr.emp cn5250y0nqpym             1 2169198547
loyees WHERE employee_id < 101
SELECT SUM(salary) FROM hr.emp au8nag2vnfw67             1 3074912455
loyees WHERE employee_id < 120

20.1.3.2 Bind Variables and Cursors

您可以开发 Oracle 应用程序以使用绑定变量而不是文字。

绑定变量是查询中的占位符。 例如,语句 SELECT SUM(salary) FROM hr.employees WHERE employee_id < :emp_id 使用绑定变量:emp_id 作为员工 ID。

Real-World Performance 小组发现使用绑定变量的应用程序性能更好、扩展性更好并且更安全。 使用绑定变量带来的主要好处包括:

  • 使用绑定变量的应用程序不易受到与使用文字的应用程序相同的 SQL 注入攻击。
  • 当相同的语句使用绑定变量时,Oracle 数据库可以利用游标共享,并在不同的值绑定到同一语句时共享计划和其他信息。
  • Oracle 数据库避免了锁定硬解析所需的共享池和库缓存的开销。

示例 20-7 绑定变量和共享游标

以下示例使用 SQL*Plus 中的 VARIABLE 命令创建 emp_id 绑定变量,然后使用三个不同的绑定值(101、120 和 165)执行查询:

VARIABLE emp_id NUMBER

EXEC :emp_id := 101;
SELECT SUM(salary) FROM hr.employees WHERE employee_id < :emp_id;
EXEC :emp_id := 120;
SELECT SUM(salary) FROM hr.employees WHERE employee_id < :emp_id;
EXEC :emp_id := 165;
SELECT SUM(salary) FROM hr.employees WHERE employee_id < :emp_id;

下面的 V$SQLAREA 查询显示了一个唯一的 SQL 语句:

COL SQL_TEXT FORMAT a34
SELECT SQL_TEXT, SQL_ID, VERSION_COUNT, HASH_VALUE
FROM   V$SQLAREA
WHERE  SQL_TEXT LIKE '%mployee%'
AND    SQL_TEXT NOT LIKE '%SQL_TEXT%';

SQL_TEXT                           SQL_ID        VERSION_COUNT HASH_VALUE
---------------------------------- ------------- ------------- ----------
SELECT SUM(salary) FROM hr.employe 4318cbskba8yh             1 615850960
es WHERE employee_id < :emp_id

VERSION_COUNT 值为 1 表示数据库重复使用同一个子游标,而不是创建三个单独的子游标。 使用绑定变量使这种重用成为可能

20.1.3.3 Bind Variable Peeking

在绑定变量查看(也称为绑定查看)中,优化器在数据库执行语句的硬解析时查看绑定变量中的值。

优化器不会在每次解析之前查看绑定变量值。 相反,优化器仅在首次调用优化器时进行查看,这是在硬解析期间。

当查询使用文字时,优化器可以使用文字值来找到最佳计划。 但是,当查询使用绑定变量时,优化器必须在 SQL 文本中不存在文字的情况下选择最佳计划。 这项任务可能非常困难。 通过在初始硬解析期间查看绑定值,优化器可以确定 WHERE 子句条件的基数,就好像使用了文字一样,从而改进计划。

因为优化器只在硬解析期间查看绑定值,所以计划可能不是对所有可能的绑定值都是最优的。 下面的例子说明了这个原则。

示例 20-8 文字导致不同的执行计划

假设您执行以下语句,这些语句使用不同的文字(101、120 和 165)执行三个不同的语句,然后显示每个语句的执行计划:

SET LINESIZE 167
SET PAGESIZE 0
SELECT SUM(salary) FROM hr.employees WHERE employee_id < 101;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR());
SELECT SUM(salary) FROM hr.employees WHERE employee_id < 120;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR());
SELECT SUM(salary) FROM hr.employees WHERE employee_id < 165;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR());

数据库硬解析了所有三个不相同的语句。 DISPLAY_CURSOR 输出(为清楚起见进行了编辑)显示优化器为前两个语句选择了相同的索引范围扫描计划,但为使用文字 165 的语句选择了全表扫描计划:

SQL_ID	cn5250y0nqpym, child number 0
-------------------------------------
SELECT SUM(salary) FROM hr.employees WHERE employee_id < 101

Plan hash value: 2410354593

-------------------------------------------------------------------------------------
|Id| Operation                            | Name         |Rows|Bytes|Cost(%CPU)|Time|
-------------------------------------------------------------------------------------
| 0| SELECT STATEMENT                     |               |  |   |2 (100)|          |
| 1|  SORT AGGREGATE                      |               |1 | 8 |       |          |
| 2|   TABLE ACCESS BY INDEX ROWID BATCHED| EMPLOYEES     |1 | 8 |2 (0)  | 00:00:01 |
|*3|    INDEX RANGE SCAN                  | EMP_EMP_ID_PK |1 |   |1 (0)  | 00:00:01 |
-------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("EMPLOYEE_ID"<101)

SQL_ID	au8nag2vnfw67, child number 0
-------------------------------------
SELECT SUM(salary) FROM hr.employees WHERE employee_id < 120

Plan hash value: 2410354593

-------------------------------------------------------------------------------------
|Id| Operation                            | Name         |Rows|Bytes|Cost(%CPU)|Time|
-------------------------------------------------------------------------------------
| 0| SELECT STATEMENT                     |               |  |   |2 (100)|          |
| 1|  SORT AGGREGATE                      |               |1 | 8 |       |          |
| 2|   TABLE ACCESS BY INDEX ROWID BATCHED| EMPLOYEES     |20|160|2 (0)  | 00:00:01 |
|*3|    INDEX RANGE SCAN                  | EMP_EMP_ID_PK |20|   |1 (0)  | 00:00:01 |
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("EMPLOYEE_ID"<120)

SQL_ID	b1tvfcc5qnczb, child number 0
-------------------------------------
SELECT SUM(salary) FROM hr.employees WHERE employee_id < 165

Plan hash value: 1756381138

-------------------------------------------------------------------------
| Id  | Operation          | Name      |Rows| Bytes |Cost(%CPU)| Time   |
-------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |           |    |       | 2 (100)|          |
|   1 |  SORT AGGREGATE    |           |  1 |     8 |        |          |
|*  2 |   TABLE ACCESS FULL| EMPLOYEES | 66 |   528 | 2   (0)| 00:00:01 |
-------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("EMPLOYEE_ID"<165)

前面的输出表明,对于返回更多行的查询,优化器认为全表扫描比索引扫描更有效。

示例 20-9 绑定变量导致游标重用

此示例重写了示例 20-8 中执行的查询,以使用绑定变量而不是文字。 您将相同的值(101、120 和 165)绑定到绑定变量 :emp_id,然后显示每个值的执行计划:

VAR emp_id NUMBER

EXEC :emp_id := 101;
SELECT SUM(salary) FROM hr.employees WHERE employee_id < :emp_id;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR());
EXEC :emp_id := 120;
SELECT SUM(salary) FROM hr.employees WHERE employee_id < :emp_id;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR());
EXEC :emp_id := 165;
SELECT SUM(salary) FROM hr.employees WHERE employee_id < :emp_id;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR());

DISPLAY_CURSOR 输出显示优化器为所有三个语句选择了完全相同的计划:

SELECT SUM(salary) FROM hr.employees WHERE employee_id < :emp_id

Plan hash value: 2410354593

-------------------------------------------------------------------------------------
| Id | Operation                            | Name      |Rows|Bytes|Cost (%CPU)|Time|
-------------------------------------------------------------------------------------
|  0 | SELECT STATEMENT                     |               | |  |2 (100)|          |
|  1 |  SORT AGGREGATE                      |               |1|8 |       |          |
|  2 |   TABLE ACCESS BY INDEX ROWID BATCHED| EMPLOYEES     |1|8 |  2 (0)| 00:00:01 |
|* 3 |    INDEX RANGE SCAN                  | EMP_EMP_ID_PK |1|  |  1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("EMPLOYEE_ID"<:EMP_ID)

相比之下,当上述语句以字面量执行时,优化器在员工ID值为165时选择了成本较低的全表扫描。这就是自适应游标共享解决的问题。

20.1.4 About the Life Cycle of Shared Cursors

当优化器解析出一个新的非 DDL 的 SQL 语句时,数据库会分配一个新的共享 SQL 区。 所需的内存量取决于语句的复杂性。

数据库可以从共享池中删除共享 SQL 区域,即使该区域对应于一个长时间未使用的打开游标。 如果打开的游标稍后用于运行它的语句,那么数据库将重新分析该语句并分配一个新的共享 SQL 区域。 数据库不会删除其语句正在执行或其行尚未完全提取的游标。

由于依赖模式对象或优化器统计信息的更改,共享 SQL 区域可能会变得无效。 Oracle 数据库使用两种技术来管理游标生命周期:失效和滚动失效。

20.1.4.1 Cursor Marked Invalid

当共享 SQL 区域被标记为无效时,数据库可以将其从共享池中删除,连同一段时间未使用的有效游标。

在某些情况下,数据库必须执行与共享池中无效共享 SQL 区域关联的语句。 在这种情况下,数据库在执行之前对语句进行硬解析。

当满足以下条件时,数据库立即将依赖的共享 SQL 区域标记为无效:

  • 当 NO_INVALIDATE 参数为 FALSE 时,DBMS_STATS 收集表、表簇或索引的统计信息。
  • SQL 语句引用模式对象,稍后由使用立即游标失效(默认)的 DDL 语句修改。
    您可以在 ALTER TABLE … IMMEDIATE VALIDATION 和 ALTER INDEX … IMMEDIATE VALIDATION 等语句上手动指定立即失效,或者在会话或系统级别将 CURSOR_INVALIDATION 初始化参数设置为 IMMEDIATE。
    注意:使用 DEFERRED VALIDATION 子句的 DDL 语句会覆盖 CURSOR_INVALIDATION 初始化参数的 IMMEDIATE 设置。

当满足上述条件时,数据库会在下次执行时重新解析受影响的语句。

当数据库使游标无效时,V S Q L . I N V A L I D A T I O N S 值增加(例如,从 0 到 1 ),并且 V SQL.INVALIDATIONS 值增加(例如,从 0 到 1),并且 V SQL.INVALIDATIONS值增加(例如,从01),并且VSQL.OBJECT_STATUS 显示 INVALID_UNAUTH。

示例 20-10 通过设置 NO_INVALIDATE=FALSE 强制游标失效

SQL> SELECT COUNT(*) FROM sales;

  COUNT(*)
----------
    918843

SQL> SELECT PREV_SQL_ID, SQL_ID FROM V$SESSION WHERE SID = SYS_CONTEXT('userenv', 'SID');

SQL_ID
-------------
1y17j786c7jbh

SQL> SELECT CHILD_NUMBER, EXECUTIONS,
  2  PARSE_CALLS, INVALIDATIONS, OBJECT_STATUS 
  3  FROM V$SQL WHERE SQL_ID = '1y17j786c7jbh';

CHILD_NUMBER EXECUTIONS PARSE_CALLS INVALIDATIONS OBJECT_STATUS
------------ ---------- ----------- ------------- -------------
           0          1           1             0 VALID

SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(null,'sales',no_invalidate => FALSE);

PL/SQL procedure successfully completed.

SQL> SELECT CHILD_NUMBER, EXECUTIONS,
  2  PARSE_CALLS, INVALIDATIONS, OBJECT_STATUS
  3  FROM V$SQL WHERE SQL_ID = '1y17j786c7jbh';

CHILD_NUMBER EXECUTIONS PARSE_CALLS INVALIDATIONS OBJECT_STATUS
------------ ---------- ----------- ------------- --------------
           0          1           1             1 INVALID_UNAUTH

20.1.4.2 Cursors Marked Rolling Invalid

当游标被标记为滚动无效时(V$SQL.IS_ROLLING_INVALID 为 Y),数据库会在较长时间内逐渐执行硬解析。

注意:当V$SQL.IS_ROLLING_REFRESH_INVALID 为Y 时,底层对象已经改变,但不需要重新编译游标。 数据库更新游标中的元数据。

滚动失效的目的

由于硬解析的急剧增加会显着降低性能,因此滚动失效(也称为延迟失效)对于同时使许多游标失效的工作负载很有用。 数据库为每个无效游标分配一个随机生成的时间段。 同时失效的 SQL 区域通常具有不同的时间段。

仅当访问游标的查询在时间段到期后执行时才会发生硬解析。 通过这种方式,数据库随着时间的推移分散了硬解析的开销。

注意:如果并行SQL语句被标记为rolling invalid,那么数据库会在下次执行时进行hard parse,而不管时间是否过期。 在 Oracle Real Application Clusters (Oracle RAC) 环境中,该技术可确保并行执行服务器和查询协调器的执行计划之间的一致性。

滚动失效的类比可能是逐渐更换破旧的办公家具。 一家公司没有立即更换所有家具,从而迫使大量的财务支出,而是为每件家具指定了不同的到期日期。 在一年的过程中,一件物品会一直使用,直到被更换,此时会产生成本。

延迟失效规范

默认情况下,DDL 指定访问对象的语句使用立即游标失效。 例如,如果您创建一个表或索引,那么引用该表或索引的游标将使用立即失效。

如果 DDL 语句支持延迟游标失效,则可以使用 ALTER TABLE … DEFERRED INVALIDATION 等语句覆盖默认行为。 选项取决于 DDL 语句。 例如,当还指定了 UNUSABLE 或 REBUILD 选项时,ALTER INDEX 仅支持 DEFERRED INVALIDATION。

DDL 的替代方法是在会话或系统级别将 CURSOR_INVALIDATION 初始化参数设置为 DEFERRED。 使用 IMMEDIATE INVALIDATION 子句的 DDL 语句会覆盖 CURSOR_INVALIDATION 初始化参数的 DEFERRED 设置。

当滚动失效发生时

如果 DEFERRED INVALIDATION 属性适用于对象,无论是作为 DDL 还是初始化参数设置的结果,那么访问该对象的语句可能会受到延迟失效的影响。 在以下任一情况下,数据库将共享 SQL 区域标记为滚动无效:

  • 当 NO_INVALIDATE 参数设置为 DBMS_STATS.AUTO_INVALIDATE 时,DBMS_STATS 收集表、表簇或索引的统计信息。 这是默认设置。

  • 在不阻止使用延迟失效的情况下,使用延迟失效发出以下语句之一:

    • ALTER TABLE on partitioned tables
    • ALTER TABLE … PARALLEL
    • ALTER INDEX … UNUSABLE
    • ALTER INDEX … REBUILD
    • CREATE INDEX
    • DROP INDEX
    • TRUNCATE TABLE on partitioned tables

    DDL 语句的子集要求 DML(INSERT、UPDATE、DELETE 或 MERGE)的游标立即失效,但 SELECT 语句不需要。 与特定 DDL 语句和受影响的游标相关的许多因素决定了 Oracle 数据库是否使用延迟失效。

20.2 CURSOR_SHARING and Bind Variable Substitution

本主题解释什么是 CURSOR_SHARING 初始化参数,以及将其设置为不同的值如何影响 Oracle 数据库使用绑定变量的方式。

20.2.1 CURSOR_SHARING Initialization Parameter

CURSOR_SHARING 初始化参数控制数据库如何处理带有绑定变量的语句。

在 Oracle Database 12c 中,该参数支持以下值:

  • EXACT
    这是默认值。 数据库只允许文本相同的语句共享一个游标。 数据库不会尝试用系统生成的绑定变量替换文字值。 在这种情况下,优化器会根据字面值为每个语句生成一个计划。
  • FORCE
    数据库用系统生成的绑定变量替换所有文字。 对于绑定变量替换文字后相同的语句,优化器使用相同的计划。

注意:CURSOR_SHARING 的 SIMILAR 值已弃用。

您可以在系统或会话级别设置 CURSOR_SHARING,或者在语句级别使用 CURSOR_SHARING_EXACT 提示。

20.2.2 Parsing Behavior When CURSOR_SHARING = FORCE

当 SQL 语句使用文字而不是绑定变量时,将 CURSOR_SHARING 初始化参数设置为 FORCE 使数据库能够用系统生成的绑定变量替换文字。 使用这种技术,数据库有时可以减少共享 SQL 区域中父游标的数量。

注意:如果语句使用 ORDER BY 子句,那么虽然数据库可能会在该子句中执行文字替换,但游标不一定是可共享的,因为列号的不同值作为文字意味着不同的查询结果并可能有不同的执行计划 . ORDER BY子句中的列号影响查询计划和执行,所以数据库不能共享两个列号不同的游标

当 CURSOR_SHARING 设置为 FORCE 时,数据库在解析期间执行以下步骤:

  1. 将语句中的所有文字复制到 PGA,并用系统生成的绑定变量替换它们
-- 替换前
SELECT SUBSTR(last_name, 1, 4), SUM(salary) 
FROM   hr.employees 
WHERE  employee_id < 101 GROUP BY last_name
-- 替换后
SELECT SUBSTR(last_name, :"SYS_B_0", :"SYS_B_1"), SUM(salary) 
FROM   hr.employees 
WHERE  employee_id < :"SYS_B_2" GROUP BY last_name
  1. 在共享池中搜索相同的语句(相同的 SQL 哈希值)
    如果没有找到相同的语句,则数据库执行硬解析。 否则,数据库继续下一步。
  2. 执行语句的软解析

如前面的步骤所示,将 CURSOR_SHARING 初始化参数设置为 FORCE 不会减少解析计数。 相反,在某些情况下,FORCE 使数据库能够执行软解析而不是硬解析。 此外,FORCE 不能防止 SQL 注入攻击,因为 Oracle 数据库会在任何注入发生后绑定值。

示例 20-11 用系统绑定变量替换文字
此示例在会话级别将 CURSOR_SHARING 设置为 FORCE,执行三个包含文字的语句,并显示每个语句的计划:

ALTER SESSION SET CURSOR_SHARING=FORCE;
SET LINESIZE 170
SET PAGESIZE 0
SELECT SUM(salary) FROM hr.employees WHERE employee_id < 101;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR());
SELECT SUM(salary) FROM hr.employees WHERE employee_id < 120;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR());
SELECT SUM(salary) FROM hr.employees WHERE employee_id < 165;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR());

以下 DISPLAY_CURSOR 输出经过编辑以提高可读性,显示所有三个语句都使用相同的计划。 优化器选择了索引范围扫描计划,因为它查看了绑定到系统绑定变量的第一个值 (101),并选择该计划作为所有值的最佳计划。 事实上,这个计划并不是所有价值观的最佳计划。 当值为165时,全表扫描效率更高。

SQL_ID	cxx8n1cxr9khn, child number 0
-------------------------------------
SELECT SUM(salary) FROM hr.employees WHERE employee_id < :"SYS_B_0"

Plan hash value: 2410354593

-------------------------------------------------------------------------------------
| Id  | Operation                           | Name       |Rows|Bytes|Cost(%CPU)|Time|
-------------------------------------------------------------------------------------
|  0 | SELECT STATEMENT                     |               |  |   |2 (100)|        |
|  1 |  SORT AGGREGATE                      |               |1 | 8 |       |        |
|  2 |   TABLE ACCESS BY INDEX ROWID BATCHED| EMPLOYEES     |1 | 8 |2 (0)  |00:00:01|
|* 3 |    INDEX RANGE SCAN                  | EMP_EMP_ID_PK |1 |   |1 (0)  |00:00:01|
-------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("EMPLOYEE_ID"<101)

对 V$SQLAREA 的查询确认 Oracle 数据库用系统绑定变量:“SYS_B_0”替换了文本,并为所有三个语句创建了一个父游标和一个子游标(VERSION_COUNT=1),这意味着所有执行共享相同的计划 .

COL SQL_TEXT FORMAT a36
SELECT SQL_TEXT, SQL_ID, VERSION_COUNT, HASH_VALUE
FROM   V$SQLAREA
WHERE  SQL_TEXT LIKE '%mployee%'
AND    SQL_TEXT NOT LIKE '%SQL_TEXT%';

SQL_TEXT                             SQL_ID        VERSION_COUNT HASH_VALUE
------------------------------------ ------------- ------------- ----------
SELECT SUM(salary) FROM hr.employees cxx8n1cxr9khn             1  997509652
 WHERE employee_id < :"SYS_B_0"

20.3 Adaptive Cursor Sharing

自适应游标共享功能使包含绑定变量的单个语句能够使用多个执行计划

游标共享是“自适应的”,因为游标会调整其行为,因此数据库不会始终对每次执行或绑定变量值使用相同的计划。

20.3.1 Purpose of Adaptive Cursor Sharing

使用绑定查看,优化器会在第一次调用游标时查看用户定义的绑定变量的值。

优化器确定任何 WHERE 子句条件的基数,就好像使用了文字而不是绑定变量一样。 但是,如果 WHERE 子句中的列有倾斜数据,则该列上可能存在直方图。 当优化器查看用户定义的绑定变量的值并选择一个计划时,该计划可能不适用于所有值。

在自适应游标共享中,数据库监控随着时间的推移访问的不同绑定值的数据,确保为特定绑定值选择最佳的游标。 例如,优化器可能为绑定值 10 选择一个计划,为绑定值 50 选择不同的计划。游标共享是“自适应的”,因为游标会调整其行为,因此优化器并不总是为每次执行或绑定选择相同的计划变量值。 因此,优化器会自动检测语句的不同执行何时会受益于不同的执行计划。

注意:自适应游标共享独立于 CURSOR_SHARING 初始化参数。 自适应游标共享同样适用于包含用户定义的和系统生成的绑定变量的语句。 自适应游标共享不适用于仅包含文字的语句。

20.3.2 How Adaptive Cursor Sharing Works: Example

自适应游标共享监视使用绑定变量的语句以确定新计划是否更有效。

假设一个应用程序执行了五次以下语句,每次都绑定不同的值:

SELECT * FROM employees WHERE salary = :sal AND department_id = :dept

在此示例中还假设直方图存在于谓词中的至少一列上。 数据库按如下方式处理这条语句:

  1. 应用程序第一次发出语句,导致硬解析。 在解析期间,数据库执行以下任务:
    • 查看绑定变量以生成初始计划。
    • 将游标标记为绑定敏感。 绑定敏感游标是其最佳计划可能取决于绑定变量值的游标。 为了确定不同的计划是否有益,数据库监视使用不同绑定值的绑定敏感游标的行为。
    • 存储有关谓词的元数据,包括绑定值的基数(在此示例中,假设仅返回 5 行)。
    • 根据查看的值创建执行计划(在本例中为索引访问)。
  2. 数据库执行游标,将绑定值和执行统计信息存储在游标中。
  3. 应用程序第二次发出语句,使用不同的绑定变量,导致数据库执行软解析,并在库缓存中找到匹配的游标。
  4. 数据库执行游标。
  5. 数据库执行以下执行后任务:
    • 数据库将第二次执行的执行统计数据与第一次执行的统计数据进行比较。
    • 数据库观察所有先前执行的统计模式,然后决定是否将游标标记为绑定感知游标。 在此示例中,假设数据库决定游标是绑定感知的。
  6. 应用程序第三次发出该语句,使用不同的绑定变量,这会导致软解析。 因为游标是绑定感知的,所以数据库执行以下操作:
    • 确定新值的基数是否与存储的基数在同一范围内。 在此示例中,基数相似:8 行而不是 5 行。
    • 重用现有子游标中的执行计划。
  7. 数据库执行游标。
  8. 应用程序第四次发出语句,使用不同的绑定变量,导致软解析。 因为游标是绑定感知的,所以数据库执行以下操作:
    • 确定新值的基数是否与存储的基数在同一范围内。 在此示例中,基数有很大不同:102 行(在具有 107 行的表中)而不是 5 行。
    • 找不到匹配的子游标。
  9. 数据库执行硬解析。 结果,数据库执行以下操作:
    • 使用第二个执行计划创建一个新的子游标(在本例中为全表扫描)
    • 在游标中存储有关谓词的元数据,包括绑定值的基数
  10. 数据库执行新游标。
  11. 数据库将新的绑定值和执行统计信息存储在新的子游标中。
  12. 应用程序第五次发出该语句,使用不同的绑定变量,这会导致软解析。 因为游标是绑定感知的,所以数据库执行以下操作:
    • 确定新值的基数是否与存储的基数在同一范围内。 在此示例中,基数为 20。
    • 找不到匹配的子游标。
  13. 数据库执行硬解析。 结果,数据库执行以下操作:
    • 使用第三个执行计划(在本例中为索引访问)创建一个新的子游标
    • 确定此索引访问执行计划与第一次执行该语句时使用的索引访问执行计划相同
    • 合并包含索引访问计划的两个子游标,这涉及将组合的基数统计信息存储到一个子游标中,并删除另一个
  14. 数据库使用索引访问执行计划执行游标。

20.3.3 Bind-Sensitive Cursors

绑定敏感游标是其最佳计划可能取决于绑定变量值的游标。

数据库在计算基数时检查了绑定值,并认为查询“敏感”以根据不同的绑定值来计划更改。 数据库监视绑定敏感游标的行为,该游标使用不同的绑定值来确定不同的计划是否有益。

优化器使用以下标准来决定游标是否对绑定敏感:

  • 优化器已经查看了绑定值以生成基数估计。
  • 绑定用于相等或范围谓词。

对于每次使用新绑定值执行的查询,数据库都会记录新值的执行统计信息,并将它们与先前值的执行统计信息进行比较。 如果执行统计数据变化很大,则数据库将游标标记为绑定感知。

示例 20-12 具有显着数据倾斜的列
此示例假定 hr.employees.department_id 列具有显着的数据倾斜。 SYSTEM 执行以下设置代码,将部门 50 中的 100,000 名员工添加到示例模式中的员工表中,总共 100,107 行,然后收集表统计信息:

DELETE FROM hr.employees WHERE employee_id > 999;

ALTER TABLE hr.employees DISABLE NOVALIDATE CONSTRAINT emp_email_uk;

DECLARE
v_counter NUMBER(7) := 1000;
BEGIN
 FOR i IN 1..100000 LOOP    
 INSERT INTO hr.employees 
   VALUES (v_counter, null, 'Doe', '[email protected]', null,'07-JUN-02',
     'AC_ACCOUNT', null, null, null, 50);
 v_counter := v_counter + 1;
 END LOOP;
END;
/
COMMIT; 

BEGIN
  DBMS_STATS.GATHER_TABLE_STATS (ownname = 'hr',tabname => 'employees');
END;
/

ALTER SYSTEM FLUSH SHARED_POOL;

以下查询显示 employees.department_id 列的直方图:

COL TABLE_NAME FORMAT a15
COL COLUMN_NAME FORMAT a20
COL HISTOGRAM FORMAT a9

SELECT TABLE_NAME, COLUMN_NAME, HISTOGRAM
FROM   DBA_TAB_COLS
WHERE  OWNER = 'HR'
AND    TABLE_NAME = 'EMPLOYEES'
AND    COLUMN_NAME = 'DEPARTMENT_ID';

TABLE_NAME      COLUMN_NAME          HISTOGRAM
--------------- -------------------- ---------
EMPLOYEES       DEPARTMENT_ID        FREQUENCY

示例 20-13 低基数查询

本示例延续了示例 20-12 中的示例。 以下查询显示值 10 对列 department_id 的基数极低,占据了 .00099% 的行:

VARIABLE dept_id NUMBER
EXEC :dept_id := 10;
SELECT COUNT(*), MAX(employee_id) FROM hr.employees WHERE department_id = :dept_id;

  COUNT(*) MAX(EMPLOYEE_ID)
---------- ----------------
         1              200

优化器选择索引范围扫描,正如对于此类低基数查询所期望的那样:

SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR);

PLAN_TABLE_OUTPUT
-------------------------------------
SQL_ID	a9upgaqqj7bn5, child number 0
-------------------------------------
select COUNT(*), MAX(employee_id) FROM hr.employees WHERE department_id = :dept_id

Plan hash value: 1642965905

-------------------------------------------------------------------------------------
| Id| Operation                           | Name       |Rows|Bytes|Cost (%CPU)|Time |
-------------------------------------------------------------------------------------
| 0| SELECT STATEMENT                     |                   |  |  |2(100)|        |
| 1|  SORT AGGREGATE                      |                   |1 |8 |      |        |
| 2|   TABLE ACCESS BY INDEX ROWID BATCHED| EMPLOYEES         |1 |8 |2  (0)|00:00:01|
|*3|    INDEX RANGE SCAN                  | EMP_DEPARTMENT_IX |1 |  |1  (0)|00:00:01|
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("DEPARTMENT_ID"=:DEPT_ID)

以下 V$SQL 查询获取有关游标的信息:

COL BIND_AWARE FORMAT a10
COL SQL_TEXT FORMAT a22
COL CHILD# FORMAT 99999
COL EXEC FORMAT 9999
COL BUFF_GETS FORMAT 999999999
COL BIND_SENS FORMAT a9
COL SHARABLE FORMAT a9

SELECT SQL_TEXT, CHILD_NUMBER AS CHILD#, EXECUTIONS AS EXEC, 
       BUFFER_GETS AS BUFF_GETS, IS_BIND_SENSITIVE AS BIND_SENS, 
       IS_BIND_AWARE AS BIND_AWARE, IS_SHAREABLE AS SHARABLE
FROM   V$SQL
WHERE  SQL_TEXT LIKE '%mployee%'
AND    SQL_TEXT NOT LIKE '%SQL_TEXT%';

SQL_TEXT               CHILD#  EXEC  BUFF_GETS BIND_SENS BIND_AWARE SHARABLE
---------------------- ------ ----- ---------- --------- ---------- --------
SELECT COUNT(*), MAX(e      0     1        196         Y          N        Y
mployee_id) FROM hr.em
ployees WHERE departme
nt_id = :dept_id

前面的输出显示了一个为低基数查询执行过一次的子游标。 游标已标记为绑定敏感,因为优化器认为最佳计划可能取决于绑定变量的值。

当游标被标记为绑定敏感时,Oracle 数据库使用不同的绑定值监视游标的行为,以确定针对不同绑定值的不同计划是否更有效。 数据库将此游标标记为绑定敏感,因为优化器使用 department_id 列上的直方图来计算谓词 WHERE department_id = :dept_id 的选择性。 因为直方图的存在表明该列是倾斜的,所以绑定变量的不同值可能需要不同的计划。

示例 20-14 高基数查询

该示例延续了示例 20-13 中的示例。 以下代码使用值 50 重新执行相同的查询,它占据了 99.9% 的行:

EXEC :dept_id := 50;
SELECT COUNT(*), MAX(employee_id) FROM hr.employees WHERE department_id = :dept_id;

  COUNT(*) MAX(EMPLOYEE_ID)
---------- ----------------
    100045	     100999

尽管这种非选择性查询使用全表扫描会更有效,但优化器会选择用于 department_id=10 的相同索引范围扫描。 这是因为数据库假定游标中的现有计划可以共享:

SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR);

PLAN_TABLE_OUTPUT
-------------------------------------
SQL_ID	a9upgaqqj7bn5, child number 0
-------------------------------------
SELECT COUNT(*), MAX(employee_id) FROM hr.employees WHERE department_id = :dept_id

Plan hash value: 1642965905

-------------------------------------------------------------------------------------
| Id| Operation                           | Name       |Rows|Bytes|Cost (%CPU)|Time |
-------------------------------------------------------------------------------------
| 0| SELECT STATEMENT                     |                   |  |  |2(100)|        |
| 1|  SORT AGGREGATE                      |                   |1 |8 |      |        |
| 2|   TABLE ACCESS BY INDEX ROWID BATCHED| EMPLOYEES         |1 |8 |2  (0)|00:00:01|
|*3|    INDEX RANGE SCAN                  | EMP_DEPARTMENT_IX |1 |  |1  (0)|00:00:01|
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("DEPARTMENT_ID"=:DEPT_ID)

V$SQL 的查询显示子游标现在已经执行了两次:

SELECT SQL_TEXT, CHILD_NUMBER AS CHILD#, EXECUTIONS AS EXEC, 
       BUFFER_GETS AS BUFF_GETS, IS_BIND_SENSITIVE AS BIND_SENS, 
       IS_BIND_AWARE AS BIND_AWARE, IS_SHAREABLE AS SHARABLE
FROM   V$SQL
WHERE  SQL_TEXT LIKE '%mployee%'
AND    SQL_TEXT NOT LIKE '%SQL_TEXT%';

SQL_TEXT               CHILD#  EXEC  BUFF_GETS BIND_SENS BIND_AWARE SHARABLE
---------------------- ------ ----- ---------- --------- ---------- --------
SELECT COUNT(*), MAX(e      0     2       1329         Y          N        Y
mployee_id) FROM hr.em
ployees WHERE departme
nt_id = :dept_id

20.3.4 Bind-Aware Cursors

绑定感知游标是绑定敏感游标,可以针对不同的绑定值使用不同的计划。

使游标具有绑定感知功能后,优化器会根据绑定值及其基数估计为未来的执行选择计划。 因此,“绑定感知”本质上意味着“当前绑定值的最佳计划”。

当执行具有绑定敏感游标的语句时,优化器使用内部算法来确定是否将游标标记为绑定感知。 该决定取决于游标是否为不同的绑定值产生明显不同的数据访问模式,从而导致与预期不同的性能成本。

如果数据库将游标标记为绑定感知,则下次游标执行时数据库将执行以下操作:

  • 根据绑定值生成新计划
  • 将为语句生成的原始游标标记为不可共享(V$SQL.IS_SHAREABLE 为 N)。 原始游标不再可用并且有资格从库缓存中老化

当同一个查询以不同的绑定值重复执行时,数据库会在 SQL 语句的“签名”(包括优化器环境、NLS 设置等)中添加新的绑定值,并对这些值进行分类。 数据库检查绑定值,并考虑当前绑定值是否导致明显不同的数据量,或者现有计划是否足够。 数据库不需要为每个新值创建新计划。

考虑这样一种情况,您执行一个具有 12 个不同绑定值的语句(每个不同值执行两次),这会导致数据库触发 5 次硬解析,并创建 2 个额外的计划。 因为数据库执行 5 次硬解析,它创建了 5 个新的子游标,即使一些游标与现有游标具有相同的执行计划。 数据库将多余的游标标记为不可用,这意味着这些游标最终会从库缓存中老化。

在最初的硬解析期间,优化器实际上是在映射绑定值和适当的执行计划之间的关系。 在这个初始阶段之后,数据库最终达到稳定状态。 使用新的绑定值执行会导致在缓存中选择最佳的子游标,而无需进行硬解析。 因此,解析的数量不随不同绑定值的数量变化。

示例 20-15 绑定感知游标
这个例子延续了“绑定敏感游标”中的例子。 以下代码使用绑定变量设置为 50 发出第二个查询 employees:

EXEC :dept_id := 50;
SELECT COUNT(*), MAX(employee_id) FROM hr.employees WHERE department_id = :dept_id;

  COUNT(*) MAX(EMPLOYEE_ID)
---------- ----------------
    100045           100999

在前两次执行期间,数据库正在监视查询的行为,并确定不同的绑定值导致查询在基数上存在显着差异。 基于这种差异,数据库会调整其行为,以便不会始终为该查询共享相同的计划。 因此,优化器根据当前绑定值生成一个新计划,该值是 50:

SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR);

PLAN_TABLE_OUTPUT
-------------------------------------
SQL_ID	a9upgaqqj7bn5, child number 1
-------------------------------------
SELECT COUNT(*), MAX(employee_id) FROM hr.employees WHERE department_id = :dept_id

Plan hash value: 1756381138

---------------------------------------------------------------------------
| Id  | Operation         | Name      | Rows  | Bytes | Cost(%CPU)| Time  |
---------------------------------------------------------------------------
|  0 | SELECT STATEMENT   |           |       |      |254 (100)|          |
|  1 |  SORT AGGREGATE    |           |     1 |    8 |         |          |
|* 2 |   TABLE ACCESS FULL| EMPLOYEES |  100K | 781K |254  (15)| 00:00:01 |
---------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("DEPARTMENT_ID"=:DEPT_ID)

以下 V$SQL 查询获取有关游标的信息:

SELECT SQL_TEXT, CHILD_NUMBER AS CHILD#, EXECUTIONS AS EXEC, 
       BUFFER_GETS AS BUFF_GETS, IS_BIND_SENSITIVE AS BIND_SENS, 
       IS_BIND_AWARE AS BIND_AWARE, IS_SHAREABLE AS SHAREABLE
FROM   V$SQL
WHERE  SQL_TEXT LIKE '%mployee%'
AND    SQL_TEXT NOT LIKE '%SQL_TEXT%';

SQL_TEXT               CHILD#  EXEC  BUFF_GETS BIND_SENS BIND_AWARE SHAREABLE
---------------------- ------ ----- ---------- --------- ---------- ---------
SELECT COUNT(*), MAX(e      0     2       1329         Y          N         N
mployee_id) FROM hr.em
ployees WHERE departme
nt_id = :dept_id

SELECT COUNT(*), MAX(e      1     1        800         Y          Y         Y
mployee_id) FROM hr.em
ployees WHERE departme
nt_id = :dept_id

前面的输出显示数据库创建了一个额外的子游标(CHILD# of 1)。 游标 0 现在标记为不可共享。 游标 1 显示许多缓冲区低于游标 0,并且标记为绑定敏感和绑定感知。 绑定感知游标可以对不同的绑定值使用不同的计划,这取决于包含绑定变量的谓词的选择性。

示例 20-16 绑定感知游标:选择最佳计划

本示例延续“示例 20-15”中的示例。 以下代码执行相同的 employees 查询,其值为 10,基数极低(只有一行):

EXEC :dept_id := 10;
SELECT COUNT(*), MAX(employee_id) FROM hr.employees WHERE department_id = :dept_id;

  COUNT(*) MAX(EMPLOYEE_ID)
---------- ----------------
         1              200

以下输出显示优化器根据当前绑定值 10 的低基数估计选择了最佳计划,即索引扫描:

SQL> SELECT * from TABLE(DBMS_XPLAN.DISPLAY_CURSOR);

PLAN_TABLE_OUTPUT
-------------------------------------
SQL_ID	a9upgaqqj7bn5, child number 2
-------------------------------------
select COUNT(*), MAX(employee_id) FROM hr.employees WHERE department_id = :dept_id

Plan hash value: 1642965905

-------------------------------------------------------------------------------------
| Id| Operation                           | Name       |Rows|Bytes|Cost (%CPU)|Time |
-------------------------------------------------------------------------------------
| 0| SELECT STATEMENT                     |                   |  |  |2(100)|        |
| 1|  SORT AGGREGATE                      |                   |1 |8 |      |        |
| 2|   TABLE ACCESS BY INDEX ROWID BATCHED| EMPLOYEES         |1 |8 |2  (0)|00:00:01|
|*3|    INDEX RANGE SCAN                  | EMP_DEPARTMENT_IX | 1|  |1  (0)|00:00:01|
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("DEPARTMENT_ID"=:DEPT_ID)

V$SQL 输出现在显示存在三个子游标:

SELECT SQL_TEXT, CHILD_NUMBER AS CHILD#, EXECUTIONS AS EXEC, 
       BUFFER_GETS AS BUFF_GETS, IS_BIND_SENSITIVE AS BIND_SENS, 
       IS_BIND_AWARE AS BIND_AWARE, IS_SHAREABLE AS SHAREABLE
FROM   V$SQL
WHERE  SQL_TEXT LIKE '%mployee%'
AND    SQL_TEXT NOT LIKE '%SQL_TEXT%';

SQL_TEXT               CHILD#  EXEC  BUFF_GETS BIND_SENS BIND_AWARE SHAREABLE
---------------------- ------ ----- ---------- --------- ---------- ---------
SELECT COUNT(*), MAX(e      0     2       1329         Y          N         N
mployee_id) FROM hr.em
ployees WHERE departme
nt_id = :dept_id

SELECT COUNT(*), MAX(e      1     1        800         Y          Y         Y
mployee_id) FROM hr.em
ployees WHERE departme
nt_id = :dept_id

SELECT COUNT(*), MAX(e      2     1          3         Y          Y         Y
mployee_id) FROM hr.em
ployees WHERE departme
nt_id = :dept_id

当游标切换到绑定感知模式时,数据库丢弃了原始游标(CHILD# of 0)。 这是一次性开销。 数据库将游标 0 标记为不可共享(SHAREABLE 为 N),这意味着此游标不可用并且将最先从游标缓存中老化。

20.3.5 Cursor Merging

如果优化器为绑定感知游标创建计划,并且如果该计划与现有游标相同,则优化器可以执行游标合并。

在这种情况下,数据库合并游标以节省库缓存中的空间。 数据库增加游标的选择性范围以包括新绑定值的选择性。

当查询使用新的绑定变量时,优化器会尝试根据绑定值的选择性相似性来找到它认为合适的游标。 如果数据库找不到这样的游标,则会创建一个新游标。 如果新游标的计划与现有游标之一相同,则数据库会合并这两个游标以节省库缓存中的空间。 合并导致数据库将一个游标标记为不可共享。 如果库缓存面临空间压力,则数据库首先使非共享游标老化。

20.3.6 Adaptive Cursor Sharing Views

您可以使用 V$ 视图进行自适应游标共享,以查看选择性范围、游标信息(例如游标是绑定感知型还是绑定敏感型)和执行统计信息。
具体来说,使用以下视图:

  • V$SQL 显示游标是绑定敏感的还是绑定感知的。
  • V$SQL_CS_HISTOGRAM 显示执行计数在三桶执行历史直方图中的分布。
  • 如果选择性用于检查游标共享,则 V$SQL_CS_SELECTIVITY 显示为每个包含绑定变量的谓词存储的选择性范围。 它包含谓词的文本,以及选择性范围的低值和高值。
  • V$SQL_CS_STATISTICS 总结了优化器用来确定是否将游标标记为绑定感知的信息。 对于执行示例,数据库跟踪处理的行、缓冲区获取和 CPU 时间。 当使用绑定集构建游标时,PEEKED 列显示 YES; 否则,该值为 NO。

20.4 Real-World Performance Guidelines for Cursor Sharing

Real-World Performance 团队制定了有关如何优化 Oracle 数据库应用程序中的游标共享的指南。

20.4.1 Develop Applications with Bind Variables for Security and Performance

Real-World Performance 小组强烈建议所有企业应用程序都使用绑定变量

Oracle 数据库应用程序旨在使用绑定变量编写。 避免导致大量用户发出动态、非共享 SQL 语句的应用程序设计。

每当 Oracle 数据库无法在库缓存中找到语句的匹配项时,它就必须执行硬解析。 尽管使用文字开发应用程序存在危险,但并非所有现实世界的应用程序都使用绑定变量。 开发人员有时会发现编写使用文字的程序更快更容易。 然而,减少开发时间并不会带来部署后更好的性能和安全性

使用绑定变量的主要好处如下:

  • 资源效率
    在每次执行之前编译程序不会有效地使用资源,但这实际上是 Oracle 数据库在执行硬解析时所做的。 数据库服务器必须消耗大量 CPU 和内存来创建游标、生成和评估执行计划等。 通过使数据库共享游标,软解析消耗的资源少得多。 如果应用程序使用文字而不是绑定变量,每天只执行几个查询,那么 DBA 可能不会将额外的开销视为性能问题。 但是,如果应用程序每秒执行数百或数千个查询,那么额外的资源开销很容易将性能降低到不可接受的水平。 使用绑定变量使数据库只执行一次硬解析,而不管语句执行多少次

  • 可扩展性
    当数据库执行硬解析时,数据库会花费更多时间获取和保存共享池和库缓存中的锁存器。 锁存器是低级序列化设备。 数据库在共享内存中闩锁结构的时间越长、越频繁,这些闩锁的队列就越长。 当多个语句共享相同的执行计划时,对闩锁的请求和闩锁的持续时间会下降。 此行为增加了可伸缩性。

  • 吞吐量和响应时间
    当数据库避免不断地重新解析和创建游标时,它的更多时间将花费在用户空间中。 Real-World Performance 小组发现,将文字更改为使用绑定通常会导致吞吐量和用户响应时间的数量级改进。

  • 安全
    防止 SQL 注入攻击的唯一方法是使用绑定变量。 恶意用户可以通过将代码“注入”到应用程序中来利用连接字符串的应用程序。

20.4.2 Do Not Use CURSOR_SHARING = FORCE as a Permanent Fix

最佳实践是编写可共享的 SQL 并对 CURSOR_SHARING 使用默认的 EXACT。

但是,对于具有许多类似语句的应用程序,将 CURSOR_SHARING 设置为 FORCE 有时可以显着改善游标共享。 用系统生成的绑定值替换文字可以减少内存使用、加快解析速度并减少闩锁争用。 然而,FORCE 并不是一个永久的开发解决方案。

作为一般准则,Real-World Performance 组建议不要在极少数情况下将 CURSOR_SHARING 设置为 FORCE 异常,然后仅当满足以下所有条件时:

  • 共享池中的语句仅在文字值上有所不同。
  • 由于大量的库缓存未命中,响应时间不是最佳的。
  • 您现有的代码存在严重的安全性和可扩展性错误——缺少绑定变量——在源代码修复之前,您需要一个临时的创可贴。
  • 您在会话级别而不是实例级别设置此初始化参数。

将 CURSOR_SHARING 设置为 FORCE 有以下缺点:

  • 它表明应用程序没有使用用户定义的绑定变量,这意味着它对 SQL 注入是开放的。 将 CURSOR_SHARING 设置为 FORCE 不会修复 SQL 注入错误或使代码更加安全。 只有在注入了任何恶意 SQL 文本后,数据库才会绑定值。
  • 数据库必须在软解析期间执行额外的工作才能在共享池中找到类似的语句。
  • 数据库会删除每个文字,这意味着它可以删除有用的信息。 例如,数据库去除了 SUBSTR 和 TO_DATE 函数中的文字值。 在文字更优化的情况下使用系统生成的绑定变量会对执行计划产生负面影响。
  • 在 SELECT 语句中包含文字的任何选定表达式的最大长度(由 DESCRIBE 返回)有所增加。 但是,返回数据的实际长度并没有改变。
  • 不支持星形变换。

20.4.3 Establish Coding Conventions to Increase Cursor Reuse

默认情况下,两个 SQL 语句文本的任何变化都会阻止数据库共享游标,包括绑定变量的名称。 此外,绑定变量大小的更改会导致游标不匹配。 因此,在应用程序代码中使用绑定变量不足以保证游标共享。

Real-World Performance 小组建议您标准化 SQL 语句和 PL/SQL 块的间距和大写约定还要为绑定变量的命名和定义建立约定。 如果数据库没有按预期共享游标,请通过查询 V$SQL_SHARED_CURSOR 开始诊断。

示例 20-17 SQL 文本的变化

在此示例中,使用绑定变量的应用程序使用相同的绑定变量值执行 7 条语句,但这些语句在文本上并不相同:

VARIABLE emp_id NUMBER
EXEC :emp_id := 101;

SELECT SUM(salary) FROM hr.employees WHERE employee_id < :emp_id;
SELECT SUM(salary) FROM hr.employees WHERE employee_id < :EMP_ID;
SELECT SUM(salary) FROM hr.employees WHERE employee_id < :Emp_Id;
SELECT SUM(salary)  FROM hr.employees WHERE employee_id < :emp_id;
select sum(salary) from hr.employees where employee_id < :emp_id;
Select sum(salary) From hr.employees Where employee_id < :emp_id;
Select sum(salary) From hr.employees Where employee_id< :emp_id;

V$SQLAREA 的查询显示没有发生游标共享:

COL SQL_TEXT FORMAT a35
SELECT SQL_TEXT, SQL_ID, VERSION_COUNT, HASH_VALUE
FROM   V$SQLAREA
WHERE  SQL_TEXT LIKE '%mployee%'
AND    SQL_TEXT NOT LIKE '%SQL_TEXT%';

SQL_TEXT                            SQL_ID        VERSION_COUNT HASH_VALUE
----------------------------------- ------------- ------------- ----------
SELECT SUM(salary) FROM hr.employee bkrfu3ggu5315             1 3751971877
s WHERE employee_id < :EMP_ID
SELECT SUM(salary) FROM hr.employee 70mdtwh7xj9gv             1  265856507
s WHERE employee_id < :Emp_Id
Select sum(salary) From hr.employee 18tt4ny9u5wkt             1 2476929625
s Where employee_id< :emp_id
SELECT SUM(salary)  FROM hr.employe b6b21tbyaf8aq             1 4238811478
es WHERE employee_id < :emp_id
SELECT SUM(salary) FROM hr.employee 4318cbskba8yh             1  615850960
s WHERE employee_id < :emp_id
select sum(salary) from hr.employee 633zpx3xm71kj             1 4214457937
s where employee_id < :emp_id
Select sum(salary) From hr.employee 1mqbbbnsrrw08             1  830205960
s Where employee_id < :emp_id

7 rows selected.

示例 20-18 绑定长度不匹配

以下代码定义了一个具有不同长度(20字节与100字节)的绑定变量,然后使用相同的绑定值执行文本相同的语句:

VARIABLE lname VARCHAR2(20)
EXEC :lname := 'Taylor';
SELECT SUM(salary) FROM hr.employees WHERE last_name = :lname;
VARIABLE lname VARCHAR2(100)
EXEC :lname := 'Taylor';
SELECT SUM(salary) FROM hr.employees WHERE last_name = :lname;

以下查询显示数据库没有共享游标:

COL SQL_TEXT FORMAT a35
SELECT SQL_TEXT, SQL_ID, VERSION_COUNT, HASH_VALUE
FROM   V$SQLAREA
WHERE  SQL_TEXT LIKE '%mployee%'
AND    SQL_TEXT NOT LIKE '%SQL_TEXT%';

SQL_TEXT                            SQL_ID	       VERSION_COUNT HASH_VALUE
----------------------------------- ------------- ------------- ----------
SELECT SUM(salary) FROM hr.employee buh8j4557r0h1             2 1249608193
s WHERE last_name = :lname

原因是因为绑定长度:

COL BIND_LENGTH_UPGRADEABLE FORMAT a15
SELECT s.SQL_TEXT, s.CHILD_NUMBER, 
       c.BIND_LENGTH_UPGRADEABLE
FROM   V$SQL s, V$SQL_SHARED_CURSOR c
WHERE  SQL_TEXT LIKE '%employee%'
AND    SQL_TEXT NOT LIKE '%SQL_TEXT%'
AND    s.CHILD_ADDRESS = c.CHILD_ADDRESS;

SQL_TEXT                            CHILD_NUMBER BIND_LENGTH_UPG
----------------------------------- ------------ ---------------
SELECT SUM(salary) FROM hr.employee            0 N
s WHERE last_name = :lname
SELECT SUM(salary) FROM hr.employee            1 Y
s WHERE last_name = :lname

20.4.4 Minimize Session-Level Changes to the Optimizer Environment

最佳做法是防止应用程序的用户更改其个人会话的优化方法和目标。 对优化器环境的任何更改都可以防止相同的语句共享游标。

示例 20-19 环境不匹配

此示例显示两个文本相同的语句,但它们不共享游标:

VARIABLE emp_id NUMBER

EXEC :emp_id := 110;

ALTER SESSION SET OPTIMIZER_MODE = FIRST_ROWS;
SELECT salary FROM hr.employees WHERE employee_id < :emp_id;
ALTER SESSION SET OPTIMIZER_MODE = ALL_ROWS;
SELECT salary FROM hr.employees WHERE employee_id < :emp_id;

V$SQL_SHARED_CURSOR 的查询显示优化器模式不匹配:

在这里插入代码片

猜你喜欢

转载自blog.csdn.net/stevensxiao/article/details/125263221