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. Схему для клиента начинаем проектировать на следующей неделе.
- ETL-витрина данных: CDC вместо полной перезагрузки · 21 октября 2014
- PostgreSQL 9.3 под 1С 8.3: конфиг для 50 пользователей · 14 августа 2014