왜 B-tree가 시계열에서 부담이 되는가

센서 로그, 결제 이벤트, 접속 기록 같은 시계열 데이터는 하루에도 수천만 건씩 쌓입니다. 이런 테이블의 created_at에 관행적으로 B-tree 인덱스를 걸지만, 문제는 인덱스 크기입니다. 1억 행 테이블의 timestamp B-tree는 쉽게 2~3GB를 차지하고, 이는 곧 메모리 압박과 쓰기 증폭(write amplification)으로 이어집니다. 매 INSERT마다 인덱스 페이지를 갱신해야 하므로 대량 적재 성능도 떨어집니다.

핵심은 시계열 데이터의 물리적 특성입니다. 시간순으로 append-only로 쌓이는 데이터는 디스크 상 물리적 순서와 시간값 순서가 거의 일치합니다. 이 "자연 정렬" 성질을 활용하지 못하는 것이 B-tree의 낭비입니다.

BRIN이 동작하는 원리

BRIN(Block Range Index)은 개별 행이 아니라 블록 범위(기본 128페이지) 단위로 최소·최대값만 저장합니다. 예를 들어 "블록 1~128에는 09:00~09:05 데이터가 있다"는 요약만 기록합니다. 쿼리가 특정 시간 범위를 조회하면, 겹치지 않는 블록 범위는 통째로 건너뛰고 후보 블록만 스캔합니다.

그 결과 인덱스 크기가 극단적으로 작아집니다. B-tree가 수 GB일 때 같은 데이터의 BRIN은 보통 수백 KB~수 MB 수준입니다.

항목B-treeBRIN
인덱스 크기(1억 행)약 2~3GB약 수백 KB~수 MB
INSERT 오버헤드큼매우 작음
범위 스캔빠름정렬돼 있으면 빠름
단건 PK 조회매우 빠름부적합

실제 생성과 조회

생성은 간단합니다. pages_per_range로 블록 범위 단위를 조절할 수 있습니다.

-- 기본 BRIN 인덱스
CREATE INDEX idx_events_created_brin
  ON events USING brin (created_at);

-- 범위를 촘촘하게(정밀도↑, 크기 소폭↑)
CREATE INDEX idx_events_created_brin32
  ON events USING brin (created_at)
  WITH (pages_per_range = 32);

-- 조회: 범위 조건이어야 효과가 크다
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*)
FROM events
WHERE created_at >= '2026-09-01'
  AND created_at <  '2026-09-02';

실행 계획에서 Bitmap Heap Scan 위에 Bitmap Index Scan on ...brin이 뜨고, Heap Blocks: lossy=로 스캔된 블록 수를 확인할 수 있습니다.

정렬 상관관계가 생명이다

BRIN의 성능은 물리적 정렬 상관관계(correlation)에 전적으로 의존합니다. 상관관계가 1에 가까우면 블록 스킵이 잘 되고, 0에 가까우면 거의 전체 스캔이 됩니다. 이 값은 통계로 확인할 수 있습니다.

SELECT attname, correlation
FROM pg_stats
WHERE tablename = 'events'
  AND attname   = 'created_at';
-- correlation 이 0.9+ 면 BRIN 에 이상적

UPDATE/DELETE가 잦거나 백필(backfill)로 과거 데이터가 뒤섞여 들어오면 상관관계가 무너집니다. 이 경우 CLUSTER events USING (b-tree 인덱스)로 물리 정렬을 복원하거나, 애초에 시간 파티셔닝으로 순서를 보장해야 합니다.

요약 정보 갱신과 autosummarize

새로 추가된 블록은 자동으로 요약되지 않을 수 있어, 최신 데이터가 인덱스 효과를 못 받는 함정이 있습니다. autosummarize 옵션이나 수동 요약으로 해결합니다.

-- 인덱스 생성 시 자동 요약 켜기
CREATE INDEX idx_events_brin_auto
  ON events USING brin (created_at)
  WITH (autosummarize = on);

-- 이미 만든 인덱스의 미요약 범위를 강제 요약
SELECT brin_summarize_new_values('idx_events_created_brin');

파티셔닝과 함께 쓰기

실무에서는 시간 기반 파티셔닝과 BRIN을 조합하는 것이 가장 안정적입니다. 파티션이 이미 시간 범위를 나눠 pruning하고, 각 파티션 내부는 append 순서라 상관관계가 자연히 유지됩니다. 파티션별로 작은 BRIN을 두면 인덱스 관리 비용과 조회 성능을 모두 얻습니다.

언제 쓰지 말아야 하는가

BRIN은 만능이 아닙니다. 단건 PK 조회, 낮은 카디널리티가 아닌 무작위 접근, 물리 정렬이 깨진 컬럼에는 부적합합니다. UNIQUE 제약이나 최신 몇 건을 뽑는 ORDER BY ... LIMIT 패턴도 B-tree가 유리합니다. 정리하면 BRIN은 "시간순으로 쌓이고, 범위로 조회하며, 개별 갱신이 드문" 대용량 시계열에서 저장·쓰기 비용을 크게 줄이는 도구입니다. 도입 전 반드시 pg_stats의 correlation과 실제 EXPLAIN ANALYZE로 검증하세요.