백엔드 데이터 인프라 24편 — DML 개요 4가지 동사 + MVCC 통합

2026-05-17백엔드 데이터 인프라

백엔드 데이터 인프라 24편. DML 4가지 동사의 큰 그림 — INSERT·UPDATE·DELETE·MERGE와 MVCC·RETURNING·CTE 통합 패턴 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 24편 — DML 개요 4가지 동사 + MVCC 통합

이 글은 백엔드 데이터 인프라 시리즈 70편 중 24편이에요. 10·13·14편 에서 INSERT·UPDATE·DELETE 각각을 다뤘으니, 이번 24편은 그 위에 — 4가지 동사 통합 + MVCC·WAL·트랜잭션과의 연관.

DML 의 큰 그림

DML (Data Manipulation Language) = 데이터 조작 언어. 4가지 동사:

동사 의미 깊이
INSERT 새 행 생성 10편
UPDATE 기존 행 수정 13편
DELETE 행 삭제 14편
MERGE UPSERT (PG 15+) 8편

이번 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;

내부 동작: 1. 옛 행 (name = 'Old') 에 "트랜잭션 X 이후 무효" 표시 2. 새 행 (name = 'New') 추가 3. 인덱스 새 위치 가리키도록 (HOT 일 땐 인덱스 갱신 X)

결과 — 같은 행이 일시적으로 "두 버전" 존재. 다른 트랜잭션은 옛 버전·새 버전 중 자기 시점에 맞는 것만 봄.

DELETE 도 "표시만"

DELETE FROM users WHERE id = 1;

내부: 1. 행에 "트랜잭션 X 이후 삭제" 표시 2. 실제 디스크에서는 안 사라짐 3. 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과 결합.

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).

(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 원하면 수동.

🎯 DML 통합 패턴

(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 루프 = 운영 표준

시리즈 다른 편

시리즈 다음 글

다음 글(25편)에서는 INSERT 깊이 — Bulk·COPY 성능·UPSERT 전략·트리거 영향.

공식 문서: PostgreSQL 18 — DML에서 더 자세한 사양을 확인할 수 있어요.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!