Всем привет.

Я разрабатываю библиотеку для работы с Entity Attribute Value (репозиторий), сокращенно EAV (модель базы данных для хранения произвольных данных). В конце прошлой статьи я спросил у вас о чём мне ещё надо написать, вы попросили показать пример использования и сделать замеры быстродействия.

Что для нас важно при работе с данными ? Скорость записи (добавления или обновления) и скорость чтения (конкретно - фильтрации по моделям одной сущности). При чём скорость поиска в приоритете, потому что записываем мы один раз в цать дней, а читаем каждую минуту/секунду и даже не один раз, а может быть и не одну сотню раз.

Фишка библиотеки в том что бы работать не с представлением построенном на базовых таблицах EAV, а работать с небольшой частью этих данных записанных в отдельное материализованное представление или в отдельную таблицу.

В Новогодние каникулы я сделал замеры производительности и хочу с вами поделиться результатами.

Что будем измерять ?

Чтение:

  • Время вычитывания всех позиций категории

  • Время формирования параметров фильтрации

  • Время фильтрации

Запись:

  • Время добавления новой характеристики (атрибута)

  • Время добавления новой товарной позиции

  • Время обновления товарной позиции

Набор данных

История разработки библиотеки началась с разработки маркет плэйса, поэтому считайте, что мы работаем с каталогом товарных позиций.

Требования к набору данных

Для тестирования времени чтения из MATERIALIZED VIEW и из TABLE не так важно сколько записей было в исходных таблицах модели EAV, поэтому несколько сотен категорий будет достаточно для проверки гипотезы.

Характеристик у категории (и соответственно у товарной позиции) не больше 100 (для обычного интернет магазина это даже много, обычно характеристик у товара не больше пары десятков).

Характеристики набора данных

С учётом таких вводных,с помощью скрипта data-generator/generate.php, был создан набор данных:

  1. количество сущностей (категорий) - 320

  2. количество атрибутов (характеристик) - 144

  3. количество моделей (позиций во всех категориях) - 29 000

  4. числовых значений - 800 000

  5. строковых значений - 600 000

Суммарный объём EAV таблиц (с индексами) - 310 Мб.

Учёт вариативности данных

Объём данных в которых надо производить поиск (фильтрацию) имеет большое значение для времени поиска.

Разбиваем все сущности на три вида: много атрибутов, среднее количество и малое количество, выбираем по одной сущности из каждого вида:

  • Много - моделей 238, атрибутов 112, всего значений 26656

  • Среднее - моделей 75, атрибутов - 34, значений - 2550

  • Малое - моделей 4, атрибутов - 5, значений 20

Измеренные значения

Все измерения в миллисекундах

Сущность у которой много моделей и атрибутов

Много - моделей 238, атрибутов 112, всего значений 26656, фильтруем по 46 колонкам (из 112).

Чтение

Способ доступа

Получить все записи, без фильтрации

Получить фильтры

Отфильтровать

VIEW

400

382,6

82,8

MATERIALIZED VIEW

5

402,8

40,9

TABLE

4,9

395,9

46,1

Получить все записи без фильтрации из VIEW невозможно, потому что у нас как минимум один фильтр - по категории - уже должен быть. Почему получилось такое чудовищно большое время, я понять не могу.

Для измерения "Отфильтровать", производился отбор по 46 колонкам (из 112).

В том что касается чтения, то фильтрация по MATERIALIZED VIEW происходит на 10% быстрей чем по TABLE.

Запись

Способ доступа

добавить атрибут

добавить модель

обновить модель

VIEW

79,7

155,6

188,7

MATERIALIZED VIEW

1 338

1 112

1 462

TABLE

3

196,5

217

Добавление атрибута в представление это его пересоздание.

Добавление атрибута в MATERIALIZED VIEW это его полное обновление.

Добавление атрибута в таблицу это ALTER TABLE, поэтому всего 3 мс.

Добавление модели подразумевает под собой, создание записи в таблице thing и создание пустых значений в таблицах word и number. Соответственно 155 мс для представления, это вставка 112 записей в таблицу значений (числовые - number, строковые - word).

Для MATERIALIZED VIEW нам снова надо его полностью пересчитать - обновить целиком.

Для таблицы мы добавляем запись у которой будет 112 пустых колонок, это всего 40,9 мс, но добавление записи происходит на основе представления, поэтому 155,6 + 40,9 = 196,5.

Обновление записи (обновление всех 112 колонок) выполняется через обновлением данных в базовых таблицах EAV и затем:

  • Для представления этого достаточно;

  • Для MATERIALIZED VIEW нам снова надо всё полностью пересчитывать;

  • Для таблицы обновление происходит независимо от базовых таблиц, просто обновляется одна строка, но эта строка обновляется только после успешного обновления данных в базовых таблицах, поэтому мы видим 217 мс, по сути собственно обновление записи это 30 мс.

Выводы

Как мы видим использование MATERIALIZED VIEW даёт максимальный выигрыш по времени чтения (в два раза быстрей чем из представления и на 10% быстрей чем из таблицы), но при этом обновление данных занимает на порядок больше времени (проигрыш в 10 раз).

Использование таблицы, даёт выигрыш по чтению в два раза, а проигрыш по записи на 15%, и этот проигрыш обусловлен тем что мы ждём успешного обновления мастер данных, если не ждать то выигрыш будет в 6 раз.

Конечно, что бы не ждать, надо доработать напильником библиотеку, завести туда асинхронность или логику создания и выполнения джобов. Конечно, написать логику для eventual consistency.

Если мы обновляем данные в нашем "универсальном" каталоге раз в час, то потеря одной секунды на обновление несущественна, и это точно стоит того что бы получить дополнительный прирост 10% по сравнению с использованием таблицы.

Если обновления данных у нас происходит несколько раз за час, то я бы использовал таблицу.

Сущность у которой среднее количество моделей и атрибутов

Среднее - моделей 75, атрибутов - 34, значений - 2550, фильтруем по 14 колонкам (из 75).

Чтение

Способ доступа

Получить все записи, без фильтрации

Получить фильтры

Отфильтровать

VIEW

49,5

83

27,9

MATERIALIZED VIEW

1,7

69

12,3

TABLE

1,4

66

12,6

Запись

Способ доступа

добавить атрибут

добавить модель

обновить модель

VIEW

24,4

50

65

MATERIALIZED VIEW

207

212

242

TABLE

3

90

90

При обновлении записи, мы снова видим порядка 30 мс на собственно обновление записи в таблице, но по сравнению с 65 мс на обновление записи в базовых таблицах, это уже 50%.

Сущность у которой малое количество моделей и атрибутов

Малое - моделей 4, атрибутов - 5, значений 20, фильтруем по 2 колонкам (из 5).

Чтение

Способ доступа

Получить все записи, без фильтрации

Получить фильтры

Отфильтровать

VIEW

2,4

10

5,4

MATERIALIZED VIEW

1

13

4

TABLE

0,8

13

2,9

Запись

Способ доступа

добавить атрибут

добавить модель

обновить модель

VIEW

6,2

8,8

8,1

MATERIALIZED VIEW

29

19

27

TABLE

4,4

4,4

10,7

Общий вывод

С уменьшением количества данных, стирается разница между способом доступа к этим данным. Для средних категорий разницы по чтению нет, разница по записи всего в два раза.

Тем не менее общая тенденция сохраняется - поиск по материализованному представлению и таблице происходит в два раза быстрей чем по представлению созданному на основе EAV таблиц.

Если количество сущностей увеличить на порядок, то время поиска в "таблице" не измениться, а время поиска в представлении увеличиться (мне так кажется), поэтому конечно при работе с EAV следует использовать "таблицы".

Вопрос о том будет это материализованное представление, или это будет собственно таблица - решать надо исходя из баланса между чтением и записью.

Что дальше ?

В следующей статье я приведу пример использования библиотеки.

Надеюсь вам было интересно.

Комментарии (9)


  1. DeadMaster
    07.01.2022 10:47
    +3

    Был у меня опыт работы с EAV, это была боль, по мере роста количества данных - росло время выборок. В базе было порядка 50 000 моделей, 2-3 дюжены атрибутов и у каждого до 10 значений, итого - порядка 600 000 записей в таблице.

    В итоге пришлось переписать всё, переведя хранение данных в jsonb поле в постгрессе, скорость выборок на сложных запросах упала с десятков секунд, до десятых долей секунды.


    1. SbWereWolf Автор
      07.01.2022 11:38

      Да, jsonb это убойная фича Постгреса.

      У вас было 600к значений, на моём наборе данных их 800к+600к = 1,4кк. У вас 50к моделей, у меня всего 29к, но разница в два раза для таких значений уже несущественна.

      Если выборка идёт за десятые доли секунды, то это сотни миллисекунды, у меня в измерениях максимум десятки миллесекунд, то есть работа с таблицей выигрывает у работы с jsonb на порядок.

      Но конечно для работы с jsonb не надо такой огород городить как у меня, но с другой стороны, когда кто то уже сделал инструмент, почему бы и нет ?

      Попробую написать бенчмарк для jsonb, спасибо за идею.


      1. Tatikoma
        07.01.2022 16:49

        Потому что EAV не сможет дать такие же эффективные запросы, как JSONB. Простой пример - есть продукты, у продуктов есть какие-то атрибуты с какими-то значениями.

        Задача - выбрать все продукты у которых есть два атрибута - A и B.

        В случае JSONB - это будет один WHERE. В случае EAV - JOIN или GROUP BY.

        Дополнительно можно сравнить производительность, если не два атрибута, а 10 или 20.


        1. SbWereWolf Автор
          07.01.2022 17:13

          Вы, простите, статью читали ? С помощью моей библиотеки это будет запрос к колонкам одной таблицы или одного материализованного представления.

          Запросы делаются не на основе мастер данных в eav таблицах, запросы делаем к предварительно сформированным материализованному представлению или таблице.

          У меня в бенчмарке замеряется выборка по 46 колонкам для первого случая, об этом написано в статье.

          Для второго случая 14, для третьего 2. Хотел это добавить в статью, но редактор выдаёт 400 ошибку. Пришлось оставить как есть.

          Jsonb не может дать такие же эффективные запросы, потому что это запросы к упакованным в одну колону данным, а не к отдельным колонкам, как в моей библиотеке. Будет или так же или медленей.


          1. Kazzman
            07.01.2022 19:07

            То есть при добавлении нового атрибута вы в таблицу добавляете еще однин столбец? Логично что разных атрибутов будет не n тысяч и в ограничение столбцов не упремся. Решаем сейчас заново похожую задачу (работающий в проде вариант - join + count), только дополнительно у нас атрибуты группируются в группы атрибутов и уже группа атрибутов используется для идентификации сущности. Интересно было прочитать про ваш подход и выводы. У нас несколько бОльшие объемы по числу сущностей и пока рабочей версией является отказ от фильтрации средствами бд. Из плюсов - полностью предсказуемая и стабильная скорость поиска.


            1. SbWereWolf Автор
              07.01.2022 19:23

              Отказ от фильтрации на стороне СУБД это очень смелый шаг.

              Для фильтрации средствами приложения вы должны всё выгрузить из СУБД в память приложения и дальше без каких либо индексов, делать выборку.

              Я не знаю .. in-memory СУБД тогда уж применяйте, если вам оперативной памяти не жалко. Тарантул например.

              Почитайте о jsonb, посмотрите доклад Олега Бартунова с последнего хайлрада, в постгресе очень сильно поработали над jsonb, это очень сильный инструмент.

              По полям внутри json можно построить индекс, выборка производиться при использовании этого индекса. К полям внутри json можно применять такие же операции сравнения как к обычным колонкам.

              Вам только придётся научиться со всем этим работать. Но оно того стоит.

              Если вам интересна идея моей библиотеки, то у меня в профиле посмотрите другие статьи по "Универсальному каталогу". Мне кажется я очень подробно описал идею.

              Следующая статья будет с примером использования.


              1. Kazzman
                07.01.2022 19:55

                У нас не только постгре, но и mssql (не свежих версий), и сдается мне что индекс по json и индекс по xml(в mssql) суть похожи, тоесть нужно в динамический запрос добавлять все искомые атрибуты и план будет страшен + всё равно с диска поднимать данные.

                Inmemory базы всё равно не объектно орентированные, математика там та же. Если только хитрые колоночные хранилища, которые и в постгре есть, а в сиквеле в этой задаче местами давали невероятные ускорения. В inmemory db тоже нужно грузить, прогрев очень долгий, на первом этапе у нас 600м сущностей грузятся десятки минут. Кроме того, самопалом можно схитрить, и ,например, 600м intов утолкать в 100мб памяти. Пока мы решили своими силами, посмотрим. Первый проект с такой организацией поехал в прод перед новым годом.


  1. ilmerainen
    07.01.2022 10:52

    n


  1. syusifov
    07.01.2022 20:49

    ЕАВ - хранилище с динамической структурой (персисентная часть характеристик - свойств и временные характеристики - свойства).

    Со временем структура (стремиться стать) становится статической.

    Классификация идет не только по свойствам, но и по их значениям.

    Малое количество определенных значений (когда значения свойства для всех объектов определены) разбивает тип на тип с подтипами.

    Малое количество определенных значений хранятся в жесткой структуре ЕАВ (там редко применяется поиск, так как эти свойства не устойчивы (в основном нулл для большинство объектов типа)), по мере роста количество определенных значений свойства для объектов типа свойство переносится в персистентную часть и так же обртано - увеличение количество непопределенных значений приводит к переносу свойства во временный отстойник.

    В некоторых случаях умирают подтипы, иногда и сами типы и граф вырождается и т.д.