본문 바로가기
C.W.K.
Stream
Lesson 01 of 10 · published

JSONB, 관계형 안의 반정형 데이터

~14 min · extensions, jsonb

Level 0스키마 새싹
0 XP0/86 lessons0/10 achievements
0/120 XP to next level120 XP to go0% complete

JSON을 저장하면서도 질의한다

JSONBJSON을 넣을 때 이진 형태로 해석해 저장해. 원문 텍스트를 그대로 보존하는 JSON과 달리 키 순서나 공백보다 값의 구조를 다루며 GIN 색인과 풍부한 연산자를 쓸 수 있어.

자주 쓰는 연산자

  • ->data->'user'처럼 JSONB 값을, ->>data->>'name'처럼 텍스트를 꺼내.
  • data @> '{"type":"click"}'@> 포함 관계를, data ? 'email'?로 키 존재를 검사해.
  • jsonb_path_exists(data, '$.orders[*] ? (@.total > 100)')처럼 SQL/JSON 경로로 중첩 구조를 찾을 수 있어.

유연함과 무규칙은 달라

이벤트 payload나 외부 API 응답처럼 모양이 정말 달라지는 부분에는 JSONB가 좋아. 모든 행에 같은 필드가 있다면 자료형과 제약을 가진 실제 열로 올려. JSONB는 스키마를 피하는 면허가 아니지만, 정말 가변적인 문서라면 많은 경우 MongoDB를 별도로 두지 않고도 다룰 수 있어.

모든 행에 같은 5개 필드가 반복된다면 JSONB가 아니라 실제 열로 승격할 신호야.

Code

JSONB 값을 만들고 조회하기·sql
CREATE TABLE events (
    id   INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    data JSONB NOT NULL
);

CREATE INDEX events_data_gin ON events USING gin (data);

INSERT INTO events (data) VALUES
('{"type":"click","page":"/home","user_id":42}'),
('{"type":"signup","plan":"pro","user_id":43}'),
('{"type":"click","page":"/blog","user_id":42}');

-- Containment 쿼리 (GIN 사용)
SELECT data->>'page' AS page, COUNT(*)
FROM   events
WHERE  data @> '{"type":"click"}'
GROUP  BY 1;

-- SELECT 의 필드 접근
SELECT id, data->>'type' AS event_type, (data->>'user_id')::int AS user_id
FROM   events;
JSONB 필드 갱신하기·sql
-- 필드 설정 또는 교체
UPDATE events SET data = jsonb_set(data, '{plan}', '"enterprise"') WHERE id = 2;

-- 필드 제거
UPDATE events SET data = data - 'page' WHERE id = 1;

-- 두 JSONB document 병합 (충돌 시 오른쪽 이김)
UPDATE events SET data = data || '{"reviewed": true}'::jsonb WHERE id = 1;

External links

Exercise

테이블에 JSONB metadata 열을 추가하고 서로 다른 키를 가진 행을 넣어. @>로 특정 키와 값을 포함한 행을 조회한 뒤 GIN 색인을 추가해 EXPLAIN 전후를 비교해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.