14. PostgreSQL JSON/JSONB κΈ°λŠ₯

14. PostgreSQL JSON/JSONB κΈ°λŠ₯

ν•™μŠ΅ λͺ©ν‘œ

  • JSONκ³Ό JSONB νƒ€μž…μ˜ 차이점 이해
  • JSON 데이터 μ €μž₯ 및 쑰회
  • JSON μ—°μ‚°μžμ™€ ν•¨μˆ˜ ν™œμš©
  • GIN 인덱슀λ₯Ό ν†΅ν•œ JSON 검색 μ΅œμ ν™”

λͺ©μ°¨

  1. JSON vs JSONB
  2. JSON 데이터 μ €μž₯
  3. JSON μ—°μ‚°μž
  4. JSON ν•¨μˆ˜
  5. 인덱싱과 μ„±λŠ₯
  6. μ‹€μ „ νŒ¨ν„΄
  7. μ—°μŠ΅ 문제

1. JSON vs JSONB

1.1 νƒ€μž… 비ꡐ

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    JSON vs JSONB                             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚  JSON                          JSONB                        β”‚
β”‚  ────────────────────         ────────────────────         β”‚
β”‚  β€’ ν…μŠ€νŠΈλ‘œ μ €μž₯               β€’ λ°”μ΄λ„ˆλ¦¬λ‘œ μ €μž₯            β”‚
β”‚  β€’ μž…λ ₯ κ·ΈλŒ€λ‘œ μœ μ§€            β€’ νŒŒμ‹± ν›„ μ €μž₯               β”‚
β”‚  β€’ 곡백/μˆœμ„œ 보쑴              β€’ 곡백 제거, ν‚€ μ •λ ¬         β”‚
β”‚  β€’ 쀑볡 ν‚€ ν—ˆμš©                β€’ λ§ˆμ§€λ§‰ ν‚€ κ°’λ§Œ μœ μ§€        β”‚
β”‚  β€’ μ €μž₯ 빠름                   β€’ μ €μž₯ μ•½κ°„ 느림             β”‚
β”‚  β€’ 처리 느림 (맀번 νŒŒμ‹±)       β€’ 처리 빠름                  β”‚
β”‚  β€’ 인덱싱 μ œν•œμ                β€’ GIN 인덱슀 지원            β”‚
β”‚                                                             β”‚
β”‚  ꢌμž₯: λŒ€λΆ€λΆ„μ˜ 경우 JSONB μ‚¬μš©                             β”‚
β”‚        JSON은 원본 ν˜•μ‹ μœ μ§€ ν•„μš”ν•  λ•Œλ§Œ μ‚¬μš©               β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

1.2 κΈ°λ³Έ μ‚¬μš©

-- ν…Œμ΄λΈ” 생성
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    attributes JSONB,    -- JSONB ꢌμž₯
    raw_data JSON        -- 원본 보쑴 ν•„μš” μ‹œ
);

-- 데이터 μ‚½μž…
INSERT INTO products (name, attributes) VALUES
('Laptop', '{"brand": "Dell", "specs": {"cpu": "i7", "ram": 16}}'),
('Phone', '{"brand": "Apple", "specs": {"model": "iPhone 15", "storage": 256}}');

-- JSON ν˜•μ‹ 검증
SELECT '{"valid": true}'::jsonb;  -- 성곡
SELECT '{invalid}'::jsonb;        -- 였λ₯˜: μœ νš¨ν•˜μ§€ μ•Šμ€ JSON

2. JSON 데이터 μ €μž₯

2.1 JSON 생성 ν•¨μˆ˜

-- json_build_object: ν‚€-κ°’ 쌍으둜 객체 생성
SELECT json_build_object(
    'name', 'John',
    'age', 30,
    'active', true
);
-- {"name": "John", "age": 30, "active": true}

-- jsonb_build_object (JSONB 버전)
SELECT jsonb_build_object(
    'product', 'Laptop',
    'price', 999.99
);

-- json_build_array: λ°°μ—΄ 생성
SELECT json_build_array(1, 2, 'three', true, null);
-- [1, 2, "three", true, null]

-- row_to_json: 행을 JSON으둜
SELECT row_to_json(t)
FROM (SELECT 1 AS id, 'test' AS name) t;
-- {"id": 1, "name": "test"}

-- to_jsonb: 값을 JSONB둜 λ³€ν™˜
SELECT to_jsonb(ARRAY[1, 2, 3]);
-- [1, 2, 3]

-- json_agg: μ—¬λŸ¬ 행을 λ°°μ—΄λ‘œ
SELECT json_agg(name) FROM products;
-- ["Laptop", "Phone"]

-- jsonb_object_agg: ν‚€-κ°’ μŒμ„ 객체둜
SELECT jsonb_object_agg(name, id) FROM products;
-- {"Laptop": 1, "Phone": 2}

2.2 JSON 데이터 μˆ˜μ •

-- jsonb_set: κ°’ μ„€μ •/μΆ”κ°€
UPDATE products
SET attributes = jsonb_set(attributes, '{specs,ram}', '32')
WHERE name = 'Laptop';

-- 쀑첩 경둜 μΆ”κ°€ (create_if_missing = true)
UPDATE products
SET attributes = jsonb_set(
    attributes,
    '{specs,gpu}',
    '"RTX 4090"',
    true  -- κ²½λ‘œκ°€ μ—†μœΌλ©΄ 생성
)
WHERE name = 'Laptop';

-- μ—¬λŸ¬ κ°’ ν•œ λ²ˆμ— μˆ˜μ •
UPDATE products
SET attributes = attributes || '{"color": "silver", "weight": 2.1}'
WHERE name = 'Laptop';

-- ν‚€ μ‚­μ œ
UPDATE products
SET attributes = attributes - 'color'
WHERE name = 'Laptop';

-- 쀑첩 ν‚€ μ‚­μ œ
UPDATE products
SET attributes = attributes #- '{specs,gpu}'
WHERE name = 'Laptop';

-- λ°°μ—΄ μš”μ†Œ μΆ”κ°€
UPDATE products
SET attributes = jsonb_set(
    attributes,
    '{tags}',
    COALESCE(attributes->'tags', '[]'::jsonb) || '"new_tag"'
);

3. JSON μ—°μ‚°μž

3.1 μ ‘κ·Ό μ—°μ‚°μž

-- -> : JSON 객체/λ°°μ—΄ μš”μ†Œ (JSON λ°˜ν™˜)
SELECT attributes->'brand' FROM products;
-- "Dell" (λ”°μ˜΄ν‘œ 포함 JSON)

-- ->> : ν…μŠ€νŠΈλ‘œ μΆ”μΆœ
SELECT attributes->>'brand' FROM products;
-- Dell (ν…μŠ€νŠΈ)

-- #> : 경둜둜 μ ‘κ·Ό (JSON λ°˜ν™˜)
SELECT attributes#>'{specs,cpu}' FROM products;
-- "i7"

-- #>> : 경둜둜 μ ‘κ·Ό (ν…μŠ€νŠΈ λ°˜ν™˜)
SELECT attributes#>>'{specs,cpu}' FROM products;
-- i7

-- λ°°μ—΄ μ ‘κ·Ό
SELECT '[1, 2, 3]'::jsonb->0;   -- 1
SELECT '[1, 2, 3]'::jsonb->-1;  -- 3 (λ§ˆμ§€λ§‰)
SELECT '[1, 2, 3]'::jsonb->10;  -- NULL (λ²”μœ„ 초과)

3.2 비ꡐ μ—°μ‚°μž (JSONB μ „μš©)

-- = : 동등 비ꡐ
SELECT * FROM products
WHERE attributes->'brand' = '"Dell"'::jsonb;

-- @> : 포함 (μ™Όμͺ½μ΄ 였λ₯Έμͺ½ 포함)
SELECT * FROM products
WHERE attributes @> '{"brand": "Dell"}'::jsonb;

-- <@ : 포함됨 (였λ₯Έμͺ½μ΄ μ™Όμͺ½ 포함)
SELECT * FROM products
WHERE '{"brand": "Dell", "specs": {}}'::jsonb <@ attributes;

-- ? : ν‚€ 쑴재
SELECT * FROM products
WHERE attributes ? 'brand';

-- ?| : ν‚€ 쀑 ν•˜λ‚˜ 쑴재 (OR)
SELECT * FROM products
WHERE attributes ?| ARRAY['brand', 'manufacturer'];

-- ?& : λͺ¨λ“  ν‚€ 쑴재 (AND)
SELECT * FROM products
WHERE attributes ?& ARRAY['brand', 'specs'];

-- || : 병합
SELECT '{"a": 1}'::jsonb || '{"b": 2}'::jsonb;
-- {"a": 1, "b": 2}

-- - : ν‚€ 제거
SELECT '{"a": 1, "b": 2}'::jsonb - 'a';
-- {"b": 2}

-- - : λ°°μ—΄ μš”μ†Œ 제거 (인덱슀)
SELECT '[1, 2, 3]'::jsonb - 1;
-- [1, 3]

-- #- : 경둜둜 제거
SELECT '{"a": {"b": 2}}'::jsonb #- '{a,b}';
-- {"a": {}}

3.3 쑰건 검색

-- νŠΉμ • κ°’ 포함
SELECT * FROM products
WHERE attributes @> '{"brand": "Dell"}';

-- 쀑첩 κ°’ 검색
SELECT * FROM products
WHERE attributes @> '{"specs": {"cpu": "i7"}}';

-- λ°°μ—΄ λ‚΄ κ°’ 검색
-- κ°€μ •: attributes = {"tags": ["laptop", "electronics"]}
SELECT * FROM products
WHERE attributes->'tags' ? 'laptop';

-- 숫자 비ꡐ
SELECT * FROM products
WHERE (attributes->>'price')::numeric > 500;

-- μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν‚€ 확인
SELECT * FROM products
WHERE NOT (attributes ? 'discontinued');

-- NULL κ°’ 확인
SELECT * FROM products
WHERE attributes->'stock' IS NULL;

-- JSON 값이 null인지 확인 (JSON nullκ³Ό SQL NULL 닀름)
SELECT * FROM products
WHERE attributes->'stock' = 'null'::jsonb;

4. JSON ν•¨μˆ˜

4.1 μΆ”μΆœ ν•¨μˆ˜

-- jsonb_extract_path: 경둜둜 κ°’ μΆ”μΆœ
SELECT jsonb_extract_path(attributes, 'specs', 'cpu') FROM products;

-- jsonb_extract_path_text: ν…μŠ€νŠΈλ‘œ μΆ”μΆœ
SELECT jsonb_extract_path_text(attributes, 'specs', 'cpu') FROM products;

-- jsonb_array_elements: 배열을 ν–‰μœΌλ‘œ ν™•μž₯
SELECT jsonb_array_elements('[1, 2, 3]'::jsonb);
-- 1
-- 2
-- 3

-- jsonb_array_elements_text: ν…μŠ€νŠΈλ‘œ ν™•μž₯
SELECT jsonb_array_elements_text('["a", "b", "c"]'::jsonb);

-- jsonb_each: 객체λ₯Ό ν‚€-κ°’ ν–‰μœΌλ‘œ
SELECT * FROM jsonb_each('{"a": 1, "b": 2}'::jsonb);
-- key | value
-- a   | 1
-- b   | 2

-- jsonb_each_text: ν…μŠ€νŠΈ κ°’μœΌλ‘œ
SELECT * FROM jsonb_each_text('{"a": 1, "b": "text"}'::jsonb);

-- jsonb_object_keys: ν‚€ λͺ©λ‘
SELECT jsonb_object_keys('{"a": 1, "b": 2}'::jsonb);
-- a
-- b

-- jsonb_array_length: λ°°μ—΄ 길이
SELECT jsonb_array_length('[1, 2, 3]'::jsonb);
-- 3

4.2 λ³€ν™˜ ν•¨μˆ˜

-- jsonb_typeof: JSON νƒ€μž… 확인
SELECT jsonb_typeof('"string"'::jsonb);  -- string
SELECT jsonb_typeof('123'::jsonb);       -- number
SELECT jsonb_typeof('true'::jsonb);      -- boolean
SELECT jsonb_typeof('null'::jsonb);      -- null
SELECT jsonb_typeof('[]'::jsonb);        -- array
SELECT jsonb_typeof('{}'::jsonb);        -- object

-- jsonb_strip_nulls: null κ°’ 제거
SELECT jsonb_strip_nulls('{"a": 1, "b": null}'::jsonb);
-- {"a": 1}

-- jsonb_pretty: 보기 μ’‹κ²Œ 좜λ ₯
SELECT jsonb_pretty('{"a":1,"b":2}'::jsonb);
/*
{
    "a": 1,
    "b": 2
}
*/

-- 배열을 PostgreSQL λ°°μ—΄λ‘œ
SELECT ARRAY(SELECT jsonb_array_elements_text('["a", "b"]'::jsonb));
-- {a,b}

-- PostgreSQL 배열을 JSON λ°°μ—΄λ‘œ
SELECT to_jsonb(ARRAY['a', 'b']);
-- ["a", "b"]

4.3 집계 ν•¨μˆ˜

-- μ—¬λŸ¬ 행을 JSON λ°°μ—΄λ‘œ
SELECT jsonb_agg(attributes) FROM products;

-- ν•„ν„°λ§ν•˜μ—¬ 집계
SELECT jsonb_agg(attributes) FILTER (WHERE name LIKE 'L%') FROM products;

-- 객체둜 집계
SELECT jsonb_object_agg(id, attributes) FROM products;

-- λ°°μ—΄ ν•©μΉ˜κΈ°
SELECT jsonb_agg(elem)
FROM products, jsonb_array_elements(attributes->'tags') AS elem;

5. 인덱싱과 μ„±λŠ₯

5.1 GIN 인덱슀

-- κΈ°λ³Έ GIN 인덱슀 (λͺ¨λ“  μ—°μ‚°μž 지원)
CREATE INDEX idx_products_attrs
ON products USING GIN (attributes);

-- jsonb_path_ops (더 μž‘κ³  빠름, @> μ—°μ‚°λ§Œ 지원)
CREATE INDEX idx_products_attrs_path
ON products USING GIN (attributes jsonb_path_ops);

-- νŠΉμ • 킀에 λŒ€ν•œ 인덱슀
CREATE INDEX idx_products_brand
ON products USING GIN ((attributes->'brand'));

-- B-tree 인덱슀 (νŠΉμ • κ°’ λΉ„κ΅μš©)
CREATE INDEX idx_products_brand_btree
ON products ((attributes->>'brand'));

-- ν•¨μˆ˜ 기반 인덱슀
CREATE INDEX idx_products_price
ON products (((attributes->>'price')::numeric));

5.2 인덱슀 μ‚¬μš© 확인

-- μ‹€ν–‰ κ³„νš 확인
EXPLAIN ANALYZE
SELECT * FROM products
WHERE attributes @> '{"brand": "Dell"}';

-- GIN μΈλ±μŠ€κ°€ μ‚¬μš©λ˜λ©΄:
-- Bitmap Index Scan on idx_products_attrs

-- 인덱슀 크기 확인
SELECT pg_size_pretty(pg_indexes_size('products'));

5.3 μ„±λŠ₯ μ΅œμ ν™”

-- 자주 μ‚¬μš©ν•˜λŠ” ν‚€λŠ” 별도 컬럼으둜
ALTER TABLE products ADD COLUMN brand VARCHAR(100);
UPDATE products SET brand = attributes->>'brand';
CREATE INDEX idx_products_brand_col ON products(brand);

-- Partial 인덱슀
CREATE INDEX idx_active_products
ON products USING GIN (attributes)
WHERE (attributes->>'active')::boolean = true;

-- 볡합 인덱슀
CREATE INDEX idx_products_composite
ON products (name, (attributes->>'brand'));

-- 톡계 μ—…λ°μ΄νŠΈ
ANALYZE products;

6. μ‹€μ „ νŒ¨ν„΄

6.1 μŠ€ν‚€λ§ˆλ¦¬μŠ€ ν…Œμ΄λΈ”

-- 이벀트 둜그 ν…Œμ΄λΈ”
CREATE TABLE events (
    id BIGSERIAL PRIMARY KEY,
    event_type VARCHAR(50) NOT NULL,
    occurred_at TIMESTAMPTZ DEFAULT NOW(),
    data JSONB NOT NULL
);

CREATE INDEX idx_events_type ON events(event_type);
CREATE INDEX idx_events_data ON events USING GIN (data);
CREATE INDEX idx_events_occurred ON events(occurred_at);

-- 이벀트 μ‚½μž…
INSERT INTO events (event_type, data) VALUES
('user_signup', '{"user_id": 123, "email": "user@example.com"}'),
('purchase', '{"user_id": 123, "product_id": 456, "amount": 99.99}'),
('page_view', '{"user_id": 123, "page": "/products", "referrer": "google"}');

-- 이벀트 쑰회
SELECT * FROM events
WHERE event_type = 'purchase'
AND (data->>'amount')::numeric > 50
AND occurred_at > NOW() - INTERVAL '7 days';

6.2 EAV (Entity-Attribute-Value) λŒ€μ²΄

-- 전톡적 EAV (느림, 볡작)
CREATE TABLE product_attributes_eav (
    product_id INT,
    attribute_name VARCHAR(100),
    attribute_value VARCHAR(255)
);

-- JSONB둜 λŒ€μ²΄ (빠름, 간단)
CREATE TABLE products_jsonb (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    base_price DECIMAL(10,2),
    attributes JSONB DEFAULT '{}'
);

-- λ‹€μ–‘ν•œ 속성 μ €μž₯
INSERT INTO products_jsonb (name, base_price, attributes) VALUES
('T-Shirt', 29.99, '{"size": "M", "color": "blue", "material": "cotton"}'),
('Laptop', 999.99, '{"cpu": "i7", "ram": 16, "storage": "512GB SSD"}'),
('Book', 15.99, '{"author": "John Doe", "pages": 300, "isbn": "123-456"}');

-- 동적 필터링
SELECT * FROM products_jsonb
WHERE attributes @> '{"color": "blue"}'
OR attributes @> '{"ram": 16}';

6.3 버전 관리

-- λ¬Έμ„œ 버전 관리
CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    title VARCHAR(200),
    current_version INT DEFAULT 1,
    content JSONB
);

CREATE TABLE document_versions (
    id SERIAL PRIMARY KEY,
    document_id INT REFERENCES documents(id),
    version INT,
    content JSONB,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    created_by INT
);

-- 트리거둜 버전 μžλ™ μ €μž₯
CREATE OR REPLACE FUNCTION save_document_version()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO document_versions (document_id, version, content, created_by)
    VALUES (OLD.id, OLD.current_version, OLD.content, current_setting('app.user_id')::int);

    NEW.current_version := OLD.current_version + 1;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER tr_document_version
BEFORE UPDATE ON documents
FOR EACH ROW
WHEN (OLD.content IS DISTINCT FROM NEW.content)
EXECUTE FUNCTION save_document_version();

6.4 JSON Schema 검증

-- CHECK μ œμ•½μ‘°κ±΄μœΌλ‘œ κ°„λ‹¨ν•œ 검증
ALTER TABLE products ADD CONSTRAINT valid_attributes CHECK (
    attributes ? 'brand' AND
    jsonb_typeof(attributes->'brand') = 'string'
);

-- ν•¨μˆ˜λ‘œ λ³΅μž‘ν•œ 검증
CREATE OR REPLACE FUNCTION validate_product_attributes(attrs JSONB)
RETURNS BOOLEAN AS $$
BEGIN
    -- ν•„μˆ˜ ν•„λ“œ 확인
    IF NOT (attrs ? 'brand') THEN
        RETURN FALSE;
    END IF;

    -- νƒ€μž… 확인
    IF jsonb_typeof(attrs->'brand') != 'string' THEN
        RETURN FALSE;
    END IF;

    -- specsκ°€ 있으면 객체여야 함
    IF attrs ? 'specs' AND jsonb_typeof(attrs->'specs') != 'object' THEN
        RETURN FALSE;
    END IF;

    RETURN TRUE;
END;
$$ LANGUAGE plpgsql IMMUTABLE;

ALTER TABLE products ADD CONSTRAINT chk_attributes
CHECK (validate_product_attributes(attributes));

7. μ—°μŠ΅ 문제

μ—°μŠ΅ 1: μ‚¬μš©μž μ„€μ • μ €μž₯

-- μš”κ΅¬μ‚¬ν•­:
-- 1. μ‚¬μš©μžλ³„ 섀정을 JSONB둜 μ €μž₯ν•˜λŠ” ν…Œμ΄λΈ” 생성
-- 2. κΈ°λ³Έ μ„€μ • 병합 ν•¨μˆ˜ μž‘μ„±
-- 3. νŠΉμ • μ„€μ • 쑰회/μ—…λ°μ΄νŠΈ ν•¨μˆ˜ μž‘μ„±

-- μŠ€ν‚€λ§ˆ 및 ν•¨μˆ˜ μž‘μ„±:

μ—°μŠ΅ 2: JSON 집계 λ³΄κ³ μ„œ

-- μš”κ΅¬μ‚¬ν•­:
-- μ£Όλ¬Έ ν…Œμ΄λΈ”μ—μ„œ λ‹€μŒ JSON ν˜•μ‹μ˜ λ³΄κ³ μ„œ 생성:
-- {
--   "total_orders": 100,
--   "total_revenue": 5000.00,
--   "by_status": {"pending": 20, "completed": 80},
--   "top_products": [{"id": 1, "count": 50}, ...]
-- }

-- 쿼리 μž‘μ„±:

μ—°μŠ΅ 3: JSON 검색 μ΅œμ ν™”

-- μš”κ΅¬μ‚¬ν•­:
-- 1. 100만 ν–‰μ˜ 이벀트 데이터 생성
-- 2. λ‹€μ–‘ν•œ 인덱슀 비ꡐ
-- 3. 졜적의 인덱슀 μ „λž΅ 수립

-- ν…ŒμŠ€νŠΈ 및 뢄석:

μ—°μŠ΅ 4: 계측적 JSON 처리

-- μš”κ΅¬μ‚¬ν•­:
-- 쑰직 ꡬ쑰 JSON 데이터 처리:
-- {"name": "CEO", "children": [{"name": "CTO", "children": [...]}]}
-- λͺ¨λ“  λ…Έλ“œ 평면화, 경둜 μΆ”μΆœ λ“±

-- μž¬κ·€ CTE ν™œμš©:

λ‹€μŒ 단계

참고 자료


← 이전: λ°±μ—…κ³Ό 볡ꡬ | λ‹€μŒ: 쿼리 μ΅œμ ν™” 심화 β†’ | λͺ©μ°¨

to navigate between lessons