PL/SQL 컬렉션

PL/SQL의 세 가지 컬렉션 유형(Associative Array, Nested Table, VARRAY)의 특성 비교, 메서드 활용, 그리고 BULK COLLECT로 대량 데이터를 효율적으로 처리하는 방법을 다룹니다.

· 5 min read · PALDYN Team

지난 글에서 트리거·시퀀스·시노님을 다뤘다. 이번에는 PL/SQL에서 다중 값을 다루는 컬렉션(Collection) 세 가지 유형을 비교하고 실무 활용 패턴을 살펴본다.

컬렉션 세 가지 유형

1. Associative Array (INDEX BY Table)

가장 자주 사용하는 유형이다. 초기화 없이 바로 사용할 수 있고, 인덱스로 숫자뿐 아니라 문자열도 사용할 수 있다.

DECLARE
  -- 숫자 인덱스
  TYPE t_sal_map IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
  v_salaries t_sal_map;

  -- 문자열 인덱스 (해시 맵처럼 활용)
  TYPE t_dept_map IS TABLE OF VARCHAR2(100) INDEX BY VARCHAR2(30);
  v_depts t_dept_map;
BEGIN
  v_salaries(100) := 5000;
  v_salaries(200) := 8000;

  v_depts('IT')      := '정보기술';
  v_depts('FINANCE') := '재무';

  DBMS_OUTPUT.PUT_LINE(v_salaries.COUNT);  -- 2
  DBMS_OUTPUT.PUT_LINE(v_depts('IT'));     -- 정보기술
END;
/

데이터베이스 컬럼에 저장할 수 없지만 PL/SQL 내 캐시·룩업 테이블로는 가장 편리하다.

2. Nested Table

Associative Array와 비슷하지만 초기화가 필요하고 데이터베이스 컬럼에 저장할 수 있다.

DECLARE
  TYPE t_names IS TABLE OF VARCHAR2(50);
  v_names t_names := t_names();  -- 빈 컬렉션으로 초기화
BEGIN
  v_names.EXTEND;
  v_names(1) := 'Kim';
  v_names.EXTEND;
  v_names(2) := 'Lee';

  FOR i IN 1..v_names.COUNT LOOP
    DBMS_OUTPUT.PUT_LINE(i || ': ' || v_names(i));
  END LOOP;
END;
/

DELETE(i)로 원소를 삭제하면 희소(Sparse) 컬렉션이 된다. 희소 컬렉션은 NEXT() 메서드로 순회해야 한다.

3. VARRAY

최대 크기를 선언해야 하는 배열이다. 저장 순서가 보장되고 데이터베이스에도 저장할 수 있다.

DECLARE
  TYPE t_tags IS VARRAY(5) OF VARCHAR2(30);  -- 최대 5개
  v_tags t_tags := t_tags('SQL', 'Oracle', 'PL/SQL');
BEGIN
  DBMS_OUTPUT.PUT_LINE(v_tags.COUNT);  -- 3
END;
/

PL/SQL 컬렉션 유형 비교


컬렉션 메서드

메서드설명
COUNT현재 원소 개수
FIRST첫 번째 인덱스
LAST마지막 인덱스
EXISTS(i)i번 원소 존재 여부
EXTEND원소 1개 추가 공간 확보
EXTEND(n)n개 추가 공간 확보
DELETE전체 삭제
DELETE(i)i번 원소 삭제
NEXT(i)i 다음 인덱스 반환
PRIOR(i)i 이전 인덱스 반환

EXTENDDELETE는 Nested Table과 VARRAY에만 적용된다. Associative Array는 자동으로 크기가 조정된다.

-- 희소 컬렉션 안전 순회
DECLARE
  TYPE t_map IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
  v_map t_map;
  i PLS_INTEGER;
BEGIN
  v_map(1) := 100;
  v_map(3) := 300;   -- 2번 인덱스 없음 (희소)
  v_map(5) := 500;

  i := v_map.FIRST;
  WHILE i IS NOT NULL LOOP
    DBMS_OUTPUT.PUT_LINE(i || ' => ' || v_map(i));
    i := v_map.NEXT(i);
  END LOOP;
END;
/

BULK COLLECT — 컬렉션으로 대량 데이터 적재

행 단위로 FETCH하면 PL/SQL과 SQL 엔진 간 컨텍스트 스위치가 행마다 발생한다. BULK COLLECT는 한 번의 SQL 실행으로 여러 행을 컬렉션에 한꺼번에 적재해 이 비용을 최소화한다.

DECLARE
  TYPE t_emp_tab IS TABLE OF employees%ROWTYPE;
  v_emps t_emp_tab;
BEGIN
  -- 전체 결과를 한 번에 (소용량 테이블)
  SELECT * BULK COLLECT INTO v_emps
  FROM   employees
  WHERE  department_id = 60;

  DBMS_OUTPUT.PUT_LINE(v_emps.COUNT || '건 로드');
END;
/

LIMIT으로 청크 처리

수백만 행을 한 번에 컬렉션에 담으면 SGA 메모리를 과도하게 사용한다. LIMIT 절로 청크 크기를 제한한다.

DECLARE
  TYPE t_rows IS TABLE OF orders%ROWTYPE;
  v_rows t_rows;
  CURSOR c_old IS SELECT * FROM orders WHERE status = 'COMPLETED';
BEGIN
  OPEN c_old;
  LOOP
    FETCH c_old BULK COLLECT INTO v_rows LIMIT 2000;
    EXIT WHEN v_rows.COUNT = 0;

    -- 배치 처리
    FOR i IN 1..v_rows.COUNT LOOP
      -- 각 행 처리
      NULL;
    END LOOP;
    COMMIT;  -- 청크 단위 커밋
  END LOOP;
  CLOSE c_old;
END;
/

적절한 LIMIT 값은 행의 폭과 PGA 설정에 따라 다르지만 500~5000 사이가 일반적이다.

컬렉션 메서드와 BULK COLLECT


FORALL — 컬렉션을 한 번에 DML

BULK COLLECT가 SELECT를 대량화하는 것처럼, FORALL은 DML을 대량화한다.

DECLARE
  TYPE t_ids IS TABLE OF employees.employee_id%TYPE;
  v_ids t_ids := t_ids(100, 101, 102, 103);
BEGIN
  -- 루프 대신 FORALL: 단 1번의 SQL 엔진 호출
  FORALL i IN 1..v_ids.COUNT
    UPDATE employees
    SET    salary = salary * 1.1
    WHERE  employee_id = v_ids(i);

  DBMS_OUTPUT.PUT_LINE(SQL%ROWCOUNT || '행 갱신');
END;
/

FORALLBULK COLLECT를 조합하면 대용량 배치 처리 성능이 크게 향상된다.


지난 글: PL/SQL 트리거, 시퀀스, 시노님


읽어주셔서 감사합니다. 😊