백엔드 데이터 인프라 24편. DML 4가지 동사의 큰 그림 — INSERT·UPDATE·DELETE·MERGE와 MVCC·RETURNING·CTE 통합 패턴 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 70편 중 24편이에요. 10·13·14편 에서 INSERT·UPDATE·DELETE 각각을 다뤘으니, 이번 24편은 그 위에 — 4가지 동사 통합 + MVCC·WAL·트랜잭션과의 연관.
DML 의 큰 그림
DML (Data Manipulation Language) = 데이터 조작 언어. 4가지 동사:
이번 24편은 "이 4동사가 PG 내부에서 어떻게 동작하나" 깊이.
PG 의 DML 내부 동작 — MVCC 기반
PG의 모든 DML 은 MVCC (Multi-Version Concurrency Control) 위에서 동작. 38편에서 깊이 다루지만 — 기본 이해.
UPDATE 는 "새 행 만들기"
직관적 이해와 다르게 — PG의 UPDATE는 "기존 행 수정" 이 아니에요. 새 행 만들고 옛 행 표시:
UPDATE users SET name = 'New' WHERE id = 1;
내부에선 옛 행 (name = 'Old') 에 "트랜잭션 X 이후 무효" 표시가 박히고, 새 행 (name = 'New') 을 추가한 뒤, 인덱스가 새 위치를 가리키게 한다 (HOT — Heap-Only Tuple, 인덱스 컬럼이 안 바뀌면 인덱스 갱신을 건너뛰는 최적화 — 일 땐 인덱스 갱신 X).
결과 — 같은 행이 일시적으로 "두 버전" 존재. 다른 트랜잭션은 옛 버전·새 버전 중 자기 시점에 맞는 것만 봄.
DELETE 도 "표시만"
DELETE FROM users WHERE id = 1;
내부에선 행에 "트랜잭션 X 이후 삭제" 표시만 박힘. 실제 디스크에서는 안 사라지고 — autovacuum 이 나중에 회수한다.
이게 "부풀어 오르는 테이블" (bloat) 의 원인. 41편 성능 팁에서 다룸.
INSERT 만 단순
INSERT INTO users (name) VALUES ('New');
새 행 추가 — "옛 버전" 없으니 단순.
트랜잭션과 DML
한 트랜잭션 안 여러 DML
BEGIN;
INSERT INTO orders (...);
UPDATE inventory SET stock = stock - 1 WHERE ...;
INSERT INTO order_logs (...);
COMMIT;
모두 같은 "트랜잭션 시각" 으로 박힘. 다른 트랜잭션이 보는 "이 트랜잭션의 결과" 는 — COMMIT 시점에 한 번에 노출.
Read-Modify-Write 패턴
BEGIN;
-- 1. 현재 잔액 확인 (락)
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- 50000
-- 2. 비즈니스 로직 (앱)
-- if balance >= 10000
-- 3. UPDATE
UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
COMMIT;
FOR UPDATE 락이 — 다른 트랜잭션의 동시 UPDATE 차단. 17편 트랜잭션 참고.
RETURNING — 모든 DML 에서
10·13·14편 의 RETURNING 통합:
INSERT INTO users (name) VALUES ('Alice') RETURNING id, created_at;
UPDATE users SET name = 'New' WHERE id = 1 RETURNING id, name, updated_at;
DELETE FROM users WHERE id = 1 RETURNING id, name, email;
세 동사 모두 — "변경된 행을 결과로". PG 특별. MySQL·다른 RDBMS는 별도 SELECT 필요.
MERGE 와 RETURNING (PG 17+)
MERGE INTO users target
USING (VALUES (1, 'Alice')) AS source(id, name)
ON target.id = source.id
WHEN MATCHED THEN UPDATE SET name = source.name
WHEN NOT MATCHED THEN INSERT (id, name) VALUES (source.id, source.name)
RETURNING merge_action(), target.*;
merge_action() = INSERT·UPDATE·DELETE 중 무엇이 일어났는지.
CTE (WITH) + DML — PG 강점
18편 윈도우·CTE 의 WITH가 DML과 결합. CTE (Common Table Expression) — 쿼리 안 임시 결과 집합.
INSERT + INSERT 체이닝
WITH new_user AS (
INSERT INTO users (name, email)
VALUES ('Alice', 'alice@example.com')
RETURNING id
)
INSERT INTO user_logs (user_id, action)
SELECT id, 'SIGNUP' FROM new_user;
한 SQL로 다단계 INSERT. "자동 ID 받아 다음 INSERT" 표준 패턴.
UPDATE + INSERT (이력 보존)
WITH updated AS (
UPDATE products SET price = price * 1.1
WHERE category = 'electronics'
RETURNING id, price AS new_price
)
INSERT INTO price_changes (product_id, new_price, changed_at)
SELECT id, new_price, NOW() FROM updated;
가격 변경 + 변경 이력 한 SQL로.
DELETE + INSERT (아카이브)
WITH archived AS (
DELETE FROM users WHERE deleted_at < NOW() - INTERVAL '1 year'
RETURNING *
)
INSERT INTO archive_users SELECT * FROM archived;
오래된 사용자 — 삭제 동시에 아카이브 테이블로 이전.
통계 갱신 — DML 후
대량 DML 후 — 통계가 부정확해질 수 있음. 40편 EXPLAIN 의 쿼리 계획자가 부정확한 통계로 잘못 판단.
DELETE FROM logs WHERE created_at < '2025-01-01';
-- 100만 행 삭제
ANALYZE logs; -- 통계 갱신
ANALYZE 명령으로 수동 갱신. 또는 autovacuum 이 자동 (기본 활성).
WAL 영향 — Write-Ahead Log
모든 DML 은 WAL 에 먼저 기록 → 디스크 적용. 영속성 (ACID — 원자성·일관성·고립성·영속성 — 의 D) 의 메커니즘.
1. INSERT/UPDATE/DELETE 발생
2. 변경 내용 WAL에 기록 (디스크 fsync)
3. 메모리 페이지 수정 (Shared Buffer)
4. 백그라운드 writer 가 페이지 디스크 적용 (나중)
WAL 덕분에 — 크래시 시에도 커밋된 변경 복구. 단 — 대량 DML 은 WAL 폭발. UNLOGGED 테이블이 "WAL 안 쓰는 빠른 옵션".
DML 안티패턴
(1) 큰 트랜잭션
BEGIN;
DELETE FROM logs WHERE created_at < '2025-01-01'; -- 1억 행
COMMIT;
1억 행 한 번에 = WAL 폭발·락·메모리 폭주. chunk 분할.
DO $$
DECLARE
deleted INTEGER;
BEGIN
LOOP
DELETE FROM logs WHERE id IN (
SELECT id FROM logs WHERE created_at < '2025-01-01' LIMIT 10000
);
GET DIAGNOSTICS deleted = ROW_COUNT;
EXIT WHEN deleted = 0;
COMMIT;
END LOOP;
END $$;
(2) 인덱스 무시 UPDATE·DELETE
UPDATE users SET name = 'X' WHERE LOWER(email) = 'alice@example.com';
-- LOWER() 가 인덱스 못 씀 — 풀스캔
citext (대소문자 무시 텍스트 타입) 또는 함수 인덱스로 해결.
(3) 외부 호출 트랜잭션 안
17편 트랜잭션 의 함정. 외부 API = 트랜잭션 밖 (이벤트·Outbox — DB 트랜잭션과 함께 메시지를 같은 테이블에 박고 별도 워커가 발행하는 패턴).
(4) 자동 커밋 의존
-- 자동 커밋 모드 — 각 SQL 개별 트랜잭션
DELETE FROM logs WHERE id = 1;
DELETE FROM order_logs WHERE log_id = 1; -- ❌ 첫 번째 실패해도 두 번째 실행
명시적 BEGIN·COMMIT·ROLLBACK.
(5) DELETE 후 통계 갱신 안 함
ANALYZE 또는 autovacuum 대기.
Spring JPA 의 DML 매핑
자바 백엔드 입문 47편 영속성 컨텍스트 의 마법 — JPA 가 4동사를 어떻게 자동 생성하나:
| JPA 호출 | 자동 SQL |
|---|---|
repo.save(new User(...)) |
INSERT + RETURNING id |
repo.findById(1).get().setName("X") (트랜잭션 안) |
UPDATE (Dirty Checking) |
repo.deleteById(1) |
DELETE |
repo.saveAll(list) |
INSERT Bulk |
직접 SQL 모르면 — "JPA가 박는 SQL이 효율적인가" 못 봄. 자바 백엔드의 깊이.
함정 5가지
(1) UPDATE 가 진짜 "수정" 인 줄
내부는 "새 행 + 옛 행 표시". autovacuum 으로 옛 행 회수.
(2) DELETE 가 즉시 공간 회수
옛 행 표시만 — 디스크 회수는 autovacuum 또는 VACUUM FULL.
(3) RETURNING 활용 안 함
INSERT·UPDATE·DELETE 모두 RETURNING. SELECT 분리 호출 줄임.
(4) CTE 미사용
큰 시스템의 다단계 작업 = CTE + DML 한 SQL. 가독성·성능 모두 우수.
(5) 통계 잊음
대량 DML 후 ANALYZE. autovacuum 의존도 가능하지만 — 즉시 정확한 EXPLAIN 원하면 수동.
(1) BEGIN-DML-COMMIT 묶음. (2) RETURNING 으로 결과 즉시. (3) CTE 로 다단계 한 SQL. (4) 대량 DML = chunk. (5) 후속 ANALYZE. 이 5가지가 PG 운영 표준.
한 줄 정리 — DML 4동사 (INSERT·UPDATE·DELETE·MERGE) + MVCC 위 동작. UPDATE/DELETE 는 옛 버전 표시·새 버전 추가. RETURNING + CTE + 트랜잭션 통합 = PG 강점. 대량 DML = chunk + autovacuum + ANALYZE.
시험 직전 한 번 더 — DML 통합 입문자가 매번 헷갈리는 것
- DML 4동사 = INSERT·UPDATE·DELETE·MERGE
- 모두 MVCC 위 동작
- UPDATE 내부 = 옛 행 표시 + 새 행 추가
- DELETE 내부 = 표시만 (autovacuum 회수)
- INSERT 만 단순
- bloat = 옛 버전 누적 → 테이블 부풀음
- autovacuum = 자동 청소
- WAL = 모든 DML 디스크 먼저 기록
- RETURNING = 모든 DML 결과 즉시
- MERGE + RETURNING (PG 17+) = merge_action()
- CTE + DML = 다단계 한 SQL
- INSERT + RETURNING + 후속 INSERT
- UPDATE + INSERT (이력 보존)
- DELETE + INSERT (아카이브)
- 대량 DML = chunk 분할
- 1억 행 = WAL 폭발 = chunk 1만씩
- 외부 API = 트랜잭션 밖
- 자동 커밋 의존 위험
- ANALYZE = 대량 DML 후 통계 갱신
- autovacuum 자동
- Read-Modify-Write = FOR UPDATE 락
- JPA Dirty Checking = 트랜잭션 안 자동 UPDATE
- @Modifying @Query = JPA Bulk DML
- LOWER() in WHERE = 인덱스 X (citext)
- chunk + autocommit 루프 = 운영 표준
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 19편 — PostgreSQL SQL 어휘 구조
- 20편 — PostgreSQL SQL 문법 전반
- 21편 — DDL 개요 데이터 정의 언어 전체 그림
- 22편 — CREATE TABLE 깊이 PARTITION·UNLOGGED·TEMPORARY
- 23편 — 제약 깊이 CHECK DEFERRABLE EXCLUDE
다음 글: