避免游标多次遍历

        游标在数据库领域被广泛使用,尤其是对于需要将SQL语句返回的数据集进行逐行处理的时候。这为数据处理提供了极大的便利性,然游标的不当
使用从某种程度上而言会降低数据库的性能。下面的是一个来自生产环境的实际例子,由于使用了参数游标,所以引发了多次遍历。

一、源代码

-->下面的procedure用于为trade生成一个新的contract_num,并将其更新到对应的记录
PROCEDURE assign_contract_num( businessdate_in trade_client_tbl.trade_date%TYPE, err_num OUT NUMBER, err_msg OUT VARCHAR2 ) IS
   myspname   CONSTANT VARCHAR2( 100 ) := 'bo_trd_dayend_pkg.assign_contract_num';
   debugpos            bo_common_pkg.debug_pos_type := 0;

	 -->注意此处的游标,此为参数游标,定义了两个输入参数一个是orderside,一个是inputdate
	 -->参数游标的参数应当作为SQL语句的过滤条件,如本例中的where子句的谓词信息
   CURSOR validtradedate( orderside_in trade_client_tbl.order_side%TYPE, inputdate_in trade_client_tbl.input_date%TYPE ) IS
      SELECT trade_date
      FROM   trade_client_tbl
      WHERE  is_valid_trade = 'Y' AND input_date = inputdate_in AND order_side = orderside_in AND contract_num IS NULL
      GROUP BY trade_date
      ORDER BY trade_date;

	 -->再次定义了一个参数游标,此时带有三个参数
   CURSOR validtradecur( orderside_in    trade_client_tbl.order_side%TYPE
                       , tradedate_in    trade_client_tbl.trade_date%TYPE
                       , inputdate_in    trade_client_tbl.input_date%TYPE ) IS
      SELECT grp_ref_id, order_side
      FROM   trade_client_tbl
      WHERE  is_valid_trade = 'Y' AND trade_date = tradedate_in AND input_date = inputdate_in
      AND order_side = orderside_in AND contract_num IS NULL
      GROUP BY grp_ref_id, order_side
      ORDER BY grp_ref_id;

   syscontract_idx     PLS_INTEGER;     -->声明变量
   syscontract_num     VARCHAR2( 20 );  -->声明变量
BEGIN
   err_num     := bo_common_pkg.c_suc_general;
   debugpos    := 5;

   -- *** Buy Trades Contract No. ***
   FOR trddaterec IN validtradedate( 'B', businessdate_in )  -->开始使用参数遍历游标
   LOOP
      BEGIN
         debugpos    := 10;

         SELECT MAX( contract_num )   -->求当月且类型为B,最大的contract_num号
         INTO   syscontract_num
         FROM   trade_client_vw
         WHERE  trade_date LIKE
                   SUBSTR( trddaterec.trade_date, 1, 6 )
                   || '%'
                AND order_side = 'B';

         IF syscontract_num IS NOT NULL THEN    -->当非空值时,调用函数从中取出contract_num的首部
            debugpos    := 15;
            syscontract_idx := extract_contract_num_idx( syscontract_num );
         ELSE
            syscontract_idx := 0;
         END IF;
      END;

      debugpos    := 20;

      FOR validtraderec IN validtradecur( 'B', trddaterec.trade_date, businessdate_in )  -->使用参数遍历第二个游标
      LOOP                                               -->注意此时的一个参数为上一个游标获得的结果集(trddaterec.trade_date)
         syscontract_idx :=                              -->此时syscontract_idx的值增加1
            syscontract_idx
            + 1;
         syscontract_num := format_contract_num( validtraderec.order_side, trddaterec.trade_date, syscontract_idx );-->格式化contract_num
         debugpos    := 25;
         DBMS_OUTPUT.put_line(   'B - '
                              || validtraderec.grp_ref_id
                              || ' - '
                              || syscontract_num );

         UPDATE trade_client_tbl                       -->将生成的syscontract_num更新到对应记录的contract_num
         SET    contract_num = syscontract_num
         WHERE  grp_ref_id = validtraderec.grp_ref_id;
      END LOOP;
   END LOOP;

   -- *** Sell Trades Contract No. ***  -->下面的这段代码则等同于上一个处理,所不同的是处理所有类型为S的记录
   FOR trddaterec IN validtradedate( 'S', businessdate_in )
   LOOP
      BEGIN
         debugpos    := 35;

         SELECT MAX( contract_num )
         INTO   syscontract_num
         FROM   trade_client_vw
         WHERE  trade_date LIKE
                   SUBSTR( trddaterec.trade_date, 1, 6 )
                   || '%'
                AND order_side = 'S';

         IF syscontract_num IS NOT NULL THEN
            debugpos    := 40;
            syscontract_idx := extract_contract_num_idx( syscontract_num );
         ELSE
            syscontract_idx := 0;
         END IF;
      END;

      debugpos    := 45;

      FOR validtraderec IN validtradecur( 'S', trddaterec.trade_date, businessdate_in )
      LOOP
         syscontract_idx :=
            syscontract_idx
            + 1;
         syscontract_num := format_contract_num( validtraderec.order_side, trddaterec.trade_date, syscontract_idx );
         debugpos    := 50;
         DBMS_OUTPUT.put_line(   'S - '
                              || validtraderec.grp_ref_id
                              || ' - '
                              || syscontract_num );

         UPDATE trade_client_tbl
         SET    contract_num = syscontract_num
         WHERE  grp_ref_id = validtraderec.grp_ref_id;
      END LOOP;
   END LOOP;

   -- Update ACC_POS_HIST_TBL contract num field.   -->最后将所有新生存的contract_num更新到历史记录表
   BEGIN
      UPDATE acc_pos_hist_tbl aph
      SET    aph.contract_num =
                (SELECT DISTINCT tc.contract_num
                 FROM   trade_client_vw tc
                 WHERE  aph.trans_num = tc.grp_ref_id )
      WHERE      aph.contract_num IS NULL
             AND aph.trans_type_cd LIKE 'TD%'
             AND EXISTS
                    (SELECT 1
                     FROM   trade_client_vw tc
                     WHERE  aph.trans_num = tc.grp_ref_id AND tc.contract_num IS NOT NULL);
   END;
END;

-->分析:
-->1.源代码中的主要目的是为表trade_client_tbl中新的trade生成contract_num
-->2.由于存在两种不同类型的order_side,一个为B(Buy),一个为S(Sell),所有使用了参数游标来完成
-->3.使用了两次嵌套循环(loop)
-->4.代码存在冗余现象。即仅仅是类型的不同而导致了游标需要多次的访问表trade_client_tbl

二、代码改进

-->通过上面的分析考虑将类型为B和S的进行合并处理,避免多次扫描原始表。(仅列出改进部分)
-->由于不能确保游标trade_date_cur得到的记录唯一,所以在此仍然使用了两次嵌套,但避免针对不同类型B和S单独处理。
-->两次游标仍然是原来的SQL语句。如果得到的trade_date唯一,则主循环仅循环一次,否则得到的trade_date多次则循环多次。
FOR trade_date_cur IN (SELECT trade_date
                       FROM   trade_client_tbl
                       WHERE  is_valid_trade = 'Y' AND input_date = businessdate_in AND contract_num IS NULL
                       GROUP BY trade_date
                       ORDER BY trade_date )
LOOP
   FOR trade_cur
      IN (SELECT grp_ref_id, order_side, trade_date
          FROM   trade_client_tbl
          WHERE  is_valid_trade = 'Y' AND input_date = businessdate_in AND contract_num IS NULL
          AND trade_date = trade_date_cur.trade_date
          GROUP BY grp_ref_id, trade_date, order_side
          ORDER BY 1, 2, 3 )
   LOOP
      v_counter   :=
               v_counter
               + 1;
      CASE                                          -->此处使用了case语句来进行区分order_side为B或S
         WHEN trade_cur.order_side = 'B' THEN       -->由于可知的是任意一条trade记录的order_side不是类型为B就是为S
            SELECT MAX( contract_num )              -->因此所需要解决的是根据类型的不同得到不同的最大的contract_num
            INTO   syscontract_num                  -->并在此基础之上来生成新的contract_num
            FROM   trade_client_vw
            WHERE  trade_date LIKE                  -->实际上这个谓词trade_date还可以缩小范围,因为每一天生成的首部是不同的
                      SUBSTR( trade_cur.trade_date, 1, 6 )
                      || '%'
                   AND order_side = 'B';
         ELSE
            SELECT MAX( contract_num )
            INTO   syscontract_num
            FROM   trade_client_vw
            WHERE  trade_date LIKE
                      SUBSTR( trade_cur.trade_date, 1, 6 )
                      || '%'
                   AND order_side = 'S';
      END CASE;

      IF syscontract_num IS NOT NULL THEN
         --   debugpos := 15;
         syscontract_idx := extract_contract_num_idx( syscontract_num );
      ELSE
         syscontract_idx := 0;
      END IF;

      syscontract_idx :=
         syscontract_idx
         + 1;
      syscontract_num := format_contract_num( trade_cur.order_side, trade_cur.trade_date, syscontract_idx );
      DBMS_OUTPUT.put_line( 'Current syscontract_num is '
                                 || syscontract_num );
      UPDATE trade_client_tbl
      SET    contract_num = syscontract_num
      WHERE  grp_ref_id = trade_cur.grp_ref_id;
   END LOOP;
END LOOP;

三、调整后结果比对

-->下面是调整前的测试
goex_admin@SYBO2> DECLARE
  2    businessdate_in CHAR(8);
  3    err_num NUMBER;
  4    err_msg VARCHAR2(32767);
  5
  6  BEGIN
  7    businessdate_in := '20111228';
  8    err_num := NULL;
  9    err_msg := NULL;
 10
 11    goex_admin.BO_TRD_DAYEND_PKG.assign_contract_num ( businessdate_in, err_num, err_msg );
 12
 13    DBMS_OUTPUT.Put_Line('ERR_NUM = ' || TO_CHAR(err_num));
 14    DBMS_OUTPUT.Put_Line('ERR_MSG = ' || err_msg);
 15
 16    DBMS_OUTPUT.Put_Line('');
 17
 18    COMMIT;
 19  END;
 20  /
B - 03586401 - B1112009143
B - 03586404 - B1112009144
B - 03586416 - B1112009145
........

ERR_NUM = 0
ERR_MSG =

PL/SQL procedure successfully completed.

Elapsed: 00:04:21.97

-->下面是调整后测试的结果,将assign_contract_num封装到一个临时的包
goex_admin@SYBO2> DECLARE
  2    businessdate_in CHAR(8);
  3    err_num NUMBER;
  4    err_msg VARCHAR2(32767);
  5
  6  BEGIN
  7    businessdate_in := '20111228';
  8    err_num := NULL;
  9    err_msg := NULL;
 10
 11    goex_admin."BO_TRD_DAYEND_PKG_tmp".assign_contract_num ( businessdate_in, err_num, err_msg );
 12    COMMIT;
 13  END;
 14  /
Current syscontract_num is B1112009143
Current syscontract_num is S1112009340
Current syscontract_num is S1112009341
........
The total count is 797

PL/SQL procedure successfully completed.

Elapsed: 00:00:03.06
goex_admin@SYBO2> spool off;

-->从上面的前后结果对比可知总计797条记录原来所需的时间为4:21.97s,调整之后仅为3.06s。如果记录多的话,差异应该更大
-->Author: Robinson Cheng         
-->Blog:   http://blog.csdn.net/robinson_0612

四、总结
1、参数游标从某种程度上而言,增加了游标的灵活性。即一次定义,多次调用。
2、对基于相同表使用参数游标将会导致对表对象的多次数据读取,增加了I/O开销。

3、尽可能的缩小数据中间结果集,如上面的获得最大的contract_num,由于生成的其唯一性,我们可以将搜索范围限制在一周或当天。

五、更多参考

启用用户进程跟踪

父游标、子游标及共享游标

绑定变量及其优缺点

dbms_xplan之display_cursor函数的使用

dbms_xplan之display函数的使用

执行计划中各字段各模块描述

使用 EXPLAIN PLAN 获取SQL语句执行计划

启用 AUTOTRACE 功能

函数使得索引列失效

Oracle 绑定变量窥探

时间: 2024-08-22 09:35:37

避免游标多次遍历的相关文章

oracle游标使用方法详解

1,什么是游标?  ①从表中检索出结果集,从中每次指向一条记录进行交互的机制.      ②关系数据库中的操作是在完整的行集合上执行的.   由 SELECT 语句返回的行集合包括满足该语句的 WHERE 子句所列条件的所有行.由该语句返回完整的行集合叫做结果集.      应用程序,尤其是互动和在线应用程序,把完整的结果集作为一个单元处理并不总是有效的.      这些应用程序需要一种机制来一次处理一行或连续的几行.而游标是对提供这一机制的结果集的扩展.      游标是通过游标库来实现的.游

mysql 存储过程中游标与多游标实例

例  代码如下 复制代码 DELIMITER $$ DROP PROCEDURE IF EXISTS getUserInfo $$ CREATE PROCEDURE getUserInfo(in date_day datetime) -- -- 实例 -- MYSQL存储过程名为:getUserInfo -- 参数为:date_day日期格式:2008-03-08 --     BEGIN declare _userName varchar(12); -- 用户名 declare _chines

mysql的游标怎么使用

使用游标(cursor) 1.声明游标 DECLARE cursor_name CURSOR FOR select_statement 这个语句声明一个游标.也可以在子程序中定义多个游标,但是一个块中的每一个游标必须有唯一的名字.声明游标后也是单条操作的,但是不能用SELECT语句不能有INTO子句. 2. 游标OPEN语句 OPEN cursor_name 这个语句打开先前声明的游标. 3. 游标FETCH语句 FETCH cursor_name INTO var_name [, var_na

SQLSERVER海量数据库的查询优化及分页算法

分页|数据|数据库|算法|优化 探讨如何在有着1000万条数据的MS SQL SERVER数据库中实现快速的数据提取和数据分页.以下代码说明了我们实例中数据库的"红头文件"一表的部分数据结构: CREATE TABLE [dbo].[TGongwen] (    --TGongwen是红头文件表名    [Gid] [int] IDENTITY (1, 1) NOT NULL ,--本表的id号,也是主键    [title] [varchar] (80) COLLATE Chines

海量数据库的查询优化及分页算法方案

分页|数据|数据库|算法|优化 随着"金盾工程"建设的逐步深入和公安信息化的高速发展,公安计算机应用系统被广泛应用在各警种.各部门.与此同时,应用系统体系的核心.系统数据的存放地――数据库也随着实际应用而急剧膨胀,一些大规模的系统,如人口系统的数据甚至超过了1000万条,可谓海量.那么,如何实现快速地从这些超大容量的数据库中提取数据(查询).分析.统计以及提取数据后进行数据分页已成为各地系统管理员和数据库管理员亟待解决的难题. 在以下的文章中,我将以"办公自动化"系

关于使用存储过程创建分页

创建|存储过程|分页 2005-1-4关于使用存储过程创建分页     今天查询有关sql server 索引的资料 无意在一篇文章的末尾发现一个关于使用存储过程创建分页的代码,直接复制到查询分析器里有误,整理调试了一个,搬上来一起学习一下.     注:原作者 freedk  以下为笔者整理 这是一个相对最优的方法在大量数据集中表现突出        建立一个 Web 应用,分页浏览功能必不可少.这个问题是数据库处理中十分常见的问题.经典的数据分页方法是:ADO 纪录集分页法,也就是利用ADO

数据量过大时数据库操作的处理

数据|数据库 随着"金盾工程"建设的逐步深入和公安信息化的高速发展,公安计算机应用系统被广泛应用在各警种.各部门.与此同时,应用系统体系的核心.系统数据的存放地――数据库也随着实际应用而急剧膨胀,一些大规模的系统,如人口系统的数据甚至超过了1000万条,可谓海量.那么,如何实现快速地从这些超大容量的数据库中提取数据(查询).分析.统计以及提取数据后进行数据分页已成为各地系统管理员和数据库管理员亟待解决的难题. 在以下的文章中,我将以"办公自动化"系统为例,探讨如何在

海量数据索引使用

数据|索引 随着"金盾工程"建设的逐步深入和公安信息化的高速发展,公安计算机应用系统被广泛应用在各警种.各部门.与此同时,应用系统体系的核心.系统数据的存放地――数据库也随着实际应用而急剧膨胀,一些大规模的系统,如人口系统的数据甚至超过了1000万条,可谓海量.那么,如何实现快速地从这些超大容量的数据库中提取数据(查询).分析.统计以及提取数据后进行数据分页已成为各地系统管理员和数据库管理员亟待解决的难题. 在以下的文章中,我将以"办公自动化"系统为例,探讨如何在有

MS SQL Server数据库查询优化及分页算法

server|分页|数据|数据库|算法|优化 探讨如何在有着1000万条数据的MS SQL SERVER数据库中实现快速的数据提取和数据分页.以下代码说明了我们实例中数据库的"红头文件"一表的部分数据结构:CREATE TABLE [dbo].[TGongwen] (    --TGongwen是红头文件表名    [Gid] [int] IDENTITY (1, 1) NOT NULL ,--本表的id号,也是主键    [title] [varchar] (80) COLLATE