백엔드 데이터 인프라 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(락 없이 테이블을 재작성하는 PG 확장) 확장입니다. 락 없이 디스크 회수가 됩니다.

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;

pg_stat_user_tables(테이블별 죽은 행·살아있는 행 통계 뷰) 에서 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;
-- ...

여기서 WAL(Write-Ahead Log, 변경 기록 로그) 이 폭발하는 게 1억 행 DELETE 의 진짜 비용이에요. 반면 파티션 단위로 "한 번에" 삭제하면 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 PG 확장) 확장은 월별 파티션을 자동 생성하고 옛 파티션도 자동 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(EU 개인정보 보호 규정) 같은 법적 요건도 충족됩니다.

RETURNING 으로 아카이브 동시

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

CTE(Common Table Expression, WITH 임시 결과 집합) 와 RETURNING 을 묶으면 삭제와 아카이브를 한 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

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

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

답글 남기기

error: Content is protected !!