TL; DR: JSONB може спростити розробку схеми БД без шкоди продуктивності в запитах.
Запровадження
Наведемо класичний приклад, напевно, одного з найстаріших варіантів використання у світі реляційних БД (база даних): ми маємо сутність, і необхідно зберегти певні властивості (атрибути) цієї сутності. Але не всі екземпляри можуть мати однаковий набір властивостей, до того ж у майбутньому, можливе додавання ще властивостей.
Найпростіший шлях вирішення цієї проблеми - це створення стовпця в таблиці БД для кожного значення якості, і просто заповнювати ті, які необхідні для певного екземпляра сутності. Чудово! Проблема вирішена до того моменту, поки ваша таблиця не містить мільйони записів і у вас не виникне необхідність додати новий запис.
Розглянемо патерн EAV (
Тим не менш, я б не писав цю нотатку, якби не було недоліків у підході з використанням EVA. Так, наприклад, для отримання однієї або декількох сутностей, які мають по 1 атрибуту, потрібно 2 join'а (об'єднання) у запиті: перший – об'єднання з таблицею атрибутів, другий – об'єднання з таблицею значень. Якщо сутність мають 2 атрибути, потрібно вже 4 join'а! Крім того, всі атрибути зазвичай зберігаються у вигляді рядків, що призводить до приведення типів як результату, так і умови WHERE. Якщо ви пишете багато запитів, це досить марнотратно, з точки зору використання ресурсів.
Незважаючи на ці очевидні недоліки, EAV вже давно використовується для вирішення таких проблем. Це були неминучі недоліки, і кращої альтернативи просто не було.
Але потім у PostgreSQL з'явилася нова "технологія"...
Починаючи з PostgreSQL 9.4, було додано тип даних JSONB для зберігання двійкових даних JSON. Хоча зберігання JSON у цьому форматі зазвичай займає трохи більше місця та часу, ніж простий текстовий JSON, виконання операцій із ним відбувається набагато швидше. Також JSONB підтримує індексування, що робить запити до них швидше.
Тип даних JSONB дозволяє нам замінити громіздкий патерн EAV шляхом додавання лише одного стовпця JSONB в нашу таблицю сутностей, що значно спрощує проектування бази даних. Але багато хто стверджує, що це має супроводжуватися зниженням продуктивності… Ось тому я і з'явилася ця стаття.
Налаштування тестової бази даних
Для цього порівняння я створив базу даних на новій установці PostgreSQL 9.5 на 80-доларовому складанні
CREATE TABLE entity (
id SERIAL PRIMARY KEY,
name TEXT,
description TEXT
);
CREATE TABLE entity_attribute (
id SERIAL PRIMARY KEY,
name TEXT
);
CREATE TABLE entity_attribute_value (
id SERIAL PRIMARY KEY,
entity_id INT REFERENCES entity(id),
entity_attribute_id INT REFERENCES entity_attribute(id),
value TEXT
);
Нижче представлена таблиця, де будуть зберігатися ті ж дані, але з атрибутами в стовпці типу JSONB – властивості.
CREATE TABLE entity_jsonb (
id SERIAL PRIMARY KEY,
name TEXT,
description TEXT,
properties JSONB
);
Виглядає набагато простіше, чи не так? Потім було додано до таблиць сутностей (суб'єкта & entity_jsonb) 10 мільйонів записів, і відповідно, було заповнено однаковими даними таблиці де використовується EAV патерн та підхід з JSONB стовпцем – entity_jsonb.properties. Таким чином, ми отримали декілька різних типів даних серед усього набору властивостей. Приклад даних:
{
id: 1
name: "Entity1"
description: "Test entity no. 1"
properties: {
color: "red"
lenght: 120
width: 3.1882420
hassomething: true
country: "Belgium"
}
}
Отже, тепер ми маємо однакові дані, для двох варіантів. Почнемо порівнювати реалізації в роботі!
Спрощення дизайну
Раніше вже було сказано, що дизайн БД був спрощений: одна таблиця, за рахунок використання стовпця JSONB для властивостей, замість використання трьох таблиць для EAV. Але як же це відображається у запитах? Оновлення однієї властивості сутності виглядає так:
-- EAV
UPDATE entity_attribute_value
SET value = 'blue'
WHERE entity_attribute_id = 1
AND entity_id = 120;
-- JSONB
UPDATE entity_jsonb
SET properties = jsonb_set(properties, '{"color"}', '"blue"')
WHERE id = 120;
Як бачимо, останній запит не виглядає простіше. Щоб оновити значення властивості в об'єкті JSONB, ми маємо використовувати функцію
Тепер давайте оберемо ту сутність, яку ми щойно оновили, за умови її нового кольору:
-- EAV
SELECT e.name
FROM entity e
INNER JOIN entity_attribute_value eav ON e.id = eav.entity_id
INNER JOIN entity_attribute ea ON eav.entity_attribute_id = ea.id
WHERE ea.name = 'color' AND eav.value = 'blue';
-- JSONB
SELECT name
FROM entity_jsonb
WHERE properties ->> 'color' = 'blue';
Я думаю, що ми можемо погодитися з тим, що друге є більш коротким (без join!) і відповідно більш читабельним. Тут перемога JSONB! Ми використовуємо оператор JSON ->>, щоб отримати колір як текстове значення з об'єкта JSONB. Існує також другий спосіб досягнення того ж результату моделі JSONB з використанням оператора @>:
-- JSONB
SELECT name
FROM entity_jsonb
WHERE properties @> '{"color": "blue"}';
Це трохи складніше: ми перевіряємо, чи містить об'єкт JSON у стовпці властивостей об'єкт, який знаходиться праворуч від оператора @>. Менш читаний, більш продуктивний (див. далі).
Спростити використання JSONB ще сильніше, коли вам потрібно вибрати кілька властивостей одночасно. Ось де дійсно підходить JSONB-підхід: ми просто вибираємо властивості як додаткові стовпці в нашому наборі результатів без необхідності об'єднань:
-- JSONB
SELECT name
, properties ->> 'color'
, properties ->> 'country'
FROM entity_jsonb
WHERE id = 120;
З EAV вам знадобиться два об'єднання для кожної властивості, яку ви хочете запросити. На мій погляд, наведені вище запити свідчать про велике спрощення в дизайні бази даних. Подивитися більше прикладів того, як писати запити до JSONB, можливо також у
Тепер настав час поговорити про продуктивність.
Продуктивність
Щоб порівняти продуктивність, я використав
Оновлення даних показало наступні результати за часом (мс). Зверніть увагу, що масштаб є логарифмічним:
Бачимо, що JSONB набагато (> 50000-x) швидше, ніж EAV, якщо не використовувати індекси з причин, зазначених вище. Коли ми індексуємо стовпці з первинними ключами, різниця майже зникає, але JSONB все ще в 1,3 рази швидше ніж EAV. Зверніть увагу, що індекс в стовпці JSONB тут не впливає, оскільки ми не використовуємо стовпець властивостей в умовах оцінки.
Для вибору даних на основі значення властивості ми отримуємо такі результати (звичайний масштаб):
Можна помітити, що JSONB знову працює швидше, ніж EAV без індексів, але коли EAV з індексами - він все ж таки працює швидше ніж JSONB. Але потім я побачив, що час для JSONB-запитів був однаковим, це підштовхнуло мене до того факту, що GIN-індекс не спрацьовують. Очевидно, коли ви використовуєте індекс GIN для стовпця із заповненими властивостями, він діє лише за умови використання оператора включення @>. Я використав це в новому тесті, що вплинуло на час: всього 0,153 мс! Це в 15000 разів швидше, ніж EAV, і в 25000 разів швидше, ніж оператор ->>.
Думаю, це було досить швидко!
Розмір таблиць БД
Давайте порівняємо розміри таблиць за обох підходів. У psql ми можемо показати розмір всіх таблиць та індексів за допомогою команди dti+
Для підходу EAV розміри таблиць становлять близько 3068 МБ, а індекси – до 3427 МБ, що у сумі дає 6,43 ГБ. Під час використання підходу з JSONB використовується 1817 МБ для таблиці та 318 МБ для індексів, що становить 2,08 ГБ. Виходить у 3 рази менше! Цей факт трохи здивував мене, тому що ми зберігаємо імена властивостей кожного об'єкта JSONB.
Але все-таки цифри говорять самі за себе: в EAV ми зберігаємо 2 цілих зовнішніх ключа на значення атрибута, в результаті чого отримуємо 8 байт додаткових даних. Крім того, в EAV всі значення властивостей зберігаються у вигляді тексту, в той час як JSONB використовуватиме числові та логічні значення всередині, де це можливо, в результаті чого виходить менший обсяг.
Підсумки
Загалом я думаю, що збереження властивостей сутностей у форматі JSONB може значно спростити проектування та обслуговування вашої бази даних. Якщо ви виконуєте багато запитів, все, що зберігається в одній таблиці з сутністю, дійсно працюватиме ефективніше. І той факт, що це спрощує взаємодію між даними, вже є плюсом, але й результуюча БД у 3 рази менша за обсягом.
Також, за зробленим тестом, можна підсумувати, що втрати продуктивності дуже незначні. У деяких випадках JSONB навіть працює швидше, ніж EAV, що робить його ще кращим. Однак цей еталонний тест, звичайно, не охоплює всі аспекти (наприклад, сутності з дуже великою кількістю властивостей, значним збільшенням числа властивостей існуючих даних…), тому, якщо у вас є якісь пропозиції про те, як їх покращити, будь ласка, не соромтеся залишати у коментарях!
Джерело: habr.com