PostgreSQL 심화 — 트랜잭션·인덱스·JSONB·파티션 · 인덱스 설계 · 이론
인덱스 설계 — 질의에 맞춰 고른다
한 줄 요약
인덱스는 '만들면 빨라지는 것' 이 아니라 질의의 모양에 맞춰 고르는 것이다. 복합 인덱스의 열 순서, 부분·표현식·커버링 인덱스, jsonb 와 시계열에 맞는 인덱스 종류를 모르면, 인덱스를 잔뜩 만들고도 순차 스캔을 보게 된다.
왜 이게 필요했나
느린 질의에 인덱스를 하나 만들었는데 계획이 그대로다. 또 하나 만든다. 여전히 순차 스캔이다. 인덱스는 다섯 개가 됐고 쓰기는 느려졌는데 읽기는 빨라지지 않았다.
원인은 인덱스가 질의의 모양과 안 맞기 때문이다. lower(email) 로 찾는데 email 에 인덱스를 걸었고, 두 열을 함께 거르는데 단일 열 인덱스 둘을 만들었고, jsonb 담김을 B 트리로 받으려 했다. 인덱스는 종류와 열 순서가 질의와 맞아떨어질 때만 쓰인다.
어떻게 동작하나
B 트리 인덱스는 값으로 정렬된 구조다. 그래서 등호와 범위 탐색에 쓰이고, 열에 함수를 씌우면 그 정렬이 쓸모없어진다. 여기서 실무의 설계 규칙이 나온다.
- 복합 인덱스는 선두 열이 전부다.
(tenant_id, kind)는 전화번호부를 성으로 정렬한 것과 같아서, 성을 아는 질의에는 좋지만 이름만 아는 질의에는 못 쓴다. 두 열을 함께 거르는 질의는 단일 열 인덱스 둘(BitmapAnd)보다 복합 인덱스 하나가 낫다. - 부분 인덱스는
WHERE status='error'처럼 일부 행만 담아, 그 조건을 자주 찾을 때 훨씬 작다. - 표현식 인덱스는
lower(email)자체에 걸어 함수 질의를 받는다. - 커버링 인덱스는
INCLUDE (amount)로 필요한 열을 실어, 힙을 안 만지는 Index Only Scan 을 만든다.
종류를 질의에 맞춘다
값의 종류마다 맞는 인덱스가 다르다.
| 대상 | 맞는 인덱스 | 이유 |
| --- | --- | --- |
| jsonb 담김(@>), 배열 포함 | GIN | 문서 안의 키·값을 역색인으로 |
| 본문 전문 검색(to_tsvector) | GIN | 단어를 역색인으로 |
| 삽입 순서와 같은 시계열 | BRIN | 블록 범위만 저장해 B 트리의 수백 분의 일 크기 |
인덱스는 공짜가 아니다. 쓰기마다 갱신되고 자리를 차지한다. 그래서 안 쓰이는 인덱스는 부채다. pg_stat_user_indexes 의 idx_scan 이 0인 인덱스는 아무도 안 쓰면서 쓰기만 느리게 한다.
실무에서 진짜 중요한 것
첫째, 인덱스는 질의를 보고 만든다. 열 하나를 보고 만드는 게 아니라, WHERE·ORDER BY·GROUP BY 에 무엇이 오는지를 보고 종류와 열 순서를 정한다. EXPLAIN (FORMAT JSON) 으로 계획이 정말 그 인덱스를 쓰는지 확인하기 전에는 끝난 게 아니다.
둘째, 큰 결과에는 인덱스가 손해일 수 있다. 조건이 전체의 상당 비율을 통과시키면, 인덱스로 힙을 랜덤 방문하는 비용이 순차 스캔보다 커진다. 옵티마이저가 인덱스를 안 쓰는 것은 대개 옳은 판단이다.
셋째, 안 쓰이는 인덱스는 지운다. idx_scan 을 정기적으로 보고, 아무도 안 쓰는 인덱스는 쓰기 비용만 무는 부채이니 걷어 낸다. 다음 실습에서 20만 행 위에서 이 설계들을 직접 만들고 계획으로 확인한다.