您的位置:首页 > 数据库 > Oracle

oracle优化学习笔记

2009-04-23 17:18 393 查看
关于in,exists,表连接
我在看oracle性能优化一书时,讲到用exists替代in,用表链接替代exists,关于前者,一般效果比较明显,exists效率明显比in高,但是如果要想表连接的效率比exists高,必须在from子句中,将记录多的表放在前面,记录少的表放后面.
假设数据表ta是数据量大的表(500万条),tb数据量较小的表(1万条),两表以id字段相关联,tb表的一条记录对应ta表的多条记录。现在统计时间2008.2.1以来ta表的记录数,由于只有tb表才有记录时间字段,故采用联合查询。

方法1,使用in:
select count(*) from ta where id in( select id from tb where dt >'20080201');
方法2,使用exists:
select count(*) from ta where exists( select 'X' from tb where tb.id=ta.id and dt >'20080201');
方法3a,使用表连接
select count(*) from tb, ta where tb.id=ta.id and dt >'20080201';
方法3b,使用表连接
select count(*) from ta, tb where tb.id=ta.id and dt >'20080201';
在实验过程中,方法2效率明显比方法1要高,但方法3a效率尽管比方法1要高

,却比方法2要低很多。后来,将from字句中表名位置换了一下,变成方法3b,意外的发现,效率远远高于方法2,相对方法1就更不用说了。

其它优化事项:

1. 带通配符(%)的like语句一般能用到索引,但当%出现在搜索词的最前面是,无法使用索引。

2. 在where语句中对列的任何操作(函数,计算,拼接等)都会使用该列上的索引失效。

3. is null或is not null 无法使用索引。

4. 任何在Order by语句的非索引项或者有计算表达式都将降低查询速度。绝对避免在order by子句中使用表达式。

5. 使用and替代不等于(<>, !=),不等于操作无法使用索引:如 ......where seq<>3000; 应改写为:......where seq>3000 and seq<3000;

6. 应尽可能使用NOT EXISTS来代替NOT IN,尽管二者都使用了NOT(不能使用索引而降低速度),但NOT EXISTS要比NOT IN查询效率更高。

关于select/fetch... bulk collect into ... limit ...句型
在使用如上句型时,通常我们都会用for或forall循环批量进行insert/update/delete操作。for/forall循环方法有好几种,如 :
方法一:
declare
type p_tb_bat_dtl is table of tb_bat_dtl%rowtype;
v_tb_bat_dtl p_tb_bat_dtl ;
v_bat_dtl p_tb_bat_dtl ;
cursor cur_tb_batch_dtl is
select * from tb_bat_dtl;
begin
open cur_tb_batch_dtl;
fetch cur_tb_batch_dtl bulk collect into v_tb_bat_dtl;
for i in v_tb_bat_dtl.FIRST..v_tb_bat_dtl.LAST loop
delete from tb_bat_dtl where bat_id=v_tb_bat_dtl(i).bat_id ;
end loop;
close cur_tb_batch_dtl;
end;
declare
type p_bat_id is table of tb_bat_dtl.bat_id%type;
v_bat_id p_bat_id ;
cursor cur_tb_batch_dtl is
select bat_id from tb_bat_dtl;
begin
open cur_tb_batch_dtl;
fetch cur_tb_batch_dtl bulk collect into v_bat_id;
forall i in v_bat_id.FIRST .. v_bat_id.LAST
delete from tb_bat_dtl where bat_id=v_bat_id(i) ;
close cur_tb_batch_dtl;
end;
方法二:
declare
type p_tb_bat_dtl is table of tb_bat_dtl%rowtype;
v_tb_bat_dtl p_tb_bat_dtl ;
cursor cur_tb_batch_dtl is
select * from tb_bat_dtl;
begin
open cur_tb_batch_dtl;
fetch cur_tb_batch_dtl bulk collect into v_tb_bat_dtl;
for i in 1..v_tb_bat_dtl.count loop
delete from tb_bat_dtl where bat_id=v_tb_bat_dtl(i).bat_id ;
end loop;
close cur_tb_batch_dtl;
end;
declare
type p_bat_id is table of tb_bat_dtl.bat_id%type;
v_bat_id p_bat_id ;
cursor cur_tb_batch_dtl is
select bat_id from tb_bat_dtl;
begin
open cur_tb_batch_dtl;
fetch cur_tb_batch_dtl bulk collect into v_bat_id;
forall i in 1..v_bat_id.count
delete from tb_bat_dtl where bat_id=v_bat_id(i) ;
close cur_tb_batch_dtl;
end;


上面的第1种方法有一个致命的弱点,就是在select/fetch... bulk collect into ... limit ...没有取到数据时,如果没有exit,则第一种方法会报错:ORA-06502: PL/SQL: numeric or value error。因为tmp.FIRST访问的对象不存在,为空值。必须对错误进行错误处理。 第2种方法不会出现这种问题。

这种语法对海量数据的操作是极为有利的,比如下面就是一种使用绑定变量的批量删除数据的存储过程:

PROCEDURE DelRrds
AS
type RowIdArray is table of rowid index by binary_integer;
rowIds RowIdArray;
BEGIN
loop
select rowid BULK COLLECT into rowIds from tb_bat_dtl
where bat_id like '0625%' and rownum < 1001;
exit when SQL%NOTFOUND;
forall i in 1 .. rowIds.COUNT
delete from tb_bat_dtl where rowid = rowIds(i);
commit;
end loop;
EXCEPTION
when OTHERS then
rollback;
END DelRrds;


上面的1000条是一个可以设定的数,根据你的服务器性能可以扩大或缩小。

关于用exit when...跳出循环

记住,通常情况下,exit when... 只跳出当前层的循环,与其它程序设计语言的break语句类似。一直弄不明白为什么网上老是看到有人说“exit语句会退出整个pl/sql或所有循环,不知道如何跳出嵌套的循环”。在嵌套的循环中,如果要直接从内层循环跳出外面多层的循环,可使用'exit 标签 when...'形式的语句,举例如下:

SQL>   BEGIN
2          <<outerloop>>
3          FOR v_outerloopcounter IN 1..2 LOOP
4               <<innerloop>>
5               FOR v_innerloopcounter IN 1..4 LOOP
6                    DBMS_OUTPUT.PUT_LINE('Outer Loop counter is '
7                         || v_outerloopcounter ||
8                         ' Inner Loop counter is ' || v_innerloopcounter);
9                         EXIT WHEN v_innerloopcounter = 3;
10              END LOOP innerloop;
11         END LOOP outerloop;
12    END;
13    /
Outer Loop counter is 1 Inner Loop counter is 1
Outer Loop counter is 1 Inner Loop counter is 2
Outer Loop counter is 1 Inner Loop counter is 3
Outer Loop counter is 2 Inner Loop counter is 1
Outer Loop counter is 2 Inner Loop counter is 2
Outer Loop counter is 2 Inner Loop counter is 3

PL/SQL procedure successfully completed.
SQL>   BEGIN
2          <<outerloop>>
3          FOR v_outerloopcounter IN 1..2 LOOP
4               <<innerloop>>
5               FOR v_innerloopcounter IN 1..4 LOOP
6                    DBMS_OUTPUT.PUT_LINE('Outer Loop counter is '
7                         || v_outerloopcounter ||
8                         ' Inner Loop counter is ' || v_innerloopcounter);
9                         EXIT outerloop WHEN v_innerloopcounter = 3;
10              END LOOP innerloop;
11         END LOOP outerloop;
12    END;
13    /
Outer Loop counter is 1 Inner Loop counter is 1
Outer Loop counter is 1 Inner Loop counter is 2
Outer Loop counter is 1 Inner Loop counter is 3

PL/SQL procedure successfully completed.


符号<<标签名>>用来标识一个标签的开始。从上面可以看出,普通情况下,exit when... 只跳出当前层的循环。 用exit 标签 when...则跳出了外层的循环。

关于游标的属性

游标有四个属性%isopen,%found, %notfound, %rowcount.在普通的fetch...into...句型中,每次取单条记录,这四个属性均能正常工作,不会出现意外情况。但是若用在句型fetch...bulk collect into...limint...中,千万要注意,不要随便使用%found, %notfound, %rowcount三个属性。比如:

declare
type RowIdArray is table of rowid index by binary_integer;
rowIds RowIdArray;
cursor mycur is select rowid from tb_bat_dtl;
BEGIN
open mycur;
loop
fetch mycur BULK COLLECT into rowIds limit 1000;
exit when mycur%NOTFOUND;
forall i in 1 .. rowIds.COUNT
delete from tb_bat_dtl where rowid = rowIds(i);
commit;
end loop;
close mycur;
EXCEPTION
when OTHERS then
rollback;
END ;
上面的例子的意图是循环每次从游标中取1000行删除,如果没有取到数据,则跳出循环。但实际上,exit when mycur%NOTFOUND 无法达到目的。在fetch...bulk collect into...limint...句型中,只有fetch到的记录数大于等于limit设定的值时,游标属性%NOTFOUND才为false,属性%rowcount值才不为0。在上面的例子中,若数据库中符合条件的数据记录数值不能整除1000,那么余数不足1000条的数据永远无法删除。解决这种问题的方法有几种:

1. 使用sql%notfound;

2. 使用sql%rowcount=0;

3. 使用集合的 COUNT属性。

上面1,2两种方法就是直接把 exit when mycur%NOTFOUND 替换成 exit when SQL%NOTFOUND或 exit when SQL%ROWCOUNT=0,第3种方法就是将 exit when mycur%NOTFOUND 替换成 exit when rowIds.COUNT=0. 具体如下:
declare
type RowIdArray is table of rowid index by binary_integer;
rowIds RowIdArray;
cursor mycur is select rowid from tb_bat_dtl;
BEGIN
open mycur;
loop
fetch mycur BULK COLLECT into rowIds limit 1000;
exit when rowIds.COUNT=0;
forall i in 1 .. rowIds.COUNT
delete from tb_bat_dtl where rowid = rowIds(i);
commit;
end loop;
close mycur;
END ;


另外一个关于%rowcount的误解,我们看下面一段代码:

begin
loop
update table_tmp set name='nana' where sex_cd ='1' and rownum <10000;
commit;
exit when (SQL%ROWCOUNT =0 );
end loop;
end;
可能一些人认为上面的代码没有问题,能将表table_tmp中所有符合条件的记录全新更新掉。但实际上,这段代码只有在表table_tmp中符合条件的记录数小于10000时,才是正确的。 因为在执行commit语句后,游标 %rowcount 的值始终是0,无法起到判断的作用。实际上,执行commit后,所有的公用游标都会被初始化。 因此,必须在commit前使用游标属性。我们只需将commit语句的位置调整一下就可以了。
begin
loop
update table_tmp set name='nana' where sex_cd ='1' and rownum <10000;
exit when (SQL%ROWCOUNT =0 );
commit;
end loop;
end;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: