游标在数据库领域被广泛使用,尤其是对于需要将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函数的使用
时间: 2024-10-31 21:04:29