Все материалы
Бизнесгайдсредний

Формулы DAX: меры, CALCULATE и контекст фильтра на примерах

Меры и столбцы DAX на одном датасете: SUM, DISTINCTCOUNT, CALCULATE, REMOVEFILTERS, DIVIDE, даты и VAR. У каждой формулы — SQL-проверка.

КейсПрактика27 сентября 2026 г.16 мин

На карточке Power BI сумма броней верна, а после выбора группы доля вдруг превышает 100%. Это типичная задача DAX: понять, какие строки сейчас видит мера и какие фильтры снимает знаменатель. Ниже разберём формулы на одной учебной неделе «Авиаперевозок». Для каждой покажем вопрос, запись DAX, ожидаемое число и SQL-запрос, который даёт то же число по тем же строкам: если ваша мера показывает другое, расхождение сразу видно. Синтаксис сверен со справочником Microsoft на 27 сентября 2026 года.

Коротко: как читать формулу DAX

Мера возвращает число для текущей выборки. Когда читатель выбирает дату или группу брони, Power BI пересчитывает её в новом контексте фильтра. Вычисляемый столбец создаёт значение для каждой строки и обычно меняется при обновлении данных, а не от щелчка по срезу. CALCULATE меняет контекст меры, REMOVEFILTERS убирает выбранный фильтр, DIVIDE аккуратно делит суммы.

Учебный факт Bookings содержит одну строку на book_ref, дату book_day, сумму total_amount, число билетов ticket_count и группу ticket_bucket («1» или «2+»). У таблицы Calendar одна строка на день и связь один-ко-многим с Bookings[book_day]; у TicketBucket — две уникальные группы и связь с Bookings[ticket_bucket]. Этот контракт модели нужен, чтобы следующие формулы имели тот же смысл, что их SQL-проверки.

Без фильтра по группе и при выборе полной недели 3–9 августа 2026 года должно быть 42 561 бронь на 3 377 874 800 ₽. Предыдущая полная неделя — 39 216 на 3 116 503 800 ₽. Для практики сначала посмотрите сборку модели Power BI, а потом возвращайтесь к мерам: DAX не исправит строку факта, размноженную неверным JOIN.

Какой расчёт где живёт
РезультатМестоКогда меняется
Группа 1 или 2+ для каждой брониСтолбецПри обновлении модели
Сумма и число бронейМераПри изменении фильтра
Доля группы в общей сумме периодаМера с изменённым контекстомПри изменении даты или группы

Контекст строки: вычисляемый столбец для группы

Задача: каждой брони назначить подпись по числу билетов. Вычисляемый столбец обрабатывает текущую строку Bookings; IF сравнивает ticket_count с единицей. В нашем подготовленном факте он даёт 28 011 строк с меткой «1» и 14 550 с меткой «2+». Выбор даты на отчёте не переписывает метку исходной строки, а лишь отбирает строки, которые попадут на экран.

SQL делает то же через CASE WHEN после того, как число билетов сгруппировано по book_ref. Нельзя написать этот столбец напрямую по детальной таблице tickets и затем без проверки суммировать стоимость брони: один book_ref там может встретиться несколько раз. Формула корректна только на зерне факта, указанном выше.

Если группа уже приходит из проверенной витрины источника, повторять её в DAX не обязательно. Вычисление до загрузки упрощает повторное использование в других инструментах. Если бизнес меняет правило группировки, укажите владельца определения и пересчитайте прошлые периоды, иначе две версии отчёта начнут расходиться.

DAX: вычисляемый столбец, одна бронь
Ticket Segment = IF(Bookings[ticket_count] = 1, "1", "2+")

SQL: CASE WHEN ticket_count=1 THEN 1 ELSE 2+ END после агрегации tickets по book_ref. Результат: 28 011 и 14 550 броней.

Контекст фильтра: SUM видит выбранный период

Задача: показать стоимость оформленных броней. Мера Amount суммирует total_amount только у строк, оставшихся после фильтров календаря и группы. На всей неделе она возвращает 3 377 874 800 ₽. На 3 августа — 452 158 300 ₽. На группе «2+» за полную неделю — 1 791 746 500 ₽. Это три результата одной формулы, а не три отдельно сохранённые суммы.

Эквивалент SQL — SUM(total_amount) с разными WHERE. Чтобы сопоставить их, важно применять одни и те же границы даты: от 3 августа включительно до 10 августа исключительно, московское время. Если у календаря другой часовой пояс, фильтр может отобрать не те строки на границе суток. Сначала сверьте детали дня, а уже потом объявляйте ошибку в DAX.

Не называйте меру Revenue: база фиксирует оформленные бронирования, а не факт оплаты или сумму после возвратов. Имя меры и подпись карточки влияют на решение пользователя не меньше, чем арифметика. Лучше длинное точное определение в описании модели, чем короткое неверное название на экране.

DAX: сумма в текущем контексте
Amount = SUM(Bookings[total_amount])

SQL: SUM(total_amount) за 03–09.08.2026 = 3 377 874 800 ₽; за 03.08 = 452 158 300 ₽.

SQL-эквивалент меры на полной неделе
SELECT SUM(total_amount) AS amount
FROM bookings
WHERE book_date >= TIMESTAMP '2026-08-03'
  AND book_date < TIMESTAMP '2026-08-10'

COUNTROWS и DISTINCTCOUNT: два разных контроля

Задача: посчитать брони. На подготовленном факте COUNTROWS(Bookings) и DISTINCTCOUNT(Bookings[book_ref]) оба дают 42 561 за неделю. Первое число — сколько строк осталось в фильтре; второе — сколько уникальных ключей. Их равенство здесь служит проверкой обещанного зерна. Если результаты разошлись, не выбирайте меньший автоматически: выясните, какие строки дублируются и почему.

SQL-эквиваленты — COUNT(*) и COUNT(DISTINCT book_ref) на той же неделе. Оба запроса выполнены и вернули 42 561. В исходной таблице tickets COUNTROWS ответил бы на другой вопрос — сколько билетов, а не броней. Имя таблицы внутри формулы не заменяет явно записанное зерно данных.

В визуале COUNTROWS может стать быстрее и понятнее, если модель уже гарантирует один ряд на бронь. DISTINCTCOUNT полезен как контроль и там, где реально нужно уникальное число. Но не маскируйте им случайно размноженную сумму: DISTINCTCOUNT(book_ref) может оставаться правильным, пока SUM(total_amount) уже завышен.

Два SQL-контроля одного факта
DAX-мераSQL-эквивалентРезультат
Booking Rows = COUNTROWS(Bookings)COUNT(*)42 561
Unique Bookings = DISTINCTCOUNT(Bookings[book_ref])COUNT(DISTINCT book_ref)42 561

CALCULATE: задать группу внутри меры

Задача: вывести в отдельной карточке стоимость броней с двумя или более билетами. CALCULATE оценивает базовую меру Amount после изменения фильтра группы на «2+». На полной неделе результат 1 791 746 500 ₽; SQL с условием ticket_count > 1 после безопасного JOIN даёт то же значение. Если дата на странице станет 3 августа, мера должна считать только этот день и выбранную группу.

Пишем условие на колонке измерения TicketBucket[bucket], потому что этой же колонкой пользуется срез. Когда выбран другой элемент того же поля, обычный фильтр в CALCULATE его заменяет; если требуется пересечение, это уже другая логика с KEEPFILTERS. Не добавляйте модификаторы вслепую: сначала сформулируйте, должна ли карточка показывать фиксированную группу или подчиняться выбору пользователя.

В нашей модели измерение имеет ровно две строки и уникальный ключ. Если на экране появляется пустое значение после выбора группы, проверьте активность связи и совпадение написания «2+» в измерении и факте. SQL-таблица контроля разделов даёт 14 550 броней и 1 791 746 500 ₽ для этой группы.

DAX: фиксированная группа в мере
Multi Amount = CALCULATE([Amount], TicketBucket[bucket] = "2+")

SQL: SUM(total_amount) WHERE ticket_count > 1 за неделю = 1 791 746 500 ₽.

REMOVEFILTERS: знаменатель без выбора группы

Задача: в таблице по двум группам вывести общую стоимость недели в знаменателе. Если использовать просто [Amount], на строке «2+» будет 1 791 746 500 ₽; деление суммы на себя даст бессмысленные 100%. CALCULATE с REMOVEFILTERS(TicketBucket[bucket]) снимает только фильтр группы и сохраняет фильтр даты. В обеих строках знаменатель станет 3 377 874 800 ₽.

SQL-эквивалент берёт сумму по всему периоду, не добавляя WHERE ticket_bucket=...; отдельный выполненный запрос с оконной суммой подтвердил для обеих групп одинаковый знаменатель. Это не «сумма за все годы»: календарный фильтр остаётся. При выборе другого периода знаменатель обязан измениться вместе с ним.

Не пишите REMOVEFILTERS() без аргумента, если хотите убрать только группу: так вы можете снять и дату, и другие фильтры, после чего доля станет не долей выбранной недели. Перед публикацией проверьте один день и одну группу; доля должна по-прежнему делиться на итог того же дня.

DAX: итог без фильтра группы
All Group Amount = CALCULATE([Amount], REMOVEFILTERS(TicketBucket[bucket]))

SQL: SUM(total_amount) по обеим группам за 03–09.08 = 3 377 874 800 ₽; фильтр даты сохранён.

ALL: когда нужен тот же снятый фильтр

Для той же задачи возможна запись с ALL(TicketBucket[bucket]). В качестве модификатора фильтра внутри CALCULATE она также убирает выбор конкретной группы, и знаменатель недели снова равен 3 377 874 800 ₽. Microsoft рекомендует REMOVEFILTERS, если цель именно убрать фильтр; ALL ещё может возвращать таблицу, поэтому его смысл шире.

SQL здесь тот же контроль общей недельной суммы без условия по группе. Мы не будем выдавать два одинаковых результата за независимые бизнес-метрики: это две DAX-записи одного правила. Если нужен не общий период, а все даты за весь срок, придётся снять фильтр календаря отдельно — и объяснить читателю, почему знаменатель не движется со срезом.

В существующих моделях ALL встречается часто. При чтении чужой меры задайте два вопроса: с каких колонок она убирает фильтры и какие оставляет? Ответ на второй важнее названия функции. В учебной модели условие касается только TicketBucket[bucket]; значит выбранная дата продолжает работать.

DAX: альтернативный модификатор группы
All Group Amount (ALL) = CALCULATE([Amount], ALL(TicketBucket[bucket]))

SQL без WHERE по группе: 3 377 874 800 ₽ на выбранной неделе.

DIVIDE: доля группы с безопасным знаменателем

Задача: показать, какая часть стоимости приходится на «2+». Числитель [Amount] в строке группы равен 1 791 746 500 ₽, знаменатель [All Group Amount] — 3 377 874 800 ₽. DIVIDE возвращает 53,04% после форматирования как процента. Для группы «1» результат 46,96%. Выполненный SQL с SUM(amount) OVER () и делением дал обе доли.

Для нулевого знаменателя DIVIDE по умолчанию возвращает пустое значение, что лучше неосмысленной бесконечности. Но пустота тоже должна иметь объяснение: новый фильтр мог выбрать день без броней либо сломать связь. Не заменяйте её нулём до понимания причины. Отчёт должен показывать дату и состояние фильтра рядом с долей.

Если вы хотите долю от общего срока наблюдения, формула будет иной: придётся явно убрать календарный фильтр. Здесь вопрос о составе выбранной недели, поэтому фильтр даты сохраняется. Это решение по смыслу, а не по синтаксису DAX.

DAX: доля текущей группы в выбранном периоде
Group Share = DIVIDE([Amount], [All Group Amount])

SQL: 1 791 746 500 / 3 377 874 800 = 53,04% для 2+; 46,96% для 1.

Проверка долей за полную неделю
ГруппаСумма, ₽Общий знаменатель, ₽Доля
11 586 128 3003 377 874 80046,96%
2+1 791 746 5003 377 874 80053,04%

Календарь: условие для расчётов по времени

Функции времени принимают даты из календарной таблицы, связанной с фактом. Подготовьте Calendar с непрерывными календарными днями хотя бы с 1 января 2025-го по 31 августа 2026-го, уникальным значением Date и активной связью с Bookings[book_day]. В нашем источнике фактические брони есть только с 17 июня по 11 августа 2026 года; календарь шире источника специально, чтобы прошлогодний период существовал как набор дат.

Microsoft объясняет настройку таблицы дат. Если сделать календарь только из дней, в которые были брони, пропуски исчезнут и часть временных функций станет труднее интерпретировать. Если на факте book_day хранится как timestamp, приведите его к календарной дате до создания связи; не связывайте день с полным временем до секунды.

Для всех следующих чисел в отчёте выбран интервал 3–9 августа 2026 года. SQL использует полуоткрытые границы на book_date; Power BI с календарём выбирает семь целых дат. Эти два способа эквивалентны лишь при одинаковом часовом поясе и непрерывной таблице дат.

TOTALYTD: накопленная сумма к 9 августа

Задача: на карточке с датой окончания 9 августа показать накопленную стоимость броней с начала календарного года. TOTALYTD([Amount], Calendar[Date]) расширяет период до 1 января – 9 августа 2026 года. Выполненный SQL вернул 250 666 броней на 19 846 736 300 ₽. Источник начинается только 17 июня, поэтому это сумма имеющихся строк с января, а не доказательство, что в январе–мае не было бизнеса.

Не ставьте такую карточку рядом с недельной суммой без подписи периода: оба числа в рублях, но основаны на разных интервалах. На строке каждого дня TOTALYTD будет накопленным значением к этому дню; на карточке выбранной полной недели — к её последней дате. Если финансовый год заканчивается не 31 декабря, нужен явно заданный годовой рубеж и новое определение периода.

Эта функция не устраняет неполноту исходных данных. Наш SQL отбирает book_date >= 2026-01-01 и < 2026-08-10, но минимальная дата таблицы — 2026-06-17. В рабочем отчёте рядом с накопительной метрикой покажите начало полноты источника; иначе сравнением с прошлым годом читатель может объяснить технический пробел бизнес-эффектом.

DAX: сумма с начала года к выбранной дате
Amount YTD = TOTALYTD([Amount], Calendar[Date])

SQL за 01.01–09.08.2026: 19 846 736 300 ₽ по 250 666 имеющимся броням; источник неполон с января.

SQL-эквивалент накопления к 9 августа
SELECT COUNT(*) AS bookings, SUM(total_amount) AS amount
FROM bookings
WHERE book_date >= TIMESTAMP '2026-01-01'
  AND book_date < TIMESTAMP '2026-08-10'

SAMEPERIODLASTYEAR: пустой результат тоже результат

Задача: сравнить выбранную неделю 3–9 августа с теми же календарными датами 2025 года. SAMEPERIODLASTYEAR сдвигает набор дат назад на год, а CALCULATE считает в нём [Amount]. SQL по 3–9 августа 2025-го нашёл ноль строк и вернул NULL для SUM. В Power BI мера ожидаемо пуста; это не ноль продаж и не «минус 100%».

Если показать на экране ноль вместо пустоты, пользователь может решить, что бизнес год назад отсутствовал. На самом деле учебная база начинается 17 июня 2026 года. Правильная подпись — «нет данных за сопоставимый период». Для продакшена сначала проверьте историю источника и полный календарь, потом вводите годовое сравнение.

Документация Microsoft описывает сдвиг дат и особые случаи конца месяца. У нас неделя в августе без такой границы; SQL-эквивалент прямой. Однако для других интервалов особенно важно сверять выбранные даты, а не автоматически заменять функцию выражением год - 1 в текстовом фильтре.

DAX: сопоставимый период прошлого года
Amount LY = CALCULATE([Amount], SAMEPERIODLASTYEAR(Calendar[Date]))

SQL за 03–09.08.2025: 0 строк, SUM = NULL. В отчёте показывайте «нет данных», не 0 ₽.

DATEADD: предыдущая полная неделя

Задача: перенести выбранные семь дат назад на семь дней. DATEADD(Calendar[Date], -7, DAY) превращает 3–9 августа в 27 июля – 2 августа. В предыдущем интервале выполненный SQL нашёл 39 216 броней на 3 116 503 800 ₽. Это правильная база для недельного сравнения при одинаковой длине и закрытости периодов.

Если пользователь выберет только 3 августа, мера покажет 27 июля, а не всю прошлую неделю. Это и есть работа в текущем контексте: DATEADD сдвигает выбранные даты, а не восстанавливает «обычную» неделю за пользователя. Перед выводом процента подпишите оба периода, чтобы не смешать один день и семь дней.

Microsoft Learn описывает требования к непрерывности выбора дат для варианта с колонкой дат. Если в календаре пробелы или пользователь выбрал несмежные даты, проверьте поведение отдельно. В нашем примере выбран цельный интервал из семи дат, поэтому SQL-эквивалент однозначен.

DAX: сдвиг выбранной недели на семь дней
Amount Previous Week = CALCULATE([Amount], DATEADD(Calendar[Date], -7, DAY))

SQL за 27.07–02.08.2026: 3 116 503 800 ₽, 39 216 броней.

SQL-эквивалент предыдущей недели
SELECT COUNT(*) AS bookings, SUM(total_amount) AS amount
FROM bookings
WHERE book_date >= TIMESTAMP '2026-07-27'
  AND book_date < TIMESTAMP '2026-08-03'

VAR: читаемое сравнение без повторения выражений

Задача: вывести относительное изменение стоимости броней к предыдущей неделе. Переменные дают имена двум уже определённым мерам и позволяют написать арифметику один раз. На полной неделе CurrentAmount равен 3 377 874 800 ₽, PreviousAmount — 3 116 503 800 ₽; DIVIDE возвращает 0,0839, а процентное форматирование — +8,39%. Выполненный SQL с двумя недельными суммами дал тот же результат.

Этот процент не доказывает причины роста. Объём броней вырос с 39 216 до 42 561, средняя стоимость почти не изменилась: 79 470,21 ₽ прежде и 79 365,49 ₽ теперь. Для решения о цене и спросе понадобятся сопоставимые недели, разрез рейсов и полнота источника. Формула отвечает только на арифметический вопрос.

Если предыдущая неделя пуста, DIVIDE даст пустое значение. Подпишите такое состояние как отсутствие базы, а не как отсутствие роста. Переменные не меняют контекст сами по себе; его задают меры и DATEADD. Это делает выражение проще для чтения и ревью, но не заменяет проверку выбранного периода.

DAX: изменение к предыдущей неделе
Weekly Growth = VAR CurrentAmount = [Amount] VAR PreviousAmount = [Amount Previous Week] RETURN DIVIDE(CurrentAmount - PreviousAmount, PreviousAmount)

SQL: (3 377 874 800 − 3 116 503 800) / 3 116 503 800 = 8,39%.

Две полные недели для проверки VAR
НеделяБронейСумма, ₽Средняя, ₽
27.07–02.0839 2163 116 503 80079 470,21
03.08–09.0842 5613 377 874 80079 365,49

Пять ошибок, которые ломают правдоподобные меры

Первая — считать столбец со стоимостью на зерне билета и умножить брони. Вторая — делить сумму группы на сумму той же группы и получить 100%. Третья — снять все фильтры ради знаменателя, включая дату. Четвёртая — использовать вычисляемый столбец для показателя, который должен реагировать на срез. Пятая — дать временной функции календарь с пропусками или связать дату с timestamp без нормализации.

Есть и шестая ловушка: SAMEPERIODLASTYEAR возвращает пустоту, которую визуал форматирует как ноль. Так отчет может нарисовать падение год к году, хотя прошлогодней истории нет вообще. Нужен флаг полноты периода и текст «нет данных». В нашей базе отсутствие 2025 года подтверждено запросом, а не предположением из диапазона графика.

При разборе чужого DAX запишите: зерно факта, фильтры страницы, фильтры визуала, что снимает CALCULATE, какое число ждёте от SQL. Это быстрее, чем подбирать функции наугад. Когда ожидание и DAX различаются, сначала проверьте связи модели и выбранные даты, затем синтаксис.

Частые вопросы и следующий шаг

«Почему мера меняется от среза, а столбец нет?» Мера вычисляется для текущего набора строк, столбец хранит результат на каждой строке. «Чем ALL отличается от REMOVEFILTERS?» В роли модификатора фильтра оба могут снять выбор группы; REMOVEFILTERS яснее выражает намерение, ALL ещё возвращает таблицу в других контекстах. «Почему доля группы больше 100%?» Обычно числитель и знаменатель посчитаны на разных выборках.

«Можно ли использовать SAMEPERIODLASTYEAR на неполной истории?» Формула вычислится, но бизнес-сравнение будет некорректным; в нашем наборе прошлогодних строк нет. «Почему DATEADD не дал прошлую полную неделю?» Он сдвигает текущий выбор дат, а не угадывает границы. «Обязательно ли заводить календарь?» Для показанных функций времени нужен корректный календарный контекст и связь с фактом.

Возьмите один разрез своего отчёта и запишите ожидаемый SQL до правки DAX. Если нужно потренировать агрегацию и даты на исходных таблицах, откройте тренажёр SQL с нуля. Затем возвращайтесь к дашборду Power BI и проверяйте две точки: весь период и одну группу.

Продолжить чтение
Вся библиотека