Оптимизация DAX в Visiology: практические советы

Оптимизация DAX в Visiology: практические советы

Введение

Данная статья описывает ключевые принципы оптимизации DAX-мер на платформе Visiology на примере модели данных Contoso. Основная цель – повысить производительность выражений, сократить избыточные итерации и использовать возможности движка DAX максимально эффективно.

Модель Contoso включает следующие таблицы:

  • Sales – данные о продажах;

  • Products – данные о товарах;

  • Returns – данные о возвратах;

  • ReturnReasons – причины возвратов;

  • Date – календарь.

Использование CALCULATE вместо построчной итерации

Построчная обработка данных, которая используется в функциях агрегации (например, SUMX) или при использовании функции FILTER, создаёт высокую нагрузку при работе с крупными таблицами. Для оптимизации производительности подобные вычисления часто можно заменить более эффективной контекстной агрегацией с помощью функции CALCULATE, которая работает с целыми столбцами данных.

До оптимизации

SUMX( FILTER( Sales, RELATED(Products[IsActive]) = 1 ), Sales[SalesAmount] )

После оптимизации

CALCULATE( SUM(Sales[SalesAmount]), FILTER( Products, Products[IsActive] = 1 ) )

Почему это быстрее:

  • Оптимизация убирает избыточные итерации: вместо того, чтобы вычислять сумму по каждой строке, SUM сразу агрегирует значения в уже установленном контексте фильтра.
    Это уменьшает количество операций и ускоряет выполнение запроса.

  • CALCULATE не проходит по каждой строке таблицы, а просто пересчитывает выражение с учётом нужного фильтра, поэтому работает быстрее, чем построчная итерация SUMX.

Прямая фильтрация справочников без применения RELATED

Функция RELATED нужна, чтобы получить значение из связанной таблицы – чаще всего из справочника, когда выполняются расчёты в таблице фактов. Она подставляет нужное поле (например, категорию, флаг или коэффициент) из справочника для каждой строки факта, используя существующие связи модели. Однако RELATED не следует использовать для фильтрации или соединения таблиц, так как это приводит к избыточным вычислениям и снижает производительность.

До оптимизации

CALCULATE( SUM(Sales[SalesAmount]), FILTER( Sales, RELATED(Products[Category]) = "Bikes" ) )

После оптимизации

CALCULATE( SUM(Sales[SalesAmount]), FILTER( Products, Products[Category] = "Bikes" ) )

Почему это правильно:

  • Фильтр накладывается на справочник, а он гораздо меньше таблицы фактов.

  • Этот фильтр автоматически распространяется на Sales через связи модели.

  • Не происходит лишних обращений к справочнику, поэтому расчёт выполняется быстрее.

Когда RELATED всё-таки нужен

RELATED остаётся полезным инструментом для подстановки значения из справочника при строчных расчётах – например, при применении коэффициента или флага внутри SUMX:

Sales With Discounts = SUMX( Sales, Sales[SalesAmount] * IF ( RELATED ( Products[IsChildCategory] ) = 1, 0.9, 1 ) )

RELATED в этом случае просто подставляет значение поля IsChildCategory из справочника для каждой строки продаж. Это нормальный и быстрый вариант использования: мы не пытаемся фильтровать таблицу фактов через RELATED, а лишь берём нужный атрибут из связанной таблицы внутри расчёта.

Перенос логических условий внутрь функции при едином фильтр-контексте

Если меры отличаются только логикой (IF, SWITCH) при одинаковом фильтр-контексте,
нужно выносить условия внутрь тела итератора. Такой подход позволяет повторно использовать фильтр для всех выражений, а не пересчитывать его каждый раз.

До оптимизации

Общий доход (Contoso) = CALCULATE( SUM(Sales[SalesAmount]) * 1.1, FILTER( Sales, Sales[BrandName] = "Contoso" Sales[ProductCategory] <> "Accessories" && Sales[StartDate] <= MAX( 'Date'[Date] ) && Sales[EndDate] > MAX( 'Date'[Date] ) ) ) Общий доход = CALCULATE( SUM(Sales[SalesAmount]), FILTER( Sales, Sales[ProductCategory] <> "Accessories" && Sales[StartDate] <= MAX( 'Date'[Date] ) && Sales[EndDate] > MAX( 'Date'[Date] ) ) )

После оптимизации

Общий доход= CALCULATE( SUMX( Sales, IF( Sales[BrandName] = "Contoso", Sales[SalesAmount] * 1.1, Sales[SalesAmount] ) ), FILTER( Sales, Sales[ProductCategory] <> "Accessories" && Sales[StartDate] <= MAX( 'Date'[Date] ) && Sales[EndDate] > MAX( 'Date'[Date] ) ) )

Почему это быстрее:

  • Фильтр применяется один раз, а не заново для каждой отдельной меры.

Применение SUMMARIZE для предварительной агрегации данных

При расчётах типа “сколько магазинов превысили порог продаж” не нужно фильтровать факт напрямую. Лучше сначала агрегировать данные по нужному измерению и работать с готовыми суммами.

До оптимизации

Active Stores = CALCULATE( DISTINCTCOUNT(Sales[StoreId]), FILTER( Sales, SUM(Sales[SalesAmount]) > 5000000 ) )

После оптимизации

Active Stores = SUMX( SUMMARIZE( Sales, Sales[StoreId] ), IF( CALCULATE( SUM( Sales[SalesAmount] ) ) > 5000000, 1, 0 ) )

Почему это быстрее:

  • SUMMARIZE создаёт промежуточную таблицу, где каждая строка соответствует одному магазину и содержит уже рассчитанную сумму продаж.

  • Такая схема снижает объём вычислений и нагрузку, поскольку фильтрация выполняется по уже сгруппированным результатам, а не по исходной таблице.


Смотрите также

Поддерживаемые функции DAX