接近DBMS核心
Closeness to the DBMS Kernel
代码的执行越接近DBMS 核心,则执行速度越快。数据库真正强大之处就在于此,例如,有些数据库管理产品支持扩展,你可以用C等较底层的语言为它编写新功能。用含有指针操作的底层语言有个缺点,即一旦指针处理出错会影响内存。仅影响到一个用户已很糟糕,何况数据库服务器(就像“服务器”名字所指的一样)出了问题会影响众多“用户”——服务器内存出了问题,所有使用这些数据的无辜的应用程序都会受影响。因此,DBMS 核心采取了负责任的做法,在沙箱(sandbox)环境中执行程序代码,这样,即使出了问题也不会影响到数据。例如,Oracle 在外部函数(external function)和它自身之间实现了一套复杂的通信机制,此机制在某些方面很像控制数据库连结的方法,以管理两个(或多个)服务器上的数据库实例之间的通信。到底采用PL/SQL 存储过程还是外部 C 函数,应综合比较后决定。如果精心编写外部 C 函数获得的好处超过了建立外部环境和上下文切换(context-switching)的成本,就应采用外部函数。但需要处理一个大数据量的表的每一行时,不要使用外部函数。这需要平衡考虑,解决问题时应完全了解备选策略的后果。
如要使用函数,始终应首选DBMS自带的函数。这不仅仅是为了避免无谓的重复劳动,还因为自带函数在执行时比任何第三方开发的代码更接近数据库核心,相应地其效率也会高出许多。
下面这个简单例子是用 Oracle SQL编写的,显示了 使用Oracle 函数所获得的效率。假设手工输入的文本数据可能包含多个相邻的“空格”,我们需要一个函数将多个空格
替换为一个空格。如果不采用Oracle Database 10g 开始提供的正规表达式(regular expression),函数代码将会是这样:
create or replace function squeeze1(p_string in varchar2)
return varchar2
is
v_string varchar2(512) := '';
c_char char(1);
n_len number := length(p_string);
i binary_integer := 1;
j binary_integer;
begin
while (i <= n_len)
loop
c_char := substr(p_string, i, 1);
v_string := v_string || c_char;
if (c_char = ' ')
then
j := i + 1;
while (substr(p_string || 'X', j, 1) = ' ')
loop
j := j + 1;
end loop;
i := j;
else
i := i + 1;
end if;
end loop;
return v_string;
end;
/
上述代码中的 'X' 在内层循环中被串接到字符串上,以避免超出字符串长度的测试。
还有别的方法消除多个空格,可以使用 Oracle 提供的字符串函数。以下为替代方案:
create or replace function squeeze2(p_string in varchar2)
return varchar2
is
v_string varchar2(512) := p_string;
i binary_integer := 1;
begin
i := instr(v_string, ' ');
while (i > 0)
loop
v_string := substr(v_string, 1, i)
|| ltrim(substr(v_string, i + 1));
i := instr(v_string, ' ');
end loop;
return v_string;
end;
/
还有第三种方法:
create or replace function squeeze3(p_string in varchar2)
return varchar2
is
v_string varchar2(512) := p_string;
len1 number;
len2 number;
begin
len1 := length(p_string);
v_string := replace(p_string, ' ', ' ');
len2 := length(v_string);
while (len2 < len1)
loop
len1 := len2;
v_string := replace(v_string, ' ', ' ');
len2 := length(v_string);
end loop;
return v_string;
end;
/
用一个简单的例子对上述三种方法进行测试,每个函数都能正确工作,且没有明显的性能差异:
SQL> select squeeze1('azeryt hgfrdt r')
2 from dual
3 /
azeryt hgfrdt r
Elapsed: 00:00:00.00
SQL> select squeeze2('azeryt hgfrdt r')
2 from dual
3 /
azeryt hgfrdt r
Elapsed: 00:00:00.01
SQL> select squeeze3('azeryt hgfrdt r')
2 from dual
3 /
azeryt hgfrdt r
Elapsed: 00:00:00.00
那么,如果每天要调用该空格替换操作几千次呢?我们构造一个接近现实负载的环境,下面的代码将建立一个用于测试的表并填入随机数据,已检测上面三个函数是否有性能差异:
create table squeezable(random_text varchar2(50))
/
declare
i binary_integer;
j binary_integer;
k binary_integer;
v_string varchar2(50);
begin
for i in 1 .. 10000
loop
j := dbms_random.value(1, 100);
v_string := dbms_random.string('U', 50);
while (j < length(v_string))
loop
k := dbms_random.value(1, 3);
v_string := substr(substr(v_string, 1, j) || rpad(' ', k)
|| substr(v_string, j + 1), 1, 50);
j := dbms_random.value(1, 100);
end loop;
insert into squeezable
values(v_string);
end loop;
commit;
end;
/
上面的脚本在测试表中建立了10 000条记录(决定 SQL 语句要执行多少次时,这是数字比较适中)。要执行该测试,运行下列语句:
select squeeze_func(random_text)
from squeezable;
我运行这个测试时,关闭了所有头信息(headers)和屏幕的显示。禁止输出可确保结果反映的是替换空格算法所花费的时间,而不是显示结果所花费的时间。这些语句会执行多次,以确保不受缓存(caching)的影响。
表2-2显示了在测试机上的运行结果。
表2-2:处理10 000条记录中空格所花的时间
|
函数 |
机制 |
时间 |
|
squeeze1 |
用 PL/SQL 循环处理字符 |
0.86 秒 |
|
squeeze2 |
Instr() + ltrim() |
0.48 秒 |
|
squeeze3 |
循环调用 replace() |
0.39 秒 |
尽管都在1秒内完成了10 000次调用,但 squeeze2的速度是squeeze1的1.8 倍,而 squeeze3 则是它的2.2 倍。为什么呢?原因很简单,因为SQL 函数比PL/SQL“离核心更近”。当函数只偶尔执行一次时,性能差异微乎其微,但在批处理程序或高负载的 OLTP 服务器中性能差异就非常明显。
总结:代码喜欢SQL内核——离核心越近,它就运行得越快。






