BRIN — 수십억 행 시계열 테이블에 인덱스 걸기
B-Tree로는 답이 안 나오는 규모
로그나 센서 데이터처럼 시간순으로 계속 쌓이기만 하는 테이블이 수십억 행 규모가 되면, B-Tree 인덱스 자체의 크기가 문제가 됩니다. 행마다 키와 포인터를 저장하는 구조라, 데이터가 커지는 만큼 인덱스도 거의 비례해서 커집니다. 대략적인 감을 잡자면, 20바이트 안팎의 B-Tree 인덱스 항목이 50억 개면 인덱스만 100GB를 넘길 수 있습니다. 이 정도 크기면 인덱스가 메모리 캐시(shared_buffers)에 다 안 올라가서, 인덱스를 쓰는데도 디스크 I/O가 발생하는 역설적인 상황이 벌어집니다.
정렬된 데이터의 특성을 이용한다
BRIN(Block Range Index)은 접근을 완전히 다르게 합니다. 개별 행을 색인하는 대신, 테이블을 물리적인 블록 묶음(기본 128페이지)으로 나누고, 각 묶음의 최솟값과 최댓값만 저장합니다.
flowchart TD subgraph 물리적_페이지 P1["페이지 1~128<br/>created_at: 03-01 ~ 03-02"] P2["페이지 129~256<br/>created_at: 03-02 ~ 03-03"] P3["페이지 257~384<br/>created_at: 03-03 ~ 03-05"] end Q["WHERE created_at = 03-02"] -->|"P1 범위에 포함? 예"| P1 Q -->|"P2 범위에 포함? 예"| P2 Q -->|"P3 범위에 포함? 아니오, 건너뜀"| Skip["스캔 제외"]
이게 통하려면 전제가 하나 필요합니다 — 컬럼 값이 물리적 저장 순서와 상관관계가 있어야 합니다. created_at 컬럼은 대부분 INSERT 순서와 거의 일치하므로(먼저 들어온 행이 먼저 저장됨), 각 페이지 묶음의 시간 범위가 서로 안 겹치거나 조금만 겹칩니다. 그래서 특정 시간 범위를 조회할 때 관련 없는 페이지 묶음을 통째로 건너뛸 수 있습니다.
CREATE INDEX idx_events_created_brin ON events
USING BRIN (created_at) WITH (pages_per_range = 32);
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM events WHERE created_at >= '2026-03-02' AND created_at < '2026-03-03';인덱스 크기 자체가 페이지 묶음 개수에 비례하기 때문에, B-Tree라면 수십 GB가 될 인덱스가 BRIN에서는 몇 MB 수준으로 끝나는 경우가 흔합니다. pages_per_range 를 낮추면 묶음이 잘게 나뉘어 조회는 더 정밀해지지만 인덱스 크기와 유지 비용이 늘고, 높이면 반대입니다.
상관관계가 깨지면 그냥 쓸모없어진다
여기서 주의할 게 있습니다. customer_id 처럼 INSERT 순서와 무관하게 흩어져 있는 컬럼에 BRIN을 걸면, 페이지 묶음마다 최솟값·최댓값 범위가 테이블 전체 범위와 거의 같아져서 사실상 아무것도 못 거르고 전체를 스캔하는 것과 다를 바 없어집니다. BRIN을 쓰기 전에 실제 상관관계를 확인하세요.
SELECT attname, correlation
FROM pg_stats
WHERE tablename = 'events' AND attname = 'created_at';correlation 이 1 또는 -1에 가까울수록(물리적 순서와 논리적 순서가 강하게 일치) BRIN이 효과적입니다. 0에 가까우면 BRIN을 걸어봐야 소용없습니다 — 이 경우엔 그냥 B-Tree를 쓰거나, pg_repack 등으로 물리적 순서 자체를 재정렬하는 걸 고려해야 합니다.
UPDATE가 잦으면 역효과가 날 수 있다
BRIN은 "쓰는 순서 = 저장 순서"라는 전제 위에 서 있는데, UPDATE는 이 전제를 깹니다. PostgreSQL은 UPDATE를 내부적으로 기존 행을 죽은 것으로 표시하고 새 행을 다른 위치에 쓰는 방식으로 처리하기 때문에(MVCC), 오래된 행을 자주 갱신하는 테이블에서는 새로 쓰인 행이 엉뚱한 페이지 묶음에 섞여 들어가 상관관계가 점점 흐트러집니다. BRIN은 추가만 되고 거의 갱신되지 않는(append-only) 테이블에 가장 잘 맞습니다. 로그, 이벤트, 센서 데이터처럼요. 자주 갱신되는 테이블이라면 BRIN보다 B-Tree나 파티셔닝을 먼저 검토하는 게 낫습니다.