PostgreSQL jsonb: индексы, операторы и когда не нужен MongoDB
Тип jsonb в PostgreSQL позволяет хранить полуструктурированные данные, индексировать их и выполнять сложные запросы — без отдельного документного хранилища. Разберёмся, как работают операторы, какие индексы ускоряют запросы и когда MongoDB действительно не нужен.
jsonb vs json — в чём разница
PostgreSQL предлагает два типа для JSON-данных:
json— хранит текст как есть. Каждый запрос парсит JSON заново. Сохраняет порядок ключей и дубликаты.jsonb— хранит в бинарном формате. Парсинг при записи, быстрый доступ при чтении. Не сохраняет порядок ключей, удаляет дубликаты.
Когда использовать json: нужно сохранить оригинальное форматирование (аудит, логирование входящих запросов). Во всех остальных случаях — jsonb. Он поддерживает индексы, операторы сравнения и containment-проверки.
-- Разница в размере: jsonb чуть больше из-за метаданных
SELECT pg_column_size('{"a":1,"b":2}'::json) AS json_size,
pg_column_size('{"a":1,"b":2}'::jsonb) AS jsonb_size;
-- json_size: 13, jsonb_size: 29 (overhead на бинарную структуру)
Операторы jsonb
Основные операторы для работы с jsonb:
-- Извлечение значения как jsonb
SELECT data->'name' FROM users; -- "Игорь" (jsonb)
-- Извлечение как текст
SELECT data->>'name' FROM users; -- Игорь (text)
-- Вложенный путь
SELECT data->'address'->>'city' FROM users;
-- Путь через массив (#> и #>>)
SELECT data#>'{tags,0}' FROM posts; -- первый тег как jsonb
SELECT data#>>'{address,city}' FROM users; -- город как text
-- Containment: содержит ли документ подструктуру
SELECT * FROM users WHERE data @> '{"role": "admin"}';
-- Existence: есть ли ключ
SELECT * FROM users WHERE data ? 'email';
-- Любой из ключей существует
SELECT * FROM users WHERE data ?| array['phone', 'telegram'];
-- Все ключи существуют
SELECT * FROM users WHERE data ?& array['name', 'email'];
Операторы модификации (PostgreSQL 9.5+):
-- Конкатенация (merge)
SELECT '{"a":1}'::jsonb || '{"b":2}'::jsonb; -- {"a":1,"b":2}
-- Удаление ключа
SELECT '{"a":1,"b":2}'::jsonb - 'a'; -- {"b":2}
-- Удаление по пути
SELECT '{"a":{"b":1,"c":2}}'::jsonb #- '{a,b}'; -- {"a":{"c":2}}
-- jsonb_set: обновление значения по пути
UPDATE users SET data = jsonb_set(data, '{role}', '"editor"')
WHERE id = 42;
Индексы для jsonb
Без индексов запросы к jsonb делают sequential scan. PostgreSQL поддерживает несколько стратегий индексирования:
GIN-индекс (основной)
-- Поддерживает @>, ?, ?|, ?&
CREATE INDEX idx_users_data ON users USING GIN (data);
-- Запрос использует индекс:
SELECT * FROM users WHERE data @> '{"role": "admin"}';
GIN с jsonb_path_ops — компактнее и быстрее, но поддерживает только @>:
CREATE INDEX idx_users_data_path ON users USING GIN (data jsonb_path_ops);
-- Работает:
SELECT * FROM users WHERE data @> '{"city": "Moscow"}';
-- НЕ работает с этим индексом: data ? 'email'
B-tree индекс на выражение
Если запросы всегда по конкретному ключу — expression index эффективнее GIN:
-- Индекс на конкретное поле
CREATE INDEX idx_users_email ON users ((data->>'email'));
-- Запрос:
SELECT * FROM users WHERE data->>'email' = 'test@example.com';
-- Для числовых полей — с кастом:
CREATE INDEX idx_orders_amount ON orders (((data->>'amount')::numeric));
SELECT * FROM orders WHERE (data->>'amount')::numeric > 1000;
Что выбрать
- GIN (default) — универсальный, подходит для произвольных запросов по любым ключам
- GIN (jsonb_path_ops) — на 30-40% компактнее, только для
@> - B-tree expression — для запросов по конкретному полю с =, <, >, LIKE
Производительность: jsonb vs отдельные колонки
jsonb не бесплатен. На таблице 10M строк: отдельная колонка + B-tree ~2ms, jsonb + expression index ~3-5ms, jsonb + GIN containment ~5-10ms. Правило: если поле участвует в WHERE/JOIN в 80%+ запросов — выносите в отдельную колонку. jsonb хорош для данных с переменной структурой.
Когда не нужен MongoDB
PostgreSQL с jsonb закрывает большинство сценариев документного хранилища:
- Хранение пользовательских настроек, метаданных, конфигов
- Событийные данные с переменной структурой (логи, аналитика)
- API-ответы от внешних сервисов
- Прототипирование без жёсткой схемы
MongoDB оправдан когда:
- Документы глубоко вложены (5+ уровней) и запросы по всем уровням
- Нужен горизонтальный шардинг из коробки на сотни ТБ
- Схема меняется каждую неделю и нет реляционных связей вообще
Если у вас уже есть PostgreSQL, транзакции, JOIN-ы и часть данных реляционная — добавить jsonb-колонку проще, чем поднимать и обслуживать отдельный кластер MongoDB.
Практические советы
- Не храните в jsonb то, что можно нормализовать — JOIN по jsonb-полю дорогой
- Используйте
jsonb_path_opsесли все запросы через@> - Для частых обновлений одного ключа —
jsonb_set()вместо перезаписи всего документа - Валидируйте структуру через CHECK-constraint:
CHECK (data ? 'name' AND data ? 'email') - В PostgreSQL 12+ используйте JSONPath для сложных запросов:
data @@ '$.price > 100'