※ PostgreSQL: VACUUM.
※ Version: PostgreSQL 17.
안녕하세요. 듀스트림입니다.
오늘 포스팅은 PostgreSQL의 핵심인 VACUUM입니다.
1. VACUUM
VACUUM은 PostgreSQL에서 데이터베이스의 성능을 유지하고, 디스크 공간을 효율적으로 관리하기 위해 주기적으로 수행해야 하는 유지 보수 작업입니다.
PostgreSQL의 MVCC(Multi-Version Concurrency Control) 시스템에서 중요한 역할을 합니다.
VACUUM 작업은 일반적으로 ACCESS SHARE LOCK을 획득합니다.
SELECT 문과 동일한 락으로, 다른 트랜잭션에서 DML 작업(INSERT, UPDATE, DELETE)을 수행할 수 있습니다.
추가로, VACUUM이 dead tuple을 정리할 때 해당 페이지에 대한 Share Update Exclusive Lock을 획득합니다.
이는 VACUUM을 하는 동안 해당 테이블에 대한 다른 DML 작업이 VACUUM 작업을 완료할 때까지 잠시 대기하게 만들 수 있습니다. (VACUUM은 트랜잭션 ID 래핑을 방지하는 중요 작업이 아닌 이상 중단되지 않습니다.)
하지만 조건부(lock 실패 시 skip or retry)로 동작하므로 VACUUM이 전체 테이블을 홀딩 하는 일은 없습니다.
• VACUUM 작업의 종류
| 명령어 유형 | 설명 |
| VACUUM | Dead tuple 제거, 공간 회수, 힌트 비트/프로즌 비트 설정, FSM/VM 갱신 |
| VACUUM FULL | 테이블 재작성(rewrite) → 디스크 공간 강제 회수, 클러스터링 효과 발생 (ACCESS EXCLUSIVE LOCK) |
| VACUUM FREEZE | 오래된 트랜잭션 튜플의 xmin을 FrozenTransactionId로 설정 |
| VACUUM ANALYZE | VACUUM 후 통계정보까지 갱신 → 옵티마이저 최적화에 도움 |
VACUUM
VACUUM VACUUM — garbage-collect and optionally analyze a database Synopsis VACUUM [ ( option [, ...] ) ] [ table_and_columns …
www.postgresql.org
postgres/src/backend/commands/vacuum.c at master · postgres/postgres
Mirror of the official PostgreSQL GIT repository. Note that this is just a *mirror* - we don't work with pull requests on github. To contribute, please see https://wiki.postgresql.org/wiki/Subm...
github.com
2. VACUUM 내부 처리 로직 (lazy vacuum)
- 스캔 전 처리
- relfrozenxid, relminmxid, FSM/VM 상태 확인
- 페이지 반복 스캔
- dead tuple 수집
- commit된 트랜잭션에 대해 힌트 비트 설정
- visibility map 설정 (모든 튜플이 visible하면 1bit ON)
- 인덱스 정리
- heap-tid 기준 불필요한 인덱스 항목 제거
- parallel vacuum이 가능 (PostgreSQL 13+)
- FSM(Free Space Map) 갱신
- 빈 페이지 또는 여유 공간 있는 블록은 FSM에 기록됨
- 프로즌 비트 처리
- freeze 대상 튜플의 xmin 값을 FrozenTransactionId(=2)로 설정 또는 HEAP_XMIN_FROZEN 비트 설정
- pg_class, pg_database 메타데이터 갱신
- relfrozenxid, datfrozenxid 등 업데이트
postgres/src/backend/access/heap/vacuumlazy.c at master · postgres/postgres
Mirror of the official PostgreSQL GIT repository. Note that this is just a *mirror* - we don't work with pull requests on github. To contribute, please see https://wiki.postgresql.org/wiki/Subm...
github.com
+ 코드를 분석해보면 아래와 같은 흐름으로 동작한다는 것을 확인하실 수 있습니다.
- 전체 플로우 개요 (heap_vacuum_rel)
- 진입점: heap_vacuum_rel(Relation rel, VacuumParams *params, BufferAccessStrategy bstrategy)
- 트랜잭션 시작, 에러 콜백 설정, 통계·락 관리 등 초기화
- lazy_scan_heap() 호출 → Phase I~III 수행
- 인덱스 통계 갱신, 트렁케이트(lazy_truncate_heap), 최종 통계 업데이트
- 진입점: heap_vacuum_rel(Relation rel, VacuumParams *params, BufferAccessStrategy bstrategy)
- Phase I – 힙 페이지 스캔 (lazy_scan_heap)
- 페이지 순차 스캔
- read_stream_begin_relation → heap_vac_scan_next_block 콜백을 통해 다음 처리할 블록 결정
- visibility map·eager scan 로직 (find_next_unskippable_block, heap_vacuum_eager_scan_setup) 으로 스킵 여부 판단
- 락 획득 시도
- ConditionalLockBufferForCleanup 로 cleanup lock 획득 시도 (SHARE UPDATE EXCLUSIVE LOCK 수준의 버퍼 락을 조건부로 시도)
- 실패 시 공유락으로 대체하고 최소한의 통계만 수집 (lazy_scan_noprune)
- 프루닝 & 프리즈
- cleanup lock 획득 시 lazy_scan_prune
- HOT 체인 제거, LP_DEAD 수집, 튜플 freeze, FSM·visibility map 갱신
- 수집된 LP_DEAD 위치(TIDs)는 dead_items_add 로 TidStore 에 저장
- 메모리 한도 도달 시점
- dead_items_info->max_bytes 초과하면 Phase I 일시 중단 후 lazy_vacuum 호출 (Phase II+III)
- FSM 갱신 (FreeSpaceMapVacuumRange)
- 페이지 순차 스캔
- Phase II – 인덱스 VACUUM (lazy_vacuum_all_indexes)
- 호출: lazy_vacuum(LVRelState *vacrel) 내부에서
- 역할:
- lazy_check_wraparound_failsafe 로 wraparound 위험 검사
- 병렬 가능 시 parallel VACUUM 초기화
- 각각의 인덱스에 대해 lazy_vacuum_one_index 호출
- 내부적으로 vac_bulkdel_one_index → IndexAM 의 ambulkdelete
- 모든 인덱스 삭제 완료 후 lazy_vacuum_heap_rel (Phase III)
- Phase III – 힙 페이지 정리 (lazy_vacuum_heap_rel)
- 호출: Phase II 직후 또는 메모리 한도 초과 시 lazy_vacuum 에서
- 동작:
- TidStoreIter 로 dead_items 순회
- read_stream_begin_relation + vacuum_reap_lp_read_stream_next 를 통해 대상 블록 결정
- 각 블록에 대해 lazy_vacuum_heap_page 호출
- LP_DEAD → LP_UNUSED 변경, line pointer array truncate
- WAL logging, visibility map 재설정(모두 보이는지 검사)
- FSM 최종 갱신
- 트렁케이트 (lazy_truncate_heap)
- 조건: 충분한 빈 페이지가 끝에 연속해서 남아 있을 때
- 함수: should_attempt_truncation → lazy_truncate_heap
- AccessExclusiveLock 을 잠깐 얻어 실제 파일 시스템 블록 축소
- 충돌 감지 시 중단
주요 함수 대응표
| 단계 | 역할 요약 | 주요 함수 |
| 초기화·진입 | 트랜잭션·에러 콜백·통계 설정 | heap_vacuum_rel |
| Phase I – 스캔 | 힙 페이지 순차 스캔, prune/freezing | lazy_scan_heap → heap_vac_scan_next_block, lazy_scan_prune, lazy_scan_noprune |
| Phase II – 인덱스 | LP_DEAD 인덱스 엔트리 삭제 | lazy_vacuum_all_indexes → lazy_vacuum_one_index |
| Phase III – 힙 | 힙 페이지에서 LP_UNUSED 처리 | lazy_vacuum_heap_rel → lazy_vacuum_heap_page |
| 트렁케이트 | 빈 페이지 제거 | should_attempt_truncation, lazy_truncate_heap |
| 메타·통계 갱신 | pg_class 통계·FSM·visibility map 갱신 | update_relstats_all_indexes, RecordPageWithFreeSpace, visibilitymap_set 등 |
++ VACUUM FULL은 아래 함수로 동작합니다. (CLUSTER 명령과 동일한 메커니즘 사용)
postgres/src/backend/commands/cluster.c at master · postgres/postgres
Mirror of the official PostgreSQL GIT repository. Note that this is just a *mirror* - we don't work with pull requests on github. To contribute, please see https://wiki.postgresql.org/wiki/Subm...
github.com
3. VACUUM ANALYZE 순서
- Heap 스캔 (lazy vacuum 방식)
- Index 정리
- Free Space Map 갱신
- Visibility Map 갱신
- 힌트 비트 및 Frozen 처리
- ANALYZE 수행 (샘플링 기반 통계 수집)
- most common values (MCV)
- histogram bounds
- null fraction
- correlation
postgres/src/backend/commands/analyze.c at master · postgres/postgres
Mirror of the official PostgreSQL GIT repository. Note that this is just a *mirror* - we don't work with pull requests on github. To contribute, please see https://wiki.postgresql.org/wiki/Subm...
github.com
4. Autovacuum
autovacuum은 테이블의 튜플 상태를 주기적으로 점검하여 VACUUM 또는 ANALYZE 작업을 자동으로 수행하는 백그라운드 프로세스입니다.
Autovacuum 트리거
| 항목 | 공식 | 기본값 |
| vacuum 트리거 | dead_tuple_count > threshold + scale × reltuples | 50 + 0.2 × n |
| analyze 트리거 | changed_tuple_count > threshold + scale × reltuples | 50 + 0.1 × n |
| insert vacuum 트리거 | insert_tuple_count > insert_threshold + insert_scale × reltuples | 1000 + 0.2 × n |
Autovacuum 파라미터
※ 관련 파라미터는 아래 포스팅 참고 부탁드립니다.
PostgreSQL: Autovacuum 파라미터
※ PostgreSQL: Autovacuum Parameters.※ Version: PostgreSQL 16. 안녕하세요. 듀스트림입니다. 오늘은 유지관리에서 중요한 Autovacuum 파라미터들을 알아보고 튜닝 포인트와 주의사항에 대해 알아보겠습니다.
dewstream.tistory.com
Autovacuum의 락과 우선순위
Autovacuum은 VACUUM과 동일한 락을 획득합니다.
Autovacuum은 VACUUM과 다르게 SHARE UPDATE EXCLUSIVE 락을 보유하고 있을 때, 다른 프로세스가 이를 획득하려 할 경우 Autovacuum 작업이 중단될 수 있습니다.
• DML과 Autovacuum의 관계
Autovacuum은 대부분의 DML(SELECT, INSERT, UPDATE, DELETE) 작업과 충돌하지 않습니다.
그러나 일부 상황에서는 DML 작업이 Autovacuum의 진행을 방해할 수 있습니다.
- DML이 Autovacuum에 미치는 영향
- DML 작업이 특정 페이지를 점유 중일 때:
- Autovacuum은 대상 페이지의 락을 획득하지 못할 경우, 해당 페이지를 vacuum하지 못하고 스킵하거나 지연될 수 있습니다. 이로 인해 Autovacuum의 성능이 저하될 수 있습니다.
- 오래 열려 있는 DML 트랜잭션:
- Autovacuum은 dead tuple을 제거하는 작업을 수행합니다. 만약 DML 트랜잭션이 너무 오래 열려 있다면, 해당 트랜잭션에서 처리한 dead tuple을 제거할 수 없기 때문에 Autovacuum의 효과가 저하되고, vacuum 작업이 지연될 수 있습니다.
- DML 작업이 특정 페이지를 점유 중일 때:
• DDL과 Autovacuum의 관계
DDL(Data Definition Language) 작업은 ACCESS EXCLUSIVE LOCK을 요청합니다.
이 락은 ACCESS SHARE LOCK과 호환되지 않아, DDL 작업이 진행 중일 경우 Autovacuum이 취소되거나 지연될 수 있습니다.
• 요약 표
| 유형 | Autovacuum과의 락 충돌 여부 | Autovacuum 영향 |
| SELECT | ❌ 없음 (ACCESS SHARE) | ✅ 영향 없음 |
| INSERT / UPDATE / DELETE | ❌ 없음 (ROW EXCLUSIVE) | ✅ 일부 페이지 vacuum 스킵 또는 지연 가능 |
| VACUUM (수동) | ❌ (병렬 실행 가능) | ✅ 독립 동작 |
| ANALYZE | ❌ 없음 | ✅ 영향 없음 |
| ALTER TABLE | ✅ 충돌 (ACCESS EXCLUSIVE) | ❌ Autovacuum 취소됨 |
| DROP TABLE, REINDEX, TRUNCATE 등 | ✅ 충돌 (ACCESS EXCLUSIVE) | ❌ Autovacuum 취소됨 |
+ 최적화 방안은 아래 포스팅 참고 부탁드립니다.
PostgreSQL: Autovacuum 최적화
※ PostgreSQL: autovacuum optimization. 안녕하세요. 듀스트림입니다. PostgreSQL을 사용하시는 분들은 VACUUM 때문에 조금씩은 머리가 아프실 거에요.이번 포스팅은 유지관리에서 가장 신경써야할 것 중 하
dewstream.tistory.com
5. 힌트 비트, 프로즌 비트, FSM, VM 갱신 원리
| 구조 | 역할 | 갱신 시기 |
| Hint Bit | 튜플의 트랜잭션 커밋 여부 캐시 | SELECT 등 읽기 시, VACUUM |
| Frozen Bit | wraparound 방지를 위해 xmin을 FrozenTransactionId로 설정 | VACUUM 시 xmin이 충분히 오래됐을 때 |
| FSM (Free Space Map) | 빈 블록 추적 → INSERT 시 재사용 가능 | VACUUM 후 사용 가능한 공간 기록 |
| VM (Visibility Map) | 페이지 단위 튜플 visibility 추적 (Index Only Scan에 사용) | VACUUM 시 페이지 내 튜플이 모두 visible 시 1bit 설정 |
• 힌트 비트 갱신 시점
lazy_scan_heap() 중, 페이지를 순회할 때 dead tuple 확인 전에, 아직 힌트 비트가 설정되지 않은 경우 clog를 확인하여 힌트 비트를 설정합니다. (힌트 비트는 VACUUM 중 튜플 단위로 설정됩니다.)
HeapTupleSetHintBits(...) → clog 확인 후 commit/abort 비트 설정
• 힌트 비트, FSM, VM 갱신 순서
힌트 비트 → FSM → VM 순으로 갱신
| 순서 | 대상 | 설명 |
| 1 | 힌트 비트 | 페이지 스캔 중 아직 힌트비트가 없으면 clog(pg_xact) 조회 → 힌트비트 설정 |
| 2 | FSM | 튜플 삭제 완료 후 빈 공간이 생긴 페이지 → FSM 기록 |
| 3 | VM | 페이지 전체가 visible 상태(all-visible)일 경우 → VM 비트 설정 |
• 힌트 비트와 프로즌 비트
힌트 비트가 먼저 설정되어 있어야 Freeze가 가능
Freeze는 조건이 만족되면 트랜잭션 ID(xmin, xmax)를 FrozenTransactionId로 대체
1. VACUUM 시작 → 페이지 스캔
2. 각 튜플에 대해 clog 조회:
- → 힌트 비트 설정 (commit/abort 여부 기록)
3. 조건 충족 시:
- → Freeze 처리 (`xmin`, `xmax` 대체)
4. 페이지 전부 Freeze 처리되었으면:
- → Visibility Map의 all-frozen 비트 설정
heap_prepare_freeze_tuple() → heapam_visibility.c
/*
해당 함수 수행 조건:
xmin이 OldestXmin보다 작고,
이미 commit 상태이면,
→ xmin = FrozenTransactionId로 변경 (힌트 비트가 먼저 설정되어 있어야 Freeze 가능)
*/
| 항목 | 힌트 비트 | 프로즌 비트 |
| 목적 | clog 조회를 생략하고 트랜잭션 커밋/어보트 여부를 캐시 | 트랜잭션 ID wraparound 방지를 위해 xmin을 FrozenXid로 대체 |
| 위치 | 튜플 헤더의 status bit (HEAP_XMIN_COMMITTED, 등) | 페이지 레벨에 기록 (visibility map의 all-frozen 비트) |
| 설정 시점 | SELECT, UPDATE, VACUUM 등에서 clog 접근 시 | VACUUM, VACUUM FREEZE에서 특정 조건 만족 시 |
| 효과 | 다음 접근 시 clog 조회 생략 | VACUUM이 해당 페이지 재처리 생략 가능 |
| 변경 대상 | 튜플 단위 | 페이지 단위 |
postgres/src/backend/access/heap/visibilitymap.c at master · postgres/postgres
Mirror of the official PostgreSQL GIT repository. Note that this is just a *mirror* - we don't work with pull requests on github. To contribute, please see https://wiki.postgresql.org/wiki/Subm...
github.com
6. 주의사항
- VACUUM FULL은 반드시 운영시간 외에 수행 → 강력한 공간 회수 vs. 강력한 락
- VM이 정확히 갱신되지 않으면 Index Only Scan이 성능 저하
- 힌트 비트 WAL에 기록되지 않으면 복제와 충돌 가능 → wal_log_hints = on 필요
- freeze가 늦으면 XID wraparound 경고 발생 → pg_class.relfrozenxid 모니터링 필수
-- Frozen XID 조회
SELECT
a.relname,
a.relfrozenxid::text AS frozen_xid
FROM
pg_catalog.pg_class a
WHERE
a.relkind = 'r' -- 'r'은 일반 테이블을 의미
AND a.relname NOT LIKE 'pg_%'; -- 시스템 테이블 제외
-- age() 함수 사용: relfrozenxid와 현저 XID의 차이 계산
SELECT relname, age(relfrozenxid) AS xid_age
FROM pg_class
WHERE relkind = 'r';
-- relfrozenxid?
-- 해당 테이블에 있는 모든 데이터가 언제 얼려졌는지를 나타냄 (어떤 트랜잭션에서 데이터가 Frozen 상태로 기록되었는지)
-- relfrozenxid는 VACUUM 프로세스에 의해 갱신됨
-- VACUUM이 실행되면 더 이상 사용되지 않는 XID를 제거하고 relfrozenxid를 최신 XID로 갱신
-- relfrozenxid는 더 이상 사용되지 않는 데이터(죽은 튜플)를 정리하기 위한 기준점 역할
오늘은 여기까지~
'PostgreSQL' 카테고리의 다른 글
| PostgreSQL: reload (0) | 2025.05.22 |
|---|---|
| PostgreSQL: TOAST (0) | 2025.05.21 |
| PostgreSQL: MVCC (0) | 2025.05.16 |
| PostgreSQL: SLRU(Simple Least Recently Used) 버퍼 캐시 (0) | 2025.05.15 |
| PostgreSQL: Latency Spike 관련 (0) | 2025.05.14 |