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(별도 저장소로 분리·압축)로 넘어가 접근 비용이 달라진다.