В разработке часто возникает необходимость работы с динамическими атрибутами для тех или иных сущностей, более того — сами сущности могут быть полностью динамическими. Думаю, что самым известным таким примером является JIRA, где администраторы могут добавлять или удалять атрибуты тикетов, после чего каждый пользователь потенциально сможет с ними работать (просматривать или изменять их значения). В то же самое время JIRA предоставляет широкие возможности для фильтрации и сортировки тикетов по динамическим атрибутам, что говорит о том, что работа с динамическими атрибутами глубоко интегрирована в хранилище данных JIRA, иначе добиться хорошей производительности при работе с большим количеством данных вряд ли бы получилось. Так, например, если есть тысячи или даже миллионы хранимых объектов (тех же тикетов в JIRA) и если бы фильтрация не была реализована в самом хранилище данных, то необходимо было бы прочитать каждый объект в память приложения, чтобы проверить, не соответствует ли он заданным условиям фильтрации. Очевидно, что такой подход не выглядит особо эффективным.


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


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


Прежде чем фильтровать данные, надо сначала определиться с тем, как правильно организовать данные в таблицах SQL. В простейшем сценарии структура таблиц для динамических атрибутов может выглядеть следующим образом:


  • Product — Список объектов, которые будут расширены динамическими атрибутами;
  • Attribute — Cписок динамических атрибутов (имя атрибута и его тип);
  • ProductAttribute — значения динамических атрибутов для объектов.

Давайте рассмотрим пример, когда у нас есть список моделей мобильных телефонов, и мы хотим динамически добавлять какие-нибудь дополнительные атрибуты, такие как «производитель», «внутренняя память (Гб)», «протоколы сотовой связи», «дата выпуска» и т. д.


В этом случае данные могут выглядеть следующим образом:



В этой простой структуре есть один момент, который может вызвать определенные проблемы — обратите внимание, что тип столбца Value — это строка. На первый взгляд это кажется логичным, так как тип атрибута может быть разным, но при этом любой из типов всегда можно представить в виде строки. Однако с точки зрения фильтрации и сортировки — это не лучший выбор, потому что в общем случае сравнение строк не соответствует сравнению закодированных значений. Так, например, при сравнении строк '2,11' и '11,2' и сравнении чисел 2.11 и 11.2 будет получен прямо противоположный результат — строка '2,11' «больше», чем строка '11,2', но число 2,11 меньше, чем число 11,2.


Другая проблема с использованием строк заключается в том, что одни и те же данные могут быть закодированы по-разному. Думаю, многие разработчики сталкивались с проблемами, вызванными разными форматами даты: 05/29/2022, 2022–05–22, 29.05.2022 — одна и та же дата, но разные строки.


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


Учитывая всё вышесказанное, рассмотрим новую структуру таблиц:



Теперь таблица значений ProductAttribute имеет по одному столбцу для каждого типа атрибута (ссылка, номер, дата), а значения-списки перемещены в отдельную таблицу ProductAttributeItem.


Используя эту структуру таблиц, мы можем попытаться выбрать некоторые объекты, соответствующие неким заданным критериям.


Фильтрация в SQL


Рассмотрим пример «сложного фильтра»:



В виде SQL он может быть выражен как:


(
  [1]/*Vendor*/ = 1/*Apple*/ 
  AND
  [2]/*Internal Memory*/ >= 64
  AND 
  [2]/*Internal Memory*/ <= 256
)
OR
([1]/*Vendor*/ = 2/*Samsung*/)

Идея состоит в том, чтобы внедрить это выражение в некий SQL-запрос таким образом, чтобы сервер баз данных выполнил фильтрацию самостоятельно. Для этого нужно сделать следующее:


-Проанализировать то какие атрибуты используются в фильтре
-Присоединить таблицу значений ProductAttribute столько раз, сколько уникальных атрибутов присутствует в фильтре.
Фильтр из примера содержит два атрибута:


-[1] Vendor — ссылка на перечисление;
-[2] Internal Memory — число.


Таким образом, окончательный запрос должен содержать под-запрос, в котором ProductAttribute соединяется два раза с правильным псевдонимами столбцов:


SELECT
    [ATTRIBUTES].ProductId
FROM
(
    SELECT
        [P].ProductId,
        [AT_1].ValueItem [1],
        [AT_2].ValueInt [2]
    FROM Product
        [P]
    LEFT JOIN ProductAttribute
        [AT_1] ON
        [AT_1].AttributeId = 1/*Vendor*/
        AND
        [AT_1].ProductId = [P].ProductId
    LEFT JOIN ProductAttribute
        [AT_2] ON
        [AT_2].AttributeId = 2/*Internal Memory*/
        AND
        [AT_2].ProductId = [P].ProductId
) [ATTRIBUTES]
WHERE
(
  [1]/*Vendor*/ = 1/*Apple*/ 
  AND
  [2]/*Internal Memory*/ >= 64
  AND 
  [2]/*Internal Memory*/ <= 256
)
OR
([1]/*Vendor*/ = 2/*Samsung*/)

Как мы видим, исходное выражение фильтра (как и любое другое с такими же атрибутами) можно внедрить в созданный запрос, и фильтрация будет выполнена непосредственно в базе данных. Помимо того, что эта фильтрация будет достаточно эффективной (там же будут индексы, да?), мы также получаем возможность выделения диапазона в отсортированном наборе данных (пагинация с FETCH OFFSET), что потребовало бы наличия всех данных в памяти приложения в том случае, если бы фильтрация не производилась бы на уровне базы данных.


Построение динамического SQL


В теории это выглядит нормально, но не совсем понятно, как использовать этот подход в реальном приложении, так как сначала нужно каким-то образом решить следующие задачи:


-Преобразовать критерии фильтрации в булевское SQL выражение;
-Подготовить правильный подзапрос со всеми необходимыми соединениями таблиц.


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


Далее в качестве примера я буду использовать библиотеку SqExpress для платформы .Net.
Конечно, вы можете использовать и другие подобные библиотеки — принципы останутся прежними.


Итак, начнем с построения логического выражения:
Примечание: здесь фильтр "захардкожен", но ничто не мешает создать его динамически из какой-либо модели (фильтра).


var vendor = SqQueryBuilder.Column("1");
var internalMemory = SqQueryBuilder.Column("2");

ExprBoolean filter = vendor == 1;
filter = filter & internalMemory >= 64 & internalMemory <= 256;
filter = filter | vendor == 2;

Небольшая проверка, что это то, что мы хотим:


Console.WriteLine(TSqlExporter.Default.ToSql(filter));

//[1]=1 AND [2]>=64 AND [2]<=256 OR [1]=2

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


List<int> filterAttributes = filter
    .SyntaxTree()
    .DescendantsAndSelf()
    .OfType<ExprColumn>()
    .Select(c => int.Parse(c.ColumnName.Name))
    .Distinct()
    .ToList();

foreach(var filterAttribute in filterAttributes)
{
    Console.WriteLine(filterAttribute);
}
//1
//2

Теперь когда мы знаем все идентификаторы динамических атрибутов в фильтре, мы можем получить их типы:


var tblAttribute = AllTables.GetAttribute();

Dictionary<int, AttributeType> typesDict = await SqQueryBuilder
    .Select(tblAttribute.AttributeId, tblAttribute.AttributeType)
    .From(tblAttribute)
    .Where(tblAttribute.AttributeId.In(filterAttributes))
    .QueryDictionary(
        database,
        r => tblAttribute.AttributeId.Read(r),
        r => (AttributeType)tblAttribute.AttributeType.Read(r));

Используя информацию об атрибутах, мы можем построить подзапрос, в котором таблица ProductAttribute будет присоединена один раз для каждого уникального атрибута:


var tblProduct = AllTables.GetProduct();

var subQueryColumns = new List<IExprSelecting> { tblProduct.ProductId };

var subQuerySelect = SqQueryBuilder
    .Select(subQueryColumns)
    .From(tblProduct);

foreach (var filterAttributeId in filterAttributes)
{
    var tblProductAttribute = AllTables.GetProductAttribute();

    subQuerySelect = subQuerySelect.LeftJoin(
        tblProductAttribute,
        on: tblProductAttribute.ProductId == tblProduct.ProductId
            & tblProductAttribute.AttributeId == filterAttributeId);

    switch (typesDict[filterAttributeId])
    {
        case AttributeType.Integer:
            subQueryColumns.Add(tblProductAttribute.ValueInt.As(filterAttributeId.ToString()));
            break;
        case AttributeType.Set:
            subQueryColumns.Add(tblProductAttribute.ValueItem.As(filterAttributeId.ToString()));
            break;
        case AttributeType.Date:
            subQueryColumns.Add(tblProductAttribute.ValueDate.As(filterAttributeId.ToString()));
            break;
    }
}

Собирая всё вместе:


var subQuery = subQuerySelect.As(SqQueryBuilder.TableAlias("ATTRIBUTES"));

var query = SqQueryBuilder
    .Select(tblProduct.ProductName)
    .From(tblProduct)
    .InnerJoin(
        subQuery, 
        on: tblProduct.ProductId == tblProduct.ProductId.WithSource(subQuery.Alias))
    .Where(filter)
    .Done();

await query.Query(database, r => Console.WriteLine(tblProduct.ProductName.Read(r)));

//iPhone 11 128
//Galaxy Note 20 Ultra

Это всё прекрасно работает с простыми атрибутами, но если тип атрибута это «Set» (значение представляет собой набор элементов), то потребуется чуть больше усилий. Например, мы хотим фильтровать по сотовым протоколам:


var cProtocols = SqQueryBuilder.Column("3");

filter = cProtocols.In(3, 5);
Console.WriteLine(TSqlExporter.Default.ToSql(filter));
//[3] IN(3,5)

Для выполнения этой фильтрации в базе данных, этот фильтра должен быть изменен следующим образом: оператор IN должен быть заменен подзапросом EXISTS для того, чтобы использовалась таблица ProductAttributeItem. Это можно сделать, изменив синтаксическое дерево исходного фильтра:


filter = (ExprBoolean)filter.SyntaxTree().Modify<ExprInValues>(exprInValues =>
{
    var column = (ExprColumn)exprInValues.TestExpression;
    var columId = int.Parse(column.ColumnName.Name);

    var t = AllTables.GetProductAttributeItem();
    return SqQueryBuilder.Exists(
        SqQueryBuilder
            .SelectOne()
            .From(t)
            .Where(
                t.AttributeId == columId
                & t.ProductId == tblProduct.ProductId
                & t.AttributeItemId.In(exprInValues.Items)));
})!;
Console.WriteLine(TSqlExporter.Default.ToSql(filter));

EXISTS
(
  SELECT 1 
  FROM [dbo].[ProductAttributeItem] 
    [A0] 
  WHERE 
     [A0].[AttributeId]=3 
     AND 
     [A0].[ProductId]=[A1].[ProductId] 
     AND 
     [A0].[AttributeItemId] IN(3,5)
)

После модификации фильтр можно использовать в запросе:


await SqQueryBuilder
    .Select(tblProduct.ProductName)
    .From(tblProduct)
    .Where(filter)
    .Query(database, r => Console.WriteLine(tblProduct.ProductName.Read(r)));

...


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


Ссылки:


  • SqGoods — это демо веб-приложение, демонстрирующее динамическую фильтрацию динамических сущностей;
  • Исходный код для этой статьи
  • SqExpress — библиотека для создания динамического SQL, которая была использована в примерах в этой статье.

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


  1. Akina
    01.06.2022 22:05
    +4

    Странно было во всей статье так и не найти термина "фасетный поиск". Как, впрочем, и аббревиатуры EAV.

    Странно видеть утверждение "мы сосредоточимся только на базах данных SQL", хотя практически весь код - это .Net, причём даже не собственно какой-то из языков, а фреймворк.

    Фраза "а значения-списки перемещены в отдельную таблицу ProductAttributeItem" просто изумила.

    Совсем не понимаю, почему "окончательный запрос должен содержать под-запрос, в котором ProductAttribute соединяется два раза с правильным псевдонимами столбцов", хотя всё прекрасно можно сделать и на одной копии таблицы. Может, потому, что именно это фреймворк не умеет?

    Совсем не понял обоснования того, что "оператор IN должен быть заменен подзапросом EXISTS" - если не считать специальных случаев (NULL), то WHERE IN и коррелированный WHERE EXISTS по результату эквивалентны (но не всегда - по плану выполнения). Или это опять именно потому, что всё происходит во фреймворке, который не умеет?

    В общем, вопросов осталось больше, чем было получено ответов.

    Да! ещё одно. Очень бы хотелось видеть текст SQL-запроса, который будет сгенерирован фреймворком на основании предложенного кода. Чтобы сравнить с тем, что напишет SQL-программист...


  1. euroUK
    01.06.2022 22:20
    +1

    Идея мягко сказать не пахнет инновационностью.

    В общем случае, если у нас не чистый SQL, а все равно есть бэк, то ничто не мешает нам:

    1) Сделать отдельные таблицы для каждого существенного типа данных вида ObjectId, PropertyId, Value где value может быть например интом, валютой, датой или даблом.

    2) Фильтры по пропертям возвращают ObjectId который джойнится или объеденяется в зависимости от операции

    3) С итоговым списком ObjectId ведется работа, например возвращается простыня значений полей которая мапится на объекты уже на бэке.


    1. 0x1000000 Автор
      02.06.2022 12:49

      Как при этом сделать сквозную сортировку и пагинацию, если фильтрация идет одновременно по нескольким полям и фильтр может включать любые логические операции (как в примере из статьи)?


  1. smple
    01.06.2022 22:56

    о опять статья о том как создать свой EAV с блэк джэком и перфомансом.

    рассказываю способ к которому можно придти от души потанцевав на граблях EAV.
    учитывая что новые атрибуты добавляются значительно реже чем читаются продукты, можно сделать хранение атрибутов товара ВНЕЗАПНО в КОЛОНКАХ таблицы продуктов.

    Тогда когда добавляется атрибут release date можно создать запрос на добавление колонки release date с типом int в таблицу product, таким образом можно добавлять сколько угодно атрибутов товарам мобильные телефоны, мы получим решение кучи проблем EAV.

    Внимательные люди могут заметить, а что если у нас не только мобильные телефоны в продуктах, но и например телевизоры ? тогда надо в продуктах добавить type_id и для каждого тайпа хранить свою таблицу product_phones (product_id, ... тут самые расширенные атрибуты) product_tvs(product_id, тут дополнительные атрибуты), например если нужно добавить пользовательский атрибут всем телевизорам, то внезапно добавим его в product_tvs

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

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

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

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


    1. Drunik
      02.06.2022 09:27
      +1

      А что делать если потребуется новый тип товара? Или отобрать товары разных типов по какому-то одному атрибуту? Цвет скажем или высота?


      1. smple
        02.06.2022 10:28

        А что делать если потребуется новый тип товара?

        В текущей статье о типах товара и не слова, все пользовательские атрибуты навешиваются на product, это сделано для упрощения, вам по сути тоже ничего не мешает повторить это процецедуры иметь кучу nullable колонок это не так плохо в отличие от проблем eav, где большая часть проблем это способы хранения V.

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

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

        Или отобрать товары разных типов по какому-то одному атрибуту?

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

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

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

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


      1. 0x1000000 Автор
        02.06.2022 12:39
        +1

        Можно просто привязать атрибуты к категории товара. Это, собственно, и сделано в демонстрационном приложении SqGoods:


        1. smple
          02.06.2022 16:31

          Можно просто привязать атрибуты к категории товара

          тогда атрибуты будут не у товара, а у категории, а значения мы где будем хранить ?

          в предложенном мной варианте категория товара воспринималась как тип и атрибуты я предложил добавлял типу товара, но только в виде колонок (product_tvs, product_phones), просто в магазинах может быть типа товара, он обычно один у товара, а могут быть категории это различные элементы меню где показывается товар и тут уже у товара будет связь не 1хN как у типов товаров, а NxM с категориями, поэтому не надо путать что является типом товара, а что категорией, у некоторых бывают и другие способы именования и структуры, речь тут совершенно не про категории или типы.

          Статья о том как фильтровать по динамическим атрибутам, выбрали EAV потом начали гарадить ад с запросами потому что значения и атрибуты лежат где то в другом месте, я предложил не использовать eav, а атрибуты добавить товарам, тогда их значения будет лежать в той же строке что и сами товары и фильтровать по таким вещам значительно легче, особенно на больших данных, я молчу о возможности создать индексы.

          Но нет лучше товары хранить в одном месте, категории в другом, к категориям привязать атрибуты, а потом эти атрибуты связать с товарами и хранить это(значения) в другой сущности (буква V из EAV) и потом удивлятся на больших данных как это все работает не очень быстро и запросы какие то не интуитивные.

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

          как я уже выше писал серебряной пули нет мы уже ушли в обсуждения категорий, вместо того чтобы признать определенные факты:
          1. EAV далеко не лучший способ хранения информации в РСУБД
          2. В некоторых случаях пользовательские атрибуты можно задавать колонками в таблице, а значение хранить в этой самой колонке у сущности (товаре), получается существенный выйгрышь в скорости чтения и удобстве фильтрации, цена этому возможные блокировки(таблицы) на этапе создание или удаление атрибутов - это бывает не во всех базах.


  1. Dgolubetd
    01.06.2022 23:37
    +1

    Не думали тупо в JSONB хранить аттрибуты? Это дает максимальную гибкость (можно замиксить любые типы товаров, сделать сложные аттрибуты и тд).


    1. 0x1000000 Автор
      02.06.2022 12:43

      Нормально с JSON могут работать далеко не все SQL базы данных, и не понятно, что с производительностью (если делать индексы на JSON, то теряется динамичность).


      1. varanio
        02.06.2022 16:59

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


        1. IvaYan
          02.06.2022 17:36
          +1

          И вы, конечно, можете подсказать подходящую базу?


          1. varanio
            02.06.2022 17:56

            postgres с типом jsonb

            я просто сталкивался на практике с EAV.  Чтобы вытащить хоть что-нибудь из базы, нужно 3-4 джойна. Если же надо написать какую-то выборку посложнее, то тушите свет. Всё умирает: и база, и программист

            и в итоге это везде выпиливалось в итоге после долгих мучений


            1. 0x1000000 Автор
              02.06.2022 18:09

              Хм... а сталкивался на практике с прямо противоположной ситуацией, когда JSON выпиливался и внедрялся EAV как раз из-за проблем с производительностью - с EAV всё стало в тысячи раз быстрее (в буквальном смысле).


  1. IvaYan
    01.06.2022 23:52

    29.05.2022, 2022–05–22, 29.05.2022 — одна и та же дата, но разные строки.

    Даты-то тоже разные, так-то. Во всяком случае вторая из них


    1. 0x1000000 Автор
      02.06.2022 09:02

      Спасибо! Поправил. Это сработала авто-замена. Должно быть " 05/29/2022, 2022–05–22, 29.05.2022 " (MM/dd/yyyy, yyyy-MM-dd, dd.MM.yyyy)


  1. nronnie
    02.06.2022 00:47

    Глубоко не втыкал, но, похоже, автора можно поздравить с изобретением EAV. Сейчас работаю с одним проектом где он повсюду - адово дрочево. Особенно учитывая, что все нынешние БД уже давно умеют работать с semistructured data в виде того же JSON.


  1. danilovmy
    02.06.2022 13:07
    +1

    я бы не стал так категорично критиковать EAV, он не плох и не хорош. Это просто один из способов хранить разнородные данные. У него есть свои преимущества относительно того же blob поля, например, скорость поиска.

    @0x1000000. у тебя 5 таблиц.

    • Product — Список объектов, которые будут расширены динамическими атрибутами;

    • Attribute — Cписок динамических атрибутов (имя атрибута и его тип);

    • ProductAttribute — значения

    • таблица значений AttributeItem

    • значения-списки перемещены в отдельную таблицу ProductAttributeItem

    .а что если я скажу что одна таблица лишняя?

    • Product товары

    • Attribute атрибуты товаров

    • AttributeItem значения атрибутов, как уникальные так и списочные. у тебя это ProductAttribute

    • ProductAttributeItem - m2m through таблица от продукта к значениям атрибутов.

    Список для каждого списочного атрибута. Храни пометку типа "список" делай выборку Attribute - AttributeItem, по типу атрибута "список".

    Выборка параметров каждого товара Product-ProductAttributeItem-AttributeItem

    Ну и я не понял в статье, почему нельзя все сделать c filter без вложенных селектов только на where and, or. Похоже, что не ту таблицу спрашиваешь. ты спрашиваешь from Product, а надо from ProductAttributeItem.


    1. 0x1000000 Автор
      02.06.2022 13:34

      Мне кажется, что данные, в предложенной вами структуре таблиц, будет сложнее обновлять т.к. всегда придется работать с двумя таблицами.

      filter без вложенных селектов

      Идея в том, что фильтр приходит "извне", где не знают о структуре хранения данных, и предикат по полю "списку" (например [Protocol] IN ('4G', '5G')) может быть вложен внутрь сложного булевского выражения. Замена такого предиката на EXISTS(SELECT ...) позволит сохранить логику исходного фильтра.


  1. varanio
    02.06.2022 16:57
    -1

    Кто в проде работал с EAV, тот в цирке не смеется. Врагу не пожелаю такой подход.

    Лучше уж в json хранить


  1. robert_ayrapetyan
    03.06.2022 07:21

    Если важна производительность, то для данных с низкой cardinality (как у вас) стоит попробовать обратные индексы на битах. Тогда вся фильтрация сводится к простым операциям над массивами битов.