Привет, Хабр! Одной из важных функций в аналитическом языке DAX является SUMMARIZECOLUMNS
, т.к. она готовит данные для дашбордов за счет декартова произведения полей группировки, если поля группировки из разных таблиц. Понятно, что на любом языке программирования можно реализовать логику, в чем-то аналогичную SUMMARIZECOLUMNS
из DAX. Интересующимся DAX-style логикой для C# из NuGet пакета DaxSharp для функцииSUMMARIZECOLUMNS
— добро пожаловать под кат :)
Вообще говоря, функция SUMMARIZECOLUMNS
из Power BI до сих пор развивается и дорабатывается Microsoft. Так, в 2024 году добавлено использование SUMMARIZECOLUMNS
в мерах в Power BI. Синтаксис SUMMARIZECOLUMNS
выглядит следующим образом:


Назначение параметров для группировки, фильтрации и выражений в общем и целом интуитивно понятно, и специфика SUMMARIZECOLUMNS
заключается в декартовом произведении полей группировки.
В связи с этим можно выделить две особенности SUMMARIZECOLUMNS
в Power BI:
декартово произведение для группировки по разным таблицам и меры «с константой»;
выполнение по частям с алгоритмической точки зрения, т.е. для декартова произведения 1 млн x 1 млн не будет 1 000 000 000 000 «неделимых строк» в результате, а будет выполнение по частям, например, в простейшем случае — первая 1000 записей.
Аналог такого поведения SUMMARIZECOLUMNS
из DAX для Power BI имеется в NuGet пакете DaxSharp. Этот пакет не обеспечивает полную поддержку DAX из Power BI, но позволяет писать DAX-style логику из SUMMARIZECOLUMNS
из Power BI с учетом декартова произведения и выполнения SUMMARIZECOLUMNS
по частям — например, получить первую 1000 записей при размерности декартова произведения 1 млн x 1 млн.
Получение декартова произведения явно задается разработчиком через метод расширения SummarizeColumnsCartesian
(если декартово произведение не нужно, то используется метод SummarizeColumns
с той же сигнатурой):
IEnumerable<(TGrouped grouped, TExpressions expressions)>
SummarizeColumnsCartesian<T, TGrouped, TExpressions>(
this IEnumerable<T> items,
Func<T, TGrouped> groupByBuilder,
Func<T?, TGrouped?, bool> filter,
Func<IEnumerable<T>, TGrouped?, TExpressions?> expressions,
int maxCount = int.MaxValue)
where TGrouped : notnull
Соответственно, в пакете DaxSharp есть только DAX-style логика SUMMARIZECOLUMNS
с учетом декартова произведения и выполнения запроса по частям, и разработчик сам отвечает за учет «схемы данных» (насколько это вообще применимо к C#), «включение или отключение» декартова произведения, фильтров и т.д. В итоге, несмотря на отсутствие DAX функционала, DaxSharp условно считать «надмножеством» функции SUMMARIZECOLUMNS
из DAX в Power BI, т.е. DAX-style логика, которая контролируется C# разработчиком, и он отвечает за обработку метаданных и построение запроса.
Конечно, удобнее всего ознакомиться с DaxSharp на примерах. Рассмотрим схему данных с таблицей продаж Sales
и справочниками Products
и Categories
.

Таблица Sales
является денормализованной, т.е. в ней хранятся значения из справочников, например, имя продукта дублируется в Sales[Product]
и соответствует Product[Product]
, такое дублирование только для целей иллюстрации. Предполагаем, что условно все связи и данные валидны — один ко многим между справочниками и таблицей фактов, чтобы не рассматривать частные случаи.
Работа SUMMARIZECOLUMNS без декартова произведения — для полей группировки из одной таблицы
Рассмотрим пример, в котором не будет декартова произведения, т.е. поля группировки из одной и той же таблицы, т.е. следующий DAX:
EVALUATE
SUMMARIZECOLUMNS(
Sales[Product],
Sales[Category],
FILTER(
Categories,
Categories[IsActive] = TRUE && Categories[Category] <> "Category1"
),
"Sum", IF(
ISBLANK(SUM(Sales[Amount])),
2,
SUM(Sales[Amount])
)
)
Результаты Power BI:

Исходные данные и соответствующий код DaxSharp:
using DaxSharp;
var data = new[]
{
(Product: "Product1", Category: "Category1", IsActive: true, Amount: 10, Quantity: 2),
(Product: "Product1", Category: "Category2", IsActive: true, Amount: 20, Quantity: 3),
(Product: "Product2", Category: "Category1", IsActive: true, Amount: 5, Quantity: 1),
(Product: "Product3", Category: "Category3", IsActive: true, Amount: 15, Quantity: 2)
}.ToList();
var results = data.SummarizeColumns(
item => new { item.Product, item.Category },
(_, _) => true,
(items, g) =>
items.Where(x => x.IsActive && x.Category != "Category1").ToArray() is { Length: > 0 } array
? array.Sum(x => x.Amount)
: 2
).ToList();
Результаты DaxSharp:
Product1, Category1, 2
Product1, Category2, 20
Product2, Category1, 2
Product3, Category3, 15
Как видно, результаты DaxSharp совпадают с Power BI (за исключением порядка) и декартова произведения нет при использовании метода SummarizeColumns
.
Работа SUMMARIZECOLUMNS с декартовым произведением — для полей группировки из разных таблиц
Рассмотрим пример, в котором будет декартово произведение, т.е. таблицы в полях группировки разные, а также для выражения из SUMMARIZECOLUMNS
даже при пустой группе будет результат (число 2) — при этом будет использован метод SummarizeColumnsCartesian
, соответствующий DAX:
EVALUATE
SUMMARIZECOLUMNS(
Products[Product],
Categories[Category],
FILTER(
Categories,
Categories[IsActive] = TRUE && Categories[Category] <> "Category1"
),
"Sum", IF(
ISBLANK(SUM(Sales[Amount])),
2,
SUM(Sales[Amount])
)
)
Результаты Power BI:

Исходные данные и код DaxSharp:
using DaxSharp;
var data = new[]
{
(Product: "Product1", Category: "Category1", IsActive: true, Amount: 10, Quantity: 2),
(Product: "Product1", Category: "Category2", IsActive: true, Amount: 20, Quantity: 3),
(Product: "Product2", Category: "Category1", IsActive: true, Amount: 5, Quantity: 1),
(Product: "Product3", Category: "Category3", IsActive: true, Amount: 15, Quantity: 2)
}.ToList();
var results = data.SummarizeColumnsCartesian(
item => new { item.Product, item.Category },
(item, g) => item is { IsActive: true, Category: not "Category1" } || g is { Category: not "Category1" },
(items, g) =>
items.ToArray() is { Length: > 0 } array
? array.Sum(x => x.Amount)
: 2
).ToList();
Результаты DaxSharp:
Product1, Category2, 20
Product2, Category2, 2
Product3, Category2, 2
Product1, Category3, 2
Product2, Category3, 2
Product3, Category3, 15
Как видно, получаем декартово произведение для Product
и Category
с учетом фильтра по категориям Categories[Category] <> "Category1"
, и результаты совпадают с Power BI, в данном случае совпадает и порядок записей.
Работа SUMMARIZECOLUMNS с размерностью 1 млн x 1 млн — первые 1000 записей
Как уже было отмечено, вторая важная особенность SUMMARIZECOLUMNS — это способность выполнения запроса по частям для декартова произведения большой размерности, например, 1 млн x 1 млн. Для этого рассмотрим пример со 100 миллионами строк в Sales
и группировкой 1 млн x 1 млн с декартовым произведением, выберем первые 1000 записей.
var stopwatch = new Stopwatch();
stopwatch.Start();
var sales = Enumerable.Range(0, 100000000)
.Select(i => (productId: i % 1000000, customerId: i % 1000000, amount: i % 100))
.ToArray();
stopwatch.Stop();
output.WriteLine($"Creation: {stopwatch.Elapsed}");
stopwatch.Restart();
var result = sales.SummarizeColumnsCartesian(
x => new {x.productId, x.customerId},
(x, g) => x is { amount: > 1 } || g is { productId: > 0 },
(x, g) => x.ToArray() is { Length: > 0 } array
? array.Max(y => y.amount) + array.Sum(y => y.amount)
: 0,
1000
).ToList();
stopwatch.Stop();
output.WriteLine($"Elapsed: {stopwatch.Elapsed}");
Соответствующий DAX:
EVALUATE
TOPN(
1000,
SUMMARIZECOLUMNS(
Products[ProductId],
Customers[CustomerId],
"Sum", IF(
ISBLANK(SUM(Sales[Amount])),
2,
SUM(Sales[Amount])
)
)
)
Запрос выполняется примерно 26 секунд.
Creation: 00:00:00.9013289
Elapsed: 00:00:25.9113662
В общих чертах видно, что пакет DaxSharp способен работать с размерностью декартова произведения полей группировки 1 млн x 1 млн для 100 млн записей, что говорит о его «условной алгоритмической корректности» для кейса 1 млн x 1 млн, или по крайней мере, об отсутствии явных проблем.
Таким образом, при использовании пакета DaxSharp нельзя сказать, что для каждого случая очевидно, как реализовать в точности такую же логику, как в DAX, но это выполнимо. Т.е. можно получить имплементацию на C#, которая по результатам совпадает с DAX из Power BI, и работает «более‑менее прилично» с алгоритмической точки зрения, что видно из примера для 100 млн и декартова произведения 1 млн x 1 млн. Безусловно, для DaxSharp напрашиваются многочисленные улучшения, например, поддержка сортировки, но в целом DaxSharp выглядит работоспособно.
Надеюсь, описанный подход может быть интересен разработчикам, успехов в обработке данных, BI и дашбордах :)
Alex-ok
Интересный подход, спасибо за разбор! Но возникает закономерный вопрос — зачем реализовывать такую логику на C#, если даже базовые SQL-движки справляются с задачами декартова произведения и агрегаций на порядок быстрее?
Например, аналогичная операция в SQL Server (декартово произведение 1 млн × 1 млн и выборка первых 1 млн строк):
У меня этот запрос выполняется за ~4 секунды.
А вот эквивалент для ClickHouse (1 млн × 1 млн, первые 1 млн. строк):
На ClickHouse — ~0.12 сек.
Понятно, что у DaxSharp немного иная цель — эмулировать семантику DAX и SUMMARIZECOLUMNS в .NET-приложениях. Но если говорить исключительно о производительности и объёмах, SQL-инструменты всё ещё выглядят значительно предпочтительнее.
koanse Автор
Спасибо за вопрос и комментарий, да, действительно, этот пакет в первую очередь для DAX-подобной семантики в C#.
Кейс для 100 млн записей в таблице фактов и декартова произведения 1 млн × 1 млн — это больше egde кейс для проверки корректности имплементации («ничего не зависает») и производительность пакета на миллионах записей ниже, чем в движках СУБД. Единственное, похоже, что сопоставимый SQL для ClickHouse (без деталей плана выполнения запроса) может выглядеть так (в общем случае поля
t1.a
иt2.a
могут быть и неключевыми в таблицахt1
иt2
, поэтому добавляется группировка):Такой запрос выполняется в 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.
ValeriyPus
Потому что это GroupBy с вызовом агрегирующей функции.
И EntityFramework транслирует все В
Или в EF:
Естественно, можно все переписать, но надо работать с выражениями.
Как я понял - в статье про то, что нужен именно синтаксис как в DAX.
koanse Автор
Хорошее замечание, что стоить иметь в виду EF, тоже рассматриваю его как направление для улучшений пакета.
И также логика
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#.