6.4 GIN 인덱스
GIN(Generalized Inverted Index)은 한 값이 여러 element를 갖는 데이터 타입에 강하다 — JSONB의 키, 배열의 element, 텍스트의 단어·trigram. 운영자가 마주치는 가장 흔한 사례는 JSONB containment, full-text search, LIKE '%foo%' 가속(pg_trgm).
구조 직관
GIN은 역색인(inverted index) 구조입니다. 각 element 별로 그 element를 포함하는 row의 ctid 목록을 보관.
element "react" → row 12, 45, 78, ...
element "redux" → row 12, 78, ...
element "vue" → row 33, 91, ...'{"tags": ["react", "redux"]}' 같은 row를 인덱싱하면, “react”·“redux” 각각의 posting list에 해당 row가 들어갑니다.
잘 처리하는 쿼리 패턴
| 데이터 타입 | 연산자 | 의미 |
|---|---|---|
jsonb | @>, <@, ?, `? | , ?&` |
text (tsvector) | @@ | full-text match |
text (pg_trgm) | LIKE '%foo%', %, <-> | 부분 일치, 유사도 |
array | @>, <@, && | 포함·겹침 |
hstore | @>, ?, `? | , ?&` |
JSONB 인덱스
CREATE TABLE events (
id bigserial PRIMARY KEY,
payload jsonb NOT NULL
);
-- 가장 일반적인 형태 — 전체 트리 인덱싱
CREATE INDEX idx_events_payload ON events USING gin (payload);
-- containment 쿼리
SELECT * FROM events
WHERE payload @> '{"user": {"city": "Seoul"}}';
-- key 존재
SELECT * FROM events WHERE payload ? 'error';
-- 키 여러 개
SELECT * FROM events WHERE payload ?& array['error', 'stack'];@>는 좌변이 우변을 포함합니다. 가장 흔히 쓰는 JSONB 쿼리.
jsonb_path_ops opclass
기본 opclass는 모든 key·value를 인덱싱하지만 큽니다. jsonb_path_ops는 path 단위로만 인덱싱해 크기가 작고 @> 쿼리에 더 빠르다 (하지만 ? 등 일부 연산자는 안 됨).
CREATE INDEX idx_events_payload_path
ON events USING gin (payload jsonb_path_ops);| opclass | 지원 연산자 | 크기 |
|---|---|---|
jsonb_ops (기본) | @>, <@, ?, `? | , ?&` |
jsonb_path_ops | @>만 | 작음·빠름 |
@>만 쓰면 jsonb_path_ops가 정석입니다.
특정 경로만 인덱싱 (expression index)
전체 트리를 인덱싱하지 말고 자주 쓰는 경로만:
CREATE INDEX idx_events_user_city
ON events ((payload->'user'->>'city'));
-- 쿼리
SELECT * FROM events
WHERE payload->'user'->>'city' = 'Seoul';이건 B-tree expression index — 일반 등호 쿼리가 빠릅니다. 운영에서 자주 쿼리하는 경로 = 표현식 인덱스, 다양한 containment = GIN 패턴이 흔합니다.
Full-text search (FTS)
CREATE INDEX idx_articles_search
ON articles USING gin (to_tsvector('simple', body));
-- 검색
SELECT * FROM articles
WHERE to_tsvector('simple', body) @@ to_tsquery('simple', 'postgres & mvcc');쿼리·인덱스 모두 같은 configuration(simple, english, korean 등)을 써야 합니다. 자세한 FTS 동작은 별도 주제 — Part XVI에서.
pg_trgm — LIKE '%foo%' 가속
CREATE EXTENSION pg_trgm;
CREATE INDEX idx_users_name_trgm
ON users USING gin (name gin_trgm_ops);
-- 부분 일치, 인덱스 활용
SELECT * FROM users WHERE name LIKE '%kim%';
-- 유사도 검색
SELECT name, similarity(name, 'gimm')
FROM users
WHERE name % 'gimm' -- similarity > pg_trgm.similarity_threshold
ORDER BY name <-> 'gimm'
LIMIT 5;trigram = 3글자 묶음. “postgres"는 pos, ost, stg, tgr, gre, res 같은 trigram들로 분해돼 인덱싱됩니다.
배열 인덱스
CREATE TABLE posts (id serial, tags text[]);
CREATE INDEX idx_posts_tags ON posts USING gin (tags);
SELECT * FROM posts WHERE tags @> ARRAY['postgres'];
SELECT * FROM posts WHERE tags && ARRAY['postgres', 'mvcc'];GIN의 비용 구조
| 측면 | 특징 |
|---|---|
| 검색 | 매우 빠름 — element별 posting list 합집합·교집합 |
| INSERT | 느림 — 한 row가 여러 element를 가지면 그만큼 entry 작성 |
| UPDATE | INSERT·DELETE 양쪽 — 컬럼 갱신이 잦으면 BLOAT 가속 |
| 인덱스 크기 | 큰 편 — element 수에 비례 |
fastupdate — INSERT 가속
GIN에는 pending list 메커니즘이 있어 새 entry를 즉시 트리에 안 꽂고 별 목록에 모았다가 일괄 처리:
fastupdate = on (기본)
gin_pending_list_limit = 4MB (기본)| 효과 | 메모 |
|---|---|
| INSERT 빠름 | pending list에만 추가 |
| 첫 SELECT 느림 | pending list 통합이 일어남 (또는 백그라운드) |
| autovacuum이 정리 | 정기적으로 pending list flush |
write-heavy + 즉시 select 둘 다 중요한 시스템에서는 fastupdate = off로 검토합니다.
BLOAT과 REINDEX
GIN은 잦은 UPDATE에 BLOAT이 빠르게 자랍니다.
-- 인덱스 크기 추적
SELECT pg_size_pretty(pg_relation_size('idx_events_payload'));
-- 재구성
REINDEX INDEX CONCURRENTLY idx_events_payload;GIN의 BLOAT 추정 도구는 B-tree만큼 정밀하지 않다 (pg_repack 등 도구 일부 한계). 정기 REINDEX 일정에 포함시키는 게 정석입니다.
운영 안티패턴
| 안티패턴 | 문제 |
|---|---|
| 모든 JSONB 컬럼에 GIN | 잦은 UPDATE에 BLOAT 폭증, 인덱스 크기 비대 |
| GIN을 unique constraint에 시도 | unique 미지원 |
| 매우 자주 갱신되는 컬럼에 GIN | INSERT/UPDATE 비용 무거움 — 표현식 B-tree 검토 |
to_tsvector(...)을 매번 호출하면서 GIN 정의는 column에 | 인덱스 활용 안 됨. to_tsvector 결과를 컬럼으로 저장하거나 표현식 인덱스 정의 일치 |
CONCURRENTLY로 시작하면서 진행률은 pg_stat_progress_create_index로 추적합니다.정리
- GIN = 한 값이 여러 element를 갖는 데이터에 강함 (JSONB·tsvector·array·trgm)
- JSONB는 보통
gin (payload jsonb_path_ops)+ 자주 쓰는 경로의 expression B-tree 조합 - pg_trgm + GIN으로
LIKE '%foo%'·유사도 검색 가속 - INSERT·UPDATE 비용이 무거움 —
fastupdate메커니즘이 완화 - BLOAT이 빠르게 자라 정기
REINDEX CONCURRENTLY필요 - unique constraint·multi-column 미지원
다음 절(6.5)에서는 대용량 정렬 데이터에 강한 BRIN 인덱스를 봅니다.