Комментарии 4
Интересный подход, спасибо за разбор! Но возникает закономерный вопрос — зачем реализовывать такую логику на C#, если даже базовые SQL-движки справляются с задачами декартова произведения и агрегаций на порядок быстрее?
Например, аналогичная операция в SQL Server (декартово произведение 1 млн × 1 млн и выборка первых 1 млн строк):
WITH T1 AS (
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 AS a
FROM sys.all_objects AS a
CROSS JOIN sys.all_objects AS b
),
T2 AS (
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 AS b
FROM sys.all_objects AS a
CROSS JOIN sys.all_objects AS b
)
SELECT TOP (1000000) *
FROM T1
CROSS JOIN T2;
У меня этот запрос выполняется за ~4 секунды.
А вот эквивалент для ClickHouse (1 млн × 1 млн, первые 1 млн. строк):
SELECT *
FROM
(
SELECT number AS a
FROM numbers(1000000)
) AS t1
JOIN
(
SELECT number AS b
FROM numbers(1000000)
) AS t2
ON 1 = 1
LIMIT 1000000;
На ClickHouse — ~0.12 сек.
Понятно, что у DaxSharp немного иная цель — эмулировать семантику DAX и SUMMARIZECOLUMNS в .NET-приложениях. Но если говорить исключительно о производительности и объёмах, SQL-инструменты всё ещё выглядят значительно предпочтительнее.
Спасибо за вопрос и комментарий, да, действительно, этот пакет в первую очередь для DAX-подобной семантики в C#.
Кейс для 100 млн записей в таблице фактов и декартова произведения 1 млн × 1 млн — это больше egde кейс для проверки корректности имплементации («ничего не зависает») и производительность пакета на миллионах записей ниже, чем в движках СУБД. Единственное, похоже, что сопоставимый SQL для ClickHouse (без деталей плана выполнения запроса) может выглядеть так (в общем случае поля t1.a
и t2.a
могут быть и неключевыми в таблицах t1
и t2
, поэтому добавляется группировка):
SELECT *
FROM
(
(SELECT number % 1000000 AS a
FROM numbers(100000000)
GROUP BY a)
) AS t1
JOIN
(
(SELECT number % 1000000 AS a
FROM numbers(100000000)
GROUP BY a)
) AS t2
ON 1 = 1
LIMIT 1000;
Такой запрос выполняется в ClickHouse уже примерно 2.2 секунды и пока не содержит агрегацию SUM
, логику IF
и других функций (ISBLANK
), т.е. полный вариант запроса будет ещё дольше выполняться. Также если вместо numbers(100000000)
для сравнения взять numbers(1000000)
, то время выполнения запроса будет около половины секунды, тоже можно сказать, что измеряется в секундах, а не миллисекундах.
С другой стороны, за счет отсутствия запроса к СУБД пакет DaxSharp быстрее для меньшего количества записей (в «таблице фактов» Sales
и соответствующей переменной sales
), но с ограничениями, т.е. если есть возможность пренебречь временем загрузки всей таблицы sales
в .NET приложение, загруженная таблица sales
содержит актуальные данные и т.д.
Например, DaxSharp быстрее для запросов с 0 записей в sales
(т.к. нет запроса к СУБД), для 1, 10, 100 записей, для нескольких тысяч и, возможно, десятков, сотен тысяч записей в sales
, в итоге производительность зависит от запроса, данных и т.д.
Также пакет DaxSharp может использоваться без СУБД, например, с данными из файла или из API.
Потому что это GroupBy с вызовом агрегирующей функции.
И EntityFramework транслирует все В
SELECT SUM(NUM), GroupLabel1, GroupLabel2
FROM (какая-то таблица или SELECT)
GROUP BY GroupLabel1, GroupLabel2
Или в EF:
data
.Where(i=>i=2)
.GroupBy(i=> new (GroupLabel1, GroupLabel2))
.Select(i=> new (i.key.GroupLabel1, i.Key.GroupLabel2, i.Sum(j=>j.NUM)));
Естественно, можно все переписать, но надо работать с выражениями.
Как я понял - в статье про то, что нужен именно синтаксис как в DAX.
Хорошее замечание, что стоить иметь в виду EF, тоже рассматриваю его как направление для улучшений пакета.
Как я понял - в статье про то, что нужен именно синтаксис как в DAX.
И также логика SUMMARIZECOLUMNS
в DAX Power BI немного сложнее, чем группировка с агрегацией, если кратко, то для примера с группировкой по Product[ProductId]
и Customer[CustomerId]
даже используются 3 группировки: «исходная» (параметр метода) x =>
new
{x.productId, x.customerId}
и на её основе в DaxSharp получаются две другие группировки при помощи рефлексии x => x.productId
и x => x.customerId
— эти группировки для декартова произведения.
На основе data
делаются расчеты выражений (с фильтрацией строк и групп) с исходной группировкой по x =>
new
{x.productId, x.customerId}
и пишутся в Dictionary
с ключом productId, x.customerId
, дальше на основе группировок из рефлексии x => x.productId
и x => x.customerId
делается перебор для декартова произведения при помощи вложенных итераторов, достаются и возвращаются результаты из подсчитанного Dictionary
, реализуется логика первых N
значений и т.д.
Т.е. в итоге наверно даже получается не столько похожий на DAX Power BI синтаксис (хотя синтаксис похож), но возможность получить результаты по DAX SUMMARIZECOLUMNS
логике декартового произведения, но на C#.
DAX-style подход в C# для SUMMARIZECOLUMNS из Power BI