lenec ru

← все посты

PostgreSQL jsonb: индексы, операторы и когда не нужен MongoDB

10K

Тип 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'

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.