공공부하자개발 · 영어 학습 노트
SQL
대용량·배치실행 계획·조인·인덱스·파티션·락·대량 처리0/8 완료
  • 01실행 계획 읽기
  • 02조인 방식: Nested Loop, Hash, Sort Merge 와 드라이빙 테이블
  • 03인덱스 튜닝
  • 04통계 정보와 힌트
  • 05파티션: 범위·목록·해시 분할, 파티션 프루닝, 파티션 단위 관리
  • 06대량 INSERT·UPDATE 와 배치 커밋
  • 07락과 데드락
  • 08대용량 삭제·아카이빙
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 대용량·배치 › 08 / 8

대용량 삭제·아카이빙

나눠서 삭제, 남길 것만 옮기기, 보관 테이블
섹션 6진행 0 / 8
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6정리‹ 이전다음 ›

3. 코드 예제

소스: sql-src/batch_08_purge_archive/01_chunked_delete.sql, 02_archive_move.sql, 03_keep_and_swap.sql.

이 절의 sql 은 모두 H2 로 실행한 평문 SQL 입니다. 데이터는 log_hist 5,000행이고 재귀 CTE 로 만들었습니다. 날짜는 2024-10-01 부터 2년치(730일 주기)에 걸쳐 있고, 기준일은 2026-01-01 입니다. H2 평문 SQL 에는 반복문이 없어서 청크 문장을 2~3번 직접 적어 "끝까지 되풀이" 흐름을 보입니다.

예제 1: 삭제 대상 확인

지우기 전에 전체 건수와 대상 건수를 먼저 셉니다. 날짜 컬럼에는 ix_log_hist_date 인덱스를 만들어 두었습니다.

sql
SELECT COUNT(*) AS total_cnt, MIN(log_date) AS min_date, MAX(log_date) AS max_date FROM log_hist;
SELECT COUNT(*) AS target_cnt FROM log_hist WHERE log_date < DATE '2026-01-01';
text
TOTAL_CNT | MIN_DATE   | MAX_DATE
----------+------------+-----------
5000      | 2024-10-01 | 2026-09-30
(1행)
text
TARGET_CNT
----------
3198
(1행)

지울 대상은 3,198건입니다. 이 숫자를 기억해 두면 청크가 끝난 뒤 남은 건수와 합계를 맞춰 볼 수 있습니다.

예제 2: 1,500건씩 나눠서 삭제

대상 중 키가 작은 1,500건을 서브쿼리로 골라 지우고 커밋합니다. 같은 문장을 세 번 되풀이하며 청크마다 남은 대상을 봅니다.

sql
DELETE FROM log_hist
 WHERE id IN (
       SELECT id
         FROM log_hist
        WHERE log_date < DATE '2026-01-01'
        ORDER BY id
        FETCH FIRST 1500 ROWS ONLY
       );
COMMIT;
SELECT COUNT(*) AS left_after_chunk1 FROM log_hist WHERE log_date < DATE '2026-01-01';

1,500건이 지워졌고 남은 대상은 1,698건입니다.

text
LEFT_AFTER_CHUNK1
-----------------
1698
(1행)

같은 DELETE 를 다시 실행합니다.

text
LEFT_AFTER_CHUNK2
-----------------
198
(1행)

세 번째는 남은 198건만 지우고 끝납니다. 청크 크기보다 적어도 있는 만큼만 지웁니다.

text
LEFT_AFTER_CHUNK3
-----------------
0
(1행)

남은 대상이 0건이 되면 반복을 끝냅니다. 실제 프로그램에서는 이 세 묶음이 반복문의 한 회차이고, 삭제된 행이 0건이면 종료 조건으로 삼습니다.

예제 3: 마무리 확인

삭제 뒤 전체 건수는 1,802건(5,000 - 3,198)이고 가장 오래된 날짜는 기준일 2026-01-01 이었습니다. 이 레슨은 삭제 속도나 공간을 재지 않고 건수와 날짜 범위로만 확인합니다.

예제 4: 보관 표로 이동하고 대사

이번에는 지우기 전에 log_archive 로 옮깁니다. 키 범위(id > 0 AND id <= 2000)와 날짜 조건을 INSERT 와 DELETE 에 똑같이 써서 같은 키 집합을 다루고, 둘을 같은 트랜잭션에 둔 채 건수를 확인한 뒤 커밋합니다.

sql
INSERT INTO log_archive (id, log_date, user_id, msg, archived_at)
SELECT id, log_date, user_id, msg, DATE '2026-09-30'
  FROM log_hist
 WHERE id > 0 AND id <= 2000
   AND log_date < DATE '2026-01-01';
DELETE FROM log_hist
 WHERE id > 0 AND id <= 2000
   AND log_date < DATE '2026-01-01';
SELECT
       (SELECT COUNT(*) FROM log_archive WHERE id <= 2000) AS copied_cnt
     , (SELECT COUNT(*) FROM log_hist WHERE id <= 2000 AND log_date < DATE '2026-01-01') AS left_cnt;
COMMIT;

INSERT 와 DELETE 모두 1,370건이 처리되었고, 복사된 건수는 1,370건, 원본에 남은 대상은 0건입니다.

text
COPIED_CNT | LEFT_CNT
-----------+---------
1370       | 0
(1행)

이 확인이 맞지 않으면 COMMIT 대신 ROLLBACK 하고 원인을 찾습니다. 청크 2(id 20014000)와 청크 3(id 40016000)도 같은 방식이고, 청크 3 의 상한이 6000 이어도 있는 만큼만 처리됩니다.

text
COPIED_CNT | LEFT_CNT
-----------+---------
1265       | 0
(1행)
text
COPIED_CNT | LEFT_CNT
-----------+---------
563        | 0
(1행)

이동 전 원본 5,000건, 보관 0건이었습니다. 이동 뒤 대사합니다.

sql
SELECT
       (SELECT COUNT(*) FROM log_hist) AS hist_cnt
     , (SELECT COUNT(*) FROM log_archive) AS arch_cnt
     , (SELECT COUNT(*) FROM log_hist) + (SELECT COUNT(*) FROM log_archive) AS sum_cnt;
text
HIST_CNT | ARCH_CNT | SUM_CNT
---------+----------+--------
1802     | 3198     | 5000
(1행)

합계가 이동 전과 같은 5,000건이므로 유실도 중복도 없습니다. 1,370 + 1,265 + 563 = 3,198 로 예제 1 의 대상 건수와도 맞습니다. 02 파일에서는 보관 표의 날짜 범위(2024-10-01~2025-12-31)와 원본과 겹치는 키 0건도 확인했습니다.

예제 5: 남길 행만 새 표로 옮기고 바꿔치기

기준일을 2026-06-01 로 바꿔 대부분을 지워야 하는 상황을 만듭니다. 남길 행은 일부입니다.

sql
SELECT
       COUNT(*) AS total_cnt
     , SUM(CASE WHEN log_date >= DATE '2026-06-01' THEN 1 ELSE 0 END) AS keep_cnt
  FROM log_hist;
text
TOTAL_CNT | KEEP_CNT
----------+---------
5000      | 745
(1행)

5,000건 중 745건만 남깁니다. 4,255건을 지우는 대신 745건만 새 표로 복사합니다. H2 는 CREATE TABLE ... AS SELECT 를 지원합니다.

sql
CREATE TABLE log_new AS
SELECT id, log_date, user_id, msg
  FROM log_hist
 WHERE log_date >= DATE '2026-06-01';
SELECT COUNT(*) AS new_cnt FROM log_new;
text
NEW_CNT
-------
745
(1행)

복사한 표에는 PK 와 인덱스가 없습니다. 교체하기 전에 새 표에서 미리 만들어 둡니다. H2 는 CREATE TABLE AS 로 만든 컬럼이 NULL 허용이어서 PK 를 걸기 전에 NOT NULL 로 바꿔야 했습니다.

sql
SELECT COUNT(*) AS pk_before FROM INFORMATION_SCHEMA.INDEXES WHERE TABLE_NAME = 'LOG_NEW';
ALTER TABLE log_new ALTER COLUMN id SET NOT NULL;
ALTER TABLE log_new ADD PRIMARY KEY (id);
CREATE INDEX ix_log_new_date ON log_new (log_date);
SELECT COUNT(*) AS idx_after FROM INFORMATION_SCHEMA.INDEXES WHERE TABLE_NAME = 'LOG_NEW';
text
PK_BEFORE
---------
0
(1행)
text
IDX_AFTER
---------
2
(1행)

복사 직후 인덱스는 0개였고, PK 와 날짜 인덱스를 만들자 2개가 되었습니다. 이제 이름을 바꿉니다. H2 는 ALTER TABLE ... RENAME TO 로 됩니다.

sql
ALTER TABLE log_hist RENAME TO log_old;
ALTER TABLE log_new RENAME TO log_hist;
SELECT COUNT(*) AS hist_cnt, MIN(log_date) AS min_date, MAX(log_date) AS max_date FROM log_hist;
SELECT COUNT(*) AS old_cnt FROM log_old;
text
HIST_CNT | MIN_DATE   | MAX_DATE
---------+------------+-----------
745      | 2026-06-01 | 2026-09-30
(1행)
text
OLD_CNT
-------
5000
(1행)

새 표가 log_hist 라는 원래 이름을 갖고, 옛 표는 log_old 로 남아 있습니다. 옛 표를 지우기 전에 남길 행 수가 일치하는지 마지막으로 확인합니다.

sql
SELECT
       (SELECT COUNT(*) FROM log_old WHERE log_date >= DATE '2026-06-01') AS old_keep_cnt
     , (SELECT COUNT(*) FROM log_hist) AS new_cnt;
DROP TABLE log_old;
text
OLD_KEEP_CNT | NEW_CNT
-------------+--------
745          | 745
(1행)

일치하므로 DROP TABLE log_old 로 옛 표를 지웠습니다. 옛 표를 곧바로 지우지 않고 며칠 두었다가 문제가 없을 때 지우기도 합니다. 이 방식에서 실제 DB 라면 권한, FK, 트리거, 이 표를 참조하는 뷰도 함께 다시 확인해야 합니다.

예제 직접 실행

아래 폴더의 SQL 파일을 Git Bash 에서 H2 메모리 DB 로 실행합니다. 방언은 파일 안의 SET MODE 로 바꿉니다.

cd sql-src/batch_08_purge_archive
ls *.sql
bash ../run.sh <파일>.sql
코드 예제
  • 예제 1: 삭제 대상 확인
  • 예제 2: 1,500건씩 나눠서 삭제
  • 예제 3: 마무리 확인
  • 예제 4: 보관 표로 이동하고 대사
  • 예제 5: 남길 행만 새 표로 옮기고 바꿔치기
이전 섹션2 핵심 원리3 / 6다음 섹션4 응용 변형 예제