백엔드 데이터 인프라 27편. DELETE 깊이 — bloat·autovacuum 동작·파티션 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 + 코드 단계적.
(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
시리즈 다른 편
- Part 2 SQL Language 깊이: 21편 · 22편 · 23편 · 24편 DML 개요 · 25편 INSERT 깊이 · 26편 UPDATE 깊이 · 27편 (현재 글)
시리즈 다음 글
다음 글(28편)에서는 쿼리 개요 — SELECT 의 전체 구조와 표현 능력.
공식 문서: PostgreSQL 18 — DML: Deleting Data에서 더 자세한 사양을 확인할 수 있어요.