백엔드 데이터 인프라 27편 — DELETE 깊이 bloat·VACUUM·파티션 DROP

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

백엔드 데이터 인프라 27편. DELETE 깊이 — bloat·autovacuum 동작·파티션 DROP·대량 삭제 안전 패턴 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 27편 — DELETE 깊이 bloat·VACUUM·파티션 DROP

이 글은 백엔드 데이터 인프라 시리즈 70편 중 27편이에요. 14편 DELETE 에서 "기본 + Soft Delete" 를 다뤘으니, 이번 27편은 그 위에 — bloat·VACUUM·파티션 DROP 운영 깊이.

DELETE 내부 — 다시

24편 DML 개요 에서 본 — DELETE 는 "표시만". 실제 디스크 공간은 안 회수. 대량 DELETE 후 — 테이블이 부풀어 있는 상태 (bloat).

-- 1억 행 테이블에서 9000만 행 DELETE
DELETE FROM logs WHERE created_at < '2025-01-01';

-- 디스크 사용량은 그대로! 9000만 행이 "표시"만 됐을 뿐

해결 = VACUUM.

VACUUM — 옛 행 회수

VACUUM logs;

VACUUM = "표시된 행" 의 공간을 "재사용 가능한 빈 공간" 으로 마킹. 디스크 크기는 안 줄어듦 — 새 INSERT 가 그 공간 재사용.

autovacuum — 자동

PG 가 기본으로 autovacuum 활성. 백그라운드에서 주기적 VACUUM. 운영에서 거의 신경 안 써도 OK — 단 큰 테이블은 튜닝 필요.

ALTER TABLE logs SET (
    autovacuum_vacuum_scale_factor = 0.1,    -- 10% 변경 시 트리거
    autovacuum_vacuum_threshold = 50000      -- 최소 5만 행 변경
);

기본은 "20% 변경 + 50행" — 큰 테이블은 너무 늦게 트리거.

VACUUM FULL — 디스크 회수

VACUUM FULL logs;

테이블을 재작성 — 빈 공간 없이 다시 작성. 디스크 크기 실제 줄어듦. 단 — 완전 락. 운영 중엔 위험.

대안 = pg_repack 확장. 락 없이 디스크 회수.

bloat 모니터링

SELECT
    schemaname, relname,
    n_dead_tup,                      -- 죽은 행
    n_live_tup,                       -- 살아있는 행
    ROUND(100.0 * n_dead_tup / NULLIF(n_dead_tup + n_live_tup, 0), 2) AS dead_pct
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;

dead_pct > 20% = bloat 의심. autovacuum 튜닝 또는 수동 VACUUM.

파티션 DROP — 대량 삭제의 우아한 방법

22편 PARTITION TABLE 의 진가.

-- 1억 행 DELETE = 30분 + WAL 폭발
DELETE FROM logs WHERE created_at < '2025-01-01';

-- 파티션 DROP = 0.1초
DROP TABLE logs_2024_01;
DROP TABLE logs_2024_02;
-- ...

파티션 단위로 "한 번에" 삭제. bloat 0, WAL 0, 락 0. 시계열 데이터의 표준 패턴.

자동화 — pg_partman

SELECT partman.create_parent(
    p_parent_table => 'public.logs',
    p_control => 'created_at',
    p_interval => '1 month',
    p_premake => 12             -- 12개월 미리 생성
);

pg_partman 확장 = 월별 파티션 자동 생성·옛 파티션 자동 DROP. 한국 회사 운영에 자주.

대량 DELETE chunk 패턴

파티션 없는 경우 — chunk 분할 + autocommit.

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;
        PERFORM pg_sleep(0.1);   -- 시스템 부담 양보
    END LOOP;
END $$;
  • chunk 10000 = 메모리·WAL 적정
  • COMMIT 마다 디스크 동기화
  • pg_sleep = 다른 작업 영향 최소
  • 인덱스 SKIP LOCKED 가능

TRUNCATE 의 깊이

TRUNCATE TABLE logs;

DELETE 와 차이는 14편 에서. 깊이 — 외래 키 참조 있으면 거부:

TRUNCATE TABLE users;
-- ERROR: cannot truncate a table referenced in a foreign key constraint

해결 — CASCADE:

TRUNCATE TABLE users CASCADE;
-- users 와 그 자식 테이블 모두 비움 (위험!)

TRUNCATE 트랜잭션

PG TRUNCATE 는 트랜잭션 안에서 동작. 다른 RDBMS (Oracle 등) 와 차이.

BEGIN;
TRUNCATE TABLE logs;
SELECT COUNT(*) FROM logs;   -- 0
ROLLBACK;
SELECT COUNT(*) FROM logs;   -- 원상 복귀

PG 운영의 큰 강점.

Soft Delete 깊이

14편 의 Soft Delete 패턴 — 운영 깊이.

부분 인덱스 적극 활용

-- 활성 사용자에만 인덱스
CREATE INDEX idx_users_email_active ON users(email)
WHERE deleted_at IS NULL;

-- 활성 사용자만 UNIQUE
CREATE UNIQUE INDEX uniq_users_email_active ON users(email)
WHERE deleted_at IS NULL;

부분 인덱스 = 작은 크기·빠른 갱신. Soft Delete 의 핵심.

진짜 삭제 정책

-- 90일 이상 지난 Soft Deleted = 진짜 삭제
DELETE FROM users
WHERE deleted_at IS NOT NULL
AND deleted_at < NOW() - INTERVAL '90 days';

스케줄러로 매일 batch. GDPR·법적 요건도 충족.

RETURNING 으로 아카이브 동시

WITH archived AS (
    DELETE FROM logs WHERE created_at < '2025-01-01'
    RETURNING *
)
INSERT INTO logs_archive SELECT * FROM archived;

삭제 + 아카이브 한 SQL.

ON DELETE CASCADE 운영 함정

CREATE TABLE orders (
    user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
    ...
);

DELETE FROM users WHERE id = 1;
-- → orders 도 자동 삭제 (수십~수만 건 가능)

CASCADE 박힌 채로 — 한 사용자 삭제가 수만 건 주문 삭제 → WAL 폭발 + 락 폭주. 운영 = NO ACTION + 앱 코드 단계적 삭제.

함정 5가지

(1) 대량 DELETE 후 VACUUM 안 함

테이블 부풀어 — 쿼리 느려짐. ANALYZE + VACUUM 또는 autovacuum 대기.

(2) bloat 모니터링 X

pg_stat_user_tables 안 보면 — bloat 알아채는 데 늦음. 주기적 점검.

(3) 파티션 안 만들고 DELETE

시계열 = 파티션이 정답. 처음부터 PARTITION BY RANGE.

(4) VACUUM FULL 운영 중

완전 락. 운영 중 절대 X. 대안 = pg_repack.

(5) CASCADE 박힌 채 대량 부모 삭제

WAL·락 폭주. NO ACTION + 코드 단계적.

🎯 DELETE 운영 4룰

(1) 시계열 = 파티션 DROP. (2) 대량 = chunk + autocommit + pg_sleep. (3) Soft Delete + 90일 진짜 삭제 batch. (4) autovacuum 튜닝 + bloat 모니터링. CASCADE 신중.

한 줄 정리 — DELETE 내부 = 표시만. bloat 누적. autovacuum + 큰 테이블은 튜닝. 시계열 = 파티션 DROP 표준. 대량 = chunk + autocommit. Soft Delete + 90일 batch + 부분 인덱스. CASCADE = NO ACTION + 코드.

시험 직전 한 번 더 — DELETE 깊이 입문자가 매번 헷갈리는 것

  • DELETE 내부 = 옛 행 표시 (디스크 안 회수)
  • bloat = 죽은 행 누적
  • VACUUM = 빈 공간 마킹 (재사용)
  • autovacuum = 자동 (기본 활성)
  • 큰 테이블 = autovacuum_vacuum_scale_factor 튜닝
  • VACUUM FULL = 디스크 실제 회수 (완전 락 — 운영 X)
  • pg_repack = 락 없는 디스크 회수
  • bloat 모니터링 = pg_stat_user_tables.n_dead_tup
  • dead_pct > 20% = autovacuum 검토
  • 파티션 DROP = 대량 삭제 우아한 방법
  • 1억 행 DELETE 30분 vs 파티션 DROP 0.1초
  • pg_partman = 파티션 자동 관리
  • 시계열 = RANGE 파티션 + 자동 DROP
  • 대량 DELETE = chunk + autocommit 루프
  • chunk 10000 + pg_sleep
  • TRUNCATE = 빠름 (FK 없을 때)
  • TRUNCATE CASCADE = 위험
  • PG TRUNCATE = 트랜잭션 안 (PG 강점)
  • Soft Delete + 부분 인덱스 = 활성만 unique
  • 90일 batch 진짜 삭제 (GDPR)
  • RETURNING + CTE = 삭제 + 아카이브 한 SQL
  • CASCADE 운영 위험 = NO ACTION + 코드 단계적
  • ANALYZE 대량 DELETE 후
  • 인덱스 = DELETE 비용 증가
  • Hibernate @SQLDelete + @Where = JPA Soft Delete

시리즈 다른 편

시리즈 다음 글

다음 글(28편)에서는 쿼리 개요 — SELECT 의 전체 구조와 표현 능력.

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

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

답글 남기기

error: Content is protected !!