Привет, Хабр! Одной из важных функций в аналитическом языке 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 млн.
Получение декартова произведения явно задается разработчиком через метод расширения SummarizeColumns
:
public static IEnumerable<(TGrouped? grouped, TExpressions expressions)> SummarizeColumns<T, TGrouped, TExpressions>(
this T[] items,
Func<T, TGrouped> groupBy,
Func<T?, TGrouped?, bool> filter,
Func<IEnumerable<T>, TGrouped?, TExpressions?> expressions,
IEnumerable<TGrouped>? orderBy = null,
int maxCount = int.MaxValue)
where TGrouped : notnull
Соответственно, в пакете DaxSharp есть только DAX-style логика SUMMARIZECOLUMNS
с учетом декартова произведения и выполнения запроса по частям, и разработчик сам отвечает за учет «схемы данных» (насколько это вообще применимо к C#), сортировку в декартовом произведении через параметр orderBy
, фильтры и т.д. В итоге, несмотря на отсутствие 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)
};
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)
};
var results = data.SummarizeColumns(
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,
from pId in Enumerable.Range(1, 3)
from cId in Enumerable.Range(1, 3)
select new { Product = $"Product{pId}", Category = $"Category{cId}" }
).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.SummarizeColumns(
x => new {x.productId, x.customerId},
(_, _) => true,
(x, g) => x.ToArray() is { Length: > 0 } array
? array.Sum(y => y.amount)
: 1,
from pId in Enumerable.Range(0, 1000000)
from cId in Enumerable.Range(0, 1000000)
select new { productId = pId, customerId = cId },
1000
).ToList();
Соответствующий DAX:
EVALUATE
TOPN(
1000,
SUMMARIZECOLUMNS(
Products[ProductId],
Categories[CategoryId],
"Sum", IF(
ISBLANK(SUM(Sales[Amount])),
1,
SUM(Sales[Amount])
)
)
)
Запрос выполняется примерно 0.7 секунд (в предыдущих версиях DaxSharp было 26 секунд).
Creation: 00:00:00.9624441
Elapsed: 00:00:00.6924334
Для 1 миллиарда записей в таблице фактов (заменены 100 млн на 1 миллиард в 3 строке — добавлен ноль) время выполнения запроса около 4 секунд:
Creation: 00:00:09.2175732
Elapsed: 00:00:04.1112009
В общих чертах видно, что пакет DaxSharp способен работать с размерностью декартова произведения полей группировки 1 млн x 1 млн для 100 млн записей и 1 миллиарда записей, что говорит о его «условной алгоритмической корректности» для кейса 1 млн x 1 млн, или по крайней мере, об отсутствии явных проблем.
Таким образом, при использовании пакета DaxSharp нельзя сказать, что для каждого случая очевидно, как реализовать в точности такую же логику, как в DAX, но это выполнимо. Т.е. можно получить имплементацию на C#, которая по результатам совпадает с DAX из Power BI, и работает «более‑менее прилично» с алгоритмической точки зрения, что видно из примера для 100 млн и декартова произведения 1 млн x 1 млн. Безусловно, для DaxSharp напрашиваются многочисленные улучшения, но в целом DaxSharp выглядит работоспособно.
Надеюсь, описанный подход может быть интересен разработчикам, успехов в обработке данных, BI и дашбордах :)