Подробное руководство по DAX. Бизнес-аналитика с Microsoft Power BI, SQL Server Analysis Services и Excel 9785970608593, 9781509306978

Расширенная и дополненная с учетом современных требований и техник, эта книга представляет собой наиболее полное руковод

240 111 35MB

Russian Pages 776 Year 2021

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Подробное руководство по DAX. Бизнес-аналитика с Microsoft Power BI, SQL Server Analysis Services и Excel
 9785970608593, 9781509306978

Table of contents :
Содержание
Об авторах
Рецензия
От команды разработчиков
Благодарности
От издательства
Предисловие ко второму изданию
Предисловие к первому изданию
Для кого предназначена эта книга?
Как мы представляем себе нашего читателя?
Структура книги
Условные обозначения
Сопутствующий контент
Глава 1. Что такое DAX?
Введение в модель данных
Введение в направление связи
DAX для пользователей Excel
Ячейки против таблиц
Excel и DAX: два функциональных языка
Итерационные функции в DAX
DAX требует изучения теории
DAX для разработчиков SQL
Работа со связями
DAX как функциональный язык
DAX как язык программирования и язык запросов
Подзапросы и условия в DAX и SQL
DAX для разработчиков MDX
Многомерность против табличности
DAX как язык программирования и язык запросов
Иерархии
Вычисления на конечном уровне
DAX для пользователей Power BI
Глава 2. Знакомство с DAX
Введение в вычисления DAX
Типы данных DAX
Операторы DAX
Конструкторы таблиц
Условные операторы
Введение в вычисляемые столбцы и меры
Вычисляемые столбцы
Меры
Введение в переменные
Обработка ошибок в выражениях DAX
Ошибки преобразования
Ошибки арифметических операций
Перехват ошибок
Генерирование ошибок
Форматирование кода на DAX
Введение в агрегаторы и итераторы
Использование распространенных функций DAX
Функции агрегирования
Логические функции
Информационные функции
Математические функции
Тригонометрические функции
Текстовые функции
Функции преобразования
Функции для работы с датой и временем
Функции отношений
Заключение
Глава 3. Использование основных табличных функций
Введение в табличные функции
Введение в синтаксис EVALUATE
Введение в функцию FILTER
Введение в функции ALL и ALLEXCEPT
Введение в функции VALUES, DISTINCT и пустые строки
Использование таблиц в качестве скалярных значений
Введение в функцию ALLSELECTED
Заключение
Глава 4. Введение в контексты вычисления
Введение в контексты вычисления
Знакомство с контекстом фильтра
Знакомство с контекстом строки
Тест на понимание контекстов вычисления
Использование функции SUM в вычисляемых столбцах
Использование ссылок на столбцы в мерах
Использование контекста строки с итераторами
Вложенные контексты строки в разных таблицах
Вложенные контексты строки в одной таблице
Использование функции EARLIER
Функции FILTER, ALL и взаимодействие между контекстами
Работа с несколькими таблицами
Контексты строки и связи
Контекст фильтра и связи
Использование функций DISTINCT и SUMMARIZE в контекстах фильтра
Заключение
Глава 5. Функции CALCULATE и CALCULATETABLE
Введение в функции CALCULATE и CALCULATETABLE
Создание контекста фильтра
Знакомство с функцией CALCULATE
Использование функции CALCULATE для расчета процентов
Введение в функцию KEEPFILTERS
Фильтрация по одному столбцу
Фильтрация по сложным условиям
Порядок вычислений в функции CALCULATE
Преобразование контекста
Повторение темы контекста строки и контекста фильтра
Введение в преобразование контекста
Преобразование контекста в вычисляемых столбцах
Преобразование контекста в мерах
Циклические зависимости
Модификаторы функции CALCULATE
Модификатор USERELATIONSHIP
Модификатор CROSSFILTER
Модификатор KEEPFILTERS
Использование модификатора ALL в функции CALCULATE
Использование ALL и ALLSELECTED без параметров
Правила вычисления в функции CALCULATE
Глава 6. Переменные
Введение в синтаксис переменных VAR
Переменные – это константы
Области видимости переменных
Использование табличных переменных
Отложенное вычисление переменных
Распространенные шаблоны использования переменных
Заключение
Глава 7. Работа с итераторами и функцией CALCULATE
Использование итерационных функций
Кратность итератора
Использование преобразования контекста в итераторах
Использование функции CONCATENATEX
Итераторы, возвращающие таблицы
Решение распространенных сценариев при помощи итераторов
Расчет среднего и скользящего среднего
Использование функции RANKX
Изменение гранулярности вычисления
Заключение
Глава 8. Логика операций со временем
Введение в логику операций со временем
Автоматические дата и время в Power BI
Автоматические столбцы с датами в Power Pivot для Excel
Шаблон таблицы дат в Power Pivot для Excel
Создание таблицы дат
Использование функций CALENDAR и CALENDARAUTO
Работа со множественными датами
Поддержка множественных связей с таблицей дат
Поддержка нескольких таблиц дат
Знакомство с базовыми вычислениями в работе со временем
Пометка календарей как таблиц дат
Знакомство с базовыми функциями логики операций со временем
Нарастающие итоги с начала года, квартала, месяца
Сравнение временных интервалов
Сочетание функций логики операций со временем
Расчет разницы по сравнению с предыдущим периодом
Расчет скользящей годовой суммы
Выбор порядка вложенности функций логики операций со временем
Знакомство с полуаддитивными вычислениями
Использование функций LASTDATE и LASTNONBLANK
Работа с остатками на начало и конец периода
Усовершенствованные методы работы с датой и временем
Вычисления нарастающим итогом
Функция DATEADD
Функции FIRSTDATE, LASTDATE, FIRSTNONBLANK и LASTNONBLANK
Использование детализации с функциями логики операций со временем
Работа с пользовательскими календарями
Работа с неделями
Пользовательские вычисления нарастающим итогом
Заключение
Глава 9. Группы вычислений
Знакомство с группами вычислений
Создание групп вычислений
Знакомство с группами вычислений
Применение элемента вычисления
Очередность применения групп вычислений
Включение и исключение мер из элементов вычисления
Косвенная рекурсия
Два основных правила
Заключение
Глава 10. Работа с контекстом фильтра
Использование функций HASONEVALUE и SELECTEDVALUE
Использование функций ISFILTERED и ISCROSSFILTERED
Понимание разницы между функциями VALUES и FILTERS
Понимание разницы между ALLEXCEPT и ALL/VALUES
Использование функции ALL для предотвращения преобразования контекста
Использование функции ISEMPTY
Привязка данных и функция TREATAS
Фильтры произвольной формы
Заключение
Глава 11. Работа с иерархиями
Вычисление процентов внутри иерархии
Работа с иерархиями типа родитель/потомок
Заключение
Глава 12. Работа с таблицами
Функция CALCULATETABLE
Манипулирование таблицами
Функция ADDCOLUMNS
Функция SUMMARIZE
Функция CROSSJOIN
Функция UNION
Функция INTERSECT
Функция EXCEPT
Использование таблиц в качестве фильтров
Применение условных конструкций OR
Ограничение расчетов постоянными покупателями с первого года
Вычисление новых покупателей
Повторное использование табличных выражений при помощи функции DETAILROWS
Создание вычисляемых таблиц
Функция SELECTCOLUMNS
Создание статических таблиц при помощи функции ROW
Создание статических таблиц при помощи функции DATATABLE
Функция GENERATESERIES
Заключение
Глава 13. Создание запросов
Знакомство с DAX Studio
Инструкция EVALUATE
Введение в синтаксис EVALUATE
Использование VAR внутри DEFINE
Использование MEASURE внутри DEFINE
Реализация распространенных шаблонов запросов в DAX
Использование функции ROW для проверки мер
Функция SUMMARIZE
Функция SUMMARIZECOLUMNS
Функция TOPN
Функции GENERATE и GENERATEALL
Функция ISONORAFTER
Функция ADDMISSINGITEMS
Функция TOPNSKIP
Функция GROUPBY
Функции NATURALINNERJOIN и NATURALLEFTOUTERJOIN
Функция SUBSTITUTEWITHINDEX
Функция SAMPLE
Автоматическая проверка существования данных в запросах DAX
Заключение
Глава 14. Продвинутые концепции языка DAX
Знакомство с расширенными таблицами
Функция RELATED
Использование функции RELATED в вычисляемых столбцах
Разница между фильтрами по таблице и фильтрами по столбцу
Использование табличных фильтров в мерах
Введение в активные связи
Разница между расширением таблиц и фильтрацией
Преобразование контекста в расширенных таблицах
Функция ALLSELECTED и неявные контексты фильтра
Знакомство с неявными контекстами фильтра
ALLSELECTED возвращает строки из итераций
Применение функции ALLSELECTED без параметров
Функции группы ALL*
Функция ALL
Функция ALLEXCEPT
Функция ALLNOBLANKROW
Функция ALLSELECTED
Функция ALLCROSSFILTERED
Использование привязки данных
Заключение
Глава 15. Углубленное изучение связей
Реализация вычисляемых физических связей
Создание связей по нескольким столбцам
Реализация связей на основе диапазонов
Циклические зависимости в вычисляемых физических связях
Реализация виртуальных связей
Распространение фильтров в DAX
Распространение фильтра с использованием функции TREATAS
Распространение фильтра с использованием функции INTERSECT
Распространение фильтра с использованием функции FILTER
Динамическая сегментация с использованием виртуальных связей
Реализация физических связей в DAX
Использование двунаправленной кросс-фильтрации
Связи типа «один ко многим»
Связи типа «один к одному»
Связи типа «многие ко многим»
Реализация связи «многие ко многим» через таблицу-мост
Реализация связи «многие ко многим» через общее измерение
Реализация связи «многие ко многим» через слабые связи
Выбор правильного типа для связи
Управление гранулярностью
Возникновение неоднозначностей в связях
Появление неоднозначностей в активных связях
Устранение неоднозначностей в неактивных связях
Заключение
Глава 16. Вычисления повышенной сложности в DAX
Подсчет количества рабочих дней между двумя датами
Данные о продажах и бюджетировании в одном отчете
Расчет сопоставимых продаж по магазинам
Нумерация последовательности событий
Вычисление продаж по предыдущему году до определенной даты
Заключение
Глава 17. Движки DAX
Знакомство с архитектурой движков DAX
Введение в движок формул
Введение в движок хранилища данных
Движок хранилища данных VertiPaq
Движок хранилища данных DirectQuery
Процедура обновления данных
Принципы работы движка хранилища данных VertiPaq
Введение в столбчатые базы данных
Сжатие данных движком VertiPaq
Сегментация и секционирование
Использование представлений динамического управления
Использование связей в движке VertiPaq
Материализация
Агрегирование
Выбор аппаратного обеспечения для VertiPaq
Возможность выбора аппаратного обеспечения
Приоритеты при выборе аппаратного обеспечения
Модель центрального процессора
Быстродействие памяти
Количество ядер процессора
Объем памяти
Дисковый ввод/вывод и постраничная подкачка
Заключение
Глава 18. Оптимизация движка VertiPaq
Сбор информации о модели данных
Денормализация
Кратность столбцов
Работа с датой и временем
Вычисляемые столбцы
Оптимизация сложных фильтров при помощи булевых вычисляемых столбцов
Обработка вычисляемых столбцов
Выбор столбцов для хранения
Оптимизация хранения столбцов
Оптимизация при помощи разделения столбцов
Оптимизация столбцов с высокой кратностью
Отключение иерархий атрибутов
Оптимизация атрибутов детализации
Управление агрегированием VertiPaq
Заключение
Глава 19. Анализ планов выполнения запросов DAX
Перехват запросов DAX
Введение в планы выполнения запросов
Создание плана выполнения запроса
Логический план выполнения запроса
Физический план выполнения запроса
Запросы движка хранилища данных
Сбор информации для оптимизации
Использование DAX Studio
Использование SQL Server Profiler
Чтение запросов движка хранилища VertiPaq
Введение в синтаксис xmSQL
Время сканирования
Внутренние события DISTINCTCOUNT
Параллелизм и кеш данных
Кеш движка VertiPaq
Функция обратного вызова CallbackDataID
Чтение запросов движка хранилища DirectQuery
Анализ составных моделей данных
Использование агрегатов в модели данных
Чтение планов выполнения запросов
Заключение
Глава 20. Оптимизация в DAX
Выбор стратегии оптимизации
Выделение выражения DAX для оптимизации
Создание проверочного запроса
Создание проверочного запроса в DAX
Создание мер для запросов в DAX Studio
Создание проверочного запроса в MDX
Анализ времени выполнения запроса и информации из плана
Поиск узких мест в движке формул и движке хранилища данных
Внесение изменений и повторные запуски тестовых запросов
Оптимизация узких мест в выражениях DAX
Оптимизация условий фильтрации
Оптимизация преобразования контекста
Оптимизация условных выражений IF
Оптимизация условных выражений IF в мерах
Выбор между функциями IF и DIVIDE
Оптимизация функции IF в итераторах
Снижение влияния функции CallbackDataID на производительность
Оптимизация вложенных итераторов
Отказ от использования табличных фильтров с функцией DISTINCTCOUNT
Уход от множественных вычислений путем использования переменных
Заключение
Предметный указатель

Citation preview

The Definitive Guide to DAX: Business intelligence with Microsoft Power BI, SQL Server Analysis Services, and Excel Marco Russo and Alberto Ferrari

Подробное руководство по DAX: бизнес-аналитика с Microsoft Power BI, SQL Server Analysis Services и Excel Марко Руссо и Альберто Феррари

Москва, 2021

УДК 004.42DAX ББК 32.97 Р89

Р89

Руссо М., Феррари А. Подробное руководство по DAX: бизнес-аналитика с Microsoft Power BI, SQL Server Analysis Services и Excel / пер. с англ. А. Ю. Гинько. – М.: ДМК Пресс, 2021. – 776 с.: ил. ISBN 978-5-97060-859-3 Расширенная и дополненная с учетом современных требований и техник, эта книга представляет собой наиболее полное руководство по языку DAX, применяемому в области бизнес-аналитики, моделирования данных и анализа. Эксперты Microsoft BI Марко Руссо и Альберто Феррари излагают как основы, так и отдельные нюансы работы с DAX: от простых табличных функций до продвинутых техник программирования и оптимизации моделей. Вы узнаете, что происходит под капотом движка DAX при запуске выражений; полученные знания пригодятся при написании быстрого и надежного кода. В книге используются примеры, которые можно запустить в бесплатной версии Power BI Desktop и разобраться во всех тонкостях синтаксиса создания переменных (VAR) в Power BI, Excel или Analysis Services. Издание предназначено для опытных пользователей и профессионалов в сфере бизнес-аналитики, использующих в своей работе DAX и аналитические инструменты от Microsoft.

УДК 004.42DAX ББК 32.97 Authorized Translation from the English language edition, entitled DEFINITIVE GUIDE TO DAX, THE: BUSINESS INTELLIGENCE FOR MICROSOFT POWER BI, SQL SERVER ANALYSIS SERVICES, AND EXCEL, 2nd Edition by MARCO RUSSO; ALBERTO FERRARI, published by Pearson Education, Inc, publishing as Microsoft Press. Russian-language edition copyright © 2021 by DMK Press. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc. Electronic RUSSIAN language edition publiched by DMK PRESS PUBLISHING LTD. Copyright © 2021. Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.

ISBN 978-1-5093-0697-8 (англ.) ISBN 978-5-97060-859-3 (рус.)

Copyright © 2020 by Alberto Ferrari and Marco Russo © Оформление, издание, перевод, ДМК Пресс, 2021

Содержание Рецензия ..................................................................................................... 14 Об авторах .................................................................................................. 15 От команды разработчиков.................................................................... 16 Благодарности ........................................................................................... 17 От издательства......................................................................................... 19 Предисловие ко второму изданию ....................................................... 20 Предисловие к первому изданию ......................................................... 21

Глава 1

Что такое DAX? .................................................................................... 27 Введение в модель данных ..................................................................... 27 Введение в направление связи ........................................................ 29 DAX для пользователей Excel ................................................................. 31 Ячейки против таблиц ....................................................................... 32 Excel и DAX: два функциональных языка ..................................... 34 Итерационные функции в DAX........................................................ 34 DAX требует изучения теории .......................................................... 35 DAX для разработчиков SQL................................................................... 35 Работа со связями ............................................................................... 35 DAX как функциональный язык ...................................................... 36 DAX как язык программирования и язык запросов ................... 37 Подзапросы и условия в DAX и SQL ................................................ 37 DAX для разработчиков MDX ................................................................. 38 Многомерность против табличности ............................................. 39 DAX как язык программирования и язык запросов ................... 39 Иерархии ............................................................................................... 40 Вычисления на конечном уровне.................................................... 41 DAX для пользователей Power BI........................................................... 41

Глава 2

Знакомство с DAX .............................................................................. 43 Введение в вычисления DAX .................................................................. 43 Типы данных DAX ............................................................................... 45 Операторы DAX ................................................................................... 48 Конструкторы таблиц ......................................................................... 49 Условные операторы .......................................................................... 50 Введение в вычисляемые столбцы и меры......................................... 51 Вычисляемые столбцы ....................................................................... 51 Меры....................................................................................................... 52 Введение в переменные .......................................................................... 56 Обработка ошибок в выражениях DAX................................................ 57 Ошибки преобразования .................................................................. 57 Ошибки арифметических операций .............................................. 58 Содержание

5

Перехват ошибок ................................................................................. 61 Генерирование ошибок ..................................................................... 64 Форматирование кода на DAX ............................................................... 65 Введение в агрегаторы и итераторы .................................................... 68 Использование распространенных функций DAX ........................... 71 Функции агрегирования.................................................................... 71 Логические функции .......................................................................... 73 Информационные функции ............................................................. 74 Математические функции ................................................................ 75 Тригонометрические функции ........................................................ 76 Текстовые функции ............................................................................ 76 Функции преобразования ................................................................. 77 Функции для работы с датой и временем ..................................... 78 Функции отношений .......................................................................... 79 Заключение ................................................................................................ 81

Глава 3

Использование основных табличных функций ............. 83 Введение в табличные функции ........................................................... 83 Введение в синтаксис EVALUATE .......................................................... 86 Введение в функцию FILTER .................................................................. 87 Введение в функции ALL и ALLEXCEPT............................................... 90 Введение в функции VALUES, DISTINCT и пустые строки .............. 94 Использование таблиц в качестве скалярных значений ...............100 Введение в функцию ALLSELECTED ....................................................102 Заключение ...............................................................................................104

Глава 4

Введение в контексты вычисления.......................................105 Введение в контексты вычисления .....................................................106 Знакомство с контекстом фильтра ................................................106 Знакомство с контекстом строки ...................................................112 Тест на понимание контекстов вычисления .....................................114 Использование функции SUM в вычисляемых столбцах .........114 Использование ссылок на столбцы в мерах ................................115 Использование контекста строки с итераторами ............................116 Вложенные контексты строки в разных таблицах .....................117 Вложенные контексты строки в одной таблице .........................119 Использование функции EARLIER .................................................123 Функции FILTER, ALL и взаимодействие между контекстами .....125 Работа с несколькими таблицами........................................................128 Контексты строки и связи ................................................................129 Контекст фильтра и связи ................................................................132 Использование функций DISTINCT и SUMMARIZE в контекстах фильтра..............................................................................136 Заключение ...............................................................................................140

Глава 5

Функции CALCULATE и CALCULATETABLE ..........................142 Введение в функции CALCULATE и CALCULATETABLE...................142 Создание контекста фильтра ..........................................................143

6

Содержание

Знакомство с функцией CALCULATE .............................................147 Использование функции CALCULATE для расчета  процентов ............................................................................................152 Введение в функцию KEEPFILTERS ................................................163 Фильтрация по одному столбцу .....................................................167 Фильтрация по сложным условиям ...............................................168 Порядок вычислений в функции CALCULATE .............................172 Преобразование контекста....................................................................177 Повторение темы контекста строки и контекста фильтра ......177 Введение в преобразование контекста .........................................179 Преобразование контекста в вычисляемых столбцах ..............183 Преобразование контекста в мерах...............................................186 Циклические зависимости ....................................................................190 Модификаторы функции CALCULATE ................................................194 Модификатор USERELATIONSHIP ..................................................195 Модификатор CROSSFILTER ............................................................198 Модификатор KEEPFILTERS .............................................................199 Использование модификатора ALL в функции CALCULATE ...200 Использование ALL и ALLSELECTED без параметров ...............202 Правила вычисления в функции CALCULATE ...................................203

Глава 6

Переменные.........................................................................................206 Введение в синтаксис переменных VAR ............................................206 Переменные – это константы ...............................................................208 Области видимости переменных.........................................................209 Использование табличных переменных............................................212 Отложенное вычисление переменных ...............................................214 Распространенные шаблоны использования переменных ...........215 Заключение ...............................................................................................217

Глава 7

Работа с итераторами и функцией CALCULATE..............219 Использование итерационных функций ...........................................219 Кратность итератора .........................................................................220 Использование преобразования контекста в итераторах .......223 Использование функции CONCATENATEX ..................................226 Итераторы, возвращающие таблицы ...........................................228 Решение распространенных сценариев при помощи  итераторов.................................................................................................232 Расчет среднего и скользящего среднего .....................................232 Использование функции RANKX....................................................235 Изменение гранулярности вычисления .......................................243 Заключение ...............................................................................................247

Глава 8

Логика операций со временем ................................................249 Введение в логику операций со временем ........................................249 Автоматические дата и время в Power BI .....................................250 Автоматические столбцы с датами в Power Pivot для Excel .....251 Содержание

7

Шаблон таблицы дат в Power Pivot для Excel ...............................251 Создание таблицы дат ............................................................................253 Использование функций CALENDAR и CALENDARAUTO .........254 Работа со множественными датами ..............................................257 Поддержка множественных связей с таблицей дат ...................257 Поддержка нескольких таблиц дат ................................................259 Знакомство с базовыми вычислениями в работе со временем ...260 Пометка календарей как таблиц дат .............................................265 Знакомство с базовыми функциями логики операций со временем ..............................................................................................266 Нарастающие итоги с начала года, квартала, месяца ...............268 Сравнение временных интервалов ...............................................270 Сочетание функций логики операций со временем .................273 Расчет разницы по сравнению с предыдущим периодом .......275 Расчет скользящей годовой суммы ...............................................276 Выбор порядка вложенности функций логики операций со временем .........................................................................................278 Знакомство с полуаддитивными вычислениями ............................280 Использование функций LASTDATE и LASTNONBLANK ..........282 Работа с остатками на начало и конец периода .........................288 Усовершенствованные методы работы с датой и временем ........292 Вычисления нарастающим итогом................................................293 Функция DATEADD ............................................................................296 Функции FIRSTDATE, LASTDATE, FIRSTNONBLANK и LASTNONBLANK ..............................................................................303 Использование детализации с функциями логики операций со временем......................................................................305 Работа с пользовательскими календарями .......................................306 Работа с неделями..............................................................................307 Пользовательские вычисления нарастающим итогом .............309 Заключение ...............................................................................................312

Глава 9

Группы вычислений .........................................................................313 Знакомство с группами вычислений ..................................................313 Создание групп вычислений.................................................................316 Знакомство с группами вычислений ..................................................322 Применение элемента вычисления...............................................325 Очередность применения групп вычислений ............................334 Включение и исключение мер из элементов вычисления.......339 Косвенная рекурсия ................................................................................341 Два основных правила ...........................................................................346 Заключение ...............................................................................................347

Глава 10 Работа с контекстом фильтра ...................................................348 Использование функций HASONEVALUE и SELECTEDVALUE .......349 Использование функций ISFILTERED и ISCROSSFILTERED ...........354 Понимание разницы между функциями VALUES и FILTERS.........357 8

Содержание

Понимание разницы между ALLEXCEPT и ALL/VALUES ................359 Использование функции ALL для предотвращения преобразования контекста ....................................................................364 Использование функции ISEMPTY ......................................................366 Привязка данных и функция TREATAS...............................................368 Фильтры произвольной формы ...........................................................372 Заключение ...............................................................................................379

Глава 11 Работа с иерархиями......................................................................381 Вычисление процентов внутри иерархии .........................................381 Работа с иерархиями типа родитель/потомок .................................386 Заключение ...............................................................................................398

Глава 12 Работа с таблицами .........................................................................399 Функция CALCULATETABLE...................................................................399 Манипулирование таблицами ..............................................................402 Функция ADDCOLUMNS....................................................................402 Функция SUMMARIZE .......................................................................405 Функция CROSSJOIN ..........................................................................409 Функция UNION ..................................................................................411 Функция INTERSECT..........................................................................415 Функция EXCEPT ................................................................................417 Использование таблиц в качестве фильтров ....................................418 Применение условных конструкций OR ......................................419 Ограничение расчетов постоянными покупателями с первого года......................................................................................422 Вычисление новых покупателей ....................................................423 Повторное использование табличных выражений при помощи функции DETAILROWS .............................................425 Создание вычисляемых таблиц............................................................427 Функция SELECTCOLUMNS ..............................................................427 Создание статических таблиц при помощи функции ROW ....429 Создание статических таблиц при помощи функции DATATABLE ...........................................................................................430 Функция GENERATESERIES ..............................................................431 Заключение ...............................................................................................432

Глава 13 Создание запросов...........................................................................433 Знакомство с DAX Studio ........................................................................433 Инструкция EVALUATE ...........................................................................434 Введение в синтаксис EVALUATE ...................................................434 Использование VAR внутри DEFINE ..............................................435 Использование MEASURE внутри DEFINE ...................................437 Реализация распространенных шаблонов запросов в DAX...........438 Использование функции ROW для проверки мер......................439 Функция SUMMARIZE .......................................................................440 Функция SUMMARIZECOLUMNS .....................................................442 Содержание

9

Функция TOPN ....................................................................................448 Функции GENERATE и GENERATEALL ...........................................454 Функция ISONORAFTER....................................................................457 Функция ADDMISSINGITEMS ..........................................................460 Функция TOPNSKIP............................................................................461 Функция GROUPBY ............................................................................461 Функции NATURALINNERJOIN и NATURALLEFTOUTERJOIN ...464 Функция SUBSTITUTEWITHINDEX .................................................466 Функция SAMPLE ...............................................................................468 Автоматическая проверка существования данных в запросах DAX .........................................................................................469 Заключение ...............................................................................................476

Глава 14 Продвинутые концепции языка DAX ...................................478 Знакомство с расширенными таблицами..........................................478 Функция RELATED..............................................................................483 Использование функции RELATED в вычисляемых столбцах................................................................................................484 Разница между фильтрами по таблице и фильтрами  по столбцу ..................................................................................................486 Использование табличных фильтров в мерах ............................489 Введение в активные связи .............................................................492 Разница между расширением таблиц и фильтрацией..............495 Преобразование контекста в расширенных таблицах ..............497 Функция ALLSELECTED и неявные контексты фильтра .................498 Знакомство с неявными контекстами фильтра ..........................499 ALLSELECTED возвращает строки из итераций .........................503 Применение функции ALLSELECTED без параметров..............506 Функции группы ALL*.............................................................................506 Функция ALL........................................................................................508 Функция ALLEXCEPT .........................................................................509 Функция ALLNOBLANKROW ............................................................509 Функция ALLSELECTED.....................................................................509 Функция ALLCROSSFILTERED ..........................................................509 Использование привязки данных .......................................................510 Заключение ...............................................................................................512

Глава 15 Углубленное изучение связей...................................................514 Реализация вычисляемых физических связей .................................514 Создание связей по нескольким столбцам ..................................514 Реализация связей на основе диапазонов ...................................517 Циклические зависимости в вычисляемых физических связях ....................................................................................................520 Реализация виртуальных связей .........................................................523 Распространение фильтров в DAX .................................................524 Распространение фильтра с использованием функции TREATAS................................................................................................526 10 Содержание

Распространение фильтра с использованием функции INTERSECT ...........................................................................................527 Распространение фильтра с использованием функции FILTER....................................................................................................528 Динамическая сегментация с использованием виртуальных связей...........................................................................529 Реализация физических связей в DAX................................................533 Использование двунаправленной кросс-фильтрации ...................536 Связи типа «один ко многим» ..............................................................538 Связи типа «один к одному» .................................................................539 Связи типа «многие ко многим» ..........................................................540 Реализация связи «многие ко многим» через таблицу-мост ..540 Реализация связи «многие ко многим» через общее  измерение ............................................................................................546 Реализация связи «многие ко многим» через слабые связи ...551 Выбор правильного типа для связи ....................................................553 Управление гранулярностью ................................................................555 Возникновение неоднозначностей в связях .....................................559 Появление неоднозначностей в активных связях .....................561 Устранение неоднозначностей в неактивных связях ...............563 Заключение ...............................................................................................565

Глава 16 Вычисления повышенной сложности в DAX ...................567 Подсчет количества рабочих дней между двумя датами ...............567 Данные о продажах и бюджетировании в одном отчете ...............575 Расчет сопоставимых продаж по магазинам ....................................578 Нумерация последовательности событий .........................................585 Вычисление продаж по предыдущему году до определенной даты .............................................................................................................588 Заключение ...............................................................................................593

Глава 17 Движки DAX .........................................................................................594 Знакомство с архитектурой движков DAX.........................................594 Введение в движок формул .............................................................596 Введение в движок хранилища данных .......................................596 Движок хранилища данных VertiPaq.............................................597 Движок хранилища данных DirectQuery ......................................598 Процедура обновления данных ......................................................599 Принципы работы движка хранилища данных VertiPaq ...............600 Введение в столбчатые базы данных ............................................600 Сжатие данных движком VertiPaq..................................................603 Сегментация и секционирование ..................................................613 Использование представлений динамического управления ..........................................................................................614 Использование связей в движке VertiPaq ..........................................617 Материализация ......................................................................................620 Агрегирование ..........................................................................................623 Содержание

11

Выбор аппаратного обеспечения для VertiPaq .................................625 Возможность выбора аппаратного обеспечения .......................626 Приоритеты при выборе аппаратного обеспечения .................626 Модель центрального процессора .................................................627 Быстродействие памяти ...................................................................628 Количество ядер процессора ...........................................................628 Объем памяти .....................................................................................629 Дисковый ввод/вывод и постраничная подкачка ......................630 Заключение ...............................................................................................630

Глава 18 Оптимизация движка VertiPaq .................................................632 Сбор информации о модели данных ..................................................632 Денормализация ......................................................................................637 Кратность столбцов .................................................................................645 Работа с датой и временем ....................................................................646 Вычисляемые столбцы ...........................................................................649 Оптимизация сложных фильтров при помощи булевых вычисляемых столбцов .....................................................................652 Обработка вычисляемых столбцов ................................................653 Выбор столбцов для хранения ..............................................................654 Оптимизация хранения столбцов .......................................................657 Оптимизация при помощи разделения столбцов .....................657 Оптимизация столбцов с высокой кратностью ..........................658 Отключение иерархий атрибутов ..................................................659 Оптимизация атрибутов детализации .........................................659 Управление агрегированием VertiPaq ................................................660 Заключение ...............................................................................................663

Глава 19 Анализ планов выполнения запросов DAX ......................664 Перехват запросов DAX ..........................................................................664 Введение в планы выполнения запросов ..........................................667 Создание плана выполнения запроса ...........................................668 Логический план выполнения запроса ........................................669 Физический план выполнения запроса........................................670 Запросы движка хранилища данных ............................................671 Сбор информации для оптимизации .................................................672 Использование DAX Studio ..............................................................673 Использование SQL Server Profiler .................................................676 Чтение запросов движка хранилища VertiPaq ..................................680 Введение в синтаксис xmSQL ..........................................................681 Время сканирования .........................................................................689 Внутренние события DISTINCTCOUNT .........................................691 Параллелизм и кеш данных.............................................................692 Кеш движка VertiPaq ..........................................................................694 Функция обратного вызова CallbackDataID.................................696 Чтение запросов движка хранилища DirectQuery ...........................702 Анализ составных моделей данных ..............................................703 12 Содержание

Использование агрегатов в модели данных ................................704 Чтение планов выполнения запросов ................................................706 Заключение ...............................................................................................713

Глава 20 Оптимизация в DAX.........................................................................715 Выбор стратегии оптимизации ............................................................716 Выделение выражения DAX для оптимизации...........................716 Создание проверочного запроса ....................................................719 Анализ времени выполнения запроса и информации из плана ................................................................................................723 Поиск узких мест в движке формул и движке хранилища данных ..................................................................................................726 Внесение изменений и повторные запуски тестовых  запросов ...............................................................................................727 Оптимизация узких мест в выражениях DAX ...................................727 Оптимизация условий фильтрации ..............................................728 Оптимизация преобразования контекста ...................................732 Оптимизация условных выражений IF.........................................739 Снижение влияния функции CallbackDataID на производительность ....................................................................751 Оптимизация вложенных итераторов ..........................................754 Отказ от использования табличных фильтров с функцией  DISTINCTCOUNT .................................................................................761 Уход от множественных вычислений путем использования переменных............................................................766 Заключение ...............................................................................................771

Предметный указатель ........................................................................................772

Рецензия Эту книгу можно смело назвать «Библией DAX». На сегодняшний день это самое подробное и глубокое описание практически всех имеющихся в языке DAX функций и нюансов их применения. Авторы данного шедевра – Альберто Феррари и Марко Руссо – одни из самых (если не самые) уважаемые и признанные эксперты в этой теме. Их сайт www.sqlbi.com – это кладезь информации для любого аналитика, а без их программ (DAX Studio, Power Pivot Utilities и др.) я уже не могу представить себе полноценную работу с данными в реальных бизнес-задачах. Со всей ответственностью могу утверждать, что эта книга – однозначный must have для любого аналитика, работающего с Power BI, или продвинутого пользователя Microsoft Excel. У меня, признаюсь, эта книга в англоязычном варианте с Amazon (еще первое издание!) уже несколько лет «живет» на полке рядом с рабочим столом и не раз выручала меня в работе и подготовке тренингов. Очень рад, что рядом с ней теперь будет стоять ее русскоязычный братблизнец. Николай Павлов, Microsoft Certified Trainer, Microsoft Most Valuable Professional (MVP), автор проекта «Планета Эксел» (www.planetaexcel.ru)

Об авторах Марко Руссо и  Альберто Феррари являются основателями сайта sqlbi.com, на котором регулярно публикуют статьи по Microsoft Power BI, Power Pivot, DAX и SQL Server Analysis Services. Они работают с DAX с момента появления первой бета-версии Power Pivot в 2009 году, и за это время сайт sqlbi.com стал одним из главных поставщиков статей и обучающих материалов по DAX. Их семинары, как очные, так и в удаленном режиме, являются основным источником вдохновения и  обучения для энтузиастов DAX. Марко и Альберто проводят консультации и обучение в области бизнес-аналитики (BI) с использованием технологий от Microsoft. За время своей практики они написали несколько книг и статей по Power BI, DAX и Analysis Services. Также они обеспечивают сообщество DAX постоянной поддержкой в  виде новых материалов для сайтов daxpatterns.com, daxformatter.com и dax.guide. Кроме того, Марко и Альберто регулярно выступают на крупнейших международных конференциях, включая Microsoft Ignite, PASS Summit и SQLBits. Связаться с Марко можно по электронной почте [email protected], а с Альберто – [email protected].

От команды разработчиков

В

ы можете не знать наших имен. Мы проводим дни за написанием кода для программ, которые вы ежедневно используете в своей работе. Мы – часть команды разработчиков Power BI, SQL Server Analysis Services и… да, мы приложили руку к созданию языка DAX и движка VertiPaq. Язык, который вы собираетесь изучать, читая эту книгу, является нашим детищем. Мы провели не один год, работая над ним, улучшая движок и находя способы для ускорения оптимизатора в  попытке превратить DAX в  простой и  лаконичный язык, призванный значительно облегчить жизнь и  повысить эффективность труда аналитиков данных. Но позвольте, это ведь предисловие к книге, так что больше ни слова о нас! Почему же мы пишем вводное слово к изданию Марко и Альберто – парней из SQLBI? Хотя бы потому, что при поиске информации по DAX в сети новички постоянно выходят на их статьи. Они начинают читать их, увлекаются языком и в конечном счете, мы надеемся, проникаются уважением к результатам нашего тяжелого труда. Мы познакомились с  Марко и  Альберто довольно давно и сразу отметили их глубочайшие познания в области SQL Server Analysis Services. И они были в числе первопроходцев нового языка DAX, изучали его и старались применить на практике. Их статьи, заметки и посты в блогах стали источником познания для многих тысяч людей. Мы пишем код, но не так много времени уделяем обучению разработчиков тому, как им пользоваться. А Марко и Альберто как раз из числа тех, кто распространяет знания о DAX по миру. Книги этих парней являются мировыми бестселлерами в  данной области, а написание подробного руководства по DAX ознаменовало собой историческую веху в популяризации языка, который мы сотворили и к которому питаем самые нежные чувства. Мы пишем код, они пишут книги, а вы изучаете DAX, привнося в свой бизнес невиданную аналитическую мощь. Вместе же мы делаем общее дело – извлекаем максимум аналитической информации из данных. И это здорово! Мариус Думитру (Marius Dumitru), руководитель отдела разработки Power BI Кристиан Петкулеску (Cristian Petculescu), главный разработчик Power BI Джеффри Ванг (Jeffrey Wang), управляющий отдела разработки ПО Кристиан Уэйд (Christian Wade), старший руководитель проекта

Благодарности

Н

аписание второго издания этой книги заняло у нас целый год – на три месяца больше, чем первого. Это было долгое и увлекательное путешествие вместе с самыми разными людьми из разных широт и часовых поясов, результатом которого стала эта книга. Мы хотели бы поблагодарить за помощь в ее написании очень многих людей, но понимаем, что всех перечислить просто не сможем. Так что просто скажем спасибо всем, кто так или иначе способствовал выпуску книги – возможно, даже не подразумевая об этом. Комментарии в  блогах, посты на форумах, обсуждения по почте, общение на технических конференциях, анализ различных сценариев – все это было для нас очень полезно, и многие из ваших идей нашли отражение в данной книге. Также мы выражаем огромную признательность всем студентам наших курсов: обучая вас, мы развивались сами! И все же отдельных людей мы не можем не выделить особо за их заметный вклад в написание книги. Начать список персональных благодарностей мы хотим с Эдуарда Меломеда. Именно он вдохновил нас на написание книги. Если бы не страстная дискуссия с  ним несколько лет назад, итогом которой стало содержание нашей первой книги по Power Pivot, написанное на салфетке, мы могли бы вовсе не отправиться в путешествие по миру DAX. Также мы очень признательны издательству Microsoft Press и его сотрудникам, внесшим весомый вклад в наш труд. Написание книги отнимает немало времени, но еще больше времени уходит на подготовительные исследования. Люди, которых мы называем «инсайдерами SSAS (SQL Server Analysis Services)», очень помогли нам в подготовке к путешествию. Кроме того, стоит особо отметить нескольких людей из Microsoft, уделивших нам свое время при описании важных концепций, касающихся Power BI и DAX. Это Мариус Думитру (Marius Dumitru), Джеффри Ванг (Jeffrey Wang), Акшай Мирчандани (Akshai Mirchandani), Кристиан Саковски (Krystian Sakowski) и Кристиан Петкулеску (Cristian Petculescu). Ваша помощь была неоценима, парни! Также мы хотим поблагодарить Амира Нетца (Amir Netz), Кристиана Уэйда (Christian Wade), Ашвини Шарма (Ashvini Sharma), Каспера Де Йонга (Kasper De Jonge) и T. K. Ананда (T. K. Anand) за многочисленные дискуссии касательно этого проекта. Эти люди помогли нам при выборе стратегического направления как в этой книге, так и в карьере в целом. Отдельные слова признательности хотелось бы сказать в  адрес женщины, изрядно поработавшей над нашим английским. Клэр Коста (Claire Costa) тщательно вычитала исходный текст книги и привела его в порядок. Клэр, мы высоко ценим твою помощь! Спасибо! Последнюю персональную благодарность мы адресуем нашему техническому рецензенту Даниилу Маслюку (Daniil Maslyuk), проверившему все без Благодарности

17

исключения фрагменты кода, примеры и  ссылки в  книге. Он обнаружил все ошибки и опечатки, которые мы не заметили, а его комментарии всегда были по делу. Результат совместной работы превзошел все наши ожидания. И если в книге ошибок оказалось меньше, чем в исходном тексте, в этом заслуга Даниила. А оставшиеся опечатки – исключительно наша вина. Спасибо, ребята!

Поддержка Если вам требуется дополнительная помощь или информация, вы можете обратиться по адресу: https://MicrosoftPressStore.com/Support. Отметим, что услуги по поддержке программного обеспечения Microsoft по этому адресу не оказываются. Если вам требуется помощь такого плана, перейдите на сайт http://support.microsoft.com.

Оставайтесь с нами Давайте продолжим общение! Заходите на наш Twitter: @MicrosoftPress.

От издательства Отзывы и пожелания Мы всегда рады отзывам наших читателей. Расскажите нам, что вы думаете об этой книге – что понравилось или, может быть, не понравилось. Отзывы важны для нас, чтобы выпускать книги, которые будут для вас максимально полезны. Вы можете написать отзыв на нашем сайте www.dmkpress.com, зайдя на страницу книги и оставив комментарий в разделе «Отзывы и рецензии». Также можно послать письмо главному редактору по адресу [email protected]; при этом укажите название книги в теме письма. Если вы являетесь экспертом в какой-либо области и заинтересованы в написании новой книги, заполните форму на нашем сайте http://dmkpress.com/ authors/publish_book/ или напишите в издательство: [email protected].

Список опечаток Хотя мы приняли все возможные меры для того, чтобы обеспечить высокое качество наших текстов, ошибки все равно случаются. Если вы найдете ошибку в одной из наших книг – возможно, ошибку в основном тексте или программном коде, – мы будем очень благодарны, если вы сообщите нам о ней. Сделав это, вы избавите других читателей от недопонимания и поможете нам улучшить последующие издания этой книги. Если вы найдете какие-либо ошибки в коде, пожалуйста, сообщите о них главному редактору по адресу [email protected], и мы исправим это в следующих тиражах.

Нарушение авторских прав Пиратство в интернете по-прежнему остается насущной проблемой. Издательство «ДМК Пресс» очень серьезно относится к вопросам защиты авторских прав и лицензирования. Если вы столкнетесь в интернете с незаконной публикацией какой-либо из наших книг, пожалуйста, пришлите нам ссылку на интернет-ресурс, чтобы мы могли применить санкции. Ссылку на подозрительные материалы можно прислать по адресу электронной почты [email protected]. Мы высоко ценим любую помощь по защите наших авторов, благодаря которой мы можем предоставлять вам качественные материалы.

От издательства

19

Предисловие ко второму издани

К

огда мы задумались о том, что пришло время обновить книгу, мы посчитали, что сделать это будет легко: в конце концов, в языке DAX за это время произошло не так много изменений, а теоретическая ценность первого издания не была утрачена. Мы полагали, что ограничимся лишь заменой рисунков с Excel на Power BI и добавим что-то по мелочи тут и там. Как же мы ошибались! Приступив к обновлению первой главы, мы очень быстро поняли, что хотим переписать в ней почти все. И так на протяжении всей книги. Так что вы держите в руках не просто второе издание, а совершенно новую книгу. И причина таких серьезных обновлений отнюдь не в том, что за это время как-то кардинально изменился язык или описываемые в книге инструменты. Скорее, мы как авторы и преподаватели изменились – надеемся, в лучшую сторону. Мы научили языку DAX тысячи людей по всему миру, неустанно работали со своими студентами и  старались максимально доходчиво объяснять им самые сложные темы. В конечном счете мы нашли совершенно новый способ донесения до читателя информации о любимом нами языке. Мы расширили количество примеров в этом издании, чтобы показать, как работает на практике то, что вы сначала изучаете в теории. При этом мы старались максимально упростить примеры без ущерба для полноты описываемой ситуации. Мы боролись с  редактором за возможность увеличить количество страниц в книге, чтобы она могла вместить все темы, которые мы собирались осветить. Но мы не изменили главный посыл книги, состоящий в том, что вам не нужно владеть языком DAX, чтобы ее читать, хотя она и не предназначена для тех, кому просто нужно решить пару задачек на DAX. Скорее, эта книга для людей, желающих в полной мере овладеть искусством программирования на DAX и познать весь его потенциал и сложность. Если вы действительно хотите использовать всю мощь языка DAX, то должны приготовиться к длительному путешествию с чтением этой книги от корки до корки и возвращением к ней с целью отыскать то, что ускользнуло от вас при первом прочтении.

Предисловие к первому издани

В

нашем авторском активе немало материалов, посвященных языку DAX. Это и книги по Power Pivot и та ли ной модели (SSAS Tabular), и посты в блогах, и статьи, и экспертные доклады, и, наконец, книга, посвященная а лонам (patterns) в  DAX. Так зачем нам было писать (а вам, надеемся, читать) еще одну книгу по DAX? Неужели об этом языке так много можно узнать? Мы, разумеется, считаем, что да. Первое, что редактор стремится выведать у  переводчика в  момент начала работы над новой книгой, – это предполагаемое количество страниц. И это не праздный интерес – на объем книги завязана и цена, и весь производственный процесс, включая распределение ресурсов издательства, и прочее. Практически все, что связано с  книгой, так или иначе зависит от количества страниц в ней. Нас как авторов это немало расстраивает. Всякий раз, когда мы садились писать книгу, мы должны были выделять приличное место для описания программных продуктов, будь то Power Pivot для Microsoft Excel или SSAS Tabular, и только затем переходить к самому языку DAX. И каждый раз мы оставались недовольны тем, что нам вновь не удалось рассказать о DAX в объеме, в котором планировали. В конце концов, не писать же книгу по Power Pivot объемом в тысячу страниц – такая книга на полке магазина напугает кого угодно. Так что нам приходилось раз за разом писать о SSAS Tabular и Power Pivot, а  проект книги по DAX продолжал пылиться в  ящике стола. Но однажды мы открыли этот ящик и решили не думать о том, что включать в новую книгу, – она должна была быть посвящена DAX целиком и полностью. Результат этого решения вы держите в руках. Здесь вы не прочитаете о  том, как создать вычисляемый столбец или какое диалоговое окно использовать для установки того или иного свойства. Эта книга – не пошаговое руководство по Microsoft Visual Studio, Power BI или Power Pivot для Excel. В ней вы сможете с головой погрузиться в мир DAX – начиная с  самых основ и  заканчивая техническими нюансами, позволяющими оптимизировать код и модель. В процессе написания мы полюбили каждую страницу нашей книги. Мы столько раз ее перечитывали, что буквально выучили наизусть. При этом мы добавляли новый контент всякий раз, когда считали это уместным, не боясь превысить лимит на объем книги, и ничего не сокращали только для того, чтобы остаться в  рамках дозволенного. Одновременно мы все больше узнавали о DAX и наслаждались своими открытиями. Но есть еще один вопрос: зачем вам вообще читать руководство по DAX? Признайтесь, вы подумали так, впервые попробовав поработать в Power Pivot или Power BI! И вы не одиноки. В свой первый раз мы подумали точно так редисловие к ерво

издани

21

же. DAX предельно прост! Он очень похож на Excel! Более того, обладая опытом работы с одним языком программирования или запросов, вы наверняка привыкли изучать другие языки, просто глядя на примеры и сопоставляя его синтаксис с уже знакомыми вам шаблонами. Мы сами допустили эту ошибку и не хотим, чтобы через это прошли и вы. DAX – очень мощный язык, который используется во все большем количестве аналитических инструментов. Потенциал его велик, но некоторые его концепции непросто понять, идя в  своих рассуждениях от частного к  общему. Например, изучение контекста вычисления в  DAX требует обратного подхода – от общего к частному. Вы начинаете с теории, а после этого обращаетесь к соответствующим практическим примерам. Именно такой подход, именуемый дедукцией, характерен для этой книги. Мы понимаем, что многим не по душе подобный метод обучения – они предпочитают идти от практики к теории, сначала разобравшись с  конкретной задачей, а  затем подводя под нее определенные теоретические выводы. Если вы сторонник такого подхода, эта книга не для вас. Мы уже писали практические книги по DAX, полные примеров и  без описания того, как работает та или иная формула и  почему тот или иной подход к коду будет более оптимальным. Их вполне можно использовать как справочник функций DAX. Цель написания данной книги была совершенно иной. Мы хотели, чтобы вы в полной мере овладели языком DAX. Все примеры в этой книге демонстрируют определенное поведение, а не решают конкретные проблемы. Если вы сможете воспользоваться формулами из этой книги в своей модели, что ж, отлично. Но помните, что это лишь приятное дополнение, но никак не основная цель написания примеров. И всегда читайте описание к примерам, чтобы не угодить в ловушку. С целью обучения мы часто приводим в них не самые оптимальные способы решения задач. Мы искренне надеемся, что вам придется по душе наше совместное путешествие в мир DAX и во время чтения книги вы получите не меньшее удовольствие, чем мы – во время ее написания.

Для кого предназначена та книга? Если вы лишь время от времени используете DAX, эта книга, скорее всего, не для вас. Есть множество книг с  простым введением в  инструменты, использующие DAX, и  в  сам язык – начиная с  самых основ и  заканчивая базовыми понятиями программирования. Мы хорошо осведомлены об этом, поскольку и сами писали такие книги. Если же вы настроены на освоение DAX очень серьезно и с далеко идущими намерениями, эта книга – ваш выбор! При этом вы можете ничего не знать об этом языке. В этом случае, правда, не надейтесь на усвоение сложных концепций с первого раза. Мы советуем прочитать книгу от корки до корки, а затем, по мере приобретения опыта, возвращаться к наиболее сложным главам для повторного прочтения. Вполне вероятно, что описанные в них техники откроются для вас по-новому. Язык DAX может быть полезен для людей, занятых в самых разных областях: пользователям Power BI может понадобиться написать формулы на DAX в своих 22

редисловие к ерво

издани

моделях данных, специалистам по работе в Excel язык DAX может пригодиться в  совместном использовании с  надстройкой Power Pivot, а  профессионалы в области и нес аналитики (business intelligence – BI) могут применять код на DAX в своих решениях вне зависимости от их масштаба. В этой книге мы попытались представить информацию, которая может оказаться полезной для всех перечисленных категорий специалистов. При этом некоторые главы (в особенности касающиеся оптимизации работы DAX) могут быть предназначены для профессионалов в  области бизнес-аналитики, поскольку содержат сложную техническую информацию. Но мы считаем, что пользователям Power BI и Excel также может быть полезно узнать возможности оптимизации выражений DAX для достижения максимальной эффективности функционирования модели. И наконец, мы хотели написать книгу не только для чтения, но и для обучения. Поначалу мы будем стараться все объяснять максимально простым языком  – с  самого нуля. Но с  усложнением концепций мы будем постепенно уходить от простоты и  приближаться к  реальности. DAX – простой язык, но использовать его не так легко. Нам потребовалось несколько лет, чтобы в полной мере освоить все его премудрости. Не ожидайте, что вы все это усвоите за несколько дней беззаботного чтения. Эта книга потребует от вас максимальной концентрации внимания. Взамен мы предлагаем вам шанс освоить всю глубину DAX и стать настоящим экспертом в этой области.

ак мы представляем себе нашего читателя? Мы предполагаем, что наш читатель обладает базовыми знаниями в области Power BI и имеет представление об анализе данных. Если у вас есть опыт использования языка DAX, тем лучше для вас – быстрее прочитаете первые главы. Но в целом для чтения книги навыки работы с этим языком не обязательны. В книге встречаются фрагменты кода на MDX и SQL, но вам не нужно знать эти языки – они приводятся здесь лишь для сравнения способов написания выражений. Если вы не поймете, что написано в этих фрагментах кода, ничего страшного. Значит, вам это не нужно. В наиболее сложных главах книги мы затронем вопросы параллелизма, доступа к  памяти, использования центрального процессора и  другие сложные темы, с  которыми далеко не все должны быть знакомы. Опытные разработчики почувствуют себя в этих главах в своей тарелке, а пользователи Power BI и Excel могут быть немного напуганы. Но без этих технических нюансов просто не обойтись при описании темы оптимизации кода на DAX. И хотя эти сложные главы больше предназначены для опытных разработчиков в области бизнесаналитики, чем для пользователей Power BI и Excel, мы уверены, что пользу от их чтения получат все без исключения.

Структура книги Эта книга построена так, что темы в ней располагаются по нарастающей – от простых к сложным. В каждой следующей главе предполагается, что вы полноредисловие к ерво

издани

23

стью усвоили материал предыдущей – мы старались практически не повторять то, о чем уже писали ранее. Именно поэтому мы настоятельно советуем читать книгу от начала до конца, не прыгая от главы к главе. Будучи прочитанной, книга может превратиться в полезный справочник по DAX. Например, если вы захотите вспомнить, как работает функция , то можете открыть конкретный раздел и  освежить память. Но обращаться к главам без их предварительного чтения мы не советуем – вы просто рискуете не до конца понять описываемую концепцию. Представляем вам описание глав этой книги: „ глава 1 содержит краткое введение в  DAX с  несколькими разделами, предназначенными для тех, кто уже знаком с другими языками, такими как SQL, MDX или язык формул Excel. В этой главе мы не представляем какие-то новые концепции, а  описываем базовые отличия между DAX и другими языками программирования, которые может знать читатель; „ в главе 2 мы познакомим вас с языком DAX. Мы пройдемся по основным терминам вроде вычисляемых столбцов и мер, а также расскажем о функциях для перехвата ошибок в выражениях. Кроме того, здесь будут перечислены все основные функции языка; „ глава 3 будет посвящена основным табличным функциям. Многие функции DAX работают с таблицами и возвращают таблицы в качестве результата. Здесь мы опишем работу большинства табличных функций, а в главах 12 и 13 расскажем о более сложных функциях для работы с таблицами; „ в главе 4 мы впервые затронем тему контекстов вычисления. Данная концепция является основополагающей в DAX, так что эта глава и следующая, возможно, являются наиболее значимыми в этой книге; „ в главе 5 мы ограничимся всего двумя функциями – и  . Это наиболее важные функции в DAX, и к их изучению можно приступать только после усвоения концепции контекстов вычисления; „ глава 6 будет посвящена переменным. Мы используем переменные в  примерах на протяжении всей книги, но именно в  этой главе познакомим вас с  их синтаксисом и  объясним назначение. Вы сможете возвращаться к этой части книги, когда будете встречаться с переменными в последующих главах; „ в главе 7 мы обсудим сладкую парочку из итерационных функций и функции CALCULATE, союз которых поистине был заключен на небесах. Использование итерационных функций совместно с техникой преобразования контекста позволит вам извлечь максимум пользы из языка DAX. В этой главе мы продемонстрируем несколько примеров, позволяющих реализовать весь потенциал данной связки; „ в главе 8 мы подробно остановимся на функциях логики операций со временем. Нарастающие итоги с  начала года и  месяца, показатели предыдущих лет, недельные интервалы и нестандартные календари – все это будет рассмотрено в этой части книги; „ глава 9 будет посвящена относительно новой особенности языка DAX – группам вычислений. Это очень мощный инструмент моделирования данных. В данной главе мы рассмотрим создание и использование групп 24

редисловие к ерво

издани

„ „ „ „

„

„ „

„

„ „

вычислений, познакомим вас с базовыми концепциями и представим несколько примеров; в главе 10 мы более подробно поговорим об особенностях использования контекста фильтра, привязке данных и  других полезных средствах для расчета сложных формул; в главе 11 вы научитесь проводить вычисления над иерархиями и работать со структурами родитель/потомок в DAX; главы 12 и 13 посвящены продвинутым табличным функциям, полезным как при написании запросов, так и при проведении сложных вычислений; прочитав главу 14, вы продвинетесь на шаг вперед в понимании контекстов вычисления, а  заодно узнаете об использовании сложных функций и  и концепции рас иренн та ли (expanded tables). Это глава для опытных пользователей, раскрывающая секреты сложных выражений DAX; глава 15 посвящена управлению связями в DAX. Благодаря этому языку в  модели данных можно создавать связи всех возможных типов. Здесь мы также приведем описание всех типов связей, которые допустимо использовать в моделях; в главе 16 мы приведем несколько примеров сложных расчетов с использованием DAX. Это будет последняя глава, посвященная непосредственно языку, и в ней мы расскажем о разных решениях и новых идеях; в главе 17 мы приведем детальное описание движка (engine) VertiPaq, являющегося самым распространенным движком ранили а данн (storage engine) в моделях с использованием DAX. Понимание особенностей движка позволит вам извлекать максимум потенциала из языка; в главе 18 мы воспользуемся знаниями, полученными в  предыдущей главе, чтобы продемонстрировать возможные способы оптимизации на уровне модели данных. Вы узнаете, как снизить количество уникальных значений в  столбце, выбрать столбцы для импорта и  повысить эффективность системы за счет выбора правильных типов связей и снижения количества используемой памяти в DAX; в главе 19 вы научитесь читать лан в олнени а росов (query plan) и  замерять производительность выражений на DAX при помощи DAX Studio и SQL Server Profiler; в главе 20 мы покажем вам несколько техник по оптимизации модели с  использованием знаний, полученных в  предыдущих главах. Мы продемонстрируем разные выражения на DAX, проанализируем их производительность, а затем представим оптимизированные варианты формул.

Условные обозначения В этой книге приняты следующие условные обозначения: „ ир помечен текст, который вводите вы; „ курсив используется для обозначения новых терминов, а также названия мер, вычисляемых столбцов, таблиц и баз данных; редисловие к ерво

издани

25

„ первые буквы в  названиях диалоговых окон, их элементов, а также команд – прописные. Например, в диалоговом окне S A ... (Сохранить как…); „ названия вкладок на ленте даются ПРОПИСН МИ БУКВАМИ; „ комбинации нажимаемых клавиш на клавиатуре обозначаются знаком плюс ( ) между названиями клавиш. Например, Ctrl A D означает, что вы должны одновременно нажать клавиши Ctrl, A и D .

Сопутству

ий контент

Для развития ваших навыков и подкрепления их практикой мы снабдили книгу сопутствующим контентом, который можно скачать по ссылке: MicrosoftPressStore.com/DefinitiveGuideDAX/downloads. Представленный архив содержит: „ бэкап базы данных Contoso Retail DW в  формате SQL Server, который вы можете использовать для самостоятельной проверки примеров. Это стандартная демонстрационная база от Microsoft, которую мы расширили путем добавления нескольких редставлений (view) для облегчения создания на ее основе модели данных; „ файлы в формате Power BI Desktop для всех примеров из этой книги. Каждому рисунку соответствует отдельный файл. Модель данных при этом практически не меняется, но вы можете использовать эти файлы для самостоятельного выполнения всех шагов, описанных в книге.

ГЛ А В А 1

Что такое DAX? DAX, или в ражени анали а данн (Data Analysis eXpressions), – это язык программирования в средах Microsoft Power BI, Microsoft Analysis Services и Microsoft Power Pivot для Excel. Он был создан в  2010 году – с  первым выходом надстройки PowerPivot для Microsoft Excel 2010. Да, тогда название PowerPivot писалось слитно, а пробел появился лишь через три года. С тех пор язык DAX постоянно набирал популярность как в среде пользователей Excel, применяющих его для создания моделей данных в Power Pivot, так и в сообществе бизнесаналитики, где этот язык используется для проектирования моделей в Power BI и Analysis Services. DAX присутствует во многих инструментах, которые объединяет один та ли н й движок (Tabular). Именно поэтому мы будем часто говорить просто о табличных моделях, подразумевая все инструменты сразу. DAX – простой язык. При этом он существенно отличается от других языков программирования, так что на освоение его новых концепций у вас может уйти немало времени. По опыту преподавания DAX тысячам студентов мы можем заметить, что с основами языка проблем обычно не возникает – можно приступить к его использованию уже через несколько часов после начала обучения. Что касается продвинутых тем вроде контекста вычисления, итерационных функций и преобразования контекста, они могут вызвать серьезные затруднения. Но не сдавайтесь! Наберитесь терпения. Когда вы вникнете в эти концепции, вы поймете всю простоту языка DAX. К нему нужно просто привыкнуть. В начале первой главы мы расскажем о том, что представляет из себя модель данных с таблицами и связями. Мы советуем прочитать эти страницы всем, независимо от опыта, чтобы понять, какую терминологию мы будем использовать на протяжении всей книги, описывая таблицы, модели и разные типы связей. В следующих разделах мы дадим полезные советы читателям, имеющим определенные навыки работы с другими языками, такими как SQL, MDX и язык формул Microsoft Excel. Каждому из этих языков мы отведем отдельный раздел, чтобы читатели могли сравнить их с DAX. Если вам это поможет, попробуйте смотреть на DAX через призму этих языков. Прочитав заключительный раздел «DAX для пользователей Power BI», переходите к следующей главе, с которой, по сути, и начинается наше путешествие в мир DAX.

Введение в модель данных Язык DAX предназначен для расчета бизнес-показателей посредством формул в модели данных. Некоторые читатели могут знать, что из себя представляет модель данных. Для остальных мы сделаем разъяснение. 1

то такое

27

Модел данн (data model) – это набор таблиц, объединенных связями. Все мы знаем, что такое таблица. Это перечисление строк, содержащих информацию, при этом каждая строка поделена на столбцы. Столбец, в  свою очередь, характеризуется определенным типом данных и  содержит единый фрагмент информации. Обычно мы называем строку в таблице записью. Табличный способ хранения информации очень удобен в плане организации данных. По сути, таблица сама по себе является моделью данных, пусть и в своей простейшей форме. А значит, когда мы вводим на лист Excel текст и цифры, мы создаем модель данных. Если модель состоит из нескольких таблиц, вполне вероятно, что вам захочется связать их. в (relationship) представляет собой объединение двух таблиц. Такие таблицы мы называем св анн ми (related). Графически связь двух таблиц обозначается линией между ними. На рис. 1.1 показан пример модели данных.

Рис 1 1

Модель данн

, состо

а из

ести та ли

Далее перечислим важные аспекты связей между таблицами: „ таблицы, объединенные связью, выполняют разные роли. Одна из них представляет сторону «один», а вторая – «многие», которые помечены на схеме данных символами «1» и «*» (звездочка) соответственно. Обратите внимание на связь между таблицами (Товары) и  (Подкатегории товаров) на рис. 1.1. Одной подкатегории может принадлежать несколько товаров, тогда как один товар может представлять только одну подкатегорию. Таким образом, таблица являет собой сторону «один» в этой связи, а   – сторону «многие»; „ существуют особые виды связей. Это связи «один к одному» (1:1) и сла е св и (weak relationships). В связи «один к одному» обе таблицы представ28

1

то такое

ляют собой сторону «один», тогда как в  слабых связях они могут находиться на стороне «многие». Такие особые виды связей не слишком распространены, и мы подробно обсудим их в главе 15; „ столбцы, использующиеся для объединения таблиц и обычно имеющие одинаковые имена, называются кл ами (keys) связи. При этом в  ключевом столбце таблицы, представляющей сторону «один», должны находиться уникальные значения без пропусков. В то же время в таблице «многие» значения в ключевом столбце могут повторяться, и чаще всего это так и есть. Столбец, содержащий исключительно уникальные значения, называется кл ом таблицы; „ связи могут образовывать цепочки. Каждый товар принадлежит какойто подкатегории, которая, в  свою очередь, представляет определенную категорию товаров. Следовательно, каждый товар можно отнести к конкретной категории. Но чтобы получить ее название, необходимо пройти к ней от товаров через цепочку из двух связей. В модели данных, представленной на рис. 1.1, присутствует цепочка связей, состоящая сразу из трех звеньев, – от таблицы к  ; „ стрелкой посередине связи обозначается на равление ерекрестной фил т ра ии (cross filter direction). По рис. 1.1 видно, что связь между таблицами и  отмечена стрелками в обоих направлениях, тогда как остальные связи в модели – однонаправленные. Стрелкой обозначается направление распространения фильтра по этой связи. Поскольку выбор правильных направлений для фильтров является одним из важнейших навыков в  работе с  моделью данных, мы подробно обсудим эту тему в  следующих главах. Обычно мы не советуем пользователям включать двуна равленну фил тра и (bidirectional filtering) в связях, как сказано в главе 15. В этой модели такая связь присутствует исключительно в образовательных целях.

Введение в направление связи Каждая связь может характеризоваться однонаправленной или двунаправленной перекрестной фильтрацией (кросс-фильтрацией). Фильтр всегда распространяется от стороны «один» к стороне «многие». Если же связь двунаправленная, то есть обозначена на схеме двумя разнонаправленными стрелками, фильтр по ней может распространяться и в обратном направлении. Приведем пример, который поможет вам лучше разобраться в  этом. Если построить отчет на основе модели данных, представленной на рис. 1.1, вынеся годы ( ) на строки, а количество проданных товаров ( ) и количество наименований товаров ( ) – в область значений, мы увидим вывод, показанный на рис. 1.2. Столбец принадлежит таблице дат ( ). А поскольку таблица представляет сторону «один» в связи с продажами ( ), движок отфильтрует таблицу по годам. Именно поэтому количество проданных товаров в отчете показано с разбивкой по годам. С таблицей товаров ( ) дело обстоит несколько иначе. Фильтрация в  этом случае работает корректно, поскольку связь, объединяющая таблицы 1

то такое

29

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

Рис 1 2

От ет де онстрир ет

ект ильтра ии о нескольки та ли а

Если модифицировать отчет, вынеся на строки цвет товаров ( ) и добавив в область значений количество дат ( ), результат также поменяется. Вывод этого отчета можно видеть на рис. 1.3.

Рис 1 3 от ете оказано, то в отс тствие дв на равленно св зи ильтра и та ли не в олн етс

Столбец , вынесенный на строки отчета, принадлежит таблице . А поскольку представляет сторону «один» в связи с таблицей , значения в  столбце посчитались корректно. Поле правильно отфильтровалось, поскольку его источником является таблица , вынесенная на строки. Неожиданные значения мы видим в  столбце 30

1

то такое

. Здесь для всех строк указано одно и то же число, представляющее общее количество строк в таблице . Фильтр, идущий от столбца , не распространяется на , поскольку связь между таблицами и   – однонаправленная. Таким образом, несмотря на то что фильтр в таблице активен, он не может распространиться на таблицу по причине однонаправленности связи. Если сделать связь между таблицами и  двунаправленной, результат будет иным, что видно по рис. 1.4.

Рис 1 4 сли активировать дв на равленн ильтра и , та ли а Date дет от ильтрована о стол Color

Теперь в столбце отображается количество дней, когда был продан как минимум один товар выбранного цвета. На первый взгляд кажется, что стоит все связи в  модели сделать двунаправленными, чтобы позволить фильтрам распространять свое действие во все стороны и  доставать правильные данные. Как вы узнаете из этой книги, такой подход почти никогда не будет оправдан. Вы должны выбирать направление фильтрации для связей в  зависимости от модели, с которой работаете. Если вы последуете нашим советам, то откажетесь от применения двунаправленной фильтрации там, где это возможно.

DAX для пользователей Excel Велика вероятность, что вы знакомы с языком формул Excel, который немного напоминает DAX. В  конце концов, корни DAX лежат в  Power Pivot для Excel, 1

то такое

31

и разработчики сделали все, чтобы эти языки были похожими. Эти сходства облегчат вам переход на DAX. Но не стоит забывать и о различиях в этих языках.

чейки против таблиц В Excel все вычисления производятся над ячейками, которые обладают координатами. Так что мы можем написать формулу вроде этой: = (A1 * 1.25) - B2

В DAX концепция ячеек с координатами просто отсутствует. Этот язык работает с таблицами и столбцами, а не с отдельными ячейками. Как следствие выражения DAX обращаются именно к таблицам и столбцам, что сказывается на синтаксисе языка. Однако концепция таблиц и столбцов не нова для Excel. Если выделить диапазон и воспользоваться пунктом (Форматировать как таблицу), можно писать формулы в Excel, обращающиеся непосредственно к таблицам и столбцам. На рис. 1.5 в столбце вычисляется выражение, ссылающееся на столбцы в той же таблице, а не на ячейки в рабочей книге.

Рис 1 5

ор

ла

ожно сс латьс на стол

та ли

В Excel можно обращаться к  столбцам, используя следующий формат: . Здесь ColumnName – название столбца, а символ говорит о том, что необходимо взять значение из текущей строки. Синтаксис получился не самым интуитивно понятным, но мы обычно и не пишем такие выражения вручную. Они появляются автоматически при нажатии на ячейку: Excel сам заботится о вставке нужного кода. Таким образом, в Excel есть два разных вида вычислений. Можно использовать стандартное обращение к ячейкам – в этом случае формула для ячейки F4 будет выглядеть так: E4*D4. Или же применять ссылки на столбцы внутри таблицы. Это позволит использовать одинаковые выражения во всех ячейках 32

1

то такое

столбца, а Excel в своих расчетах будет брать значение из конкретной строки. В отличие от Excel, DAX работает исключительно с таблицами. Все формулы должны ссылаться на столбцы внутри таблиц. Например, в DAX предыдущая формула будет выглядеть так: Sales[SalesAmount] = Sales[ProductPrice] * Sales[ProductQuantity]

Как видите, каждое название столбца предваряется наименованием соответствующей таблицы. В  Excel мы не указываем названия таблиц, поскольку там формулы работают внутри одной таблицы. DAX же работает в модели данных, состоящей из нескольких таблиц. Как следствие мы просто обязаны конкретно указывать таблицы, ведь в разных таблицах могут находиться столбцы с одинаковыми названиями. Многие функции DAX работают подобно аналогичным функциям в  Excel. К примеру, функция в обоих языках применяется одинаково: Excel ЕСЛИ ( [@SalesAmount] > 10; 1; 0) DAX IF ( Sales[SalesAmount] > 10; 1; 0)

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

Рис 1 6

ожно сослатьс на весь стол е , о стив си вол

в ор

ле

Значение столбца одинаковое для всех строк и равно общему итогу по столбцу . Иными словами, в Excel существует четкое синтаксическое разграничение между обращением к  ячейке в  конкретной строке и к столбцу в целом. 1

то такое

33

DAX ведет себя иначе. В  этом языке для вычисления столбца рис. 1.6 можно было бы использовать следующую формулу:

из

AllSales := SUM ( Sales[SalesAmount] )

И здесь нет никаких отличий между извлечением значения из текущей строки или из всего столбца. DAX понимает, что мы хотим просуммировать все значения из столбца, поскольку его название передается в качестве аргумента в агрегирующую функцию (здесь это функция ). Таким образом, если Excel требует явного указания, какие данные извлекать из столбца, DAX решает эту неоднозначность автоматически. Такая разница в  подходах к  вычислениям может приводить в замешательство – по крайней мере, поначалу.

Excel и DAX: два функциональных языка В чем язык формул Excel и DAX похожи, так это в том, что оба они являются функциональными языками программирования. Функциональные языки состоят из выражений, в основе которых лежат вызовы функций. В Excel и DAX не реализованы концепции операторов, циклов и переходов, характерные для большинства языков программирования. В DAX буквально все является выражениями. Это бывает непросто понять тем, кто приходит из других языков программирования, а для пользователей Excel, наоборот, должно быть привычно.

Итерационные функции в DAX Концепция, которая может оказаться для вас в новинку, – это итерационные функции, или просто итератор (iterators). В Excel все расчеты выполняются последовательно, по одному за раз. В предыдущем примере вы видели, что для того, чтобы рассчитать итог по продажам, мы создали столбец, в котором цена умножалась на количество. На втором шаге мы подсчитывали сумму по этой колонке. Получившийся результат впоследствии можно использовать в качестве знаменателя при подсчете, например, доли продаж по каждому товару. В DAX все это можно сделать за один шаг с помощью итерационных функций. Итератор делает ровно то, что и должен, исходя из названия, – проходит по таблице и производит вычисления в каждой строке, одновременно агрегируя запрошенное значение. Таким образом, вычисления из предыдущего примера можно произвести при помощи одной итерационной функции : AllSales := SUMX ( Sales; Sales[ProductQuantity] * Sales[ProductPrice] )

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

1

то такое

является то, что программирование на DAX менее визуально по сравнению с формулами Excel. Мы ведь даже не видим столбца с результатом умножения цены на количество – он существует только во время вычисления. Как вы узнаете позже, у вас есть возможность создания вычисляемых столбцов для хранения подобных промежуточных вычислений. Но делать это не рекомендуется, поскольку тогда будут задействованы дополнительные ресурсы памяти и  может пострадать производительность, если вы не используете режим DirectQuery совместно с агрегациями, о чем мы поговорим в главе 18.

DAX требует изучения теории Будем откровенны: DAX является не единственным языком программирования, для использования которого вам понадобится обширная теоретическая база. Разница лишь в подходе. Признайтесь, вы ведь частенько ищете в интернете сложные формулы и  шаблоны, которые помогут вам в  решении вашего собственного сценария. И шансы на то, что вы найдете подходящую формулу для Excel, достаточно высоки – вам останется лишь адаптировать ее под свои нужды. Но в DAX дела обстоят иначе. Вам придется досконально изучить этот язык и понять, как работает контекст вычисления, чтобы написать работающий код. Без должной теоретической базы вам может показаться, что DAX производит свои вычисления каким-то магическим образом или что он выдает цифры, не имеющие с реальностью ничего общего. Проблема не в DAX, а в том, что вы не понимаете всех тонкостей его работы. К счастью, теоретическая база языка DAX ограничивается всего несколькими концепциями, которые мы опишем в главе 4. Приготовьтесь много учиться. После освоения этой главы DAX перестанет быть для вас тайной, а мастерство его использования будет зависеть исключительно от приобретенного опыта. Помните: знание – всего лишь полдела. И не пытайтесь двигаться дальше, пока досконально не освоите концепцию контекстов вычисления.

DAX для разработчиков SQL Если вы знакомы с  языком SQL, значит, у  вас уже есть опыт работы со множеством таблиц и связей. В этом плане вы почувствуете себя в DAX как дома. По сути, вычисления здесь базируются на выполнении запросов к нескольким таблицам, объединенным связями, и агрегировании значений.

Работа со связями Первые отличия между SQL и DAX заметны в области организации связей в модели данных. В SQL можно настроить внешние ключи в таблицах для определения связей, но движок никогда не будет использовать эти связи без явного указания. Например, если у нас есть таблицы и  , и столбец является первичным ключом в  и внешним – в  , можно написать следующий запрос: 1

то такое

35

SELECT Customers.CustomerName, SUM ( Sales.SalesAmount ) AS SumOfSales FROM Sales INNER JOIN Customers ON Sales.CustomerKey = Customers.CustomerKey GROUP BY Customers.CustomerName

Хотя мы определили в  модели внешние ключи для осуществления связей, нам все равно необходимо всякий раз явно указывать в запросе условия для выполнения соединений. Это приводит к  увеличению объема запросов, зато можно каждый раз использовать разные условия для связей, что дает максимум свободы в извлечении данных. В DAX связи являются составной частью модели данных, и  все они – . А раз так, вам нет необходимости каждый раз указывать их в запросе, DAX автоматически будет использовать связи при задействовании объединенных таблиц. Так что на DAX можно переписать предыдущий запрос SQL следующим образом: EVALUATE SUMMARIZECOLUMNS ( Customers[CustomerName]; "SumOfSales", SUM ( Sales[SalesAmount] ) )

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

DAX как функциональный язык SQL – декларативный язык. Вы определяете набор данных, который желаете извлечь, посредством оператора , при этом не беспокоясь о  том, как именно движок это сделает. DAX, напротив, является функциональным языком. В нем каждое выражение является вызовом функции. При этом параметры функции, в свою очередь, также могут быть вызовами функций. Анализ всех этих параметров приводит к созданию сложного плана выполнения запроса, который и вычисляется движком DAX с целью получить результат. Например, если нам понадобится получить информацию о покупателях, живущих в Европе, мы можем написать следующий запрос на SQL: SELECT Customers.CustomerName, SUM ( Sales.SalesAmount ) AS SumOfSales

36

1

то такое

FROM Sales INNER JOIN Customers ON Sales.CustomerKey = Customers.CustomerKey WHERE Customers.Continent = 'Europe' GROUP BY Customers.CustomerName

В языке DAX мы не объявляем условие в  операторе WHERE. Вместо этого мы используем специальную функцию для осуществления фильтрации, как показано ниже: EVALUATE SUMMARIZECOLUMNS ( Customers[CustomerName]; FILTER ( Customers; Customers[Continent] = "Europe" ); "SumOfSales"; SUM ( Sales[SalesAmount] ) )

Вы видите, как работает функция : она возвращает только покупателей, проживающих в  Европе, как мы и  хотели. Порядок, в  котором мы встраиваем функции в код, и виды функций, которые используем, очень важны как с точки зрения получения результата, так и в плане производительности запросов. В языке SQL это тоже важно, хотя там мы больше надеемся на о тими атор а росов (query optimizer) при построении наилучшего плана выполнения. В DAX оптимизатор также занят своими прямыми обязанностями, но на вас как на разработчике лежит большая ответственность за написание быстро работающего кода.

DAX как язык программирования и язык запросов В SQL существует четкое разделение между языком запросов и  языком программирования, то есть набором инструкций, используемых для создания раним ро едур (stored procedures), редставлений (views) и других объектов в базе данных. В каждом диалекте SQL присутствуют свои операторы, призванные обогатить язык. Но в DAX не делается четких разграничений между языком запросов и языком программирования. Множество функций работают с таблицами и возвращают таблицы в качестве результата. Функция из предыдущего кода – лишь один из примеров. В этом отношении DAX, пожалуй, проще SQL. Изучая его как язык программирования – а им он изначально и является, – вы узнаете все необходимое для использования его и в качестве языка запросов.

Подзапросы и условия в DAX и SQL Одной из мощнейших особенностей языка запросов SQL является возможность использования подзапросов. В DAX применяется похожая концепция, но с учетом функциональной направленности языка. 1

то такое

37

Например, чтобы извлечь информацию о покупателях, сделавших покупки на сумму более 100, можно написать следующий запрос SQL: SELECT CustomerName, SumOfSales FROM ( SELECT Customers.CustomerName, SUM ( Sales.SalesAmount ) AS SumOfSales FROM Sales INNER JOIN Customers ON Sales.CustomerKey = Customers.CustomerKey GROUP BY Customers.CustomerName ) AS SubQuery WHERE SubQuery.SumOfSales > 100

В DAX можно добиться похожего эффекта с  использованием вложенных функций, как показано ниже: EVALUATE FILTER ( SUMMARIZECOLUMNS ( Customers[CustomerName]; "SumOfSales", SUM ( Sales[SalesAmount] ) ); [SumOfSales] > 100 )

В этом коде результаты подзапроса, извлекающего и  , прогоняются через функцию , которая оставляет в результирующем наборе только строки со значениями , превышающими 100. В данный момент вы можете не понимать, что делает этот код. Но, постепенно постигая все премудрости DAX, вы обнаружите, что в этом языке использовать подзапросы намного легче, чем в SQL, и код получается более естественным по причине функциональной природы DAX.

DAX для разработчиков MDX Многие специалисты в области бизнес-аналитики переключаются на DAX как на новый язык табличного движка Tabular. В прошлом они использовали язык для построения и обращения к многомерн м модел м данн (Multidimensional models) Analysis Services. Если вы из их числа, приготовьтесь изучать абсолютно новый язык, поскольку у DAX и MDX не так много общего. Более того, некоторые концепции в DAX будут сильно напоминать вам MDX, но смысл их будет совершенно иным. 38

1

то такое

По опыту можем сказать, что путь от MDX к DAX наиболее тернист. Чтобы изучить DAX, вам придется забыть все, что вы знаете о MDX. Выкиньте из головы многомерн е ространства (multidimensional spaces) и  приготовьтесь к приобретению новых знаний с нуля.

Многомерность против табличности MDX работает в многомерном пространстве, определенном моделью данных. Его форма зависит от и мерений (dimensions) и иерар ий (hierarchies), присутствующих в модели, и в свою очередь определяет систему координат многомерного пространства. Пересечения наборов элементов в разных измерениях определяют точки в многомерном пространстве. Может понадобиться немало времени, чтобы понять, что элемент любой иерархии атрибута – это не более чем точка в многомерном пространстве. В DAX все намного проще. Тут нет измерений, элементов и точек в многомерном пространстве. Да и самого многомерного пространства тоже нет. Есть иерархии, которые мы можем определять в модели данных, но они существенно отличаются от иерархий в MDX. Пространство DAX построено на таблицах, столбцах и  связях. Таблицы в  модели Tabular не являются ни гру ами мер (measure group), ни измерениями. Это просто таблицы, для проведения вычислений в которых вы можете сканировать их, фильтровать и суммировать значения. Все базируется на двух основных концепциях: таблицах и связях. Скоро вы узнаете, что с  точки зрения моделирования данных табличный движок предоставляет меньше возможностей по сравнению с многомерным. Но в данном случае это не означает, что в вашем распоряжении будет меньший аналитический потенциал, поскольку вы всегда можете использовать DAX в качестве языка программирования, чтобы обогатить модель данных. Истинный потенциал движка Tabular заключается в потрясающей скорости DAX. Обычно разработчики стараются не злоупотреблять языком MDX без необходимости, поскольку оптимизировать такие запросы бывает непросто. DAX, напротив, славится своим впечатляющим быстродействием. Так что большинство сложных вычислений вы будете производить не в модели данных, а в формулах DAX.

DAX как язык программирования и язык запросов И DAX, и MDX являются одновременно и языками программирования, и языками запросов. В MDX это разделение обусловлено наличием скриптов, в которых помимо базового языка MDX можно использовать специальные операторы вроде , применимые исключительно в скриптах. В запросах на извлечение данных в MDX вы пользуетесь оператором . В DAX все несколько иначе. Вы можете использовать его как язык программирования для определения вычисляемых столбцов, вычисляемых таблиц и мер. И если концепция вычисляемых столбцов и  таблиц является новинкой в  DAX, то меры очень напоминают вычисляемые элементы в MDX. Можно также использовать DAX в качестве языка запросов – например, для извлечения информации из модели Tabular при помощи луж от етов (Reporting Services). При этом в функциях DAX нет четкого разграничения в плане использования – все они 1

то такое

39

могут быть применены как в запросах, так и при вычислении выражений. Более того, в  модели Tabular можно также использовать запросы, написанные на языке MDX. Таким образом, хотя MDX и может использоваться с табличной моделью данных в качестве языка запросов, когда речь идет о программировании в среде Tabular, единственным вариантом является DAX.

Иерархии Производя большинство вычислений с помощью языка MDX, вы полагаетесь на иерархии. Если вам необходимо получить сумму продаж по предыдущему году, вам придется извлечь из иерархии и использовать это выражение для переопределения фильтра в MDX. Например, вы можете написать такую формулу для осуществления расчетов по предыдущему году на MDX: CREATE MEMBER CURRENTCUBE.[Measures].[SamePeriodPreviousYearSales] AS ( [Measures].[Sales Amount], ParallelPeriod ( [Date].[Calendar].[Calendar Year], 1, [Date].[Calendar].CurrentMember ) );

В мере используется функция , возвращающая соседний элемент относительно на иерархии . Таким образом, это вычисление базируется на иерархиях, определенных в модели. В DAX мы бы для этого использовали контекст фильтра и стандартные функции для работы с датой и временем, как показано ниже: SamePeriodPreviousYearSales := CALCULATE ( SUM ( Sales[Sales Amount] ); SAMEPERIODLASTYEAR ( 'Date'[Date] ) )

Можно произвести это вычисление разными способами, в  том числе при помощи функции , но идея остается прежней: вместо использования иерархий мы применяем фильтрацию таблиц. Это очень существенное различие, и вам, вероятно, будет не хватать иерархий в DAX, пока не привыкнете к новой для себя концепции. Еще одним весомым отличием между этими языками является то, что в MDX вы ссылаетесь на , тогда как функция агрегации, которая вам нужна, уже определена в модели. В DAX предопределенные агрегации не используются. Фактически, как вы заметили, вычисляемое выражение в  приведенном выше примере следующее: SUM(Sales[Sales Amount]). Никаких предопределенных агрегаций в  модели нет. Мы определяем их тогда, когда нам нужно. Всегда можно создать меру, вычисляющую сумму продаж, но эта тема выходит за рамки этого раздела и будет описана позже в данной книге. 40

1

то такое

Более существенным отличием DAX от MDX является то, что в MDX очень активно используется инструкция для реализации бизнес-логики (опять же с использованием иерархий), тогда как в DAX применяется совсем другой подход. Вообще, работы с иерархиями не хватает этому языку. Например, если нам нужно очистить меру на уровне , в MDX мы могли бы написать следующее выражение: SCOPE ( [Measures].[SamePeriodPreviousYearSales], [Date].[Month].[All] ) THIS = NULL; END SCOPE;

В DAX нет функций, похожих на SCOPE, и для получения аналогичного результата придется выполнить проверку контекста фильтра, как показано ниже: SamePeriodPreviousYearSales := IF ( ISINSCOPE ( 'Date'[Month] ); CALCULATE ( SUM ( Sales[Sales Amount] ); SAMEPERIODLASTYEAR ( 'Date'[Date] ) ); BLANK () )

Из кода функции понятно, что она возвратит результат, только если пользователь находится в календарной иерархии на уровне месяца или ниже. В противном случае функция вернет пустое значение ( ). Позже вы узнаете, как работает эта функция. Стоит отметить, что это выражение более уязвимо к  ошибкам, чем код на MDX. Да, честно говоря, языку DAX очень не хватает функций для работы с иерархиями.

Вычисления на конечном уровне Используя язык MDX, вы, возможно, привыкли избегать проведения расчетов на коне ном уровне (leaf-level) элементов. Это настолько медленная операция, что всегда будет предпочтительнее предварительно рассчитывать значения и использовать агрегацию для возврата результата. В DAX вычисления на конечном уровне работают невероятно быстро, а  предварительные агрегации служат другим целям и  используются только в  работе с  большими наборами данных. Вам придется несколько изменить подход к проектированию моделей данных. В большинстве случаев модели, идеально подходящие для многомерной среды SQL Server Analysis Services, будут не лучшим образом показывать себя в движке Tabular, и наоборот.

DAX для пользователей Power BI Если вы пропустили предыдущие разделы и сразу оказались здесь, что ж, приветствуем! Язык DAX является родным для Power BI. И если у вас нет опыта работы с Excel, SQL или MDX, Power BI станет для вас первой средой, в которой вы 1

то такое

41

сможете изучать DAX. В отсутствие навыков построения моделей данных при помощи других инструментов вам будет приятно узнать, что Power BI является мощнейшим средством анализа и моделирования, а DAX во всем ему помогает. Возможно, вы не так давно начали работать с Power BI, а сейчас хотите сделать очередной качественный шаг вперед. Если это так, приготовьтесь к увлекательному путешествию в мир DAX. Вот вам наш совет: не ожидайте, что уже через пару дней вы сможете писать сложный код на DAX. Этот язык потребует от вас полной концентрации и внимания, а на его освоение может уйти немало времени, включая практическую работу. По опыту можем сказать, что после проведения первых простых вычислений на DAX вы будете просто восхищены. Но восхищение пропадет, когда вы дойдете до изучения контекстов вычислений и функции  – наиболее сложных составляющих языка. В этот момент вам все покажется очень сложным. Но не отчаивайтесь! Большинство разработчиков DAX проходили через это. На этой стадии вы уже так много всего изучите, что бросать все будет просто жалко. Читайте и практикуйтесь снова и снова, и вы увидите, что озарение придет раньше, чем вы ожидаете. И тогда вы очень быстро завершите чтение этой книги – уже в статусе гуру по DAX. Контексты вычислений – это сердце языка DAX. Освоение их может занять много времени. Мы не знаем никого, кому удалось бы узнать все о DAX за пару дней. Но, как и с любым сложным делом, со временем вы научитесь получать наслаждение от мелочей. А когда решите, что знаете уже все, перечитайте книгу заново. Уверяем, вы найдете для себя массу полезных нюансов, которые при первом прочтении казались не такими важными, но с приобретением опыта смогут заиграть новыми красками. Насладитесь остатком этой книги!

ГЛ А В А 2

Знакомство с DAX В этой главе мы начнем говорить о DAX. Вы познакомитесь с синтаксисом языка, ключевыми различиями между вычисляемыми столбцами, мерами (которые в старых версиях Excel назывались вычисляемыми полями) и основными функциями DAX. Поскольку это лишь вводная глава, мы не будем слишком углубляться в работу функций, а  оставим это на потом. Здесь же мы ограничимся их перечислением и знакомством с языком в целом. Описывая особенности моделей данных в  Power BI, Power Pivot или Analysis Services, мы будем использовать термин Tabular, даже если та или иная особенность не присутствует во всех этих продуктах. Например, говоря «DirectQuery в  модели Tabular», мы будем иметь в виду режим DirectQuery в Power BI и Analysis Services, поскольку в Excel он не поддерживается.

Введение в вычисления DAX Перед тем как приступать к сложным формулам, необходимо изучить основы DAX. Сюда включается синтаксис языка, поддерживаемые типы данных, основные операторы и способы обращения к столбцам и таблицам. Именно этим концепциям будут посвящены следующие несколько разделов. Мы используем DAX для проведения вычислений в  столбцах таблиц. Мы можем агрегировать значения, производить подсчет или поиск нужных нам чисел, но в конечном счете мы работаем с таблицами и столбцами. Так что для начала нам надо понять, как правильно обращаться к столбцам. Обычно принято писать название таблицы в одинарных кавычках, за которыми идет наименование столбца, заключенное в квадратные скобки, как показано ниже: 'Sales'[Quantity]

При этом допустимо опускать кавычки, если название таблицы не начинается с цифры, не содержит пробелов и не является зарезервированным словом вроде или . Кроме того, название таблицы можно не указывать, если мы обращаемся к  столбцам или мерам внутри таблицы, где определена формула. Таким образом, [Quantity] будет вполне допустимым выражением, если оно определено в вычисляемом столбце или мере в таблице . И все же мы очень не советуем вам опускать названия таблиц в формулах. Сейчас мы не можем должным 2

нако ство с

43

образом аргументировать этот довод, но вы все поймете, когда прочитаете главу 5 «Введение в CALCULATE и CALCULATETABLE». Стоит отметить, что при чтении кода DAX очень важно уметь определять, где речь идет о  мерах (которые мы обсудим позже), а  где о  столбцах. Существует негласное правило всегда указывать названия таблиц в случае со столбцами и опускать их в мерах. Чем раньше вы примете эту доктрину, тем легче вам будет жить в мире DAX. Так что вам нужно поскорее привыкать к такому обращению к столбцам и мерам: Sales[Quantity] * 2 [Sales Amount] * 2

-- Это ссылка на столбец -- Это ссылка на меру

Вы поймете, почему приняты такие правила, после знакомства с концепцией преобразования контекста в главе 5. А пока просто доверьтесь нам и примите такое соглашение.

омментарии в DAX ред д е коде в в ерв е видели строки с ко ентари и в тот з к оддерживает как одностро н е, так и ногостро н е ко ентарии Одностро н е редвар тс знака и -- или //, ри то остав а с асть строки с итаетс ко ентарие = Sales[Quantity] * Sales[Net Price] -- Однострочный комментарий = Sales[Quantity] * Sales[Unit Cost] // Еще один пример однострочного комментария Многостро н е ко ентарии на ина тс си вола и /* и закан ива тс */ нализатор ро скает все содержи ое ежд ти и знака и, с ита его зако ентированн = IF ( Sales[Quantity] > 1; /* Первый пример многострочного комментария Здесь может быть написано что угодно, и оно будет проигнорировано анализатором DAX */ "Multi"; /* Типичным использованием многострочного комментария является изолирование части кода, который не будет выполняться Следующий оператор IF будет проигнорирован, поскольку он включен в многострочный комментарий IF ( Sales[Quantity] = 1; "Single"; "Special note" ) */ "Single" ) е старатьс из егать ко ентариев в кон е в ражени в о ределени ер, в исл е стол ов и та ли акие ко ентарии ог т ть росто не видн ро е того, они ог т не оддерживатьс вс о огательн и инстр ента и вроде , о которо расскаже озже в то главе

44

2

нако ство с

ипы данных DAX DAX может выполнять вычисления над данными семи разных типов. С течением времени Microsoft вводила разные названия для одних и тех же ти ов данн (data type), что привело к некоторой неразберихе. В табл. 2.1 показаны различные имена, под которыми можно встретить типы данных в DAX. АБЛИ А 2 1

и

данн ип данных в Power Pivot и Analysis Services

ип данных ип данных в DAX в Power BI Integer Decimal Currency

Соответству ий об епринятый тип данных например, в SQL Server

Decimal

Currency ,

,

Date

Boolean String Binary

ип данных об ектной модели Tabular Tabular b ect Model  T M int64

Boolean String – Binary

– Binary



В этой книге мы будем использовать типы данных из первой колонки табл. 2.1, следуя стандартам сообщества бизнес-аналитики. Например, в Power BI столбец, содержащий TRUE или FALSE, может характеризоваться типом данных TRUE/FALSE, тогда как в SQL Server он может представлять тип BIT. В то же время историческим и более употребимым типом для таких данных будет Boolean. В DAX используется очень мощная подсистема для работы с типами данных, так что вам не стоит особенно беспокоиться о них. Результирующий тип данных в выражении DAX выводится из типов составляющих частей выражений. Вам необходимо иметь это в  виду, если выражение вернет значение не того типа, который вы ожидали. В этом случае нужно проверить типы данных составных частей выражения. Например, если одним из слагаемых в сумме является дата, результат также будет иметь тип даты. Если же суммируются целые числа, результат окажется целочисленного типа, несмотря на использование того же оператора. Такое поведение именуется ерегру кой о ераторов (operator overloading) и показано на рис. 2.1, где в столбец записывается результат прибавления к  числа 7. Sales[OrderDatePlusOneWeek] = Sales[Order Date] + 7

Типом данных результирующего столбца будет дата. В дополнение к  перегрузке операторов DAX автоматически конвертирует строки в числовые значения и обратно, когда это необходимо. Например, если 2

нако ство с

45

использовать оператор &, предназначенный для конкатенации строк, DAX конвертирует аргументы в  строки. Следующее выражение вернет значение «54» как строку: 5

4

А если использовать оператор +, возвращенное значение окажется числовым: 5

4

Рис 2 1 ри авление елого исла к дате риводит к о разовани ново дат , отста е от на ально на заданное коли ество дне

Таким образом, тип результата в DAX зависит от оператора, а не от исходных столбцов, значения которых приводятся к нужному типу автоматически согласно требованиям выбранного оператора. И  хотя такое поведение анализатора внешне выглядит удобным, далее в этой главе вы увидите, к каким ошибкам может приводить автоматическое приведение типов. Также стоит отметить, что не все операторы поддерживают подобное поведение. Например, операторы сравнения не могут сравнивать строки с числами. Получается, что складывать строки с числами можно, а сравнивать – нет. Более подробную информацию по типам данных можно получить по ссылке: https://docs.microsoft. com/en-us/power-bi/desktop-data-types. С учетом сложности правил мы бы посоветовали вам вовсе избегать автоматического приведения типов данных. Если вам потребуется воспользоваться приведением типов, лучше контролировать этот процесс и  указывать все явно. Например, предыдущий пример можно переписать так: = VALUE ( "5" ) + VALUE ( "4" )

Тем, кто привык работать с Excel и другими языками, типы данных DAX могут показаться знакомыми. При этом нюансы работы с разными типами зависят от конкретного движка и могут различаться в Power BI, Power Pivot и Analysis Services. Больше информации по типам данных в Analysis Services можно найти по адресу: http://msdn.microsoft.com/en-us/libr 21 , а по Power BI: https://docs.microsoft.com/en-us/power-bi/desktop-data-types. Здесь же мы кратко расскажем о каждом типе данных. 46

2

нако ство с

Integer В DAX есть только один целочисленный тип данных , позволяющий хранить 64-битные значения. Во всех внутренних расчетах движок также использует 64-битные целые числа.

Deci al Тип данных призван хранить числа с плавающей запятой в формате двойной точности. Не путайте этот тип данных DAX с типами и  в  . Соответствующим типом данных для в  SQL является .

Currency Тип данных , также известный в Power BI как дес ти ное исло с фик сированной а той (Fixed Decimal Number), представляет числа с  четырьмя знаками после запятой, которые хранятся в виде 64-битных целых чисел, деленных на 10 000. Суммирование и  вычитание значений с  участием типа Currency игнорирует десятичные знаки в количестве больше четырех, а умножение и деление дают на выходе число с плавающей запятой, тем самым увеличивая точность значения. В  общем случае если необходимо использовать больше четырех десятичных знаков, нужно использовать тип . По умолчанию тип данных включает в себя символ валюты. Но можно использовать этот символ и с типами и  , так же, как применять тип без указания валюты.

DateTi e В DAX даты хранятся в типе данных . Внутренне этот тип хранится в  виде чисел с  плавающей запятой, целая часть которых равна количеству дней, прошедших до означенной даты с  30 декабря 1899 года, а дробная отражает долю последнего дня. Таким образом, часы, минуты и секунды переводятся в долю прошедшего дня. Следующее выражение возвращает текущую дату плюс один день (24 часа): = TODAY () + 1

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

Ошибка високосного года рогра лектронн та ли 1 2 , видев свет в 1 год , вкралась о и ка ранени ин ор а ии в ти е данн DateTime ри рас ета разра от ики ос итали 1 00 год как високосн , от он таки не вл лс ело в то , то оследни год столети ожет ть високосн только ри словии его делени ез остатка на 00

2

нако ство с

47

азра от ики ерво версии ленно со ранили т о и к , то о ес е ить сов ести ость с 12 с те ор кажда нова верси т нет за со о т застарел о и к из те же соо ражени сов ести ости а о ент издани книги, в 201 год , та о и ка со ран етс в дл о ратно сов ести ости с рис тствие то о и ки или же росто осо енности ожет риводить к нето ност во вре енн интервала рань е 1 арта 1 00 года ак то ерво о и иально оддерживае о дато в вл етс 1 арта 1 00 года ислени , роизводи е до то дат , ог т риводить к о и о н рез льтата , и здесь н жно ро вл ть оль осторожность

Boolean Тип данных предназначен для хранения логических выражений. Например, тип вычисляемого столбца со следующей формулой будет установлен в  : = Sales[Unit Price] > Sales[Unit Cost]

Также значения типа могут быть отражены как числа: как 1, а  как 0. Такая нотация иногда оказывается полезной – например, для сортировки, поскольку TRUE FALSE.

String Все строки в DAX хранятся в кодировке , в которой каждый символ занимает 16 бит. По умолчанию операция сравнения между строками в DAX не чувствительна к регистру, так что строки «Power BI» и «POWER BI» будут считаться идентичными.

Variant Тип данных используется для выражений, которые могут возвращать значения разных типов в  зависимости от внешних условий. Например, следующее выражение может вернуть как целое число, так и строку, так что тип возвращаемого значения будет : IF ( [measure] > 0; 1; "N/A" )

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

Binary Тип данных используется в модели данных для хранения изображений и другой неструктурированной информации. В DAX этот тип недоступен. Он главным образом использовался в  Power View, но в  других средствах вроде Power BI не применялся.

Операторы DAX Теперь, когда вы понимаете всю важность о ераторов для определения типов данных результатов выражений, пришло время познакомиться с ними самими. В табл. 2.2 представлен список операторов, доступных в DAX. 48

2

нако ство с

АБЛИ А 2 2

О ератор

ип оператора Ско ки

Символ Использование ор док ред ествовани о ера и и гр ировка арг ентов ри ети еские Сложение итание ножение / еление авно Сравнение = е равно

Боль е > Боль е или равно >= Мень е < Мень е или равно Строкова & онкатена и строк конкатена и оги еские && оги еское ежд дв || лев и в ражени и оги еское ежд дв лев и в ражени и а ождение ле ента в с иске оги еское отри ание

Пример 2 2 2 2 0 100 0 100

0 0

, 0

Также логические операции доступны в DAX в качестве функций с синтаксисом, похожим на Excel. Например, можно написать такие выражения: AND ( [CountryRegion] = "USA"; [Quantity] > 0 ) OR ( [CountryRegion] = "USA"; [Quantity] > 0 )

Эти выражения полностью эквивалентны приведенным ниже: [CountryRegion] = "USA" && [Quantity] > 0 [CountryRegion] = "USA" || [Quantity] > 0

Использование функций вместо операторов при вычислении булевой логики помогает при написании сложных формул. Фактически когда дело касается форматирования объемных фрагментов кода, функции использовать бывает легче, и читаются они лучше операторов. Главным недостатком функций является то, что они могут принимать только два аргумента. Так что для сравнения более двух составляющих придется вкладывать одну функцию в другую.

онструкторы таблиц В DAX существует возможность создания анонимн та ли (anonymous tables) прямо в коде. Если в предполагаемой таблице должен быть только один столбец, можно написать значения через точку с запятой, по одному для каждой строки, и заключить список в фигурные скобки. Допустимо каждое значение в списке заключать в круглые скобки, но для таблицы с одним столбцом это не обязательно. Таким образом, следующие две строки кода эквивалентны: 2

нако ство с

49

{ "Red"; "Blue"; "White" } { ( "Red" ); ( "Blue" ); ( "White" ) }

Если в таблице будет несколько столбцов, внутренние скобки обязательны. При этом значения в строках для одного и того же столбца должны быть одного типа. В противном случае DAX приведет все значения к обобщенному типу данных, подходящему для всех строк в столбце. { ( "A"; 10; 1,5; DATE ( 2017; 1; 1 ); CURRENCY ( 199,99 ); TRUE ); ( "B"; 20; 2,5; DATE ( 2017; 1; 2 ); CURRENCY ( 249,99 ); FALSE ); ( "C"; 30; 3,5; DATE ( 2017; 1; 3 ); CURRENCY ( 299,99 ); FALSE ) }

Конструкторы таблиц часто используются с оператором дующий синтаксис вполне приемлем в выражениях DAX:

. Например, сле-

'Product'[Color] IN { "Red"; "Blue"; "White" } ( 'Date'[Year]; 'Date'[MonthNumber] ) IN { ( 2017; 12 ); ( 2018; 1 ) }

Вторая строка в приведенном выше примере демонстрирует синтаксис для сравнения набора столбцов или кортежа (tuple) с  использованием оператора . Такой синтаксис не может быть использован с операторами сравнения. Иными словами, следующее выражение в DAX недопустимо: ( 'Date'[Year]; 'Date'[MonthNumber] ) = ( 2007; 12 )

Но вы всегда можете переписать это выражение с применением оператора с конструктором таблицы из одной строки, как в примере ниже: ( 'Date'[Year]; 'Date'[MonthNumber] ) IN { ( 2007; 12 ) }

Условные операторы В DAX вы можете создавать условные выражения при помощи функции . Например, можно написать выражение, возвращающее строку «MULTI» или «SINGLE» в зависимости от того, превышает ли количество товаров единицу. IF ( Sales[Quantity] > 1; "MULTI"; "SINGLE" )

У функции три параметра, при этом обязательными из них являются только первые два. Третий опциональный и  по умолчанию принимает значение . Взгляните на следующий код: IF ( Sales[Quantity] > 1; Sales[Quantity] )

50

2

нако ство с

Он абсолютно равнозначен своей полной версии: IF ( Sales[Quantity] > 1; Sales[Quantity]; BLANK () )

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

Вычисляемые столбцы В зависимости от используемого инструмента вы можете создавать вычисляемые столбцы разными способами. При этом концепция не меняется: в исл ем й стол е (calculated column) представляет собой еще один столбец в модели данных, содержимое которого не загружается из источника, а вычисляется посредством формулы DAX. Вычисляемый столбец во многом похож на любой другой столбец в таблице, и мы можем использовать его в строках, колонках, фильтрах и области значений матри (matrix) или любого другого отчета. Более того, можно даже строить связи на основании вычисляемых столбцов. Выражения DAX, определенные для вычисляемого столбца, производят вычисления в контексте текущей строки таблицы, которой принадлежит этот столбец. Любое обращение к этому столбцу вернет его значение для текущей строки. У нас нет возможности напрямую обратиться к значениям в других строках. Если вы используете режим им орта (Import Mode), установленный в Tabular по умолчанию, а не DirectQuery, важно будет помнить, что вычисляемые столбцы рассчитываются во время загрузки информации из базы данных и затем сохраняются в модели данных. Такое поведение может показаться странным, если вы привыкли к вычисляемым колонкам в SQL, которые рассчитываются в момент выполнения запроса и не занимают память. В моделях Tabular все вычисляемые столбцы хранятся в  памяти и  рассчитываются в  момент обработки таблицы. Такое поведение полезно, когда мы имеем дело со сложными вычисляемыми столбцами. В этом случае время на сложные расчеты будет расходоваться во время загрузки данных в модель, а не во время запросов, что повысит быстродействие отчетов. Но не стоит забывать о том, что вычисляемые столбцы расходуют драгоценную оперативную память. Например, если у нас есть вычисляемый столбец со сложной формулой, можно поддаться соблазну и разбить расчеты на несколько промежуточных вычисляемых столбцов. Если в процессе 2

нако ство с

51

разработки проекта такое решение допустимо, то в финальном продукте лучше избегать подобных методов, поскольку каждое промежуточное вычисление сохраняется в оперативной памяти, расходуя тем самым драгоценные ресурсы. В случае с  моделями, основанными на DirectQuery, дело обстоит иначе. В этом режиме расчеты в вычисляемых столбцах производятся «на лету», в момент обращения движка Tabular к  источнику данных. Это может приводить к  образованию тяжелых запросов, выполняемых в  источнике, что негативно сказывается на быстродействии отчетов.

Расчет длительности выполнения заказа редставьте, то нас в та ли е Sales ран тс дата заказа и дата оставки с ольз ти два стол а, ожно легко в ислить длительность в олнени заказа в дн оскольк дат ран тс в виде коли ества дне , ро ед и с 0 дека р 1 года, оже ол ить разни в дн ежд дв дата и те о ного в итани Sales[DaysToDeliver] = Sales[Delivery Date] - Sales[Order Date] о из за того, то о а стол а и е т ти дат , рез льтат также окажетс дато , а дл еревода его в елое исло н жно дет вос ользоватьс нк ие риведени ти ов Sales[DaysToDeliver] = INT ( Sales[Delivery Date] - Sales[Order Date] ) ез льтат в

ислени

оказан на рис 2 2

Рис 2 2 итание одно дат из др го с ослед и ти а озволило на ол ить разни ежд дата и в дн

рео разование

Меры Вычисляемые столбцы – безусловно, очень удобный и полезный инструмент, но производить вычисления в модели данных можно и другим способом. Если вы не хотите рассчитывать значение для каждой строки, а  вместо этого вам может понадобиться агрегировать данные по нескольким строкам, ваш выбор – мера (measure). Например, вы можете определить несколько вычисляемых столбцов в таблице Sales для расчета валовой ри ли (gross margin): Sales[SalesAmount] = Sales[Quantity] * Sales[Net Price] Sales[TotalCost] = Sales[Quantity] * Sales[Unit Cost] Sales[GrossMargin] = Sales[SalesAmount] – Sales[TotalCost]

52

2

нако ство с

А что произойдет, если вы захотите увидеть валовую прибыль в  процентном отношении к сумме продаж? Вы могли бы создать для этого вычисляемый столбец со следующей формулой: Sales[GrossMarginPct] = Sales[GrossMargin] / Sales[SalesAmount]

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

Рис 2 3

стол

е

равильн е зна ени в строка , но итог о и о н

Итоговое значение рассчитывается как сумма процентов по всем строкам. А  значит, когда нам необходимо агрегировать значения в  процентах, мы не можем полагаться на содержимое вычисляемых столбцов. Вместо этого в своих расчетах мы должны опираться на суммы по столбцам. Здесь, к  примеру, нам необходимо поделить сумму по столбцу GrossMargin на сумму по столбцу SalesAmount. Мы должны включать в  расчеты агрегированные значения, а агрегация по вычисляемым столбцам здесь не годится. Иными словами, нужно вычислить отношение сумм, а не сумму отношений. Было бы неправильно также просто изменить тип агрегации в  столбце GrossMarginPct на расчет среднего значения – в этом случае вычисление также окажется неверным. Отчет с усредненными итогами показан на рис. 2.4, и вы можете легко проверить, что результат вычисления (330,31 / 732,23) должен быть не 45,96 , как показано, а 45,11 . Правильным решением будет создать GrossMarginPct в виде меры: GrossMarginPct := SUM ( Sales[GrossMargin] ) / SUM (Sales[SalesAmount] )

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

нако ство с

53

ли, что для создания меры мы использовали знак := вместо обычного =. Таким стандартом мы будем пользоваться на протяжении всей книги, и это позволит вам отличать в коде вычисляемые столбцы от мер.

Рис 2 4

С ена

нк ии агрегировани на

не дала н жного рез льтата

После объявления GrossMarginPct в  качестве меры вы можете построить корректный отчет, показанный на рис. 2.5.

Рис 2 5

Мера

дала равильн

рез льтат

И вычисляемые столбцы, и  меры используют выражения DAX, разница состоит в  контексте вычисления. Мера рассчитывается в  контексте видимого элемента или в  контексте запроса DAX, тогда как вычисляемый столбец – 54

2

нако ство с

в контексте строки таблицы, которой он принадлежит. Контекст видимого элемента (позже вы узнаете, что его также называют контекстом фильтра) зависит от выбора пользователя в  отчете или формата запроса DAX. Таким образом, используя для меры выражение SUM(Sales[SalesAmount]), мы указываем движку провести агрегацию по всем видимым элементам. Если же в формуле вычисляемого столбца написать Sales[SalesAmount], будет вычисляться значение столбца SalesAmount из этой таблицы в текущей строке. Мера должна быть определена внутри таблицы. Это одно из требований языка DAX. В то же время нельзя сказать, что мера принадлежит конкретной таблице, поскольку мы можем при желании перемещать ее из таблицы в таблицу без потери функциональности.

Различия между вычисляемыми столбцами и мерами ес отр на все свои с одства, ежд в исл е и стол а и и ера и есть одно с ественное разли ие на ение в в исл е о стол е расс ит ваетс в о ент о новлени данн , и в ка естве контекста ис ольз етс контекст строки ез льтат в то сл ае не зависит от де стви ользовател Мера, в сво о ередь, о ерир ет агрегированн и данн и в тек е контексте атри е или сводно та ли е, к ри ер , ис одн е та ли от ильтрован в соответствии с координата и еек, и данн е агрегирован и расс итан с ис ользование ти ильтров н и слова и, ера всегда о ерир ет агрегированн и данн и в контексте в ислени , с котор ознакои с в главе

Выбор между вычисляемыми столбцами и мерами Теперь, когда вы знаете отличия между вычисляемыми столбцами и мерами, поговорим о том, когда и что нужно использовать. Иногда это будет не принципиально, но в  большинстве случаев особенности вычислений будут однозначно определять правильность выбора. Как разработчик вы должны отдавать предпочтение вычисляемым столбцам, если хотите: „ использовать вычисленные результаты в качестве срезов, размещать их на строках или столбцах (в отличие от области значений) в матрице или сводной таблице, а также применять их в качестве фильтров в запросах DAX; „ определить выражение, строго привязанное к конкретной строке. Например, выражение Price * Quantity не может вычисляться на основании средних значений или сумм исходных столбцов; „ хранить категории. Это могут быть диапазоны значений для меры, диапазоны возрастов покупателей (0–18, 18–25 и т. д.). Такие категории часто используются в качестве фильтров или срезов. В то же время вам придется воспользоваться мерой, если вы хотите отображать показатели, реагирующие на выбор пользователя или выступающие в качестве агрегируемых значений в отчетах, например: „ для вывода процента прибыли по выбранным в отчете фильтрам; „ для расчета отношения показателя по одному товару ко всему ассортименту при сохранении фильтра по году и региону. 2

нако ство с

55

Многие расчеты можно произвести как с  использованием вычисляемого столбца, так и посредством меры, но при этом нужно использовать разные выражения DAX. Например, мы могли бы определить GrossMargin как вычисляемый столбец со следующей формулой: Sales[GrossMargin] = Sales[SalesAmount] - Sales[TotalProductCost]

А в качестве меры выражение было бы иным: GrossMargin := SUM ( Sales[SalesAmount] ) - SUM ( Sales[TotalProductCost] )

В этом случае мы посоветовали бы использовать меру. Будучи вычисляемой в  момент запроса, она не потребует для хранения дополнительной памяти и дискового пространства. Помните: если вы можете произвести вычисления обоими способами, лучше отдавать предпочтение мере. Вычисляемыми столбцами нужно пользоваться только в  особых случаях, когда это действительно необходимо. Пользователи с опытом работы в Excel обычно предпочитают вычисляемые столбцы мерам, поскольку они очень напоминают им привычные вычисления в своей родной стихии. И все же лучшим выбором для произведения расчетов в DAX является мера. аз еетс , в свои рас ета ера ожет о ра атьс к одно или нескольки в исл е стол а О ратное также воз ожно, сть и не столь о евидно е ствительно, в в исл е о стол е ожно сс латьс на ер то сл ае ера дет в исл тьс в контексте тек е строки, а рез льтат дет со ран тьс в стол е и не дет зависеть от в ора ользовател оне но, только о ределенн е о ераии с ера и с осо н в то сл ае дать зна и е рез льтат , оскольк в ислени в ера о но строго завис т от в ора ользовател ро е того, вс ки раз, когда в ис ольз ете ер вн три в исл е ого стол а, в олагаетесь на рео разование контекста, то вл етс родвин то те нико в ислени в еред ти насто тельно реко енд е ва ро итать и досконально своить атериал етверто глав , в которо одро но о ис ва тс контекст в ислени и рео разование контекста

Введение в переменные При написании выражений DAX можно избежать включения в них повторного кода и тем самым улучшить читаемость формул за счет использования переменных. Взгляните на следующие выражения: VAR TotalSales = SUM ( Sales[SalesAmount] ) VAR TotalCosts = SUM ( Sales[TotalProductCost] ) VAR GrossMargin = TotalSales - TotalCosts RETURN GrossMargin / TotalSales

Переменн е (variables) в  языке DAX определяются ключевым словом . После объявления переменных обязательным является включение секции, 56

2

нако ство с

начинающейся с ключевого слова , для определения результата выражения. При этом вы можете объявить сразу несколько переменных, которые будут храниться локально – в выражении, в котором определены. К переменной, объявленной внутри выражения, нельзя обращаться извне. В DAX не предусмотрены глобальные переменные. Это означает, что вы не можете объявить переменные, которые можно будет использовать во всей модели. Применительно к переменным в DAX используется так называемое лени вое в исление (lazy evaluation), также именуемое отложенным. Азначит, если вы объявили переменную, которая не используется в коде, ее значение не будет вычислено. Когда переменная потребуется в дальнейших расчетах, она будет инициализирована, но только один раз, а при повторном обращении к ней будет использовано уже рассчитанное ранее значение. Таким образом, применение переменных позволяет оптимизировать код при наличии сложных повторяющихся вычислений. Переменные являются важным инструментом в DAX. Как вы узнаете из главы 4, использовать переменные бывает очень полезно, поскольку в них применяется контекст в ислени (evaluation context) вместо контекста, в котором используется переменная. В  главе 6 мы подробно расскажем о  переменных и их использовании. Кроме того, мы будем пользоваться переменными на протяжении всей книги.

Обработка ошибок в выражениях DAX Вы уже усвоили основы синтаксиса DAX, а теперь пришло время узнать, как в  этом языке о ра ат ва тс во ника ие о и ки. Выражения DAX могут содержать недопустимые вычисления из-за ссылки в формуле на ошибочные данные. Например, в формуле может возникнуть ошибка деления на ноль или попытка выполнить арифметическую операцию со столбцом, содержащим нечисловые данные. Полезно узнать, как подобные ошибки обрабатываются в DAX по умолчанию и как можно перехватывать их самостоятельно. Перед началом обсуждения посмотрим, какие виды ошибок могут появляться в формулах DAX: „ ошибки преобразования; „ ошибки арифметических операций; „ пустые или отсутствующие значения.

Ошибки преобразования Первый вид ошибки – о и ка рео ра овани . Как мы уже видели ранее в этой главе, DAX автоматически преобразует значения между строковыми и числовыми типами, когда это необходимо. Так что все перечисленные выражения являются допустимыми: "10" + 32 = 42 "10" & 32 = "1032" 10 & 32 = "1032"

2

нако ство с

57

DATE (2010;3;25) = 3/25/2010 DATE (2010;3;25) + 14 = 4/8/2010 DATE (2010;3;25) & 14 = "3/25/201014"

Эти формулы корректны, поскольку оперируют константами. А как насчет следующей формулы, при условии что в столбце хранятся текстовые данные? Sales[VatCode] + 100

Поскольку первым операндом суммирования является столбец с  текстом, вы как разработчик должны позаботиться о том, чтобы все значения из этого столбца могли быть преобразованы в числа. Если DAX с этим не справится, возникнет ошибка преобразования. Вот пара типичных ситуаций: "1 + 1" + 0 = Не удается преобразовать значение "1+1" типа Text в тип Number. DATEVALUE ("25/14/2010") = Не удается преобразовать значение "25/14/2010" типа Text в тип Date.

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

Ошибки арифметических операций Второй тип ошибок – о и ки арифмети ески о ера ий – возникает в результате выполнения некорректных арифметических действий вроде деления на ноль или извлечения квадратного корня из отрицательного числа. Это не ошибки преобразования, DAX генерирует их всякий раз, когда происходит попытка вызова функции или использования оператора с недопустимыми значениями. Ошибка деления на ноль требует особого подхода, поскольку ее возникновение не очевидно (пожалуй, за исключением математиков). Когда происходит деление на ноль, DAX возвращает специальное значение (бесконечность). В особых случаях, когда ноль делится на ноль или на , DAX возвращает другое специальное значение (Not A Number – не число). Поскольку тут мы имеем дело с неочевидным поведением, мы решили свести результаты вычислений в табл. 2.3. АБЛИ А 2 3

С е иальн е зна ени рез льтатов ри делении на ноль

Выражение 10 0 0 0 0 10 0 0

Результат

Важно заметить, что значения и  не являются ошибками, это специальные значения в DAX. Фактически при делении числа на ошибка не генерируется. Вместо этого возвращается ноль: 58

2

нако ство с

9954 / ( 7 / 0 ) = 0

За исключением этой особой ситуации, DAX будет возвращать арифметическую ошибку при вызове функции с недопустимым параметром, например при попытке взять квадратный корень из отрицательного числа: SQRT ( -1 ) = Аргумент или функция "SQRT" имеет неправильный тип данных, либо результат имеет слишком большой или слишком маленький размер.

При обнаружении подобной ошибки DAX прекратит дальнейшее вычисление выражения и сгенерирует ошибку. Для проверки того, вернуло ли выражение ошибку, можно воспользоваться функцией . Далее в этой главе мы покажем такой сценарий. Стоит отметить, что специальные значения вроде в некоторых инструментах, например в Power BI, отображаются как обычные значения. В других инструментах, таких как сводные таблицы в Excel, эти значения могут восприниматься как ошибки.

Пустые или отсутству

ие значения

Третий тип ошибки, который мы рассмотрим, характеризуется не каким-то ошибочным выполнением условия, а  нали ием уст на ений. В  соседстве с  другими элементами присутствие пустых значений в  выражениях может приводить к непредсказуемым результатам или ошибкам в вычислении. DAX обрабатывает пустые или отсутствующие значения, а  также пустые ячейки одинаково, заменяя их значением . само по себе является даже не значением, а способом идентификации таких условий. В DAX получить значение можно путем вызова одноименной функции, результат которой отличается от пустой строки. Например, следующее выражение всегда будет возвращать значение , которое в разных клиентских инструментах может отображаться как пустая строка или «(blank)»: = BLANK ()

Само по себе это выражение не несет никакой смысловой нагрузки – функция оказывается полезной тогда, когда нам необходимо вернуть пустое значение. Допустим, вы хотите отобразить пустую строку вместо нуля. В следующем выражении рассчитаем размер скидки для продажи и вернем пустое значение для нулевых результатов: =IF ( Sales[DiscountPerc] = 0; -- Проверяем, есть ли скидка BLANK (); -- Возвращаем пустое значение, если скидки нет Sales[DiscountPerc] * Sales[Amount] -- Иначе рассчитываем скидку )

, по существу, не является ошибкой, это просто пустое значение. Таким образом, выражение, в  котором содержится , может возвращать значение или пустоту в зависимости от требований расчетов. Например, следующее выражение вернет всякий раз, когда Sales[Amount] будет являться : 2

нако ство с

59

= 10 * Sales[Amount]

Иными словами, результат арифметической операции будет , если один или оба из ее операндов – . Это создает неразбериху, когда необходимо проверить выражение на пустоту. Из-за выполнения неявных преобразований бывает невозможно понять, вернуло ли выражение при использовании оператора сравнения ноль (или пустую строку) или . Следующие выражения всегда будут возвращать TRUE: BLANK () = 0 BLANK () = ""

-- Всегда вернет TRUE -- Всегда вернет TRUE

Таким образом, если в столбцах Sales[DiscountPerc] или Sales[Clerk] будут содержаться пустые значения, следующие условия вернут даже при сравнении с 0 и пустой строкой: Sales[DiscountPerc] = 0 -- Вернет TRUE, если DiscountPerc либо BLANK, либо 0 Sales[Clerk] = "" -- Вернет TRUE, если Clerk либо BLANK, либо ""

В таких случаях можно использовать функцию чения на пустоту:

для проверки зна-

ISBLANK ( Sales[DiscountPerc] ) -- Вернет TRUE, только если DiscountPerc – BLANK ISBLANK ( Sales[Clerk] ) -- Вернет TRUE, только если Clerk – BLANK

Действие функции в арифметических и логических операциях в выражениях DAX показано в следующих примерах: BLANK () + 10 * BLANK BLANK () / BLANK () /

BLANK () = BLANK () () = BLANK () 3 = BLANK () BLANK () = BLANK ()

Однако функция оказывает влияние на итоговый результат не во всех формулах в DAX. Некоторые вычисления игнорируют пустые значения. Вместо этого возвращаемое значение зависит от других величин в формуле. Примерами таких операций могут быть сложение, вычитание, деление на и логические операции с участием . Следующие примеры показывают поведение некоторых операций с  : BLANK () − 10 = −10 18 + BLANK () = 18 4 / BLANK () = Infinity 0 / BLANK () = NaN BLANK () || BLANK () = FALSE BLANK () && BLANK () = FALSE ( BLANK () = BLANK () ) = TRUE ( BLANK () = TRUE ) = FALSE ( BLANK () = FALSE ) = TRUE ( BLANK () = 0 ) = TRUE ( BLANK () = "" ) = TRUE ISBLANK ( BLANK() ) = TRUE FALSE || BLANK () = FALSE

60

2

нако ство с

FALSE && BLANK () = FALSE TRUE || BLANK () = TRUE TRUE && BLANK () = FALSE

Пустые значения в Excel и SQL ина е о ра ат ва тс ст е зна ени се ст е зна ени та вос риниа тс как н лев е, если они аств т в ари ети ески о ера и сложени или ножени , но ри то ог т возвра ать о и к в о ера и делени или логи ески в ражени ст е зна ени NULL в в ражени о ра ат ва тс ина е, е в знаени BLANK ак в видели ранее в то главе, в ражени , в котор рис тств т зна ени BLANK, далеко не всегда возвра а т BLANK, тогда как нали ие NULL в инстр кии о ти всегда озна ает итогов NULL то отли ие о ень важно ит вать ри ра оте с рел ионно азо данн в режи е , оскольк в одо но сл ае одни в ислени д т роизводитьс в , др гие в рез льтате разни а в одода к ст зна ени в дв движка ожет о ерн тьс неожиданн оведение за росов

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

Перехват ошибок Теперь, когда вы познакомились с  видами ошибок, которые могут возникать в  DAX, самое время научиться перехватывать их, исправлять или, по крайней мере, выводить понятное для пользователя сообщение. Появление ошибок в выражениях DAX зачастую связано со значениями в столбцах таблиц, к которым обращается само выражение. Мы научим вас выявлять ошибки в выражениях и  возвращать сообщения о  них. Общепринятой техникой обработки ошибок в DAX является их обнаружение и возврат значения по умолчанию или сообщения об ошибке. Для этого в языке используется сразу несколько функций. Первой из них является функция  – очень похожая на , но вместо оценки условия проверяющая выражение на ошибки. Типичные примеры использования функции приведены ниже: = IFERROR ( Sales[Quantity] * Sales[Price]; BLANK () ) = IFERROR ( SQRT ( Test[Omega] ); BLANK () )

Если в любом из столбцов Sales[Quantity] или Sales[Price] в первом выражении находится строка, которую невозможно преобразовать в число, все выражение в целом вернет пустое значение. Иначе результатом будет произведение значений двух этих столбцов. Во втором выражении результатом будет пустое значение всякий раз, когда в столбце Test[Omega] будет оказываться отрицательное число. 2

нако ство с

61

Использование функции в таком виде заменяет собой более многословный код с применением функции совместно с  : = IF ( ISERROR ( Sales[Quantity] * Sales[Price] ); BLANK (); Sales[Quantity] * Sales[Price] ) = IF ( ISERROR ( SQRT ( Test[Omega] ) ); BLANK (); SQRT ( Test[Omega] ) )

Первый вариант без функции более предпочтителен, и вы можете использовать его всегда, когда возвращается то же значение, которое проверяется на ошибки. К тому же в этом случае вам не придется два раза писать одно и то же выражение, как в  последнем примере, а  значит, код станет более надежным и понятным. Функцию следует использовать в случаях, когда из выражения возвращается другое значение – не то, которое проверялось на ошибки. Кто-то может вовсе не проверять выражение на ошибки, а  вместо этого тестировать параметры на допустимость значений перед их обработкой. Например, в случае с функцией , вычисляющей квадратный корень из выражения, можно было предварительно проверить, является ли ее параметр положительным числом: = IF ( Test[Omega] >= 0; SQRT ( Test[Omega] ); BLANK () )

А с учетом того, что третий аргумент функции по умолчанию равен можно записать это выражение более лаконично:

,

= IF ( Test[Omega] >= 0; SQRT ( Test[Omega] ) )

Зачастую приходится проверять, являются ли значения пустыми. Функция проверяет переданный аргумент и возвращает в случае, если он является пустым. Это бывает полезно, особенно когда значение недоступно, но нельзя при этом считать его нулевым. В следующем примере мы рассчитаем стоимость доставки заказа, а если поле с указанием веса (Weight) не заполнено, будем брать стоимость доставки по умолчанию (DefaultShippingCost): = IF ( ISBLANK ( Sales[Weight] ); Sales[DefaultShippingCost];

62

2

нако ство с

-- Если вес не заполнен -- то возвращаем стоимость доставки -- по умолчанию

Sales[Weight] * Sales[ShippingPrice]

-- иначе умножаем вес на тариф стоимости -- доставки

)

Если просто умножить вес на тариф, мы получим пустые значения для стоимости доставки для всех заказов с незаполненным весом из-за характерного поведения значений в операциях умножения. Используя переменные, вы должны отлавливать ошибки во время их объявления, а не использования. Посмотрите на примеры ниже. Первая строчка кода вернет ноль, вторая – ошибку, а третья выдаст разные результаты в зависимости от версии продукта, использующего DAX (в последних версиях также будет сгенерирована ошибка): IFERROR ( SQRT ( -1 ); 0 )

-- Вернет 0

VAR WrongValue = SQRT ( -1 ) RETURN IFERROR ( WrongValue; 0 )

-- Ошибка возникает здесь, так что результатом -- всегда будет ошибка -- Эта строка никогда не будет выполнена

IFERROR ( VAR WrongValue = SQRT ( -1 ) RETURN WrongValue; 0 )

-- Разные результаты в зависимости от версии -- IFERROR сгенерирует ошибку в версии 2017 -- IFERROR вернет 0 в версиях до 2016

Ошибка возникает в  момент вычисления переменной WrongValue, так что движок никогда не вызовет функцию во втором примере. А в третьем фрагменте результат зависит от версии продукта. При перехвате ошибок нужно с особой внимательностью относиться к переменным.

Избегайте использования функций перехвата ошибок ес отр на то то те о ти иза ии кода де отдельно о с ждать далее в то книге, ва же се ас олезно дет знать, то нк ии ере вата о и ок с осо н негативн о разо сказатьс на ективности в ражени ро ле а не в то , то ти нк ии едленн е са и о се е росто движок не ожет ис ользовать о ти альн лан в олнени за роса в сл ае возникновени о и ки оль инстве сл аев редварительна роверка о ерандов на до сти ость зна ени дет л и ре ение в сравнении с ис ользование нк и дл о ра отки о и ок а риер, в есто такого раг ента кода IFERROR ( SQRT ( Test[Omega] ); BLANK () ) л

е

дет ис ользовать тако вариант

IF ( Test[Omega] >= 0; SQRT ( Test[Omega] ); BLANK () )

2

нако ство с

63

торо вариант не н ждаетс в ере вате о и ок, а зна ит, дет в олн тьс ст рее ервого то о ее равило Более одро но о о ти иза ии кода де говорить в главе 1 е одни оводо отказатьс от ис ользовани нк ии IFERROR вл етс то, то она не еет ере ват вать о и ки на олее гл око ровне вложенности а ри ер, в след е раг енте кода ос ествл етс ере ват о и ок в стол е Table[Amount] на сл а содержани в не не ислов зна ени ак от е али в е, то довольно дорогосто а о ера и , оскольк она в олн етс дл каждо строки в та ли е SUMX ( Table; IFERROR ( VALUE ( Table[Amount] ); BLANK () ) ) е ерь ос отрите на след ее в ражение десь о ри ине о ти иза ии движка те же о и ки, котор е ере ват вались в ред д е ри ере, ере ват ватьс не д т сли в стол е Table[Amount] встретитс не исловое зна ение только в одно строке, все в ражение в ело сгенерир ет о и к , котора не дет ере ва ена нкие IFERROR. IFERROR ( SUMX ( Table; VALUE ( Table[Amount] ) ); BLANK () ) нк и ISERROR арактериз етс таки же оведение , как и IFERROR с ольз те и осторожно и дл ере вата только те о и ок, котор е возника т не осредственно в локе IFERROR/ISERROR, а не на вложенн ровн

Генерирование ошибок Иногда ошибка – это просто ошибка, и формула не должна при ее возникновении возвращать значение по умолчанию. Более того, при возврате любого значения результат может оказаться неправильным. Например, если в конфигурационной таблице содержится противоречивая информация, не соответствующая действительности, необходимо предупредить об этом пользователя, вместо того чтобы возвращать заведомо ложные данные, которые могут быть приняты за истину. Более того, нам может понадобиться создать собственное сообщение об ошибке, а не пользоваться общим – так мы сможем лучше донести до пользователя суть проблемы. Представьте, что вам необходимо вычислить квадратный корень из абсолютной температуры, выраженной в градусах Кельвина, чтобы соответствующим образом скорректировать значение скорости звука в  сложном научном расчете. Очевидно, мы не ожидаем, что температура может быть отрицательной. Если же это произошло, значит, возникли проблемы при измерении, и нам необходимо сгенерировать ошибку и остановить дальнейшие расчеты. В этом случае представленный ниже код будет таить в себе потенциальную опасность: 64

2

нако ство с

= IFERROR ( SQRT ( Test[Temperature] ); 0 )

Для того чтобы сделать код более безопасным, мы должны сами генерировать ошибку. Перепишем формулу следующим образом: = IF ( Test[Temperature] >= 0; SQRT ( Test[Temperature] ); ERROR ( "Значение температуры не может быть отрицательным. Вычисление прервано." ) )

Форматирование кода на DAX Перед тем как продолжить говорить о  DAX, позвольте нам уделить немного внимания одному важному аспекту для любого языка программирования  – форматировани кода. DAX – функциональный язык, так что вне зависимости от сложности выражения оно, по сути, представляет собой вызов функции. В свою очередь, совокупность вложенных функций определяет сложность всего выражения в целом. В DAX нередко можно увидеть выражения на десять, а то и двадцать строк. И  с  ростом количества строк большую важность приобретает вопрос единообразного форматирования кода для его лучшей читаемости. Каких-то «официальных» правил форматирования кода DAX не существует, но мы считаем важным рассказать, каких принципов придерживаемся мы сами. Разумеется, наше форматирование нельзя считать эталонным, и вы можете предпочесть иные правила. С этим нет никаких проблем – выбирайте свой стиль и  придерживайтесь его. Единственное, что вам необходимо помнить: форматируйте свой код и никогда не и ите сложн е формул в одну строку ина е у вас во никнут ро лем и ран е ем в ред олагаете Чтобы понять важность форматирования исходного текста запросов, взгляните на следующее выражение, работающее с датой и временем. Это сложная формула, но не самая сложная из тех, что вы будете писать. Вот как будет выглядеть код без должного форматирования: IF(CALCULATE(NOT ISEMPTY(Balances); ALLEXCEPT (Balances; BalanceDate));SUMX (ALL(Balances [Account]); CALCULATE(SUM (Balances[Balance]);LASTNONBLANK(DATESBETWEEN( BalanceDate[Date]; BLANK();MAX(BalanceDate[Date]));CALCULATE(COUNTROWS(Balances))))); BLANK())

Понять, что делает эта формула, с первого взгляда просто невозможно. Тут даже не видно, какая функция является внешней и  сколько параметров она принимает. Студенты частенько просят нас разобраться в своих формулах, написанных подобным образом. И знаете, что мы делаем в первую очередь? Конечно, форматируем код. Вот как может выглядеть та же формула в отформатированном виде: 2

нако ство с

65

IF ( CALCULATE ( NOT ISEMPTY ( Balances ); ALLEXCEPT ( Balances; BalanceDate ) ); SUMX ( ALL ( Balances[Account] ); CALCULATE ( SUM ( Balances[Balance] ); LASTNONBLANK ( DATESBETWEEN ( BalanceDate[Date]; BLANK (); MAX ( BalanceDate[Date] ) ); CALCULATE ( COUNTROWS ( Balances ) ) ) ) ); BLANK () )

Код остался прежним, но теперь мы хотя бы отчетливо видим три входных параметра внешней функции . Кроме того, по единообразным отступам легко отличить блоки кода и понять, что выполняет каждый из них. Да, код остался таким же сложным, как и раньше, но теперь проблема в языке, а не в форматировании. С  введением дополнительных переменных выражение может несколько упроститься, пусть и за счет увеличения в объеме, но и в этом случае форматирование играет очень важную роль, позволяя визуально выделить области действия переменных: IF ( CALCULATE ( NOT ISEMPTY ( Balances ); ALLEXCEPT ( Balances; BalanceDate ) ); SUMX ( ALL ( Balances[Account] ); VAR PreviousDates = DATESBETWEEN ( BalanceDate[Date]; BLANK (); MAX ( BalanceDate[Date] ) )

66

2

нако ство с

VAR LastDateWithBalance = LASTNONBLANK ( PreviousDates; CALCULATE ( COUNTROWS ( Balances ) ) ) RETURN CALCULATE ( SUM ( Balances[Balance] ); LastDateWithBalance ) ); BLANK () )

DAXFor atter co М создали са т, осв енн ор атировани кода на зна ально сделали его дл се , то не тратить драго енное вре на ор атирование каждо ор л в ис одно тексте осле того как са т зара отал, ре или оказать его все , кто так же, как и , не желает ор атировать текст вр н Одновре енно с ти о л ризировали свои рин и и од од к ор атировани кода Посетить наш сайт вы можете по адресу www.daxformatter.com. Интерфейс сайта предельно прост – вставляйте свой текст на DAX и жмите кнопку FORMAT. Страница перезагрузится, и перед вами будет отформатированный код, который вы сможете перенести обратно в свой инструмент. Вот краткий перечень правил, которых мы придерживаемся при форматировании кода DAX: „ всегда отделяйте названия функций вроде , и  от других элементов кода пробелом и пишите их прописными буквами; „ ссылайтесь на столбцы таблиц следующим образом:  – без пробела между названием таблицы и открывающей квадратной скобкой. Не опускайте наименование таблицы; „ названия мер пишите без указания названия таблицы: ; „ всегда ставьте пробелы после точек с запятыми, а не перед ними; „ если формула прекрасно укладывается в одну строку, не используйте никаких правил; „ если формула не помещается в строку, следуйте таким рекомендациям: – название функции с  открывающей скобкой выносите на отдельную строку; – каждый параметр функции пишите на отдельной строке с дополнительным отступом из четырех пробелов и завершающей точкой с запятой; – закрывающую скобку размещайте непосредственно под названием соответствующей ей функции. 2

нако ство с

67

Это базовые правила. С полным списком принципов форматирования кода можно ознакомиться по адресу: http://sql.bi/daxrules. Если вы решите, что вам подходят другие правила форматирования исходного текста, используйте их. Основная цель этого действия состоит в облегчении чтения кода, так что вы вольны выбирать свой стиль. Главное, чтобы форматирование позволяло максимально быстро обнаруживать ошибки в коде, – в этом его основное предназначение. К примеру, если бы в изначально показанном коде без форматирования движок выдал ошибку, связанную с отсутствием закрывающей скобки, вам было бы очень непросто понять, где именно нужно ее поставить. В отформатированном тексте, напротив, хорошо видны соответствия между функциями и их скобками.

Помо ь при форматировании кода на DAX ор атирование кода на зада а не роста , оскольк о но и ри одитс зани атьс в не оль о око ке и с тексто аленького раз ера зависи ости от версии инстр ент , и редлага т разли н е средства дл на исани кода на След ие совет ог т о о ь ва ри ра оте с тексто в ти редактора ƒ ƒ ƒ

то вели ить ри т, окр тите колесо и с зажато клави е Ctrl то о лег ит тение ере од на нов строк ос ествл етс одновре енн нажатие Shift Enter если ва не нравитс рогра ировать не осредственно в редакторе, в ожете исать ис одн текст в др ги рогра а , таки как Блокнот или , а зате ереносить его в редактор

ри взгл де на код вает не росто сраз он ть, то еред ва и в исл е стол е или ера на и стать и книга ис ольз е о н знак равенства дл создани в исл е стол ов и знак рисваивани дл о ределени ер CalcCol = SUM ( Sales[SalesAmount] ) -- это вычисляемый столбец Store[CalcCol] = SUM ( Sales[SalesAmount] ) -- это вычисляемый столбец -- в таблице Store CalcMsr := SUM ( Sales[SalesAmount] ) -- а это мера аконе , совет е ри о влении в исл е стол ов и ер всегда каз вать название соответств е та ли дл в исл е стол ов и никогда дл ер того равила де ридерживатьс во все ри ера данно книги

Введение в агрегаторы и итераторы Почти во всех моделях данных есть необходимость оперировать агрегированными значениями. DAX предлагает сразу несколько функций, агрегирующих значения в столбцах таблицы и возвращающих скалярный результат. Мы называем эту группу функ и ми агрегировани (aggregation functions). Например, следующая мера вычисляет сумму по всему столбцу в  таблице : Sales := SUM ( Sales[SalesAmount] )

68

2

нако ство с

Функция агрегирует все строки в таблице, если используется в вычисляемом столбце. При использовании в  мере в  расчет берутся только строки, проходящие через фильтры по срезам, строкам и столбцам в отчете. В DAX много агрегирующих функций, в числе которых , , , и  , и их поведение отличается лишь способом агрегирования: SUM возвращает сумму значений,  – минимальное число и т. д. Почти все эти функции работают исключительно с числовыми значениями и датами, и лишь функции и  способны оперировать со строками. Более того, DAX никогда не учитывает при агрегировании пустые ячейки, и такое его поведение отличается от Excel (подробнее об этом мы расскажем далее в данной главе). Примечание нк ии и в дел тс свои оведение если они ри ен тс с дв ара етра и, то возвра а т из ни ини альное или акси альное зна ение соответственно аки о разо , MIN 1, 2 вернет 1, а MAX 1, 2 2 одо ное оведение олезно, когда н жно сравнить рез льтат сложн в ражени , оскольк озвол ет изежать ногократного и овторени в коде с ис ользование нк ии IF.

Все описанные выше функции агрегирования работают исключительно со столбцами. Таким образом, агрегирование применяется только к  значениям одного столбца. Но есть функции, способные оперировать, целыми выражениями, а не со столбцами. Из-за принципа своей работы они названы итерационными функциями, или просто итераторами (iterators). Эти функции очень полезны, особенно когда вам необходимо провести вычисления с  использованием столбцов из связанных таблиц или снизить количество вычисляемых столбцов в таблице. Итераторы принимают как минимум два параметра: таблицу, в которой будет проводиться сканирование, и выражение, которое будет вычисляться для каждой строки в таблице. После выполнения сканирования таблицы с вычислением указанного выражения для каждой строки функция приступает к агрегированию результатов в соответствии со своей семантикой. Например, мы можем рассчитать количество дней, требуемых для доставки товаров по заказам в  вычисляемом столбце с  названием и  построить отчет по полученным данным. Его результаты представлены на рис. 2.6. Заметьте, что в итоговом значении произведено суммирование дней, что не несет никакой пользы в подобных расчетах: Sales[DaysToDeliver] = INT ( Sales[Delivery Date] - Sales[Order Date] )

Итог с усредненным значением мы можем получить в мере с названием , показывающей количество дней доставки для каждого заказа и среднее значение в строке итогов: AvgDelivery := AVERAGE ( Sales[DaysToDeliver] )

Результат работы этой меры показан на рис. 2.7. Мера вычисляет среднее значение, применяя агрегирование к вычисляемому столбцу. Но можно избежать этого промежуточного шага создания вычисляемого столбца при помощи применения итератора, что позволит сэкономить ресурсы. И  хотя функция не умеет агрегировать выражения, 2

нако ство с

69

с этим прекрасно справляется ее коллега . При помощи нее мы можем пройти по всем строкам в таблице, вычислить необходимое значение для каждой строки и агрегировать полученные результаты. Вот код для меры, позволяющей ограничиться одним шагом вместо двух: AvgDelivery := AVERAGEX ( Sales, INT ( Sales[Delivery Date] - Sales[Order Date] ) )

Рис 2 6 итогово строке види с дне , от огли ожелать видеть среднее зна ение

Рис 2 7

ново

ере оказано агрегирование дне

о средне

Главным преимуществом такого подхода является то, что здесь мы не полагаемся на вычисляемый столбец. В результате мы вовсе можем обойтись без него, возложив всю функциональность на итератор. Большинство итерационных функций имеют такие же названия, как у их неитерационных аналогов, но с добавлением буквы X. Например, у функции есть зеркальное отражение в виде , у   – . Но есть и итераторы, не имеющие аналогов среди обычных функций. Далее в этой книге вы позна70

2

нако ство с

комитесь с функциями , , и другими – все они, по сути, являются итераторами, хоть и не выполняют агрегирующие действия. Впервые используя DAX, вы могли бы подумать, что итерационные функции по своей природе должны быть весьма медленными. Метод построчного обхода таблицы может показаться довольно затратным для ентрал ного ро ессора (CPU). На самом же деле итераторы работают очень быстро и ни в чем не уступают традиционным агрегирующим функциям. Фактически обычные агрегаторы являются укороченными версиями соответствующих итерационных функций, представляя так называемый синтакси еский са ар (syntax sugar). Посмотрите на следующую функцию: SUM ( Sales[Quantity] )

При выполнении это выражение переводится в  форму соответствующей итерационной функции такого вида: SUMX ( Sales; Sales[Quantity] )

Единственным преимуществом использования функции в  данном случае является более короткое выражение. При этом между функциями и  нет никакой разницы в скорости выполнения применительно к одному столбцу. Это фактически синонимы. Подробнее мы коснемся поведения этих функций в главе 4. Именно там мы познакомим вас с  концепцией контекста вычисления и  более детально опишем работу итерационных функций.

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

Функции агрегирования В предыдущих разделах мы кратко коснулись основных агрегаторов , , и  . Вы узнали, что функции и  , например, работают только со столбцами числового типа. DAX также предлагает альтернативный синтаксис для функ ий агрегирова ни , унаследованных из Excel, с добавлением окончания A к имени функции, чтобы они выглядели и вели себя как в Excel. Однако эти функции могут оказаться полезными только применительно к столбцам с типом , в которых значение TRUE расценивается как 1, а FALSE – как 0. Применительно к текстовым столбцам эти функции будут давать 0. Так что использование функции MAXA со столбцом текстового типа вне зависимости от его содержимого всегда выдаст 0. Более того, DAX никогда не учитывает в расчетах агрегации пустые ячейки. И  хотя эти функции могут применяться к  нечисловым столбцам без 2

нако ство с

71

возврата ошибки, результат их будет не так полезен, поскольку в DAX отсутствует автоматическое приведение текстовых столбцов к числовым. Это функции , , и  . Мы бы советовали не использовать функции, поведение которых в будущем сохранится для обратной совместимости с существующим кодом. Примечание ес отр на то то названи ти нк и сов ада т со статисти ески и нк и и, в и они ис ольз тс о разно , оскольк в кажд столе ранит данн е строго одного ти а, и и енно ти ти о о редел етс оведение агрегир е нк ии до сти о раз е ать в одно стол е разнородн ин ор а и , тогда как в то невоз ожно, в не стол строго ти изирован сли стол в назна ен ислово ти , все зна ени в не ог т ть ли о ислов и, ли о ст и сли стол е и еет текстов ти , все ти нк ии кро е COUNTA д т возвра ать дл него 0, даже если текст ожет ть ереведен в исло то же вре в о енка зна ени на и ринадлежность ислово ти роизводитс от е ки к е ке о то ри ине такие нк ии д т ес олезн ри енительно к текстов стол а только нк ии MIN и MAX оддержива т ра от с текстов и зна ени и

Функции, которые вы изучили ранее, полезны для выполнения агрегирования значений. Но иногда вам может потребоваться просто посчитать значения, а не агрегировать их. Для этих целей DAX представляет сразу несколько функций: „ оперирует со всеми типами данных, за исключением ; „ работает со столбцами всех типов; „ возвращает количество пустых ячеек ( или пустые строки) в столбце; „ подсчитывает количество строк в таблице; „ возвращает количество уникальных значений в столбце, включая пустые значения, если они есть; „ возвращает количество уникальных значений в столбце, исключая пустые значения. и   – почти идентичные функции в DAX. Они возвращают количество непустых значений в столбце вне зависимости от их типа. Эти функции унаследованы от Excel, где подсчитывает значения всех типов данных, включая текст, тогда как работает только с числовыми значениями. Если нужно подсчитать количество пустых значений в столбце, можно воспользоваться функцией , которая наравне учитывает и пустые значения. Наконец, для подсчета количества строк в таблице существует функция , принимающая в  качестве параметра не столбец, а таблицу. Последние две функции из этого раздела  – и   – очень полезны, поскольку делают ровно то, что и должны, исходя из названий, – считают количество уникальных значений в столбце, который принимают в качестве единственного параметра. Разница между ними заключается в том, что функция считает как одно из значений, тогда как игнорирует такие значения. 72

2

нако ство с

Примечание нк и DISTINCTCOUNT о вилась в в версии 2012 года о того оента дл одс ета никальн зна ени в стол е в н жден ли ользоватьс констр к ие COUNTROWS ( DISTINCT ( table[column] ) ) оне но, ис ользовать одн нк и DISTINCTCOUNT дл того л е и ро е нк и DISTINCTCOUNTNOBLANK о вилась только в 201 год , ривнес в рост се антик в ражени COUNT DISTINCT из ез нео оди ости на исани длинн в ражени

Логические функции Иногда нам необходимо встроить логическое условие в  выражение – например, для осуществления различных вычислений в  зависимости от значения в столбце или перехвата ошибки. В этих случаях нам помогут логи еские функ ии. В разделе, посвященном обработке ошибок, мы уже упомянули две важнейшие функции из этой группы: и  . Первую из них мы также описывали в разделе с условными выражениями. Логические функции – одни из простейших в  DAX и  выполняют ровно те действия, которые заложены в  их названиях. К  таким функциям относятся , , , , , и  . Например, если вам необходимо получить результат перемножения цены на количество только в случае, если цена ( ) содержит числовое значение, вы можете воспользоваться следующим шаблоном: Sales[Amount] = IFERROR ( Sales[Quantity] * Sales[Price]; BLANK ( ) )

Если бы мы не использовали функцию IFERROR, то при наличии недопустимого значения в столбце каждая строка вычисляемого столбца содержала бы ошибку, поскольку присутствие ошибки в одной строке автоматически распространяется на весь столбец. Применение функции позволило перехватить возникшую ошибку в строке и заменить значение в ней на . Еще одной интересной функцией из этой группы является функция . Она полезна в случае, если в столбце содержится небольшое количество уникальных значений и  мы хотим реализовать разное поведение выражения в зависимости от текущего значения. Представьте, что в столбце (размер) таблицы содержатся значения S, M, L или XL и  нам необходимо расшифровать их. Для этого мы можем создать вычисляемый столбец со следующей формулой с вложенными функциями : 'Product'[SizeDesc] = IF ( 'Product'[Size] = "S"; "Small"; IF ( 'Product'[Size] = "M"; "Medium"; IF ( 'Product'[Size] = "L"; "Large"; IF ( 'Product'[Size] = "XL";

2

нако ство с

73

"Extra Large"; "Other" ) ) ) )

Более лаконичная запись формулы с использованием функции ла бы выглядеть так:

мог-

'Product'[SizeDesc] = SWITCH ( 'Product'[Size]; "S"; "Small"; "M"; "Medium"; "L"; "Large"; "XL"; "Extra Large"; "Other" )

Код стал лучше читаться, но быстрее он при этом не стал, поскольку при выполнении функция все равно заменяется на вложенные инструкции . Примечание нк и SWITCH асто ис ольз етс дл роверки ара етра и о ределени рез льтир его зна ени ер а ри ер, в огли создать та ли араетров, состо из тре строк со зна ени и , и , и дать ользовател воз ожность в ирать ти агрега ии в ере ак асто делали до 201 года е ерь, когда о вились гр в ислени , котор е одро но о с ди в главе , одо на нео оди ость от ала р в ислени вл тс олее ред о тительн инстр енто дл в ислени зна ени с ето в ранн ользователе ара етров

Совет сть один л о тн ножественн роверок в з етс в на ор вложенн ис ользовать ножественн

с осо ис ользовани нк ии SWITCH дл ос ествлени одно в ражении оскольк та нк и в итоге рео ранк и IF, где в ираетс ервое сов ав ее словие, ожно е роверки след и о разо

SWITCH ( TRUE (); Product[Size] = "XL" && Product[Color] = "Red"; "Red and XL"; Product[Size] = "XL" && Product[Color] = "Blue"; "Blue and XL"; Product[Size] = "L" && Product[Color] = "Green"; "Green and L" ) с ользование нк ии TRUE в ка естве ервого ара етра говорит на ор слови , соответств и TRUE».

ерни ерв

Информационные функции При необходимости проанализировать тип выражения вы можете воспользоваться одной из информа ионн функ ий DAX. Все эти функции возвращают значение типа и могут быть использованы в любом логическом выра74

2

нако ство с

жении. К информационным функциям относятся: , , , , и  . Когда в качестве параметра вместо выражения передается столбец, функции , и  всегда возвращают TRUE или FALSE в зависимости от типа данных столбца и проверки на пустоту каждой ячейки. В результате эти функции становятся почти бесполезными в DAX – они просто были унаследованы в первой версии движка от Excel. Вам, должно быть, интересно, можно ли использовать функцию с текстовым столбцом для проверки возможности конвертирования его значений в  числа. К  сожалению, такое применение этой функции невозможно. Единственный способ проверить, можно ли перевести текст в число, в DAX – попытаться это сделать и отловить соответствующую ошибку. Например, для проверки, можно ли значение из столбца (имеющего текстовый тип) перевести в число, вы должны использовать код, подобный приведенному ниже: Sales[IsPriceCorrect] = NOT ISERROR ( VALUE ( Sales[Price] ) )

Сначала движок DAX попытается перевести строку в  число. Если ему это удастся, он вернет (поскольку результатом функции будет ), а иначе – (поскольку результатом будет ). Таким образом, для строк, в которых в качестве цены проставлено текстовое значение «N/A», проверка не пройдет. Если же мы попытаемся использовать функцию для аналогичной проверки, как в примере ниже, результат всегда будет : Sales[IsPriceCorrect] = ISNUMBER ( Sales[Price] )

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

Математические функции Набор математи ески функ ий, доступных в DAX, схож с аналогичным набором в Excel – с похожим синтаксисом и поведением. К самым распространенным математическим функциям можно отнести следующие: , , , , , , , , , , и  . Для генерации случайных чисел в DAX применяются функции и  . Используя функции и  , можно проверить числа. Функции и  полезны для вычисления наибольшего общего делителя и наименьшего общего кратного двух чисел соответственно. Функция QUOTIENT возвращает целую часть результата деления двух чисел. Также стоит упомянуть несколько функ ий округлени исел. Фактически вы можете самыми разными способами добиться одного и  того же результата. Внимательно рассмотрите формулы следующих столбцов с результатами вычислений, представленными на рис. 2.8: FLOOR = FLOOR ( Tests[Value]; 0,01 ) TRUNC = TRUNC ( Tests[Value]; 2 )

2

нако ство с

75

ROUNDDOWN = ROUNDDOWN ( Tests[Value]; 2 ) MROUND = MROUND ( Tests[Value]; 0,01 ) ROUND = ROUND ( Tests[Value]; 2 ) CEILING = CEILING ( Tests[Value]; 0,01 ) ISO.CEILING = ISO.CEILING ( Tests[Value]; 0,01 ) ROUNDUP = ROUNDUP ( Tests[Value]; 2 ) INT = INT ( Tests[Value] ) FIXED = FIXED ( Tests[Value]; 2; TRUE )

Рис 2 8

ез льтат ис ользовани разли н

нк и окр глени

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

ригонометрические функции DAX предлагает богатый выбор тригонометри ески функ ий, среди которых можно отметить , , , , , , и  . Префикс в виде буквы A приведет к  вычислению обратных тригонометрических функций: арккосинуса, арксинуса и т. д. Мы не будем подробно останавливаться на этих функциях, поскольку их действие весьма прозрачно. Функции и  помогут вам осуществить конверсию в градусы и радианы соответственно, а функция вернет в качестве результата квадратный корень из переданного параметра, предварительно умноженного на число .

екстовые функции Большинство текстов функ ий в  DAX похожи на свои аналоги из Excel, за некоторыми исключениями. Среди текстовых функций можно выделить следующие: , , , , , , , , , , , , , , , , и  . Эти функции применяются для манипулирования текстом и извлечения необходимой информации из строк, содержащих множество значений. На рис. 2.9 показан пример извлечения имени и фамилии из строк, содержащих перечисление через запятую имени, фамилии, а также обращения, которое необходимо убрать из результата. 76

2

нако ство с

Рис 2 9

звле ение и ени и а илии осредство текстов

нк и

Для начала необходимо получить позиции запятых в исходном тексте. После этого мы используем полученную информацию для извлечения нужных составляющих из текста. Формула вычисляемого столбца может вернуть неправильный результат, если в строке меньше двух запятых. К тому же она выдаст ошибку, если запятых нет вовсе. Вторая формула – для вычисляемого столбца  – учитывает эти нюансы и выводит правильный результат в случае недостатка запятых. People[Comma1] = IFERROR ( FIND ( ","; People[Name] ); BLANK ( ) ) People[Comma2] = IFERROR ( FIND ( " ,"; People[Name]; People[Comma1] + 1 ); BLANK ( ) ) People[SimpleConversion] = MID ( People[Name]; People[Comma2] + 1; LEN ( People[Name] ) ) & " " & LEFT ( People[Name]; People[Comma1] - 1 ) People[FirstLastName] = TRIM ( MID ( People[Name]; IF ( ISNUMBER ( People[Comma2] ); People[Comma2]; People[Comma1] ) + 1; LEN ( People[Name] ) ) ) & IF ( ISNUMBER ( People[Comma1] ); " " & LEFT ( People[Name]; People[Comma1] - 1 ); "" )

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

Функции преобразования Ранее вы усвоили, что DAX осуществляет автоматическое преобразование типов данных под нужды конкретного оператора. Несмотря на это, в языке также есть несколько полезных функ ий, позволяющих преобразовывать типы данных явно. Функция , к примеру, предпринимает попытку преобразовать аргумент к типу Currency, тогда как  – к целочисленному типу. Функции и  принимают в качестве параметра дату и время соответственно и возвращают корректное значение типа . Функция преобразовы2

нако ство с

77

вает текстовое значение в числовое, а  принимает в качестве первого параметра число, а в качестве второго – текстовый формат и выполняет соответствующее преобразование. Часто функции и  применяются совместно. Например, следующий пример возвращает строку «2019 янв 12»: = FORMAT ( DATE ( 2019; 01; 12 ); "yyyy mmm dd" )

Обратная операция преобразования строки в тип помощи функции .

выполняется при

DATEVALUE с датами в разных форматах нк и DATEVALUE арактериз етс разн оведение в зависи ости от ор ата Согласно евро е ско стандарт дат за ис ва тс в виде , тогда как а ерикански ор ат ред ис вает каз вать сна ала ес аки о разо , дата 2 еврал дет редставлена о разно в дв ор ата сли в ередадите в нк и DATEVALUE строк , котор невоз ожно дет рео разовать в корректн дат с ис ользование региональн настроек о ол ани , в есто того то в дать о и к , о таетс о ен ть еста и день и ес нк и DATEVALUE также оддерживает недв с сленн ор ат дат в виде а ри ер, след ие три в ражени верн т 2 еврал 201 года вне зависи ости от региональн настроек DATEVALUE ( "28/02/2018" ) DATEVALUE ( "02/28/2018" ) DATEVALUE ( "2018-02-28" )

-- Это 28 февраля в европейском формате -- Это 28 февраля в американском формате -- Это 28 февраля вне зависимости от формата

Б вает, то нк и DATEVALUE не генерир ет о и к даже в те сл а , когда в от нее того ожидаете о так ж зад ано разра от ика и

Функции для работы с датой и временем Работа с датой и временем – неотъемлемая часть любой аналитической деятельности. В DAX есть множество функций для оперирования с календарными вычислениями. Некоторые из них перекликаются с аналогичными функциями из Excel, облегчая преобразования в/из формата . Вот лишь несколько функ ий дл ра от с датой и временем в DAX: , , , , , , , , , , , , , , , и  . Этот инструментарий предназначен для работы с календарем, но в него не входят специальные функции логики о ера ий со временем (time intelligence), позволяющие, к примеру, сравнивать агрегированные значения из разных лет и рассчитывать меры нарастающим итогом с начала года. Эти функции составляют отдельный набор инструментов DAX, с которым мы подробно познакомимся только в восьмой главе. Как мы уже упоминали ранее, значения типа данных внутренне хранятся как числа с плавающей запятой, где целая часть представляет количество дней, прошедших с  30 декабря 1899 года, а дробная – долю текущего дня. Таким образом, часы, минуты и  секунды преобразуются в  десятичные 78

2

нако ство с

доли дня. Получается, что прибавление целого числа к дате фактически переносит ее на это количество дней вперед. Но вам, возможно, покажутся более удобными функции для извлечения дня, месяца и года из определенной даты. Следующие формулы лежат в  основе вычисляемых столбцов, показанных на рис. 2.10: 'Date'[Day] = DAY ( Calendar[Date] ) 'Date'[Month] = FORMAT ( Calendar[Date]; "mmmm" ) 'Date'[MonthNumber] = MONTH ( Calendar[Date] ) 'Date'[Year] = YEAR ( Calendar[Date] )

Рис 2 10

звле ение составл

и

асте дат

ри о о и с е иальн

нк и

Функции отношений В DAX есть две полезные функ ии, которые позволят вам осуществлять навигацию по связям внутри модели данных. Это функции и  . Вы уже знаете, что вычисляемые столбцы могут ссылаться на значения других столбцов в таблице, в которой они определены. Таким образом, вычисляемый столбец, созданный в таблице , может обращаться к любым столбцам из этой таблицы. А что, если вам понадобится обратиться к столбцам другой таблицы? Вообще, в  формулах этого делать нельзя, за исключением случая, когда таблица, на которую вы хотите сослаться, объединена с текущей таблицей при помощи связи. Так что вы легко можете обратиться к столбцу в связанной таблице посредством функции . Представьте, что вам необходимо создать вычисляемый столбец в таблице Sales, в  котором будет применяться понижающий коэффициент на базовую стоимость в случае принадлежности проданного товара категории Cell phones («Мобильные телефоны»). Чтобы это сделать, нам необходимо будет как-то обратиться к признаку категории товара, который находится в другой таблице. Но, как вы видите по рис. 2.11, от таблицы можно добраться до через промежуточные таблицы и  . Вне зависимости от того, через сколько связей придется пройти до нужной таблицы, движок DAX отыщет нужную информацию и вернет в исходную формулу. Таким образом, формула для вычисляемого столбца может выглядеть следующим образом: 2

нако ство с

79

Sales[AdjustedCost] = IF ( RELATED ( 'Product Category'[Category] ) = "Cell Phone"; Sales[Unit Cost] * 0,95; Sales[Unit Cost] )

Рис 2 11

а ли а Sales о осредованно св зана с та ли е Product Category

Функция обеспечивает доступ по связи со стороны «многие» к стороне «один», поскольку в этом случае у нас будет максимум одна целевая строка. Если соответствующих строк в  связанной таблице не будет, функция вернет значение . Если вам нужно обратиться по связи со стороны «один» к стороне «многие», функция вам не поможет, поскольку в этом случае результатом может быть сразу несколько строк. Здесь необходимо использовать функцию . Результатом выполнения этой функции будет таблица, содержащая все связанные строки по запросу, соответствующие выбранной строке. Например, если вас интересует, сколько товаров содержится в каждой категории, вы можете создать вычисляемый столбец в таблице со следующей формулой: 'Product Category'[NumOfProducts] = COUNTROWS ( RELATEDTABLE ( Product ) )

Как видно по рис. 2.12, в этом столбце будет отображено количество товаров по каждой категории.

Рис 2 12 оли ество товаров о категори ожно ос итать нк ие RELATEDTABLE

80

2

нако ство с

Как и  , функция может проходить через целую цепочку связей, всегда следуя от стороны «один» к стороне «многие». Часто эта функция используется вместе с итераторами. Например, если нам нужно для каждой категории перемножить количество на цену и просуммировать результаты, можно написать следующую формулу в вычисляемом столбце: 'Product Category'[CategorySales] = SUMX ( RELATEDTABLE ( Sales ); Sales[Quantity] * Sales[Net Price] )

Результат этого вычисления показан на рис. 2.13.

Рис 2 13 С ис ользование нк ии RELATEDTABLE и итератора с огли ол ить с родаж о категори

Поскольку мы имеем дело с  вычисляемым столбцом, результаты сохраняются в таблице и не меняются в зависимости от выбора пользователя в отчете, как в случае с мерой.

Закл чение В этой главе вы познакомились с некоторыми функциями языка DAX и встретились с фрагментами кода. После одного прочтения вы могли не запомнить все функции, но чем чаще вы будете их использовать на практике, тем быстрее привыкнете к ним. Наиболее важные моменты, которые вы узнали из этой главы: „ вычисляемые столбцы являются частью таблицы, в которой они созданы, и значения в них рассчитываются на этапе обновления данных, а не меняются в зависимости от выбора пользователя; „ меры представляют собой вычисления на языке DAX. В  отличие от вычисляемых столбцов, значения в них рассчитываются не в момент обновления данных, а в момент запроса. Соответственно, выбор пользователя в отчетах будет влиять и на значения мер; 2

нако ство с

81

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

ГЛ А В А 3

Использование основных табличных функций В этой главе вы познакомитесь с  базовыми табличными функциями языка DAX. а ли н е функ ии (table functions) отличаются от обычных тем, что возвращают не скалярные значения, а целые таблицы. Они бывают очень полезны в запросах DAX и сложных вычислениях, требующих прохода по таблицам. Здесь мы покажем вам несколько примеров таких вычислений. В данной главе мы лишь познакомим вас с концепцией табличных функций и покажем несколько из них в действии, а не будем подробно описывать работу всех табличных функций языка. С большим количеством функций мы столкнемся при дальнейшем их изучении в главах 12 и 13. Здесь же мы поработаем с самыми распространенными табличными функциями DAX и посмотрим, как их можно использовать в различных сценариях, включая скалярные выражения на DAX.

Введение в табличные функции До сих пор вы видели выражения на DAX, возвращающие строки или числа. Такие выражения называются скал рн ми (scalar expressions). Создавая меру или вычисляемый столбец, вы, по сути, пишете скалярные выражения, как на примерах ниже: = 4 + 3 = "DAX – прекрасный язык" = SUM ( Sales[Quantity] )

Главной целью создания мер является их вывод в отчетах, сводных таблицах и графиках. В конце концов, в основе всех этих отчетов лежат цифры – иными словами, скалярные выражения. И все же при вычислении этих выражений вам нередко приходится использовать таблицы. Например, в простой мере, вычисляющей сумму продаж, для итераций используется таблица: Sales Amount := SUMX ( Sales; Sales[Quantity] * Sales[Net Price] )

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

та ли н

нк и

83

таблице, а по результату табличной функции, как показано ниже. Тут мы вычисляем сумму продаж только по товарам, купленным в количестве двух штук и более: Sales Amount Multiple Items := SUMX ( FILTER ( Sales; Sales[Quantity] > 1 ); Sales[Quantity] * Sales[Net Price] )

В этой формуле мы использовали функцию вместо ссылки на таблицу. Как ясно из названия, эта функция фильтрует содержимое таблицы по определенному условию. Подробнее мы расскажем об этой функции позже. Сейчас же вам достаточно знать, что любую ссылку на таблицу в выражениях можно заменить на результат выполнения табличной функции. Важно ред д е ри ере ис ользовали ильтра и сов естно с нк ие с ировани то не л а рактика след и глава окаже , как строить олее ги кие и ективн е ильтр ри о о и нк ии CALCULATE десь же не тае с на ить вас исать о ти альн е за рос на , а росто оказ вае , как та ли н е нк ии ожно ис ользовать в рост в ражени озже ри ени ти кон е ии к олее сложн с енари

В главе 2 вы научились использовать переменные как составную часть выражений на DAX. Там мы хранили в переменных скалярные величины. Но они вполне подходят и для хранения таблиц. Предыдущий пример можно было бы переписать так с использованием переменной: Sales Amount Multiple Items := VAR MultipleItemSales = FILTER ( Sales; Sales[Quantity] > 1 ) RETURN SUMX ( MultipleItemSales; Sales[Quantity] * Sales[Unit Price] )

В переменной будет храниться целая таблица, поскольку ей присваивается результат выполнения табличной функции. Мы настоятельно советуем вам использовать переменные всегда, когда это возможно, ведь они существенно облегчают чтение кода. К тому же, просто присваивая значения переменным, вы одновременно создаете документацию к своему коду. Также в  вычисляемом столбце или внутри итерации вы можете использовать функцию для доступа к строкам связанной таблицы. Например, в следующем вычисляемом столбце в таблице мы рассчитаем сумму продаж по соответствующему товару: 84

с ользование основн

та ли н

нк и

'Product'[Product Sales Amount] = SUMX ( RELATEDTABLE ( Sales ); Sales[Quantity] * Sales[Unit Price] )

Кроме того, табличные функции можно вкладывать одну в другую. В следующем примере мы вычисляем сумму продаж только по товарам, которые продавались в количестве больше одной штуки: 'Product'[Product Sales Amount Multiple Items] = SUMX ( FILTER ( RELATEDTABLE ( Sales ); Sales[Quantity] > 1 ); Sales[Quantity] * Sales[Unit Price] )

В этом примере функция вложена внутрь . В таких случаях движок DAX сначала вычисляет результат вложенной функции, а  затем переходит к выполнению внешней. Примечание ак в видите даль е, вложенн е та ли н е нк ии иногда ог т вводить в за е ательство, оскольк ор док в олнени нк и CALCULATE/CALCULATETABLE и FILTER отли аетс след е разделе ознако и с с оведение нк ии FILTER О исание нк и CALCULATE и CALCULATETABLE дет дано в главе

Как правило, мы не можем использовать результат табличной функции в качестве значения для меры или вычисляемого столбца, поскольку они требуют скалярных выражений. Но мы вправе присвоить результат выполнения табличной функции вычисляемой таблице. В исл ема та ли а (calculated table) представляет собой таблицу, содержимое которой определяется выражением на DAX, а не загружается из источника. Например, мы можем создать вычисляемую таблицу, содержащую все товары с ценой за единицу, превышающей 3000. Для этого достаточно использовать следующее выражение: ExpensiveProducts = FILTER ( 'Product'; 'Product'[Unit Price] > 3000 )

Создание вычисляемых таблиц допустимо в Power BI и Analysis Services, но не в Power Pivot для Excel (на 2019 год). Чем больше у вас будет опыта работы с табличными функциями, тем чаще вы будете их использовать для создания сложных моделей данных с применением вычисляемых таблиц и/или сложных табличных выражений внутри мер. с ользование основн

та ли н

нк и

85

Введение в синтаксис EVALUATE Редакторы запросов вроде DAX Studio бывают очень удобны в плане написания сложных табличных выражений. При использовании таких инструментов ключевым словом для просмотра результата табличного выражения является : EVALUATE FILTER ( 'Product'; 'Product'[Unit Price] > 3000 )

Можно запустить на выполнение предыдущий запрос в любом из инструментов, поддерживающих DAX (DAX Studio, Microsoft Excel, SQL Server Management Studio, Reporting Services и т. д.). Запрос DAX – это обычное выражение, возвращающее таблицу, которое предваряет ключевое слово . Полный синтаксис достаточно сложен, и мы рассмотрим его в главе 13. Здесь же мы познакомим вас только с его базовыми параметрами, показанными ниже: [DEFINE { MEASURE [] = }] EVALUATE

[ORDER BY { [{ASC | DESC}]} [; ...]]

Инструкция может оказаться полезной для определения мер с областью действия, ограниченной данным запросом. Это бывает удобно при отладке формул, поскольку мы можем определить локальную меру, проверить ее как следует и интегрировать код в модель данных, только когда все недостатки будут устранены. Большая часть синтаксиса опциональна, а  простейшим использованием этого ключевого слова является извлечение всех строк и столбцов из существующей таблицы, как показано на рис. 3.1. EVALUATE 'Product'

Рис 3 1

ез льтат в

олнени за роса в

Инструкция ORDER BY, как понятно из названия, предназначена для сортировки результирующего набора: 86

с ользование основн

та ли н

нк и

EVALUATE FILTER ( 'Product'; 'Product'[Unit Price] > 3000 ) ORDER BY 'Product'[Color]; 'Product'[Brand] ASC; 'Product'[Class] DESC

Примечание жно от етить, то настро ка Sort By Colu n Сортировать о стол , о ределенна дл одели, не вли ет на сортировк в за росе ор док сортировки, казанн в инстр к ии EVALUATE, ожет вкл ать только стол , рис тств ие в рез льтир е на оре ак то клиент, созда и за рос дина и ески, должен с итать сво ство Sort By Colu n из етаданн одели, вкл ить стол е дл сортировки в за рос и зате до олнить инстр к и соответств и словие ORDER BY.

Ключевое слово само по себе потенциала запросам не добавляет. Вся мощь запросов заключается в умелом использовании множества табличных функций, доступных в языке DAX. В следующих разделах вы узнаете, как можно производить эффективные вычисления, комбинируя разные табличные функции.

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

Функция принимает в качестве параметров таблицу и логическое условие фильтрации. Результатом выполнения этой функции является набор строк из исходной таблицы, удовлетворяющих заданному условию. Функция одновременно является и  табличной функцией, и  итератором. Чтобы вернуть результат, она сканирует таблицу построчно, сверяя каждую строку с условием. Иными словами, функция запускает итерации по таблице. Например, следующее выражение возвращает все товары бренда Fabrikam: FabrikamProducts = FILTER ( 'Product'; 'Product'[Brand] = "Fabrikam" )

с ользование основн

та ли н

нк и

87

Функция часто используется для уменьшения количества строк в итерациях. К примеру, если вам нужно посчитать сумму продаж по товарам красного цвета, вы можете создать меру со следующей формулой: RedSales := SUMX ( FILTER ( Sales; RELATED ( 'Product'[Color] ) = "Red" ); Sales[Quantity] * Sales[Net Price] )

Результат вычисления этой меры можно видеть на рис. 3.2 вместе с общими продажами.

Рис 3 2

Мера RedSales отражает родажи искл

ительно о красн

товара

Мера при вычислении проходит по ограниченному набору товаров красного цвета. Функция добавляет свои условия к существующим. Например, в строке Audio мера показывает продажи по красным товарам из категории Audio. Функции можно вкладывать одну в другую. В принципе, вложенные функции идентичны использованию функции совместно с  . Следующие два выражения возвращают один и тот же результат: FabrikamHighMarginProducts = FILTER ( FILTER ( 'Product'; 'Product'[Brand] = "Fabrikam" ); 'Product'[Unit Price] > 'Product'[Unit Cost] * 3 ) FabrikamHighMarginProducts = FILTER ( 'Product'; AND (

88

с ользование основн

та ли н

нк и

'Product'[Brand] = "Fabrikam"; 'Product'[Unit Price] > 'Product'[Unit Cost] * 3 ) )

В то же время скорость двух этих запросов применительно к таблицам большого объема может быть разной в зависимости от и ирател ности (selectivity) условий. При разной избирательности условий первым лучше применять то, которое обладает большей избирательностью, – именно его и стоит размещать во вложенной функции . Например, если в таблице есть много товаров бренда Fabrikam и мало товаров, цена которых минимум втрое превышает их стоимость, следует разместить условие, сверяющее цену и стоимость, во вложенном запросе, как показано ниже. Сделав это, вы первым примените более ограничивающий фильтр, что позволит значительно снизить количество итераций при последующей проверке бренда: FabrikamHighMarginProducts = FILTER ( FILTER ( 'Product'; 'Product'[Unit Price] > 'Product'[Unit Cost] * 3 ); 'Product'[Brand] = "Fabrikam" )

Применение функции делает код более надежным и  легким для чтения. Представьте, что вам нужно посчитать количество красных товаров в  базе. Без использования табличных функций ваша формула могла бы выглядеть так: NumOfRedProducts := SUMX ( 'Product'; IF ( 'Product'[Color] = "Red"; 1; 0 ) )

Внутренний возвращает 1 или 0 в зависимости от цвета товара, а функция подсчитывает сумму получившихся единичек. И хотя эта формула работает, выглядит она не лучшим образом. Можно сформулировать код для меры более изящно: NumOfRedProducts := COUNTROWS ( FILTER ( 'Product'; 'Product'[Color] = "Red" ) )

Это выражение лучше отражает намерения автора запроса. Более того, данный код лучше читается не только человеком, но и машиной, а это позволит оптимизатору движка DAX построить более эффективный план выполнения запроса, что положительно скажется на его быстродействии. с ользование основн

та ли н

нк и

89

Введение в функции ALL и ALLEXCEPT В предыдущем разделе вы познакомились с функцией , полезной в случаях, когда нам необходимо ограничить количество строк в  результирующем наборе. Но иногда нам требуется обратное – расширить набор строк для нужд конкретных вычислений. В  DAX есть множество функций для этих целей, в числе которых , , , и  . В этом разделе мы рассмотрим первые две функции из данного перечня. Последние две будут описаны далее в этой главе, а с функцией вы познакомитесь только в главе 14. Функция возвращает все строки таблицы или все значения из одного или нескольких столбцов в  зависимости от переданных параметров. Например, следующее выражение DAX вернет вычисляемую таблицу с копиями всех строк из таблицы : ProductCopy = ALL ( 'Product' )

Примечание ри ен ть нк и ALL в в исл е та ли а нет нео оди ости, оскольк на ни не оказ ва т вли ни становленн е ильтр в от ета та нк и дет гораздо олее олезна в ера , как дет оказано далее

Функция может пригодиться при расчете процентов или соотношений, поскольку позволяет игнорировать установленные в  отчете фильтры. Представьте, что вам понадобился отчет, показанный на рис. 3.3, в котором в разных столбцах отражены сумма продажи и доля этой суммы от общего итога по продажам.

Рис 3 3

от ете оказан с

родаж и и

ро ент от о

и

родаж

В мере рассчитывается сумма продажи путем осуществления итераций по таблице и перемножения значений в столбцах и  : Sales Amount := SUMX (

90

с ользование основн

та ли н

нк и

Sales; Sales[Quantity] * Sales[Net Price] )

Чтобы узнать долю суммы продаж, необходимо разделить этот показатель на общую сумму продаж. Таким образом, нам нужно как-то получить итоговые продажи, несмотря на наложенный фильтр по категории. Это можно легко сделать при помощи функции . Следующая формула позволяет рассчитать общие продажи вне зависимости от выбранных в отчете фильтров: All Sales Amount := SUMX ( ALL ( Sales ); Sales[Quantity] * Sales[Net Price] )

В этой формуле мы заменили на , тем самым применив функцию для обращения ко всей таблице. Теперь мы можем получить долю продаж путем обычного деления: Sales Pct := DIVIDE ( [Sales Amount]; [All Sales Amount] )

На рис. 3.4 показаны все три меры вместе.

Рис 3 4

ере All Sales Amount все зна ени одинаков е и равн о

е с

е родаж

Параметром функции не может быть табличное выражение. Это должна быть либо таблица, либо перечень столбцов. Вы уже увидели, что делает функция с таблицей. А что будет, если дать ей список столбцов? В этом случае функция вернет уникальный набор значений из переданных столбцов исходной таблицы. Вычисляемая таблица из следующего примера будет содержать уникальные значения из столбца таблицы : Categories = ALL ( 'Product'[Category] )

На рис. 3.5 показан результат этого вычисления. В качестве параметров в функцию можно передать несколько столбцов из одной таблицы. В этом случае она вернет все уникальные комбинации значений этих столбцов. Например, мы можем получить список категорий с подс ользование основн

та ли н

нк и

91

категориями, добавив в параметры функции Результат данной функции показан на рис. 3.6:

столбец

.

Categories = ALL ( 'Product'[Category]; 'Product'[Subcategory] )

Рис 3 5 с ользование нк ии ALL с казание стол а озволило извле ь с исок никальн категори товаров

Функция игнорирует все ранее наложенные фильтры при вычислении результата. При этом мы можем использовать ее в качестве параметра итерационных функций, таких как и  , или как фильтрующий аргумент функции , с которой мы познакомимся в главе 5. Если нам нужно включить большинство, но не все столбцы в итоговый результат, мы можем вместо функции воспользоваться ее коллегой – . Синтаксисом этой функции предусмотрена передача ссылки на таблицу, а также столбцы, которые мы хотим исключить из результата. В итоге функция вернет уникальные строки из оставшихся столбцов исходной таблицы.

Рис 3 6

С исок содержит никальн е со етани категори и одкатегори

Можно использовать функцию для написания выражений на DAX, включающих в  итоговый результат столбцы, которые могут появиться 92

с ользование основн

та ли н

нк и

в таблице в будущем. Например, если в таблице содержится пять столбцов ( , , , , ), следующие два выражения вернут одинаковый результат: ALL ( 'Product'[Product Name]; 'Product'[Brand]; 'Product'[Class] ) ALLEXCEPT ( 'Product'; 'Product'[ProductKey]; 'Product'[Color] )

и 

Если мы в  будущем добавим в  таблицу еще два столбца , функция проигнорирует их, тогда как функция вернет эквивалент следующего выражения:

ALL ( 'Product'[Product Name]; 'Product'[Brand]; 'Product'[Class]; 'Product'[Unit Cost]; 'Product'[Unit Price] )

Иными словами, в функции мы указываем столбцы, которые мы хотим видеть, тогда как в  перечисляем столбцы, которые хотим исключить из вывода. Функция часто бывает полезна в качестве параметра функции при выполнении сложных вычислений, и  подобная конструкция редко поддается упрощению. Подробнее о таком использовании функции вы узнаете позже в этой книге.

Самые продаваемые категории и подкатегории л де онстра ии ра от ALL в ка естве та ли но нк ии редстави , то на н жно создать панель мониторинга с ото ражение категории и одкатегории товаров, с а родажи о котор ини в два раза рев ает средн с родажи л того сна ала должн в ислить средн с родажи о одкатегории, а зате , когда зна ение дет ол ено, в вести с исок одкатегори , с а родажи о котор ини вдвое оль е того среднего зна ени След и код ос ествл ет н жн на рас ет, и совет е ва одро но его из ить, то он ть вс о ь ри енени та ли н нк и и ере енн BestCategories = VAR Subcategories = ALL ( 'Product'[Category]; 'Product'[Subcategory] ) VAR AverageSales = AVERAGEX ( Subcategories; SUMX ( RELATEDTABLE ( Sales ); Sales[Quantity] * Sales[Net Price] ) ) VAR TopCategories = FILTER ( Subcategories; VAR SalesOfCategory = SUMX ( RELATEDTABLE ( Sales ); Sales[Quantity] * Sales[Net Price] ) RETURN SalesOfCategory >= AverageSales * 2 )

с ользование основн

та ли н

нк и

93

RETURN TopCategories ерво ере енно Subcategories ранитс с исок никальн со етани категори и одкатегори ере енно AverageSales в исл тс средние зна ени с родаж о каждо одкатегории аконе , в ере енн TopCategories о адает с исок из Subcategories, из которого д т дален одкатегории, с а родажи о котор не рев ает средн родаж AverageSales вдвое ез льтат в ражени оказан на рис

Рис 3 7 Са о котор

е родавае е одкатегории, с ини вдвое рев ает средн

а родажи с родажи

осле того как в своите нк и CALCULATE и контекст ильтра, в с ожете наисать редставленн е в е в ислени с ис ользование гораздо олее лакони ного и ективного синтаксиса о же се ас в видите, то с о о ь ко инировани та ли н нк и в в ражени ожно роизводить довольно сложн е в ислени , рез льтат котор ог т ть о е ен на анели ониторинга и в от ет

Введение в функции VALUES, DISTI CT и пустые строки В предыдущем разделе вы видели, что использование функции со столбцом в  качестве параметра возвращает таблицу, состоящую из уникальных значений этого столбца. В DAX есть еще две похожие функции, служащие примерно тем же целям, – и  . Эти функции работают почти идентично, единственным отличием является то, как они обрабатывают пустые строки, которые могут присутствовать в таблице. Позже в этом разделе мы объясним вам природу образования этих пустых строк, а пока сосредоточимся на этих двух функциях. Функция всегда возвращает набор уникальных значений из столбца. Результатом работы функции также будут уникальные значения, но только видимые. Легко проследить это различие на примере, представленном ниже: NumOfAllColors := COUNTROWS ( ALL ( 'Product'[Color] ) ) NumOfColors := COUNTROWS ( VALUES ( 'Product'[Color] ) )

В мере таблицы

подсчитывается количество уникальных цветов из , тогда как в  попадут только те цвета, которые

94

с ользование основн

та ли н

нк и

видны в отчете в данный момент, то есть прошедшие фильтрацию. Результат вычисления двух этих мер в разрезе категорий товаров представлен на рис. 3.8.

Рис 3 8

нк и VALUES дл каждо категории возвра ает ее од ножество ветов

Поскольку отчет построен в разрезе категорий, очевидно, что в каждой из них могут присутствовать товары определенных цветов, но не всех возможных. Функция возвращает набор уникальных значений из столбца в рамках наложенных фильтров. Если использовать функции или в вычисляемом столбце или вычисляемой таблице, их результат будет полностью совпадать с итогом работы функции , поскольку эти объекты не зависят от внешних фильтров. В то же время, будучи использованными внутри меры, функции и  строго подчиняются наложенным фильтрам, тогда как функция их просто игнорирует. Как мы уже сказали, действие этих двух функций очень похоже. Теперь пришло время разобраться в  их отличии, которое сводится к  способу обработки пустых строк в таблицах. Но сначала нужно понять, как могли попасть пустые строки в нашу таблицу, если мы не добавляли их в нее явно. Дело в том, что движок DAX автоматически создает пустые строки в таблице, находящейся в  отношении на стороне «один», в  случае присутствия недействительной связи. Чтобы продемонстрировать эту ситуацию на примере, давайте удалим из таблицы все товары серебряного цвета. Поскольку изначально у нас было 16 уникальных цветов товаров в модели, логично было бы предположить, что теперь их стало 15. Вместо этого мы видим довольно неожиданную картину – в нашем отчете, показанном на рис. 3.9, мера по-прежнему показывает число 16, а сверху добавилась строка без названия категории. Поскольку таблица находится в связи с  на стороне «один», каждой строке в таблице должна соответствовать строка в таблице . А  поскольку мы умышленно удалили строки с  одним из цветов из таблицы товаров, получилось, что множество строк в таблице остались без соответствия с  зависимыми записями в  . Важно подчеркнуть, что мы не удаляли строки из таблицы , мы удалили именно товары с определенным цветом, чтобы намеренно нарушить действие связи. И для гарантии того, что отсутствующие строки будут участвовать во всех вычислениях, движок автоматически добавил в таблицу строку с пусс ользование основн

та ли н

нк и

95

тыми значениями во всех столбцах, и  все «осиротевшие» строки из таблицы привязались к этой пустой строке. Важно та ли Product до авилась только одна ста строка, нес отр на то то сраз несколько товаров, на котор е сс лалась та ли а Sales, тратили св зь с соответств и ProductKey в та ли е Product.

Рис 3 9 ерво строке от ета в ведена ста категори , а о ее коли ество ветов осталось равн 1

Мы видим, что в отчете, изображенном на рис. 3.9, в первой строке указана пустая категория, при этом мера показывает один цвет. Это число отражает наличие строки с  пустой категорией, цветом и  всеми остальными столбцами. Вы не увидите эту строку при просмотре таблицы, поскольку она автоматически создается на этапе загрузки модели данных. Если в какой-то момент связь вновь станет действительной – к примеру, мы вернем в таблицу товары серебряного цвета, – пустая строка пропадет из таблицы. Некоторые функции DAX учитывают в своих расчетах пустые строки, другие – нет. Допустим, функция воспринимает пустую строку как полноценную запись в таблице и возвращает ее при обращении. Функция ведет себя иначе и  не учитывает пустые строки в  расчетах. Разницу между ними легко проследить на примере следующей меры, основанной на функции , а не : NumOfDistinctColors := COUNTROWS ( DISTINCT ( 'Product'[Color] ) )

Результат вычисления этой меры показан на рис. 3.10. В хорошо продуманной модели данных не должны появляться недействительные связи. Таким образом, если модель правильно спроектирована, обе функции всегда будут давать одинаковые результаты. Но вы должны помнить о различиях в работе этих функций на случай возникновения недействительных связей в  модели данных. В  противном случае ваши вычисления могут давать непредсказуемые результаты. Представьте, что вам необходимо рас96

с ользование основн

та ли н

нк и

считать среднюю сумму продажи по товарам. Одним из возможных вариантов будет определить общую сумму продажи и  затем поделить ее на количество товаров. Сделать это можно при помощи такого кода: AvgSalesPerProduct := DIVIDE ( SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ); COUNTROWS ( VALUES ( 'Product'[Product Code] ) ) )

Рис 3 10 Мера оказ вает а в итога ото ражает исло 1 , а не 1

стое зна ение дл

сто категории,

Результат вычисления данной меры показан на рис.  3.11. Очевидно, что здесь есть какая-то ошибка, поскольку в первой строке мы видим огромную и бессмысленную сумму.

Рис 3 11 соответств

ерво строке стоит огро на с а сто категории товаров

а,

с ользование основн

та ли н

нк и

97

Это загадочное большое число в  строке с  пустой категорией относится к продажам товаров серебряного цвета, которых больше нет в таблице . Иными словами, эта пустая строка ассоциируется с товарами серебряного цвета, которые больше не представлены в справочнике. В числителе функции мы учитываем все продажи серебряных товаров, тогда как в знаменателе будет присутствовать единственная строка, возвращенная функцией . Получается, что один несуществующий товар (пустая строка) вобрал в себя все продажи по разным товарам из таблицы , по которым нет соответствий в таблице . Именно это привело к образованию такого гигантского числа. И проблема тут в наличии недействительной связи в модели, а не в формуле как таковой. Какую бы формулу мы ни написали, в таблице не станет меньше строк с  отсутствующими товарами в  справочнике. Но будет полезно взглянуть, как разные функции возвращают разные наборы данных. Посмотрите на следующие две формулы: AvgSalesPerDistinctProduct := DIVIDE ( SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ); COUNTROWS ( DISTINCT ( 'Product'[Product Code] ) ) ) AvgSalesPerDistinctKey := DIVIDE ( SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ); COUNTROWS ( VALUES ( Sales[ProductKey] ) ) )

В первом случае мы использовали функцию вместо . В результате функция вернула пустое значение, которое и  попало в вывод. Во втором варианте мы, как и раньше, применили функцию , но на этот раз мы считаем строки по столбцу . Помните, что в  таблице присутствует множество значений , относящихся к одной и той же пустой строке. Результат вычисления новых мер показан на рис. 3.12.

Рис 3 12 о и о н

98

ри нали ии неде ствительно св зи все ер , скорее всего, и кажда о свое

с ользование основн

та ли н

нк и

д т

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

Функция VALUES с множественными столбцами нк ии VALUES и DISTINCT ог т рини ать в ка естве ара етра только один стол е та ли ти нк и нет соответств и аналогов дл рие а нескольки стол ов, как в сл ае с ALL и ALLNOBLANKROW сли ва нео оди о извле ь никальн е со етани види стол ов в от ете, нк и VALUES ва не о ожет главе 12 в знаете, то аналог в ражени VALUES ( 'Product'[Category]; 'Product'[Subcategory] ) ожет

ть за исан так

SUMMARIZE ( 'Product'; 'Product'[Category]; 'Product'[Subcategory] )

Позже вы увидите, что функции и  часто используются в качестве параметра в итераторах. И в случае отсутствия недействительных связей они дают одинаковые результаты. Проходя по строкам таблицы при помощи итерационных функций, вы должны рассматривать пустую строку как полноценную запись, чтобы гарантировать просмотр всех значений без исключения. В целом лучше всегда использовать функцию , а  приберечь для случаев, когда вам нужно будет явно исключить из результата возможные пустые строки. Позже в  этой книге вы узнаете, как применение функции вместо может предотвратить появление икли е ски ависимостей (circular dependencies). Мы поговорим об этом в главе 15. Функции и  могут принимать в  качестве аргумента не только столбец, но и целую таблицу. В этом случае, однако, они ведут себя поразному: „ функция возвращает уникальные значения из таблицы, не учитывая пустые строки. Таким образом, в выводе будут отсутствовать дубликаты; „ функция возвращает строки исходной таблицы без удаления дубликатов вместе с пустой строкой, если такие есть в таблице. Дублирующиеся записи остаются в итоговом наборе. с ользование основн

та ли н

нк и

99

Использование таблиц в качестве скалярных значений Несмотря на то что является табличной функцией, мы будем часто использовать ее для вычисления скалярных величин. Это возможно благодаря одной особенности DAX, заключающейся в том, что таблица с одним столбцом и одной строкой может быть интерпретирована как скалярное значение. Представьте, что мы строим отчет по количеству брендов с разбивкой по категориям и подкатегориям, как показано на рис. 3.13.

Рис 3 13 от ете оказано коли ество рендов дл каждо категории и одкатегории

При формировании отчета нам может понадобиться также видеть названия брендов в  таблице. Одним из решений может быть использование функции для извлечения наименований брендов вместо их количества. Это возможно только в случае, если категория или подкатегория представлена единственным брендом. Можно просто вернуть значение функции , и DAX автоматически конвертирует его в  скалярную величину. А  чтобы убедиться, что бренд в строке только один, можно дополнить выражение проверкой, как показано ниже: Brand Name := IF ( COUNTROWS ( VALUES ( Product[Brand] ) ) = 1; VALUES ( Product[Brand] ) )

Результат вычисления этой меры показан на рис.  3.14. Пустые ячейки в столбце означают, что в этой категории или подкатегории есть сразу несколько брендов. В формуле меры используется функция для проверки того, что в столбце Brand таблицы для данного выбора присутствует только одно значение. Это довольно часто используемый шаблон в DAX, и для него существует более простая функция , проверяющая столбец 100

с ользование основн

та ли н

нк и

на единственное видимое выражение. Ниже показан более оптимальный синтаксис для меры с использованием функции . Brand Name := IF ( HASONEVALUE ( 'Product'[Brand] ); VALUES ( 'Product'[Brand] ) )

Рис 3 14 огда нк и VALUES возвра ает одн строк , ожно еревести ее зна ение в скал рн вели ин , как оказано в ере Brand Name

А чтобы еще больше облегчить жизнь разработчикам, DAX предлагает функцию, автоматически проверяющую столбец на единственное значение и возвращающую его в виде скалярной величины. Для множественных вхождений допустимо задать в  функции значение по умолчанию. Речь идет о  функции , с  помощью которой можно переписать предыдущую меру следующим образом: Brand Name := SELECTEDVALUE ( 'Product'[Brand] )

Включив в качестве второго аргумента значение по умолчанию, можно соответствующим образом обработать ситуации со множественными вхождениями: Brand Name := SELECTEDVALUE ( 'Product'[Brand]; "Multiple brands" )

Результат вычисления этой меры показан на рис. 3.15. А что, если вместо строки «Multiple brands» (Несколько брендов) мы захотим видеть перечисление названий этих брендов? В  этом случае мы можем пройти по таблице, возвращенной функцией , примененной к столбцу , и использовать функцию для сращивания множественных значений: [Brand Name] := CONCATENATEX ( VALUES ( 'Product'[Brand] );

с ользование основн

та ли н

нк и

101

'Product'[Brand]; ", " )

Рис 3 15 нк и SELECTEDVALUE возвра ает зна ение о ол ани в сл ае, если в категори или одкатегори в одит сраз несколько рендов

Теперь в случае присутствия в строке нескольких брендов их наименования будут аккуратно перечислены через запятую, что видно по рис. 3.16.

Рис 3 16

нк и CONCATENATEX

еет со ирать в строк содержи ое та ли

Введение в функци ALLSELECTED Последняя табличная функция, относящаяся к разряду базовых, – это . На самом деле это очень сложная функция – возможно, самая сложная из табличных функций в DAX. В главе 14 мы расскажем обо всех ее нюансах, а сейчас просто познакомимся с ней, поскольку ее использование может быть крайне полезным и на начальной стадии изучения языка. Функция применяется для извлечения списка значений из таблицы или столбца с учетом только внешних фильтров, не входящих в элемент 102

с ользование основн

та ли н

нк и

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

Рис 3 17 От ет редставл ет со о

Значение в столбце

атри

,

и срез , раз е енн е на одно страни е

рассчитано при помощи следующей меры:

Sales Pct := DIVIDE ( SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ); SUMX ( ALL ( Sales ); Sales[Quantity] * Sales[Net Price] ) )

Поскольку в  знаменателе формулы используется функция , результат будет вычисляться по итогам всех продаж вне зависимости от установленных фильтров. И если нам вздумается в срезах выбрать конкретные категории товаров, процент все равно продолжит считаться по отношению к общим продажам. На рис. 3.18 показано, что произойдет, если выбрать не все категории.

Рис 3 18 с ользование о родажа

нк ии ALL ведет к рас ета относительно о

его итога

Несколько строк в отчете исчезли, как и ожидалось, но проценты по оставшимся не изменились. Более того, итог по столбцу не равен 100 . Если и вам кажется, что результаты получились неверными и правильно было бы рассчитывать проценты не относительно общего итога по продажам, а относительно видимых категорий, вам поможет функция . Если в  знаменателе меры использовать функцию вместо , то расчеты будут производиться с учетом всех фильтров за пределами нашей матрицы. Иными словами, в знаменателе будут учтены все категории товаров, кроме Audio, Music и TV, которые остались невыбранными. с ользование основн

та ли н

нк и

103

Sales Pct := DIVIDE ( SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ); SUMX ( ALLSELECTED ( Sales ); Sales[Quantity] * Sales[Net Price] ) )

Результат вычисления этой меры показан на рис. 3.19.

Рис 3 19 С ис ользование нк ии ALLSELECTED ро ент в ислились только с ето вне ни ильтров

Итог по столбцу вновь вернулся к значению 100 , и все вычисления были произведены только в рамках видимой области, а не относительно общих итогов. Функция очень мощная и полезная. К сожалению, за эту мощь приходится платить повышенной сложностью. Подробно обо всех нюансах использования этой функции мы расскажем далее в книге. Из-за своей сложности функция зачастую возвращает не самые очевидные результаты. Это не значит, что они неправильные, но понять их бывает непросто даже опытным специалистам по DAX. Однако и в таких простых ситуациях, как эта, функция также бывает чрезвычайно полезной.

Закл чение Как вы узнали из данной главы, даже базовые табличные функции DAX обладают очень серьезным потенциалом и позволяют производить достаточно сложные вычисления. , , и   – весьма распространенные функции языка, встречающиеся в формулах довольно часто. В использовании табличных функций очень важно умело сочетать их в выражениях  – так вы сможете поднять планку сложности расчетов на новый качественный уровень. А в совокупности с функцией и техникой преобразования контекста табличные функции способны очень компактно и лаконично производить невероятно сложные вычисления. В следующих главах мы познакомим вас с  контекстами вычислений и  функцией . После этого вы по-новому взглянете на то, что уже узнали о табличных функциях, ведь использование их в качестве параметров функции позволит извлечь максимум потенциала из этой связки.

ГЛ А В А 4

Введение в контексты вычисления На этой стадии чтения книги вы уже овладели основами языка DAX. Вы знаете, как создавать вычисляемые столбцы и меры, и понимаете предназначение основных функций DAX. В этой главе вы выйдете на новый уровень владения языком, а усвоив полную теоретическую базу DAX, станете настоящим гуру. С теми знаниями, что у вас уже есть, вы способны строить разные интересные отчеты, но для создания более сложных формул вам просто необходимо погрузиться в изучение контекстов вычисления. Фактически на этой концепции основываются все продвинутые возможности языка DAX. Однако нам стоит предостеречь наших читателей. Концепция контекстов вычисления довольно проста сама по себе, и вы очень скоро ее усвоите. Несмотря на это, в ней есть несколько важных нюансов, которые просто необходимо понять. Если этого не сделать сразу, в какой-то момент вы рискуете потеряться в мире DAX. У нас есть опыт обучения языку DAX тысяч людей, так что мы можем с уверенностью сказать, что это нормально. На определенной стадии вам, возможно, покажется, что формулы DAX работают каким-то магическим образом, и вы не понимаете, как именно это происходит. Не беспокойтесь, вы не одиноки. Большинство наших студентов проходили через это, и многие еще пройдут в будущем. Причина в том, что им не удается постигнуть все нюансы контекстов вычисления с  первого раза. И  решение здесь только одно – вернуться к этой главе позже, прочитать ее заново и закрепить в памяти то, что ускользнуло от вас при первом прочтении. Стоит отметить, что контексты вычисления играют важную роль при совместном использовании с функцией  – вероятно, наиболее мощной и  сложной для понимания функцией во всем языке. Мы познакомимся с  в  следующей главе и  будем использовать на протяжении всей оставшейся части книги. Досконально изучить поведение функции без полного понимания концепции контекстов вычисления будет проблематично. С другой стороны, усвоить всю значимость контекстов вычисления, не опробовав в действии функцию , тоже невозможно. По опыту написания прошлых книг мы можем предположить, что именно эта и  следующая главы будут наиболее полезными для усвоения языка DAX, и именно здесь многие из вас оставят закладку на будущее. На протяжении всей книги мы будем использовать концепции, с которыми познакомимся здесь. В главе 14 вы узнаете о расширенных таблицах и тем самым завершите изучение контекстов вычисления. Здесь же мы лишь начнем ведение в контекст в

ислени

105

знакомиться с этой концепцией, чтобы в будущем вы были готовы к освоению таких мощных инструментов, как расширенные таблицы. Таким образом, вы изучите всю теоретическую базу, касающуюся контекстов вычисления, за несколько шагов.

Введение в контексты вычисления В DAX существует два контекста в ислени (evaluation context): контекст фил тра (filter context) и контекст строки (row context). В следующих разделах вы познакомитесь с ними и научитесь их использовать в работе. Но перед началом изучения необходимо отметить важную вещь, состоящую в том, что эти два контекста представляют совершенно разные концепции с  разной функциональностью и принципами применения. Одной из самых распространенных ошибок новичков в  DAX является то, что они путают эти два контекста, считая, что контекст строки является лишь разновидностью контекста фильтра. Но это не так. Контекст фильтра ограничивает выводимые данные, тогда как контекст строки осуществляет итерации по таблице. Когда в  DAX идут итерации по таблице, фильтрация не осуществляется, и наоборот. Несмотря на всю кажущуюся простоту этой концепции, мы знаем по опыту, что усвоить ее бывает непросто. Похоже, наш мозг всегда стремится пробиться к  знаниям кратчайшим путем – когда он видит какие-то сходства в концепциях, он предпочитает для упрощения объединять эти концепции в  одну. Не попадайтесь на эту удочку. Всякий раз, когда вам вдруг покажется, что два контекста вычисления выглядят похоже, остановитесь и повторите, словно мантру: «Контекст фильтра ограничивает выводимые данные, а  контекст строки осуществляет итерации по таблице. Это не одно и то же». Контекст вычисления по определению представляет собой контекст, в  котором происходит вычисление выражения на DAX. На самом деле одно и то же выражение может производить разные результаты в зависимости от контекста, в котором оно выполняется. Такое поведение выражений интуитивно понятно, и именно поэтому мы можем оперировать формулами на DAX даже без глубокого изучения контекстов вычисления. Вы ведь в первых трех главах уже писали код на DAX и  при этом ничего не знали о  контекстах. Но сейчас вы переходите на совершенно новый уровень, а значит, вам необходимо разложить по полочкам в голове уже полученные знания по DAX и приготовиться к посвящению в язык со всей его безграничной мощью.

Знакомство с контекстом фильтра Для начала давайте разберемся, что из себя представляет контекст в исле ни . В DAX все выражения вычисляются внутри определенного контекста. Контекст – это своеобразное «окружение», в котором выполняется формула. Возьмем для примера следующую меру: Sales Amount := SUMX ( Sales; Sales[Quantity] * Sales[Net Price] )

106

ведение в контекст в

ислени

В этой формуле вычисляется сумма произведений значений столбцов количества и  цены из таблицы . Мы можем использовать созданную меру в отчете, что видно по рис. 4.1.

Рис 4 1 Мера Sales Amount ез контекстов оказ вает итогов оказатель

Само по себе это число не представляет большого интереса. При этом формула сделала ровно то, что мы и попросили, – посчитала сумму по указанному выражению. Но в реальном отчете нам может понадобиться осуществить некоторые срезы по столбцам. Например, можно выбрать бренды товаров, разместить их в строках матрицы, и в результате мы получим уже более показательный отчет, представленный на рис. 4.2.

Рис 4 2 Мера Sales Amount оказ вает с о каждо ренд в отдельн строка

родаж

Итоговое значение продаж по-прежнему присутствует в  отчете, и  сейчас оно представляет сумму продаж по отдельным брендам. Все значения в отчете в совокупности составляют предмет некого детализированного анализа. При этом можно отметить одну странность: формула делает не то, что мы ей сказали. Фактически мы не видим в каждой строке сумму по всем продажам. Вместо этого мы видим сумму продаж по конкретному бренду. Заметьте при этом, что нигде в коде меры мы не указывали, что формула должна работать с подмножествами данных. Эта фильтрация произошла где-то за пределами формулы. В каждой строке мы видим свое собственное значение по причине того, что наша формула выполняется в  определенном контексте вычисления. Можете думать о контексте вычисления как о своеобразном окружении, обрамляющем ячейку, в котором происходит вычисление. ведение в контекст в

ислени

107

DAX се исле и рои о тс соот етст у а и та е ор ула о ет а ать со ер е о ра бу у и ри е е о к ра абора а .

е ко тексте. е ре ультат ,

Здесь мы имеем дело с контекстом фил тра, который, как понятно из названия, осуществляет фильтрацию таблиц. И как мы уже говорили, одна и та же формула может давать совершенно разные результаты в зависимости от того, в каком контексте вычисления она была выполнена. Такое поведение формул кажется интуитивно понятным, но вы должны хорошо усвоить эту концепцию, поскольку она скрывает в себе ряд сложностей. У каждой ячейки в отчете свой собственный контекст фильтра. Вы должны иметь в виду, что в каждой отдельно взятой ячейке может производиться свое вычисление – как если бы это был другой запрос, не зависящий от остальных ячеек в отчете. Движок DAX может применять определенную внутреннюю оптимизацию для ускорения вычислений, но вы должны четко понимать, что в каждой ячейке производится собственное автономное вычисление предписанного выражения DAX. Таким образом, значение в итоговой строке отчета, показанного на рис. 4.2, не рассчитывается путем суммирования всех значений в  столбце. Оно вычисляется отдельно посредством агрегирования всех строк в таблице , несмотря на то что для других строк в отчете это вычисление уже было произведено. Таким образом, в зависимости от выражения DAX итоговая строка может содержать результат, не зависящий от других строк в отчете. Примечание на и ри ера дл ростот ис ольз е от ет ти а атри а о ожно задавать контекст в ислени и не осредственно в за роса , с е в ознако итесь в след и глава а данно стадии дет л е д ать только о от ета , то в наи олее росто и виз ально виде он ть о ис вае е кон е ии

Когда поле вынесено в строки, контекст фильтра отбирает по одному бренду для каждой строки. Если усложнить матрицу, добавив годы в столбцы, мы получим результат, показанный на рис. 4.3.

Рис 4 3

108

Мера Sales Amount, от ильтрованна

ведение в контекст в

о ренда и года

ислени

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

Рис 4 4

онтекст о редел етс на оро

оле , в несенн

в строки и стол

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

В Power BI контекст фильтра строится путем комбинирования различных визуальных элементов из графического интерфейса. На самом деле контекст фильтра для конкретной ячейки вычисляется при помощи объединения всех ведение в контекст в

ислени

109

фильтров, расположенных в  строках, столбцах, срезах и  других визуальных фильтрующих элементах. Взгляните на рис. 4.5.

Рис 4 5 ти и но от ете контекст содержит ножество ле ентов, вкл а срез и ильтр

Контекст фильтра верхней левой ячейки (бренд: A.Datum, год: CY 2007, значение: 57 276,00) состоит не только из полей строки и  столбца, но также из фильтров по виду деятельности (Professional) и континенту (Europe), расположенных слева в своих визуальных элементах. Все эти фильтры составляют единый контекст фильтра, действительный для каждой ячейки, и DAX применяет его ко всей модели данных перед вычислением формулы. Формально можно сказать, что контекст фильтра представляет собой набор фильтров. Фильтр, в свою очередь, является списком кортежей, а каждый кортеж – это набор значений для определенных столбцов. На рис. 4.6 визуально показано действие контекста фильтра, в рамках которого вычисляется значение в  ячейке. Каждый элемент отчета является составной частью контекста фильтра, и в каждой ячейке контекст фильтра будет свой.

Calendar Year CY 2007

Education High School Partial College

Brand Contoso

Рис 4 6

110

из альное редставление контекста ильтра в от ете

ведение в контекст в

ислени

Контекст фильтра из примера на рис. 4.6 состоит из трех фильтров. Первый фильтр содержит кортеж по полю с  единственным значением CY 2007. Второй фильтр представляет собой два кортежа для поля со значениями High School и Partial College. В третьем фильтре присутствует один кортеж для поля со значением Contoso. Вы могли заметить, что каждый отдельный фильтр содержит кортежи для одного столбца. Позже вы узнаете, как создавать кортежи для нескольких столбцов. Такие кортежи являются одновременно очень мощным и сложным инструментом в руках разработчика. Перед тем как идти дальше, давайте вспомним меру, с которой мы начали этот раздел: Sales Amount := SUMX ( Sales; Sales[Quantity] * Sales[Net Price] )

Вот как правильно звучит предназначение этой меры: мера в исл ет сумму рои ведений стол ов и дл все строк та ли види м в теку ем контексте фил тра То же самое применимо и к более простым агрегациям. Рассмотрим такую меру: Total Quantity := SUM ( Sales[Quantity] )

Здесь производится суммирование значений по столбцу Quantity для всех строк таблицы Sales, видимых в  текущем контексте фильтра. Лучше понять действия, которые выполняет эта формула, можно на примере соответствующей функции : Total Quantity := SUMX ( Sales; Sales[Quantity] )

Глядя на эту формулу, можно предположить, что контекст фильтра оказывает влияние на выражение , в результате чего из таблицы возвращаются только строки, видимые в текущем контексте фильтра. Это правда, как и то, что контекст фильтра влияет и на перечисленные ниже меры, для которых не существует соответствующих итерационных функций: Customers := DISTINCTCOUNT ( Sales[CustomerKey] ) --Colors := VAR ListColors = DISTINCT ( 'Product'[Color] ) --RETURN COUNTROWS ( ListColors ) --

Подсчитываем количество покупателей в контексте фильтра Уникальные цвета в контексте фильтра Количество уникальных цветов

Вас уже, наверное, раздражают наши постоянные повторения о  том, что контекст фильтра всегда активен и влияет на все расчеты. Но DAX требует от вас предельной внимательности. Сложность этого языка состоит не в освоении новых функций, а в наличии множества тонких нюансов представленных концепций. А когда эти концепции совмещаются, мы получаем довольно сложные сценарии. Сейчас контекст фильтра у нас определен в самом отчете. Но когда вы научитесь создавать контексты фильтра самостоятельно (этому будет посвящена следующая глава), на первый план выйдет умение определять, какой контекст активен в той или иной части формулы. ведение в контекст в

ислени

111

Знакомство с контекстом строки В предыдущем разделе вы познакомились с контекстом фильтра. Теперь пришло время узнать, что из себя представляет второй вид контекста вычисления, а именно контекст строки. Помните о том, что хоть оба контекста и являются разновидностями контекста вычисления, они представляют совершенно разные концепции. Ранее вы узнали, что главным предназначением контекста фильтра, как ясно из названия, является выполнение отбора, или фильтрации, таблиц. Контекст строки не является инструментом для фильтрации таблиц. Его забота – осуществлять итерации по таблице и  вычислять значения в столбцах. На этот раз мы будем использовать вычисляемый столбец для подсчета валовой прибыли: Sales[Gross Margin] = Sales[Quantity] * ( Sales[Net Price] - Sales[Unit Cost] )

Значения в этом столбце для каждой строки будут отличаться, как видно по рис. 4.7.

Рис 4 7 на ени в в исл е о стол и завис т от др ги стол ов

е Gross Margin разн е

Как мы и ожидали, значения в созданном нами вычисляемом столбце для каждой строки будут свои. Это вполне естественно, поскольку значения других трех столбцов, от которых зависит наша новая величина, также разнятся. Как и в случае с контекстом фильтра, причина таких различий состоит в наличии определенного контекста вычисления. Но на этот раз контекст не фильтрует таблицу. Вместо этого он идентифицирует строку, для которой выполняется вычисление. Примечание онтекст строки сс лаетс на конкретн строк в рез льтате та ли ного в ражени е стоит тать его со строко в от ете нет воз ожности на р сс латьс на строки или стол в от ета на ени , оказ вае е в атри е в и сводно та ли е в , вл тс рез льтато в ислени ер в контексте ильтра или зна ени и, со раненн и в о н или в исл е стол а та ли

112

ведение в контекст в

ислени

Мы знаем, что значения вычисляемого столбца рассчитываются построчно, но как DAX понимает, в какой строке мы находимся в текущий момент? В этом ему помогает специальный вид контекста вычисления, называемый контекстом строки. Когда мы добавляем вычисляемый столбец к таблице из миллиона строк, DAX одновременно создает контекст строки, вычисляющий значение в столбце строка за строкой. Во время добавления вычисляемого столбца DAX по умолчанию создает контекст строки. В этом случае нет необходимости делать это вручную – вычисляемый столбец и так всегда вычисляется в  контексте строки. Но вы уже знаете, как создавать контекст строки вручную – при помощи итератора. Фактически мы можем написать для подсчета валовой прибыли меру следующего содержания: Gross Margin := SUMX ( Sales; Sales[Quantity] * ( Sales[Net Price] - Sales[Unit Cost] ) )

В этом случае, поскольку мы имеем дело с мерой, контекст строки автоматически не создается. Функция , будучи итератором, создает контекст строки, который начинает проходить по таблице построчно. Во время итерации происходит запуск второго выражения с  функцией внутри контекста строки. Таким образом, на каждой итерации DAX знает, какие значения использовать для трех столбцов, присутствующих в выражении. Контекст строки появляется, когда мы создаем вычисляемый столбец или рассчитываем выражение внутри итерации. Другого способа создать контекст строки не существует. Можно считать, что контекст строки необходим нам для извлечения значения столбца для конкретной строки. Например, следующее выражение для меры недопустимо. Формула пытается вычислить значение столбца , но в отсутствие контекста строки не может получить информацию о строке, для которой необходимо произвести вычисление: Gross Margin := Sales[Quantity] * ( Sales[Net Price] - Sales[Unit Cost] )

Эта формула будет вполне допустимой для вычисляемого столбца, но не для меры. И причина не в том, что вычисляемые столбцы и меры как-то поразному используют формулы DAX. Просто вычисляемый столбец располагает контекстом строки, созданным автоматически, а мера – нет. Если вам необходимо внутри меры вычислить определенное выражение построчно, вам придется использовать итерационную функцию для принудительного создания контекста строки. Примечание Сс лка на стол е тре ет нали и контекста строки дл возврата зна ени из стол а та ли Стол е также ожет ис ользоватьс в ка естве арг ента дл некотор нк и , не рас олага и контексто строки а ри ер, нк ии DISTINCT и DISTINCTCOUNT ог т рини ать на в од стол е , не о редел ри то контекст строки о в в ражени сс лка на стол е должна о ладать контексто строки, то ожно ло извле ь конкретное зна ение

ведение в контекст в

ислени

113

Здесь мы должны повторить одну важную концепцию: контекст строки не является разновидностью контекста фильтра, отбирающей одну строку. Контекст строки не фильтрует модель, а лишь указывает движку DAX, какую строку таблицы использовать. Если вам необходимо применить фильтр в модели данных, нужно воспользоваться контекстом фильтра. С другой стороны, если вам нужно вычислить какое-то выражение построчно, контекст строки прекрасно справится с этой задачей.

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

Использование функции SUM в вычисляемых столбцах В первом примере мы будем использовать агрегирующую функцию внутри вычисляемого столбца. Каким будет результат вычисления следующей формулы, написанной в вычисляемом столбце таблицы ? Sales[SumOfSalesQuantity] = SUM ( Sales[Quantity] )

Помните, что внутри движок DAX преобразует эту формулу в  выражение с итератором следующего вида: Sales[SumOfSalesQuantity] = SUMX ( Sales; Sales[Quantity] )

Поскольку здесь мы имеем дело с  вычисляемым столбцом, его значение будет рассчитываться построчно в рамках контекста строки. Какой вывод вы ожидаете увидеть в итоге? На выбор предлагаем три ответа: „ значение столбца для текущей строки, свое для каждой записи; „ итог по столбцу , одинаковый для всех строк; „ ошибку, поскольку мы не можем использовать функцию в вычисляемом столбце. Остановитесь и подумайте, как бы вы ответили на этот вопрос. Вопрос вполне правомочный. Ранее мы говорили, что подобную формулу можно прочитать так: «Получить сумму по количеству для всех строк, видимых в текущем контексте фильтра». А поскольку код выполняется для вычисляемого столбца, DAX будет проводить вычисление построчно в  рамках контекста строки. В свою очередь, контекст строки не фильтрует таблицу. Единственный контекст, который способен это делать, – контекст фильтра. Все это ставит перед нами новый вопрос: а каким будет контекст фильтра в момент вычисления 114

ведение в контекст в

ислени

этого выражения? Ответ достаточно прост: контекст фильтра будет пустым. Вообще, контекст фильтра создается при помощи визуальных элементов отчета и дополнительных условий в запросе, а значения в вычисляемом столбце рассчитываются в момент обновления данных, когда никакие фильтры еще не наложены. Таким образом, функция применяется ко всей таблице , агрегируя значения столбца для всех записей таблицы . Так что верным будет второй ответ. В  вычисляемом столбце будет одинаковое значение для всех строк, и оно будет отражать общий итог по столбцу . На рис. 4.8 показан вывод отчета с вычисляемым столбцом .

Рис 4 8 нк и SUM ( Sales[Quantity] ) в в рас ростран етс на все строки та ли

исл е о стол

е

Из этого примера видно, что два контекста вычисления могут мирно сосуществовать и при этом не взаимодействовать друг с другом. Оба контекста влияют на итоговые результаты, но делают они это по-разному. Агрегирующие функции вроде , и  используют только контекст фильтра, игнорируя при этом контекст строки. Если вы выбрали первый вариант ответа, что делают многие студенты, это нормально. Это означает, что вы по-прежнему путаете контекст фильтра с контекстом строки. Еще раз повторим, что контекст фильтра фильтрует, контекст строки осуществляет итерации по таблице. Первый ответ выбирают те, кто полагаются на интуицию, и теперь вы понимаете, почему. Если же вы сделали правильный выбор, что ж, поздравляем – значит, эта глава помогла вам понять разницу между различными контекстами.

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

ислени

115

GrossMargin% := ( Sales[Net Price] - Sales[Unit Cost] ) / Sales[Unit Cost]

Какой результат мы получим? Как и в первой задаче, мы предлагаем вам три варианта ответа на выбор: „ выражение выглядит корректно, пора запускать отчет; „ здесь есть ошибка, такую формулу писать нельзя; „ такое выражение написать можно, но оно вернет ошибку при формировании отчета. Как и  в  первом случае, остановитесь ненадолго, подумайте над ответом и только потом продолжайте чтение. В этом коде мы ссылаемся на столбцы и  без использования агрегирующих функций. Так что DAX придется вычислять значение по каждому из столбцов для конкретной строки. Но у движка нет возможности определить, с какой именно строкой он имеет дело в данный момент, поскольку мы не запускали итерации по таблице, а код написали не в вычисляемом столбце, а в мере. Иными словами, здесь в распоряжении DAX нет контекста строки, который мог бы помочь извлечь значение из нужного нам столбца в рамках этого выражения. Помните, что при создании меры автоматически не появляется контекст строки, это происходит только при создании вычисляемого столбца. Если нам нужен контекст строки внутри меры, необходимо воспользоваться итерационными функциями. Таким образом, и  здесь правильным вариантом ответа будет второй. Мы не можем написать такую формулу, поскольку она синтаксически неверна, и ошибка возникнет непосредственно в момент сохранения формулы.

Использование контекста строки с итераторами Вы уже знаете, что DAX создает контекст строки всякий раз, когда мы добавляем в таблицу вычисляемый столбец или начинаем проходить по таблице при помощи итерационной функции. Когда мы имеем дело с вычисляемым столбцом, понятие контекста строки вполне прозрачно и понятно. Фактически мы можем создавать вычисляемые столбцы, и не зная о наличии какого-то контекста строки. Причина в том, что движок DAX автоматически создает контекст строки в момент создания вычисляемого столбца. Так что нам нет смысла беспокоиться о его создании или использовании. С другой стороны, когда мы проходим по таблице при помощи итератора, мы лично ответственны за создание и использование контекста строки. Более того, применяя итерационные функции, мы вправе создавать множественные контексты строки, вложенные друг в друга, что увеличивает сложность кода. Это обусловливает важность досконального понимания применения контекста строки совместно с итераторами. Посмотрите на следующее выражение на DAX: IncreasedSales := SUMX ( Sales; Sales[Net Price] * 1.1 )

Поскольку функция является итератором, она создает контекст строки в рамках таблицы и использует его во время перебора по таблице. Контекст строки проходит по таблице (первый параметр функции) и на каж116

ведение в контекст в

ислени

дой итерации предоставляет текущее значение строки выражению, располагающемуся во втором параметре. Иными словами, DAX вычисляет внутреннее выражение (второй параметр функции ) в контексте строки, содержащей текущую строку из первого параметра. Стоит отметить, что два параметра функции используют разные контексты. Фактически каждый фрагмент кода DAX выполняется в  контексте, в  котором он был вызван. Таким образом, в  момент вычисления выражения могут быть активны контекст фильтра и один или несколько контекстов строк. Посмотрите на следующую формулу с комментариями: SUMX ( Sales; Sales[Net Price] * 1.1

-- Внешние контексты фильтра и строки -- Внешние контексты фильтра и строки + новый контекст -- строки

)

Первый параметр, , обрабатывается в контексте, пришедшем из вызывающей области кода. В свою очередь, второй параметр, являющийся выражением, вычисляется одновременно во внешних контекстах и вновь созданном контексте строки. Все итерационные функции ведут себя одинаково: 1) обрабатывают первый переданный параметр в  существующих контекстах для определения списка строк для сканирования; 2) создают новый контекст строки для каждой строки таблицы, определенной на первом шаге; 3) осуществляют итерации по таблице и  вычисляют второй параметр в  существующем контексте вычисления, включая вновь созданный контекст строки; 4) агрегируют значения, рассчитанные на предыдущем шаге. Помните, что исходные контексты продолжают действовать в  момент вычисления внутреннего выражения. Итераторы лишь добавляют новый контекст строки, они не изменяют существующий контекст фильтра. Например, если во внешнем фильтре определено условие, ограничивающее товары по красному цвету (Red), этот фильтр останется активным на протяжении всех итераций. Также стоит всегда держать в уме, что контекст строки осуществляет итерации по таблице, а не фильтрует ее. Так что у него нет никаких инструментов для переопределения внешнего контекста фильтра. Это правило действует всегда, однако здесь есть один важный, но не вполне очевидный нюанс. Если во внешнем контексте вычисления содержится контекст строки по той же самой таблице, что и во внутреннем, то прежний контекст будет скрыт при вычислении вложенных выражений. У  новичков DAX этот сложный аспект традиционно является источником ошибок, так что мы рассмотрим эту особенность языка в следующих двух разделах.

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

ислени

117

итерации. На первый взгляд кажется, что открывать новый цикл в рамках существующего – это довольно странно. Но в DAX это является весьма распространенной практикой, поскольку вложенные итераторы позволяют строить действительно мощные выражения. Например, в представленном ниже коде мы видим сразу три уровня вложенности итераторов, сканирующих три разные таблицы: , и  . SUMX ( 'Product Category'; -- Сканируем таблицу Product Category SUMX ( -- Для каждой категории RELATEDTABLE ( 'Product' ); -- Сканируем товары SUMX ( -- Для каждого товара RELATEDTABLE ( 'Sales' ); -- Сканируем продажи по товару Sales[Quantity] -* 'Product'[Unit Price] -- Получаем сумму по этой продаже * 'Product Category'[Discount] ) ) )

В выражении на максимальном уровне вложенности, где идет обращение к трем столбцам, мы ссылаемся сразу на три таблицы. Фактически в этот момент активны сразу три контекста строки – по одному на каждую из трех таблиц, по которым мы проходим. Также стоит отметить, что две вложенные функции возвращают строки из связанных таблиц, начиная с текущего контекста строки. Так что функция , будучи вызванной в контексте строки, пришедшем из таблицы , возвращает товары только указанной категории. То же самое касается и вызова функции , возвращающей продажи по конкретному товару. При этом показанный код является далеко не самым оптимальным с точки зрения читаемости и производительности. Вкладывать итераторы один в другой принято только в случае, если строк для перебора будет не так много: сотни – нормально, тысячи – приемлемо, миллионы – плохо. В противном случае может серьезно пострадать производительность запроса. Предыдущую формулу мы использовали, исключительно чтобы продемонстрировать возможность создания множественных вложенных контекстов строки. Позже в этой книге вы увидите более полезные примеры с применением вложенных итераторов. Здесь же мы отметим, что данную формулу можно было написать гораздо более лаконично с использованием единого контекста строки и функции : SUMX ( Sales; Sales[Quantity] * RELATED ( 'Product'[Unit Price] ) * RELATED ( 'Product Category'[Discount] ) )

Когда у нас есть множество контекстов строки в рамках разных таблиц, мы можем использовать их для ссылки на эти таблицы в одном выражении DAX. Но 118

ведение в контекст в

ислени

существует более сложный сценарий, в котором вложенные контексты строки принадлежат одной и той же таблице. Именно такой случай мы рассмотрим в следующем разделе.

Вложенные контексты строки в одной таблице Может показаться, что сценарий с вложенными контекстами строки в рамках одной и той же таблицы – явление довольно редкое. Но это не так. Этот прием встречается повсеместно, и чаще всего его можно увидеть в формулах вычисляемых столбцов. Представьте, что вам необходимо ранжировать товары по их цене. Наиболее дорогой товар в ассортименте должен получить ранг, равный единице, второй по дороговизне – двойку и т. д. Мы могли бы решить эту задачу с использованием функции , но в образовательных целях покажем, как обойтись более простыми функциями языка DAX. Для определения ранга можно просто подсчитать количество товаров в таблице, цена на которые превышает цену текущего товара. Если в базе не окажется товаров с более высокой ценой, мы присвоим текущему товару первый ранг. Если функция подсчета строк вернет единицу, значит, ранг текущего товара будет равен двум. Все, что нам нужно сделать, – это посчитать, у скольких товаров цена выше нашей, и прибавить к результату единицу. Мы могли бы попробовать написать следующую формулу для вычисляемого столбца, где PriceOfCurrentProduct – это временная заглушка для подстановки в дальнейшем цены текущего товара: 1. 'Product'[UnitPriceRank] = 2. COUNTROWS ( 3. FILTER ( 4. 'Product'; 5. 'Product'[Unit Price] > PriceOfCurrentProduct 6. ) 7. ) + 1

Функция вернет список товаров с  ценой, большей, чем у текущего товара, а  подсчитает количество возвращенных строк. Единственная проблема – как написать формулу для нахождения цены текущего товара, чтобы заменить ей временный заполнитель PriceOfCurrentProduct? Под текущим товаром мы подразумеваем значение в  заданном столбце текущей строки в момент вычисления выражения. И эта задача на самом деле сложнее, чем может показаться. Обратите внимание на пятую строку кода. В  ней выражение Product[Unit Price] ссылается на значение столбца в текущем контексте строки. А какой контекст строки активен на момент выполнения пятой строки кода? Таких контекстов два. Поскольку наш код написан в  вычисляемом столбце, у нас есть автоматически созданный контекст строки для сканирования таблицы . В то же время функция сама по себе является итератором, а  значит, создает свой собственный контекст строки, распространяющийся на ту же самую таблицу . Эта ситуация показана графически на рис. 4.9. ведение в контекст в

ислени

119

Рис 4 9 о вре в олнени вн треннего в ражени с сраз два контекста строки дл та ли Product

еств

т

Внешняя рамка характеризует контекст строки вычисляемого столбца, осуществляющего итерации по таблице . В то же время внутренняя рамка демонстрирует контекст строки функции , которая проходит по той же самой таблице. Следовательно, значение выражения Product[Unit Price] будет напрямую зависеть от того, в каком контексте строки выполняется вычисление. Получается, что ссылка на столбец Product[Unit Price] во внутренней рамке может ссылаться исключительно на значение текущей итерации функции . Проблема же состоит в том, что в этой внутренней рамке нам необходимо как-то обратиться к значению столбца со ссылкой на внешний контекст строки вычисляемого столбца, который в данный момент скрыт. Если мы не создаем внутри вычисляемого столбца дополнительных контекстов строки при помощи итераторов, обратиться к  нужному столбцу можно просто по имени в текущем контексте строки, как показано ниже: Product[Test] = Product[Unit Price]

Чтобы еще более наглядно продемонстрировать проблему, давайте попробуем вычислить значение Product[Unit Price] в обеих рамках, вставив в код специальные заглушки. В результате мы получили разные значения, что видно по рис.  4.10, где мы добавили вычисление выражения Product[Unit Price] прямо перед исключительно в образовательных целях. Products[UnitPriceRank] = Product[UnitPrice] + COUNTROWS ( FILTER ( Product, Product[Unit Price] >= PriceOfCurrentProduct ) )+1

Рис 4 10 о вне не ра ке в ражение Product[Unit Price] сс лаетс на тек и контекст строки в исл е ого стол а

120

ведение в контекст в

ислени

Напомним, что происходит в нашем сценарии: „ внутренний контекст строки, созданный функцией , скрывает внешний контекст строки; „ наша задача – сравнить значения выражения Product[Unit Price] во внутреннем и внешнем контекстах; „ если написать операцию сравнения во внутренней рамке, мы не сможем напрямую обратиться к значению Product[Unit Price] из внешнего контекста. Но мы ведь можем получить значение цены товара во внешней рамке, а значит, лучшим решением будет там же сохранить ее в переменной. В результате мы сможем получить значение переменной в контексте строки вычисляемого столбца с использованием следующего кода: 'Product'[UnitPriceRank] = VAR PriceOfCurrentProduct = 'Product'[Unit Price] RETURN COUNTROWS ( FILTER ( 'Product'; 'Product'[Unit Price] > PriceOfCurrentProduct ) ) + 1

Лучше делать подобные формулы более многословными и использовать побольше переменных, чтобы можно было проследить все этапы вычисления. Кроме того, такой код будет легче читать и поддерживать: 'Product'[UnitPriceRank] = VAR PriceOfCurrentProduct = 'Product'[Unit Price] VAR MoreExpensiveProducts = FILTER ( 'Product'; 'Product'[Unit Price] > PriceOfCurrentProduct ) RETURN COUNTROWS ( MoreExpensiveProducts ) + 1

На рис. 4.11 графически показаны разные контексты строки в этом коде. Так вам будет легче понять, в каком контексте вычисляется какое выражение. На рис. 4.12 показан результат ранжирования товаров, проведенный при помощи нашего вычисляемого столбца. Поскольку в нашей таблице оказалось сразу 14 товаров с самой высокой ценой, у всех у них проставился ранг, равный единице. У товаров со второй по величине ценой ранг был установлен в значение 15. Было бы здорово, если бы товары в таблице ранжировались последовательными числами 1, 2, 3 и т. д., а не 1, 15, 19, как это происходит сейчас. Мы совсем скоро исправим этот недочет, а сейчас позвольте сделать небольшое отступление. Чтобы решить предложенную задачу ранжирования, необходимо очень хорошо понимать, что из себя представляет контекст строки, уметь точно определять, какой контекст строки активен в том или ином фрагменте кода, и, что ведение в контекст в

ислени

121

более важно, разбираться в том, как именно контекст строки влияет на вычисление выражения DAX. Стоит еще раз подчеркнуть, что одно и то же выражение Product[Unit Price], вычисленное в разных участках кода, дало совершенно разные результаты из-за разницы контекстов. Без полного понимания того, как работают контексты вычисления в DAX, разбираться в столь сложном коде будет очень проблематично.

Рис 4 11 на ение ере енно в исл етс во вне не контексте строки

Рис 4 12 UnitPriceRank отли н ри ер ис ользовани дл навига ии о вложенн контекста строки

122

ведение в контекст в

ислени

ере енн

Как видите, даже простое вычисление ранга с использованием двух контекстов строки вызвало у нас серьезные трудности. А в главе 5 мы будем работать с  примерами со множественными контекстами, и  там сложность кода будет гораздо выше. Но если вы понимаете, как взаимодействуют контексты, все будет просто. Перед тем как двигаться дальше, вам просто необходимо хорошо разобраться в контекстах вычисления. Именно поэтому мы посоветовали бы вам пробежаться по этому разделу еще раз, а может, и по всей главе, чтобы закрепить усвоенный материал. Это значительно облегчит дальнейший процесс обучения языку DAX. А сейчас мы решим последнюю проблему – с последовательностями рангов. Постараемся привести их к привычному виду 1, 2, 3 и т. д. Решение будет гораздо более простым, чем вы могли бы себе представить. Фактически в предыдущем фрагменте кода мы концентрировались на подсчете количества товаров с большей ценой. В результате 14 товарам был присвоен ранг 1, а следующие товары получили ранг 15. Значит, считать товары было не самой лучшей идеей. Гораздо лучше будет считать цены – в этом случае все 14 товаров с одной ценой сольются в один. 'Product'[UnitPriceRankDense] = VAR PriceOfCurrentProduct = 'Product'[Unit Price] VAR HigherPrices = FILTER ( VALUES ( 'Product'[Unit Price] ); 'Product'[Unit Price] > PriceOfCurrentProduct ) RETURN COUNTROWS ( HigherPrices ) + 1

На рис. 4.13 показан новый вычисляемый столбец наряду со столбцом UnitPriceRank. Последним шагом в этой задаче был переход на подсчет цен вместо товаров, и решение оказалось более простым, чем можно было ожидать. Чем больше вы будете работать с DAX, тем легче вам будет начать мыслить категориями временных таблиц, создаваемых для определенных вычислений. Из данного примера вы узнали, что лучшим способом управления множественными контекстами в рамках одной таблицы является создание вспомогательных переменных. Помните, что переменные были введены в языке только в  2015 году. Так что вы вполне можете столкнуться с  примерами на DAX, которые были написаны до введения переменных, но прекрасно справлялись со множественными контекстами строки с использованием функции , с которой мы и познакомимся в следующем разделе.

Использование функции EARLIER Язык DAX предоставляет нам функцию , предназначенную специально для обращения к внешнему контексту строки. Функция извлекает значение по столбцу с использованием предыдущего контекста строки вместо текущего. Так что мы вполне можем переписать формулу для нашей заглушки PriceOfCurrentProduct следующим образом: EARLIER ( Product[UnitPrice] ). ведение в контекст в

ислени

123

Рис 4 13 Стол е оскольк с итает ен , а не товар

оказ вает олее оказательн е ранги,

Многие новички пугаются функции , поскольку не очень хорошо понимают концепцию контекста строки и не до конца осознают пользу вложенных друг в друга контекстов строки из одной и той же таблицы. С другой стороны, функция не представляет сложности для тех, кто усвоил понятия контекстов строки и их вложенности. Можно написать наш предыдущий код с использованием этой функции и без применения переменных: 'Product'[UnitPriceRankDense] = COUNTROWS ( FILTER ( VALUES ( 'Product'[Unit Price] ); 'Product'[UnitPrice] > EARLIER ( 'Product'[UnitPrice] ) ) ) + 1

Примечание нк и EARLIER рини ает второ арг ент, каз ва и на то, сколько агов н жно ро стить, то ла воз ожность ере р гн ть ерез несколько контекстов Более того, с еств ет также нк и EARLIEST, озвол а о ратитьс к са овер не контекст строки, о ределенно дл данно та ли реальности же ни нк и EARLIEST, ни второ арг ент нк ии EARLIER асто не ис ольз тс сли с енарии с дв вложенн и контекста и строки встре а тс на рактике довольно асто, три и олее ровн вложенности достато но редкое вление то же с о вление в ере енн нк и EARLIER ракти ески в ла из о ра ени оль инство разра от иков отда т ред о тение и енно ере енн

124

ведение в контекст в

ислени

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

Функции FILTER, ALL и взаимодействие между контекстами Ранее мы использовали функцию исключительно для осуществления фильтрации таблицы. Это очень распространенная функция, необходимая для создания новых ограничений, накладывающихся на существующий контекст фильтра. Представьте, что вам нужно создать меру, в которой будет подсчитано количество красных товаров. С учетом знаний, полученных нами ранее, мы можем легко написать такую формулу: NumOfRedProducts := VAR RedProducts = FILTER ( 'Product'; 'Product'[Color] = "Red" ) RETURN COUNTROWS ( RedProducts )

Эту меру спокойно можно использовать в  отчетах. Например, мы можем вынести наименования брендов на строки и построить отчет, показанный на рис. 4.14.

Рис 4 14 Можно одс итать коли ество красн товаров в та ли е с ис ользование нк ии FILTER

Перед тем как двигаться дальше, остановитесь на минутку и  подумайте о  том, как именно DAX получил эти значения. Столбец принадлежит таблице . Внутри каждой ячейки отчета контекст фильтра накладывает ведение в контекст в

ислени

125

ограничения по одному конкретному бренду. Таким образом, в каждой ячейке мы видим количество товаров определенного бренда и при этом исключительно красного цвета. Причина этого в том, что функция проходит по таблице в том виде, в котором она видна в рамках текущего контекста фильтра, включающего конкретный бренд. Кажется, что это все очень просто, но не лишним будет повторить это несколько раз, чтобы уж точно не забыть. Такое поведение меры становится более очевидным с добавлением к отчету среза, фильтрующего товары по цвету. На рис. 4.15 показаны два идентичных отчета со срезами по цвету, каждый из которых фильтрует отчет непосредственно справа от себя. В отчете слева выбран фильтр по красному цвету товаров (Red), и цифры в этом отчете такие же, как на рис. 4.14. В то же время правый отчет, где в срезе указан лазурный цвет (Azure), остался пустым.

Рис 4 15 расс ит вает ер NumOfRedProducts, рини а во вни ание вне ни контекст ильтра, заданн в срезе

В правом отчете функция проходит по таблице в  рамках внешнего контекста фильтра, включающего в  себя лазурный цвет, при этом в самой мере прописан фильтр по красному цвету, что приводит к несогласованным данным и пустому выводу в отчете. Иными словами, в каждой ячейке этого отчета мера возвращает BLANK. На этом примере мы показали, что в одной и той же формуле вычисление производится как во внешнем контексте фильтра, учитывающем срезы вне самого отчета, так и в контексте строки, созданном непосредственно функцией внутри формулы. Оба контекста действуют одновременно и влияют на результат вычисления. Контекст фильтра DAX использует для оценки таблицы , а контекст строки – для построчной проверки фильтрующего условия во время итераций в функции . Мы хотим заново повторить эту концепцию: функция FILTER не изменяет контекст фильтра.  – это итерационная функция, которая сканирует таблицу (уже отфильтрованную при помощи контекста фильтра) и  возвращает набор данных, соответствующий условиям фильтра. В отчете, показанном на рис. 4.14, контекст фильтра включает в себя только бренды, и после возврата результата из функции он по-прежнему фильтрует только бренды. Добавив в отчет срезы по цвету (рис. 4.15), мы расширили контекст фильтра до брендов и  цветов. Именно поэтому в  левом отчете функция вернула 126

ведение в контекст в

ислени

все товары из итераций, а в правом список товаров оказался пустым. В обоих отчетах функция не изменяла контекст фильтра, а только сканировала таблицу и возвращала отфильтрованный результат. На этом этапе кому-то из вас, вероятно, хотелось бы иметь функцию, позволяющую получить полный список красных товаров, независимо от выбора пользователя в срезах. И здесь на помощь придет уже знакомая нам функция . Эта функция возвращает содержимое таблицы, игнориру ри этом контекст фил тра. Давайте определим новую меру, назовем ее и пропишем для нее следующую формулу: NumOfAllRedProducts := VAR AllRedProducts = FILTER ( ALL ( 'Product' ); 'Product'[Color] = "Red" ) RETURN COUNTROWS ( AllRedProducts )

В этом случае в функции будут запускаться итерации не по таблице , а по выражению ALL ( Product ). Функция игнорирует установленный контекст фильтра и всегда возвращает все строки из таблицы, так что функция в данном случае вернет красные товары, даже если таблица была предварительно отфильтрована по другим брендам или цветам. Результат, показанный на рис.  4.16, может вас удивить, несмотря на свою корректность.

Рис 4 16

Мера NumOfAllRedProducts верн ла неожиданн

рез льтат

Здесь есть пара любопытных моментов, и мы хотим поговорить о них подробно: „ результат во всех ячейках равен 99 вне зависимости от бренда в строке; „ бренды в левом отчете отличаются от брендов в правом. Заметим, что 99 – это количество всех красных товаров в таблице, а не их количество по конкретному бренду. Функция , как и ожидалось, проигнориведение в контекст в

ислени

127

ровала все фильтры, наложенные на таблицу . При этом игнорируются не только фильтры по цвету, но и по брендам. Возможно, вы этого не хотели. Однако функция работает именно так – она очень простая и мощная, но действует по принципу «или все, или ничего», игнорируя абсолютно все фильтры, наложенные на указанную таблицу. В данный момент у вас не хватает знаний, чтобы обеспечить игнорирование только части установленных фильтров. В нашем примере логичнее было бы игнорировать лишь фильтр по цвету. Но с функцией , которая позволяет более выборочно управлять наложенными фильтрами, мы познакомимся только в следующей главе. Теперь рассмотрим второй момент, заключающийся в том, что список брендов в  двух отчетах отличается. Поскольку у  нас в  срезе выбран только один цвет, полная матрица отчета вычисляется с учетом этого цвета. В левом отчете у нас выбран красный цвет, а в правом – лазурный. Этот выбор ограничивает список товаров, а значит, и список брендов. Перечень брендов, используемый для вывода в отчет, строится с учетом текущего контекста фильтра, содержащего фильтр по цвету. После определения списка брендов происходит вычисление меры, в результате чего мы получаем число 99 вне зависимости от текущего бренда и цвета. Таким образом, в левом отчете мы видим список брендов, в которых есть товары красного цвета, а в правом – лазурного, тогда как все цифры в обоих отчетах показывают количество товаров красного цвета, независимо от бренда. Примечание оведение того от ета не арактерно дл нк ии SUMMARIZECOLUMNS, ис ольз е о в М ие в главе 1

, а оль е од одит дл ознако и с с то нк-

На данном этапе мы закончим работать с  этим примером. Решение этого сценария придет позже, когда мы познакомимся с функцией , дающей больше свободы в работе с контекстами фильтра. Здесь мы использовали этот пример, чтобы показать вам, что даже простые формулы могут давать неожиданные результаты вследствие взаимодействия контекстов и сосуществования в одном и том же выражении контекста фильтра и контекста строки.

Работа с несколькими таблицами Теперь, когда вы изучили основы контекстов вычисления, можно поговорить о том, как ведут себя контексты при наличии связей между таблицами. Мало какие модели данных состоят всего из одной таблицы. Скорее всего, вы будете иметь дело с  несколькими таблицами, объединенными связями. А  если таблицы и  объединены связью, означает ли это, что фильтр, наложенный на таблицу , будет автоматически распространяться на таблицу ? А как насчет фильтра на ? Будет ли он распространяться на ? Поскольку существует два вида контекста вычисления (контекст фильтра и контекст строки) и две стороны у связей («один» и «многие»), у нас набирается ровно четыре сценария для анализа. 128

ведение в контекст в

ислени

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

Рис 4 17 Модель данн

дл из ени взаи оде стви

ежд контекста и и св з

и

Стоит отметить пару деталей относительно представленной модели данных: „ от таблицы к таблице ведет целая цепочка связей через таблицы и  ; „ единственная двунаправленная связь объединяет таблицы и  . Все остальные связи в модели – однонаправленные. Подобная модель данных может быть очень полезной при изучении взаимодействий между контекстами вычисления и связями, о чем мы будем говорить в следующих разделах.

онтексты строки и связи Контекст строки осу ествл ет итера ии о та ли е он не фил трует. Речь идет о  построчном сканировании таблицы и  последовательном выполнении той или иной операции. Обычно в  отчетах нам нужны какие-то агрегации вроде суммы или среднего значения. Во время прохода по таблице контекст строки перебирает строки конкретной таблицы, предоставляя доступ к инфорведение в контекст в

ислени

129

мации по всем столбцам, но только из этой таблицы. В других таблицах – даже связанных с нашей – контекст строки в этот момент не создан. Иными словами, контекст строки сам по себе автоматически не взаимодействует с существующими в модели связями. Давайте рассмотрим для примера вычисляемый столбец в  таблице , хранящий разницу между ценой товара в таблице фактов и ценой из справочника. Следующий код на языке DAX работать не будет, поскольку мы пытаемся ссылаться на столбец Product[UnitPrice], в то время как контекст строки для таблицы не создан: Sales[UnitPriceVariance] = Sales[Unit Price] - RELATED ( 'Product'[Unit Price] )

Функция требует наличия контекста строки (то есть итерации) в таблице, находящейся в связи на стороне «многие». Если контекст строки будет активным на стороне «один», функция нам не поможет, поскольку она найдет сразу несколько строк, следуя по связи. В случае, когда мы осуществляем итерации по таблице со стороны «один», придется воспользоваться функцией . Функция возвращает все строки из таблицы, находящейся в связи на стороне «многие», соотносящиеся с таблицей, по которой мы осуществляем итерации. Например, если вам необходимо подсчитать количество продаж по каждому товару, вам поможет следующая формула для вычисляемого столбца в таблице : Product[NumberOfSales] = VAR SalesOfCurrentProduct = RELATEDTABLE ( Sales ) RETURN COUNTROWS ( SalesOfCurrentProduct )

Это выражение подсчитывает количество строк в таблице , соответствующих выбранному товару. Результат вычисления меры представлен на рис. 4.18.

Рис 4 18 нк и RELATEDTABLE ожет на стороне один

ть олезна в контексте строки

При этом обе функции – и   – способны проходить по целым цепочкам связей, а не ограничены доступом только к таблице, объ130

ведение в контекст в

ислени

единенной с текущей напрямую. Допустим, вы можете создать вычисляемый столбец с такой же формулой, как в предыдущем примере, но на этот раз в таблице : 'Product Category'[NumberOfSales] = VAR SalesOfCurrentProductCategory = RELATEDTABLE ( Sales ) RETURN COUNTROWS ( SalesOfCurrentProductCategory )

Результатом будет количество продаж по каждой категории товаров, при этом доступ к таблице будет осуществляться не непосредственно, а сразу через две транзитные таблицы и  . Похожим образом вы можете создать вычисляемый столбец в таблице с копией наименования категории из таблицы : 'Product'[Category] = RELATED ( 'Product Category'[Category] )

В этом случае функция проделает путь от таблицы через транзитную таблицу .

к 

Примечание динственн искл ение из того равила дет оведение нк и RELATED и RELATEDTABLE в св з ти а один к одно сли две та ли о единен тако св зь , ожно ри ен ть л из ти нк и , но рез льтато дет ли о знаение стол а, ли о та ли а с одно строко в зависи ости от ис ольз е о нк ии

Также стоит отметить, что для успешного прохождения по цепочке все связи должны быть одного типа, то есть «один ко многим» или «многие к одному». Если две таблицы будут связаны через промежуточную та ли у мост (bridge table) связями «один ко многим» и «многие к одному» соответственно, ни , ни не будут корректно работать при условии, что все связи будут однонаправленными. При этом из этих двух функций только умеет работать с  двунаправленными связями, как будет показано далее. С другой стороны, связь типа «один к  одному» ведет себя одновременно как связь «один ко многим» и «многие к одному». Так что такая связь вполне может быть одним из звеньев цепочки, соединяющей несколько таблиц. Например, в нашей модели данных таблица связана с  , а   – с  . При этом таблицы и  объединены связью типа «один ко многим», а  с   – «многие к одному». Получается, что у нас есть цепочка связей между таблицами и  . Но при этом две связи из этой цепочки различаются по типу. Такой сценарий характеризуется образованием связи «многие ко многим». Одному покупателю соответствуют многие купленные им товары, и в то же время один товар могут приобрести сразу несколько покупателей. Подробно о связях типа «многие ко многим» мы будем говорить в главе 15, а сейчас нас интересуют только вопросы, связанные с контекстом строки. Если вы будете использовать функцию для таблиц, объединенных связью типа «многие ко многим», то получите неправильные результаты. Посмотрите на следующий вычисляемый столбец, созданный в таблице : ведение в контекст в

ислени

131

Product[NumOfBuyingCustomers] = VAR CustomersOfCurrentProduct = RELATEDTABLE ( Customer ) RETURN COUNTROWS ( CustomersOfCurrentProduct )

В результате мы получим не количество покупателей, приобретавших конкретный товар, а общее количество покупателей, как показано на рис. 4.19.

Рис 4 19

нк и RELATEDTABLE не ра отает со св зь

ногие ко ноги

Функция не может пробиться по цепочке связей, поскольку они различаются по типу. Контекст строки от таблицы не достигает таблицы . Интересно следующее: если мы будем проходить по цепочке связей в обратном направлении, то есть попытаемся получить количество товаров, которые были приобретены конкретным покупателем, результат окажется правильным, – мы увидим для каждого отдельного покупателя свое количество товаров. Причина такого поведения модели не в распространении контекста строки, а в преобразовании контекста, осуществляемом функцией . Последнее замечание мы сделали исключительно для полноты картины. Вы гораздо лучше поймете, как это работает, прочитав главу 5.

онтекст фильтра и связи В предыдущем разделе вы узнали, что контекст строки предназначен для осуществления итераций по таблице, а  значит, не использует связи. Контекст фильтра, напротив, фильтрует данные. Кроме того, контекст фильтра не принадлежит какой-то одной таблице, а воздействует на всю модель данных в целом. И сейчас мы можем немного обновить нашу мантру, посвященную контекстам вычисления, чтобы она отражала истинную картину: Контекст фил тра фил трует модел данн л ет итера ии о та ли е

а контекст строки осу еств

Поскольку контекст фильтра воздействует на всю модель, логично предположить, что для этого он использует связи между таблицами. При этом взаимодействие контекста фильтра со связями осуществляется автоматически и зависит от на равлени кросс фил тра ии (cross-filter direction), установленного 132

ведение в контекст в

ислени

для каждой из них. Направление кросс-фильтрации обозначается в  модели данных маленькой стрелкой посередине связи, как видно на рис. 4.20.

а равление кросс ильтра ии дв на равленна

а равление кросс ильтра ии однона равленна

Рис 4 20

оведение контекста ильтра и св зе

Контекст фильтра распространяется по связи в  направлении, показанном стрелкой. Во всех без исключения связях распространение контекста фильтра осуществляется от стороны «один» к  стороне «многие», тогда как обратное распространение допустимо только при включении режима двунаправленной кросс-фильтрации. Связь с установленным однонаправленным режимом кросс-фильтрации называется однона равленной св (unidirectional relationship), а с двунаправленным режимом – двуна равленной (bidirectional relationship). Такое поведение контекста фильтра интуитивно понятно. И  хотя мы ранее специально не касались этой терминологии, фактически во всех отчетах, которые мы использовали, так или иначе было реализовано описанное поведение контекста фильтра. Например, в типичном отчете с фильтром по цвету товаров ( ) и агрегацией по количеству проданных товаров ( ) вполне логично ожидать, что фильтр от таблицы распространит свое действие на таблицу . Причина такого поведения фильтра в том, что таблица находится в связи с  на стороне «один», что позволяет фильтрам беспрепятственно распространяться с таблицы товаров на таблицу продаж вне зависимости от установленного направления кроссфильтрации. Поскольку в нашей модели данных присутствует как двунаправленная связь, так и  множество однонаправленных, можно продемонстрировать поведение фильтра на примере, используя три меры, подсчитывающие количество строк в трех разных таблицах: , и  . [NumOfSales] := COUNTROWS ( Sales ) [NumOfProducts] := COUNTROWS ( Product ) [NumOfCustomers] := COUNTROWS ( Customer )

ведение в контекст в

ислени

133

В следующем отчете мы вынесли на строки столбец . Таким образом, каждая мера рассчитывается в  контексте фильтра, включающем цвет товара. Результаты отчета показаны на рис. 4.21.

Рис 4 21

е онстра и

оведени контекста ильтра и св зе

В этом примере фильтры беспрепятственно распространяются по связям от стороны «один» к стороне «многие». Фильтр начинает движение с  . Далее он распространяется на таблицу , расположенную в связи с  на стороне «многие», и на саму таблицу . В то же время в мере для всех строк в таблице показано одно и то же значение, отражающее общее количество покупателей в  базе. Это происходит из-за невозможности распространения фильтра по связи между таблицами и  от таблицы к  . В результате фильтр проходит от таблицы к  , но до не доходит. Вы могли заметить, что связь между таблицами и  в нашей модели двунаправленная. В связи с этим контекст фильтра, включающий информацию из таблицы , легко распространится на и  . Мы можем доказать это, немного перестроив отчет и вынеся на строки ра ование вместо . Результат показан на рис. 4.22. На этот раз фильтр берет свое начало с таблицы . Он легко достигает таблицы , поскольку она располагается в связи на стороне «многие». Далее фильтр распространяется с таблицы на благодаря двунаправленному характеру связи между этими таблицами. Заметьте, что наличие в цепочке единственной двунаправленной связи не делает таковой всю цепочку. Например, похожая мера, призванная подсчитывать количество подкатегорий, наглядно демонстрирует, что контекст фильтра не может распространиться от таблицы на : 134

ведение в контекст в

ислени

Рис 4 22 ильтра и о о разовани та ли а Product также ильтр етс

ок

ателе ,

NumOfSubcategories := COUNTROWS ( 'Product Subcategory' )

Добавление новой меры к  предыдущему отчету привело к  результату, показанному на рис. 4.23. Заметьте, что количество подкатегорий для всех строк оказалось одинаковым.

Рис 4 23 з за однона равленности св зи та ли а ок не ожет ильтровать данн е о одкатегори

ателе

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

Рис 4 24 сли св зь дв на равленна , ок атели ог т ильтровать одкатегории товаров

ведение в контекст в

ислени

135

Для распространения контекста строки по связям мы используем функции и  , тогда как для распространения контекста фильтра никаких дополнительных функций не требуется. Контекст фильтра накладывает ограничения на модель, а не на таблицу. Таким образом, после применения контекста фильтра к любой таблице фильтр автоматически распространяется по связям на всю модель. Важно о на и ри ера огло сложитьс в е атление, то вкл ение дв на равленно кросс ильтра ии дл все ез искл ени та ли в одели данн вл етс о ти альн в оро , оскольк в то сл ае все ильтр д т авто ати ески возде ствовать на вс одель Но то не так одро нее оговори о св з в главе 1 в на равленн е св зи нес т в се е о ределенн сложность, но ва ока рано о то знать ак то ни ло, ис ользовать такие св зи ожно только с олн они ание воз ожн оследстви ак равило, в должн вкл ать режи дв на равленно ильтра ии дл св зи в конкретно ере, ис ольз нк и CROSSFILTER, и делать то н жно только в сл ае кра не нео оди ости

Использование функций DISTI CT и SUMMARI E в контекстах фильтра На данном этапе вы должны уже хорошо разбираться в контекстах вычисления, и мы используем эти знания для пошагового решения одного интересного сценария. Попутно мы расскажем вам о некоторых неочевидных нюансах, которые, надеемся, смогут расширить ваши фундаментальные знания в области контекстов строки и контекстов фильтра. Также в этом разделе мы более подробно коснемся функции , которую вскользь затронули в главе 3. Перед тем как начать, заметим, что в процессе разбора сценария мы будем постепенно переходить от неправильных вариантов решения к правильному. Это сделано в образовательных целях, ведь нашей задачей является научить вас писать код на DAX, а не предоставить готовое решение. Создавая меры, вы, разумеется, поначалу будете допускать ошибки, и на этом примере мы постараемся подробно описать наш ход мыслей, что может помочь вам в будущем самостоятельно исправлять неточности в своем коде. Задача перед нами стоит следующая: вычислить средний возраст покупателей компании Contoso. И хотя на первый взгляд кажется, что вопрос сформулирован корректно, на самом деле это не так. О каком возрасте мы говорим? О текущем или о том, в котором они совершали покупки? Если человек покупал товары трижды, должны ли мы считать это одной покупкой или тремя разными при подсчете среднего? А что, если он приобретал товары в разном возрасте? Нужно сформулировать задачу более конкретно. И мы это сделали: ре уетс ос итат средний во раст оку ателей на момент оку ки ри этом все о ку ки совер енн е еловеком в одном во расте у ит ват тол ко один ра . Процесс решения можно условно разбить на две стадии: „ вычисление возраста покупателя на момент покупки; „ усреднение вычисленного показателя. 136

ведение в контекст в

ислени

Возраст покупателя меняется со временем, так что нам нужно иметь возможность сохранять его в таблице . Что ж, будем реализовывать хранение возраста покупателя на момент приобретения товара в каждой строке таблицы Sales. Следующий вычисляемый столбец вполне подойдет для решения этой задачи: Sales[Customer Age] = DATEDIFF ( -- Вычисляем разницу между RELATED ( Customer[Birth Date] ); -- датой рождения покупателя Sales[Order Date]; -- и датой покупки YEAR -- в годах )

Поскольку является вычисляемым столбцом, его значение вычисляется в  контексте строки, осуществляющем итерации по таблице . В  формуле нам потребовалось обратиться к  столбцу из таблицы , находящейся на стороне «один» в  связи с  таблицей . В этом случае мы можем использовать функцию для доступа к целевой таблице. При этом в базе данных Contoso есть немало покупателей, у которых не заполнено поле даты рождения. И функция послушно вернет пустое значение, если таковым будет ее первый параметр. Поскольку наша задача состоит в получении среднего возраста покупателей, нашей первой (и неверной) мыслью может быть создание меры, вычисляющей среднее значение по столбцу: Avg Customer Age Wrong := AVERAGE ( Sales[Customer Age] )

Результат, который мы получим, будет некорректным, поскольку в столбце будет много повторяющихся значений, если покупатель несколько раз приобретал товары в  одном возрасте. Согласно нашей задаче, в этом случае необходимо учитывать только один факт покупки, а наша текущая формула работает иначе. На рис. 4.25 показан результат вычисления нашей меры в сравнении с корректными ожидаемыми значениями. Проблема состоит в том, что возраст каждого покупателя должен быть учтен лишь раз. Возможное решение – и тоже неправильное – заключается в  применении функции к столбцу с возрастом и дальнейшем усреднении полученного значения, как показано в мере ниже: Avg Customer Age Wrong Distinct := AVERAGEX ( -- Проходим по уникальным значениям возрастов DISTINCT ( Sales[Customer Age] ); -- и рассчитываем среднее значение Sales[Customer Age] -- по этому показателю )

Это решение, как мы уже сказали, также неправильное. Фактически функция просто вернет уникальные значения возрастов из нашей базы продаж. Таким образом, два покупателя, совершивших покупки в одном возрасте, посчитаются лишь раз. Получается, что мы хотели учитывать покупателей в одном возрасте один раз, а учитываем сам возраст без привязки к покупателям. На рис. 4.26 показан вывод меры в сравнении с правильными значениями. Как видите, мы все еще далеки от правильного решения. ведение в контекст в

ислени

137

Рис 4 25 ростое среднение возраста ок не дало ожидае рез льтатов

Рис 4 26

среднение никальн

ателе

возрастов ок

ателе также не о огло

Можно, конечно, попробовать заменить в нашей формуле параметр функции с  на , чтобы получился следующий код: Avg Customer Age Invalid Syntax := AVERAGEX ( DISTINCT ( Sales[CustomerKey] );

138

-- Проходим по уникальным значениям -- Sales[CustomerKey] и рассчитываем среднее

ведение в контекст в

ислени

Sales[Customer Age]

-- по этому показателю

)

Но такая формула вовсе не выполнится, поскольку содержит ошибку. Сможете найти ее самостоятельно, не читая следующий абзац? Дело в  том, что функция , как любой итератор, создает при запуске контекст строки. Первым параметром в функцию передается DISTINCT ( Sales[CustomerKey] ). Функция возвращает таблицу с единственным столбцом, содержащим уникальные значения кодов покупателей. Таким образом, контекст строки, созданный функцией , будет содержать только один столбец, а именно . DAX просто не сможет вычислить значение в контексте строки, содержащем только . Что нам нужно, так это каким-то образом получить контекст строки с грану л рност (granularity) на уровне , который также будет содержать . А мы помним, что функция , которую мы проходили в главе 3, умеет создавать набор уникальных комбинаций двух столбцов из таблицы. Это ее свойство и  поможет нам написать правильную формулу, отвечающую всем нашим требованиям: Correct Average := AVERAGEX ( SUMMARIZE ( Sales; Sales[CustomerKey]; Sales[Customer Age] ); Sales[Customer Age] )

--------

Проходим по всем существующим комбинациям в таблице Sales из ключа покупателя и его возраста и считаем средний возраст

Как обычно, можно использовать переменные, чтобы разбить выполнение кода на этапы. Заметьте, что обращение к столбцу по-прежнему требует ссылки на таблицу во втором параметре функции . Дело в том, что переменная может содержать в себе таблицу, но не может использоваться в качестве ссылки на нее. Correct Average := VAR CustomersAge = SUMMARIZE ( Sales; Sales[CustomerKey]; Sales[Customer Age] ) RETURN AVERAGEX ( CustomersAge; Sales[Customer Age] )

-----

Существующие комбинации в таблице Sales из ключа покупателя и его возраста

-- Проходим по сочетаниям -- ключей и возрастов покупателей в таблице Sales -- и вычисляем среднее значение возраста

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

ислени

139

зом, разные покупатели с одинаковым возрастом будут учитываться отдельно. Функция игнорирует присутствие в  таблице, она использует только возраст покупателей. Столбец нужен лишь для корректного подсчета количества уникальных возрастов. Необходимо подчеркнуть, что наша мера вычисляется в  рамках контекста фильтра в  отчете. Таким образом, функцией будут обработаны и возвращены только те покупатели, которые приобретали товары. В каждой ячейке отчета действует свой контекст фильтра, учитывающий только тех покупателей, которые приобрели как минимум один товар определенного цвета, указанного в отчете.

Закл чение Пришло время вспомнить, что мы узнали из этой главы о контекстах вычисления: „ существуют два контекста вычисления: контекст фильтра и  контекст строки. При этом они не являются разновидностями одной концепции: контекст фильтра фильтрует всю модель данных, а контекст строки осуществляет итерации по одной таблице; „ чтобы понять поведение той или иной формулы, необходимо правильно оценить оба контекста вычисления, поскольку они действуют одновременно; „ DAX открывает контекст строки автоматически всякий раз, когда создается вычисляемый столбец в таблице. Также создать контекст строки можно программно при помощи итерационной функции. Каждая такая функция открывает свой контекст строки; „ контексты строки можно вкладывать друг в друга, и в случае если они действуют в одной и той же таблице, внутренний контекст строки будет скрывать внешний. Для сохранения значений, полученных в  определенном контексте строки, можно использовать переменные. В  ранних версиях DAX, в которых переменные не присутствовали, можно было для обращения к предыдущему контексту строки обращаться при помощи функции . Сегодня в использовании этой функции нет необходимости; „ при проходе по таблице, являющейся результатом выполнения табличного выражения, контекст строки содержит только столбцы, возвращенные табличным выражением; „ в клиентских инструментах наподобие Power BI контекст фильтра создается при размещении элементов в строках, столбцах, срезах и фильтрах. Контекст фильтра также может быть создан программно при помощи функции , о которой мы будем говорить в следующей главе; „ контекст строки не распространяется по связям автоматически. При необходимости распространить его вручную можно использовать функции и  . При этом каждую из этих функций нужно использовать в  контексте строки строго на определенной стороне связи: на стороне «многие»,  – на стороне «один»; 140

ведение в контекст в

ислени

„ контекст фильтра фильтрует модель данных, используя связи в ней в соответствии с их направлениями кросс-фильтрации. Фильтры всегда распространяются от стороны «один» к стороне «многие». Если установить режим двунаправленной кросс-фильтрации для связи, фильтры будут распространяться по ней и в обратном направлении – от стороны «многие» к стороне «один». К этому моменту вы усвоили все самые сложные концепции языка DAX. Эти концепции полностью определяют и регулируют процесс вычисления всех ваших формул и являются настоящими столпами языка. Если написанные вами выражения не дают ожидаемых результатов, велика вероятность, что вы просто не до конца усвоили перечисленные выше правила. Как мы уже сказали во введении, на первый взгляд эти правила выглядят весьма простыми. Такими они и являются на самом деле. Сложность заключается в том, что в своих выражениях на DAX вам часто придется поддерживать разные активные контексты вычисления в  разных частях формулы. Мастерство в работе со множественными контекстами вычисления приходит с опытом, и мы постараемся обеспечить вам его при помощи многочисленных примеров в следующих главах. В процессе самостоятельного написания формул на DAX к вам постепенно придет понимание того, какие контексты в тот или иной момент используются и каких функций они требуют. Шаг за шагом вы освоите все тонкости языка и станете настоящим гуру в мире DAX.

ГЛ А В А 5

Функции CALCULATE и CALCULATETABLE В этой главе мы продолжим путешествовать по миру DAX и детально опишем лишь одну функцию . Сразу заметим, что все сказанное далее будет относиться и к функции , отличающейся от лишь тем, что она возвращает таблицу, а не скалярную величину. Для простоты изложения мы будем показывать примеры с использованием функции , но вы должны помнить, что они также будут работать и с функцией .  – наиболее важная, полезная и сложная функция в языке DAX, так что она заслуживает отдельной главы. Усвоить саму функцию не составит большого труда, поскольку она выполняет не так много действий. Сложность функций и  состоит в том, что только они в языке DAX способны создавать новые контексты фильтра. Так что, несмотря на свою видимую простоту, использование этих функций в выражениях DAX сразу повышает их сложность. Материал в этой главе по трудности усвоения не уступает содержимому предыдущей главы. Мы советуем вам внимательно прочитать ее, усвоив базовую концепцию функции , и двигаться дальше. А затем, когда вы столкнетесь со сложностями при понимании той или иной формулы, можете вернуться и перечитать эту главу заново. Вероятнее всего, вы будете обнаруживать для себя что-то новое при каждом следующем прочтении.

Введение в функции CALCULATE и CALCULATETABLE В предыдущей главе мы говорили главным образом о двух контекстах вычисления: контексте строки и контексте фильтра. Контекст строки создается автоматически при добавлении в таблицу вычисляемого столбца, а также может быть создан программно при задействовании итерационной функции. Контекст фильтра появляется в модели данных в момент конфигурирования отчета пользователем, а о его программном создании мы пока не говорили. Именно для управления контекстом фильтра и  существуют в  языке DAX функции и  . На самом деле только эти две функции способны создавать новые контексты фильтра при помощи манипулирования существующими. Здесь и далее мы будем показывать примеры преимущественно с  использованием функции , но вы должны помнить, что все 142

нк ии

и

эти же операции доступны и  для функции , единственным отличием которой от является то, что она возвращает таблицу, а не скалярную величину. Позже в этой книге – в главах 12 и 13 – мы рассмотрим больше примеров с использованием функции .

Создание контекста фильтра В этом разделе мы покажем на примере, зачем вам может понадобиться создавать контексты фильтра. Вы также увидите, что формулы без использования созданных программно контекстов фильтра могут оказаться чересчур многословными и плохо читаемыми. Дополнение этих функций вручную созданными контекстами способно значительно облегчить код, ранее казавшийся очень сложным. Contoso – это компания, торгующая электроникой по всему миру. При этом определенная часть их товаров принадлежит собственному бренду Contoso. Наша задача – построить отчет, в  котором можно было бы в  сумме и  в  процентах сравнить валовую прибыль от продажи товаров собственного бренда со сторонними. Для начала определимся с базовыми расчетами, показанными ниже: Sales Amount := SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) Gross Margin := SUMX ( Sales; Sales[Quantity] * ( Sales[Net Price] - Sales[Unit Cost] ) ) GM % := DIVIDE ( [Gross Margin]; [Sales Amount] )

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

Рис 5 1 ри ер в от ете озвол в разрезе категори товаров

т егло о енить валов

ри

ль ко

ании

Следующие шаги в работе с этим отчетом будут не самыми простыми. Финальный отчет, к которому мы хотели бы прийти, показан на рис. 5.2. Тут мы нк ии

и

143

видим два дополнительных столбца, в  которых выводится валовая прибыль по нашему собственному бренду Contoso, выраженная в деньгах и процентах.

Рис 5 2 о ренд

оследни дв

колонка

оказана валова

ри

ль в деньга и ро ента

У вас уже достаточно знаний, чтобы самостоятельно написать код для этих двух мер. В действительности, по причине того, что нам необходимо ограничить вычисления единственным брендом, лучше всего будет воспользоваться функцией , которая и создана для подобных операций: Contoso GM := VAR ContosoSales = -- Сохраняем строки из Sales по товарам бренда Contoso FILTER ( -- в отдельную переменную Sales; RELATED ( 'Product'[Brand] ) = "Contoso" ) VAR ContosoMargin = -- Проходим по табличной переменной ContosoSales, SUMX ( -- чтобы рассчитать валовую прибыль только для Contoso ContosoSales; Sales[Quantity] * ( Sales[Net Price] - Sales[Unit Cost] ) ) RETURN ContosoMargin

В табличной переменной содержатся строки из исходной таблицы , относящиеся исключительно к  товарам бренда Contoso. После вычисления этой переменной мы проходим по строкам в  ней при помощи функции и рассчитываем валовую прибыль. Поскольку итерации мы запускаем по таблице , а фильтр накладываем на таблицу , нам необходимо использовать функцию для извлечения соответствующих товаров из . Похожим образом мы можем вычислить валовую прибыль для бренда Contoso в процентах – для этого нам придется дважды пройти по исходной таблице продаж: Contoso GM % := VAR ContosoSales = -- Сохраняем строки из Sales по товарам бренда Contoso FILTER ( -- в отдельную переменную Sales; RELATED ( 'Product'[Brand] ) = "Contoso"

144

нк ии

и

) VAR ContosoMargin = -- Проходим по табличной переменной ContosoSales, SUMX ( -- чтобы рассчитать валовую прибыль только для Contoso ContosoSales; Sales[Quantity] * ( Sales[Net Price] - Sales[Unit Cost] ) ) VAR ContosoSalesAmount = -- Проходим по табличной переменной ContosoSales, SUMX ( -- чтобы рассчитать сумму продаж только для Contoso ContosoSales; Sales[Quantity] * Sales[Net Price] ) VAR Ratio = DIVIDE ( ContosoMargin; ContosoSalesAmount ) RETURN Ratio

Код для меры получился чуть более длинным, но в плане логики он во многом повторяет формулы меры . Хотя эти меры и выводят правильные результаты, легко заметить, что присущая DAX элегантность куда-то вдруг пропала. В  самом деле, у  нас в  модели уже есть две меры для вычисления валовой прибыли в деньгах и процентах, но из-за необходимости накладывать дополнительные фильтры на бренд нам пришлось, по сути, переписывать их заново. Стоит подчеркнуть, что наши базовые меры и  вполне способны справиться с  вычислениями по бренду Contoso. По рис.  5.2 видно, что валовая прибыль для товаров бренда Contoso составляет 3 877 070,65 в деньгах и 52,73 – в процентах. Но мы можем получить те же самые цифры и при помощи среза по бренду для наших мер и  , как видно по рис. 5.3.

Рис 5 3 Срез о ренд озволил ол ить данн е о в ера Gross Margin и GM %

В выделенной строке контекст фильтра был создан путем наложения фильтра по бренду Contoso. Как мы помним, контекст фильтра фильтрует всю модель нк ии

и

145

данных в целом. Таким образом, наложенный на столбец фильтр оказал воздействие и на таблицу благодаря связи, присутствующей между таблицами и  . Так что мы просто воспользовались косвенной фильтрацией таблиц по связям. Ах, если бы мы могли создать контекст фильтра для меры программно и отфильтровать при помощи него только бренд Contoso. Тогда две оставшиеся меры мы бы вычислили очень легко и просто. И здесь на помощь приходит функция . Полное описание функции мы дадим далее в этой главе. А сейчас просто посмотрим на ее синтаксис: CALCULATE ( Выражение, Условие1, ... УсловиеN )

Функция может принимать любое количество параметров. Единственным обязательным параметром при этом является первый, в  котором содержится выражение для вычисления. Условия, следующие за выражением, называются аргументами фил тра (filter arguments). Функция создает новый контекст фильтра, основываясь на переданных аргументах фильтра. Созданный контекст фильтра применяется ко всей модели, и в рамках него вычисляется выражение из первого параметра. Таким образом, воспользовавшись функцией , можно существенно упростить код для мер и  : Contoso GM := CALCULATE ( [Gross Margin]; 'Product'[Brand] = "Contoso" ) Contoso GM % := CALCULATE ( [GM %]; 'Product'[Brand] = "Contoso" )

-- Рассчитываем валовую прибыль -- в контексте фильтра, где бренд = Contoso

-- Рассчитываем валовую прибыль в процентах -- в контексте фильтра, где бренд = Contoso

И снова здравствуйте, простота и элегантность языка DAX! Создав контекст фильтра, в котором бренд отфильтрован по названию Contoso, мы смогли воспользоваться существующими мерами с  измененным поведением, вместо того чтобы писать все заново. Функция позволяет создавать новые контексты фильтра путем манипулирования фильтрами в текущем контексте. Как видите, это позволило нам сделать наш код элегантным и лаконичным. В следующих разделах мы представим полное, более формализованное определение функции и подробно расскажем, как она работает и как можно воспользоваться всеми ее преимуществами. Пока мы оставим наш пример в том виде, в каком он есть, хотя на самом деле изначальное определение мер по бренду Contoso не в полной мере эквивалентно семантически итоговому определению. Между ними есть некоторые различия, которые необходимо очень хорошо понимать. 146

нк ии

и

Знакомство с функцией CALCULATE Теперь, когда вы увидели в действии функцию , пришло время познакомиться с  ней ближе. Как мы уже говорили ранее, является единственной функцией в DAX, способной модифицировать контекст фильтра. Напомним, что все, что мы говорим о функции , касается также и  . На самом деле функция не изменяет существующий контекст фильтра, а создает новый, объединяя его параметры фильтра с существующим контекстом фильтра. По выходу из функции созданный ей контекст фильтра удаляется, и в действие вступает прежний контекст фильтра. Мы уже представляли вам синтаксис функции : CALCULATE ( Выражение, Условие1, ... УсловиеN )

Первым параметром в функцию передается выражение, которое будет вычислено. Но перед тем как начать вычисление, функция анализирует аргументы фильтра, переданные в  качестве остальных параметров, используя их для манипулирования контекстом фильтра. Первое, что очень важно уяснить, – это то, что переданные в функцию аргументы фильтра не являются логическими выражениями, это таблицы. Всякий раз, когда в качестве параметра в функцию поступает логическое выражение, DAX переводит его в таблицу значений. В предыдущем разделе мы использовали следующее выражение: Contoso GM := CALCULATE ( [Gross Margin]; 'Product'[Brand] = "Contoso" )

-- Рассчитываем валовую прибыль -- в контексте фильтра, где бренд = Contoso

Использование логического выражения в качестве второго параметра функции является лишь упрощением полноценной языковой конструкции, часто называемым синтаксическим сахаром. На самом деле предыдущую формулу нужно читать так: Contoso GM := CALCULATE ( [Gross Margin]; FILTER ( ALL ( 'Product'[Brand] ); 'Product'[Brand] = "Contoso"

------

Рассчитываем валовую прибыль с использованием допустимых значений Product[Brand] все значения Product[Brand], содержащие строку "Contoso"

) )

Приведенные выше выражения полностью эквивалентны, между ними нет никаких семантических или иных отличий. И все же на первых порах мы настоятельно рекомендуем вам использовать табличную форму записи для аргументов фильтра. Это сделает поведение функции более очевидным. Когда вы освоитесь с  данной функцией, более удобной для вас может стать короткая форма записи. Ее легче читать и воспринимать. нк ии

и

147

Аргумент фильтра – это таблица, то есть список значений. Таблица, переданная в функцию в качестве параметра, определяет список значений, которые будут видимы для столбца во время вычисления выражения. В нашем предыдущем примере функция возвращает таблицу из одной строки, содержащей столбец со значением «Contoso». Иными словами, «Contoso» – единственное значение, которое в функции будет видимым для столбца . Таким образом, функция отфильтрует модель данных только по товарам бренда Contoso. Посмотрите на следующие два выражения: Sales Amount := SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) Contoso Sales := CALCULATE ( [Sales Amount]; FILTER ( ALL ( 'Product'[Brand] ); 'Product'[Brand] = "Contoso" ) )

В мере второй параметр функции , вложенной в  , сканирует выражение , так что все ранее наложенные фильтры по брендам перезаписываются новым фильтром. Более очевидным такое поведение становится в  отчете с  этой мерой и  срезом по брендам. На рис. 5.4 мера показывает одно и то же значение во всех строках, и оно совпадает со столбцом для бренда Contoso.

Рис 5 4 Мера Contoso Sales ереза ис вает с ри о о и ильтра о

148

нк ии

и

еств

и

ильтр

В каждой строке отчета создается свой контекст фильтра, включающий конкретный бренд. Например, в  строке с  брендом Litware контекст фильтра, установленный в  отчете изначально, включает только это значение Litware и больше ничего. Функция оценивает свой аргумент фильтра, возвращающий таблицу с брендами, содержащую только бренд Contoso. Созданный фильтр перезаписывает существующий фильтр, который был установлен на тот же столбец. Графическое представление этого процесса можно видеть на рис. 5.5.

Рис 5 5 ильтр о ренд из нк ии CALCULATE

ереза исан ильтро

о

Функция не перезаписывает весь исходный контекст фильтра. Она заменяет на новые фильтры по столбцам, которые присутствуют и в старом контексте, и в новом. Фактически если заменить срез в отчете, вынеся в строки категории товаров вместо брендов, результат будет иным, что видно по рис. 5.6. Теперь в  отчете присутствует срез по столбцу , тогда как функция при вычислении меры применяет фильтр на столбец . Два фильтра воздействуют на разные столбцы таблицы . Таким образом, никакой перезаписи не происходит, и оба фильтра объединяются в  единый контекст фильтра. В  результате в  каждой строке новой меры показываются продажи товаров конкретной категории, входящих в бренд Contoso. Графически этот сценарий показан на рис. 5.7. нк ии

и

149

Рис 5 6 сли от ет изна ально ильтр етс то ильтр о ренда росто о единитс с ранее настроенн контексто ильтра

Рис 5 7 о разн

о категори

,

нк и CALCULATE ереза ис вает ильтр о одно стол а роис одит о единение

и то

Теперь, когда вы усвоили базовую концепцию функции подытожить ее семантические особенности:

же стол

, можно

„ функция создает копию существующего контекста фильтра; „ функция оценивает каждый аргумент фильтра и для каждого условия создает список доступных значений по указанным столбцам; „ если аргументы фильтра затрагивают один и тот же столбец, фильтры по ним объединяются при помощи оператора (или, как сказали бы математики, посредством пересечения множеств); 150

нк ии

и

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

в олн ет е е одно важное де ствие, а и енно и контекст строки в квивалентн контекст ильт о то олее одро но ри овторно ро тении CALCULATE создает контекст ильтра на основе с -

Функция принимает фильтры двух типов: „ с исок а е и в виде табличного выражения. В этом случае вы передаете конкретный список значений, который хотите сделать видимым в новом контексте фильтра. При этом в фильтре может содержаться таблица с любым количеством столбцов. И фильтром будут рассматриваться только существующие комбинации значений в разных столбцах; „ ло и еское ра е ие, как, например, Product[Color] = "White". Этот тип фильтра должен работать с одним столбцом, поскольку результатом должен быть список значений для одного столбца. Такой тип аргумента фильтра также называется редикатом (predicate). Если вы используете для фильтров логическое выражение, DAX все равно преобразует его в список значений. Таким образом, если написать: Sales Amount Red Products := CALCULATE ( [Sales Amount]; 'Product'[Color] = "Red" )

DAX трансформирует это выражение в: Sales Amount Red Products := CALCULATE ( [Sales Amount]; FILTER ( ALL ( 'Product'[Color] ); 'Product'[Color] = "Red" ) )

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

и

151

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

Использование функции CALCULATE для расчета процентов Вы уже достаточно узнали о  функции , и  теперь пришло время воспользоваться ей для проведения определенных вычислений. Целью этого раздела будет привлечь ваше внимание к некоторым особенностям функции , не заметным с первого взгляда. Позже в этой главе мы поговорим о  еще более продвинутых аспектах применения данной функции. Сейчас же сосредоточимся на проблемах, с которыми вы можете столкнуться при работе с  на первых порах. Одним из наиболее распространенных шаблонов вычислений является расчет процентов. Работая с процентами, очень важно четко определять тип вычислений, который вам необходим. В этом разделе вы увидите, как разное использование функций и  может приводить к  совершенно различным результатам. Начнем с простого расчета процентов. Нашей целью будет построить простой отчет с  суммами продаж по категориям товаров и  их долями от общего итога. Результат, который мы хотим получить, показан на рис. 5.8.

Рис 5 8 стол е Sales Pct оказана дол о отно ени к о е с е родаж

родаж о категории товаров

Чтобы рассчитать процент, необходимо поделить значение меры из текущего контекста фильтра на значение в контексте фильтра, игнорирующем существующий фильтр по категории товара. Фактически значение для первой строки (Audio) составляет 1,26 , что является результатом деления 384 518,16 на 30 591 343,98. В каждой строке отчета контекст фильтра содержит ссылку на текущую категорию. Таким образом, мера изначально будет показывать правильный результат по сумме продаж по этой категории. В знаменателе мы должны как-то проигнорировать текущий контекст фильтра, чтобы получить общую сумму продаж. Поскольку аргументами фильтра функции являются таблицы, нам достаточно передать табличную функцию, которая 152

нк ии

и

будет игнорировать текущий контекст фильтра по столбцу категории товара, а значит, всегда возвращать все категории вне зависимости от установленных фильтров. Ранее вы узнали, что такие возможности нам предоставляет функция . Посмотрите на следующее определение меры: All Category Sales := CALCULATE ( [Sales Amount]; ALL ( 'Product'[Category] ) )

-- Меняет контекст фильтра -- для суммы продаж так, -- чтобы были видны все (ALL) категории

Функция удаляет фильтр по столбцу в текущем контексте фильтра. Следовательно, в каждой ячейке таблицы будет проигнорирован фильтр, установленный на категорию, а именно тот фильтр, который был наложен в строке. Посмотрите на рис. 5.9. Вы видите, что во всех строках таблицы в  мере показывается одно и то же число, а  именно итог по мере .

Рис 5 9 нк и ALL далила ильтр о категории, так то контекст ильтра в нк ии CALCULATE не содержит ограни ени о то стол

Мера сама по себе не представляет никакого интереса. Маловероятно, что пользователю понадобится создать отчет с одинаковыми значениями в столбце по всем строкам. Но это значение прекрасно подойдет нам в качестве знаменателя при вычислении процента продаж по категории. Формула для вычисления этого процента может быть написана следующим образом: Sales Pct := VAR CurrentCategorySales = [Sales Amount] VAR AllCategoriesSales = CALCULATE (

-----

нк ии

CurrentCategorySales содержит сумму продаж в текущем контексте AllCategoriesSales содержит сумму продаж в контексте фильтра,

и

153

[Sales Amount]; ALL ( 'Product'[Category] )

-- где все категории товаров -- видимы

) VAR Ratio = DIVIDE ( CurrentCategorySales; AllCategoriesSales ) RETURN Ratio

Как видно из этого примера, сочетание табличных функций с  позволяет выполнять сложные вычисления довольно просто. В данной книге мы будем часто пользоваться этим приемом, поскольку в DAX на нем основано большинство вычислений. Примечание нк и ALL о ладает с е и и еско се антико ри ис ользовании в ка естве арг ента ильтра в нк ии CALCULATE акти ески она не за ен ет тек и контекст ильтра все и зна ени и есто того нк и CALCULATE ис ольз ет ALL дл далени ильтра о стол категории товаров из контекста ильтра такого оведени есть о ределенн е о о н е ект , котор е сли ко сложн дл того, то раз ирали и здесь Останови с на ни олее одро но далее в то главе

Как мы уже отметили во вводной части раздела, при расчете процентов, подобных этим, необходимо соблюдать большую осторожность. Здесь наши проценты будут работать правильно, только если срез в  отчете выполнен по категориям товаров. В коде удаляется фильтр по категории, но не затрагиваются другие возможные фильтры. Таким образом, если в отчет включить другие фильтры, результат может оказаться неожиданным. Взгляните на отчет, показанный на рис.  5.10, где мы также вынесли поле в  строки – на второй уровень детализации.

Рис 5 10 о авление вета товара в от ет ривело к неожиданн рез льтата на то ровне

154

нк ии

и

Похоже, на уровне категорий мы получили правильные проценты, тогда как на уровне цветов цифры не соответствуют действительности. Проценты по цветам при суммировании не бьются ни с итогами по категориям, ни с общей долей, равной 100 . Чтобы узнать, что значат и как рассчитываются конкретные значения в отчете, полезно взять для примера одну ячейку и попытаться понять, что при ее вычислении происходит с контекстом фильтра. Посмотрите на рис. 5.11.

Рис 5 11 нк и ALL с Product[Category] дал ет ильтр с категории товаров, но оставл ет его о вета

Исходный контекст фильтра, созданный в отчете, содержал фильтры по категориям и цветам. Фильтр по цветам не был перезаписан функцией  – она затронула только фильтр по категориям. В результате в итоговый контекст фильтра вошел лишь фильтр по цветам. Следовательно, в знаменателе при расчете процента будет содержаться сумма продаж по всем товарам конкретного цвета (в рассматриваемой строке – черного (Black)) и всех без исключения категорий. Нельзя сказать, что эти ошибки в расчетах стали для нас неожиданностью. В нашей формуле изначально была прописана работа с фильтром по категориям товаров, все остальные возможные фильтры она не затрагивает. Та же самая формула в другом отчете великолепно отработает. Смотрите, что будет, если нк ии

и

155

группировки по строкам поменять местами – сначала поставить цвета, а лишь затем категории товаров. Такой отчет показан на рис. 5.12.

Рис 5 12 осле того как вета и категории о ен лись еста и, и р стали ос сленн и

Теперь этот отчет имеет смысл. Формула меры не изменилась, но цифры стали интуитивно понятными из-за смены внешнего вида. Теперь цифры точно указывают проценты по категориям товаров внутри каждого цвета, а  их итог везде составляет 100 . Иными словами, при необходимости рассчитывать те или иные проценты необходимо быть очень внимательными к  определению знаменателя. Функции и   – ваши главные помощники по этой части, но детали формулы нужно корректировать в зависимости от требований. Вернемся к нашему примеру. Мы хотим, чтобы проценты правильно считались как по категориям товаров, так и по их цветам. Существуют разные способы решения этой задачи, но все они приводят к отличающимся результатам. Сейчас мы рассмотрим несколько из них. Первым и  очевидным решением возникшей проблемы может быть написание функции , которая будет удалять фильтры как с  категорий товаров, так и с цветов. Добавление еще одного аргумента фильтра в функцию позволит нам это сделать: Sales Pct := VAR CurrentCategorySales = [Sales Amount] VAR AllCategoriesAndColorSales =

156

нк ии

и

CALCULATE ( [Sales Amount]; ALL ( 'Product'[Category] ); -- Два условия ALL могут быть заменены на ALL ( 'Product'[Color] ) -- ALL ( 'Product'[Category]; 'Product'[Color]) ) VAR Ratio = DIVIDE ( CurrentCategorySales; AllCategoriesAndColorSales ) RETURN Ratio

Новая мера будет прекрасно работать в  отчетах с  категориями товаров и  цветами, но она не избавилась от недостатков своих прежних версий. Да, она показывает правильные результаты по категориям и цветам, что видно по рис. 5.13, но проблемы вернутся, если добавить в отчет еще один срез.

Рис 5 13 С ис ользование нк ии ALL о категори ро ент стали оказ вать корректн е и р

и вета товаров

Чтобы не возникало проблем с добавлением в отчет новых столбцов из таблицы , можно всю ее включить в функцию , как показано ниже: Sales Pct All Products := VAR CurrentCategorySales = [Sales Amount] VAR AllProductSales = CALCULATE ( [Sales Amount]; ALL ( 'Product' ) ) VAR Ratio = DIVIDE ( CurrentCategorySales; AllProductSales

нк ии

и

157

) RETURN Ratio

Функция , которой передана целая таблица , удаляет фильтры со всех столбцов из этой таблицы. На рис.  5.14 вы можете видеть вывод новой меры.

Рис 5 14 нк и ALL с та ли е Product в ка естве арг со все ее стол ов

ента дал ет ильтр

До сих пор вы видели, что совместное использование функций и  позволяет удалять фильтры со столбца, нескольких столбцов и целой таблицы. Истинная мощь функции заключается в  ее возможностях управлять контекстами фильтра, но даже этим ее потенциал не ограничивается. Фактически вы можете осуществлять срезы и вычислять проценты сразу по нескольким таблицам из модели данных. Например, если вы захотите сделать выборку по категориям товаров и континенту проживания покупателя, последняя созданная нами мера не даст ожидаемых результатов, что видно по рис. 5.15.

Рис 5 15 Срез о стол а из разн та ли возвра ает нас к не равильн рез льтата одс ета ро ентов

158

нк ии

и

На этот раз вы и сами понимаете источник проблемы. В знаменателе формулы мы удалили все фильтры из таблицы , но фильтр по столбцу удален не был. Таким образом, здесь будут учтены продажи по всем товарам покупателям с определенного континента. Как и в предыдущем примере, тут мы можем снова добавить в аргументы фильтра функции необходимый параметр: Sales Pct All Products and Customers := VAR CurrentCategorySales = [Sales Amount] VAR AllProductAndCustomersSales = CALCULATE ( [Sales Amount]; ALL ( 'Product' ); ALL ( Customer ) ) VAR Ratio = DIVIDE ( CurrentCategorySales; AllProductAndCustomersSales ) RETURN Ratio

Используя функцию внутри , мы смогли удалить фильтр сразу с двух таблиц. Результат, представленный на рис. 5.16, ожидаемо оказался верным.

Рис 5 16

с ользование ALL с дв

та ли а и озволило далить ильтр с о еи

C двумя таблицами в  мы попали в  такую же ситуацию, как и  с  двумя столбцами из одной таблицы. При добавлении третьей таблицы фильтры по ней вновь удаляться не будут. Одним из решений для удаления фильтров со всех таблиц, которые могут повлиять на расчеты, является включение в функцию самой таблицы фактов. В нашей модели данных таблицей фактов является . А так можно написать формулу в мере для раснк ии

и

159

чета совокупного процента вне зависимости от количества фильтров, взаимодействующих с таблицей : Pct All Sales := VAR CurrentCategorySales = [Sales Amount] VAR AllSales = CALCULATE ( [Sales Amount]; ALL ( Sales ) ) VAR Ratio = DIVIDE ( CurrentCategorySales; AllSales ) RETURN Ratio

В этой мере используются связи в  модели данных для удаления фильтров с любой таблицы, способной фильтровать таблицу . На данном этапе мы не можем объяснить все подробности того, как это работает, поскольку в этом процессе задействованы расширенные таблицы, с которыми мы познакомимся в  главе 14. Вы можете насладиться поведением новой меры, взглянув на рис. 5.17, – мы убрали из отчета суммы и вынесли календарные годы в столбцы. Заметьте, что столбец принадлежит таблице , упоминание которой не присутствует в  нашей мере. Несмотря на это, фильтр по таблице был удален в числе прочих фильтров по таблице .

Рис 5 17 нк и ALL с та ли е актов в ка естве арг дал ет также ильтр со все св занн та ли

ента

Перед тем как завершить этот длинный пример с вычислением процентов, мы покажем вам еще один способ управления контекстами фильтра. Как вы видите по рис. 5.17, все проценты, как и ожидалось, вычислены относительно общих итогов. А что, если нам понадобится подсчитать долю продаж в рамках каждого года? В  этом случае новый контекст фильтра, созданный функцией , должен быть соответствующим образом подготовлен. А  именно 160

нк ии

и

в знаменателе должны подсчитываться итоги по всем продажам без учета любых фильтров, за исключением текущего года. Этого можно добиться следующими двумя действиями: „ удалить фильтры с таблицы фактов; „ восстановить фильтр по году. Имейте в виду, что оба условия будут действовать одновременно, даже если кажется, что это два последовательных шага. Вы уже умеете удалять все фильтры с таблицы фактов. Теперь пришло время научить восстанавливать существующий фильтр. Примечание то главе стави лени контекста и ильтра озже с одс ето ро ента в ра ка види LECTED.

се е ель на ить вас азов те ника равокаже олее росто с осо ре ить зада в та ли е итогов ри о о и нк ии ALLSE-

В главе 3 вы познакомились с  функцией . Она возвращает список значений столбца в  текущем контексте фильтра. А  поскольку результатом функции является таблица, ее вполне можно использовать в качестве аргумента фильтра в функции . В этом случае функция применит фильтр к указанному столбцу, ограничив его значения списком, возвращенным функцией . Взгляните на следующий код: Pct All Sales CY := VAR CurrentCategorySales = [Sales Amount] VAR AllSalesInCurrentYear = CALCULATE ( [Sales Amount]; ALL ( Sales ); VALUES ( 'Date'[Calendar Year] ) ) VAR Ratio = DIVIDE ( CurrentCategorySales; AllSalesInCurrentYear ) RETURN Ratio

Будучи использованной в отчете, эта мера рассчитает проценты по продажам в рамках каждого отдельного года, что видно по рис. 5.18. На рис. 5.19 графически показано выполнение этой сложной формулы. Вот что происходит на этой диаграмме: „ в ячейке, содержащей значение 4,22 (продажи товаров категории Cell Phones (Мобильные телефоны) за Calendar Year (Календарный год) 2007), контекст фильтра включает в себя Cell phones и CY 2007; „ в функции присутствует два аргумента фильтра: и  : нк ии

и

161

– функция удаляет фильтр с таблицы ; – функция выполняется в исходном контексте фильтра, в котором присутствует значение CY 2007. И именно его функция и возвращает как единственное значение, видимое в текущем контексте фильтра.

Рис 5 18 нк и VALUES озвол ет асти но восстановить контекст ильтра те извле ени стол ов из ис одного контекста

Рис 5 19 ажно он ть, то нк и VALUES в олн етс в ра ка ис одного контекста ильтра

Два аргумента фильтра функции применяются к текущему контексту фильтра, в результате чего создается новый контекст фильтра, содержа162

нк ии

и

щий единственный фильтр . В знаменателе формулы вычисляется общая сумма продаж в рамках контекста фильтра, состоящего из одного года CY 2007. Крайне важно уяснить, что аргументы фильтра функции оцениваются в рамках исходного контекста фильтра, в котором эта функция вызывается. Фактически функция меняет контекст фильтра, но это происходит только после оценки аргументов фильтра. Использование функции для таблицы с  последующим вызовом функции для столбца является распространенной техникой для замены контекста фильтра на фильтр по отдельному столбцу. Примечание ред д и ри ер также ожно ло ре ить ри о о и нк ии ALLEXCEPT ри то се антика ис ользовани св зки ALL/VALUES отли аетс от ри енени нк ии ALLEXCEPT главе 10 одро но расскаже о то , е и енно отли аетс ис ользование нк ии ALLEXCEPT от оследовательности ALL/VALUES.

Вы, наверное, заметили по этим примерам, что сама по себе функция не так уж и  сложна. Ее поведение довольно просто описать. В то же время сложность кода, в котором активно используется функция , заметно возрастает. На самом деле все, что вам нужно, – это сосредоточить внимание на контекстах фильтра и понять, как именно функция их создает. Простое вычисление процентов сопряжено с большими сложностями, кроющимися в мелочах. Если не понять должным образом, как работают контексты вычисления, DAX останется для вас загадкой. Ключ ко всем тонкостям этого языка находится как раз в искусном управлении контекстами вычисления. При этом в рассмотренных нами примерах было всего по одной функции . В  действительно сложных формулах количество одновременно использующихся контекстов нередко доходит до четырех-пяти, и в них вы можете увидеть не одну функцию . Было бы неплохо, если бы вы прочитали этот раздел о расчетах процентов как минимум дважды. По опыту можем сказать, что второе прочтение всегда дается легче, и человек обращает гораздо больше внимания на важные нюансы кода. Мы решили показать вам этот сложный пример, чтобы подчеркнуть важность освоения теоретической базы при работе с  функцией . Незначительные изменения в коде способны кардинальным образом повлиять на результаты вычислений. После повторного прочтения предлагаем вам переходить к следующим разделам, где мы больше внимания уделим теории, а не практике.

Введение в функци

EEPFILTERS

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

и

163

Audio Sales := CALCULATE ( [Sales Amount]; 'Product'[Category] = "Audio" )

Как видно по рис. 5.20, новая мера по всем строкам заполнена одним и тем же значением из категории Audio.

Рис 5 20

Мера Audio Sales во все строка в водит с

родаж о категории

Функция перезаписывает существующие фильтры по столбцам, на которые накладываются новые фильтры. Все оставшиеся столбцы контекста фильтра остаются неизменными. Если же вы не хотите, чтобы существующие фильтры перезаписывались, можете обернуть аргумент фильтра в  функцию . Например, если вы хотите показывать сумму продаж по категории Audio в случае ее присутствия в контексте фильтра, а в противном случае выводить пустое значение, вы можете написать следующую формулу: Audio Sales KeepFilters := CALCULATE ( [Sales Amount]; KEEPFILTERS ( 'Product'[Category] = "Audio" ) )

Функция

представляет собой второй модификатор функции , первым был . Позже в этой главе мы еще поговорим про модификаторы функции . меняет подход функции к  применению фильтров в  новом контексте фильтра. В  этом случае, вместо того чтобы перезаписывать существующий фильтр по одному и тому же столбцу, функция просто добавляет новый фильтр к  предыдущему. В  результате значение окажется видимым только в тех ячейках, где отфильтрованная категория была включена в исходный контекст фильтра. Вы можете видеть это на рис. 5.21. Функция делает ровно то, что и должна, исходя из названия. Она сохраняет существующий фильтр и добавляет к контексту фильтра новый. На рис. 5.22 графически показана работа этого модификатора. 164

нк ии

и

Рис 5 21 ере Audio Sales KeepFilters родажи о категории оказан только в соответств е строке и в итога

Рис 5 22 онтекст ильтра, созданн вкл ает одновре енно категории

осредство и

нк ии KEEPFILTERS,

Поскольку функция предотвращает перезапись, новый фильтр, создаваемый посредством аргумента фильтра функции , попросту добавляется к  существующему контексту. Если проследить за поведением меры в строке с категорией Cell Phones, можно заметить, что результирующий контекст фильтра будет включать в себя одновременно фильтры по категориям Cell Phones и Audio. Пересечение двух противоречащих друг другу условий приведет к образованию пустого набора данных, что повлечет за собой вывод пустого значения в ячейке. нк ии

и

165

Поведение функции становится более очевидным, когда в срезе по столбцу выбрано сразу несколько элементов. Давайте рассмотрим следующие меры, фильтрующие категории одновременно по Audio и Computers: одна с использованием модификатора , другая – без: Always Audio-Computers := CALCULATE ( [Sales Amount]; 'Product'[Category] IN { "Audio"; "Computers" } ) KeepFilters Audio-Computers := CALCULATE ( [Sales Amount]; KEEPFILTERS ( 'Product'[Category] IN { "Audio"; "Computers" } ) )

На рис. 5.23 видно, что версия меры с  рассчитывает значения только для категорий Audio и Computers, оставляя остальные строки в столбце пустыми. При этом в итоговой строке просуммированы продажи по категориям Audio и Computers.

Рис 5 23 контекст

Моди икатор KEEPFILTERS озвол ет о ильтра

единить стар

и нов

При этом функция может использоваться как с предикатом, так и с таблицей. По сути, предыдущую меру можно переписать в более развернутом виде: KeepFilters Audio-Computers := CALCULATE ( [Sales Amount]; KEEPFILTERS ( FILTER ( ALL ( 'Product'[Category] ); 'Product'[Category] IN { "Audio"; "Computers" } ) ) )

166

нк ии

и

Этот пример мы показали исключительно в образовательных целях. Для аргумента фильтра следует использовать простой синтаксис с предикатами. Накладывая фильтр на один столбец, можно не указывать функцию явным образом. Позже мы рассмотрим более сложные примеры, в которых указание функции будет обязательным. В таких случаях модификатор может обрамлять функцию , как вы увидите в следующих разделах.

Фильтрация по одному столбцу В предыдущем разделе мы рассмотрели аргументы фильтра, ссылающиеся на один столбец в функции . Но важно отметить, что в одном выражении у вас может быть сразу несколько ссылок на один и тот же столбец. Допустим, следующий пример синтаксиса с  двойной ссылкой на один столбец таблицы ( ) вполне употребим: Sales 10-100 := CALCULATE ( [Sales Amount]; Sales[Net Price] >= 10 && Sales[Net Price] = 10 && Sales[Net Price] = 10; Sales[Net Price] = 1000 )

Такая формула не сработает, поскольку аргумент фильтра функции ссылается сразу на два столбца в выражении. Следовательно, DAX не сможет автоматически преобразовать такой фильтр в корректное выражение с использованием функции . Лучшим способом здесь является использование таблицы, в которой будут присутствовать все комбинации значений столбцов, имеющихся в предикате: Sales Large Amount := CALCULATE ( [Sales Amount]; FILTER ( ALL ( Sales[Quantity]; Sales[Net Price] ); Sales[Quantity] * Sales[Net Price] >= 1000 ) )

В результате будет создан контекст с  фильтром по двум столбцам и  количеством строк, соответствующим числу уникальных комбинаций столбцов и  , удовлетворяющих условиям фильтра. Такой контекст фильтра показан на рис. 5.24.

Quantity

Net Price

1

1000.00

1

1001.00

1

1199.00





2

500.00

2

500.05





3

333.34



Рис 5 24 ильтр о нескольки стол а вкл ает в се все со етани оле Quantity и Net Price, роизведение котор дет не ень е 1000

нк ии

и

169

Результат применения такого фильтра показан на рис. 5.25.

Рис 5 25 Мера Sales Large Amount оказ вает только транзак ии с с оль е или равно 1000

о ,

Стоит отметить, что срез, показанный на рис. 5.25, не ограничивает значения в  отчете. Представленные два значения в  срезе отражают минимальное и максимальное значения столбца в таблице – не более. На следующем шаге мы покажем, как наша мера взаимодействует с установленным пользователем фильтром. При написании мер, подобных , необходимо иметь в виду то, что существующие фильтры по столбцам и  будут перезаписаны. В  самом деле, поскольку в  аргументе фильтра используется функция по двум столбцам, все ранее установленные фильтры по ним – а  в  нашем случае это значения в  срезе – будут проигнорированы. Вывод отчета на рис. 5.26 абсолютно такой же, как на рис. 5.25, несмотря на то что мы ограничили в срезе значениями 500 и 3000. Результат может вас удивить.

Рис 5 26 о категории не ло родаж в казанно но в ере Sales Large Amount все равно есть зна ение

еново диа азоне,

Вас может удивить тот факт, что мера заполнена значениями по категориям Audio и Music, Movies and Audio Books. По товарам из этих групп действительно не было продаж в диапазоне цен, установленном в контексте фильтра при помощи среза. А значения в этих строках присутствуют. Причина в  том, что контекст фильтра, созданный посредством среза, был попросту проигнорирован мерой , которая перезаписала фильтры по столбцам и  . Если внимательно изучить два пред170

нк ии

и

ставленных отчета, можно заметить, что значения в столбце в них полностью идентичны, как если бы никакого среза в отчете и не было. Давайте посмотрим, как было вычислено значение нашей меры для строки Audio: Sales Large Amount := CALCULATE ( CALCULATE ( [Sales Amount]; FILTER ( ALL ( Sales[Quantity]; Sales[Net Price] ); Sales[Quantity] * Sales[Net Price] >= 1000 ) ); 'Product'[Category] = "Audio"; Sales[Net Price] >= 500 )

Из этого фрагмента кода следует, что вызов во внутренней функции полностью игнорирует фильтр по , установленный во внешней . Здесь мы можем использовать модификатор , чтобы избежать перезаписи фильтров: Sales Large Amount KeepFilter := CALCULATE ( [Sales Amount]; KEEPFILTERS ( FILTER ( ALL ( Sales[Quantity]; Sales[Net Price] ); Sales[Quantity] * Sales[Net Price] >= 1000 ) ) )

Вывод новой меры

показан на рис. 5.27.

Рис 5 27 с ользование оди икатора KEEPFILTERS озволило вкл ить в рас ет вне ние срез в от ете

нк ии

и

171

Еще одним способом использовать в мере сложные фильтры является включение в  формулу фильтра по таблице, а  не по столбцу. Такую технику в  основном предпочитают новички в мире DAX, при этом она таит немало опасностей. С использованием табличного фильтра переписать предыдущую меру можно так: Sales Large Amount Table := CALCULATE ( [Sales Amount]; FILTER ( Sales; Sales[Quantity] * Sales[Net Price] >= 1000 ) )

Как вы помните, все аргументы фильтра в  функции оцениваются в рамках контекста фильтра, в котором эта функция была вызвана. Таким образом, итерации по таблице будут производиться только по строкам, удовлетворяющим условиям внешнего контекста фильтра, включающего фильтр по . Таким образом, семантика новой меры полностью согласуется с предыдущей мерой . И хотя такой подход выглядит логичным и несложным, применять его следует с особой осторожностью, поскольку он может привести к проблемам с производительностью отчета и корректностью результатов. В главе 14 мы подробнее разберем детали возможных проблем. Пока же достаточно будет запомнить, что лучше всего стараться использовать фильтры с минимально возможным количеством столбцов. Кроме того, следует избегать использования табличных фильтров, которые обычно негативно сказываются на производительности меры. Таблица может быть довольно большой, и ее сканирование с целью оценки предикатов может занимать немало времени. С другой стороны, в мере количество итераций равно числу уникальных сочетаний значений в столбцах и  . А это число обычно намного меньше количества строк в таблице .

Порядок вычислений в функции CALCULATE Обычно в выражениях DAX первыми вычисляются вложенные операции. Посмотрите на следующую формулу: Sales Amount Large := SUMX ( FILTER ( Sales; Sales[Quantity] >= 100 ); Sales[Quantity] * Sales[Net Price] )

Перед тем как вызывать функцию , DAX оценит результат выполнения табличной функции . По сути, функция осуществляет итерации по таблице. А поскольку ее аргументом является таблица, полученная в результате запуска функции , она не может приступить к работе, пока не завершит172

нк ии

и

ся выполнение функции . Это правило распространяется в DAX на все функции, за исключением и  . Особенность этих функций состоит в том, что они сначала оценивают свои аргументы фильтра и лишь затем вычисляют выражение из первого параметра, которое и обусловливает итоговый результат. Осложняет ситуацию тот факт, что функция сама меняет контекст фильтра. Все аргументы фильтра оцениваются в рамках контекста фильтра, в  котором вызвана функция , при этом каждый фильтр обрабатывается независимо от остальных. Порядок следования фильтров внутри функции не имеет значения. Таким образом, все меры, указанные ниже, будут полностью эквивалентны: Sales Red Contoso := CALCULATE ( [Sales Amount]; 'Product'[Color] = "Red"; KEEPFILTERS ( 'Product'[Brand] = "Contoso" ) ) Sales Red Contoso := CALCULATE ( [Sales Amount]; KEEPFILTERS ( 'Product'[Brand] = "Contoso" ); 'Product'[Color] = "Red" ) Sales Red Contoso := VAR ColorRed = FILTER ( ALL ( 'Product'[Color] ); 'Product'[Color] = "Red" ) VAR BrandContoso = FILTER ( ALL ( 'Product'[Brand] ); 'Product'[Brand] = "Contoso" ) VAR SalesRedContoso = CALCULATE ( [Sales Amount]; ColorRed; KEEPFILTERS ( BrandContoso ) ) RETURN SalesRedContoso

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

и

173

Это правило оказывается полезным при использовании вложенных функций . В таком случае внешние фильтры будут оценены первыми, а внутренние – последними. Понимание работы вложенных функций очень важно, ведь вы сталкиваетесь с этим каждый раз, когда вкладываете меры друг в друга. Взгляните на следующий пример, где мера вызывает меру : Sales Red := CALCULATE ( [Sales Amount]; 'Product'[Color] = "Red" ) Green calling Red := CALCULATE ( [Sales Red]; 'Product'[Color] = "Green" )

Чтобы сделать вызов меры из другой меры более очевидным, напишем полный код с вложенными функциями : Green calling Red Exp := CALCULATE ( CALCULATE ( [Sales Amount]; 'Product'[Color] = "Red" ); 'Product'[Color] = "Green" )

Порядок вычислений тут будет следующим. . Сначала в  рамках внешней функции применяется фильтр Product[Color] = "Green". 2. Затем во внутренней функции применяется фильтр Product[Color] = "Red". Этот фильтр перезаписывает предыдущий. 3. В последнюю очередь DAX вычисляет значение с  действующим фильтром Product[Color] = "Red". Таким образом, результаты вычисления мер и  будут одинаковыми и  будут отражать продажи красных товаров, что видно по рис. 5.28. Примечание М ривели такое о исание оследовательности искл ительно в о разовательн ел де ствительности движок ри ен ет отложенн о енк контекстов ильтра аки о разо , в редставленно в е коде о енка вне него ильтра ожет вовсе не роизо ти о ри ине ненадо ности то сделано искл ительно дл о ти иза ии в олнени за росов и никои о разо не вли ет на се антик нк ии CALCULATE.

174

нк ии

и

Рис 5 28 оследние три ер верн ли одинаков е рез льтат , отража ие родажи о красн товара

Рассмотрим последовательность выполнения операций и оценку фильтров на другом примере: Sales YB := CALCULATE ( CALCULATE ( [Sales Amount]; 'Product'[Color] IN { "Yellow"; "Black" } ); 'Product'[Color] IN { "Black"; "Blue" } )

Изменение контекста фильтра в  мере рис. 5.29.

Рис 5 29

н тренни

показано графически на

ильтр ереза ис вает вне ни

нк ии

и

175

Как вы уже видели ранее, внутренний фильтр по столбцу перезаписывает внешние. Таким образом, мера выдаст результаты по товарам желтого ( ) и черного ( ) цветов. Использование модификатора во внутренней функции позволит сохранить внешний фильтр: Sales YB KeepFilters := CALCULATE ( CALCULATE ( [Sales Amount]; KEEPFILTERS ( 'Product'[Color] IN { "Yellow"; "Black" } ) ); 'Product'[Color] IN { "Black"; "Blue" } )

Изменение контекста фильтра в  мере рис. 5.30.

показано на

Рис 5 30 с ользование оди икатора KEEPFILTERS озвол ет нк ии CALCULATE не ереза ис вать с еств и контекст ильтра

Поскольку оба фильтра сохранились, их наборы, по сути, пересеклись. Таким образом, в итоговом контексте фильтра единственным видимым цветом товаров останется черный ( ), поскольку только он присутствует в обоих фильтрах. При этом порядок следования аргументов фильтра внутри одной и той же функции не имеет значения – все они применяются к  контексту фильтра независимо друг от друга. 176

нк ии

и

Преобразование контекста В главе 4 мы несколько раз повторили, что контекст строки и контекст фильтра представляют собой совершенно разные концепции. И  это правда. Как правда и то, что функция обладает уникальной способностью трансформировать контекст строки в контекст фильтра. Эта операция получила название ре о ра овани контекста (context transition) и описывается следующим образом: унк и отмен ет действие л ого контекста строки на ав томати ески рео ра ует все стол и теку его контекста строки в ар гумент фил тра ис ол у и факти еские на ени в строке о которой осу ествл етс итера и Концепцию преобразования контекста новичкам в  DAX будет усвоить нелегко. Даже специалисты с опытом могут испытывать проблемы, когда сталкиваются с тонкими нюансами этой концепции. Мы абсолютно уверены, что данного ранее определения преобразования контекста будет совершенно недостаточно для всестороннего понимания этой возможности языка. В этой главе мы попытаемся объяснить вам, что из себя представляет преобразование контекста, на примерах, постепенно двигаясь от простого к сложному. Но перед этим необходимо убедиться, что вы досконально понимаете, что такое контекст строки и контекст фильтра.

Повторение темы контекста строки и контекста фильтра Давайте повторим все важные факты, касающиеся контекста строки и контекста фильтра, при помощи рис.  5.31, на котором показан отчет по продажам с  вынесенными на строки брендами и  вспомогательная диаграмма, описывающая схему работы контекстов. Таблицы и  на диаграмме не отражают реальные данные. В них показано несколько строк для облегчения понимания общей картины.

Рис 5 31 а диагра е с е ати но оказано в ри о о и нк ии SUMX

нк ии

олнение росто итера ии

и

177

Приведенные ниже комментарии помогут вам проверить свое понимание полного процесса вычисления меры по строке с брендом Contoso: „ в отчете создан контекст фильтра, включающий в  себя фильтр ; „ действие фильтра распространяется на всю модель данных, включая таблицы и  ; „ контекст фильтра ограничивает набор строк в  итерациях для функции по таблице . В результате функция SUMX проходит только по строкам из таблицы , относящимся к товарам бренда Contoso; „ в таблице на представленном рисунке содержится две строки по товару A, принадлежащему бренду Contoso; „ функция проходит по этим двум строкам. Итогом первой итерации будет результат 1*11,00, составляющий 11,00, а по второй – 2*10,99, что дает 21,98; „ функция возвращает сумму полученных на предыдущем шаге результатов; „ во время осуществления итераций по таблице функция проходит только по видимой части таблицы, создавая контекст строки для каждой видимой строки; „ в первой строке таблицы значение столбца равно 1, а    – 11. В  следующей строке значения будут уже другие. В каждом столбце есть текущее значение, зависящее от строки, по которой в данный момент осуществляется итерация. Для каждой отдельной строки значения в столбцах могут отличаться; „ во время итерации одновременно существуют контекст строки и контекст фильтра. При этом контекст фильтра остается неизменным (с фильтром по бренду Contoso), поскольку ни одна функция его не меняла. В свете предстоящего разговора о  преобразовании контекстов последний пункт приобретает важное значение. Во время осуществления итераций по таблице контекст фильтра действительно остается неизменным и  содержит фильтр по бренду Contoso. Контекст строки в  это время занят, собственно, выполнением итераций по таблице . Каждый столбец таблицы содержит свое значение, и контекст строки предоставляет очередное значение, считывая его из текущей строки. Помните о том, что контекст строки занят осуществлением итераций по таблице, контекст фильтра этим не занимается. Это очень важное замечание. Мы настоятельно рекомендуем вам дважды убедиться в  том, что вы досконально поймете следующий сценарий. Представьте, что вы создали меру для подсчета количества строк в таблице со следующей формулой: NumOfSales := COUNTROWS ( Sales )

В отчете данная мера будет показывать количество транзакций в  таблице в  рамках текущего контекста фильтра. В  результате, показанном на рис.  5.32, нет ничего неожиданного: для каждого бренда свое количество транзакций. 178

нк ии

и

Рис 5 32 Мера одс ит вает коли ество строк, види в тек е контексте ильтра в та ли е Sales

В таблице присутствует 37 984 записи для бренда Contoso, а это значит, что именно столько итераций будет сделано по таблице. Мера , которую мы обсуждали выше, справится со своей работой за 37 984 операции умножения. Сможете ли вы, вооружившись полученными знаниями, ответить на вопрос о том, какой результат покажет следующая мера в строке с брендом Contoso? Sum Num Of Sales := SUMX ( Sales; COUNTROWS ( Sales ) )

Не спешите с  ответом. Подумайте хорошенько и дайте осмысленную версию. В следующем абзаце мы дадим правильный ответ. Контекст фильтра включает в себя бренд Contoso. По предыдущим примерам мы знаем, что функция должна сделать ровно 37 984 итерации. И для каждой из этих 37 984 строк функция подсчитает количество видимых строк из таблицы в текущем контексте фильтра. При этом контекст фильтра все это время будет оставаться неизменным, так что для каждой из строк функция будет выдавать ответ 37 984. Следовательно, функция просуммирует значение 37 984 ровно 37 984 раза. И результатом меры будет 37 984 в квадрате. Вы можете убедиться в этом, взглянув на рис. 5.33, на котором показан вывод этого отчета. Теперь, когда вы освежили память относительно контекста строк и контекста фильтра, можно приступать к изучению концепции преобразования контекста.

Введение в преобразование контекста Контекст строки создается всякий раз, когда по таблице начинают осуществляться итерации. Внутри одной итерации значения столбцов зависят от контекста строки. Для демонстрации этого процесса подойдет хорошо знакомая вам мера: Sales Amount := SUMX (

нк ии

и

179

Sales; Sales[Quantity] * Sales[Unit Price] )

Рис 5 33 ере Sum Num Of Sales в водитс зна ение NumOfSales в квадрате, оскольк дл каждо строки роизводитс одс ет строк

На каждой итерации в столбцах и  содержится свое значение, зависящее от текущего контекста строки. В предыдущем разделе мы показывали, что если выражение внутри итерации не привязано к контексту строки, оно будет вычисляться в  контексте фильтра. А  значит, результаты могут быть неожиданными, по крайней мере для новичков в DAX. Несмотря на это, вы вольны использовать любые функции внутри контекста строки. Но среди прочих сильно выделяется функция . Вызванная в  рамках контекста строки, она отменяет его действие еще до вычисления своего выражения. Внутри выражения, вычисляемого функцией , все предыдущие контексты строки утрачивают свое действие. Таким образом, следующее выражение для меры будет недопустимым, выдаст синтаксическую ошибку: Sales Amount := SUMX ( Sales; CALCULATE ( Sales[Quantity] ) )

-- Нет контекста строки внутри CALCULATE, ОШИБКА !

Причина в  том, что значение столбца не может быть получено внутри функции , поскольку она отменяет действие контекста строки, в котором была вызвана. Но это только часть того, что происходит во время операции преобразования контекста. Вторая, и главная, часть заключается в том, что функция переносит все столбцы из текущего контекста строки вместе с  их значениями в  аргументы фильтра. Посмотрите на следующий код: 180

нк ии

и

Sales Amount := SUMX ( Sales; CALCULATE ( SUM ( Sales[Quantity] ) ) -- функция SUM не требует наличия контекста -- строки )

У этой функции нет аргументов фильтра. Единственным ее аргументом является само выражение, которое требуется вычислить. Так что эта функция вряд ли перезапишет существующий контекст фильтра. Вместо этого она молча создаст множество аргументов фильтра. Фактически для каждого столбца в таблице, по которой осуществляются итерации, будет создан свой фильтр. Вы можете посмотреть на рис. 5.34, чтобы получить первое представление о том, как работает преобразование контекста. Мы уменьшили количество столбцов для простоты восприятия.

Рис 5 34 зов нк ии CALCULATE в контексте строки ведет к создани контекста ильтра с о разование ильтра дл каждого стол а та ли

После начала итераций функция попадает в первую строку таблицы и  пытается вычислить выражение SUM ( Sales[Quantity] ). При этом она создает по одному аргументу фильтра для каждого столбца в таблице, по которой осуществляются итерации. В нашем примере таких столбцов три: , и  . Созданный в результате преобразования из контекста строки контекст фильтра содержит текущие значения (A, 1, 11.00) для каждого столбца ( , и  ). Конечно, подобная операция выполняется для каждой из трех строк во время итераций функции . По сути, результат предыдущего выражения эквивалентен следующему: CALCULATE ( SUM ( Sales[Quantity] );

нк ии

и

181

Sales[Product] = "A"; Sales[Quantity] = 1; Sales[Net Price] = 11 ) + CALCULATE ( SUM ( Sales[Quantity] ); Sales[Product] = "B"; Sales[Quantity] = 2; Sales[Net Price] = 25 ) + CALCULATE ( SUM ( Sales[Quantity] ); Sales[Product] = "A"; Sales[Quantity] = 2; Sales[Net Price] = 10,99 )

Эти аргументы фильтра скрыты. Они добавляются движком DAX автоматически, и повлиять на этот процесс никак нельзя. Поначалу концепция преобразования контекста кажется очень странной. Но к ней нужно привыкнуть, чтобы понять всю ее прелесть. Освоить преобразование контекста бывает не так просто, но это действительно очень мощная концепция. Давайте подытожим рассуждения, приведенные выше, после чего поговорим о некоторых аспектах более подробно: „

реобра о а ие ко текста оро осто а о ера и . Если использовать преобразование контекста применительно к  таблице из десяти столбцов и  миллиона строк, функции придется применять десять фильтров миллион раз. В любом случае это будет довольно долго. Это не значит, что не стоит использовать преобразование контекста вовсе. Но применять функцию следует довольно осторожно; „ ито о реобра о а и ко текста е об атель о бу ет о а строка. Исходный контекст строки, в  котором вызывается функция , всегда указывает ровно на одну строку. Контекст строки идет последовательно – от строки к строке. Но когда контекст строки переходит в  контекст фильтра посредством преобразования контекста, вновь образованный контекст будет фильтровать все строки, удовлетворяющие выбранным значениям. Таким образом, неправильно говорить, что преобразование контекста ведет к созданию контекста фильтра с одной строкой. Это очень важный момент, и мы вернемся к нему в следующих разделах; „ реобра о а ие ко текста а е ст ует столб , е рисутст у ие ор уле. Несмотря на то что столбцы, используемые в фильтре, скрыты, они являются частью выражения. Это делает любую формулу, в  которой есть функция , намного сложнее, чем кажется на первый взгляд. При использовании преобразования контекста все столбцы таблицы становятся скрытыми аргументами фильтра. Такое поведение может приводить к образованию неожиданных зависимостей, и мы поговорим об этом далее в этом разделе;

182

нк ии

и

„

реобра о а ие ко текста со ает ко текст ильтра а ос о а ии ко текста строки. Вы должны помнить нашу любимую мантру: «Контекст фильтра фильтрует, контекст строки осуществляет итерации по таблице». Преобразуя контекст строки в контекст фильтра, мы, по сути, меняем саму природу фильтра. Вместо прохода по одной строке DAX осуществляет фильтрацию всей модели данных, используя при этом связи между таблицами. Иными словами, преобразование контекста, примененное к  одной таблице, способно распространить фильтрацию далеко за пределы этой отдельной таблицы, в  которой был изначально создан контекст строки; „ реобра о а ие ко текста роис о ит се а, ко а есть акти ко текст строки. Всякий раз, когда вы будете использовать функцию в вычисляемом столбце, будет происходить преобразование контекста. При создании вычисляемого столбца контекст строки появляется автоматически, и этого достаточно для того, чтобы произошло преобразование контекста; „ реобра о а ие ко текста атра и ает се ко текст строки. Когда мы имеем дело с вложенными итерациями по разным таблицам, преобразование контекста будет учитывать все активные контексты строки. Таким образом, эта операция отменит действие всех из них и создаст аргументы фильтра для всех без исключения столбцов, которые участвуют в итерациях во всех активных контекстах строки; „ реобра о а ие ко текста от е ет е ст ие ко тексто строки. Несмотря на то что мы повторили это уже не один раз, важно обратить на этот аспект особое внимание. Ни один из внешних контекстов строк не будет действовать внутри выражения, вычисляемого при помощи функции . Все внешние контексты строки будут трансформированы в соответствующие поля контекста фильтра. Как мы уже говорили ранее, многие из этих аспектов нуждаются в дополнительном объяснении. В оставшейся части данного раздела мы углубимся в некоторые очень важные моменты. И  хотя мы написали об этих аспектах как о предостережениях, на самом деле это просто особенности концепции. Если игнорировать их, результаты вычислений могут оказаться непредсказуемыми. Но когда вы освоите этот прием, то сможете использовать его по своему усмотрению. Единственным отличием между странным поведением и полезной возможностью – по крайней мере, в DAX – является глубина знаний в этой области.

Преобразование контекста в вычисляемых столбцах Значение в вычисляемом столбце рассчитывается в рамках контекста строки. Следовательно, использование функции в  вычисляемом столбце автоматически приведет к преобразованию контекста. Давайте применим эту особенность в таблице , для того чтобы особым образом пометить товары, продажа которых приносит компании более 1 от всех продаж. Чтобы произвести требуемое вычисление, нам нужны два значения: сумма продаж по конкретному товару и общая сумма продаж. Для вычисления первонк ии

и

183

го из них необходимо отфильтровать таблицу так, чтобы в расчет брался только этот товар, тогда как при расчете общей суммы продаж нужно снять все фильтры по товарам. Посмотрите на представленный ниже код: 'Product'[Performance] = VAR TotalSales = SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) VAR CurrentSales = CALCULATE ( SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) ) VAR Ratio = 0.01 VAR Result = IF ( CurrentSales >= TotalSales * Ratio; "High Performance product"; "Regular product" ) RETURN Result

-- Общая сумма продаж -- Sales не отфильтрована, -- так что считаются все продажи

-- Происходит преобразование контекста -- Продажи только по одному товару -- Здесь мы вычисляем продажи -- по конкретному товару -- 1 %, выраженный как число

-- Очень популярный товар -- Обычный товар

Вы, наверное, заметили, что между двумя переменными есть лишь одно небольшое различие: переменная рассчитывается путем осуществления обычных итераций, а в  тот же самый код DAX заключен в функцию . Поскольку мы имеем дело с вычисляемым столбцом, при встрече с  функцией происходит преобразование контекста строки в  контекст фильтра. При этом контекст фильтра распространяется на всю модель данных, достигая таблицы и фильтруя ее по одному выбранному товару. Таким образом, несмотря на внешнее сходство, эти переменные выполняют совершенно разные функции. В  подсчитывается общая сумма продаж по всем товарам, поскольку контекст фильтра в рамках вычисляемого столбца всегда пустой и  не фильтрует товары. В то же время отражает сумму продаж по конкретному товару благодаря преобразованию контекста, выполненному в функции . Оставшаяся часть кода вопросов вызывать не должна – здесь просто выполняется проверка на соответствие определенному условию и присвоение товару соответствующего статуса. Созданный вычисляемый столбец можно использовать в отчете, как показано на рис. 5.35. В коде вычисляемого столбца мы использовали функцию и инициированное ей преобразование контекста. Перед тем как двигаться дальше, давайте посмотрим, все ли нюансы мы учли. В таблице достаточно мало строк – всего несколько тысяч. Так что производительность вычисляемого столбца просесть не должна. Контекст фильтра, созданный функцией , включает в  себя все столбцы. Есть ли у  нас гарантия, 184

нк ии

и

что в переменную попадут продажи только по выбранному товару? В нашем конкретном случае да, поскольку у нас все товары уникальные – это обеспечивается тем, что в таблице содержится столбец с уникальными значениями. Следовательно, образованный путем преобразования из контекста строки контекст фильтра будет гарантированно содержать одну строку.

Рис 5 35

и ь ет ре товара ол или стат с

О ень о л рн

В этом случае мы полностью можем полагаться на преобразование контекста, ведь каждая строка в таблице, по которой осуществляются итерации, уникальная в своем роде. Но так будет не всегда. И сейчас мы продемонстрируем ситуацию, не подходящую для преобразования контекста. Создадим следующий вычисляемый столбец в таблице : Sales[Wrong Amt] = CALCULATE ( SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) )

Будучи вычисляемым столбцом, при создании способствует образованию контекста строки. Функция преобразует контекст строки в контекст фильтра, и функция проходит по всем строкам в таблице с набором значений в столбцах, соответствующим текущей строке из таблицы . Проблема в том, что в таблице продаж нет столбца с уникальными значениями. Таким образом, вполне вероятно, что мы обнаружим сразу несколько строк с идентичными значениями столбцов, которые будут отфильтрованы вместе. Иными словами, у нас нет никакой гарантии, что функция пройдет только по одной строке в столбце . Если вам повезет, в вашей таблице окажется много дубликатов строк, и итоговое значение, рассчитанное в рамках вычисляемого столбца, окажется оченк ии

и

185

видно неправильным. В этом случае проблема может быть быстро обнаружена и  локализована. Но в  большинстве случаев дублей будет довольно мало, что сильно затруднит поиск ошибочного значения. Пример базы данных, который бы используем в нашей книге, не является исключением. Посмотрите на отчет, показанный на рис. 5.36, где в столбце выведено правильное значение, а в столбце  – ошибочное.

Рис 5 36

Боль инство зна ени в стол

а сов ада т

Как видите, значения в  столбцах отличаются только по бренду Fabrikam и в итоговой строке. Дело в том, что в таблице есть несколько дублей по товарам бренда Fabrikam, и  именно это привело к  двойным подсчетам. Но важно то, что присутствие таких строк в столбце может быть вполне обоснованным: один и тот же покупатель мог приобрести один товар в том же самом магазине и в тот же день – утром и вечером, тогда как в таблице хранится только дата без указания времени. А поскольку таких случаев будет очень мало, в основной своей массе результаты будут выглядеть корректно. В то же время ошибка будет, поскольку в своих расчетах мы опирались на данные из таблицы с дубликатами. И  чем больше будет дубликатов, тем сильнее будет расхождение. В этом случае полагаться на преобразование контекста будет ошибкой. Когда нет гарантии, что все записи в таблице будут уникальными, прием с преобразованием контекста может оказаться небезопасным. И эксперт по языку DAX должен уметь предвидеть такие ситуации. Кроме того, в таблице может быть много записей – до нескольких миллионов. Так что наш вычисляемый столбец не только рискует выдавать неправильные результаты, но и рассчитываться он может очень долго.

Преобразование контекста в мерах Хорошее понимание концепции преобразования контекста очень важно и по причине следующей интересной особенности языка DAX: 186

нк ии

и

Каждая ссылка на меру неявным образом обрамляется в функцию CALCULATE. Это приводит к тому, что обращение к мере в присутствии любого контекста строки автоматически ведет к преобразованию контекста. Именно поэтому в DAX так важно соблюдать единообразные принципы именования при обращении к столбцам (с обязательным указанием названия таблицы) и мерам (без указания таблицы). Таким образом, всегда важно помнить о возможности возникновения неявного преобразования контекста при чтении и написании кода на DAX. Приведенное нами короткое определение в начале этого раздела нуждается в подробном пояснении с примерами. Первое, что стоит отметить, – любая мера при обращении к  ней автоматически заключается в  функцию . Посмотрите на следующий код, где мы создаем меру и вычисляемый столбец в таблице : Sales Amount := SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) 'Product'[Product Sales] = [Sales Amount]

Вычисляемый столбец , как мы и ожидали, рассчитывает меру только для текущего товара в таблице . На самом деле код вычисляемого столбца при обращении к  мере неявным образом оборачивает ее в функцию , что приводит к такой формуле: 'Product'[Product Sales] = CALCULATE SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) )

Без функции вычисляемый столбец оказался бы заполненным одинаковыми значениями, суммирующими продажи по всем товарам. Присутствие функции запускает операцию преобразования контекста, что и позволяет добиться правильного результата. Таким образом, ссылка на меру всегда вызывает функцию . Это очень важно помнить для написания коротких и мощных выражений на DAX. В то же время в этой области потенциально могут возникать ошибки, если забыть, что при обращении к мере в рамках действующего контекста строки происходит преобразование контекста. Как правило, вы всегда можете заменить меру на соответствующее выражение, заключенное в функцию . Давайте рассмотрим следующее определение меры , вычисляющей максимальное значение по мере в рамках дня: нк ии

и

187

Max Daily Sales := MAXX ( 'Date'; [Sales Amount] )

Эта формула интуитивно понятна. Но мера должна рассчитываться по каждой дате, а значит, таблицу продаж в ней нужно отфильтровать по конкретной дате. Именно это нам помогает сделать преобразование контекста. Внутренне DAX заменяет меру на ее выражение, заключенное в функцию , как показано ниже: Max Daily Sales := MAXX ( 'Date'; CALCULATE ( SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) ) )

В главе 7 мы будем использовать эту особенность языка при написании сложных формул на DAX для решения специфических сценариев. Сейчас же вам достаточно знать, что такое преобразование контекста, и  понимать, что оно возникает в следующих случаях: „ когда функции или вызываются в присутствии любого контекста строки; „ когда идет обращение к мере в рамках контекста строки, поскольку DAX автоматически обрамляет вызов меры в функцию . Существует и другая опасность для потенциального возникновения ошибки, связанная с предположением о том, что любую меру в коде можно заменить на ее определение. Это не так. Это допустимо лишь в том случае, если вы делаете это не внутри контекста строки, например в другой мере, но в  рамках контекста строки этого делать нельзя. Это правило легко забыть, поэтому мы приведем пример, в  котором произведем заведомо неправильные расчеты, поддавшись ошибочным суждениям. Вы, наверное, заметили, что в предыдущем нашем примере для вычисляемого столбца мы дважды повторили одинаковый фрагмент кода с итерациями по таблице . Повторим эту формулу: 'Product'[Performance] = VAR TotalSales = SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) VAR CurrentSales = CALCULATE (

188

нк ии

и

-- Общая сумма продаж -- Sales не отфильтрована, -- так что считаются все продажи

-- Происходит преобразование контекста

SUMX ( Sales; -- Продажи только по одному товару Sales[Quantity] * Sales[Net Price] -- Здесь мы вычисляем продажи ) -- по конкретному товару ) VAR Ratio = 0.01 VAR Result = IF ( CurrentSales >= TotalSales * Ratio; "High Performance product"; "Regular product" ) RETURN Result

-- 1 %, выраженный как число

-- Очень популярный товар -- Обычный товар

Код с итерациями внутри функции действительно в точности повторяется в обеих переменных. Разница состоит только в том, что в одном случае он заключен в функцию , а в другом – нет. Кажется, что можно было бы разместить этот повторяющийся код в отдельной мере и использовать ее в переменных. Этот вариант выглядит очень логичным, особенно если повторяющийся код не будет состоять из простой итерации при помощи функции , а будет длинным и сложным. К сожалению, такой способ сократить формулу неприменим, поскольку DAX автоматически заключает код, на который ссылается мера, в функцию . Представьте, что мы создали меру , а  затем в  вычисляемом столбце дважды обратились к ней при объявлении переменных: один раз с использованием функции , другой – без. Sales Amount := SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) 'Product'[Performance] = VAR TotalSales = [Sales Amount] VAR CurrentSales = CALCULATE ( [Sales Amount] ) VAR Ratio = 0.01 VAR Result = IF ( CurrentSales >= TotalSales * Ratio; "High Performance product"; "Regular product" ) RETURN Result

Выглядит этот код неплохо, но при запуске даст ошибочные результаты. Причина в том, что оба вызова меры внутри вычисляемого столбца автоматически неявным образом будут обернуты в функцию . Таким образом, в переменной окажется не общая сумма продаж по всем товарам, а  продажа по текущему товару – как раз из-за скрытого заключения нк ии

и

189

выражения в  функцию , а  значит, и  выполнения преобразования контекста. В переменной при этом окажется то же самое значение. Здесь второе обрамление в функцию будет просто избыточным – одна такая функция уже присутствует здесь в неявном виде из-за ссылки на меру в контексте строки, открытом в вычисляемом столбце. Если мы развернем код, то увидим все это сами: 'Product'[Performance] = VAR TotalSales = CALCULATE ( SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) ) VAR CurrentSales = CALCULATE ( CALCULATE ( SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) ) ) VAR Ratio = 0.01 VAR Result = IF ( CurrentSales >= TotalSales * Ratio; "High Performance product"; "Regular product" ) RETURN Result

Каждый раз, когда в коде DAX вы видите ссылку на меру, вы должны подразумевать обрамляющую ее функцию . Она там просто есть, и все. В главе 2 мы говорили, что при обращении к столбцам лучше всегда явно указывать название таблицы и никогда не делать этого, ссылаясь на меры. И причина этого заключается в том, что мы обсуждаем сейчас. Читая код DAX, пользователь должен четко понимать, ссылается ли тот или иной фрагмент на меру либо столбец таблицы. И признанным стандартом для разработчиков является избегание употребления имени таблицы перед мерой. Автоматическое обрамление кода в  функцию позволяет легко писать сложные формулы с использованием итераций. В главе 7 мы побольше поработаем с этим на примерах, решая специфические сценарии.

иклические зависимости Разрабатывая модель данных, вы должны обращать особое внимание на возможность появления так называемых икли ески ависимостей (circular de190

нк ии

и

pendencies) в формулах. В этом разделе вы узнаете, что такое циклические зависимости и  как избегать их появления в  вашей модели данных. Перед тем как приступать к  обсуждению циклических зависимостей, стоит поговорить о простых линейн ависимост (linear dependencies) на конкретных примерах. Посмотрите на такую формулу для вычисляемого столбца: Sales[Margin] = Sales[Net Price] - Sales[Unit Cost]

Образованный вычисляемый столбец зависит от двух столбцов: и  . Это означает, что для того, чтобы вычислить значение , DAX необходимо предварительно узнать значения двух других столбцов. Зависимости  – важная часть модели данных DAX, поскольку именно они определяют порядок расчетов в вычисляемых столбцах и таблицах. В нашем примере значение в столбце Margin может быть вычислено только после расчета и  . Разработчику не стоит заботиться о зависимостях. DAX прекрасно справляется с  ними сам путем построения сложных схем последовательности вычисления всех внутренних объектов. Но бывает, что при написании кода в  этой последовательности возникает икли еска а висимост . Причина ее появления в том, что DAX не может самостоятельно определить порядок вычисления выражений при попадании в бесконечный цикл. Рассмотрим следующие два вычисляемых столбца: Sales[MarginPct] = DIVIDE ( Sales[Margin]; Sales[Unit Cost] ) Sales[Margin] = Sales[MarginPct] * Sales[Unit Cost]

Вычисляемый столбец зависит от , тогда как , в свою очередь, зависит от . Возникает замкнутый цикл зависимостей. При попытке сохранить последнюю формулу DAX выдаст ошибку, говорящую об обнаружении циклической зависимости. Циклические зависимости возникают в коде не так часто, поскольку разработчики делают все, чтобы их не было. К тому же сама проблема понятна всем. B не может зависеть от A, если A зависит от B. Но бывает, что циклические зависимости возникают. Не потому, что разработчик этого захотел, а из-за недопонимания всех тонкостей языка DAX. В этом сценарии мы будем использовать функцию . Представьте, что в таблице есть вычисляемый столбец со следующей формулой: Sales[AllSalesQty] = CALCULATE ( SUM ( Sales[Quantity] ) )

Попробуйте ответить на вопрос: от какого столбца зависит значение в вычисляемом столбце ? На первый взгляд кажется, что единственным столбцом, от которого зависит , является Sales[Quantity], поскольку другие столбцы в выражении просто не упоминаются. Как же легко забыть про действительную семантику функции и связанную с ней концепцию преобразования контекста! Поскольку функция вызвана в контексте строки, текущие значения всех столбцов таблицы будут включены в выражение, пусть и незримо. Таким образом, полное выражение, которое поступит на исполнение движку DAX, будет выглядеть так: нк ии

и

191

Sales[AllSalesQty] = CALCULATE ( SUM ( Sales[Quantity] ); Sales[ProductKey] = ; Sales[StoreKey] = ; ...; Sales[Margin] = )

Как видите, список столбцов, от которых зависит , включает в себя полный набор столбцов таблицы. Поскольку функция была вызвана в  контексте строки, выражение автоматически получает зависимости от всех без исключения столбцов таблицы, по которой осуществляются итерации. Это более очевидно в  случае с  вычисляемым столбцом, в  котором контекст строки присутствует по умолчанию. Если написать один вычисляемый столбец с использованием функции , ничего страшного не произойдет. Проблема возникнет, если создать сразу два вычисляемых столбца в  таблице с  применением функции , инициирующей преобразование контекста для обоих столбцов. Так что попытка создания следующего вычисляемого столбца завершится неудачей: Sales[NewAllSalesQty] = CALCULATE ( SUM ( Sales[Quantity] ) )

Причина возникновения ошибки в том, что функция автоматически принимает все столбцы таблицы в качестве аргументов фильтра. А добавление в таблицу нового столбца влияет на определение других столбцов. Если бы DAX позволил нам создать столбец , код двух вычисляемых столбцов выглядел бы примерно так: Sales[AllSalesQty] = CALCULATE ( SUM ( Sales[Quantity] ); Sales[ProductKey] = ; ...; Sales[Margin] = ; Sales[NewAllSalesQty] = ) Sales[NewAllSalesQty] = CALCULATE ( SUM ( Sales[Quantity] ); Sales[ProductKey] = ; ...; Sales[Margin] = ; Sales[AllSalesQty] = )

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

нк ии

и

Обнаружить эту проблему бывает не так просто, но решается она легко. Если в таблице, в которой создаются вычисляемые столбцы с функцией , присутствует столбец с уникальными записями и DAX знает об этом, при преобразовании контекста будет фильтроваться значение только этого столбца. Представьте, что мы создали следующий вычисляемый столбец в таблице : 'Product'[ProductSales] = CALCULATE ( SUM ( Sales[Quantity] ) )

В данном случае нет никакой необходимости добавлять все столбцы в  качестве аргументов фильтра. В таблице есть столбец , содержащий уникальные значения. И DAX знает о существовании этого столбца, поскольку таблица находится в связи с  на стороне «один». А значит, во время преобразования контекста движок не будет добавлять фильтр для каждого столбца таблицы. Таким образом, код может преобразоваться в  подобный: 'Product'[ProductSales] = CALCULATE ( SUM ( Sales[Quantity] ); 'Product'[ProductKey] = )

Как видите, вычисляемый столбец в таблице зависит исключительно от поля . В этом случае вы можете создавать множество вычисляемых столбцов в данной таблице – каждое из них будет зависеть только от ключевого столбца. Примечание оследнее за е ание о нк ии CALCULATE в де ствительности не сли ко верно М ис ользовали тот довод искл ительно в о разовательн ел а са о деле нк и CALCULATE до авл ет в ка естве арг ентов ильтра все стол та ли независи о от того, есть в не кл евое оле или нет ри то вн тренн зависи ость создаетс только дл стол а с никальн и зна ени и али ие кл евого ол озвол ет создавать ножество в исл е стол ов с ис ользование нк ии CALCULATE ри то се антика нк ии остаетс режне все ез искл ени стол та ли , о которо ос ествл тс итера ии, вкл а тс в исло арг ентов ильтра

Мы уже сказали выше, что в таблице, в которой есть дублирующиеся строки, полагаться на преобразование контекста не стоит. Возможность появления циклических зависимостей  – еще один повод отказаться от использования функции , инициирующей преобразование контекста, в случае отсутствия гарантии уникальности строк в таблице. Кроме того, одного наличия в  таблице столбца с  уникальными значениями недостаточно для того, чтобы надеяться, что при преобразовании контекста функция будет зависеть только от него. Модель данных также должна быть оповещена о  присутствии ключевого столбца. А  как сообщить DAX о наличии такого столбца? Есть множество способов передать эту информацию движку: нк ии

и

193

„ если таблица находится в  связи с  другой таблицей на стороне «один», столбец, по которому осуществляется связь, помечается как уникальный. Эта техника работает во всех инструментах; „ если для таблицы установлено свойство «Отметить как таблицу дат» (Mark As Date Table), столбец с датами по умолчанию считается уникальным. Подробно мы будем говорить об этом в главе 8; „ вы можете вручную установить свойство уникальности для столбца, используя пункт «Поведение таблицы» (Table Behavior). Этот способ работает только в Power Pivot для Excel и Analysis Services Tabular. В Power BI на момент написания книги такой функционал не реализован. Выполнения любого из этих условий будет достаточно, чтобы движок DAX посчитал, что в таблице есть ключевое поле. Это позволит вам использовать функцию , не опасаясь возникновения циклических зависимостей. В  этом случае преобразование контекста будет зависеть исключительно от ключевого столбца. Примечание М говори о тако оведении движка как о его осо енности, но на са о деле то ли ь о о н ект о ти иза ии Се антика з ка ред олагает создание зависи осте от все стол ов Однако в ра ка одно из ранни о ти иза и ло становлено, то ри нали ии кл евого ол зависи ость дет создаватьс только дл него одного Сегодн о ень ногие ользователи ри ен т т осо енность, став со вре ене составно асть з ка некотор с енари , на ри ер когда в ор ле заде ств етс нк и USERELATIONSHIP, о ти иза и не в олн етс , то возвра ает нас к о и ке, св занно с икли ески и зависи ост и

Модификаторы функции CALCULATE Как вы уже узнали из этой главы, функция  – исключительно мощная и гибкая, и она позволяет писать очень сложный код на DAX. До сих пор мы рассматривали только работу с аргументами фильтра функции и преобразованием контекста. Но есть еще одна важная концепция, без понимания которой невозможно в полной мере овладеть навыками использования функции . Речь идет о модификатора (modifier) этой функции. Ранее мы уже познакомились с  двумя модификаторами функции : и  . И если может использоваться и как модификатор, и как табличная функция, то является исключительно модификатором аргументов фильтра, а значит, определяет способ взаимодействия конкретного фильтра с  исходным контекстом фильтра. Функция может использовать несколько модификаторов, влияющих на подготовку нового контекста фильтра. И  главным из них, пожалуй, является функция , с которой вы уже хорошо знакомы. Когда применяется к фильтрам функции , она выступает исключительно в качестве модификатора, а не табличной функции. К модификаторам функции также относятся , и  . Их мы рассмотрим отдельно. Что касается модификаторов , , 194

нк ии

и

и  , все они обладают одинаковыми равилами стар инства (precedence rules) с модификатором . В этом разделе мы познакомимся со всеми этими модификаторами, а затем обсудим вопросы, связанные с правилами старшинства. В заключение мы составим полную схему правил для функции .

Модификатор USERELATI

S IP

Первым модификатором функции , с которым вы познакомитесь, будет . Посредством этого модификатора функция способна активировать ту или иную связь во время вычисления выражения. Изначально в модели данных присутствуют как активн е (active), так и неактивн е (inactive) связи. Неактивные связи могут появиться, например, в  случае наличия нескольких связей между таблицами, при этом активной в любой момент времени может быть только одна из них. Например, в таблице мы можем хранить как дату заказа, так и дату поставки. Обычно анализ продаж производится на основании дат заказов, но для специфических мер вполне может потребоваться учет даты поставки. В этом случае вы можете изначально создать две связи между таблицами и  : одну на основании поля (Дата заказа), а вторую – на основании (Дата поставки). Модель данных при этом будет выглядеть как на рис. 5.37.

Рис 5 37 а ли Sales и Date о единен дв св з и, но активно в л о о ент вре ени ожет ть только одна из ни

нк ии

и

195

В каждый отдельный момент времени активной может быть только одна связь между двумя таблицами в модели. На рис. 5.37 активна связь по столбцу , тогда как связь по лишь ждет своего часа. Чтобы написать меру с использованием связи по столбцу , необходимо активировать ее на время выполнения вычисления. В этом случае вам поможет модификатор , как показано в следующем фрагменте кода: Delivered Amount:= CALCULATE ( [Sales Amount]; USERELATIONSHIP ( Sales[Delivery Date]; 'Date'[Date] ) )

В результате связь между столбцами и  будет активна на протяжении всего вычисления меры. В это время связь по полю будет неактивна. Еще раз акцентируем ваше внимание на том, что в любой момент времени между двумя таблицами может быть активна только одна связь. Таким образом, модификатор временно активирует одну связь, деактивируя при этом связь, активную за пределами выполнения функции . На рис. 5.38 наглядно показано отличие между мерой , рассчитанной по связи с  , и новой мерой , в основании которой лежит связь по полю .

Рис 5 38 азни а ежд родажа и о дате заказа и дате оставки

Используя модификатор для активации связи, важно иметь в виду один важный момент: связи определяются в момент использования ссылки на таблицу, а  не в  момент вызова или любой другой функции для работы со связями. Мы подробнее обсудим этот нюанс в главе 14, когда будем говорить о  расширенных таблицах. Сейчас же достаточно будет одного несложного примера. Следующая мера для расчета суммы по товарам, доставленным в 2007 году, не сработает: 196

нк ии

и

Delivered Amount 2007 v1 := CALCULATE ( [Sales Amount]; FILTER ( Sales; CALCULATE ( RELATED ( 'Date'[Calendar Year] ); USERELATIONSHIP ( Sales[Delivery Date]; 'Date'[Date] ) ) = "CY 2007" ) )

Фактически функция отменит действие контекста строки, созданного функцией во время итераций. Так что внутри выражения в  нельзя использовать функцию . Одним из способов написать нужную нам формулу будет следующий: Delivered Amount 2007 v2 := CALCULATE ( [Sales Amount]; CALCULATETABLE ( FILTER ( Sales; RELATED ( 'Date'[Calendar Year] ) = "CY 2007" ); USERELATIONSHIP ( Sales[Delivery Date]; 'Date'[Date] ) ) )

В этой формуле мы обращаемся к  таблице после того, как функция активировала нужную нам связь. Так что функция внутри будет использовать связь на основании . Мера покажет правильный результат, но лучше при подобных вычислениях полагаться на распространение контекста фильтра, а не на функцию : Delivered Amount 2007 v3 := CALCULATE ( [Sales Amount]; 'Date'[Calendar Year] = "CY 2007"; USERELATIONSHIP ( Sales[Delivery Date]; 'Date'[Date] ) )

Когда мы используем модификатор в  функции , все аргументы фильтра вычисляются с  учетом этого модификатора  – нк ии

и

197

вне зависимости от порядка их следования. Например, в  представленной мере модификатор будет оказывать влияние на предикат с использованием , несмотря на то что расположен позже. Такая особенность поведения осложняет применение альтернативных связей в вычисляемых столбцах. Ссылка на таблицу присутствует в определении вычисляемого столбца в неявном виде. Так что мы не можем контролировать этот момент и  изменить такое поведение при помощи функции с модификатором . Важно отметить, что сам по себе модификатор не является фильтрующим элементом в  выражении. Это не аргумент фильтра, а  просто модификатор, регламентирующий применение остальных указанных фильтров к  модели данных. Если внимательно посмотреть на определение меры , можно заметить, что в  аргументе фильтра по 2007 году не указано, какую связь использовать: по столбцу или . Этот момент как раз и определяется модификатором . Таким образом, функция сначала модифицирует структуру модели данных, активируя нужную связь, и только после этого применяет аргумент фильтра. Если бы последовательность действий была иной, то есть аргумент фильтра всегда применялся бы с  использованием текущей связи, мера показала бы неправильный результат. В области применения аргументов фильтра и  модификаторов в  функции действуют определенные правила старшинства. Первое правило гласит, что модификаторы в функции всегда применяются раньше аргументов фильтра, так что все фильтры накладываются уже на измененную версию модели данных. Более детально правила старшинства в функции мы обсудим позже.

Модификатор CR SSFILTER Следующим модификатором функции , который мы изучим, будет . в некоторой степени похож на , поскольку также оказывает влияние на архитектуру связей в модели данных. В то же время может выполнять две операции: „ изменять направление кросс-фильтрации существующих связей; „ деактивировать связь. Модификатор позволяет сделать активной нужную для вычисления связь, но он не может деактивировать связь между двумя таблицами, не активируя при этом другую связь. работает иначе. Этот модификатор принимает два параметра, указывающих на столбцы, вовлеченные в связь, и третий параметр, который может принимать одно из следующих значений: (Нет связи), (Односторонняя связь) или (Двусторонняя связь). Например, в следующей мере рассчитывается количество уникальных цветов товаров после установки двунаправленной кросс-фильтрации для связи между таблицами и  : 198

нк ии

и

NumOfColors := CALCULATE ( DISTINCTCOUNT ( 'Product'[Color] ); CROSSFILTER ( Sales[ProductKey]; 'Product'[ProductKey]; BOTH ) )

Как и в случае с  , модификатор не устанавливает фильтры. Он только изменяет структуру связей, оставляя задачу фильтрации данных аргументам фильтра. В  этом примере модификатор оказывает влияние лишь на функцию , поскольку других аргументов фильтра в данной функции не представлено.

Модификатор EEPFILTERS Ранее в этой главе мы уже встречались с модификатором . Чисто технически является модификатором не функции , а  ее аргументов фильтра. И  действительно, этот модификатор не оказывает влияния на вычисление выражения внутри . Вместо этого он определяет способ применения конкретного аргумента фильтра к итоговому контексту фильтра, созданному функцией . Мы уже детально обсуждали поведение функции в выражениях, подобных тому, что показано ниже: Contoso Sales := CALCULATE ( [Sales Amount]; KEEPFILTERS ( 'Product'[Brand] = "Contoso" ) )

Присутствие модификатора означает, что фильтр по столбцу не будет перезаписывать ранее существовавшие фильтры по этому столбцу. Вместо этого он будет добавлен к текущему контексту фильтра. Модификатор применяется индивидуально к тому аргументу фильтра, в котором указан, и не меняет семантику функции в целом. Есть и еще один вариант использования , пусть и не столь очевидный. Можно применять его в качестве модификатора для таблицы, по которой осуществляются итерации, как показано ниже: ColorBrandSales := SUMX ( KEEPFILTERS ( ALL ( 'Product'[Color]; 'Product'[Brand] ) ); [Sales Amount] )

Присутствие в качестве функции верхнего уровня внутри итератора вынуждает DAX применять этот модификатор ко всем неявным аргументам фильтра, созданным функцией во время преобразования контекста. Фактически во время итераций по значениям столбцов и  функция вызывает как составную часть вычисления меры . В  результате происходит преобразование коннк ии

и

199

текста, и контекст строки превращается в контекст фильтра путем добавления аргументов фильтра для полей и  . А поскольку при этом был использован модификатор , в  момент преобразования контекста не будут перезаписаны текущие фильтры. Вместо этого будет выполнено их пересечение с  новыми фильтрами. Это не самая распространенная техника использования модификатора . В главе 10 мы рассмотрим несколько примеров на эту тему.

Использование модификатора ALL в функции CALCULATE Как вы узнали в главе 3, представляет собой табличную функцию. Кроме того, она может быть использована и в качестве модификатора функции , когда присутствует в  ней как аргумент фильтра. Название функции остается тем же, но семантика использования совместно с  для многих может оказаться неожиданной. Глядя на следующий пример, можно было бы подумать, что функция выдаст все годы и тем самым изменит контекст фильтра, сделав все годы видимыми: All Years Sales := CALCULATE ( [Sales Amount]; ALL ( 'Date'[Year] ) )

Однако это не так. Будучи использованной в  качестве функции верхнего уровня в аргументе фильтра функции , удаляет существующий фильтр вместо создания нового. Так что здесь эту функцию можно было бы назвать не , а  . Но по историческим соображениям было решено оставить название функции неизменным. Мы же поясним на примере, как работает эта функция. Если воспринимать как табличную функцию, можно интерпретировать работу так, как показано на рис. 5.39. Внутренний по столбцу представляет собой функцию верхнего уровня в рамках . А значит, она ведет себя не как табличная функция. Здесь ее действительно более уместно было бы назвать . Фактически, вместо того чтобы вернуть все годы, тут действует как модификатор функции , удаляющий все фильтры с аргумента. Что на самом деле происходит в этом коде, показано на рис. 5.40. Разница между этими поведениями незначительная. В большинстве вычислений столь небольшие отличия в  семантике останутся незамеченными. Но при написании более сложных формул эти нюансы могут сыграть решающую роль. Сейчас же вам нужно запомнить, что когда функция используется в качестве , то она выступает в роли модификатора , а не табличной функции. Это очень важно по причине определенности порядка применения фильтров в функции . Модификаторы функции применяются к итоговому контексту фильтра до явных аргументов фильтра. Рассмотрим 200

нк ии

и

пример, в котором и  указаны для разных аргументов фильтра функции . В этом случае результат будет таким же, как если бы фильтр применялся к этому же столбцу без модификатора . Таким образом, следующие два определения меры дадут одинаковый результат: Sales Red := CALCULATE ( [Sales Amount]; 'Product'[Color] = "Red" ) Sales Red := CALCULATE ( [Sales Amount]; KEEPFILTERS ( 'Product'[Color] = "Red" ); ALL ( 'Product'[Color] ) )

Рис 5 39 Можно редставить, то ALL возвра ает все год и ис ольз ет тот с исок дл ереза иси с еств его контекста ильтра

Причина в том, что здесь выступает в качестве модификатора функции . Следовательно, он будет применен раньше, чем . Такие же правила старшинства распространяются и на все другие функции с префиксом , в числе которых , , и  . Обычно мы обращаемся к этой группе функций по общему имени *. Как правило, функции выступают в качестве модификаторов , когда присутствуют в ней в виде функций верхнего уровня в аргументах фильтра. нк ии

и

201

Рис 5 40 нк и ALL дал ет все ранее наложенн е ильтр из контекста, д и ис ользованно как REMOVEFILTER

Использование ALL и ALLSELECTED без параметров С функцией мы познакомились в  главе 3. Мы представили ее вам так рано, поскольку она действительно очень полезная. Как и  все функции группы , функция может играть роль модификатора в  , когда включена в нее как функция верхнего уровня в аргументах фильтра. Более того, когда мы описывали функцию , то говорили о ней как о табличной функции, которая умеет возвращать как один столбец, так и целую таблицу. В следующем фрагменте кода рассчитаем процент продаж по отношению ко всем цветам товаров, выбранным за границами текущей визуализации отчета. Это возможно благодаря способности функции восстанавливать контекст фильтра за пределами визуализации  – в  нашем случае по столбцу Product[Color]. SalesPct := DIVIDE ( [Sales]; CALCULATE ( [Sales]; ALLSELECTED ( 'Product'[Color] ) ) )

Того же эффекта можно добиться, написав ALLSELECTED ( Product ) – без указания конкретного столбца. Более того, в  качестве модификаторов   функции и  могут использоваться вовсе без параметров. 202

нк ии

и

Таким образом, следующий синтаксис вполне применим в формуле: SalesPct := DIVIDE ( [Sales]; CALCULATE ( [Sales]; ALLSELECTED ( ) ) )

Здесь, как вы понимаете, не может быть использована как табличная функция. Это модификатор функции , дающий команду восстановить контекст фильтра, который был активным за пределами текущей визуализации отчета. Описание того, как происходит вычисление в этом случае, будет довольно сложным. В главе 14 мы выведем использование функции на новый уровень. Функция без параметров очищает контекст фильтра со всех таблиц в модели данных, восстанавливая при этом контекст без активных фильтров. Теперь, когда мы полностью рассмотрели структуру функции , можно детально поговорить о порядке вычисления ее элементов.

Правила вычисления в функции CALCULATE В заключительном разделе этой длинной и трудной главы мы решили представить подробный обзор функции . Возможно, вы не раз будете обращаться к этому разделу за помощью при дальнейшем чтении книги. Если вам нужно будет уточнить какие-то нюансы поведения функции , ответы на свои вопросы вы наверняка найдете здесь. Не бойтесь возвращаться к этому списку. Мы работаем с DAX уже много лет и  по-прежнему при написании сложных формул иногда обращаемся к  этим правилам. DAX – простой и мощный язык, но очень легко забыть какие-то детали, которые могут повлиять на расчеты в том или ином сценарии. Итак, представляем вам общую картину работы функции : „ функция вызывается в  контексте вычисления, который состоит из контекста фильтра и одного или нескольких контекстов строки. Этот контекст называется исходным; „ функция создает новый контекст фильтра, в рамках которого вычисляет выражение, переданное в нее первым параметром. Новый контекст содержит в себе только контекст фильтра. Все контексты строки переходят в контекст фильтра по правилам преобразования контекста; „ функция принимает на вход три типа параметров: – выражение, которое будет вычислено в новом контексте фильтра. Это выражение всегда передается первым параметром; – набор явно заданных аргументов фильтра, применяющихся к исходному контексту фильтра. Каждый аргумент может быть снабжен модификатором, например ; нк ии

и

203

– набор модификаторов функции , способных менять модель данных и/или структуру исходного контекста фильтра, удаляя фильтры или изменяя схему связей; „ если исходный контекст включает в себя один или несколько контекстов строки, функция инициирует операцию преобразования контекста, добавляя скрытые и неявные аргументы фильтра. Неявные аргументы, полученные из контекстов строки путем итераций по табличным выражениям, помеченным модификатором , сохраняют этот модификатор и в новом контексте фильтра. При обращении с  принятыми параметрами функция следует очень четкому алгоритму. Если разработчик планирует писать сложные формулы в  DAX, он просто обязан понимать всю последовательность действий функции . . Функция оценивает все явные аргументы фильтра в исходном контексте вычисления. Сюда включаются и контексты строки (если есть), и  контекст фильтра. Все явные аргументы оцениваются независимо друг от друга в исходном контексте вычисления. После окончания оценки функция приступает к  созданию нового контекста фильтра. 2. Функция создает копию исходного контекста фильтра для подготовки нового контекста. При этом она отменяет действие контекстов строки, поскольку в новом контексте вычисления не будет контекста строки. 3. Функция выполняет преобразование контекста. При этом используются текущие значения столбцов из исходных контекстов строки для создания фильтра с уникальными значениями для всех столбцов, по которым осуществляются итерации в  исходных контекстах строки. Фильтр может содержать больше одной строки. Нет никакой гарантии, что новый контекст фильтра будет состоять ровно из одной строки. Если в исходном контексте вычисления не было активных контекстов строки, этот шаг пропускается. После применения к новому контексту всех неявных аргументов фильтра, созданных на этапе преобразования контекста, функция переходит к следующему шагу. 4. Функция приступает к  оценке модификаторов , и  . Этот шаг выполняется только после шага  3. Это очень важно, поскольку означает, что на данном этапе мы можем удалить последствия преобразования контекста путем использования функции , как будет описано в главе 10. Модификаторы функции применяются только после выполнения преобразования контекста, чтобы можно было повлиять на его последствия. 5. Функция оценивает все явные аргументы фильтра в исходном контексте фильтра. На этом этапе происходит применение этих фильтров к новому контексту фильтра, созданному на шаге 4. Поскольку преобразование контекста к этому моменту уже выполнено, аргументы могут перезаписывать новый контекст фильтра – после удаления фильтра (эти фильтры не удаляются при помощи модификаторов группы 204

нк ии

и

) и  после обновления структуры связей модели. При этом оценка аргументов фильтра происходит в рамках исходного контекста фильтра и не подвержена влиянию со стороны других модификаторов или фильтров из той же функции . Контекст фильтра, образованный в результате выполнения пятого шага, используется для вычисления выражения функции .

ГЛ А В А 6

Переменные Переменн е в  языке DAX играют важную роль сразу по двум причинам: вопервых, они делают код более легким для восприятия, во-вторых, положительно влияют на его производительность. В данной главе мы подробно обсудим создание и использование переменных в DAX, а вопросы производительности и  читаемости кода затрагиваются на протяжении всей книги. В  самом деле, мы используем переменные почти в  каждом примере, а  иногда показываем версии формул с переменными и без них, чтобы вы почувствовали разницу. Гораздо позже, в главе 20, мы покажем случаи, когда переменные могут значительно увеличить производительность кода. Здесь же мы просто соберем воедино всю важную и полезную информацию о переменных.

Введение в синтаксис переменных VAR В выражениях объявление переменной начинается с ключевого слова , следом за чем идет обязательный блок , определяющий возвращаемый результат. Так выглядит типичный код, использующий переменную: VAR SalesAmt = SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) RETURN IF ( SalesAmt > 100000; SalesAmt; SalesAmt * 1.2 )

В одном блоке может быть объявлено сразу несколько переменных, тогда как блок в блоке должен быть один. Очень важно отметить, что блоки по своей сути являются выражениями. А значит, их можно применять везде, где допустимо использовать выражения. Это позволяет нам объявить переменные внутри итерации или в составе более сложного выражения, как показано в примере ниже: VAR SalesAmt = SUMX (

206

ере енн е

Sales; VAR Quantity = Sales[Quantity] VAR Price = Sales[Price] RETURN Quantity * Price ) RETURN ...

Обычно переменные объявляются в  начале кода меры и  используются на протяжении всего его определения. Но это лишь условность. В  сложных выражениях объявление переменных на разных уровнях вложенности внутри функций  – вполне обычная практика. В  предыдущем примере переменные и  инициализируются для каждой строки в таблице во время осуществления итераций при помощи функции . Значения этих переменных будут недоступны за пределами выражения, вычисленного функцией для каждой строки. В переменной может храниться как скалярная величина, так и таблица. При этом тип самой переменной может – и часто это так и есть – отличаться от типа выражения, возвращаемого в блоке . Кроме того, внутри одного блока / могут присутствовать переменные разных типов, хранящие как скалярные величины, так и таблицы. Зачастую переменные используются для разбиения сложной формулы на более мелкие логические шаги – в этом случае результат каждого шага записывается в  отдельную переменную. В  следующем примере демонстрируется использование переменных для хранения промежуточных результатов вычисления: Margin% := VAR SalesAmount = SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) VAR TotalCost = SUMX ( Sales; Sales[Quantity] * Sales[Unit Cost] ) VAR Margin = SalesAmount - TotalCost VAR MarginPerc = DIVIDE ( Margin; TotalCost ) RETURN MarginPerc

Та же самая формула без использования переменных будет читаться гораздо хуже: Margin% := DIVIDE ( SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) - SUMX ( Sales; Sales[Quantity] * Sales[Unit Cost]

ере енн е

207

); SUMX ( Sales; Sales[Quantity] * Sales[Unit Cost] ) )

Более того, преимущество версии с переменными состоит еще и в том, что каждая из них вычисляется только один раз. К примеру, в предыдущем примере встречается дважды, но поскольку это переменная, DAX гарантирует, что ее значение будет вычислено лишь один раз. В блоке вы можете написать любое выражение. Но обычно принято указывать здесь только одну переменную. Допустим, в предыдущем примере мы могли бы избавиться от переменной и после ключевого слова написать DIVIDE ( Margin; TotalCost ). Однако использование в блоке переменной позволяет легко изменить возвращаемое значение из меры. Это бывает полезно при проверке значений промежуточных шагов. Если в нашем примере мера будет возвращать ошибочный результат, можно будет проверить значения на всех промежуточных шагах, каждый раз включая меру в отчет. То есть мы бы заменили сначала на , затем на , а после этого на в заключительном блоке . Запуск этих отчетов дал бы нам понять, что на самом деле происходит внутри нашей меры.

Переменные 

то константы

Несмотря на свое название, переменные в языке DAX в действительности являются константами. Однажды присвоив значение переменной, мы не сможем его изменить. Например, будучи объявленной внутри итератора, переменная каждый раз создается заново и  инициализируется. Более того, обратиться к этой переменной можно будет только внутри итерационной функции, в рамках которой она объявлена. Amount at Current Price := SUMX ( Sales; VAR Quantity = Sales[Quantity] VAR CurrentPrice = RELATED ( 'Product'[Unit Price] ) VAR AmountAtCurrentPrice = Quantity * CurrentPrice RETURN AmountAtCurrentPrice ) -- Любые ссылки на переменные Quantity, CurrentPrice или AmountAtCurrentPrice -- будут недействительными за пределами функции SUMX

Значение переменной вычисляется один раз в  области видимости своего определения ( ), а не там, где к ней обращаются. В следующем фрагменте кода мера будет всегда возвращать значение 100  , поскольку на переменную не распространяется влияние функции . Значение этой 208

ере енн е

переменной будет вычислено лишь однажды, и  каждая очередная ссылка на нее будет возвращать одно и то же значение вне зависимости от того, в каком контексте фильтра происходит обращение к этой переменной. % of Product := VAR SalesAmount = SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) RETURN DIVIDE ( SalesAmount; CALCULATE ( SalesAmount; ALL ( 'Product' ) ) )

В последнем примере мы использовали переменную там, где лучше было применить меру. Если мы хотим избежать дублирования кода для в разных частях выражения, правильно будет использовать меру вместо переменной, чтобы результат оказался таким, как мы ожидаем. В следующем примере мы создали две меры и получили правильный результат: Sales Amount := SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) % of Product := DIVIDE ( [Sales Amount]; CALCULATE ( [Sales Amount]; ALL ( 'Product' ) ) )

В этом случае мера будет вычислена дважды в  двух разных контекстах фильтра, что приведет к разным результатам вычислений, что нам и нужно.

Области видимости переменных Любая переменная в своем определении может ссылаться на другие переменные, объявленные в рамках того же блока / . Все переменные, инициализированные во внешнем блоке , также будут доступны. В своем определении переменные могут ссылаться на другие переменные, объявленные в  коде до нашей переменной, но не после. Следующий код отработает правильно: Margin := VAR SalesAmount = SUMX ( Sales; Sales[Quantity] * Sales[Net Price] )

ере енн е

209

VAR TotalCost = SUMX ( Sales; Sales[Quantity] * Sales[Unit Cost] ) VAR Margin = SalesAmount - TotalCost RETURN Margin

Если же перенести объявление переменной Margin в начало меры, DAX не примет такой синтаксис. Дело в том, что в этом случае переменная ссылается на переменные и  , которые в данный момент еще не объявлены: Margin := VAR Margin = SalesAmount - TotalCost -- Ошибка: SalesAmount и TotalCost не объявлены VAR SalesAmount = SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) VAR TotalCost = SUMX ( Sales; Sales[Quantity] * Sales[Unit Cost] ) RETURN Margin

Поскольку к переменной невозможно обратиться до ее объявления, нет риска создания циклических зависимостей между переменными или образования любого рода рекурсивных определений. Блоки / допустимо вкладывать один в  другой или содержать несколько таких блоков в одном выражении. ласт видимости еременн (scope of variables) для каждого из этих сценариев будет своя. Например, в следующем фрагменте кода переменные и  объявлены в двух разных областях видимости, не вложенных друг в  друга. Таким образом, ни в один момент времени мы не сможем обратиться сразу к обеим переменным в одном выражении: Margin := SUMX ( Sales, ( VAR LineAmount = Sales[Quantity] * Sales[Net Price] RETURN LineAmount ) -- Скобки закрывают область видимости переменной LineAmount -- Переменная LineAmount будет недоступна здесь и далее ( VAR LineCost = Sales[Quantity] * Sales[Unit Cost] RETURN LineCost ) )

Разумеется, мы привели этот пример исключительно в образовательных целях. Гораздо лучше будет объявить обе переменные рядом и свободно использовать внутри меры : 210

ере енн е

Margin := SUMX ( Sales; VAR LineAmount = Sales[Quantity] * Sales[Net Price] VAR LineCost = Sales[Quantity] * Sales[Unit Cost] RETURN LineAmount - LineCost )

В качестве еще одного обучающего примера интересно рассмотреть действительную область видимости переменных в  случае, когда скобки не применяются, а в выражении объявляются и используются сразу несколько переменных в отдельных блоках / . Посмотрите следующий пример: Margin := SUMX ( Sales; VAR LineAmount = Sales[Quantity] * Sales[Net Price] RETURN LineAmount VAR LineCost = Sales[Quantity] * Sales[Unit Cost] RETURN LineCost -- Здесь переменная LineAmount по-прежнему доступна )

Выражение, стоящее после первого ключевого слова , является частью единого выражения. Так что объявление переменной на самом деле вложено в определение переменной LineAmount. Применение скобок для разграничения блоков и  использование надлежащих отступов делают этот факт более очевидным: Margin := SUMX ( Sales; VAR LineAmount = Sales[Quantity] * Sales[Net Price] RETURN ( LineAmount - VAR LineCost = Sales[Quantity] * Sales[Unit Cost] RETURN ( LineCost -- Здесь переменная LineAmount по-прежнему доступна ) ) )

Поскольку, как было показано в предыдущем примере, переменные могут быть объявлены внутри выражений, они могут быть определены также и в рамках выражений, принадлежащих другим переменным. Иными словами, переменные в DAX могут быть вложенными. Посмотрите на следующий пример: Amount at Current Price := SUMX ( 'Product';

ере енн е

211

VAR CurrentPrice = 'Product'[Unit Price] RETURN -- Переменная CurrentPrice доступна во внутренней функции SUMX SUMX ( RELATEDTABLE ( Sales ); VAR Quantity = Sales[Quantity] VAR AmountAtCurrentPrice = Quantity * CurrentPrice RETURN AmountAtCurrentPrice ) -- Ссылки на переменные Quantity и AmountAtCurrentPrice -- будут недоступны за пределами внутренней функции SUMX ) -- Ссылки на переменную CurrentPrice -- будут недоступны за пределами внешней функции SUMX

Основные правила, касающиеся области видимости переменных: „ переменная доступна после ключевого слова соответствующего блока / . Также доступ к этой переменной будет у всех переменных, объявленных позже нее в одном блоке / . Блок / может заменять собой любое выражение DAX, и  внутри этого выражения к переменной будет доступ. Иными словами, к переменной можно обращаться с момента ее объявления и до конца выражения, следующего за ключевым словом , являющимся частью того же блока / ; „ переменная недоступна за пределами своего блока / . После выражения, следующего за ключевым словом , переменная, объявленная в этом блоке / , будет не видна, и обращение к ней выдаст синтаксическую ошибку.

Использование табличных переменных В переменной может храниться как скалярная величина, так и таблица. Тип переменной зависит от ее определения. Например, если выражение, используемое для инициализации переменной, является табличным, то и сама переменная приобретет табличный тип. Рассмотрим следующий код: Amount := IF ( HASONEVALUE ( Slicer[Factor] ); VAR Factor = VALUES ( Slicer[Factor] ) RETURN DIVIDE ( [Sales Amount]; Factor ) )

212

ере енн е

Если Slicer[Factor] в текущем контексте фильтра окажется столбцом с одной строкой, то ее значение может быть преобразовано в скалярную величину. Переменная хранит таблицу, поскольку при ее объявлении была использована функция , принадлежащая к табличному типу. Если не проверять выражение Slicer[Factor] на присутствие одной строки, присвоение значения переменной произойдет успешно. Ошибка же возникнет на втором параметре функции , где происходит обращение к этой переменной. И это будет ошибка преобразования. Если в переменной содержится таблица, скорее всего, вам захочется пройти по ней при помощи итераций. Важно отметить, что во время таких итераций обращаться к столбцам табличной переменной нужно по имени исходной таблицы. Иными словами, название табличной переменной не является севдони мом (alias) наименования лежащей в ее основании таблицы: Filtered Amount := VAR MultiSales = FILTER ( Sales; Sales[Quantity] > 1 ) RETURN SUMX ( MultiSales; -- MultiSales не является названием таблицы при обращении к столбцам -- Попытка записи MultiSales[Quantity] приведет к возникновению ошибки Sales[Quantity] * Sales[Net Price] )

Несмотря на то что функция осуществляет итерации по табличной переменной , при обращении к столбцам и  необходимо использовать префикс , являющийся названием исходной таблицы. Обратиться к столбцу при помощи выражения MultiSales[Quantity] нельзя. На данный момент одним из ограничений DAX является то, что переменная в коде не может называться так же, как одна из таблиц в модели данных. Это предотвращает возможную путаницу между обращениями к таблице и переменной. Рассмотрим следующую формулу: SUMX ( LargeSales; Sales[Quantity] * Sales[NetPrice] )

Читающий этот код сразу поймет, что является ссылкой на табличную переменную, поскольку при обращении к столбцам используется другой префикс, а именно . Но в DAX возможные неоднозначности трактовки решили снять на уровне языка. Так что одно название может относиться либо к физической таблице, либо к табличной переменной, но не к обеим сразу. На первый взгляд кажется, что это очень логичное и удобное ограничение, призванное исключить неразбериху с именами в коде. Однако в долгосрочной перспективе оно может доставлять некоторые неудобства. В самом деле, объявляя в  коде переменную, вы должны обеспокоиться тем, чтобы в  будущем в  модели данных не появилась таблица с таким же названием. В  противном случае вы получите ошибку на этапе создания новой таблицы. Любые ограниере енн е

213

чения синтаксиса, предполагающие учет возможных событий в будущем – таких как именование таблиц, – являются потенциально проблемными, если не сказать больше. По этой причине, когда Power BI генерирует код DAX, он снабжает все имена переменных префиксом в виде двух знаков подчеркивания ( ). Вероятность того, что пользователь назовет таблицу в модели данных таким именем, невелика. Примечание одо ное оведение ожет ть из енено в д е , то озволит разра от ика наз вать ере енн е и та ли одинаково огда то роизо дет, ожно дет оль е не о асатьс , то в како то о ент кто то за о ет создать та ли с и ене , которое же ло ис ользовано в ере енно ри сов адении и ен во из ежание неоднозна ности ожно дет ользоватьс одинарн и кав ка и дл и еновани та ли , как оказано ниже variableName -- имя переменной 'tableName' -- имя таблицы сли разра от ик дет ис ользовать генератор кода в с еств и в ражени , названи та ли ог т ть закл ен и в одинарн е кав ки сли и ена та ли и ере енн не ересека тс , о то ожно не за отитьс

Отложенное вычисление переменных Как вы уже знаете, DAX рассчитывает значение каждой переменной в том контексте вычисления, в  котором она определена, а  не в том, из которого была вызвана. Но при этом само вычисление ее значения произойдет только тогда, когда в коде впервые встретится ссылка на эту переменную. Данная техника получила название ленивое в исление (lazy evaluation), также именуемое отложенным. Такой подход очень важен в плане производительности: переменная, которая по тем или иным причинам не будет участвовать в вычислении выражения, не будет и рассчитана. Кроме того, будучи вычисленной один раз, переменная не будет рассчитываться повторно в той же области видимости. Рассмотрим следующий пример: Sales Amount := VAR SalesAmount = SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) VAR DummyError = ERROR ( "Эта ошибка никогда не произойдет" ) RETURN SalesAmount

Переменная не используется на протяжении всего кода, а значит, ее значение никогда не будет вычислено. Таким образом, ошибка никогда не возникнет, и мера будет работать корректно. Очевидно, что никто не будет писать такой код. Целью этого примера было показать, что DAX экономно относится к драгоценным ресурсам центрального 214

ере енн е

процессора при вычислении значений переменных и  задействует их только в случае необходимости. Вы можете полагаться на такое поведение движка при написании своих формул. Если в вашем коде будет несколько раз использоваться одно и то же выражение, лучше будет определить для него переменную. Это гарантирует однократное вычисление переменной. И с точки зрения производительности это даже более важно, чем вы можете себе представить. Подробнее мы рассмотрим эту тему в главе 20, а здесь просто сформулируем основную идею. Оптимизатор DAX располагает специальным процессом, который называется о ределение одформул (sub-formula detection). В сложных фрагментах кода этот процесс отвечает за поиск повторяющихся подформул, которые можно вычислить лишь раз. Взгляните на следующий код: SalesAmount TotalCost Margin Margin%

:= := := :=

SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) SUMX ( Sales; Sales[Quantity] * Sales[Unit Cost] ) [SalesAmount] – [TotalCost] DIVIDE ( [Margin]; [TotalCost] )

Мера здесь вызывается дважды: один раз при вычислении , второй – для расчета . В зависимости от качества оптимизатора может быть выявлен факт двукратного обращения к одной и той же переменной и определена возможность вычислить ее значение один раз. Однако оптимизатору не всегда удается эффективно выполнять поиск этих подформул. Вам как разработчику обычно лучше известно, какие выражения предполагается использовать в коде многократно. Если вы привыкли в  своих формулах использовать переменные, возьмите за правило определять в качестве переменных подформулы. Многократно используя ссылки на них в коде, вы поможете оптимизатору построить максимально эффективный план выполнения запроса.

Распространенные шаблоны использования переменных В данном разделе мы обсудим вопросы практического использования переменных. Конечно, мы не приведем исчерпывающий список сценариев, в которых использование переменных может оказаться полезным. Мы покажем лишь наиболее распространенные шаблоны, часто встречающиеся на практике. Первая и одна из важнейших причин для применения переменных в коде состоит в  возможности снабдить его своеобразной документацией. Представьте, что вам нужно написать сложный многоступенчатый расчет с использованием функции . Предварительная запись аргументов фильтра функции поможет повысить легкость восприятия кода. Семантика выражения и его производительность при этом не изменятся в лучшую сторону. Фильтры в любом случае будут проанализированы за границами преобразования контекста, запущенного функцией , и контексты фильтра будут вычислены отложенным способом. Но код станет легко читаемым, а для ере енн е

215

разработчика это чрезвычайно важно. Давайте рассмотрим следующую формулу для меры: Sales Large Customers := VAR LargeCustomers = FILTER ( Customer; [Sales Amount] > 10000 ) VAR WorkingDaysIn2008 = CALCULATETABLE ( ALL ( 'Date'[IsWorkingDay]; 'Date'[Calendar Year] ); 'Date'[IsWorkingDay] = TRUE (); 'Date'[Calendar Year] = "CY 2008" ) RETURN CALCULATE ( [Sales Amount]; LargeCustomers; WorkingDaysIn2008 )

Использование переменных для хранения отфильтрованных таблиц с покупателями и датами позволило разбить итоговое вычисление на три этапа: определение покупателей с  определенной суммой продаж, ограничение периода для анализа и, наконец, вычисление меры с двумя примененными фильтрами. Может показаться, что мы говорим только о стилистике программного кода, но не стоит забывать о том, что у элегантных и простых формул больше шансов выдавать на выходе корректный результат. В  процессе упрощения кода разработчик сможет лучше понять его функционал и  исключить возможные ошибки. Любое выражение объемом больше десяти строк следует разбивать на отдельные переменные. Это также поможет программисту сосредоточиться на более мелких фрагментах кода. Еще одним распространенным шаблоном для применения переменных является присутствие в запросе вложенных друг в друга контекстов строки из одной и той же таблицы. В этом случае вы можете использовать переменные для хранения информации из скрытых контекстов строки и тем самым избежать применения функции : 'Product'[RankPrice] = VAR CurrentProductPrice = 'Product'[Unit Price] VAR MoreExpensiveProducts = FILTER ( 'Product'; 'Product'[Unit Price] > CurrentProductPrice ) RETURN COUNTROWS ( MoreExpensiveProducts ) + 1

Контексты фильтра также могут быть вложенными друг в друга. Но это не создает таких проблем с  написанием кода, как в  случае с  вложенными кон216

ере енн е

текстами строки. С разными уровнями контекстов фильтра часто приходится сталкиваться при необходимости сохранения результатов предварительных расчетов для дальнейшего их использования после смены контекста фильтра. К примеру, если вам нужно узнать, какие покупатели приобретают товары на сумму больше средней, следующий код вам не подойдет: AverageSalesPerCustomer := AVERAGEX ( Customer, [Sales Amount] ) CustomersBuyingMoreThanAverage := COUNTROWS ( FILTER ( Customer; [Sales Amount] > [AverageSalesPerCustomer] ) )

Причина этого в  том, что мера будет вычисляться внутри итерации по таблице . А  значит, мы смело можем мысленно обрамлять нашу меру в функцию , которая инициирует преобразование контекста. Следовательно, мера вместо своего прямого предназначения будет на каждой итерации выдавать результат по одному текущему покупателю. В итоге наша мера всегда будет показывать пустое значение. Чтобы получить правильный результат, необходимо вычислить значение меры за пределами итерации. И в этом нам поможет переменная: AverageSalesPerCustomer := AVERAGEX ( Customer; [Sales Amount] ) CustomersBuyingMoreThanAverage := VAR AverageSales = [AverageSalesPerCustomer] RETURN COUNTROWS ( FILTER ( Customer; [Sales Amount] > AverageSales ) )

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

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

217

на DAX, задумайтесь о том, чтобы разбить его на отдельные переменные. Вы будете благодарны сами себе в следующий раз, когда будете разбираться в своих формулах. Код с использованием переменных обычно получается менее лаконичным, чем формулы без переменных. Но объемный код – не проблема, когда каждая составляющая его часть предельно проста для понимания. К сожалению, в некоторых инструментах написание длинного кода на DAX, превышающего десять строк, является проблематичным. В результате вы можете отдать предпочтение более короткому коду без использования переменных, который будет проще ввести в редактор DAX того же Power BI. Но это неправильные доводы. Конечно, все мы хотим, чтобы появились инструменты, позволяющие писать длинный код на DAX с  комментариями и  множеством переменных. И такие инструменты скоро появятся. А пока, вместо того чтобы вписывать заведомо ущербные формулы непосредственно в редакторы существующих инструментов BI, можно воспользоваться сторонними программными продуктами вроде DAX Studio для написания полноценных запросов на DAX и вставки готовых формул обратно в Power BI или Visual Studio.

ГЛ А В А 7

Работа с итераторами и функцией CALCULATE

В предыдущих главах мы много говорили о теоретических основах языка DAX: о контекстах фильтра и строки, а также о преобразовании контекста. Это основа любых выражений в DAX. Мы также представили вам итерационные функции и  показали, как использовать их в  формулах. Но истинная мощь итераторов кроется в их использовании совместно с контекстами вычисления и преобразованием контекста. В данной главе мы выведем понимание итераторов на новый уровень, расскажем о наиболее распространенных практиках их использования и познакомимся с целым рядом новых функций. Умелое обращение с итерационными функциями являет собой очень важный навык для любого разработчика DAX. А использование итераторов совместно с преобразованием контекста – и вовсе уникальная особенность языка. По опыту преподавания можем сказать, что студентам часто бывает непросто сразу осознать всю мощь итерационных функций. Но это не значит, что это такая уж сложная тема для освоения. Концепция применения итераторов на самом деле довольно проста, как и их совместное использование с техникой преобразования контекста. Что бывает действительно сложно, так это понять, что какая-то непростая на первый взгляд задача легко решается при помощи итерационных функций. Именно поэтому мы решили сделать акцент на вычислениях, в которых вам могут оказаться полезными итераторы.

Использование итерационных функций Большинство итерационных функций принимают минимум два параметра: таблицу для осуществления итераций и выражение, которое необходимо вычислить на каждом проходе в контексте строки, создаваемом во время итераций. Вот простейший пример использования итератора : Sales Amount := SUMX ( Sales; Sales[Quantity] * Sales[Net Price] )

-- Таблица для осуществления итераций -- Выражение для вычисления в каждой строке

а ота с итератора и и

нк ие

219

Функция проходит по таблице и для каждой строки выполняет умножение цены на количество. Итерационные функции отличаются друг от друга тем, как обращаются с промежуточными результатами. Функция представляет собой простой итератор, суммирующий результаты построчных вычислений. Важно понимать различия между двумя параметрами. В первом содержится результат табличного выражения для осуществления итераций. Этот параметр вычисляется до начала итераций. Второй параметр представляет собой выражение, которое не рассчитывается до прохода по таблице. Вместо этого его вычисление происходит в контексте строки на каждой итерации. В официальной документации Microsoft нет строгой классификации итерационных функций. Более того, там даже не указано, какие параметры представляют собой значение, а какие – выражение, вычисляемое на каждой итерации. В инструкции по адресу https://dax.guide все функции, рассчитывающие выражение в контексте строки, помечены специальным маркером (ROW CONTEXT) для выделения параметров, вычисляемых в контексте строки. Все функции, параметры которых имеют такую отметку, являются итераторами. Некоторые итерационные функции принимают более двух параметров. Например, у функции множество параметров, тогда как простые итераторы , замечательно обходятся двумя. В данной главе мы опишем работу разных итерационных функций, но сначала рассмотрим важные аспекты, объединяющие все без исключения итераторы.

ратность итератора Первой важной характеристикой итерационных функций является их крат ност (iterator cardinality). Кратност итератора называется количество строк, по которым осуществляются итерации. Если в следующем примере в таблице будет миллион строк, кратность итератора будет равна миллиону: Sales Amount := SUMX ( Sales; -- В таблице Sales 1 млн строк, значит, Sales[Quantity] * Sales[Net Price] -- выражение вычислится ровно 1 млн раз )

Говоря о  кратности, мы редко оперируем цифрами. Фактически в  предыдущем примере кратность итератора напрямую зависит от количества строк в таблице . В таком случае мы обычно говорим, что кратность итератора такая же, как кратность таблицы . Чем больше строк будет в таблице, тем больше итераций выполнится. Если мы имеем дело с вложенными друг в друга итерационными функциями, результирующая кратность будет составляться из кратностей двух итераторов – вплоть до произведения количества строк в исходных таблицах. Рассмотрим следующую формулу: Sales at List Price 1 := SUMX ( 'Product';

220

а ота с итератора и и

нк ие

SUMX ( RELATEDTABLE ( Sales ); 'Product'[Unit Price] * Sales[Quantity] ) )

Представленное выражение включает в  себя две итерационные функции. Внешняя проходит по таблице . Таким образом, ее кратность будет равна кратности таблицы . Затем, внутри внешней итерации, для каждого товара проводится сканирование по таблице и возврат только тех строк, которые связаны с текущим товаром. В нашем случае, поскольку каждой строке в таблице соответствует только одна строка в таблице , итоговая кратность выражения будет равна кратности таблицы . Если бы выражение во вложенной итерации не зависело от внешней таблицы, кратность выросла бы в разы. Рассмотрим следующий пример. В нем мы рассчитываем те же значения, но, в отличие от первого случая, к таблице продаж обращаемся не по связи, а при помощи функции , ограничивающей количество строк в таблице : Sales at List Price High Cardinality := SUMX ( VALUES ( 'Product' ); SUMX ( Sales; IF ( Sales[ProductKey] = 'Product'[ProductKey]; 'Product'[Unit Price] * Sales[Quantity]; 0 ) ) )

В данном примере внутренняя функция каждый раз проходит по всей таблице и  при помощи условной функции отбирает строки, относящиеся к текущему товару. Здесь кратность внешней функции будет совпадать с кратностью таблицы , а внутренней – с таблицей . Общая кратность выражения составит произведение двух составных кратностей, что намного больше, чем в  предыдущем примере. Отметим, что это выражение мы показали исключительно в образовательных целях. На практике подобная формула будет отличаться очень низкой производительностью. Лучше будет переписать это выражение так: Sales at List Price 2 := SUMX ( Sales; RELATED ( 'Product'[Unit Price] ) * Sales[Quantity] )

Кратность этой итерационной функции, как и в случае с мерой , будет равна кратности таблицы , но план выполнения запроса при этом будет более оптимальным. Заметьте, что здесь нет вложенных итераторов. а ота с итератора и и

нк ие

221

Вложенные итерации часто возникают вследствие преобразования контекста. С  первого взгляда и  не скажешь, что в  следующем выражении присутствуют вложенные итерационные функции: Sales at List Price 3 := SUMX ( 'Product'; 'Product'[Unit Price] * [Total Quantity] )

Но здесь внутри функции есть ссылка на меру , что нельзя не учитывать. Вот как будет выглядеть развернутый код нашей меры, включая определение меры : Total Quantity := SUM ( Sales[Quantity] )

-- Внутреннее представление: SUMX ( Sales, Sales[Quantity] )

Sales at List Price 4 := SUMX ( 'Product'; 'Product'[Unit Price] * CALCULATE ( SUMX ( Sales; Sales[Quantity] ) ) )

Теперь вы видите, что в этой формуле присутствуют вложенные итераторы: внутри . Более того, появилась еще и функция , инициирующая преобразование контекста. При наличии вложенных итераторов есть возможность оптимизировать план выполнения только для внутренней функции. Присутствие внешних итераторов требует создания временных таблиц в  памяти компьютера. В  этих таблицах хранятся промежуточные результаты вычислений вложенных итерационных функций. В  результате получаем низкую производительность формул и расход драгоценных ресурсов компьютера. А значит, использования вложенных итераторов следует избегать в случаях, когда кратность внешних функций достаточно высока – от нескольких миллионов строк и выше. Заметим, что в присутствии преобразования контекста бывает не так просто правильно спланировать вложенность итераторов. Типичной ошибкой является создание вложенных итераций с применением меры, которая может повторно использовать существующую меру. Это опасно, когда существующая логика меры повторно используется внутри итератора. Рассмотрим следующую формулу: Sales at List Price 5 := SUMX ( 'Sales'; RELATED ( 'Product'[Unit Price] ) * [Total Quantity] )

222

а ота с итератора и и

нк ие

Внешне мера очень напоминает меру . К сожалению, тот факт, что здесь итерации во внешнем цикле осуществляются по таблице , нарушает сразу несколько правил преобразования контекста, изложенных в главе 5. Преобразование контекста тут выполняется на таблице большого объема ( ), и, что еще хуже, нет никакой гарантии, что в ней не будет дублирующихся строк. Следовательно, мало того, что эта формула будет выполняться медленно, она может выдавать неправильные результаты. Это не значит, что вложенные итерационные функции использовать не следует. Есть масса сценариев, в  которых эта концепция вполне применима. И в оставшейся части главы мы приведем целый ряд примеров с уместным использованием вложенных итераторов.

Использование преобразования контекста в итераторах Вычисление может потребовать задействования вложенных итерационных функций – например, когда необходимо рассчитать значение меры в разных контекстах. И  в  этих случаях на помощь приходит преобразование контекста, позволяющее писать лаконичный и эффективный код для действительно сложных вычислений. Рассмотрим меру, подсчитывающую максимальные дневные продажи за определенный период времени. Описание меры очень важно, поскольку помогает сразу определиться с  ее гранулярностью. Чтобы решить задачу, нам необходимо сначала посчитать дневные продажи за период, а  затем найти максимальное значение из полученного ряда. Можно предположить, что нам понадобится таблица, в которой будут собраны дневные продажи и по которой мы будем впоследствии искать максимум. Но в DAX нет необходимости строить такую таблицу. Вместо этого можно обратиться за помощью к итерационным функциям, которые способны решить эту задачу без обращения к вспомогательным таблицам. Алгоритм решения задачи будет следующим: „ проходим по таблице ; „ рассчитываем сумму дневных продаж за день; „ находим максимум среди значений, полученных на предыдущем шаге. Можно написать подобную меру следующим образом: Max Daily Sales 1 := MAXX ( 'Date; VAR DailyTransactions = RELATEDTABLE ( Sales ) VAR DailySales = SUMX ( DailyTransactions; Sales[Quantity] * Sales[Net Price] ) RETURN DailySales )

а ота с итератора и и

нк ие

223

Но лучше будет применить подход, в котором используется неявное преобразование контекста с мерой : Sales Amount := SUMX ( Sales; Sales[Quantity] * Sales[Net Price] ) Max Daily Sales 2 := MAXX ( 'Date'; [Sales Amount] )

В обоих случаях мы имеем дело с вложенными итераторами. Внешние итерации запускаются по таблице , в которой должно быть не больше нескольких сотен записей. Более того, каждая строка в этой таблице уникальна. Так что обе меры выполнятся безопасно и быстро. При этом первая версия получилась более многословной, поскольку в  ней пошагово выполняется весь алгоритм. Во второй версии скрываются многие детали, что делает код более легким для восприятия, а преобразование контекста незаметно переносит фильтр с таблицы на . На рис.  7.1 представлен отчет с  максимальными дневными продажами по месяцам.

Рис 7 1

вод ер Max Daily Sales о года и ес

а

Воспользовавшись преобразованием контекста, можно сделать код более элегантным и  интуитивно понятным. Единственное, чего стоит опасаться в случае с использованием преобразования контекста, – это снижения производительности вычисления:  не стоит обращаться к  мерам внутри объемных итераторов. При просмотре отчета с  рис. 7.1 возникает логичный вопрос: а  в  какой именно день каждого месяца продажи достигали максимума? Например, из 224

а ота с итератора и и

нк ие

отчета ясно, что в какой-то день в январе 2007 года было продано товаров на 92  244,07 доллара. Но в  какой день конкретно? Итераторы в  связке с  преобразованием контекста являют собой достаточно мощный инструмент, чтобы ответить и на этот вопрос. Взгляните на следующий код: Date of Max := VAR MaxDailySales = [Max Daily Sales] VAR DatesWithMax = FILTER ( VALUES ( 'Date'[Date] ); [Sales Amount] = MaxDailySales ) VAR Result = IF ( COUNTROWS ( DatesWithMax ) = 1; DatesWithMax; BLANK () ) RETURN Result

Сначала мы сохраняем значение меры в переменную . Затем создаем временную таблицу с датами, в которые продажи равнялись значению переменной . Если такая дата была одна, фильтр возвратит единственную строку. Если же дат было несколько, возвращаем пустое значение, оповещающее о том, что конкретную дату определить не удалось. Результат вывода можно видеть на рис. 7.2.

Рис 7 2 Мера Date of Max оказ вает дат с акси ально дневно родаже

Использование итераторов в DAX требует, чтобы вы определились со следующими составляющими алгоритма и ровно в таком порядке: „ гранулярность, на которой вы хотите произвести вычисление; „ выражение для вычисления на этом уровне гранулярности; „ тип агрегации. а ота с итератора и и

нк ие

225

В предыдущем примере ( ) гранулярность была определена на уровне даты, в качестве выражения была выбрана сумма продаж, а типом агрегации явилась функция . В итоге мы получили максимальные дневные продажи. Существует множество сценариев, где такой шаблон может быть полезен. Еще один пример – подсчет средней суммы продаж по покупателю. Если думать об этой задаче категориями, описанными выше, получится такая последовательность: гранулярность  – отдельный покупатель, выражение  – сумма продаж, тип агрегации – . В результате четкого следования этому мыслительному процессу мы пришли к простой и понятной формуле: Avg Sales by Customer := AVERAGEX ( Customer; [Sales Amount] )

Эту меру вполне можно использовать в отчетах вроде того, что показан на рис. 7.3, где выводятся средние продажи по покупателям в разрезе континентов и лет.

Рис 7 3

вод ер Avg Sales by Customer о континента и года

Преобразование контекста в итерационных функциях – довольно мощный инструмент. Но использование этой концепции способно снизить производительность вычислений. Чтобы этого не происходило, необходимо уделять внимание кратности внешнего итератора в формуле. Это позволит вам писать более эффективный код на DAX.

Использование функции C

CATE ATEX

В данном разделе мы покажем вариант использования функции для отображения значений фильтров, выбранных пользователем в отчете. Представьте, что вы строите простую визуализацию по продажам в  разрезе континентов и лет, а затем встраиваете ее в сложный отчет, где пользователь может выбрать срез по цветам товаров. Сам элемент фильтра при этом может находиться как рядом с визуализацией, так и на другой странице. Если фильтр расположен на соседней странице, при просмотре отчета не понятно, сформирован он по товарам всех цветов или каких-то отдельных. В этом случае полезно будет добавить метку в отчет, в текстовом виде отображающую сделанный пользователем выбор, как показано на рис. 7.4. Просмотреть выбранные пользователем цвета можно при помощи функции . Функция пригодится, чтобы сконвертировать полученную таблицу в строку. Взгляните на определение меры , кото226

а ота с итератора и и

нк ие

рую мы использовали для вывода пользовательского выбора цветов в отчете, показанном на рис. 7.4: Selected Colors := "Showing " & CONCATENATEX ( VALUES ( 'Product'[Color] ); 'Product'[Color]; ", "; 'Product'[Color]; ASC ) & " colors."

Рис 7 4

Метка вниз от ета оказ вает тек

и в

ор ользовател

Функция проходит по списку цветов и  составляет из них строку с разделителем в виде запятой. Как видите, у этой функции много параметров. Как обычно, первые два представляют таблицу для сканирования и вычисляемое выражение. В третьем параметре передается символ разделителя, а в четвертом и пятом – поле для сортировки и ее направление ( или ). Единственным минусом этой меры является то, что в  случае отсутствия пользовательского выбора будет выведен длинный список из всех цветов в модели. К тому же если пользователь выберет больше пяти цветов, строка окажется слишком длинной, и всю ее поместить в отчет не удастся. Мы можем решить эти проблемы, дополнив нашу меру: Selected Colors := VAR Colors = VALUES ( 'Product'[Color] ) VAR NumOfColors = COUNTROWS ( Colors ) VAR NumOfAllColors = COUNTROWS ( ALL ( 'Product'[Color] ) ) VAR AllColorsSelected = NumOfColors = NumOfAllColors VAR SelectedColors = CONCATENATEX ( Colors; 'Product'[Color]; ", ";

а ота с итератора и и

нк ие

227

'Product'[Color]; ASC ) VAR Result = IF ( AllColorsSelected; "Showing all colors."; IF ( NumOfColors > 5; "More than 5 colors selected, see slicer page for details."; "Showing " & SelectedColors & " colors." ) ) RETURN Result

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

Рис 7 5

зависи ости от в

ора ользовател

етка оказ вает разн е соо

ени

Но и  последняя версия нашей меры не идеальна. В  случае если пользователь, к примеру, выберет пять цветов, но в текущем выборе будет представлено только четыре цвета из-за сокрытия некоторых цветов другими фильтрами, в  мере выведется неполный список, в  него будут включены только присутствующие цвета. В главе 10 мы еще поработаем с данной мерой и решим эту проблему. Чтобы написать окончательную версию меры, нам сначала нужно познакомиться с новыми функциями для исследования содержимого текущего контекста фильтра.

Итераторы, возвра а

ие таблицы

До сих пор мы работали только с итерационными функциями, агрегирующими значения. Но есть итераторы, возвращающие таблицы, полученные путем объ228

а ота с итератора и и

нк ие

единения исходной таблицы с выражениями, вычисленными в контексте строки итерации. И две из них – и   – представляют для нас большой интерес. О них мы и расскажем в данном разделе. Как ясно из названия, функция добавляет столбцы к табличному выражению, переданному в  качестве первого параметра. Для каждого добавляемого столбца функции необходимо знать его название и определяющее его выражение. Например, мы можем добавить два столбца к списку цветов, отображающих количество товаров этого цвета и сумму продаж по товарам этого цвета: Colors = ADDCOLUMNS ( VALUES ( 'Product'[Color] ); "Products"; CALCULATE ( COUNTROWS ( 'Product' ) ); "Sales Amount"; [Sales Amount] )

Результатом данного выражения будет таблица, состоящая из трех столбцов: цвета товара, полученного из значений столбца Product[Color], и  двух новых столбцов, добавленных функцией , как видно по рис. 7.6.

Рис 7 6 Стол до авлен и расс итан

и нк ие ADDCOLUMNS

Функция возвращает все столбцы из исходной таблицы, по которой осуществляет итерации, добавляя при этом новые. А чтобы из исходной таблицы взять только определенный набор столбцов, можно использовать функцию , возвращающую лишь запрошенные столбцы. Например, мы можем переписать предыдущую формулу следующим образом: Colors = SELECTCOLUMNS ( VALUES ( 'Product'[Color] );

а ота с итератора и и

нк ие

229

"Color"; 'Product'[Color]; "Products"; CALCULATE ( COUNTROWS ( 'Product' ) ); "Sales Amount"; [Sales Amount] )

Результат будет таким же, но в этом случае необходимо указать столбец явным образом. Функция бывает полезна, когда нужно сократить количество столбцов в таблице, часто являющейся результатом промежуточных вычислений. Функции и  могут быть удобны при создании новых таблиц, как было показано в  нашем первом примере. Также они часто применяются при создании мер, чтобы сделать код более быстрым и понятным. Взгляните на меру, вычисляющую максимальные дневные продажи, с которой мы уже встречались ранее в данной главе: Max Daily Sales := MAXX ( 'Date'; [Sales Amount] ) Date of Max := VAR MaxDailySales = [Max Daily Sales] VAR DatesWithMax = FILTER ( VALUES ( 'Date'[Date] ); [Sales Amount] = MaxDailySales ) VAR Result = IF ( COUNTROWS ( DatesWithMax ) = 1; DatesWithMax; BLANK () ) RETURN Result

Если внимательно вчитаться в код, можно заметить, что он далеко не так оптимален с точки зрения производительности. Фактически для вычисления переменной DAX необходимо подсчитывать дневные продажи товаров, чтобы найти максимум. В  процессе вычисления второй переменной движку приходится снова рассчитывать дневные продажи для поиска дат с  максимальными показателями. Таким образом, мы дважды проходим по таблице и каждый раз вычисляем сумму продажи за каждый день. Теоретически оптимизатор DAX достаточно продвинут, чтобы понять, что можно вычислять дневные продажи лишь раз, а затем использовать уже вычисленное ранее значение, но никто не может гарантировать, что так и будет. Воспользовавшись функцией , мы можем написать более быстрый код для этой меры. Мы сделаем это, предварительно подготовив таблицу с дневными продажами и  сохранив ее в  переменную. Затем мы используем эти данные 230

а ота с итератора и и

нк ие

для вычисления значения максимальной дневной продажи и дня, когда это произошло: Date of Max := VAR DailySales = ADDCOLUMNS ( VALUES ( 'Date'[Date] ); "Daily Sales"; [Sales Amount] ) VAR MaxDailySales = MAXX ( DailySales; [Daily Sales] ) VAR DatesWithMax = SELECTCOLUMNS ( FILTER ( DailySales; [Daily Sales] = MaxDailySales ); "Date"; 'Date'[Date] ) VAR Result = IF ( COUNTROWS ( DatesWithMax ) = 1; DatesWithMax; BLANK () ) RETURN Result

Алгоритм работы данного кода похож на предыдущий, за исключением некоторых деталей: „ переменная содержит таблицу с  датами и  суммами продаж в эти даты. Эта таблица – результат работы функции ; „ переменная больше не вычисляет дневные продажи. Вместо этого она сканирует предварительно вычисленную таблицу в  переменной , что положительно отражается на времени выполнения формулы; „ то же самое происходит и в случае с  , которая сканирует таблицу в переменной . А поскольку после этого нам нужны будут только даты, а не дневные продажи, мы воспользовались функцией для исключения столбца с  дневными продажами из результата. Итоговая версия кода получилась более сложной по сравнению с первоначальной. Но простотой формул часто приходится жертвовать во время оптимизации кода. Чтобы сделать код более быстрым, приходится писать более сложные конструкции. В главах 12 и 13 мы детальнее обсудим работу функций и  . А поговорить есть о чем, особенно если вы хотите использовать результат функции в итераторе с дальнейшим преобразованием контекста. а ота с итератора и и

нк ие

231

Решение распространенных сценариев при помо и итераторов В данном разделе мы продолжим работать с примерами, в которых используются уже известные вам итерационные функции, а также познакомимся с еще одним полезным итератором – . Вы научитесь рассчитывать скользящее среднее и  почувствуете разницу между использованием для этих целей итератора и обычной арифметической операции. Позже в этом разделе мы дадим полное определение функции , полезной при расчете рангов, основываясь на выражениях.

Расчет среднего и скользя его среднего Вы можете рассчитать среднее значение по набору данных, воспользовавшись одной из следующих функций языка DAX: „ A A : возвращает среднее значение по столбцу с числами; „ A A X: рассчитывает среднее значение по выражениям, вычисленным в таблице. Примечание есть е е одна нк и дл рас ета средни зна ени AVERAGEA, она возвра ает среднее о ислов зна ени из текстового стол а о ва не след ет ее ис ользовать нк и AVERAGEA рис тств ет в только дл сов ести ости с ро ле а с то нк ие закл аетс в то , то когда в ис ольз ете в ка естве ее ара етра текстов стол е , даже не таетс рео разовать текстов е зна ени в исла, как то делает есто того он дет в давать н ли ак то та нк и вл етс а сол тно ес олезно нк и AVERAGE в тако сит а ии вернет о и к , деонстрир невоз ожность в ислени средни зна ени о текстов данн

Ранее в данной главе мы уже рассказывали про расчет средних значений по таблице. В этом разделе мы пойдем чуть дальше и рассмотрим метод расчета скользящего среднего. Допустим, вам необходимо проанализировать дневные продажи в  базе данных Contoso. Если просто построить график по дневным продажам за период, понять по нему что-то будет сложно из-за больших отклонений, как видно по рис. 7.7. Чтобы сгладить линию графика, обычно используется техника расчета средних значений за определенное количество дней раньше текущего. В  нашем примере мы будем вычислять среднее по 30 последним дням. Таким образом, в каждой точке на графике будет показано усредненное значение по продажам за предыдущие 30 дней. Этот метод поможет убрать пики с графика и облегчит понимание тренда. Приведем формулу расчета среднего за последние 30 дней: AvgXSales30 := VAR LastVisibleDate = MAX ( 'Date'[Date] ) VAR NumberOfDays = 30 VAR PeriodToUse =

232

а ота с итератора и и

нк ие

FILTER ( ALL ( 'Date' ); AND ( 'Date'[Date] > LastVisibleDate - NumberOfDays; 'Date'[Date] LastVisibleDate - NumberOfDays; 'Date'[Date] 0; [NumOfWorkingDays] ) ) VAR Result = DIVIDE ( [Sales Amount]; WorkingDays ) RETURN Result

Рис 7 20

о ес

а зна ени

равильн е, но к итога есть оль ие во рос

Новая мера позволила нам выправить значения на уровне годов, как видно по рис. 7.21, но она по-прежнему далека от идеала. Выполняя вычисления на разных уровнях гранулярности, необходимо обеспечить их правильность. Функция проходит по столбцу с месяцами с января по декабрь. На уровне годов все теперь считается правильно, но с  итоговым значением остались проблемы, как видно по рис. 7.22. Когда в контексте фильтра присутствует год, итерации по месяцам работают правильно, поскольку после преобразования контекста в новом контексте фильтра оказывается как месяц, так и год. Однако в итоговой строке год в кона ота с итератора и и

нк ие

245

тексте фильтра отсутствует. Соответственно, в фильтре остается только месяц, и формула вычисляется не по этому месяцу в рамках текущего года, а по этому месяцу в рамках всех лет.

Рис 7 21 зна ени

с ользование итера ионно родаж на ровне годов

нк ии озволило скорректировать

Рис 7 22 о каждо год зна ени рев а т 000, а в строке о его итога види сильно заниженное исло

Иными словами, проблема заключается в  том, что мы осуществляем итерации только по столбцу с  месяцами. Правильной гранулярностью в  итерации будет не месяц, а месяц вместе с годом. И лучшим вариантом здесь будет создание столбца, в котором будет храниться месяц с годом. В нашей модели данных такой столбец есть, и  он называется . Таким образом, чтобы исправить формулу, достаточно изменить столбец для итераций следующим образом: SalesPerWorkingDay := VAR WorkingDays = SUMX ( VALUES ( 'Date'[Calendar Year Month] ); IF ( [Sales Amount] > 0;

246

а ота с итератора и и

нк ие

[NumOfWorkingDays] ) ) VAR Result = DIVIDE ( [Sales Amount]; WorkingDays ) RETURN Result

Финальная версия кода работает правильно, поскольку считает значение для итогов на правильном уровне гранулярности. Результат можно видеть на рис. 7.23.

Рис 7 23 исление ор л на равильно ровне гран л рности озволило ривести в ор док итогов е зна ени

Закл чение Как обычно, подведем итоги того, что мы узнали из этой главы: „ итерационные функции являются важнейшей составляющей DAX, и чем больше вы будете использовать этот язык, тем чаще вам придется с ними сталкиваться; „ в DAX присутствуют два вида итераций: первый из них применяется для простых последовательных вычислений строка за строкой, а второй использует технику преобразования контекста. В мере , которую мы часто применяем в данной книге, происходит построчное перемножение количества на цену. В  этой главе мы также познакомились с  итераторами, использующими преобразование контекста. Они представляют собой очень мощный инструмент для проведения более сложных вычислений; „ используя итерационные функции совместно с  преобразованием контекста, необходимо тщательно следить за их кратностью – она должна быть достаточно мала. Кроме того, нужно постараться гарантировать уникальность строк в  таблице, по которой осуществляются итерации. В  противном случае вы рискуете, что код будет выполняться медленно и с ошибками; „ работая со средними значениями в  отношении дат, дважды подумайте о том, подходят ли для ваших целей итерационные функции. Например, а ота с итератора и и

нк ие

247

функция не учитывает в процессе вычисления пустые значения, а при работе с датами это может оказаться не всегда верно. Так что тщательно продумывайте свой сценарий – каждый случай уникален; „ итераторы могут оказаться полезными при расчете значений на разных уровнях гранулярности, как вы видели в  последнем примере. Работая с разными гранулярностями, очень важно проверять все расчеты, чтобы они выполнялись на правильном уровне. В оставшейся части книги вы еще не раз встретитесь с  итерационными функциями. Уже в следующей главе, в которой мы будем говорить про логику операций со временем, вы увидите множество расчетов, большинство из которых основывается на итерациях.

ГЛ А В А 8

Логика операций со временем Практически в каждой модели данных так или иначе будет присутствовать логика операций со временем. DAX предлагает множество функций для упрощения таких расчетов, и вы можете использовать их с пользой, если ваша модель данных удовлетворяет определенным требованиям. Если же у вас очень специфическая модель в отношении работы с датами и временем, вы всегда можете написать свои функции, отвечающие особенностям вашего бизнеса. Из этой главы вы узнаете, как средствами DAX реализовать распространенные приемы работы со временем, среди которых расчет сумм нарастающим итогом с начала года, сравнение сопоставимых периодов разных лет и другие вычисления, в том числе опирающиеся на неаддитивн е (non-additive) и  о луаддитивн е (semi-additive) меры. Вы научитесь использовать специальные функции DAX для работы со временем, а также познакомитесь со специфичными методами для создания нестандартных календарей и расчетов на основе недель.

Введение в логику операций со временем Обычно в  любой модели данных присутствует таблица с  датами или календарь. Фактически, осуществляя срезы в отчетах по году или месяцу, лучше всего пользоваться столбцами из таблицы, специально предназначенной для работы с датами и временем. Использовать для этих целей вычисляемые столбцы с извлеченными частями дат из полей типа или  – менее предпочтительный вариант. Этот выбор обусловлен сразу несколькими причинами. Использование таблицы с датами делает модель более простой и понятной для навигации. Кроме того, у вас появляется возможность пользоваться специальными функциями DAX для работы с логикой операций со временем. По сути, для корректной работы большинству подобных функций DAX требуется наличие отдельной таблицы с датами. В случае если в модели данных присутствует сразу несколько полей с датами, например если есть даты заказа и даты поставки, у вас есть выбор: либо поддерживать несколько связей с единой таблицей дат, либо создать несколько календарей. Модели данных в обоих вариантах будут отличаться, как и сами расчеты. Позже в данной главе мы поговорим про этот нелегкий выбор более подробно. Так или иначе, если в вашей модели присутствуют столбцы с датами, без создания как минимум одной таблицы дат вам будет не обойтись. Power BI и Power огика о ера и со вре ене

249

Pivot для Excel предлагают свои возможности для автоматического создания таблиц и столбцов для работы с датами, тогда как в Analysis Services отсутствуют специальные средства для работы с датами и временем. При этом стоит признать, что реализация этих особенностей не лучшим образом сочетается с содержанием единой таблицы с датами в модели данных. Кроме того, эти средства обладают рядом ограничений, так что лучше создавать календари в модели самостоятельно. В следующих разделах мы расскажем об этом подробнее.

Автоматические дата и время в Power BI В Power BI есть настройка автомати ески дат и времени, располагающаяся в секции агру ка данн (Data Load) меню Параметр и настройки (Options). Окно настроек показано на рис. 8.1.

Рис 8 1 ново одели данн вкл ен о ол ани

нкт Автоматические дата и время

Когда эта настройка включена (по умолчанию), Power BI автоматически создает отдельную таблицу для каждого столбца типа или в модели данных. Здесь и далее мы будем называть такое поле стол ом с датой (date column). Создание вспомогательных таблиц позволяет автоматически выполнять фильтрацию в таких столбцах по году, кварталу, месяцу и дню. Подобные таблицы невидимы для пользователя и недоступны для редактирования. При 250

огика о ера и со вре ене

подключении к модели данных Power BI Desktop посредством DAX Studio эти таблицы становятся видимыми для разработчиков. У настройки автоматической даты и времени есть два существенных недостатка: „ Power BI Desktop создает по отдельной таблице для каждого столбца с датой. Это приводит к  образованию большого количества не связанных между собой таблиц в модели. В связи с этим создание простого отчета с выводом заказанных и проданных товаров в одной матрице становится настоящим вызовом; „ эти таблицы скрыты и не могут быть изменены разработчиком. Соответственно, можете даже не надеяться добавить в них, к примеру, день недели. Совсем скоро вы научитесь создавать собственные удобные таблицы дат, которые дадут вам полную свободу. К тому же это вопрос всего нескольких строчек кода на DAX. Позволить модели данных нарушить правила хорошего тона в  моделировании только ради того, чтобы вы сэкономили пару минут на ее создание, – не лучший выбор.

Автоматические столбцы с датами в Power Pivot для Excel В Power Pivot для Excel также есть возможность автоматически создавать структур о лег а ие ра оту с датами. Но тут она реализована еще хуже, чем в Power BI. Фактически, когда вы используете столбец с датами в сводной таблице, Power Pivot создает набор вычисляемых столбцов в той же таблице. Таким образом, в  таблице сами собой появляются дополнительные столбцы с годом, названием месяца, кварталом и номером месяца, необходимым для сортировки. В сумме четыре новых столбца в таблице. Плохо то, что здесь унаследованы все недостатки Power BI и добавлены свои. Если в вашей таблице несколько столбцов с датами, количество вспомогательных колонок начнет неумолимо расти. Нет никакой возможности использовать одни и те же поля для фильтрации разных дат, как в Power BI. И наконец, если столбец с датой присутствует в таблице с  миллионами строк, что часто бывает, эти вычисляемые столбцы существенно увеличивают размер файла модели и объем используемой ей памяти. Эту особенность в Excel можно отключить на странице с настройками, как показано на рис. 8.2.

аблон таблицы дат в Power Pivot для Excel Excel предлагает еще один инструмент, который работает лучше, чем ранее описанная особенность. Начиная с 2017 года в Power Pivot для Excel есть возможность создания та ли дат (date table) на панели инструментов Power Pivot (на вкладке Конструктор), как показано на рис. 8.3. Нажатие на пункт о ать (New) в раскрывающемся списке кнопки аблиа ат (D ) приведет к  созданию новой таблицы в  модели данных с набором вычисляемых столбцов, включающих год, месяц и день недели. Вам останется только правильно настроить связи между таблицами. Также у  вас огика о ера и со вре ене

251

есть возможность изменить названия и формулы вычисляемых столбцов и добавить новые.

Рис 8 2 настро ка есть воз ожность откл стол ов дат и вре ени в сводн та ли а

Рис 8 3

252

дл

ить авто ати еское гр

ожно создать та ли

огика о ера и со вре ене

дат р

ирование

о из анели инстр

ентов

Кроме того, вы можете сохранить существующую таблицу как шаблон, который может быть использован в  будущем при создании других таблиц дат. В целом эта техника работает нормально. Таблица дат, созданная при помощи Power Pivot, является обычной таблицей и отвечает всем требованиям к календарю. Учитывая тот факт, что Power Pivot для Excel не поддерживает вычисляемые таблицы, можно назвать эту возможность крайне полезной.

Создание таблицы дат Как вы уже знаете, первым шагом на пути создания вычислений с использованием дат является создание соответствующей таблицы с  календарем. Это очень важная таблица в модели данных, и к ее созданию необходимо подходить довольно тщательно. В данном разделе мы подробно обсудим все тонкости создания таблицы дат. Двумя главными особенностями при работе с такими таблицами являются технический аспект и аспект моделирования данных. С технической точки зрения таблицы дат должны отвечать следующим требованиям: „ таблица дат обязана включать в  себя все даты, входящие в  аналитический период. Например, если самой ранней датой в таблице является 3 июля 2016 года, а самой поздней – 27 июля 2019-го, диапазон дат в календаре должен начинаться 1 января 2016 года и заканчиваться 31 декабря 2019-го. Иными словами, в календаре должны полностью присутствовать все годы, в которые осуществлялись продажи. При этом между датами не должно быть пропусков – все без исключения даты должны присутствовать в  таблице вне зависимости от того, были транзакции в этот день или нет; „ таблица дат должна содержать один столбец типа с уникальными значениями. При этом тип наиболее предпочтителен, поскольку гарантирует отсутствие хранения времени. Если столбец содержит часть, отвечающую за время, то все эти части должны быть идентичными во всей таблице; „ совсем не обязательно, чтобы связь между таблицей Sales и календарем была основана на поле с типом . Эти таблицы вполне могут быть связаны по полю с целочисленным типом, но при этом столбец с типом должен присутствовать; „ календарь должен быть помечен в модели данных как таблица дат. И хотя это не строго обязательно, так вам будет проще писать код. Мы поговорим об этой особенности далее в данной главе. Важно ови ки о но склонн создавать огро н та ли дат с гораздо оль и коли ество лет, е нео оди о то о и ка а ри ер, ожно за олнить календарь все и года и на ина с 1 00 го о 2100 росто на вс ки сл а исто те ни ески така та ли а дат ра отать дет, но к ее ективности в в ислени не ре енно возникн т во рос е, то в календаре содержались только те год , дл котор в одели с еств т транзак ии

огика о ера и со вре ене

253

С точки зрения теории достаточно, чтобы в таблице дат содержался всего один столбец с этими самыми датами. Но пользователю обычно требуется анализировать данные по годам, месяцам, кварталам, дням недели и многим другим атрибутам. Соответственно, идеальная таблица дат должна быть дополнена вспомогательными столбцами, которые, хоть и не используются движком, значительно облегчат жизнь пользователю. Если вы загружаете календарь из существующего внешнего источника, вполне вероятно, что все необходимые столбцы там уже присутствуют. Если необходимо, дополнительные колонки можно создать при помощи вычисляемых столбцов или подкорректировав запрос к источнику. Всегда более предпочтительно поработать с внешним источником при помощи запросов, чем создавать вычисляемые столбцы в модели. Их количество желательно ограничить до предела. Еще одним способом является создание таблицы дат в виде вычисляемой таблицы в DAX. Мы подробно расскажем об этом варианте в следующих разделах, когда будем говорить о функциях и  . Примечание Слово Date вл етс зарезервированн в дл соответств е нк ии DATE ак то ва нео оди о закл ать его в кав ки ри ис ользовании в каестве названи та ли , нес отр на то то оно не содержит ро елов и с е иальн си волов ро е того, в ожете ис ользовать та ли с название Dates в есто Date, то из ежать нео оди ости всегда о нить о кав ка о не стоит за вать о рее ственности в и еновании о ектов в одели данн сли др гие та ли в и ен ете в единственно исле, то и с та ли е дат желательно ридерживатьс такого од ода

Использование функций CALE DAR и CALE DARAUT Если в  вашем источнике данных отсутствует таблица дат, вы всегда можете создать ее самостоятельно при помощи функций и  . Обе эти функции возвращают таблицу, состоящую из одного столбца типа . И если функция требует задания нижней и верхней границ предполагаемого интервала дат, то просто сканирует все столбцы с датами в модели данных, находит самую раннюю и самую позднюю даты и заполняет таблицу на основании всех лет между этими значениями. Например, простая таблица дат, учитывающая все возможные годы транзакций из таблицы , может быть построена следующим образом: Date = CALENDAR ( DATE ( YEAR ( MIN ( Sales[Order Date] ) ); 1; 1 ); DATE ( YEAR ( MAX ( Sales[Order Date] ) ); 12; 31 ) )

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

254

огика о ера и со вре ене

Функция сканирует все поля с датами в модели данных, за исключением вычисляемых столбцов. Например, если вы используете функцию для создания таблицы в  модели, в  которой содержатся продажи с 2007 по 2011 год, а в таблице есть также столбец с самой ранней датой в 2004 году, результатом будет интервал с 1 января 2004 года по 31 декабря 2011-го. Если в модели будут и другие столбцы типа дата, они также окажут действие на интервал, генерируемый функцией . Часто бывает, что в календаре оказываются даты, совершенно не нужные для анализа. Например, если среди прочих дат в модели будет присутствовать поле с датами рождения покупателей, функция при создании календаря будет учитывать годы рождения самого пожилого и самого молодого покупателей. В результате мы получим очень объемную таблицу дат, что может негативно сказаться на производительности вычислений. Функция также принимает необязательный параметр, отвечающий за номер последнего месяца финансового года. Если этот параметр передан, функция при создании календаря будет вести отсчет с  первого дня следующего месяца и до последнего дня месяца, номер которого передан в качестве параметра. Это бывает полезно, когда в организации финансовый год заканчивается не 31 декабря, а, скажем, 30 июня, как показано в следующем примере создания календаря: Date = CALENDARAUTO ( 6 )

Функцию использовать легче, чем , поскольку она сама определяет границы календаря. Но при этом может включить в  таблицу нежелательные даты. На этот случай есть возможность ограничить даты, автоматически генерируемые этой функцией, при помощи фильтра следующим образом: Date = VAR MinYear = YEAR ( MIN ( VAR MaxYear = YEAR ( MAX ( RETURN FILTER ( CALENDARAUTO ( ); YEAR ( [Date] ) >= YEAR ( [Date] ) = MinYear && YEAR ( [Date] ) = DATE ( 2007; 1; 1 ); 'Date'[Date]