除非迫不得已不要用游标

        不管是sql server 还是 oracle都提供了游标,其一般用途就是从查询结构中遍历数据。游标很好用,许多初学oracle或者sql server的程序员都喜欢用,然而如果滥用游标的话,对程序的性能会造成很大的影响。我认为:除非不得已,不要使用游标。下面是我的一次优化经历。
问题:
        在我们的系统中,需要管理产品模型,对PDM了解的人知道,其实产品模型可以当作一个特殊的零部件来处理,所以在数据表的时候产品模型与零部件使用相同的数据表来存放数据,只不过是用一个flag来标示是零部件还是产品模型。产品模型具有版本。现在的要求的是查出最新的所有的零部件信息,查询的时候允许用户自定义查询条件。
        用户自定义查询条件是从界面上通过各种选项来构造的,因此传递的是动态SQL语句,为了提高效率数据库上建立了一个存储过程专门来查询产品模型。下面是一个初学oracle不久的编程人员编写的存储过程:

 1PROCEDURE GETPRODMODELBYSQL(p_sql        IN VARCHAR2,                 
 2                               cur_mi_prodmodel OUT SYS_BASE.BASECURSOR) AS
 3   sqlstr varchar2(1000);
 4   idstr  varchar2(1000);
 5   
 6   
 7   dis_id number;
 8   maxver number;
 9        cursor disid_curs is
10              select distinct pt_id
11              from mi_part
12              where pt_flag = 2;                               
13   BEGIN
14    open disid_curs;
15         loop
16             fetch disid_curs
17             into dis_id;
18             exit when disid_curs% notfound;
19             
20             select max(pv_id)
21             into maxver
22             from mi_partver
23             where pv_partid = dis_id;
24             
25             if maxver is null then
26                 idstr := idstr;
27             else
28                 idstr := idstr || maxver || ',';
29             end if;
30          end loop;
31
32          idstr  := rtrim(idstr, ',');
33          idstr  := ltrim(idstr, ',');
34          if idstr is null then
35              sqlstr := p_sql || ' ORDER BY PV_PRODUCTID,PT_PARTNUMBER';
36          else
37              sqlstr := p_sql || ' and MI_PARTVER.pv_id in (' || idstr || ')  ORDER BY PV_PRODUCTID,PT_PARTNUMBER';
38           end if;
39
40        open cur_mi_prodmodel for sqlstr;
41   END GETPRODMODELBYSQL;

这个存储过程可以正常工作,但是其速度执行起来非常慢,如果数据表中有很多数据的话,其速度很可能是直线下降,究其原因是因为使用游标来遍历然后构造查询语句。对这个存储过程进行优化:
 1PROCEDURE GETPRODMODELBYSQL(p_sql        IN VARCHAR2,                 
 2                               cur_mi_prodmodel OUT SYS_BASE.BASECURSOR) AS
 3   s varchar2(300);
 4   sqlstr varchar2(1000);
 5   /*idstr  varchar2(1000);
 6   
 7   
 8   dis_id number;
 9   maxver number;
10        cursor disid_curs is
11              select distinct pt_id
12              from mi_part
13              where pt_flag = 2;  */
                             
14   BEGIN
15    /*open disid_curs;
16         loop
17             fetch disid_curs
18             into dis_id;
19             exit when disid_curs% notfound;
20             
21             select max(pv_id)
22             into maxver
23             from mi_partver
24             where pv_partid = dis_id;
25             
26             if maxver is null then
27                 idstr := idstr;
28             else
29                 idstr := idstr || maxver || ',';
30             end if;
31          end loop;*/

32          s:=' and MI_PARTVER.pv_id in (select a.pv_id from mi_partver a,mi_part c where a.pv_partid = c.pt_id and c.pt_flag =2 and
33a.pv_id in (select e.pv_id from mi_partver e
34where e.pv_id in (select max(b.pv_id) from mi_partver b where e.pv_partid = b.pv_partid)))';
35          /*idstr  := rtrim(idstr, ',');
36          idstr  := ltrim(idstr, ',');
37          if idstr is null then
38              sqlstr := p_sql || ' ORDER BY PV_PRODUCTID,PT_PARTNUMBER';
39          else
40              sqlstr := p_sql || ' and MI_PARTVER.pv_id in (' || idstr || ')  ORDER BY PV_PRODUCTID,PT_PARTNUMBER';
41           end if;*/

42              sqlstr := p_sql || s ||  'ORDER BY PV_PRODUCTID,PT_PARTNUMBER';
43        open cur_mi_prodmodel for sqlstr;
44   END GETPRODMODELBYSQL;

这里的有效语句是使用查询语句来构造查询,其结果是速度得到了数量级的提高,而且避免了一个隐患就是缓冲区溢出,因为原来的存储过程中定义了几个varchar2(1000)的变量,实际上这个变量的上界是不可以预期的,而缓冲区溢出也是很多初学oracle的程序员经常忽略甚至想不到的问题。
0
0
(请您对文章做出评价)
« 上一篇:作软件的人永远都是受气包吗?
» 下一篇:HowTO: Create an Event Log Source in code, without the Permission errors
posted @ 2006-12-05 21:06 释子 阅读(2479) 评论(27)  编辑 收藏 网摘 所属分类: ORACLE技术

  回复  引用  查看    
#1楼2006-12-05 21:14 | 小陆      
在记录集上定义一个游标以后,在游标的生命周期内,一旦这个表发生了变动,为了解决冲突,数据库会把所有变动的内容必须写到临时表空间内。一旦游标生命周期较长,很容易将临时表空间用完,导致数据库发生错误。
存储过程做业务逻辑一定要慎重,让数据库既承担数据存储的重任,又要运行业务规则,时间长了,随着数据越来越多,需求越做越复杂,矛盾会渐渐的暴露出来,意想不到的问题会大量出现。

  回复  引用    
#2楼2006-12-05 21:29 | 关门放狗[未注册用户]
如果能把修改后语句里面的in子句用inner join来改写,将会带来更好的性能,也是数量级的提升
  回复  引用  查看    
#3楼2006-12-05 23:07 | aspnetx      
问一下楼主多大岁数?
  回复  引用  查看    
#4楼2006-12-06 07:29 | Henry Liang      
楼上的要征婚?^^
  回复  引用  查看    
#5楼2006-12-06 09:04 | 飞不动了      
我一般很少用游标,凡是用游标的地方用临时表都可以实现的!
  回复  引用  查看    
#6楼[楼主]2006-12-06 09:58 | 皇帝的新装      
@aspnetx
??

  回复  引用  查看    
#7楼[楼主]2006-12-06 11:16 | 皇帝的新装      
@关门放狗
请指教。

  回复  引用    
#8楼2006-12-06 12:02 | 关门放狗[未注册用户]
举例来说,在下面语句
s:=' and MI_PARTVER.pv_id in (select a.pv_id from mi_partver a,mi_part c where a.pv_partid = c.pt_id and c.pt_flag =2 and
33a.pv_id in (select e.pv_id from mi_partver e
34where e.pv_id in (select max(b.pv_id) from mi_partver b where e.pv_partid = b.pv_partid)))';

中的第一个in部分,我们可以把后面改写为如下:
select a.pv_id
from mi_partver a
inner join mi_part c
on a.pv_partid = c.pt_id and c.pt_flag=2
inner join
(select e.pv_id from mi_partver e
inner join (select pv_partid, max(pv_id) from mi_partver group by pv_partid) b
on e.pv_partid = b.pv_partid) t
on a.pv_id=t..pv_id

其实还可以和前面的考虑使用inner join而不使用in,对于每一个in来说,你的记录越多,子查询就越多,非不得已,我们一般都避免使用in,比如你有1000条记录,那么就要做1000次in里面的子查询,比如上面改过的这个sql,只需要做3次查询,所以还是有一定帮助的。上面的语句不能保证100%正确,没有测试过,但是大体意思就是这些。


  回复  引用  查看    
#9楼[楼主]2006-12-06 12:12 | 皇帝的新装      
@关门放狗
谢谢!我测试看看

  回复  引用  查看    
#10楼[楼主]2006-12-06 12:32 | 皇帝的新装      
@关门放狗
果然要快很多,再次感谢!

  回复  引用    
#11楼2008-12-19 15:37 | 黄昌楠[未注册用户]
优化后感觉失去存储过程的含义了。
和直接跑SQL 有什么区别? 请赐教

  回复  引用    
#12楼2008-12-19 15:39 | 黄昌楠[未注册用户]
主题不够和谐 ^_^

给人感觉游标是个不稳定的炸弹...

  回复  引用    
#13楼2008-12-19 20:45 | 查[未注册用户]
--引用--------------------------------------------------
黄昌楠: 主题不够和谐 ^_^

给人感觉游标是个不稳定的炸弹...
--------------------------------------------------------
--引用--------------------------------------------------
黄昌楠: 优化后感觉失去存储过程的含义了。
和直接跑SQL 有什么区别? 请赐教

--------------------------------------------------------
存储过程应该是什么样?