-- 느림: 애플리케이션에서 10만 번 반복
-- for (Order o : orders) { insert(o); }
-- 빠름: 한 문장으로 집합 처리
INSERT INTO order_summary (customer_id, order_cnt, total_amount, base_dt)
SELECT customer_id,
COUNT(*),
SUM(amount),
TRUNC(SYSDATE)
FROM orders
WHERE order_dt >= TRUNC(SYSDATE) - 1
AND order_dt < TRUNC(SYSDATE)
GROUP BY customer_id;이 파트에서 다루는 내용
한 건씩 처리하면 느립니다
애플리케이션에서 조회한 결과를 반복문으로 돌면서 한 건씩 INSERT하거나 UPDATE하는 코드를 흔히 봅니다. 10만 건이면 10만 번의 왕복이 생깁니다.
DB는 집합을 한 번에 다루도록 만들어져 있습니다. 같은 일을 한 문장으로 표현하면 왕복이 사라지고, 옵티마이저가 전체를 보고 최적의 방법을 고를 수 있습니다.
DECLARE
TYPE t_ids IS TABLE OF orders.order_id%TYPE;
v_ids t_ids;
CURSOR c IS
SELECT order_id FROM orders
WHERE status_cd = 'READY' AND order_dt < SYSDATE - 30;
BEGIN
OPEN c;
LOOP
FETCH c BULK COLLECT INTO v_ids LIMIT 5000; -- 5000건씩 묶어서
EXIT WHEN v_ids.COUNT = 0;
FORALL i IN 1 .. v_ids.COUNT -- 한 번에 전달
UPDATE orders SET status_cd = 'EXPIRED'
WHERE order_id = v_ids(i);
COMMIT;
END LOOP;
CLOSE c;
END;
/LIMIT 없이 BULK COLLECT 하면 전체를 메모리에 올려 PGA가 터질 수 있습니다. 수천 건 단위로 끊는 것이 기본입니다. 단순 조건이면 이런 반복 없이 UPDATE 한 문장이 더 낫습니다.
대량 적재는 다이렉트 패스를 검토합니다
일반 INSERT는 빈 공간을 찾아 채워 넣습니다. 대량 적재에서는 이 과정 자체가 비용입니다.
APPEND 힌트를 쓰면 기존 공간을 찾지 않고 테이블의 마지막 지점 뒤에 새 블록으로 이어 붙입니다. UNDO도 거의 만들지 않아 훨씬 빠릅니다.
대신 제약이 붙습니다. 테이블에 배타 락이 걸리고, 커밋 전까지는 그 트랜잭션에서도 해당 테이블을 조회할 수 없습니다. 삭제로 생긴 빈 공간도 재사용하지 않아 파일이 계속 커집니다.
야간 배치, 이력 테이블 적재, 초기 데이터 이관처럼 그 시간에 다른 작업이 없을 때 씁니다.
온라인 업무가 동시에 도는 테이블에는 쓰지 않습니다. 배타 락 때문에 업무가 멈춥니다.
REDO 기록을 줄여 더 빨라지지만, 그 구간은 복구할 수 없습니다. 백업 계획과 함께 결정해야 하고 DBA 협의가 필요합니다.
적재 전에 인덱스를 사용 불가로 두고 적재 후 재생성하면 빠릅니다. 재생성 시간과 그동안의 조회 영향을 함께 계산합니다.
INSERT /*+ APPEND */ INTO order_archive
SELECT * FROM orders WHERE order_dt < ADD_MONTHS(SYSDATE, -12);
COMMIT; -- 커밋해야 다른 세션과 자기 세션 모두 조회 가능커밋 전에 같은 테이블을 조회하면 ORA-12838 이 납니다. 오류가 아니라 정상 동작이니 적재 후 커밋하고 진행합니다.
대량 삭제는 DELETE가 최선이 아닙니다
DELETE는 지운 행마다 UNDO와 REDO를 만듭니다. 수백만 건이면 그 자체가 큰 작업이고, 다 지워도 파일 크기는 줄지 않습니다. 사용 흔적이 남아 이후 전체 스캔이 여전히 느립니다.
삭제할 양이 남길 양보다 훨씬 많다면 발상을 뒤집는 편이 빠릅니다.
가장 빠르고 공간도 회수됩니다. 다만 DDL이라 되돌릴 수 없고 테이블 배타 락이 걸립니다. 운영 중에는 쓰지 않습니다.
남길 것만 새 테이블로 만들고 이름을 바꿔 치웁니다. 인덱스와 제약조건, 권한을 다시 만들어야 하므로 절차를 미리 준비합니다.
날짜로 파티션이 나뉘어 있으면 파티션 하나를 잘라내는 것으로 끝납니다. 초 단위로 끝나고 UNDO도 생기지 않습니다.
지울 양이 적고 조건이 복잡하면 DELETE가 맞습니다. Part 11에서 다룬 커밋 단위 쪼개기를 함께 적용합니다.
-- 1) 남길 데이터만 새 테이블로
CREATE TABLE orders_new AS
SELECT * FROM orders WHERE order_dt >= ADD_MONTHS(SYSDATE, -12);
-- 2) 인덱스와 제약조건을 새 테이블에 동일하게 생성
-- (기존 정의를 미리 스크립트로 뽑아 둡니다)
-- 3) 이름 교체
ALTER TABLE orders RENAME TO orders_old;
ALTER TABLE orders_new RENAME TO orders;
-- 4) 확인 후 정리
DROP TABLE orders_old PURGE;3번 단계에서 잠깐 테이블이 없는 순간이 있습니다. 무중단이 필요하면 이 방식은 맞지 않습니다. 또 기존 인덱스·제약·권한·트리거를 빠짐없이 옮겼는지 확인해야 합니다. 절차가 많아 실수하기 쉬우므로 사전에 스크립트로 준비합니다.
파티셔닝은 큰 테이블을 관리 단위로 나눕니다
논리적으로는 하나의 테이블인데 물리적으로는 여러 조각으로 나눠 저장하는 방식입니다. 주문 테이블을 월 단위로 나누는 식입니다.
이득은 두 가지입니다. 조회 조건에 파티션 키가 있으면 해당 조각만 읽어 범위가 줄어들고, 오래된 데이터를 조각째 잘라내 관리할 수 있습니다.
CREATE TABLE orders_part (
order_id NUMBER,
customer_id NUMBER,
order_dt DATE NOT NULL,
amount NUMBER(12, 2)
)
PARTITION BY RANGE (order_dt)
INTERVAL (NUMTOYMINTERVAL(1, 'MONTH')) -- 월별 자동 생성
(
PARTITION p_init VALUES LESS THAN (DATE '2026-01-01')
);
-- 12개월 지난 데이터를 파티션째 제거 (초 단위로 끝납니다)
ALTER TABLE orders_part DROP PARTITION FOR (DATE '2025-01-15');INTERVAL 을 쓰면 새 기간의 데이터가 들어올 때 파티션이 자동으로 만들어집니다. 매달 파티션을 미리 추가하는 운영 작업이 사라집니다.
파티셔닝은 Oracle Enterprise Edition의 별도 옵션입니다. Standard Edition에는 없습니다. 실습용 Free 에디션에서 동작하더라도 운영 라이선스와는 별개이므로, 도입을 검토할 때는 현재 계약 범위를 반드시 먼저 확인합니다.
이관은 검증까지가 작업입니다
데이터를 옮기는 것보다 제대로 옮겨졌는지 확인하는 것이 어렵습니다. 건수만 맞춰 보고 끝내면 값이 틀어진 것을 나중에 발견합니다.
- 건수 대조는 기본입니다. 다만 건수가 같다고 값이 같은 것은 아닙니다.
- Part 4에서 다룬 MINUS 양방향 대사로 실제 값을 비교합니다. 양쪽 모두 0건이어야 합니다.
- 금액이나 수량 컬럼은 합계를 비교합니다. 전체를 비교하기 어려운 대용량에서 빠르게 확인할 수 있습니다.
- NULL 건수를 컬럼별로 대조합니다. Part 2의 빈 문자열 문제가 여기서 드러납니다.
- 날짜 컬럼은 시분초가 잘리지 않았는지 확인합니다. 부록 대조표의 정밀도 항목과 연결됩니다.
- 이관 전 백업과 되돌리는 절차를 먼저 준비합니다. 준비 없이 시작하지 않습니다.
-- 1) 건수
SELECT (SELECT COUNT(*) FROM orders_old) AS old_cnt,
(SELECT COUNT(*) FROM orders_new) AS new_cnt
FROM DUAL;
-- 2) 합계
SELECT (SELECT SUM(amount) FROM orders_old) AS old_sum,
(SELECT SUM(amount) FROM orders_new) AS new_sum
FROM DUAL;
-- 3) NULL 건수 (빈 문자열 문제 확인)
SELECT COUNT(*) - COUNT(status_cd) AS null_status_old FROM orders_old;
SELECT COUNT(*) - COUNT(status_cd) AS null_status_new FROM orders_new;
-- 4) 값 대사 (양방향 모두 0건이어야 함)
SELECT COUNT(*) FROM (
SELECT order_id, status_cd, amount FROM orders_old
MINUS
SELECT order_id, status_cd, amount FROM orders_new
);배치는 다시 돌릴 수 있어야 합니다
배치는 반드시 중간에 실패합니다. 네트워크가 끊기거나 공간이 부족하거나 예상 못 한 데이터가 들어옵니다. 실패를 막는 것보다 실패 후 복구할 수 있게 만드는 것이 중요합니다.
같은 배치를 두 번 돌려도 결과가 같아야 합니다. INSERT만 하면 중복이 쌓입니다. MERGE를 쓰거나 대상 구간을 먼저 지우고 넣습니다.
어디까지 처리했는지 별도 테이블에 남깁니다. 중간 커밋을 한다면 필수입니다.
배치 시작 시점의 대상을 먼저 확정합니다. 도는 동안 새로 들어온 데이터가 섞이면 결과를 설명할 수 없게 됩니다.
처리 건수, 소요 시간, 오류 건수를 남깁니다. 평소 수치를 알아야 이상한 날을 알아챕니다.
대량 처리의 판단은 대부분 같은 질문으로 좁혀집니다. 한 건씩 할 일인가 집합으로 할 일인가, 지울 것이 많은가 남길 것이 많은가, 그 시간에 다른 업무가 도는가. 이 세 가지에 답하면 방법이 정해집니다.