본문 바로가기
Database2026년 8월 15일6분 읽기

HOT 업데이트 — 인덱스 쓰기를 건너뛰는 PostgreSQL 최적화

YS
김영삼
조회 6
HOT 업데이트 — 인덱스 쓰기를 건너뛰는 PostgreSQL 최적화

HOT(Heap-Only Tuple) 업데이트는 PostgreSQL이 행을 수정할 때 인덱스를 건드리지 않고 넘어가는 최적화입니다. 조건은 두 가지. 바꾸는 컬럼이 어떤 인덱스에도 포함되지 않을 것, 그리고 새 버전이 같은 데이터 페이지 안에 들어갈 공간이 있을 것. 이 두 개가 맞으면 인덱스 쓰기가 통째로 사라지고, 그만큼 쓰기 부하와 WAL 양이 줄어듭니다.

개인적으로 이 개념을 제대로 알기 전과 후는 UPDATE 위주 테이블을 설계하는 감각이 완전히 달랐습니다. "왜 이 테이블만 인덱스 부풀림이 심하지?"의 범인이 대부분 HOT을 못 타는 UPDATE였거든요.

왜 인덱스를 건너뛸 수 있나

MVCC 때문에 UPDATE는 새 튜플을 만든다고 했습니다. 원래대로라면 이 새 위치를 모든 인덱스가 가리키도록 인덱스 엔트리도 새로 만들어야 합니다. 그런데 생각해보면, 인덱스에 안 들어가는 컬럼만 바꿨다면 인덱스 키 값은 그대로입니다. 굳이 인덱스를 새로 쓸 이유가 없죠.

PostgreSQL은 이때 옛 튜플에서 새 튜플로 이어지는 HOT 체인을 페이지 안에 만듭니다. 인덱스는 여전히 옛 위치를 가리키지만, 조회 시 그 체인을 따라가 최신 버전을 찾습니다. 인덱스 입장에선 아무 일도 없었던 겁니다.

-- 인덱스가 balance에는 없다고 하자
CREATE TABLE counters (id int primary key, hits bigint, note text);
CREATE INDEX ON counters (note);   -- note에만 인덱스
-- hits만 바꾸는 UPDATE → HOT 후보 (인덱스 컬럼 아님)
UPDATE counters SET hits = hits + 1 WHERE id = 42;
-- note를 바꾸는 UPDATE → HOT 불가 (인덱스 갱신 필요)
UPDATE counters SET note = 'x' WHERE id = 42;

내가 HOT을 타고 있는지 확인하기

추측하지 말고 숫자로 봅니다. pg_stat_user_tables에 누적 카운터가 있습니다.

SELECT relname,
       n_tup_upd        AS total_updates,
       n_tup_hot_upd    AS hot_updates,
       round(100.0 * n_tup_hot_upd / NULLIF(n_tup_upd,0), 1) AS hot_pct
FROM pg_stat_user_tables
WHERE n_tup_upd > 0
ORDER BY n_tup_upd DESC;

hot_pct가 높을수록 좋습니다. 90%가 넘으면 훌륭하고, UPDATE가 많은 테이블인데 이 값이 10~20%대라면 뭔가 새고 있는 겁니다. 대개 (1) 바꾸는 컬럼에 걸린 불필요한 인덱스, (2) 페이지에 빈 공간이 없어서 새 튜플이 다른 페이지로 밀려나는 경우입니다.

HOT 비율을 끌어올리는 두 지렛대

지렛대방법효과
인덱스 다이어트자주 바뀌는 컬럼의 인덱스 제거HOT 조건 1 충족
fillfactor 낮추기fillfactor=80~90으로 여유 공간 확보HOT 조건 2 충족(같은 페이지)
-- 자주 UPDATE되는 테이블은 페이지에 여유를 남긴다
ALTER TABLE counters SET (fillfactor = 85);
VACUUM FULL counters;   -- 재작성하며 fillfactor 반영(락 주의)

fillfactor를 85로 두면 새 페이지를 채울 때 15%를 비워둡니다. 그 여유 공간 덕에 UPDATE로 생기는 새 튜플이 같은 페이지에 앉을 확률이 올라가고, 그만큼 HOT 성공률이 오릅니다. 기본값 100은 읽기 전용에는 좋지만 UPDATE 헤비 테이블엔 독이 될 수 있습니다.

실전 팁: "그냥 인덱스 많이 걸어두면 좋겠지"라는 습관이 HOT을 죽입니다. 검색에 안 쓰는 인덱스, 특히 자주 바뀌는 updated_at·카운터·상태 플래그 컬럼에 무심코 건 인덱스가 대표적인 범인입니다.

자주 묻는 질문

HOT이면 VACUUM이 필요 없나요?

아닙니다. HOT 체인의 죽은 버전도 공간을 차지합니다. 다만 PostgreSQL은 페이지를 읽을 때 HOT 가지치기(pruning)로 같은 페이지 안 죽은 버전을 가볍게 정리할 수 있어 VACUUM 부담이 줄어듭니다. 인덱스 정리가 없어 청소가 훨씬 싸다는 게 핵심 이득입니다.

fillfactor를 무조건 낮추면 좋은가요?

아니요. 낮출수록 같은 데이터가 더 많은 페이지에 흩어져 테이블이 커지고 순차 스캔이 느려집니다. UPDATE가 거의 없는 테이블은 100(기본)이 낫습니다. UPDATE 헤비 테이블만 선택적으로 낮추세요.

ALTER TABLE로 fillfactor만 바꾸면 기존 데이터에 바로 적용되나요?

아닙니다. 새로 쓰이는 페이지부터 적용됩니다. 기존 페이지에 반영하려면 VACUUM FULL이나 pg_repack으로 재작성해야 합니다. 전자는 강한 락을 거니 운영 중엔 후자를 권합니다.

어떤 컬럼을 인덱스에서 빼야 할지 어떻게 정하나요?

pg_stat_user_indexesidx_scan을 보세요. 스캔 횟수가 0에 가까운 인덱스는 검색에 안 쓰이면서 HOT만 방해하고 있을 가능성이 큽니다. 사용량이 없고 자주 바뀌는 컬럼의 인덱스가 1순위 제거 후보입니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록