※ PostgreSQL: Heap Fetch.
안녕하세요. 듀스트림입니다.
오늘은 실행계획에서 볼 수 있는 힙 페치에 대한 내용입니다.
1. Heap Fetch란?
PostgreSQL은 인덱스를 통해 행의 위치(TID: Tuple ID) 를 알아내고, 그 다음에 테이블의 실제 데이터 블록(heap) 에 접근해서 행 전체를 읽습니다. 이 두 번째 접근이 바로 "Heap Fetch" 입니다.
예를 들어,
EXPLAIN (ANALYZE, BUFFERS)
SELECT name FROM users WHERE id = 10;
→ 이 쿼리가 Index Scan using users_pkey 로 실행된다면 실행계획에 아래와 같은 항목이 붙을 수 있습니다.
Index Scan using users_pkey on users (cost=0.29..8.30 rows=1 width=32)
Index Cond: (id = 10)
Buffers: shared hit=3
Heap Fetches: 1
→ 여기서 Heap Fetches: 1 은 인덱스로 조건을 찾은 뒤 실제 테이블 블록을 한 번 읽었다는 뜻입니다.
2. 왜 Heap Fetch가 필요한가?
인덱스에는 일반적으로 검색 조건에 필요한 컬럼만 들어 있고, 실제 SELECT 절에서 필요한 나머지 컬럼들은 테이블에서 읽어와야 합니다.
따라서 다음 경우에 Heap Fetch가 발생합니다.
| 상황 | Heap Fetch 필요 여부 |
| 인덱스에 필요한 컬럼이 전부 있음 | ❌ (Heap Fetch 없음 → “Index Only Scan”) |
| 인덱스에 일부 컬럼만 있음 | ✅ (Heap Fetch 발생 → “Index Scan”) |
| 인덱스만으로 MVCC 가시성 판별 불가 | ✅ (VACUUM 미진행 등으로 인한 Visibility Map 미활성 상태) |
3. Heap Fetch가 성능에 미치는 영향
Heap Fetch는 랜덤 I/O(Random Access) 를 유발하기 때문에 대용량 테이블에서는 성능 저하의 원인이 됩니다.
예를 들어, 10만 건을 인덱스로 찾고 그만큼 heap을 각각 읽는다면 랜덤 블록 접근 10만 번이 일어나는 셈입니다.
성능 영향 요약
| 항목 | 영향 |
| I/O 패턴 | 랜덤 읽기 증가 |
| 캐시 히트 | 낮음 (heap 블록 재사용률이 낮아집니다.) |
| CPU | I/O 대기 시간이 길어짐 |
4. 줄이는 방법
▸ Index Only Scan 유도: 인덱스에 필요한 컬럼을 모두 포함시키면 heap 접근이 없어집니다.
-- 인덱스
CREATE INDEX idx_users_id_name ON users(id, name);
-- 쿼리
SELECT name FROM users WHERE id = 10;
→ Heap Fetch 없음
→ 실행계획에서 Index Only Scan으로 표시됨
단, Visibility Map이 최신 상태여야 합니다.
▸ Heap Fetch 줄이기 위한 팁
| 전략 | 설명 |
| INCLUDE 인덱스 컬럼 (PostgreSQL 11+) | SELECT에 필요한 컬럼을 인덱스에 “INCLUDE”로 추가 |
| VACUUM 주기 관리 | Visibility Map 최신화 → Index Only Scan 가능 |
| CLUSTER / REINDEX | 인덱스와 힙의 물리적 근접성 향상 → 랜덤 I/O 감소 |
| 쿼리 튜닝 | 필요 없는 컬럼 조회 제거 (heap 접근 최소화) |
+ 튜닝을 하다보면 이런 것도 볼 수 있습니다.
CTE aaa
-> GroupAggregate (... rows=12 loops=1)
-> Sort (...)
-> Nested Loop (... rows=52236 loops=1)
-> Nested Loop (... rows=52236 loops=1)
...
-> Index Only Scan using table1_col_1_idx on table1 t1
Index Cond: (id = s.id)
...
Heap Fetches: 10440605
- 내부 단계가 Index Only Scan으로 표시되지만 Heap Fetches가 10,440,605건.
- 바깥에서 52,236건을 들고 오며 Nested Loop로 내부 인덱스 탐색을 반복 → 힙 접근이 누적.
▸ 왜 Index Only Scan인데 Heap Fetches가 많을까요?
Index Only Scan의 전제: Visibility Map
• PostgreSQL은 인덱스만으로 결과를 반환하려 할 때, 해당 힙 페이지가 모든 트랜잭션에 가시(all-visible)한지 Visibility Map(VM)에서 확인합니다.
→ 페이지가 all-visible이 아니면 힙을 추가로 확인해야 하므로 Index Scan과 비용 차이가 거의 없어집니다.
VM은 각 힙 페이지의 가시성(및 동결 여부)을 비트맵으로 관리하는 별도 포크(*_vm파일)에 저장됩니다.
▸ 여기서 추가적인 성능 이슈는 뭘까요?
중첩 루프 증폭 효과
• Nested Loop는 왼쪽 행마다 오른쪽을 다시 스캔합니다.
• 오른쪽이 인덱스로 빠르게 접근 가능하면 유리하지만, 힙 접근이 필요한 경우 그 비용이 행 수만큼 누적됩니다.
• 위 계획은 바깥 52K 행 × 내부 힙 확인 반복으로 Heap Fetches가 폭증했습니다.
▸ 이럴 때는 어떻게 해야할까요?
일단 VACUUM ANALYZE부터 돌리고, Autovacuum이 충분히 자주 돌 수 있게 튜닝해 주면 됩니다.
오늘은 여기까지~
'PostgreSQL' 카테고리의 다른 글
| PostgreSQL: ctid (0) | 2025.11.07 |
|---|---|
| PostgreSQL: Heap Table Structure (0) | 2025.11.05 |
| PostgreSQL: ALTER TABLE ... (0) | 2025.10.31 |
| PostgreSQL: 캐시 히트 (0) | 2025.10.29 |
| PostgreSQL: Merge Join (0) | 2025.10.22 |