본문으로 건너뛰기
김신건의 로그

[DB Internals] GIN Index 깊이: 구조, jsonb_path_ops, tsvector

· 수정 · 📖 약 1분 · 435자/단어 #gin #postgresql #index #inverted #full-text #jsonb
GIN, GIN index, Generalized Inverted Index, jsonb_path_ops, tsvector GIN, posting list, posting tree, fastupdate GIN

정의

GIN (Generalized Inverted Index) = 한 값이 여러 항목 을 가지는 데이터 (배열, JSON, tsvector) 의 역색인. PostgreSQL 의 강력한 확장점.

일반 인덱스 비교는 gin-gist-hash-indexes 참고. 본 페이지는 내부 구조 에 집중.

왜 GIN?

flowchart LR
    B["B-Tree: 한 row = 한 key"]
    B --> B1["ex: name = 'kim'"]
    G["GIN: 한 row = 여러 key"]
    G --> G1["ex: tags = ['ruby', 'rails', 'redis']"]
    G --> G2["ex: payload = { 'user_id': 42, 'tags': ['a', 'b'] }"]
    G --> G3["ex: to_tsvector('rabbit hat magic')"]

구조

flowchart TB
    Root["Entry B-Tree<br/>(term/element)"]
    Root --> E1["'ruby' → posting list"]
    Root --> E2["'rails' → posting list"]
    Root --> E3["'redis' → posting list"]
    E1 --> P1["Posting list: [doc1, doc3, doc7, ...]"]
    P1 -->|"크면"| PT["Posting Tree<br/>(B-Tree of doc IDs)"]
구성의미
Entry Tree모든 term 의 B-Tree
Posting ListTerm → doc ID 목록 (작을 때 배열)
Posting TreeTerm 의 doc 이 많을 때 B-Tree

예시 (배열 컬럼)

CREATE TABLE posts (
  id BIGSERIAL PRIMARY KEY,
  tags TEXT[]
);

INSERT INTO posts (tags) VALUES
  ('{"ruby","rails","redis"}'),   -- id=1
  ('{"rails","postgres"}'),        -- id=2
  ('{"ruby","postgres","json"}');  -- id=3

CREATE INDEX idx_tags ON posts USING gin(tags);

내부:

Entry B-Tree:
  'json'     → [3]
  'postgres' → [2, 3]
  'rails'    → [1, 2]
  'redis'    → [1]
  'ruby'     → [1, 3]

Query: tags @> ARRAY['ruby', 'postgres']
  → 'ruby' posting: [1, 3]
  → 'postgres' posting: [2, 3]
  → 교집합: [3]

연산자

연산자의미
@> (contains)왼쪽이 오른쪽 포함tags @> ARRAY['ruby']
<@ (contained by)왼쪽이 오른쪽에 포함tags <@ ARRAY['ruby','rails','redis']
&& (overlap)하나라도 공통tags && ARRAY['ruby','python']
? (key exists)JSON key 존재payload ? 'user_id'
?| (any key)여러 key 중 하나payload ?| array['a','b']
?& (all keys)모든 key 존재payload ?& array['a','b']

jsonb_ops vs jsonb_path_ops

-- 1. jsonb_ops (기본): 모든 key 와 value 인덱싱
CREATE INDEX idx_payload_full ON events USING gin(payload);

-- 2. jsonb_path_ops: @> 만 인덱싱 (더 작음, 더 빠름)
CREATE INDEX idx_payload_path ON events USING gin(payload jsonb_path_ops);
jsonb_opsjsonb_path_ops
인덱스 크기~1/3
지원 연산자@>, ?, ?|, ?&, @?, @@@>
Insert 속도느림빠름
Query 속도보통빠름 (@>)

IMPORTANT

대부분 @> 만 쓴다jsonb_path_ops 권장. 크기 1/3, 속도 3x.

Full-Text Search (tsvector)

CREATE TABLE articles (
  id BIGSERIAL PRIMARY KEY,
  body TEXT,
  body_tsv TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', body)) STORED
);

CREATE INDEX idx_body_tsv ON articles USING gin(body_tsv);

-- 검색
SELECT * FROM articles
WHERE body_tsv @@ to_tsquery('english', 'rabbit & hat');

-- 랭킹
SELECT *, ts_rank(body_tsv, query) AS rank
FROM articles, to_tsquery('english', 'rabbit') query
WHERE body_tsv @@ query
ORDER BY rank DESC;

pg_trgm (LIKE 최적화)

CREATE EXTENSION pg_trgm;

CREATE INDEX idx_name_trgm ON users
USING gin(name gin_trgm_ops);

-- Prefix / suffix / substring 모두 인덱스 활용!
SELECT * FROM users WHERE name LIKE '%kim%';
SELECT * FROM users WHERE name ILIKE 'kim%';
SELECT * FROM users WHERE name % 'kim';   -- 유사도

pg_trgm = 3-gram (trigram) 분해 → GIN. LIKE 성능 획기적 향상.

Fastupdate + Pending List

flowchart LR
    Insert["INSERT"] --> Pending["Pending list<br/>(빠른 accumulate)"]
    Pending -.gin_pending_list_limit.-> Merge["주기적 merge into GIN"]
    Merge --> GIN[(GIN 본체)]

기본 활성. INSERT/UPDATE 폭증 시 GIN 재계산이 비쌈 → 일단 임시 저장 + 배치 merge.

-- 비활성 (query 성능 우선)
CREATE INDEX ... USING gin(...) WITH (fastupdate = off);

-- 임계값 조정
gin_pending_list_limit = 4MB

CAUTION

Pending list 가 크면 query 시 pending list + GIN 둘 다 스캔 → 느림. VACUUM 이 merge 트리거.

GIN 크기 문제

-- 인덱스 크기 확인
SELECT pg_size_pretty(pg_relation_size('idx_payload_full'));

-- 재빌드 (bloat 정리)
REINDEX INDEX CONCURRENTLY idx_payload_full;

큰 GIN 인덱스 (수십 GB) 는 흔함. jsonb_path_ops 로 크기 대폭 감소.

GIN + partial index

-- 특정 조건만 인덱싱
CREATE INDEX idx_recent_tags ON posts USING gin(tags)
WHERE created_at > now() - interval '30 days';

Hot data 만 인덱싱 → 크기 감소 + 빌드 시간 단축.

Insert 성능

GIN insert cost:
  - term 하나마다 posting list update
  - N terms in row → N updates

배열 컬럼에 30개 tag 삽입:
  30 개의 posting list 업데이트 = 매우 비쌈

대안:
  - fastupdate ON (기본): 임시 저장
  - Bulk load: COPY + CREATE INDEX (기존 인덱스 drop)

GIN vs GiST vs Bloom

GINGiSTBloom
자료구조InvertedBalanced treeBit vector
Insert 속도느림빠름매우 빠름
Query 속도빠름중간매우 빠름 (fp)
다값 지원아니오
사용jsonb, array, tsvectorspatial, rangemulti-column =

흔한 함정

WARNING

  1. jsonb_ops 항상 사용 = 크기 3배, 속도 저하. @> 만이면 jsonb_path_ops.
  2. Pending list 무한 성장 = query 느림. VACUUM 정기.
  3. 작은 배열에도 GIN = 배열 항목 수 <10 면 별도 컬럼 + B-Tree 가 나을 수도.
  4. HOT update 안 됨 in GIN = GIN 은 tuple ID 저장 → row 위치 바뀌면 update.

관련 위키

이 글의 용어 (5개)
[DB Internals] B-Tree 내부: split, merge, search 알고리즘database-internals
정의 B-Tree (Bayer & McCreight, 1972) = 디스크 I/O 최적화된 balanced multi-way search tree. 모든 leaf 깊이 동일 + …
[DB Internals] R-Tree: 공간 인덱스, MBR, splitdatabase-internals
정의 R-Tree (Guttman, 1984) = 공간 데이터 (2D, 3D) 를 인덱싱하는 balanced tree. 각 노드는 Minimum Bounding Rectangle…
[DB] GIN / GiST / Hash / BRIN: 비-B-tree 인덱스database-internals
정의 B-tree 외 PostgreSQL 의 특수 인덱스 들. 자세한 B-tree 는 참고. | 인덱스 | 적합 | |---|---| | B-tree | = < > BETWEEN…
[DB] PostgreSQL: 프로세스 모델, MVCC, WAL, 확장성database-internals
정의 PostgreSQL 은 오픈소스 ORDBMS. 1986 UC Berkeley POSTGRES 의 후예. MVCC, 확장 가능 타입, JSONB, full-text searc…
[PostgreSQL] JSONB: 장점과 단점, 인덱싱, 사용 패턴database-internals
정의 JSONB (Binary JSON, PG 9.4+) = PostgreSQL 의 binary 직렬화된 JSON. 인덱싱 가능 + 빠른 query 가 과의 핵심 차이. [!IM…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기