14. PostgreSQL JSON/JSONB κΈ°λ₯
14. PostgreSQL JSON/JSONB κΈ°λ₯¶
νμ΅ λͺ©ν¶
- JSONκ³Ό JSONB νμ μ μ°¨μ΄μ μ΄ν΄
- JSON λ°μ΄ν° μ μ₯ λ° μ‘°ν
- JSON μ°μ°μμ ν¨μ νμ©
- GIN μΈλ±μ€λ₯Ό ν΅ν JSON κ²μ μ΅μ ν
λͺ©μ°¨¶
- JSON vs JSONB
- JSON λ°μ΄ν° μ μ₯
- JSON μ°μ°μ
- JSON ν¨μ
- μΈλ±μ±κ³Ό μ±λ₯
- μ€μ ν¨ν΄
- μ°μ΅ λ¬Έμ
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 νμ©:
λ€μ λ¨κ³¶
- 15_쿼리_μ΅μ ν_μ¬ν - JSON 쿼리 μ΅μ ν
- 17_μλμ°_ν¨μ_λΆμ - JSONκ³Ό μλμ° ν¨μ
- PostgreSQL JSON Documentation
μ°Έκ³ μλ£¶
β μ΄μ : λ°±μ κ³Ό 볡ꡬ | λ€μ: 쿼리 μ΅μ ν μ¬ν β | λͺ©μ°¨