Привет, Хабр! Некоторые функции DAX из Power BI могут выглядеть интуитивно понятными, но при детальном рассмотрении ведут себя не совсем ожидаемо. Например, SUMMARIZE не агрегирует «сразу», в месте использования, но SUMMARIZE реализуется через «виртуальную», «отложенную» агрегацию за счет сохранения состояния. Для суммы, т.е. SUM, промежуточное состояние и есть сумма и особенности SUMMARIZE не проявляются, но для любой другой агрегации (например, среднего AVERAGE) становится понятно, что в Power BI уже учитывается так называемое состояние для корректного расчета SUMMARIZE, т.е. данные по всем записям сохраняются в состоянии. В других СУБД единственный аналог — только State и Merge комбинаторы из ClickHouse, поэтому для иллюстрации состояний будет рассмотрен пример из ClickHouse, соответствующий DAX с SUMMARIZE и AVERAGE. Интересующимся особенностями SUMMARIZE в Power BI — добро пожаловать под кат :)
Рассмотрим на примере одной таблицы sales с полями class, color и amount с данными:
class |
color |
amount |
deluxe |
red |
1 |
economy |
red |
2 |
economy |
black |
3 |
economy |
black |
4 |
Вначале рассмотрим простой DAX с суммированием SUM:
EVALUATE
SUMMARIZECOLUMNS(
'sales'[class],
"sum",
CALCULATE(
SUM (sales[amount]),
SUMMARIZE(sales, sales[color])
)
)
Для суммирования не существует отдельного состояния, так как сумма и есть само состояние. Понятно, что даже если добавить агрегацию (сумму) по sales[color], и затем агрегировать (суммировать) по sales[class], из-за суммирования результат не меняется. Можно проиллюстрировать это по шагам для агрегации SUM, что «интуитивно понятный» SUMMARIZE в виде «немедленной агрегации» для суммирования SUM работает корректно, хотя на самом деле Power BI так не выполняет расчеты.
Вначале агрегируем (суммируем) по sales[color]:
class |
color |
SUM(amount) |
deluxe |
red |
1 |
economy |
red |
2 |
economy |
black |
7 |
Далее агрегируем (суммируем) по sales[class]:
class |
SUM(amount) |
deluxe |
1 |
economy |
9 |
Получаем, что результат совпадает с Power BI:

Видно, что для суммирования как вида агрегации проблем нет и результат такого способа вычисления соответствует результату Power BI.
Но что будет для других видов агрегаций, например, для среднего AVERAGE?
EVALUATE
SUMMARIZECOLUMNS(
'sales'[class],
"average",
CALCULATE(
AVERAGE (sales[amount]),
SUMMARIZE(sales, sales[color])
)
)
Попробуем проверить корректность предыдущего «интуитивно понятного» подхода для AVERAGE агрегации, точнее, покажем, почему он некорректен.
Понятно, что при расчете среднего корректный ответ должен быть 1 для deluxe и 3 для economy:

Проиллюстрируем на примере, что «немедленная» агрегация (по аналогии с SUM(sales[amount])) не работает для AVERAGE(sales[amount]).
Агрегируем и считаем среднее по sales[class], sales[color] (как мы раньше пробовали с SUM), вначале агрегируем по sales[color]:
class |
color |
AVERAGE(amount) |
deluxe |
red |
1 |
economy |
red |
2 |
economy |
black |
3.5 |
Видим, что для economy и black среднее равно (3+4)/2 = 3.5.
Далее агрегируем со средним значением ещё раз, но теперь по sales[class], и видим, что результат неверный, т.к. для economy (2+3.5/2) = 2.75 вместо 3.
class |
AVERAGE(amount) |
deluxe |
1 |
economy |
2.75 |
Таким образом, для SUM всё корректно, т.к. это «кумулятивная мера», и состояние сохранять дополнительно не нужно, сумма и есть состояние, но для AVERAGE — все сложнее, и SUMMARIZE «виртуальный», то есть по сути сохраняется состояние для всех записей таблицы, чтобы можно было агрегировать в нужный момент по нужным полям.
Способ вычисления, который подходил для SUM, не подходит для AVERAGE, т.к. нужно хранить состояние.
Таким образом, немедленная агрегация («интуитивно понятная») в месте SUMMARIZE с использованием обычной агрегации (без сохранения состояния, всех записей) хоть и может выглядеть разумной, но является некорректным подходом и дает некорректный результат.
И в общем и целом, такие агрегации нежелательны (дважды расчет среднего, «среднее от среднего»), и это может быть одной из причин, что SUMMARIZE в Power BI работает именно так, с состояниями, и не искажает результат «средними от средних».
Сложно сказать, какая реализация SUMMARIZE используется в Power BI — возможно, через флаги колонок (и агрегация в нужный момент) или через механизмы вида State Merge.
К слову, такой механизм сохранения состояния в чистом виде, доступный для разработчика, есть только в ClickHouse в комбинаторах State Merge, в других СУБД в лучшем случае может быть реализация сохранения состояния внутри движка.
Для ClickHouse можно написать SQL, соответсвующий рассмотренному DAX c AVERAGE, с использованием комбинаторов State и Merge:
CREATE OR REPLACE TABLE sales
(
class String,
color String,
amount Int32
)
ENGINE = MergeTree
ORDER BY (class, color);
INSERT INTO sales (class, color, amount)
VALUES ('deluxe', 'red', 1),
('economy', 'red', 2),
('economy', 'black', 3),
('economy', 'black', 4);
SELECT class,
avgMerge(avg_state) AS average
FROM (
SELECT class,
color,
avgState(amount) AS avg_state
FROM sales
GROUP BY class, color
)
GROUP BY class;
Такой запрос возвращает корректный результат, совпадающий с Power BI:
class |
average |
deluxe |
1 |
economy |
3 |
В других СУБД нет возможности писать запросы с колонками состояний (avgState), объединять их (avgMerge) и использовать эти колонки в запросах.
Получается, механизм сохранения состояния работает не во всех СУБД, как же ведет себя Power BI в рассмотренных DAX запросах для SUM и AVERAGE? Поведение Power BI не является открытым, но из анализа SQL в DAX Studio видно, что Power BI игнорирует SUMMARIZE, анализирует структуру запроса, колонки, возможно флаги, и т.д. и не делает агрегацию из SUMMARIZE как для AVERAGE, так и для SUM.


Как видно из псевдо-SQL в DAX Studio, в обоих случаях агрегации по sales[color] нет, и есть только агрегация по sales[class].
Надеюсь, такая привязка SUMMARIZE к реалиям СУБД может быть интересна, успешных запросов и дашбордов :)