B-tree만 쓰다 만난 GIN·BRIN의 세계를, 인덱스 타입별 강점과 선택 기준으로 정리한 글입니다.

B-tree가 잘하는 것과 못하는 것
데이터베이스를 오래 다뤄 온 분이라도, 인덱스라는 단어를 들으면 자동으로 B-tree를 떠올리는 경우가 많습니다. 저 역시 10년 넘게 PostgreSQL과 MySQL을 만지면서 CREATE INDEX 한 줄이면 세상 모든 성능 문제가 풀리는 줄 알았던 시절이 있었죠. B-tree가 워낙 범용적이라 그렇습니다. 정렬된 트리 구조 덕분에 동등 비교(WHERE id = 100), 범위 검색(BETWEEN, >, <), 그리고 ORDER BY 정렬까지 한 인덱스로 깔끔하게 처리해 줍니다. 대부분의 OLTP 워크로드에서 B-tree만으로 90%는 커버가 됩니다.
문제는 나머지 10%였습니다. B-tree는 값을 '통째로' 비교하는 구조라, 값 안쪽을 들여다봐야 하는 질의에는 손을 못 씁니다. 제가 실제로 벽에 부딪혔던 대표적인 경우를 꼽아 보면 이렇습니다.
- 전문 검색:
WHERE body LIKE '%장애%'처럼 문자열 중간을 찾는 질의. 앞부분이 고정된'장애%'는 B-tree가 쓰이지만, 앞에%가 붙는 순간 인덱스는 무용지물이 되죠. - JSONB·배열 포함 검색:
WHERE tags @> '["urgent"]'같은 포함 연산자. B-tree는 JSONB 문서 하나를 하나의 값으로만 보기 때문에 내부 키를 색인하지 못합니다. - 공간 데이터: 좌표 범위가 겹치는지 묻는
&&연산. 2차원 이상의 겹침은 1차원 정렬 트리로 표현이 안 됩니다. - 초거대 append 전용 테이블: 로그나 시계열처럼 계속 쌓이기만 하는 테이블. 여기서는 B-tree가 동작은 하지만 인덱스 크기가 감당이 안 됩니다.
여기에 한 가지 덧붙이자면, B-tree의 약점은 '값을 통째로 본다'는 특성에서 파생됩니다. 예컨대 한 행의 배열 컬럼에 태그가 다섯 개 들어 있으면, 사람은 그걸 다섯 개의 검색 대상으로 보지만 B-tree는 배열 하나를 단일 값으로만 취급합니다. 그래서 "이 태그를 포함하는 행"이라는 질의에 인덱스를 태우지 못하고 결국 Seq Scan으로 떨어지죠. 전문 검색이나 JSONB도 원리는 똑같습니다. '내부를 쪼개서 색인해야 하는 데이터'에는 B-tree의 정렬 트리 모델 자체가 맞지 않는 것입니다.
특히 마지막 항목은 뒤에서 따로 풀어놓을 만큼 저를 오래 괴롭혔습니다. 아래 지도는 인덱스 타입을 질의 성격에 따라 어디에 배치해야 하는지 제가 팀 위키에 그려 붙였던 그림입니다.
타입별 강점
PostgreSQL이 좋은 이유는 B-tree 말고도 여러 인덱스 타입을 기본 제공한다는 점입니다. 각각은 특정 질의 형태에 최적화돼 있어서, 상황에 맞게 고르면 성능이 몇 배가 아니라 몇 십 배씩 벌어지곤 하죠. 제가 실무에서 쓰면서 정리한 용도는 다음과 같습니다.
- Hash — 오직 동등 비교(
=)만을 위한 타입입니다. 범위나 정렬은 못 하지만, 순수 등가 조회에서는 B-tree보다 살짝 가볍습니다. 세션 토큰처럼 긴 문자열을=로만 찾는 컬럼에 가끔 씁니다. 다만 B-tree가 워낙 잘해서 실전 채택률은 낮은 편이죠. - GiST — 공간 데이터와 범위 겹침의 왕입니다. PostGIS의 좌표 검색,
tsrange예약 시간대가 겹치는지 확인하는&&배제 제약 등에 씁니다. 회의실 예약 서비스에서 시간대 중복을 막을 때EXCLUDE USING gist를 걸어 두면 애플리케이션 코드 없이 DB가 충돌을 막아 주죠. - GIN — 전문 검색, JSONB, 배열의 포함 검색을 담당합니다.
to_tsvector기반 문서 검색,jsonb @> ...,array @> ...가 전부 GIN 몫입니다. 하나의 행에 여러 토큰이 들어 있는 '다중 값' 컬럼을 색인할 때 압도적입니다. - BRIN — Block Range Index. 정렬돼 쌓이는 초대형 테이블용입니다. 개별 행이 아니라 블록 묶음의 최소·최대값만 요약해 저장하기 때문에, 인덱스가 놀랄 만큼 작습니다.
아래 표는 위 네 타입을 한눈에 비교하려고 만든 것입니다.

제가 GIN의 위력을 처음 체감한 건 사내 검색 기능을 만들 때였습니다. 문서 30만 건을 LIKE '%키워드%'로 훑던 질의가 평균 2.3초씩 걸리던 걸, to_tsvector 컬럼에 GIN 인덱스를 얹자 20ms대로 떨어졌습니다. 100배 넘게 빨라진 거죠. 대신 GIN은 쓰기 비용이 큽니다. 하나의 행을 넣을 때 토큰 수만큼 인덱스 항목이 갱신되기 때문에, 삽입이 잦은 테이블에서는 fastupdate 옵션과 gin_pending_list_limit를 함께 조율해야 부하가 잡힙니다. 강점과 비용이 늘 짝으로 온다는 점을 잊으면 안 되겠죠.
핵심은 "이 컬럼을 어떤 연산자로 조회하는가"입니다. =·범위·정렬이면 B-tree, 포함(@>)·전문 검색이면 GIN, 공간·겹침이면 GiST, 그리고 물리적으로 정렬된 거대 테이블이면 BRIN. 이 한 줄만 몸에 배어도 인덱스 설계의 절반은 끝난 셈이죠.
시계열에 BRIN을 써 본 이야기
2024년 초, 제가 맡았던 IoT 센서 수집 파이프라인 이야기입니다. 디바이스들이 초당 수천 건씩 측정값을 밀어 넣는 구조였는데, measurements 테이블이 약 18억 행, 물리 크기로 약 240GB까지 불어나 있었습니다. 조회는 대부분 WHERE recorded_at BETWEEN ... AND ... 형태의 시간 범위였고, 저는 아무 의심 없이 recorded_at에 B-tree 인덱스를 걸어 두고 있었죠.
문제는 인덱스 크기였습니다. 이 B-tree 하나가 약 9.7GB를 차지했고, 매일 파티션을 붙일 때마다 인덱스 유지 비용과 autovacuum 부담이 눈에 띄게 커졌습니다. 야간 배치가 도는 시간에 인덱스 재구성이 겹치면 수집 지연이 밀리기까지 했습니다. 그때 동료가 "이거 append-only에 시간순으로 쌓이잖아, BRIN 안 봤어?"라고 던진 한마디가 시작이었습니다.
BRIN이 통했던 이유는 데이터의 물리적 순서였습니다. recorded_at은 삽입 순서와 거의 일치했기 때문에, 각 블록 묶음의 최소·최대 시각만 요약해도 범위 질의에서 관련 없는 블록을 통째로 건너뛸 수 있었습니다. 바꾼 명령은 사실상 이 정도였습니다.
-- 블록 요약 인덱스로 교체 (128개 블록 단위 요약)
CREATE INDEX idx_meas_brin ON measurements
USING brin (recorded_at) WITH (pages_per_range = 128);
결과는 인상적이었습니다. 9.7GB짜리 B-tree가 BRIN에서는 약 40MB로, 240분의 1 수준까지 줄었습니다. 인덱스 빌드 시간도 수십 분에서 1분 남짓으로 짧아졌고, 무엇보다 autovacuum이 인덱스를 훑는 부담이 사라지면서 야간 수집 지연이 없어졌습니다.
물론 공짜는 아니었습니다. 트레이드오프는 솔직히 적어 둬야 공정하겠죠.
- 좁은 범위 조회(예: 특정 1분 구간)에서는 BRIN이 블록 묶음을 넉넉히 훑기 때문에, 핀포인트 조회는 B-tree가 여전히 더 빨랐습니다.
- 데이터의 물리 순서가 컬럼 값과 어긋나면(중간
UPDATE가 잦거나 순서가 뒤섞이면) BRIN의 블록 요약이 넓어져 효율이 급락합니다. 저희는 append-only라 다행이었죠. - 새로 들어온 블록은
brin_summarize_new_values()로 요약을 갱신해 줘야 최신 구간까지 잘 걸립니다. 저희는 이 호출을 수집 배치 끝단에 넣어 자동화했죠.
한 가지 더 배운 건 pages_per_range 값의 감각입니다. 이 값이 작으면 요약이 촘촘해져 조회는 정확해지지만 인덱스가 커지고, 크면 반대가 되죠. 저는 128로 시작해 실제 조회 패턴을 EXPLAIN ANALYZE로 며칠 관찰한 뒤 64로 낮춰 조회 효율과 크기의 균형점을 잡았습니다. 결국 인덱스 튜닝은 이론값이 아니라 우리 데이터의 실제 분포와 질의를 보고 맞춰 가는 작업이라는 걸 다시 확인했습니다.
지금이라면 시계열·로그 테이블은 처음부터 시간 컬럼에 BRIN을 깔고, 정말 좁은 핀포인트 조회가 잦은 컬럼에만 B-tree를 추가하는 식으로 설계하겠습니다. 아래 비교 그림이 그때의 크기 차이를 한눈에 보여 줍니다.

인덱스는 하나의 만능 도구가 아니라, 질의 형태에 맞춰 고르는 연장통이라는 걸 그해에 제대로 배웠습니다. B-tree만 알던 저에게 GIN과 BRIN은 정말 새로운 세계였죠.