본문으로 건너뛰기
6.4 GIN 인덱스

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 작성
UPDATEINSERT·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 미지원
매우 자주 갱신되는 컬럼에 GININSERT/UPDATE 비용 무거움 — 표현식 B-tree 검토
to_tsvector(...)을 매번 호출하면서 GIN 정의는 column에인덱스 활용 안 됨. to_tsvector 결과를 컬럼으로 저장하거나 표현식 인덱스 정의 일치
GIN 인덱스의 동시 빌드는 매우 느릴 수 있다. 큰 JSONB 컬럼은 인덱스 빌드만 수십 분 걸리는 경우도. 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 인덱스를 봅니다.