Files
2nd/10_Wiki/Topics/Coding/DB_JSONB_Postgres_Patterns.md
T
2026-05-09 21:08:02 +09:00

4.4 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
db-jsonb-postgres-patterns Postgres JSONB — 인덱스 / 쿼리 / 마이그레이션 Coding draft B conceptual 2026-05-09 2026-05-09
database
postgres
jsonb
vibe-coding
language applicable_to
SQL / Postgres
Backend
JSONB
GIN index
jsonb_path_query
schemaless

Postgres JSONB

Schemaless 가 필요할 때만. JSON 보다 JSONB (binary, indexable). GIN 인덱스로 빠른 검색. 하지만 정형 데이터는 컬럼이 항상 우월.

📖 핵심 개념

  • JSON: text 저장, parse 매 query. 순서 / 공백 보존.
  • JSONB: binary, parse 한 번, 더 빠름. 사실상 항상 JSONB.
  • GIN 인덱스: 모든 key/path 검색.
  • Operator: ->, ->>, @>, ?, #>, jsonb_path_query.

💻 코드 패턴

스키마

CREATE TABLE events (
  id BIGSERIAL PRIMARY KEY,
  type TEXT NOT NULL,
  data JSONB NOT NULL DEFAULT '{}'::jsonb,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- GIN 인덱스 (모든 key)
CREATE INDEX events_data_gin ON events USING GIN (data);

-- 또는 특정 path 만 (작고 빠름)
CREATE INDEX events_user ON events ((data->>'userId'));

Read 패턴

-- 추출 (JSON 으로) vs (text 로)
SELECT data->'user' FROM events;       -- jsonb
SELECT data->>'userId' FROM events;    -- text

-- nested
SELECT data#>>'{user,address,city}' FROM events;

-- Containment
SELECT * FROM events WHERE data @> '{"userId": "u1"}';

-- Key 존재
SELECT * FROM events WHERE data ? 'userId';

-- jsonb_path (JSON Path)
SELECT * FROM events WHERE data @? '$.items[*] ? (@.qty > 10)';

Write 패턴

-- 통째 update
UPDATE events SET data = '{"userId":"u1"}'::jsonb WHERE id = 1;

-- 부분 update
UPDATE events SET data = data || '{"shipped": true}'::jsonb WHERE id = 1;

-- nested set
UPDATE events SET data = jsonb_set(data, '{user,name}', '"Alice"', true) WHERE id = 1;

-- 키 제거
UPDATE events SET data = data - 'temp' WHERE id = 1;

배열

-- 길이
SELECT jsonb_array_length(data->'tags') FROM events;

-- 펼치기
SELECT id, tag FROM events, jsonb_array_elements_text(data->'tags') AS tag;

-- append
UPDATE events SET data = jsonb_set(data, '{tags}', (data->'tags') || '"new"'::jsonb);

Generated column (자주 read 하는 키)

ALTER TABLE events ADD COLUMN user_id UUID
  GENERATED ALWAYS AS ((data->>'userId')::UUID) STORED;

CREATE INDEX events_user_id ON events(user_id); -- 일반 b-tree, 빠름

TS — pg + zod 검증

const EventDataSchema = z.object({
  userId: z.string().uuid(),
  items: z.array(z.object({ id: z.string(), qty: z.number() })),
});

async function insertEvent(data: z.infer<typeof EventDataSchema>) {
  EventDataSchema.parse(data); // app 레벨 검증
  return db.events.create({ data: { type: 'order', data } });
}

마이그레이션 — JSONB 키 → 컬럼

-- 1. 컬럼 추가
ALTER TABLE events ADD COLUMN user_id UUID;

-- 2. 백필
UPDATE events SET user_id = (data->>'userId')::UUID WHERE user_id IS NULL;

-- 3. NOT NULL
ALTER TABLE events ALTER COLUMN user_id SET NOT NULL;

-- 4. 인덱스
CREATE INDEX events_user_id ON events(user_id);

-- 5. 앱 코드 전환 후 JSONB 의 userId 키 제거
UPDATE events SET data = data - 'userId';

🤔 의사결정 기준

상황 추천
항상 같은 필드 일반 컬럼
사용자별 다른 필드 (form builder) JSONB
외부 API raw 저장 JSONB
자주 read 하는 JSONB key Generated column
자주 search 하는 key Partial 인덱스 ((data->>'k'))
Schemaless 강 필요 MongoDB / Firestore 고려
Type safety 필요 컬럼 + 검증

안티패턴

  • 모든 데이터 JSONB: type safety / 인덱스 효율 잃음. 정형은 컬럼.
  • JSON 사용 (B 안 붙임): parse 매번, 인덱스 약함.
  • GIN 인덱스 모든 컬럼: 큰 인덱스. 필요한 path 만.
  • JSONB 안에 거대 nested object: 1MB+ row 부담.
  • Atomic 갱신 안 함: read → modify → write race condition.
  • 검증 없음: 잘못된 schema 가 흘러옴.
  • NULL vs 'null'::jsonb 혼동: 다름. 명확히.

🤖 LLM 활용 힌트

  • 항상 JSONB (J 아님).
  • Generated column 으로 자주 read 컬럼화.
  • Zod 같은 schema 검증 app 에서.

🔗 관련 문서