인덱스 스캔 vs 비트맵 스캔: xmin/xmax의 역할¶
🐘 PostgreSQL: 왜 인덱스만으로 부족한가?¶
PostgreSQL에서 데이터를 조회할 때, 인덱스에 모든 컬럼이 포함되어 있음에도 불구하고 Index Only Scan 대신 Bitmap Scan이나 Heap 접근이 발생하는 이유는 크게 두 가지 메커니즘 때문입니다.
1. MVCC와 가시성(Visibility) 문제¶
PostgreSQL은 MVCC(Multi-Version Concurrency Control)를 통해 동시성을 관리합니다.
- 데이터의 상태: 모든 행(Tuple)은 생성 및 삭제 시점을 추적하기 위해 숨겨진 시스템 열을 가집니다.
xmin (Transaction Minimum): 행(튜플)이 삽입(INSERT)되거나 업데이트(UPDATE)될 때 해당 행을 생성한 트랜잭션 ID. 용도: 이 행이 언제 만들어졌는지, 어떤 트랜잭션이 유효한지 확인.xmax (Transaction Maximum): 행이 삭제(DELETE)되거나 업데이트(UPDATE)될 때 해당 행을 만료시킨 트랜잭션 ID. (0이면 유효한 데이터) 행이 언제 삭제/변경되었는지 추적.- 인덱스의 한계: B-Tree 등의 인덱스 구조에는 이
xmin,xmax정보가 포함되어 있지 않습니다. 오직 데이터 값과 실제 데이터 위치(TID)만 저장합니다. - 힙(Heap) 접근의 이유: 인덱스에서 조건에 맞는 데이터를 찾아도, 해당 데이터가 현재 내 트랜잭션에서 "보이는 버전"인지 확인하려면 결국 테이블 원본(Heap)의 시스템 열을 확인해야 합니다.
2. 가시성 지도 (Visibility Map)의 역할¶
매번 힙에 접근하는 성능 저하를 막기 위해 PostgreSQL은 가시성 지도(Visibility Map)를 사용합니다.
- All-visible 상태: 특정 페이지의 모든 행이 모든 트랜잭션에게 보인다고 판단되면 가시성 지도에 표시됩니다.
- 최적화: 인덱스 스캔 시 가시성 지도를 확인하여 해당 페이지가 "All-visible"이라면, 힙에 가지 않고 Index Only Scan을 수행합니다.
3. 데이터 삽입 직후의 상황 (Issue Case)¶
수천 개의 데이터를 방금 삽입(Bulk Insert)한 직후에는 다음과 같은 문제가 발생합니다.
| 현상 | 원인 | 결과 |
|---|---|---|
| 신뢰도 저하 | 새로 삽입된 데이터는 아직 가시성 지도에 업데이트되지 않음. | 플래너가 데이터의 유효성을 확신할 수 없음. |
| 스캔 방식 변경 | 인덱스만으로는 가시성 확인이 불가능함. | Bitmap Scan 등을 통해 힙 영역을 강제 확인. |
4. 해결책: VACUUM¶
이 문제를 해결하고 성능을 최적화하려면 VACUUM 명령어가 필요합니다.
- 동작: PostgreSQL은 xmin과 xmax를 비교하여 현재 트랜잭션이 행을 볼 수 있는지 결정합니다. VACUUM 작업은 xmax를 기반으로 더 이상 필요 없는(dead) 튜플을 청소하고 가시성 지도를 최신 상태로 갱신합니다.
- 효과: 가시성 지도가 업데이트되면, 이후 동일 쿼리 실행 시 플래너가 힙 접근 없이 Index Only Scan을 선택하게 됩니다.
📝 핵심 요약 테이블¶
| 항목 | 설명 |
|---|---|
| xmin | 행을 생성한 트랜잭션 ID. 데이터의 생성 시점 결정. |
| xmax | 행을 삭제/변경한 트랜잭션 ID. 데이터의 만료 시점 결정. |
| Index Only Scan | 가시성 지도가 최신일 때만 힙 접근 없이 인덱스로만 처리하는 방식. |
| 성능 최적화 | 대량 작업 후 VACUUM 또는 ANALYZE를 통해 통계 및 가시성 정보 갱신 필요. |