PL/SQL提供了不同的方法来执行SQL语句。主要的两个类别是静态SQL和动态SQL。动态SQL能够进一步分成三个子类别:EXECUTEI
MMEDIATE、OPEN/FETCH/CLOSE以及dbms_sql包。唯一与解析相关的功能是它们都可以使用绑定变量。而实际上,只有重用语句和缓存客户端语句的部分可以使用。它们并不是对所有SQL语句类别都有效。
注意鉴于PL/SQL是在数据库引擎中运行的,讨论客户端语句缓存好像有点奇怪。其实,从SQL引擎的视角来看,PL/SQL引擎就是一个客户端。在这个客户端中,客户端语句缓存的概念将在这里实现
在本节例子中提供的PL/SQL代码块来自ParsingTest1.sql、ParsingTest2.sql以及ParsingTest3.sql脚本,分别实现测试案例1、2和3。
1.1.1.1 静态SQL
静态SQL被集成到PL/SQL语言中。就像它的名称一样它是静态的,因此,在PL/SQL编译期间SQL语句必须是完全已知的。出于这个原因,如果一条SQL语句引用了PL/SQL变量,则不可避免地要使用绑定变量。例如,不可能使用静态SQL写出一段代码来重现测试案例1。
编写静态SQL有两种方式。第一种是基于隐式游标,但它没有控制游标生命周期的能力。下面的PL/SQL代码块实现了测试案例2:
DECLARE
l_pad VARCHAR2(4000);
BEGIN
FOR
i IN 1..10000
LOOP
select pad INTO l_pad FROM t WHERE val = i;
END
LOOP;
END;
/
第二种方式是基于显式游标。在这种情况下,可以对游标进行某些控制。不管怎样,打开/解析/执行阶段被合并成一个单独的操作(OPEN)。这意味着仅可以控制提取和关闭阶段。下面的PL/SQL代码块实现了测试案例2:
DECLARE
CURSOR c (p_val NUMBER) IS SELECT pad FROM t WHERE val = p_val;
l_pad VARCHAR2(4000);
BEGIN
FOR
i IN 1..10000
LOOP
OPEN c(i);
FETCH c INTO l_pad;
CLOSE c;
END
LOOP;
END;
/
尽管这两种方式都防止了不良代码(测试案例1),但它们也不允许写出特别高效的代码(测试案例3)。这是因为没有完全控制游标。但从性能的角度看,这两种方法是类似的。
为了解决这个问题,可以使用客户端语句缓存。缓存游标的最大数量由session_cached_cursors初始化参数决定。在10.2版本中,默认的缓存游标数量是20,而从11.1版本开始是50。这个初始化参数,并不与客户端语句缓存直接相关,而是“错误”地配置了它!事实上,这与用于控制服务器端语句缓存的初始化参数是同一个。
1.1.1.2 本地动态SQL:EXECUTE IMMEDIATE
从游标管理的角度来看,基于EXECUTE
IMMEDIATE的本地动态SQL与使用隐式游标的静态SQL类似。换句话说,它不能控制游标的生命周期。下面的PL/SQL代码块实现了测试案例2:
DECLARE
l_pad VARCHAR2(4000);
BEGIN
FOR
i IN 1..10000
LOOP
EXECUTE
IMMEDIATE 'SELECT pad FROM t WHERE val = :1' INTO l_pad USING i;
END
LOOP;
END;
/
没有了对游标的控制,不可能写出实现测试案例3的代码。出于这个原因,可以像静态SQL那样使用客户端游标缓存。
1.1.1.3 本地动态SQL:OPEN/FETCH/CLOSE
从游标管理的角度来看,基于OPEN/FETCH/CLOSE的本地动态SQL与使用隐式游标的静态SQL类似。换句话说,它仅能控制提取(FETCH)阶段。下面的PL/SQL代码块实现了测试案例2:
DECLARE
TYPE t_cursor IS REF CURSOR;
l_cursor t_cursor;
l_pad VARCHAR2(4000);
BEGIN
FOR
i IN 1..10000
LOOP
OPEN l_cursor FOR 'SELECT pad FROM t WHERE val = :1' USING i;
FETCH l_cursor INTO l_pad;
CLOSE l_cursor;
END
LOOP;
END;
/
没有对游标的完全控制,不可能写出实现测试案例3的代码。此外,使用基于OPEN/FETCH/CLOSE的动态SQL,数据库引擎无法利用客户端语句缓存。这意味着要解决这种代码引起的解析问题的唯一途径,是使用EXECUTE IMMEDIATE或dbms_sql包对语句进行改写。作为一种变通方案,还可以考虑服务器端语句缓存。
1.1.1.4 本地动态SQL:dbms_sql包
dbms_sql包提供对游标的生命周期的完全控制。在下面的PL/SQL代码块中(测试案例2),请注意显式编码的每一步:
DECLARE
l_cursor INTEGER;
l_pad VARCHAR2(4000);
l_retval INTEGER;
BEGIN
FOR
i IN 1..10000
LOOP
l_cursor := dbms_sql.open_cursor;
dbms_sql.parse(l_cursor, 'SELECT pad FROM t WHERE val = :1', 1);
dbms_sql.define_column(l_cursor, 1, l_pad, 10);
dbms_sql.bind_variable(l_cursor, ':1', i);
l_retval := dbms_sql.execute(l_cursor);
IF dbms_sql.fetch_rows(l_cursor) > 0
THEN
NULL;
END IF;
dbms_sql.close_cursor(l_cursor);
END
LOOP;
END;
/
因为可以完全控制游标,实现测试案例3就没有问题了。下面的PL/SQL代码块展示了这样的例子。注意,为了避免不必要的软解析,准备游标(open_cursor、parse、define_column)和关闭游标(close_cursor)的过程被放置到循环外。
DECLARE
l_cursor INTEGER;
l_pad VARCHAR2(4000);
l_retval INTEGER;
BEGIN
l_cursor := dbms_sql.open_cursor;
dbms_sql.parse(l_cursor, 'SELECT pad FROM t WHERE val = :1', 1);
dbms_sql.define_column(l_cursor, 1, l_pad, 10);
FOR
i IN 1..10000
LOOP
dbms_sql.bind_variable(l_cursor, ':1', i);
l_retval := dbms_sql.execute(l_cursor);
IF dbms_sql.fetch_rows(l_cursor) > 0
THEN
NULL;
END IF;
END
LOOP;
dbms_sql.close_cursor(l_cursor);
END;
/
使用dbms_sql包时,数据库引擎无法利用客户端语句缓存。所以,为了优化一个有太多软解析的应用程序(如测试案例2),必须修改它以重用游标(如测试案例3)。作为一个权宜方案,可以考虑服务器端语句缓存。