DATABASE/SQL, PL-SQL

컬렉션 - 2

devstep88 2021. 2. 4. 22:32

컬렉션 연산

컬렉션 변수 간의 할당

컬렉션은 할당 연산자 :=를 사용하여 동일 타입의 변수 간에 값의 복사가 가능하다,

DECLARE
	TYPE string_array IS TABLE OF VATCHAR2(100);
    v_arr1 string_array;
    v_arr2 string_array;
    
    PROCEDURE p_print_collection_count(a_title VARCHAR2, a_coll string_array)
    IS
    BEGIN
    	IF a_coll IS NULL THEN
        	DBMS_OUTPUT_PUT_LINE(a_title || ': '|| '컬렉션이 NULL입니다.');
        ELSE
        	DBMS_OUTPUT_PUT_LINE(a_title || ': '|| '컬렉션항목이' || a_coll.COUNT|| '건입니다.');
        END IF;
    END;
 BEGIN
 	v_arr1 := string_array('사과', '배', '수박', '망고');
    v_arr2 := v_arr1;  -- 컬렉션 변수 간의 할당 연산을 통한 복사
    p_print_collection_count('1, v_arr1', v_arr1):
    p_print_collection_count('2, v_arr2', v_arr2):
    
    --NULL 할당
    v_arr2 := NULL;
    p_print_collection_count('3, v_arr3', v_arr3):
END;
1. v_arr1: 컬렉션항목이 4건입니다.
2. v_arr2: 컬렉션항목이 4건입니다.
3. v_arr3: 컬렉션이 NULL입니다.

주의할 점은 실제적인 구조가 동일하더라도 타입명이 다르면 할당이 불가능하다는 것이다.

 

컬렉션 비교

Associative Array끼리는 비교가 불가능하다. VARRAY와 Nested Table만 제한된 범위 내에서 비교가 가능하다.

 

- NULL과의 비교: VARRAY와 Nested Table은 IS NULL 연산을 지원한다.

DECLARE
	TYPE string_array IS TABLE OF VATCHAR2(100);
    v_arr1 string_array := string_array('사과', '수박', '망고', '배');
    v_arr2 string_array;
    
    BEGIN
    	IF v_arr1 IS NULL THEN
        	DBMS_OUTPUT_PUT_LINE('v_arr1 IS NULL');
        ELSE
        	DBMS_OUTPUT_PUT_LINE('v_arr1 IS NOT NULL');
        END IF;
        IF v_arr2 IS NOT NULL THEN
        	DBMS_OUTPUT_PUT_LINE('v_arr2 IS NOT NULL');
        ELSE
        	DBMS_OUTPUT_PUT_LINE('v_arr2 IS NULL');
        END IF;
    END;

 

- 동치 혹은 부동 비교:  VARRAY와 Nested Table은 동치 비교(= 비교) 또는 부등 비교(<>, !=, ~=, ^=)를 지원한다.

                             두 컬렉션이 같은지 여부는 컬렉션의 항목 개수와 값이 동일한지의 여부다.

DECLARE
	TYPE string_array IS TABLE OF VARCHAR2(100);
    v_arr1 string_array := string_array('사과', '수박', '망고', '배');
    v_arr2 string_array := string_array('사과', '수박', '망고', '배');
    v_arr3 string_array := string_array('사과', '수박');
BEGIN
	IF v_arr1 = v_arr2 THEN
    	DBMS_OUTPUT_PUT_LINE('v_arr1 = v_arr2');
    ELSE
    	DBMS_OUTPUT_PUT_LINE('v_arr1 <> v_arr2');
    END IF;
    IF v_arr1 = v_arr3 THEN
    	DBMS_OUTPUT_PUT_LINE('v_arr1 = v_arr3');
    ELSE
    	DBMS_OUTPUT_PUT_LINE('v_arr1 <> v_arr3');
    END IF;
    IF v_arr1 <> v_arr3 THEN
    	DBMS_OUTPUT_PUT_LINE('v_arr1 <> v_arr3');
    ELSE
    	DBMS_OUTPUT_PUT_LINE('v_arr1 = v_arr3');
    END IF;
END;

 

컬렉션 메소드

컬렉션에 대한 정보를 얻거나 컬렉션에 대한 변경을 위해 PL/SQL 내장 컬렉션 메소드가 제공된다.

컬렉션변수.컬렉션메소드(파라미터)

파라미터는 메소드에 따라 개수가 다르며 동일 메소드라도 용도에 따라 다른 개수의 파라미터를 가지도록 중복 정의되었다.

- 컬렉션 메소드

- DELETE : 프로시저

-> 컬렉션에서 항목을 삭제한다.   - DELETE: 컬렉션의 모든 항목을 삭제

                                            - DELETE(n): n번째 항목을 삭제

                                            - DELETE(m, n) : m번째 항목부터 시작해서 n개의 항목을 삭제

-> DELETE는 세가지 컬렉션 유형 모두에서 사용할 수 있지만 DELETE(n), DELETE(m, n)은 VARRAY와 Nested Table에만      사용 사능하다.

 

- TRIM: 프로시저

-> VARRAY이나 Nested Table의 끝 부분의 항목을 삭제한다.   - TRIM: 마지막 한 개 항목을 삭제

                                                                                - TRIM(n): 마지막의 n개 항목을 삭제

 

- EXTEND: 프로시저

-> VARRAY나 Nested Table의 끝에 항목을 추가한다.   - EXTEND: 컬렉션에 한개의 NULL항목 추가

                                                                       - EXTEND(n): 컬렉션에 n개의 NULL항목 추가

                                                                       - EXTEND(n, i): 컬렉션에 i번째 항목의 복사본을 n개 추가

 

- EXISTS: 함수

-> VARRAY나 Nested Table의 지정된 항목이 존재하면 TRUE를 반환한다. 존재하지 않는 항목에 대해서는 FALSE을 반환한다.   EXISTS(n): n번째 항목이 존재하면 TRUE, 아니면 FALSE을 반환한다.

 

- FIRST: 함수

-> 컬렉션의 첫번째 인덱스를 반환한다.

 

- LAST: 함수

-> 컬렉션의 마지막 인덱스를 반환한다.

 

-COUNT : 함수

-> 컬렉션이 가진 항목의 개수를 반환한다.

 

- LIMIT: 함수

-> 컬렉션이 가질 수 있는 항목의 최대 개수를 반환한다. VARRAY만이 LIMIT값을 가지며, 항목의 최대 개수 제한이 없는 Associative Array와 Nested Table의 LIMIT는 NULL이다.

 

- PRIOR: 함수

-> 지정된 인덱스 바로 앞의 인덱스를 반환한다. 컬렉션의 바로 앞 인덱스가 비어 있지 않다면 지정된 인덱스보다 1이 작은 값을 반환한다. 컬렉션의 첫 번째 인덱스(FIRST)에 대해서는 NULL을 반환한다. NEXT와 함께 컬렉션 항목의 앞/뒤로 이동하기 위해 사용된다,

 - PRIOR(n): n번쨰 항목의 바로 이전 항목의 인덱스를 반환

 

-NEXT: 함수

-> 지정된 인덱스 바로 뒤의 인덱스를 반환한다. 컬렉션의 바로 다음 인덱스가 비어있지 않다면 지정된 인덱스보다 1이 큰 값을 반환한다. 컬렉션의 마지막 인덱스(LAST)에 대해서는 NULL을 반환한다.

- NEST(n): n번쨰 항목의 바로 다음 항목의 인덱스를 반환

 

다차원 컬렉션

오라클 PL/SQL은 기본적으로 다차원 컬렉션을 지원하지 않는다.

하지만 유사하게 사용할 수는 있다.

DECLARE
	TYPE arr_1d_type IS TABLE OF VARCHAR2(100); -- Nested Table
    v_arr1_1 arr_1d_type := arr_id_type('사과', '배');				-- 초기화
    v_arr1_2 arr_1d_type := arr_id_type('오렌지', '자몽', '망고'); -- 초기화
    v_arr1_3 arr_1d_type := arr_id_type('포도', '앵두');			-- 초기화
    
    TYPE arr_2d_type IS TABLE OF arr_1d_type;	-- 2차원 Nested Table 타입 선언
    v_arr2 arr_2d_type := arr_2d_type(v_arr1_1, v_arr1_2); -- 2차원 컬렉션 초기화(1차원 컬렉션 사용)
BEGIN
	v_arr2.EXTEND;
    v_arr2(3) := v_arr1_3;
    DBMS_OUTPUT_PUT_LINE('v_arr2(2)(3) = ' ||v_arr2(2)(3)); -- 다차원 배열의 항목 참조
END;

배열 처리

PL/SQL의 배열처리는 BULK COLLECT 또는 FORALL 키워드를 사용하여 하나의 SQL문으로 여러 건의 데이터를 한 번에 처리할 수 있도록 하는 기능이다.

항목 BULK COLLECT FORALL
동작 방식 SELECT문이 서버에서 실행되어 여러 건의 결과가 추출되면 결과 로우를 한 번에 클라이언트로 회신하여 배열에 저장 DML 처리할 여러 건의 데이터를 배열에 담아 서버로 한 번에 전송하여 서버에서 한 번에 실행
데이터 이동 방향 테이블 -> 컬렉션 컬렉션 -> 테이블
사용 구문 SELECT, FETCH INSERT, UPDATE, DELETE, MERGE

배열 처리를 사용하여 얻을 수 있는 대표적인 장점

  • 단순성: 배열을 사용하면 SQL처리가 간단해진다. LOOP를 사용하여 처리하지 않고도 여러 개의 레코드를 하나의 SQL문으로 처리할 수 있다.
  • 성능 개선: 배열 처리는 SQL의 실행횟수와 서버로의 왕복횟수를 동시에 줄임으로써 응답시간, CPU사용률, 때로는 네트워크 사용량 측면에서 PL/SQL의 실행 성능을 개선시킨다. 예를 들어 LOOP를 사용하여 10건의 결과를 한 로우씩 읽어 들이는 경우에는 최소 10번의 데이터베이스 호출이 필요하지만 BULK COLLECT INTO를 사용하여 배열로 읽어 들인다면 한 번의 데이터베이스 호출만 실행하면 된다.

사실 오라클 PL/SQL에서 말하는 배열이란 앞에서 설명한 컬렉션이다.

배열처리에는 세가지 유형의 컬렉션이 모두 사용 가능하다.

단 Associative Array의 경우 인덱스가 정수인 유형에만 배열 처리에 사용할 수 있고, 문자열을 인덱스로 사용하는 유형에는 사용할 수 없다.

 

SELECT문에서의 배열 처리

SELECT문에서의 배열처리는 SELECT문의 결과 로우를 서버에서 클라이언트로 가져올 때 한번에 여러 건씩 가져오는 것을 말한다. PL/SQL에서 SELECT문의 배열 처리에는 BULK COLLECT INTO 키워드가 사용된다.

SELECT문의 배열처리 유형은

1. LIMIT 키워드 미사용 : 전체 결과를 한 번의 오퍼레이션으로 PL/SQL의 컬렉션 변수에 저장한다.

                               LIMIT 키워드를 사용하지 않는 경우에는 커서와 LOOP문을 사용하여 반복적으로 결과를

                              FETCH하는 것을 지원하지 않는다.

                           이 유형은 SELECT문의 결과의 최대 건수를 미리 알고 있고 건수가 많지 않은 경우에 사용하기 적합

 

- LIMIT 키워드를 사용하지 않는 SELECT문에서의 배열 처리 유형

유형 설명
건수 제한 없음 SELECT될 건수를 제한하지 않는다.
테이블의 전체 로우를 가져오거나 WHERE절의 조건을 만족하는 로우 전체를 가져온다.
ROWNUM 사용 WHERE절에 ROWNUM을 사용하여 SELECT될 최대 건수를 제한한다.
SAMPLE 사용 SAMPLE 키워드를 사용하여 샘플링할 비율을 제한하거나 샘플링할 블록의 수를 제한한다.
FETCH FIRST 사용 FETCH FIRST절을 사용하여 가져올 최대 건수를 지정한다. 
오라클 버전 12c부터 사용 가능하다.

 

2. FETCH문에 LIMIT 키워드 사용

: 커서 FETCH문에서 한 번에 읽어 들이는 건수를 제한하여 배열로 읽어들이며 LOOP를 사용해 이를 반복처리한다.

결과의 최대 건수를 모르거나 결과 건수가 많아서 메모리 부담이 있을 경우 사용하기에 적합하다.

 

-- LIMIT 키워드 없이 전체 건 조회 또는 ROWNUM을 사용하여 SELECT 건수 제한
DECLARE
	TYPE emp_rec IS TABLE OF emp%ROWTYPE;
    v_emp_arr emp_rec;
BEGIN
	-- 한 번의 실행으로 emp 테이블의 모든 로우를 배열에 읽어 들인다.
    SELECT *
    	BULK COLLECT INTO v_emp_arr
        FROM emp;
       DBMS_OUTPUT_PUT_LINE('건수1: '||v_emp_arr.COUNT);
       
       -- LIMIT 키워드 없이도 ROWNUM을 사용하여 건수 제한이 가능
       -- 한 번의 실행으로 emp 테이블의 로우 10건을 배열에 읽어 들인다.
       -- 최대 10까지가 v_emp에 담길 수 있다.
       SELECT *
       		BULK COLLECT INTO v_emp_arr
            FROM emp
            WHERE ROWNUM <= 10;
          DBMS_OUTPUT_PUT_LINE('건수2 : '||v_emp_arr.COUNT);
END;
-- LIMIT 키워드 없이 SAMPLE 또는 FETCH FIRST을 사용하여 SELECT 건수 제한
DECLARE
	TYPE emp_tab_type IS TABLE OF emp%ROWTYPE;
    v_emp emp_tab_type;
BEGIN
	-- SAMPLE절을 사용하여 건수 제한
    -- SAMPLE 뒤의 숫자 10은 건수가 아니라 퍼센트(%)를 지정하는 숫자
    --정확히 10%를 조회하는 것이 아니라 10%에 해당하는 건수를 추정하는 방법을 사용하므로
    -- 실제로 조회되는 결과 건수는 매 실행 시마다 달라질 수 있다.
    SELECT *
    	BULK COLLECT INTO v_emp
        FROM emp SAMPLE(10);	-- 10%를 샘플링하여 조회
       DBMS_OUTPUT_PUT_LINE('SAMPLE 건수: '||v_emp.COUNT);
       
       -- FETCH FIRST절을 사용하여 건수 제한
       -- 12c에서 새로 생긴 기능
       SELECT *
       		BULK COLLECT INTO v_emp
            FROM emp
            FETCH FIRST 10 ROWS ONLY;
          DBMS_OUTPUT_PUT_LINE('FETCH FIRST 10 건수 : '||v_emp.COUNT);
END;

FETCH FIRST는 건수를 지정하거나 비율을 지정하는 두 가지 형태로 사용할 수 있다.

ONLY는 지정된 건수 또는 비율의 건수를 가져오고, WITH TIES는 ORDER BY와 같이 사용되어 마지막 FETCH되는 건과 동일한 정렬키를 가지는 로우들을 모두 추가로 가져온다.

 

-- LIMIT절을 사용하여 SELECT 건수 제한
DECLARE
	TYPE emp_rec IS TABLE OF emp%ROWTYPE;
    v_emp_arr emp_rec;
    c_size_limit  CONSTANT PLS_INTEGER := 10; -- 한 번에 읽어 들일 최대 건수 제한
    v_fetched  PLS_INTEGER;
    CURSOR c IS		-- 이 유형의 처리는 CURSOR를 사용
    	SELECT *
        FROM emp;
BEGIN
	v_fetched := 0;
    OPEN c;  -- 커서 열기
    LOOP
    	FETCH c BULK COLLECT INTO v_emp_arr
         	LIMIT c_size_limit;	-- LIMIT를 사용하여 한 번에 읽어들일 최대 건수를 제한한다.
            
       DBMS_OUTPUT_PUT_LINE(v_emp_arr.COUNT||'건');
       
       IF 0 < v_emp_arr.COUNT THEN
       	FOR i IN v_emp_arr.FIRST .. v_emp_arr.LAST
        LOOP
        	DBMS_OUTPUT_PUT_LINE(CHR(9)||'순서 = '|| TO_cHAR(v_fetched+i, '99') ||
            	'   사번 = '|| v_emp_arr(i).empno||', 이름 = '|| v_emp_arr(i).ename);
        END LOOP;
        v_fetched := c%ROWCOUNT; -- 처리된 건수
       END IF;
     
       EXIT WHEN c%NOTFOUND; -- 더 이상의 데이터가 없으면 종료. 모든 처리가 끝난 후 호출해야 함
    END LOOP;   
      
    CLOSE c;
END;

DML문에서의 배열 처리

INSERT, DELETE, UPDATE, MERGE문과 같은 DML문에서의 배열처리에는 FORALL 키워드가 사용된다,

FORALL 인덱스명 IN 범위절
	DML문;

범위절의 표현방식은 다음과 같은 세가지가 있다.

  • '..'을 사용: 데이터 배열에서 처리할 범위를 지정한다. 배열에는 빈 항목이 있으면 안된다. 하한값 .. 상한값 형태로 사용되며 범위가 연속적인 경우에 사용하기에 적합하다.
  • 'INDICES OF'를 사용: 데이터 배열에서 유효한(비어있지 않은) 항목만을 처리하도록 지정한다. 배열에 빈 항목이 있다면 처리 대상에서 제외된다, 중간에 빈 항목이 있는 경우에 유용하게 사용할 수 있다.
  • 'VALUES OF' 사용: 데이터 배열에서 처리할 항목의 인덱스를 또 다른 배열로 제공한다. 항목 중의 일부만을 간접적으로 지정할 때 유용하게 사용할 수 있다.
CREATE TABLE t(
	id INTEGER PRIMARY KEY,
    name VARCHAR2(100)
);

DECLARE
	TYPE id_arr_type IS TABLE OF PLS_INTEGER;
    TYPE name_arr_type IS TABLE OF t.name%TYPE;
    v_id_arr id_arr_type := id_arr_type(1,2,3,4,5);
    v_name_arr name_arr_type := name_arr_type('이순신', '강감찬', '을지문덕', '김유신', '김윤후');
    v_name_arr2 name_arr_type := name_arr_type('강희안', '김홍도', '신윤복', '정선', '장승업');
BEGIN
	DELETE FROM t;
    
	-- INSERT문에서의 배열처리. ".."을 사용하여 범위 지정
  	FORALL i IN v_id_arr.FIRST .. v_id_arr.LAST
    	INSERT INTO t(id, name) VALUES(v_id_arr(i), v_name_arr(i));
       DBMS_OUTPUT_PUT_LINE('INSERT COUNT = '||SQL%ROWCOUNT);
       
       -- UPDATE문에서의 배열처리. INDICES OF를 사용하여 범위지정
       FORALL i IN INDICES OF v_id_arr
       	UPDATE t
           SET name = v_name_arr(i)
         WHERE ID = v_id_arr(i);
          DBMS_OUTPUT_PUT_LINE('UPDATE COUNT = '||SQL%ROWCOUNT);
       
        -- MERGE문에서의 배열처리. ".."를 사용하여 범위지정
       FORALL i IN v_id_arr.FIRST .. v_id_arr.LAST
       	MERGE INTO t
           USING(
          	SELECT id
            FROM t
           WHERE id = v_id_arr(i)) u
        ON (t.id = u.id)
        WHEN MATCHED THEN
        	INSERT (id, name)
            VALUES (v_id_arr(i), v_name_arr(i));
          DBMS_OUTPUT_PUT_LINE('MERGE COUNT = '||SQL%ROWCOUNT);
          
       -- DELETE문에서의 배열처리. VALUES OF를 사용하여 범위지정
       FORALL i IN VALUES OF v_id_arr
       	DELETE FROM t WHERE id = v_id_arr(i);
          DBMS_OUTPUT_PUT_LINE('DELETE COUNT = '||SQL%ROWCOUNT);    
END;

 

FORALL문과 예외 처리

기본적으로 FORALL 처리 중 오류를 만나면 FORALL문 전체가 롤백된다.

하지만 예외처리가 되어 있는 경우 다르게 동작하는데 오류를 만나기 전까지 처리된 건을 롤백하지 않고 오류를 만난 건만 처리가 롤백되며 문장은 중단된다.