작업 큐로 쓰는 job_queue 테이블이 INSERT 후 처리되면 DELETE 하는 구조인데, 실제 남아있는 행은 얼마 없는데도 테이블 디스크 사용량이 계속 커지고 SELECT count(*)도 갈수록 느려집니다.
SELECT relname, n_live_tup, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables WHERE relname = 'job_queue';
relname | n_live_tup | n_dead_tup | last_autovacuum
-----------+------------+------------+---------------------
job_queue | 12000 | 4200000 | 2026-08-01 03:11보시면 살아있는 행은 1만 2천인데 dead tuple이 420만이에요. last_autovacuum도 열흘 넘게 안 돌았고요. autovacuum이 알아서 청소해준다고 알고 있었는데 왜 안 도는 건지, 그리고 이미 부풀어버린 용량은 어떻게 줄이는지 궁금합니다.
댓글 1
Postgres는 어떤 행이 삭제돼도, 그 행이 아직 열려 있는 오래된 트랜잭션의 스냅샷에서 보일 수 있으면 물리적으로 못 지웁니다. 그래서 다음을 먼저 의심하세요. 첫째, 오래 살아있는 트랜잭션. pg_stat_activity에서 idle in transaction 이나 xact_start가 아주 옛날인 세션이 있는지 보세요. 이런 세션 하나가 xmin horizon을 붙잡고 있으면 그 이후 죽은 튜플을 통째로 못 치웁니다. 둘째, 안 쓰는 replication slot. pg_replication_slots에 active=false인 슬롯이 남아 있으면 마찬가지로 청소를 막습니다. 이 둘 중 하나가 원인인 경우가 정말 많아요.
또 하나, VACUUM은 죽은 공간을 '재사용 가능'으로 표시할 뿐 OS로 디스크를 돌려주진 않습니다. 그래서 이미 부푼 물리 용량을 줄이려면 VACUUM FULL(테이블 잠금, 서비스 중엔 부담) 이나 pg_repack(온라인으로 재작성) 을 써야 해요. count(*)가 느린 것도 결국 이 죽은 페이지까지 다 스캔하기 때문이고요.
근본 대책으로는, job_queue처럼 churn이 심한 테이블은 autovacuum_vacuum_scale_factor를 이 테이블만 낮게(예: 0.01) 잡아서 훨씬 자주 청소되게 하세요. ALTER TABLE job_queue SET (autovacuum_vacuum_scale_factor = 0.01) 식으로요. 더 나아가면 큐를 날짜/상태로 파티셔닝해서 DELETE 대신 파티션 DROP/TRUNCATE로 비우는 구조가 이런 bloat에서 자유롭습니다. DELETE를 대량으로 반복하는 큐 패턴 자체가 Postgres랑은 궁합이 안 좋아요.
우선 pg_stat_activity랑 replication slot부터 확인해 보시고, 뭐가 xmin을 잡고 있었는지 알려주시면 더 좁혀 드릴게요.