소스: 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번 직접 적어 "끝까지 되풀이" 흐름을 보입니다.
지우기 전에 전체 건수와 대상 건수를 먼저 셉니다. 날짜 컬럼에는 ix_log_hist_date 인덱스를 만들어 두었습니다.
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';TOTAL_CNT | MIN_DATE | MAX_DATE
----------+------------+-----------
5000 | 2024-10-01 | 2026-09-30
(1행)TARGET_CNT
----------
3198
(1행)지울 대상은 3,198건입니다. 이 숫자를 기억해 두면 청크가 끝난 뒤 남은 건수와 합계를 맞춰 볼 수 있습니다.
대상 중 키가 작은 1,500건을 서브쿼리로 골라 지우고 커밋합니다. 같은 문장을 세 번 되풀이하며 청크마다 남은 대상을 봅니다.
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건입니다.
LEFT_AFTER_CHUNK1
-----------------
1698
(1행)같은 DELETE 를 다시 실행합니다.
LEFT_AFTER_CHUNK2
-----------------
198
(1행)세 번째는 남은 198건만 지우고 끝납니다. 청크 크기보다 적어도 있는 만큼만 지웁니다.
LEFT_AFTER_CHUNK3
-----------------
0
(1행)남은 대상이 0건이 되면 반복을 끝냅니다. 실제 프로그램에서는 이 세 묶음이 반복문의 한 회차이고, 삭제된 행이 0건이면 종료 조건으로 삼습니다.
삭제 뒤 전체 건수는 1,802건(5,000 - 3,198)이고 가장 오래된 날짜는 기준일 2026-01-01 이었습니다. 이 레슨은 삭제 속도나 공간을 재지 않고 건수와 날짜 범위로만 확인합니다.
이번에는 지우기 전에 log_archive 로 옮깁니다. 키 범위(id > 0 AND id <= 2000)와 날짜 조건을 INSERT 와 DELETE 에 똑같이 써서 같은 키 집합을 다루고, 둘을 같은 트랜잭션에 둔 채 건수를 확인한 뒤 커밋합니다.
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건입니다.
COPIED_CNT | LEFT_CNT
-----------+---------
1370 | 0
(1행)이 확인이 맞지 않으면 COMMIT 대신 ROLLBACK 하고 원인을 찾습니다. 청크 2(id 20014000)와 청크 3(id 40016000)도 같은 방식이고, 청크 3 의 상한이 6000 이어도 있는 만큼만 처리됩니다.
COPIED_CNT | LEFT_CNT
-----------+---------
1265 | 0
(1행)COPIED_CNT | LEFT_CNT
-----------+---------
563 | 0
(1행)이동 전 원본 5,000건, 보관 0건이었습니다. 이동 뒤 대사합니다.
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;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건도 확인했습니다.
기준일을 2026-06-01 로 바꿔 대부분을 지워야 하는 상황을 만듭니다. 남길 행은 일부입니다.
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;TOTAL_CNT | KEEP_CNT
----------+---------
5000 | 745
(1행)5,000건 중 745건만 남깁니다. 4,255건을 지우는 대신 745건만 새 표로 복사합니다. H2 는 CREATE TABLE ... AS SELECT 를 지원합니다.
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;NEW_CNT
-------
745
(1행)복사한 표에는 PK 와 인덱스가 없습니다. 교체하기 전에 새 표에서 미리 만들어 둡니다. H2 는 CREATE TABLE AS 로 만든 컬럼이 NULL 허용이어서 PK 를 걸기 전에 NOT NULL 로 바꿔야 했습니다.
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';PK_BEFORE
---------
0
(1행)IDX_AFTER
---------
2
(1행)복사 직후 인덱스는 0개였고, PK 와 날짜 인덱스를 만들자 2개가 되었습니다. 이제 이름을 바꿉니다. H2 는 ALTER TABLE ... RENAME TO 로 됩니다.
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;HIST_CNT | MIN_DATE | MAX_DATE
---------+------------+-----------
745 | 2026-06-01 | 2026-09-30
(1행)OLD_CNT
-------
5000
(1행)새 표가 log_hist 라는 원래 이름을 갖고, 옛 표는 log_old 로 남아 있습니다. 옛 표를 지우기 전에 남길 행 수가 일치하는지 마지막으로 확인합니다.
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;OLD_KEEP_CNT | NEW_CNT
-------------+--------
745 | 745
(1행)일치하므로 DROP TABLE log_old 로 옛 표를 지웠습니다. 옛 표를 곧바로 지우지 않고 며칠 두었다가 문제가 없을 때 지우기도 합니다. 이 방식에서 실제 DB 라면 권한, FK, 트리거, 이 표를 참조하는 뷰도 함께 다시 확인해야 합니다.