Cursor 直译过来就是 “游标” ,它是 Oracle 数据库中 SQL 解析和执行的载体。Oracle 数据库是用 C 语言写的,所以从本质上来说,可以将 Cursor 理解成是 C 语言中的一种结构。

Oracle 数据库中的 Cursor 分为两种类型:一种是 Shared Curosr;另一种是 Session Cursor。

Oracle 里的 Shared Cursor

在描述 Shared Cursor 的详细含义之前,先介绍一下 Oracle 数据库中库缓存(Library Cache)的作用和其组成结构。

Oracle-SGA

库缓存实际上是 SGA 中的一块内存区域(更确切地说,库缓存是 Shared Pool)中的一块内存区域),它的主要作用是缓存刚刚执行过的 SQL 语句和 PL/SQL 语句(如存储过程、函数、包、触发器)所对应的执行计划、解析树、Pcode、Mcode 等对象。当同样的 SQL 语句和 PL/SQL 语句再次被执行时,就可以利用已经缓存在 Library Cache 中的那些相关对象而无须再次从头开始解析,这样就提高了这些 SQL 语句和 PL/SQL 语句在重复执行时的执行效率。

缓存在库缓存中的对象我们称之为库缓存对象(Library Cache Objet),所有的库缓存对象都是以一种名为库缓存对象句柄(Library Cache Object Handle)的结构存储在库缓存中,Oracle 是通过访问相关的库缓存对象句柄来访问对应的库缓存对象的。

Shared Cursor 的含义

Oracle 数据库中的 Shared Cursor 就是指缓存在库缓存(Library Cache)中的一种库缓存对象,说白了就是指缓存在库缓存里的 SQL 语句和匿名 PL/SQL 语句所对应的库缓存对象。Shared Cursor 是 Oracle 缓存在 Library Cache 中的几十种库缓存对象之一,它所对应的库缓存对象句柄的 Namespace 属性的值是 CRSR(也就是 Cursor 的缩写)。

Shared Cursor 中会存储目标 SQL 的 SQL 文本、解析树、该 SQL 所涉及的对象定义、该 SQL 所使用的绑定变量类型和长度,以及该 SQL 的执行计划等信息。

Shared Cursor 的分类

Oracle 数据库中的 Shared Cursor 又细分为 Parent Cursor(父游标)和 Child Curosr(子游标)这两种类型,可以分别通过查询视图 V$SQLAREAV$SQL 来查看当前缓存在库缓存中的 Parent Cursor 和 Child Cursor。

  • V$SQLAREA 用于查看 Parent Cursor

  • V$SQL 用于查看 Child Cursor

在 Oracle 数据库里,任意一个目标 SQL 一定会同时对应两个 Shared Cursor,其中一个是 Parent Cursor,另外一个是 Child Cursor。Parent Cursor 会存储该 SQL 的 SQL 文本,而该 SQL 真正的可以被重用的解析树和执行计划则存储在 Child Cusor 中。

Parent Cursor 和 Child Cursor 实例(一个 Parent Cursor 对应一个 Child Cursor)

  • 以 SCOTT 用户登录数据库
SQL> conn scott/tiger@localhost/orcl
Connected to Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 
Connected as scott@localhost/orcl
  • 执行如下 SQL 语句
SQL> select empno, ename from emp;

EMPNO ENAME
----- ----------
 7369 SMITH
 7499 ALLEN
 7521 WARD
 7566 JONES
 7654 MARTIN
 7698 BLAKE
 7782 CLARK
 7788 SCOTT
 7839 KING
 7844 TURNER
 7876 ADAMS
 7900 JAMES
 7902 FORD
 7934 MILLER

14 rows selected。

当一条 SQL 第一次被执行的时候,Oracle 会同时产生一个 Parent Cursor 和一个 Child Cursor。上述 SQL 是第一次执行,所以现在 Oracle 应该会同时产生一个 Parent Cursor 和 一个 Child Cursor。

  • 根据 SQL 文本查询该 SQL 对应的 Parent Cursor 的信息
SQL> select sql_text, sql_id, version_count from v$sqlarea where sql_text like 'select empno, ename%';

SQL_TEXT                                 SQL_ID        VERSION_COUNT
---------------------------------------- ------------- -------------
select empno, ename from emp             djjzt7nqb62ns             1

原目标 SQL 在 V$SQLAREA 中只有一条匹配记录,且这条记录的列 VERSION_COUNT 的值为 1(VERSION_COUNT 表示某个 Parent Cursor 所拥有的 Child Cursor 的数量),这说明 Oracle 在执行原目标 SQL 时确实只产生了一个 Parent Cursor 和一个 Child Cursor。

  • 根据 SQL_ID 查询该 SQL 对应的所有 Child Cursor 的 信息
SQL> select sql_id, parsing_schema_name, hash_value, child_number from v$sql where sql_id = 'djjzt7nqb62ns';

SQL_ID        PARSING_SCHEMA_NAME            HASH_VALUE CHILD_NUMBER
------------- ------------------------------ ---------- ------------
djjzt7nqb62ns SCOTT                           749931160            0

上述 SQL_ID 在 V$SQL 中只有一条匹配记录,而且这条记录的 CHILD_NUMBER 的值为 0(CHILD_NUMBER 表示某个 Child Cursor 所对应的子游标号),说明 Oracle 在执行原目标 SQL 时确实只产生了一个子游标号为 0 的 Child Cursor。

  • 将原目标 SQL 中的表名从小写改为大写后再次执行
SQL> select empno, ename from EMP;

EMPNO ENAME
----- ----------
 7369 SMITH
 7499 ALLEN
 7521 WARD
 7566 JONES
 7654 MARTIN
 7698 BLAKE
 7782 CLARK
 7788 SCOTT
 7839 KING
 7844 TURNER
 7876 ADAMS
 7900 JAMES
 7902 FORD
 7934 MILLER

14 rows selected

由于 Oracle 会根据目标 SQL 的 SQL 文本的哈希值去相应的 Hash Bucket 中查找匹配的 Parent Cursor,而哈希运算是对大小写敏感的,所以当我们执行上述改写后的目标 SQL 时,大写 EMP 所对应的 Hash Bucket 和小写 emp 所对应的 Hash Bucket 极有可能不是同一个 Hash Bucket(即使是同一个 Hash Bucket 也没有关系,因为 Oracle 还会继续比对 Parent Cursor 所在的库缓存对象句柄的 Name 属性值,小写 emp 所对应的 Parent Cursor 的 Name 值为 “select empno, ename from emp”,大写 EMP 所对应的 Parent Cursor 的 Name 值为 “select empno, ename from EMP” ,两者显然是不相等的)。

也就是说,小写 emp 所对应的 Parent Cursor 并不是大写 EMP 所要查找的 Parent Cursor,两者不能共享,所以此时 Oracle 肯定会新生成一对 Parent Cursor 和 Child Cursor。

  • 根据 SQL 文本查询该 SQL 对应的 Parent Cursor 的信息
SQL> select sql_text, sql_id, version_count from v$sqlarea where sql_text like 'select empno, ename%';

SQL_TEXT                                 SQL_ID        VERSION_COUNT
---------------------------------------- ------------- -------------
select empno, ename from emp             djjzt7nqb62ns             1
select empno, ename from EMP             3fcrxtakyb5wa             1
  • 根据 SQL_ID 查询该 SQL 对应的所有 Child Cursor 的 信息
SQL> select sql_id, parsing_schema_name, hash_value, child_number from v$sql where sql_id = '3fcrxtakyb5wa';

SQL_ID        PARSING_SCHEMA_NAME            HASH_VALUE CHILD_NUMBER
------------- ------------------------------ ---------- ------------
3fcrxtakyb5wa SCOTT                          2783287178            0

从上述查询结果可以看出,针对大写 EMP 所对应的目标 SQL,Oracle 确实生成了一个 Parent Cursor 和一个 Child Cursor。

Parent Cursor 和 Child Cursor 实例(一个 Parent Cursor 对应多个 Child Cursor)

  • 创建 sh 用户并登录该用户
SQL> create user sh identified by sh;

User created


SQL> grant connect, resource to sh;

Grant succeeded


SQL> grant select on scott.emp to sh;

Grant succeeded


SQL> conn sh/sh@localhost/orcl;
Connected to Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 
Connected as sh@localhost/orcl
  • 在 sh 用户下创建一个同名表 EMP
SQL> create table emp as select * from scott.emp;

Table created
  • 再次执行上述小写 emp 所对应的目标 SQL
SQL> select empno, ename from emp;

EMPNO ENAME
----- ----------
 7369 SMITH
 7499 ALLEN
 7521 WARD
 7566 JONES
 7654 MARTIN
 7698 BLAKE
 7782 CLARK
 7788 SCOTT
 7839 KING
 7844 TURNER
 7876 ADAMS
 7900 JAMES
 7902 FORD
 7934 MILLER

14 rows selected

Oracle 会根据目标 SQL 的 SQL 文本的哈希值去相应的 Hash Bucket 中查找匹配的 Parent Cursor,找到了匹配的 Parent Cursor 后还得遍历从属于该 Parent Cursor 的所有 Child Cursor(因为可以被重用的解析树和执行计划都存储在 Child Cursor 中)。

对于上述 SQL 而言,因为同样的 SQL 文本之前在 SCOTT 用户下已经执行过,在 Library Cache 中也已经生成了对应的 Parent Cursor 和 Child Cursor,所以这里 Oracle 根据上述 SQL 的 SQL 文本的哈希值去 Library Cache 中查找匹配的 Parent Cursor 是肯定能匹配到记录的。

但接下来遍历从属于该 Parent Cursor 下的所有 Child Cursor 时,Oracle 会发现对应的 Child Cursor 中存储的解析树和执行计划此时是不能重用的,因为此时的 Child Cursor 里存储的解析树和执行计划针对的是 SCOTT 用户下的表 EMP,而上述 SQL 针对的则是 SH 用户下的同名表 EMP,待查询的目标表根本就不是同一个表,解析树和执行计划当然就不能共享了

这意味着 Oracle 还得针对上述 SQL 从头再做一次解析,并把解析后的解析树和执行计划存储在一个新生成的 Child Cursor 里,再把这个 Child Cursor 挂在上述 Parent Cursor 下。也就是说,一旦上述 SQL 执行完毕,该 SQL 对应的 Parent Cursor 下就会有两个 Child Cursor,一个 Child Cursor 中存储的是针对 SCOTT 用户下表 EMP 的解析树和执行计划,另外一个 Child Cursor 中存储的则是针对 SH 用户下的同名表 EMP 的解析树和执行计划。

  • 根据 SQL 文本查询该 SQL 对应的 Parent Cursor 的信息
SQL> select sql_text, sql_id, version_count from v$sqlarea where sql_text like 'select empno, ename%';

SQL_TEXT                                 SQL_ID        VERSION_COUNT
---------------------------------------- ------------- -------------
select empno, ename from emp             djjzt7nqb62ns             2
select empno, ename from EMP             3fcrxtakyb5wa             1

上述 SQL(即 “select empno, ename from emp”)在 V$SQLAREA 中匹配记录的列 VERSION_COUNT 的值为 2,说明 Oracle 在执行该 SQL 时确实产生了一个 Parent Cursor 和两个 Child Cursor。

  • 根据 SQL_ID 查询该 SQL 对应的所有 Child Cursor 的 信息
SQL> select sql_id, parsing_schema_name, hash_value, child_number from v$sql where sql_id = 'djjzt7nqb62ns';

SQL_ID        PARSING_SCHEMA_NAME            HASH_VALUE CHILD_NUMBER
------------- ------------------------------ ---------- ------------
djjzt7nqb62ns SCOTT                           749931160            0
djjzt7nqb62ns SH                              749931160            1

上述 SQL 在 V$SQL 中有两条匹配记录,且这两条记录的 CHILD_NUMBER 的值分别为 0 和 1,说明 Oracle 在执行上述 SQL 时确实产生了两个 Child Cursor,它们的子游标号分别为 0 和 1。

Oracle 在解析目标 SQL 时去库缓存中查找匹配 Shared Cursor 的过程可以用下面的图来说明。

oracle-shared-cursor

(1)根据目标 SQL 的 SQL 文本的哈希值去库缓存中查找匹配的 Hash Bucket;
(2)然后在匹配的 Hash Bucket 的库缓存对象链表中查找匹配的 Parent Cursor,当然,在查找匹配 Parent Cursor 的过程中肯定会比对目标 SQL 的 SQL 文本,因为不同的 SQL 文本计算出来的哈希值可能是相同的;
(3)如果找到了匹配的 Parent Cursor,则 Oracle 接下来就会遍历从属于该 Parent Cursor 下的所有 Child Cursor 用以查找匹配的 Child Cursor;
(3.1)如果找到了匹配的 Child Cursor,则 Oracle 就会把存储于该 Child Cursor 中的解析树和执行计划直接拿过来重用,而不用再从头开始解析;
(3.2)如果找不到匹配的 Child Cursor,则意味着没有可以共享的解析树和执行计划,接下来 Oracle 就会重头开始解析上述目标 SQL,新生成一个 Child Cursor,并把这个 Child Cursor 挂在对应的 Parent Cursor 下;
(4)如果找不到匹配的 Parent Cursor,则也意味着此时没有可以共享的解析树和执行计划,Oracle 就会从头开始解析上述目标 SQL,新生成一个 Parent Cursor 和一个 Child Cursor,并把它们挂在对应的 Hash Bucket 中。

硬解析

硬解析(Hard Parse)是指 Oracle 在执行目标 SQL 时,在库缓存中找不到可以重用的解析树和执行计划,而不得不从头开始解析目标 SQL 并生成相应的 Parent Cursor 和 Child Cursor 的过程。

硬解析实际上有两种类型:

  • 一种是在库缓存中找不到匹配的 Parent Cursor,此时 Oracle 会从头开始解析目标 SQL,新生成一个 Parent Cursor 和一个 Child Cursor,并把它们挂在对应的 Hash Bucket 中;

  • 另外一种是找到了匹配的 Parent Cursor 但未找到匹配的 Child Cursor,此时 Oracle 也会从头开始解析该目标 SQL,新生成一个 Child Cursor,并把这个 Child Cursor 挂在对应的 Parent Cursor 下。

过多的硬解析会导致系统性能受到严重影响,硬解析相关信息可查看系统 AWR 报告。

软解析

软解析(Soft Parse)是指 Oracle 在执行目标 SQL 时,在 Library Cache 中找到了匹配的 Parent Cursor 和 Child Cursor,并将存储在 Child Cursor 中的解析树和执行计划直接拿过来重用而无须从头开始解析的过程。

如果 OLTP 类型的系统在执行目标 SQL 时能够广泛使用软解析,则系统的性能和可扩展性就会比全部使用硬解析时有显著的提升,执行目标 SQL 时需要消耗的系统资源(主要体现在 CPU 上)也会显著降低。

Oracle 里的 Session Cursor

Session Cursor 的含义

Oracle 数据库里第二种类型的 Cursor 就是 Session Cursor,它是当前 Session 解析和执行 SQL 的载体,换句话说,Session Cursor 用于在当前 Session 中解析和执行 SQL

和 Shared Cursor 一样,Session Cursor 也是 Oracle 自定义的一种 C 语言复杂结构,它也是以哈希表的方式缓存起来的,只不过缓存在 PGA 中,而不是像 Shared Cursor 那样缓存在 SGA 的库缓存(Library Cache)里。

关于 Session Cursor,有以下几点需要注意。

  • Session Cursor 与 Session 是一 一对应的,不同 Session 的 Session Cursor 之间没法共享,这与 Shared Cursor 有本质区别;
  • Session Cursor 是有生命周期的,每个 Session Cursor 在使用的过程中都至少会经历一次 Open、Parse、Bind、Execute、Fetch 和 Close 中的一个或多个阶段,用过的 Session Cursor 不一定会缓存在对应 Session 的 PGA 中,这取决于参数 SESSION_CACHED_CURSORS 的值是否大于 0;
  • 既然 Session Cursor 也是以哈希表的方式缓存在 PGA 中,意味着 Oracle 会通过相关的哈希运算来存储和访问在当前 Session 的 PGA 中的对应 Session Cursor。这种访问机制实际上和 Shared Cursor 是一样的,即可以简单地认为 Oracle 是根据目标 SQL 的 SQL 文本的哈希值去 PGA 中的相应 Hash Bucket 中查找匹配的 Session Cursor。

Oracle 可以通过 Session Cursor 找到对应的 Parent Cursor,进而就可以找到对应的 Child Cursor 中目标 SQL 的解析树和执行计划,然后 Oracle 就可以按照这个解析树和执行计划来执行目标 SQL 了。

Session Cursor 的相关参数

  • OPEN_CURSORS

参数 OPEN_CURSORS 用于设定单个 Session 中同时能够以 Open 状态并存的 Session Cursor 的总数。

SQL> conn scott/tiger@localhost/orcl
Connected to Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 
Connected as scott@localhost/orcl

SQL> show parameter open_cursors;
NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
open_cursors                         integer     300

从上述查询结果可知,OPEN_CURSORS 的值为 300,意味着在这个库里,单个 Session 中同时能够以 Open 状态并存的 Session Cursor 的总数不能超过 300,否则 Oracle 会报错 “ORA-1000: maximum open cursors exceeded”。

  • SESSION_CACHED_CURSOR

参数 SESSION_CACHED_CURSOR 用于设定单个 Session 中能够以 Soft Closed 状态并存的 Session Cursor 的总数,即用于设定单个 Session 能够缓存在 PGA 中的 Session Cursor 的总数。

SQL> show parameter session_cached_cursors;
NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
session_cached_cursors               integer     50

从上述查询结果可知,SESSION_CACHED_CURSOR 的值为 50,意味着在这个库里,单个 Session 中同时能够以 Soft Closed 状态缓存在 PGA 中的 Session Cursor 的总数不能超过 50。

(1)如果参数 SESSION_CACHED_CURSOR 的值等于 0,那么 Session Cursor 就会正常执行 Close 操作。这样,当目标 SQL 再次重复执行时,此时是可以找到匹配的 Shared Cursor 的,但依然找不到匹配的 Session Cursor(因为之前硬解析时对应的 Session Cursor 已经被 Close 掉了),这意味着 Oracle 还必须为该 SQL 新生成一个 Session Cursor,并且该 Session Cursor 还会再经历一次 Open、Parse、Bind、Execute、Fetch 和 Close 中的一个或多个阶段;

(2)如果参数 SESSION_CACHED_CURSOR 的值大于 0,那么当满足一定的额外条件(SQL 解析和执行计划的次数要超过三次)时 Oracle 就不会对 Session Cursor 执行 Close 操作,而是会将其标记为 Soft closed,同时将其缓存在当前 Session 的 PGA 中。这样做的好处是,当目标 SQL 再次被重复执行时,此时 Shared Cursor 和 Session Cursor 就都能够找到匹配记录了,这意味着 Oracle 已经不需要为该 SQL 再新生成一个 Session Cursor,而是只需要从当前 Session 的 PGA 中将之前已经被标记为 Soft closed 的匹配 Session Cursor 直接拿过来用就可以了。

和第一种方式(软解析)相比,此时 Oracle 就省掉了 Open 一个新的 Session Cursor 所需要耗费的资源和时间。另外,Close 一个现有 Session Cursor 也不需要做了(只需要将其标记为 Soft Closed,同时将其缓存在当前 Session 的 PGA 中就可以了)。当然,剩下的 Parse、Bind、Execute、Fetch 还是需要做的,这个过程就是所谓的 “软软解析”。

在 Oracle 11gR2 中,一个 Session Cursor 能够被缓存在 PGA 中的必要条件是该 Session Cursor 所对应的 SQL 解析和执行计划的次数要超过 3 次。

下面开启两个 Session(分别标记为 Session1 和 Session2),在 Session1 中反复执行目标 SQL 语句 “select count(*) from emp”,在 Session2 中通过视图 V$OPEN_CURSOR 来观察 Session1 对应的 Session Cursor 的缓存情况:

先在 Session1 中第一次执行上述目标 SQL。

--Session1
SQL> select sid from v$mystat where rownum < 2;

       SID
----------
       129

SQL> select count(*) from emp;

  COUNT(*)
----------
        14

从 Session2 的如下查询结果可以看出,当目标 SQL 第一次执行完毕后,Oracle 并没有将其对应的 Session Cursor 缓存在 PGA 中。

--Session2
SQL> select sql_text, cursor_type from v$open_cursor where user_name = 'SCOTT' and sid = 129 and sql_text like 'select count(*) from emp%';

SQL_TEXT                       CURSOR_TYPE
------------------------------ --------------------

然后在 Session1 中第二次执行目标 SQL。

--Session1
SQL> select count(*) from emp;

  COUNT(*)
----------
        14

从 Session2 的如下查询结果可以看出,当目标 SQL 第二次执行完毕后,Oracle 也没有将其对应的 Session Cursor 缓存在 PGA 中。

--Session2
SQL> select sql_text, cursor_type from v$open_cursor where user_name = 'SCOTT' and sid = 129 and sql_text like 'select count(*) from emp%';

SQL_TEXT                       CURSOR_TYPE
------------------------------ --------------------

然后在 Session1 中第三次执行目标 SQL。

--Session1
SQL> select count(*) from emp;

  COUNT(*)
----------
        14

从 Session2 的如下查询结果可以看出,当目标 SQL 第三次执行完毕后,Oracle 虽然已经将其对应的 Session Cursor 缓存在 PGA 中了,但缓存的 Session Cursor 的类型为 DICTIONARY LOOKUP CURSOR CACHED,这个类型不正确。

--Session2
SQL> select sql_text, cursor_type from v$open_cursor where user_name = 'SCOTT' and sid = 129 and sql_text like 'select count(*) from emp%';

SQL_TEXT                       CURSOR_TYPE
------------------------------ ----------------------------------------
select count(*) from emp       DICTIONARY LOOKUP CURSOR CACHED

最后在 Session1 中第四次执行目标 SQL。

--Session1
SQL> select count(*) from emp;

  COUNT(*)
----------
        14

从 Session2 的如下查询结果可以看出,当目标 SQL 第四次执行完毕后,Oracle 已经将其对应的 Session Cursor 缓存在 PGA 中了,而且此时缓存的 Session Cursor 的类型为 SESSION CURSOR CACHED,这个类型才是我们想要缓存的 Session Cursor 类型。

--Session2
SQL> select sql_text, cursor_type from v$open_cursor where user_name = 'SCOTT' and sid = 129 and sql_text like 'select count(*) from emp%';

SQL_TEXT                       CURSOR_TYPE
------------------------------ ----------------------------------------
select count(*) from emp       SESSION CURSOR CACHED

Session Cursor 的种类有用法

Oracle 数据库里的 Session Cursor 又细分为三种类型,分别是隐式游标(Implicit Cursor)、显示游标(ExpLicit Cursor)和参考游标(Ref Cursor)。

关于这三种游标的使用方法详见博客 Oracle 游标

Shared Cursor 和 Session Cursor 的关系

在 Oracle 里面,Shared Cursor 和 Session Cursor 之间的关联关系如下:

  • 无论是硬解析、软解析还是软软解析,Oracle 在解析和执行目标 SQL 时,始终会先去当前 Session 的 PGA 中寻找是否存在匹配的缓存 Session Cursor
  • 如果在当前 Session 的 PGA 中找不到匹配的缓存 Session Cursor,Oracle 就会去库缓存(Library Cache)中查找是否存在匹配的 Parent Cursor。如果找不到,Oracle 就会新生成一个 Session Cursor 和一对 Shared Cursor(即 Parent Cursor 和 Child Cursor);如果找到了匹配的 Parent Cursor,但找不到匹配的 Child Cursor,Oracle 就会新生成一个 Session Cursor 和一个 Child Cursor(这个 Child Cursor 会被挂在之前找到的匹配 Parent Cursor 下)。无论哪一种情况,这两个过程对应的都是硬解析
  • 如果在当前 Session 的 PGA 中找不到匹配的缓存 Session Cursor,但在库缓存中找到了匹配的 Parent Cursor 和 Child Cursor,则 Oracle 会新生成一个 Session Cursor 并重用刚刚找到的匹配 Parent Cusror 和 Child Cursor,这个过程对应的就是软解析
  • 如果在当前 Session 的 PGA 中找到了匹配的缓存 Session Cursor,此时就不再需要新生成一个 Session Cursor,并且也不再需要像软解析那样得去库缓存中查找匹配的 Parent Cursor 了,因为 Oracle 此时可以重用找到的匹配 Session Cursor,并且可以通过这个 Session Cursor 直接访问该 SQL 对应的 Parent Cusror 和 Child Cursor,这个过程就是软软解析

参考资料

《基于 Oracle 的 SQL 优化 -- 崔华》

原创文章,转载请注明出处:http://www.opcoder.cn/article/45/