JSON, массивы, расширенные типы
JSON, массивы и расширенные типы PostgreSQL¶
PostgreSQL — не просто реляционная СУБД. Она поддерживает полуструктурированные данные (JSONB), массивы, диапазоны и вычисляемые столбцы. Это позволяет хранить гибкие данные без NoSQL-костылей.
JSONB — полуструктурированные данные¶
JSONB хранит JSON в бинарном формате: парсится при записи, быстрый при чтении. Поддерживает индексы и богатый набор операторов.
JSON vs JSONB¶
| Критерий | JSON | JSONB |
|---|---|---|
| Хранение | Текст (as is) | Бинарный формат |
| Дубликаты ключей | Сохраняются | Последний побеждает |
| Порядок ключей | Сохраняется | Не гарантирован |
| Индексация | Нет | GIN, B-tree (на выражениях) |
| Скорость записи | Быстрее | Чуть медленнее (парсинг) |
| Скорость чтения | Медленнее (репарсинг) | Быстрее |
Правило: почти всегда используй JSONB
JSON нужен только если критичен порядок ключей (например, логирование raw-ответов API).
Операторы JSONB¶
-- Извлечение (наша таблица products имеет поле attributes JSONB)
-- Получить значение по ключу → JSONB
SELECT attributes -> 'brand' FROM products;
-- → "Apple" (с кавычками, тип jsonb)
-- Получить значение по ключу → TEXT
SELECT attributes ->> 'brand' FROM products;
-- → Apple (без кавычек, тип text)
-- Вложенный путь → JSONB
SELECT attributes -> 'specs' -> 'cpu' FROM products;
-- Вложенный путь → TEXT
SELECT attributes #>> '{specs,cpu}' FROM products;
Операторы проверки¶
-- Содержит ли JSONB другой JSONB? (containment)
SELECT * FROM products
WHERE attributes @> '{"brand": "Apple"}';
-- Существует ли ключ?
SELECT * FROM products WHERE attributes ? 'color';
-- Существует ли хотя бы один ключ?
SELECT * FROM products WHERE attributes ?| array['color', 'size'];
-- Существуют ли все ключи?
SELECT * FROM products WHERE attributes ?& array['brand', 'color'];
Модификация JSONB¶
-- Добавить/обновить ключ
UPDATE products
SET attributes = attributes || '{"warranty_months": 12}'
WHERE product_id = 1;
-- Удалить ключ
UPDATE products
SET attributes = attributes - 'warranty_months'
WHERE product_id = 1;
-- Обновить вложенное значение
UPDATE products
SET attributes = jsonb_set(attributes, '{storage_gb}', '256')
WHERE product_id = 1;
Индексирование JSONB¶
-- GIN на весь JSONB (поддерживает @>, ?, ?|, ?&)
CREATE INDEX idx_products_attrs ON products USING gin (attributes);
-- GIN с jsonb_path_ops (только @>, но на 30% компактнее)
CREATE INDEX idx_products_attrs_path ON products USING gin (attributes jsonb_path_ops);
-- B-tree на конкретное поле (для =, <, >, сортировка)
CREATE INDEX idx_products_brand ON products ((attributes ->> 'brand'));
JSONB в аналитике¶
-- Агрегация: сколько товаров каждого бренда?
SELECT attributes ->> 'brand' AS brand, COUNT(*)
FROM products
WHERE attributes ? 'brand'
GROUP BY 1
ORDER BY 2 DESC;
-- JSON-агрегация: собрать массив объектов
SELECT customer_id,
jsonb_agg(jsonb_build_object(
'product', name,
'rating', rating
)) AS reviews
FROM reviews r
JOIN products p USING (product_id)
GROUP BY customer_id;
Массивы¶
PostgreSQL нативно поддерживает массивы любого типа.
Объявление и вставка¶
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title TEXT,
tags TEXT[] -- массив строк
);
INSERT INTO articles (title, tags)
VALUES ('ETL-паттерны', ARRAY['etl', 'data-engineering', 'best-practices']);
-- Альтернативный синтаксис
INSERT INTO articles (title, tags)
VALUES ('SQL для DE', '{sql, postgresql, analytics}');
Операторы массивов¶
-- Содержит ли массив элемент?
SELECT * FROM articles WHERE 'etl' = ANY(tags);
-- Содержит ли массив все элементы?
SELECT * FROM articles WHERE tags @> ARRAY['etl', 'sql'];
-- Пересечение (overlap)
SELECT * FROM articles WHERE tags && ARRAY['python', 'sql'];
-- Длина массива
SELECT title, array_length(tags, 1) AS tag_count FROM articles;
-- Развернуть массив в строки
SELECT title, unnest(tags) AS tag FROM articles;
Индексирование массивов¶
Массивы vs отдельная таблица
Массивы удобны для небольших наборов (теги, метки). Для связей many-to-many с собственными атрибутами — используй отдельную таблицу (нормальная форма).
Range Types — диапазоны¶
Встроенные типы для диапазонов чисел, дат, timestamp.
CREATE TABLE room_bookings (
id SERIAL PRIMARY KEY,
room TEXT,
period TSRANGE NOT NULL, -- [start, end)
EXCLUDE USING gist (room WITH =, period WITH &&) -- запрет пересечений!
);
INSERT INTO room_bookings (room, period) VALUES
('A101', '[2025-03-15 09:00, 2025-03-15 12:00)'),
('A101', '[2025-03-15 13:00, 2025-03-15 17:00)');
-- Это упадёт — пересечение!
INSERT INTO room_bookings (room, period) VALUES
('A101', '[2025-03-15 11:00, 2025-03-15 14:00)');
-- ERROR: conflicting key value violates exclusion constraint
Типы диапазонов¶
| Тип | Описание |
|---|---|
INT4RANGE |
Целые числа |
NUMRANGE |
Числа с дробной частью |
TSRANGE |
Timestamp без TZ |
TSTZRANGE |
Timestamp с TZ |
DATERANGE |
Даты |
Операторы¶
-- Содержит ли диапазон значение?
SELECT * FROM room_bookings WHERE period @> '2025-03-15 10:00'::timestamp;
-- Пересечение
SELECT * FROM room_bookings WHERE period && '[2025-03-15 11:00, 2025-03-15 14:00)';
-- Границы
SELECT lower(period), upper(period) FROM room_bookings;
GENERATED столбцы¶
Вычисляемые столбцы, которые автоматически обновляются. Два вида: STORED (хранится на диске) и VIRTUAL (только в PG 17+).
CREATE TABLE order_items_v2 (
item_id SERIAL PRIMARY KEY,
quantity INT NOT NULL,
unit_price NUMERIC(10,2) NOT NULL,
discount NUMERIC(5,2) DEFAULT 0,
-- Автоматически рассчитывается
total NUMERIC(12,2) GENERATED ALWAYS AS (quantity * unit_price * (1 - discount / 100)) STORED
);
INSERT INTO order_items_v2 (quantity, unit_price, discount) VALUES (3, 1000, 10);
SELECT total FROM order_items_v2;
-- → 2700.00 (3 * 1000 * 0.9)
STORED vs VIRTUAL
STORED занимает место на диске, но не пересчитывается при SELECT. VIRTUAL (PG 17+) не хранится, вычисляется при чтении. Для тяжёлых вычислений — STORED, для лёгких — VIRTUAL.
Полезные функции¶
generate_series — генерация рядов¶
-- Числа от 1 до 10
SELECT generate_series(1, 10);
-- Даты за последний месяц
SELECT generate_series(
CURRENT_DATE - INTERVAL '30 days',
CURRENT_DATE,
'1 day'
)::date AS day;
-- Заполнить пропуски в данных (LEFT JOIN с рядом дат)
SELECT d.day, COALESCE(COUNT(o.order_id), 0) AS orders
FROM generate_series('2024-06-01'::date, '2024-06-30'::date, '1 day') AS d(day)
LEFT JOIN orders o ON o.order_date = d.day
GROUP BY d.day
ORDER BY d.day;
Конвертация типов¶
-- CAST
SELECT CAST('42' AS INTEGER);
SELECT '42'::INTEGER;
-- to_char / to_date / to_timestamp
SELECT to_char(NOW(), 'YYYY-MM-DD HH24:MI');
SELECT to_date('15-03-2025', 'DD-MM-YYYY');
Попробуй сам¶
Поработай с JSONB-данными в таблице products.
Проверь себя¶
Источники¶
- PostgreSQL: JSON Types — JSON и JSONB
- PostgreSQL: JSON Functions — операторы и функции для JSON
- PostgreSQL: Arrays — массивы
- PostgreSQL: Range Types — диапазоны
- PostgreSQL: Generated Columns — вычисляемые столбцы