백엔드 데이터 인프라 35편. PG 인덱스 6가지 종류 — B-Tree·Hash·GIN·GiST·BRIN·SP-GiST의 의미와 언제 무엇 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 70편 중 35편이에요. 34편 인덱스 소개 가 "왜·언제" 였다면, 이번 35편은 "PG가 가진 6가지 인덱스 종류" — 각자 다른 상황에 정답.
PG 인덱스 6종 — 한눈에
| 인덱스 | 용도 | 예 |
|---|---|---|
| B-Tree (기본) | 정렬 가능한 모든 타입 | =·<·>·BETWEEN·ORDER BY |
| Hash | 정확 비교만 | = (B-Tree 가 거의 항상 우수) |
| GIN | 다중 값 (배열·JSONB·전문 검색) | @>·?·tsvector |
| GiST | 기하·범위·전문 검색 | &&·<<·@> |
| BRIN | 대용량 정렬 데이터 | created_at 시계열 |
| SP-GiST | 공간 분할 트리 | 텍스트·접두사 |
거의 99% = B-Tree + GIN. 나머지는 특수 상황.
(1) B-Tree — 기본 표준
CREATE INDEX idx ON users(email);
-- 또는 명시
CREATE INDEX idx ON users USING BTREE (email);
지원 연산자:
WHERE col = ?
WHERE col < ? / > ? / <= ? / >= ?
WHERE col BETWEEN ? AND ?
WHERE col IS NULL / IS NOT NULL
WHERE col LIKE 'prefix%' -- 앞 와일드카드 없을 때
ORDER BY col
복합 인덱스도 B-Tree.
CREATE INDEX idx ON orders(user_id, status, created_at DESC);
정렬 옵션
CREATE INDEX idx ON orders(created_at DESC NULLS LAST);
ORDER BY 와 같은 방향이면 — 정렬 비용 0.
복합 인덱스 순서 — 매우 중요
복합 인덱스 (a, b, c) 의 활용 규칙:
-- ✅ 활용 (왼쪽 prefix)
WHERE a = ?
WHERE a = ? AND b = ?
WHERE a = ? AND b = ? AND c = ?
WHERE a = ? AND b > ? -- a 정확 + b 범위
-- ⚠️ 부분 활용
WHERE a = ? AND c = ? -- a 만 인덱스 활용
-- ❌ 비활용
WHERE b = ? -- a 빠짐
WHERE c = ? -- a, b 빠짐
룰: "가장 자주 사용·정확 비교" 컬럼을 앞쪽.
인덱스 컬럼 순서 결정 — 3가지 기준
- 선별성(Selectivity) — 값 분포 다양한 컬럼 앞
- 정확 비교(=) 자주 나오는 컬럼 앞
- 범위(<·>) 컬럼은 뒤
예 — 주문 검색:
-- WHERE user_id = 1 AND status = 'PAID' AND created_at >= '2026-01-01'
CREATE INDEX idx ON orders(user_id, status, created_at);
user_id 정확 (선별성 높음) → status 정확 → created_at 범위. 표준 순서.
(2) Hash 인덱스
CREATE INDEX idx ON users USING HASH (email);
= 만 지원. B-Tree 도 = 빠른데 — Hash 가 "이론적으로 약간 더 빠를 수 있지만" 실무 차이 거의 없음. 거의 안 씀.
PG 10+ 에서 WAL 지원 — 옛 "Hash 인덱스는 복제 안 됨" 문제 해결. 다만 여전히 B-Tree 우세.
(3) GIN — 다중 값의 강자
-- 배열
CREATE INDEX idx ON posts USING GIN (tags);
-- JSONB
CREATE INDEX idx ON events USING GIN (payload);
CREATE INDEX idx ON events USING GIN (payload jsonb_path_ops);
-- 전문 검색 (tsvector)
CREATE INDEX idx ON articles USING GIN (to_tsvector('english', body));
지원 연산자:
- 배열 = @>·<@·&&·= ANY
- JSONB = @>·?·?|·?&·jsonb_path_ops
- 전문 검색 = @@
GIN 의 함정 — 쓰기 비용
GIN 인덱스 = "읽기 빠름, 쓰기 느림". 배열·JSON 의 모든 요소를 인덱스에 박음. 한 행 INSERT 가 N개 인덱스 항목 생성.
대용량 쓰기 시 — fastupdate = on (기본) 이 "임시 보관소(pending list)" 에 모았다가 일괄 처리. 빠른 INSERT.
(4) GiST — 기하·범위·다양
-- 기하
CREATE INDEX idx ON locations USING GiST (point);
-- 범위
CREATE INDEX idx ON bookings USING GIST (period);
-- EXCLUDE 제약과 짝
CREATE TABLE bookings (
period TSTZRANGE,
EXCLUDE USING GIST (period WITH &&)
);
23편 EXCLUDE 의 핵심.
GIN vs GiST: - GIN = 다중 값에 강 (배열·JSONB) - GiST = 범위·기하·"커스텀 데이터 구조"
btree_gist 확장
CREATE EXTENSION btree_gist;
EXCLUDE USING GIST (room_id WITH =, period WITH &&)
GiST 가 = 연산자도 지원하도록.
(5) BRIN — 대용량 시계열
CREATE INDEX idx ON logs USING BRIN (created_at);
Block Range Index = 페이지 범위별 "최소·최대" 만 기록. B-Tree 의 1/100 크기·1/100 메모리.
특징: - ✓ 매우 작은 크기 - ✓ 큰 테이블에 빠른 빌드 - ✗ B-Tree 보다 조회 약간 느림 - 데이터가 물리 순서대로 박혀 있어야 효율적
완벽한 시나리오 — 로그·시계열 (시간 순서로 자연 정렬).
-- 1억 행 로그 테이블
CREATE INDEX idx_logs_created_brin ON logs USING BRIN (created_at);
-- 크기 = 100KB (B-Tree 는 2GB)
SELECT * FROM logs WHERE created_at >= '2026-05-17';
-- BRIN 활용 — 적절한 페이지만 스캔
(6) SP-GiST — 공간 분할
CREATE INDEX idx ON ip_logs USING SPGIST (ip);
비균등 데이터 구조 (Quad-tree·KD-tree). 텍스트 접두사·IP 등. 잘 안 씀.
어느 인덱스 — 룰
일반 컬럼 → B-Tree
정확 비교만 → B-Tree (Hash X)
배열·JSONB → GIN
범위 타입 → GiST
기하 (PostGIS) → GiST
대용량 시계열 (created_at) → BRIN
전문 검색 → GIN (tsvector)
커버링 인덱스 — INCLUDE
CREATE INDEX idx_orders ON orders(user_id) INCLUDE (status, amount);
INCLUDE = 인덱스에 "검색 키는 아니지만 함께 저장". 인덱스만으로 결과 답 가능 (Index Only Scan) → 테이블 안 봐도 됨.
SELECT status, amount FROM orders WHERE user_id = 1;
-- Index Only Scan — 매우 빠름
자주 함께 조회되는 컬럼 INCLUDE.
Index Only Scan
EXPLAIN 출력의 좋은 신호.
Index Only Scan using idx_orders on orders
Heap Fetches: 0 ← 테이블 안 봄
Heap Fetches: 0 = 인덱스만으로 답. 매우 빠름. INCLUDE 또는 "모든 결과 컬럼이 인덱스에" 일 때.
함정 5가지
(1) 복합 인덱스 순서 잘못
선별성 낮은 컬럼 앞에 박으면 — 인덱스 효율 낮음.
(2) Hash 인덱스 신규 사용
B-Tree 가 거의 항상 우수. Hash 는 거의 안 씀.
(3) GIN 쓰기 비용 무시
배열·JSONB 가 자주 변경되면 GIN 비용 큼. fastupdate = on 의존.
(4) BRIN 을 정렬 안 된 데이터에
행이 무작위 박힌 컬럼에 BRIN = 효율 0. created_at 같은 자연 정렬만.
(5) ORDER BY 인덱스 방향 불일치
CREATE INDEX idx ON orders(created_at ASC);
SELECT ... ORDER BY created_at DESC;
-- 역방향 스캔 (가능하지만 일부 느림)
자주 쓰는 방향으로.
(1) 일반 = B-Tree (기본). (2) 배열·JSONB = GIN. (3) 범위·기하 = GiST. (4) 대용량 시계열 = BRIN. (5) 복합 인덱스 순서 = 선별성 + 정확 비교 + 범위. INCLUDE 로 Index Only Scan.
한 줄 정리 — 인덱스 6종. 실무 99% = B-Tree·GIN. 복합 인덱스 순서 = 선별성·정확 비교·범위. GIN = 배열·JSONB·전문 검색. GiST = 범위·기하·EXCLUDE. BRIN = 시계열. INCLUDE 로 Index Only Scan.
시험 직전 한 번 더 — 인덱스 종류 입문자가 매번 헷갈리는 것
- 인덱스 6종 = B-Tree·Hash·GIN·GiST·BRIN·SP-GiST
- 실무 99% = B-Tree + GIN
- B-Tree = 정렬 가능 타입 기본
- =·<·>·BETWEEN·LIKE 'prefix%'·ORDER BY
- 복합 인덱스 = (a, b, c) 왼쪽 prefix
- WHERE b = ? = 인덱스 X (a 빠짐)
- WHERE a + b = 인덱스 OK
- 순서 결정 = 선별성·정확 비교·범위
- 정확 비교 컬럼 앞, 범위 뒤
- Hash =
=만, 거의 안 씀 - GIN = 배열·JSONB·전문 검색
- @>·?·?|·?&·@@
- GIN 쓰기 비용 = fastupdate
- GiST = 범위·기하·EXCLUDE
- btree_gist = GiST +
= - TSTZRANGE·POINT
- BRIN = 대용량 정렬 데이터
- 크기 1/100, 속도 약간 느림
- 시계열 created_at 표준
- 데이터가 물리 정렬되어야 효율
- SP-GiST = 비균등 (잘 안 씀)
- INCLUDE = 커버링 인덱스
- Index Only Scan = 인덱스만으로 (테이블 X)
- Heap Fetches: 0 = 좋은 신호
- ORDER BY 인덱스 방향 일치 권장
- 운영 = CONCURRENTLY 무조건
- 한국 백엔드 = B-Tree 복합 + GIN JSONB + BRIN 로그
시리즈 다른 편
- Part 2 SQL Language 깊이: 33편 JSONB · 34편 인덱스 소개 · 35편 (현재 글)
시리즈 다음 글
다음 글(36편)에서는 인덱스 종합 — 운영 전략·튜닝·함정 모음.
공식 문서: PostgreSQL 18 — Indexes: Types에서 더 자세한 사양을 확인할 수 있어요.