본문으로 건너뛰기
6.5 BRIN 인덱스

6.5 BRIN 인덱스

BRIN(Block Range Index)은 대용량 + 물리 저장 순서와 값 순서가 강하게 상관된 데이터에 특화된 인덱스입니다. 다른 인덱스가 row 단위 entry를 갖는 반면, BRIN은 페이지 범위(block range) 단위 요약값만 저장해 인덱스가 매우 작습니다. log·시계열·append-only 테이블의 기본 후보입니다.

동작 원리

테이블을 128페이지(기본) 묶음으로 나누고, 각 묶음마다 min/max 같은 요약값을 저장합니다.

range 0 (pages 0~127):    min=1,    max=12500
range 1 (pages 128~255):  min=12501, max=25000
range 2 (pages 256~383):  min=25001, max=37500
...

쿼리 WHERE id BETWEEN 20000 AND 21000:

  • range 0: max=12500 < 20000 → 건너뜀
  • range 1: 겹침 → 페이지 128~255만 스캔
  • range 2: min=25001 > 21000 → 건너뜀

페이지 대다수를 건너뛰므로 B-tree만큼 빠르진 않지만 매우 작은 인덱스로 큰 효과.

잘 맞는 데이터

조건예시
물리 순서 = 값 순서 (correlation ≈ 1.0)append-only insert가 만든 자연 순서
매우 큰 테이블수억~수십억 row
범위 쿼리 위주날짜·ID 범위 검색

대표 사례:

  • created_at (시간 순서 INSERT)
  • event_id (시퀀스)
  • IoT 센서 데이터의 timestamp
  • 로그 테이블

잘 안 맞는 데이터

  • random insert (UUID v4 등) — correlation 낮음
  • 자주 UPDATE되는 컬럼
  • 작은 테이블 — 비용 차이 무의미

생성

CREATE INDEX idx_events_created_at_brin
  ON events USING brin (created_at);

-- 페이지 범위 크기 조절
CREATE INDEX idx_events_created_at_brin
  ON events USING brin (created_at) WITH (pages_per_range = 32);
옵션기본의미
pages_per_range128한 range가 몇 페이지
autosummarizeoff새 데이터 들어올 때 자동 요약

작은 pages_per_range는 인덱스 크기 ↑, 정밀도 ↑. 보통 32~128 사이.

크기 비교

인덱스1억 row 기준 크기 (대략)
B-tree (created_at)약 2GB
BRIN (created_at, default)약 1MB

차이가 2000배. 작은 인덱스는 캐시에 다 들어가고, ANALYZE·VACUUM 부담도 작습니다.

관리

신선도 유지

새 row가 들어오면 마지막 range의 max만 자동 갱신되지만, 새 range가 시작되면 명시적 summarize가 필요합니다.

-- 강제 summarize
SELECT brin_summarize_new_values('idx_events_created_at_brin'::regclass);

autosummarize = on이면 PostgreSQL이 알아서 — 대부분 켜는 게 좋습니다.

대량 INSERT 후

ETL이 끝나면 한 번씩:

VACUUM events;   -- BRIN range 갱신·요약 보강

opclass

같은 데이터 타입이라도 opclass에 따라 BRIN이 저장하는 요약이 달라집니다.

opclass저장잘 처리하는 쿼리
*_minmax_ops (기본)min, max=, <, >, BETWEEN
*_minmax_multi_ops (PG 14+)여러 min/max 묶음비연속 분포에 유리
*_bloom_ops (PG 14+)bloom filter= 등호 — correlation 낮은 데이터에도 유효
*_inclusion_opsbounding box범위 타입·기하

bloom_ops가 새로운 가능성: correlation이 낮은 컬럼에 BRIN을 적용해 확률적으로 페이지를 건너뛸 수 있습니다. tenant_id 같은 카테고리 컬럼에 효과적.

CREATE EXTENSION bloom;   -- bloom_ops는 일부 타입은 별도 extension
CREATE INDEX idx_events_tenant_brin
  ON events USING brin (tenant_id int4_bloom_ops);

EXPLAIN의 BRIN

 Bitmap Heap Scan on events
   Recheck Cond: (created_at BETWEEN '2026-01-01' AND '2026-01-15')
   Rows Removed by Index Recheck: 12000
   ->  Bitmap Index Scan on idx_events_created_at_brin
         Index Cond: (created_at BETWEEN '2026-01-01' AND '2026-01-15')
특징의미
항상 Bitmap Heap ScanBRIN은 정확한 row 위치를 모름. 후보 페이지를 모은 뒤 recheck
Rows Removed by Index Recheckrange 안에 잘못 들어온 row를 SQL 조건으로 재거름

recheck가 너무 많으면 pages_per_range를 줄여 정밀도 ↑.

운영 의사결정

    flowchart TD
  Q["대용량 테이블<br/>범위 쿼리"] --> CORR{"INSERT 순서 =<br/>값 순서?"}
  CORR -- "yes (시계열·시퀀스)" --> BRIN["BRIN<br/>(minmax_ops)"]
  CORR -- "낮음" --> BLOOM{"등호 쿼리만?"}
  BLOOM -- "yes" --> BBRIN["BRIN bloom_ops"]
  BLOOM -- "no" --> BTREE["B-tree<br/>(또는 파티션)"]

  classDef brin fill:#d1fae5,stroke:#047857,color:#064e3b
  classDef btree fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
  classDef q fill:#fed7aa,stroke:#c2410c,color:#7c2d12
  class Q,CORR,BLOOM q
  class BRIN,BBRIN brin
  class BTREE btree
  

파티셔닝과의 조합

BRIN과 파티셔닝은 보완 관계. 큰 시계열 테이블을 월 단위 파티션으로 나누고, 각 파티션 안에서 created_at에 BRIN을 둡니다. 파티션이 큰 범위를 처내고, BRIN이 세부 범위를 처냄.

정리

  • BRIN = 페이지 범위 단위 요약값만 저장 → 매우 작은 인덱스
  • 잘 맞는 데이터: append-only, 시계열, correlation ≈ 1.0
  • random insert에는 부적합 (단, bloom_ops로 등호 쿼리는 가능)
  • 옵션: pages_per_range(정밀도), autosummarize(자동 갱신)
  • EXPLAIN에는 항상 Bitmap Heap Scan + Recheck
  • 파티셔닝과 조합해 대용량 시계열에 최적

다음 절(6.6)에서는 일반 인덱스 전부에 적용 가능한 partial · expression 인덱스를 봅니다.