PostgreSQL

PostgreSQL: Heap Fetch

dewstream 2025. 11. 3. 08:00
728x90

※ 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이 충분히 자주 돌 수 있게 튜닝해 주면 됩니다.

오늘은 여기까지~

 

728x90

'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