ADG Оставить заявку
Блог Данные и аналитика 5 мин чтения

PostgreSQL 9.4: JSONB и конец разговора про EAV

PostgreSQL 9.4 выходит с типом JSONB - бинарное хранение JSON с GIN-индексами. Тестируем на реальном кейсе вариативных атрибутов товаров, сравниваем с MongoDB.

Контекст момента

PostgreSQL 9.4 выходит с типом JSONB - бинарным хранением JSON с поддержкой GIN-индексов и улучшенной логической репликацией

PostgreSQL 9.4 вышел на этой неделе, и главное в этом релизе - не репликация, хотя там тоже есть что обсудить. Главное - тип JSONB. Это не то же самое, что JSON, который был в 9.3: там данные хранились как текст и индексировать их толком не получалось. JSONB - бинарное представление, которое разбирается при записи один раз и дальше живёт как структура. Следствие: GIN-индексы, которые работают по содержимому документа, а не по его текстовому представлению.

Нас это заинтересовало не абстрактно, а по конкретному клиентскому кейсу, который мы тащим последние пару месяцев.

Проблема: вариативные атрибуты без EAV

У клиента - интернет-магазин с несколькими тысячами SKU. Товары разнородные: электроника, одежда, инструменты. У каждой категории свой набор атрибутов. Куртка имеет размер, цвет, тип утеплителя. Дрель - мощность, тип патрона, максимальный диаметр сверления. Пересечений почти нет.

Классическое решение в реляционной базе - EAV: таблица attribute_values с колонками entity_id, attribute_name, value. Мы прошли через этот вариант с другим клиентом пару лет назад и хорошо помним чем заканчивается: JOIN-цепочки на несколько таблиц для любого запроса, PIVOT для нормального чтения, катастрофическое поведение планировщика на больших выборках, и SQL, который никто кроме автора не в состоянии читать без подготовки.

Альтернатива, которую нам предлагали со стороны - перейти на MongoDB. Документная модель для этого случая действительно логична: каждый товар - документ со своим набором полей. Не нужно ничего придумывать с атрибутами.

Но у клиента уже работающая PostgreSQL-инфраструктура, там же транзакционная часть с заказами, там же данные для аналитики. Тащить второй СУБД только ради атрибутов товаров - это операционные затраты, которые не очевидно окупаются.

JSONB как третий вариант

Идея простая: атрибуты товара хранить в колонке attrs jsonb прямо в таблице товаров. Выглядит примерно так:

CREATE TABLE products (
    id          BIGINT PRIMARY KEY,
    category_id INT NOT NULL,
    name        TEXT NOT NULL,
    attrs       JSONB
);

-- Дрель
INSERT INTO products VALUES (
    1, 42, 'Дрель Bosch GSB 13 RE',
    '{"power_w": 600, "chuck_type": "keyless", "max_drill_mm": 13, "impact": true}'
);

-- Куртка
INSERT INTO products VALUES (
    2, 17, 'Куртка Сплав Фрегат',
    '{"size": "L", "color": "navy", "insulation": "synthetic", "weight_g": 850}'
);

Запрос по содержимому атрибутов с GIN-индексом:

CREATE INDEX idx_products_attrs ON products USING GIN (attrs);

-- Найти все дрели с мощностью больше 500W
SELECT id, name FROM products
WHERE category_id = 42
  AND (attrs->>'power_w')::int > 500;

Мы проверили производительность на тестовой базе с ~80 тысячами записей - примерно столько у клиента планируется в горизонте года. Запросы по конкретным атрибутам с GIN-индексом работают быстро. Без индекса - sequential scan, что предсказуемо.

Сравнение с MongoDB

Запустили аналогичный тест на MongoDB 2.6 (текущая стабильная версия). Наполнили ту же коллекцию, создали индекс по power_w, замерили типичные запросы.

Результаты неоднозначные, и это честный ответ:

  • Запросы по одному атрибуту с индексом - MongoDB немного быстрее, разница в пределах заметной, но не драматической.
  • JOIN с таблицей категорий и таблицей заказов - PostgreSQL выигрывает без вариантов. MongoDB здесь не умеет JOIN нативно, приходится делать несколько запросов в приложении.
  • Транзакции с обновлением атрибутов и заказа одновременно - только PostgreSQL, MongoDB в версии 2.6 транзакций между документами не поддерживает.
  • Аналитические запросы через GROUP BY, оконные функции - PostgreSQL, без конкурентов.

Для клиента с его задачей вывод очевидный: оставаться на PostgreSQL и использовать JSONB. Атрибуты - это часть продуктового каталога, который живёт в одной экосистеме с заказами, пользователями и аналитикой. Разрывать это ради документного хранилища не имеет смысла.

Что ещё в 9.4

Помимо JSONB в релизе есть несколько вещей, которые нас интересуют с инфраструктурной стороны:

  • Logical decoding - механизм для декодирования WAL в пользовательский формат. Это основа для логической репликации и CDC без триггеров. Пока это низкоуровневый API, но потенциал очевидный.
  • pg_prewarm - расширение для прогрева буферного кеша после рестарта. Мелочь, но приятная: после перезапуска сервера не нужно ждать пока кеш наполнится органически.
  • WITH ORDINALITY для UNNEST - позволяет разворачивать массивы с сохранением позиции элемента. Нишевая вещь, но в ETL-пайплайнах встречается.

Где осторожность не помешает

JSONB - не серебряная пуля. Несколько моментов, которые важно держать в голове:

  • Схема всё равно нужна, просто неявная. Если в одних товарах power_w это число, а в других строка - запрос сломается при касте. Валидация атрибутов перекладывается на приложение.
  • GIN-индекс по всему JSONB большой. На колонке с разнородными документами он может занять заметный процент от размера таблицы. Если нужны индексы только по конкретным полям - лучше создавать частичные индексы по конкретному ключу.
  • Обновление одного поля требует перезаписи всего документа. В PostgreSQL 9.4 нет операции UPDATE для части JSONB-документа - нужно читать, менять в приложении, писать обратно. Атомарного патчинга отдельного ключа в 9.4 нет; это минус по сравнению с MongoDB.

В целом - мы довольны. Для кейса с вариативными атрибутами JSONB закрывает задачу чище, чем EAV, и без необходимости тащить MongoDB рядом с PostgreSQL. Схему для клиента начинаем проектировать на следующей неделе.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.