Files
2nd/10_Wiki/Topic_Programming/Coding/DB_JSONB_Postgres_Patterns.md
T
Antigravity Agent 9148c358d0 docs(10_Wiki): 위키 전체 재구성 — Topic_* 폴더를 4개 카테고리로 통합 + 대규모 중복 제거
Topic_Agent/Topic_Blog/Topics/Topics_Biz/Topics_Meeting/Topics_Rag의 마크다운 지식 문서를
Topic_General/Topic_Programming/Topic_Graphic/Topic_Business 4개 카테고리로 재분류.

- 중복 제거: frontmatter의 status:duplicate/merged + duplicate_of/redirect_to 필드로
  자기 자신을 중복으로 선언한 리다이렉트 stub 1032개 제거, 완전 동일 내용 파일 472개 제거,
  동일 파일명·다른 내용 충돌 시 더 큰(완전한) 버전만 유지(162개 제거) — 총 1639개 중복 제거.
- 분류: 폴더 단위로 명확한 항목(AI_and_ML/Coding/Architecture 등 → Programming,
  Comfyui/Visual_Effects → Graphic, Topics_Biz/Topics_Meeting/사업 등 → Business,
  Poetic_Blog_Writing/창의성/Game_Design 등 → General)은 폴더 우선순위로,
  나머지 혼재 폴더(Topic_Agent/Topic_Blog/Topics 루트/Thinking & Reasoning/Other/UI_UX_Assets)는
  title/tags 키워드 스코어링으로 파일 단위 분류(불명확한 경우 General로 폴백).
  원본 폴더명은 "From_*" 서브폴더로 보존해 추적 가능성 유지.
- 최종 배치: Programming 2784 / General 1608 / Graphic 285 / Business 249 = 4926개 문서.
- 에이전트 운영 상태(.astra/.agent/.obsidian/sessions/memory/_company/docs/lessons/_shared/src)는
  지식 콘텐츠가 아니므로 재분류 대상에서 제외하고 원위치 유지.
- Topics/Topic_email(상위 보호 폴더 Topic_email과 파일명 100% 중복) 삭제 — 보호 폴더 자체는 미변경.
- 완전히 비게 된 Topic_Agent/Topic_Blog/Topics_Biz/Topics_Rag 폴더 제거.
2026-07-05 00:33:48 +09:00

4.3 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 에서.

🔗 관련 문서