6. PostgreSQL: JSON/JSONB
PostgreSQL имеет мощную поддержку JSON, позволяя хранить полуструктурированные данные в реляционной базе.
JSON vs JSONB
Заголовок раздела «JSON vs JSONB»graph LR A[JSON Types] --> B[JSON] A --> C[JSONB]
B --> B1[Текстовое хранение] B --> B2[Быстрая вставка] B --> B3[Медленный поиск]
C --> C1[Бинарное хранение] C --> C2[Медленнее вставка] C --> C3[Быстрый поиск] C --> C4[Поддержка индексов]| Характеристика | JSON | JSONB |
|---|---|---|
| Хранение | Текст (как введено) | Бинарный формат |
| Вставка | Быстрая | Медленнее (парсинг) |
| Поиск | Медленный | Быстрый |
| Индексы | Нет | GIN, GiST |
| Порядок ключей | Сохраняется | Не гарантируется |
| Дубликаты ключей | Сохраняются | Удаляются |
| Пробелы | Сохраняются | Удаляются |
Рекомендация: Почти всегда используйте JSONB!
Создание таблицы с JSONB
Заголовок раздела «Создание таблицы с JSONB»CREATE TABLE products ( id SERIAL PRIMARY KEY, name VARCHAR(200) NOT NULL, category VARCHAR(100), attributes JSONB, created_at TIMESTAMP DEFAULT NOW());
-- Вставка данныхINSERT INTO products (name, category, attributes) VALUES('Laptop Dell XPS 15', 'Computers', '{ "brand": "Dell", "model": "XPS 15", "specs": { "cpu": "Intel i7", "ram": 16, "storage": 512, "screen": 15.6 }, "tags": ["business", "portable", "premium"], "price": 1500, "in_stock": true}'),('iPhone 15 Pro', 'Phones', '{ "brand": "Apple", "model": "iPhone 15 Pro", "specs": { "storage": 256, "camera": "48MP", "screen": 6.1 }, "tags": ["mobile", "5G", "premium"], "price": 999, "in_stock": true}');Операторы для работы с JSON/JSONB
Заголовок раздела «Операторы для работы с JSON/JSONB»Извлечение данных
Заголовок раздела «Извлечение данных»-- -> возвращает JSONSELECT attributes -> 'brand' FROM products;-- Результат: "Dell"
-- ->> возвращает текстSELECT attributes ->> 'brand' FROM products;-- Результат: Dell
-- Вложенные объектыSELECT attributes -> 'specs' -> 'cpu' FROM products;SELECT attributes -> 'specs' ->> 'cpu' FROM products; -- Intel i7
-- Массивы (индекс с 0)SELECT attributes -> 'tags' -> 0 FROM products; -- "business"SELECT attributes -> 'tags' ->> 1 FROM products; -- portable
-- Путь через несколько уровнейSELECT attributes #> '{specs, ram}' FROM products;SELECT attributes #>> '{specs, ram}' FROM products; -- 16Проверка существования
Заголовок раздела «Проверка существования»-- ? - ключ существует на верхнем уровнеSELECT * FROM products WHERE attributes ? 'brand';
-- ?& - все ключи существуютSELECT * FROM products WHERE attributes ?& ARRAY['brand', 'price'];
-- ?| - хотя бы один ключ существуетSELECT * FROM products WHERE attributes ?| ARRAY['brand', 'manufacturer'];
-- @> - содержит JSONSELECT * FROM products WHERE attributes @> '{"brand": "Dell"}';
-- <@ - содержится в JSONSELECT * FROM products WHERE '{"brand": "Dell"}' <@ attributes;Поиск в массивах
Заголовок раздела «Поиск в массивах»-- Массив содержит элементSELECT * FROM productsWHERE attributes -> 'tags' ? 'premium';
-- Массив содержит любой из элементовSELECT * FROM productsWHERE attributes -> 'tags' ?| ARRAY['mobile', '5G'];
-- Массив содержит все элементыSELECT * FROM productsWHERE attributes -> 'tags' ?& ARRAY['premium', 'portable'];
-- Альтернативный синтаксисSELECT * FROM productsWHERE attributes @> '{"tags": ["premium"]}';Индексы для JSONB
Заголовок раздела «Индексы для JSONB»GIN индекс
Заголовок раздела «GIN индекс»-- Базовый GIN индекс для всего документаCREATE INDEX idx_products_attributes ON products USING GIN(attributes);
-- Поддерживает операторы: ?, ?&, ?|, @>, @@
-- GIN индекс для конкретного поляCREATE INDEX idx_products_tags ON products USING GIN((attributes -> 'tags'));
-- Для полнотекстового поискаCREATE INDEX idx_products_search ON productsUSING GIN(to_tsvector('english', attributes ->> 'name'));Выражения в индексах
Заголовок раздела «Выражения в индексах»-- Индекс на конкретное полеCREATE INDEX idx_products_brand ON products((attributes ->> 'brand'));
-- Индекс на вложенное полеCREATE INDEX idx_products_cpu ON products((attributes #>> '{specs, cpu}'));
-- Теперь можно эффективно искатьSELECT * FROM products WHERE attributes ->> 'brand' = 'Dell';SELECT * FROM products WHERE attributes #>> '{specs, cpu}' = 'Intel i7';Изменение JSONB данных
Заголовок раздела «Изменение JSONB данных»jsonb_set - обновление значения
Заголовок раздела «jsonb_set - обновление значения»-- Обновление поля верхнего уровняUPDATE productsSET attributes = jsonb_set( attributes, '{price}', '1299')WHERE name = 'Laptop Dell XPS 15';
-- Обновление вложенного поляUPDATE productsSET attributes = jsonb_set( attributes, '{specs, ram}', '32')WHERE id = 1;
-- Создание нового поляUPDATE productsSET attributes = jsonb_set( attributes, '{discount}', '10', true -- create_missing = true)WHERE category = 'Computers';Добавление и удаление полей
Заголовок раздела «Добавление и удаление полей»-- Добавление поля (concatenation)UPDATE productsSET attributes = attributes || '{"warranty": "2 years"}'WHERE id = 1;
-- Удаление поляUPDATE productsSET attributes = attributes - 'discount'WHERE id = 1;
-- Удаление вложенного поляUPDATE productsSET attributes = attributes #- '{specs, old_field}'WHERE id = 1;Операции с массивами
Заголовок раздела «Операции с массивами»-- Добавление элемента в массивUPDATE productsSET attributes = jsonb_set( attributes, '{tags}', (attributes -> 'tags') || '"new-tag"')WHERE id = 1;
-- Удаление элемента из массива (индекс 0)UPDATE productsSET attributes = jsonb_set( attributes, '{tags}', (attributes -> 'tags') - 0)WHERE id = 1;JSON функции
Заголовок раздела «JSON функции»Построение JSON
Заголовок раздела «Построение JSON»-- json_build_objectSELECT json_build_object( 'id', id, 'name', name, 'brand', attributes ->> 'brand') FROM products;
-- json_agg - агрегация в массивSELECT category, json_agg( json_build_object( 'name', name, 'price', attributes -> 'price' )) as productsFROM productsGROUP BY category;
-- jsonb_object_agg - агрегация в объектSELECT jsonb_object_agg(name, attributes -> 'price') as pricesFROM products;Разбор JSON
Заголовок раздела «Разбор JSON»-- jsonb_each - развернуть объект в строкиSELECT * FROM jsonb_each('{"a": 1, "b": 2, "c": 3}');-- key | value-- a | 1-- b | 2-- c | 3
-- jsonb_array_elements - развернуть массивSELECT * FROM products, jsonb_array_elements(attributes -> 'tags') as tag;
-- jsonb_to_record - JSON в записьSELECT * FROM jsonb_to_record('{"name": "John", "age": 30}')AS x(name text, age int);Валидация с JSON Schema
Заголовок раздела «Валидация с JSON Schema»-- Функция для валидации (требует расширения)CREATE EXTENSION IF NOT EXISTS plpython3u;
CREATE OR REPLACE FUNCTION validate_product_attributes(data jsonb)RETURNS boolean AS $$ # Простая валидация if 'brand' not in data: return False if 'price' not in data or float(data['price']) <= 0: return False return True$$ LANGUAGE plpython3u;
-- Constraint с валидациейALTER TABLE productsADD CONSTRAINT valid_attributesCHECK (validate_product_attributes(attributes));Или с триггером:
CREATE OR REPLACE FUNCTION check_product_attributes()RETURNS TRIGGER AS $$BEGIN IF NOT (NEW.attributes ? 'brand') THEN RAISE EXCEPTION 'brand is required'; END IF; IF NOT (NEW.attributes ? 'price') THEN RAISE EXCEPTION 'price is required'; END IF; RETURN NEW;END;$$ LANGUAGE plpgsql;
CREATE TRIGGER validate_product BEFORE INSERT OR UPDATE ON products FOR EACH ROW EXECUTE FUNCTION check_product_attributes();TypeScript примеры
Заголовок раздела «TypeScript примеры»import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL,});
interface ProductAttributes { brand: string; model: string; specs: { cpu?: string; ram?: number; storage?: number; }; tags: string[]; price: number; in_stock: boolean;}
// Создание продуктаasync function createProduct( name: string, category: string, attributes: ProductAttributes) { const result = await pool.query( 'INSERT INTO products (name, category, attributes) VALUES ($1, $2, $3) RETURNING *', [name, category, attributes] ); return result.rows[0];}
// Поиск по JSONBasync function findProductsByBrand(brand: string) { const result = await pool.query( `SELECT id, name, attributes FROM products WHERE attributes @> $1`, [JSON.stringify({ brand })] ); return result.rows;}
// Поиск по тегамasync function findProductsByTags(tags: string[]) { const result = await pool.query( `SELECT id, name, attributes FROM products WHERE attributes -> 'tags' ?| $1`, [tags] ); return result.rows;}
// Обновление ценыasync function updatePrice(id: number, newPrice: number) { await pool.query( `UPDATE products SET attributes = jsonb_set(attributes, '{price}', $2) WHERE id = $1`, [id, JSON.stringify(newPrice)] );}
// Фильтрация по диапазону ценasync function findProductsByPriceRange(min: number, max: number) { const result = await pool.query( `SELECT id, name, attributes ->> 'price' as price FROM products WHERE (attributes ->> 'price')::numeric BETWEEN $1 AND $2 ORDER BY (attributes ->> 'price')::numeric`, [min, max] ); return result.rows;}
// Агрегация с группировкойasync function getProductStatsByCategory() { const result = await pool.query(` SELECT category, COUNT(*) as total_products, AVG((attributes ->> 'price')::numeric) as avg_price, jsonb_agg( jsonb_build_object( 'name', name, 'brand', attributes ->> 'brand', 'price', attributes -> 'price' ) ) as products FROM products GROUP BY category `); return result.rows;}💡 Best Practices
Заголовок раздела «💡 Best Practices»- Используйте JSONB вместо JSON (почти всегда)
- Создавайте GIN индексы для часто используемых полей
- Валидируйте структуру через триггеры или constraints
- Не злоупотребляйте: Если структура фиксирована — используйте обычные колонки
- Комбинируйте реляционные и JSONB данные для гибкости
Когда использовать JSONB
Заголовок раздела «Когда использовать JSONB»✅ Хорошо для:
- Гибкая схема (разные атрибуты для разных продуктов)
- Метаданные и настройки
- Логи и события
- API ответы
- Интеграции с внешними системами
❌ Плохо для:
- Данные с фиксированной структурой
- Частые JOIN’ы по полям внутри JSON
- Данные, требующие строгой типизации
- Поля для foreign key
⚠️ Частые ошибки
Заголовок раздела «⚠️ Частые ошибки»- Использование JSON вместо JSONB
- Отсутствие индексов на часто запрашиваемых полях
- Хранение всего в JSON (избыточная гибкость)
- Забывают приведение типов при сравнении чисел
Следующий урок: Full-Text Search в PostgreSQL →