JSONB в PostgreSQL 9.4 вместо MongoDB: кейс с конфигурациями оборудования
Клиент хотел MongoDB для хранения конфигураций оборудования. Показали, что JSONB в PostgreSQL даёт индексы и транзакции без жертв - переехали на PostgreSQL.
PostgreSQL 9.4 с типом JSONB набирает популярность как альтернатива MongoDB для полудокументных данных
Клиент пришёл с готовым решением: хочу MongoDB. Под хранение конфигураций производственного оборудования - несколько тысяч единиц, у каждой свой набор параметров, схема от модели к модели разная. Логика понятна: гибкая схема, документная модель, в интернете хвалят.
Мы не стали сразу отговаривать. Попросили рассказать подробнее, что рядом с этими конфигурациями живёт.
Что было под капотом
Система учёта оборудования уже работала на PostgreSQL. В ней - реестр активов, договоры обслуживания, история инцидентов, пользователи с правами. Конфигурации оборудования хотели вынести в MongoDB именно потому что структура у них нефиксированная: у одной модели двадцать параметров, у другой пять, и иногда появляются новые поля при добавлении нового типа оборудования. В реляционной схеме это либо EAV (таблица ключ-значение, которую потом больно читать), либо ALTER TABLE каждый раз.
Проблема в том, что конфигурация не живёт изолированно. При изменении конфигурации должен создаваться аудит-лог, а аудит ссылается на пользователя, договор и конкретный актив. Транзакция: либо конфигурация изменена и лог создан, либо ничего. Разносить это по двум базам - значит писать компенсирующую логику на уровне приложения и всё равно не получать ACID.
Мы смотрели похожую ситуацию с датчиками в 2013-м - там MongoDB имела смысл для временных рядов без транзакционных требований. Здесь ситуация другая.
JSONB как аргумент
PostgreSQL 9.4 мы пробовали в январе на тестовом стенде, в том числе смотрели на JSONB для конфигурационных документов. Теперь была реальная задача.
Суть типа jsonb: документ хранится в разобранном бинарном виде, парсится один раз при записи. Это открывает нормальную индексацию - GIN-индекс по всему столбцу без перечисления конкретных ключей. Запрос «найди все устройства, у которых в конфиге выставлен параметр X со значением Y» делается как обычный SELECT с GIN-индексом, а не полным сканом таблицы с парсингом каждого документа.
Показали клиенту конкретную схему:
CREATE TABLE device_configs (
id bigserial PRIMARY KEY,
asset_id bigint NOT NULL REFERENCES assets(id),
config jsonb NOT NULL,
changed_at timestamptz NOT NULL DEFAULT now(),
changed_by bigint NOT NULL REFERENCES users(id)
);
CREATE INDEX idx_device_configs_config ON device_configs USING GIN (config);
Новый тип оборудования с другим набором параметров - просто пишем документ с другими ключами, никакого ALTER TABLE. Запрос по содержимому:
SELECT asset_id, config
FROM device_configs
WHERE config @> '{"firmware_version": "2.1.4"}';
Оператор @> - «содержит ли документ такую подструктуру» - работает через GIN-индекс, а не seq scan.
Что это даёт против MongoDB в данном случае
Три момента, которые решили дело:
- Транзакции. Изменение конфигурации и запись в аудит-лог - в одной транзакции, без дополнительной логики в приложении.
- Ссылочная целостность.
asset_idиchanged_by- внешние ключи, база контролирует что не запишем конфиг для несуществующего актива. - Один стек. Не надо разворачивать и сопровождать MongoDB рядом с PostgreSQL, настраивать коннекции из приложения к двум базам, думать о консистентности между ними.
MongoDB в этом сценарии дала бы гибкую схему, но потребовала бы либо жертвы транзакциями, либо усложнения архитектуры - паттерн two-phase commit на уровне приложения, который никто не любит реализовывать и который всё равно не даёт таких же гарантий.
Как переезжали
Конфигурации уже были в системе - в классической EAV-таблице: device_id, param_name, param_value. Написали миграцию: SELECT по всем записям, агрегация в JSON-документ по device_id, INSERT в новую таблицу с jsonb-столбцом. Прошло без потерь, данные проверили сравнением счётчиков и выборочным чтением.
Приложение изменили в одном месте: вместо нескольких запросов на чтение параметров по отдельности - один SELECT с разбором JSON-документа на стороне приложения. Запись - тоже один INSERT с документом вместо нескольких INSERT в EAV.
На GIN-индекс ушло время при первоначальном построении по существующим данным - порядка нескольких минут на несколько тысяч записей. Дальше индекс обновляется инкрементально при каждой вставке.
Что получили
Аудит-лог работает транзакционно. Гибкая схема для конфигураций есть. Запросы по содержимому документов работают через индекс. MongoDB в проекте нет.
Клиент поначалу скептически отнёсся к «мы сделаем это в PostgreSQL» - звучало как «реляционщики не умеют в документные данные». После демонстрации запросов с оператором @> и GIN-индексом вопросы отпали.
Работы по хранилищу и аналитике для таких систем ведём в рамках DWH и бизнес-аналитики.
- PostgreSQL 9.4 на тестовом стенде: JSONB быстрее, материализованные представления удобнее · 20 января 2015
- MongoDB и NoSQL: когда это реально применимо · 18 апреля 2013