소스: sql-src/batch_06_bulk_dml/01_set_based.sql, 02_chunked.sql, 03_verify.sql.
이 절의 sql 은 모두 H2 로 실행한 평문 SQL 입니다. 원천은 staging 4,000행이고 재귀 CTE 로 만들었으며, 대상은 target 입니다. H2 평문 SQL 에는 반복문이 없어서 나눠 처리하는 문장을 직접 2~3번 되풀이해 "한 번에 N건씩 끝까지" 흐름을 보입니다.
한 행씩 넣는 형태는 아래와 같습니다. 예시로 3건만 넣었고, 실제 프로그램에서는 이 문장을 애플리케이션 루프가 4,000번 반복합니다.
INSERT INTO target_row (id, cust_id, amt, status_code, created) VALUES (1, 2, 200, 'B', DATE '2025-01-02');
INSERT INTO target_row (id, cust_id, amt, status_code, created) VALUES (2, 3, 300, 'C', DATE '2025-01-03');
INSERT INTO target_row (id, cust_id, amt, status_code, created) VALUES (3, 4, 400, 'A', DATE '2025-01-04');ROW_BY_ROW_CNT
--------------
3
(1행)같은 일을 한 문장으로 하면 4,000건이 한 번에 들어갑니다.
INSERT INTO target (id, cust_id, amt, status_code, created)
SELECT
id
, cust_id
, amt
, status_code
, created
FROM staging;TARGET_CNT
----------
4000
(1행)문장 수는 4,000 대 1 이고 왕복·파싱·커밋도 그만큼 줄어듭니다. 이 레슨은 속도 숫자를 재지 않으므로 건수만 확인합니다.
값을 직접 넣을 때는 여러 행 VALUES 로 한 문장에 씁니다. H2 에서 되는 형태이고 MySQL·MSSQL(2008 이상)에서도 같은 문법입니다.
INSERT INTO code_map (code, label) VALUES
('A', '정상')
, ('B', '보류')
, ('C', '취소');CODE | LABEL
-----+------
A | 정상
B | 보류
C | 취소
(3행)집합 UPDATE 는 CASE 로 여러 규칙을 한 문장에 담습니다. 금액에 따라 등급을 매기고 취소 건은 수수료를 0 으로 합니다.
UPDATE target
SET grade = CASE WHEN amt >= 7000 THEN 'A' WHEN amt >= 3000 THEN 'B' ELSE 'C' END
, fee = CASE WHEN status_code = 'C' THEN 0 WHEN amt >= 7000 THEN amt / 100 ELSE amt / 50 END
WHERE grade IS NULL;4,000행이 한 문장으로 바뀌었고, 등급별로 묶어 확인합니다.
SELECT
grade
, COUNT(*) AS cnt
, SUM(fee) AS fee_sum
FROM target
GROUP BY grade
ORDER BY grade;GRADE | CNT | FEE_SUM
------+------+--------
A | 1148 | 63961
B | 1640 | 108194
C | 1212 | 24194
(3행)1148 + 1640 + 1212 는 4,000 이고, grade IS NULL 인 행은 0건입니다. WHERE 의 grade IS NULL 은 이미 처리된 행을 건너뛰게 해 재실행을 안전하게 만듭니다.
이제 4,000건을 1,500건씩 나눠 적재합니다. 범위는 id > :last AND id <= :last + 1500 이고, H2 평문 SQL 에서는 :last 자리에 값을 직접 씁니다. 청크마다 진행 표에 마지막 키를 기록하고 COMMIT 합니다.
CREATE TABLE batch_progress (job_name VARCHAR(20) PRIMARY KEY, last_id INT, done_cnt INT);
INSERT INTO batch_progress VALUES ('load_target', 0, 0);청크 1 은 id 가 0 초과 1500 이하인 행입니다.
INSERT INTO target (id, cust_id, amt, status_code, created)
SELECT id, cust_id, amt, status_code, created FROM staging WHERE id > 0 AND id <= 1500;
UPDATE batch_progress SET last_id = 1500, done_cnt = done_cnt + 1500 WHERE job_name = 'load_target';
COMMIT;LAST_ID | DONE_CNT
--------+---------
1500 | 1500
(1행)청크 2 는 시작점을 진행 표의 last_id(1500)로 잡아 1500 초과 3000 이하를 넣고 같은 방식으로 기록합니다.
LAST_ID | DONE_CNT
--------+---------
3000 | 3000
(1행)청크 3 은 3000 초과 4500 이하이지만 원천에는 4000번까지만 있어 남은 1,000건이 들어갑니다. 마지막 청크가 작아도 같은 문장으로 끝납니다.
LAST_ID | DONE_CNT
--------+---------
4500 | 4000
(1행)여기서 last_id 는 상한 값(4500)이고 done_cnt 는 실제로 처리한 건수입니다. 세 청크가 끝난 뒤 대상 표를 확인합니다.
SELECT COUNT(*) AS target_cnt, MIN(id) AS min_id, MAX(id) AS max_id FROM target;TARGET_CNT | MIN_ID | MAX_ID
-----------+--------+-------
4000 | 1 | 4000
(1행)실제 프로그램에서는 이 세 묶음이 반복문의 한 회차이고, 청크가 0건이 되면 반복이 끝납니다.
이번에는 target 의 processed_yn 컬럼으로 미처리(N)만 1,500건씩 갱신합니다. 갱신할 행의 키를 서브쿼리로 골라 FETCH FIRST 로 건수를 제한하고, 갱신과 동시에 processed_yn 을 'Y' 로 바꿉니다.
UPDATE target
SET grade = CASE WHEN amt >= 7000 THEN 'A' WHEN amt >= 3000 THEN 'B' ELSE 'C' END
, processed_yn = 'Y'
WHERE id IN (
SELECT id
FROM target
WHERE processed_yn = 'N'
ORDER BY id
FETCH FIRST 1500 ROWS ONLY
);
COMMIT;시작 전 미처리는 4,000건이었습니다. 같은 문장을 세 번 되풀이하며 청크마다 남은 건수를 봅니다.
TODO_BEFORE
-----------
4000
(1행)TODO_AFTER_CHUNK1
-----------------
2500
(1행)TODO_AFTER_CHUNK2
-----------------
1000
(1행)세 번째 청크는 남은 1,000건만 갱신하고 끝납니다.
TODO_AFTER_CHUNK3
-----------------
0
(1행)미처리가 0건이 되면 반복을 끝냅니다. 이 방식은 중간에 멈춰도 processed_yn 이 진행 상황을 가지고 있어서, 같은 문장을 다시 돌리기만 하면 이어집니다.
끝난 뒤 같은 UPDATE 를 다시 돌려도 미처리 건수(rerun_todo)는 0 이라 처리할 행이 없어서 아무것도 바뀌지 않습니다. 멱등성이 지켜진 것입니다.
일부러 마지막 청크가 빠진 채 끝난 상태를 만들었습니다. 3,500건까지만 적재된 상태에서 건수와 금액 합계를 대조합니다. 실무 09 이관·검증의 대사 방식과 같습니다.
SELECT
(SELECT COUNT(*) FROM staging) AS src_cnt
, (SELECT COUNT(*) FROM target) AS tgt_cnt
, (SELECT SUM(amt) FROM staging) AS src_amt
, (SELECT SUM(amt) FROM target) AS tgt_amt;SRC_CNT | TGT_CNT | SRC_AMT | TGT_AMT
--------+---------+----------+---------
4000 | 3500 | 19517200 | 17115200
(1행)건수와 합계가 모두 다르므로 대상에 빠진 행이 있습니다. 누락 건수는 NOT EXISTS 로 셉니다.
SELECT COUNT(*) AS missing_cnt
FROM staging s
WHERE NOT EXISTS (SELECT 1 FROM target t WHERE t.id = s.id);MISSING_CNT
-----------
500
(1행)누락분만 다시 적재합니다. 이미 있는 키는 NOT EXISTS 로 건너뛰므로 이 문장은 여러 번 돌려도 같은 결과입니다.
INSERT INTO target (id, cust_id, amt, status_code, created)
SELECT s.id, s.cust_id, s.amt, s.status_code, s.created
FROM staging s
WHERE NOT EXISTS (SELECT 1 FROM target t WHERE t.id = s.id);500건이 들어갔고 다시 대사합니다.
SRC_CNT | TGT_CNT | SRC_AMT | TGT_AMT
--------+---------+----------+---------
4000 | 4000 | 19517200 | 19517200
(1행)키 중복 건수(dup_keys), processed_yn = 'N' 인 행 수(todo_cnt)와 grade IS NULL 인 행 수(grade_null)도 모두 0 이었습니다. 대사 통과의 기준은 건수·합계 일치, 누락 0, 중복 0, 미처리 0 입니다.