2013-08-09 5 views
3

(이 모든 오라클 10g입니다) :비 벌크 바인딩 PL/SQL 코드를 벌크 바인딩 코드로 변환해야하는 이유는 무엇입니까? 그리고 Buk 바인딩을 지켜야 할 이유가 있습니까?

내가 대학에서 나의 첫 직장에있어
CREATE OR REPLACE FUNCTION bar(...) 
IS 
    v_first_type VARCHAR2(100) ; 
    v_second_type VARCHAR2(100); 

    CURSOR cur IS SELECT a,b FROM source_table ; 
    v_a int; 
    v_b char; 
BEGIN 
    OPEN cur; 
    <<l_next>> --10G doesn't have the continue statement. 
    LOOP 
     FETCH cur INTO v_a, v_b ; 
     EXIT WHEN cur%NOTFOUND ; 

     --Ignore Record Case: ignore the record entirely 
     IF a == -1 THEN 
      -- do something 
      GOTO l_next ; --10g doesn't have the continue statement. 
     ELSE 
      -- do something else 
      v_first := 'SUCCESS' ; 
     END IF; 

     -- Transform Case: 
     IF b == 'z' THEN 
      -- do something 
      v_second := 'something'; 
     ELSE 
      -- do something 
      v_second := 'something else'; 
     END IF; 


     INSERT INTO report_table VALUES (v_first, v_second); 
    END LOOP; 
    CLOSE cur; 
EXCEPTION 
    ... 
END; 

. 위의 일반적인 프레임 워크처럼 보이는 몇 가지 레거시 코드를 살펴 보았습니다. (몇 줄의 코드가 길고 훨씬 복잡한 처리 (세트 기반 솔루션이 가능하지 않음)를 제외하고는).

하나의 테이블에서 커서로 많은 행을 가져 와서 커서를 반복하고, 변환을 수행하고, 결과를 보고서 테이블에 삽입합니다. 커서는 모든 레코드를 삽입하지 않습니다 - 레코드에 문제가 있거나 어떤 이유로 든 그것을 좋아하지 않으면 레코드를 삽입하지 않고 건너 뜁니다 (GOTO 문 참조).

문제 1 : 끝에 FORALL을 수행하는 대신 루프 내부에서 삽입이 하나씩 발생합니다.

문제 2 : 커서는 BULK COLLECT를 사용하지 않습니다.

또한 BULK COLLECT를 다시 사용하지 않는 커서가있는 저장 프로 시저가 있으며 커서의 레코드를 반복하면서이 함수를 실행합니다. 반복되는 각 레코드의 끝에 하나의 커밋이 발행됩니다. 여기에 내가 쓴 함수에는 커밋이 없다. 결합 배열로 COLLECT BULK 사용)

CREATE OR REPLACE FUNCTION bar(...) 
IS 
    CURSOR cur IS SELECT a,b FROM source_table ; 

    TYPE t_source IS TABLE OF cur%ROWTYPE INDEX BY PLS_INTEGER; 
    TYPE t_report IS TABLE OF destination_table%ROWTYPE INDEX BY PLS_INTEGER; 
    v_sources t_source; 
    v_reports t_report 
    v_report_inx INT := 0; -- To Prevent Sparse Collection 
BEGIN 
    OPEN cur; 
    <<l_next>> --10G doesn't have the continue statement. 
    LOOP 
     FETCH cur BULK COLLECT INTO v_sources LIMIT 100 ; 
     EXIT WHEN v_sources.count = 0 ; 

     FOR i IN 1 .. v_sources LOOP 
      --Ignore Record Case: ignore the record entirely 
      IF v_sources(i).a == -1 THEN 
       -- do something 
       GOTO l_next ; --10g doesn't have the continue statement. 
      ELSE 
       -- do something else 
       v_reports(v_report_inx).first := 'SUCCESS' ; 
      END IF; 

      -- Transform Case: 
      IF v_sources(i).b == 'z' THEN 
       -- do something 
       v_reports(v_report_inx).second := 'something'; 
      ELSE 
       -- do something 
       v_reports(v_report_inx).second := 'something else'; 
      END IF; 

      v_report_inx := v_report_inx + 1; 
     END LOOP; 


    END LOOP; 

    FORALL i in 1 .. v_reports.count 
      INSERT INTO report_table (first, second) VALUES (v_reports(i).first, v_reports(i).v_second); 

    CLOSE cur; 
EXCEPTION 
    ... 
END; 

재질 변화가 1이고, 2)의 다른 연결 배열 중 FORALL 사용 :

는 I는 다음과 같이 표시하는 코드를 재기록 할.

나는 두 가지 질문이 있습니다

내가 처음 코드에서 제공하는 프레임 워크를 기반으로

1) 내 변경이 그것을 할 수있는 가장 뛰어난 방법입니다? 다른 방법으로 해 주시겠습니까?

2) 누군가가 대량 수거 및 포틀링을 사용하지 않을 것이라고 생각하지 않는 이유가 있습니까? 아마도 레거시 코드에서 아직 실현하지 못했던 복잡한 처리가 있을까요? 이 코드는 원래 2002 년에 만들어 졌으므로 (그래서 나는 8i 또는 9i라고 가정 할 것입니다) 그 이후로 업데이트되었습니다. 9i had bulk binding. 8i also은 벌크 바인딩을 가졌다. 두 가지 모두 연관 배열을 가졌습니다. 그래서 저는 그들이 대량 바인딩을 사용하지 않았던 이유가 있어야만합니다.

+0

11g에서 Oracle PL/SQL optimizer는 자신의 코드를 대량으로 사용하도록 "변환"합니다. 때로는 차이점을 측정 할 수없는 경우가 있습니다. – ibre5041

+1

처리가 간단한 경우 가장 좋은 방법은 SQL 문으로 변환하는 것입니다. 파이프 라인 기능을 살펴보십시오. 오라클 문서의 데이터웨어 하우스 책에는 대량 etl 프로세스를 구현하는 방법에 대한 많은 아이디어가 있습니다. – haki

+0

@ivan, 그건 멋지다. 나는 몰랐다. 그러나 우리는 직장에서 10g을 사용합니다. 이 기능은 현재 존재하지 않습니까? 그 소식통을 올리시겠습니까? 나는 그것을 읽고 싶다. –

답변

2

bulk collect으로 마이그레이션하는 일반적인 아이디어에는 아무런 문제가 없습니다. 일괄 작업은 컨텍스트 스위치 및 데이터베이스 왕복 횟수를 최소화합니다.

코드에 잘못된 일반적인 문제가 하나뿐입니다. LIMIT 절은 대량 작업에 의한 메모리 과용을 방지하므로 대량 수집과 함께 사용하는 것이 적절합니다. 그러나 v_reports은 제어되지 않고 커집니다. 따라서 대량 삽입물을 루프 내부로 옮기고 나중에 v_reports을 지우십시오.

변경된 코드에는 약간의 부정확성이 있습니다. 아래 코드 조각을 검토하십시오. /**/ 스타일의 주석은 내 것입니다.

CREATE OR REPLACE FUNCTION bar(...) 
IS 
    CURSOR cur IS SELECT a,b FROM source_table ; 

    TYPE t_source IS TABLE OF cur%ROWTYPE INDEX BY PLS_INTEGER; 
    TYPE t_report IS TABLE OF destination_table%ROWTYPE INDEX BY PLS_INTEGER; 
    v_sources t_source; 
    v_reports t_report 

    /* 1. correct type is same as type of index 
     2. There are nothing wrong with sparse collections, but a separate 
      counter which incremented continuously needed for t_report. 
    */ 
    v_report_inx PLS_INTEGER := 0; -- To Prevent Sparse Collection 

BEGIN 
    OPEN cur; 
    <<l_next>> --10G doesn't have the continue statement. 
    LOOP 
     FETCH cur BULK COLLECT INTO v_sources LIMIT 100 ; 

     /* On last step v_sources.count < 100, not exactly 0. 
      Also if there are no elements then no processing done, 
      so check at the end of loop. 
     EXIT WHEN v_sources.count = 0; 
     */ 

     /* correct way is to loop from 1 to count 
      (.last and .first not usable because both is null for empty array) 
     */ 
     FOR i IN 1 .. v_sources.count LOOP 

      v_report_inx := v_report_inx + 1; 

      --Ignore Record Case: ignore the record entirely 
      IF v_sources(i).a = -1 THEN 
       -- do something 
       GOTO l_next ; --10g doesn't have the continue statement. 
      END IF; 

      /* No need for ELSE here, just execution continues */ 
      -- do something else 
      v_reports(v_report_inx).first := 'SUCCESS' ; 


      -- Transform Case: 
      IF v_sources(i).b = 'z' THEN 
       -- do something 
       v_reports(v_report_inx).second := 'something'; 
      ELSE 
       -- do something 
       v_reports(v_report_inx).second := 'something else'; 
      END IF; 

     END LOOP; 


     /* Use "indicies of" construct to deal with sparsed collections */ 
     FORALL i in indices of v_reports 
       /* t_report already declared with %ROWTYPE 
       so just insert entire row, it works faster */ 
       INSERT INTO report_table VALUES v_reports(i); 

     /* Cleanup after insert */ 
     v_reports.delete; 

     /* If number of selected records less than LIMIT then last row reached. */ 
     EXIT WHEN v_sources.count < 100; 

    END LOOP; 


    CLOSE cur; 
EXCEPTION 
    ... 
END; 

업데이트 @jonearles에

감사합니다. 그는 PL/SQL에서 커서를 처리하는 다양한 접근법에 대한 성능을 테스트하도록 권장했습니다.

다음은 3 000 000 개의 레코드로 테스트 한 결과입니다. 명확한 명시 적 커서에서 대량 수집 방식으로의 마이그레이션은 실제 성능 향상을 가져옵니다.
동시에 대량 수집 옵션이있는 명시 적 커서와 올바르게 선택 된 LIMIT은 항상 암시 적 커서보다 성능이 뛰어나지 만 그 차이는 수용 할 수있는 범위에 있습니다. 다음은

Variant name   | Time (sec) 
------------------------------------- 
bulk_cursor_limit_500 | 1.26 
bulk_cursor_limit_100 | 1.52 
bulk_unlimited   | 1.75 
implicit_cursor  | 1.83 
plain_cursor   | 27.20 

테스트를위한 코드 (제한 SQLFiddle 예를 here)

계획 설정입니다

drop table t 
/
drop table log_run 
/
create table t(a number, b number) 
/
insert into t select level, level from dual connect by level <= 3000000 
/

create table log_run(id varchar2(30), seconds number); 
/

delete log_run 
/

단일 테스트 실행

declare 
    cursor test_cur is 
    select a, b from t; 

    test_rec test_cur%rowtype; 
    counter number; 

    vStart timestamp; 
    vEnd timestamp; 
    vTimeFormat varchar2(30) := 'SSSSS.FF9'; 
begin 

    vStart := systimestamp; 

    open test_cur; 
    loop 
    fetch test_cur into test_rec; 
    exit when test_cur%notfound; 
    counter := counter + 1; 
    end loop; 
    close test_cur; 

    vEnd := systimestamp; 
    insert into log_run(id, seconds) 
    values('plain_cursor', 
      to_number(to_char(vEnd,vTimeFormat)) 
      - 
      to_number(to_char(vStart,vTimeFormat)) 
     ) 
    ; 

end; 
/

--Implicit cursor 
--0.2 seconds 
declare 
    test_rec t%rowtype; 
    counter number; 

    vStart timestamp; 
    vEnd timestamp; 
    vTimeFormat varchar2(30) := 'SSSSS.FF9'; 
begin 

    vStart := systimestamp; 

    for c_test_rec in (select a, b from t) loop 
    test_rec.a := c_test_rec.a; 
    test_rec.b := c_test_rec.b; 
    counter := counter + 1; 
    end loop; 

    vEnd := systimestamp; 
    insert into log_run(id, seconds) 
    values('implicit_cursor', 
      to_number(to_char(vEnd,vTimeFormat)) 
      - 
      to_number(to_char(vStart,vTimeFormat)) 
     ) 
    ; 

end; 
/

declare 
    cursor test_cur is 
    select a, b from t; 

    type t_test_table is table of t%rowtype; 

    test_tab t_test_table; 
    counter number; 

    vStart timestamp; 
    vEnd timestamp; 
    vTimeFormat varchar2(30) := 'SSSSS.FF9'; 
begin 

    vStart := systimestamp; 

    open test_cur; 
    loop 
    fetch test_cur bulk collect into test_tab limit 100; 
    for i in 1 .. test_tab.count loop 
     counter := counter + 1; 
    end loop; 

    exit when test_tab.count < 100; 
    end loop; 

    close test_cur; 

    vEnd := systimestamp; 
    insert into log_run(id, seconds) 
    values('bulk_cursor_limit_100', 
      to_number(to_char(vEnd,vTimeFormat)) 
      - 
      to_number(to_char(vStart,vTimeFormat)) 
     ) 
    ; 

end; 
/


declare 
    cursor test_cur is 
    select a, b from t; 

    type t_test_table is table of t%rowtype; 

    test_tab t_test_table; 
    counter number; 

    vStart timestamp; 
    vEnd timestamp; 
    vTimeFormat varchar2(30) := 'SSSSS.FF9'; 
begin 

    vStart := systimestamp; 

    open test_cur; 
    loop 
    fetch test_cur bulk collect into test_tab limit 500; 
    for i in 1 .. test_tab.count loop 
     counter := counter + 1; 
    end loop; 

    exit when test_tab.count < 500; 
    end loop; 

    close test_cur; 

    vEnd := systimestamp; 
    insert into log_run(id, seconds) 
    values('bulk_cursor_limit_500', 
      to_number(to_char(vEnd,vTimeFormat)) 
      - 
      to_number(to_char(vStart,vTimeFormat)) 
     ) 
    ; 

end; 
/

declare 

    type t_test_table is table of t%rowtype; 

    test_tab t_test_table; 
    counter number; 

    vStart timestamp; 
    vEnd timestamp; 
    vTimeFormat varchar2(30) := 'SSSSS.FF9'; 
begin 

    vStart := systimestamp; 

    select * bulk collect into test_tab from t; 

    for i in 1 .. test_tab.count loop 
    counter := counter + 1; 
    end loop; 

    vEnd := systimestamp; 
    insert into log_run(id, seconds) 
    values('bulk_unlimited', 
      to_number(to_char(vEnd,vTimeFormat)) 
      - 
      to_number(to_char(vStart,vTimeFormat)) 
     ) 
    ; 

end; 
/

평균 결과

을 선택
1

GOTO가 사용되지 않도록이 코드를 다시 작성했습니다 (필자는 이전에 구조화되지 않은 구조화 된 프로그래머라고 생각합니다 .-)). 또한 명시 적 커서를 없애고 FOR 루프로 커서를 사용하면 10g으로 시작하는 것이 종종 배후에서 일괄 처리됩니다. 시도 : 루프의 명시적인 제어를 허용하면서

CREATE OR REPLACE FUNCTION bar(...) 
IS 
    v_first_type VARCHAR2(100) ; 
    v_second_type VARCHAR2(100); 
BEGIN 
    <<OUTER_LOOP>> 
    FOR aRow In (SELECT A, B FROM SOURCE_TABLE) 
    LOOP 

     <<INNER_LOOP>> 
     LOOP -- This loop is used to allow us to skip the later INSERT, and 
      -- will only be passed through once for each row returned by 
      -- the FOR loop. 
     --Ignore Record Case: ignore the record entirely 
     IF aRow.A == -1 THEN 
      -- do something 
      EXIT INNER_LOOP; -- rather than GOTO 
     ELSE 
      -- do something else 
      v_first := 'SUCCESS' ; 
     END IF; 

     -- Transform Case: 
     IF aRow.B == 'z' THEN 
      -- do something 
      v_second := 'something'; 
     ELSE 
      -- do something 
      v_second := 'something else'; 
     END IF; 


     INSERT INTO report_table VALUES (v_first, v_second); 

     EXIT INNER_LOOP; -- the "loop" is used to allow the INSERT to be 
          -- skipped and thus we don't ever want to go back 
          -- to the top 
     END LOOP; -- INNER_LOOP 
    END LOOP; -- OUTER_LOOP 
EXCEPTION 
    ... 
END; 

참고 명시 적 출구와 내부 루프의 사용은 위에서 아래로 흐름을 유지.

오라클 프로파일 러에서이 코드를 실행하여 어떤 코드 줄이 가장 많이 사용되는지 이해하는 것이 좋습니다. 병목 현상이 어디인지 추측하여 코드를 최적화하려는 것은 시간 낭비입니다. 당신이 그것을 프로파일 링 할 때까지 당신은 추측하고 있습니다. 그리고 당신은 내가 얼마나 자주 잘못했는지 짐작할 수 없을 것입니다. :-) 코드는 가장 어두운 곳에서 시간을 보낸다.

공유하고 즐기십시오.

+0

"장면 뒤에 벌크 바인딩"이 재미있을 것 같습니다. 그것에 대한 증거를 제공해 줄 수 있습니까? – ThinkJet

+0

@ThinkJet 자동 대량 수집에 대한 간단한 데모와 성능 기능에 대한 Oracle 프레젠테이션에 대한 링크는 내 대답 [여기] (http://stackoverflow.com/a/13151348/409172)의 두 번째 부분을 참조하십시오. –

+0

@BobJarvis, 왜 코드에서 대량 바인딩을 사용하지 않기로 선택 했습니까? 나는 'EXIT'문을 사용하여 'GOTO'문을 잃어 버릴 생각을 좋아합니다. 프로파일 러를 확인해 볼께. 고마워. –