데이터베이스 용어 사전
PostgreSQLGIN 인덱스 · 부분 인덱스 · 표현식 인덱스

JSONB

PostgreSQL이 JSON을 이진 형태로 저장하는 타입. 인덱스를 걸 수 있어 관계형 안의 문서형으로 쓴다.

PostgreSQL이 JSON을 파싱된 이진 형태로 저장하는 타입. 관계형 테이블 안에서 문서형 유연성을 얻는다.

json과 jsonb의 차이

  • json — 입력 텍스트를 그대로 보관한다

    • 공백·키 순서·중복 키가 유지된다. 읽을 때마다 파싱한다. 인덱스 불가
  • jsonb — 파싱해서 이진 구조로 저장한다

    • 공백 제거 · 키 정렬 · 중복 키 제거. 저장은 조금 느리고 조회가 빠르다
    • ★ 인덱스를 걸 수 있다

실무에서는 사실상 항상 jsonb를 쓴다.

조회 연산자

CREATE TABLE event (id BIGINT PRIMARY KEY, payload JSONB);

SELECT payload -> 'user'  FROM event;      -- jsonb 반환
SELECT payload ->> 'name' FROM event;      -- text 반환 (화살표 두 개)
SELECT payload #> '{user,addr,city}' FROM event;   -- 경로로 파고든다

-- 포함 여부 — GIN 인덱스가 가장 잘 듣는 연산자
SELECT * FROM event WHERE payload @> '{"status":"PAID"}';
SELECT * FROM event WHERE payload ? 'coupon_id';    -- 키 존재 여부

인덱스 — 두 가지 방식

-- ① GIN — 문서 전체를 색인한다. @> ? 같은 연산에 두루 통한다
CREATE INDEX idx_event_payload ON event USING GIN (payload);

-- ② 표현식 B-Tree — 특정 키 하나만 색인한다. 훨씬 작고 빠르다
CREATE INDEX idx_event_status ON event ((payload ->> 'status'));
SELECT * FROM event WHERE payload ->> 'status' = 'PAID';

자주 쓰는 키가 정해져 있으면 ②가 압도적으로 낫다. GIN은 유연하지만 크고, 쓰기 부담도 크다.

-- jsonb_path_ops — 포함(@>) 연산만 쓸 거면 인덱스가 훨씬 작아진다
CREATE INDEX idx_event_payload ON event USING GIN (payload jsonb_path_ops);

언제 쓰고 언제 쓰지 말아야 하나

쓸 만한 곳

  • 스키마가 자주 바뀌는 부가 속성 (상품마다 다른 스펙)
  • 외부 API 응답 원문 보관
  • 이벤트 페이로드 로그

쓰지 말아야 할 곳

  • 조인 · 집계 · 제약이 필요한 핵심 데이터
  • 자주 부분 갱신되는 큰 문서 (jsonb 는 통째로 다시 쓴다)

마지막이 중요하다 — jsonb의 한 키만 바꿔도 문서 전체가 새 버전으로 다시 쓰인다. 큰 문서를 자주 갱신하면 MVCC 관점에서 매우 비싸다.

면접 함정

  • "jsonb를 쓰면 스키마가 필요 없다" → 스키마 검증 책임이 애플리케이션으로 옮겨 갈 뿐이다. 오타 난 키가 조용히 저장된다.
  • "-> 와 ->> 는 같다" → 전자는 jsonb, 후자는 text다. 비교·인덱스에서 결과가 달라진다.

성능 함정 — 부분 갱신이 전체 재작성이다

-- 큰 문서의 키 하나만 바꿔도 행 전체가 새 버전으로 다시 쓰인다
UPDATE event SET payload = jsonb_set(payload, '{status}', '"PAID"') WHERE id = 1;
-- payload 가 100KB 면 100KB 를 다시 쓴다 → TOAST 재작성 + 죽은 튜플

자주 바뀌는 값은 일반 칼럼으로 빼는 것이 정석이다.

-- 자주 갱신·조회되는 키만 실제 칼럼으로 승격한다
ALTER TABLE event ADD COLUMN status VARCHAR(20)
  GENERATED ALWAYS AS (payload ->> 'status') STORED;   -- 생성 칼럼
CREATE INDEX ON event (status);

크기를 재 본다

SELECT pg_size_pretty(sum(pg_column_size(payload))) FROM event;
SELECT id, pg_column_size(payload) FROM event ORDER BY 2 DESC LIMIT 5;

2KB를 넘으면 TOAST(별도 저장소로 분리·압축)로 넘어가 접근 비용이 달라진다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — PostgreSQL 심화 — JSONB·파티셔닝·확장·VACUUM(17)