Привет, Хабр! Одной из важных функций-модификаторов в DAX является REMOVEFILTERS
, он позволяет, например, убрать фильтр для расчета знаменателя в доле. Однако логика REMOVEFILTERS
для столбцов может выглядеть неочевидной, например, REMOVEFILTERS
только для одного поля, по которому есть условие в FILTER
, не влияет на результат DAX запроса. Так, REMOVEFILTERS(customer[customer_id])
не влияет на FILTER
в SUMMARIZECOLUMNS
вида FILTER(customer, customer[customer_id] > 2)
и для сброса фильтра нужен REMOVEFILTERS(customer)
по всей таблице. В связи с этим удобно представить принципы работы REMOVEFILTERS
более формально, например, в виде ER диаграммы с подписанными связями. Для построения ER диаграммы был выбран Mermaid и генерация кода диаграммы реализована на C#. Интересующимся особенностями REMOVEFILTERS
— добро пожаловать под кат :)
Рассмотрим простую схему данных с одной таблицей фактов sales
и двумя справочниками — клиентов customer
и продуктов product
:

В таблице фактов sales
есть 5 строк:

Рассмотрим простейший DAX запрос без фильтра, здесь CALCULATE
не нужен, он добавлен только для иллюстрации будущего места применения REMOVEFILTERS
, в результатах запроса видны данные по 3 продуктам:

Добавим фильтр по customer[customer_id]
, и останутся данные только для третьего продукта:

Теперь рассмотрим применение REMOVEFILTERS
. Для иллюстрации рассмотрим вначале REMOVEFILTERS
для полей, которые не меняют результат запроса и «стреляют мимо».
REMOVEFILTERS
по product[product_id]
не изменяет результат DAX запроса:

REMOVEFILTERS
по customer[customer_id]
не изменяет результат:

REMOVEFILTERS
по customer[customer_name]
также не изменяет результат:

Видно, что сброс фильтра в Power BI через REMOVEFILTERS
по полям может быть не так очевиден. Рассмотрим теперь REMOVEFILTERS
по тем полям, которые изменяют результаты DAX запроса.
REMOVEFILTERS
по колонке группировкиproduct[product_name]
изменяет результат запроса — теперь связь таблицы продаж sales
с таблицей продуктов product
теряется и в SUMMARIZECOLUMNS
получаем декартово произведение всех продуктов и продаж, причем фильтр по customer
сохраняется:

REMOVEFILTERS
по всем столбцам customer
(или просто REMOVEFILTERS(customer)
) изменяет результат запроса — снимается фильтр по customer
:

С учетом того, что есть удобные средства представления ER схем вроде Mermaid, удобно рассмотреть REMOVEFILTERS
на основе прототипа работы со схемой данных на C#, который также и генерирует ER диаграмму на Mermaid. Представим таблицы в виде словаря с колонками tables
, для упрощения не будем пользоваться ID таблиц и колонок и будем называть колонки полным именем с учетом имени таблицы. Связи между таблицами в направлении*:1
представим в виде словаря manyToOne
с Tuple-ключом (string table, string column)
— имя таблицы и имя колонки из таблицы many
и таким же (string table, string column)
хранимым значением — имя таблицы и имя колонки из таблицы one
.
Также для корректной ER диаграммы в Mermaid нужен тип данных, поэтому используется словарь typeByColumnName
с типами данных по имени колонки для всех таблиц.
var tables = new Dictionary<string, HashSet<string>>
{
{ "sales", ["sales.order_number", "sales.customer_id", "sales.product_id", "sales.amount"] },
{ "customer", ["customer.customer_id", "customer.customer_name"] },
{ "product", ["product.product_id", "product.product_name", "product.product_color"] }
};
var manyToOne = new Dictionary<(string table, string column), (string table, string column)>
{
{("sales", "sales.customer_id"), ("customer", "customer.customer_id")},
{("sales", "sales.product_id"), ("product", "product.product_id")}
};
var typeByColumnName = new Dictionary<string, string>
{
{ "sales.order_number", "INTEGER" },
{ "sales.customer_id", "INTEGER" },
{ "sales.product_id", "INTEGER" },
{ "sales.amount", "DOUBLE" },
{ "customer.customer_id", "INTEGER" },
{ "customer.customer_name", "STRING" },
{ "product.product_id", "INTEGER" },
{ "product.product_name", "STRING" },
{ "product.product_color", "STRING" }
};
Далее при помощи метода ToMermaidErDiagram
можно сгенерировать ER диаграмму, и это будет общая диаграмма, без учета конкретного контекста CALCULATE
в рамках DAX запроса:
public static string ToMermaidErDiagram(
Dictionary<string, HashSet<string>> tables,
Dictionary<(string table, string column), (string table, string column)> manyToOne,
Dictionary<string, string> typeByColumnName)
{
var sb = new StringBuilder();
sb.AppendLine("erDiagram");
foreach (var (tableName, columns) in tables)
{
sb.AppendLine($" {tableName} {{");
foreach (var column in columns)
{
sb.AppendLine($" {typeByColumnName.GetValueOrDefault(column, "STRING")} {column.Replace(".", "_")}");
}
sb.AppendLine(" }");
}
foreach (var (many, one) in manyToOne)
{
sb.AppendLine($" {one.table} ||--o{{ {many.table} : \"{many.table}-{one.table}\"");
}
return sb.ToString();
}
Само получение Mermaid ER диаграммы с помощью этого метода выглядит следующим образом:
var erDiagram = ToMermaidErDiagram(tables, manyToOne, typeByColumnName);
На mermaid.live сгенерированная диаграмма выглядит так:

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

К примеру, такая расстановка полей группировки и агрегации SUM
соответствует следующему DAX запросу:
EVALUATE
SUMMARIZECOLUMNS(
product[product_name],
"result", CALCULATE(SUM(sales[amount])
)
)
В качестве иллюстрации, в рамках прототипа легко также можно и объединить таблицы (но DAX так делает не всегда, например, логика SUMMARIZECOLUMNS
не всегда подразумевает именно такое объединение таблиц):
var joinedTables = new Dictionary<string, (List<string> columns, List<string> tables)>();
foreach (var (manyToOneTable, oneTable) in manyToOne)
{
if (!joinedTables.TryGetValue(manyToOneTable.table, out var joinedTable))
{
joinedTable = new(tables[manyToOneTable.table].ToList(), [manyToOneTable.table]);
joinedTables.Add(manyToOneTable.table, joinedTable);
}
foreach (var column in tables[oneTable.table])
{
joinedTable.columns.Add(column);
}
joinedTable.tables.Add(oneTable.table);
}
var erDiagram = ToMermaidErDiagram(joinedTables.Select(t => (t.Key, t.Value.columns))
.ToDictionary(x => x.Key, x => x.columns.ToHashSet()), [], typeByColumnName);
Тогда ER диаграмма будет:

Видно, что построение диаграммы упрощается благодаря полному наименованию столбцов с указанием имени таблицы, и становится достаточно заменить точку на знак _
, т.е. например, заменить sales.customer_id
на sales_customer_id
. Видно, что по умолчанию получаем таблицу sales
со справочниками в соответствии со связями many-to-one
.
Теперь можно вернуться к поведению REMOVEFILTERS
. На основе рассмотренных кейсов видны две особенности REMOVEFILTERS
в DAX Power BI в базовых кейсах:
при фильтрации через группировку по полю (например, в
SUMMARIZECOLUMNS
) использованиеREMOVEFILTERS
по этому полю удаляет группировку и связь с этой таблицей (REMOVEFILTERS(product[product_name]
), также при отсутствии связейSUMMARIZECOLUMNS
для двух таблиц генерирует декартово произведение;при фильтрации через
FILTER
по таблице удаление одного поля не помогает, для отмены фильтрации нужно сделатьREMOVEFILTERS
по всем полям таблицы.
Таким образом, можно представить эту логику так, что в DAX есть механизм объединения таблиц в соответствии со сгруппированными полями, агрегациями, связями и т.д., и REMOVEFILTERS
удаляет метаданные (связи), и из-за этого меняются результаты. Благодаря прототипу на C# и сгенерированной Mermaid диаграмме можно проиллюстрировать все формально, для этого можно взять сгенерированную ER диаграмму для трех таблиц и актуализировать названия связей:
erDiagram
sales {
INTEGER sales_order_number
INTEGER sales_customer_id
INTEGER sales_product_id
DOUBLE sales_amount
}
customer {
INTEGER customer_customer_id
STRING customer_customer_name
}
product {
INTEGER product_product_id
STRING product_product_name
STRING product_product_color
}
customer ||--o{ sales : "фильтрация по таблице customer в FILTER, поля customer[customer_id] и customer[customer_name]"
product ||--o{ sales : "группировка по product[product_name]"
Сама ER диаграмма с подписанными назначениями связей (использованные поля), которая актуальна для REMOVEFILTERS
, выглядит следующим образом:

Такая схема в чем-то может быть формальным представлением контекста CALCULATE
, которое может использоваться для REMOVEFILTERS
. Эта схема для конкретного контекста, для «конкретной точки» DAX, с учетом группировки. Например, из такой схемы становится наглядно, почему при REMOVEFILTERS(customer[customer_id], customer[customer_name])
(или просто REMOVEFILTERS(customer)
) меняются результаты и отменяется фильтр по customer
в таком DAX:
EVALUATE
SUMMARIZECOLUMNS(
product[product_name],
FILTER(
customer,
customer[customer_id] > 2
),
"result", CALCULATE(
SUM(sales[amount]),
REMOVEFILTERS(
customer[customer_id],
customer[customer_name]
)
)
)

В этом случае фильтр по customer
в связи между sales
и customer
отменяется, но сама связь между таблицами остается, в подписи для связи между sales
и customer
уже нет фильтра, есть только sales-customer
:

При REMOVEFILTERS
по полю группировкиREMOVEFILTERS(product[product_name])
связь удаляется полностью и SUMMARIZECOLUMNS
делает декартово произведение product
на sales
с отфильтрованными результатами по customer[customer_id]
:

Эта схема данных соответствует контексту CALCULATE
в DAX:
EVALUATE
SUMMARIZECOLUMNS(
product[product_name],
FILTER(
customer,
customer[customer_id] > 2
),
"result", CALCULATE(
SUM(sales[amount]),
REMOVEFILTERS(
product[product_name]
)
)
)

Соответственно, другие REMOVEFILTERS
(например, REMOVEFILTERS(customer[customer_id])
) «бьют мимо» и не влияют на результат, т.е. связи могут быть удалены только при REMOVEFILTERS
по всем колонкам, указанным в описании связи.
Такого вида формализация и ER диаграмма с описанием связей упрощает понимание REMOVEFILTERS
, т.к. исходя из неочевидных принципов его работы он может работать и «мимо».
Надеюсь, описанные особенности REMOVEFILTERS
в Power BI могут быть интересны, успехов в мерах и дашбордах :)