Uploaded by Саня

Подробное руководство по DAX

advertisement
Марко Руссо и Альберто Феррари
Подробное руководство по DAX:
бизнес-аналитика с Microsoft Power BI,
SQL Server Analysis Services и Excel
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.
Связаться с Марко можно по электронной почте marco.russo@sqlbi.com, а с Альберто – alberto.ferrari@sqlbi.com.
От команды разработчиков
В
ы можете не знать наших имен. Мы проводим дни за написанием кода для
программ, которые вы ежедневно используете в своей работе. Мы – часть
команды разработчиков 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, зайдя на страницу книги и оставив комментарий в разделе «Отзывы и рецензии». Также
можно послать письмо главному редактору по адресу dmkpress@gmail.com; при
этом укажите название книги в теме письма.
Если вы являетесь экспертом в какой-либо области и заинтересованы в написании новой книги, заполните форму на нашем сайте http://dmkpress.com/
authors/publish_book/ или напишите в издательство: dmkpress@gmail.com.
Список опечаток
Хотя мы приняли все возможные меры для того, чтобы обеспечить высокое
качество наших текстов, ошибки все равно случаются. Если вы найдете ошибку
в одной из наших книг – возможно, ошибку в основном тексте или программном коде, – мы будем очень благодарны, если вы сообщите нам о ней. Сделав
это, вы избавите других читателей от недопонимания и поможете нам улучшить последующие издания этой книги.
Если вы найдете какие-либо ошибки в коде, пожалуйста, сообщите о них
главному редактору по адресу dmkpress@gmail.com, и мы исправим это в следующих тиражах.
Нарушение авторских прав
Пиратство в интернете по-прежнему остается насущной проблемой. Издательство «ДМК Пресс» очень серьезно относится к вопросам защиты авторских прав и лицензирования. Если вы столкнетесь в интернете с незаконной
публикацией какой-либо из наших книг, пожалуйста, пришлите нам ссылку на
интернет-ресурс, чтобы мы могли применить санкции.
Ссылку на подозрительные материалы можно прислать по адресу электронной почты dmkpress@gmail.com.
Мы высоко ценим любую помощь по защите наших авторов, благодаря которой мы можем предоставлять вам качественные материалы.
От издательства
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 () + BLANK () = BLANK ()
10 * BLANK () = BLANK ()
BLANK () / 3 = BLANK ()
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 <tableName>[<name>] = <expression> }]
EVALUATE <table>
[ORDER BY {<expression> [{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 ( <table>; <condition> )
Функция
принимает в качестве параметров таблицу и логическое
условие фильтрации. Результатом выполнения этой функции является набор строк из исходной таблицы, удовлетворяющих заданному условию.
Функция
одновременно является и табличной функцией, и итератором. Чтобы вернуть результат, она сканирует таблицу построчно, сверяя
каждую строку с условием. Иными словами, функция
запускает итерации по таблице.
Например, следующее выражение возвращает все товары бренда 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] <= 100
)
Фактически это выражение приводится к следующему:
Sales 10-100 :=
CALCULATE (
[Sales Amount];
FILTER (
ALL ( Sales[Net Price] );
Sales[Net Price] >= 10 && Sales[Net Price] <= 100
)
)
Результирующий контекст фильтра, созданный функцией
, добавляет всего один фильтр на столбец
. Важной особенностью
предикатов, используемых в качестве аргументов фильтров в функции
, является то, что они по своей сути являются таблицами, а не условиями. По первому из предыдущих двух фрагментов кода можно понять, что
функция
оценивает условие. На самом деле она оценивает список
всех значений столбца
, удовлетворяющих условию. После этого
использует эту таблицу значений для осуществления фильтрации
в модели данных.
Два условия, объединенных логическим
, могут быть представлены
как два отдельных фильтра. В действительности предыдущее выражение эквивалентно следующему:
Sales 10-100 :=
CALCULATE (
[Sales Amount];
Sales[Net Price] >= 10;
Sales[Net Price] <= 100
)
нк ии
и
167
При этом стоит иметь в виду, что множественные аргументы фильтра
функции
всегда объединяются посредством логического
.
Так что при необходимости применить объединение фильтров при помощи
логического
вам придется использовать одно условие, как показано ниже:
Sales Blue+Red :=
CALCULATE (
[Sales Amount];
'Product'[Color] = "Red" || 'Product'[Color] = "Blue"
)
Используя множественные условия, вы можете объединять два независимых фильтра в едином контексте фильтра. Следующая мера всегда будет возвращать пустое значение, поскольку не бывает товаров одновременно красного и синего цвета:
Sales Blue and Red :=
CALCULATE (
[Sales Amount];
'Product'[Color] = "Red";
'Product'[Color] = "Blue"
)
Фактически эта мера преобразуется в следующую меру с одним фильтром:
Sales Blue and Red :=
CALCULATE (
[Sales Amount];
'Product'[Color] = "Red" && 'Product'[Color] = "Blue"
)
Аргумент фильтра будет всегда возвращать пустой список цветов, допустимых в контексте фильтра. Следовательно, эта мера всегда будет возвращать
пустое значение.
Всякий раз, когда аргумент фильтра ссылается на один столбец, вы можете
использовать предикат. И мы настоятельно советуем вам делать именно так,
поскольку это позволяет сделать код более легким для восприятия. С условиями, объединенными посредством логического
, следует поступать точно
так же. Но не стоит забывать, что это лишь синтаксический сахар. Функция
работает исключительно с таблицами, даже если компактный синтаксис говорит об обратном.
С другой стороны, если в аргументе фильтра содержатся ссылки на два
столбца и более, вам необходимо использовать функцию
в качестве табличного выражения. О том, как это делать, вы узнаете из следующего раздела.
Фильтрация по сложным условиям
Аргумент фильтра, ссылающийся на множество столбцов, требует явного использования в формуле табличного выражения. И очень важно владеть разными техниками для написания подобных фильтров. Помните, что хорошей
168
нк ии
и
практикой считается использование фильтров с минимальным количеством
столбцов, необходимым для написания предиката.
Представьте, что вам нужно создать меру для агрегации продаж только по
тем транзакциям, сумма которых больше или равна 1000. Чтобы получить сумму продаж по транзакции, нам необходимо перемножить значения столбцов
и
, поскольку мы не храним эти произведения в таблице
базы данных Contoso. Скорее всего, вам захочется написать формулу, подобную следующей, но, увы, работать она не будет:
Sales Large Amount :=
CALCULATE (
[Sales Amount];
Sales[Quantity] * 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 не отфильтрована,
Sales[Quantity] * Sales[Net Price]
-- так что считаются все продажи
)
VAR CurrentSales =
CALCULATE (
-- Происходит преобразование контекста
SUMX (
Sales;
-- Продажи только по одному товару
Sales[Quantity] * Sales[Net Price] -- Здесь мы вычисляем продажи
)
-- по конкретному товару
)
VAR Ratio = 0.01
-- 1 %, выраженный как число
VAR Result =
IF (
CurrentSales >= TotalSales * Ratio;
"High Performance product";
-- Очень популярный товар
"Regular product"
-- Обычный товар
)
RETURN
Result
Вы, наверное, заметили, что между двумя переменными есть лишь одно небольшое различие: переменная
рассчитывается путем осуществления
обычных итераций, а в
тот же самый код 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] = <CurrentValueOfProductKey>;
Sales[StoreKey] = <CurrentValueOfStoreKey>;
...;
Sales[Margin] = <CurrentValueOfMargin>
)
Как видите, список столбцов, от которых зависит
, включает в себя
полный набор столбцов таблицы. Поскольку функция
была вызвана в контексте строки, выражение автоматически получает зависимости от
всех без исключения столбцов таблицы, по которой осуществляются итерации.
Это более очевидно в случае с вычисляемым столбцом, в котором контекст
строки присутствует по умолчанию.
Если написать один вычисляемый столбец с использованием функции
, ничего страшного не произойдет. Проблема возникнет, если создать
сразу два вычисляемых столбца в таблице с применением функции
, инициирующей преобразование контекста для обоих столбцов. Так что
попытка создания следующего вычисляемого столбца завершится неудачей:
Sales[NewAllSalesQty] = CALCULATE ( SUM ( Sales[Quantity] ) )
Причина возникновения ошибки в том, что функция
автоматически принимает все столбцы таблицы в качестве аргументов фильтра. А добавление в таблицу нового столбца влияет на определение других столбцов.
Если бы DAX позволил нам создать столбец
, код двух вычисляемых столбцов выглядел бы примерно так:
Sales[AllSalesQty] =
CALCULATE (
SUM ( Sales[Quantity] );
Sales[ProductKey] = <CurrentValueOfProductKey>;
...;
Sales[Margin] = <CurrentValueOfMargin>;
Sales[NewAllSalesQty] = <CurrentValueOfNewAllSalesQty>
)
Sales[NewAllSalesQty] =
CALCULATE (
SUM ( Sales[Quantity] );
Sales[ProductKey] = <CurrentValueOfProductKey>;
...;
Sales[Margin] = <CurrentValueOfMargin>;
Sales[AllSalesQty] = <CurrentValueOfAllSalesQty>
)
Как видите, две выделенные строки ссылаются друг на друга. Получается,
что столбец
зависит от значения столбца
, который,
в свою очередь, находится в зависимости от
. В результате мы получаем циклическую зависимость. DAX обнаруживает ее и запрещает нам сохранять код, ведущий к ее образованию.
192
нк ии
и
Обнаружить эту проблему бывает не так просто, но решается она легко. Если
в таблице, в которой создаются вычисляемые столбцы с функцией
,
присутствует столбец с уникальными записями и DAX знает об этом, при преобразовании контекста будет фильтроваться значение только этого столбца.
Представьте, что мы создали следующий вычисляемый столбец в таблице
:
'Product'[ProductSales] = CALCULATE ( SUM ( Sales[Quantity] ) )
В данном случае нет никакой необходимости добавлять все столбцы в качестве аргументов фильтра. В таблице
есть столбец
, содержащий уникальные значения. И DAX знает о существовании этого столбца, поскольку таблица
находится в связи с
на стороне «один». А значит,
во время преобразования контекста движок не будет добавлять фильтр для
каждого столбца таблицы. Таким образом, код может преобразоваться в подобный:
'Product'[ProductSales] =
CALCULATE (
SUM ( Sales[Quantity] );
'Product'[ProductKey] = <CurrentValueOfProductKey>
)
Как видите, вычисляемый столбец
в таблице
зависит исключительно от поля
. В этом случае вы можете создавать множество
вычисляемых столбцов в данной таблице – каждое из них будет зависеть только от ключевого столбца.
Примечание
оследнее за е ание о
нк ии 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 := SUMX ( Sales; Sales[Quantity] * Sales[Net Price] )
TotalCost := SUMX ( Sales; Sales[Quantity] * Sales[Unit Cost] )
Margin
:= [SalesAmount] – [TotalCost]
Margin%
:= 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
)
)
VAR Result =
CALCULATE (
AVERAGEX ( 'Date'; [Sales Amount] );
PeriodToUse
)
RETURN
Result
Рис 7 7
нализировать гра ик дневн
родаж о ень ро ле ати но
Сначала в формуле определяется последний видимый день. Поскольку контекст фильтра в визуализации установлен на уровне даты, мы получим выбранную дату. После этого мы определяем 30-дневный набор дат ранее последней
даты. На заключительном шаге мы используем полученный период в качестве
фильтра функции
, чтобы вложенная в нее функция
вычисляла среднее значение за эти даты.
Результат данного вычисления показан на рис. 7.8. Как видите, линия на графике оказалась куда более плавной по сравнению с дневным графиком, что
позволяет анализировать тенденции по продажам.
Когда пользователь полагается на функции вычисления средних значений
вроде
, нужно с большой осторожностью относиться к полученному
результату. Фактически при расчете среднего DAX игнорирует пустые значеа ота с итератора и и
нк ие
233
ния. Если в какой-то день из выбранного периода продаж не было, этот день не
будет учтен вовсе при расчете среднего. Об этой особенности нужно помнить.
Функция
не подразумевает использование нуля в случае отсутствующего значения. И такое поведение может быть нежелательным при расчете
средних показателей по дням.
Рис 7 8
Скольз
ее среднее за оследние 0 дне дает олее лавн
гра ик
Если вам необходимо дни с отсутствующими продажами учитывать как
нулевые, то лучше будет использовать обычную функцию деления вместо
. Кроме того, этот метод будет быстрее, поскольку преобразование
контекста, возникающее в случае использования функции
, требует больше памяти и времени для выполнения. Посмотрите на такой вариант
вычисления скользящего среднего, в котором единственное изменение было
сделано внутри функции
:
AvgXSales30 :=
VAR LastVisibleDate = MAX ( 'Date'[Date] )
VAR NumberOfDays = 30
VAR PeriodToUse =
FILTER (
ALL ( 'Date' );
AND (
'Date'[Date] > LastVisibleDate - NumberOfDays;
'Date'[Date] <= LastVisibleDate
)
)
VAR Result =
CALCULATE (
234
а ота с итератора и и
нк ие
DIVIDE ( [Sales Amount], COUNTROWS ( 'Date' ) );
PeriodToUse
)
RETURN
Result
Здесь мы не пользуемся функцией AVERAGEX для подсчета средних значений, а значит, дни с отсутствием продаж будут учитываться как нулевые. Это
изменение отразится на графике, но совсем незначительно. К тому же значения на новом графике могут быть меньше предыдущих, но не больше, поскольку знаменатель в формуле будет время от времени превышать предыдущие
значения, что видно по рис. 7.9.
Как часто бывает в бизнес-аналитике, в данном случае нельзя одно из решений считать однозначно лучше другого. Все зависит от требований к отчетности. DAX предлагает самые разные способы достижения результата, и лишь вам
решать, каким из них воспользоваться. Например, в новой мере использование функции
позволило учитывать дни без продаж как нулевые,
но в то же время в учет попали выходные и праздничные дни. Правильно ли
это – зависит от ваших требований к отчетности, и при необходимости вы можете легко переписать меру, чтобы она учитывала специфику бизнеса.
Рис 7 9
азн е етод рас ета скольз
его среднего ривели к разн
Использование функции RA
рез льтата
X
Функция
используется для вычисления ранга элементов согласно указанному типу сортировки. Типичным примером применения функции
является ранжирование товаров или покупателей на основании объемов проа ота с итератора и и
нк ие
235
даж.
принимает несколько параметров, но в большинстве случаев используется только два из них. Остальные параметры необязательные и применяются довольно редко.
Представьте, что вам необходимо построить отчет по категориям товаров
с ранжированием по объему продаж, показанный на рис. 7.10.
Рис 7 10 Мера Rank Cat on Sales оказ вает ранг категории,
ис од из о е а родаж
В этом сценарии можно использовать функцию
. Эта функция относится к разряду итерационных и является предельно простой в применении.
В то же время ее использование может быть сопряжено с определенными трудностями, которые требуют более детального пояснения.
Код меры
представлен ниже:
Rank Cat on Sales :=
RANKX (
ALL ( 'Product'[Category] );
[Sales Amount]
)
Функция
выполняется в три этапа.
. Функция
создает та ли у оиска (lookup table) в процессе сканирования исходной таблицы, переданной в качестве первого параметра.
Во время итераций происходит вычисление выражения из второго параметра в контексте строки итерации. После создания таблицы поиска
выполняется ее сортировка.
2. Функция
вычисляет выражение, переданное вторым параметром, в исходном контексте вычисления.
3. Функция
возвращает позицию значения, вычисленного на втором шаге, в отсортированной таблице поиска.
Алгоритм работы функции показан на рис. 7.11 на примере ранжирования
категории «Cameras and camcorders» по мере
.
Теперь рассмотрим схему работы функции
в показанном выше примере подробно:
„ в процессе итераций по исходной таблице строится таблица поиска.
В данном случае мы использовали табличное выражение ALL ( 'Product'
[Category] ), чтобы проигнорировать текущий контекст фильтра. Ина236
а ота с итератора и и
нк ие
че таблица поиска состояла бы только из одной строки с текущей категорией;
„ значение меры
отличается для каждой категории по причине преобразования контекста. При использовании итерационной
функции образуется контекст строки. А поскольку выражением для вычисления является мера, неявным образом содержащая в себе функцию
, DAX запустит преобразование контекста и рассчитает меру
только для одной текущей категории;
„ в таблице поиска будут содержаться исключительно значения выражения. Ссылки на категории здесь не нужны, поскольку ранжирование выполняется только по значениям, если таблица отсортирована правильно;
„ рассчитывается значение меры
за пределами итераций –
в исходном контексте вычисления. Изначально контекст фильтра включал в себя только категорию Cameras and camcorders. Таким образом, на
этом шаге будет вычислено значение меры по этой категории товаров;
„ значение 2 является результатом поиска рассчитанного значения меры
в отсортированной таблице поиска.
аг 1
а ли а
оиска
аг 2
на ение
аг
ози и
ез льтат
Рис 7 11
нк и
о редел ет ранг категории
в три та а
Вы могли заметить, что в итоговой строке функция
выдала значение 1. Это значение не имеет никакого смысла, поскольку операция ранжирования не подразумевает никакой агрегации итогов. Несмотря на это, в итоговой строке был проведен такой же анализ, как и в остальных строках, но
результат никого не интересует. На рис. 7.12 изображен процесс расчета значения ранга для итогов.
Значение, полученное на втором шаге, составляет общую сумму продажи,
которая будет всегда больше, чем аналогичные показатели по отдельным категориям товаров. Так что единичка в итоговой строке – никакая не ошибка,
а, скорее, особенность поведения функции
, вычисляющая значение, не
имеющее никакого смысла на уровне итогов. Правильно будет скрывать эти
значения силами языка DAX. По сути, операция ранжирования категорий имеет смысл только в том случае, если в текущем контексте фильтра выбрана одна
категория. Так что здесь мы можем воспользоваться функцией
,
чтобы обеспечить вычисление меры лишь там, где это необходимо:
а ота с итератора и и
нк ие
237
Rank Cat on Sales :=
IF (
HASONEVALUE ( 'Product'[Category] );
RANKX (
ALL ( 'Product'[Category] );
[Sales Amount]
)
)
аг 1
а ли а
оиска
аг 2
на ение
аг
ози и
ез льтат
Рис 7 12
итогово строке ранг всегда
если та ли а оиска отсортирована о
дет равен едини е,
вани
Эта мера вернет пустые значения для строк с множественным выбором
категорий в текущем контексте фильтра, а значит, выведет пустоту в итогах.
Когда вы используете функцию
, а в общем случае любую меру, зависящую от специфики текущего контекста фильтра, то всегда должны снабжать
ее условием, чтобы расчеты проводились только для нужных ячеек, а во всех
остальных случаях выводили пустые значения или ошибки. Именно это и показано в предыдущем примере.
Как мы упоминали ранее, функция
может принимать еще несколько
параметров, помимо двух обязательных. Таких параметров может быть три:
„ третьим параметром является значение, которое может оказаться полезным в случае, если вам необходимо использовать разные выражения для
таблицы поиска и ранжирования;
„ четвертым параметром можно передать способ сортировки таблицы поиска. Двумя допустимыми значениями этого параметра могут быть
и
. Значением по умолчанию является
, при котором столбец
сортируется по убыванию, а минимальное значение ранга будет соответствовать максимальному числу;
„ пятый параметр определяет метод расчета ранга в случае равенства значений. Вы можете указать два значения этого параметра:
или
.
Если передать
, одинаковые значения будут удалены из таблицы
поиска. В противном случае они сохранятся.
Давайте рассмотрим эти параметры на примерах.
Третий параметр функции
можно использовать в случаях, когда для
формирования значений в таблице поиска и собственно ранжирования ис238
а ота с итератора и и
нк ие
пользуются разные выражения. Представьте, что нам необходимо провести
ранжирование с использованием следующей таблицы, представленной на
рис. 7.13.
Рис 7 13
есто дина и еско
та ли
оиска всегда ожно
ис ользовать стати еск
Если вы хотите использовать эту таблицу в качестве таблицы поиска, то для
ее построения нужно использовать значение, отличное от меры
.
Тут вам и пригодится третий параметр функции
. Таким образом, чтобы
осуществить ранжирование по определенной таблице поиска – в нашем случае
это
, – следует использовать приведенную ниже меру:
Rank On Fixed Table :=
RANKX (
'Sales Ranking';
'Sales Ranking'[Sales];
[Sales Amount]
)
Таблица поиска будет построена путем расчета значения 'Sales Ranking'[Sales]
в контексте строки таблицы
. А когда таблица поиска будет готова,
функция
приступит к расчету меры
в исходном контексте вычисления.
Результат ранжирования показан на рис. 7.14.
Рис 7 14
анжирование с ис ользование
ер Sales Amount
о иксированно та ли е оиска Sales Ranking
Весь процесс ранжирования изображен на рис. 7.15, где также видно, что таблица поиска сортируется перед использованием.
а ота с итератора и и
нк ие
239
аг 1
а ли а оиска
аг 2
на ение
аг
ози и
ез льтат
Рис 7 15
ри ис ользовании иксированно та ли
оиска в ражение,
ри ен е ое дл остроени та ли
оиска, отли аетс от ис ользованного на
аге 2
Четвертый параметр функции может принимать значение
или
и влияет на тип сортировки таблицы поиска. По умолчанию используется значение
, означающее, что чем выше значение, тем ниже ранг. При применении значения
более низким значениям будут соответствовать низкие
ранги из-за сортировки таблицы поиска по возрастанию.
Пятый параметр будет полезен при наличии одинаковых значений. Чтобы
продемонстрировать этот случай, мы используем другую меру –
.
В этой мере значения округляются до ближайшего числа, кратного миллиону.
А в срезах мы используем бренды:
Rounded Sales := MROUND ( [Sales Amount]; 1000000 )
Затем определим две меры для ранжирования: одну используем для определения ранга по умолчанию (со значением
), а вторую для альтернативного
ранга (со значением
):
Rank On Rounded Sales :=
RANKX (
ALL ( 'Product'[Brand] );
[Rounded Sales]
)
Rank On Rounded Sales Dense :=
RANKX (
ALL ( 'Product'[Brand] );
[Rounded Sales];
;
;
DENSE
)
Результаты вычисления двух мер будут отличаться. Мера по умолчанию будет подсчитывать количество одинаковых рангов, и для следующего отличающегося значения будет использоваться ранг с определенным шагом. В мере
с использованием
в качестве последнего параметра функции
ранги будут расти вне зависимости от количества повторений. Результат вычисления обеих мер показан на рис. 7.16.
240
а ота с итератора и и
нк ие
Рис 7 16
с ользование зна ени DENSE и SKIP риводит к разн
в рис тствии одинаков зна ени в та ли е оиска
рез льтата
По сути, применение значения
выполняет операцию
применительно к таблице поиска перед ее использованием.
этого не делает,
используя таблицу поиска в том виде, в котором она была построена изначально.
Применяя функцию
, важно уделять особое внимание ее первому
параметру для получения желаемого результата. В предыдущих примерах мы
указывали в качестве первого параметра выражение ALL ( Product[Brand] ),
поскольку в наши планы входило ранжирование по всем брендам. Для краткости мы не использовали условие с функцией
. В своих запросах
вы никогда не должны их пропускать, иначе рискуете получить неожиданные
результаты. Например, следующая мера будет выдавать ошибочные значения,
если в отчете не будет использоваться срез по брендам:
Rank On Sales :=
RANKX (
ALL ( 'Product'[Brand] );
[Sales Amount]
)
На рис. 7.17 мы выполнили срез по цветам товаров, и результат везде оказался равен единице.
Рис 7 17 анжирование о ренда
даст неожиданн е рез льтат
в от ете со срезо о вет товаров
а ота с итератора и и
нк ие
241
Причина в том, что в таблице поиска окажутся продажи со срезом по бренду и цвету, тогда как значения для поиска будут включать только цвета. А поскольку продажи по определенному цвету всегда будут выше, чем любое значение из подгруппы по бренду, ранг всегда будет равен единице. Добавление
условия с использованием
может помочь выводить пустые
значения для ранга, в случае если в текущем контексте вычисления есть что-то
еще, помимо одного бренда.
Наконец, функция
часто используется совместно с
.
Если пользователь выбрал определенный поднабор из общего количества брендов, функция
может привести к образованию пропусков в ранжировании,
поскольку она возвращает все бренды вне зависимости от выбранных в срезе.
Сравните следующие две меры:
Rank On Selected Brands :=
RANKX (
ALLSELECTED ( 'Product'[Brand] );
[Sales Amount]
)
Rank On All Brands :=
RANKX (
ALL ( 'Product'[Brand] );
[Sales Amount]
)
На рис. 7.18 показан вывод этих мер в присутствии фильтра по определенным брендам в срезе.
Рис 7 18
с ользование нк ии ALLSELECTED
в рез льтате ри енени
нк ии ALL
Использование функции RA
ерет ро ски в ранга , ол ив иес
EQ
нк и RANK.EQ в
соответств ет аналоги но
нк ии в
Она возвра ает
ранг зна ени в с иске, редоставл
ри то асть нк иональности RANKX
в
дете ис ользовать т
нк и редко, разве то дл ереноса ор л из
нк и RANK.EQ и еет след
и синтаксис
RANK.EQ ( <value>; <column> [; <order>] )
242
а ота с итератора и и
нк ие
ара етр <value> ожет ть
в ражение дл в ислени , а <column> редставл ет
еств
и стол е , о которо
дет роизводитьс ранжирование рети араетр нео зательн и ожет рини ать зна ение 0 дл сортировки стол а о
вани и 1 о возрастани
в есто елого стол а в нк и
ожет ть ередан
диа азон еек
а е всего в ерв
ара етр дет ередаватьс тот же са
стол е , то
ровести ранжирование вн три него Одни из с енариев, когда ва
ожет отре оватьс ис ользовать разн е ара етр , вл етс нали ие дв та ли одна
дл зна ени , котор е нео оди о ранжировать, на ри ер конкретна гр
а товаров,
втора
дл олного на ора ле ентов, к ри ер с исок все товаров Однако из за
ограни ени , наложенн
на ара етр <column> в астности, в не ожете ис ользовать в не в ражени или л
е та ли н е нк ии, вкл а ADDCOLUMNS и SELECTCOLUMNS , нк и RANK.EQ о
но ри ен етс в в исл е о стол е с ереда е
одно и то же колонки из то же та ли в ка естве ара етров, как оказано ниже
с
Product[Price Rank] =
RANK.EQ ( Product[Unit Price]; Product[Unit Price] )
нк и RANKX на ного олее о на о сравнени с RANK.EQ ак то, из ив оследн , ва вр д ли за о етс тратить ного вре ени на освоение ее енее
ективного аналога
Изменение гранулярности вычисления
Существует ряд сценариев, в которых формулы не могут быть вычислены на
уровне итогов. Вместо этого значения должны вычисляться на более высоких
уровнях и затем агрегироваться.
Представьте, что вам необходимо подсчитать сумму продаж в расчете на
каждый рабочий день. Количество рабочих дней в каждом месяце разное изза наличия выходных и праздничных дней. Для простоты в этом примере мы
не будем учитывать праздники, а возьмем только субботу и воскресенье в качестве нерабочих дней. В реальных примерах необходимо также принимать
в расчет праздничные дни.
В таблице
у нас есть столбец с названием
, в котором содержатся 1 или 0 в зависимости от того, рабочий это день или нет. Такие флаги
удобно хранить в качестве целочисленных значений, поскольку это упрощает
подсчет рабочих или праздничных дней. Следующие две меры вычисляют общее количество дней и количество рабочих дней в рамках текущего контекста
фильтра:
NumOfDays := COUNTROWS ( 'Date' )
NumOfWorkingDays := SUM ( 'Date'[IsWorkingDay] )
На рис. 7.19 представлен отчет с этими двумя мерами.
Основываясь на этих мерах, можно вычислить сумму продаж в расчете на
один рабочий день. Это значение очень полезно для вычисления показателя
эффективности для каждого месяца с учетом валовых продаж и количества
дней, в которые эти продажи совершались. Предполагаемая формула представляется довольно простой, но она скрывает в себе определенные трудности,
которые мы решим при помощи итерационных функций. Как мы уже не раз
делали в данной книге, мы будем приближаться к правильному расчету шаг за
а ота с итератора и и
нк ие
243
шагом, параллельно указывая на ошибки. Цель этого примера состоит не в том,
чтобы предоставить вам готовый шаблон решения. Мы вместе допустим ошибки, типичные для подобных задач, и вместе же их исправим.
Рис 7 19
исло ра о и дне отли аетс в каждо
в зависи ости от коли ества с
от и воскресени
ес
е
Как и можно было ожидать, простая операция деления меры
на
количество рабочих дней даст правильный результат только на уровне месяца.
Любопытно, что в итоговой строке значение оказалось меньше, чем даже в любом отдельно взятом месяце:
SalesPerWorkingDay := DIVIDE ( [Sales Amount]; [NumOfWorkingDays] )
На рис. 7.20 вы можете видеть результат вычисления этой меры.
Если посмотреть на итоговое значение по 2007 году, можно обнаружить число 17 985,16. Это довольно мало с учетом того, что в каждом месяце этого года
продажи превышали отметку в 37 000,00. Причина в том, что общее количество
рабочих дней в 2017 году составляло 261, включая месяцы, когда продаж не
было. В нашей модели данных продажи стартовали только в августе 2007-го,
и было бы неправильно учитывать в расчете средних значений те месяцы, когда продажи не велись. Та же проблема проявится и в периоде, содержащем последнюю дату с заполненной информацией. Например, в текущем году общее
количество рабочих дней затронет и будущие месяцы.
Есть несколько способов исправить формулу. Мы выберем самый простой из
них: если в месяце не было продаж, мы не будем учитывать его при подсчете
количества дней.
Поскольку вычисление производится помесячно, нам необходимо в формуле проходить по месяцам и проверять, были ли в этом месяце продажи. Если да,
то рабочие дни этого месяца будут учитываться в общем количестве рабочих
244
а ота с итератора и и
нк ие
дней. В противном случае этот месяц просто пропускается. Функция
может нам реализовать данный алгоритм:
по-
SalesPerWorkingDay :=
VAR WorkingDays =
SUMX (
VALUES ( 'Date'[Month] );
IF (
[Sales Amount] > 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 ( Sales[Order Date] ) )
VAR MaxYear = YEAR ( MAX ( Sales[Order Date] ) )
RETURN
FILTER (
CALENDARAUTO ( );
YEAR ( [Date] ) >= MinYear &&
YEAR ( [Date] ) <= MaxYear
)
Результирующая таблица будет содержать даты только из интервала таблицы продаж. При этом вычислять первый и последний день года совсем не обязательно, функция
справится с этим сама.
После получения необходимого списка дат разработчику остается дополнить календарь необходимыми столбцами, применяя выражения на DAX. Приведем пример часто используемых столбцов для календарей с дальнейшим их
выводом, показанным на рис. 8.4:
Date =
VAR MinYear = YEAR ( MIN ( Sales[Order Date] ) )
VAR MaxYear = YEAR ( MAX ( Sales[Order Date] ) )
огика о ера и со вре ене
255
RETURN
ADDCOLUMNS (
FILTER (
CALENDARAUTO ( );
YEAR ( [Date] ) >= MinYear &&
YEAR ( [Date] ) <= MaxYear
);
"Year"; YEAR ( [Date] );
"Quarter Number"; INT ( FORMAT ( [Date]; "q" ) );
"Quarter"; "Q" & INT ( FORMAT ( [Date]; "q" ) );
"Month Number"; MONTH ( [Date] );
"Month"; FORMAT ( [Date]; "mmmm" );
"Week Day Number"; WEEKDAY ( [Date] );
"Week Day"; FORMAT ( [Date]; "dddd" );
"Year Month Number"; YEAR ( [Date] ) * 100 + MONTH ( [Date] );
"Year Month"; FORMAT ( [Date]; "mmmm" ) & " " & YEAR ( [Date] );
"Year Quarter Number"; YEAR ( [Date] ) * 100 + INT ( FORMAT ( [Date]; "q" ) );
"Year Quarter"; "Q" & FORMAT ( [Date]; "q" ) & "-" & YEAR ( [Date] )
)
Рис 8 4
ри о о и
нк ии ADDCOLUMNS ожно создать та ли
дат в одно в ражении
Такого же результата можно добиться, создавая вычисляемые столбцы прямо в пользовательском интерфейсе. Главным преимуществом использования
функции
является возможность повторного применения этого
кода в других проектах.
Использование шаблонов DAX для работы с датами
редставленн в е ри ер л риведен искл ительно в о разовательн
ел , и в не
ли оставлен только са е важн е стол
, то код ожно ло
раз естить в книге о в интернете ожно на ти и др гие а лон дл ра от с дата и а ри ер,
создали сво
а лон дл
и раз естили его о адрес
https://www.sqlbi.com/tools/dax-date-template/
также ожете извле ь из того
а лона код на
и ис ользовать его в свои роекта в
256
огика о ера и со вре ене
Работа со множественными датами
Если в вашей модели данных присутствует несколько столбцов с датами, вы
должны сделать выбор: либо оперировать множеством связей с единой таблицей дат, либо создать несколько календарей. Это очень важный выбор, от которого будут зависеть написание кода DAX в будущем и глубина возможного
анализа в вашей модели данных.
Представьте, что в таблице
у вас есть три поля с датами:
„
„
„
: дата оформления заказа;
: дата ожидаемой поставки товара;
: дата фактической поставки товара.
Разработчик может создать связи по всем трем столбцам к единой таблице
дат, подразумевая, что в любой момент времени активной может быть только
одна из них. А может создать три отдельных календаря, чтобы иметь возможность свободно осуществлять срезы по всем этим столбцам. К тому же вполне
вероятно, что другие таблицы также будут содержать столбцы с датами. Например, в таблице
могут присутствовать даты, связанные с закупками,
ав
– с составлением бюджетного плана. В конце концов, почти в любой
модели данных присутствует множество столбцов с датами, и только разработчик модели способен понять, как лучше с ними обращаться.
В следующих разделах мы подробно поговорим о представленных вариантах и посмотрим, какое влияние этот выбор оказывает на написание кода
на DAX.
Поддержка множественных связей с таблицей дат
При моделировании данных существует возможность создания нескольких
связей между двумя таблицами. При этом в любой момент времени активной
может быть лишь одна из созданных связей, остальные остаются неактивными. Неактивные связи могут быть активированы в функции
при
помощи модификатора
, как мы показывали в главе 5.
Рассмотрим модель данных, представленную на рис. 8.5. Между таблицами
и
создано две связи, но лишь одна из них может быть активной. На представленном примере активной является связь между столбцами
и
.
Вы можете создать две меры по продажам, основывающиеся на разных связях с таблицей
:
Ordered Amount :=
SUMX ( Sales; Sales[Net Price] * Sales[Quantity] )
Delivered Amount :=
CALCULATE (
SUMX ( Sales; Sales[Net Price] * Sales[Quantity] );
USERELATIONSHIP ( Sales[Delivery Date]; 'Date'[Date] )
)
огика о ера и со вре ене
257
Рис 8 5
ктивна св зь соедин ет стол
Sales[Order Date] и Date[Date]
В первой мере
используется активная связь между таблицами
и
, в основе которой лежит столбец
. Вторая мера
Delivered Amount использует то же выражение DAX, но при этом полагается
на связь по полю
. Модификатор
меняет
активную связь между таблицами
и
в контексте фильтра, определенном функцией
. В отчете, показанном на рис. 8.6, выведены обе эти
меры.
Рис 8 6
на ени в ера Ordered Amount и Delivered Amount отли а тс
оскольк дата оставки огла ереско ить на след
и ес
о ес
а ,
Использование множественных связей с таблицей дат увеличивает общее количество созданных мер в модели данных. Обычно в таком случае разработчик
создает конкретные меры для использования с определенными датами. Если
вы не хотите поддерживать большое количество мер в модели и желаете ис258
огика о ера и со вре ене
пользовать любую созданную меру применительно к разным датам, то можете
прибегнуть к помощи групп вычислений, описываемых в следующих главах.
Поддержка нескольких таблиц дат
Вместо того чтобы дублировать меры, можно создавать копии таблиц дат в модели данных – по одной для каждой даты. Таким образом, мера будет вычислять значение, исходя из выбранной пользователем даты в отчете. В плане
поддержки это решение может показаться более оптимальным, поскольку ведет к уменьшению количества мер и позволяет, например, выбирать продажи,
пересекающиеся по двум месяцам. Но в то же время дублирование таблиц дат
усложняет использование модели данных в целом. Допустим, вы можете построить отчет с общим количеством заказов, сформированных в январе, а доставленных в феврале того же года. Но удобно отобразить эту информацию на
графике довольно затруднительно.
Такой способ организации данных также известен как работа с ролев ми
и мерени ми (role-playing dimension). Таблица дат представляет собой измерение, которое дублируется для каждой связи, а значит, и для каждой роли.
В целом же два этих подхода (активирование связей и дублирование таблиц
дат) являются взаимодополняющими.
Чтобы создать таблицы
и
, вы должны создать две копии существующего календаря в модели данных, меняя при этом название. На
рис. 8.7 показана модель данных, содержащая несколько таблиц дат, объединенных связями с таблицей
.
Рис 8 7
ажда дата в та ли е Sales св зана со свое та ли е дат
Важно
ри создании ко ии та ли дат в должн
изи ески д лировать ее в одели данн
аки о разо , л
е всего дет создать разн е редставлени дл разн
ролев
из ерени в исто нике данн , то
стол
в ни наз вались о разно
и и ели разное содержи ое а ри ер, в есто того то
создавать стол е с и ене
Year в каждо календаре, л
е дет назвать и Order Year и Delivery Year в та ли е дат
ор ировани заказов и и оставки соответственно
то сл ае навига и о от ета с ественно роститс то также видно о рис
Более того, оро е рактико
вл етс ранение разного содержи ого в стол а
а ри ер, в
ожете до авл ть
ре икс к год в зависи ости от роли та ли
CY
ата создани дл
содержи ого стол а Order Year и DY дл
ата оставки
огика о ера и со вре ене
259
На рис. 8.8 показана матрица с одновременным выводом дат из двух календарей. Такой отчет не может быть создан при выборе подхода с поддержкой
множественных связей с единой таблицей
. Как видите, уникальные названия столбцов и содержимое с конкретными префиксами помогает сделать
отчет более легким для восприятия. Во избежание неразберихи между датами
оформления заказов и их поставки мы используем префиксы
и
соответственно.
Рис 8 8
азн е ре икс в содержи о
где дата заказа, а где дата оставки
озвол
т
стро он ть,
В подходе с разными таблицами дат одна и та же мера может давать разные
результаты в зависимости от используемого столбца в фильтре. Однако было
бы неправильно выбирать этот вариант организации хранения данных только
по причине снижения количества мер в модели данных. В конце концов, в этом
случае вы не сможете вынести в отчет одну и ту же меру, сгруппированную по
разным датам. Представьте себе линейную диаграмму, отображающую меру
по столбцам
и
. Для этого вам понадобится,
чтобы на оси дат учитывались данные из одной таблицы
, а со множественными календарями в модели такого результата будет добиться довольно
проблематично.
Если вашим первостепенным приоритетом является уменьшение количества мер в модели данных и возможность вычислять одну и ту же меру по разным датам, вам стоит присмотреться к группам вычислений, которые будут
описаны в главе 9, с содержанием единой таблицы дат в модели. Единственной пользой от присутствия множества календарей в модели является возможность использования пересечений одной и той же меры в одной визуализации
по разным датам, как показано на рис. 8.8. Во всех остальных случаях лучшим
выбором будет содержание одной таблицы дат в модели данных со множественными связями.
Знакомство с базовыми вычислениями
в работе со временем
В предыдущих разделах вы узнали, как правильно создать таблицу дат, которая
пригодится вам для осуществления вычислений при работе с датами и временем. В DAX есть множество функций для облегчения таких вычислений. Использовать эти функции довольно просто, но при этом они помогают производить очень сложные и полезные расчеты. Понимание деталей работы этих
260
огика о ера и со вре ене
функций позволит вам быстро начать применять их в своей работе. В целях
обучения мы сначала покажем, как производить вычисления с датами и временем в DAX стандартными средствами – с использованием функций
,
,
и
. Позже в данной главе мы перейдем
к применению специализированных функций из раздела логики операций со
временем для тех же расчетов, и вы увидите, как они помогают облегчить написание кода и сделать его гораздо более легким для восприятия.
Мы решили использовать такой подход в обучении сразу по нескольким
причинам. Но главная из них в том, что, когда речь заходит о логике операций
со временем, далеко не все вычисления могут быть произведены с применением стандартных функций DAX. В какой-то момент в карьере разработчика
DAX вам понадобится осуществить более сложный расчет, чем просто сумма
нарастающим итогом с начала года, и вы обнаружите, что в DAX нет специальных функций, удовлетворяющих вашим требованиям. Если вы опытный
разработчик, то для вас это не будет большой проблемой. Вы закатаете рукава
и в итоге напишете правильный фильтр без использования специализированных функций DAX. Если же у вас нет достаточного опыта в разработке на языке
DAX, вам придется несладко.
Посмотрим на практике, что из себя представляет логика операций со временем. Представьте, что у вас есть простая мера, вычисление которой производится в текущем контексте фильтра:
Sales Amount :=
SUMX ( Sales; Sales[Net Price] * Sales[Quantity] )
Поскольку таблицы
и
объединены связью, текущий фильтр в таблице
ограничит выбор в
. Чтобы произвести вычисление в таблице
за другой временной период, разработчику необходимо будет изменить
существующий фильтр в таблице дат. Например, чтобы получить сумму продаж нарастающим итогом с начала года при текущем фильтре по февралю
2007 года, необходимо перед осуществлением итераций по таблице
изменить контекст фильтра таким образом, чтобы в него вошли как январь, так
и февраль этого года.
Для этого можно использовать уже знакомую вам функцию
с указанием аргумента фильтра, которая вернет сумму нарастающим итогом
на февраль 2007 года:
Sales Amount Jan-Feb 2007 :=
CALCULATE (
SUMX ( Sales; Sales[Net Price] * Sales[Quantity] );
FILTER (
ALL ( 'Date' );
AND (
'Date'[Date] >= DATE ( 2007; 1; 1 );
'Date'[Date] <= DATE ( 2007; 2; 28 )
)
)
)
Результат вычисления этой меры показан на рис. 8.9.
огика о ера и со вре ене
261
Рис 8 9
ез льтато в ислени ер
дет с
а родаж
за нварь и евраль 200 года вне зависи ости от ес а в строке
Функция
, используемая в качестве аргумента фильтра в
,
возвращает набор дат, замещающий собой текущий выбор в таблице
.
Иными словами, несмотря на то что в текущем контексте фильтра присутствует фильтр по месяцу, исходящий от строк, мера рассчитывается совсем по другому интервалу.
Очевидно, что мера, возвращающая сумму продаж по двум статическим месяцам, никакого интереса не представляет. Но, поняв сам механизм, вы можете
вооружиться некоторыми стандартными функциями DAX и правильно вычислить сумму продаж нарастающим итогом, как показано ниже:
Sales Amount YTD :=
VAR LastVisibleDate = MAX ( 'Date'[Date] )
VAR CurrentYear = YEAR ( LastVisibleDate )
VAR SetOfDatesYtd =
FILTER (
ALL ( 'Date' );
AND (
'Date'[Date] <= LastVisibleDate;
YEAR ( 'Date'[Date] ) = CurrentYear
)
)
VAR Result =
CALCULATE (
SUMX ( Sales; Sales[Net Price] * Sales[Quantity] );
SetOfDatesYtd
)
RETURN
Result
И хотя код этой меры получился более сложным, принцип расчета остался
прежним. Сначала мы сохраняем в переменную
последнюю видимую дату в текущем контексте фильтра. После этого в переменную
записываем год этой даты. В третьей переменной
хранятся
все даты с начала года по последнюю видимую дату. Этим набором дат мы
262
огика о ера и со вре ене
заменяем текущий контекст фильтра в отношении дат для вычисления нарастающего итога, как видно по рис. 8.10.
Рис 8 10 Мера Sales Amount YTD расс ит вает нараста
ос
е родаж с ис ользование
нк ии
и итог
Как мы и утверждали, вы вполне можете производить вычисления при работе с датой и временем, пользуясь при этом стандартными функциями DAX.
Важно понимать, что такие вычисления по своей сути ничем не отличаются
от любых других, в которых также производятся манипуляции с контекстом
фильтра. Поскольку мера призвана агрегировать значения по другому набору
дат, ее вычисление производится в два этапа. Сначала мы определяем новый
фильтр по датам, а затем применяем его к модели данных для произведения
вычисления. Все расчеты в области даты и времени производятся одинаково.
И когда вы поймете базовые принципы, логика операций со временем перестанет быть для вас тайной.
Перед тем как двигаться дальше, стоит подробнее остановиться на том, как
поступает DAX со связями по столбцам с датами. Взгляните на чуть измененный предыдущий код, где вместо фильтра по всей таблице дат мы используем
фильтр только по столбцу
:
Sales Amount YTD :=
VAR LastVisibleDate = MAX ( 'Date'[Date] )
VAR CurrentYear = YEAR ( LastVisibleDate )
VAR SetOfDatesYtd =
FILTER (
ALL ( 'Date'[Date] );
AND (
'Date'[Date] <= LastVisibleDate;
YEAR ( 'Date'[Date] ) = CurrentYear
)
)
VAR Result =
огика о ера и со вре ене
263
CALCULATE (
SUMX ( Sales; Sales[Net Price] * Sales[Quantity] );
SetOfDatesYtd
)
RETURN
Result
Если использовать эту меру в отчете вместо предыдущей, мы не увидим
никаких изменений. Фактически обе меры вычисляют одинаковые значения,
хотя не должны. Давайте рассмотрим, как работает мера на примере одной
строки – допустим, апреля 2007 года.
Контекст фильтра в этой строке включает в себя год 2007 и месяц апрель.
Соответственно, в переменной
окажется значение 30 апреля 2007 года, а в
– 2007. Табличная переменная
, согласно методике расчета, будет содержать все даты между 1 января 2007 года
и 30 апреля 2007 года. Иными словами, в строке с апрелем 2007 года выполняемая формула будет эквивалентна следующей:
CALCULATE (
CALCULATE (
[Sales Amount];
AND (
'Date'[Date] >= DATE ( 2007; 1; 1);
'Date'[Date] <= DATE ( 2007; 04; 30 )
)
);
'Date'[Year] = 2007;
'Date'[Month] = "April"
)
-- Этот фильтр эквивалентен
-- результату функции FILTER
-- Эти фильтры приходят из строки
-- матрицы по апрелю 2007 года
Если вы вспомните, что уже знаете о контекстах фильтра и функции
, то поймете, что этот код не должен правильно вычислять нарастающий
итог с начала года. И действительно, аргумент фильтра внутренней функции
вернет таблицу, содержащую столбец
. Таким образом, этот
фильтр должен перезаписать все существующие фильтры по столбцу
,
оставив фильтры по другим столбцам неизменными. А поскольку во внешней
функции
применяются фильтры к столбцам
и
,
итоговый контекст фильтра, в котором рассчитывается мера Sales Amount , должен содержать только апрель 2007 года. Но мера все же возвращает правильные
значения, включая все остальные месяцы начиная с января.
Причина такого неожиданно правильного поведения меры в том, что DAX
особым образом обрабатывает результаты, в случае если таблицы связаны по
столбцу с датой, как у нас. Всякий раз, когда фильтр применяется к столбцу
типа
или
, который используется как связующий между двумя
таблицами, DAX автоматически добавляет функцию
ко всей таблице дат
в качестве дополнительного аргумента фильтра функции CALCULATE. Таким
образом, предыдущий пример преобразуется так:
CALCULATE (
CALCULATE (
264
огика о ера и со вре ене
[Sales Amount];
AND (
-- Этот фильтр эквивалентен
'Date'[Date] >= DATE ( 2007; 1; 1);
-- результату функции FILTER
'Date'[Date] <= DATE ( 2007; 04; 30 )
);
ALL ( 'Date' )
-- Эта строка добавлена движком DAX автоматически
);
'Date'[Year] = 2007;
'Date'[Month] = "April"
-- Эти фильтры приходят из строки
-- матрицы по апрелю 2007 года
)
Когда фильтр применяется к столбцу типа
или
, являющемуся
основанием для связи «один ко многим», DAX автоматически распространяет действие фильтра на другую таблицу, заменяя фильтры по любым другим
столбцам в той же таблице поиска.
Это сделано для того, чтобы максимально упростить логику операций со
временем, когда таблицы связаны по столбцу с датой. В следующем разделе
мы расскажем про специальную отметку для таблиц дат, позволяющую реализовать такое же поведение для связей не по столбцу с датой.
Пометка календарей как таблиц дат
Применение фильтра к столбцу с датами в календаре прекрасно работает, если
этот столбец используется в качестве основы для связи. Но вам может понадобиться связать таблицы по другому столбцу. Во многих календарях есть поле
с целочисленным значением – обычно в формате YYYYMMDD, – по которому
и производится объединение с другими таблицами в модели данных.
Чтобы продемонстрировать такую ситуацию, мы создали столбец
в таблицах
и
. После этого настроили связь между этими таблицами
по данному полю, а не по полю типа дата. Получившуюся модель данных можно видеть на рис. 8.11.
Рис 8 11 Св зь ежд та ли а и Sales и Date организована о стол
DateKey ти а Integer
Теперь тот же самый код с вычислением суммы продаж нарастающим итогом с начала года, который раньше работал нормально, вдруг сломался. Результат вычисления нашей меры показан на рис. 8.12.
огика о ера и со вре ене
265
Рис 8 12
с ользование ело исленного стол а дл св зи
ривело к о и о н
рез льтата в ислени ер
Как видите, после изменения связи отчет показывает одинаковые значения
в столбцах с мерами
и
. Поскольку связь между
таблицами более не основывается на столбце типа
, DAX автоматически не добавляет функцию
к таблице дат. А следовательно, новый фильтр
по дате будет пересекаться с существующим – из внешнего запроса, что приведет к неправильным расчетам.
Здесь возможны два решения. Во-первых, можно вручную добавлять
ко
всем вычислениям, связанным с логикой операций со временем. Это довольно обременительный вариант для разработчика, который должен не забывать
вставлять данную функцию во все аналогичные расчеты. Второй способ гораздо более удобный и заключается в пометке календаря как таблицы дат на панели инструментов.
Если таблица дат помечена соответствующим образом, DAX будет автоматически добавлять модификатор
ко всем вычислениям, даже если связь
организована не по столбцу с датами. Помните о том, что после специальной
пометки календаря
будет добавляться всякий раз, когда происходит изменение контекста фильтра по столбцу с датами. В некоторых ситуациях такое
поведение будет нежелательным, и разработчику придется писать довольно
сложный код, чтобы наладить правильную фильтрацию. Мы расскажем об
этом далее в данной главе.
Знакомство с базовыми функциями логики
операций со временем
Теперь, когда вы узнали базовые механизмы вычислений при работе с датой
и временем, пришло время упростить код. Если бы разработчикам DAX приходилось писать сложные выражения с использованием функции FILTER всякий
266
огика о ера и со вре ене
раз, когда необходимо рассчитать простую сумму нарастающим итогом с начала года, жизнь не казалась бы им медом.
Для облегчения вычислений, связанных с логикой операций со временем,
в DAX имеется целый ряд специальных функций, автоматически выполняющих фильтрацию, которую мы производили вручную в предыдущем примере.
Вспомним меру
, которую мы написали ранее:
Sales Amount YTD :=
VAR LastVisibleDate = MAX ( 'Date'[Date] )
VAR CurrentYear = YEAR ( LastVisibleDate )
VAR SetOfDatesYtd =
FILTER (
ALL ( 'Date'[Date] );
AND (
'Date'[Date] <= LastVisibleDate;
YEAR ( 'Date'[Date] ) = CurrentYear
)
)
VAR Result =
CALCULATE (
SUMX ( Sales; Sales[Net Price] * Sales[Quantity] );
SetOfDatesYtd
)
RETURN
Result
С использованием специальной функции
тельно упростить, приведя к следующему виду:
этот код можно значи-
Sales Amount YTD :=
CALCULATE (
SUMX ( Sales; Sales[Net Price] * Sales[Quantity] );
DATESYTD ( 'Date'[Date] )
)
Функция
сделала все то же самое, что и гораздо более громоздкий
код с использованием фильтрации. Поведение меры и ее эффективность при
этом остались прежними, но на написание этой формулы у вас уйдет намного меньше времени, которое можно потратить на изучение этих специальных
функций.
Простые вычисления нарастающим итогом с начала года, квартала, месяца, а также сравнение показателей текущего года с предыдущим можно очень
легко реализовать при помощи базовых функций для работы с логикой временных периодов. Более сложные расчеты могут потребовать сочетания разных специальных функций DAX. Написание действительно объемного и сложного кода на DAX может понадобиться только при необходимости построения
нестандартных календарей наподобие недельного или в больших комплексных расчетах, где стандартного набора функций DAX может оказаться недостаточно.
огика о ера и со вре ене
267
Примечание
се нк ии логики о ера и со вре ене ри ен т ильтр к стол
с дата и та ли Date С некотор и ри ера и ра от с дато и вре ене в встретитесь в данно книге, а олн
ере ень с е иальн
нк и
с а лона и из то
о ласти рас оложен о адрес http://www.daxpatterns.com/time-patterns/.
В следующих разделах мы познакомимся с базовыми вычислениями в DAX
при помощи специальных функций логики операций со временем. Позже
в данной главе коснемся более сложных выражений.
Нараста
ие итоги с начала года, квартала, месяца
Все вычисления показателей нарастающим итогом – будь то с начала года,
квартала или месяца – очень похожи друг на друга. Разница лишь в том, что
итоги с начала месяца актуальны только на уровне дня, тогда как годовые
и квартальные итоги часто используются для анализа месячных показателей.
Чтобы вычислить сумму продаж нарастающим итогом с начала года, необходимо изменить контекст фильтра по датам в выражении таким образом, чтобы
начало периода вычисления перенеслось на 1 января текущего года, а конец
остался на месяце, соответствующем выбранной ячейке. Простой пример подобного вычисления приведен ниже:
Sales Amount YTD :=
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
)
Функция
возвращает таблицу, заполненную датами с начала года
и до последней даты в текущем контексте фильтра. Эта таблица впоследствии
используется в качестве аргумента фильтра в функции
для установки обновленного контекста фильтра, в котором будет вычислена мера
. В одну группу с
входят еще две аналогичные функции для
вычисления меры нарастающим итогом с начала месяца (
) и с начала квартала (
). Пример работы этих функций можно видеть на
рис. 8.13.
Этот подход базируется на использовании функции
. Но в DAX
также есть ряд функций, упрощающих вычисление мер нарастающим итогом.
Это
,
и
. В следующем примере показано предыдущее вычисление с использованием функции
:
YTD Sales :=
TOTALYTD (
[Sales Amount];
'Date'[Date]
)
Синтаксис этой меры отличается от предыдущего примера, поскольку функция
принимает на вход название меры для вычисления в качестве
268
огика о ера и со вре ене
первого параметра и столбец с датами в качестве второго. В остальном поведение этих двух мер идентично. Функция
скрывает в себе вложенную
функцию
, что само по себе ограничивает ее использование. Если
в расчете участвует функция
, нужно сделать все, чтобы она была
видимой. Это сделает код более очевидным, в том числе из-за преобразования
контекста, который может быть инициирован функцией
.
Рис 8 13 Мер Sales Amount YTD и Sales Amount QTD
в веден в есте с азово еро Sales Amount
Похожим образом вы можете использовать и функции для расчета нарастающих итогов по месяцам и кварталам, как показано ниже:
QTD Sales := TOTALQTD ( [Sales Amount]; 'Date'[Date] )
QTD Sales := CALCULATE ( [Sales Amount]; DATESQTD ( 'Date'[Date] ) )
MTD Sales := TOTALMTD ( [Sales Amount]; 'Date'[Date] )
MTD Sales := CALCULATE ( [Sales Amount]; DATESMTD ( 'Date'[Date] ) )
Вычисление нарастающего итога с начала года в случае использования нестандартного финансового календаря с отличной от 31 декабря датой окончания отчетного периода требует передачи дополнительного необязательного
параметра в функции
и
. В следующем примере показан
расчет нарастающего итога для нестандартного финансового года:
Fiscal YTD Sales := TOTALYTD ( [Sales Amount]; 'Date'[Date]; "06-30" )
Fiscal YTD Sales := CALCULATE ( [Sales Amount]; DATESYTD ( 'Date'[Date]; "06-30" ) )
Опциональный параметр "06-30" соответствует дате окончания финансового года – 30 июня. Целый ряд специальных функций логики операций со
временем в DAX принимает дополнительный параметр для этих целей. Это
функции
,
,
,
,
,
,
и
.
огика о ера и со вре ене
269
Важно зависи ости от региональн настроек ва
ожет отре оватьс сна ала каз вать день, а зате
ес
также ожете ис ользовать строк в ор ате
, то
из ежать неоднозна ности в трактовке дат
то сл ае казание года не
дет оказ вать вли ни на о ределение оследне дат
инансового года ри рас ете
нараста и итогов
Fiscal YTD Sales := TOTALYTD ( [Sales Amount]; 'Date'[Date]; "30-06" )
Fiscal YTD Sales := CALCULATE ( [Sales Amount]; DATESYTD ( 'Date'[Date]; "30-06" ) )
Fiscal YTD Sales := CALCULATE ( [Sales Amount]; DATESYTD ( 'Date'[Date]; "2018-06-30" ) )
о состо ни на и нь 201 года не ис равлен о и ки в рас ета , в сл ае если инансов
год на инаетс в арте и закан иваетс в еврале одро нее о то ро ле е
и с осо а ее ре ени
оговори далее в данно главе
Сравнение временных интервалов
Многие вычисления требуют сравнения показателей текущего временного интервала с тем же интервалом в прошедшем году. Это может быть полезно для
сравнения тенденций в определенный период нынешнего года и прошлого.
При осуществлении таких вычислений вам поможет функция
:
PY Sales := CALCULATE ( [Sales Amount]; SAMEPERIODLASTYEAR ( 'Date'[Date] ) )
Функция
возвращает набор аналогичных дат периода, сдвинутых ровно на год назад.
представляет собой частный случай более общей функции
, которая предназначена
для сдвигов любых временных интервалов на определенное количество шагов.
Среди этих интервалов могут быть следующие:
,
,
и
.
Например, вы можете переписать предыдущую меру с использованием функции
для сдвига текущего контекста фильтра ровно на год назад:
PY Sales := CALCULATE( [Sales Amount]; DATEADD ( 'Date'[Date]; -1; YEAR ) )
В общем случае функция
является более мощной по сравнению
с
, поскольку может вычислять аналогичные показатели
не только по прошлому году, но и по прошлому кварталу, месяцу и даже дню:
PQ Sales := CALCULATE ( [Sales Amount]; DATEADD ( 'Date'[Date]; -1; QUARTER ) )
PM Sales := CALCULATE ( [Sales Amount]; DATEADD ( 'Date'[Date]; -1; MONTH ) )
PD Sales := CALCULATE ( [Sales Amount]; DATEADD ( 'Date'[Date]; -1; DAY ) )
На рис. 8.14 показан результат вычисления некоторых из этих мер.
Еще одной полезной функцией является
, похожая на
, но возвращающая полный интервал, указанный в третьем параметре,
а не частичный, как функция
. Таким образом, несмотря на то что в текущем контексте фильтра выбран один месяц, следующая мера с использованием функции
вернет сумму продаж за весь предыдущий год:
PY Total Sales :=
CALCULATE ( [Sales Amount]; PARALLELPERIOD ( 'Date'[Date]; -1; YEAR ) )
270
огика о ера и со вре ене
Рис 8 14
нк и DATEADD озвол ет с естить вре енно
на л ое коли ество интервалов
ериод
Применяя другие значения параметров, можно получить информацию за
соответствующие временные интервалы:
PQ Total Sales :=
CALCULATE ( [Sales Amount]; PARALLELPERIOD ( 'Date'[Date]; -1; QUARTER ) )
На рис. 8.15 показаны значения мер с использованием функции
за предыдущий год и квартал.
Есть функции, близкие по смыслу, но не идентичные функции
. Это
,
,
,
,
,
,
и
. Эти функции ведут себя так же, как
, при условии что выбран один элемент,
соответствующий названию конкретной функции, – год, квартал, месяц или
день. Если выбрано несколько периодов, функция
вернет
сдвинутые во времени значения для всех из них. Если же использовать более
специализированные функции по году, кварталу, месяцу или дню, то в случае
множественного выбора элементов будет возвращен единственный элемент,
смежный с выбранным периодом вне зависимости от количества элементов
в нем. Например, следующий код вернет набор из марта, апреля и мая 2008 года, если выбран второй квартал 2008 года (апрель, май и июнь):
PM Total Sales :=
CALCULATE ( [Sales Amount]; PARALLELPERIOD ( 'Date'[Date]; -1; MONTH ) )
Следующий же код в случае выбора второго квартала 2008 года вернет только март:
Last PM Sales :=
CALCULATE ( [Sales Amount]; PREVIOUSMONTH( 'Date'[Date] ) )
огика о ера и со вре ене
271
Рис 8 15
нк и
PARALLELPERIOD возвра ает
олн
казанн
ериод,
а не тек и ериод, сдвин т
во вре ени
Разница в поведении между двумя мерами хорошо видна по рис. 8.16. Мера
возвращает значение за декабрь 2007 года как для всего 2008 года, так и для его первого квартала, тогда как
всегда возвращает
агрегацию за то же количество месяцев, что и в текущем выборе, – за три для
квартала и за двенадцать для года. Это происходит, даже если исходный выбор
смещается назад на один месяц.
Рис 8 16
нк и
PREVIOUSMONTH возвра ает
один ес , даже если
в ис одно в оре на одитс
квартал или год
272
огика о ера и со вре ене
Сочетание функций логики операций со временем
Одной из полезных особенностей функций логики операций со временем является их сочетаемость, помогающая проводить более сложные вычисления.
Первым параметром большинства этих функций является столбец с датами
в календаре. На самом деле это только синтаксический сахар, позволяющий
избегать написания полной формулы, требующей передачи первым параметром таблицы, как видно в следующем сравнении двух эквивалентных мер. При
вычислении столбец с датами невидимо для нас трансформируется в таблицу
с уникальными значениями, активными в текущем контексте фильтра после
преобразования из контекста строки, если таковой присутствовал:
PY Sales :=
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
)
-- это эквивалентно следующей записи:
PY Sales :=
CALCULATE (
[Sales Amount];
DATESYTD ( CALCULATETABLE ( DISTINCT ( 'Date'[Date] ) ) )
)
Таким образом, функции логики операций со временем принимают в качестве первого параметра таблицу и работают как машина времени. Эти функции просто берут содержимое переданной таблицы и смещают его во времени
на необходимое количество лет, кварталов, месяцев или дней. А поскольку они
получают на вход таблицу, значит, на ее месте спокойно может оказаться любое табличное выражение, включая другую функцию логики операций со временем. Это открывает возможности для сочетания разных временных функций при помощи каскадных вложений.
Например, мы можем сравнить сумму продаж нарастающим итогом с начала года с соответствующим значением прошлого года. Это можно сделать при
помощи сочетания функций
и
в одном выражении. Любопытно отметить при этом, что изменение порядка следования
функций не повлияет на результат:
PY YTD Sales :=
CALCULATE (
[Sales Amount];
SAMEPERIODLASTYEAR ( DATESYTD ( 'Date'[Date] ) )
)
-- это эквивалентно следующей записи:
PY YTD Sales :=
CALCULATE (
огика о ера и со вре ене
273
[Sales Amount];
DATESYTD ( SAMEPERIODLASTYEAR ( 'Date'[Date] ) )
)
Можно также использовать функцию
, чтобы сместить во времени текущий контекст фильтра, а затем вызвать функцию, которая, в свою
очередь, также проанализирует переданный контекст и сместит его во времени. Следующие две меры
эквивалентны предыдущим двум; меры
и
были определены ранее в данной главе:
PY YTD Sales :=
CALCULATE (
[YTD Sales];
SAMEPERIODLASTYEAR ( 'Date'[Date] )
)
-- это эквивалентно следующей записи:
PY YTD Sales :=
CALCULATE (
[PY Sales];
DATESYTD ( 'Date'[Date] )
)
Результат вычисления меры
видите, значения меры
.
можно видеть на рис. 8.17. Как
сдвинуты на год при расчете меры
Рис 8 17 С
а родаж нараста и итого за ро л год
ожет ть ол ена те со етани
нк и логики о ера и со вре ене
274
огика о ера и со вре ене
Все примеры из этого раздела могут быть адаптированы для работы с кварталами, месяцами и днями, но не с неделями. В DAX нет специальных функций
логики операций со временем для подсчета недель из-за отсутствия строгого
соответствия между неделями и годами, кварталами и месяцами. Таким образом, при необходимости вам придется самостоятельно реализовывать выражения для работы с неделями. Далее в этой главе мы покажем один пример
подобных вычислений.
Расчет разницы по сравнени с предыду им периодом
Одной из распространенных операций при работе со временем является расчет разницы между нынешним значением меры и ее значением в предыдущем году. Эта разница может быть выражена как в абсолютных величинах, так
и в процентах. Вы уже видели, как можно получить прошлогоднее значение
меры при помощи функции
:
PY Sales := CALCULATE ( [Sales Amount]; SAMEPERIODLASTYEAR ( 'Date'[Date] ) )
Чтобы вычислить разницу между двумя значениями меры
в абсолютном выражении, достаточно воспользоваться простой арифметической
операцией вычитания. При этом необходимо добавить проверку, чтобы показывать разницу между значениями только в случае присутствия обоих. Здесь
лучше будет воспользоваться переменными, дабы не вычислять одну меру
дважды. Меру
(от «year-over-year» – «по сравнению с предыдущим
годом») можно определить так:
YOY Sales :=
VAR CySales = [Sales Amount]
VAR PySales = [PY Sales]
VAR YoySales =
IF (
NOT ISBLANK ( CySales ) && NOT ISBLANK ( PySales );
CySales - PySales
)
RETURN
YoySales
Чтобы вычислить разницу между значениями нарастающим итогом текущего года по сравнению с предыдущим, необходимо задействовать меры
и
, которые мы определили ранее:
YTD Sales := TOTALYTD ( [Sales Amount]; 'Date'[Date] )
PY YTD Sales :=
CALCULATE (
[Sales Amount];
DATESYTD ( SAMEPERIODLASTYEAR ( 'Date'[Date] ) )
)
YOY YTD Sales :=
VAR CyYtdSales = [YTD Sales]
огика о ера и со вре ене
275
VAR PyYtdSales = [PY YTD Sales]
VAR YoyYtdSales =
IF (
NOT ISBLANK ( CyYtdSales ) && NOT ISBLANK ( PyYtdSales );
CyYtdSales - PyYtdSales
)
RETURN
YoyYtdSales
Зачастую в отчетах разницу между годами лучше выводить не в абсолютных
значениях, а в процентах, рассчитать которые в нашем случае можно, поделив
меру
на
. За точку отсчета мы будем принимать прошлогодний показатель продаж, причем разница 100 будет означать двукратное увеличение значения за год. В следующем выражении мы рассчитаем меру
при помощи функции
, чтобы избежать ошибки деления на ноль
в строках, для которых отсутствует прошлогодний показатель:
YOY Sales% := DIVIDE ( [YOY Sales]; [PY Sales] )
Похожая формула будет и для расчета разницы в процентах применительно
к нарастающим итогам с начала года. Назовем эту меру
:
YOY YTD Sales% := DIVIDE ( [YOY YTD Sales]; [PY YTD Sales] )
На рис. 8.18 показан отчет со всеми рассчитанными мерами.
Рис 8 18
от ете оказан все ер , сравнива
ие два года
Расчет скользя ей годовой суммы
Еще одним распространенным экономическим показателем, не учитывающим сезонные изменения, является скол
а годова сумма (moving annual
total – MAT). Этот показатель учитывает агрегацию значения за последние
12 месяцев. В главе 7 мы говорили о методике расчета скользящего среднего.
276
огика о ера и со вре ене
Здесь мы хотели бы показать формулу для расчета похожего значения при помощи функций логики операций со временем.
Например, рассчитаем скользящую годовую сумму продаж (мера
)
для марта 2008 года, сложив показатели продажи с апреля 2007 года по март
2008-го. Сделать это можно при помощи функции
. Функция
возвращает список дат, входящих в заданный интервал. При
этом единицей измерения могут быть года, кварталы, месяцы или дни.
MAT Sales :=
CALCULATE (
[Sales Amount];
DATESINPERIOD (
'Date'[Date];
MAX ( 'Date'[Date] );
-1;
YEAR
)
)
-- Рассчитываем меру в новом контексте фильтра,
-- измененном следующим аргументом фильтра.
-- Возвращает таблицу, содержащую
-- значения Date[Date]
-- начиная с последней видимой даты
-- и заканчивая датой, отстоящей
-- от нее на год назад
Использование функции
обычно идеально подходит при
расчете скользящей годовой суммы. В образовательных целях полезно будет
рассмотреть и другие техники получения такого же результата. Вот еще один
вариант расчета меры
:
MAT Sales :=
CALCULATE (
[Sales Amount];
DATESBETWEEN (
'Date'[Date];
NEXTDAY ( SAMEPERIODLASTYEAR ( LASTDATE ( 'Date'[Date] ) ) );
LASTDATE ( 'Date'[Date] )
)
)
Эта реализация меры требует особого рассмотрения. В формуле используется функция
, возвращающая список дат переданного столбца в рамках указанных дат. Поскольку функция
работает на
уровне дней, даже если в отчете используются месяцы, второй и третий параметры функции должны быть выражены в днях. Чтобы получить последнюю
дату, можно использовать функцию
. Функция
похожа на
, но вместо значения она возвращает таблицу, которая впоследствии может быть передана в качестве параметра другой функции логики операций со
временем. Получив последнюю видимую дату, мы переносим ее на год назад
при помощи функции
, после чего берем следующий
день, применив функцию
.
Одна из проблем показателя скользящей годовой суммы заключается в том,
что при его расчете используется агрегация в виде суммирования. Чтобы получить средние значения, необходимо разделить получившиеся суммы на количество месяцев, включенных в интервал. Так мы получим скол
у среднего
дову сумму (moving annual average – MAA):
огика о ера и со вре ене
277
MAA Sales :=
CALCULATE (
DIVIDE ( [Sales Amount]; DISTINCTCOUNT ( 'Date'[Year Month] ) );
DATESINPERIOD (
'Date'[Date];
MAX ( 'Date'[Date] );
-1;
YEAR
)
)
Как видите, используя функции логики операций со временем, можно проводить довольно сложные расчеты. На рис. 8.19 приведен отчет, в котором выведены и скользящая годовая сумма, и скользящая среднегодовая сумма.
Рис 8 19 Мер MAT Sales и MAA Sales легко реализовать
с о о ь
нк и логики о ера и со вре ене
Выбор порядка вложенности функций логики операций
со временем
При работе со вложенными функциями логики операций со временем очень
важно выбрать правильный порядок иерархии. В предыдущем примере мы использовали следующее выражение для извлечения первого дня для интервала
при расчете скользящей годовой суммы:
NEXTDAY ( SAMEPERIODLASTYEAR ( LASTDATE ( 'Date'[Date] ) ) )
и
Такого же результата можно добиться, поменяв местами функции
:
SAMEPERIODLASTYEAR ( NEXTDAY ( LASTDATE ( 'Date'[Date] ) ) )
278
огика о ера и со вре ене
Результат почти всегда будет одинаковым, но в последнем случае мы рискуем получить неправильную дату в конце периода. Мера для вычисления
скользящей годовой суммы со следующей формулой может выдавать неправильные результаты:
MAT Sales Wrong :=
CALCULATE (
[Sales Amount];
DATESBETWEEN (
'Date'[Date];
SAMEPERIODLASTYEAR ( NEXTDAY ( LASTDATE ( 'Date'[Date] ) ) );
LASTDATE ( 'Date'[Date] )
)
)
Эта версия формулы будет давать ошибочные цифры на верхней границе
периода. Это легко увидеть в отчете, представленном на рис. 8.20.
Рис 8 20 Мера MAT Sales Wrong в дает не равильн
рез льтат в кон е 200 года
Вплоть до 30 декабря 2009 года все значения меры верны, однако в последний день года сумма оказалась сильно завышена. Дело в том, что для 31 декабря 2009 года функция
должна вернуть таблицу, содержащую 1 января
2010 года, но не может этого сделать по причине отсутствия такой даты в нашем календаре. В результате функция
возвращает пустую таблицу.
Функция
, получив на вход пустую таблицу, также возвращает пустую таблицу. А поскольку функция
ожидает на
вход скалярную величину, пустота, сгенерированная функцией
, будет воспринята ей как значение
. В свою очередь,
,
приведенный к типу
, дает ноль, что, как мы знаем, соответствует
дате 30 декабря 1899 года. Следовательно, функция
возвратит
все даты из календаря, поскольку пустое значение в качестве нижней даты не
устанавливает никаких ограничений на интервал. Как итог мы получаем совершенно неправильный результат.
огика о ера и со вре ене
279
Решение здесь очень простое и состоит в использовании правильного порядка вложенности функций. Если первой будет вызвана функция
, то из 31 декабря 2009 года мы перенесемся ровно на год назад – в 31 декабря 2008 года. А применение функции
к этой дате даст
вполне корректное значение 1 января 2009 года, которое присутствует в нашей
таблице дат.
Как правило, все функции логики операций со временем возвращают набор
дат, существующих в календаре. Если конкретной даты в календаре нет, результатом работы функции будет пустая таблица, которая трансформируется
в
. В некоторых сценариях такое поведение функций может приводить
к непредсказуемым результатам, как показано в данном разделе. При расчете скользящей годовой суммы более быстро и безопасно отработает функция
, но описанные в этом разделе нюансы могут пригодиться вам
при комбинировании функций логики операций со временем во время вычисления других показателей.
Знакомство с полуаддитивными вычислениями
Техника агрегирования значений по разным временным интервалам, которую
вы изучили, прекрасно работает с обычными аддитивными мерами. ддитив
ной (additive measure) называется мера, вычисления по которой могут агрегироваться простой операцией суммирования при срезе по любому атрибуту.
Давайте для примера возьмем меру, отражающую сумму продаж. Сумма продаж по всем покупателям будет равна арифметической сумме всех продаж по
каждому отдельному покупателю. Также верно и то, что сумма продаж за год
составляется из продаж за каждый день этого года. В аддитивных мерах нет
ничего особенного – их очень легко понять и использовать.
Но не все вычисления являются аддитивными. Существуют и неаддитив
н е мер (non-additive measure). Примером такой меры может служить количество уникальных полов покупателей. Для каждого отдельного покупателя
результат будет составлять 1. Если вычислять это значение для группы покупателей, итог никогда не будет превышать количество полов (в базе Contoso
это три: пустое значение, M (Male) и F (Female)). Таким образом, итог по группе покупателей, дат или любому другому атрибуту не может быть рассчитан
путем простого суммирования индивидуальных значений. Неаддитивные
меры – частые гости в отчетах, и в большинстве случаев они характеризуют
как раз уникальное количество чего бы то ни было. Неаддитивные меры труднее понять и использовать по сравнению с традиционными аддитивными. Но
есть и третий – гораздо более сложный – тип аддитивности, именуемый полуаддитивностью.
Полуаддитивн е мер (semi-additive measure) характеризуются одним типом агрегации (обычно это сумма) по одним столбцам и другим (например,
значением на последнюю дату) – по другим. Отличным примером такой меры
может являться баланс банковского счета. Баланс всех клиентов банка можно рассчитать путем суммирования индивидуальных балансов. В то же время
баланс клиента за год не является суммой балансов по месяцам. Вместо этого
280
огика о ера и со вре ене
он равняется сумме баланса на последнюю дату года. Таким образом, срез балансов по покупателям агрегируется стандартным способом, тогда как срез по
дате требует совершенно иного подхода. Посмотрите для примера на рис. 8.21.
Рис 8 21
раг ент данн
о ол аддитивно
ере
По этому отчету мы видим, что баланс на счете Кэти Джордан (Katie Jordan)
на конец января составлял 1687,00, тогда как по истечении февраля он увеличился до 2812,00. Рассматривая два месяца вместе, мы не можем суммировать
балансы по ним. Вместо этого мы берем последний доступный баланс. С другой стороны, суммарный баланс клиентов на конец января можно рассчитать
путем сложения балансов по всем трем клиентам.
Если использовать простую операцию суммирования для всех вычислений
по этой мере, может получиться отчет, показанный на рис. 8.22.
Рис 8 22
десь
види два ти а итогов итоги о дата
дл каждого клиента и итоги о все клиента в ра ка разн
вре енн
ериодов
Как видите, значения по месяцам показаны правильно. Но по кварталам
и годам производится обычное суммирование балансов, что не может нас
огика о ера и со вре ене
281
устраивать. Правильный отчет продемонстрирован на рис. 8.23, здесь по атрибуту времени всегда берется последнее известное значение.
Рис 8 23
тот от ет оказ вает равильн е зна ени
Работать с полуаддитивными мерами бывает непросто как раз из-за разных
подходов к агрегации значений по разным осям и необходимости обращать
внимание на все нюансы. В следующих разделах мы познакомимся с базовыми
техниками взаимодействия с полуаддитивными вычислениями.
Использование функций LASTDATE и LAST
BLA
DAX предлагает сразу несколько функций для удобной работы с полуаддитивными мерами. Но в обращении с такими нестандартными вычислениями найти нужную функцию – это только полдела. Здесь необходимо уделять
внимание самым незначительным нюансам в расчетах. В данном разделе мы
продемонстрируем разные версии одного и того же кода, которые будут или
не будут работать в зависимости от данных. Неправильные решения мы показываем исключительно в образовательных целях. К тому же в более сложных
сценариях к правильному варианту решения нужно идти поэтапно.
Первой функцией, с которой мы познакомимся, будет
. Мы уже
использовали эту функцию ранее при расчете скользящей годовой суммы.
Функция
возвращает таблицу из одной строки, содержащей последнюю видимую дату в текущем контексте фильтра. Будучи использованной
в качестве аргумента фильтра в функции
,
перезаписывает контекст фильтра по таблице дат таким образом, чтобы видимой осталась
только последняя дата выбранного временного периода. В следующем фрагменте кода вычисляется последний доступный баланс с использованием функции
, перезаписывающей контекст фильтра по таблице
:
LastBalance :=
CALCULATE (
SUM ( Balances[Balance] );
282
огика о ера и со вре ене
LASTDATE ( 'Date'[Date] )
)
Функция
очень проста в использовании, но, к сожалению, она не
годится для многих полуаддитивных расчетов. Фактически эта функция сканирует таблицу дат, всегда возвращая последнюю видимую дату. Например, на
уровне месяца она всегда вернет последний день месяца, а на уровне квартала – последний день квартала. Если на этот день нет данных в рассчитываемой
мере, результат будет пустым значением. Посмотрите на рис. 8.24. Здесь мы не
видим результатов по третьему кварталу (Q3), а также общих итогов. Поскольку мера
по третьему кварталу вернула пустое значение, этот квартал даже не отображается в отчете, что приводит в некоторое замешательство.
Рис 8 24 От ет с нк ие LASTDATE вл етс не олн
если на оследн
дат ес а нет ин ор а ии
,
Если вместо месяца на нижнем уровне отчета использовать группировку по
дням, проблема использования функции
станет еще более очевидной, как видно по рис. 8.25. Третий квартал теперь стал видим, но значения по
нему отсутствуют.
В случае если в отчете могут присутствовать данные на предпоследний
день даты, а на последний – отсутствовать, лучше будет использовать функцию
. Функция
представляет собой итератор,
сканирующий таблицу и возвращающий последнее значение, для которого
второй параметр возвращает непустое значение. В нашем примере мы используем функцию
для сканирования таблицы
в поисках
последней даты, для которой присутствовало значение в таблице
:
LastBalanceNonBlank :=
CALCULATE (
SUM ( Balances[Balance] );
LASTNONBLANK (
'Date'[Date];
COUNTROWS ( RELATEDTABLE ( Balances ) )
)
)
огика о ера и со вре ене
283
Рис 8 25
ри срезе о дн данн е рис тств т на нижне
но на ровн агрега ии
види
ст е зна ени
ровне от ета,
На уровне месяца функция
проходит по всем дням месяца
и для каждого из них проверяет таблицу
на присутствие значений.
Внутренняя функция
выполняется в контексте строки итератора
, а значит, возвращает строки только по текущей дате. Если
баланса для этой даты не было, результатом вычисления будет пустая таблица,
и функция
вернет пустое значение. В итоге на выходе функция
выдаст последнюю дату, для которой значение второго параметра было не
.
Если по всем клиентам балансы заполнены на одинаковые даты, функция
отработает идеально. Но в нашем примере для одного и того
же месяца балансы по разным клиентам указаны для разных дат, и это создает
определенные проблемы. Как мы уже говорили в начале раздела, в случае с полуаддитивными мерами дьявол кроется в деталях. Функция
справляется с задачей гораздо лучше, чем
, поскольку выполняет поиск последней даты с непустым значением. Но для расчета итоговых показателей она не годится, что видно по рис. 8.26.
По каждому отдельному клиенту расчеты выглядят правильными. И действительно, последний известный баланс на счету Кэти Джордан составлял
2531,00, и именно эта сумма была помещена в итоговой строке. Для Луиса Бонифаца (Luis Bonifaz) и Маурицио Маканьо (Maurizio Macagno) мы также видим
правильные цифры. И все же итоговые значения не совсем верны. Общая итоговая цифра в отчете показывает значение 1950,00, что совпадает с балансом
Маурицио Маканьо. Согласитесь, довольно странно выглядит, когда в итоговой
строке отчета показаны три значения (2531,00, 2205,00 и 1950,00), а в общем
итоге присутствует только последнее из них.
Причину такого поведения отчета объяснить непросто. Когда контекст
фильтра включает в себя Кэти Джордан, последней датой с актуальным значением является 15 июля. В случае с Маурицио Маканьо такой датой будет уже
18 июля. А когда в контексте фильтра не присутствуют клиенты вовсе, послед284
огика о ера и со вре ене
ней датой будет 18 июля, что совпадает с датой последнего актуального баланса Маурицио Маканьо. Ни у Кэти Джордан, ни у Луиса Бонифаца на 18 июля
баланс не значится. Таким образом, для июля формула покажет только значение по Маурицио.
Рис 8 26 От ет о ти равильн
о рос в з ва т только строки итогов и третьего квартала
Как часто и бывает, в данном случае в поведении DAX нет никаких ошибок.
Проблема лишь в том, что наш код пока не учитывает тот факт, что для разных
клиентов в нашей модели данных могут присутствовать разные последние
даты баланса.
В зависимости от наших требований формула может быть поправлена
разными способами. Для начала нужно определиться с тем, что показывать
в строке итогов. Учитывая то, что на 18 июля в модели есть только частичные
данные, можно:
„ принять 18 июля как последнюю дату для всех клиентов вне зависимости
от того, какие последние даты для каждого из них. Таким образом, если
у клиента нет данных на конкретную дату, значит, у него был нулевой
баланс;
„ учитывать последнюю дату для каждого отдельного клиента и агрегировать итоги, исходя из этого. В этом случае актуальным балансом клиента
будет считаться последний доступный для него баланс.
Оба определения правильны, и здесь все зависит от требований отчета. А раз
так, мы рассмотрим написание кода для обоих вариантов. Наиболее простым
из них является тот, в котором последней датой считается та, на которую есть
какие-то данные, вне зависимости от клиентов. Для этого варианта нам лишь
слегка придется изменить поведение функции
:
LastBalanceAllCustomers :=
VAR LastDateAllCustomers =
CALCULATETABLE (
огика о ера и со вре ене
285
LASTNONBLANK (
'Date'[Date];
COUNTROWS ( RELATEDTABLE ( Balances ) )
);
ALL ( Balances[Name] )
)
VAR Result =
CALCULATE (
SUM( Balances[Balance] );
LastDateAllCustomers
)
RETURN
Result
Здесь мы использовали функцию
для очистки фильтра по
клиенту при вычислении выражения с функцией
. В этом случае для строки общих итогов функция
всегда будет возвращать 18 июля – не учитывая текущего клиента в контексте фильтра. В результате итоговые балансы Кэти Джордан и Луиса Бонифаца остались пустыми, что
видно по рис. 8.27.
Рис 8 27
с ользование едино оследне дат дл все клиентов
ривело к разн
рез льтата в итогово строке
Второй вариант требует чуть больших пояснений. Когда мы используем
свою последнюю дату для каждого клиента, итоговые значения не могут быть
рассчитаны, просто исходя из действующего контекста фильтра на уровне
итогов. Формула должна вычислять подытоги для каждого клиента и агрегировать итоги. Это тот самый случай, когда применение итерационной функции
является самым простым и эффективным решением. В следующем примере
внешний итератор
используется для того, чтобы суммировать промежуточные итоги по клиентам:
286
огика о ера и со вре ене
LastBalanceIndividualCustomer :=
SUMX (
VALUES ( Balances[Name] );
CALCULATE (
SUM ( Balances[Balance] );
LASTNONBLANK (
'Date'[Date];
COUNTROWS ( RELATEDTABLE ( Balances ) )
)
)
)
Эта мера рассчитывает последнюю дату индивидуально для каждого клиента. После этого происходит агрегирование значений. Результат работы меры
можно видеть на рис. 8.28.
Рис 8 28
е ерь итоги дл каждого клиента в вод тс на сво
оследн
дат
Примечание
ри оль о коли естве клиентов ера LastBalanceIndividualCustomer ожет в исл тьс не
ективно ри ина в то , то в ор ле содержитс два вложенн
итератора, и вне ни отли аетс оль е гран л рность Более
ективн
и
ст
р
од од к то зада е дет о исан в главе 10, и закл аетс он в ис ользовании
нк ии TREATAS, котор
о с ди далее в данно книге
Как вы поняли, сложность в обращении с полуаддитивными мерами кроется
вовсе не в коде, а в выборе подхода к таким мерам. Когда этот выбор сделан,
написать правильную формулу не представляет большого труда.
В данном разделе мы остановились на двух наиболее распространенных
функциях для работы с полуаддитивными мерами:
и
. Есть еще две похожие функции для получения первой даты периода,
а не последней. Их названия
и
. Существуют и друогика о ера и со вре ене
287
гие функции, целью которых является упрощение расчетов в сценариях, подобных описанному выше. Мы поговорим о них в следующем разделе.
Работа с остатками на начало и конец периода
Язык DAX предлагает множество функций вроде
, облегчающих получение значений на начало и конец определенного периода. И хотя эти функции довольно полезны, у всех из них есть определенные ограничения, которые мы описывали в предыдущем разделе. В общем, они нормально работают
только при наличии данных для всех без исключения дат.
Речь идет о функциях
,
,
и соответствующих им аналогах
,
и
. Как ясно из названия, функция
всегда возвращает
1 января выбранного года в текущем контексте фильтра.
и
дадут начало квартала и месяца соответственно.
В качестве примера мы подготовили другой сценарий, в котором будут использованы полуаддитивные меры. В демонстрационном файле содержатся
цены на акции Microsoft в период с 2013 по 2018 год. Цены актуальны для каждого дня. Но какие значения будут показываться на других уровнях? Скажем,
для квартала? Обычно в таких случаях выводят последнюю цену акций за период. В общем, и здесь нас ждет работа с полуаддитивными мерами.
Простая реализация с получением последней цены акций за период прекрасно работает в несложных отчетах. Следующая формула вычисляет последнюю цену за выбранный период, усредняя ее в случае наличия нескольких
строк за один день:
Last Value :=
CALCULATE (
AVERAGE ( MSFT[Value] );
LASTDATE ( 'Date'[Date] )
)
Мера хорошо отработает на дневном графике цен на акции, пример которого показан на рис. 8.29.
Но в том, что график так приятно выглядит, нет нашей заслуги как разработчиков кода. Просто мы вынесли даты на ось X, и клиентский инструмент –
в данном случае Power BI – неплохо поработал над тем, чтобы проигнорировать
пустые значения в нашем наборе данных. В результате мы получили непрерывную линию на графике. Но если ту же меру вынести в область значений
в матрице со срезом по году и месяцу, мы увидим пропуски в ячейках, как
показано на рис. 8.30.
Использование функции
подразумевает возможность появления
пустых ячеек, в случае если в последний день конкретного месяца не было значения в таблице. А это может быть, если этот день приходился на выходные или
праздничные дни. Правильная формула для меры
будет выглядеть
так:
Last Value :=
CALCULATE (
288
огика о ера и со вре ене
AVERAGE ( MSFT[Value] );
LASTNONBLANK (
'Date'[Date];
COUNTROWS ( RELATEDTABLE ( MSFT ) )
)
)
Рис 8 29
ине н
гра ик с ежедневн
Рис 8 30 Матри а о года и ес
и ена и на ак ии в гл дит в е атл
а содержит
ст е
е
е ки
Если к таким формулам подходить со всей осторожностью, результаты не
станут для вас неожиданными. Что бы мы делали, если бы нам понадобилось
рассчитать прирост цен на акции Microsoft с начала квартала? Можно было бы
написать такой код, который будет ошибочным:
SOQ :=
CALCULATE (
огика о ера и со вре ене
289
AVERAGE ( MSFT[Value] );
STARTOFQUARTER ( 'Date'[Date] )
)
SOQ% :=
DIVIDE (
[Last Value] - [SOQ];
[SOQ]
)
Функция
возвращает дату начала текущего квартала вне
зависимости от того, есть ли значение в эту дату. Например, начало первого
квартала – дата 1 января, являющаяся праздничным днем. Соответственно,
цен на акции на 1 января просто не бывает, и предыдущая мера выдаст результат, показанный на рис. 8.31.
Рис 8 31
нк и STARTOFQUARTER вернет дат на ала квартала
вне зависи ости от того, л ли тот день раздни н
Легко заметить, что для первого квартала значение меры
не вычислено.
И такая проблема будет в каждом квартале, который начинается с нерабочего
дня. Чтобы получить значения на начало или конец периода, но учитывать при
этом только даты, для которых заполнены данные, следует прибегнуть к помощи функций
и
в сочетании с другими функциями логики операций со временем, такими как
.
Гораздо лучшей реализацией меры
будет следующая:
SOQ :=
VAR FirstDateInQuarter =
CALCULATETABLE (
290
огика о ера и со вре ене
FIRSTNONBLANK (
'Date'[Date];
COUNTROWS ( RELATEDTABLE( MSFT ) )
);
PARALLELPERIOD ( 'Date'[Date]; 0; QUARTER )
)
VAR Result =
CALCULATE (
AVERAGE ( MSFT[Value] );
FirstDateInQuarter
)
RETURN
Result
Этот код, конечно, труднее понять и написать. Зато работать он будет вне зависимости от отсутствия каких-либо данных в исходной таблице. Вывод новой
версии меры
можно видеть на рис. 8.32.
Рис 8 32
ова верси ер SOQ в водит данн е
вне зависи ости от рас ределени в одн и раздни н
дне
Не боясь показаться излишне занудными, мы просто вынуждены еще раз
повторить концепцию работы с полуаддитивными мерами, которую описывали в начале раздела. Дьявол кроется в деталях. Язык DAX предлагает множество функций для работы с датами, но они изящно работают только в случае присутствия данных в таблице для всех без исключения дат. К сожалению,
в реальности это далеко не всегда так. И в таких сценариях нужно очень осторожно подходить к использованию стандартных функций логики операций
со временем. Скорее, эти функции можно рассматривать как кирпичики для
составления более сложных выражений. Комбинируя функции для работы со
огика о ера и со вре ене
291
временем, можно производить очень тонкие расчеты, учитывающие все особенности конкретной модели данных.
Именно поэтому, вместо того чтобы дать вам по одному работающему примеру для каждой функции, мы предпочли провести вас через всю логику при
написании сложных вычислений. Цель этой главы – и всей книги в целом – состоит не в том, чтобы показать, как пользоваться функциями. Мы хотим, чтобы
вы научились мыслить категориями DAX, самостоятельно определять нюансы
того или иного вычисления и производить расчеты даже тогда, когда стандартных средств языка для этого оказывается недостаточно.
В следующем разделе мы сделаем еще один шаг вперед в этом направлении
и покажем, как можно производить сложные вычисления в области временных периодов без применения функций логики операций со временем. Цель
на этот раз будет не только образовательная. Дело в том, что, работая с нестандартными календарями, например с недельным, вы не сможете воспользоваться специальными функциями DAX. Так что вы должны быть готовы к тому,
что вам придется писать сложный код без помощи функций логики операций
со временем.
Усовершенствованные методы работы с датой
и временем
В данном разделе мы обсудим важные особенности работы функций логики
операций со временем. Чтобы углубиться в эту тему, мы напишем несколько
вычислений с использованием стандартных функций языка DAX, таких как
,
,
,
и
. Цель этого раздела состоит не в том, чтобы
отговорить вас от использования специальных функций для работы со временем в пользу стандартных. Наоборот, мы сделаем все, чтобы вы как можно
глубже разобрались в принципах их работы на конкретных примерах. Это поможет вам в будущем самостоятельно писать достаточно сложные формулы,
даже если для этого будет не хватать стандартного набора функций. Также вы
увидите, что переход на стандартные функции DAX зачастую может приводить
к значительному увеличению объема кода, поскольку в специальных функциях логики операций со временем многие действия от вас просто скрыты.
Умение писать вычисления для работы с датами и временем при помощи
традиционных функций DAX пригодится вам при работе с нестандартными
календарями, в которых первым днем года может быть отнюдь не 1 января.
В частности, вы сможете свободно работать с недельными календарями стандарта
(International Organization for Standardization). В этом случае ваши
обычные представления о том, что год, месяц и квартал могут быть легко вычислены по конкретной дате, не будут иметь ничего общего с реальностью. Вы
вправе корректировать логику выражений по своему усмотрению путем изменения условий в фильтрах, а можете воспользоваться помощью дополнительных столбцов в таблице дат, чтобы чересчур не усложнять итоговую формулу.
Примеры такого подхода вы найдете далее в этом разделе, когда мы будем обсуждать использование нестандартных календарей.
292
огика о ера и со вре ене
Вычисления нараста
им итогом
Ранее мы уже описывали работу специальных функций, предназначенных
для вычисления показателей нарастающим итогом с начала месяца, квартала и года –
,
и
соответственно. Результат этих
функций очень напоминает результат выполнения стандартной функции
с определенным набором параметров. Возьмем, к примеру, функцию
:
DATESYTD ( 'Date'[Date] )
В расширенном виде эту функцию можно заменить функцией
енной в
, как показано ниже:
, встро-
CALCULATETABLE (
VAR LastDateInSelection = MAX ( 'Date'[Date] )
RETURN
FILTER (
ALL ( 'Date'[Date] );
'Date'[Date] <= LastDateInSelection
&& YEAR ( 'Date'[Date] ) = YEAR ( LastDateInSelection )
)
)
Или взять функцию
:
DATESMTD ( 'Date'[Date] )
Этот код легко меняется на следующий:
CALCULATETABLE (
VAR LastDateInSelection = MAX ( 'Date'[Date] )
RETURN
FILTER (
ALL ( 'Date'[Date] );
'Date'[Date] <= LastDateInSelection
&& YEAR ( 'Date'[Date] ) = YEAR ( LastDateInSelection )
&& MONTH ( 'Date'[Date] ) = MONTH ( LastDateInSelection )
)
)
Не стоит уточнять, что и функция
работает по похожему шаблону. Все альтернативные варианты действуют примерно одинаково: извлекают
информацию о годе, месяце или квартале из последней даты в текущем выборе, а затем используют полученное значение для построения подходящего
фильтра.
Преобразование контекста и функции логики операций со временем
, наверное, за етили, то в ред д и
раг ента кода
везде закл али все
в ражение в о ра л
нк и CALCULATETABLE то делалось дл того, то инииировать рео разование контекста, то нео оди о в сл ае, если дата казана в каестве сс лки на стол е анее в данно главе
же говорили, то казание стол а
огика о ера и со вре ене
293
с дато в ка естве ервого ара етра нк ии логики о ера и со вре ене авто атиески ведет к ол ени та ли
те вложенного в зова нк и CALCULATETABLE
и DISTINCT:
DATESYTD ( 'Date'[Date] )
рео раз етс в
DATESYTD ( CALCULATETABLE ( DISTINCT ( 'Date'[Date] ) ) )
аки о разо , рео разование контекста возникает только дл еревода стол а
в та ли
того не роис одит, если в ка естве ара етра в нк и
ередан не столе , а та ли а ак то олее то н аналог нк ии DATESYTD дет след
и
DATESYTD ( 'Date'[Date] )
Он рео раз етс в
VAR LastDateInSelection =
MAXX ( CALCULATETABLE ( DISTINCT ( 'Date'[Date] ) ); [Date] )
RETURN
FILTER (
ALL ( 'Date'[Date] );
'Date'[Date] <= LastDateInSelection
&& YEAR ( 'Date'[Date] ) = YEAR ( LastDateInSelection )
)
рео разование контекста не роис одит, если в ка естве ара етра в
гики о ера и со вре ене ередаетс та ли а
нк и
ло-
Функция
, генерируемая при передаче ссылки на столбец,
необходима в случае, если есть активный контекст строки. Взгляните на следующие два вычисляемых столбца, созданных в таблице
:
'Date'[CountDatesYTD] = COUNTROWS ( DATESYTD ( 'Date'[Date] ) )
'Date'[CountFilter] =
COUNTROWS (
VAR LastDateInSelection =
MAX ( 'Date'[Date] )
RETURN
FILTER (
ALL ( 'Date'[Date] );
'Date'[Date] <= LastDateInSelection
&& YEAR ( 'Date'[Date] ) = YEAR ( LastDateInSelection )
)
)
И хотя они выглядят похоже, это на самом деле не так. Результат вычисления
этих столбцов виден на рис. 8.33.
возвращает количество дней с начала года до даты в текущей
строке. Чтобы добиться такого результата, функции
нужно проанализировать текущий контекст фильтра и извлечь выбранный период. Однако,
поскольку речь идет о вычисляемых столбцах, контекста фильтра у нас нет. Поведение столбца
объяснить проще. При вычислении максимальной
294
огика о ера и со вре ене
даты всегда возвращается последняя дата из календаря, поскольку в контексте
фильтров нет никаких фильтров.
ведет себя иначе, ведь функция
инициирует преобразование контекста по причине передачи
в нее столбца с датами. Таким образом, создается контекст фильтра с одной
текущей датой из итерации.
Рис 8 33
стол е CountFilter
не в олн етс рео разование контекста,
а в стол е CountDatesYTD в олн етс
Если вы используете функцию
, но при этом знаете, что код не
будет запускаться внутри контекста строки, можете убрать внешнюю функцию
, которая в этом случае будет просто не нужна. Это характерно для аргумента фильтра в функции
, вызванной не внутри итератора, – где обычно используется функция
. В таких случаях вместо
функции
можно написать:
VAR LastDateInSelection = MAX ( 'Date'[Date] )
RETURN
FILTER (
ALL ( 'Date'[Date] );
'Date'[Date] <= LastDateInSelection
&& YEAR ( 'Date'[Date] ) = YEAR ( LastDateInSelection )
)
С другой стороны, чтобы получить дату из контекста строки, например в вычисляемом столбце, можно извлечь эту дату из текущей строки в переменную
вместо использования функции
:
VAR CurrentDate = 'Date'[Date]
RETURN
FILTER (
ALL ( 'Date'[Date] );
'Date'[Date] <= CurrentDate
&& YEAR ( 'Date'[Date] ) = YEAR ( CurrentDate )
)
огика о ера и со вре ене
295
Функция
позволяет указать дату окончания года, что бывает полезно для вычислений нарастающим итогом в финансовых календарях. Например, если финансовый год начинается 1 июля, в качестве даты окончания
года можно указать дату 30 июня одним из следующих способов:
DATESYTD ( 'Date'[Date]; "06-30" )
DATESYTD ( 'Date'[Date]; "30-06" )
Чтобы не закладываться на региональные настройки, можно использовать
функцию
вместо
следующим образом:
VAR LastDateInSelection = MAX ( 'Date'[Date] )
RETURN
FILTER (
ALL ( 'Date'[Date] );
'Date'[Date] > DATE ( YEAR ( LastDateInSelection ) - 1; <месяц>; <день> )
&& 'Date'[Date] <= LastDateInSelection
)
Важно
ео оди о о нить, то де ствие нк ии DATESYTD всегда на инаетс с дат ,
след
е за казанно дато окон ани
инансового года ро ле
ог т возникн ть, когда инансов
год ко ании на инаетс 1 арта акти ески на ало года
в то сл ае ожет ть как 2
еврал , так и 2
еврал в зависи ости от того, високосн год или нет о состо ни на а рель 201 года нк и DATESYTD тот с енари
корректно не о ра ат вает аки о разо , в одел ко ани , инансов
год котор на инаетс 1 арта, нк и DATESYTD ри ен ть не след ет С о одн
те дл
то ро ле
ожно ознако итьс о адрес http://sql.bi/fymarch.
Функция DATEADD
Функция
извлекает набор дат, смещенный во времени на определенное количество интервалов. При анализе контекста фильтра функция
определяет, является ли текущий выбор месяцем или другим особым
периодом, как, например, начало или конец месяца. Допустим, если функция
используется для смещения полного месяца на квартал назад, часто
в итоговом наборе дат будет больше строк, чем в текущем выборе. Это происходит из-за того, что функция определяет, что выбран ровно месяц, а значит,
и вернуть нужно месяц с определенным смещением вне зависимости от того,
сколько в нем будет дней.
Такое особое поведение функции
описывается при помощи трех
правил, о которых мы расскажем в данном разделе. С учетом этих правил будет
очень затруднительно написать замену для функции
для обобщенной таблицы дат. Код получится крайне сложным, и поддерживать его будет
почти невозможно. Функция
использует только значения столбца
с датами, извлекая из него необходимую информацию вроде года, квартала
или месяца. Эту логику было бы очень трудно реализовать стандартными средствами DAX. С другой стороны, воспользовавшись дополнительными столбцами таблицы
, можно написать альтернативную версию функции
.
296
огика о ера и со вре ене
Мы рассмотрим эту технику позже в данной главе – в разделе с пользовательскими календарями.
Сейчас же посмотрим на следующую формулу:
DATEADD ( 'Date'[Date]; -1; MONTH )
Близкий, но не точный аналог этого выражения на языке DAX будет выглядеть так:
VAR OffsetMonth = -1
RETURN TREATAS (
SELECTCOLUMNS (
CALCULATETABLE ( DISTINCT ( 'Date'[Date] ) );
"Date"; DATE (
YEAR ( 'Date'[Date] );
MONTH ( 'Date'[Date] ) + OffsetMonth;
DAY ( 'Date'[Date] )
)
);
'Date'[Date]
)
Примечание
ред д е
ри ере и в др ги
ор ла из то глав
ис ольз е
нк и TREATAS, котора ри ен ет та ли ное в ражение к контекст
ильтра
о стол а , казанн
во второ и остальн
ара етра Ближе с то
нк ие в
ознако итесь в главе 10
Данная формула будет также работать и для января, поскольку при указании
значения для месяца меньше нуля происходит смещение на предыдущий год.
Однако эта реализация будет правильно функционировать, только если в целевом месяце столько же дней, сколько и в текущем. При смещении с февраля на
январь формула пропустит два или три дня в зависимости от года. В то же время если сместиться с марта на февраль, результат может захватить дни марта.
У функции
нет таких проблем, она возвращает целый месяц при
смещении, если в исходном диапазоне был выбран ровно месяц. Для реализации такого поведения функция
следует трем следующим правилам.
. Функция
возвращает только даты, существующие в столбце
с датами. Если ожидаемые даты в ней не найдены, функция вернет только те даты, которые есть в таблице.
2. Если дата отсутствует в соответствующем месяце после операции смещения, то функция
вернет последний день соответствующего
месяца.
3. Если текущий выбор включает в себя два последних дня месяца, то в результат функции
войдут все даты между соответствующими
днями в смещенной таблице и окончанием месяца.
Нескольких примеров будет достаточно, чтобы понять действие перечисленных выше правил. Рассмотрим следующие меры:
осуществляет
подсчет выбранных дней,
подсчитывает количество дней в смеогика о ера и со вре ене
297
щенном месяце, а
.
возвращает диапазон дат, выбранных функцией
Day count :=
COUNTROWS ( 'Date' )
PM Day count :=
CALCULATE ( [Day count]; DATEADD ( 'Date'[Date]; -1; MONTH ) )
PM Range :=
CALCULATE (
VAR MinDate = MIN ( 'Date'[Date] )
VAR MaxDate = MAX ( 'Date'[Date] )
VAR Result =
FORMAT ( MinDate; "MM/DD/YYYY - " ) & FORMAT ( MaxDate; "MM/DD/YYYY" )
RETURN
Result;
DATEADD ( 'Date'[Date]; -1; MONTH )
)
„
ра ило применяется, когда выбор находится рядом с границами диапазона дат, включенных в столбец с датами. Например, по рис. 8.34 видно, что меры
и
возвращают правильные значения
по февралю 2007 года, поскольку даты из января 2007-го присутствуют
в столбце с датами. В то же время эти меры не отрабатывают в январе
2007 года, ведь в нашем столбце с датами нет декабря 2006-го.
Рис 8 34
ранн е дат с е а тс на ес
назад
Главная причина того, почему таблица
должна содержать все даты
в рамках одного года, заключается в особенностях поведения функции
. Всегда помните, что многие функции логики операций со временем внутренне используют функцию
. Таким образом, присутствие в таблице дат всех без исключения дней без пропусков является
залогом правильной работы функций для работы с датами.
298
огика о ера и со вре ене
„
ра ило 2 является также очень важным, поскольку в разных месяцах
разное количество дней. 31-е число присутствует далеко не во всех месяцах. И если этот день выбран в исходном диапазоне, то в смещенном
периоде он будет представлен как последний день месяца. На рис. 8.35
показано, как последние дни марта переносятся на один и тот же последний день февраля, поскольку 29, 30 и 31 февраля в соответствующем году
просто не было.
Рис 8 35
ата, отс тств
а в елево
за ен етс на оследни день ес а
ес
е,
Следствием из этого правила является то, что в итоговом наборе может
оказаться меньше дней, чем в исходном. Это вполне естественно, если
представить смещение с целого марта на февраль, в котором дней всегда меньше, и в итоге мы получим 28 или 29 дней вместо 31. Но когда
в вашем исходном выборе меньше дней, результат может вас несколько
удивить. По рис. 8.36 видно, что пять выбранных дней в марте могут превратиться всего в два дня в феврале.
Рис 8 36
екотор е дни из ис одного в ора ог т ревра атьс
в один и тот же день в рез льтате в олнени
нк ии DATEADD
„
ра ило 3 описывает особый случай, когда в исходную выборку включен
последний день месяца. Например, представьте себе начальный диапазон из трех дней с 29 июня по 1 июля 2007 года. В этой выборке всего три
огика о ера и со вре ене
299
дня, но среди них есть последний день месяца, а именно 30 июня. Когда
функция
смещает диапазон назад, она включает в себя последний день мая, то есть 31 мая. На рис. 8.37 изображен этот случай, и к нему
стоит присмотреться внимательнее. Как вы могли заметить, 30 июня
превратилось в 30 мая. Только если в выборку включаются и 29 июня,
и 30 июня, результирующий набор будет содержать 31 мая. В этом случае
количество дней в предыдущем месяце будет больше, чем в исходном выборе: два выбранных дня в июне 2007 года превратятся в три дня в мае
того же года.
Рис 8 37 ез льтат нк ии DATEADD вкл ает все дат
ежд ерв
и оследни дн и в ранного диа азона осле о ера ии с е ени
Причина для установки этих правил состоит в том, чтобы формулы логики
операций со временем интуитивно понятно работали на уровне месяцев. Как
видно по рис. 8.38, сравнивая показатели по месяцам, мы видим ясную и легкую для понимания картину. Смещенный диапазон включает в себя все дни
предыдущего месяца.
Рис 8 38
300
Мера PM Day count оказ вает коли ество дне в ред д
огика о ера и со вре ене
е
ес
е
Понимание перечисленных выше правил необходимо для того, чтобы справляться со случаями частичного выбора дней в смещенном месяце. Например,
представьте, что вам нужно наложить фильтр на отчет по дням недели. В этот
фильтр могут не быть включены последние дни месяца, что гарантировало
бы выбор полного предыдущего месяца. Кроме того, смещение, выполняемое
функцией
, учитывает количество дней в месяце, а не дней недели.
Применение фильтра к столбцу с датами таблицы
также подразумевает
неявное добавление модификатора
на эту таблицу, что приведет к удалению всех ранее наложенных фильтров на календарь, включая дни недели. Таким образом, срез по дням недели просто несовместим в отчете с функцией
, он попросту выдает неправильный результат.
На рис. 8.39 показан отчет с выводом меры
, отображающей
значение
предыдущего месяца:
PM Sales DateAdd :=
CALCULATE (
[Sales Amount];
DATEADD ( 'Date'[Date]; -1; MONTH )
)
Рис 8 39
не соглас
на ени в ере PM Sales DateAdd
тс с еро Sales Amount о ред д
е
ес
Мера
образует фильтр дней, не соответствующий полному
месяцу. В результате происходит смещение дней выбранного месяца с включением дополнительных дней в конце месяца, согласно правилу 3. Этот фильтр
полностью перезаписывает выбор по
для значений предыдущего месяца. В результате мы получаем неправильные результаты в столбце
– иногда даже большие, чем в мере
, что видно на
примере марта и мая 2007 года.
Чтобы обеспечить правильный результат, здесь нужно провести дополнительные вычисления, как показано ниже в мере
. Мы примеогика о ера и со вре ене
301
няем фильтр к столбцу
, сохраняя при этом фильтр по
и удаляя по всем остальным столбцам таблицы
при помощи функции
. Вычисляемый столбец
представляет собой
сквозной порядковый номер месяца:
Date[YearMonthNumber] =
'Date'[Year] * 12 + 'Date'[Month Number] – 1
PM Sales Weekday :=
VAR CurrentMonths = DISTINCT ( 'Date'[YearMonthNumber] )
VAR PreviousMonths =
TREATAS (
SELECTCOLUMNS (
CurrentMonths;
"YearMonthNumber"; 'Date'[YearMonthNumber] - 1
);
'Date'[YearMonthNumber]
)
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Week Day] );
PreviousMonths
)
RETURN
Result
Результат вычисления меры показан на рис. 8.40.
Рис 8 40
на ени ер PM Sales Weekday
в то ности соответств т и ра из Sales Amount за ред д
и
ес
Однако это решение будет работать только для данного отчета. Если бы дни
были выбраны на основании другого критерия, например по первым шести
302
огика о ера и со вре ене
дням месяца, мера
взяла бы полный месяц, тогда как мера
в этом случае отработала бы корректно. Методы вычислений
напрямую зависят от столбцов, видимых пользователю. Например, в следующей мере
используется функция
для проверки активности
фильтра по столбцу
. Более подробно мы будем говорить о функции
в главе 10.
PM Sales :=
IF (
ISFILTERED ( 'Date'[Day of Week] );
[PM Sales Weekday];
[PM Sales DateAdd]
)
Функции FIRSTDATE, LASTDATE, FIRST
и LAST
BLA
BLA
В разделе по полуаддитивным мерам ранее в данной главе мы уже говорили
о двух похожих функциях:
и
. Эти функции ведут
себя по-разному, как и их аналоги
и
.
Функции
и
оперируют исключительно со столбцом с датами. Они возвращают первую и последнюю даты из текущего контекста фильтра соответственно, игнорируя при этом любые данные в связанных таблицах:
FIRSTDATE ( 'Date'[Date] )
LASTDATE ( 'Date'[Date] )
По сути, функция
просто извлекает из столбца с датами минимальное значение, а
– максимальное. Получается, что функции
и
работают так же, как
и
, за одним существенным отличием:
и
возвращают таблицу и инициируют
преобразование контекста, тогда как
и
возвращают скалярные величины без осуществления преобразования контекста.
Рассмотрим следующее выражение:
CALCULATE (
SUM ( Inventory[Quantity] );
LASTDATE ( 'Date'[Date] )
)
Можно переписать эту формулу с использованием функции
, но код при этом увеличится в объеме:
вместо
CALCULATE (
SUM ( Inventory[Quantity] );
FILTER (
ALL ( 'Date'[Date] );
'Date'[Date] = MAX ( 'Date'[Date] )
)
)
огика о ера и со вре ене
303
Помимо этого, функция
выполняет преобразование контекста.
Так что точным эквивалентом функции
будет следующее выражение:
CALCULATE (
SUM ( Inventory[Quantity] );
VAR LastDateInSelection =
MAXX ( CALCULATETABLE ( DISTINCT ( 'Date'[Date] ) ); 'Date'[Date] )
RETURN
FILTER (
ALL ( 'Date'[Date] );
'Date'[Date] = LastDateInSelection
)
)
Преобразование контекста важно, когда вы пользуетесь функциями
/
в условиях наличия контекста строки. Лучше всего пользоваться функциями
/
при написании выражений фильтров,
поскольку в этом случае ожидается табличное выражение, тогда как функции
/
лучше подойдут при составлении логических выражений в контексте
строки, где обычно требуются скалярные величины. Действительно, функция
, используемая со ссылкой на столбец, подразумевает выполнение
преобразования контекста, что скрывает внешний контекст фильтра.
Например, стоит предпочесть
/
функциям
/
при
использовании в аргументе фильтра функций
/
,
поскольку синтаксис в этом случае будет проще. При этом стоит использовать
функции
/
в тех случаях, когда преобразование контекста в результате
применения функций
/
может повлиять на итоги вычислений. Примером такого использования может быть функция
. Следующее выражение фильтрует даты для расчета промежуточных итогов:
FILTER (
ALL ( 'Date'[Date] );
'Date'[Date] <= MAX ( 'Date'[Date] )
)
Здесь правильно использовать функцию
. Фактически применение
функции
вместо
привело бы к извлечению всех дат вне зависимости от текущего выбора из-за нежелательного преобразования контекста.
Таким образом, следующее выражение всегда будет выдавать полный набор
дат. Это происходит из-за того, что функция
по причине выполнения преобразования контекста возвращает значение
по каждой
строке в итерации функции
:
FILTER (
ALL ( 'Date'[Date] );
'Date'[Date] <= LASTDATE ( 'Date'[Date] ) -- это условие всегда возвращает истину
)
и
Функции
304
и
отличаются от
. По своей сути они являются итераторами, а это означает, что они
огика о ера и со вре ене
проходят по таблице в контексте строки и возвращают последнее (или первое)
значение, для которого второй параметр будет непустым. Обычно во второй
параметр этих функций передается либо мера, либо выражение с использованием функции
для выполнения преобразования контекста.
Чтобы получить правильное значение для последней непустой даты для
конкретной меры/таблицы, необходимо использовать выражение, подобное
следующему:
LASTNONBLANK ( 'Date'[Date]; CALCULATE ( COUNTROWS ( Inventory ) ) )
Эта формула вернет последнюю дату (в текущем контексте фильтра), для которой существуют строки в таблице
. Для этой цели можно использовать и следующую формулировку:
LASTNONBLANK ( 'Date'[Date]; COUNTROWS ( RELATEDTABLE ( Inventory ) ) )
Это выражение вернет последнюю дату (также в текущем контексте фильтра), для которой есть связанные строки в таблице
.
Стоит отметить, что функции
/
могут принимать данные любого типа в качестве первого параметра, тогда как
/
требуют на вход столбец типа
или
. Таким
образом, функции
и
вполне могут быть использованы и с другими таблицами вроде покупателей или товаров, хотя это
встречается редко.
Использование детализации с функциями логики операций
со временем
Детали а и (drillthrough) представляет собой операцию запроса к строкам
источника данных, соответствующим контексту фильтра, используемому
в определенном вычислении. Всегда, когда вы применяете функции логики
операций со временем, вы изменяете контекст фильтра в таблице
. Это
приводит к получению результата меры, отличного от результата ее вычисления в исходном контексте фильтра. Используя клиентское приложение, позволяющее выполнять детализацию в отчете, например Excel с его сводными
таблицами, вы могли наблюдать неожиданное для вас поведение операции
детализации данных. По сути, детализация не учитывает изменения в контексте фильтра, определенном самой мерой. Вместо этого она учитывает только
контекст фильтра, определенный строками, столбцами, фильтрами и срезами
сводной таблицы.
Например, по умолчанию детализация по марту 2007 года всегда будет возвращать одни и те же строки вне зависимости от функций логики операций со
временем, используемых в мере. Если применяется функция
, можно ожидать, что результатом будет общее количество дней с января по март
этого года. От функции
мы будем ждать марта предыдущего года, а от
– строк по 31 марта 2007 года. На самом деле по
умолчанию все перечисленные фильтры всегда будут возвращать строки по
марту 2007 года. Это поведение можно контролировать при помощи свойства
огика о ера и со вре ене
305
(Строки детализации) в модели Tabular. На момент написания данной книги (апрель 2019 года) это свойство доступно в Analysis Services 2017
и Azure Analysis Services, но в Power BI и Power Pivot для Excel оно отсутствует.
В свойстве Detail Rows должен применяться тот же фильтр, что и в соответствующей мере. Например, у нас есть мера, рассчитывающая сумму продаж
нарастающим итогом с начала года:
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
)
Свойство Detail Rows для этой меры должно содержать следующую формулу:
CALCULATETABLE (
Sales;
-- Этим выражением также определяются возвращаемые
-- столбцы
DATESYTD ( 'Date'[Date] )
)
Работа с пользовательскими календарями
Как вы уже знаете, стандартные функции логики операций со временем в DAX
поддерживают только традиционный григорианский календарь. Он базируется на солнечном календаре, разделенном на 12 месяцев, в каждом из которых
свое количество дней. Эти функции удобно применять для анализа данных по
годам, кварталам, месяцам или дням. Но существуют и другие модели, базирующиеся на своих определениях временных периодов. К ним относится, например, недел н й календар с со л дением стандарта
(ISO week date system).
Если вам необходимо работать с нестандартными календарями, вам придется переписать всю логику операций со временем, поскольку специальными
функциями DAX из этой области вы воспользоваться не сможете.
Когда речь заходит о нестандартных календарях, нужно понимать, что их
существует огромное множество, и осветить работу в каждом из них просто
невозможно. Так что мы лишь покажем вам несколько примеров расчетов для
случаев, когда вы не сможете воспользоваться стандартными функциями DAX
для работы со временем.
С целью упрощения вычислений в таких случаях принято переносить часть
бизнес-логики непосредственно в таблицу дат путем создания соответствующих столбцов. Специальные функции логики операций со временем не используют в своей работе другие столбцы из таблицы дат, кроме как столбец
с датами. Это было сделано специально, чтобы язык не зависел от присутствия
дополнительных метаданных для определения года, квартала, месяца или дня,
как это было в MDX и Analysis Services Multidimensional. Будучи владельцем
своей модели данных и кода на DAX, вы можете строить свои собственные
предположения о работе, что позволит значительно упростить код для работы
с нестандартными вычислениями, связанными с датой и временем.
306
огика о ера и со вре ене
В этом заключительном разделе главы мы представим вам несколько примеров с формулами для работы с нестандартными календарями. Если понадобится, вы всегда можете найти больше информации, примеров и готовых
решений в следующих статьях:
„ временные шаблоны: http://www.daxpatterns.com/time-patterns/;
„ работа со временем в недельных календарях: http://sql.bi/isoweeks/.
Работа с неделями
DAX не предоставляет специальных функций логики операций со временем,
адаптированных к работе с недельными календарями. Причина в том, что есть
множество способов и техник определения недель в рамках года и осуществления расчетов внутри недели. Часто неделя может пересекать границы года,
квартала или месяца. Вам необходимо написать собственный код для произведения вычислений при использовании недельных календарей. Например,
в рамках недельного календаря ISO даты 1 и 2 января 2011 года принадлежат
52-й неделе 2010 года, а первая неделя 2011 года начинается только 3 января.
И хотя стандартов существует много, вы можете усвоить общий подход, который пригодится в работе с любым недельным календарем. Суть этого подхода состоит в создании дополнительных вычисляемых столбцов в таблице дат
для хранения соответствий между неделями и годами/кварталами и месяцами, которым они принадлежат. А смена календаря будет означать, что вам необходимо будет просто обновить данные в таблице
, не модифицируя при
этом код мер.
Например, вы можете расширить таблицу дат следующими вычисляемыми
столбцами, чтобы она поддерживала недельный стандарт ISO:
'Date'[Calendar Week Number] = WEEKNUM ( 'Date'[Date]; 1 )
'Date'[ISO Week Number] = WEEKNUM ( 'Date'[Date]; 21 )
'Date'[ISO Year Number] = YEAR ( 'Date'[Date] + ( 3 - WEEKDAY ( 'Date'[Date]; 3 ) ) )
'Date'[ISO Week] = "W" & 'Date'[ISO Week Number] & "-" & 'Date'[ISO Year Number]
'Date'[ISO Week Sequential] = INT ( ( 'Date'[Date] - 2 ) / 7 )
'Date'[ISO Year Day Number] =
VAR CurrentIsoYearNumber = 'Date'[ISO Year Number]
VAR CurrentDate = 'Date'[Date]
VAR DateFirstJanuary = DATE ( CurrentIsoYearNumber; 1; 1 )
VAR DayOfFirstJanuary = WEEKDAY ( DateFirstJanuary; 3 )
VAR OffsetStartIsoYear = - DayOfFirstJanuary + ( 7 * ( DayOfFirstJanuary > 3 ) )
VAR StartOfIsoYear = DateFirstJanuary + OffsetStartIsoYear
VAR Result = CurrentDate - StartOfIsoYear
RETURN
Result
На рис. 8.41 показаны созданные столбцы. Столбец
будет видим для
пользователей, тогда как
останется невидимым и будет использоваться для внутренних вычислений. В столбце
будет
храниться количество дней, прошедших с начала года ISO. С этими вспомогательными столбцами будет гораздо легче производить сравнение различных
временных периодов.
огика о ера и со вре ене
307
Рис 8 41
исл е
е стол
в та ли е дат дл
оддержки недель
Разработчик может строить собственные агрегации нарастающим итогом
с начала года, используя столбец
вместо извлечения года непосредственно из даты. Техника по своей сути останется той же, что и в обсуждении ранее в данной главе. Мы только добавили дополнительную проверку на
выбор одного года ISO перед вызовом функции
:
ISO YTD Sales :=
IF (
HASONEVALUE ( 'Date'[ISO Year Number] );
VAR LastDateInSelection = MAX ( 'Date'[Date] )
VAR YearSelected = VALUES ( 'Date'[ISO Year Number] )
VAR Result =
CALCULATE (
[Sales Amount];
'Date'[Date] <= LastDateInSelection;
'Date'[ISO Year Number] = YearSelected;
ALL ( 'Date' )
)
RETURN
Result
)
На рис. 8.42 показан вывод меры
для начала 2008 года в сравнении с мерой, вычисленной с применением стандартной функции
.
Заметьте, что версия ISO включает в себя 31 декабря 2007 года – дату, входящую в состав 2008 года ISO.
При сравнении показателей с предыдущим годом необходимо сопоставлять соответствующие недели. А поскольку даты при этом могут быть разные,
легче всего использовать другие столбцы таблицы дат для реализации логики
сравнения. Распределение недель внутри года всегда одинаковое, ведь любая
неделя состоит из семи дней. В то же время месяцы насчитывают разное количество дней, а значит, не могут похвастаться такой же универсальностью.
В недельных календарях вы можете упростить вычисления путем поиска тех
же относительных дней в предыдущем году, которые были выбраны в текущем
контексте фильтра.
308
огика о ера и со вре ене
Рис 8 42
ере ISO YTD Sales 1 дека р 200 года
вкл аетс в 200 год
Следующая мера фильтрует текущую выборку дней применительно к предыдущему году. Эта техника также работает, когда в выборку включены полные
недели, поскольку дни здесь выбраны по столбцу
, а не по
дате как таковой.
ISO PY Sales :=
IF (
HASONEVALUE ( 'Date'[ISO Year Number] );
VAR DatesInSelection = VALUES ( 'Date'[ISO Year Day Number] )
VAR YearSelected = VALUES ( 'Date'[ISO Year Number] )
VAR PrevYear = YearSelected - 1
VAR Result =
CALCULATE (
[Sales Amount];
DatesInSelection;
'Date'[ISO Year Number] = PrevYear;
ALL ( 'Date' )
)
RETURN
Result
)
На рис. 8.43 показан отчет с выводом меры
. Справа мы добавили
информацию о продажах 2007 года, чтобы вам было легче понять, как производится выборка данных в мере
.
Обращаться с недельными календарями довольно просто по причине предположений, которые можно сделать по поводу симметрии между одними
и теми же днями в разные годы. С расчетами по месяцам такая логика обычно
несовместима, так что если вам необходимо использовать обе иерархии (месяцы и недели), то придется писать свои расчеты для каждой из них.
Пользовательские вычисления нараста
им итогом
Ранее в данной главе вы узнали, как можно переписать стандартную функцию
, использующуюся для произведения вычисления нарастающим
итогом с начала года. И там мы использовали атрибуты даты, такие как год,
огика о ера и со вре ене
309
из столбца с датами. Когда речь идет о календарях ISO, мы более не можем
полагаться исключительно на столбец с датами. Вместо этого воспользуемся
созданными ранее вычисляемыми столбцами. В этом разделе мы продемонстрируем на примере, как вместо извлечения атрибутов даты использовать
вспомогательные столбцы в таблице дат.
Рис 8 43
ере ISO PY Sales расс итан
о аналоги но неделе
родажи ред д
Рассмотрим стандартную меру
:
его года
YTD Sales :=
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
)
Соответствующий синтаксис для меры в DAX без использования специальных функций логики операций со временем будет выглядеть следующим образом:
YTD Sales :=
VAR LastDateInSelection = MAX ( 'Date'[Date] )
VAR Result =
CALCULATE (
[Sales Amount];
'Date'[Date] <= LastDateInSelection
&& YEAR ( 'Date'[Date] ) = YEAR ( LastDateInSelection )
)
RETURN
Result
310
огика о ера и со вре ене
Если вы используете нестандартный календарь, вам следует заменить функцию
на обращение к созданному столбцу с годом, как показано в следующей мере
:
YTD Sales Custom :=
VAR LastDateInSelection = MAX ( 'Date'[Date] )
VAR LastYearInSelection = MAX ( 'Date'[Calendar Year Number] )
VAR Result =
CALCULATE (
[Sales Amount];
'Date'[Date] <= LastDateInSelection;
'Date'[Calendar Year Number] = LastYearInSelection;
ALL ( 'Date' )
)
RETURN
Result
Можно использовать этот же шаблон для написания вычислений нарастающим итогом с начала квартала или месяца. Единственным отличием будет столбец, к которому вы будете обращаться вместо столбца
:
QTD Sales Custom :=
VAR LastDateInSelection = MAX ( 'Date'[Date] )
VAR LastYearQuarterInSelection = MAX ( 'Date'[Calendar Year Quarter Number] )
VAR Result =
CALCULATE (
[Sales Amount];
'Date'[Date] <= LastDateInSelection;
'Date'[Calendar Year Quarter Number] = LastYearQuarterInSelection;
ALL ( 'Date' )
)
RETURN
Result
MTD Sales Custom :=
VAR LastDateInSelection = MAX ( 'Date'[Date] )
VAR LastYearMonthInSelection = MAX ( 'Date'[Calendar Year Month Number] )
VAR Result =
CALCULATE (
[Sales Amount];
'Date'[Date] <= LastDateInSelection;
'Date'[Calendar Year Month Number] = LastYearMonthInSelection;
ALL ( 'Date' )
)
RETURN
Result
Эти формулы можно использовать как в работе со стандартными календарями (если вы хотите улучшить производительность за счет использования
режима
), так и с пользовательскими (если вы работаете с нестандартными временными периодами).
огика о ера и со вре ене
311
Закл чение
В этой длинной главе вы познакомились с основами применения функций логики операций со временем в DAX. Вот главные моменты, которые вы должны
были усвоить:
„ в Power Pivot и Power BI существуют свои механизмы для автоматического создания таблицы дат. Но пользоваться этой возможностью не стоит,
за исключением случаев, когда вы будете иметь дело с совсем уж простой
моделью данных. Очень важно иметь полный контроль над своими календарями, а упомянутые выше механизмы не позволяют вам производить необходимые изменения в таблицах дат;
„ чтобы создать таблицу дат вручную, достаточно воспользоваться функцией
и написать пару строчек на DAX. Однако стоит потратить время на создание действительно удобной таблицы дат, поскольку в дальнейшем вы сможете использовать ее в своих новых проектах. Вы
также можете загрузить из сети шаблоны для создания таблицы дат;
„ календарь должен быть явным образом помечен как таблица дат для облегчения применения функций логики операций со временем;
„ в DAX существует множество специальных функций логики операций со
временем. Большинство из них возвращают таблицу с датами, которую
в дальнейшем можно использовать в качестве аргумента фильтра функции
;
„ вы должны научиться воспринимать функции логики операций со временем как своеобразные кирпичики для построения комплексных выражений. Сочетая разные функции, вы сможете производить действительно сложные и полезные вычисления со временем;
„ если ваши требования не позволяют воспользоваться стандартными
функциями логики операций со временем, значит, пришло время закатать рукава и написать собственные формулы при помощи традиционных функций языка DAX;
„ в данной книге приведено немало примеров по работе с датой и временем. Но еще больше шаблонов вы найдете по адресу: https://www.daxpatterns.com/time-patterns/.
ГЛ А В А 9
Группы вычислений
В 2019 году произошло серьезное обновление DAX, в рамках которого, помимо
прочего, были представлены так называемые гру
в ислений (calculation
groups). На создание групп вычислений разработчиков DAX вдохновила похожая концепция в языке MDX, известная под названием в исл ем е элемен
т (calculated members). Если вы уже знакомы с этой концепцией, вам будет
куда проще освоить данную тему. И все же между вычисляемыми элементами
в MDX и группами вычислений в DAX есть существенные различия, так что вне
зависимости от имеющихся знаний мы советовали бы вам прочитать, что из
себя представляет эта новинка в DAX и как с ее помощью можно производить
впечатляющие вычисления.
Группы вычислений использовать очень просто. Однако проектирование
модели данных с наличием нескольких групп вычислений и использованием
элементов вычислений в мерах может оказаться затруднительным. Мы постараемся рассказать обо всем по порядку, чтобы вы избежали возможных сложностей. Отклонение от описанного нами пути при разработке модели возможно при очень хорошем понимании концепции групп вычислений.
Группы вычислений – это абсолютная новинка в DAX, и на момент написания книги (апрель 2019 года) эта технология не была закончена и официально
выпущена. На протяжении данной главы мы будем отмечать моменты, которые могут претерпеть изменения в будущих версиях этой концепции. Также
мы советуем вам посетить страницу https://www.sqlbi.com/calculation-groups, где
можно найти обновленный материал данной главы и примеры использования
групп вычислений в DAX.
Знакомство с группами вычислений
Перед тем как дать определение групп вычислений, полезно будет рассмотреть
требования бизнес-аналитики, приведшие к появлению этой концепции. А поскольку вы только что завершили читать главу, касающуюся логики операций
со временем, мы используем пример именно из этой области.
Определим в нашей модели данных следующие меры для расчета суммы
продаж, общих издержек, прибыли и количества проданных товаров:
Sales Amount := SUMX ( Sales; Sales[Quantity] * Sales[Net Price] )
Total Cost := SUMX ( Sales; Sales[Quantity] * Sales[Unit Cost] )
Margin := [Sales Amount] - [Total Cost]
Sales Quantity := SUM ( Sales[Quantity] )
р
в
ислени
313
Все четыре меры очень полезны сами по себе и позволяют проводить анализ
деятельности компании. Более того, все они являются прекрасными кандидатами на вычисление нарастающим итогом. Количество проданных товаров
нарастающим итогом с начала года может быть не менее интересным показателем, чем сумма продаж или сумма прибыли. То же самое касается и других операций работы со временем: вычисление показателя за тот же период
в предыдущем году, процентное изменение по сравнению с предыдущим годом и многое другое.
Но если мы захотим создать такие расчеты для всех наших мер, то общее
количество мер в модели данных очень быстро превысит все мыслимые пределы. Мы бы никому не пожелали пользоваться моделью, общее количество мер
в которой исчисляется сотнями. К тому же большинство мер в нашем случае будут написаны по одному и тому же шаблону – меняться будет только название
меры. Посмотрите для примера на список мер, вычисляющих нарастающий
итог с начала года по четырем указанным выше мерам:
YTD Sales Amount :=
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
)
YTD Total Cost :=
CALCULATE (
[Total Cost];
DATESYTD ( 'Date'[Date] )
)
YTD Margin :=
CALCULATE (
[Margin];
DATESYTD ( 'Date'[Date] )
)
YTD Sales Quantity :=
CALCULATE (
[Sales Quantity];
DATESYTD ( 'Date'[Date] )
)
Все перечисленные меры отличаются лишь базовой мерой, с которой производится действие. А именно накладывается один и тот же фильтр
, в рамках которого вычисляется значение первого параметра функции
. Было бы здорово, если бы у разработчика была возможность написать какое-то общее определение с шаблоном для подстановки нужной
меры:
YTD <Measure> :=
CALCULATE (
<Measure>;
314
р
в
ислени
DATESYTD ( 'Date'[Date] )
)
Этот код не соответствует синтаксису языка DAX, но он дает определенное
представление о том, что из себя представляют элемент в ислени (calculation item). Предыдущий код можно прочитать так: когда нужно в олнит рас
ет мер нараста им итогом с на ала года римените фил тр
к стол у
осле его в ислите меру. В этом и состоит суть элемента
в ислени . Он представляет собой выражение на языке DAX с а лоном а
олнителем (placeholder). Этот шаблон заменяется на переданную меру непосредственно перед вычислением результата. Иными словами, элемент вычисления является разновидностью выражения, которое может быть применено
к любой мере.
Зачастую разработчику может понадобиться производить разного рода вычисления в области логики операций со временем. Как мы уже отмечали в начале раздела, операции расчета нарастающим итогом с начала года или квартала, а также сравнения с аналогичным периодом предыдущего года являются
составной частью единой группы вычислений. И поэтому в DAX представлены
как элементы вычисления, так и группы. ру а в ислений являет собой набор
элементов вычисления, объединенных общей тематикой
Продолжим писать псевдокод на DAX:
CALCULATION GROUP "Time Intelligence"
CALCULATION ITEM CY := <Measure>
CALCULATION ITEM PY := CALCULATE ( <Measure>; SAMPEPERIODLASTYEAR ( 'Date'[Date] ) )
CALCULATION ITEM QTD := CALCULATE ( <Measure>; DATESQTD ( 'Date'[Date] ) )
CALCULATION ITEM YTD := CALCULATE ( <Measure>; DATESYTD ( 'Date'[Date] ) )
Как видите, мы объединили четыре меры, связанные с логикой операций со
временем, в одну группу с названием
. Всего в четырех строчках кода мы, по сути, определили десятки мер, поскольку элементы вычисления могут быть применены к самым разным мерам в модели данных. Таким
образом, создав меру, разработчик автоматически получит в свое распоряжение расчет нарастающего итога с начала года и квартала, а также сравнение
с аналогичным периодом предыдущего года по этой мере.
Вы пока еще не постигли все нюансы групп вычислений, но в данный момент, для того чтобы создать свою первую группу, вам необходимо ответить
только на один вопрос: как пользователь будет выбирать конкретную разновидность операции? Как мы уже говорили, элемент вычисления не является
мерой, это лишь ее разновидность. Так что у пользователя должна быть возможность вставить в отчет конкретную меру с одной или несколькими ее разновидностями. А поскольку пользователи привыкли выбирать в отчеты столбцы из таблиц, группы вычислений было решено реализовать, как если бы они
являлись столбцами в таблице, а элементы вычисления – значениями в этих
столбцах. Следовательно, пользователь может вынести группу вычислений
на столбцы в матрице, чтобы таким образом отобразить различные вариации
меры. Например, ранее описанные элементы вычисления можно расположить
на столбцах матрицы, как показано на рис. 9.1, чтобы провести различные операции над мерой
.
р
в
ислени
315
Рис 9 1 Можно ользоватьс гр
о в ислени так,
как если
она вл лась стол о та ли в одели данн
Создание групп вычислений
Реализация групп вычислений в модели Tabular зависит от пользовательского
интерфейса инструмента разработчика. На момент написания книги (апрель
2019 года) ни Power BI, ни SQL Server Data Tools (SSDT) для Analysis Services не
обладали специальным интерфейсом для групп вычислений, а доступны они
были только через API о ектной модели
(Tabular Ob ect Model – TOM).
Первым инструментом, предоставившим возможность для работы с группами
вычислений, стал Tabular Editor – редактор с открытым исходным кодом, доступный по адресу https://tabulareditor.github.io/.
Чтобы создать группу вычислений при помощи Tabular Editor, необходимо выбрать пункт N
в меню Model. В результате группа будет отображена в модели данных как таблица со специальной иконкой.
На рис. 9.2 видно созданную группу вычислений, которую мы переименовали
в
.
Группа вычислений представляет собой специальную таблицу с единственным столбцом, по умолчанию в Tabular Editor названным
. В нашей модели данных мы переименовали столбец в
, после чего добавили три
элемента вычисления ( D,
D для нарастающих итогов и SPLY для сравнения с аналогичным периодом в предыдущем году), выбрав в контекстном
меню столбца
пункт New
. Каждый элемент вычисления
содержит свое выражение на DAX, как показано на рис. 9.3.
Функция
представляет собой реализацию шаблона
Measure , который мы использовали ранее в псевдокоде DAX. Действительный код DAX для каждого элемента вычисления представлен ниже. Комментарий, предшествующий каждому выражению, идентифицирует соответствующий элемент вычисления.
316
р
в
ислени
Примечание
е всегда в ражать изнес логик
одели данн
ерез ер
огда в одель вкл ен гр
в ислени , клиент
не озвол ет разра от ик
роизводить агрега и стол ов, оскольк гр
в ислени
ог т ть ри енен
только к ера
они не оказ ва т никакого вли ни на нк ии агрегировани , а о ерир т только ера и
Рис 9 2
гр
а в ислени Time Intelligence
ото ражаетс как осо енна та ли а
Рис 9 3
ажд
ле ент в
которое ожно из енить в
ислени содержит в ражение на
,
р
в
ислени
317
--- Calculation Item: YTD
-CALCULATE (
SELECTEDMEASURE ();
DATESYTD ( 'Date'[Date] )
)
--- Calculation Item: QTD
-CALCULATE (
SELECTEDMEASURE ();
DATESQTD ( 'Date'[Date] )
)
--- Calculation Item: SPLY
-CALCULATE (
SELECTEDMEASURE ();
SAMEPERIODLASTYEAR ( 'Date'[Date] )
)
С таким определением пользователю будет представлена новая таблица
с единственным столбцом
, содержащим три значения:
D,
D и SPLY. Пользователь имеет право создавать срезы по этому
столбцу или выносить его на строки или столбцы визуализации, как если бы он
представлял собой обычный столбец в таблице. Например, если пользователь
выберет значение
D, движок применит элемент вычисления
D к любой
мере, которая находится в отчете. На рис. 9.4 показана матрица, в которую вынесена мера
. А поскольку в срезе выбрана только вариация меры
D, в отчете показаны нарастающие итоги меры с начала года.
Рис 9 4
огда ользователь в ирает TD, зна ени дл
расс ит ва тс нараста и итого с на ала года
318
р
в
ислени
ер в от ете
Если в том же отчете пользователь выберет разновидность SPLY, результаты
будут другими, что видно по рис. 9.5.
Рис 9 5
ор SPL ривел к из енени зна ени в ере Sales Amount
те ерь они редставл т оказатели, с е енн е на год назад
Если оставить столбец в срезе без выбора или выбрать сразу несколько значений, движок не будет предпринимать никаких действий с мерой, оставив ее
неизменной, что видно по рис. 9.6.
Рис 9 6
огда ни одно зна ение в срезе не в
ера оказ ваетс ез из енени
рано,
Примечание
оведение гр
в ислени ез в ора зна ени или со ножественн
в оро
ожет из енитьс в д е
а а рель 201 года оведение ер со ножественн
в оро и ез в ора одинаковое о в д и верси
то ожет из енитьс
на ри ер, ри ножественно в оре ожет о вл тьс о и ка
р
в
ислени
319
Однако группы вычислений способны на гораздо большее. В начале раздела мы представили вам четыре меры:
,
,
и
. Было бы здорово, если бы пользователь мог выбирать, какую метрику
показывать, а не только вид применяемого вычисления. В этом случае мы могли бы построить общий отчет с четырьмя возможными метриками по месяцу
и году, предоставив выбор конкретной метрики пользователю. Отчет должен
выглядеть примерно так, как показано на рис. 9.7.
Рис 9 7
от ете оказан рас ет TD, ри ененн к ере Margin,
но ользователь в раве в рать л ое др гое со етание ер и в ислени
В примере, показанном на рис. 9.7, пользователь решил посмотреть значение прибыли нарастающим итогом с начала года. Но он также может просматривать любые комбинации двух групп вычислений:
и
.
Чтобы построить этот отчет, мы создали дополнительную группу вычислений
, вместившую в себя элементы вычисления
,
,
и
. В выражении для каждого элемента вычисления прописана просто ссылка на соответствующую меру, как показано на рис. 9.8 на
примере элемента вычисления S
A
.
Когда в модели данных присутствует несколько групп вычислений, очень
важно определить порядок, в котором они будут применяться движком DAX.
За этот аспект отвечает свойство
: первой применяется группа с максимальным значением этого свойства. Для достижения желаемого результата
мы увеличили значение свойства
у группы вычислений
до 10, как показано на рис. 9.9. Таким образом, движок применяет группу
вычислений
раньше, чем
, свойство
которой
осталось нулевым по умолчанию. Позже в данной главе мы обсудим порядок
применения групп вычислений более подробно.
В следующем коде DAX представлены все элементы вычисления в группе
вычислений Metric:
--- Calculation Item: Margin
--
320
р
в
ислени
[Margin]
--- Calculation Item: Sales Amount
-[Sales Amount]
--- Calculation Item: Sales Quantity
-[Sales Quantity]
--- Calculation Item: Total Cost
-[Total Cost]
Рис 9 8 р
ав
кажд из котор
ислени Metric вкл ает в се
редставл ет соответств
ет ре ле ента в
ер
ислени ,
В этих элементах вычисления оригинальные меры не изменяются, они
представлены в исходном виде. Чтобы добиться такого поведения, достаточно
не указывать в коде элемента функцию
. Эта функция очень
часто применяется в элементах вычисления, но это отнюдь не обязательно.
Последний пример также полезен для демонстрации одного из многих нюансов, связанных с использованием групп вычислений. Если пользователь выберет метрику
, в отчете будет показано количество проданных товаров в таком же формате (с двумя знаками после запятой), как и для других
метрик. А поскольку исходная мера
характеризуется целочисленным
р
в
ислени
321
типом, было бы удобно удалить эти знаки после запятой или изменить форматирование. Ранее мы уже говорили, что присутствие в выражении нескольких
групп вычислений требует указания порядка их применения, как было показано в предыдущем примере. Это лишь одно из многих правил, которых следует
придерживаться для создания эффективных групп вычислений.
Примечание
сли в ис ольз ете Analysis Services, о ните, то до авление гр
в ислени тре ет о новлени соответств
е е та ли , то
ле ент в ислени
оказались види
ользовател
то не са ое о евидное тре ование, ведь раз е ение
ново
ер , до сти , не н ждаетс в одо но о новлении она становитс видио ользовател сраз осле со ранени
о оскольк гр
и ле ент в ислени
редставлен на клиенте в виде та ли и стол ов, осле и раз е ени нео оди о
за стить о ера и о новлени , то загр зить вн тренние данн е в та ли и стол такое о новление дет роизводитьс авто ати ески ри о о и ользовательского интер е са, но то только на и до сл , оскольк на о ент на исани
книги в то инстр енте гр
в ислени не ли редставлен
Рис 9 9 Сво ство Precedence о редел ет, в како
д т ри ен тьс к ере
ор дке гр
в
ислени
Знакомство с группами вычислений
В предыдущем разделе мы сосредоточились на использовании групп вычислений и их создании при помощи Tabular Editor. Сейчас же мы подробнее познакомимся со свойствами и поведением групп и элементов вычислений.
Существует два вида сущностей: группы вычислений и элементы вычисления. Группа вычислений представляет собой коллекцию элементов вычисле322
р
в
ислени
ния, объединенную по выбранному пользователем критерию. Обе сущности
обладают своим набором свойств, которые корректно должны быть установлены разработчиком. Сейчас мы познакомим вас с этими свойствами, а в оставшейся части главы представим несколько подробных примеров использования
групп и элементов вычислений.
Группа вычислений является довольно простой сущностью, определяемой
следующими свойствами:
„ названием группы или свойством Name. Под этим именем таблица, представляющая группу вычислений, будет представлена на клиенте;
„ очередностью применения группы вычислений к мерам, то есть свойством
. При наличии множества активных групп вычислений
это число используется для определения порядка, в котором группы вычислений будут применяться к мерам;
„ свойством Name для атрибута группы вычислений. Это название будет
дано столбцу с элементами вычисления, отображаемому на клиенте.
Элемент вычисления – сущность чуть более сложная, и она включает следующие свойства:
„ название элемента вычисления (Name). Это свойство характеризует значение, которое будет присутствовать в столбце. По сути, элемент вычисления представлен в группе вычислений одной строкой;
„ выражение элемента вычисления (
). Выражение на языке DAX,
которое может включать специальные функции вроде
. Это выражение определяет, как будет применяться к мере элемент
вычисления;
„ порядок сортировки элементов вычисления (
). Этим свойством
определяется, как элементы вычисления будут отсортированы при представлении пользователю, и напоминает сортировку по столбцу в модели
данных. По состо ни на а рел
года это свойство недосту но но
должно т реали овано к в оду рели а;
„ строка форматирования (
S
). Если не указана, то будет унаследована от базовой меры. Если же модификатор меняет вычисление,
можно переопределить строку форматирования меры этим свойством.
Свойство
является очень важным, поскольку позволяет добиться предсказуемого поведения от мер в модели данных, соответствующего
применяемому к ним элементу вычисления. Представьте, что в вашей группе
вычислений есть два элемента вычисления, выполняющих операции со временем: YOY (year-over-year) представляет разницу в значениях между выбранным периодом и аналогичным периодом предыдущего года, а YOY% выводит
разницу YOY между периодами в процентном отношении:
--- Calculation Item: YOY
-VAR CurrYear =
SELECTEDMEASURE ()
VAR PrevYear =
CALCULATE (
р
в
ислени
323
SELECTEDMEASURE ();
SAMEPERIODLASTYEAR ( 'Date'[Date] )
)
VAR Result =
CurrYear - PrevYear
RETURN Result
--- Calculation Item: YOY%
-VAR CurrYear =
SELECTEDMEASURE ()
VAR PrevYear =
CALCULATE (
SELECTEDMEASURE ();
SAMEPERIODLASTYEAR ( 'Date'[Date] )
)
VAR Result =
DIVIDE (
CurrYear - PrevYear;
PrevYear
)
RETURN Result
Применение этих элементов вычисления в отчете дает правильные результаты, но если не переопределить свойство
для элемента YOY%,
то его значение будет выводиться с двумя знаками после запятой, а не в виде
процента, как показано на рис. 9.10.
Рис 9 10
ле ент в ислени
YOY и YOY% о лада т те же
ор атирование , то и ера
Sales Amount
На рис. 9.10 видно, что элемент вычисления YOY унаследовал строку форматирования от исходной меры
, что в данном случае вполне приемлемо. Что касается элемента YOY%, было бы логичнее выводить его в отчет не с десятичными знаками, а в виде процента. То есть для января хотелось
бы видеть не –0,12, а –12 . Таким образом, нам необходимо переопределить
строку форматирования для этого столбца, чтобы она не зависела от исходной
324
р
в
ислени
меры. Чтобы добиться желаемого результата, нужно в свойстве
элемента вычисления YOY% выбрать проценты и тем самым переопределить
базовые правила форматирования меры. Результат показан на рис. 9.11. Если
свойство
не задано для элемента вычисления, будет использовано
существующее значение.
Рис 9 11
ле енте в ислени YOY%
ерео редел етс строка ор атировани
ер Sales Amount
При этом свойство
может быть задано как в фиксированном
виде, так и – для более сложных случаев – в виде выражения DAX, возвращающего строку форматирования. В последнем случае допустимо обращаться
к строке форматирования текущей меры при помощи функции SELECTEDMEASUREFORMATSTRING. Например, если в модели данных есть мера, возвращающая выбранную в данный момент валюту, и вы хотите добавить в отображении символ соответствующей валюты, вы можете использовать для этого
следующий код:
SELECTEDMEASUREFORMATSTRING () & " " & [Selected Currency]
Настройка строк форматирования элементов вычисления может быть очень
полезна для сохранения привычного восприятия модели данных пользователем. При этом разработчик должен помнить, что строка форматирования элемента вычисления будет распространяться на все меры, используемые с этим
элементом. Кроме того, при наличии нескольких групп вычислений в отчете
результат этого свойства также будет зависеть от очередности применения
групп, о чем мы поговорим в следующем разделе.
Применение лемента вычисления
До сих пор в своих объяснениях относительно того, что из себя представляют
элементы вычисления, мы не углублялись в детали. Причина этого в том, что
сначала мы хотели показать, как работает эта концепция на практике – без
р
в
ислени
325
лишних подробностей, которые могли вас отвлечь от основной идеи. Мы сказали о том, что элементы вычисления могут применяться пользователями –
скажем, путем их включения в срезы. При наличии активного в текущем контексте фильтра элемента вычисления он фактически заменяет собой исходную
меру. По сути, вместо вычисления меры происходит вычисление выражения,
прописанного в соответствующем элементе.
Представьте, что у вас есть следующий элемент вычисления:
--- Calculation Item: YTD
-CALCULATE (
SELECTEDMEASURE ();
DATESYTD ( 'Date'[Date] )
)
Чтобы применить элемент вычисления в выражении, необходимо отфильтровать группу вычислений. Можно создать фильтр путем вызова функции
, как в следующем примере. Именно эту технику используют клиентские инструменты в срезах и элементах визуализации:
CALCULATE (
[Sales Amount];
'Time Intelligence'[Time calc] = "YTD"
)
В группах вычислений нет ничего магического – это просто таблицы, а значит, они могут быть отфильтрованы функцией
, как и все другие
таблицы. Когда функция
применяет фильтр к элементу вычисления, DAX использует определение этого элемента для переопределения выражения, после чего запускает его на выполнение.
Таким образом, основываясь на определении указанного элемента вычисления, предыдущий код будет интерпретироваться следующим образом:
CALCULATE (
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
)
)
Примечание
о вн тренне
нк ии CALCULATE до сти о ис ользовать
нк и
ISFILTERED дл роверки в ождени ле ента в ислени в ильтр на е
ри ере
рали вне н
ильтра и в в ражении дл ростот вос ри ти , то
оказать,
то ле ент в ислени же
л ри енен ри то
ле ент в ислени со ран ет
свои ильтр , и в дальне и одв ражени
о режне
ожет ть в олнена заена ер
Несмотря на кажущуюся простоту при использовании в простых сценариях,
элементы вычисления таят в себе определенные сложности. Применение эле326
р
в
ислени
мента вычисления выполняет замену исходной меры на выражение элемента.
Обратите внимание на формулировку: в олн ет амену ис одной мер . А без
меры элемент вычисления не выполняет никаких преобразований. Например,
в следующей формуле не будет применяться элемент вычисления, поскольку
в ней нет ссылки на меру:
CALCULATE (
SUMX ( Sales; Sales[Quantity] * Sales[Net Price] );
'Time Intelligence'[Time calc] = "YTD"
)
В этом случае элемент вычисления не будет выполнять никаких преобразований, поскольку в первом параметре функции
отсутствует ссылка
на меру. Таким образом, после применения элемента вычисления будет выполнен следующий код:
CALCULATE (
SUMX ( Sales; Sales[Quantity] * Sales[Net Price] )
)
Если выражение в функции
будет содержать сразу несколько
ссылок на меры, все они будут заменены на определение элемента вычисления. Например, следующая мера
содержит сразу две ссылки на
меры:
и
:
CR YTD :=
CALCULATE (
DIVIDE (
[Total Cost];
[Sales Amount]
);
'Time Intelligence'[Time calc] = "YTD"
)
Чтобы получить код, который будет выполнен, необходимо все ссылки на
меры заменить на определение соответствующего элемента вычисления, как
показано в мере
:
CR YTD Actual Code :=
CALCULATE (
DIVIDE (
CALCULATE (
[Total Cost];
DATESYTD ( 'Date'[Date] )
);
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
)
)
)
р
в
ислени
327
В данном случае результат выполнения этого кода будет эквивалентен следующему фрагменту меры
, чуть более понятному:
CR YTD Simplified :=
CALCULATE (
CALCULATE (
DIVIDE (
[Total Cost];
[Sales Amount]
);
DATESYTD ( 'Date'[Date] )
)
)
Все три меры вернут одинаковые результаты, как показано на рис. 9.12.
Рис 9 12
Мер CR YTD, CR YTD Actual Code и CR YTD Simplified да т одинаков е и р
Но вам необходимо соблюдать предельную осторожность, поскольку формула меры
не в точности соответствует коду, сгенерированному
элементом вычисления, который показан в мере
. Да, в данном конкретном случае эти меры эквивалентны. Но в более сложных сценариях
результаты будут совершенно разными, и понять причину этих различий может
быть очень непросто. Давайте рассмотрим пару примеров. В первом из них мера
будет содержать две вложенные функции
: во
внешней будет устанавливаться фильтр на 2008 год, а во внутренней – на 2009-й:
Sales YTD 2008 2009 :=
CALCULATE (
CALCULATE (
[Sales Amount];
'Date'[Calendar Year] = "CY 2009"
);
'Time Intelligence'[Time calc] = "YTD";
'Date'[Calendar Year] = "CY 2008"
)
328
р
в
ислени
Также в фильтре внешней функции
указан элемент вычисления
D. Но при этом выражение не будет изменено, поскольку оно не содержит
напрямую ни одной ссылки на меру. В результате функция
применяет элемент вычисления, но это не ведет ни к каким изменениям в коде.
Обратите внимание на то, что ссылка на меру
находится в области видимости внутренней функции
. Применение элемента вычисления оказывает влияние на меры в текущей области видимости контекста
фильтра и никак не затрагивает меры во вложенных функциях. Эти меры могут
быть преобразованы своей функцией
(или эквивалентным кодом
вроде функции
либо преобразования контекста), в которой
может присутствовать, а может и не присутствовать тот же фильтр с указанием
элемента вычисления.
Когда внутренняя функция
применяет свой контекст фильтра,
она не меняет статус фильтра элемента вычисления. Таким образом, элемент
вычисления сохраняет свое место в фильтре и будет сохранять его, пока другая
функция
не изменит его. Здесь ситуация такая же, как если бы мы
имели дело с обычным столбцом. Во внутренней функции
присутствует ссылка на меру, и DAX применяет к ней элемент вычисления. Результирующий код показан ниже в мере
:
Sales YTD 2008 2009 Actual Code :=
CALCULATE (
CALCULATE (
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
);
'Date'[Calendar Year] = "CY 2009"
);
'Date'[Calendar Year] = "CY 2008"
)
Результаты вычисления этих двух мер показаны на рис. 9.13. Выбор на срезе
слева распространяется на матрицу посередине, в которой размещены меры
и
При этом выбор года CY
2008 перекрывается годом CY 2009. Это легко проверить, взглянув на матрицу
справа, где показан вывод меры
, трансформированной элементом вычисления
D по годам CY 2008 и CY 2009. Цифры в средней матрице
соответствуют столбцу CY 2009 справа.
Функция
применяется в тот момент, когда контекст фильтра
установлен на 2009 год, а не на 2008-й. Несмотря на то что элемент вычисления
стоит в фильтрах рядом с 2008 годом, в действительности его применение происходит в другом контексте фильтра, а именно во внутренней функции. Такое
поведение выглядит не совсем логично, если не сказать больше. И чем сложнее
выражение будет использоваться внутри функции
, тем труднее будет понять, как это все работает.
Такое поведение элементов вычисления наводит на мысль о том, что использовать их для модификации выражения надо в том и только в том случае,
если выражение само по себе является ссылкой на меру. Предыдущий пример
р
в
ислени
329
мы использовали, чтобы продемонстрировать вам это важное правило, а теперь рассмотрим более сложный сценарий. В следующей формуле рассчитаем
количество рабочих дней в тех месяцах, когда были продажи:
SUMX (
VALUES ( 'Date'[Calendar Year month] );
IF (
[Sales Amount] > 0; -- Ссылка на меру
[# Working Days]
-- Ссылка на меру
)
)
Рис 9 13 Мер Sales YTD 2008 2009 и Sales YTD 2008 2009 Actual Code
да т одинаков е рез льтат
Это вычисление может быть полезным для расчета меры
в отношении к рабочим дням по месяцам, когда совершались продажи. В следующем примере это выражение используется в составе более сложной формулы:
DIVIDE (
[Sales Amount]; -- Ссылка на меру
SUMX (
VALUES ( 'Date'[Calendar Year month] );
IF (
[Sales Amount] > 0; -- Ссылка на меру
[# Working Days]
-- Ссылка на меру
)
)
)
Если это выражение использовать внутри функции
, отфильтрованной по элементу вычисления
D, получим следующую меру, которая будет выдавать неожиданные результаты:
Sales WD YTD 2008 :=
CALCULATE (
DIVIDE (
[Sales Amount]; -- Ссылка на меру
330
р
в
ислени
SUMX (
VALUES ( 'Date'[Calendar Year month] );
IF (
[Sales Amount] > 0; -- Ссылка на меру
[# Working Days]
-- Ссылка на меру
)
)
);
'Time Intelligence'[Time calc] = "YTD";
'Date'[Calendar Year] = "CY 2008"
)
Можно было бы предположить, что эта мера вычисляет сумму продаж в расчете на количество рабочих дней с учетом только тех месяцев, когда были продажи. Иначе говоря, итоговый код можно было бы представить так:
Sales WD YTD 2008 Expected Code :=
CALCULATE (
CALCULATE (
DIVIDE (
[Sales Amount]; -- Ссылка на меру
SUMX (
VALUES ( 'Date'[Calendar Year month] );
IF (
[Sales Amount] > 0; -- Ссылка на меру
[# Working Days]
-- Ссылка на меру
)
)
);
DATESYTD ( 'Date'[Date] )
);
'Date'[Calendar Year] = "CY 2008"
)
Вы, наверное, заметили, что мы все три меры внутри выражения отметили
специальными комментариями. И это не случайно. Применение элемента вычисления происходит к ссылке на меру, а не ко всему выражению. А значит,
итоговый код с заменой мер на элементы вычисления, активные в текущем
контексте фильтра, будет таким:
Sales WD YTD 2008 Actual Code :=
CALCULATE (
DIVIDE (
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
);
SUMX (
VALUES ( 'Date'[Calendar Year month] );
IF (
CALCULATE (
[Sales Amount];
р
в
ислени
331
DATESYTD ( 'Date'[Date] )
) > 0;
CALCULATE (
[# Working Days];
DATESYTD ( 'Date'[Date] )
)
)
)
;
'Date'[Calendar Year] = "CY 2008"
)
Эта последняя версия формулы будет совершенно неправильно считать рабочие дни, фактически суммируя нарастающие итоги по количеству рабочих
дней с начала года для всех месяцев в текущем контексте фильтра. Понятно,
что правильных результатов от такого алгоритма можно не ждать. По одному
выбранному месяцу результат оказался правильным (просто повезло), но по
кварталу или году ошибка в расчетах более чем очевидна. Это хорошо заметно
на рис. 9.14.
Рис 9 14
ез льтат разн
верси
ер Sales WD о все квартала 200 года
Мера
возвращает правильные результаты
по кварталам, тогда как в мерах
и
цифры оказались занижены. И это неудивительно, поскольку количество
рабочих дней в знаменателе в этих мерах рассчитывается путем сложения нарастающих итогов с начала года по всем месяцам в выбранном периоде.
Можно легко избежать проблем с этим, если придерживаться главного правила: использовать функцию
с элементами вычисления только для
мер. При написании корректной меры
мы включили
в функцию
только одну меру, вычисленную заранее:
--- Мера Sales WD
-Sales WD :=
DIVIDE (
[Sales Amount];
SUMX (
VALUES ( 'Date'[Calendar Year month] );
IF (
[Sales Amount] > 0;
332
р
в
ислени
[# Working Days]
)
)
)
--- Мера Sales WD YTD 2008 Fixed
-- Новая версия меры Sales WD YTD 2008 с применением элемента вычисления YTD
-Sales WD YTD 2008 Fixed :=
CALCULATE (
[Sales WD];
-- Ссылка на меру
'Time Intelligence'[Time calc] = "YTD";
'Date'[Calendar Year] = "CY 2008"
)
В этом случае код, сгенерированный в результате применения элемента вычисления, получится гораздо более интуитивно понятным:
Sales WD YTD 2008 Fixed Actual Code :=
CALCULATE (
CALCULATE (
[Sales WD];
DATESYTD ( 'Date'[Date] )
);
'Date'[Calendar Year] = "CY 2008"
)
Из данного примера видно, что фильтр с функцией
применяется
ко всему выражению в целом, что ведет к образованию кода, ожидаемого от
применения элемента вычисления. Результаты вычисления мер
и
также показаны на рис. 9.14.
Для простейших вычислений допустимо отходить от сформулированного
выше главного правила использования элементов вычисления. Но при этом
нужно дважды подумать о возможных последствиях, поскольку при усложнении выражения велика вероятность того, что оно начнет выдавать неправильные результаты.
При использовании клиентских инструментов вроде Power BI вам не придется беспокоиться об этих деталях. В таких приложениях производится принудительная проверка того, что элементы вычисления применяются правильно, и в итоговом запросе все действия производятся исключительно с одной
мерой. Но вам как разработчику DAX придется неоднократно использовать
элементы вычисления в качестве фильтра в функции
. И когда вы
это делаете, обращайте внимание на выражение, используемое в качестве первого параметра функции. Если хотите, чтобы функция
гарантированно выдавала правильные результаты, позаботьтесь о том, чтобы в выражении всегда находилась ссылка на меру. Никогда не используйте
с выражениями.
Наконец, мы советуем вам изучать элементы вычисления путем переписывания готовых выражений функции
. Это поможет вам лучше понять, что происходит в движке DAX.
р
в
ислени
333
Очередность применения групп вычислений
В предыдущем разделе мы говорили о том, что элемент вычисления следует
применять исключительно к мерам. При этом есть возможность применить
к одной мере сразу несколько элементов. Несмотря на то что в каждой группе
вычислений может быть активен только один элемент, присутствие нескольких групп может позволить применить к мере больше одного элемента вычисления. Это возможно, когда пользователь работает со множественными
срезами по разным группам вычисления или когда в функции
присутствуют фильтры по элементам вычисления из разных групп. В начале главы
мы определили две группы вычислений: одну для определения базовой меры,
а вторую – для расчета, связанного с логикой операций со временем, который
должен быть применен к базовой мере.
Если в текущем контексте фильтра активны несколько элементов вычисления, важно определиться с тем, какой из них будет применяться первым. Для
этого необходимо определить правила очередности их применения. В DAX
при наличии нескольких групп вычислений обязательно требуется установить
свойство
для каждой из них. В данном разделе мы рассмотрим на
примерах, как правильно устанавливать это свойство, и увидим, как при этом
будут меняться результаты.
Для подготовки демонстрации мы создали две группы вычислений, в каждой из которых присутствует по одному элементу вычисления:
-------------------------------------------------------- Calculation Group: 'Time Intelligence'[Time calc]
--------------------------------------------------------- Calculation Item: YTD
-CALCULATE (
SELECTEDMEASURE ();
DATESYTD ( 'Date'[Date] )
)
-------------------------------------------------------- Calculation Group: 'Averages'[Averages]
--------------------------------------------------------- Calculation Item: Daily AVG
-DIVIDE (
SELECTEDMEASURE ();
COUNTROWS ( 'Date' )
)
D представляет собой обычный нарастающий итог с начала года, а D
A
рассчитывает среднедневное значение путем деления значения меры на
количество дней в текущем контексте фильтра. Оба элемента вычисления пре334
р
в
ислени
красно работают, как видно по рис. 9.15, где мы использовали две меры для
индивидуального применения элементов:
YTD :=
CALCULATE (
[Sales Amount];
'Time Aggregation'[Aggregation] = "YTD"
)
Daily AVG :=
CALCULATE (
[Sales Amount];
'Averages'[Averages] = "Daily AVG"
)
Рис 9 15 Мер Daily AVG и YTD ра ота т равильно
ри ри енении ле ентов в ислени к ера отдельно
Но сценарий значительно усложнится, если использовать два элемента вычисления одновременно. Взгляните на следующее определение меры
:
Daily YTD AVG :=
CALCULATE (
[Sales Amount];
'Time Intelligence'[Time calc] = "YTD";
'Averages'[Averages] = "Daily AVG"
)
К мере применяются оба элемента вычисления одновременно, что порождает конфликт очередности их применения. Должен ли движок DAX сначала
применить элемент
D, а затем D
A , или наоборот? Иными словами,
какое из указанных ниже выражений в результате должно образоваться?
р
в
ислени
335
--- Сначала применяется YTD, а затем DIVIDE
-DIVIDE (
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
);
COUNTROWS ( 'Date' )
)
--- Сначала применяется DIVIDE, а затем YTD
-CALCULATE (
DIVIDE (
[Sales Amount];
COUNTROWS ( 'Date' )
);
DATESYTD ( 'Date'[Date] )
)
Вероятно, правильным будет второй вариант. Но без специальных на то указаний DAX не может сам сделать правильный выбор. А значит, разработчик
должен ему в этом помочь.
Очередность применения элементов вычисления напрямую зависит от значений свойства
соответствующих им групп. Элемент из группы вычислений с наибольшим значением очередности будет применяться первым,
а все остальные – следом за ним в порядке убывания очередности. На рис. 9.16
показан результат неправильного расчета со следующими выбранными параметрами:
„ группа вычислений
–
: 0;
„ группа вычислений
–
: 10.
Рис 9 16
336
ере Daily YTD AVG в
р
в
исл
ислени
тс не равильн е рез льтат
Очевидно, что по всем месяцам, кроме января, мера
показывает некорректные значения. Давайте разберемся, что же произошло. Очередность группы вычислений
установлена в 10, а значит, эта группа будет вступать в действие первой. Применение элемента вычисления D
A
приводит к следующему вычислению:
CALCULATE (
DIVIDE (
[Sales Amount];
COUNTROWS ( 'Date' )
);
'Time Intelligence'[Time calc] = "YTD"
)
В этот момент DAX активирует элемент вычисления
D из группы вычислений
. Применение элемента
D приводит к изменению
единственной меры в этой формуле, а именно
. Соответственно,
результирующий код меры
получится таким:
DIVIDE (
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
);
COUNTROWS ( 'Date' )
)
Следовательно, итоговый результат будет получен путем деления значения
меры
, вычисленного под действием элемента вычисления
D,
на количество дней в выбранном месяце. Например, значение меры для декабря было получено путем деления 9 353 814,87 ( D, примененный к
) на 31 (количество дней в декабре). Но правильный результат должен
быть гораздо меньше, поскольку элемент
D необходимо применять как
к числителю, так и к знаменателю функции
, используемой в элементе
вычисления D
A .
Чтобы решить эту задачу, нужно сначала применить элемент
D, а затем
D
A . В этом случае изменение контекста фильтра для столбца
произойдет раньше, чем будет вычислено значение
по таблице дат.
Для этого мы изменим значение свойства
для группы вычислений
на 20, что приведет к следующим настройкам:
„ группа вычислений
–
: 20;
„ группа вычислений
–
: 10.
С такими настройками очередности применения групп вычислений результат меры
будет правильным, что видно по рис. 9.17.
В этом случае DAX сначала применяет элемент вычисления
D из группы
вычислений
, преобразуя выражение следующим образом:
CALCULATE (
CALCULATE (
[Sales Amount];
р
в
ислени
337
DATESYTD ( 'Date'[Date] )
);
'Averages'[Averages] = "Daily AVG"
)
Рис 9 17 Мера Daily YTD AVG оказ вает равильн
рез льтат
После этого применяется элемент D
A
из группы вычислений
, заменяя меру на функцию
, что приводит к такому выражению:
CALCULATE (
DIVIDE (
[Sales Amount];
COUNTROWS ( 'Date' )
);
DATESYTD ( 'Date'[Date] )
)
Теперь при вычислении значения для декабря в знаменателе будут учитываться все 365 дней года, а значит, и общий результат будет правильным. Обратите внимание также, что в этом примере мы строго следовали нашему главному правилу использования элементов вычисления, то есть применяли их
исключительно к мерам. При отображении меры
в визуализации
Power BI применение одного из двух элементов вычисления преобразовало
меру таким образом, что результат перестал соответствовать действительности. Получается, в нашем сценарии недостаточно было просто следовать нашему правилу – нужно было еще верно определить очередность применения
групп вычислений.
Все элементы внутри одной группы обладают одной и той же родительской
очередностью применения. Невозможно для разных элементов вычисления
в рамках одной группы задать разные значения очередности.
Целочисленное свойство
задается именно для группы вычислений. Чем выше его значение, тем выше приоритет группы. Таким образом, элементы группы с наивысшим приоритетом будут применяться к мерам в пер338
р
в
ислени
вую очередь. Иными словами, DAX применяет группы вычисления в порядке
следования значений их свойств
от большего к меньшему. Само абсолютное значение этого свойства не несет никакой информации. Смысл имеет только относительное значение
в сравнении с другими группами.
Кроме того, в модели данных не могут присутствовать две группы вычислений
с одинаковыми значениями свойства
.
Поскольку все группы вычислений должны иметь свои уникальные значения свойства
, этому необходимо уделить внимание при проектировании модели данных. Правильный выбор очередности применения групп
вычислений на этапе создания модели имеет важнейшее значение, ведь изменение этого свойства у той или иной группы может повлиять на уже готовые
отчеты. Если в вашей модели данных присутствует несколько групп вычислений, потратьте время, чтобы убедиться, что все вычисления дают ожидаемые
результаты при использовании любой комбинации элементов вычисления. Без
должной проверки очень высока вероятность того, что будет допущена ошибка
с очередностью применения групп к мерам.
Вкл чение и искл чение мер из лементов вычисления
Существуют сценарии, в которых тот или иной элемент вычисления подходит
не для всех мер. По умолчанию элемент вычисления распространяет свое влияние на все без исключения меры. Но в руках разработчика есть инструмент, позволяющий ограничить сферу влияния элементов на меры.
В DAX можно написать условия для определения того, какая именно мера
вычисляется в данный момент. Для этого служат функции
и
. Давайте попробуем ограничить применение элемента вычисления D
A
таким образом, чтобы меры, в которых
вычисляются проценты, не трансформировались в среднедневные показатели. Функция
возвращает
, если мера, вычисляемая
функцией
, входит в список переданных параметров:
-------------------------------------------------------- Calculation Group: 'Averages'[Averages]
--------------------------------------------------------- Calculation Item: Daily AVG
-IF (
ISSELECTEDMEASURE (
[Sales Amount];
[Gross Amount];
[Discount Amount];
[Sales Quantity];
[Total Cost];
[Margin]
);
DIVIDE (
SELECTEDMEASURE ();
р
в
ислени
339
COUNTROWS ( 'Date' )
)
)
Как видите, код позволяет выбрать, для каких мер рассчитывать среднедневное значение. Для всех мер, не вошедших в указанный список, применение элемента вычисления D
A
будет возвращать пустое значение. Если
вам необходимо включить в список все меры, кроме определенных, код можно
переписать так:
-------------------------------------------------------- Calculation Group: 'Averages'[Averages]
--------------------------------------------------------- Calculation Item: Daily AVG
-IF (
NOT ISSELECTEDMEASURE ( [Margin %] );
DIVIDE (
SELECTEDMEASURE ();
COUNTROWS ( 'Date' )
)
)
В обоих случаях мера
будет исключена из числа мер, к которым
будет применяться элемент вычисления D
A , как видно на рис. 9.18.
Рис 9 18
ле ент в
ислени Daily AVG не ри ен етс к ере Margin %
Еще одной функцией, позволяющей анализировать выбранную меру, является функция
, возвращающая не булево значение,
а строку с названием меры. Эту функцию можно применять вместо
, как показано ниже:
-------------------------------------------------------- Calculation Group: 'Averages'[Averages]
340
р
в
ислени
--------------------------------------------------------- Calculation Item: Daily AVG
-IF (
NOT ( SELECTEDMEASURENAME () = "Margin %" );
DIVIDE (
SELECTEDMEASURE ();
COUNTROWS ( 'Date' )
)
)
Результат вычисления будет одинаковым, но предпочтительнее использовать функцию
, и сразу по нескольким причинам:
„ если допустить опечатку в названии меры при использовании функции
, DAX просто вернет значение
, не сообщив
при этом об ошибке;
„ при наличии опечатки с применением функции
выражение выдаст ошибку, говорящую о неправильном входном параметре функции;
„ если мера будет переименована в модели данных, все выражения с применением функции
будут автоматически исправлены в редакторе модели с помощью адресной привязки, тогда как строки,
использующиеся в функции
, придется исправлять вручную.
Функцию
следует использовать в случаях, когда
бизнес-логика подразумевает трансформирование меры посредством элемента вычисления в зависимости от внешней конфигурации. Например, эта функция пригодится, если у вас есть таблица со списком мер, призванная регулировать поведение элемента вычисления при помощи внешней конфигурации,
которая может быть изменена без необходимости корректировать код на DAX.
освенная рекурсия
Элементы вычисления в DAX не предполагают возникновения полноценной
рекурсии. Но можно воспользоваться ограниченным вариантом этой концепции, получившим название косвенна рекурси (sideways recursion). Мы поясним эту непростую тему на примерах. Но начнем с объяснения того, что из
себя представляет рекурсия и почему так важно обсудить эту тему. Рекурсия
может возникать, когда элемент вычисления ссылается сам на себя, что приводит к бесконечному циклу. Давайте рассмотрим пример.
Представьте, что в группе вычислений
есть два следующих
элемента:
-------------------------------------------------------- Calculation Group: 'Time Intelligence'[Time calc]
р
в
ислени
341
--------------------------------------------------------- Calculation Item: YTD
-CALCULATE (
SELECTEDMEASURE ();
DATESYTD ( 'Date'[Date] )
)
--- Calculation Item: SPLY
-CALCULATE (
SELECTEDMEASURE ();
SAMEPERIODLASTYEAR ( 'Date'[Date] )
)
Нам необходимо добавить третий элемент вычисления, который будет рассчитывать нарастающий итог с начала года по предыдущему году. В главе 8 вы
узнали, что такой тип вычислений можно легко осуществить путем комбинирования двух функций логики операций со временем:
и
. Следующее выражение поможет решить наш сценарий:
--- Calculation Item: PYTD
-CALCULATE (
SELECTEDMEASURE ();
DATESYTD ( SAMEPERIODLASTYEAR ( 'Date'[Date] ) )
)
По причине простоты вычисления эту формулу вполне можно назвать оптимальной. Но для разминки мозгов мы попробуем написать этот код иначе.
У нас ведь уже есть элемент вычисления для расчета нарастающего итога с начала года ( D). Может, попробовать применить этот элемент повторно, чтобы
не использовать вложенные функции логики операций со временем? Взгляните на такой вариант определения для элемента вычисления
D:
--- Calculation Item: PYTD
-CALCULATE (
SELECTEDMEASURE ();
SAMEPERIODLASTYEAR ( 'Date'[Date] );
'Time Intelligence'[Time calc] = "YTD"
)
Этот элемент вычисления служит тем же целям, что и предыдущий, но использует при этом совершенно иную технику. Функция
смещает текущий контекст фильтра ровно на год назад, а нарастающий
итог с начала года мы получим, применив уже имеющийся элемент вычисле342
р
в
ислени
ния
D из группы
. Как мы и предполагали, в этом случае код станет
более трудным для понимания. Но в более сложных сценариях возможность
использовать ранее созданные элементы вычисления может помочь избежать
многократного повторения одного и того же кода в определении меры.
Это действительно мощный инструмент, применимый в комплексных решениях. И основан он на принципе рекурсии, который стоит пояснить отдельно. Как вы видели на примере
D, синтаксически вполне допустимо определять новый элемент вычисления на основании существующего из той же
группы вычислений. Если бы концепция рекурсии была применима в DAX без
ограничений, это могло бы приводить к очень сложным зависимостям. К примеру, элемент вычисления A мог бы зависеть от элемента B, который зависел
бы от C, а тот, в свою очередь, зависел от A. Следующий вымышленный пример
демонстрирует эту ситуацию:
-------------------------------------------------------- Calculation Group: Infinite[Loop]
--------------------------------------------------------- Calculation Item: Loop A
-CALCULATE (
SELECTEDMEASURE ();
Infinite[Loop] = "Loop B"
)
--- Calculation Item: Loop B
-CALCULATE (
SELECTEDMEASURE ();
Infinite[Loop] = "Loop A"
)
Если попытаться использовать эту группу вычислений в выражении, как
показано в следующем примере, DAX не удастся применить элементы вычисления, поскольку элемент A требует применения элемента B, который, в свою
очередь, ожидает применения элемента A:
CALCULATE (
[Sales Amount];
Infinite[Loop] = "Loop A"
)
В некоторых языках программирования допускается использование таких
циклических зависимостей при определении выражений – обычно в функциях, что ведет к образованию так называемых рекурсивн о ределений (recursive
definition). Определение рекурсивной функции предполагает использование
самой себя. Рекурсия является очень мощной концепцией, но разработчикам
бывает не так просто писать подобные функции, а оптимизаторам – искать
наиболее эффективный план выполнения запроса.
р
в
ислени
343
Именно поэтому в DAX не допускается написание рекурсивных элементов
вычисления. Вместо этого разработчик имеет право в одном элементе вычисления ссылаться на другой элемент из той же группы, но повторный вызов
элемента недопустим. Иными словами, можно использовать функцию
для применения элемента вычисления к мере, но примененный элемент
не может прямо или косвенно вызывать исходный элемент вычисления. Именно в этом и заключается принцип косвенной рекурсии. Полноценная рекурсия
в DAX недопустима, но повторно использовать элементы вычисления без обратных вызовов можно.
Примечание
сли в знако
с з ко
, то должн знать, то в не до сти о исользование как косвенно , так и олно енно рек рсии От асти о то
с итаетс
олее сложн
з ко о сравнени с
ро е того, олно енна рек рси за аст
вле ет за со о ро ле
в лане роизводительности
то е е одна ри ина, о которо в
она не ис ольз етс
Стоит также помнить, что рекурсия может возникать и вследствие установки
фильтра на элемент вычисления в самой мере – без взаимных вызовов между
элементами. Рассмотрим следующий пример определения мер (
,
,
) и элементов вычисления (A и B):
--- Определение мер
-Sales Amount := SUMX ( Sales; Sales[Quantity] * Sales[Net Price] )
MA := CALCULATE ( [Sales Amount]; Infinite[Loop] = "A" )
MB := CALCULATE ( [Sales Amount]; Infinite[Loop] = "B" )
-------------------------------------------------------- Calculation Group: Infinite[Loop]
--------------------------------------------------------- Calculation Item: A
-[MB]
--- Calculation Item: B
-[MA]
Элементы вычисления не ссылаются непосредственно друг на друга. Но при
этом они ссылаются на меры, которые, в свою очередь, ссылаются на элементы,
провоцируя запуск бесконечного цикла. Можно заметить это, последовательно
пройдя по шагам. Рассмотрим следующее выражение:
CALCULATE (
[Sales Amount];
Infinite[Loop] = "A"
)
344
р
в
ислени
Применение элемента вычисления A порождает такой результат:
CALCULATE (
CALCULATE ( [MB] )
)
При этом мера
внутренне ссылается на меру
и элемент вычисления B, что приводит к следующему преобразованию кода:
CALCULATE (
CALCULATE (
CALCULATE (
[Sales Amount];
Infinite[Loop] = "B"
)
)
)
На этом этапе применение элемента вычисления B приводит к такому результату:
CALCULATE (
CALCULATE (
CALCULATE (
CALCULATE ( [MA] )
)
)
)
Мера
внутренне ссылается на меру
A, что ведет к следующему преобразованию кода:
и элемент вычисления
CALCULATE (
CALCULATE (
CALCULATE (
CALCULATE (
CALCULATE (
[Sales Amount];
Infinite[Loop] = "A"
)
)
)
)
)
В результате мы вернулись к исходному выражению и фактически вошли
в бесконечный цикл элементов вычисления, применяемых к выражению, хотя
сами элементы при этом друг на друга не ссылаются. Вместо этого они вызывают меру, которая ссылается на элементы вычисления. К счастью, движок
DAX достаточно интеллектуален, чтобы определить возникновение такой ситуации, и выдаст ошибку.
Косвенная рекурсия может приводить к написанию очень запутанных выражений, непростых для понимания и потенциально ведущих к неправильр
в
ислени
345
ным расчетам. Сложность использования элементов вычисления совместно
с косвенной рекурсией проявляется в случаях, когда меры внутренне применяют элементы вычисления при помощи функции CALCULATE, а пользователь меняет выбор элементов в интерфейсе – например, в срезах отчета
в Power BI.
Мы советуем ограничить использование косвенной рекурсии в коде DAX до
предела, даже если это приведет к появлению повторений в формулах. Косвенную рекурсию можно безопасно использовать только в скрытых группах вычислений, чтобы пользователь никак не мог повлиять на результат. Помните,
что в Power BI пользователи могут определять собственные меры в отчетах,
и без должного понимания сложных концепций вроде рекурсии они рискуют
наделать ошибок, даже не осознавая этого.
Два основных правила
Как мы уже отмечали в начале главы, есть всего два основных правила, которых необходимо придерживаться, чтобы не возникало проблем при использовании элементов вычисления:
„ используйте элементы вычисления для модификации выражений, представляющих собой ссылку на меру. И никогда не используйте их с более
сложными конструкциями;
--- Как надо
-SalesPerWd :=
CALCULATE (
[Sales Amount];
'Time Intelligence'[Time calc] = "YTD"
)
--- Как не надо. Никогда так не делайте!
-SalesPerWd :=
CALCULATE (
SUMX ( Customer; [Sales Amount] );
меру
'Time Intelligence'[Time calc] = "YTD"
)
-- Ссылка на меру. Это правильно
-- Сложное выражение, а не ссылка на
„ избегайте создания косвенной рекурсии в доступных пользователям
группах вычислений. Безопасно использовать рекурсию можно исключительно в скрытых группах. Если вы все же решили применить эту концепцию в своей модели данных, внимательно проследите за тем, чтобы
косвенная рекурсия не превратилась в полноценную, иначе возникнет
ошибка.
346
р
в
ислени
Закл чение
Группы вычислений представляют собой очень мощный инструмент, позволяющий упростить модель данных. Возможность создавать множество вариаций
одних и тех же мер при помощи групп вычислений позволяет существенно
сократить количество дублирующегося кода в обычных мерах, количество которых иначе могло бы составить несколько сотен. Кроме того, пользователям
нравятся группы вычислений за возможность создания собственных сочетаний вычислений.
Будучи разработчиком DAX, вы должны хорошо понимать преимущества
и недостатки групп вычислений. Вот основные принципы, которые мы старались донести до вас в данной главе:
„ группа вычислений представляет собой набор из элементов вычисления;
„ элемент вычисления является разновидностью меры. Использование
функции
позволяет корректировать ход вычисления;
„ элемент вычисления способен переопределять исходное выражение
и строку форматирования текущей меры;
„ если в модели присутствует несколько групп вычислений, разработчик
обязан определить очередность применения их элементов, чтобы исключить неоднозначность их поведения;
„ элементы вычисления применяются к ссылкам на меры, а не к выражениям. Использование элементов вычисления с выражениями, не состоящими из одной меры, может привести к неожиданным результатам.
Таким образом, лучше всего применять элементы исключительно к выражениям, представляющим единственную ссылку на меру;
„ разработчик вправе использовать косвенную рекурсию при определении
элементов вычисления, но это может серьезно усложнить выражение
в целом. Следует ограничить использование косвенной рекурсии скрытыми группами вычислений и никогда не применять эту концепцию
в группах, видимых пользователю;
„ следование советам из этой главы облегчит жизнь разработчикам и позволит избежать чрезмерного усложнения сценариев, что характерно для
использования групп вычислений.
Помните, что группы вычислений являются одной из новинок в языке DAX.
Это очень мощная концепция, которую мы, по сути, только начинаем изучать.
Мы будем постоянно обновлять контент страницы в интернете, указанной
в данной главе, на которой вы сможете ознакомиться с новыми статьями и постами в блоге по этой интересной и важной теме.
ГЛ А В А 10
Работа с контекстом фильтра
В предыдущих главах вы научились создавать контексты фильтра для проведения сложных вычислений. Например, в главе, посвященной работе с датой
и временем, вы узнали, как можно сочетать функции логики операций со временем для сравнения разных временных интервалов. В предыдущей главе вы
научились использовать группы вычислений для упрощения кода на DAX и облегчения работы пользователю. В настоящей главе мы познакомим вас со множеством функций для чтения текущего состояния контекста фильтра, при помощи которых вы сможете корректировать поведение формул в зависимости
от настройки фильтров и текущего выбора пользователя. Это очень мощные
функции, хотя используются они не так часто. И все же хорошее понимание их
работы просто необходимо для создания полноценных мер, которые будут работать в любых отчетах, а не только там, где вы использовали их в первый раз.
Формула может работать или нет в зависимости от того, какой контекст
фильтра установлен в данный момент. Например, вы можете написать формулу, которая будет прекрасно функционировать на уровне месяца, но при этом
выдавать неправильные результаты по годам. Еще один пример – ранжирование покупателей. Формула будет работать корректно, если в текущем контексте фильтра будет выбран один покупатель, а для множественного выбора покажет неверные цифры. Следовательно, чтобы работать в любых отчетах, мера
должна проверять текущее состояние контекста фильтра и только после этого
вычислять значение и возвращать результат. Если контекст фильтра удовлетворяет требованиям формулы, она должна возвращать осмысленное значение. В противном случае, когда текущий контекст фильтра содержит фильтры,
несовместимые с кодом, лучше всего будет вернуть пустое значение.
Внимательно относитесь к тому, чтобы ни одна формула никогда не возвращала неправильный результат. Всегда лучше вернуть пустое значение, чем
некорректные цифры. Пользователь будет крутить вашу модель данных, не обладая знаниями о том, как внутренне устроен ваш код. И как разработчик DAX
вы ответственны за то, чтобы меры работали в любых условиях.
Для каждой функции, с которой вы познакомитесь в данной главе, мы покажем сценарии, где она наиболее применима. Но ваши собственные сценарии,
конечно, будут отличаться от наших демонстраций. Читая об этих функциях,
думайте о том, как они могут улучшить вашу модель данных.
Также в этой главе мы коснемся двух важных концепций: рив ки данн
(data lineage) и применения функции
. До этого вы уже использовали
концепцию привязки данных, даже не догадываясь об этом и не зная всех ее
особенностей. В данной главе мы коснемся этой темы более подробно и представим несколько сценариев, в которых можно использовать эти концепции.
348
10
а ота с контексто
ильтра
Использование функций AS
и SELECTEDVALUE
EVALUE
Как мы уже сказали во вводной части главы, осмысленность результатов некоторых вычислений напрямую зависит от текущего выбора пользователя.
В качестве примера рассмотрим следующую формулу, вычисляющую сумму
продаж нарастающим итогом с начала квартала:
QTD Sales :=
CALCULATE (
[Sales Amount];
DATESQTD ( 'Date'[Date] )
)
Как видно по рис. 10.1, мера прекрасно работает для месяцев и кварталов, но
за 2007 год выводит значение 2 731 424,16.
Рис 10 1 Мера QTD Sales в исл етс на ровне лет,
но ее рез льтат ноги ожет дивить
На самом деле на уровне лет мера
показывает актуальное значение для последнего квартала этого года, что на уровне месяцев соответствует
показателю за декабрь. Кто-то скажет, что для года значение этой меры вообще
не несет никакого смысла. Если быть точнее, мера не должна рассчитываться
и для уровня квартала. То есть наша мера имеет осмысленное значение только на уровне месяца и ниже. Иными словами, в нашем случае мера должна
выводить значения по месяцам, а на уровне кварталов и лет ячейки должны
оставаться пустыми.
10
а ота с контексто
ильтра
349
В этом случае вам на помощь придет функция
. Например,
для того чтобы очистить ячейки на уровне кварталов и лет в нашем отчете, достаточно определить, что в выборке находится несколько месяцев. Это будет
соответствовать кварталам и годам, тогда как на уровне месяца в текущей выборке будет находиться только один месяц. Таким образом, добавив условие
в нашу формулу, мы добьемся желаемого результата, как показано ниже:
QTD Sales :=
IF (
HASONEVALUE ( 'Date'[Month] );
CALCULATE (
[Sales Amount];
DATESQTD ( 'Date'[Date] )
)
)
Результат вычисления этой меры показан на рис. 10.2.
Рис 10 2
а ита ер QTD Sales
ри о о и нк ии HASONEVALUE
озволила оставить ст и е ки
с нежелательн и зна ени и
Этот простой пример на самом деле очень важен. Вместо того чтобы оставить формулу как есть, мы пошли на шаг дальше и добавили проверку, позволившую выводить значения только там, где они имеют смысл. Если есть риск,
что формула может выдавать неверное значение в определенных состояниях
контекста фильтра, необходимо производить определенную проверку на минимальные требования и действовать соответствующе.
В главе 7 вы уже видели подобный сценарий при работе с функцией
.
Там мы производили ранжирование покупателей и использовали функцию
для гарантии того, что в текущем контексте фильтра выбран
единственный покупатель.
Также функция
часто используется в сценариях с логикой
операций со временем из-за большого количества агрегаций, которые способны выдавать осмысленные результаты только на определенных уровнях
350
10
а ота с контексто
ильтра
контекста фильтра. Во всех остальных случаях ячейки в отчетах желательно
оставлять пустыми.
Еще одним распространенным случаем использования функции
является извлечение одного выбранного значения из текущего контекста фильтра. Есть множество сценариев, в которых это может пригодиться, но
с появлением групп вычислений их количество существенно уменьшится. Мы
опишем один сценарий с применением разновидности анализа «что, если».
В подобных ситуациях разработчик обычно создает таблицу параметров, позволяя пользователю выбрать в срезе одно значение, после чего этот параметр
используется в коде для изменения хода вычисления.
Представим, что вы оцениваете сумму продаж путем корректировки значений предыдущих лет в соответствии с уровнем инфляции. Для осуществления
анализа пользователь должен выбрать годовой уровень инфляции, который
будет использоваться для всех дат транзакций вплоть до сегодняшнего дня.
Уровень инфляции представлен здесь как параметр алгоритма. Для начала
нам необходимо построить таблицу с инфляцией, из которой пользователь будет делать выбор. В нашем примере достаточно будет значений от 0 до 20
с шагом 0,5 . Фрагмент этой таблицы вы можете видеть на рис. 10.3.
Рис 10 3
стол е Inflation
содержатс зна ени от 0 до 20
с аго 0,
Пользователь выбирает желаемое значение при помощи среза, после чего
формула пересчитывается для всех лет вплоть до сегодняшнего дня. Если пользователь не сделал выбор или выбрал больше одного значения, формула должна использовать значение инфляции по умолчанию, равное 0 .
Итоговый отчет показан на рис. 10.4.
Примечание
ара етр то, если
в
создает та ли
зование то же те ники, то о исана в данно разделе
и срез с ис оль-
Несколько важных замечаний по поводу этого отчета:
„ пользователь может выбрать желаемый уровень инфляции в срезе, расположенном вверху слева;
10
а ота с контексто
ильтра
351
„ вверху справа в отчете показан год, используемый для корректировки. За
основу берется дата последней продажи в модели данных;
„ в мере
сумма продаж анализируемого года умножается на коэффициент, зависящий от выбранного пользователем уровня инфляции;
„ в строке итогов в формуле должны использоваться разные коэффициенты для разных лет.
Рис 10 4
ара етр Inflation
равл ет ножителе
о оказател
ред д и лет
Простейшей формулой является определение отчетного года. Здесь мы извлекаем максимальную дату заказа из таблицы
:
Reporting year := "Reporting year: " & YEAR ( MAX ( Sales[Order Date] ) )
Выбранный пользователем уровень инфляции можно получить при помощи функций
или
, поскольку при выборе единственного показателя
они будут возвращать одно и то же значение, являющееся выбранным уровнем
инфляции. При этом пользователь может не выбрать уровень инфляции вовсе
или выбрать сразу несколько значений. В этом случае формула должна отрабатывать корректно, используя значение по умолчанию.
Лучшим способом проверить, что пользователь выбрал единственное значение в списке уровней инфляции, является использование функции
352
10
а ота с контексто
ильтра
образом:
. Соответствующий фрагмент кода может выглядеть следующим
User Selected Inflation :=
IF (
HASONEVALUE ( 'Inflation Rate'[Inflation] );
VALUES ( 'Inflation Rate'[Inflation] );
0
)
Из-за частого использования такого шаблона в языке DAX была введена
специальная функция
, позволяющая значительно упростить
предыдущий код:
User Selected Inflation := SELECTEDVALUE ( 'Inflation Rate'[Inflation]; 0 )
Функция
принимает два параметра. Вторым из них является значение по умолчанию, которое будет возвращено, если в столбце, переданном в качестве первого параметра, выбрано более одного элемента.
Добавив меру
в модель данных, необходимо определиться со множителем инфляции для выбранного года. Если в качестве года для
корректировки инфляции использовать последний год в модели, то при расчете множителя нам необходимо будет пройти по всем годам между последним
годом и выбранным и перемножить выражения
для каждого из них:
Inflation Multiplier :=
VAR ReportingYear =
YEAR ( CALCULATE ( MAX ( Sales[Order Date] ); ALL ( Sales ) ) )
VAR CurrentYear =
SELECTEDVALUE ( 'Date'[Calendar Year Number] )
VAR Inflation = [User Selected Inflation]
VAR Years =
FILTER (
ALL ( 'Date'[Calendar Year Number] );
AND (
'Date'[Calendar Year Number] >= CurrentYear;
'Date'[Calendar Year Number] < ReportingYear
)
)
VAR Multiplier =
MAX ( PRODUCTX ( Years; 1 + Inflation ); 1 )
RETURN
Multiplier
Остается только суммировать данные по продажам с учетом полученного
множителя по годам. Код меры
представлен ниже:
Inflation Adjusted Sales :=
SUMX (
VALUES ( 'Date'[Calendar Year] );
[Sales Amount] * [Inflation Multiplier]
)
10
а ота с контексто
ильтра
353
Использование функций ISFILTERED
и ISCR SSFILTERED
Иногда задача заключается не в выборе одного значения из контекста фильтра,
а в определении того, что столбец или таблица включены в активный фильтр
в текущем контексте. Поводом для контроля включения в фильтр обычно является проверка на то, что все значения из данного столбца видимы в отчете.
Дело в том, что в присутствии фильтра некоторые значения могут оказаться
скрыты, а значит, и результат может быть неточным.
Столбец может быть отфильтрован как по причине непосредственного применения к нему фильтра, так и в результате фильтрации другого столбца, способствующей наложению косвенного фильтра на интересующий нас столбец.
Рассмотрим эту ситуацию на следующем примере:
RedColors :=
CALCULATE (
[Sales Amount];
'Product'[Color] = "Red"
)
Во время вычисления меры
внешняя функция
применяет фильтр к столбцу
. Таким образом, этот столбец оказывается отфильтрованным. В языке DAX есть специальная функция
,
позволяющая определить, включен ли проверяемый столбец в фильтр. Функция
возвращает
или
в зависимости от того, наложен ли
прямой фильтр на столбец, переданный в качестве параметра. Если функции
передать целую таблицу, она вернет
только в том случае, если
на любой из столбцов этой таблицы наложен прямой фильтр. В противном случае результатом будет
.
Несмотря на то что в рассматриваемом нами случае непосредственный фильтр
применяется только к столбцу
, все остальные столбцы таблицы
также окажутся косвенно отфильтрованными. Например, в столбце
будут показываться только те бренды, в которых есть хотя бы один красный товар. Если под определенным брендом красные товары не производятся, он останется невидимым по причине фильтра, наложенного на столбец
.
Такие косвенные фильтры распространяются на все столбцы в таблице
,
кроме напрямую отфильтрованного столбца
. А значит, количество
видимых элементов в этих столбцах будет ограничено. Иными словами, к ним
применена кросс фил тра и (cross-filtering) или перекрестная фильтрация.
Говорят, что столбец включен в перекрестную фильтрацию, если существует
фильтр, способный ограничить количество его видимых элементов как напрямую, так и косвенно. Функция
как раз и предназначена для
определения того, попадает ли тот или иной столбец в кросс-фильтрацию.
Важно отметить, что если столбец включен в фильтр, он автоматически считается включенным и в кросс-фильтрацию. Обратное не является истиной:
столбец может включаться в перекрестную фильтрацию, но при этом не быть
отфильтрованным напрямую. Функция
может работать как
354
10
а ота с контексто
ильтра
со столбцами, так и с таблицами. На самом же деле если один столбец в таблице
участвует в перекрестной фильтрации, то в ней участвуют и все остальные. Так
что эту функцию стоит применять именно с таблицами, а не со столбцами. Вы
по-прежнему можете встретить код, в котором в качестве параметра функции
передается столбец. Просто изначально эта функция работала исключительно со столбцами, а со временем была расширена до таблиц.
Поэтому иногда можно встретить ее использование по-старому.
Поскольку фильтры распространяются на всю модель данных, установка
фильтра на таблицу
автоматически распространится на все связанные
таблицы. К примеру, если отфильтровать столбец
, ограничение
будет также наложено и на таблицу
. Таким образом, все столбцы таблицы
будут участвовать в кросс-фильтрации, образовавшейся посредством наложения фильтра на столбец
.
Чтобы продемонстрировать поведение этих функций, мы используем модель данных, несколько отличающуюся от той, которая рассматривается на
протяжении всей книги. Мы удалили некоторые таблицы, а связь между таблицами
и
сделали двунаправленной. С получившейся моделью
данных можно ознакомиться по рис. 10.5.
Рис 10 5
то
одели данн
св зь ежд та ли а и Sales и Product дв на равленна
Напишем для этой модели следующий набор мер:
Filter Gender := ISFILTERED ( Customer[Gender] )
Cross Filter Customer := ISCROSSFILTERED ( Customer )
Cross Filter Sales := ISCROSSFILTERED ( Sales )
Cross Filter Product := ISCROSSFILTERED ( 'Product' )
Cross Filter Store := ISCROSSFILTERED ( Store )
10
а ота с контексто
ильтра
355
Далее вынесем эти меры в столбцы матрицы, а в строки отправим
и
. Результат можно видеть на рис. 10.6.
Рис 10 6
Матри а, де онстрир
а
оведение
нк и ISFILTERED и ISCROSSFILTERED
Некоторые комментарии по результатам отчета:
„ столбец
напрямую отфильтрован только в тех строках,
где есть активный фильтр по этому столбцу. На уровне континентов, где
фильтр установлен лишь на
, столбец
не отфильтрован;
„ вся таблица
подвержена кросс-фильтрации, когда установлен
фильтр по столбцам
или
;
„ то же самое касается и таблицы
. Присутствие фильтра по любому
столбцу таблицы
автоматически накладывает кросс-фильтрацию на таблицу
, поскольку она находится на стороне «многие»
в связи с таблицей
;
„ таблица
не подпадает под перекрестную фильтрацию, поскольку
фильтр на таблице
не распространяется на
, а связь между этими таблицами однонаправленная;
„ так как связь между таблицами
и
двунаправленная, фильтр,
установленный на таблицу
, автоматически распространяется на
. Следовательно, таблица
будет подвержена кросс-фильтрации при наложении фильтра на любую другую таблицу этой модели
данных.
Функции
и
не так часто используются в выражениях DAX. Они применяются при выполнении продвинутой оптимизации
для проверки набора фильтров по столбцам, чтобы позволить коду выполняться по-разному в зависимости от установленных фильтров. Еще одна область
применения этих функций – работа с иерархиями, с которыми мы познакомимся в главе 11.
Учтите, что нельзя полагаться на наличие фильтра при определении того,
все ли значения столбца будут видимы. На самом деле столбец может быть от356
10
а ота с контексто
ильтра
фильтрован и участвовать в кросс-фильтрации, но при этом все его значения
будут видимы. Продемонстрируем это на примере простой меры:
Test :=
CALCULATE (
ISFILTERED ( Customer[City] );
Customer[City] <> "DAX"
)
В таблице
нет города с названием «DAX». Следовательно, фильтр
не окажет на исходную таблицу никакого влияния – все ее строки останутся
видимыми. Получается, что в столбце
показываются все существующие значения, несмотря на активный фильтр по этому столбцу и то, что
мера Test возвращает значение
.
Для определения того, все ли возможные значения видимы в столбце или
таблице, лучше воспользоваться подсчетом количества строк под действием
разных контекстов. В этом способе есть определенные нюансы, о которых мы
поговорим в следующей главе.
Понимание разницы между функциями VALUES
и FILTERS
Функция
очень похожа на
, но у нее есть одно важное отличие.
Если
возвращает значения, видимые в текущем контексте фильтра, то
– значения, отфильтрованные в текущем контексте фильтра.
И хотя эти определения также выглядят похожими, на самом деле они отличаются. К примеру, вы можете отфильтровать при помощи среза четыре цвета товаров, скажем черный, коричневый, лазурный и синий. Но из-за других
наложенных фильтров видимыми в отчете могут оказаться только два цвета,
если остальные не присутствуют в выборке. В таком сценарии функция
вернет два цвета, тогда как
– четыре. Этот пример хорошо подходит для
демонстрации различий между этими двумя функциями.
В данном случае мы будем использовать файл Excel, подключенный к модели данных Power BI. Причина этого в том, что на момент написания данной
книги функция
работает не так, как ожидается, в связке с
, которая используется в Power BI для осуществления запросов к модели. Так что этот пример не будет работать в Power BI.
Примечание
о ани
осведо лена о то ро ле е ис ользовани
нкии FILTERS в
, и, веро тно, в д и верси
род кта она дет ре ена о
то
роде онстрировать тот ри ер в книге,
в н жден
ли ис ользовать
в ка естве клиентского риложени , оскольк он не ис ольз ет нк и SUMMARIZECOLUMNS.
В главе 7 мы показывали пример использования функции
для размещения метки в отчете с указанием выбранных при помощи среза
10
а ота с контексто
ильтра
357
цветов. Там мы в результате пришли к сложной формуле с использованием
итераторов и переменных. Здесь мы прибегнем к помощи более простой версии кода:
Selected Colors :=
"Showing " &
CONCATENATEX (
VALUES ( 'Product'[Color] );
'Product'[Color];
", ";
'Product'[Color];
ASC
) & " colors."
Рассмотрим отчет с двумя срезами: в первом выбрана только одна категория, а во втором – несколько цветов, как показано на рис. 10.7.
Рис 10 7
от в срезе в рано ет ре вета, ера Selected Colors
оказ вает только два из ни
Несмотря на то что в срезе мы выбрали четыре различных цвета, мера
вернула только два из них. Причина в том, что функция
возвращает значения столбца в рамках текущего контекста фильтра. А в нашей
модели данных не оказалось товаров из категории «TV and Video» лазурного
(Azure) и синего (Blue) цветов. Таким образом, хотя в контекст фильтра и включены четыре цвета, функция
возвращает только два из них.
Если в мере заменить функцию
на
, будут возвращены все
отфильтрованные значения вне зависимости от того, есть ли в текущем контексте фильтра товары, представляющие эти значения:
Selected Colors :=
"Showing " &
CONCATENATEX (
FILTERS ( 'Product'[Color] );
'Product'[Color];
", ";
'Product'[Color];
358
10
а ота с контексто
ильтра
ASC
) & " colors."
С новой версией меры
в отчете в качестве выбранных показываются все четыре цвета, что видно по рис. 10.8.
Рис 10 8 С ис ользование
возвра ает все ет ре вета
нк ии FILTERS ера Selected Colors
Наряду с
DAX предлагает еще одну функцию для определения того, что на столбец наложен только один активный фильтр:
. Синтаксис и применение этой функции очень похожи на
.
Единственным отличием между ними является то, что функция
может возвращать значение
при наличии единственного активного
фильтра, в то время как
вернет
, поскольку значение, хоть
и отфильтровано, не является видимым.
Понимание разницы между ALLEXCEPT
и ALL VALUES
В предыдущем разделе мы представили вам функции
и
, предназначенные для определения присутствия фильтра. Но одного
факта наличия фильтра недостаточно, чтобы понять, видимы ли все значения
из столбца или таблицы в отчете. Лучше всего для этого подсчитать количество строк в текущем контексте фильтра и сравнить с количеством строк без
фильтров.
Взгляните на рис. 10.9. Мера
проверяет наличие фильтра по
столбцу
при помощи функции
, тогда как в поле
просто выводится количество строк в таблице
:
NumOfCustomers := COUNTROWS ( Customer )
Как видите, когда покупателем является компания, его пол, как и ожидается,
будет пустым значением. Во второй строке матрицы фильтр по столбцу
10
а ота с контексто
ильтра
359
активен, а значит, в поле
возвращается значение
время фильтр, по сути, ничего не фильтрует, потому что в столбце
только одно значение, и оно видимо.
Рис 10 9
ес отр на нали ие ильтра о стол
все ок атели види
. В то же
есть
Customer[Gender],
Наличие фильтра еще не означает, что таблица на самом деле будет отфильтрована. Оно лишь указывает на активное состояние фильтра. Чтобы проверить,
все ли покупатели видимы, лучше полагаться на простой подсчет строк. Если
количество строк в таблице с наложенным и снятым фильтром по столбцу
будет одинаковым, значит, фильтр, несмотря на свою активность, по сути
своей ничего не фильтрует.
Проводя подобные вычисления, необходимо обращать внимание на детали
контекста фильтра и поведения функции
. Есть два способа проверить одно и то же условие:
„ посчитать покупателей с наложенной функцией
на поле
„ посчитать покупателей с тем же типом (
или
).
;
Несмотря на то что в отчете на рис. 10.9 два вычисления возвращают одинаковый результат, если изменить столбец, используемый в матрице, цифры
будут разными. К тому же у каждого вычисления есть свои за и против, и об
этом стоит помнить при выборе решения для определенного сценария. Начнем с простейшего:
All Gender :=
CALCULATE (
[NumOfCustomers];
ALL ( Customer[Gender] )
)
Функция
удаляет все фильтры по столбцу
, оставляя все прочие
фильтры неизменными. В результате происходит подсчет количества покупателей в текущем контексте фильтра без учета половой принадлежности. Вы
можете видеть результат на рис. 10.10 вместе с мерой
, сравнивающей два расчета.
В мере
выводятся правильные результаты. Ее недостатком является жесткое указание в коде того, что мы снимаем фильтр со столбца
.
Например, если использовать эту меру в матрице со срезом по континенту,
360
10
а ота с контексто
ильтра
результат будет не таким, как мы ожидаем. Вы можете видеть это на рис. 10.11,
где мера
всегда возвращает значение
.
Рис 10 10
о второ строке ера All customers visible возвра ает True,
нес отр на то то стол е Gender от ильтрован
Рис 10 11
ильтр
та ли
о континента ,
ол ае не равильн е рез льтат
При этом нельзя сказать, что мера неправильно считает значения. Она все
правильно считает, если срез в отчете выполнен по полю
. Чтобы обеспечить мере независимость от этого поля, необходимо пойти другим путем,
а именно удалить все фильтры с таблицы
, за исключением столбца
.
Удаление всех фильтров, кроме одного, выглядит не самой сложной задачей.
Но здесь есть одна ловушка, о которой стоит помнить. Первой функцией, которая приходит на ум, является
. Но, к сожалению, в данном сценарии
использование этой функции может привести к неожиданным результатам.
Рассмотрим следующую формулу:
AllExcept Type :=
CALCULATE (
[NumOfCustomers];
ALLEXCEPT ( Customer; Customer[Customer Type] )
)
Функция
удаляет все существующие фильтры с таблицы
, за исключением фильтра по столбцу
. При использовании
в предыдущем отчете мера покажет правильные результаты, как видно по
рис. 10.12.
10
а ота с контексто
ильтра
361
Рис 10 12
нк и ALLEXCEPT сни ает зависи ость от ола
и ра отает с л
и стол а и
Эта мера будет работать не только с полем
. Поменяв континент на
пол в отчете, мы все равно увидим правильные результаты, что показано на
рис. 10.13.
Рис 10 13
нк и ALLEXCEPT ра отает и с оло тоже
Несмотря на видимую корректность, в этой формуле есть скрытая ловушка. Функции
при использовании в качестве аргумента фильтра функции
работают как модификаторы, о чем мы рассказывали в главе 5.
Модификаторы не возвращают таблицу, использованную в качестве фильтра.
Вместо этого они просто удаляют фильтры из контекста фильтра.
Обратите внимание на строку с пустым полом. В этой группе 385 покупателей, и все это компании. Если удалить столбец
из отчета,
единственным столбцом в контексте фильтра останется пол. Пустое значение
в этом столбце говорит о том, что в текущем контексте фильтра видимы только
компании. При этом отчет, показанный на рис. 10.14, выглядит неожиданно –
мера
показывает одинаковые значения для всех строк, а именно
общее количество покупателей.
Рис 10 14
нк и ALLEXCEPT
дает неожиданн е рез льтат ,
если оле Customer Type
не редставлено в от ете
362
10
а ота с контексто
ильтра
Функция
удалила все фильтры с таблицы
, за исключением фильтра по столбцу
. Но у нас в отчете нет поля
, по которому можно было бы сохранить фильтр. Таким образом, единственным фильтром в контексте фильтра остался фильтр по полю
, который был успешно удален функцией
.
Столбец
участвует в кросс-фильтрации, но не в прямой фильтрации. Так что в распоряжении функции
нет ни одного столбца,
фильтр по которому можно было бы сохранить, а значит, ее действие будет
эквивалентно действию функции
по всей таблице. В данном случае правильно будет воспользоваться парой функций
и
вместо
. Взгляните на следующую формулу:
All Values Type :=
CALCULATE (
[NumOfCustomers];
ALL ( Customer );
VALUES ( Customer[Customer Type] )
)
Несмотря на внешнюю схожесть с предыдущим выражением, семантически
эта формула значительно отличается. Функция
удаляет все фильтры с таблицы
. В то же время функция
составляет список значений
столбца
в текущем контексте фильтра. Как мы уже сказали, по типу покупателя прямого фильтра у нас нет, но это поле участвует
в кросс-фильтрации. Следовательно, функция
возвратит только значения, видимые в текущем контексте фильтра вне зависимости от того, какой
столбец участвует в фильтре, создающем кросс-фильтрацию по типу покупателя. Результат вы можете видеть на рис. 10.15.
Рис 10 15 Сов естное ис ользование
дало ожидае
рез льтат
нк и ALL и VALUES
Из этого урока вы должны вынести четкое понимание отличий между использованием функции
и сочетания функций
и
в качестве аргумента фильтра в
. Причина этих отличий в том, что семантика функций
предполагает именно удаление фильтров. Функции
этой группы никогда не добавляют фильтры к существующему контексту, они
только удаляют их.
Разница между нюансами добавления и удаления фильтров во многих сценариях бывает не важна. Но есть случаи, когда эта разница имеет решающее
значение, как в примере выше.
10
а ота с контексто
ильтра
363
Здесь, как и на протяжении всей книги, мы пытались показать вам, насколько важно точно подходить к формулировке выражений на языке DAX. Использование функции
без полного понимания всех ее нюансов может
привести к неожиданным результатам. DAX скрывает большую часть сложностей, предлагая интуитивно понятное поведение функций в большинстве
случаев. Но эти сложности, пусть и тщательно скрытые, никуда не исчезают.
И вам необходимо досконально разбираться в тонкостях контекста фильтра
и функции
, чтобы в полной мере овладеть искусством языка DAX.
Использование функции ALL для предотвра ения
преобразования контекста
На данном этапе чтения книги вы уже хорошо знакомы с процедурой преобразования контекста. Это очень мощная концепция языка, которую мы не раз
использовали для произведения полезных вычислений. Но есть случаи, когда необходимо не допустить выполнения преобразования контекста или, по
крайней мере, ограничить его действие. И чтобы этого добиться, вам пригодятся те же самые функции из группы
.
Важно помнить, что функция
выполняется в строго определенной последовательности. Сначала оцениваются аргументы фильтра, затем,
если функция выполняется в рамках контекста строки, происходит преобразование контекста, следом за чем применяются модификаторы, и только после
этого функция
применяет результат аргументов фильтра к контексту фильтра. Вы можете воспользоваться этим строгим порядком действий
в своих интересах, помня о том, что модификаторы функции
, к которым относятся и функции группы
, применяются после преобразования контекста. А это означает, что фильтрующий модификатор способен переопределить действие преобразования контекста.
Рассмотрим следующий фрагмент кода:
SUMX (
Sales;
CALCULATE (
...;
ALL ( Sales )
)
)
Функция
вызывается в контексте строки, созданном итератором
, проходящим по таблице
. Следовательно, здесь неизбежно произойдет преобразование контекста. Но поскольку в функции
присутствует модификатор
, движок DAX понимает, что все фильтры
с таблицы
должны быть удалены.
Описывая функцию
, мы говорили, что при ее выполнении сначала будет произведено преобразование контекста, то есть наложены фильтры на таблицу
, а затем функция
удалит эти фильтры. Однако опти364
10
а ота с контексто
ильтра
мизатор DAX не так глуп. Зная, что впоследствии будут удалены все фильтры
с таблицы
при помощи функции
, он просто отка
аетс от
ол е и
реобра о а и ко текста, при этом удаляя все существующие
контексты строки.
Такое поведение движка можно использовать в самых разных сценариях,
и в частности при работе с вычисляемыми столбцами. В вычисляемых столбцах всегда присутствует контекст строки. Это означает, что любая мера, упомянутая в коде такого столбца, будет вычислена в контексте фильтра только для
текущей строки.
Представьте, что вам необходимо в вычисляемом столбце рассчитать процент суммы продаж по конкретному товару относительно всех товаров в модели данных. Получить сумму продаж по текущему товару в вычисляемом столбце очень просто – достаточно вычислить меру
. Преобразование
контекста гарантирует вычисление меры исключительно для одной текущей
строки. При этом в знаменателе нам необходимо указать продажи по всем товарам. Но как это сделать при действующем преобразовании контекста? Все
очень просто – можно предотвратить операцию преобразования контекста
путем указания модификатора
в функции
, как показано в следующем примере:
'Product'[GlobalPct] =
VAR SalesProduct = [Sales Amount]
VAR SalesAllProducts =
CALCULATE (
[Sales Amount];
ALL ( 'Product' )
)
VAR Result =
DIVIDE ( SalesProduct; SalesAllProducts )
RETURN
Result
Еще раз напоминаем, что функция
способна предотвратить операцию
преобразования контекста, поскольку является модификатором функции
, а значит, применяется после преобразования контекста.
Если необходимо рассчитать процент продаж относительно заданной категории товаров, код придется немного переписать:
'Product'[CategoryPct] =
VAR SalesProduct = [Sales Amount]
VAR SalesCategory =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Product'; 'Product'[Category] )
)
VAR Result
DIVIDE ( SalesProduct; SalesCategory )
RETURN
Result
Результат вычисления этих двух мер виден на рис. 10.16.
10
а ота с контексто
ильтра
365
Рис 10 16 Мер GlobalPct и CategoryPct
ис ольз т оди икатор ALL и ALLEXCEPT дл
в олнени рео разовани контекста
редотвра ени
Использование функции ISEMPT
Функция
используется для определения того, является ли таблица,
переданная в качестве параметра, пустой, что означает, что в ней нет видимых
значений в текущем контексте фильтра. Функцию
можно заменить
выражением с подсчетом количества строк в табличном выражении:
COUNTROWS ( VALUES ( 'Product'[Color] ) ) = 0
Но использование функции
ным:
позволяет сделать код более элегант-
ISEMPTY ( VALUES ( 'Product'[Color] ) )
С точки зрения производительности всегда лучше использовать в подобных
случаях функцию
, поскольку она сообщает движку DAX, что именно
нужно проверить. Функция
требует полного сканирования таблицы для подсчета строк, тогда как для выполнения функции
в этом нет
необходимости.
Представьте, что вам нужно определить количество покупателей, никогда
не приобретавших определенный товар. Эту задачу можно решить следующим
образом:
NonBuyingCustomers :=
VAR SelectedCustomers =
CALCULATETABLE (
DISTINCT ( Sales[CustomerKey] );
ALLSELECTED ()
366
10
а ота с контексто
ильтра
)
VAR CustomersWithoutSales =
FILTER (
SelectedCustomers;
ISEMPTY ( RELATEDTABLE ( Sales ) )
)
VAR Result =
COUNTROWS ( CustomersWithoutSales )
RETURN
Result
На рис. 10.17 показан отчет с двумя выведенными мерами.
Рис 10 17
ере NonBuyingCustomers одс ит ваетс коли ество ок
никогда не рио ретав и в ранн е товар
ателе ,
Пользоваться функцией
довольно просто. В этом примере мы использовали ее, чтобы продемонстрировать читателю одну особенность. В предыдущем фрагменте кода мы сохраняем список покупателей в переменную
и позже проходим по ней при помощи функции
, чтобы проверить на
пустоту результат выполнения функции
.
Если содержимым таблицы в переменной
является список
ключей покупателей (Sales[CustomerKey]), как DAX может узнать о связях этих
значений с таблицей
? Код покупателя как значение ничем не отличается
от количества проданных товаров. Число есть число. Разница состоит только
в значении этого числа. Представляя код покупателя, число 120 будет указывать на покупателя с кодом 120, тогда как в качестве количества то же самое
число будет означать уже количество проданных товаров.
По сути, список чисел не обладает выраженным смыслом, будучи использованным в качестве фильтра. DAX умеет хранить информацию об источнике
значений в столбце путем привязки данных, о чем мы расскажем в следующем
разделе.
10
а ота с контексто
ильтра
367
Привязка данных и функция TREATAS
Как мы заметили в предыдущем разделе, список значений сам по себе не обладает смыслом, если неизвестно, что представляют собой эти данные. Представьте, что у вас есть анонимная таблица со значениями Red (Красный) и Blue
(Синий):
{ "Red", "Blue" }
Мы прекрасно понимаем, что речь идет о цветах. Более того, на этом этапе чтения книги мы даже можем предположить, что, скорее всего, здесь подразумеваются цвета товаров. Но для DAX это не более чем две строки. Отфильтровать таблицу по этим строкам движок не может. Поэтому следующее
выражение всегда будет возвращать общую сумму продажи:
Test :=
CALCULATE (
[Sales Amount];
{ "Red"; "Blue" }
)
Примечание
ред д а ера не в даст о и к казанн
арг ент ильтра дет
ри енен к анони но та ли е ез оказани вли ни на изи еские та ли в одели
данн
На рис. 10.18 видно, что результат выполнения меры везде равен значению
самой меры
, поскольку функция
не выполняет никакой фильтрации.
Чтобы значение могло фильтровать модель, движку DAX необходимо знать,
к каким данным привязано это значение. Говорят, что значение, представляющее столбец в модели, обладает рив кой данн (data lineage) к этому столбцу.
И наоборот, значение, никак не привязанное к данным в модели, называется
анонимн м (anonymous value). В предыдущем примере в мере
была использована анонимная таблица в качестве аргумента фильтра функции
, которая не могла отфильтровать итоговый набор данных.
В следующем фрагменте показан рабочий пример использования анонимной таблицы в качестве фильтра. Заметьте, что мы использовали развернутый
синтаксис указания аргумента функции
исключительно в образовательных целях – простого предиката для фильтрации столбца
было бы более чем достаточно:
Test :=
CALCULATE (
[Sales Amount];
FILTER (
ALL ( 'Product'[Color] );
'Product'[Color] IN { "Red"; "Blue" }
)
)
368
10
а ота с контексто
ильтра
Рис 10 18
ильтр с ис ользование
анони но та ли не оказ вает
вли ни на рез льтат
Привязка данных здесь выполняется следующим образом: функция
возвращает таблицу, содержащую все цвета товаров. При этом результат содержит
значения из исходного столбца, так что DAX известна трактовка этих значений.
Функция
сканирует таблицу, содержащую все цвета, на предмет вхождения в список значений анонимной таблицы (Red и Blue). В результате функция
возвращает таблицу, содержащую значения столбца
,
так что функция
знает, что фильтр необходимо применить именно
к столбцу
.
Привязку данных можно представить как своеобразный ярлык или тег, прикрепленный к каждому столбцу и однозначно идентифицирующий его позицию в модели данных.
Обычно вам не следует беспокоиться о привязке данных, поскольку всю
сложную работу DAX берет на себя, и внешне все выглядит просто и понятно.
Например, когда табличное значение присваивается переменной, DAX выполняет привязку данных, которая впоследствии незримо используется во всех
выражениях, где присутствует эта переменная.
Причиной же для изучения процесса привязки данных является то, что вы
вправе менять эти связи при необходимости. В каких-то ситуациях важно поддерживать привязку данных, настроенную по умолчанию, но иногда может понадобиться изменить привязку того или иного столбца.
Функция, призванная менять привязку данных по столбцам, называется
. В качестве первого параметра она принимает таблицу, после чего
следуют ссылки на столбцы в модели данных. В результате функция
обновляет привязку переданной таблицы, последовательно соединяя ее столбцы с переданными в качестве параметров ссылками. Например, предыдущая
мера
может быть переписана следующим образом:
10
а ота с контексто
ильтра
369
Test :=
CALCULATE (
[Sales Amount];
TREATAS ( { "Red"; "Blue" }; 'Product'[Color] )
)
Функция
вернет таблицу, содержащую значения с привязкой
к столбцу
. Таким образом, новая мера
включит в себя продажи только по красным и синим товарам, что видно по рис. 10.19.
Рис 10 19
нк и TREATAS озволила ерео ределить рив зк данн
в анони но та ли е, то ривело к воз ожности в олнить ильтра и
Правила выполнения привязки данных очень просты. Обычная ссылка на
столбец поддерживает привязку к таблице, тогда как выражение всегда будет
анонимным. По сути, выражение генерирует ссылку на анонимный столбец.
В следующем примере возвращается таблица с двумя столбцами, содержащими одинаковые значения. Разница между ними будет состоять лишь в том, что
первый столбец будет поддерживать привязку данных к исходной таблице,
тогда как второй – нет, поскольку является новым столбцом:
ADDCOLUMNS (
VALUES ( 'Product'[Color] );
"Color without lineage"; 'Product'[Color] & ""
)
Функция
может пригодиться, если необходимо обновить привязку
данных для одного или нескольких столбцов в табличном выражении. Пример,
370
10
а ота с контексто
ильтра
который мы рассмотрели до этого, был ознакомительным. Сейчас же мы увидим более интересный вариант использования привязки данных в сценарии
работы с датой и временем. В главе 8 мы написали следующую меру для расчета даты с использованием функции
применительно к полуаддитивному вычислению:
LastBalanceIndividualCustomer :=
SUMX (
VALUES ( Balances[Name] );
CALCULATE (
SUM ( Balances[Balance] );
LASTNONBLANK (
'Date'[Date];
COUNTROWS ( RELATEDTABLE ( Balances ) )
)
)
)
Данная мера работает, но при этом страдает от одного существенного недостатка: она содержит в себе две итерации, и оптимизатор вряд ли справится с задачей нахождения идеального плана выполнения этого запроса. Было
бы лучше создать таблицу, содержащую имена клиентов и последние даты их
балансов, после чего использовать эту таблицу в качестве аргумента фильтра
функции
для фильтрации дат для каждого клиента. Оказывается,
что в этом случае очень полезной может оказаться функция
:
LastBalanceIndividualCustomer Optimized :=
VAR LastCustomerDate =
ADDCOLUMNS (
VALUES ( Balances[Name] );
"LastDate"; CALCULATE (
MAX ( Balances[Date] );
DATESBETWEEN ( 'Date'[Date]; BLANK(); MAX ( Balances[Date] ) )
)
)
VAR FilterCustomerDate =
TREATAS (
LastCustomerDate;
Balances[Name];
'Date'[Date]
)
VAR SumLastBalance =
CALCULATE (
SUM ( Balances[Balance] );
FilterCustomerDate
)
RETURN
SumLastBalance
Данная мера работает следующим образом:
„ в табличной переменной
сохраняются последние даты
с наличием информации по каждому клиенту. В результате мы полу10
а ота с контексто
ильтра
371
чим таблицу из двух столбцов: первый представляет ссылку на столбец
, а второй является анонимным, поскольку вычисляется
в выражении;
„ табличные переменные
и
заполняются одним и тем же содержимым. При этом при формировании переменной
была использована функция
, что
позволило осуществить привязку ее столбцов к данным в модели. Таким образом, первый столбец представляет собой ссылку на столбец
, а второй – на
;
„ на заключительном шаге мы используем табличную переменную
в качестве аргумента фильтра функции
.
Поскольку теперь таблица корректно привязана к данным в модели,
функция
осуществляет фильтрацию модели данных таким
образом, чтобы для каждого клиента осталась одна дата. Это и будет последняя дата, на которую есть данные об этом клиенте в таблице
.
В подавляющем большинстве случаев функция
используется для
изменения привязки данных в таблицах, состоящих из одного столбца. Здесь
же мы показали более сложный пример использования этой функции, где
привязка осуществлялась сразу по двум столбцам. При этом привязка данных
в таблице, являющейся результатом выражения на DAX, может включать столбцы из разных таблиц. Когда такая таблица применяется к контексту фильтра,
это часто ведет к образованию так называемого фильтра произвольной формы, о чем мы поговорим в следующем разделе.
Фильтры произвольной формы
Фильтры в контексте могут быть двух разновидностей: простым фильтром или
фил тром рои вол ной форм (arbitrarily shaped filter). Все фильтры, которые
мы рассматривали в книге до сих пор, обладали простой формой. В данном
разделе мы поговорим о фильтрах произвольной формы и способах их применения в коде. Фильтры произвольной формы могут быть созданы в сводной таблице в Excel или путем написания кода меры на языке DAX, тогда как
в пользовательском интерфейсе Power BI на данный момент необходимо использовать для их создания специальные элементы визуализации. В этом разделе мы расскажем о фильтрах произвольной формы и работе с ними в DAX.
Начнем с описания различий между простым фильтром и фильтром произвольной формы в контексте фильтра:
„
ильтр о столб у представляет собой список значений из одного
столбца. Например, список из трех цветов – красного, синего и зеленого – является фильтром по столбцу. В следующем выражении мы используем функцию
для создания фильтра по столбцу в контексте
фильтра, влияющего только на столбец
:
CALCULATE (
[Sales Amount];
372
10
а ота с контексто
ильтра
'Product'[Color] IN { "Red"; "Blue"; "Green" }
)
„
росто
ильтр – это фильтр по одному и более столбцам, представляющий собой набор из нескольких фильтров по столбцу. Почти все фильтры,
которые мы использовали в данной книге до сих пор, являлись простыми. Простые фильтры создаются с использованием нескольких аргументов фильтра в функции
:
CALCULATE (
[Sales Amount];
'Product'[Color] IN { "Red"; "Blue" };
'Date'[Calendar Year Number] IN { 2007; 2008; 2009 }
)
Этот код также может быть записан с использованием простого фильтра
с двумя столбцами:
CALCULATE (
[Sales Amount];
TREATAS (
{
( "Red"; 2007 );
( "Red"; 2008 );
( "Red"; 2009 );
( "Blue"; 2007 );
( "Blue"; 2008 );
( "Blue"; 2009 )
};
'Product'[Color];
'Date'[Calendar Year Number]
)
)
Поскольку простой фильтр содержит в себе все возможные сочетания
значений двух столбцов, проще выражать его с использованием двух
фильтров по столбцу;
„ ильтр рои оль о
ор
– это любой фильтр, который не может
быть выражен через простой фильтр. Взгляните на следующее выражение:
CALCULATE (
[Sales Amount];
TREATAS (
{
( "CY 2007"; "December" );
( "CY 2008"; "January" )
};
'Date'[Calendar Year];
'Date'[Month]
)
)
10
а ота с контексто
ильтра
373
Фильтр по году и месяцу не является фильтром по столбцу, поскольку
в него вовлечены сразу два столбца. Более того, этот фильтр не включает
все возможные комбинации этих столбцов в модели данных. Фактически невозможно фильтровать годы и месяцы отдельно. Здесь мы имеем
дело со ссылками на два года и два месяца, и в таблице
есть четыре
комбинации для предложенных значений, но фильтр включает в себя
только две из них. Иными словами, если бы мы использовали фильтры
по столбцу, то результирующий контекст фильтра также включал бы значения Январь 2007 и Декабрь 2008, которые не описаны в предыдущем
примере. А значит, перед нами фильтр произвольной формы.
Фильтр произвольной формы – это не просто фильтр со множеством столбцов. Конечно, фильтр со множеством столбцов может быть произвольной
формы, но он также может представлять собой и простой фильтр. В следующем примере у нас как раз простой фильтр, хоть он и состоит из нескольких
столбцов:
CALCULATE (
[Sales Amount];
TREATAS (
{
( "CY 2007"; "December" );
( "CY 2008"; "December" )
};
'Date'[Calendar Year];
'Date'[Month]
)
)
Этот фрагмент кода может быть переписан с использованием двух фильтров
по столбцу следующим образом:
CALCULATE (
[Sales Amount];
'Date'[Calendar Year] IN { "CY 2007"; "CY 2008" };
'Date'[Month] = "December"
)
Кажется, что выражения с использованием фильтров произвольной формы
писать непросто, но они могут быть достаточно легко определены в пользовательском интерфейсе Excel и Power BI. На момент написания книги в Power
BI такие фильтры можно строить только при помощи специального элемента
визуализации ерар и еский сре (Hierarchy Slicer), который позволяет определить фильтры, базируясь на иерархии по нескольким столбцам. Например, на
рис. 10.20 видно, как этот элемент визуализации отображает разные месяцы
из 2007 и 2008 годов.
В Microsoft Excel фильтр произвольной формы строится при помощи встроенного инструмента фильтрации, как показано на рис. 10.21.
Фильтры произвольной формы использовать в DAX довольно сложно по
причине производимых в них изменений в рамках контекста фильтра функцией
. По сути, когда функция
применяет фильтр
374
10
а ота с контексто
ильтра
к столбцу, она удаляет ранее наложенные фильтры на этот столбец, заменяя
их новыми. Это обычно приводит к потере произвольным фильтром своей изначальной формы. В результате формулы начинают выдавать неправильные
цифры, и поддерживать их становится очень сложно. Чтобы продемонстрировать эту проблему, мы будем усложнять код шаг за шагом, пока проблема
не проявится.
Рис 10 20
ри ильтра ии иерар ии
ожет о разоватьс ильтр роизвольно
ор
Рис 10 21
ильтр роизвольно ор
созда тс ри о о и встроенн средств ильтра ии
10
а ота с контексто
ильтра
375
Представьте, что вы определили простую меру, перезаписывающую год на
2007-й:
Sales Amount 2007 :=
CALCULATE (
[Sales Amount];
'Date'[Calendar Year] = "CY 2007"
)
Функция
перезаписывает фильтр по году, но при этом не трогает
фильтр по месяцу. Будучи использованной в отчете, мера может показывать
неожиданные результаты, что видно по рис. 10.22.
Рис 10 22
200 год ереза исал ред д
и
ильтр о год
В 2007 году цифры в обоих столбцах одинаковые, тогда как в 2008-м мера
по-прежнему выводит данные по 2007 году, притом что месяц остался без изменений. Таким образом, в ячейке по январю 2008 года показаны продажи за январь 2007-го. То же самое касается февраля и марта. Важно отметить при этом, что в исходный фильтр не входили первые три месяца
2007 года, а после замены фильтра по году формула начала показывать эти
цифры. Как видим, пока никаких аномалий нет.
Ситуация станет более запутанной, если вы захотите узнать среднемесячные
продажи. Одним из решений является запуск итераций по месяцам и агрегирование промежуточных значений при помощи функции
:
Monthly Avg :=
AVERAGEX (
VALUES ( 'Date'[Month] );
[Sales Amount]
)
Результат работы этой меры виден на рис. 10.23. Итоговые цифры на этот раз
оказались чересчур большими.
Понять проблему в данном случае сложнее, чем решить ее. Давайте сосредоточимся на ячейке, выдающей неправильное значение, – общем итоге по мере
. Контекст фильтра в строке итогов у нас следующий:
376
10
а ота с контексто
ильтра
TREATAS (
{
( "CY 2007"; "September" );
( "CY 2007; "October" );
( "CY 2007"; "November" );
( "CY 2007"; "December" );
( "CY 2008"; "January" );
( "CY 2008"; "February" );
( "CY 2008"; "March" )
};
'Date'[Calendar Year];
'Date'[Month]
)
Рис 10 23
тогов е зна ени
они гораздо оль е
вно не вл
тс средни и о ес
а ,
Чтобы проследить за выполнением формулы в этой ячейке, напишем полное выражение с указанием соответствующего контекста фильтра в функции
, вычисляющей меру
. Также мы раскроем и код меры
, чтобы провести полноценную симуляцию запуска формулы:
CALCULATE (
AVERAGEX (
VALUES ( 'Date'[Month] );
CALCULATE (
SUMX (
Sales;
Sales[Quantity] * Sales[Net Price]
)
)
);
TREATAS (
{
( "CY 2007"; "September" );
( "CY 2007"; "October" );
( "CY 2007"; "November" );
10
а ота с контексто
ильтра
377
( "CY 2007"; "December" );
( "CY 2008"; "January" );
( "CY 2008"; "February" );
( "CY 2008"; "March" )
};
'Date'[Calendar Year];
'Date'[Month]
)
)
Ключом к решению создавшейся проблемы является понимание того, что
происходит во время выполнения выделенной жирным шрифтом функции
. Эта функция вызывается в рамках контекста строки, проходящего
по столбцу
. Следовательно, происходит преобразование контекста, в результате чего текущий месяц добавляется к контексту фильтра. То есть
в конкретном месяце, скажем в январе, этот месяц будет добавлен к контексту
фильтра, при этом все ранее наложенные фильтры по месяцу будут удалены,
тогда как другие фильтры останутся нетронутыми.
При выполнении функции
на уровне января контекст фильтра
будет включать в себя январь 2007 и 2008 годов, поскольку исходный контекст
фильтровал сразу два года. Таким образом, на каждой итерации DAX рассчитывает сумму продажи по одному и тому же месяцу в двух разных годах. Именно
поэтому результат получается завышенным по сравнению с месячными продажами.
Изначальная форма произвольного фильтра оказалась утеряна, поскольку
функция
переопределила фильтр по одному из столбцов, участвующих в общем фильтре. В результате мы получили неправильные цифры.
Решить возникшую проблему гораздо проще, чем вы думаете. На самом деле
достаточно будет проводить итерации по столбцу с гарантированно уникальными значениями. Так что если мы будем проходить не по столбцу с месяцами, в котором значения не уникальны, а по столбцу
, сочетающему в себе год и месяц, то формула будет возвращать правильные цифры:
Monthly Avg :=
AVERAGEX (
VALUES ( 'Date'[Calendar Year Month] );
[Sales Amount]
)
Используя эту версию меры
, мы будем на каждой итерации за
счет преобразования контекста переопределять фильтр по столбцу
, в котором объединены вместе год и месяц. В результате мы всегда
будем получать сумму продаж за один конкретный месяц, что нам и требовалось. Отчет с обновленной мерой показан на рис. 10.24.
Если в нашем распоряжении нет столбца с уникальными значениями, подходящего для итератора в отношении кратности, можно для решения проблемы воспользоваться функцией
. Показанная ниже альтернативная версия меры работает корректно, поскольку вместо замены предыдущего
фильтра просто добавляет фильтр по месяцу к существующему произвольному
фильтру, что позволяет сохранить его изначальную форму:
378
10
а ота с контексто
ильтра
Monthly Avg KeepFilters :=
AVERAGEX (
KEEPFILTERS ( VALUES ( 'Date'[Month] ) );
[Sales Amount]
)
Рис 10 24
тера ии о стол
с никальн
озволили ри ти к верн
рас ета
и зна ени
и
Фильтры произвольной формы встречаются в реальных отчетах не так часто.
Но пользователи имеют вполне законное право их создавать по своему усмотрению. Чтобы обеспечить правильный расчет меры в присутствии фильтра
произвольной формы, необходимо следовать двум простым правилам:
„ осуществляя итерации по столбцу, убедитесь в том, что он содержит уникальные значения на том уровне гранулярности, на котором производятся вычисления. Например, в таблице
больше 12 месяцев, а значит,
для подобных итераций лучше будет использовать столбец
;
„ если первое правило выполнить невозможно, допустимо воспользоваться функцией
для гарантии сохранения формы произвольного фильтра в контексте фильтра. Помните при этом, что функция
способна изменить семантику вычисления. В связи с этим
необходимо внимательно отнестись к тому, чтобы она не внесла ошибочные расчеты в меру.
Если вы будете следовать этим простым правилам, ваш код всегда будет
должным образом защищен в присутствии фильтров произвольной формы.
Закл чение
В данной главе вы познакомились с несколькими функциями для анализа содержимого контекста фильтра и/или изменения поведения мер в зависимости
от контекста. Мы также рассмотрели некоторые важные техники управления
контекстом фильтра и описали его возможные состояния. Вот наиболее важные концепции, о которых было рассказано в главе:
10
а ота с контексто
ильтра
379
„ столбец может быть отфильтрован напрямую, а может входить в состав
перекрестного фильтра, когда действие на него распространяется вследствие прямой фильтрации другого столбца или таблицы. Вы можете проверить, является ли столбец участником обычной или кросс-фильтрации
при помощи функций
и
соответственно;
„ функция
проверяет, одно ли значение из указанного
столбца видимо в текущем контексте фильтра. Это бывает полезно перед
извлечением этого значения при помощи функции
. Функция
, в свою очередь, призвана упростить использование шаблона
/
;
„ использование функции
не эквивалентно применению пары
функций
и
. В присутствии кросс-фильтра второй вариант является более безопасным, поскольку учитывает в процессе вычисления
наличие перекрестных фильтров;
„ функция
и ее аналоги, начинающиеся с
, могут быть использованы в выражениях для предотвращения преобразования контекста. Фактически применение функции
в формуле вычисляемого столбца или
в любом другом контексте строки вынуждает DAX отказаться от операции преобразования контекста;
„ каждый столбец в таблице обладает так называемой привязкой данных.
Привязка данных позволяет DAX применять фильтры и использовать
связи. Привязка данных сохраняется при прямой ссылке на столбец таблицы и утрачивается при использовании выражений;
„ привязка данных к одному или нескольким столбцам в модели может
быть осуществлена при помощи функции
;
„ не все фильтры являются простыми. Пользователь вправе создавать более сложные фильтры либо посредством интерфейса, либо в самом коде.
Наиболее сложным видом фильтров являются фильтры произвольной
формы. Сложность при их использовании напрямую связана со взаимодействием с функцией
и преобразованием контекста.
Возможно, вам не удастся сразу запомнить все функции и концепции, описанные в данной главе. Но важно то, что вы познакомились с ними уже на этом
этапе изучения языка DAX. С приобретением опыта вы, безусловно, будете
сталкиваться на практике с ситуациями, рассмотренными в данной главе. Но
вы всегда можете вернуться к этим страницам книги и освежить в памяти способы решения тех или иных проблемных сценариев.
В следующей главе мы будем использовать многие функции, с которыми вы
познакомились здесь, для осуществления вычислений в рамках иерархий. Как
вы узнаете, работа с иерархиями по большей части основана на понимании
формы текущего контекста фильтра.
ГЛ А В А 11
Работа с иерархиями
ерар ии (hierarchy) очень часто присутствуют в моделях данных с целью облегчения работы пользователя с заранее известными вложенностями. При
этом в DAX нет специальных функций для осуществления вычислений внутри иерархий. В результате проведение простейших расчетов вроде получения
того или иного показателя в процентном отношении к его подгруппе требует
написания сложного кода на DAX, да и поддержка и сопровождение вычислений в рамках иерархий – задача не из простых.
Однако изучать методы и принципы работы с иерархиями крайне необходимо, поскольку сценарии с их использованием очень распространены. В данной
главе мы покажем, как создавать базовые вычисления в присутствии иерархий
и как использовать язык DAX для преобразования иерархии типа родитель/потомок в обычную иерархию.
Вычисление процентов внутри иерархии
Распространенной задачей при работе с иерархиями является создание меры,
которая будет вести себя по-разному в зависимости от уровня выбранного элемента. Примером такой меры может служить доля показателя по отношению
к родительскому элементу. При этом мера должна работать на любом уровне
иерархии и показывать процент по отношению к группе, являющейся для него
непосредственным родителем.
Представьте иерархический справочник товаров, в котором уровнями будут
категории, подкатегории и собственно товары. Наша мера должна для категории показывать долю относительно итогов, для подкатегории – относительно
соответствующей категории, а для товара – относительно его подкатегории.
Таким образом, в зависимости от уровня иерархии вычисления будут разными.
Пример отчета с иерархиями показан на рис. 11.1.
В Excel можно создать подобное вычисление в сводной таблице при помощи
опции До ол итель е
исле и (S
A ), чтобы переложить
бремя расчетов на Excel. Но если вы хотите использовать вычисление вне зависимости от используемого клиентского приложения, то лучше всего будет
написать собственную меру, которая будет рассчитываться непосредственно
в модели данных. К тому же изучение этой техники может пригодиться вам
и в других сценариях.
К сожалению, в DAX вычисление доли показателя относительно родительского элемента – задача не самая простая. Здесь мы сталкиваемся с первым
11
а ота с иерар и
и
381
серьезным ограничением языка DAX, не позволяющим создать универсальную
меру, которая работала бы с произвольным количеством столбцов в отчете.
Причина этого в том, что DAX не знает, как был построен тот или иной отчет
или как использовалась иерархия в клиентских инструментах. DAX не имеет
ни малейшего понятия о том, как именно пользователь собирает отчет. Движок
просто получает запрос, в котором не указано, что будет вынесено в строки,
что – в столбцы и какие фильтры и срезы будут использоваться при построении отчета.
Рис 11 1
Мера PercOnParent о огает л
е он ть зна ени в от ете
Несмотря на то что универсальная формула не может быть создана, вы вполне можете написать меру, которая будет возвращать корректные значения при
правильном использовании. Поскольку наша иерархия насчитывает три уровня (категории, подкатегории и товары), мы начнем с создания трех разных мер
для каждого из них:
PercOnSubcategory :=
DIVIDE (
[Sales Amount];
CALCULATE (
[Sales Amount];
ALLSELECTED ( Product[Product Name] )
)
)
PercOnCategory :=
DIVIDE (
[Sales Amount];
CALCULATE (
[Sales Amount];
382
11
а ота с иерар и
и
ALLSELECTED ( Product[Subcategory] )
)
)
PercOnTotal :=
DIVIDE (
[Sales Amount];
CALCULATE (
[Sales Amount];
ALLSELECTED ( Product[Category] )
)
)
Эти три меры прекрасно справляются со своими расчетами. На рис. 11.2
представлен отчет с использованием всех трех мер.
Рис 11 2 Мер
равильно ра ота т только на свои
ровн
Несложно заметить, что созданные нами меры показывают корректные
результаты только на соответствующих им уровнях. В остальных случаях они
возвращают значение 100 , в чем мало пользы. Более того, создание трех мер
не входит в наши планы, нам хотелось бы уместить все расчеты в одной мере.
И мы сделаем это на следующем шаге.
Начнем с очистки значений 100 для меры
. Мы хотели бы
избежать произведения расчетов, если в выбранной строке отсутствует столбец
. А значит, нам необходимо проверить, входит ли столбец
в фильтр запроса, формирующего матрицу. Для этой цели есть специальная функция
. Функция
возвращает значение
, если
столбец, переданный ей в качестве аргумента, входит в состав фильтра и принадлежит к столбцам, используемым для выполнения группировки. Таким образом, формула может быть изменена следующим образом:
PercOnSubcategory :=
IF (
11
а ота с иерар и
и
383
ISINSCOPE ( Product[Product Name] );
DIVIDE (
[Sales Amount];
CALCULATE (
[Sales Amount];
ALLSELECTED ( Product[Product Name] )
)
)
)
На рис. 11.3 показан отчет с использованием этой меры.
Рис 11 3
с ольз
нк и ISINSCOPE,
из ер PercOnSubcategory
рали ес олезн е зна ени 100
Та же техника может быть использована для удаления значений 100
из
других мер. Обратите внимание, что в мере
мы должны проверять, что столбец
входит в область видимости, а
– нет.
Причина этого в том, что когда в отчете применяется срез по столбцу
с использованием иерархии, автоматически производится срез и по
столбцу
, выводя при этом наименование товара, а не подкатегории. Во избежание дублирования кода для этих условий лучше будет написать
единую меру, которая будет производить разные вычисления в зависимости от
видимого уровня иерархии, основываясь на результате сканирования иерархии при помощи функции
с нижнего уровня до верхнего. Представляем вам код получившейся меры
:
PercOnParent :=
VAR CurrentSales = [Sales Amount]
VAR SubcategorySales =
CALCULATE (
[Sales Amount];
ALLSELECTED ( Product[Product Name] )
)
VAR CategorySales =
384
11
а ота с иерар и
и
CALCULATE (
[Sales Amount];
ALLSELECTED ( Product[Subcategory] )
)
VAR TotalSales =
CALCULATE (
[Sales Amount];
ALLSELECTED ( Product[Category] )
)
VAR RatioToParent =
IF (
ISINSCOPE ( Product[Product Name] );
DIVIDE ( CurrentSales; SubcategorySales );
IF (
ISINSCOPE ( Product[Subcategory] );
DIVIDE ( CurrentSales; CategorySales );
IF (
ISINSCOPE ( Product[Category] );
DIVIDE ( CurrentSales; TotalSales )
)
)
)
RETURN RatioToParent
Использование меры
, как и ожидалось, позволило получить
правильные результаты, что видно по рис. 11.4.
Рис 11 4 Мера PercOnParent о
единила три стол
а в один
Теперь нам не нужны те три меры, которые мы написали в начале главы.
Единая мера
проводит все вычисления и выводит результаты на
соответствующих уровнях иерархии.
11
а ота с иерар и
и
385
Примечание
ор док, в которо
ере ислен словн е о ератор
, важен М наинае
роверк с нижнего ровн иерар ии и осте енно одни ае с в е сли
из енить ор док о ода иерар ии, рез льтат в ислени окаж тс не равильн и
ажно о нить, то ри ильтра ии одкатегории в иерар ии авто ати ески ильтр етс и категори
Мера
будет работать корректно только в том случае, если пользователь вынесет правильную иерархию в строки. Например, если категорию
товаров заменить на цвет, полученные результаты понять будет непросто. Так
что эта мера применима только для иерархии товаров вне зависимости от того,
выбраны ли в отчете соответствующие поля.
Работа с иерархиями типа родитель потомок
Внутренне DAX не поддерживает в чистом виде иерархии типа родитель/потомок, характерные для баз данных Multidimensional в Analysis Services. При этом
в языке DAX есть специальные функции, служащие для в равнивани (flatten)
иерархий типа родитель/потомок в обычные иерархии на основании столбцов.
Этих функций достаточно для большинства сценариев, хотя вам и придется делать прикидку на этапе разработки модели о максимальной глубине иерархии. В данном разделе мы научим вас при помощи функций DAX создавать
иерар ии ти а родител отомок (parent/child hierarchy), иногда называемые
иерархиями P/C.
Типичный пример такой иерархии изображен на рис. 11.5.
Annabel
Michael
Catherine
Bill
Brad
Harry
Chris
Рис 11 5
Julie
Vincent
ра и еское редставление иерар ии ти а родитель ото ок
Иерархии типа родитель/потомок обладают следующими характерными
особенностями:
„ количество уровней может быть неодинаковым внутри иерархии. К примеру, путь от Аннабель (Annabel) к Майклу (Michael) вмещает в себя два
уровня, в то время как в той же иерархии путь от Билла (Bill) к Крису
(Chris) включает три уровня;
„ иерархия обычно хранится в одной таблице со ссылками на родительский элемент в каждой строке.
Традиционный вариант хранения данных об иерархии типа родитель/потомок показан на рис. 11.6.
386
11
а ота с иерар и
и
Рис 11 6 а ли а содержит ин ор а и
о иерар ии ти а родитель ото ок
Несложно догадаться, что в столбце
указан ключ родительского
элемента. Например, у Кэтрин (Catherine) в этом поле стоит цифра 6, являющаяся ключом для Аннабель. Проблема с такой моделью данных состоит в том,
что в данный момент таблица связана сама с собой, то есть две таблицы, участвующие в отношении, фактически являются одной и той же таблицей в модели
данных.
Табличные модели данных не поддерживают связи таблицы самой с собой.
Следовательно, мы должны изменить модель данных таким образом, чтобы
иерархия типа родитель/потомок преобразовалась в обычную иерархию, где
каждый столбец представляет свой уровень иерархии.
Перед тем как погрузиться в детали иерархий типа родитель/потомок, стоит
сделать еще одно замечание. Взгляните на рис. 11.7, на котором изображена
таблица со значениями, которые мы хотим агрегировать с использованием
иерархии.
Рис 11 7 а ли а с данн и
дл иерар ии ти а родитель ото ок
В строках таблицы фактов содержатся ссылки как на элементы коне но
го уровн (leaf-level), так и на промежуточные элементы иерархии. Возьмем,
к примеру, строку с Аннабель. В этой строке содержится числовое значение, но
не стоит забывать, что у Аннабель есть три дочерних элемента. Таким образом,
11
а ота с иерар и
и
387
при суммировании данных по Аннабель формула должна учитывать как эту
строку, так и все дочерние элементы.
На рис. 11.8 показан результат, которого мы хотим добиться.
Рис 11 8
ез льтат рос отра иерар ии
ри о о и атри
Чтобы прийти к конечной цели, нам необходимо проделать существенный
путь. Первым шагом после загрузки таблицы в модель данных будет создание
вычисляемого столбца, в котором будет храниться путь для достижения каждого элемента. Поскольку мы не можем использовать традиционные связи
между таблицами, придется призвать на помощь специальные функции DAX
для работы с иерархиями типа родитель/потомок.
В новом вычисляемом столбце с именем
мы воспользуемся функцией
:
Persons[FullPath] = PATH ( Persons[PersonKey]; Persons[ParentKey] )
Функция
принимает два параметра. Первым является ключ таблицы
(в нашем случае это
), а вторым – ключ родительского элемента. Функция рекурсивно проходит по таблице и для каждого элемента вычисляет путь, выраженный в последовательности ключей, разделенных вертикальной чертой ( ). На рис. 11.9 видна функция
в действии.
Сам по себе столбец
не представляет большого интереса. В то же
время он обладает исключительной важностью, являясь основой для расчета
других вычисляемых столбцов на пути построения иерархии. На следующем
шаге мы создадим еще три вычисляемых столбца, по одному для каждого
уровня иерархии:
Persons[Level1] = LOOKUPVALUE(
Persons[Name];
Persons[PersonKey]; PATHITEM ( Persons[FullPath]; 1; INTEGER )
)
Persons[Level2] = LOOKUPVALUE(
388
11
а ота с иерар и
и
Persons[Name];
Persons[PersonKey]; PATHITEM ( Persons[FullPath]; 2; INTEGER )
)
Persons[Level3] = LOOKUPVALUE(
Persons[Name];
Persons[PersonKey]; PATHITEM ( Persons[FullPath]; 3; INTEGER )
)
Рис 11 9
содержитс
стол
олн
е FullPath
ть к ле ент
Вычисляемые столбцы названы Level1, Level2 и Level3, а единственное отличие при их создании кроется во втором параметре, переданном функции
, который принимает значения 1, 2 и 3 соответственно. При создании
вычисляемых столбцов была использована функция
для поиска строки, в которой значение поля
эквивалентно результату функции
. Сама функция
возвращает указанный во втором
параметре элемент из столбца, построенного при помощи функции
, или
пустое значение, если в качестве второго параметра передано число, превышающее длину пути. Получившаяся таблица показана на рис. 11.10.
Рис 11 10
стол
а Level содержатс зна ени соответств
и
ровне иерар ии
В этом примере мы использовали три столбца – такова максимальная глубина нашей иерархии. В реальных примерах вам придется рассчитывать макси11
а ота с иерар и
и
389
мальную вложенность и создавать соответствующее количество вычисляемых
столбцов. Таким образом, несмотря на то что количество уровней в иерархии
типа родитель/потомок не является фиксированным, чтобы реализовать подобную иерархию в модели данных, придется определиться заранее с ее максимальной глубиной. Всегда лучше добавить пару лишних уровней для будущего расширения иерархии без необходимости обновлять модель данных.
Теперь нам нужно преобразовать набор столбцов в иерархию. Также, поскольку остальные поля не несут никакой полезной информации, мы скроем их от клиентских приложений. На этом этапе мы можем построить отчет
с иерархией, вынесенной на строки, и суммами в области значений, но результат нас не удовлетворит. На рис. 11.11 показан вывод отчета в виде матрицы.
С этим отчетом есть пара проблем:
„ под строкой с именем Аннабель есть две пустые строки с указанием суммы по самой Аннабель;
„ под Кэтрин есть пустая строка также со значением по Кэтрин. То же самое
можно сказать и о других строках.
В иерархии всегда показываются три уровня вложенности, даже для путей
с максимальной глубиной, равной двум, как в случае с Гарри (Harry), у которого
нет детей.
Рис 11 11
ерар и в гл дит не так,
как
ожидали, в не сли ко
ного строк
Эти проблемы связаны с визуализацией результатов. В остальном цифры
считаются правильно, поскольку под строкой с Аннабель находятся все ее дочерние элементы. Важным аспектом этого решения было то, что нам удалось
имитировать связь таблицы самой с собой (также известную как рекурсивну
св
(recursive relationship)), используя функцию
для создания вычис390
11
а ота с иерар и
и
ляемого столбца. Остальные сложности касались отображения результатов, но
мы, по крайней мере, продвинулись к правильному решению.
Следующей нашей задачей будет удаление пустых значений. Например, во
второй строке вывода значение 600 должно принадлежать Аннабель, а не пустой ячейке. Мы можем решить эту проблему, исправив формулу для уровней.
Избавимся от пустых значений путем вывода элемента предыдущего уровня,
если достигли конца пути. Ниже представлен измененный код для меры
:
PC[Level2] =
IF ( PATHLENGTH ( Persons[FullPath] ) >= 2;
LOOKUPVALUE(
Persons[Name];
Persons[PersonKey]; PATHITEM ( Persons[FullPath]; 2; INTEGER )
);
Persons[Level1]
)
Формула для
в изменениях не нуждается, поскольку первый уровень
всегда существует. Формулу для
мы изменили, руководствуясь тем же
шаблоном, что и для
. С обновленными формулами таблица выглядит
так, как показано на рис. 11.12.
Рис 11 12 С из ененн и ор ла и в стол
никогда не дет ст зна ени
а Level
Как видите, пустые ячейки исчезли из отчета. Но строк в выводе все равно
слишком много. На рис. 11.13 выделены две строки.
Обратите внимание на вторую и третью строки отчета. В них мы видим одно
и то же значение из иерархии, а именно Аннабель. Мы могли бы смириться
с показом второй строки, поскольку она выводит значение для Аннабель, но
третья строка тут точно лишняя, она не дает нам никакой новой информации.
Как видите, решение о том, показывать или скрывать строку, базируется на
глубине вложенности элемента. Мы можем позволить пользователю опуститься до второго уровня в иерархии Аннабель, но третий ему видеть ни к чему.
Мы вполне можем хранить в вычисляемом столбце длину пути для достижения строки. Длина пути покажет, что Аннабель является корневым элементом
11
а ота с иерар и
и
391
иерархии. И правда, это ведь элемент первого уровня с путем, содержащим только одно значение. Кэтрин, к примеру, является элементом второго уровня с длиной пути, равной двум, поскольку она – дочь Аннабель. Кроме того, хоть это и не
так очевидно, но Кэтрин видима также и на первом уровне иерархии, ведь ее
значение агрегируется в родительском элементе, то есть в Аннабель. Имя Кэтрин
показывается по причине того, что в столбце
для нее указана Аннабель.
Рис 11 13
ново от ете
ст зна ени нет
Зная уровень каждого элемента в иерархии, мы можем определить, что этот
элемент должен быть видим, когда в отчете показана иерархия до этого уровня. На более глубоких уровнях иерархии в отчете этот элемент должен быть
скрыт. Чтобы реализовать этот алгоритм, нам необходимы еще два параметра:
„ глубина вложенности каждого элемента. Это фиксированная величина
для каждой строки иерархии, а значит, она может храниться в вычисляемом столбце;
„ текущая глубина просмотра визуального элемента отчета. Эта величина
является динамической и зависит от текущего контекста фильтра. Она
должна быть выражена при помощи меры, поскольку ее значение зависит
от отчета, и для каждой строки отчета оно будет свое. Например, Аннабель
представляет элемент первого уровня, но она появляется в трех строках по
причине того, что текущая глубина отчета содержит три разных значения.
Глубину вложенности элемента рассчитать несложно. Добавим новый вычисляемый столбец с простой формулой к таблице
:
Persons[NodeDepth] = PATHLENGTH ( Persons[FullPath] )
Функция
вычисляет количество элементов в строке, возвращаемой функцией
. На рис. 11.14 показан отчет с новым вычисляемым
столбцом.
Глубину вложенности вычислить было весьма просто. Сложнее определить
глубину просмотра отчета, поскольку это необходимо делать в мере. При этом
сама логика расчета будет несложной – похожую технику мы уже использовали при работе с обычными иерархиями. В мере будет использована функция
для определения того, какие столбцы в иерархии входят в фильтр.
392
11
а ота с иерар и
и
Рис 11 14
стол
е NodeDepth ранитс вели ина ровн вложенности ле ента
Кроме того, в этой формуле мы воспользуемся тем, что значения типа
могут легко конвертироваться в целочисленные величины, где
равно
1, а
– 0:
BrowseDepth :=
ISINSCOPE ( Persons[Level1] ) +
ISINSCOPE ( Persons[Level2] ) +
ISINSCOPE ( Persons[Level3] )
Таким образом, если в фильтр включен только
, значением меры
будет 1. Если столбцы
и
отфильтрованы, а
– нет,
вернется 2, и т. д. На рис. 11.15 представлено вычисление меры
.
Рис 11 15
ере BrowseDepth
содержитс гл ина рос отра от ета
Мы постепенно приближаемся к окончательному решению нашего сценария. Последнее, что нам нужно знать, – это то, что по умолчанию в отчете будут
скрыты строки, в которых каждая вычисленная мера возвращает пустое значение. Мы воспользуемся этой особенностью для скрытия нежелательных строк.
Преобразуя значение меры
в
для строк, которые мы не хотим
видеть в отчете, можно скрыть их в матрице. Таким образом, в своем решении
мы будем использовать:
11
а ота с иерар и
и
393
„ уровень вложенности каждого элемента, хранящийся в вычисляемом
столбце
;
„ глубину просмотра текущей ячейки в отчете, выраженную в мере
;
„ скрытие нежелательных строк путем установки в них пустых значений.
Самое время объединить всю эту информацию в одной мере, как показано
ниже:
PC Amount :=
IF (
MAX (Persons[NodeDepth]) < [BrowseDepth];
BLANK ();
SUM(Sales[Amount])
)
Чтобы понять, как работает эта мера, взгляните на отчет, показанный на
рис. 11.16. В нем есть все необходимое, чтобы уловить суть этой формулы.
Рис 11 16
от ете оказан все ро еж то н е вели ин ,
ис ольз
иес в ор ле
Если вы посмотрите на первую строку с Аннабель, то увидите, что
здесь равен 1, поскольку это корневой элемент иерархии. При этом
в столбце
, определенном как
, указано значение 2, сообщая о том, что для текущего элемента должны быть показаны данные не только на первом уровне, но и на втором. Таким образом,
для текущего элемента будет показана также информация о детях, а значит,
он должен быть видимым. Во второй строке по Аннабель в столбцах
и
указаны 2 и 1 соответственно. Дело в том, что контекст
фильтра отбирает все строки, где в
и
указано значение Аннабель,
а в иерархии есть только одна такая строка, соответствующая самой Аннабель.
394
11
а ота с иерар и
и
Но у Аннабель поле
равно 1, а поскольку глубина просмотра отчета
здесь у нас равна 2, необходимо скрыть эту строку. Так что в мере
для нее мы вернем пустое значение.
Будет полезно, если для остальных строк вы проведете подобный анализ самостоятельно. Так вы лучше поймете, как на самом деле работает эта формула.
И хотя вы можете просто вернуться к этой главе и скопировать формулу, когда
у вас появится такая необходимость, будет лучше, если вы разберетесь, как она
работает. Это даст вам возможность понять, как контекст фильтра взаимодействует с различными частями формулы.
И в заключение нам нужно убрать из отчета все вспомогательные столбцы,
оставив только
. Теперь отчет приобрел тот вид, который мы и хотели получить, что видно по рис. 11.17.
Рис 11 17 Оставив в от ете
единственн
ер ,
те са
скр ли нежелательн е строки
Главным недостатком примененного нами подхода является то, что такой
же шаблон необходимо будет использовать для каждой меры, которую пользователь захочет добавить в отчет с готовой иерархией. Если мера для нежелательных строк не будет возвращать пустые значения, строки просто не скроются и будут портить весь шаблон отчета.
На данном этапе нас удовлетворяет полученный результат. Но есть небольшая
проблема. Если взглянуть на итоговую строку по Аннабель, мы увидим значение
3200. Сумма значений по ее детям составляет 2600. Потерялось еще одно значение 600, принадлежащее самой Аннабель. Кому-то такой визуализации отчета
будет достаточно, поскольку значение по родителю легко получить путем вычитания суммы по детям из итога по самому элементу. Но если сравнить отчет
с нашей изначальной целью, несложно заметить, что необходимо разместить
родительский элемент и в качестве дочернего для себя самого. На рис. 11.18
представлено сравнение полученного нами результата и желательного.
Сейчас мы уже хорошо представляем себе технику дальнейших действий.
Чтобы показать данные по Аннабель, необходимо найти условие, которое позволит нам идентифицировать этот элемент как видимый. Но в подобном случае условие будет не таким простым. Нам нужно показать элементы, не являющиеся конечными (то есть имеющие потомков) и при этом обладающие
собственными значениями. При этом мы должны показать эти элементы на
11
а ота с иерар и
и
395
вложенном уровне. Остальные элементы (являющиеся конечными или не обладающие собственными значениями) должны оставаться скрытыми.
Рис 11 18 ре е
рез льтат
не достигн т, н жно оказать е е
несколько строк
Первое, что нам нужно сделать, – это создать вычисляемый столбец с отображением того, является ли элемент конечным, то есть не имеющим потомков.
В DAX написать такое условие не составит труда: конечными являются элементы, не являющиеся родителем ни для одного из других элементов. Чтобы
проверить это, нам достаточно посчитать элементы, для которых исследуемый
узел будет родительским. Если мы получим нулевое значение, значит, элемент
расположен на конечном уровне. Следующий код вполне подойдет для описанной процедуры:
Persons[IsLeaf] =
VAR CurrentPersonKey = Persons[PersonKey]
VAR PersonsAtParentLevel =
CALCULATE (
COUNTROWS ( Persons );
ALL ( Persons );
Persons[ParentKey] = CurrentPersonKey
)
VAR Result = ( PersonsAtParentLevel = 0 )
RETURN Result
На рис. 11.19 показан вычисляемый столбец
, добавленный к модели
данных.
Теперь, когда мы определили, какие элементы нашей иерархии являются
конечными, пришло время написать итоговую формулу для работы с нашей
иерархией:
FinalFormula =
VAR TooDeep = [MaxNodeDepth] + 1 < [BrowseDepth]
VAR AdditionalLevel = [MaxNodeDepth] + 1 = [BrowseDepth]
VAR Amount =
SUM ( Sales[Amount] )
396
11
а ота с иерар и
и
VAR HasData =
NOT ISBLANK ( Amount )
VAR Leaf =
SELECTEDVALUE (
Persons[IsLeaf];
FALSE
)
VAR Result =
IF (
NOT TooDeep;
IF (
AdditionalLevel;
IF (
NOT Leaf && HasData;
Amount
);
Amount
)
)
RETURN
Result
Рис 11 19
стол
е IsLeaf дл коне н
ле ентов казано зна ение True
Использование переменных позволило сделать формулу более легкой для
восприятия. Приведем некоторые замечания по работе этого кода:
„ в переменной
проверяется, превышает ли глубина просмотра
отчета максимальный уровень вложенности элемента, увеличенный на
единицу. Таким образом выполняется проверка на предельную глубину
просмотра;
„ переменная
содержит результат проверки на то, что текущая глубина просмотра является дополнительным уровнем для элементов, обладающих собственным значением и не являющихся при этом конечными;
„ в переменной
мы проверяем, есть ли у элемента собственное
значение;
11
а ота с иерар и
и
397
„ переменная
проверяет, является ли элемент конечным;
„ переменная
хранит итоговый результат формулы, что облегчает
вывод промежуточных значений на этапе написания кода.
Оставшаяся часть кода в основном состоит из вложенных условий , служащих для поддержания логики формулы.
Понятно, что если бы в моделях данных была встроенная возможность работы с иерархиями типа родитель/потомок, нам не пришлось бы проделывать
всю эту работу. В конце концов, формула получилась не самая легкая для восприятия, к тому же она требует хорошего понимания контекстов вычисления
и моделирования данных в целом.
Важно
сли уровень совместимости
ва е
одели данн
равен
1 00, в
ожете вос ользоватьс с е иальн
сво ство
то сво ство озвол ет авто ати ески скр вать ст е зна ени
о состо ни на а рель 201 года то
сво ство недост но в
и
одро ное о исание того, как ис ользовать
то сво ство в та ли н
одел данн , ожно на ти о адрес https://docs.microsoft.
com/en-us/sql/analysis-services/what-s-new-in-sql-server-analysis-servic 201
201 сли ис ольз е
ва и инстр ент оддерживает сво ство
,
насто тельно реко енд е вос ользоватьс и , в есто того то
исать сложн код
на
, оказанн в е, дл скр ти ровне в нес алансированно иерар ии
Закл чение
В данной главе вы узнали, как проводить вычисления в модели данных при наличии иерархий. Как обычно, пройдемся по основным концепциям, которые
здесь были предложены:
„ иерархии не являются составной частью языка DAX. Они могут быть построены в модели данных, но DAX не умеет ссылаться на иерархии и использовать их в своих вычислениях;
„ чтобы определить уровень иерархии, пригодится специальная функция
. Эта функция не определяет уровень просмотра. Вместо этого
она идентифицирует лишь наличие фильтра по столбцу;
„ для расчета долей внутри родительского элемента необходимо уметь
анализировать текущий уровень иерархии и подбирать подходящий набор фильтров для воссоздания фильтра родительского элемента;
„ с иерархиями типа родитель/потомок в DAX можно работать, используя
предопределенную функцию
и создавая набор вспомогательных
столбцов – по одному для каждого уровня иерархии;
„ унарн е о ератор (unary operators), часто использующиеся в иерархиях
типа родитель/потомок, могут стать настоящим испытанием. С их упрощенными разновидностями (только /–) можно работать путем написания довольно сложного кода на DAX. Решение действительно комплексных сценариев потребует написания еще более сложного кода на DAX, но
эта тема выходит за границы данной главы.
ГЛ А В А 12
Работа с таблицами
Таблицы играют важную роль в формулах DAX. В предыдущих главах вы научились осуществлять итерации по таблицам, создавать вычисляемые таблицы
и выполнять некоторые вычисления, требующие навыков работы с таблицами.
Более того, аргументы фильтра функции
также представляют собой таблицы, и при написании сложных выражений умение строить правильные таблицы фильтров является немаловажным фактором. DAX предлагает
богатый набор функций для работы с таблицами. В данной главе мы познакомимся с теми из них, которые предназначены для создания таблиц и манипулирования ими.
Описание большинства новых функций мы будем сопровождать примерами, которые будут полезны как в работе с самими таблицами, так и в качестве
пособия по написанию сложных мер.
Функция CALCULATETABLE
Первой функцией для работы с таблицами, с которой мы познакомимся, будет
. Да, мы уже не раз использовали ее в данной книге. Здесь
же мы дадим более полное описание работы функции и поделимся советами
о том, когда ее стоит использовать.
Функция
работает точно так же, как и ее аналог
, за исключением того, что ее результатом является таблица, тогда как на
выходе
всегда будет скалярная величина, будь то целое число или
строка. К примеру, если вам необходимо создать таблицу, в которой будут содержаться только красные товары, вы можете сделать это так:
CALCULATETABLE (
'Product';
'Product'[Color] = "Red"
)
Часто задаваемым вопросом в этом случае является следующий: а какая разница между функциями
и
? Ведь предыдущее выражение можно записать и так:
FILTER (
'Product';
'Product'[Color] = "Red"
)
12
а ота с та ли а и
399
Несмотря на полную идентичность записи, за исключением названия самой функции, семантически две эти функции очень сильно отличаются. При
выполнении функции
сначала происходит изменение контекста фильтра, а затем вычисляется выражение. Функция
, напротив,
осуществляет итерации по таблице, переданной в качестве первого параметра,
извлекая при этом строки, удовлетворяющие условию. Иными словами, функция
не изменяет контекст фильтра.
Различия между этими функциями можно увидеть на следующем примере:
Red Products CALCULATETABLE =
CALCULATETABLE (
ADDCOLUMNS (
VALUES ( 'Product'[Color] );
"Num of Products"; COUNTROWS ( 'Product' )
);
'Product'[Color] = "Red"
)
Результат выполнения данного кода показан на рис. 12.1.
Рис 12 1
на одитс
азе данн
красн товаров
При использовании функции
контекст фильтра, в котором
выполняются функции
и
, отфильтрован по красному цвету. Соответственно, в результате мы получили одну строку, содержащую
красный цвет с указанием количества товаров этого цвета. Иными словами,
функция
подсчитывает уже только красные товары, не требуя преобразования контекста от строки, сгенерированной функцией
.
Если заменить функцию
на
, результат будет другим. Взгляните на следующий пример:
Red Products FILTER external =
FILTER (
ADDCOLUMNS (
VALUES ( 'Product'[Color] );
"Num of Products"; COUNTROWS ( 'Product' )
);
'Product'[Color] = "Red"
)
На этот раз результат будет уже не 99. Вместо этого мы увидим общее количество товаров в модели данных, как показано на рис. 12.2.
Рис 12 2
ес отр на единственн строк с казание красного вета,
зна ение в стол е Num of Products соответств ет о е коли еств товаров
400
12
а ота с та ли а и
В итоговой таблице также присутствует только одна строка с указанием красного цвета, но теперь количество товаров равно 2517, а не 99, что соответствует
общему количеству товаров в базе. Причина в том, что функция
не меняет контекст фильтра. Более того, она вычисляется после функции
.
Следовательно, функция
проходит по всем товарам, и
подсчитывает их общее количество из-за отсутствия преобразования
контекста. И только затем функция
выбирает строку с красным цветом.
Если вы хотите использовать функцию
вместо
, выражение должно быть написано иначе, чтобы функция
инициировала преобразование контекста:
Red Products FILTER internal =
ADDCOLUMNS (
FILTER (
VALUES ( 'Product'[Color] );
'Product'[Color] = "Red"
);
"Num of Products"; CALCULATE ( COUNTROWS ( 'Product' ) )
)
Теперь результат вновь будет 99. Чтобы получить тот же итог, что и при вызове функции
, нам пришлось изменить порядок выполнения формулы. Здесь функция
запускается первой, после чего подсчет
строк полагается на операцию преобразования контекста, чтобы заставить
контекст строки от функции
стать контекстом фильтра для
функции
.
Функция
в процессе выполнения меняет контекст фильтра. Это очень мощная функция, поскольку она распространяет свое действие на
множество других функций в выражении DAX. Но есть у этой функции и свои
ограничения, связанные с типом фильтра, который она создает. К примеру,
функция
способна накладывать фильтры только на столбцы,
принадлежащие модели данных. Если вам нужно будет получить список покупателей с суммой продаж, превышающей один миллион, функция
вам не подойдет, поскольку
является мерой. Функция
не может устанавливать фильтр на меру, тогда как FILTER –
может. Это показано в следующем выражении – здесь заменить функцию
на
нельзя, будет синтаксическая ошибка:
Large Customers =
FILTER (
Customer;
[Sales Amount] > 1000000
)
Функция
, как и
, в процессе выполнения осуществляет преобразование контекста и может быть дополнена всеми модификаторами, применимыми к функции
:
,
,
и многими другими. Это делает функцию
более
мощной по сравнению с
. Но это отнюдь не означает, что вы должны
перестать использовать функцию
и полностью переходить на
12
а ота с та ли а и
401
. У каждой из этих функций есть свои достоинства и недостатки, и выбор должен быть осознанным.
Как правило, функцию
следует использовать, когда вам
достаточно будет ограничиться применением фильтров к столбцам в модели
данных или когда необходимо воспользоваться ее дополнительной функциональностью вроде преобразования контекста или использования модификаторов контекста фильтра.
Манипулирование таблицами
DAX предлагает сразу несколько функций для манипулирования таблицами.
Эти функции могут быть использованы для создания вычисляемых таблиц,
таблиц для осуществления итераций или применения их результатов в качестве аргументов фильтра функции
. В данном разделе мы поговорим обо всех этих функциях и рассмотрим полезные примеры. Существуют
также и другие табличные функции, которые главным образом применяются
в запросах. О них мы расскажем в главе 13.
Функция ADDC LUM S
Функция
представляет собой итератор, возвращающий все
строки и столбцы табличного выражения из первого аргумента и добавляющий к итоговому результату вновь созданные столбцы. Например, на выходе
следующего выражения будет таблица со всеми цветами товаров, присутствующими в модели данных, и суммой продаж по каждому из них:
ColorsWithSales =
ADDCOLUMNS (
VALUES ( 'Product'[Color] );
"Sales Amount"; [Sales Amount]
)
Результат показан на рис. 12.3.
Будучи итератором, функция
вычисляет значения столбцов
в контексте строки. В нашем примере мы получили суммы продаж по товарам конкретных цветов, поскольку в столбце
используется мера.
Следовательно, меру
тут же окружает функция
, инициируя преобразование контекста. Если вместо меры используется обычное
выражение, чаще всего функция
указывается явно как раз для выполнения преобразования контекста.
Функция
нередко используется совместно с функцией
для наложения фильтра на временный вычисляемый столбец. Например, если
вам необходимо получить список товаров с суммой продаж, равной или превышающей 150 000, вы можете воспользоваться следующей конструкцией:
HighSalesProducts =
VAR ProductsWithSales =
402
12
а ота с та ли а и
ADDCOLUMNS (
VALUES ( 'Product'[Product Name] );
"Product Sales"; [Sales Amount]
)
VAR Result =
FILTER (
ProductsWithSales;
[Product Sales] >= 150000
)
RETURN Result
Рис 12 3
ез льтат содержит все вета товаров
ис
родаж о ни
Результат показан на рис. 12.4.
Рис 12 4
рез льтир
и на ор вкл
ен названи товаров и с
родаж
Того же результата можно добиться самыми разными способами, даже не
используя функцию
. Код, приведенный ниже, гораздо проще,
12
а ота с та ли а и
403
чем предыдущий, хотя здесь в итоговый результат мы не включили столбец
:
FILTER (
VALUES ( 'Product'[Product Name] );
[Sales Amount] >= 150000
)
Функция
бывает полезна при вычислении сразу нескольких
столбцов или если необходимо произвести какие-то дополнительные вычисления со столбцами. Представьте, что вам необходимо выделить набор товаров, общая сумма продаж по которым составляет 15 от всех продаж компании. Это не самая простая задача, и здесь нам потребуется целый алгоритм
действий.
. Вычислить сумму продаж по каждому товару.
2. Рассчитать сумму продаж нарастающим итогом, агрегируя каждый товар с теми товарами, сумма продаж по которым превышает текущую.
3. Перевести нарастающие итоги в проценты относительно общего итога
по продажам.
4. Вернуть только те товары, процент по которым меньше или равен 15.
Решить эту задачу за один шаг было бы довольно сложно, но если разбить ее
на четыре этапа, все станет гораздо проще:
Top Products =
VAR TotalSales = [Sales Amount]
VAR ProdsWithSales =
ADDCOLUMNS (
VALUES ( 'Product'[Product Name] );
"ProductSales"; [Sales Amount]
)
VAR ProdsWithRT =
ADDCOLUMNS (
ProdsWithSales;
"RunningTotal";
VAR SalesOfCurrentProduct = [ProductSales]
RETURN
SUMX (
FILTER (
ProdsWithSales;
[ProductSales] >= SalesOfCurrentProduct
);
[ProductSales]
)
)
VAR Top15Percent =
FILTER (
ProdsWithRT;
[RunningTotal] / TotalSales <= 0.15
)
RETURN Top15Percent
404
12
а ота с та ли а и
Результат можно видеть на рис. 12.5.
Рис 12 5 Са е о л рн е товар , с
дали ко ании 1
о и родаж
арн е родажи о котор
В этом примере мы реализовали решение при помощи вычисляемой таблицы, но существуют и другие варианты. Например, вы могли бы пройти по
табличной переменной
при помощи итератора
, чтобы создать меру, вычисляющую сумму продаж по этим товарам.
Как и применительно к большинству функций в DAX, вы можете рассматривать функцию
в качестве одного из кирпичиков. Только научившись сочетать эти кирпичики при построении действительно сложных выражений, вы сможете в полной мере овладеть языком DAX.
Функция SUMMARI E
Функция
является одной из наиболее часто используемых в языке
DAX. Эта функция сканирует таблицу, переданную ей в качестве первого аргумента, и объединяет столбцы этой или связанных таблиц в одну или несколько
групп. Главным образом функция
используется для получения искомой комбинации значений вместо извлечения полного списка.
Для примера подсчитаем количество уникальных цветов товаров, по которым были продажи. На основании полученных данных мы построим отчет,
в котором выведем общее количество цветов и количество цветов, по которым
была как минимум одна продажа. Следующие две меры обеспечат нам желаемый результат:
Num of colors :=
COUNTROWS (
VALUES ( 'Product'[Color] )
)
Num of colors sold :=
COUNTROWS (
12
а ота с та ли а и
405
SUMMARIZE ( Sales; 'Product'[Color] )
)
Результат вычисления этой меры в разрезе брендов можно видеть на
рис. 12.6.
Рис 12 6
ере Num of colors sold ис ольз етс
нк и SUMMARIZE
дл одс ета коли ества ветов, о котор
ли родажи
В данном случае мы использовали функцию
для группировки
таблицы продаж по столбцу
, после чего подсчитали количество
строк в результирующей таблице. Поскольку функция
выполняет операцию группировки по заданной таблице, в итоговый результат будут
включены только цвета, на которые есть ссылки в таблице
. В то же время
выражение VALUES ( Product[Color] ) возвращает список уникальных цветов
в модели данных вне зависимости от того, были по ним продажи или нет.
Используя функцию
, вы можете группировать таблицу по любому количеству столбцов с учетом того, что они доступны из нее по связям
«многие к одному» или «один к одному». Например, чтобы рассчитать среднедневные продажи по товарам, можно написать следующее выражение:
AvgDailyQty :=
VAR ProductsDatesWithSales =
SUMMARIZE (
Sales;
'Product'[Product Name];
'Date'[Date]
)
VAR Result =
AVERAGEX (
406
12
а ота с та ли а и
ProductsDatesWithSales;
CALCULATE (
SUM ( Sales[Quantity] )
)
)
RETURN Result
Результат вычисления этой меры показан на рис. 12.7.
Рис 12 7
от ете оказан среднедневн е родажи товаров
о года и ренда
Здесь мы использовали функцию
для сканирования таблицы
и ее группировки по наименованиям товаров и датам продажи. В результирующую таблицу при этом попадут только строки с днями, когда были произведены продажи. Затем функция
проходит по временной таблице, возвращенной функцией
, и подсчитывает средние значения.
Если по конкретному товару не было продажи в какой-то день, то он не будет
включен в таблицу.
Функция
также может использоваться по подобию функции
, добавляя столбцы к результирующей таблице. Например, код
предыдущей меры может быть записан так:
AvgDailyQty :=
VAR ProductsDatesWithSalesAndQuantity =
SUMMARIZE (
Sales;
'Product'[Product Name];
'Date'[Date];
"Daily qty"; SUM ( Sales[Quantity] )
)
VAR Result =
AVERAGEX (
ProductsDatesWithSalesAndQuantity;
[Daily qty]
12
а ота с та ли а и
407
)
RETURN Result
В этом случае функция
возвращает временную таблицу с наименованием товара, датой продажи и вновь созданным столбцом
. Позже
именно по этому столбцу будет рассчитано среднее значение функцией
. При этом ис оль о ать у к и S
A I
л со а и о ол итель
столб о о ре е о табли е кра е е реко е уетс , поскольку в этом случае одновременно создаются контекст строки и контекст фильтра.
По этой причине итоговые результаты могут оказаться сложными для понимания, если будет инициировано преобразование контекста либо путем включения
в выражение меры, либо за счет явного указания функции
. Если вам
необходимо добавить столбцы в таблицу, созданную функцией
, лучше всего использовать связку функций
и
:
AvgDailyQty :=
VAR ProductsDatesWithSales =
SUMMARIZE (
Sales;
'Product'[Product Name];
'Date'[Date]
)
VAR ProductsDatesWithSalesAndQuantity =
ADDCOLUMNS (
ProductsDatesWithSales;
"Daily qty"; CALCULATE ( SUM ( Sales[Quantity] ) )
)
VAR Result =
AVERAGEX (
ProductsDatesWithSalesAndQuantity;
[Daily qty]
)
RETURN Result
Несмотря на то что код получился чуть более многословным, читать и писать его проще из-за наличия единственного контекста строки, на который
действует преобразование контекста. Этот контекст строки создается функцией
во время итераций по временной таблице, возвращенной
функцией
. Примененный шаблон более прост для понимания
и в большинстве случаев будет работать быстрее.
У функции
есть и другие необязательные параметры, которые
можно использовать. Они служат для подсчета промежуточных итогов и добавления столбцов к результирующей таблице. Мы намеренно решили не останавливаться на этих опциях, чтобы вы лучше могли уяснить наш главный посыл: функ и
редна на ена дл гру ировки та ли и не должна
ис ол оват с дл в ислени до олнител н стол ов. И хотя в чужих формулах вы зачастую можете встретить вариант использования функции
с созданием дополнительных столбцов, всегда помните, что это далеко не самая лучшая практика, и в таких случаях всегда лучше пользоваться
связкой функций
/
.
408
12
а ота с та ли а и
Функция CR SS
I
Функция
производит перекрестное соединение двух таблиц, возвращая декартово рои ведение (cartesian product) двух исходных таблиц. Иными словами, эта функция возвращает все возможные комбинации значений из
таблиц. Например, следующее выражение вернет все сочетания наименований
товаров и годов:
CROSSJOIN (
ALL ( 'Product'[Product Name] );
ALL ( 'Date'[Calendar Year] )
)
Если в модели данных содержится 1000 наименований товаров, а календарь насчитывает пять календарных лет, результирующая таблица вернет
5000 строк. Функция
чаще используется в запросах, нежели в формулах мер. Но существуют сценарии, в которых применение этой функции
крайне оправдано по причине ее высокой производительности.
Рассмотрим случай использования условной функции
применительно
к двум столбцам в аргументе фильтра функции
. Поскольку в функции
аргументы фильтра объединяются посредством о ера ии е
ресе ени (intersection), использование здесь функции
нужно рассмотреть
более подробно. Вот один из вариантов применения функции CALCULATE для
фильтрации всех товаров, которые принадлежат категории Audio или выполнены в черном (Black) цвете:
AudioOrBlackSales :=
VAR CategoriesColors =
SUMMARIZE (
'Product';
'Product'[Category];
'Product'[Color]
)
VAR AudioOrBlack =
FILTER (
CategoriesColors;
OR (
'Product'[Category] = "Audio";
'Product'[Color] = "Black"
)
)
VAR Result =
CALCULATE (
[Sales Amount];
AudioOrBlack
)
RETURN Result
Этот код выдает правильные результаты и выполняется оптимально с точки
зрения производительности. Функция
сканирует таблицу
,
в которой, как предполагается, не так много строк. А значит, ее фильтрация не
займет много времени.
12
а ота с та ли а и
409
Но если нам необходимо будет фильтровать столбцы из разных таблиц, например цвета товаров и годы, ситуация изменится. Давайте немного усложним предыдущий пример, взяв столбцы из двух разных таблиц, чтобы функция
вынуждена была сканировать таблицу
:
AudioOr2007 Sales :=
VAR CategoriesYears =
SUMMARIZE (
Sales;
'Product'[Category];
'Date'[Calendar Year]
)
VAR Audio2007 =
FILTER (
CategoriesYears;
OR (
'Product'[Category] = "Audio";
'Date'[Calendar Year] = "CY 2007"
)
)
VAR Result =
CALCULATE (
[Sales Amount];
Audio2007
)
RETURN Result
Таблица
довольно объемная, в ней могут содержаться сотни миллионов строк. Таким образом, сканирование этой таблицы на предмет извлечения
всех возможных комбинаций категорий товаров и годов будет очень затратной
операцией. В то же время результирующий фильтр должен оказаться не таким
большим – в нем будет содержаться всего несколько категорий и годов. Но для
его создания движку придется сканировать всю исходную таблицу продаж.
В таких случаях мы рекомендуем строить небольшую временную таблицу,
содержащую все комбинации категорий товаров и годов, а затем фильтровать
ее, как показано в коде ниже:
AudioOr2007 Sales :=
VAR CategoriesYears =
CROSSJOIN (
VALUES ( 'Product'[Category] );
VALUES ( 'Date'[Calendar Year] )
)
VAR Audio2007 =
FILTER (
CategoriesYears;
OR (
'Product'[Category] = "Audio";
'Date'[Calendar Year] = "CY 2007"
)
)
VAR Result =
410
12
а ота с та ли а и
CALCULATE (
[Sales Amount];
Audio2007
)
RETURN Result
Полное перекрестное соединение из категорий товаров и годов будет содержать несколько сотен строк, и вычисление этой меры займет гораздо меньше
времени, чем в предыдущем случае.
Функция
применяется не только для ускорения выполнения расчетов. Иногда бывает нужно извлечь элементы из таблицы даже в случае отсутствия операций по ним. Например, используя функцию
для
сканирования продаж по категориям товаров и странам, мы получим пересечение только по тем элементам, по которым были продажи определенных
товаров. Это нормальное поведение функции
, так что здесь нет
ничего удивительного. Но иногда отсутствие события бывает важнее его присутствия. Допустим, перед вами стоит задача определить, по каким брендам не
было продаж в определенных регионах. В этом случае вам необходимо будет
писать сложное выражение с использованием функции
, чтобы в результирующий набор были включены все комбинации значений. В следующей
главе мы рассмотрим больше примеров с участием функции
.
Функция U I
является функцией для работы со множествами, объединяющей две
таблицы. Возможность объединения содержимого двух таблиц может быть полезной в самых разных обстоятельствах. Чаще эта функция используется при
работе с вычисляемыми таблицами и реже – с мерами. Например, в следующей
таблице будут объединены все страны из таблиц
и
:
AllCountryRegions =
UNION (
ALL ( Customer[CountryRegion] );
ALL ( Store[CountryRegion] )
)
Результат этого выражения показан на рис. 12.8.
Рис 12 8
нк и UNION
не дал ет д ликат
12
а ота с та ли а и
411
Функция
не выполняет удаление дублирующихся элементов перед
возвращением результата. Таким образом, Австралия (Australia), которая
встречается в обеих таблицах, попала в итоговый набор дважды. Если необходимо, вы можете воспользоваться функцией
для удаления дубликатов.
На протяжении книги мы уже не раз применяли функцию
для
получения списка уникальных значений из столбца, как он видим в текущем
контексте фильтра. Функция
также может быть использована с табличным выражением в качестве параметра, и в этом случае она вернет список
уникальных строк из таблицы. Следующий вариант идеально подойдет для
удаления возможных дубликатов по странам:
DistinctCountryRegions =
VAR CountryRegions =
UNION (
ALL ( Customer[CountryRegion] );
ALL ( Store[CountryRegion] )
)
VAR UniqueCountryRegions =
DISTINCT ( CountryRegions )
RETURN UniqueCountryRegions
Результат выполнения этого выражения показан на рис. 12.9.
Рис 12 9
нк и DISTINCT
далила д ликат из та ли
Функция
поддерживает привязку данных в исходных таблицах, если
она выполнена в них одинаково. В предыдущей формуле в результирующей
таблице функции
привязки данных нет, поскольку в первой таблице
у нас был столбец
, а во второй –
.
А раз привязка данных в исходных таблицах была разная, в результирующей
таблице будет создана абсолютно новая привязка, не относящаяся ни к одному
из существующих столбцов в модели данных. Поэтому в следующем выражении для всех стран будет выведена одна и та же сумма продажи:
DistinctCountryRegions =
VAR CountryRegions =
UNION (
ALL ( Customer[CountryRegion] );
ALL ( Store[CountryRegion] )
412
12
а ота с та ли а и
)
VAR UniqueCountryRegions =
DISTINCT ( CountryRegions )
VAR Result =
ADDCOLUMNS (
UniqueCountryRegions;
"Sales Amount"; [Sales Amount]
)
RETURN Result
Результирующая таблица показана на рис. 12.10.
Рис 12 10 Стол е CountryRegion не редставлен в одели данн
а зна ит, он не дет ильтровать ер Sales Amount
,
Если в вычисляемой таблице должны быть выведены как сумма продаж, так
и количество магазинов, включая все страны покупателей и магазинов, для
фильтрации можно применить более сложное выражение:
DistinctCountryRegions =
VAR CountryRegions =
UNION (
ALL ( Customer[CountryRegion] );
ALL ( Store[CountryRegion] )
)
VAR UniqueCountryRegions =
DISTINCT ( CountryRegions )
VAR Result =
ADDCOLUMNS (
UniqueCountryRegions;
"Customer Sales Amount";
VAR CurrentRegion = [CountryRegion]
RETURN
CALCULATE (
[Sales Amount];
Customer[CountryRegion] = CurrentRegion
);
"Number of stores";
VAR CurrentRegion = [CountryRegion]
RETURN
CALCULATE (
12
а ота с та ли а и
413
COUNTROWS ( Store );
Store[CountryRegion] = CurrentRegion
)
)
RETURN Result
Результат показан на рис. 12.11.
Рис 12 11
ри о о и сложного в ражени с ри енение
на далось от ильтровать и агазин , и родажи
нк ии CALCULATE
В этом примере функция
применяет фильтр к таблицам продаж
и магазинов, используя значение из текущей итерации функции
по табличному выражению, являющемуся результатом функции
. Еще
одним способом получить тот же результат является восстановление привязки
данных при помощи уже известной нам функции
, с которой мы познакомились в главе 10. Код такого выражения может выглядеть следующим
образом:
DistinctCountryRegions =
VAR CountryRegions =
UNION (
ALL ( Customer[CountryRegion] );
ALL ( Store[CountryRegion] )
)
VAR UniqueCountryRegions =
DISTINCT ( CountryRegions )
VAR Result =
ADDCOLUMNS (
UniqueCountryRegions;
"Customer Sales Amount"; CALCULATE (
[Sales Amount];
TREATAS (
{ [CountryRegion] };
Customer[CountryRegion]
414
12
а ота с та ли а и
)
);
"Number of stores"; CALCULATE (
COUNTROWS ( Store );
TREATAS (
{ [CountryRegion] };
Store[CountryRegion]
)
)
)
RETURN Result
Результат выполнения двух последних выражений будет одинаковым. Отличаются они лишь техникой, использованной для распространения фильтра
с нового столбца на существующий в модели данных. Кроме того, в последнем
примере мы применили конструктор таблиц: при помощи фигурных скобок
мы преобразовали
в таблицу, которую использовали в качестве
параметра функции
.
Поскольку функция
при объединении информации из разных столбцов утрачивает их исходную привязку данных, для ее восстановления удобно
применять функцию
. Также стоит отметить, что функция
игнорирует значения, отсутствующие в целевом столбце.
Функция I TERSECT
Функция
так же, как и
, предназначена для работы со множествами, но, в отличие от
, она не объединяет данные из двух таблиц,
а возвращает их пересечение, то есть только строки, присутствующие в обеих
таблицах. Эта функция была очень популярна до появления функции
, поскольку она позволяет применять результат табличного выражения
в качестве фильтра к другим таблицам и столбцам. После появления
функция
стала использоваться гораздо реже.
Если вам, к примеру, необходимо получить список покупателей, приобретавших товары как в 2007 году, так и в 2008-м, можно сделать это следующим
образом:
CustomersBuyingInTwoYears =
VAR Customers2007 =
CALCULATETABLE (
SUMMARIZE ( Sales; Customer[Customer Code] );
'Date'[Calendar Year] = "CY 2007"
)
VAR Customers2008 =
CALCULATETABLE (
SUMMARIZE ( Sales; Customer[Customer Code] );
'Date'[Calendar Year] = "CY 2008"
)
VAR Result =
INTERSECT ( Customers2007; Customers2008 )
RETURN Result
12
а ота с та ли а и
415
Что касается привязки данных, функция
сохраняет ее только для
первой таблицы. В предыдущем примере обе таблицы обладают одинаковой
привязкой данных. Если функции
будут переданы таблицы с разной привязкой данных, в итоговой таблице сохранится привязка только для
первой из них. Например, страны, в которых есть и магазины, и покупатели,
можно получить следующим образом:
INTERSECT (
ALL ( Store[CountryRegion] );
ALL ( Customer[CountryRegion] )
)
В итоговой таблице сохранится привязка данных к столбцу
. Так что если попытаться получить сумму продаж по полученным городам, то фильтр будет наложен по столбцу
, но не по
:
SalesStoresInCustomersCountries =
VAR CountriesWithStoresAndCustomers =
INTERSECT (
ALL ( Store[CountryRegion] );
ALL ( Customer[CountryRegion] )
)
VAR Result =
ADDCOLUMNS (
CountriesWithStoresAndCustomers;
"StoresSales"; [Sales Amount]
)
RETURN Result
Результат можно видеть на рис. 12.12.
Рис 12 12 Стол е StoresSales
за олнен только о города
агазинов,
а не о города ок ателе
В последнем примере столбец
щиеся к городу, где расположен магазин.
416
12
а ота с та ли а и
содержит лишь продажи, относя-
Функция EXCEPT
– последняя функция для работы со множествами, которую мы вам
представим в данном разделе. Функция
удаляет из первой таблицы
строки, присутствующие во второй. Таким образом, она, по сути, вычитает
одно множество из другого. Например, если вам необходимо получить список
покупателей, приобретавших товары в 2007 году, но не купивших ни одного
товара в 2008-м, это можно сделать так:
CustomersBuyingIn2007butNotIn2008 =
VAR Customers2007 =
CALCULATETABLE (
SUMMARIZE ( Sales; Customer[Customer Code] );
'Date'[Calendar Year] = "CY 2007"
)
VAR Customers2008 =
CALCULATETABLE (
SUMMARIZE ( Sales; Customer[Customer Code] );
'Date'[Calendar Year] = "CY 2008"
)
VAR Result =
EXCEPT ( Customers2007; Customers2008 )
RETURN Result
Первые несколько строк с кодами покупателей показаны на рис. 12.13.
Рис 12 13 Ограни енн с исок ок ателе ,
рио ретав и товар в 200 год , но не в 200
Как обычно, вы можете использовать это вычисление в качестве аргумента
фильтра функции
, чтобы рассчитать суммы продаж по этим покупателям. Функция
часто используется при анализе потребительского
поведения. Например, с ее помощью вы можете проводить вычисления, связанные с приходом, возвращением и уходом покупателей.
Существуют разные реализации одних и тех же вычислений в этой области,
каждая из которых ориентирована на конкретную модель данных. Представленная ниже реализация не всегда будет наиболее оптимальной, но она обладает достаточной гибкостью и простотой для понимания. Для расчета количества покупателей, приобретавших товары в этом году, но не в прошлом, мы
вычтем множество клиентов с покупками в прошлом году из общего списка
покупателей:
SalesOfNewCustomers :=
VAR CurrentCustomers =
VALUES ( Sales[CustomerKey] )
VAR CustomersLastYear =
12
а ота с та ли а и
417
CALCULATETABLE (
VALUES ( Sales[CustomerKey] );
DATESINPERIOD ( 'Date'[Date]; MIN ( 'Date'[Date] ) - 1; -1; YEAR )
)
VAR CustomersNotInLastYear =
EXCEPT ( CurrentCustomers; CustomersLastYear )
VAR Result =
CALCULATE ( [Sales Amount]; CustomersNotInLastYear )
RETURN Result
Реализация этого кода в виде меры будет работать с любыми фильтрами
и предоставляет широкие возможности для осуществления срезов по любым
столбцам. При этом стоит помнить, что данная реализация определения новых покупателей будет не самой оптимальной в плане производительности.
Мы привели этот пример только для демонстрации работы функции
.
Позже в данной главе мы рассмотрим более быстрый, хоть и более сложный
вариант этого вычисления.
Что касается привязки данных, то функция
, как и
, сохраняет ее только для первой таблицы. Например, следующее выражение
вычисляет продажи покупателям, живущим в городах, где нет наших магазинов:
SalesInCountriesWithNoStores :=
VAR CountriesWithActiveStores =
CALCULATETABLE (
SUMMARIZE ( Sales; Store[CountryRegion] );
ALL ( Sales )
)
VAR CountriesWithSales =
SUMMARIZE ( Sales; Customer[CountryRegion] )
VAR CountriesWithNoStores =
EXCEPT ( CountriesWithSales; CountriesWithActiveStores )
VAR Result =
CALCULATE (
[Sales Amount];
CountriesWithNoStores
)
RETURN Result
Результат функции
фильтрует столбец
, поскольку таблица, построенная с использованием этого столбца, указана в функции
в качестве первого аргумента.
Использование таблиц в качестве фильтров
Функции для манипулирования таблицами часто используются с целью построения сложных фильтров в рамках функции
. В данном разделе мы разберем полезные примеры, каждый из которых позволит вам сделать
еще один шаг в освоении языка DAX.
418
12
а ота с та ли а и
Применение условных конструкций R
Первым примером, в котором вам пригодится умение манипулировать таблицами, будет следующий. Представьте, что вам необходимо реализовать условную конструкцию
между двумя выборами в двух разных срезах, а не
,
предлагаемую по умолчанию клиентскими инструментами Excel и Power BI.
Отчет, показанный на рис. 12.14, содержит два среза. По умолчанию Power
BI выполнит пересечение двух выборов. В результате цифры в отчете покажут
продажи бытовой техники (Home Appliances) покупателям, окончившим среднюю школу (High School).
Рис 12 14
о ол ани в олн етс ересе ение в оров в среза ,
так то все в ранн е ильтр ри ен тс одновре енно
Но иногда вместо пересечения двух условий вам может понадобиться выполнить их объединение. Иными словами, желаемые цифры должны отображать как продажи бытовой техники, так и продажи покупателям, окончившим
среднюю школу. Поскольку Power BI не умеет объединять выбранные фильтры
по условию
, нам придется выполнить это объединение вручную – посредством написания кода на языке DAX.
Необходимо помнить о том, что в каждой ячейке отчета контекст фильтра
свой, и он включает как фильтр по категории товаров, так и фильтр по образованию. И нам нужно заменить оба этих фильтра. Есть множество способов это
сделать. Мы покажем три из них.
Первое и, возможно, самое простое выражение, способное помочь нам
в этой ситуации, приведено ниже:
OR 1 :=
VAR CategoriesEducations =
CROSSJOIN (
ALL ( 'Product'[Category] );
ALL ( Customer[Education] )
12
а ота с та ли а и
419
)
VAR CategoriesEducationsSelected =
FILTER (
CategoriesEducations;
OR (
'Product'[Category] IN VALUES ( 'Product'[Category] );
Customer[Education] IN VALUES ( Customer[Education] )
)
)
VAR Result =
CALCULATE (
[Sales Amount];
CategoriesEducationsSelected
)
RETURN Result
Сначала мы выполняем перекрестное соединение всех возможных категорий товаров и уровней образования. После создания таблицы на нее накладывается фильтр, убирающий значения, не соответствующие выбранным условиям, а затем результирующий набор строк используется в качестве аргумента
фильтра в функции
. В итоге функция
переопределяет
текущие фильтры по категориям товаров и уровню образования, что ведет
к формированию отчета, показанного на рис. 12.15.
Рис 12 15
от ет вкл ен родажи о категории Б това те ника
или OR ок ател с соответств
и ровне о разовани
Как мы и говорили, первый вариант решения задачи оказался очень простым для реализации и понимания. Однако если в таблицах, использующихся
в качестве фильтров, будет достаточно много строк или условий будет больше двух, результирующая временная таблица может стать недопустимо большой. В этом случае можно попытаться уменьшить ее размер, применив вместо
функцию
, что мы и сделали во второй реализации этой
меры:
420
12
а ота с та ли а и
OR 2 :=
VAR CategoriesEducations =
CALCULATETABLE (
SUMMARIZE (
Sales;
'Product'[Category];
Customer[Education]
);
ALL ( 'Product'[Category] );
ALL ( Customer[Education] )
)
VAR CategoriesEducationsSelected =
FILTER (
CategoriesEducations;
OR (
'Product'[Category] IN VALUES ( 'Product'[Category] );
Customer[Education] IN VALUES ( Customer[Education] )
)
)
VAR Result =
CALCULATE (
[Sales Amount];
CategoriesEducationsSelected
)
RETURN Result
По своей логике вторая реализация меры очень похожа на первую, а единственным отличием является присутствие функции
вместо
. Также стоит отметить, что функция
должна быть выполнена
в контексте фильтра, очищенном от фильтров по столбцам
и
.
В противном случае текущий выбор в срезе окажет нежелательное влияние на
результат функции
, что сведет на нет действие фильтра.
Есть и третий вариант решения этого сценария. Потенциально он наиболее
быстрый, хотя и не такой простой для понимания. Здесь мы принимаем допущение о том, что если категория товара присутствует в списке выбранных категорий, то нам подойдет любой уровень образования покупателя. То же самое
справедливо и для уровней образования применительно к категориям товаров. Таким образом, мы пришли к третьему варианту реализации нашей меры:
OR 3 :=
VAR Categories =
CROSSJOIN (
VALUES ( 'Product'[Category] );
ALL ( Customer[Education] )
)
VAR Educations =
CROSSJOIN (
ALL ( 'Product'[Category] );
VALUES ( Customer[Education] )
)
VAR CategoriesEducationsSelected =
12
а ота с та ли а и
421
UNION ( Categories; Educations )
VAR Result =
CALCULATE (
[Sales Amount];
CategoriesEducationsSelected
)
RETURN Result
Как видите, один и тот же расчет можно выполнить самыми разными способами. Разница лишь в скорости выполнения кода и простоте восприятия.
Способность создавать разные реализации одной и той же формулы очень
пригодится вам при чтении заключительных глав книги, посвященных оптимизации, где вы научитесь оценивать производительность различных версий
кода и выбирать из них наиболее оптимальную.
Ограничение расчетов постоянными покупателями
с первого года
В качестве еще одного примера манипулирования таблицами рассмотрим
анализ продаж по годам, но только по тем покупателям, которые приобретали
у нас товары в первый год выбранного периода. Иными словами, мы вычисляем список клиентов, покупавших наши товары в первый год, отображенный
в элементе визуализации, и анализируем продажи по ним в следующие годы,
игнорируя тех покупателей, которые пришли к нам позже.
Наше вычисление можно условно разделить на три шага.
. Определяем первый год продаж по товарам.
2. Сохраняем список покупателей, приобретавших товары в этот год, в переменную, игнорируя все остальные фильтры.
3. Рассчитываем сумму продаж по покупателям из второго шага в выбранном периоде.
В представленном ниже коде реализован этот алгоритм с использованием
переменных для хранения промежуточных результатов:
SalesOfFirstYearCustomers :=
VAR FirstYearWithSales =
CALCULATETABLE (
FIRSTNONBLANK (
'Date'[Calendar Year];
[Sales Amount]
);
ALLSELECTED ()
)
VAR CustomersFirstYear =
CALCULATETABLE (
VALUES ( Sales[CustomerKey] );
FirstYearWithSales;
ALLSELECTED ()
)
VAR Result =
422
12
а ота с та ли а и
CALCULATE (
[Sales Amount];
KEEPFILTERS ( CustomersFirstYear )
)
RETURN Result
В переменной
мы сохранили первый год, в котором были
продажи. Обратите внимание, что функция
возвращает таблицу с привязкой данных к столбцу
. В переменную
мы извлекаем список всех покупателей, приобретавших товары
в первый год. Заключительный шаг самый простой – здесь мы просто применяем фильтр по покупателям. Таким образом, в каждой ячейке отчета будут
отображаться продажи исключительно по покупателям, найденным на втором шаге. Модификатор
позволит дополнительно отфильтровать
этих покупателей, скажем, по странам.
Результат вычисления меры показан на рис. 12.16. Из этого отчета понятно, что покупатели, пришедшие к нам в 2007 году, с каждым годом делают все
меньше покупок.
Рис 12 16
родажи о года среди ок
ателе , ри ед и к на в 200 год
Этот пример очень важно понять. Существует множество сценариев, в которых необходимо установить фильтр по дате, определить по нему набор данных, после чего проанализировать поведение этого набора, будь то покупатели, товары или магазины, по другим временным периодам. С помощью этого
шаблона можно легко реализовать анализ повторных продаж и другие вычисления с похожими требованиями.
Вычисление новых покупателей
В предыдущем разделе этой главы, во время изучения функции
, мы
показали вам способ вычисления новых покупателей. Здесь мы представим
вам более эффективный метод определения новых покупателей с использованием табличных функций.
Идея этого алгоритма следующая. Сначала мы определим самую раннюю
дату продажи для каждого покупателя. После этого проверим, входит ли эта
дата для конкретного клиента в выбранный нами период. Если да, значит, этого покупателя можно считать новым относительно текущего периода.
12
а ота с та ли а и
423
Представляем вам код меры:
New Customers :=
VAR CustomersFirstSale =
CALCULATETABLE (
ADDCOLUMNS (
VALUES ( Sales[CustomerKey] );
"FirstSale"; CALCULATE (
MIN ( Sales[Order Date] )
)
);
ALL ( 'Date' )
)
VAR CustomersWith1stSaleInCurrentPeriod =
FILTER (
CustomersFirstSale;
[FirstSale] IN VALUES ( 'Date'[Date] )
)
VAR Result =
COUNTROWS ( CustomersWith1stSaleInCurrentPeriod )
RETURN Result
В формуле переменной
необходимо применить функцию
к таблице
, чтобы можно было сканировать таблицу продаж за период,
предшествующий выбранному. Результат выражения показан на рис. 12.17.
Рис 12 17
от ете ото ражено о ее коли ество ок
и коли ество нов клиентов за 200 год
ателе
Если пользователь решит добавить фильтр к отчету, скажем, по категориям
товаров, то к числу новых покупателей будут относиться те, которые впервые
за выбранный период приобрели товар выбранной категории. Таким образом,
в зависимости от установленных фильтров один и тот же покупатель может
считаться новым или постоянным. Добавляя другие модификаторы функции
424
12
а ота с та ли а и
к первой переменной, можно получить различные вариации этой
формулы. Например, добавив ALL ( Product ), мы укажем формуле при определении нового покупателя учитывать любые товары, а не только выбранные.
Добавление ALL ( Store ) позволит не принимать в расчет конкретные магазины.
Использование I , C
TAI SR
иC
TAI S
ред д е ри ере, как и во ноги др ги ,
ис ользовали кл евое слово IN дл
о ределени того, рис тств ет ли зна ение в та ли е ри в олнении кода IN трансор ир етс в в зов нк ии CONTAINSROW, так то след
ие две инстр к ии д т
квивалентн и
Product[Color] IN { "Red"; "Blue"; "Yellow" }
CONTAINSROW ( { "Red"; "Blue"; "Yellow" }; Product[Color] )
тот синтаксис также ра отает и со ножество стол
ов
( 'Date'[Year]; 'Date'[MonthNumber] ) IN { ( 2018; 12 ); ( 2019; 1 ) }
CONTAINSROW ( { ( 2018; 12 ); ( 2019; 1 ) }; 'Date'[Year]; 'Date'[MonthNumber] )
режни верси
кл ев е слова IN и CONTAINSROW
ли недост н
льтернативо и
ла
нк и CONTAINS, тре
а на в од ар из стол а и зна ени дл оиска в та ли е ри то
нк и CONTAINS вл етс
енее
ективно
о сравнени с IN и CONTAINSROW
оскольк в режни верси
не ло также
и констр кторов та ли , нк и CONTAINS ри одилось ис ользовать с олее ногословн
синтаксисо
VAR Colors =
UNION (
ROW ( "Color"; "Red" );
ROW ( "Color"; "Blue" );
ROW ( "Color"; "Yellow" )
)
RETURN
CONTAINS ( Colors; [Color]; Product[Color] )
а о ент на исани книги ис ользование кл евого слова IN вл етс наи олее
о ти альн
с осо о
оиска зна ени в та ли е
ражени с IN о ень рост дл
они ани , а в лане роизводительности они не ст а т ор ла с нк ие CONTAINSROW.
Повторное использование табличных выражений
при помо и функции DETAILR S
В сводных таблицах в Excel есть возможность извлечь исходные данные, на
основании которых было рассчитано значение в ячейке. В интерфейсе Excel
для этого нужно выбрать пункт S
D
( ока ать етали) в контекстном меню – технически эта операция называется детали а ией данн (drillthrough). Это может вводить в заблуждение, поскольку в Power BI термин детализация относится к возможности переходить с одной страницы отчета на
другую по задуманным автором отчета правилам. Именно поэтому свойство,
12
а ота с та ли а и
425
позволяющее управлять детализацией, было названо
ра е ие строк етали а ии (D
) в модели Tabular и представлено в SQL
Server Analysis Services 2017. По состоянию на апрель 2019 года эта особенность
недоступна в Power BI, но может быть включена в будущих релизах.
Выражение строк детализации представляет собой табличное выражение
DAX, ассоциированное с мерой и вызываемое в момент показа деталей. Это
выражение вычисляется в контексте фильтра меры. Идея состоит в том, что
если мера изменяет контекст фильтра в процессе вычисления переменной,
в выражении строк детализации должны быть произведены такие же трансформации контекста фильтра.
Рассмотрим меру
, вычисляющую другую меру
нарастающим итогом с начала года:
Sales YTD :=
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
)
Выражение строк детализации для этой меры должно использовать функцию
, изменяющую контекст фильтра соответствующим образом. Например, следующее выражение будет возвращать все столбцы таблицы
с начала года, участвующего в вычислении:
CALCULATETABLE (
Sales;
DATESYTD ( 'Date'[Date] )
)
Клиентский инструмент DAX запускает это выражение при помощи специальной функции
, указывая в качестве параметра меру, которой
принадлежит выражение:
DETAILROWS ( [Sales YTD] )
Функция
осуществляет вызов табличного выражения, ассоциированного с мерой. А значит, вы можете создать скрытые меры для хранения
длинных табличных выражений, зачастую используемых в качестве аргументов фильтра в других мерах DAX. Рассмотрим меру
с ассоциированным выражением, извлекающим все даты раньше самой поздней даты
в текущем контексте фильтра:
-- Выражение строк детализации для меры Cumulative Total
VAR LastDateSelected = MAX ( 'Date'[Date] )
RETURN
FILTER (
ALL ( 'Date'[Date] );
'Date'[Date] <= LastDateSelected
)
Это выражение можно использовать в других мерах, применяя функцию
:
426
12
а ота с та ли а и
Cumulative Sales Amount :=
CALCULATE (
[Sales Amount];
DETAILROWS ( [Cumulative Total] )
)
Cumulative Total Cost :=
CALCULATE (
[Total Cost];
DETAILROWS ( [Cumulative Total] )
)
Больше примеров по данной теме можно найти по адресу: https://www.sqlbi.
com/articles/creating-table-functions-in-dax-using-detailrows/. При этом стоит помнить, что использование функции
для запуска ассоциированных
выражений является лишь обходным путем, за неимением в DAX пользовательских функций, а значит, может приводить к проблемам с производительностью. Многие примеры применения функции
можно переписать с помощью групп вычислений, и эта техника уйдет в прошлое, когда в DAX
появятся меры, возвращающие таблицы, или пользовательские функции.
Создание вычисляемых таблиц
Все табличные функции, о которых мы рассказывали в предыдущих разделах,
могут быть использованы либо в качестве табличных фильтров в функции
, либо для создания вычисляемых таблиц и запросов. Ранее мы в основном описывали функции, более пригодные для использования в качестве
фильтра, сейчас же поговорим о функциях, чаще применяющихся для создания
вычисляемых таблиц. Есть и другие функции, главным образом использующиеся в запросах, но о них мы расскажем в следующей главе. В то же время необходимо помнить, что функции DAX не ограничиваются какой-то одной областью
применения. Ничто не мешает вам использовать функции
,
или
(о них мы поговорим позже) в коде меры или
в качестве табличного фильтра. Это лишь вопрос удобства применения – некоторые функции удобнее использовать в определенных ситуациях.
Функция SELECTC LUM S
Функцию
удобно использовать для ограничения количества
выбираемых столбцов из таблицы. Кроме того, она обладает возможностью добавлять новые столбцы, подобно функции
. На практике функция
служит для осуществления выборки данных из столбцов,
как оператор
в языке SQL.
Наиболее частым использованием этой функции является сканирование
таблицы и возвращение ограниченного набора столбцов. Например, следующее выражение возвращает из таблицы покупателей только столбцы с образованием и полом:
12
а ота с та ли а и
427
SELECTCOLUMNS (
Customer;
"Education"; Customer[Education];
"Gender"; Customer[Gender]
)
Результат выражения со множеством дублирующихся строк показан на
рис. 12.18.
Рис 12 18
нк и SELECTCOLUMNS
возвра ает та ли с д ликата и
Функция
сильно отличается от
. Если функция
осуществляет группировку результирующего набора, то
просто ограничивает количество столбцов в выводе. Следовательно,
результирующая таблица на выходе функции
может содержать повторяющиеся строки, а в случае с
– нет. Для ограничения
количества столбцов на вход функции
необходимо подать
пары значений с наименованием столбца и выражением. Кроме того, в результирующем наборе могут оказаться не только существующие столбцы, но и новые. Например, следующая формула добавляет к таблице покупателей новый
столбец
, в котором указаны имя покупателя и в скобках его код:
SELECTCOLUMNS (
Customer;
"Education"; Customer[Education];
"Gender"; Customer[Gender];
"Customer"; Customer[Name] & " (" & Customer[Customer Code] & ")"
)
Результирующую таблицу можно видеть на рис. 12.19.
Рис 12 19
нк и SELECTCOLUMNS
ожет до авл ть нов е стол
к та ли е, одо но нк ии
ADDCOLUMNS
428
12
а ота с та ли а и
Функция
сохраняет привязку данных для тех столбцов, которые выбраны путем простого указания ссылки на исходный столбец, тогда
как для столбцов, образованных при помощи выражений, будет выполнена
новая привязка. Например, в следующем примере вернется таблица из двух
столбцов: первый будет привязан к столбцу
в модели данных,
а для второго будет создана новая привязка, несмотря на то что значения
в этих столбцах будут одинаковые:
SELECTCOLUMNS (
Customer;
"Наименование покупателя с привязкой данных"; Customer[Name],
"Наименование покупателя без привязки данных"; Customer[Name] & ""
)
Создание статических таблиц при помо и функции R
– одна из самых простых функций, она возвращает таблицу с одной строкой. На вход функции
подаются пары с наименованием столбца и его значением, а на выходе мы получаем таблицу с одной строкой и заданным количеством столбцов. Например, следующее выражение вернет таблицу из одной
строки с двумя столбцами, заполненными суммой и количеством продаж:
ROW (
"Sales"; [Sales Amount];
"Quantity"; SUM ( Sales[Quantity] )
)
Результат этого выражения показан на рис. 12.20.
Рис 12 20
нк и ROW
создает та ли с одно строко
После появления в DAX конструктора таблиц функция
стала использоваться гораздо реже. Например, предыдущее выражение можно записать так:
{
( [Sales Amount]; SUM ( Sales[Quantity] ) )
}
В этом случае наименования столбцов будут сгенерированы автоматически,
что показано на рис. 12.21.
Рис 12 21
онстр ктор та ли генерир ет
наи еновани стол ов авто ати ески
При использовании конструктора таблиц строки разделяются точкой с запятой. Для создания нескольких столбцов необходимо использовать скобки для
объединения их в строку. Главным отличием между функцией
и синтак12
а ота с та ли а и
429
сисом с фигурными скобками является то, что при использовании первой вы
можете задать имена столбцов, тогда как второй вариант этого сделать не позволяет, что осложняет обращение к столбцам в дальнейшем.
Создание статических таблиц при помо и функции
DATATABLE
Использование функции
ограничивается созданием таблицы с единственной строкой. Если же вам необходимо, чтобы в созданной таблице было
несколько строк, вам придется воспользоваться функцией
. При
этом функция
позволяет задать не только имена столбцов, но и их
типы данных с содержимым. Например, если вам понадобится таблица из трех
строк для выполнения кластеризации по ценам, самым простым способом создать ее будет следующий:
DATATABLE (
"Segment"; STRING;
"Min"; DOUBLE;
"Max"; DOUBLE;
{
{ "LOW"; 0; 20 };
{ "MEDIUM"; 20; 50 };
{ "HIGH"; 50; 99 }
}
)
Результат этого выражения показан на рис. 12.22.
Рис 12 22 а ли а, сгенерированна
нк ие DATATABLE
В качестве типа данных столбцов можно указывать одно из следующих значений:
,
,
,
,
или
. Синтаксис этой функции в отношении использования скобок сильно отличается от
конструктора таблиц. В функции
фигурные скобки используются
для разделения строк, тогда как в конструкторе таблиц применяются круглые
скобки, а фигурными выделяется вся таблица в целом.
Серьезным ограничением функции
является то, что все значения в таблице должны быть константами. Любые выражения на языке DAX
здесь недопустимы. Это делает функцию
не столь популярной. Те
же конструкторы таблиц дают разработчику гораздо больше гибкости.
Можно использовать функцию
для определения простых вычисляемых таблиц с неизменными значениями. В SQL Server Data Tools (SSDT) для
Analysis Services Tabular функция
используется, когда разработчик
430
12
а ота с та ли а и
вставляет данные из буфера обмена в модель, тогда как Power BI для определения таблиц с константами использует Power Query. Это еще одна причина,
по которой функция
не пользуется большой популярностью среди
пользователей Power BI.
Функция GE ERATESERIES
Функция
является служебной и предназначена для создания
списка значений по переданным параметрам нижней и верхней границ, а также шага. Например, следующее выражение сгенерирует список из 20 чисел от
1 до 20:
GENERATESERIES ( 1; 20; 1 )
Тип данных будет зависеть от введенных значений и может быть либо числовым, либо
. Например, если вам понадобится таблица, содержащая время, следующее выражение поможет вам быстро создать таблицу из
86 400 строк – по одной на каждую секунду:
Time =
GENERATESERIES (
TIME ( 0; 0; 0 );
TIME ( 23; 59; 59 );
TIME ( 0; 0; 1 )
)
-- Начальное значение
-- Конечное значение
-- Шаг: 1 секунда
Изменив шаг и добавив дополнительные столбцы, можно создать небольшую таблицу, которая может служить в качестве измерения, например для осуществления среза продаж по времени:
Time =
SELECTCOLUMNS (
GENERATESERIES (
TIME ( 0; 0; 0 );
TIME ( 23; 59; 59 );
TIME ( 0; 30; 0 )
);
"Time"; [Value];
"HH:MM AMPM"; FORMAT ( [Value]; "HH:MM AM/PM" );
"HH:MM"; FORMAT ( [Value]; "HH:MM" );
"Hour"; HOUR ( [Value] );
"Minute"; MINUTE ( [Value] )
)
Результат этого выражения можно видеть на рис. 12.23.
Использовать функцию
в мерах не принято, она чаще применяется для создания простых табличек, которые можно применять в качестве срезов, чтобы пользователь имел возможность выбрать нужный параметр.
Например, в Power BI функция
используется для добавления
параметров при выполнении анализа «что, если».
12
а ота с та ли а и
431
Рис 12 23
ос ользовав ись нк и и GENERATESERIES и SELECTCOLUMNS,
ожно легко с астерить та ли со вре ене
Закл чение
В данной главе вы познакомились со множеством табличных функций. А в следующей встретитесь еще с несколькими. Здесь мы главным образом сосредоточили внимание на функциях, применяющихся для создания вычисляемых
таблиц и сложных аргументов фильтра в функциях
и
. Помните, что в книге мы приводим примеры того, что возможно
сделать при помощи языка DAX. Реальные же решения конкретных сценариев
вам придется разрабатывать самостоятельно.
Функции, с которыми вы познакомились в данной главе:
„
– для добавления новых столбцов в исходную таблицу;
„
– для выполнения группировки после сканирования таблицы;
„
– для получения декартова произведения двух таблиц;
„
,
и
– для выполнения базовых операций со
множествами применительно к таблицам;
„
– для выбора определенных столбцов из таблицы;
„
,
и
– для создания таблиц с постоянными величинами в качестве вычисляемых таблиц.
В следующей главе мы расскажем еще о нескольких табличных функциях
и главным образом сосредоточимся на построении сложных запросов и сложных вычисляемых таблиц.
ГЛ А В А 13
Создание запросов
В данной главе мы продолжим наше путешествие по миру DAX и изучим еще
несколько полезных табличных функций. В основном мы сконцентрируемся
на функциях, применяемых при подготовке запросов и создании вычисляемых таблиц, а не при написании мер. Помните о том, что большинство функций, с которыми вы познакомитесь в этой главе, вполне можно применять
и при написании мер, пусть и с некоторыми ограничениями, на которых мы
также подробно остановимся.
Для каждой функции будет приведен пример запроса, использующего ее.
При написании главы мы поставили себе две цели: познакомить вас с новыми функциями и показать несколько рабочих шаблонов, которые вы сможете
адаптировать к своей модели данных.
Все демонстрационные материалы в главе представлены в виде текстовых
файлов с запросами для выполнения в DAX Studio с подключением к общему
файлу с данными Power BI. Этот файл, в свою очередь, содержит модель данных
Contoso, с которой мы работаем на протяжении всей книги.
Знакомство с DAX Studio
– это бесплатный инструмент, доступный по адресу www.daxstudio.
org, помогающий при написании запросов, отладке кода и измерении производительности запросов.
Это живой продукт с постоянно обновляющейся функциональностью. Вот
список наиболее важных особенностей DAX Studio:
„ возможность подключаться к Analysis Services, Power BI и Power Pivot для
Excel;
„ полноценный редактор запросов для написания сложного кода;
„ автоматическое форматирование исходного кода при помощи сервиса
daxformatter.com;
„ автоматическое определение мер для отладки или тонкой настройки
производительности;
„ детализированная информация о производительности ваших запросов.
Хотя есть и другие инструменты для проверки и написания запросов на языке DAX, мы настоятельно рекомендуем нашим читателям скачать и установить
DAX Studio. Если вы еще сомневаетесь, представьте, что весь код, показанный
в данной книге, был написан нами именно в DAX Studio. А ведь мы работаем
с DAX дни напролет и хотим, чтобы наш код был максимально эффективным.
1
Создание за росов
433
Полная документация по DAX Studio доступна по адресу http://daxstudio.org/
documentation/.
Инструкция EVALUATE
является специальной инструкцией DAX, необходимой для выполнения запросов. Ключевое слово
, за которым следует название
таблицы, возвращает результат табличного выражения. При этом одной или
нескольким инструкциям
может предшествовать целый ряд дополнительных определений локальных таблиц, столбцов, мер и переменных, область видимости которых распространяется на весь пакет инструкций
, выполняемый как единое целое.
Например, следующий запрос, в котором функция
следует
за ключевым словом
, вернет список красных товаров:
EVALUATE
CALCULATETABLE (
'Product';
'Product'[Color] = "Red"
)
Прежде чем углубиться в изучение продвинутых табличных функций, мы
познакомимся с дополнительными опциями инструкции
, которые
будем активно использовать при написании сложных запросов.
Введение в синтаксис EVALUATE
Инструкцию
можно условно разделить на три части:
„ ра ел о ре еле и начинается с ключевого слова
и включает в себя список определений локальных сущностей, таких как таблицы,
столбцы, переменные и меры. Можно написать один раздел определений для всего запроса, в то время как в запросе может присутствовать
несколько инструкций
;
„
ра е ие а роса начинающееся с инструкции
, выражение запроса представляет собой, по сути, табличное выражение на языке
DAX, возвращающее результат. В запросе может быть множество выражений запроса, каждое из которых должно начинаться ключевым словом
и обладать своим набором модификаторов результата;
„ о и икатор ре ультата это необязательный раздел в рамках инструкции
, начинающийся с ключевого слова
. Служит для выполнения сортировки результата, а также для вывода ограниченного набора столбцов при помощи инструкции
.
Первая и третья секции
являются опциональными. Так что простейшим вариантом использования ключевого слова
будет указание
следом за ним имени таблицы из модели данных. Однако, используя
таким образом, разработчик добровольно отказывается от массы полез434
1
Создание за росов
ных особенностей этой мощной инструкции, которые не только можно, но
и нужно изучать.
Ниже представлен пример запроса:
DEFINE
VAR MinimumAmount = 2000000
VAR MaximumAmount = 8000000
EVALUATE
FILTER (
ADDCOLUMNS (
SUMMARIZE ( Sales; 'Product'[Category] );
"CategoryAmount"; [Sales Amount]
);
AND (
[CategoryAmount] >= MinimumAmount;
[CategoryAmount] <= MaximumAmount
)
)
ORDER BY [CategoryAmount]
Этот запрос вернет результат, показанный на рис. 13.1.
Рис 13 1
итогов на ор вкл ен категории с с
в интервале ежд 2 000 000 и 000 000
о
родаж
В этом примере мы объявили две переменные для хранения нижней и верхней границ суммы продаж. После этого запрос извлекает все категории товаров, по которым сумма продаж входит в выбранный интервал. И наконец,
выполняется сортировка результатов по сумме продаж. Синтаксис очень простой, но при этом довольно мощный, и в следующих разделах мы подробнее
расскажем о каждом разделе инструкции
. Но стоит помнить, что все
эти особенности годятся только при написании запросов. Если вы собираетесь
свой код в дальнейшем использовать в вычисляемой таблице, мы советуем вам
отказаться от использования ключевых слов
и
и сосредоточиться исключительно на выражении запроса. Вычисляемая таблица определяется не запросом DAX, а табличным выражением.
Использование VAR внутри DEFI E
В разделе определений допустимо использовать ключевое слово
для инициализации переменных. Переменные, определенные в запросе, в отличие
от выражений DAX, не должны заканчиваться разделом
. По сути, за
формирование результата отвечает инструкция
. В дальнейшем для
различия между переменными, использующимися в выражениях, и перемен1
Создание за росов
435
ными из раздела
в запросах мы будем называть первые еременн ми
в ражений (expression variables), а вторые – еременн ми а росов (query variables).
Как и переменные выражений, переменные запросов могут хранить как скалярные величины, так и таблицы. Например, предыдущий запрос может быть
переписан следующим образом с использованием табличной переменной:
DEFINE
VAR MinimumAmount = 2000000
VAR MaximumAmount = 8000000
VAR CategoriesSales =
ADDCOLUMNS (
SUMMARIZE ( Sales; 'Product'[Category] );
"CategoryAmount"; [Sales Amount]
)
EVALUATE
FILTER (
CategoriesSales;
AND (
[CategoryAmount] >= MinimumAmount;
[CategoryAmount] <= MaximumAmount
)
)
ORDER BY [CategoryAmount]
Переменные запросов действуют в области видимости всего пакета инструкций
, которые выполняются как единое целое. Это означает, что после инициализации переменная может использоваться в любом из следующих
далее запросов. Единственным ограничением является то, что к переменной
можно обращаться только после ее объявления. Если в предыдущем примере
переменную
объявить раньше переменных
или
, будет выдана синтаксическая ошибка, поскольку в
вы пытаетесь ссылаться на переменные, которые еще не объявлены. Это
простое ограничение очень полезно и помогает избежать образования циклических зависимостей. Такое же ограничение действует и на переменные выражений, так что в этом плане наблюдается полная преемственность.
Если в запросе содержится несколько секций
, объявленные ранее
переменные будут доступны во всех из них. Например, в генерируемых Power
BI запросах секция
используется для хранения фильтров срезов в переменных, после чего идут несколько блоков
для разных расчетов
внутри визуального элемента.
Переменные также могут быть объявлены и внутри секции
. В этом
случае это будут переменные выражения, и их область видимости сузится до
конкретного табличного выражения. Например, предыдущий пример может
быть переписан следующим образом:
EVALUATE
VAR MinimumAmount = 2000000
VAR MaximumAmount = 8000000
VAR CategoriesSales =
436
1
Создание за росов
ADDCOLUMNS (
SUMMARIZE ( Sales; 'Product'[Category] );
"CategoryAmount"; [Sales Amount]
)
RETURN
FILTER (
CategoriesSales;
AND (
[CategoryAmount] >= MinimumAmount;
[CategoryAmount] <= MaximumAmount
)
)
ORDER BY [CategoryAmount]
Как видите, теперь переменные стали неотъемлемой частью табличного выражения, а значит, для определения результата необходимо использовать ключевое слово
. Область видимости этих переменных в данном случае
будет ограничена этой секцией
.
Выбор между переменными выражений и переменными запросов зависит
от конкретной задачи – у обоих видов переменных есть свои преимущества
и недостатки. Если вы хотите использовать переменную в следующих определениях таблиц или столбцов, то ваш выбор – переменная запроса. Если же в обращении к переменной в последующих определениях (или секциях
)
нет необходимости, лучше ограничиться переменной выражения. Фактически
если переменная является составной частью выражения, будет гораздо проще
использовать выражение для расчета вычисляемой таблицы или включения
в меру. В противном случае вам придется менять синтаксис запроса для преобразования его в выражение.
Для выбора между двумя типами переменных можно воспользоваться следующим простым правилом. Используйте переменные выражений всегда,
когда это возможно, а переменные запросов – только в случаях крайней необходимости. Одним из ограничений переменных запросов является то, что их
достаточно проблематично повторно использовать в других формулах.
Использование MEASURE внутри DEFI E
Еще одной сущностью, которую можно объявить локально внутри запроса, является мера. Это можно сделать при помощи ключевого слова
. Меры
в запросах ведут себя приблизительно так же, как и обычные меры, за исключением того, что они существуют только в рамках запроса. При определении
меры обязательно нужно указывать имя таблицы, в которой она будет размещена. Приведем пример такой меры:
DEFINE
MEASURE Sales[LargeSales] =
CALCULATE (
[Sales Amount];
Sales[Net Price] >= 200
)
1
Создание за росов
437
EVALUATE
ADDCOLUMNS (
VALUES ( 'Product'[Category] );
"Large Sales"; [LargeSales]
)
Результат запроса можно видеть на рис. 13.2.
Рис 13 2 Мера за роса LargeSales в
в стол е Large Sales
исл етс дл каждо категории товаров
Меры в запросах используются для двух целей. Во-первых, что весьма очевидно, для написания более сложных выражений, которые могут многократно
использоваться в рамках запроса. Второе предназначение мер в запросах состоит в их использовании для отладки кода и настройке производительности. Дело
в том, что если мера в запросе и мера в модели данных называются одинаково,
приоритет внутри запроса будет у первой. Иными словами, ссылки на меру по
имени внутри запроса будут обращаться к локальной мере, а не к глобальной,
объявленной в модели. В то же время все другие ссылки на эту меру в модели
данных продолжат обращаться к мере, существующей вне запроса. И для того
чтобы оценить поведение запроса при изменении нужной меры в модели данных, вам достаточно включить меру с точно таким же именем в сам запрос.
Тестируя поведение меры, лучше всего будет написать запрос, использующий ее, добавить локальную копию этой меры и заняться отладкой или оптимизацией кода. По окончании процесса код меры в модели данных можно
смело заменить на отлаженный код из запроса. В DAX Studio для этой цели
есть специальная функция: инструмент предлагает разработчику автоматически добавить все инструкции
в запрос с целью ускорения
процесса.
Реализация распространенных шаблонов запросов
в DAX
Теперь, когда вы познакомились с синтаксисом инструкции
, мы готовы представить вам ряд функций, полезных при написании запросов. Для
наиболее распространенных из них мы также покажем примеры, которые помогут лучше понять использование этих функций.
438
1
Создание за росов
Использование функции R
для проверки мер
Представленная в предыдущей главе функция
обычно используется для
извлечения значения из меры или проведения анализа плана ее выполнения.
Инструкция
принимает таблицу на вход и возвращает также таблицу. Если все, что вам нужно, – это получить значение меры,
просто
не примет ее в качестве аргумента, потому что ожидает на вход таблицу. Обойти это ограничение можно при помощи функции
, способной превратить
любую скалярную величину в таблицу, как показано в следующем примере:
EVALUATE
ROW ( "Result"; [Sales Amount] )
Результат выполнения этого запроса показан на рис. 13.3.
Рис 13 3
нк и ROW возвра ает та ли
с одно строко
Такого же результата можно добиться, используя конструктор таблиц:
EVALUATE
{ [Sales Amount] }
На рис. 13.4 можно видеть результат выполнения запроса.
Рис 13 4
онстр ктор та ли
возвра ает та ли с одно строко
и стол о с и ене Value
Функция
позволяет разработчику самому задать наименование столбца, тогда как конструктор таблиц такой возможности не дает. Кроме того,
с помощью функции
можно создать таблицу с несколькими столбцами,
каждому из которых дать свое имя и выражение. Если есть необходимость
смоделировать присутствие среза, можно воспользоваться функцией
, как показано ниже:
EVALUATE
CALCULATETABLE (
ROW (
"Sales"; [Sales Amount];
"Cost"; [Total Cost]
);
'Product'[Color] = "Red"
)
Результат выполнения запроса показан на рис. 13.5.
Рис 13 5
нк и ROW ожет ис ользоватьс дл создани
та ли с нескольки и стол а и, ри то зна ени в е ка
в исл тс в ра ка тек его контекста ильтра
1
Создание за росов
439
Функция SUMMARI E
Мы уже использовали функцию
в предыдущих главах книги. Тогда мы говорили, что эта функция способна группировать строки по столбцам
и добавлять новые значения. И если операцию группирования данных при
помощи функции
можно считать совершенно безопасной, то добавление столбцов способно приводить к неожиданным результатам, которые
бывает трудно понять и исправить.
И хотя добавление столбцов при помощи функции
– это плохая
идея, мы представим вам два способа использования этой функции для выполнения данной операции. Нам важно, чтобы читатели не растерялись, если
увидят подобный код, написанный кем-то другим. Но мы еще раз повторим,
что ис оль о ать у к и S
A I
л оба ле и столб о , а реиру
и
а е и , е елатель о
Если кто-то использует функцию
для подсчета значений, спешим напомнить, что вы также можете снабжать результирующую таблицу дополнительными строками с подытогами. Для этого в функции
есть специальный модификатор с именем
, который меняет функцию
агрегирования по столбцам таким образом, чтобы в итоговый набор были добавлены предварительные итоги. Взгляните на следующий запрос:
EVALUATE
SUMMARIZE (
Sales;
ROLLUP (
'Product'[Category];
'Date'[Calendar Year]
);
"Sales"; [Sales Amount]
)
ORDER BY
'Product'[Category];
'Date'[Calendar Year]
Модификатор
указывает функции
не только подсчитывать суммы продаж в таблице
по категориям товаров и годам, но также
добавлять строки в результирующий набор с пустыми значениями в столбце
с годом, в которых будет содержаться подытог по конкретной категории товаров. А поскольку столбец с категорией тоже помечен как
, в наборе
также появится строка с пустыми значениями в обоих группируемых столбцах,
в которой будет содержаться общий итог. Результат выполнения этого запроса
показан на рис. 13.6.
В строках, созданных при помощи
, группируемые столбцы содержат
пустые значения. Если в исходном столбце есть пустые значения, то в итоговом наборе окажется сразу две строки с пустым значением в этом столбце:
одна с агрегированным значением по пустой категории, а вторая – с подытогом. Чтобы лучше отличать их и облегчить пометку строк с подытогами, можно
добавить специальные столбцы с функцией
в выражении, как показано ниже:
440
1
Создание за росов
EVALUATE
SUMMARIZE (
Sales;
ROLLUP (
'Product'[Category];
'Date'[Calendar Year]
);
"Sales"; [Sales Amount];
"SubtotalCategory"; ISSUBTOTAL ( 'Product'[Category] );
"SubtotalYear"; ISSUBTOTAL ( 'Date'[Calendar Year] )
)
ORDER BY
'Product'[Category];
'Date'[Calendar Year]
Рис 13 6 Моди икатор ROLLUP
создает до олнительн е строки с од тога и
ри в олнении нк ии SUMMARIZE
Последние два столбца будут заполнены булевыми значениями: если в строке содержатся подытоги по соответствующему столбцу, будет указано
,
если нет –
, что видно по рис. 13.7.
Рис 13 7
нк и ISSUBTOTAL возвра ает True,
если в строке редставлен од тог
С использованием функции
ными от предварительных итогов.
можно легко отличать строки с дан1
Создание за росов
441
Важно
нк и SUMMARIZE не должна ис ользоватьс дл до авлени стол ов в рез льтир
и на ор ри ер с ри енение св зки нк и ROLLUP и ISSUBTOTAL
редставили, то
на
итатель не тер лс , если встретит так
ко ина и в жо
коде
не должн ри ен ть в свои в ражени
нк и SUMMARIZE с то ель
л
е ис ользовать нк и SUMMARIZECOLUMNS или св зк ADDCOLUMNS и SUMMARIZE, когда ри енить SUMMARIZECOLUMNS невоз ожно
Функция SUMMARI EC LUM S
– очень мощная и универсальная функция применительно к запросам. В одной этой функции, по сути, уместилось все, что необходимо для работы с запросами. Судите сами, функция
позволяет вам задать:
„ набор столбцов для осуществления группировки по подобию функции
, с опциональным выводом подытогов;
„ набор новых столбцов в результирующей таблице – как связка функций
и
;
„ набор фильтров для применения к модели данных перед выполнением
группировки, подобно функции
.
И наконец, функция
удаляет из результата строки,
в которых значения всех добавленных столбцов пустые. Неудивительно, что
Power BI использует функцию
для выполнения почти
всех запросов.
Вот простой пример использования функции
:
EVALUATE
SUMMARIZECOLUMNS (
'Product'[Category];
'Date'[Calendar Year];
"Amount"; [Sales Amount]
)
ORDER BY
'Product'[Category];
'Date'[Calendar Year]
Этот запрос группирует информацию по категориям товаров и годам, рассчитывая сумму продаж в рамках контекста фильтра, содержащего текущую
категорию и год, для каждой строки. Результат выполнения запроса показан
на рис. 13.8.
Годы, в которые не было продаж (например, 2005-й), не включаются в итоговый вывод. Причина в том, что для этих строк результат добавленного столбца
с суммой продаж является пустым значением, а этого достаточно для его исключения из итогового набора. Если разработчик хочет, чтобы строки с пустыми
значениями остались в результирующей таблице, он может использовать модификатор
, как показано в измененной версии предыдущего запроса:
EVALUATE
SUMMARIZECOLUMNS (
442
1
Создание за росов
'Product'[Category];
'Date'[Calendar Year];
"Amount"; IGNORE ( [Sales Amount] )
)
ORDER BY
'Product'[Category];
'Date'[Calendar Year]
Рис 13 8
а также с
а ли а содержит сгр
родаж о ти ко
ированн е категории и год ,
ина и
Как результат функция
проигнорирует тот факт, что
в столбце
содержится пустое значение, и включит его в набор.
В результате мы получим таблицу с пустыми значениями в столбце с продажами, как видно по рис. 13.9.
Рис 13 9
с ользование оди икатора IGNORE озвол ет со ран ть
в рез льтир
е на оре строки с ст и зна ени и
Если функция
добавляет к набору несколько столбцов,
разработчик вправе выбирать, по каким из них игнорировать пустые значения
(при помощи модификатора
), а по каким осуществлять проверку. На
практике же обычно принято удалять все строки с пустыми значениями.
1
Создание за росов
443
Функция
позволяет также подсчитывать промежуточные итоги, используя функции
и
.
Если бы вам при написании предыдущего запроса потребовалось выводить
подытог по годам, вы могли бы пометить столбец Date[Calendar Year] при помощи функции
, указав также имя дополнительного столбца, в котором будет выводиться информация о том, подытог в строке или нет:
EVALUATE
SUMMARIZECOLUMNS (
'Product'[Category];
ROLLUPADDISSUBTOTAL (
'Date'[Calendar Year];
"YearTotal"
);
"Amount"; [Sales Amount]
)
ORDER BY
'Product'[Category];
'Date'[Calendar Year]
Теперь в результирующий набор будут включены строки с подытогами по
годам, а также дополнительный столбец
, в котором для строк с промежуточными итогами будет стоять значение
. На рис. 13.10 показан вывод этого запроса с выделенными строками итогов.
Рис 13 10
нк и ROLLUPADDISSUBTOTAL до авл ет строки с од тога и
и создает до олнительн стол е с ин ор а ие о ни
Группируя таблицу по множеству столбцов, вы можете пометить функцией
сразу несколько из них. Это позволит создать несколько уровней группировки. Например, следующий запрос подводит промежуточные итоги по категориям товаров для всех годов, а также по годам для
всех категорий:
EVALUATE
SUMMARIZECOLUMNS (
444
1
Создание за росов
ROLLUPADDISSUBTOTAL (
'Product'[Category];
"CategoryTotal"
);
ROLLUPADDISSUBTOTAL (
'Date'[Calendar Year];
"YearTotal"
);
"Amount"; [Sales Amount]
)
ORDER BY
'Product'[Category];
'Date'[Calendar Year]
Строки с подытогами по годам без категорий и по категориям без разделения на годы выделены на рис. 13.11.
Рис 13 11
нк и ROLLUPADDISSUBTOTAL с осо на гр
о нескольки колонка
ировать та ли
Если вам нужно вывести подытоги для группы столбцов, вам придет на помощь модификатор
. В следующем запросе добавляется объединенный подытог по категориям и годам, в результате чего в таблице появляется только одна дополнительная строка:
EVALUATE
SUMMARIZECOLUMNS (
ROLLUPADDISSUBTOTAL (
ROLLUPGROUP (
'Product'[Category];
'Date'[Calendar Year]
);
"CategoryYearTotal"
);
"Amount"; [Sales Amount]
1
Создание за росов
445
)
ORDER BY
'Product'[Category];
'Date'[Calendar Year]
На рис. 13.12 показан результат с одной строкой с итогами.
Рис 13 12
нк и ROLLUPADDISSUBTOTAL создает до олнительн
и стол е с од тога и
строк
Последней особенностью функции
является способность фильтровать результат по подобию функции
. Вы можете указать один или несколько фильтров, используя таблицы в качестве дополнительных аргументов. Например, следующий запрос извлекает продажи
только по покупателям с определенным уровнем образования (High School).
Результат будет таким же, как на рис. 13.12, но с меньшими суммами:
EVALUATE
SUMMARIZECOLUMNS (
ROLLUPADDISSUBTOTAL (
ROLLUPGROUP (
'Product'[Category];
'Date'[Calendar Year]
);
"CategoryYearTotal"
);
FILTER (
ALL ( Customer[Education] );
Customer[Education] = "High School"
);
"Amount"; [Sales Amount]
)
Заметим, что в функции
использование лаконичного
синтаксиса аргументов фильтра в виде предикатов, как в функциях
и
, запрещено. А значит, следующий код выдаст ошибку:
EVALUATE
SUMMARIZECOLUMNS (
446
1
Создание за росов
ROLLUPADDISSUBTOTAL (
ROLLUPGROUP (
'Product'[Category];
'Date'[Calendar Year]
);
"CategoryYearTotal"
);
Customer[Education] = "High School";
"Amount"; [Sales Amount]
-- Такой синтаксис недопустим
)
Причина в том, что аргументы фильтра в функции
должны быть выражены в виде таблиц, и никакие сокращения в этом случае
неприемлемы. Самый простой и компактный способ добавить фильтр к функции
– использовать функцию
:
EVALUATE
SUMMARIZECOLUMNS (
ROLLUPADDISSUBTOTAL (
ROLLUPGROUP (
'Product'[Category];
'Date'[Calendar Year]
);
"CategoryYearTotal"
);
TREATAS ( { "High School" }; Customer[Education] );
"Amount"; [Sales Amount]
)
Как вы заметили, функция
является очень мощной, но
у нее есть свои серьезные ограничения. Дело в том, что она не может быть вызвана, если было произведено преобразование внешнего контекста фильтра.
Именно поэтому эта функция так полезна в запросах, но не может полноценно заменить связку функций
и
в мерах, поскольку
просто не будет работать в большинстве отчетов. Меры часто используются
в таких элементах визуализации, как матрица или график, где они внутренне
вычисляются в контексте строки для каждого значения, выведенного в отчет.
Чтобы еще лучше продемонстрировать ограничения функции
в рамках контекста строки, рассмотрим следующий запрос для вычисления суммарных продаж по товарам с использованием неэффективного,
но допустимого способа:
EVALUATE
{
SUMX (
VALUES ( 'Product'[Category] );
CALCULATE (
SUMX (
ADDCOLUMNS (
VALUES ( 'Product'[Subcategory] );
"SubcategoryTotal"; [Sales Amount]
1
Создание за росов
447
);
[SubcategoryTotal]
)
)
)
}
Если заменить функцию
на
даст ошибку по причине того, что функция
на в контексте, преобразованном функцией
запрос не выполнится:
, запрос выбыла вызва. А значит, следующий
EVALUATE
{
SUMX (
VALUES ( 'Product'[Category] );
CALCULATE (
SUMX (
SUMMARIZECOLUMNS (
'Product'[Subcategory];
"SubcategoryTotal"; [Sales Amount]
);
[SubcategoryTotal]
)
)
)
}
В основном функция
неприменима в мерах, поскольку меры обычно рассчитываются в рамках сложных запросов, сгенерированных клиентскими инструментами. Такие запросы с большой степенью вероятности используют концепцию преобразования контекста, а значит, функции
в них просто не место.
Функция T P
Функция
позволяет отсортировать таблицу и вернуть набор из заданного количества строк. Это бывает полезно, когда нужно ограничить количество
возвращаемых строк. Например, когда Power BI показывает содержимое таблицы, вся таблица целиком не извлекается. Вместо этого из таблицы берется
несколько первых строк для заполнения экранного пространства. Оставшаяся
часть таблицы извлекается по требованию, когда пользователь осуществляет
прокрутку в элементе визуализации. Также функцию
можно использовать для получения ограниченного списка лучших элементов по тому или иному показателю, например лучших покупателей, товаров и т. д.
Три товара с максимальной суммой продаж можно получить при помощи
следующего запроса, вычисляющего меру
для каждой строки
в таблице
:
EVALUATE
TOPN (
448
1
Создание за росов
3;
'Product';
[Sales Amount]
)
В результирующей таблице будут содержаться все столбцы из исходной таблицы. Но обычно при выполнении подобных запросов пользователя не интересуют все столбцы, так что лучше предварительно ограничить исходную
таблицу нужными столбцами. В следующем примере показана версия запроса
с уменьшенным количеством столбцов. Результат выполнения запроса отображен следом на рис. 13.13:
EVALUATE
VAR ProductsBrands =
SUMMARIZE (
Sales;
'Product'[Product Name];
'Product'[Brand]
)
VAR Result =
TOPN (
3;
ProductsBrands;
[Sales Amount]
)
RETURN Result
ORDER BY 'Product'[Product Name]
Рис 13 13
нк и TOPN ильтр ет ис одн
на основании зна ени стол а Sales Amount
та ли
Вероятно, вам может понадобиться включить в итоговый набор и саму меру
, чтобы можно было правильно отсортировать результаты. В таком
случае лучше всего будет добавить нужный столбец к предварительно сгруппированной исходной таблице, после чего выполнять функцию
. Таким
образом, один из самых распространенных шаблонов применения функции
показан в следующем примере:
EVALUATE
VAR ProductsBrands =
SUMMARIZE (
Sales;
'Product'[Product Name];
'Product'[Brand]
)
1
Создание за росов
449
VAR ProductsBrandsSales =
ADDCOLUMNS (
ProductsBrands;
"Product Sales"; [Sales Amount]
)
VAR Result =
TOPN (
3;
ProductsBrandsSales;
[Product Sales]
)
RETURN Result
ORDER BY [Product Sales] DESC
Результат выполнения этого запроса показан на рис. 13.14.
Рис 13 14
нк и TOPN возвра ает ерв е
отсортированно о в ражени
строк та ли
,
Исходная таблица может быть отсортирована как по возрастанию, так и по
убыванию значения для отбора. По умолчанию сортировка будет выполнена по убыванию, чтобы возвращать строки с максимальными значениями
фильтруемого столбца. Четвертый необязательный параметр функции
как раз и призван устанавливать направление сортировки. Значения 0 или
(по умолчанию) отсортируют таблицу по убыванию, а 1 или
– по
возрастанию.
Важно
е та те сортировк ис одн данн
ри о о и нк ии TOPN с сортировко рез льтир
его на ора, ос ествл е о осредство кл евого слова ORDER BY
инстр к ии EVALUATE ара етр
нк ии TOPN отве ает искл ительно за сортировк
та ли , генерир е о то
нк ие
Если в исходной таблице присутствуют строки с одинаковыми значениями
фильтруемого столбца, функция
не гарантирует возвращения запрошенного количества строк. Вместо этого она вернет все строки с одинаковыми
значениями. Например, в следующем запросе мы запросили четыре ведущих
бренда по суммам продаж, при этом искусственно создав дубликаты за счет
использования функции округления
:
EVALUATE
VAR SalesByBrand =
ADDCOLUMNS (
VALUES ( 'Product'[Brand] );
"Product Sales"; MROUND ( [Sales Amount]; 1000000 )
450
1
Создание за росов
)
VAR Result =
TOPN (
4;
SalesByBrand;
[Product Sales]
)
RETURN Result
ORDER BY [Product Sales] DESC
В результирующий набор было включено пять строк, а не четыре, как мы
запросили, поскольку у брендов Litware и Proseware оказались одинаковые
продажи, округленные до миллиона, по 3 000 000. Обнаружив дублирующиеся значения, функция
вернула обе записи, не зная, какой из них отдать
предпочтение, как видно по рис. 13.15.
Рис 13 15
рис тствии одинаков
зна ени
нк и TOPN ожет верн ть
оль е строк, е
за росили
Во избежание этого в функцию
можно передать дополнительные
столбцы для сортировки. Фактически вместо одного третьего параметра можно использовать целый перечень столбцов. Например, чтобы выбрать первые
четыре бренда по продажам и при этом в случае равенства значений отдать
предпочтение тому из них, который стоит выше в алфавитном порядке, можно
использовать следующую запись:
EVALUATE
VAR SalesByBrand =
ADDCOLUMNS (
VALUES ( 'Product'[Brand] );
"Product Sales"; MROUND ( [Sales Amount]; 1000000 )
)
VAR Result =
TOPN (
4;
SalesByBrand;
[Product Sales]; 0;
'Product'[Brand]; 1
)
RETURN Result
ORDER BY [Product Sales] DESC
В результате, показанном на рис. 13.16, исчез бренд Proseware, поскольку
конкурирующий с ним Litware находится выше в алфавитном порядке. Заметьте, что в запросе мы использовали сортировку по убыванию для суммы продаж
и по возрастанию – для наименования брендов.
1
Создание за росов
451
Рис 13 16
с ольз до олнительн е ара етр сортировки,
ожно из авитьс от кон ликтов одинаков зна ени в итогово та ли е
Помните, что даже добавление дополнительных параметров сортировки не
гарантирует вам извлечения строго запрошенного количества строк. Функция
все равно может возвращать больше строк в случае полной идентичности фильтруемых столбцов. Дополнительные параметры сортировки лишь
снижают вероятность появления одинаковых значений. Если же вам необходимо гарантированно получить указанное количество строк из таблицы, стоит
добавить к порядку сортировки поле с уникальными значениями, что исключит вероятность появления дубликатов.
Рассмотрим более сложный пример использования функции
с применением функций для работы со множествами и переменных. Нам необходимо сформировать отчет по десяти лучшим товарам, исходя из суммы продаж, и при этом добавить в вывод дополнительную строку Others (Остальные),
в которой будут объединены все оставшиеся позиции. Возможная реализация
этого примера показана ниже:
EVALUATE
VAR NumOfTopProducts = 10
VAR ProdsWithSales =
ADDCOLUMNS (
VALUES ( 'Product'[Product Name] );
"Product Sales"; [Sales Amount]
)
VAR TopNProducts =
TOPN (
NumOfTopProducts;
ProdsWithSales;
[Product Sales]
)
VAR RemainingProducts =
EXCEPT ( ProdsWithSales; TopNProducts )
VAR OtherRow =
ROW (
"Product Name"; "Others";
"Product Sales"; SUMX (
RemainingProducts;
[Product Sales]
)
)
VAR Result =
UNION ( TopNProducts; OtherRow )
RETURN Result
ORDER BY [Product Sales] DESC
452
1
Создание за росов
Сначала мы сохраняем в переменной
таблицу по всем товарам с продажами. Затем выбираем из этого списка первые десять позиций,
записывая результат в переменную
. В переменной
мы воспользовались функцией
для извлечения всех товаров,
не вошедших в десять лучших. Разбив исходный набор товаров на две группы
(
и
), мы создаем таблицу из одной строки с именем товара Others, в которой подсчитана сумма продаж по всем оставшимся товарам из переменной
. После этого мы используем функцию
для объединения наборов из десяти лучших товаров с агрегированной
строкой из остальных. Результат выполнения запроса показан на рис. 13.17.
Рис 13 17
до олнительно строке
не вкл енн е в ерв дес тк
со ран все товар ,
Результаты в отчете выводятся правильные, но внешний вид немного смущает. Строка с остальными товарами выводится первой в списке и может располагаться в любом месте отчета в зависимости от значения в ней. Вам же,
скорее всего, захочется разместить эту строку в конце списка, тогда как первые
десять товаров должны находиться вверху с сортировкой по сумме продаж по
убыванию.
Эту задачу можно решить путем добавления столбца для сортировки, который отправит строку с остальными товарами в нижнюю часть списка:
EVALUATE
VAR NumOfTopProducts = 10
VAR ProdsWithSales =
ADDCOLUMNS (
VALUES ( 'Product'[Product Name] );
"Product Sales"; [Sales Amount]
)
VAR TopNProducts =
TOPN (
NumOfTopProducts;
ProdsWithSales;
[Product Sales]
)
1
Создание за росов
453
VAR RemainingProducts =
EXCEPT ( ProdsWithSales; TopNProducts )
VAR RankedTopProducts =
ADDCOLUMNS(
TopNProducts;
"SortColumn"; RANKX ( TopNProducts; [Product Sales] )
)
VAR OtherRow =
ROW (
"Product Name"; "Others";
"Product Sales"; SUMX (
RemainingProducts;
[Product Sales]
);
"SortColumn"; NumOfTopProducts + 1
)
VAR Result =
UNION ( RankedTopProducts; OtherRow )
RETURN
Result
ORDER BY [SortColumn]
Результат выполнения этого запроса показан на рис. 13.18.
Рис 13 18
ри нали ии стол а SortColumn
разра от ик ожет сво одно равл ть сортировко рез льтир
е та ли
Функции GE ERATE и GE ERATEALL
является очень мощной функцией, реализующей логику инструкции OUTER APPLY из языка SQL. В качестве параметров функция
принимает таблицу и выражение. Функция проходит по таблице, вычисляет
выражение в контексте строки итерации и затем объединяет строку текущей
итерации с таблицей, возвращенной переданным выражением. Поведение
этой функции похоже на объединение, но вместо объединения с таблицей происходит связка с результатом выражения, выполненного для каждой строки.
Это очень универсальная функция.
454
1
Создание за росов
Чтобы продемонстрировать поведение функции
, вернемся к предыдущему примеру с
и расширим его. На этот раз вместо подсчета лучших товаров за всю историю работы компании мы будем выделять первую
тройку за каждый год. Эту задачу можно разбить на два шага: создать выражение для подсчета первых трех товаров, а затем вычислить его для каждого года.
Одним из способов расчета первых трех товаров является следующий:
EVALUATE
VAR ProductsSold =
SUMMARIZE (
Sales;
'Product'[Product Name]
)
VAR ProductsSales =
ADDCOLUMNS (
ProductsSold;
"Product Sales"; [Sales Amount]
)
VAR Top3Products =
TOPN (
3;
ProductsSales;
[Product Sales]
)
RETURN
Top3Products
ORDER BY [Product Sales] DESC
В результате, показанном на рис. 13.19, выведены только три товара.
Рис 13 19
нк и TOPN возвра ает три са
за вс истори ко ании
ри
льн
товара
Если предыдущий запрос выполнить в рамках контекста фильтра, включающего конкретный год, результат будет иным. Формула вернет самые продаваемые товары за этот год. И здесь приходит на помощь функция
. Мы
используем ее для осуществления итераций по годам и вычисления выражения с функцией
для каждой итерации. Для каждого года функция
вернет по три товара, после чего функция
объединит полученные
результаты по итерациям. Вот как может выглядеть этот запрос:
EVALUATE
GENERATE (
VALUES ( 'Date'[Calendar Year] );
CALCULATETABLE (
VAR ProductsSold =
1
Создание за росов
455
SUMMARIZE ( Sales; 'Product'[Product Name] )
VAR ProductsSales =
ADDCOLUMNS ( ProductsSold; "Product Sales"; [Sales Amount] )
VAR Top3Products =
TOPN ( 3; ProductsSales; [Product Sales] )
RETURN Top3Products
)
)
ORDER BY
'Date'[Calendar Year];
[Product Sales] DESC
Результат выполнения запроса показан на рис. 13.20.
Рис 13 20
с тре са
нк и GENERATE о един ет год
и родавае
и товара и о каждо
из ни
Если вам необходимо выделить по три товара в рамках категорий товаров,
достаточно обновить таблицу для итераций в функции
, как показано ниже:
EVALUATE
GENERATE (
VALUES ( 'Product'[Category] );
CALCULATETABLE (
VAR ProductsSold =
SUMMARIZE ( Sales; 'Product'[Product Name] )
VAR ProductsSales =
ADDCOLUMNS ( ProductsSold; "Product Sales"; [Sales Amount] )
VAR Top3Products =
TOPN ( 3; ProductsSales; [Product Sales] )
RETURN Top3Products
)
)
ORDER BY
'Product'[Category];
[Product Sales] DESC
Как видно по рис. 13.21, теперь в отчете выведены первые тройки товаров
в рамках каждой категории.
456
1
Создание за росов
Рис 13 21
ро од
в каждо из ни
о категори
,
оже в делить о три са
о л рн
товара
Если выражение, переданное функции
в качестве второго параметра, возвращает пустую таблицу, эта строка исключается из итогового набора. Если же необходимо оставить их в результирующей таблице, можно использовать функцию
. Например, в 2005 году продаж не было,
а значит, для этого года и не может быть самых продаваемых товаров. Функция
не включит этот год в итоговый результат, тогда как
оставит его в выводе, как показано ниже:
EVALUATE
GENERATEALL (
VALUES ( 'Date'[Calendar Year] );
CALCULATETABLE (
VAR ProductsSold =
SUMMARIZE ( Sales; 'Product'[Product Name] )
VAR ProductsSales =
ADDCOLUMNS ( ProductsSold; "Product Sales"; [Sales Amount] )
VAR Top3Products =
TOPN ( 3; ProductsSales; [Product Sales] )
RETURN Top3Products
)
)
ORDER BY
'Date'[Calendar Year];
[Product Sales] DESC
Результат выполнения этого запроса показан на рис. 13.22.
Функция IS
RAFTER
является специальной функцией и часто используется Power BI
и другими инструментами для постраничного вывода отчетов. В то же время
разработчики довольно редко прибегают к ее помощи при написании запросов и мер. Когда пользователь формирует отчет в Power BI, движок извлекает из
источника ровно столько строк, сколько необходимо для размещения на одной
странице. Для этого он использует уже знакомую нам функцию
.
1
Создание за росов
457
Рис 13 22
нк и GENERATEALL оставл ет в на оре год ,
о котор
не ло родаж, а GENERATE нет
При просмотре таблицы с товарами пользователь каждый раз располагается на определенной странице. Например, на рис. 13.23 последней строкой на
странице является товар Stereo Bluetooth Headphones New Gen, а стрелкой обозначена относительная позиция страницы в списке.
Рис 13 23
ри сканировании та ли
Product ользователь достиг о ределенно то ки
Прокручивая отчет ниже, пользователь может дойти до последней строки,
которая была извлечена в рамках предыдущего страничного запроса. В этот
момент Power BI выполняет новый запрос, извлекая таким образом следующие
строки. При этом снова будет использована функция
, поскольку Power BI
каждый раз извлекает определенное количество строк из модели данных. Но
важно, чтобы это были следующие строки за просмотренными. Именно здесь
на помощь приходит функция
. Полный запрос, выполняемый
Power BI во время прокрутки отчета, показан ниже, а результат вывода – на
рис. 13.24:
458
1
Создание за росов
EVALUATE
TOPN (
501;
FILTER (
KEEPFILTERS (
SUMMARIZECOLUMNS (
'Product'[Category];
'Product'[Color];
'Product'[Product Name];
"Sales_Amount"; 'Sales'[Sales Amount]
)
);
ISONORAFTER (
'Product'[Category]; "Audio"; ASC;
'Product'[Color]; "Yellow"; ASC;
'Product'[Product Name]; "WWI Stereo Bluetooth Headphones New Generation M370
Yellow"; ASC
)
);
'Product'[Category]; 1;
'Product'[Color]; 1;
'Product'[Product Name]; 1
)
ORDER BY
'Product'[Category];
'Product'[Color];
'Product'[Product Name]
Рис 13 24 След
а страни а со строка и,
на ина с оследне строки в ред д е на оре
В начале кода запускается функция
по результату функции
.
используется для удаления ранее просмотренных строк, а для того чтобы получить следующую страницу, прибегает к помощи функции
. То же самое условие могло быть выражено и через обычную булеву логику.
Фактически фрагмент с функцией
здесь можно заменить на следующий код:
'Product'[Category] > "Audio"
|| ( 'Product'[Category] = "Audio" && 'Product'[Color] > "Yellow" )
|| ( 'Product'[Category] = "Audio"
&& 'Product'[Color] = "Yellow"
&& 'Product'[Product Name]
1
Создание за росов
459
>= "WWI Stereo Bluetooth Headphones New Generation M370 Yellow"
)
Но функцию
применять гораздо лучше. Во-первых, код будет
читаться легче, во-вторых, план выполнения запроса с большой степенью вероятности окажется более эффективным.
Функция ADDMISSI GITEMS
– еще одна специальная функция, часто используемая
Power BI и редко – самими разработчиками. Она призвана добавлять в набор
строки, которые могли быть пропущены функцией
. Например, следующее выражение использует функцию
для группирования данных по годам. Результат запроса показан на рис. 13.25.
Рис 13 25
с отс тств
нк и SUMMARIZECOLUMNS не вкл
и и родажа и, то есть когда в стол
ает в в вод год
е Amt на одитс
стое зна ение
Годы без продаж не включаются в итоговый набор функцией
. Чтобы извлечь строки, проигнорированные функцией
, можно использовать функцию
:
EVALUATE
ADDMISSINGITEMS (
'Date'[Calendar Year];
SUMMARIZECOLUMNS (
'Date'[Calendar Year];
"Amt"; [Sales Amount]
);
'Date'[Calendar Year]
)
ORDER BY 'Date'[Calendar Year]
Результат выполнения этого запроса показан на рис. 13.26, где мы выделили
строки, возвращенные функцией
. Строки с пустым значением в столбце
были добавлены функцией
.
Рис 13 26
нк и ADDMISSINGITEMS до авл ет
в рез льтат строки с ст и зна ени и
в стол е Amt
460
1
Создание за росов
Функция
также может принимать различные модификаторы и параметры для лучшего контроля над подытогами и фильтрами.
Функция T P S IP
Функция
преимущественно используется Power BI для вывода нескольких строк из объемных исходных данных в области представления данных. Другие инструменты вроде Power Pivot и SQL Server Data Tools используют
другие техники для быстрого просмотра и фильтрации исходных необработанных данных. Причина для их использования состоит в возможности быстро
просмотреть фрагмент данных без необходимости ожидать материализации
всего набора данных. Функция
и другие техники подробно описаны
по адресу: http://www.sqlbi.com/articles/querying-raw-data-to-tabular/.
Функция GR UPB
Функция
применяется для выполнения группировки таблицы по
одному или нескольким столбцам, агрегируя значения подобно связке функций
и
. Главным отличием функций
и
является то, что функция
умеет группировать столбцы,
для которых привязка данных не соответствует столбцам в модели данных,
тогда как
допустимо использовать только со столбцами, определенными в модели. Кроме того, в столбцах, добавленных к таблице функцией
, необходимо использовать итерационные функции вроде
и
для агрегирования данных.
Рассмотрим пример группирования таблицы продаж по году и месяцу
с агрегированием значения суммы продаж. Это возможно сделать при помощи
следующего запроса, результат которого показан на рис. 13.27:
EVALUATE
GROUPBY (
Sales;
'Date'[Calendar Year];
'Date'[Month];
'Date'[Month Number];
"Amt"; AVERAGEX (
CURRENTGROUP ();
Sales[Quantity] * Sales[Net Price]
)
)
ORDER BY
'Date'[Calendar Year];
'Date'[Month Number]
В плане производительности функция
может проседать применительно к большим наборам данных – начиная от нескольких десятков тысяч
записей и выше. Фактически функция приступает к осуществлению группировки только после завершения процесса материализации таблицы, а значит, она
не слишком применима к большим наборам данных. Кроме того, в большин1
Создание за росов
461
стве случаев запросы легче выразить через сочетание функций
и
. Предыдущий пример можно записать следующим образом:
EVALUATE
ADDCOLUMNS (
SUMMARIZE (
Sales;
'Date'[Calendar Year];
'Date'[Month];
'Date'[Month Number]
);
"Amt"; AVERAGEX (
RELATEDTABLE ( Sales );
Sales[Quantity] * Sales[Net Price]
)
)
ORDER BY
'Date'[Calendar Year];
'Date'[Month Number]
Рис 13 27
с ользование нк ии GROUPBY
дл одс ета средни родаж о года и ес а
Примечание Стоит от етить, то в ред д е за росе на в оде из нк ии SUMMARIZE дет та ли а со стол а и из та ли Date ак то когда озже нк и AVERAGEX
ос ествл ет итера ии о рез льтат
нк ии RELATEDTABLE, в та ли е, возвра енно
нк ие RELATEDTABLE, д т год и ес из тек е итера ии нк ии ADDCOLUMNS
о рез льтир
е та ли е из
нк ии SUMMARIZE о ните, то рив зка данн
в то сл ае со ранитс аки о разо , на в оде нк ии SUMMARIZE
ол и
та ли с рив зко данн
Одним из преимуществ функции
является ее способность группировать столбцы, добавленные к запросу функциями
или
. Ниже приведен пример, где функция
не может быть использована в качестве альтернативы
:
EVALUATE
VAR AvgCustomerSales =
AVERAGEX (
Customer;
[Sales Amount]
462
1
Создание за росов
)
VAR ClassifiedCustomers =
ADDCOLUMNS (
VALUES ( Customer[Customer Code] );
"Customer Category"; IF (
[Sales Amount] >= AvgCustomerSales;
"Above Average"; -- выше среднего
"Below Average" -- ниже среднего
)
)
VAR GroupedResult =
GROUPBY (
ClassifiedCustomers;
[Customer Category];
"Number of Customers"; SUMX (
CURRENTGROUP ();
1
)
)
RETURN GroupedResult
ORDER BY [Customer Category]
Результат выполнения запроса можно видеть на рис. 13.28.
Рис 13 28
нк и GROUPBY еет гр
ировать стол
до авленн е в ро ессе в олнени за роса
,
Предыдущий пример демонстрирует одновременно и преимущества, и недостатки функции
. Сначала в коде создается новый столбец в таблице с покупателями, в котором вычисляется, приобрел ли данный покупатель
товаров за все время на сумму выше среднего или ниже. В результате мы выполняем группировку по созданному временному столбцу и считаем количество покупателей в обеих категориях.
Выполнение группировки по временному столбцу – очень полезная особенность функции
. Но при этом нам приходится использовать итерационную функцию
с проходом по
с использованием
константы 1. Причина в том, что столбцы, добавленные в таблицу функцией
, обязательно должны вычисляться путем прохождения по
. Простое выражение вроде COUNTROWS ( CURRENTGROUP () ) здесь не
сработает.
Существует не так много сценариев, где функция
может оказаться полезной. В основном это случаи, когда необходимо группировать столбцы,
созданные непосредственно в запросе. Стоит также помнить, что столбец, по
которому будет осуществляться группировка, должен обладать низкой кратностью. Иначе вы можете столкнуться с проблемами производительности
и перерасхода памяти.
1
Создание за росов
463
Функции ATURALI
ER
I и ATURALLEFT UTER
I
Движок DAX использует связи в модели данных автоматически всякий раз,
когда разработчик запускает запрос на выполнение. Но иногда бывает полезно объединить в запросе две таблицы, не связанные физически. Например, вы
можете объявить табличную переменную и затем связать вычисляемую таблицу с этой переменной.
Представьте, что вам необходимо рассчитать средние продажи по категориям и построить отчет с показом категорий с продажами ниже среднего,
примерно средними и выше среднего. Столбец легко вычислить при помощи
функции
. Однако если результирующий набор должен быть отсортирован конкретным способом, нам придется одновременно получать описание
категории и порядок сортировки (новый столбец) с использованием похожего
кода.
Но можно поступить иначе – рассчитать только одно из двух значений, а затем использовать временную таблицу со связью для извлечения описания категории. Именно такой подход показан в следующем запросе:
EVALUATE
VAR AvgSales =
AVERAGEX (
VALUES ( 'Product'[Brand] );
[Sales Amount]
)
VAR LowerBoundary = AvgSales * 0.8
VAR UpperBoundary = AvgSales * 1.2
VAR Categories =
DATATABLE (
"Cat Sort"; INTEGER;
"Category"; STRING;
{
{ 0; "Below Average" }; -- ниже среднего
{ 1; "Around Average" }; -- около среднего
{ 2; "Above Average" } -- выше среднего
}
)
VAR BrandsClassified =
ADDCOLUMNS (
VALUES ( 'Product'[Brand] );
"Sales Amt"; [Sales Amount];
"Cat Sort"; SWITCH (
TRUE ();
[Sales Amount] <= LowerBoundary; 0;
[Sales Amount] >= UpperBoundary; 2;
1
)
)
VAR JoinedResult =
NATURALINNERJOIN (
Categories;
BrandsClassified
464
1
Создание за росов
)
RETURN JoinedResult
ORDER BY
[Cat Sort];
'Product'[Brand]
Перед описанием запроса полезно будет взглянуть на его результат, показанный на рис. 13.29.
Рис 13 29 Стол е Cat Sort должен ть ис ользован
в арг енте стол е дл сортировки в
Сначала мы создаем таблицу с брендами, суммами продаж и столбцом
со значением между 0 и 2. Это значение будет использовано в переменной
в качестве ключа для извлечения описания категории. Финальная
связка между временной таблицей и переменной осуществляется при помощи функции
, при этом связь устанавливается по столбцу
.
Функция
выполняет связь между таблицами на основании столбцов, имеющих одинаковые имена в обеих таблицах. Функция
делает то же самое, но вместо внутренней связи осуществляет левое внешнее соединение таблиц. Таким образом, из первой таблицы
сохраняются строки даже в том случае, если для них нет соответствующих строк
во второй таблице.
Если обе таблицы физически определены в модели данных, они могут
быть объединены только посредством связи. Связи помогают получить объединенную информацию из таблиц, как это происходит в SQL. Обе функции –
и
,и
– используют связи между
таблицами, если они имеются. В противном случае для объединения таблиц
необходимо, чтобы таблицы обладали одинаковой привязкой данных.
Например, следующий запрос возвращает все строки из таблицы
,
имеющие соответствующие им строки в таблице
, при этом в результирующий набор будут включены все неповторяющиеся столбцы из обеих
таблиц:
1
Создание за росов
465
EVALUATE
NATURALINNERJOIN ( Sales; Product )
Следующий запрос вернет все строки из таблицы
по которым не было записей в таблице
:
, включая те товары,
EVALUATE
NATURALLEFTOUTERJOIN ( Product; Sales )
В обоих случаях столбец, по которому выполняется объединение, будет лишь
раз включен в итоговый набор, в котором будут присутствовать все столбцы из
обеих таблиц.
Серьезным ограничением этих функций является то, что они не могут устанавливать соответствие между двумя столбцами с отсутствием связи и разной
привязкой данных. На практике это часто означает, что две таблицы с одним
или более столбцами с одинаковыми именами и без наличия связи не могут
быть объединены. В качестве обходного пути можно использовать знакомую
нам функцию
для переопределения привязки данных. В статье по
адресу https://www.sqlbi.com/articles/from-sql-to-dax-joining-tables/ это ограничение и способы его обхода описаны более подробно.
И все же функции
и
не так
часто употребляются в языке DAX – гораздо реже, чем аналогичные инструкции в SQL.
Важно
нк ии NATURALINNERJOIN и NATURALLEFTOUTERJOIN ог т оказатьс олезн и дл о единени вре енн та ли , в котор
рив зка данн дл стол ов не
соответств ет изи ески стол а в одели данн
то
о единить та ли
в одели, ежд котор и нет св зи, нео оди о ри егн ть к о о и
нк ии TREATAS
дл ерео ределени рив зки данн стол ов, котор е д т ис ользоватьс в св зке
Функция SUBSTITUTE IT I DEX
Функция
служит для замены в наборе строк столбцов, соответствующих заголовкам матрицы, на их порядковые номера.
Разработчики не так часто используют эту функцию из-за ее повышенной
сложности. При этом функция
могла бы подойти
при создании динамического пользовательского интерфейса для написания
запросов на DAX. Power BI, например, использует эту функцию при работе
с матрицами.
Представим, что у нас есть матрица в Power BI, показанная на рис. 13.30.
Результатом выполнения запроса на DAX всегда является таблица. Каждая
ячейка в матрице отчета соответствует одной строке в таблице, возвращаемой запросом. Чтобы корректно отобразить данные в отчете, Power BI использует функцию SUBSTITUTEWITHINDEX для преобразования имен столбцов матрицы (CY 2007, CY 2008 и CY 2009) в последовательность чисел для
облегчения заполнения матрицы во время считывания результатов. Приведем упрощенную версию запроса на DAX, сгенерированного для предыдущей
матрицы:
466
1
Создание за росов
DEFINE
VAR SalesYearCategory =
SUMMARIZECOLUMNS (
'Product'[Category];
'Date'[Calendar Year];
"Sales_Amount"; [Sales Amount]
)
VAR MatrixRows =
SUMMARIZE (
SalesYearCategory;
'Product'[Category]
)
VAR MatrixColumns =
SUMMARIZE (
SalesYearCategory;
'Date'[Calendar Year]
)
VAR SalesYearCategoryIndexed =
SUBSTITUTEWITHINDEX (
SalesYearCategory;
"ColumnIndex"; MatrixColumns;
'Date'[Calendar Year]; ASC
)
-- Первый результирующий набор: заголовки столбцов матрицы
EVALUATE
MatrixColumns
ORDER BY 'Date'[Calendar Year]
-- Второй результирующий набор: строки матрицы и их содержание
EVALUATE
NATURALLEFTOUTERJOIN (
MatrixRows;
SalesYearCategoryIndexed
)
ORDER BY
'Product'[Category];
[ColumnIndex]
Рис 13 30 Матри а в
, остроенна ри о о и за роса
с ис ользование
нк ии SUBSTITUTEWITHINDEX
1
Создание за росов
467
В запросе содержится два блока
. Первый из них возвращает содержимое заголовков столбцов, как показано на рис. 13.31.
Рис 13 31
ез льтат извле ени заголовков стол
из атри в
ов
Второй блок
возвращает оставшуюся часть матрицы, собирая по
строке для каждой ячейки ее содержимого. В результирующем наборе каждая
строка будет содержать столбцы, необходимые для заполнения заголовков
строк матрицы, следом за которыми будут идти цифры для отображения, а затем столбец с индексом, созданный при помощи функции
. Эта таблица показана на рис. 13.32.
Рис 13 32 Содержи ое строк атри в
,
сгенерированное ри о о и нк ии SUBSTITUTEWITHINDEX
Функция
главным образом используется для построения визуальных элементов в Power BI, таких как матрица.
Функция SAMPLE
Функция
предназначена для извлечения ограниченной выборки строк
из таблицы. В качестве аргументов функция принимает требуемое количество
записей, имя таблицы и порядок сортировки. В итоговую выборку функции
включаются первая и последняя строки таблицы, а также недостающее
количество строк до требуемого с равномерным распределением по таблице.
Например, следующий запрос возвращает набор из десяти товаров, предварительно отсортировав таблицу по столбцу
:
EVALUATE
SAMPLE (
10;
ADDCOLUMNS (
VALUES ( 'Product'[Product Name] );
"Sales"; [Sales Amount]
468
1
Создание за росов
);
'Product'[Product Name]
)
ORDER BY 'Product'[Product Name]
Результат выполнения запроса показан на рис. 13.33.
Рис 13 33
нк и SAMPLE возвра ает ограни енн
из та ли с равно ерн
рас ределение
на ор данн
Функция
активно используется клиентскими инструментами DAX
для заполнения данными осей на графиках. Также эта функция может пригодиться при выполнении статистических расчетов на основании ограниченного набора данных из таблицы.
Автоматическая проверка су ествования данных
в запросах DAX
Многие функции языка DAX поддерживают поведение, известное как авто
мати еска роверка су ествовани (auto-exists). Этот механизм задействуется при объединении в функции двух таблиц. При написании запросов очень
важно помнить об этой особенности DAX, и хотя в большинстве случаев такое
поведение функций является интуитивно понятным, иногда оно может стать
причиной неожиданных результатов.
Рассмотрим следующее выражение:
EVALUATE
SUMMARIZECOLUMNS (
'Product'[Category];
'Product'[Subcategory]
)
ORDER BY
'Product'[Category];
'Product'[Subcategory]
1
Создание за росов
469
Результатом выполнения этого запроса может быть как полное перекрестное соединение категорий и подкатегорий, так и список всех существующих
комбинаций значений этих двух столбцов. На самом деле каждая категория
содержит ограниченный список подкатегорий, так что в списке существующих
комбинаций двух столбцов будет меньше строк, чем в их перекрестном соединении.
Чисто интуитивно кажется, что функция
должна выдавать как раз список комбинаций двух полей. Именно так и происходит на
самом деле, и причина подобного поведения функции кроется в автоматической проверке существования. В результирующей таблице, показанной на
рис. 13.34, для категории выведены только три подкатегории, а не перечень из
всех возможных подкатегорий.
Рис 13 34
из с еств
нк и SUMMARIZECOLUMNS возвра ает с исок
и ко ина и зна ени
Автоматическая проверка существования вступает в силу всякий раз, когда
в запросе осуществляется группировка по столбцам из одной и той же таблицы. И когда этот механизм задействован, в результирующий набор автоматически включаются только существующие комбинации значений столбцов. Это
приводит к уменьшению количества строк в выводе и использованию более
эффективного плана выполнения запроса. Если же в запросе применяются
столбцы из разных таблиц, результат будет другим. В этом случае функция
выдаст полное перекрестное соединение двух столбцов.
Это видно на примере следующего запроса, результат выполнения которого
показан на рис. 13.35:
EVALUATE
SUMMARIZECOLUMNS (
'Product'[Category];
'Date'[Calendar Year]
)
ORDER BY
'Product'[Category];
'Date'[Calendar Year]
Несмотря на то что обе таблицы объединены посредством связей с таблицей
и в модели данных присутствуют годы, в которые не было транзакций, ме470
1
Создание за росов
ханизм автоматической проверки существования не активируется, поскольку
столбцы в выражении используются из разных таблиц.
Рис 13 35
создаетс и
ри астии в в ражении стол ов из разн
олное ерекрестное соединение
та ли
Помните о том, что функция
удаляет строки, в которых во всех дополнительных столбцах агрегированные значения возвращают пустоту. Таким образом, если в предыдущий запрос добавить меру
, функция
исключит из результирующего набора
годы и категории, по которым не было продаж, как показано на рис. 13.36:
DEFINE
MEASURE Sales[Sales Amount] =
SUMX (
Sales;
Sales[Quantity] * Sales[Net Price]
)
EVALUATE
SUMMARIZECOLUMNS (
'Product'[Category];
'Date'[Calendar Year];
"Sales"; [Sales Amount]
)
ORDER BY
'Product'[Category];
'Date'[Calendar Year]
Поведение предыдущего запроса не согласуется с механизмом автоматической проверки существования, поскольку его выполнение основывается на результате вычисления, включающего агрегацию. Константные выражения при
этом игнорируются. Например, если вместо пустого значения мы будем выводить 0, будет сформирован список из всех категорий по всем годам. Результат
выполнения следующего запроса показан на рис. 13.37:
1
Создание за росов
471
DEFINE
MEASURE Sales[Sales Amount] =
SUMX (
Sales;
Sales[Quantity] * Sales[Net Price]
)
EVALUATE
SUMMARIZECOLUMNS (
'Product'[Category];
'Date'[Calendar Year];
"Sales"; [Sales Amount] + 0 -- Возвращает 0 вместо пустого значения
)
ORDER BY
'Product'[Category];
'Date'[Calendar Year]
Рис 13 36
рис тствие стол а с агрега ие
ривело к искл ени строк с ст и зна ени
и
Рис 13 37
ри возвра ении н л в есто стого зна ени
нк и SUMMARIZECOLUMNS в водит все строки
В то же время такой подход для вывода всех строк не сработает, если столбцы
в функции будут браться из одной таблицы. В этом случае всегда будет приме472
1
Создание за росов
няться механизм автоматической проверки существования. Например, в следующем примере в выводе останутся только существующие комбинации полей
и
, несмотря на то что в мере мы по-прежнему выводим
0 вместо пустого значения:
DEFINE
MEASURE Sales[Sales Amount] =
SUMX (
Sales;
Sales[Quantity] * Sales[Net Price]
)
EVALUATE
SUMMARIZECOLUMNS (
'Product'[Category];
'Product'[Subcategory];
"Sales"; [Sales Amount] + 0
)
ORDER BY
'Product'[Category];
'Product'[Subcategory]
Результат выполнения этого запроса показан на рис. 13.38.
Рис 13 38
нк и SUMMARIZECOLUMNS ри ен ет авто ати еск
роверк
с ествовани к стол а из одно та ли , даже если агрега и возвра ает 0
Важно также учитывать особенности работы механизма автоматической
проверки существования применительно к функции
. Фактически функция
добавляет в итоговый результат строки,
которые были удалены функцией
по причине наличия
пустых значений. При этом функция
не возвращает в результирующий набор строки, исключенные в результате применения автоматической проверки существования к столбцам из одной таблицы. Так что
в результате выполнения следующего запроса будет возвращен тот же набор
данных, показанный на рис. 13.38, что и в предыдущем примере:
DEFINE
MEASURE Sales[Sales Amount] =
SUMX (
Sales;
1
Создание за росов
473
Sales[Quantity] * Sales[Net Price]
)
EVALUATE
ADDMISSINGITEMS (
'Product'[Category];
'Product'[Subcategory];
SUMMARIZECOLUMNS (
'Product'[Category];
'Product'[Subcategory];
"Sales"; [Sales Amount] + 0
);
'Product'[Category];
'Product'[Subcategory]
)
ORDER BY
'Product'[Category];
'Product'[Subcategory]
О механизме автоматической проверки существования важно помнить
всегда, когда вы используете функцию
. В то же время
поведение функции
отличается. Эта функция обязательно требует
указать в качестве параметра таблицу, которую будет использовать как мост
(bridge), соединяющий столбцы. Этот мост обеспечивает поведение, схожее
с автоматической проверкой существования. Например, в следующем фрагменте кода мы получим комбинацию из категорий товаров и годов, по которым есть движения в таблице
. Результат выполнения этого запроса показан на рис. 13.39:
EVALUATE
SUMMARIZE (
Sales;
'Product'[Category];
'Date'[Calendar Year]
)
Рис 13 39
нк и SUMMARIZE возвра ает
ко ина и категори товаров и годов,
о котор
ли родажи
Причина того, что в таблице выводятся только сочетания столбцов с продажами, заключается в том, что функция
использует таблицу
474
1
Создание за росов
в качестве отправной точки при выполнении группировки. Таким образом,
категории товаров и годы, на которые нет ссылок в таблице
, автоматически исключаются из результатов. Так что даже если результаты и получатся
одинаковыми при выполнении функций
и
,
получены они будут совершенно разными способами.
Кроме того, стоит помнить об особенностях выполнения запросов в разных
клиентских инструментах. Например, если пользователь выберет в Power BI
категорию товара и год, не включив при этом меру, отчет выдаст список существующих комбинаций этих столбцов в таблице
. И причина не в том,
что здесь был применен механизм автоматической проверки существования.
Просто Power BI добавляет свои правила к существующей логике DAX. В результате простой отчет с годом и категорией товаров превращается в сложный
запрос, показанный ниже:
EVALUATE
TOPN (
501;
SELECTCOLUMNS (
KEEPFILTERS (
FILTER (
KEEPFILTERS (
SUMMARIZECOLUMNS (
'Date'[Calendar Year];
'Product'[Category];
"CountRowsSales"; CALCULATE ( COUNTROWS ( 'Sales' ) )
)
);
OR (
NOT ( ISBLANK ( 'Date'[Calendar Year] ) );
NOT ( ISBLANK ( 'Product'[Category] ) )
)
)
);
"'Date'[Calendar Year]"; 'Date'[Calendar Year];
"'Product'[Category]"; 'Product'[Category]
);
'Date'[Calendar Year]; 1;
'Product'[Category]; 1
)
Подсвеченная строка показывает, что Power BI добавляет скрытый расчет
количества строк в таблице
. А поскольку функция
удаляет из набора все строки с пустыми агрегациями, это, по сути, имитирует
применение механизма автоматической проверки существования при сочетании столбцов из одной таблицы.
Power BI добавляет этот скрытый расчет только в случае отсутствия мер
в отчете, используя при этом таблицу, с которой все таблицы в функции
объединены связью «один ко многим». Как только в запросе
появится мера, Power BI удалит этот скрытый расчет и будет опираться на значение добавленной меры, а не на количество строк в таблице
.
1
Создание за росов
475
Чаще всего поведение функций
и
является интуитивно понятным. Однако в сложных сценариях, например при
наличии связей типа «многие ко многим», результаты могут оказаться неожиданными. В этом коротком разделе мы лишь поверхностно познакомились с механизмом автоматической проверки существования в DAX. Более подробное описание этой особенности с примерами сложных сценариев можно
найти в статье Understanding DAX Auto-Exist («Понимание механизма автоматической проверки существования в DAX») по адресу:
/
articles/understanding-dax-auto-exist/. В этой статье также показаны случаи, когда этот механизм становится причиной появления неожиданных результатов
в отчете.
Закл чение
В данной главе мы познакомились с функциями, полезными при написании
запросов. Помните, что все эти функции, за исключением
и
, можно применять и при написании мер. Но для
того чтобы писать действительно сложные запросы, вам понадобится опыт сочетания всех этих функций.
Вот самые важные моменты, которые мы рассмотрели в главе:
„ некоторые функции DAX более полезны при применении в запросах.
А есть и такие, которые в большинстве случаев применяются даже не разработчиками в процессе написания выражений, а самими клиентскими
инструментами при создании запросов. Но знать об этих функциях все
же необходимо. Когда-нибудь вам придется столкнуться с ними в чужом
коде, и базовые знания в этот момент не помешают;
„ запросы начинаются с ключевого слова
. Используя эту инструкцию, вы можете определять переменные и меры, область видимости которых будет ограничена конкретным запросом;
„ инструкцию
нельзя использовать для создания вычисляемых
таблиц. Они создаются при помощи выражений. Таким образом, при написании запроса для вычисляемой таблицы вы не можете создавать локальные меры и столбцы;
„ функция
полезна для группировки данных и зачастую используется совместно с функцией
;
„ функция
является поистине универсальной. Ее
мощь может пригодиться при написании действительно сложных запросов – недаром она активно используется инструментом Power BI. В то же
время функция
не может быть использована при
преобразовании контекста. Это серьезно ограничивает ее применение
в мерах;
„ функция
полезна при извлечении лучших (или худших) представителей в той или иной классификации;
„ функция
реализует в DAX логику инструкции OUTER APPLY
из языка SQL. Она применима, когда вам необходимо построить таблицу
476
1
Создание за росов
с двумя наборами столбцов: первый выступает в качестве фильтра, а значения во втором напрямую зависят от первого;
„ остальные функции из этого раздела в основном используются различными инструментами при создании запросов.
Также стоит понимать, что все табличные функции, описанные в предыдущих главах, могут быть использованы и при написании запросов. Так что
инструментарий создания запросов на языке DAX отнюдь не ограничен функциями, продемонстрированными в данной главе.
ГЛ А В А 14
Продвинутые концепции
языка DAX
До этого момента мы рассказали вам все, что знаем сами, об основах DAX, его
фундаментальных принципах, базирующихся на контексте строки, контексте
фильтра и преобразовании контекста. В предыдущих главах мы не раз упоминали загадочную главу 14, в которой вы будете посвящены в тайны языка
DAX. Должно быть, вам захочется прочитать эту главу несколько раз, чтобы как
следует усвоить написанное. По опыту можем сказать, что первое прочтение
может вызвать у разработчика вопрос вроде «Почему же это все так сложно?».
Но, изучив концепции, описанные в данной главе, вы начнете понимать, что
у многих трудностей, с которыми вы сталкиваетесь при изучении языка, есть
один общий знаменатель. И как только вы осмыслите это, все станет гораздо
проще.
Ранее мы говорили, что в этой главе вы сможете выйти на качественно новый уровень. И если каждую главу книги рассматривать как очередной уровень
в игре, то сейчас вам предстоит сразиться с боссом! В самом деле, концепции
расширенных таблиц и неявных контекстов фильтра – не самые простые темы
для усвоения. Но когда вы разберетесь, что к чему, то сможете взглянуть на
весь пройденный ранее материал по-новому. Мы бы даже порекомендовали
вам перечитать книгу с начала после окончания этой главы. Второе прочтение
позволит вам докопаться до сути того, что могло ускользнуть от вашего понимания при первом ознакомлении. Мы понимаем, что решение прочитать
книгу повторно требует немалых усилий. Но мы лишь обещали, что эта книга
поможет вам стать настоящим гуру в мире DAX. Никто не говорил, что это будет легко…
Знакомство с расширенными таблицами
Первая и наиболее важная концепция, которую вам предстоит освоить в данной главе, – это рас иренн е та ли
(expanded tables). В DAX каждая таблица имеет свою расширенную версию. Эта версия включает в себя все родные
столбцы таблицы плюс все столбцы из таблиц, находящихся на стороне «один»
в связях типа «многие к одному» с исходной таблицей.
Рассмотрим модель данных, представленную на рис. 14.1.
Расширение таблиц происходит со стороны «многие» к стороне «один». Таким образом, чтобы построить расширенную таблицу, мы начинаем двигаться
478
1
родвин т е кон е
ии з ка
от конкретной таблицы и добавляем столбцы из всех остальных таблиц, связанных с текущей и находящихся в этих связях на стороне «один». Например,
таблицы
и
объединены связью «многие к одному», поэтому расширенная версия таблицы
будет включать в себя все столбцы из таблицы
. В то же время расширенная версия таблицы
не будет
содержать никаких дополнительных столбцов. И правда, единственной связанной с ней таблицей является
, но она находится в этой
связи на стороне «многие». Таким образом, расширение может происходить
только от таблицы
к
, но не наоборот.
Рис 14 1 Модель данн
дл о исани кон е
ии рас иренн
та ли
Расширение таблиц не ограничивается одним уровнем связей. Например, от
таблицы
мы можем легко добраться до таблицы
, проходя
при этом исключительно по связям типа «многие к одному». Таким образом,
расширенная таблица
будет включать в себя столбцы из таблиц
,
и
. А поскольку таблица
также связана
ис
, в ее расширенную версию войдут и все столбцы из календаря. Иными
словами, расширенная таблица
включает в себя всю модель данных.
Но таблице
мы уделим чуть больше внимания. Фактически она может
быть отфильтрована при помощи таблицы
, поскольку связь между этими
двумя таблицами обозначена в модели данных как двунаправленная. Но при
этом она имеет тип «один ко многим», а не «многие к одному». Таким образом,
расширенная версия таблицы
будет включать только свои столбцы, несмотря на возможность осуществления фильтрации со стороны таблиц
,
,
и
. Дело в том, что механизм фильтрации и механизм расширения таблиц никак не связаны друг с другом. Двунаправленная фильтрация инициируется в коде DAX с использованием совершенно иного механизма, описание которого выходит за рамки данной главы.
Особенности распространения двунаправленных фильтров мы будем более
подробно обсуждать в главе 15.
1
родвин т е кон е
ии з ка
479
Повторив те же действия по расширению таблиц, которые мы проделали
с
, с остальными таблицами в модели данных, мы получим полное их описание, представленное в табл. 14.1.
АБЛИ А 14 1
ас иренн е версии та ли
аблица
Date
Sales
Product
Расширенная версия
Date
се та ли в одели данн
,
,
,
В модели данных могут присутствовать связи трех разных типов: «один
к одному», «один ко многим» и «многие ко многим». И правило здесь простое: расширение таблиц всегда выполняется к стороне «один». Следующие
простые примеры помогут вам лучше разобраться с этой концепцией. Представьте, что у вас есть модель данных, показанная на рис. 14.2. С точки зрения
моделирования она – далеко не идеал, но в образовательных целях вполне
сгодится.
Рис 14 2
то одели данн
дв на равленн е
о е св зи
один к одно
и
ногие ко ноги
Мы намеренно использовали такие сложные связи в этом примере. В таблице
содержится по одной строке для
, так что для
каждой категории в этой таблице будет несколько строк, а столбец ProductCategoryKey будет содержать неуникальные значения. Обе связи при этом
помечены в модели данных как двунаправленные. Связь между таблицами
и
имеет тип «один к одному», а связь между
и
– «многие ко многим», также именуемый сла ой св
(weak relationship). Но правило для всех одно: расширение таблиц выполняется только к стороне «один» вне зависимости от того, с какой стороны оно
началось.
Соответственно, в представленной модели данных таблицы
и
будут взаимно расширяться, и их расширенные версии будут абсолютно идентичными. В то же время таблицы
и
не расширяют друг друга, поскольку обе они находятся в связи на стороне «многие».
В таких случаях расширения таблиц не происходит. Когда обе таблицы расположены в связи на стороне «многие», эта связь автоматически становится слабой. Это не значит, что такой способ соединения таблиц обладает какими-то
480
1
родвин т е кон е
ии з ка
слабостями или недостатками – слабые связи, как и двунаправленная фильтрация, работают и выполняют свои задачи, просто они никак не связаны с расширением таблиц.
Понимание концепции расширенных таблиц важно само по себе. Кроме того,
оно помогает лучше усвоить принципы распространения контекста фильтра
в формулах DAX. Если фильтр применен к столбцу, все расширенные таблицы,
содержащие в себе этот столбец, также будут отфильтрованы. Это утверждение
требует дополнительных пояснений.
Мы представили концепцию расширенных таблиц модели данных, показанной на рис. 14.1, в виде диаграммы, изображенной на рис. 14.3.
Рис 14 3
редставление одели данн
рас иренн та ли
в виде диагра
ро ает виз ализа и
В строках диаграммы перечислены все столбцы, присутствующие в нашей
модели данных, а в столбцах – таблицы. Заметьте, что некоторые названия
столбцов присутствуют на схеме дважды. Дублирование наименований лишь
отражает тот факт, что в модели данных допустимо использовать одинаковые
имена столбцов в разных таблицах. Также мы закрасили области применения столбцов на пересечении с таблицами, чтобы можно было легко отличить
собственные столбцы таблиц от столбцов их расширений. У нас есть два типа
столбцов:
„ ро
е столб
(native columns) представляют собой столбцы, принадлежащие исходной таблице, и на пересечениях диаграммы отмечены
темно-серым цветом;
„ с
а
е столб
(related columns) – это столбцы, добавленные в расширенную версию исходной таблицы по связям, – на диаграмме отмечены светло-серым цветом.
Такая диаграмма помогает определить, какие таблицы фильтруются по
определенному столбцу. Например, в следующей мере используется функция
для применения фильтра по столбцу
:
RedSales :=
CALCULATE (
1
родвин т е кон е
ии з ка
481
SUM ( Sales[Quantity] );
'Product'[Color] = "Red"
)
Мы можем использовать нашу диаграмму для поиска таблиц, содержащих
столбец
. Глядя на рис. 14.4, можно легко заметить, что наш выбор
затрагивает две таблицы:
и
.
Рис 14 4
одсветка строки с вето товара озвол ет о ределить,
какие та ли
д т от ильтрован
Можно использовать эту же диаграмму и для проверки того, как контекст
фильтра распространяется по связям. После наложения фильтра на любой
столбец, находящийся в связи на стороне «один», все остальные таблицы,
в расширенные версии которых входит этот столбец, также будут отфильтрованы. В этот список включаются все таблицы, находящиеся в соответствующих
связях на стороне «многие».
Если мыслить категориями расширенных таблиц, будет гораздо легче понять принципы распространения контекста фильтра в целом. Фактически
контекст фил тра рас ростран етс на все рас иренн е та ли
содержа
ие фил труем е стол
. Рассуждая таким образом, вам больше не нужно
учитывать наличие связей между таблицами. Расширение таблиц выполняется
по связям. После расширения таблицы связь, по сути, включается в саму расширенную таблицу, и думать о ней больше не нужно.
Примечание
а етьте, то ильтр о стол
с вето товара также рас ростран етс
и на та ли Date, от исто те ни ески атри т Color не в одит в рас иренн верси
то та ли
десь
и ее дело с
екто дв на равленно ильтра ии ажно отетить, то ильтр, становленн
о ол Color, достигает та ли Date не ерез рас иренн е та ли
н тренне
за скает с е иальн код дл в олнени ильтра ии
осредство дв на равленн св зе , тогда как ильтра и ри о о и рас иренн
та ли ос ествл етс авто ати ески азни а ри то скр та от осторонни глаз, но
ее о ень важно они ать о же са ое касаетс и сла
св зе они ис ольз т не расиренн е та ли , а сво е аниз
ильтра ии
482
1
родвин т е кон е
ии з ка
Функция RELATED
Ссылаясь на таблицу в DAX, вы всегда имеете дело именно с расширенной таблицей. С точки зрения семантики функция
не выполняет никаких
действий. Вместо этого она всего лишь обеспечивает доступ извне к связанным столбцам расширенной таблицы. В следующем примере столбец
принадлежит расширенной таблице
, и функция
просто предоставляет доступ к нему посредством контекста строки в таблице
:
SUMX (
Sales;
Sales[Quantity] * RELATED ( 'Product'[Unit Price] )
)
В отношении расширенных таблиц очень важно понимать, что расширение
происходит в момент определения таблиц, а не в момент обращения к ним.
Рассмотрим следующий запрос:
EVALUATE
VAR SalesA =
CALCULATETABLE (
Sales;
USERELATIONSHIP ( Sales[Order Date]; 'Date'[Date] )
)
VAR SalesB =
CALCULATETABLE (
Sales;
USERELATIONSHIP ( Sales[Delivery Date]; 'Date'[Date] )
)
RETURN
GENERATE (
VALUES ( 'Date'[Calendar Year] );
VAR CurrentYear = 'Date'[Calendar Year]
RETURN
ROW (
"Sales From A"; COUNTROWS (
FILTER (
SalesA;
RELATED ( 'Date'[Calendar Year] ) = CurrentYear
)
);
"Sales From B"; COUNTROWS (
FILTER (
SalesB;
RELATED ( 'Date'[Calendar Year] ) = CurrentYear
)
)
)
)
В переменных
и
хранятся две копии таблицы
, вычисленные в контекстах фильтра с двумя разными активными связями: в таблице
1
родвин т е кон е
ии з ка
483
используется связь между столбцами
и
,ав
– межи
.
После вычисления этих двух переменных функция
начинает осуществлять итерации по годам, создавая при этом два дополнительных столбца,
которые будут заполнены значениями, равными количеству строк в таблицах
и
с применением фильтра по текущему году к столбцу RELATED
( 'Date'[Calendar Year] ). Мы вынуждены были написать такой витиеватый
запрос, чтобы избежать возникновения преобразования контекста, и как раз
в функции
подобного преобразования не происходит.
В целом же вопрос здесь состоит в понимании того, что именно происходит
во время вызова функций
в строках, выделенных жирным шрифтом.
Если не мыслить категориями расширенных таблиц, ответить на этот вопрос
будет затруднительно. На момент вызова функции
активной является связь между столбцами
и
, поскольку обе переменные уже были вычислены ранее и оба модификатора
выполнили свою работу. В то же время переменные
и
хранят
расширенные таблицы, и их расширения были сделаны в момент активности
разных связей. А поскольку функция
дает доступ только к столбцу
в расширенной таблице, значит, во время осуществления итераций по таблице
мы получим доступ посредством этой функции к году заказа, а при проходе по
– к году поставки.
Разница заметна в выводе, показанном на рис. 14.5. Если бы не расширенные таблицы, мы могли бы ожидать одинаковых значений по годам в обоих
столбцах.
ду
Рис 14 5
дв
рас ета
ильтр
тс разн е год
Использование функции RELATED в вычисляемых
столбцах
Как мы уже говорили, функция RELATED позволяет получить доступ к связанным столбцам в расширенной таблице. В то же время расширение таблицы
происходит в момент определения таблицы, а не обращения к ней. Это приводит к тому, что изменение связей в вычисляемых столбцах становится проблематичным.
Взгляните на фрагмент модели данных, изображенный на рис. 14.6, с двумя
связями между таблицами
и
.
484
1
родвин т е кон е
ии з ка
Рис 14 6 Межд та ли а и Sales и Date есть две св зи,
но в кажд
о ент вре ени активна только одна из ни
Допустим, нам понадобилось создать вычисляемый столбец в таблице
для проверки того, была ли осуществлена поставка товара в том же квартале, в котором был сделан заказ. В таблице
есть столбец
, который может быть использован для выполнения сравнения. К сожалению, получить квартал, в котором была осуществлена поставка, будет гораздо труднее, чем извлечь квартал с датой заказа.
Если использовать в выражении вычисляемого столбца конструкцию RELATED
( 'Date'[Calendar Year Quarter] ), будет применена активная в данный момент
связь, что позволит нам получить квартал, в котором был создан заказ. И даже
следующая формула не позволит нам использовать другую связь для вычисления:
Sales[DeliveryQuarter] =
CALCULATE (
RELATED ( 'Date'[Calendar Year Quarter] );
USERELATIONSHIP (
Sales[Delivery Date];
'Date'[Date]
)
)
Здесь есть сразу несколько проблем. Первая из них заключается в том, что
функция
удаляет контекст строки, но при этом она нужна, чтобы изменить активную связь для вызова функции
. Следовательно,
функцию
нельзя использовать здесь в качестве аргумента фильтра
функции
, поскольку она требует наличия контекста строки. Есть
и еще одна любопытная проблема. Дело в том, что функция
в любом
случае не сработала бы, потому что контекст строки для вычисляемого столбца
создается в момент определения таблицы. Этот контекст генерируется автоматически, так что таблица всегда расширяется с использованием связи, определенной по умолчанию.
Более того, не существует идеального решения данной проблемы. Лучше
всего здесь будет воспользоваться функцией
. Это функция поиска, извлекающая значение из таблицы, в которой определенный столбец соответствует переданному посредством параметра значению. Квартал поставки
по заказу можно вычислить следующим образом:
Sales[DeliveryQuarter] =
LOOKUPVALUE (
1
родвин т е кон е
ии з ка
485
'Date'[Calendar Year Quarter];
'Date'[Date];
Sales[Delivery Date]
-- Возвращает квартал года,
-- где значение в столбце Date[Date]
-- соответствует значению Sales[Delivery Date]
)
Функция
ищет значение по точному совпадению. Более сложные условия в ней недопустимы. Если необходимо, можно написать выражение
посложнее с использованием функции
. Более того, поскольку здесь
мы используем функцию
в вычисляемом столбце, контекст
фильтра будет пустым. Но даже если бы контекст фильтра активно фильтровал
модель данных, функция
проигнорировала бы это. Эта функция
всегда выполняет поиск строки в таблице, игнорируя любые контексты фильтра. Кроме того, в качестве последнего аргумента в функцию
можно передать значение по умолчанию, которое будет использоваться при
отсутствии совпадений.
Разница между фильтрами по таблице
и фильтрами по столбцу
В DAX существует огромная разница между фильтрацией по таблице и по
столбцу. Табличные фильтры являются мощнейшим инструментом в руках
опытных разработчиков, но при неправильном использовании могут приводить к неожиданным результатам. И начнем мы как раз со сценария, в котором
применение табличного фильтра дает неверные расчеты. Позже в этом разделе мы приведем пример правильного использования табличных фильтров
в сложных сценариях.
Новичок в языке DAX, скорее всего, скажет, что эти два выражения дадут
одинаковый результат:
CALCULATE (
[Sales Amount];
Sales[Quantity] > 1
)
CALCULATE (
[Sales Amount];
FILTER (
Sales;
Sales[Quantity] > 1
)
)
На самом деле между этими двумя формулами есть огромная разница.
В первой фильтр применяется к столбцу, а во второй – к таблице. Несмотря
на то что в некоторых сценариях эти выражения выдадут одинаковые цифры,
в целом они серьезно отличаются. И чтобы продемонстрировать эти отличия,
объединим данные выражения в одном запросе:
486
1
родвин т е кон е
ии з ка
EVALUATE
ADDCOLUMNS (
VALUES ( 'Product'[Brand] );
"FilterCol"; CALCULATE (
[Sales Amount];
Sales[Quantity] > 1
);
"FilterTab"; CALCULATE (
[Sales Amount];
FILTER (
Sales;
Sales[Quantity] > 1
)
)
)
Результат выполнения этого запроса, показанный на рис. 14.7, удивит
многих.
В столбце
показаны правильные значения, тогда как в соседнем
цифры повторяются и составляют итог по всем брендам. И чтобы понять, как это получилось, необходимо снова включить мышление категориями
расширенных таблиц.
Рассмотрим в деталях поведение вычисления
. Аргумент фильтра
функции
осуществляет итерации по таблице
, возвращая при
этом строки, в которых количество проданных товаров превышает единицу.
Результатом вызова функции
является ограниченный набор строк из
таблицы
. А мы помним, что в DAX любое обращение к таблице подразумевает ее расширенную версию. Поскольку таблица
объединена связью
с таблицей
, ее расширенная версия будет также включать все столбцы
из таблицы
. И в числе прочих здесь будет и столбец
.
Рис 14 7
ерво стол е оказан равильн е рез льтат ,
а во второ зна ени овтор тс и равн о е итог о стол
Аргументы фильтра функции
вычисляются в исходном контексте фильтра, игнорируя результат будущего преобразования контекста.
1
родвин т е кон е
ии з ка
487
В то же время фильтр по столбцу
вступает в силу уже после операции
преобразования контекста. Следовательно, результирующий набор на выходе
функции
будет включать значения по всем брендам, соответствующим
строкам с количеством проданных товаров, большим единицы. В самом деле,
во время осуществления итераций функцией
никаких фильтров на
столбец
наложено не было.
При создании нового контекста фильтра функция
выполняет
два последовательных шага:
1) осуществляет преобразование контекста;
2 применяет аргументы фильтра.
Таким образом, аргументы фильтра могут переопределить результат преобразования контекста. Поскольку функция
выполняет итерации
по брендам, преобразование контекста в каждой строке должно приводить
к установке фильтра по конкретному бренду. Но так как в результирующем
наборе на выходе функции
также содержится информация о бренде,
она будет обладать большим приоритетом по сравнению с итогами преобразования контекста. В результате мы в каждой строке будем видеть итоговое
значение по мере
для всех транзакций с количеством проданных
товаров, превышающим единицу, вне зависимости от выбранного бренда.
Использование табличных фильтров существенно затруднено из-за особенностей функционирования расширенных таблиц. Применяя фильтр к таблице,
мы, по сути, применяем его к ее расширенной версии, что может приводить
к неожиданным побочным эффектам. Отсюда следует золотое правило: пытайтесь избегать применения табличных фильтров, когда это возможно. Работая со столбцами, вы сможете писать более простые и понятные выражения,
тогда как фильтрация целых таблиц может доставлять проблемы.
Примечание асс отренн здесь ри ер не ожет ть с легкость ри енен в ера ,
о ределенн
в одели данн
ри ина в то , то ера всегда не вно в исл етс
вн три нк ии CALCULATE, в олн
е рео разование контекста асс отри след
ер
Multiple Sales :=
CALCULATE (
[Sales Amount];
FILTER (
Sales;
Sales[Quantity] > 1
)
)
ри ис ользовании ер в от ете воз ожн
ри ерно так
за рос на з ке
EVALUATE
ADDCOLUMNS (
VALUES ( 'Product'[Brand] );
"FilterTabMeasure"; [Multiple Sales]
)
488
1
родвин т е кон е
ии з ка
ог
в гл деть
ас ирение та ли
риведет к тако
рео разовани за роса
EVALUATE
ADDCOLUMNS (
VALUES ( 'Product'[Brand] );
"FilterTabMeasure"; CALCULATE (
CALCULATE (
[Sales Amount];
FILTER (
Sales;
Sales[Quantity] > 1
)
)
)
)
зов ерво
нк ии CALCULATE риведет к рео разовани контекста, то овли ет
на о а арг ента второ
нк ии CALCULATE, вкл а
нк и FILTER даже если итогов
рез льтат дет соответствовать на е ис одно стол
FilterCol, ис ользование та ли ного ильтра не ре енно скажетс на роизводительности в ислени ак
то на ва совет ис ольз те ильтр о стол а всегда, когда то воз ожно
Использование табличных фильтров в мерах
В предыдущем разделе мы продемонстрировали первый пример, в котором
понимание концепции расширенных таблиц помогло нам правильно интерпретировать результат. Но есть и другие сценарии, в которых могут пригодиться расширенные таблицы. К тому же в предыдущих главах мы не раз использовали эту концепцию, просто не объясняли толком, что к чему.
Например, в главе 5 при описании процедуры удаления фильтров, наложенных на модель данных, мы использовали следующий код в отчете, к которому
был применен срез по категориям товаров:
Pct All Sales :=
VAR CurrentCategorySales =
[Sales Amount]
VAR AllSales =
CALCULATE (
[Sales Amount];
ALL ( Sales )
)
VAR Result =
DIVIDE (
CurrentCategorySales;
AllSales
)
RETURN
Result
Почему же выражение ALL ( Sales ) удаляет все наложенные фильтры? Если
не мыслить категориями расширенных таблиц, функция
здесь должна
1
родвин т е кон е
ии з ка
489
удалять фильтры только с таблицы
, оставляя все остальные фильтры нетронутыми. Но по факту применение функции
к таблице
привело
к фильтрации всей расширенной таблицы продаж. А поскольку расширение
таблицы
распространяется на все связанные таблицы, включая
,
,
,
и остальные, эта операция позволила снять все наложенные фильтры с модели данных.
В большинстве случаев такое поведение является желаемым и интуитивно
понятным. Но это не умаляет важности досконального понимания тонкостей
поведения расширенных таблиц. Без полноценного осознания этой концепции нетрудно допустить неточности в ключевых вычислениях. В следующем
примере мы покажем, как простое вычисление может стать настоящей проблемой при отсутствии понимания принципов расширения таблиц. Мы узнаем,
почему не стоит применять табличные фильтры с функцией CALCULATE, если
только разработчик не собирается намеренно воспользоваться преимуществами побочных эффектов от расширения таблиц. Об этом мы расскажем в следующих разделах.
Посмотрите на отчет, представленный на рис. 14.8. Слева у нас есть срез по
столбцу
, а в матрице отображаются данные по продажам по подкатегориям товаров с указанием процента относительно итогового показателя.
Рис 14 8
стол е Pct оказана дол
о одкатегори относительно итога
родаж
Поскольку в столбце с процентной долей продаж нам необходимо разделить
текущее значение меры
на ее значение по всем подкатегориям
в рамках выбранной категории, первым (и неверным) решением может быть
следующий код:
Pct :=
DIVIDE (
[Sales Amount];
CALCULATE (
[Sales Amount];
ALL ( 'Product Subcategory' )
)
)
Идея была в том, чтобы удалить фильтр со столбца
и оставить по столбцу
, чтобы получить корректный результат. Однако результат, показанный на рис. 14.9, оказался не таким, как мы ожидали.
490
1
родвин т е кон е
ии з ка
Рис 14 9
ерва реализа и
ер Pct дала неверн
рас ет ро ентов
Проблема в том, что выражение ALL ( 'Product Subcategory' ) удаляет фильтры
с расширенной версии таблицы
, а не с исходной. Таблица
расширяется за счет
. Следовательно, функция
удаляет фильтры не только с таблицы
, но и с
. Таким образом, в знаменателе у нас будет рассчитан итог по всем категориям товаров, что приведет к неправильному расчету процентов.
Здесь есть сразу несколько решений, и мы вычислим требуемую нам меру,
используя разные подходы. К примеру, в следующей мере
мы
рассчитаем процент по выбранным подкатегориям в сравнении со связанными категориями. Удалив фильтр с расширенной таблицы
,
мы затем восстанавливаем фильтр по таблице
путем вызова
функции
:
Pct Of Categories :=
DIVIDE (
[Sales Amount];
CALCULATE (
[Sales Amount];
ALL ( 'Product Subcategory' );
VALUES ( 'Product Category' )
)
)
Еще один способ вычислить правильные проценты мы покажем на примере меры
, в которой используем функцию
без
аргументов. Функция
восстанавливает контекст фильтра по срезам, находящимся за пределами визуального элемента, при этом разработчику
даже не надо беспокоиться о расширенных таблицах:
Pct Of Visual Total :=
DIVIDE (
[Sales Amount];
CALCULATE (
[Sales Amount];
ALLSELECTED ()
)
)
Использование функции
, конечно, привлекает своей простотой. Но далее в разделе мы познакомим вас с не вн ми контекстами фил тра
1
родвин т е кон е
ии з ка
491
(shadow filter contexts), что позволит вам в полной мере разобраться в принципах работы загадочной функции
. Эта функция невероятно
мощная, но в сложных выражениях использовать ее следует с большой осторожностью.
Еще один способ решить нашу проблему заключается в использовании
функции
, которая поможет сопоставить значения по выбранным
подкатегориям со значениями по категориям, отмеченным в срезе:
Pct :=
DIVIDE (
[Sales Amount];
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Product Subcategory'; 'Product Category' )
)
)
В этой формуле используется вариант синтаксиса функции
, который мы не применяли ранее, а именно с передачей в качестве параметров
двух таблиц, а не таблицы и списка столбцов.
Функция
удаляет все фильтры с переданной таблицы, за исключением столбцов, указанных в параметрах со второго и далее. Этот список
может включать в себя любые столбцы (или таблицы), принадлежащие расширенной версии таблицы, переданной в качестве первого аргумента. А поскольку расширенная таблица
полностью включает в себя
таблицу
, результат вычисления будет правильным. Фактически это выражение удалит фильтры с полной расширенной таблицы
, не затронув при этом столбцы расширенной таблицы
.
Здесь стоит отметить, что расширенные таблицы могут доставлять немало
проблем, если ваша модель данных неправильно денормализована. На протяжении большей части книги мы используем версию модели данных
,
в которой
и
хранятся просто как столбцы в таблице
, а не как обособленные таблицы. Иначе говоря, мы денормализовали категорию и подкатегорию в таблице товаров. В правильно денормализованной
модели данных процесс расширения таблиц между
и
проходит
более естественным образом. Как часто это бывает, чем лучше спроектирована
модель данных, тем проще будет код на DAX.
Введение в активные связи
Еще один важный аспект, который стоит учитывать при работе с расширенными таблицами, – это активность связей в модели данных. Обычно в моделях
с большим количеством связей между таблицами легко запутаться. И в этом
разделе мы покажем пример, когда присутствие множества связей является
настоящей проблемой.
Представьте, что вам необходимо рассчитать меры
и
. Эти меры легко вычислить, воспользовавшись полезной функцией
. В следующем примере показан вариант создания этих мер:
492
1
родвин т е кон е
ии з ка
Sales Amount :=
SUMX (
Sales;
Sales[Quantity] * Sales[Net Price]
)
Delivered Amount :=
CALCULATE (
[Sales Amount];
USERELATIONSHIP ( Sales[Delivery Date]; 'Date'[Date] )
)
Результат вычисления этих мер показан на рис. 14.10.
Рис 14 10
ри создании ер Sales Amount и Delivered Amount
ли ис ользован разн е св зи
А вот вариант написания меры
причине использования табличного фильтра:
, который не сработает по
Delivered Amount =
CALCULATE (
[Sales Amount];
CALCULATETABLE (
Sales;
USERELATIONSHIP ( Sales[Delivery Date]; 'Date'[Date] )
)
)
Эта мера выдает пустые значения, что видно по рис. 14.11.
Рис 14 11
с ользование та ли ного ильтра
в ере Delivered Amount ривело к в вод
ст зна ени
Разберемся, почему мера стала выдавать пустые значения. Наверняка здесь
не обошлось без участия расширенных таблиц. И правда, на выходе функции
1
родвин т е кон е
ии з ка
493
мы получили расширенную версию таблицы
, в которой
помимо остальных таблиц включена также таблица
. В момент вычисления функции
активной связью является связь по столбцу
. Следовательно, функция
вернет все заказы, поставка которых была осуществлена в указанном году, в виде расширенной таблицы.
Когда функция
используется в качестве аргумента фильтра другой функции
, ее результат фильтрует таблицы
и
посредством расширенной таблицы
, которая использует связь между
столбцами
и
. Но по окончании действия функции
в силу вновь вступает связь по умолчанию по столбцам
и
. Таким образом, фильтр по датам вновь устанавливается на дату заказа, а не дату поставки. Иными словами, таблица с датами поставки используется для фильтрации дат заказа. После этой операции
видимыми будут только те строки, где значения столбцов
и
равны, то есть поставка произошла в день заказа. Но в модели данных нет таких строк, а значит, и результат будет пустым.
Для еще лучшего разъяснения этой концепции представьте, что в таблице
у нас есть всего пара строк, как показано в табл. 14.2.
АБЛИ А 14 2
rder Date
12 1 200
01 0 200
ри ер та ли
Sales с дв
строка и
Delivery Date
01 0 200
01 10 200
Quantity
100
200
Если в отчете выбран 2008 год, функция
вернет расширенную версию таблицы
, которая, помимо прочих, будет включать столбцы,
показанные в табл. 14.3.
АБЛИ А 14 3
ез льтато в зова нк ии CALCULATETABLE вл етс
рас иренна та ли а Sales, вкл а а оле Date[Date], ри ед ее о св зи
с Sales[Delivery Date]
rder Date
12 1 200
01 0 200
Delivery Date
01 0 200
01 10 200
Quantity
100
200
Date
01 0 200
01 10 200
При применении в качестве фильтра столбец
использует активную связь, то есть связь между столбцами
и
. В этот
момент расширенная таблица
выглядит так, как показано в табл. 14.4.
АБЛИ А 14 4
ас иренна та ли а Sales с ис ользование св зи о
ол ани
о стол
Sales[Order Date]
rder Date
12 1 200
01 0 200
494
Delivery Date
01 0 200
01 10 200
1
родвин т е кон е
Quantity
100
200
ии з ка
Date
12 1 200
01 0 200
В результате строки, видимые в табл. 14.3, пытаются отфильтровать таблицу,
представленную в табл. 14.4. Но для всех строк значения в столбце
будут
разными, а значит, все строки из результирующей таблицы будут удалены.
В общем случае в итоговый набор попадут только строки с одинаковыми
значениями в столбцах
и
, поскольку значения поля
будут идентичными в обеих расширенных таблицах, построенных с использованием разных связей. В этот раз фильтрующий эффект
исходил от активной связи. Изменение связи внутри
оказывает локальное действие только внутри этой функции, но за ее пределами активной
вновь становится связь по умолчанию.
Как обычно, отметим, что такое поведение является вполне корректным.
Понять его непросто, но это не делает его неправильным. Повторим, что использования табличных фильтров необходимо избегать, когда это возможно.
Применяя их, можно получать корректные результаты, а можно погрязнуть
в чрезмерно сложном и непредсказуемом сценарии. Более того, меры, использующие фильтры по столбцам вместо табличных фильтров, прекрасно работают и легки для восприятия.
Разработчики, не следующие золотому правилу не использовать табличные
фильтры, обычно платят дважды: первый раз – пытаясь понять, как работают
их фильтры, а второй – осознавая чудовищное падение производительности
их расчетов.
Разница между расширением таблиц и фильтрацией
Как мы уже упоминали ранее, расширение таблиц выполняется по связям от
стороны «многие» к стороне «один». Рассмотрим модель данных, представленную на рис. 14.12, в которой мы намеренно сделали все связи двунаправленными.
Рис 14 12
се св зи в то
одели данн
1
дв на равленн е
родвин т е кон е
ии з ка
495
Несмотря на двунаправленный характер связи между таблицами
и
, расширенная версия таблицы с товарами будет включать
подкатегории, тогда как расширенная версия подкатегорий не будет включать
товары.
Движок DAX выполняет специальный фильтрующий код в выражениях, создавая эффект обоюдного расширения таблиц в модели данных. Похожее поведение наблюдается при использовании функции
. Так что в большинстве случаев меры будут работать так, как если бы участвующие в связи
таблицы расширялись в обоих направлениях. Но не стоит забывать, что в действительности расширения таблиц по связям в сторону «многие» не происходит.
Эта разница оказывается важна при использовании функций
или
. Если разработчик применяет функцию
для выполнения группировки таблицы, основываясь на столбцах связанной с ней таблицы, он должен воспользоваться столбцами расширенной версии таблицы. Например, следующее выражение с применением функции
работает
прекрасно:
EVALUATE
SUMMARIZE (
'Product';
'Product Subcategory'[Subcategory]
)
В то же время обратная операция с попыткой сгруппировать категории товаров по цвету потерпит неудачу:
EVALUATE
SUMMARIZE (
'Product Subcategory';
'Product'[Color]
)
Ошибка с описанием «Столбец с именем
, указанный в функции
, не присутствует в исходной таблице» говорит сама за себя – расширенная версия таблицы
действительно не содержит столбец
. Как и
, функция
также работает исключительно со столбцами, принадлежащими расширенной таблице.
Так же, как и в предыдущем примере, таблицу
нельзя сгруппировать по
столбцам, принадлежащим другим таблицам, несмотря на то что она объединена с
двунаправленной связью:
EVALUATE
SUMMARIZE ( 'Date'; 'Product'[Color] )
Есть только один случай взаимного расширения двух таблиц – когда они
объединены посредством связи типа «один к одному». В таком варианте расширенные версии обеих таблиц будут включать друг друга. Причина в том, что
связь «один к одному» делает две таблицы семантически идентичными: каждая строка в одной таблице напрямую связана со строкой в другой таблице.
496
1
родвин т е кон е
ии з ка
Следовательно, можно представить эти таблицы как одну общую, разбитую на
два набора столбцов.
Преобразование контекста в расширенных таблицах
Расширение таблиц также оказывает влияние на преобразование контекста.
Контекст строки преобразуется в эквивалентный контекст фильтра для всех
столбцов, принадлежащих расширенной версии таблицы. Рассмотрим следующий запрос, возвращающий категорию товара, используя при этом две разные
техники: функцию
в контексте строки и функцию
с преобразованием контекста:
EVALUATE
SELECTCOLUMNS (
'Product';
"Product Key"; 'Product'[ProductKey];
"Product Name"; 'Product'[Product Name];
"Category RELATED"; RELATED ( 'Product Category'[Category] );
"Category Context Transition"; CALCULATE (
SELECTEDVALUE ( 'Product Category'[Category] )
)
)
ORDER BY [Product Key]
Результат запроса будет включать два столбца
и
с идентичными значениями, что видно по рис. 14.13.
Рис 14 13
атегори товара в дв
стол
а
ол ена разн
и с осо а и
В столбце
отображается название категории выбранного в текущей строке товара. Это значение может быть извлечено при помощи функции
, если нам доступен контекст строки в таблице
.
При вычислении значения столбца
используется совершенно иная техника. Здесь мы имеем дело с преобразованием контекста,
выполненным функцией
. В результате преобразованный контекст
фильтрует не только таблицу
, но и связанные с ней таблицы
и
по выбранному товару. А поскольку в этот момент
в контексте фильтра находится только одна строка из таблицы
,
функция
вернет единственное значение столбца
из
этой таблицы.
И хотя этот побочный эффект расширенных таблиц хорошо известен, не
стоит полагаться на такое поведение при извлечении связанных столбцов. Не1
родвин т е кон е
ии з ка
497
смотря на то что результат может оказаться правильным, есть вероятность, что
производительность пострадает. Решение с преобразованием контекста может
оказаться менее эффективным в случае оперирования со множеством строк
в таблице
. Операция преобразования контекста обычно обходится недешево. Далее в данной книге мы еще не раз упомянем, что для оптимизации
кода желательно снизить количество преобразований контекста. Таким образом, в этом конкретном случае лучшим способом обращения к категории товара будет использование функции
. Так вы сможете избежать преобразования контекста, что необходимо для применения функции
.
Функция ALLSELECTED и неявные контексты
фильтра
– очень удобная и полезная функция, скрывающая в себе огромную ловушку. По нашему мнению, это самая сложная функция в языке DAX,
хотя выглядит она очень безобидно. В данном разделе мы посвятим вас во все
технические подробности реализации функции
, а также дадим
несколько советов по поводу того, когда стоит ее использовать, а когда лучше
обойтись без нее.
, как и любая другая функция из группы
, может быть
использована двумя способами: как табличная функция и как модификатор
функции
. И ее поведение в этих двух сценариях будет серьезно отличаться. Более того, это единственная функция в языке DAX, прибегающая
к помощи так называемых не вн контекстов фил тра (shadow filter contexts).
Сначала мы изучим поведение функции
, затем расскажем, что
из себя представляют загадочные контексты фильтра, недаром именуемые неявными, а в конце раздела дадим пару советов по оптимальному использованию функции
.
Функцию
можно использовать чисто интуитивно. Рассмотрим
следующий отчет, представленный на рис. 14.14.
Рис 14 14
от ете оказан
родажи о разли н
ренда с ро ентн
и дол
и
В отчете используется срез для осуществления фильтрации по брендам.
В строках показывается сумма продажи по каждому бренду и процент от об498
1
родвин т е кон е
ии з ка
щих продаж по всем выбранным брендам. Формула для вычисления меры
с процентами очень проста:
Pct :=
DIVIDE (
[Sales Amount];
CALCULATE (
[Sales Amount];
ALLSELECTED ( 'Product'[Brand] )
)
)
Интуитивно можно понять, что функция
вернет значения
брендов, выбранных за пределами текущего элемента визуализации, – в нашем случае это все бренды от Adventure Works до Proseware включительно. Но
ведь Power BI посылает запрос движку DAX, который понятия не имеет о наших
элементах визуализации.
Как же DAX узнает о том, что выбрано в срезе, а что – в самой матрице? Ответ прост – он об этом и не узнает. Функция
на самом деле и не
возвращает значения столбцов (или таблиц), отфильтрованных за пределами
нашей визуализации. Она выполняет совершенно иное действие, в качестве
побочного эффекта возвращая в большинстве случаев тот результат, который
нам и нужен. Правильное определение функции
включает в себя
два утверждения:
„ будучи использованной в качестве табличной функции,
возвращает набор значений, видимый в последнем неявном контексте
фильтра;
„ при использовании функции
в качестве модификатора
функции
она восстанавливает последний неявный контекст
фильтра по переданному параметру.
Конечно, эти утверждения нуждаются в дополнительном пояснении.
Знакомство с неявными контекстами фильтра
Прежде чем знакомиться с неявными контекстами фильтра, полезно будет
взглянуть на запрос, сгенерированный Power BI для вывода результата, показанного на рис. 14.14:
DEFINE
VAR __DS0FilterTable =
TREATAS (
{
"Adventure Works";
"Contoso";
"Fabrikam";
"Litware";
"Northwind Traders";
"Proseware"
};
1
родвин т е кон е
ии з ка
499
'Product'[Brand]
)
EVALUATE
TOPN (
502;
SUMMARIZECOLUMNS (
ROLLUPADDISSUBTOTAL (
'Product'[Brand];
"IsGrandTotalRowTotal"
);
__DS0FilterTable;
"Sales_Amount"; 'Sales'[Sales Amount];
"Pct"; 'Sales'[Pct]
);
[IsGrandTotalRowTotal]; 0;
'Product'[Brand]; 1
)
ORDER BY
[IsGrandTotalRowTotal] DESC;
'Product'[Brand]
Этот запрос проанализировать будет не так просто, и не столько из-за его
сложности, сколько по причине того, что он был автоматически сгенерирован
движком и в принципе не предназначен для чтения. Мы преобразовали его
в запрос, близкий по концепции к оригиналу, но более пригодный для анализа:
EVALUATE
VAR Brands =
FILTER (
ALL ( 'Product'[Brand] );
'Product'[Brand]
IN {
"Adventure Works";
"Contoso";
"Fabrikam";
"Litware";
"Northwind Traders";
"Proseware"
}
)
RETURN
CALCULATETABLE (
ADDCOLUMNS (
VALUES ( 'Product'[Brand] );
"Sales_Amount"; [Sales Amount];
"Pct"; [Pct]
);
Brands
)
Результат этого запроса очень похож на тот, что мы видели ранее, за исключением того, что здесь нет строки итогов. Вывод показан на рис. 14.15.
500
1
родвин т е кон е
ии з ка
Рис 14 15
ез льтат о ти тако же, как в ред д
за искл ение отс тстви итогов
е от ете,
Сделаем несколько важных замечаний по поводу этого запроса:
„ внешняя функция
создает контекст фильтра, состоящий из шести брендов;
„ функция
осуществляет итерации по шести брендам, видимым внутри
;
„ меры
и
вычисляются внутри итерации. Перед их вычислением происходит преобразование контекста, так что в контекст фильтра каждой из них включается только один текущий бренд;
„ мера
не изменяет контекст фильтра, тогда как мера
использует функцию
для модификации контекста фильтра;
„ после изменения контекста фильтра функцией
внутри
меры
измененный контекст будет вновь насчитывать шесть брендов
вместо одного текущего.
Последний пункт этого перечня наиболее важен для понимания того, что
из себя представляет неявный контекст фильтра и как на самом деле DAX использует функцию
. Ключом к происходящему является итератор
, проходящий по шести выбранным брендам, из которых в результате преобразования контекста видимым остается только один, и функции
необходимо каким-то образом восстановить контекст фильтра,
содержащий все шесть изначальных брендов.
Давайте более детально разберемся, как в действительности выполняется
запрос. Здесь – на третьем шаге – мы впервые встретимся с неявным контекстом фильтра.
1. Функция
создает контекст фильтра, состоящий из
шести брендов.
2. Функция
возвращает шесть видимых брендов функции
.
3. Будучи итератором, функция
создает неявный контекст
фильтра, содержащий результат выполнения функции
, непосредственно перед началом осуществления итераций:
неявный контекст фильтра похож на обычный, за исключением того,
что он создается неактивным и никак не влияет на вычисления;
активировать неявный контекст фильтра можно лишь при помощи
функции
, что мы скоро увидим. Пока достаточно будет
запомнить, что в неявном контексте фильтра находятся все шесть выбранных брендов;
1
родвин т е кон е
ии з ка
501
чтобы отличать обычный контекст фильтра от неявного, в данном разделе будем именовать его вн м (explicit filter context).
4. Во время итерации преобразование контекста выполняется для отдельно взятой строки. Таким образом, в результате этого преобразования
создается явный контекст фильтра, содержащий единственный бренд.
5. Когда при вычислении меры
движок DAX встречает функцию
, происходит следующее: у к и A S
D осста а лиает осле и
е
ко текст ильтра о столб у или таблие, ере а о
ка ест е ара етра, либо о се столб а , если
у к и ALLSELECTED
а а бе ара етро (поведение функции
без параметров описывается в следующем разделе). Поскольку в последнем неявном контексте фильтра содержится шесть выбранных брендов, все они вновь становятся видимыми.
Этот несложный пример позволил нам объяснить, что из себя представляет
неявный контекст фильтра. Предыдущий запрос демонстрирует, как функция
использует неявный контекст фильтра для извлечения контекста фильтра за пределами текущего элемента визуализации. Заметьте, что
в описании процесса мы ни разу не упомянули какие-либо визуализации, характерные для Power BI. Фактически движок DAX ничего не знает о том, какой
именно элемент используется для отображения информации. Он просто принимает запрос DAX, ничего более.
Чаще всего функция
извлекает правильный контекст фильтра.
Дело в том, что все элементы визуализации в Power BI и большинство элементов отображения в других инструментах генерируют похожие запросы. Эти
автоматически сгенерированные запросы всегда включают в себя итерационную функцию верхнего уровня, создающую неявный контекст фильтра по отображаемым элементам. Именно поэтому создается ощущение, что функция
восстанавливает контекст фильтра, находящийся за пределами
текущей визуализации.
Теперь, когда вы лучше узнали предназначение и принципы работы функции
, пришло время рассказать об условиях, необходимых для ее
корректного использования:
„ запрос должен содержать итерационную функцию. Без итератора не будет создан неявный контекст фильтра, а значит, функция
отработает неправильно;
„ если перед вызовом функции
стоит несколько итераторов,
она восстановит только последний из созданных контекстов фильтра.
Иными словами, использование функции
внутри итератора, выполняющегося в коде меры, с большой долей вероятности будет
приводить к непредсказуемым результатам, поскольку мера почти всегда
вычисляется в рамках другого итератора в запросе, созданном клиентским инструментом;
„ если столбцы, переданные функции
в качестве параметров, не содержатся в неявном контексте фильтра, функция ничего не будет делать.
Как видите, функция
оказалась далеко не такой простой, как
представлялось. Разработчики в основном предпочитают использовать ее для
502
1
родвин т е кон е
ии з ка
извлечения внешнего контекста фильтра в визуализации. Ранее в данной книге мы уже использовали функцию
с этой целью, но каждый раз
дважды проверяли, чтобы были выполнены все необходимые условия для ее
корректной работы, пусть и не объясняли досконально, что происходит на самом деле.
В целом семантика функции
напрямую связана с извлечением неявных контекстов фильтра, и, по счастливой случайности (на самом деле
именно так и было задумано), ее применение ведет к извлечению контекста
фильтра, созданного за пределами текущего элемента визуализации.
Опытные разработчики прекрасно понимают, как работает функция
, и используют ее только в тех сценариях, где это допустимо. Злоупотребление данной функцией в условиях, непригодных для ее корректного использования, может привести к нежелательным результатам, и винить в этом
нужно будет разработчика, а не какую-то функцию
…
Золотое правило использования функции
звучит так: у ки A S
D о ет б ть ис оль о а а л и ле е и
е
е о
ко текста ильтра только ра ка
ер , ре ста ле о
атри е
или ру о
ле е те и уали а ии. Разработчик может не ждать корректного поведения от меры с
внутри, использованной в рамках итерационной функции, что мы продемонстрируем в следующих разделах. Именно поэтому мы как разработчики DAX всегда предпочитаем следовать одному
простому правилу: если в коде меры содержится функция
, эту
меру ни в коем случае не стоит вызывать из другой меры. Дело в том, что в цепочку вызовов мер легко может закрасться итерационная функция, в рамках
которой может быть вызвана мера, включающая в себя функцию
.
ALLSELECTED возвра ает строки из итераций
Чтобы еще лучше продемонстрировать поведение функции
, немного изменим предыдущий запрос. Вместо того чтобы осуществлять итерации по выражению VALUES ( Product[Brand] ), пройдемся при помощи функции
по ALL ( Product[Brand] ):
EVALUATE
VAR Brands =
FILTER (
ALL ( 'Product'[Brand] );
'Product'[Brand]
IN {
"Adventure Works";
"Contoso";
"Fabrikam";
"Litware";
"Northwind Traders";
"Proseware"
}
)
RETURN
CALCULATETABLE (
1
родвин т е кон е
ии з ка
503
ADDCOLUMNS (
ALL ( 'Product'[Brand] );
"Sales_Amount"; [Sales Amount];
"Pct"; [Pct]
);
Brands
)
В этом обновленном сценарии неявный контекст фильтра, созданный функцией
до начала итераций, содержит все имеющиеся бренды,
а не только выбранные. Таким образом, во время вычисления меры
функция
восстановит этот неявный контекст фильтра, тем самым сделав видимыми все бренды. Результат, показанный на рис. 14.16, отличается от
того, что мы видели на рис. 14.15.
Рис 14 16
нк и ALLSELECTED восстанавливает зна ени
из тек его икла итера и , а не ред д и контекст ильтра
Как видите, все бренды, как и ожидалось, стали видимыми, но при этом цифры по ним отличаются, несмотря на то что наши изменения вычислений не касались. Поведение функции
в этом сценарии вполне корректное.
Разработчикам может показаться, что она ведет себя несколько неожиданно,
поскольку при вычислении меры
полностью игнорируется контекст фильтра, созданный в переменной
. Но функция
делает ровно
то, что ей предписано, – возвращает последний неявный контекст фильтра,
а в нашей версии кода в этом контексте будут находиться все бренды, а не только выбранные. Функция
создает неявный контекст фильтра со
строками, по которым будут осуществляться итерации, и здесь это все бренды,
присутствующие в модели данных.
Если вам необходимо восстановить предыдущий контекст фильтра, одной
функции
будет недостаточно. Вам придется использовать модификатор
функции
, который призван восстанавливать
предыдущий контекст фильтра. Интересно, какой результат выдаст формула
с использованием этого модификатора:
504
1
родвин т е кон е
ии з ка
EVALUATE
VAR Brands =
FILTER (
ALL ( 'Product'[Brand] );
'Product'[Brand]
IN {
"Adventure Works";
"Contoso";
"Fabrikam";
"Litware";
"Northwind Traders";
"Proseware"
}
)
RETURN
CALCULATETABLE (
ADDCOLUMNS (
KEEPFILTERS ( ALL ( 'Product'[Brand] ) );
"Sales_Amount"; [Sales Amount];
"Pct"; [Pct]
);
Brands
)
Будучи использованной в качестве модификатора итерационной функции,
не изменяет результат таблицы, по которой осуществляется проход. Вместо этого она дает команду итератору применить
как
неявный модификатор функции
в процессе преобразования контекста при осуществлении итераций. В результате функция
возвращает
все бренды, и в созданном неявном контексте фильтра также будут находиться
все бренды. Но при преобразовании контекста будет сохранен предыдущий
фильтр, примененный внешней функцией
с переменной
. Таким образом, запрос вернет все бренды компании, но значения будут
рассчитаны только для тех из них, которые были выбраны в фильтре, что видно
по рис. 14.17.
Рис 14 17
нк и ALLSELECTED
с ис ользование
оди икатора
KEEPFILTERS дала совсе др го
рез льтат с оль и коли ество
ст
еек
1
родвин т е кон е
ии з ка
505
Применение функции ALLSELECTED без параметров
Как ясно из названия,
принадлежит к группе функций
.
А значит, при использовании в качестве модификатора функции
она будет удалять ранее установленные фильтры. Если столбец, переданный
функции в качестве параметра, присутствует в каком-либо из неявных контекстов фильтра, будет произведено восстановление последнего из неявных
контекстов по этому столбцу. Если неявных контекстов фильтра нет, функция
не будет выполнять никаких действий.
Будучи использованной в качестве модификатора
, функция
, как и
, также может не принимать параметров. В этом случае она восстановит последний неявный контекст фильтра по любому столбцу.
Но это произойдет только в том случае, если столбец присутствует в каком-либо неявном контексте. Если столбец отфильтрован только при помощи явных
фильтров, ситуация с фильтрацией по нему не изменится.
Функции группы ALL
По причине повышенной сложности функций группы
в данном разделе
мы представим вам сводный обзор по ним. Все функции из этой группы ведут
себя по-разному, и для овладения ими в полной мере потребуется немало опыта. Здесь же мы познакомим вас с основными концепциями применения этих
полезных функций.
В группу
входят следующие функции:
,
,
,
и
. Все перечисленные функции могут
быть использованы как в качестве обычных табличных функций, так и в роли
модификаторов функции
. При этом в первом случае понять их
поведение бывает намного легче. Будучи использованными в качестве модификаторов
, эти функции могут производить неожиданные результаты, поскольку в процессе выполнения они удаляют ранее наложенные
фильтры.
В табл. 14.5 мы свели воедино краткое описание функций из группы
.
В оставшейся части раздела мы поговорим о каждой из них более подробно.
Колонка «Табличная функция» в табл. 14.5 относится к сценариям, в которых
функции группы
используются внутри выражений DAX, тогда как в колонке «Модификатор функции
» приведены принципы их работы
при использовании в качестве функций верхнего уровня в аргументах фильтра
функции
.
Еще одно существенное различие между двумя типами использования этих
функций заключается в том, что когда вы извлекаете результат их работы через инструкцию
, в него включаются только столбцы из исходной
таблицы, а не из ее расширенной версии. При этом все внутренние вычисления, включая преобразование контекста, всегда используют соответствующие
расширенные таблицы. В следующих примерах мы покажем разные варианты
использования функции
. Эти же концепции могут быть применены к любой функции из группы
.
506
1
родвин т е кон е
ии з ка
АБЛИ А 14 5 Сводн
Функция
ALL
о зор
нк и из гр
абличная функция
озвра ает никальн е
зна ени из стол а или
та ли
озвра ает никальн е
зна ени из та ли ,
игнорир
ри то
ильтр о некотор
стол а рас иренно
та ли
озвра ает никальн е
зна ени из стол а
или та ли , игнорир
ст е строки,
до авленн е дл
неде ствительн св зе
ALL*
Модификатор функции CALCULATE
дал ет л
е ранее наложенн е
ильтр со стол ов или рас иренн
та ли
икогда не до авл ет ильтр ,
а только дал ет и
дал ет ранее наложенн е ильтр
с рас иренно та ли , за искл ение
стол ов или та ли , ереданн
в ка естве арг ентов
дал ет л
е ранее наложенн е
ильтр со стол ов или рас иренн
та ли
о и о того, до авл ет ильтр,
дал
и
ст е строки аки
о разо , даже если ильтров в та ли е
нет, нк и до авл ет один ильтр
к контекст
осстанавливает оследни не вн
озвра ает никальн е
зна ени из стол а или контекст ильтра в та ли а или
стол а , если таково и еетс
на е
та ли , как они видн
не в олн ет никаки де стви
нк и
в оследне созданно
всегда до авл ет ильтр , даже если
не вно контексте
тек и ильтр вкл ает все зна ени
ильтра
едост на дл
дал ет ранее наложенн е ильтр
ис ользовани в ка естве с рас иренно та ли , вкл а
та ли , дост н е на р
или
та ли но
нк ии
косвенно ерез дв на равленн
ильтра и
нк и
никогда не до авл ет ильтр , а только
дал ет и
Давайте сначала используем
ции:
в качестве стандартной табличной функ-
SUMX (
ALL ( Sales );
-- ALL – это табличная функция
Sales[Quantity] * Sales[Net Price]
)
В следующем примере мы напишем сразу две формулы, использующие итерации. В обоих случаях обращение к мере
выполняет преобразование контекста применительно к расширенной таблице. При использовании
в качестве табличной функции
возвращает всю расширенную таблицу.
FILTER (
Sales;
[Sales Amount] > 100
-- Преобразование контекста выполняется
-- по расширенной таблице
)
FILTER (
1
родвин т е кон е
ии з ка
507
ALL ( Sales );
-- ALL – табличная функция
[Sales Amount] > 100
-- Преобразование контекста все равно
-- выполняется по расширенной таблице
)
В следующем примере применим функцию
в качестве модификатора
функции
для удаления любых фильтров с расширенной версии
таблицы
:
CALCULATE (
[Sales Amount];
ALL ( Sales )
)
-- ALL – модификатор CALCULATE
Последний пример, хоть и будет очень похож на предыдущий, на самом деле
сильно отличается. Здесь функция
используется не в качестве модификатора
– вместо этого она применяется как аргумент функции
. В этом случае
будет вести себя как обычная табличная функция, возвращая целую расширенную таблицу
.
CALCULATE (
[Sales Amount];
FILTER ( ALL ( Sales ); Sales[Quantity] > 0 ) -- ALL – табличная функция
-- Контекст фильтра все равно принимает
-- в качестве фильтра расширенную таблицу
)
Далее мы приведем более детальное описание функций, входящих в группу
. Все они выглядят очень просто, но в действительности не так просты в использовании. В большинстве случаев они будут вести себя так, как вы
и предполагаете, но в пограничных ситуациях могут выдавать неожиданные
результаты. Бывает непросто запомнить все эти правила и особенности их поведения. Надеемся, табл. 14.5 еще не раз пригодится вам при использовании
функций из группы
.
Функция ALL
В качестве табличной функции
применять очень просто. Она возвращает
все уникальные значения по одному или нескольким столбцам либо по всей
таблице. При использовании внутри функции
в качестве модификатора
начинает вести себя как гипотетическая функция
.
Если какой-либо из столбцов включен в фильтр, функция удалит эту фильтрацию. Важно пояснить при этом, что если столбец отфильтрован при помощи
перекрестной фильтрации, функция
его не затронет. Данная функция удаляет только фильтры, установленные напрямую. Таким образом, использование выражения
в качестве модификатора функции
может оставить активным кросс-фильтр на столбце
, если
установлен фильтр на другом столбце таблицы
. Функция
оперирует расширенными таблицами. Именно поэтому выражение
удалит
фильтры со всех таблиц в модели данных: расширенная таблица
, как мы
508
1
родвин т е кон е
ии з ка
уже говорили, включает в себя всю модель данных. Применение функции
без аргументов удалит все фильтры со всей модели данных.
Функция ALLEXCEPT
Будучи использованной в качестве табличной функции,
возвращает все уникальные значения столбцов из таблицы, за исключением
столбцов, указанных в качестве аргументов. При использовании функции
в качестве фильтра в результат будет включена вся расширенная таблица.
Применение функции
как аргумента фильтра в
приведет к аналогичному поведению функции
, за исключением того, что
фильтры не будут удалены со столбцов, переданных в качестве аргументов.
Важно помнить, что использование функции
и связки
/
не равносильно. Функция
просто удаляет фильтры, тогда как
в сочетании
/
функция
удаляет фильтры, а
сохраняет
кросс-фильтрацию путем добавления нового фильтра. Это очень тонкое, но
существенное различие.
Функция ALL
BLA
R
При использовании в качестве табличной функции
ведет
себя как функция
, за исключением того, что не возвращает пустые строки, которые могут появиться из-за присутствия недействительных связей.
При этом функция
может возвращать пустые строки, если
они присутствуют в исходной таблице. Гарантированно будут удалены только те строки, которые были автоматически добавлены движком для устранения недействительных связей. Будучи примененной в качестве модификатора
, функция
заменяет все существующие фильтры
новыми, удаляющими пустые строки. Таким образом, по всем столбцам будет
выполняться фильтрация на наличие пустых значений.
Функция ALLSELECTED
В качестве табличной функции
возвращает значения из таблицы (или столбца) так, как они представлены в последнем созданном неявном
контексте фильтра. Как модификатор функции
,
восстанавливает последний неявный контекст фильтра по каждому столбцу. Если
столбцы присутствуют в разных неявных контекстах, функция восстанавливает последний контекст для каждого столбца.
Функция ALLCR SSFILTERED
Функция
может быть использована исключительно как
модификатор функции
, но не как самостоятельная табличная
функция. У этой функции есть только один аргумент, представляющий собой
таблицу. Функция
удаляет все фильтры с расширенной
таблицы (подобно функции
), а также со столбцов и таблиц, попавших под
1
родвин т е кон е
ии з ка
509
перекрестную фильтрацию из-за наличия двунаправленных связей, прямо
или косвенно объединяющих их с расширенной таблицей.
Использование привязки данных
В главе 10 мы познакомились с концепцией привязки данных и научились
контролировать ее при помощи функции
. В главах 12 и 13 мы также показали, как конкретные табличные функции способны манипулировать
привязкой данных результирующего набора. В этом разделе мы подведем итоги всего изученного материала по данной теме, а также уточним некоторые
нюансы, о которых не говорили ранее.
Представляем вам базовые правила работы концепции привязки данных:
„ каждый столбец в модели обладает своей уникальной привязкой данных;
„ когда модель фильтруется при помощи контекста фильтра, фактически
фильтрация распространяется на столбцы с той же привязкой данных,
что и у столбцов в текущем контексте фильтра;
„ поскольку фильтр является результатом таблицы, важно знать, как табличные функции могут влиять на привязку данных в результирующем
наборе:
в основном столбцы, используемые для осуществления группировки,
сохраняют в итоговом наборе свою привязку данных;
для столбцов с результатами агрегирования всегда создается новая
привязка данных;
для столбцов, созданных при помощи функций
и
,
также создается новая привязка;
столбцы, созданные функцией
, сохраняют свою
привязку данных в случае, если их выражение включает в себя только
ссылку на столбец. В остальных случаях будет создана новая привязка.
В следующем примере мы попытались сформировать таблицу, в которой для
каждого цвета должна быть выведена общая сумма продаж по товарам этого
цвета. Однако поскольку столбец
был создан функцией
, его
привязка данных не соответствует столбцу
из модели данных,
несмотря на одинаковое содержимое. Обратите внимание, что в нашем примере мы действовали пошагово: сначала создали столбец , а затем выбрали из
таблицы только его. Если бы в итоговой таблице оставались и другие столбцы,
результат был бы совсем иным.
DEFINE
MEASURE Sales[Sales Amount] =
SUMX ( Sales; Sales[Quantity] * Sales[Net Price] )
EVALUATE
VAR NonBlueColors =
FILTER (
ALL ( 'Product'[Color] );
'Product'[Color] <> "Blue"
510
1
родвин т е кон е
ии з ка
)
VAR AddC2 =
ADDCOLUMNS (
NonBlueColors;
"[C2]"; 'Product'[Color]
)
VAR SelectOnlyC2 =
SELECTCOLUMNS ( AddC2; "C2"; [C2] )
VAR Result =
ADDCOLUMNS ( SelectOnlyC2; "Sales Amount"; [Sales Amount] )
RETURN Result
ORDER BY [C2]
В итоге мы получили столбец с цветами, но мера
для каждого из
них выдает одинаковую сумму, равную итоговым продажам по таблице
.
Вывод запроса показан на рис. 14.18.
Рис 14 18
рив зка данн нового стол а C2
не сов адает с рив зко стол а Product[Color]
из одели
Для изменения привязки данных можно использовать функцию
.
В следующем варианте запроса мы устанавливаем привязку данных нового
столбца к столбцу
в модели, что позволяет функции
рассчитывать меру
с использованием преобразования контекста по столбцу
:
DEFINE
MEASURE Sales[Sales Amount] =
SUMX ( Sales; Sales[Quantity] * Sales[Net Price] )
EVALUATE
VAR NonBlueColors =
FILTER (
ALL ( 'Product'[Color] );
'Product'[Color] <> "Blue"
)
1
родвин т е кон е
ии з ка
511
VAR AddC2 =
ADDCOLUMNS (
NonBlueColors;
"[C2]"; 'Product'[Color]
)
VAR SelectOnlyC2 =
SELECTCOLUMNS ( AddC2; "C2"; [C2] )
VAR TreatAsColor =
TREATAS ( SelectOnlyC2; 'Product'[Color] )
VAR Result =
ADDCOLUMNS ( TreatAsColor; "Sales Amount"; [Sales Amount] )
RETURN Result
ORDER BY 'Product'[Color]
В качестве побочного эффекта функция
столбец
в
, что мы учли в инструкции
исправленного запроса показан на рис. 14.19.
также переименовала
. Результат выполнения
Рис 14 19 е ерь рив зка данн
в стол е Color соответств ет
рив зке стол а Product[Color]
Закл чение
В данной главе мы изучили две важные концепции: расширенные таблицы
и неявные контексты фильтра.
Расширенные таблицы являются фундаментом DAX. Вам может понадобиться какое-то время, чтобы начать мыслить категориями расширенных таблиц.
Но как только вы поймете все нюансы этой концепции, вам будет намного легче работать со связями. Разработчику приходится работать с расширенными
таблицами напрямую не так часто, но знать об их существовании нужно обязательно, ведь зачастую только они позволяют досконально понять, почему
результат того или иного выражения получился именно таким.
512
1
родвин т е кон е
ии з ка
В этом смысле неявные контексты фильтра сильно напоминают расширенные таблицы: эту концепцию нелегко понять и осознать, но иногда только она
способна пролить свет на то, почему мы получили те или иные цифры в отчете.
Без полного понимания неявных контекстов фильтра за написание сложных
мер с использованием функции
можно даже не браться.
Кроме того, обе концепции являются настолько сложными, что мы советуем вам избегать их использования там, где это возможно. В следующей главе
мы покажем несколько примеров, в которых расширенные таблицы придутся
очень кстати. Что касается неявных контекстов фильтра, то их использование
в коде не имеет смысла. Скорее, это техническое средство языка DAX, позволяющее разработчикам рассчитывать итоги на уровне элемента визуализации.
Избежать задействования расширенных таблиц можно путем использования в аргументах фильтра функции
фильтров по столбцам, а не по
таблицам. Так вы значительно упростите свой код. Чаще всего от использования расширенных таблиц можно отказаться, если речь не идет о какой-нибудь
специфической мере.
Чтобы избежать использования неявных контекстов фильтра, следует в первую очередь отказаться от применения мер с функцией
внутри
итераторов. Единственной итерацией перед вызовом функции
должна быть итерация, созданная движком запросов, – в большинстве случаев это Power BI. Обращение к мерам, использующим функцию
,
внутри итераций – верный путь к излишнему усложнению ваших вычислений.
Если вы будете следовать этим двум советам, ваш код на языке DAX станет
простым и понятным. Конечно, эксперты способны оценить по достоинству
сложность кода, но в то же время они прекрасно понимают, когда стоит избегать излишнего нагромождения. Отказ от использования табличных фильтров
и мер с
внутри итераций не сделает вас менее образованным
в глазах окружающих. Более того, таким образом вы еще на шаг приблизитесь
к экспертам, желающим, чтобы их код работал как можно более гладко и безотказно.
ГЛ А В А 15
Углубленное изучение связей
На этом этапе мы раскрыли вам все секреты DAX. В предыдущих главах мы рассказали вам все, что было возможно, о синтаксисе и функциональности языка.
Но впереди еще длинный путь. Вам предстоит прочитать еще две главы, посвященные непосредственно языку DAX, после чего мы погрузимся в вопросы
оптимизации. В следующей главе мы представим вам несколько идей относительно продвинутых вычислений с использованием DAX, а в этой расскажем,
как при помощи DAX создавать более сложные типы связей между таблицами. К таким типам связей относятся вычисляемые физические и виртуальные
связи. Затем мы подробнее поговорим о различных видах физических связей,
включая связи типа «один к одному», «один ко многим» и «многие ко многим». Каждый из этих типов связей достоин отдельного рассмотрения. Также
мы посвятим достаточно времени вопросам неоднозначности моделей данных. В моделях данных могут встречаться неоднозначности, и вы должны быть
в курсе этого, чтобы правильно и вовремя реагировать.
В конце данной главы мы посвятим время теме, больше касающейся моделирования данных, нежели самого языка DAX. Речь идет о связях на разных
уровнях гранулярности. При проектировании сложных моделей данных для
расчета бюджета и продаж вы неминуемо столкнетесь с таблицами с разными
уровнями гранулярности и должны уметь грамотно с ними обращаться.
Реализация вычисляемых физических связей
Первый набор связей, который мы рассмотрим, – это в исл ем е фи и еские
св и (calculated physical relationships). Бывают случаи, когда традиционными
связями в модели данных воспользоваться не получается. Например, у вас может просто не быть ключевого столбца в одной из таблиц, или вам необходимо
использовать в связи поле, вычисленное по сложной формуле. В таких сценариях лучше всего будет прибегнуть к созданию связей с использованием вычисляемых столбцов. В результате у вас будет полноценная физическая связь,
и единственным ее отличием от обычной связи будет то, что ключевым столбцом в ней будет выступать вычисляемый столбец, а не физический столбец
в модели данных.
Создание связей по нескольким столбцам
Табличная модель данных предусматривает возможность создания связей
между таблицами исключительно по одному столбцу. Такая модель не позво514
1
гл ленное из ение св зе
ляет использовать несколько столбцов на одной стороне связи. И все же связи
по нескольким столбцам могут оказаться очень полезными в моделях данных,
не подверженных изменениям. Вот два способа создания таких связей:
„ определить вычисляемый столбец, содержащий сочетание двух или более ключей, и использовать его в качестве нового ключа для связи;
„ денормализовать столбцы целевой таблицы (находящейся в связи «один
ко многим» на стороне «один») при помощи функции
.
Представьте, что мы вводим в модели данных Contoso акцию «Товары дня»,
суть которой состоит в том, что в разные дни мы будем давать определенную
скидку на конкретные товары. Соответствующая модель данных изображена
на рис. 15.1.
Рис 15 1
а ли
Discounts нео
оди о о
единить с та ли е Sales о дв
стол
а
В таблице
содержится три столбца:
,
и
. Если
разработчику понадобятся эти данные для вычисления общей суммы скидок,
он столкнется с серьезной проблемой, состоящей в том, что для каждой отдельной продажи скидка будет зависеть от значений столбцов
и
. Получается, что между таблицами
и
невозможно создать
связь: для этого нам потребовалось бы объединить таблицы по двум столбцам,
а DAX позволяет создавать связи только по одному полю.
Первым способом решения задачи может быть создание сопоставимых вычисляемых столбцов в обеих таблицах, сочетающих значения из двух других
столбцов:
Sales[DiscountKey] =
COMBINEVALUES (
"-",
Sales[Order Date],
Sales[ProductKey]
)
1
гл ленное из ение св зе
515
Discounts[DiscountKey] =
COMBINEVALUES(
"-",
Discounts[Date],
Discounts[ProductKey]
)
В этих вычисляемых столбцах мы прибегли к помощи функции
. Функция
принимает в качестве параметров разделитель и список выражений, которые будут объединены вместе как строки
с указанным разделителем. Для выполнения этой операции можно было воспользоваться обычной конкатенацией строк, но функция
обладает определенными преимуществами. Эта функция оказывается чрезвычайно полезной при создании связей на основании вычисляемых столбцов,
если в модели данных используется режим DirectQuery. Функция
предполагает – но не утверждает, – что если входные данные различны, то и строки на выходе будут отличаться. С учетом этого предположения
использование функции
при создании вычисляемых столбцов для последующего объединения таблиц в режиме DirectQuery позволяет
генерировать наиболее оптимальные условия во время выполнения запроса.
Примечание Более одро но о етода о ти иза ии COMBINEVALUES сов естно с ита те о адрес https://www.sqlbi.com/articles/using-combinevalues-to-optimize-directquery-performance/.
После создания столбцов можно приступать к объединению таблиц при помощи связи. В моделях данных связи на основании вычисляемых столбцов работают так же безопасно, как и обычные связи.
Это достаточно прямолинейное решение, и оно годится в большинстве случаев. Однако существуют сценарии, когда такой вариант будет неоптимальным из-за необходимости создавать два дополнительных столбца с большим
количеством значений. Как вы узнаете из последующих глав по оптимизации,
это может негативно сказаться на размере модели данных и ее производительности.
Второй способ решения этой задачи состоит в использовании функции
. Применив эту функцию, вы можете денормализовать скидку
в таблице
, определив для нее новый вычисляемый столбец:
Sales[Discount] =
LOOKUPVALUE (
Discounts[Discount];
Discounts[ProductKey]; Sales[ProductKey];
Discounts[Date];
Sales[Order Date]
)
В этом случае вам не придется создавать новую связь. Вместо этого вы просто денормализуете значение скидки из таблицы
в таблице
при
помощи функции поиска.
516
1
гл ленное из ение св зе
Оба варианта вполне применимы, и выбор между ними зависит от конкретной задачи. Если от этой связки вам необходимо получить только одно значение скидки, то способ с денормализацией столбца подойдет вам лучше по
причине своей простоты. К тому же он не так требователен к памяти компьютера, поскольку мы фактически создаем только один вычисляемый столбец
с меньшим количеством уникальных значений по сравнению с двумя столбцами в первом варианте.
Но если в таблице
содержится несколько столбцов, информация из
которых может понадобиться нам в расчетах, то придется создавать не одно
денормализованное поле в таблице
для хранения всех нужных нам данных. Это приведет к пустой трате памяти и увеличению времени, требуемого
для обработки данных. В этом случае лучше подойдет вариант со связями посредством составных ключей.
Показанный в данном разделе пример был очень важен, поскольку с его помощью мы смогли продемонстрировать возможность создавать связи на основе вычисляемых столбцов. Таким образом, пользователь может наладить
любые необходимые ему связи в модели данных, при условии что он сумеет
вычислить и материализовать требуемые составные ключи в вычисляемых
столбцах. В следующем примере мы покажем, как создавать связи на основе
статических диапазонов. Расширив эту концепцию, вы сможете устанавливать
самые разные связи между таблицами.
Реализация связей на основе диапазонов
Чтобы вы лучше усвоили пользу от вычисляемых физических связей, мы рассмотрим сценарий со стати еской сегмента ией (static segmentation) товаров
по цене. Цена за единицу товара – очень вариативный показатель со множеством возможных значений, и анализ на основе конкретных значений цены нам
ничего не даст. В таких случаях обычно применяется техника разбиения цен по
сегментам с использованием конфигурационной таблицы, подобной той, что
показана на рис. 15.2.
Рис 15 2
а ли а Configuration дл
ранени диа азонов ен на товар
Как и в предыдущем примере, мы не можем создать прямую связь между
таблицами
и
. Причина в том, что в конфигурационной таблице ключ зависит от целого диапазона значений, а в DAX связи на основе целого спектра значений не поддерживаются. Мы могли бы вычислить значение
ключа в таблице
при помощи вложенных операторов , но в этом случае
нам пришлось бы включать значения из конфигурационной таблицы прямо
в формулу, как показано ниже, чего нам, конечно, хотелось бы избежать:
1
гл ленное из ение св зе
517
Sales[PriceRangeKey] =
SWITCH (
TRUE (),
Sales[Net Price] <= 10; 1;
Sales[Net Price] <= 30; 2;
Sales[Net Price] <= 80; 3;
Sales[Net Price] <= 150; 4;
5
)
Приемлемое решение не должно основываться на указании конкретных ценовых диапазонов внутри формулы. Вместо этого код должен быть напрямую
связан с конфигурационной таблицей, чтобы при ее изменении обновлялась
вся модель.
В таком случае лучше всего будет денормализовать в таблице
названия
диапазонов при помощи вычисляемого столбца. Шаблон кода в этом случае будет похож на предыдущий, но сама формула будет отличаться, поскольку воспользоваться функцией
здесь мы не сможем:
Sales[PriceRange] =
VAR FilterPriceRanges =
FILTER (
PriceRanges;
AND (
PriceRanges[MinPrice] <= Sales[Net Price];
PriceRanges[MaxPrice] > Sales[Net Price]
)
)
VAR Result =
CALCULATE (
VALUES ( PriceRanges[PriceRange] );
FilterPriceRanges
)
RETURN
Result
Заметьте, что функция
здесь используется для извлечения одинарного значения. В общем виде эта функция возвращает таблицу, но, как мы рассказывали в главе 3, если в этой таблице всего одна строка и один столбец,
то она может быть преобразована в скалярную величину, в случае если того
требует выражение.
Функция
здесь всегда будет возвращать одну строку из конфигурационной таблицы, так что на вход функции
гарантированно будет подаваться таблица из одной строки и одного столбца. Соответственно, результатом вычисления функции
будет описание ценового диапазона
товара из текущей строки в таблице
. Если конфигурационная таблица
построена корректно, это выражение всегда будет возвращать одно значение.
В случае если диапазоны будут пересекаться или, наоборот, иметь разрывы,
функция
может вернуть несколько строк, что приведет к ошибке всего
выражения.
518
1
гл ленное из ение св зе
Показанная техника позволяет выполнять денормализацию описаний ценовых диапазонов товаров в таблице
. Если пойти еще дальше, можно денормализовать не описание, а ключ, чтобы можно было на основании этого вычисляемого столбца построить физическую связь. Но на этом шаге нужно быть
особенно внимательными. Простого изменения названия столбца
для извлечения ключа будет достаточно, но для построения связи этого не хватит. В следующем фрагменте кода добавлена обработка возникновения ошибки и возврат пустого значения в этом случае:
Sales[PriceRangeKey] =
VAR FilterPriceRanges =
FILTER (
PriceRanges;
AND (
PriceRanges[MinPrice] <= Sales[Net Price];
PriceRanges[MaxPrice] > Sales[Net Price]
)
)
VAR Result =
CALCULATE (
IFERROR (
VALUES ( PriceRanges[PriceRangeKey] );
BLANK ()
);
FilterPriceRanges
)
RETURN
Result
Теперь в вычисляемом столбце
всегда будет находиться корректное значение. К сожалению, при попытке создать связь между таблицами
и
по столбцу
возникает ошибка, связанная
с циклической зависимостью. При связывании вычисляемых столбцов и таблиц такая ошибка появляется довольно часто.
В нашем случае исправить ситуацию довольно легко: достаточно использовать функцию
вместо
в выделенной жирным шрифтом строке формулы. После этого связь будет создана успешно. Результат вычисления
этой формулы показан на рис. 15.3.
Рис 15 3 е ерь
легко оже делать срез
о еново диа азон товаров
Замена функции
на
позволила нам избавиться от ошибки,
связанной с циклической зависимостью. Механизмы, лежащие в основе этих
1
гл ленное из ение св зе
519
зависимостей, достаточно сложны. В следующем разделе мы подробно расскажем о причинах, приводящих к возникновению циклических зависимостей
при создании связей между вычисляемыми столбцами и таблицами, а также
поясним, почему использование функции
решило проблему.
иклические зависимости
в вычисляемых физических связях
В предыдущем примере мы создали вычисляемый столбец, на основании которого было выполнено объединение двух таблиц. Это привело к появлению
ошибки, связанной с циклической зависимостью. Работая с вычисляемыми
физическими связями, вы будете достаточно часто сталкиваться с такого рода
ошибками, так что нелишним будет потратить немного времени, чтобы разобраться с источником их возникновения. А заодно научитесь их избегать.
Давайте восстановим формулу вычисляемого столбца в сокращенном виде:
Sales[PriceRangeKey] =
CALCULATE (
VALUES ( PriceRanges[PriceRangeKey] );
FILTER (
PriceRanges;
AND (
PriceRanges[MinPrice] <= Sales[Net Price];
PriceRanges[MaxPrice] > Sales[Net Price]
)
)
)
Значения в вычисляемом столбце
зависят от содержимого конфигурационной таблицы
. Если диапазоны в
изменятся, должны пересчитаться и значения вычисляемого столбца в таблице
.
В этой формуле есть сразу несколько упоминаний таблицы
, так что
зависимость столбца от нее абсолютно очевидна. И гораздо менее очевидно то,
что создание связи между этим столбцом и таблицей
само по себе
создает обратную зависимость.
В главе 3 мы упоминали, что движок DAX автоматически создает пустую
строку на стороне «один», если связь недействительная. Так что если таблица находится в связи на стороне «один», ее содержимое напрямую зависит от
правильности связи, которая, в свою очередь, зависит от значений в столбце,
используемом для создания этой связи.
В нашем сценарии при создании связи между таблицами
и
на основании столбца
в таблице
могут как
присутствовать пустые строки, так и нет – в зависимости от значений столбца
. Иначе говоря, когда значение в столбце
изменяется, содержимое таблицы
также может поменяться. В то же
время при изменении таблицы
может потребоваться обновление
столбца
, даже если добавленная пустая строка и не будет
использоваться. Именно это и является причиной обнаружения движком DAX
520
1
гл ленное из ение св зе
циклической зависимости. Человеческому глазу трудно усмотреть здесь несоответствия, но алгоритмы DAX легко их обнаруживают.
Если бы инженеры-разработчики языка DAX не позаботились об этой проблеме, мы бы не смогли создавать связи на основе вычисляемых столбцов. Но
они снабдили движок особой логикой, которая может пригодиться в подобных
случаях.
Результатом этих усилий стали два вида зависимостей, которые присутствуют
в DAX: ависимост о формуле (formula dependency) и ависимост о уст м
строкам (blank row dependency). Для нашего примера справедливо следующее:
„ столбец
зависит от таблицы
как по формуле (в которой есть ссылки на таблицу
), так и по пустым
строкам (в нем используется функция
, которая может возвращать дополнительную пустую строку);
„ таблица
зависит от столбца
только по
пустым строкам. Изменение значения в столбце
не
приведет к изменению содержимого таблицы
, оно может повлиять только на присутствие в ней пустой строки.
Чтобы разорвать замкнутый круг циклической зависимости, достаточно исключить зависимость столбца
от присутствия пустой строки в таблице
. Для этого необходимо, чтобы все функции, используемые в формуле, не зависели от пустых строк. Функция
, к примеру,
оставляет в результирующем наборе пустую строку, если таковая присутствует
в исходной таблице. А значит, функция
зависит от пустых строк. В то
же время функция
исключает из вывода пустые строки, присутствующие в источнике. Следовательно, она не зависит от наличия пустых строк.
Если использовать функцию
вместо
, столбец
также не будет зависеть от пустых строк. В результате две сущности
(таблица и столбец) сохранят зависимость друг от друга, но природа этой зависимости изменится. Таблица
будет зависеть от столбца
по пустым строкам, тогда как обратная зависимость будет исключительно по формуле. А поскольку эти две зависимости не связаны друг с другом,
замкнутый круг циклической зависимости будет разорван, и мы сможем создать связь, как планировали.
Создавая вычисляемые столбцы, которые предполагается использовать
в будущем для построения связей, необходимо следовать простым правилам:
„ использовать функцию
вместо
;
„ использовать функцию
вместо
;
„ не использовать в функции
фильтры с компактным синтаксисом.
Первые два замечания вполне понятны. А пункт с функцией
мы
поясним на следующем примере. Рассмотрим такое выражение:
=
CALCULATE (
MAX ( Customer[YearlyIncome] );
Customer[Education] = "High school"
)
1
гл ленное из ение св зе
521
На первый взгляд, эта формула никак не зависит от наличия пустых строк
в таблице
. На самом же деле это не так. Причина в том, что DAX автоматически расширяет синтаксис функции
, если аргументы фильтра в ней указаны в компактной форме, следующим образом:
=
CALCULATE (
MAX ( Customer[YearlyIncome] );
FILTER (
ALL ( Customer[Education] );
Customer[Education] = "High school"
)
)
Выделенная жирным шрифтом строка содержит функцию
, что создает
зависимость по пустым строкам. Такие моменты бывает непросто обнаружить,
но если вы понимаете базовые принципы образования циклических зависимостей, то с легкостью сможете устранить причины такого поведения. Предыдущий пример может быть переписан следующим образом:
=
CALCULATE (
MAX ( Customer[YearlyIncome] );
FILTER (
ALLNOBLANKROW ( Customer[Education] );
Customer[Education] = "High school"
)
)
Использовуя функцию
явным образом, мы избавились от
зависимости по пустым строкам в таблице
.
Стоит отметить, что в коде зачастую прячутся функции, полагающиеся на
пустые строки. Для примера рассмотрим фрагмент кода из предыдущего раздела, где мы создавали вычисляемую физическую связь на основании диапазона цен. Вот оригинальная формула:
Sales[PriceRangeKey] =
CALCULATE (
VALUES ( PriceRanges[PriceRangeKey] );
FILTER (
PriceRanges;
AND (
PriceRanges[MinPrice] <= Sales[Net Price];
PriceRanges[MaxPrice] > Sales[Net Price]
)
)
)
Присутствие функции
в этой формуле вполне объяснимо. Но есть
еще один способ написать похожее вычисление, используя вместо
функцию
, чтобы формула не возвращала ошибку в случае видимости сразу нескольких строк:
522
1
гл ленное из ение св зе
Sales[PriceRangeKey] =
VAR FilterPriceRanges =
FILTER (
PriceRanges;
AND (
PriceRanges[MinPrice] <= Sales[Net Price];
PriceRanges[MaxPrice] > Sales[Net Price]
)
)
VAR Result =
CALCULATE (
SELECTEDVALUE ( PriceRanges[PriceRangeKey] );
FilterPriceRanges
)
RETURN Result
К сожалению, при попытке создать связь этот код выдаст уже знакомую нам
ошибку, связанную с наличием циклических зависимостей, несмотря на то что
мы вроде не использовали функцию
. И все же в неявном виде эта функция здесь присутствует. Дело в том, что функция
внутренне
преобразуется в следующий синтаксис в выражении:
Sales[PriceRangeKey] =
VAR FilterPriceRanges =
FILTER (
PriceRanges;
AND (
PriceRanges[MinPrice] <= Sales[Net Price];
PriceRanges[MaxPrice] > Sales[Net Price]
)
)
VAR Result =
CALCULATE (
IF (
HASONEVALUE ( PriceRanges[PriceRangeKey] );
VALUES ( PriceRanges[PriceRangeKey] );
BLANK ()
);
FilterPriceRanges
)
RETURN
Result
После раскрытия полной версии кода присутствие функции
стало
гораздо более очевидным. А отсюда и зависимость от пустых строк, повлекшая
за собой ошибку, связанную с наличием циклической зависимости.
Реализация виртуальных связей
В предыдущих разделах мы рассказали, как создавать вычисляемые физические связи на основании вычисляемых столбцов. Но существуют сценарии,
1
гл ленное из ение св зе
523
в которых использование физических связей будет нелучшим решением.
И тогда на помощь приходят связи виртуальные. Виртуал на св
(virtual relationship) имитирует связь физическую. С точки зрения пользователя такая
связь ничем не отличается от обычной, за исключением того, что зрительно
в модели данных она не присутствует. А поскольку связи в модели нет, ответственность за распространение фильтров от одной таблице к другой ложится
на разработчика DAX.
Распространение фильтров в DAX
Одной из самых мощных особенностей DAX является возможность распространять установленные фильтры между таблицами по связям. Но бывают
ситуации, когда физическую связь между двумя сущностями наладить очень
трудно, если не невозможно. В этом случае на выручку приходят выражения
DAX, позволяющие разными способами имитировать физически построенные
связи. В данном разделе мы познакомимся с различными техниками распространения фильтров на примере идеально подходящего для этого сценария.
Компания Contoso размещает свою рекламу в газетах и интернете, выбирая
для этого один или несколько брендов каждый месяц. Информация о рекламируемых брендах хранится в таблице
с указанием года и месяца. Фрагмент этой таблицы вы можете видеть на рис. 15.4.
Рис 15 4
та ли е содержитс о одно строке дл каждого ренда о те
когда ти ренд в од т в рекла н ка ани
ес
а ,
Сразу отметим, что в этой таблице нет ключевого столбца с уникальными
значениями. Несмотря на то что все строки в ней являются уникальными,
в каждом столбце присутствует масса дубликатов. Следовательно, в связи эта
таблица никак не может находиться на стороне «один». И этот факт обретет
немалую важность, когда мы подробнее опишем задачу.
А задача состоит в том, чтобы создать меру для подсчета суммы продаж по
товарам только за период времени, когда они были включены в рекламную
524
1
гл ленное из ение св зе
кампанию. Чтобы решить этот сценарий, необходимо для начала определить,
был ли тот или иной бренд включен в рекламную кампанию в конкретном
месяце. Если бы мы могли создать связь между таблицами
и
, написать код не составило бы труда. К сожалению, связь здесь наладить
не так-то просто (так было задумано в образовательных целях).
Одним из возможных решений может быть создание в обеих таблицах вычисляемого столбца, сочетающего в себе год, месяц и бренд. Таким образом,
мы могли бы последовать технике создания связи на основании нескольких
столбцов, описанной в предыдущем разделе. Но в данном случае есть и другие
способы решения задачи – без необходимости создавать вычисляемые поля.
Одно из решений (далеко не самое оптимальное) включает в себя использование итерационных функций. Можно пройти по таблице
построчно
и проверить, входил ли бренд текущего товара в текущем месяце в рекламную
кампанию. Таким образом, следующая мера решит нашу проблему, пусть и далеко не самым эффективным способом:
Advertised Brand Sales :=
SUMX (
FILTER (
Sales;
CONTAINS (
'Advertised Brands';
'Advertised Brands'[Brand]; RELATED ( 'Product'[Brand] );
'Advertised Brands'[Calendar Year]; RELATED ( 'Date'[Calendar Year] );
'Advertised Brands'[Month]; RELATED ( 'Date'[Month] )
)
);
Sales[Quantity] * Sales[Net Price]
)
В мере используется функция
, осуществляющая поиск строки
в таблице. Функция
принимает в качестве первого параметра таблицу для осуществления поиска. Следом параметры идут парами: столбец для
поиска и значение для поиска. В нашем примере функция
вернет
значение
, если в таблице
будет как минимум одна строка
с текущим годом, месяцем и брендом, где слово текущий означает актуальную
итерацию функции
по таблице
.
Эта мера правильно вычисляет результат, как показано на рис. 15.5, но при
этом в ней есть целый ряд проблем.
Вот две наиболее серьезные проблемы, характерные для предыдущего кода:
„ функция
осуществляет итерации по таблице
, которая сама по
себе является очень объемной, и для каждой строки вызывает функцию
. И как бы быстро ни выполнялась функция
, миллионы ее вызовов способны обрушить производительность любой меры;
„ в мере не используется ранее созданная мера
, рассчитывающая сумму продаж. В данном простом примере это не столь важно, но
если бы в мере
была заложена более серьезная математика,
такой подход был бы неприемлем из-за лишнего дублирования прописанной ранее логики.
1
гл ленное из ение св зе
525
Рис 15 5 Мера Advertised Brand Sales оказ вает с
в одив и на о ент родажи в рекла н ка ани
родаж о ренда ,
Наиболее оптимальным способом решения этой задачи будет использование функции
для распространения фильтров с таблицы
на таблицы
(используя в качестве фильтра бренд) и
(используя год и месяц). Это можно сделать разными способами, которые мы
и опишем в следующих разделах.
Распространение фильтра с использованием функции
TREATAS
Первым и лучшим вариантом решения этого сценария будет использование
функции
для распространения фильтра с
на остальные таблицы. Как мы уже говорили в предыдущих главах, функция
предназначена для изменения привязки данных в таблице таким образом,
чтобы содержащиеся в ней столбцы могли быть применены в качестве фильтров к другим столбцам в модели данных.
Таблица
не содержит ни единой связи с другими таблицами в модели данных. Таким образом, в обычных условиях содержащиеся в ней
столбцы не могут быть использованы в качестве фильтра. Но функция
дает нам возможность изменить привязку данных в таблице
так, что ее содержимое можно будет использовать в аргументах фильтра функции
и распространять их действие на всю модель данных. В следующей вариации меры мы используем этот прием:
Advertised Brand Sales TreatAs :=
VAR AdvertisedBrands =
SUMMARIZE (
'Advertised Brands';
'Advertised Brands'[Brand];
'Advertised Brands'[Calendar Year];
'Advertised Brands'[Month]
)
526
1
гл ленное из ение св зе
VAR FilterAdvertisedBrands =
TREATAS (
AdvertisedBrands;
'Product'[Brand];
'Date'[Calendar Year];
'Date'[Month]
)
VAR Result =
CALCULATE ( [Sales Amount]; KEEPFILTERS ( FilterAdvertisedBrands ) )
RETURN
Result
Функция
извлекает бренды, годы и месяцы из таблицы с рекламными кампаниями. Затем функция
принимает получившуюся
таблицу и изменяет в ней привязку данных таким образом, чтобы столбцы ассоциировались с брендами, годами и месяцами в модели данных. В результирующей таблице
все данные четко привязаны к модели,
так что мы можем использовать ее в качестве фильтра, чтобы видимыми остались бренды, годы и месяцы из рекламных кампаний.
Стоит отдельно отметить, что нам пришлось использовать в функции
модификатор
. Если этого не сделать, функция
переопределит фильтры по бренду, году и месяцу, а нам это не нужно.
В таблице
должны сохраняться фильтры, пришедшие из элемента визуализации (например, мы можем формировать отчет по одному бренду и году),
и добавляться фильтры из таблицы
. Таким образом, нам понадобилось использовать модификатор
для получения корректного результата.
Этот вариант намного лучше предыдущего, с итерациями по таблице продаж. Во-первых, здесь мы повторно используем меру
, что позволило нам избежать повторного написания кода. Во-вторых, не осуществляем
итерации по объемной таблице
для поиска, а сканируем лишь небольшую
по размерам таблицу
, после чего применяем полученный
фильтр к модели данных и вычисляем необходимую нам меру
.
Несмотря на то что эта мера может быть менее понятной интуитивно, в плане
эффективности она будет значительно превосходить меру с использованием
функции
.
Распространение фильтра с использованием функции
I TERSECT
Еще одним способом добиться того же результата является использование
функции
. Логика здесь будет примерно такой же, как и в примере
с
, но в плане производительности этот метод будет немного уступать
первому. В следующем коде мы реализовали концепцию распространения
фильтров с использованием функции
:
Advertised Brand Sales Intersect :=
VAR SelectedBrands =
1
гл ленное из ение св зе
527
SUMMARIZE (
Sales;
'Product'[Brand];
'Date'[Calendar Year];
'Date'[Month]
)
VAR AdvertisedBrands =
SUMMARIZE (
'Advertised Brands';
'Advertised Brands'[Brand];
'Advertised Brands'[Calendar Year];
'Advertised Brands'[Month]
)
VAR Result =
CALCULATE (
[Sales Amount];
INTERSECT (
SelectedBrands;
AdvertisedBrands
)
)
RETURN
Result
Функция
сохраняет привязку данных из таблицы, переданной
первым параметром. Так что результирующая таблица сохранит способность
фильтровать таблицы
и
. На этот раз использование модификатора
нам не понадобилось, поскольку на выходе первой функции
и так будут только видимые бренды и месяцы; функция
лишь удаляет из этого списка сочетания, которые не включались в рекламную
кампанию.
При выполнении этого кода нам понадобилось просканировать таблицу
, чтобы извлечь комбинации брендов и месяцев, а позже произвести еще
одно сканирование для вычисления суммы продаж. В этом данная мера уступает своему аналогу с применением функции
. Но изучить эту технику просто необходимо, поскольку она может пригодиться при использовании
других функций для работы со множествами, таких как
и
. Функции этой группы могут объединяться для создания более сложных фильтров
и написания комплексных мер без особых усилий.
Распространение фильтра с использованием функции
FILTER
Третьей альтернативой для разработчиков DAX в вопросе распространения
фильтров по таблицам является совместное использование функций
и
. В данном случае код будет похож на первую версию с
, за
тем лишь исключением, что в нем будет использоваться функция
вместо
, и никаких итераций по таблице
мы проводить не будем.
Следующий код реализует этот вариант:
528
1
гл ленное из ение св зе
Advertised Brand Sales Contains :=
VAR SelectedBrands =
SUMMARIZE (
Sales;
'Product'[Brand];
'Date'[Calendar Year];
'Date'[Month]
)
VAR FilterAdvertisedBrands =
FILTER (
SelectedBrands;
CONTAINS (
'Advertised Brands';
'Advertised Brands'[Brand]; 'Product'[Brand];
'Advertised Brands'[Calendar Year]; 'Date'[Calendar Year];
'Advertised Brands'[Month]; 'Date'[Month]
)
)
VAR Result =
CALCULATE (
[Sales Amount];
FilterAdvertisedBrands
)
RETURN
Result
В функции
, присутствующей здесь в качестве аргумента фильтра
, используется та же техника с применением функции
, что
и в первом примере. Но на этот раз итерации выполняются не по всей таблице
, а по результирующему набору, полученному из функции
.
Как мы объясняли в главе 14, использовать таблицу
как аргумент фильтра
функции
было бы неправильно из-за расширенных таблиц. Лучше
подвергать фильтру только три столбца. На выходе функции
данные обладают корректной привязкой к столбцам в модели. Кроме того, нам нет
необходимости использовать модификатор
, поскольку на выходе
функции
и так будут только выбранные значения для бренда, года
и месяца.
С точки зрения производительности этот вариант худший из трех, хотя он
и быстрее изначальной меры с функцией
. Также стоит отметить, что
у всех техник с использованием функции
есть одно общее преимущество, состоящее в отсутствии необходимости дублировать бизнес-логику вычисления в мере
, чем не могла похвастаться наша первая
попытка с итератором
.
Динамическая сегментация с использованием
виртуальных связей
Во всех предыдущих примерах мы использовали код на DAX для расчета значений и распространения фильтров в отсутствие связей, хотя вполне могли решить задачу путем создания физической связи. Но бывают случаи, когда нала1
гл ленное из ение св зе
529
дить физическую связь просто не представляется возможным, как в примере,
который мы рассмотрим в данном разделе.
Задачу, связанную со статической сегментацией данных, которую мы рассмотрели ранее в данной главе, мы решили при помощи создания виртуальной
связи. В случае со статической сегментацией распределение продаж по категориям выполняется посредством вычисляемого столбца. Если говорить о дина
ми еской сегмента ии (dynamic segmentation), то здесь классификация данных
выполняется «на лету» – она основывается не на вычисляемом столбце, как
в примере с ценовыми группами, а на динамическом вычислении вроде суммы продаж. Для выполнения динамической сегментации у нас должны быть
определенные критерии для фильтра. В нашем примере мы будем выполнять
фильтрацию покупателей на основании значения меры
.
В данном примере конфигурационная таблица будет содержать наименования сегментов и их границы, как показано на рис. 15.6.
Рис 15 6
он иг ра ионна та ли а
дл в олнени дина и еско сег ента ии
Если клиент совершил покупки на сумму от 75 до 100 долларов, мы отнесем
его к сегменту Low (Низкий), как видно из представленной конфигурационной
таблицы. Важный нюанс заключается в том, что значение меры, которое мы
сверяем с нашей таблицей, напрямую зависит от пользовательского выбора
в отчете. Например, если пользователь выберет в срезе один цвет, то динамическая сегментация покупателей будет выполнена исключительно по товарам
выбранного цвета. А поскольку у нас выполняются динамические расчеты, физическую связь между таблицами мы построить просто не можем. Взгляните
на отчет, показанный на рис. 15.7, на котором продемонстрировано распределение покупателей по сегментам с разбивкой по годам, причем учет ведется
исключительно по выбранным категориям товаров.
Рис 15 7
ажд
ок атель в одит в сво сег ент,
ри то в разн е год сег ент него ожет ть разн
Принадлежность покупателя к тому или иному сегменту может меняться
из года в год. Например, в 2008 году он может попасть в категорию Very Low
530
1
гл ленное из ение св зе
(Очень низкий), а годом позже подняться до уровня Medium (Средний). Более
того, при изменении выбора в фильтре по категориям товаров будут меняться
и данные в отчете.
Фактически у пользователя должно сложиться ощущение, что связь в модели данных на самом деле существует и каждый покупатель принадлежит своему сегменту. При этом физическую связь мы создать здесь никак не можем.
И причина как раз в том, что один покупатель может быть отнесен к разным
сегментам в разных ячейках отчета. В этом случае поставленную задачу можно
решить только при помощи DAX.
Нам необходимо написать меру, подсчитывающую количество покупателей,
принадлежащих тому или иному сегменту. Иными словами, мера вычисляет,
сколько покупателей входит в каждый сегмент, основываясь на данных из текущего контекста фильтра. Формула выглядит довольно просто, но при этом
нуждается в определенных пояснениях:
CustInSegment :=
SUMX (
Segments;
COUNTROWS (
FILTER (
Customer;
VAR SalesOfCustomer = [Sales Amount]
VAR IsCustomerInSegment =
AND (
SalesOfCustomer > Segments[MinSale];
SalesOfCustomer <= Segments[MaxSale]
)
RETURN
IsCustomerInSegment
)
)
)
За исключением итогов, все строки отчета, показанного на рис. 15.7, выполняются в контексте фильтра, включающем один конкретный сегмент. Таким
образом, функция
будет проходить только по одной строке. Использование этой функции облегчает извлечение границ сегмента из конфигурационной таблицы (
и
) и позволяет правильно рассчитать значения
в присутствии фильтров. Внутри итератора
функция
считает клиентов с суммой покупок (для повышения производительности, сохраненной в переменной
), входящей в интервал текущего сегмента.
Полученная мера будет аддитивной по сегментам и покупателям и неаддитивной по другим фильтрам. Вы могли заметить, что в итоговой колонке
по первой строке отчета стоит значение 213, хотя сумма значений по строке
дает 214. Причина в том, что в итоговой ячейке мера подсчитывает количество покупателей из этого сегмента по трем годам. Похоже, один из учтенных
клиентов совершил за три года столько покупок, что в итоге был переведен
в следующую категорию.
И хотя такое поведение меры может быть не слишком понятно интуитивно,
на самом деле неаддитивность вычислений по времени может оказаться очень
1
гл ленное из ение св зе
531
полезной. Чтобы сделать меру аддитивной по годам, необходимо изменить
формулу, добавив к расчетам временную характеристику. Например, следующая мера будет аддитивной по оси времени. Но при этом она потеряет в гибкости, поскольку теперь ее не получится использовать без включения в отчет
измерения с годами:
CustInSegment Additive :=
SUMX (
VALUES ( 'Date'[Calendar Year] );
SUMX (
Segments;
COUNTROWS (
FILTER (
Customer;
VAR SalesOfCustomer = [Sales Amount]
VAR IsCustomerInSegment =
AND (
SalesOfCustomer > Segments[MinSale];
SalesOfCustomer <= Segments[MaxSale]
)
RETURN
IsCustomerInSegment
)
)
)
)
Как видно по рис. 15.8, значения по строкам теперь корректно суммируются,
хотя общий итог (сумма по всем годам и сегментам) по-прежнему может показывать неправильные цифры.
Рис 15 8 е ерь с
о строка в
но с о и итого
ог т ть ро ле
исл
тс
равильно,
Отдав предпочтение правильному подсчету итогов по сегментам, мы вынуждены были принести в жертву общий итог по сегментам и годам. Например, конкретный покупатель мог принадлежать к сегменту Very Low (Очень
низкий) в 2009 году и к сегменту Very High (Очень высокий) в 2008-м. Таким образом, в общем итоге этот клиент будет учтен дважды. Ячейка с общим итогом
на рис. 15.8 содержит значение 1472, тогда как общее количество покупателей
составило 1437, как видно на предыдущем рис. 15.7.
532
1
гл ленное из ение св зе
К сожалению, в подобных расчетах аддитивность мер часто является настоящей проблемой. По своей природе такие меры являются неаддитивными. елание сделать их аддитивными на первый взгляд кажется вполне естественным, но
чаще всего это будет приводить к путанице в расчетах. Всегда важно обращать
внимание на такие нюансы, и обычно мы советуем не пытаться всеми силами
делать меру аддитивной без должного понимания возможных последствий.
Реализация физических связей в DAX
Связь может быть сил ной (strong) или сла ой (weak). При использовании сильной связи движок DAX точно знает, что на стороне «один» этой связи гарантированно будут уникальные значения. Если движку не удается убедиться
в уникальности ключей, он классифицирует связь как слабую. При этом слабая
связь может образоваться либо по причине того, что движку не удалось удостовериться в уникальности значений, либо из-за технических сложностей, о которых мы расскажем далее в этом разделе, или же в результате намеренных
действий разработчика. Слабые связи не входят в состав расширенных таблиц,
о которых мы говорили в главе 14.
Начиная с 2018 года Power BI допускает создание составн моделей данн
(composite model). В таких моделях разрешено сочетать данные в режиме VertiPaq (копия данных из источника предварительно загружается и кешируется
в памяти) и в режиме DirectQuery (обращение к источнику данных происходит
только в момент запроса). Подробно режимы DirectQuery и VertiPaq будут описаны в главе 17.
Единая модель данных может содержать какое-то количество таблиц, сохраненных в режиме VertiPaq, и какое-то – в режиме DirectQuery. Более того,
таблицы DirectQuery могут происходить из разных источников, образуя так называемые острова данн (data island) DirectQuery.
Чтобы лучше различать данные, хранящиеся в режимах VertiPaq и DirectQuery, мы будем говорить о них как о континенте (continent) (VertiPaq) и островах
(источники данных DirectQuery), как показано на рис. 15.9.
DirectQuery Island
DirectQuery Island
VertiPaq Continent
Рис 15 9
составно
одели данн
та ли
рас ределен
1
о острова
гл ленное из ение св зе
533
Хранилище VertiPaq представляет собой не что иное, как еще один остров
данных. Мы называем его континентом только потому, что этот остров наиболее востребован при работе.
Связь объединяет две таблицы. Если таблицы принадлежат одному острову,
связь между ними будет именоваться внутриостровной, иначе – межостровной. Последние всегда представляют собой слабые связи. Таким образом, расширенные таблицы никогда не пересекают острова.
Связи между таблицами характеризуются кратност (cardinality), которая
бывает трех типов. И разница между ними есть как в техническом плане, так
и в области семантики. Здесь мы не будем излишне глубоко вдаваться во все
нюансы этих разновидностей, поскольку для этого потребовалось бы сделать
ряд отступлений, что не входит в наши планы. Вместо этого мы остановимся
на технических подробностях физических связей и посмотрим, какое влияние
они оказывают на код DAX.
Всего существует три типа кратности связи:
„ крат ость с
и о и ко
о и
это наиболее распространенный
тип кратности связи. На стороне «один» в связи располагается столбец
с уникальными значениями, тогда как на стороне «многие» могут быть
(и часто бывают) дубликаты. В некоторых клиентских инструментах делаются различия между связями «один ко многим» и «многие к одному».
Но по своей сути это одно и то же. Все зависит от порядка расположения
таблиц: связь «один ко многим» между таблицами
и
можно
представить и как связь «многие к одному» между
и
;
„ крат ость с
и о и к о о у этот тип кратности связи встречается довольно редко. Здесь на обеих сторонах связи должны быть столбцы
с уникальными значениями. Более точным названием такого типа кратности связи было бы «ноль или один к нулю или одному», поскольку присутствие записи в одной таблице не обязательно должно означать присутствие соответствующей строки в другой;
„ крат ость с
и
о ие ко
о и
в этом случае на обеих сторонах
связи столбцы могут содержать дублирующиеся значения. Эта кратность
была представлена в 2018 году, и, к сожалению, для нее было выбрано не
самое удачное название. В сущности, в теории моделирования данных
термин «многие ко многим» относится к другой реализации, использующей сочетание связей «один ко многим» и «многие к одному». Важно
понимать, что в этом случае мы говорим не о связи «многие ко многим»,
а именно о кратности «многие ко многим».
Чтобы избежать неоднозначности трактовки и не путаться с канонической
терминологией моделирования данных, в которой связь «многие ко многим»
означает совсем другую реализацию, мы будем использовать следующие аббревиатуры для описания кратности связи:
„ «один ко многим»: мы будем именовать такую кратность S
, от SingleMany-Relationship (один–многие–связь);
„ «один к одному»: здесь мы остановимся на аббревиатуре SS , от SingleSingle-Relationship (один–один–связь);
„ «многие ко многим»: такой тип кратности мы будем называть
, от
Many-Many-Relationship (многие–многие–связь).
534
1
гл ленное из ение св зе
Еще одной важной деталью является то, что связь «многие ко многим» всегда
будет слабой – вне зависимости от того, одному или разным островам принадлежат таблицы. Если разработчик обе стороны связи обозначит как «многие»,
связь автоматически будет трактоваться как слабая, без возможности расширения таблиц.
Вдобавок каждая связь характеризуется направлением кросс-фильтрации.
Это направление используется для распространения контекста фильтра. Кроссфильтрация может принимать два значения:
„ о о а ра ле а S
контекст фильтра всегда распространяется
в одном направлении. В связи «один ко многим» это направление будет
от стороны «один» к стороне «многие». Это стандартное и наиболее желаемое поведение;
„
у а ра ле а B
контекст фильтра распространяется в обоих
направлениях. Такой тип распространения фильтра называется также
двуна равленной кросс фил тра ией (bidirectional cross-filter), а иногда –
двунаправленной связью. В связи «один ко многим» контекст фильтра
продолжит распространяться от стороны «один» к стороне «многие», но
также получит и новое направление – от стороны «многие» к стороне
«один».
Доступные направления кросс-фильтрации зависят от типа связи:
„ вS
всегда доступен выбор между однонаправленной и двунаправленной кросс-фильтрацией;
„ в SS всегда используется двунаправленная кросс-фильтрация. Поскольку обе стороны связи характеризуются как «один», а сторон «многие»
просто нет, единственным вариантом является двунаправленная кроссфильтрация;
„ в
обе стороны связи помечены как «многие». Это сценарий, противоположный SSR: обе стороны связи могут быть как источником, так
и целью распространения контекста фильтра. В этом случае разработчик
может остановить выбор на двунаправленной кросс-фильтрации, чтобы
контекст фильтра распространялся в обе стороны. Либо он может выбрать
однонаправленную кросс-фильтрацию, при этом указав, от какой таблицы к какой будет осуществляться распространение контекста фильтра.
Как и в случае с другими связями, однонаправленная кросс-фильтрация
будет лучшим выбором. Позже в данной главе мы поговорим об этом более подробно.
В табл. 15.1 мы подытожили информацию о разных типах связей, доступных
направлениях кросс-фильтрации, их влиянии на распространение контекста
фильтра и вариантах создания слабой/сильной связи.
Когда две таблицы объединены сильной связью, в таблице, расположенной
на стороне «один», может содержаться дополнительная пустая строка, в случае
если связь оказалась недействительной. Таким образом, если в таблице на стороне «многие» содержатся значения, не присутствующие в таблице на стороне
«один», к последней будет добавлена пустая строка. Эту особенность мы объясняли в главе 3. Дополнительная пустая строка никогда не появляется, если
связь между таблицами слабая.
1
гл ленное из ение св зе
535
АБЛИ А 15 1
азли н е ти
ип связи Направление кроссфильтрации
Однона равленна
в на равленна
св зе
Распространение
контекста фильтра
От сторон один
к стороне ногие
о е сторон
в на равленна
о е сторон
Однона равленна
в на равленна
жно в рать исто ник
о е сторон
Слабая сильная связь
Сла а , если ежостровна ,
ина е сильна
Сла а , если ежостровна ,
ина е сильна
Сла а , если ежостровна ,
ина е сильна
сегда сла а
сегда сла а
Ранее мы уже говорили, что не будем касаться темы выбора разработчиком
того или иного типа связи. Этот выбор должен сделать специалист по моделированию данных, основываясь при этом на доскональном понимании семантики конкретной модели. С точки зрения DAX каждая связь ведет себя поразному, и важно понимать отличия между типами связей и их влияние на код
DAX.
В следующих разделах мы подробно остановимся на этих различиях и дадим несколько советов по выбору типов связей в модели.
Использование двунаправленной
кросс-фильтрации
Двуна равленна кросс фил тра и (bidirectional crossfilter) может быть реализована двумя способами: непосредственно в модели данных или в коде
DAX с использованием модификатора
в функции
,
о чем мы рассказывали в главе 5. Как правило, двунаправленную кроссфильтрацию не стоит включать в модели данных без особой необходимости.
Причина в том, что наличие таких фильтраций в модели значительно усложняет процесс распространения контекста фильтра вплоть до полной его непредсказуемости.
В то же время существуют сценарии, в которых двунаправленная кроссфильтрация может оказаться чрезвычайно полезной. Взгляните на отчет,
изображенный на рис. 15.10, он построен на базе модели данных Contoso со
связями, установленными в режим однонаправленной кросс-фильтрации.
Слева в отчете мы видим два среза: по брендам, который распространяется на
столбец
, и по странам с фильтрацией столбца
. Несмотря на то что в Армении (Armenia) не продавались товары бренда
Northwind Traders, в срезе
есть упоминание этой страны.
Причина в том, что контекст фильтра на столбце
оказывает
влияние на таблицу
из-за установленной связи типа «один ко многим»
между таблицами
и
. Но от таблицы
фильтр не распространяется на таблицу
, поскольку она находится на стороне «один» в связи
типа «один ко многим» между таблицами
и
. Именно поэтому
536
1
гл ленное из ение св зе
в срезе
остаются доступными для выбора все страны вне зависимости от того, были ли в них продажи товаров выбранного бренда. Иными
словами, два представленных элемента визуализации со срезами не синхронизированы друг с другом. При этом в матрице строка с Арменией не выводится, поскольку значение меры
по ней дает пустоту, а по умолчанию в матрицах не показываются строки с пустыми значениями в выведенных
в отчет мерах.
Рис 15 10
срезе CountryRegion содержатс стран с н лев
и родажа и
Если для вас важно, чтобы срезы были синхронизированы между собой, можно включить двунаправленную кросс-фильтрацию между таблицами
и
, что приведет к образованию модели данных, показанной на рис. 15.11.
Рис 15 11
росс ильтра и дл св зи ежд та ли а и Customer и Sales
стала дв на равленно
Установка двунаправленной кросс-фильтрации приведет к тому, что в срезе по
останутся только значения, для которых есть соответствия
в таблице
. По рис. 15.12 видно, что срезы стали синхронизированными,
что может понравиться пользователю.
Двунаправленная кросс-фильтрация может оказаться полезной для отчетов, но за все приходится платить. Двунаправленные связи могут негативно
1
гл ленное из ение св зе
537
сказаться на производительности модели данных в целом, поскольку контекст
фильтра при их использовании должен распространяться по связям в обе стороны. К тому же фильтрация таблиц от стороны «один» к стороне «многие»
происходит гораздо быстрее, чем в обратном направлении. Так что если держать в уме эффективность модели данных, от использования двунаправленных
связей стоит отказаться. Более того, такие связи способны создавать предпосылки для образования неоднозначностей в модели. Этой темы мы коснемся
далее в данной главе.
Рис 15 12
кл ение режи а дв на равленно кросс ильтра ии
озволило син ронизировать срез в от ете
Примечание
с ольз
ильтр
ровн виз ализа ии, ожно сократить коли ество
види
ле ентов в виз ально ле енте
ез ри енени дв на равленно
ильтра ии в св зи сожалени , на а рель 201 года в
е е не оддержива тс ильтр ровн виз ализа ии огда они стан т дост н дл срезов, ис ользование
дв на равленно кросс ильтра ии дл ограни ени коли ества види
ле ентов
в ильтре останетс в ро ло
Связи типа один ко многим
в и ти а один ко многим (one-to-many relationships) являются наиболее
распространенными в моделировании данных. Например, именно такой тип
связи используется для объединения таблиц
и
. Такая связь говорит о том, что для одного товара может присутствовать множество записей
в таблице продаж, тогда как одна строка в таблице продаж соответствует только одному товару. Таким образом, таблица
в этой связи будет располагаться на стороне «один», а
– на стороне «многие».
Более того, при анализе данных у пользователя должна быть возможность
осуществлять фильтрацию по атрибутам товаров и вычислять соответствующие значения в таблице
. По умолчанию контекст фильтра будет распространяться от таблицы
(сторона «один») к таблице
(сторона
538
1
гл ленное из ение св зе
«многие»). При необходимости можно изменить поведение распространения
контекста, установив двунаправленный тип кросс-фильтрации для связи между этими таблицами.
При наличии сильной связи типа «один ко многим» расширение таблицы
всегда выполняется в направлении, обратном распространению фильтра, –
от стороны «многие» к стороне «один». В случае если связь недействительна,
в таблице на стороне «один» может появиться дополнительная пустая строка.
С точки зрения семантики слабая связь типа «один ко многим» ведет себя так
же, как сильная, за исключением того, что пустая строка не появляется. Кроме того, запросы, построенные с использованием слабых связей типа «один
ко многим», в большинстве случаев будут отличаться худшей производительностью.
Связи типа один к одному
в и ти а один к одному (one-to-one relationships) используются при моделировании данных крайне редко. По сути, две таблицы, объединенные таким
типом связи, представляют собой единую таблицу, разделенную надвое. При
правильном проектировании такие таблицы должны быть объединены перед
загрузкой в модель данных.
Таким образом, самой правильной моделью обращения с таблицами, объединенными связью «один к одному», будет их слияние. Исключением из правила является сценарий, в котором данные поступают в одну сущность из разных источников, которые должны обновляться отдельно друг от друга. В этом
случае лучше предпочесть загрузку таблиц в модель данных отдельно во избежание сложных и дорогостоящих преобразований на этапе обновления. Так
или иначе, при работе со связями типа «один к одному» пользователю необходимо уделять особое внимание следующим аспектам:
„ кросс-фильтрация для связи типа «один к одному» всегда будет двунаправленной. Для такой связи просто нет возможности установить однонаправленную фильтрацию. Таким образом, фильтр, примененный к одной таблице, всегда будет распространяться на вторую, и наоборот, если
связь не деактивирована при помощи функции
или физически в модели данных;
„ как уже было сказано в главе 14, если две таблицы объединены сильной
связью типа «один к одному», расширенная версия каждой из них будет
полностью включать в себя другую таблицу. Иначе говоря, наличие сильной связи «один к одному» ведет к созданию двух одинаковых расширенных таблиц;
„ поскольку обе таблицы в связи представляют собой сторону «один», если
связь между ними будет одновременно сильной и недействительной (то
есть если в ключевом столбце одной из них будут присутствовать значения, которых не будет во второй), в обеих таблицах могут появиться
пустые строки. Более того, значения в столбцах обеих таблиц, которые
используются для создания связи, должны быть уникальными.
1
гл ленное из ение св зе
539
Связи типа многие ко многим
в и ти а многие ко многим (many-to-many relationships) представляют собой очень мощный инструмент моделирования данных и встречаются гораздо
чаще, чем связи типа «один к одному». Работать с такими связями не так-то
просто, но научиться обращаться с ними стоит по причине их огромного аналитического потенциала.
Связь типа «многие ко многим» образуется в модели данных всякий раз,
когда две сущности не удается объединить посредством обычной связи «один
ко многим». Существует два типа таких связей и несколько способов решения
этих двух сценариев. В следующих разделах мы представим разные техники
для работы со связями типа «многие ко многим».
Реализация связи многие ко многим через таблицу-мост
Следующий пример мы позаимствовали из банковской сферы. Банк хранит
расчетные счета в одной таблице, а клиентов – в другой. При этом один счет
может принадлежать разным клиентам, а у одного клиента может быть несколько расчетных счетов. Таким образом, мы не можем хранить информацию
о клиенте непосредственно в таблице расчетных счетов, так же как не можем
учитывать расчетные счета прямо в таблице клиентов. Следовательно, этот
сценарий не может быть воспроизведен при помощи обычных связей между
счетами и клиентами.
Традиционным решением подобной задачи является создание дополнительной таблицы, в которой будут храниться соответствия между различными клиентами и их расчетными счетами. Такая таблица обычно именуется
та ли ей мостом (bridge table), а ее пример показан в модели данных на
рис. 15.13.
Рис 15 13 а ли а AccountsCustomers св зана одновре енно и с та ли е Accounts,
и с Customers
540
1
гл ленное из ение св зе
В данном случае связь типа «многие ко многим» между таблицами
реализована посредством создания таблицы-моста с именем
. Каждая строка в этой таблице соответствует одной связке клиента с расчетным счетом.
Сейчас созданная нами модель данных пока не работает. Отчет со срезом по
таблице
формируется правильно, поскольку таблица
фильтрует таблицу
, находясь в этой связи на стороне «один». В то же
время если в срез поместить таблицу
, отчет сломается, ведь фильтр
из таблицы
распространяется на таблицу-мост
,
а таблицы
уже не достигает по причине однонаправленности кроссфильтрации между этими таблицами. При этом в связи между таблицами
и
на стороне «один» должна располагаться первая из
них, поскольку в столбце AccountKey соблюдается условие уникальности данных, а в таблице
могут присутствовать дубликаты.
На рис. 15.14 видно, что значения в поле
не применяют никаких фильтров к мере
, отображенной в матрице.
и
Рис 15 14 а ли а Accounts, в несенна на строки,
ильтр ет зна ени , тогда как та ли а Customers на стол
а
нет
Этот сценарий можно решить, установив двунаправленную кросс-фильтрацию между таблицами
и
либо непосредственно
в модели данных, либо используя функцию
, как показано ниже:
-- Версия с использованием функции CROSSFILTER
SumOfAmt CF :=
CALCULATE (
SUM ( Transactions[Amount] );
CROSSFILTER (
AccountsCustomers[AccountKey];
Accounts[AccountKey];
BOTH
)
)
Теперь наш отчет показывает более осмысленную информацию, что видно
по рис. 15.15.
Установка двунаправленной кросс-фильтрации непосредственно в модели
данных может быть полезна тем, что обеспечивает автоматическое применение фильтров ко всем вычислениям, включая неявные меры, создаваемые кли1
гл ленное из ение св зе
541
ентскими инструментами, такими как Excel или Power BI. Однако присутствие
подобных связей в модели данных существенно усложняет процесс распространения фильтров и может негативно сказаться на производительности вычислений в мерах, которые не должны затрагиваться этими фильтрами. Более
того, если позже в модель данных будут добавлены новые таблицы, наличие
в ней двунаправленной кросс-фильтрации может создать неоднозначность,
которую можно будет устранить только путем изменения фильтрации. А это,
в свою очередь, может нарушить работу уже существующих отчетов. Так что
перед тем как включить режим двунаправленной кросс-фильтрации для той
или иной связи между таблицами, дважды подумайте о возможных последствиях таких действий.
Рис 15 15
кл ив дв на равленн кросс ильтра и ,
до ились от ер равильн рез льтатов
Конечно, вы вольны допускать присутствие в своей модели данных связей
с двунаправленной кросс-фильтрацией. Но причин, перечисленных в этой
книге, а также нашего личного опыта, которым мы с вами делимся, должно
быть достаточно, чтобы отказаться от создания таких связей в модели. Мы
предпочитаем работать с простыми и надежными моделями данных и являемся сторонниками использования в мерах функции
всегда, когда
это необходимо. С точки зрения производительности варианты с включением
двунаправленной кросс-фильтрации в модели данных и применением функции
в DAX практически идентичны.
Также нашу задачу можно решить и с помощью довольно сложного кода на
DAX. Несмотря на всю свою сложность, эта формула даст нам определенную
гибкость. Одним из вариантов написания меры
без использования
функции
является применение результата функции
в качестве аргумента фильтра в
, как показано ниже:
-- Версия с использованием функции SUMMARIZE
SumOfAmt SU :=
CALCULATE (
SUM ( Transactions[Amount] );
SUMMARIZE (
AccountsCustomers;
Accounts[AccountKey]
)
)
542
1
гл ленное из ение св зе
Функция
возвращает столбец с привязкой данных к
, тем самым фильтруя таблицу
, а следом и
кого же результата можно добиться и при помощи функции
:
. Та-
-- Версия с использованием функции TREATAS
SumOfAmt TA :=
CALCULATE (
SUM ( Transactions[Amount] );
TREATAS (
VALUES ( AccountsCustomers[AccountKey] );
Accounts[AccountKey]
)
)
В этом случае функция
возвращает значения столбца
, отфильтрованные по таблице
, а функция
меняет привязку данных таким образом, чтобы была отфильтрована таблица
, а следом за ней и
.
Наконец, можно произвести похожее вычисление и при помощи более простой формулы с применением расширенных таблиц. Здесь стоит заметить, что
расширение таблицы-моста выполняется и в сторону
, и в сторону
, что позволяет добиться почти такого же результата, как в предыдущих примерах. При этом данный код получился чуть короче:
-- Версия с использованием расширенных таблиц
SumOfAmt ET :=
CALCULATE (
SUM ( Transactions[Amount] );
AccountsCustomers
)
Несмотря на множество вариаций, все эти решения могут быть сгруппированы по единственному общему признаку:
„ с использованием двунаправленной кросс-фильтрации в DAX;
„ с подстановкой таблицы в качестве аргумента фильтра функции
.
Формулы из этих двух групп будут вести себя по-разному, в случае если связь
между таблицами
и
станет недействительной. Мы знаем,
что в такой ситуации в таблицу, находящуюся на стороне «один», добавляется
пустая строка. Если в таблице
будут содержаться ссылки на расчетные счета, которых нет в таблице
, связь между таблицами
и
будет считаться недействительной, и в таблицу
будет
добавлена пустая строка. Этот эффект не распространится на таблицу
. Таким образом, пустая строка не будет добавлена в таблицу
, она
будет присутствовать только в таблице
.
Следовательно, осуществление среза таблицы
по столбцу
покажет пустую строку, тогда как фильтрация
по
не
обнаружит записей, связанных с пустой строкой. Такое поведение может приводить в замешательство, и для демонстрации этого примера мы добавили
строку в таблицу
с несоответствующим значением в поле
1
гл ленное из ение св зе
543
и суммой 10 000,00. Разница в выводе показана на рис. 15.16, где в матрице
слева отображен срез по столбцу
, а справа – по
. В качестве меры использовался расчет с применением функции
.
Рис 15 16
стол е CustomerName ста строка отс тств ет,
и итоговое зна ение в раво атри е кажетс не равильн
Когда матрица фильтруется по столбцу
, в ней появляется пустая
строка с суммой счета, равной 10 000,00. В то же время срез по столбцу
пустую строку не выводит. Фильтр начинает свое действие со столбца
в таблице
, но в таблице
нет значений, которые могли бы включить в фильтр пустую строку из таблицы
. В результате значение, ассоциированное с пустой строкой, оказалось
включено только в общий итог, поскольку на этом уровне контекст фильтра не
включает в себя значение по столбцу
. Таким образом, на уровне итогов таблица
не включается в перекрестную фильтрацию – все
ее строки, включая пустую, становятся активными, и вычисление меры дает
сумму 15 000,00.
Заметьте, что мы в качестве примера использовали пустую строку, но такой же сценарий возник бы и в случае, если бы в таблице
появилась
строка, не относящаяся ни к одному из клиентов. Эти значения в результате
будут включены только в общий итог по причине того, что фильтр по клиентам
удаляет счета, не связанные ни с одним клиентом. Это замечание очень важно,
поскольку результат, который вы видели на рис. 15.16, может быть и не связан
с наличием недействительной связи. Например, если бы транзакция на сумму
10 000,00 была произведена по служебному аккаунту (Service), присутствующему в таблице
, но не имеющему соответствий в таблице
,
название счета из столбца
появилось бы в отчете, несмотря на отсутствие соответствий с таблицей клиентов. Эта ситуация показана в отчете на
рис. 15.17.
Примечание С енари , изо раженн на рис 1 1 , никои о разо не нар ает сс ло н
елостность рел ионно аз данн , как то
ло в сл ае с от ето , оказанн
на рис 1 1
азе данн
ожет ть реализована до олнительна логика дл
отслеживани возникновени одо н сит а и
544
1
гл ленное из ение св зе
Рис 15 17 С ет
сл же н
из стол а CustomerName
не св зан ни с одни зна ение
Если мы воспользуемся техникой вычисления меры, полагающейся не на
функцию
, а на фильтрацию таблицы в функции
,
результат может оказаться иным. Строки, недостижимые из таблицы-моста,
всегда исключаются из фильтра. А поскольку поддержка фильтра здесь обеспечивается функцией
, эти значения не будут включены даже в общий
итог. Иначе говоря, фильтр всегда будет оставаться активным. Получившийся
результат можно видеть на рис. 15.18.
Рис 15 18
с ользование те ники с та ли н
ривело к скр ти
сто строки и искл ени
до олнительн зна ени из о его итога
и ильтра и
Здесь мы не только лишились добавки к общему итогу – пустая строка также
пропала и из отчета с фильтром по столбцу
, она просто была отсеяна
табличным фильтром функции
.
Ни одно из полученных нами значений нельзя считать полностью верным
или неверным. Более того, если таблица-мост будет включать ссылки на все
строки из таблицы
, соответствующие
, то две меры покажут одинаковый результат. Разработчики вольны сами выбирать технику расчетов в зависимости от своих требований.
1
гл ленное из ение св зе
545
Примечание
лане роизводительности ре ение с ис ользование та ли в ка
естве арг ента ильтра
нк ии CALCULATE всегда дет роигр вать из за нео оди ости сканировать та ли
ост AccountsCustomers то озна ает, то в л о
от ете, ис ольз
е
ер ез ильтра о та ли е Customers, адение
ективности
дет акси альн , то окажетс а сол тно ес олезн , если дл каждого с ета
дет о ределен как ини
один клиент аки о разо , о ол ани л
е всегда
останавливать в ор на ера с ис ользование дв на равленно кросс ильтра ии,
если елостность данн
озвол ет гарантировать одинаков е рез льтат
акже не
за ва те, то ре ени , основанн е на рас ирении та ли , д т ра отать только ри
нали ии сильн св зе , а зна ит, если в ва е сл ае та ли о единен ри о ои сла
св зе , л
е ред о есть те ник с дв на равленно кросс ильтра ие
одро нее о актора , вли
и на в ор ре ени , ожно о итать в статье о
адрес https://www.sqlbi.com/articles/many-to-many-relationships-in-power-bi-and-ex201 .
Реализация связи многие ко многим
через об ее измерение
В данном разделе мы покажем вам еще один сценарий применения связи
«многие ко многим», которая, однако, с технической точки зрения таковой вовсе не является. Здесь мы определим связь между двумя сущностями на уровне
гранулярности, отличном от первичного ключа.
Этот пример мы взяли из области бюджетирования, где информация о бюджете хранится в таблице, содержащей страну, бренд и бюджет в расчете на один
год. Модель данных, которой мы будем пользоваться, показана на 15.19.
Рис 15 19
та ли е Budget содержатс стол
546
1
CountryRegion, Brand и Budget
гл ленное из ение св зе
Если мы хотим выводить в одном отчете цифры по продажам и бюджету,
нам необходимо иметь возможность одновременно фильтровать таблицы
и
. В таблице
есть столбец
, который также присутствует и в таблице
. Однако значения в этом столбце неуникальны
в обеих таблицах. Столбец
из таблицы по бюджетированию присутствует
также и в таблице
, и значения в нем тоже неуникальны. Можно было бы
написать простую меру
, в которой выполнялось бы обычное суммирование по столбцу
в одноименной таблице.
Budget Amt :=
SUM ( Budget[Budget] )
Матрица со срезом по столбцу
выведет результат,
показанный на рис. 15.20. Мера
во всех строках показывает одинаковые цифры, а именно сумму по столбцу
.
Рис 15 20 Мера Budget Amt не ильтр етс
о стол
Customer[CountryRegion]
и всегда оказ вает одинаков рез льтат
Существует несколько решений этого сценария. Первое из них связано с созданием виртуальной связи с использованием одной из описанных ранее в данной главе техник, что позволит объединить обе таблицы единым фильтром.
Например, использование функции
поможет решить вопрос с распространением фильтра с таблиц
и
на
, как показано
в следующем коде:
Budget Amt :=
CALCULATE (
SUM ( Budget[Budget] );
TREATAS (
VALUES ( Customer[CountryRegion] );
Budget[CountryRegion]
);
TREATAS (
VALUES ( 'Product'[Brand] );
Budget[Brand]
)
)
Теперь мера
правильно использует фильтр по таблицам
и/или
, что видно по рис. 15.21.
1
гл ленное из ение св зе
547
Для этого решения характерны следующие ограничения:
„ если в таблице
появится новый бренд, не присутствующий в таблице
, информация по нему не попадет в отчет. Как результат,
цифры в отчете будут неправильными;
„ вместо использования самого надежного варианта с распространением
фильтра по физической связи мы предпочли фильтровать таблицу
при помощи кода DAX. В объемных моделях данных это может негативно
сказаться на производительности отчета.
Рис 15 21 Мера Budget Amt
ильтр етс о стол
Customer[CountryRegion]
Лучшее решение этого сценария потребует от нас незначительного изменения модели данных с добавлением таблицы, которая будет выступать в качестве единого фильтра для таблиц
и
. Создать ее можно легко
и просто при помощи вычисляемых таблиц непосредственно в коде DAX:
CountryRegions =
DISTINCT (
UNION (
DISTINCT ( Budget[CountryRegion] );
DISTINCT ( Customer[CountryRegion] )
)
)
В этой формуле происходит извлечение всех значений из столбца
таблиц
и
и объединение их в единую таблицу с удалением дубликатов. В результате получим новую таблицу, содержащую все
значения
без дубликатов из таблиц
и
. Таким же
образом можно объединить таблицы
и
, оперируя со столбцами
и
.
Brands =
DISTINCT (
UNION (
DISTINCT ( 'Product'[Brand] );
DISTINCT ( Budget[Brand] )
)
)
После того как эти вспомогательные вычисляемые таблицы созданы, остается лишь соединить их связями с существующими таблицами в нашей модели
данных, как показано на рис. 15.22.
548
1
гл ленное из ение св зе
Рис 15 22
одели данн
CountryRegions и Brands
о вились две до олнительн е та ли
В обновленной модели данных таблица
будет фильтровать таблицы
и
,а
сможет легко распространять фильтр на
таблицы
и
. Следовательно, нам не нужно будет использовать
функцию
, как в предыдущем примере. Простой функции
будет
достаточно для извлечения корректных значений из таблиц
и
, как
показано в следующем коде меры
. В отчете, внешний вид которого
уже был показан на рис. 15.21, мы при этом должны использовать столбцы из
таблиц
и
.
Budget Amt :=
SUM ( Budget[Budget] )
Если установить двунаправленную кросс-фильтрацию для связей между
таблицами
и
, а также между
и
, можно
и вовсе скрыть таблицы
и
от глаз пользователя, распространяя фильтры от таблиц
и
на
без написания дополнительного кода на DAX. В результате мы получим модель данных, показанную
на рис. 15.23, в которой между таблицами
и
существует логическая связь на уровне гранулярности столбца
. То же самое можно
сказать и о таблицах
и
, но в этом случае уровень гранулярности
будет установлен по столбцу
.
Результат отчета будет таким же, как было показано на рис. 15.21. Обратите
внимание, что связь между таблицами
и
была образована за
счет комбинации связей «многие к одному» и «один ко многим». Установка дву1
гл ленное из ение св зе
549
направленной кросс-фильтрации для связи между таблицами
и
обеспечила распространение контекста фильтра с таблицы
на
, но не наоборот. Если бы двунаправленная кросс-фильтрация была
установлена и для связи между таблицами
и
, модель стала бы до определенной степени неоднозначной, и это помешало бы применить
такой же шаблон к связям между таблицами
и
.
Примечание Модель, оказанна на рис 1 2 , страдает от те же ограни ени , то и ее
ред ественни а с рис 1 1 если в та ли е с
джето о в тс ренд или стран ,
не тенн е в та ли а Product или Customer соответственно, зна ени о ни
ог т не
оказ ватьс в от ете Более одро но
косне с то ро ле
в след
е разделе
Рис 15 23
осле становки дв на равленно кросс ильтра ии
вс о огательн е та ли
ог т ть скр т
Заметим, что чисто технически созданные нами связи не являются связями
типа «многие ко многим». В этой модели данных мы связали таблицы
и
(то же самое и с
) на уровне гранулярности, отличном от конкретных товаров, а именно на уровне брендов. Такого же результата можно
было добиться и более простым, но при этом менее эффективным способом,
используя слабые связи, что будет описано в следующем разделе. Кроме того,
объединение таблиц по альтернативным уровням гранулярности таит в себе
определенные нюансы и сложности, о которых мы поговорим далее в данной
главе.
550
1
гл ленное из ение св зе
Реализация связи многие ко многим через слабые связи
В предыдущем примере мы объединили таблицы
и
посредством вспомогательной таблицы. В октябре 2018 года мы получили возможность создавать в DAX так называемые слабые связи между таблицами, с помощью которых можно проще решить подобный сценарий.
Слабая связь будет установлена между таблицами в случае наличия дублирующихся значений в обоих столбцах, использующихся для объединения таблиц. Иными словами, модель данных, подобная той, что была показана на
рис. 15.23, может быть создана и путем непосредственного соединения таблиц
и
по столбцу
– без использования вспомогательной таблицы
, как это было в предыдущем разделе. В результате мы получим модель, изображенную на рис. 15.24.
Рис 15 24 а ли а Budget на р
ри о о и сла
св зе
о
единена с та ли а и Customer и Product
Создавая слабые связи, разработчик имеет возможность выбрать направление распространения контекста фильтра. Как и в случае со связью «один ко
многим», слабая связь может быть как однонаправленной, так и двунаправленной. В нашем примере связи должны быть однонаправленными, и контекст
фильтра должен распространяться от таблиц
и
к таблице
. Установка двунаправленных связей внесет в модель данных неоднозначность.
Таблицы на обоих концах слабых связей представляют сторону «многие».
А значит, в столбцах, использующихся для объединения таблиц, могут присут1
гл ленное из ение св зе
551
ствовать дублирующиеся значения. Обновленная модель данных будет работать точно так же, как модель, представленная на рис. 15.23, и получение корректных результатов не потребует от нас написания дополнительного кода на
DAX в мерах или вычисляемых таблицах.
Однако в представленной модели данных присутствует определенная ловушка, о которой читатель должен знать. По причине слабости связи между
таблицами ни в одной из них не будет появляться дополнительная пустая строка в случае недействительности соединения. Иными словами, если в таблице
появятся страна или бренд, не представленные в таблицах
или
, значения по ним будут скрыты в отчете, как и в случае с моделью, показанной на рис. 15.24.
Чтобы продемонстрировать такое поведение, мы немного модифицируем
содержимое таблицы
– заменим Германию (Germany) на Италию (Italy).
В нашей модели нет ни одного покупателя из Италии. Результат отчета, представленный на рис. 15.25, может вас удивить.
Рис 15 25
сли св зь ежд та ли а и Budget и Customer неде ствительна,
рез льтат от ета ог т ть неожиданн и
В строке по Германии наша мера выдает пустое значение. И это вполне естественно, поскольку мы перекинули весь бюджет Германии на Италию. Но вот
что интересно:
„ в таблице отсутствует строка с бюджетом по Италии;
„ итоговое значение по бюджетам превышает сумму двух представленных
в столбце значений.
Фильтр, установленный по столбцу
, распространяется на таблицу
посредством слабой связи. В результате в таблице
остаются видимыми только выбранные страны. А поскольку Италия не представлена в столбце
, значение по ней не выводится. Однако, когда по столбцу
фильтр не установлен, таблица
также оказывается освобождена от фильтров. А значит, в общий итог
будут включены бюджеты по всем ее строкам, включая Италию.
Таким образом, мера
напрямую зависит от присутствия фильтра
по столбцу
, а если связь станет недействительной, результаты могут оказаться очень неожиданными.
Слабые связи являются достаточно мощным инструментом, способствующим проектированию сложных моделей данных без необходимости создавать
вспомогательные таблицы. Но особенность этих связей, состоящая в том, что
таблицы не дополняются пустой строкой для отсутствующих значений, может
приводить к непредсказуемым результатам в отчетах. Сначала мы показали бо552
1
гл ленное из ение св зе
лее сложную технику с созданием вспомогательных таблиц, после чего продемонстрировали решение с применением слабых связей. В целом эти варианты
служат одной цели, разница лишь в том, что в случае с созданием дополнительных таблиц значения, присутствующие только в одной из двух связанных таблиц, будут показаны в отчете, что может быть полезно в некоторых сценариях.
Если мы выполним такую же замену Германии на Италию в модели данных
с таблицами
и
, которая была представлена на рис. 15.23,
вывод отчета будет более понятным.
Рис 15 26
а ена ер ании на тали
о еи стран с корректн и зна ени и
ривела к в вод в от ете
Выбор правильного типа для связи
При создании комплексных моделей данных могут быть использованы связи
разных типов. Работая со сложными сценариями, вы то и дело сталкиваетесь
с непростым выбором между созданием физической связи и виртуальной.
Если говорить в общем, эти разновидности связей служат одной цели: обеспечить распространение контекста фильтра с одной таблицы на другую. Но
с точки зрения производительности и реализации могут быть варианты:
„ и и еска с
ь о ре ел етс
о ели а
, то а как иртуаль а су ест ует только ко е а DAX. На диаграмме модели данных
показаны все физические связи, которыми объединены таблицы. Чтобы
добраться до виртуальных связей, нужно внимательно изучить выражения DAX, используемые в мерах, вычисляемых столбцах и вычисляемых
таблицах. При необходимости использовать логическую связь в разных
мерах вам придется каждый раз дублировать ее код, если речь идет не об
элементах в группах вычислений. С физическими связями работать куда
проще, и они реже становятся причиной ошибок;
„ и и еские с
и акла
а т о ра и е ие а табли у, ре ста л
у сторо у о и . Связи типа «один ко многим» и «один к одному» требуют, чтобы в столбце, находящемся на стороне «один», не было
дубликатов и пустых строк. Если это условие будет нарушено, операция
обновления данных завершится ошибкой. Здесь есть серьезное отличие
по сравнению с ограничениями на внешние ключи в реляционной базе
данных, где столбец на стороне «многие» должен включать в себя только
значения, присутствующие на другой стороне связи. В табличной модели
данных такого ограничения нет;
1
гл ленное из ение св зе
553
и и еска с
ь б стрее иртуаль о . При создании физической
связи движок строит дополнительную структуру данных, помогающую
ускорить выполнение запросов за счет привлечения движка хранилища
данных. Создание виртуальной связи всегда требует дополнительной работы от движка формул, который уступает в скорости подсистеме хранилища данных. Различия между движком формул и движком хранилища
данных будут подробно описаны в главе 17.
В большинстве случаев лучшим выбором будет физическая связь. При этом
в плане производительности нет никакой разницы между обычной связью (основанной на столбцах в источнике данных) и вычисляемой физической связью
(базирующейся на вычисляемых столбцах). Движок рассчитывает значения
в вычисляемых столбцах в момент обработки (когда данные обновляются), так
что сложность выражения большой роли не играет – связь является физической, и движок может использовать все свои ресурсы для расчетов.
Виртуальная связь представляет собой абстрактную концепцию. Фактически каждый раз, когда происходит распространение контекста фильтра с одной таблицы на другую в коде на DAX, между ними создается виртуальная
связь. Такие связи вычисляются в момент выполнения запроса, и у движка нет
никаких дополнительных ресурсов в виде структур, создаваемых для физических связей, чтобы как-то оптимизировать план выполнения запроса. Поэтому
всегда, когда у вас есть такая возможность, отдавайте предпочтение физическим связям в сравнении с виртуальными.
Связи типа «многие ко многим» занимают промежуточную позицию между
физическими связями и виртуальными. Можно определить связь «многие ко
многим» в модели данных при помощи двунаправленной фильтрации или
расширенных таблиц. Чаще всего присутствие связи в модели данных будет
более выгодным решением по сравнению с подходом, основывающимся на
использовании расширенных таблиц, поскольку в этом случае движок получает дополнительные рычаги при оптимизации плана выполнения запроса за
счет отключения распространения фильтров там, где они не нужны. В то же
время варианты с расширением таблиц и использованием двунаправленной
кросс-фильтрации при активном фильтре будут иметь примерно одинаковую
эффективность, хотя чисто технически генерируют совершенно разные планы
выполнения запроса.
С точки зрения производительности вы должны расставлять приоритеты
при выборе типа связи следующим образом:
„ физические связи типа «один ко многим» будут обладать наибольшей
эффективностью и по максимуму задействуют движок VertiPaq. Вычисляемые физические связи будут работать с той же скоростью, что и связи,
основанные на физических столбцах;
„ связи с использованием двунаправленной кросс-фильтрации, связи
«многие ко многим» с расширенными таблицами и слабые связи должны идти на втором месте в списке приоритетов. Они обладают хорошей
производительностью и активно используют движок, пусть и не по максимуму;
„ виртуальные связи замыкают наш список приоритетов по причине
возможной низкой производительности. Заметьте, что вы можете и не
„
554
1
гл ленное из ение св зе
столкнуться с этими проблемами, если будете соблюдать все меры предосторожности и выполнять требования оптимизации, о которых мы поговорим в следующих главах.
Управление гранулярность
Как мы уже упоминали в предыдущих разделах, при помощи вспомогательных таблиц или слабых связей можно осуществить связь между таблицами на
уровне гранулярности ниже первичного ключа. В последнем примере мы связывали таблицу
с таблицами
и
. При этом связь с таблицей
была создана на уровне брендов, а с таблицей
– на уровне
стран.
Если в модели данных присутствуют связи со сниженным уровнем гранулярности, необходимо проявлять осторожность при написании мер, использующих эти связи. На рис. 15.27 представлена исходная модель данных с двумя
слабыми связями типа «многие ко многим» между таблицами
и
с одной стороны и
– с другой.
Рис 15 27
а ли
Customer, Product и Budget о
единен сла
и св з
и
Контекст фильтра распространяется по слабым связям от таблицы к таблице
в соответствии с выбранным уровнем гранулярности. Это утверждение справедливо для любых связей. Фактически между таблицами
и
контекст фильтра также распространяется на уровне гранулярности столбца, по
которому построена связь. В случае если связь базируется на столбце, являющемся первичным ключом в таблице, ее поведение будет интуитивно понятным. Если же гранулярность связи будет ниже, как в случае со слабыми связями, очень легко будет прийти к вычислениям, смысл которых понять будет
затруднительно.
Рассмотрим для примера таблицу
. Связь между ней и таблицей
установлена на уровне бренда. Таким образом, мы можем построить матрицу со срезом меры
по столбцу
и получить осознанные результаты, показанные на рис. 15.28.
1
гл ленное из ение св зе
555
Рис 15 28 Срез та ли с
джето
о ренд в дал равильн е рез льтат
Ситуация осложнится, если в анализ будут вовлечены другие столбцы из таблицы
. В отчете, показанном на рис. 15.29, мы добавили срез, чтобы отфильтровать вывод по нескольким цветам товаров, и вывели цвет в столбцы
матрицы. Результат оказался неожиданным.
Рис 15 29
Срез
джета о ренд и вет товаров ривел к неожиданн
рез льтата
Обратите внимание, что по брендам значения – там, где они есть, – выводятся одинаковые вне зависимости от фильтра по цвету. При этом итоговые
значения по цветам отличаются, а общий итог явно не соответствует сумме
итогов по цветам.
Чтобы понять, что произошло с цифрами, построим упрощенную версию
матрицы без учета брендов. На рис. 15.30 показан отчет, в котором выведена
мера
со срезом только по столбцу
.
Взгляните на цифру бюджета по товарам синего цвета (Blue). В начале вычисления этой ячейки контекст фильтра по таблице
содержал значение Blue. Но у нас не во всех брендах присутствуют синие товары. Например,
в бренде The Phone Company не представлено ни одного товара синего цвета,
556
1
гл ленное из ение св зе
как видно по рис. 15.29. Таким образом, столбец
попал под действие перекрестного фильтра по столбцу
, и в результирующий
набор вошли все бренды, за исключением бренда The Phone Company. Распространение контекста фильтра на таблицу
производится на уровне гранулярности
. Таким образом, после применения фильтра таблица
будет включать все бренды, за исключением The Phone Company.
Рис 15 30 Срез только о вет озволит л
то роис одит с е ка и в от ете
е он ть,
Получившееся значение является суммой по всем брендам, кроме The Phone
Company. При переходе по связи информация о цвете товаров была утеряна.
Связь между
и
была использована во время перекрестной фильтрации
по
, но при этом фильтр таблицы
выполняется исключительно по столбцу
. Иначе говоря, в каждой ячейке мы видим сумму
по всем брендам, в которых есть как минимум один товар выбранного цвета.
Такое поведение отчета может понадобиться очень редко. Есть несколько сценариев, когда такие расчеты будут восприниматься как правильные, но в большинстве случаев пользователь будет ожидать несколько иные цифры.
Проблема будет проявляться всякий раз, когда пользователь будет анализировать агрегацию значений на уровне гранулярности, не соответствующем
уровню гранулярности связи. Хорошей практикой является скрытие значений
в случаях, когда выбранная гранулярность не поддерживается связью. И здесь
возникает вопрос: как определить, что выводимые значения в отчете находятся на правильном уровне гранулярности? Чтобы ответить на него, создадим
еще несколько мер.
Мы начали с построения матрицы, содержащей бренды (правильная гранулярность) и цвета товаров (неправильная гранулярность). Добавим в отчет
также меру
, показывающую количество строк в таблице
в рамках текущего контекста фильтра:
NumOfProducts :=
COUNTROWS ( 'Product' )
Результат можно видеть в отчете, показанном на рис. 15.31.
Ключом к решению этого сценария является дополнительная мера
. На строке с брендом A. Datum мера
показывает количество видимых товаров 132, что соответствует общему количеству товаров
бренда A. Datum. При дальнейшей фильтрации отчета по цвету товара (или
любому другому столбцу) количество видимых товаров будет уменьшаться.
Значения из таблицы
при этом имеют смысл, только если все 132 товара видимы. Если товаров в выборе меньше, значение меры
будет бессмысленным. Следовательно, нужно скрывать меру
, когда
количество видимых товаров в точности не соответствует количеству товаров
в выбранном бренде.
1
гл ленное из ение св зе
557
Рис 15 31
на ение ер Budget Amt равильно в
и не равильно дл ветов
исл етс дл
рендов
Мера, в которой подсчитываются товары на уровне гранулярности брендов,
представлена ниже:
NumOfProducts Budget Grain :=
CALCULATE (
[NumOfProducts];
ALL ( 'Product' );
VALUES ( 'Product'[Brand] )
)
В данном случае необходимо использовать связку функций
/
вместо
. Разницу между этими двумя подходами мы описывали
в главе 10. Теперь достаточно перед выводом меры
сравнить две
наши новые меры. Если их значения будут равны, можно выводить меру
, если нет – пустое значение, в результате чего строка будет скрыта в отчете. Реализация этого плана показана в мере
:
Corrected Budget :=
IF (
[NumOfProducts] = [NumOfProducts Budget Grain];
[Budget Amt]
)
На рис. 15.32 представлен полный отчет со всеми созданными мерами. При
этом в мере
значения скрыты в строках, где гранулярность отчета не соответствует гранулярности таблицы
.
Этот же шаблон может быть применен и к таблице
с установкой
гранулярности на уровне столбца
. Подробнее об этом шаблоне
можно почитать по адресу https://www.daxpatterns.com/budget-patterns/.
558
1
гл ленное из ение св зе
Рис 15 32
на ени в ере Corrected Budget скр т ,
когда е ка оказана на несов ести о ровне гран л рности
Всякий раз, когда вы используете связи, построенные на основании уровня гранулярности, отличного от первичного ключа таблицы, необходимо осуществлять дополнительную проверку в мерах, чтобы не показывать их значения на несовместимых уровнях гранулярности. Наличие в модели данных
слабых связей требует повышенного внимания к таким нюансам.
Возникновение неоднозначностей в связях
Говоря о связях, не стоит забывать о возможном появлении неодно на ности
(ambiguity) в модели данных. Неоднозначность возникает в случае, если от
одной таблицы к другой можно добраться несколькими путями, и, к сожалению, в больших моделях данных такие ситуации бывает очень трудно распознать.
Легче всего получить неоднозначность в модели данных можно, создав
более одной связи между двумя таблицами. Например, в таблице
у нас
хранятся две даты: дата заказа и дата поставки. Если создать между таблицами
и
две связи на основании двух этих столбцов, одна из них будет
неактивной. На рис. 15.33 такая связь между таблицами
и
показана
пунктирной линией.
Если бы обе связи одновременно были активными, модель данных стала бы
неоднозначной. Иными словами, движок DAX не знал бы, по какой из двух связей распространять фильтры при переходе от таблицы
к
.
Когда речь идет всего о двух таблицах, неоднозначность модели данных
очень легко обнаружить и понять. Но с ростом количества таблиц делать это
становится все труднее. Движок сам следит за тем, чтобы при создании модели
данных в ней не было никаких неоднозначностей. При этом он пользуется до1
гл ленное из ение св зе
559
вольно сложными алгоритмами, которые человеку понять не так просто. В результате иногда движок не усматривает неопределенностей в моделях, в которых они на самом деле присутствуют.
Рис 15 33
Межд дв
та ли а и активно
ожет
ть только одна св зь
Давайте рассмотрим модель данных, показанную на рис. 15.34. Перед тем
как читать дальше, внимательно вглядитесь в эту модель и ответьте на вопрос,
является ли она неоднозначной.
Рис 15 34
сть ли в то одели данн неоднозна ность
С ожет ли разра от ик создать так
одель, или движок в даст о и к
Ответ на этот вопрос сам по себе неоднозначный и звучит так: эта модель
содержит неоднозначность для человека, но не для движка DAX. И все же это
не самая удачная модель данных, поскольку ее достаточно сложно анализировать. Но для начала разберемся, где в ней скрывается неоднозначность.
560
1
гл ленное из ение св зе
Заметьте, что для связи между таблицами
и
установлена двунаправленная кросс-фильтрация, а это значит, что контекст фильтра может быть
передан от таблицы
к
и далее к
. Теперь посмотрите на
таблицу
. Мы можем легко распространить контекст фильтра от нее через
и
на таблицу
. В то же время фильтр из таблицы
может
добраться до
и по непосредственной связи между этими двумя таблицами. Таким образом, эта модель данных будет считаться неоднозначной, поскольку в ней существует больше одного пути для распространения контекста
фильтра между двумя таблицами.
Несмотря на это, создавать и использовать подобные модели данных в DAX
вполне допустимо, поскольку движок располагает определенными правилами
для снижения количества неоднозначностей, встречаемых в моделях. В данном случае будет применено правило о распространении контекста фильтра
между этими двумя таблицами по кратчайшему пути. Так что DAX позволит
создать такую модель данных, невзирая на наличие в ней неоднозначности.
Но это отнюдь не означает, что нужно создавать и работать с такими моделями. Наоборот, это является плохой практикой, и мы настоятельно советуем
вам отказаться от использования моделей данных, в которых есть неоднозначности.
Но ситуация с неоднозначностями может быть еще более запутанной. Неоднозначности в моделях могут появляться как в результате создания связей
между таблицами, так и при выполнении кода DAX, если разработчик использует в функции
такие модификаторы, как, например,
или
. У вас может быть мера, которая прекрасно работает. Но при обращении к ней внутри другой меры, использующей для
активации нужной связи функцию
, она вдруг начинает выдавать
неправильные цифры. Причиной может быть неоднозначность, появившаяся
в модели данных как раз из-за применения функции
. Но мы не
собираемся пугать наших читателей, мы просто хотим предостеречь их в связи
с теми сложностями, которые могут возникать, когда речь заходит о неоднозначностях в модели данных.
Появление неоднозначностей в активных связях
Наш первый пример будет базироваться на модели данных, представленной
на рис. 15.34. В матрице со срезом по годам мы выведем меры
и
(простые вычисления с использованием итератора
).
Результат этого отчета показан на рис. 15.35.
Рис 15 35 Стол е Calendar Year ильтр ет та ли
но с ис ользование каки св зе
1
Receipts,
гл ленное из ение св зе
561
От таблицы
до
можно добраться двумя способами:
„ напрямую (по связи, объединяющей эти две таблицы);
„ в обход, используя путь от
к
, далее от
к
и наконец
от
к
.
Представленная модель данных не считается неоднозначной, поскольку
движок DAX всегда может выбрать кратчайший путь для распространения контекста фильтра между таблицами. Располагая прямой связью между
и
, движок игнорирует все остальные пути между этими таблицами. Если же
кратчайший путь оказывается недоступен, приходится задействовать обходные
пути. Посмотрите, что произойдет, если создать новую меру, вычисляющую меру
после деактивации прямой связи между таблицами
и
:
Rec Amt Longer Path :=
CALCULATE (
[Receipts Amt];
CROSSFILTER ( 'Date'[Date]; Receipts[Sale Date]; NONE )
)
В мере
связь, установленная между таблицами
и
, разрывается, в результате чего движку приходится идти обходным путем. Результат вычисления новой меры показан на рис. 15.36.
Рис 15 36
ере Rec Amt Longer Path ис ольз етс длинн
дл ильтра ии та ли Receipts из та ли Date
ть
Остановитесь и подумайте, что будут характеризовать цифры, полученные
при вычислении меры
. Пока не сделаете предположение, не
читайте дальше, поскольку в следующих абзацах мы дадим правильный ответ.
Фильтр стартует из таблицы
и переходит в таблицу
. Оттуда он распространяется на
. В результате этой фильтрации мы получим список
товаров, которые продавались в выбранные даты. Иначе говоря, для 2007 года
в фильтр попадут только те товары, которые продавались в этом году. После
этого фильтр переходит в таблицу
. Таким образом, результат вычисления данной меры будет отражать общую сумму по таблице
по всем
товарам, которые продавались в конкретном году. Далеко не самое интуитивно понятное значение.
Наиболее сложным нюансом в приведенной выше формуле является установка в функции
режима
. Вам может показаться, что этот
код просто деактивирует имеющуюся связь. На самом деле деактивация одной
связи автоматически активирует альтернативный путь. Так что в этой мере
не просто удаляется связь между таблицами, но и активируются другие связи,
явно не указанные в формуле.
562
1
гл ленное из ение св зе
В этом сценарии неоднозначность в модель данных вносится по причине
установки двунаправленной кросс-фильтрации для связи между таблицами
и
. Наличие двунаправленных связей в модели данных – очень
скользкий путь, часто ведущий к образованию неоднозначностей, которые
фиксируются движком DAX, но могут остаться незамеченными для разработчика. После многих лет работы с DAX мы можем ответственно заявить, что наличия двунаправленной кросс-фильтрации в модели данных стоит избегать
любыми способами, если только плюсы от ее установки не перевешивают все
известные минусы. А в тех редких сценариях, где присутствие таких связей
оправдано, мы настоятельно рекомендуем несколько раз проверить модель
данных на наличие неоднозначностей. Более того, эту проверку стоит повторять каждый раз, когда в модели появляется новая таблица или связь. А проводить такую проверку в модели данных с количеством таблиц, превышающим
50, достаточно утомительно. Избежать же подобной участи можно, избавившись от присутствия связей с двунаправленной кросс-фильтрацией на этапе
проектирования модели.
Устранение неоднозначностей в неактивных связях
Несмотря на то что двунаправленные связи действительно являются главной
причиной возникновения неоднозначностей в моделях данных, не они одни
виноваты в их появлении. Фактически разработчик может спроектировать
идеальную модель данных без намеков на присутствие неоднозначностей, а во
время выполнения запросов эти неоднозначности начнут себя обнаруживать.
Взгляните на модель данных, представленную на рис. 15.37. В ней нет никаких неоднозначностей.
Рис 15 37
то
деактивирована
одели неоднозна носте нет, оскольк
отен иально о асна св зь
Обратите внимание на таблицу
. Она фильтрует таблицу
посредством единственной активной связи (между столбцами
и
).
При этом всего между этими таблицами может быть две связи, одна из которых
1
гл ленное из ение св зе
563
была деактивирована, чтобы избежать образования неоднозначности. Также
в модели присутствует связь между таблицами
и
на основании
столбца
, и эту связь пришлось сделать неактивной. Если позже мы активируем ее, то получим второй путь для распространения контекста
фильтра между таблицами
и
, что автоматически внесет элемент неоднозначности в модель данных. Таким образом, наша модель данных работает
корректно, поскольку использует исключительно активные связи. Но что произойдет, если явным образом активировать одну или более неактивных связей
внутри функции
? Модель тут же станет неоднозначной. Например,
в следующей мере мы активируем связь между таблицами
и
:
First Date Sales :=
CALCULATE (
[Sales Amount];
USERELATIONSHIP ( Customer[FirstSale]; 'Date'[Date] )
)
Поскольку модификатор
делает связь между календарем
и покупателями активной, внутри функции
модель становится неоднозначной. А поскольку движок DAX не может работать с моделью, в которой присутствует неоднозначность, он вынужден деактивировать другие связи. В результате он отказывается от выполнения фильтрации по кратчайшему
пути, которым является прямая связь между таблицами
и
. Получается, что во избежание проявления неоднозначностей движок по умолчанию
использует кратчайший путь при выполнении фильтрации, но при явном
указании активировать связь между таблицами
и
при помощи
функции
решает деактивировать связь между таблицами
и
.
Применение функции
привело к тому, что движок отказался от использования прямой связи между таблицами
и
. Вместо
этого он распространил контекст фильтра сначала с таблицы
на
,
а затем добрался до таблицы
. Соответственно, при выборе покупателя
и периода эта мера будет показывать сумму всех транзакций по нему, но только в строке с датой первой покупки этого клиента. Вывод данной меры показан
на рис. 15.38.
Рис 15 38 Мера First Date Sales
оказ вает все родажи клиент ,
рас олага и на дате ерво ок
564
1
гл ленное из ение св зе
ки
В мере
всегда будет показываться сумма продаж из таблицы
по конкретному клиенту, при этом значения в датах, не соответствующих
дате первой его покупки, останутся пустыми. В плане бизнес-аналитики эта
мера пригодится для выполнения проекции по будущим продажам конкретному клиенту на дату его прихода. И хотя кто-то найдет смысл в таком разрезе
анализа, вряд ли этот отчет будет отвечать вашим требованиям.
Здесь мы не ставим себе цель разобраться в том, как именно движок решает проблемы с возникновением неоднозначности в модели данных. Правила,
которыми руководствуется DAX в этом случае, никогда не были документированы, а значит, со временем они могут измениться. Настоящей проблемой
является то, что неоднозначность может проявляться в моделях данных, изначально лишенных всяких намеков на неоднозначность, при активации ранее
неактивных связей. Ну а понимание того, какой именно путь для распространения контекста фильтра выберет движок, чтобы устранить неоднозначность,
связано больше с догадками, чем с точными науками.
Когда речь заходит о связях и неоднозначностях, лучше всего отдавать предпочтение максимальной простоте. В DAX заложены сложные алгоритмы устранения неоднозначностей в моделях данных, и движку по силам решить эту
задачу почти для каждой модели. Для того чтобы вызвать ошибку времени выполнения, связанную с неоднозначностью модели данных, достаточно одновременно использовать несколько функций
. Только в этом
случае движок выдаст ошибку. Например, в следующий код меры изначально
заложена неоднозначность:
First Date Sales ERROR :=
CALCULATE (
[Sales Amount];
USERELATIONSHIP ( Customer[FirstSale]; 'Date'[Date] );
USERELATIONSHIP ( 'Date'[Date]; Sales[Date] )
)
В данном случае, активировав обе связи, DAX просто не сможет избавить
модель от неоднозначности и выдаст ошибку. Несмотря на это, сама мера может быть без проблем определена в модели данных. Ошибка проявится только
в момент вычисления меры с фильтром по дате.
В данном разделе мы не старались описать все доступные опции моделирования данных. Мы лишь хотели обратить ваше внимание на проблемы, которые могут проявляться в случае использования неправильно спроектированной модели. Построить идеальную модель данных очень нелегко. Но важно
помнить, что использование двунаправленной кросс-фильтрации и неактивных связей без полного понимания возможных последствий – это первый шаг
к проектированию непредсказуемой модели данных.
Закл чение
Связи являются важной частью моделирования данных. В табличной модели
данных представлены три типа связей: «один ко многим», «один к одному»
1
гл ленное из ение св зе
565
и слабые связи «многие ко многим». При этом название «многие ко многим»,
применяемое в пользовательских интерфейсах некоторых программ, может
сбивать с толку и идти вразрез с концепцией моделирования данных. Каждая
связь может способствовать распространению фильтра от таблицы к таблице
как в одном направлении, так и в обоих. Исключение составляют связи «один
к одному», по которым контекст фильтра всегда передается в обе стороны.
Существующие инструменты могут быть расширены в области логического
моделирования данных за счет построения вычисляемых физических связей
или виртуальных связей с помощью функций
и
, а также
расширения таблиц. Связи типа «многие ко многим» между сущностями могут
быть реализованы посредством использования таблиц-мостов и полагаться на
двунаправленную кросс-фильтрацию, примененную к связям в цепочке.
Все описанные в данной главе концепции являются очень мощными, но при
этом таят в себе опасность. Создание связей между таблицами требует повышенного внимания. Разработчик должен постоянно тщательно проверять модель данных на предмет наличия неоднозначностей, а также следить за тем,
чтобы неоднозначности не появлялись вследствие использования функций
и
.
Чем больше модель данных, тем больше вероятность допущения ошибок.
Если в модели присутствуют неактивные связи, вы должны четко понимать,
почему именно они неактивны и что произойдет в момент их активации.
Тщательная работа на этапе проектирования модели позволит вам в будущем
облегчить написание выражений на языке DAX, тогда как плохо продуманная
схема данных будет доставлять разработчику в процессе взаимодействия с ней
немало проблем.
ГЛ А В А 16
Вычисления повышенной
сложности в DAX
В последней главе, посвященной языку DAX, и перед тем, как перейти к вопросам оптимизации, мы хотим показать вам несколько примеров реализации
сложных вычислений. Здесь, как и раньше, мы не ставим себе цель показать
работающие шаблоны, которые вы можете без изменений использовать в своих проектах, – такие шаблоны можно найти по адресу https://www.daxpatterns.
com. Вместо этого мы продемонстрируем вам процесс написания формул разной степени сложности – это позволит вам еще на шаг приблизиться к тому,
чтобы думать на языке DAX.
DAX требует от разработчика творческого мышления. Теперь, когда вы узнали все секреты этого языка, пришло время применить свои знания на практике. Начиная со следующей главы мы будем подробно говорить об оптимизации
вычислений, а здесь затронем тему производительности меры и посмотрим,
как можно определить сложность той или иной формулы.
При этом пока мы не будем стремиться к идеальной производительности
наших мер, поскольку для этого необходимо обладать знаниями, которые вы
приобретете в следующих главах. Здесь же мы попробуем получать одни и те
же результаты, используя при этом разные формулы, и параллельно оценивать их сложность. Когда мы начнем разбираться с оптимизацией кода, умение
формулировать одни и те же вычисления по-разному очень вам пригодится.
Подсчет количества рабочих дней
между двумя датами
Если у вас есть две даты, вы легко и просто можете узнать разницу между ними
в днях при помощи обычной операции вычитания. В таблице
у нас есть
два столбца с датами: в одном хранится дата заказа, в другом – дата поставки.
Среднее количество дней, требуемое для поставки товара заказчику, можно
вычислить по следующей формуле:
Avg Delivery :=
AVERAGEX (
Sales;
INT ( Sales[Delivery Date] - Sales[Order Date] + 1)
)
1
ислени
ов
енно сложности в
567
Поскольку внутренне даты хранятся в виде целых чисел, представляющих
дни, формула покажет правильный результат. При этом будет несправедливо
говорить, что на поставку заказа, который был оформлен в пятницу, а привезен в понедельник, ушло три дня, если суббота и воскресенье были выходными
днями. Фактически в этом случае на поставку ушел всего один день, как если
бы заказ был оформлен в понедельник, а доставлен во вторник. Так что правильнее было бы говорить о разнице в рабочих днях между двумя датами. Мы
представим вам сразу несколько версий подобного расчета и попытаемся выбрать лучший из них в плане эффективности и гибкости.
В Excel существует специальная функция для этих целей: ЧИСТРАБДНИ
(NETWORKDAYS). В DAX, к сожалению, аналога этой функции не существует.
Зато в этом языке представлено множество других функций, которые можно
использовать как строительные блоки при написании сложных вычислений,
подобных этому. Для начала решим эту задачу путем простого подсчета количества рабочих дней между двумя датами, исключив выходные дни:
Avg Delivery WD :=
AVERAGEX (
Sales;
VAR RangeOfDates =
DATESBETWEEN (
'Date'[Date];
Sales[Order Date];
Sales[Delivery Date]
)
VAR WorkingDates =
FILTER (
RangeOfDates;
NOT ( WEEKDAY ( 'Date'[Date] ) IN { 1; 7 } )
)
VAR NumberOfWorkingDays =
COUNTROWS ( WorkingDates )
RETURN
NumberOfWorkingDays
)
Для каждой строки в таблице
формула создает временную таблицу
в переменной
, где хранятся все даты между датой заказа и датой поставки. После этого происходит отсев субботних и воскресных дней
с записью результата в таблицу
, а затем в переменную
помещается результат подсчета строк в получившейся таблице. На
рис. 16.1 показан график, на котором отчетливо видна разница между средними сроками поставки товаров с учетом и без учета рабочих дней.
Мера работает правильно, но в ней есть сразу несколько недостатков. Вопервых, она не учитывает праздничные дни. Например, даже если исключить
из расчета субботы и воскресенья, 1 января все равно будет считаться рабочим днем, если он не выпадает на выходные. То же самое можно сказать и об
остальных праздниках в году. Кроме того, и производительность этой меры
оставляет желать лучшего.
568
1
ислени
ов
енно сложности в
Рис 16 1 Средние сроки оставки в дн
и в ра о и дн
отли а тс
Чтобы учитывать в расчете праздничные дни, необходимо прежде всего обладать информацией о том, какие дни в году являются праздничными. И лучше
всего ее хранить в вычисляемом столбце
прямо в таблице
. После
этого новый столбец можно включить в нашу формулу следующим образом:
Avg Delivery WD DT :=
AVERAGEX (
Sales;
VAR RangeOfDates =
DATESBETWEEN (
'Date'[Date];
Sales[Order Date];
Sales[Delivery Date]
)
VAR NumberOfWorkingDays =
CALCULATE (
COUNTROWS ( 'Date' );
RangeOfDates;
NOT ( WEEKDAY ( 'Date'[Date] ) IN { 1; 7 } );
'Date'[Is Holiday] = 0
)
RETURN
NumberOfWorkingDays
)
Чтобы не использовать слишком много условий в выражении, можно объединить информацию о том, является ли конкретный день рабочим, в одном
столбце со значениями
или
. Это снизит сложность меры
и сместит логику ближе к данным, что повысит гибкость формулы.
Что касается процесса выполнения формулы, то он состоит из следующих
шагов:
„ запуск итераций по таблице
1
;
ислени
ов
енно сложности в
569
„ создание для каждой строки в таблице
временной таблицы со всеми
датами в промежутке между датой заказа и датой поставки.
Если в таблице
миллион записей, а средний срок поставки равен семи
дням, сложность меры исчисляется примерно семью миллионами. Это значит,
что движку DAX необходимо в процессе вычисления формулы миллион раз
создавать таблицу с семью строками.
Сложность расчета можно снизить, либо уменьшив количество итераций
во внешней функции
, либо снизив количество строк во временной
таблице с рабочими днями. Стоит отметить, что нет никакой необходимости
производить это вычисление для каждой строки в таблице заказов, поскольку у всех заказов с одинаковыми комбинациями значений в столбцах
и
будет один и тот же срок поставки. Так что можно сначала
сгруппировать таблицу
по этим столбцам, после чего вычислить значение
для каждой пары значений. Тем самым мы сможем снизить число итераций во
внешней функции
, но при этом утратим информацию о том, сколько именно заказов представлено в каждой комбинации дат. Эту проблему можно решить, если считать не обычные средние значения, а средневзвешенные,
используя количество заказов в качестве веса.
Эта идея может быть реализована следующим образом:
Avg Delivery WD WA :=
VAR NumOfAllOrders =
COUNTROWS ( Sales )
VAR CombinationsOrderDeliveryDates =
SUMMARIZE (
Sales;
Sales[Order Date];
Sales[Delivery Date]
)
VAR DeliveryWeightedByNumOfOrders =
SUMX (
CombinationsOrderDeliveryDates,
VAR RangeOfDates =
DATESBETWEEN (
'Date'[Date];
Sales[Order Date];
Sales[Delivery Date]
)
VAR NumOfOrders =
CALCULATE (
COUNTROWS ( Sales )
)
VAR WorkingDays =
CALCULATE (
COUNTROWS ( 'Date' );
RangeOfDates;
NOT ( WEEKDAY ( 'Date'[Date] ) IN { 1; 7 } );
'Date'[Is Holiday] = 0
)
VAR NumberOfWorkingDays = NumOfOrders * WorkingDays
570
1
ислени
ов
енно сложности в
RETURN
NumberOfWorkingDays
)
VAR AverageWorkingDays =
DIVIDE (
DeliveryWeightedByNumOfOrders;
NumOfAllOrders
)
RETURN
AverageWorkingDays
Этот код уже не так прост для восприятия. И здесь возникает резонный вопрос: стоит ли так усложнять код ради повышения его производительности?
Как и всегда, ответ на него зависит от множества факторов. Перед тем как
приступать к оптимизации, всегда полезно провести несколько тестов, чтобы
определить, на самом ли деле количество итераций в формуле снизилось. В нашем случае для этого достаточно запустить следующий запрос, возвращающий
общее количество продаж и число уникальных комбинаций значений столбцов
и
:
EVALUATE
{ (
COUNTROWS ( Sales ),
COUNTROWS (
SUMMARIZE (
Sales,
Sales[Order Date],
Sales[Delivery Date]
)
)
) }
-- Результат:
--- Value1 | Value2
--------------------- 100231 |
6073
В нашей демонстрационной модели данных оказалась 100 231 строка в таблице
и всего 6073 уникальные комбинации из даты заказа и даты поставки. Таким образом, в наиболее сложной части формулы меры
нам удалось более чем на порядок снизить количество обрабатываемых
записей. А значит, написание этого кода будет вполне оправданным шагом
в плане оптимизации. В следующих главах вы научитесь точно определять, как
то или иное улучшение влияет на время выполнения запроса. Сейчас же мы
будем говорить только о сложности кода.
В нашем случае сложность меры
зависит от количества
комбинаций даты заказа и даты поставки, а также от среднего срока поставки
заказов. Если последний показатель будет находиться в пределах нескольких
дней, наша мера будет вычисляться очень быстро. И наоборот, если средний
срок поставки заказов у нас исчисляется годами, снижение эффективности
1
ислени
ов
енно сложности в
571
меры будет налицо, ведь функция
в нашей формуле будет возвращать таблицы из нескольких сотен строк.
Поскольку выходных дней в календаре гораздо меньше, чем рабочих, можно
считать именно их. С этим допущением алгоритм может быть таким:
1) вычислить разницу в днях между двумя датами;
2 подсчитать количество нерабочих дней в интервале;
3 вычесть из результата, полученного на первом шаге, результат второго.
Этот алгоритм можно реализовать следующим образом:
Avg Delivery WD NWD :=
VAR NonWorkingDays =
CALCULATETABLE (
VALUES ( 'Date'[Date] );
WEEKDAY ( 'Date'[Date] ) IN { 1; 7 };
ALL ( 'Date' )
)
VAR NumOfAllOrders =
COUNTROWS ( Sales )
VAR CombinationsOrderDeliveryDates =
SUMMARIZE (
Sales;
Sales[Order Date];
Sales[Delivery Date]
)
VAR DeliveryWeightedByNumOfOrders =
CALCULATE (
SUMX (
CombinationsOrderDeliveryDates;
VAR NumOfOrders =
CALCULATE (
COUNTROWS ( Sales )
)
VAR NonWorkingDaysInPeriod =
FILTER (
NonWorkingDays;
AND (
'Date'[Date] >= Sales[Order Date];
'Date'[Date] <= Sales[Delivery Date]
)
)
VAR NumberOfNonWorkingDays =
COUNTROWS ( NonWorkingDaysInPeriod )
VAR DeliveryWorkingDays =
Sales[Delivery Date] - Sales[Order Date] - NumberOfNonWorkingDays + 1
VAR NumberOfWorkingDays =
NumOfOrders * DeliveryWorkingDays
RETURN
NumberOfWorkingDays
)
)
VAR AverageWorkingDays =
572
1
ислени
ов
енно сложности в
DIVIDE (
DeliveryWeightedByNumOfOrders;
NumOfAllOrders
)
RETURN
AverageWorkingDays
В модели данных, используемой для этой книги, представленный код выполняется медленнее, чем предыдущий. При этом он может быть более эффективным в других моделях, где сроки поставки заказов больше. Решить, какой
подход лучше использовать в конкретном случае, можно только эмпирически.
Почему в переменной on or ingDays используется ALL?
оследне ри ере ри в ислении ере енно NonWorkingDays ри ен етс
нки ALL к та ли е Date ри то ранее
не ри егали к то
нк ии в о ожи
та ли а , котор е озже ис ользовались в ка естве ильтров ри ина в то , то в ред д и варианта ер
ользовались нк ие DATESBETWEEN, котора са а о
се е игнорир ет контекст ильтра
ри ор ировании атри
та ли а Date ожет
ть от ильтрована дл анализа
сокра енного ериода
то сл ае дл заказов с дато о ор лени за редела и
в ранного интервала зна ени
д т расс итан неверно то
того из ежать,
еред одс ето нера о и дне из авл е с от тек его контекста ильтра о та лие Date.
ри то стоит от етить, то ис ользование нк ии ALL здесь вовсе не о зательно
ава те расс отри след
и вариант ере енно
CALCULATETABLE (
VALUES ( 'Date'[Date] );
NOT ( WEEKDAY ( 'Date'[Date] ) IN { 1; 7 } )
)
десь
из авились от ри енени в CALCULATE нк ии ALL все же ALL рис тств ет в то в ражении в не вно виде, и то легко за етить, если рас ирить ор л
до ее олного варианта
CALCULATETABLE (
VALUES ( 'Date'[Date] );
FILTER (
ALL ( 'Date'[Date] );
NOT ( WEEKDAY ( 'Date'[Date] ) IN { 1; 7 } )
)
)
оскольк та ли а Date о е ена в одели данн
как та ли а с дата и, движок
дет авто ати ески ри ен ть к не
нк и ALL.
от
огли
оставить в коде такое в ражение,
не отели, то
на а
ор ла в гл дела таинственно и
ла сложно дл они ани
о то
ред о ли
олее ногословн , но олее легки дл тени вариант
Также стоит помнить, что одним из лучших средств оптимизации является
предварительный расчет значений. Мы знаем, что количество рабочих дней
1
ислени
ов
енно сложности в
573
между двумя датами всегда будет одинаковым вне зависимости от алгоритма расчета. Мы также знаем, что в нашей модели данных есть порядка шести
тысяч уникальных комбинаций даты заказа и даты поставки. И ничто не мешает нам заранее рассчитать количество рабочих дней в этих комбинациях,
сохранив результат в физической скрытой таблице. Таким образом мы сможем
избавиться от необходимости проводить этот расчет во время выполнения запроса – достаточно будет простой операции поиска.
Скрытую таблицу, о которой мы сказали, можно создать следующим образом:
WD Delta =
ADDCOLUMNS (
SUMMARIZE (
Sales;
Sales[Order Date];
Sales[Delivery Date]
);
"Duration"; [Avg Delivery WD WA]
)
Создав физическую таблицу, мы можем воспользоваться всеми бонусами
предварительно рассчитанных значений, изменив нашу формулу так:
Avg Delivery WD WA Precomp :=
VAR NumOfAllOrders =
COUNTROWS ( Sales )
VAR CombinationsOrderDeliveryDates =
SUMMARIZE (
Sales;
Sales[Order Date];
Sales[Delivery Date]
)
VAR DeliveryWeightedByNumOfOrders =
SUMX (
CombinationsOrderDeliveryDates;
VAR NumOfOrders =
CALCULATE (
COUNTROWS ( Sales )
)
VAR WorkingDays =
LOOKUPVALUE (
'WD Delta'[Duration];
'WD Delta'[Order Date]; Sales[Order Date];
'WD Delta'[Delivery Date]; Sales[Delivery Date]
)
VAR NumberOfWorkingDays = NumOfOrders * WorkingDays
RETURN
NumberOfWorkingDays
)
VAR AverageWorkingDays =
DIVIDE (
DeliveryWeightedByNumOfOrders;
574
1
ислени
ов
енно сложности в
NumOfAllOrders
)
RETURN
AverageWorkingDays
Трудно представить, что такой серьезный уровень оптимизации может потребоваться при простом расчете разницы в рабочих днях между двумя датами. Но мы и не пытались как-то особенно оптимизировать эту меру. Вместо
этого мы хотели показать вам разные способы достижения одних и тех же результатов: от наиболее интуитивно понятного до сложного, с элементами оптимизации, который вряд ли пригодится вам в большинстве сценариев.
Данные о продажах и б джетировании
в одном отчете
Рассмотрим модель данных, содержащую сведения о бюджетировании на текущий год вместе с фактическими продажами. В начале года нам известны
только бюджеты по месяцам. С течением года начинают появляться цифры
фактических продаж, и нам становится интересно, во-первых, сравнить их
с данными прогнозов, а во-вторых, скорректировать цифры на следующие месяцы с учетом накопленной информации по продажам.
Чтобы смоделировать сценарий, мы удалили все продажи в базе после 15 августа 2009 года и создали таблицу
, содержащую цифры по бизнес-планированию на весь год. Посмотреть результат можно в отчете, показанном на
рис. 16.2.
Рис 16 2
родажи остались о авг ст,
а рогноз сделан на весь год
Зададимся следующим вопросом. Фактические продажи с начала года по
15 августа составили около 24 млн. Какими будут итоговые продажи за год,
если принять во внимание как фактические данные, так и прогнозируемые?
1
ислени
ов
енно сложности в
575
Учтите, что данные о продажах у нас есть только по 15 августа, а значит, в этом
месяце нам придется сочетать факт с прогнозом.
Сначала запишем в переменную дату последней продажи в модели данных.
Использовать простую функцию
здесь не получится, поскольку данные
о продажах у нас содержатся в базе не по текущий день. Правильнее будет найти последнюю дату продаж в таблице
. Функция
прекрасно сработает,
но при этом важно помнить, что на результат может повлиять пользовательский выбор. Например, рассмотрим следующую меру:
LastDateWithSales := MAX ( 'Sales'[OrderDateKey] )
По разным брендам, а в общем случае и по любым другим выбранным
фильтрам эта мера должна показывать свою дату. Это видно на рис. 16.3.
Рис 16 3
о каждо
ренд
есть сво оследн дата родажи
Правильно будет удалять все наложенные фильтры перед расчетом последней даты продаж. Таким образом мы сможем получить нужную нам дату 15 августа 2009 года. Если по какому-либо из брендов не было продаж 15 августа,
будет использоваться нулевое значение, а не бюджет по последней существующей дате продажи для этого дня. Таким образом, корректной формулой для
расчета меры
будет следующая:
LastDateWithSales :=
CALCULATE (
MAX ( 'Sales'[OrderDateKey] );
ALL ( Sales )
)
Удалив фильтры с таблицы
(то есть с ее расширенной версии), мы тем
самым сказали мере игнорировать любые фильтры, приходящие из запроса,
а значит, возвращаемым значением в нашем случае всегда будет 15 августа
2009 года. Теперь нам необходимо написать формулу, которая будет возвращать
значение меры
для всех дат, предшествующих последней дате продажи, и
– для последующих. Вот простая реализация такой меры:
576
1
ислени
ов
енно сложности в
Adjusted Budget :=
VAR LastDateWithSales =
CALCULATE (
MAX ( Sales[OrderDateKey] );
ALL ( Sales )
)
VAR AdjustedBudget =
SUMX (
'Date';
IF (
'Date'[DateKey] <= LastDateWithSales;
[Sales Amount];
[Budget Amt]
)
)
RETURN AdjustedBudget
На рис. 16.4 показан вывод новой меры
Рис 16 4
ере Adjusted Budget ис ольз етс
.
акт или лан в зависи ости от дат
На данном этапе мы можем вычислить сложность полученной меры. Внешние итерации в формуле выполняются посредством функции
по таблице
. За год проходит 365 циклов. На каждой итерации формула проходит в зависимости от даты либо по таблице
, либо по
, сопровождая свои
действия преобразованием контекста. Было бы неплохо уменьшить общее
количество итераций, а заодно и число повторных преобразований контекста
и/или расчетов агрегаций в больших по размеру таблицах
и
.
На самом деле нам нет необходимости проходить по датам. Единственной
причиной для этого является то, что код будет лучше восприниматься. Немного измененный алгоритм может выглядеть следующим образом.
. Разделяем текущий выбор по таблице
следней даты продаж.
1
ислени
на два набора: до и после поов
енно сложности в
577
2. Рассчитываем сумму продаж за предыдущий период.
3. Рассчитываем бюджет на будущее.
4. Суммируем значения, полученные на двух предыдущих шагах.
К тому же мы не должны рассчитывать сумму продаж только за период до
последней даты продаж. После этой даты все равно продаж нет, а значит, нет
необходимости фильтровать даты при расчете суммы продаж. Единственный
расчет, который нужно ограничить, – это бюджет. Иными словами, в формуле
может вычисляться общая сумма продаж и прибавляться вычисленный бюджет по датам, превышающим последнюю дату продаж. Таким образом, мы
приходим к измененной формуле меры
:
Adjusted Budget Optimized :=
VAR LastDateWithSales =
CALCULATE (
MAX ( Sales[OrderDateKey] );
ALL ( Sales )
)
VAR SalesAmount = [Sales Amount]
VAR BudgetAmount =
CALCULATE (
[Budget Amt];
KEEPFILTERS ( 'Date'[DateKey] > LastDateWithSales )
)
VAR AdjustedBudget = SalesAmount + BudgetAmount
RETURN
AdjustedBudget
Результат вычисления меры
получился идентичным мере
, но ее сложность при этом уменьшилась. В обновленной версии нам необходимо всего по разу сканировать таблицы
и
,
причем последнюю с дополнительным ограничивающим фильтром по таблице
. Заметим также, что здесь нам пришлось использовать модификатор
. В противном случае фильтр по таблице
переопределил бы
значения в текущем контексте фильтра, что привело бы к неправильным расчетам. В результате финальная версия оказалась менее простой для восприятия и понимания, но с точки зрения производительности она серьезно выиграла.
Как и в случае с предыдущими примерами, один и тот же алгоритм можно
реализовать в формулах совершенно по-разному. Поиск оптимального варианта требует немалого опыта и понимания внутреннего устройства движка
DAX. Но и простых рассуждений о кратности выражений бывает достаточно,
чтобы провести первый этап оптимизации.
Расчет сопоставимых продаж по магазинам
Сценарий, который мы рассмотрим в данном разделе, представляет собой
довольно обширное семейство вычислений. Компания Contoso располагает
множеством магазинов по всему миру, в каждом из которых есть несколько
578
1
ислени
ов
енно сложности в
отделов, специализирующихся на продаже конкретных категорий товаров.
При этом список отделов постоянно меняется: какие-то из них закрываются,
какие-то обновляются, появляются и новые. И при выполнении анализа продаж очень важно сравнивать только сопоставимые отделы. В противном случае
легко сделать вывод, что отдел является неприбыльным, если не учесть, что
какое-то время в выбранном периоде он просто не работал.
Концепция сравнения сопоставимых сущностей распространяется на всю
бизнес-аналитику. В нашем случае мы будем анализировать исключительно
магазины и категории товаров, по которым были продажи за все выбранные
годы. При этом в разрезе категории товаров по году должны учитываться
только те магазины, в которых осуществлялись продажи в этом году. В данном
случае вы можете использовать в качестве уровня гранулярности сравнения
месяцы или недели, при этом вам не придется целиком менять приведенный
здесь алгоритм.
Рассмотрим отчет, показанный на рис. 16.5, в котором анализируются продажи по одной категории товаров (Audio) в Германии за три календарных года.
Рис 16 5
е все агазин
ли откр т в в
то е ает роведени анализа
ранн
ериод,
Магазин в Берлине (Berlin) был закрыт на протяжении всего 2007 года,
а в одном из двух магазинов Кельна (Koln) в 2008 году был ремонт, так что он
полноценно работал только два года из трех. Чтобы сравнение продаж отражало истинную картину происходящего, необходимо ограничить анализ теми
магазинами, которые работали постоянно.
И хотя правила определения сопоставимости в зависимости от конкретных
требований могут быть самыми разными, хорошей практикой является сохранение статусов сопоставимости сущностей в отдельной таблице. В этом случае
изменения в бизнес-логике не будут негативно сказываться на скорости выполнения запросов – обновляться будет только таблица со статусами. В нашем
1
ислени
ов
енно сложности в
579
примере мы создадим таблицу
и будем хранить в каждой строке
комбинацию из года, категории товаров и магазина вместе с указанием статуса, который будет принимать два значения: Open (Открыто) или Closed (Закрыто). На рис. 16.6 показан вывод статусов по немецким магазинам, при этом
показаны только статусы Open, а Closed скрыты для лучшего восприятия.
Рис 16 6
та ли е StoresStatus ранитс ин ор а и о то ,
ла ли в конкретно
агазине откр та родажа данно категории товаров в тот год
Наибольший интерес представляет последний столбец, из которого понятно, что на протяжении всех трех лет были открыты всего четыре магазина.
А значит, по категории товаров Audio можно проводить сравнительный анализ
исключительно по этим четырем магазинам. При изменении выбора дат будут
обновлены и статусы. Например, если в текущем выборе оставить только 2007
и 2008 годы, статусы магазинов изменятся, что видно по рис. 16.7.
Мера для определения сопоставимых продаж должна делать следующее:
„ определять список магазинов, которые были открыты во все отчетные
годы и по всем категориям товаров;
„ использовать результат вычисления первого шага для фильтрации меры
, тем самым ограничивая значения только теми магазинами и категориями товаров, по которым были продажи в отчетные годы.
Прежде чем двигаться дальше, необходимо провести тщательный анализ
модели данных. Диаграмма модели представлена на рис. 16.8.
Давайте сделаем несколько замечаний по этой модели данных:
„ таблицы
и
объединены слабой связью «многие ко многим» по столбцу
, при этом кросс-фильтрация направлена в сторону таблицы
. Таким образом, таблица
фильтрует таблицу
, но не наоборот;
„ связь между таблицами
и
– обычная, типа
«один ко многим»;
580
1
ислени
ов
енно сложности в
„ все остальные связи в модели данных также «один ко многим» с однонаправленной кросс-фильтрацией, как и в большинстве примеров из этой
книги;
„ в таблице
содержится по одной строке для каждой комбинации из магазина, категории товаров и года. При этом статус может принимать два значения: Open или Closed. Иными словами, в этой таблице
нет никаких пропусков. Это важный факт для снижения сложности формулы.
Рис 16 7
стол
е Total
ит ва тс стат с
агазина о все в
Рис 16 8
та ли е StoreStatus ран тс стат с дл все ко
и категори товаров
1
ислени
ов
ранн
года
ина и годов
енно сложности в
581
На первом шаге определим, какие отделы были открыты на протяжении всех
выбранных в отчете лет. Для этого необходимо отфильтровать таблицу
по указанной категории товаров и всем выбранным годам. Если после
этого во всех оставшихся строках статус будет иметь значение Open, значит,
отдел был открыт на протяжении всего выбранного периода. Если же среди
статусов будут попадаться значения Closed, это будет означать, что в какие-то
периоды времени анализируемый отдел не работал. Следующий запрос позволяет выполнить это вычисление:
EVALUATE
VAR StatusGranularity =
SUMMARIZE (
Receipts,
Store[Store Name],
'Product Category'[Category]
)
VAR Result =
FILTER (
StatusGranularity,
CALCULATE (
SELECTEDVALUE ( StoresStatus[Status] ),
ALLSELECTED ( 'Date'[Calendar Year] )
) = "Open"
)
RETURN
Result
В запросе выполняются итерации по комбинациям магазин/категория,
и для каждого сочетания проверяется значение статуса. В случае если столбец
будет содержать больше одного статуса, функция
вернет пустое значение, что автоматически исключит текущую пару из
результата фильтра.
Определившись с отделами, которые работали на протяжении всех лет в отчетном периоде, мы можем использовать полученный набор данных в качестве аргумента фильтра в функции
следующим образом:
OpenStoresAmt :=
VAR StatusGranularity =
SUMMARIZE (
Receipts;
Store[Store Name];
'Product Category'[Category]
)
VAR OpenStores =
FILTER (
StatusGranularity;
CALCULATE (
SELECTEDVALUE ( StoresStatus[Status] );
ALLSELECTED ( 'Date'[Calendar Year] )
) = "Open"
)
582
1
ислени
ов
енно сложности в
VAR AmountLikeForLike =
CALCULATE (
[Amount];
OpenStores
)
RETURN
AmountLikeForLike
При выводе в матрицу отчета эта мера даст результат, показанный на
рис. 16.9.
Рис 16 9 Мера OpenStoresAmt озвол ет ол ить рез льтат только дл
котор е ли откр т на рот жении всего от етного ериода
агазинов,
Как видите, магазины, которые работали на протяжении выбранного интервала с перебоями, пропали из отчета. Вам очень важно освоить эту технику,
поскольку она является одной из самых мощных в DAX. Возможность создать
временную таблицу с отфильтрованными данными и затем использовать ее
для ограничения итоговых расчетов является основой многих сложных вычислений в этом языке.
В нашем примере мы использовали вспомогательную таблицу, в которой сохранили информацию о статусе магазинов по годам. Мы могли добиться такого же результата, анализируя таблицу
и делая выводы о статусе магазинов по их фактическим продажам. Если продажи были, можно предположить,
что магазин был открыт. К сожалению, обратное утверждение будет верно не
всегда. Отсутствие продаж за определенный период времени еще не говорит
о том, что соответствующий отдел был закрыт. В худшем для магазина сценарии нулевые продажи могут свидетельствовать о полном отсутствии спроса на
эту категорию товаров, несмотря на то что отдел все это время работал.
Последнее замечание скорее относится к области моделирования данных,
чем к DAX, но мы посчитали нужным об этом сказать. Если вы хотите извлекать информацию о статусах магазинов непосредственно из таблицы
,
нужно еще немного поработать над запросом.
Ниже мы приведем версию меры
без создания вспомогательной таблицы
. Для каждой пары магазина и категории товаров
в формуле выполняется проверка на равенство между количеством лет, в которые были продажи, и количеством выбранных лет в отчете. Если продажи
1
ислени
ов
енно сложности в
583
были только для двух годов из трех, мы будем считать, что в этот год отдел был
закрыт. Так может выглядеть реализация этого вычисления:
OpenStoresAmt Dynamic :=
VAR SelectedYears =
CALCULATE (
DISTINCTCOUNT ( 'Date'[Calendar Year] );
CROSSFILTER ( Receipts[SaleDateKey]; 'Date'[DateKey]; BOTH );
ALLSELECTED ()
)
VAR StatusGranularity =
SUMMARIZE (
Receipts;
Store[Store Name];
'Product Category'[Category]
)
VAR OpenStores =
FILTER (
StatusGranularity;
VAR YearsWithSales =
CALCULATE (
DISTINCTCOUNT ( 'Date'[Calendar Year] );
CROSSFILTER ( Receipts[SaleDateKey]; 'Date'[DateKey]; BOTH );
ALLSELECTED ( 'Date'[Calendar Year] )
)
RETURN
YearsWithSales = SelectedYears
)
VAR AmountLikeForLike =
CALCULATE (
[Amount];
OpenStores
)
RETURN
AmountLikeForLike
Сложность этой версии меры получилась выше, чем у предыдущей. Фактически мы вынуждены распространять контекст фильтра с таблицы
на
таблицу
для каждой категории товаров, чтобы вычислить количество лет
с продажами. Чаще всего в таблице
строк будет гораздо больше, чем
во вспомогательной таблице со статусами магазинов, а значит, представленная мера будет вычисляться медленнее, чем версия с использованием таблицы
. Но, по сути, единственное отличие между этими двумя версиями
меры состоит в содержимом функции
. Вместо того чтобы анализировать содержимое вспомогательной таблицы, мы сканируем физическую таблицу
. Однако в целом шаблон вычисления остался прежним.
Еще одной важной деталью применительно к этой мере является способ вычисления переменной
. Здесь нам не подойдет выбор всех отмеченных лет при помощи функции
, ведь нам нужно получить
не просто все годы, а только те из них, в которые были продажи. Например,
если в таблице
представлено десять лет, а продажи были только в трех из
584
1
ислени
ов
енно сложности в
них, функция
ним пустые значения.
посчитала бы все годы без продаж и вернула по
Нумерация последовательности событий
В данном разделе мы рассмотрим на удивление распространенный шаблон,
состоящий в необходимости проведения нумерации событий с возможностью
быстрого поиска первого из них, последнего или предшествующего текущему. Представим, что нам необходимо пронумеровать заказы в рамках каждого
покупателя в базе Contoso. Результатом наших усилий должен стать дополнительный вычисляемый столбец с единицей для первого заказа конкретного
покупателя, двойкой для второго и т. д. Для всех покупателей значение 1 в этом
столбце будет означать первый заказ.
Предупреждение
а не о исание ре ени то зада и с важного ред реждени
о то , то некотор е из ере исленн
ор л ог т в олн тьс довольно долго М
оказ вае ва разли н е вариант ре ени с енариев, о с ждае и и ри оди
к о ти ально в ор сли в за отите о ро овать некотор е из редставленн
орл в одели данн , риготовьтесь к то ительно ожидани рез льтатов е ь ожет
идти о нескольки аса и дес тка гига а т ис ользованно о еративно а ти риенительно к на е де онстра ионно
одели данн
сли же в не желаете ждать
так долго, росто ита те тот раздел, в кон е которого
риведе ри ер наи олее
ективно ор л
Ожидаемый результат показан на рис. 16.10.
Рис 16 10
стол е Order Position содержатс
в разрезе ок ателе
ор дков е но ера заказов
В первом варианте решения задачи мы будем опираться на подсчет количества заказов покупателя, предшествующих по дате формирования текуще1
ислени
ов
енно сложности в
585
му заказу. К сожалению, гранулярность дня мы здесь использовать не можем,
поскольку один и тот же покупатель может разместить два заказа в день, а это
собьет нашу последовательность. Но есть и хорошая новость: у каждого заказа
существует свой уникальный номер, который увеличивается от заказа к заказу.
Таким образом, мы можем использовать столбец
для подсчета
количества заказов по конкретному клиенту, предшествующих текущему.
Следующий код реализует эту логику:
Sales[Order Position] =
VAR CurrentOrderNumber = Sales[Order Number]
VAR Position =
CALCULATE (
DISTINCTCOUNT ( Sales[Order Number] );
Sales[Order Number] <= CurrentOrderNumber;
ALLEXCEPT (
Sales;
Sales[CustomerKey]
)
)
RETURN
Position
Несмотря на свою внешнюю простоту, эта формула довольно сложная.
В функции
применяется фильтр по номеру заказа и преобразование контекста, инициированное вычисляемым столбцом. Для каждой строки
в таблице
движок, по сути, вынужден сканировать ту же таблицу
.
Таким образом, сложность формулы исчисляется возведенным в квадрат
количеством строк в таблице продаж. Например, если в таблице
будет
100 000 записей, формуле придется 100 000 раз пройти по 100 000 строк, что
выливается в 10 млрд итераций. В результате эта мера может вычисляться часами и способна поставить на колени любые серверные мощности.
В главе 5 мы уже говорили об использовании функции
с сопутствующим преобразованием контекста применительно к объемным таблицам. Всякий разработчик должен стремиться к отказу от применения этой
техники – в противном случае есть большой риск свести эффективность формул к нулю.
Можно предложить и гораздо лучшую реализацию той же идеи. Вместо использования функции
с дорогостоящим преобразованием контекста можно создать временную таблицу со всеми комбинациями значений
столбцов
и
. После этого можно применить к полученной таблице ту же логику с подсчетом количества заказов с номерами, меньшими текущего в рамках одного покупателя. Ниже представлена формула:
Sales[Order Position] =
VAR CurrentCustomerKey = Sales[CustomerKey]
VAR CurrentOrderNumber = Sales[Order Number]
VAR CustomersOrders =
ALL (
Sales[CustomerKey];
Sales[Order Number]
586
1
ислени
ов
енно сложности в
)
VAR PreviousOrdersCurrentCustomer =
FILTER (
CustomersOrders;
AND (
Sales[CustomerKey] = CurrentCustomerKey;
Sales[Order Number] <= CurrentOrderNumber
)
)
VAR Position =
COUNTROWS ( PreviousOrdersCurrentCustomer )
RETURN
Position
Эта формула будет выполняться гораздо быстрее. Количество уникальных
комбинаций значений из столбцов
и
составляет
26 000, что намного меньше, чем прежние 100 000. Кроме того, отказ от преобразования контекста позволит оптимизатору построить более эффективный
план выполнения запроса.
Вычислительная сложность этой формулы по-прежнему высока, да и код
получился не самым простым для восприятия. Лучше всего подобные задачи
с нумерацией решать при помощи специальной функции
. Эта функция
применяется для выполнения ранжирования значений в таблицах и наиболее
эффективно нумерует строки. Фактически порядковый номер заказа в нашем
случае совпадает с возрастающим рангом заказа в рамках каждого отдельного
покупателя.
Ниже представлена реализация предыдущей формулы с использованием
функции
:
Sales[Order Position] =
VAR CurrentCustomerKey = Sales[CustomerKey]
VAR CustomersOrders =
ALL (
Sales[CustomerKey];
Sales[Order Number]
)
VAR OrdersCurrentCustomer =
FILTER (
CustomersOrders;
Sales[CustomerKey] = CurrentCustomerKey
)
VAR Position =
RANKX (
OrdersCurrentCustomer;
Sales[Order Number];
Sales[Order Number];
ASC;
DENSE
)
RETURN
Position
1
ислени
ов
енно сложности в
587
Функция
является очень хорошо оптимизированной. В ней заложен
внутренний алгоритм сортировки, быстро работающий даже с большими наборами данных. Применительно к демонстрационной модели данных разница в скорости выполнения между двумя последними запросами оказалась не
слишком велика, при этом глубокий анализ плана выполнения запросов показал, что вариант с использованием функции
является наиболее оптимальным. Анализу планов выполнения запросов мы посвятим следующие
главы данной книги.
На этом примере мы вновь показали, что одну и ту же задачу можно решить
совершенно разными способами. Вариант с применением функции
может показаться наименее очевидным для новичка в DAX, поэтому мы предложили разные техники решения этого сценария. Это может стать неплохой
пищей для размышлений.
Вычисление продаж по предыду ему году
до определенной даты
В данном разделе мы немного расширим пример с использованием логики
операций со временем. Допустим, нам необходимо сравнить продажи текущего и предыдущего годов и при этом за предыдущий год учитывать только даты,
предшествующие указанной. Чтобы продемонстрировать этот пример, мы
удалили из демонстрационной модели данных все продажи позже 15 августа
2009 года. Таким образом, 2009 год остался неполным, как и август этого года.
По отчету, показанному на рис. 16.11, видно, что значения по продажам после августа 2009 года остались пустыми.
Рис 16 11
осле авг ста 200 года родажи не за олнен
Когда на строках присутствуют месяцы, все ясно и понятно. Пользователь
сразу поймет, что 2009 год не завершен, а значит, сравнивать итоговые цифры по нему с предыдущими годами бессмысленно. В то же время разработчи588
1
ислени
ов
енно сложности в
ки иногда создают отчеты, которые при всей своей полезности могут вводить
пользователей в заблуждение. Давайте рассмотрим следующие две меры:
PY Sales :=
CALCULATE (
[Sales Amount];
SAMEPERIODLASTYEAR ( 'Date'[Date] )
)
Growth :=
DIVIDE (
[Sales Amount] - [PY Sales];
[PY Sales]
)
Пользователь легко может применить эти меры при построении отчета, показанного на рис. 16.12, и с грустью констатирует существенное падение продаж по всем брендам.
Рис 16 12
От ет оказ вает снижение родаж о все
ренда
Как вы понимаете, в этом отчете 2008 и 2009 годы сравниваются неправильно. В текущем 2009 году продажи учитываются только по 15 августа, тогда как
в предыдущем в расчет берется весь год целиком, включая сентябрь и последующие месяцы.
Чтобы сравнивать годы корректно, необходимо учитывать в каждом из них
продажи исключительно до 15 августа – только в этом случае можно будет полагаться на показатели роста или падения продаж. В общем случае все предыдущие годы должны быть ограничены последней датой продажи в текущем
году.
Как обычно, эту задачу можно решить самыми разными способами, и в этом
разделе мы рассмотрим некоторые из них. Первый подход заключается в изменении меры
таким образом, чтобы она принимала в расчет только
даты, предшествующие последней дате продаж. Вот один из способов написать
такое вычисление:
1
ислени
ов
енно сложности в
589
PY Sales :=
VAR LastDateInSales =
CALCULATETABLE (
LASTDATE ( Sales[Order Date] );
ALL ( Sales )
)
VAR LastDateInDate =
TREATAS (
LastDateInSales;
'Date'[Date]
)
VAR PreviousYearLastDate =
SAMEPERIODLASTYEAR ( LastDateInDate )
VAR PreviousYearSales =
CALCULATE (
[Sales Amount];
SAMEPERIODLASTYEAR ( 'Date'[Date] );
'Date'[Date] <= PreviousYearLastDate
)
RETURN
PreviousYearSales
В первой переменной сохраняется последнее значение из столбца
по всем продажам. В нашем случае это будет дата 15 августа 2009 года. В следующей переменной (
) мы будем хранить полученное ранее
значение с измененной привязкой данных к столбцу
. Этот шаг нам
необходим, поскольку все функции логики операций со временем работают
исключительно с датами. Использование их со столбцами других типов может
привести к неожиданным результатам, и мы продемонстрируем это позже.
Итак, в переменной
у нас содержится дата 15 августа 2009 года
с правильной привязкой данных. Функция
перенесет
эту дату на год назад. И наконец, функция
рассчитает нужное нам
значение по предыдущему году с применением двух фильтров: текущего выбора, сдвинутого на год назад, и всех дат ранее 15 августа 2008 года.
Результат вычисления этой меры представлен на рис. 16.13.
Рис 16 13
рез льтат
590
е ерь, когда
вз ли со остави
ожно сравнивать
1
ислени
ов
е ериод ,
енно сложности в
Очень важно понимать, зачем нам в предыдущем примере понадобилось
использовать функцию
. Малоопытный разработчик DAX мог бы написать эту меру следующим образом, без использования функции
:
PY Sales Wrong :=
VAR LastDateInSales =
CALCULATETABLE (
LASTDATE ( Sales[Order Date] );
ALL ( Sales )
)
VAR PreviousYearLastDate =
SAMEPERIODLASTYEAR ( LastDateInSales )
VAR PreviousYearSales =
CALCULATE (
[Sales Amount];
SAMEPERIODLASTYEAR ( 'Date'[Date] );
'Date'[Date] <= PreviousYearLastDate
)
RETURN
PreviousYearSales
Плохо то, что в нашей демонстрационной модели данных эти две меры выдают абсолютно одинаковые результаты, а это лишний раз показывает, что
ошибка в последней формуле может оказаться не столь очевидной с первого
взгляда. В чем же здесь проблема? На выходе функция
выдает таблицу, состоящую из одного столбца с такой же привязкой данных,
как в исходном столбце. Если функции
передать столбец с привязкой к столбцу
в модели данных, то она должна
вернуть значения, входящие в список возможных значений из этого столбца.
Но, будучи столбцом в таблице
,
может не содержать все возможные даты. Например, если в уик-энд не было продаж, то эти даты не будут
присутствовать в списке возможных значений столбца
. В этом
случае функция
вернет пустое значение.
По рис. 16.14 видно, что произойдет с отчетом, если удалить из таблицы
все транзакции за 15 августа 2008 года.
Рис 16 14
в деленно
и ст и зна ени и
раг енте оказан стол е с еро PY Sales Wrong
1
ислени
ов
енно сложности в
591
Поскольку последней датой продаж в нашей модели является 15 августа
2009 года, мы от нее смещаемся на год назад, получая дату 15 августа 2008 года, которая не присутствует в столбце
. В результате функция
вернет пустое значение. После этого в действие вступит второй фильтр функции
, который должен будет вернуть дату,
меньшую или равную пустому значению. Понятно, что таких дат просто не существует, в результате чего мера
ожидаемо вернет пустое значение.
В нашем примере мы намеренно удалили транзакции за 15 августа 2008 года
из таблицы
. На практике эта проблема может проявиться для любой даты,
если в соответствующий ей день предыдущего года не было продаж. Всегда
помните, что функции логики операций со временем призваны работать исключительно с таблицами дат. Использование их со столбцами других типов
может приводить к неожиданным результатам.
Как мы уже говорили, когда логика вычисления понятна, можно реализовать
ее самыми разными способами. Мы здесь показали только одну технику, другие варианты вы можете придумать сами.
Также хочется заметить, что такие сценарии лучше решать при наличии доступа к изменению модели данных. В самом деле, рассчитывать последнюю
дату продаж каждый раз, когда необходимо провести вычисление, и смещать
ее на год (или какой угодно другой срок) назад – это довольно банальное решение, не защищенное от ошибок. Гораздо лучше рассчитать заранее, должна
ли та или иная дата участвовать в сравнении, и хранить эту информацию непосредственно в таблице
.
Для этого можно создать вычисляемый столбец в таблице дат, показывающий, должна текущая дата включаться в сравнение с предыдущим годом или
нет. В нашем случае все даты до 15 августа получат значение
, а после
15 августа –
.
Формула для расчета вычисляемого столбца может быть такой:
'Date'[IsComparable] =
VAR LastDateInSales =
MAX ( Sales[Order Date] )
VAR LastMonthInSales =
MONTH ( LastDateInSales )
VAR LastDayInSales =
DAY ( LastDateInSales )
VAR LastDateCurrentYear =
DATE ( YEAR ( 'Date'[Date] ); LastMonthInSales; LastDayInSales )
VAR DateIncludedInCompare =
'Date'[Date] <= LastDateCurrentYear
RETURN
DateIncludedInCompare
Создав вычисляемый столбец, мы можем изменить формулу для меры
следующим образом:
PY Sales :=
CALCULATE (
[Sales Amount];
592
1
ислени
ов
енно сложности в
SAMEPERIODLASTYEAR ( 'Date'[Date] );
'Date'[IsComparable] = TRUE
)
Этот код не только легче читать и отлаживать, он и работать будет гораздо быстрее предыдущей реализации. Причина в том, что нам больше не надо
каждый раз тратить время на поиск последней даты продажи в таблице
,
смещать на год назад и применять к модели данных в качестве фильтра. Обновленный код меры состоит из простой функции
, проверяющей
в одном из фильтров булево значение. На этом примере мы хотели показать,
что иногда сложную логику из фильтра можно переместить в вычисляемый
столбец, расчет которого происходит на этапе обновления данных, а не во время ожидания пользователем формирования отчета.
Закл чение
В данной главе вы не познакомились ни с одной новой функцией языка DAX.
Вместо этого мы поставили себе цель донести до вас, что любую задачу можно
решить множеством способов. При этом мы не касались внутреннего устройства движка – важной темы, усвоение которой необходимо для перехода к вопросам оптимизации. Но даже беглого анализа кода и симуляции его поведения часто бывает достаточно для выбора более эффективной формулы расчета.
Помните, что эта глава не была посвящена готовым шаблонам. Вы можете
пользоваться этими наработками в своих проектах, но не стоит считать представленные здесь формулы оптимальными. Мы лишь хотели, чтобы вы взглянули на одни и те же сценарии под разными углами.
В следующих главах вы узнаете, что выработать универсальные и уникальные шаблоны в DAX практически невозможно. Код, с поразительной быстротой работающий в одной модели данных, может быть далеко не самым оптимальным в другой модели или даже в той же самой, но с иным распределением
данных.
Если вы всерьез решили заняться оптимизацией кода на DAX, приготовьтесь
к погружению в недра его движка и знакомству со всеми его хитросплетениями. Увлекательное и захватывающее путешествие уже ждет вас, переворачивайте страницу – и в путь!
ГЛ А В А 17
Движки DAX
До сих пор нашей главной целью было научить вас премудростям языка DAX.
Теперь, набравшись практики, вы должны сместить свои акценты с написания
кода, который работает, на код, который работает быстро и эффективно. А это
потребует от вас глубокого знания внутреннего устройства движка DAX. Следующие главы данной книги будут посвящены искусству измерения и улучшения производительности кода на DAX.
Если говорить более конкретно, мы рассмотрим внутреннюю архитектуру
движков, отвечающих за выполнение запросов на языке DAX. Фактически запросы могут выполняться как в модели данных, целиком загруженной в память, так и находящейся в исходном источнике данных или сочетающей в себе
оба состояния.
Начиная с данной главы мы немного отклонимся от темы DAX и рассмотрим
низкоуровневые технические нюансы реализации продуктов, использующих
в своей основе DAX. Это очень важная тема, но вы должны понимать, что детали реализации продуктов часто меняются. Мы сделали все возможное, чтобы
рассказать о том пласте информации, который вряд ли скоро изменится, соблюдая при этом баланс между детальными сведениями и пользой в тех областях, которые меньше подвержены изменениям с течением времени. И все
же при современном темпе роста технологий никто не гарантирует, что наша
информация не устареет в ближайшие годы. Наиболее актуальные сведения,
как водится, можно отыскать в сети – в статьях и постах в блогах.
Новые версии движков выходят каждый месяц, и оптимизатор запросов также не стоит на месте в плане построения эффективных планов выполнения запросов. Мы собираемся рассказать вам о том, как работают движки, а не перечислять правила написания эффективного кода на DAX, которые скоро могут
устареть. Иногда мы будем советовать вам тот или иной подход к решению задачи, но вы всегда должны проверять, подходит ли это для вашего конкретного
сценария.
Знакомство с архитектурой движков DAX
Язык DAX используется во множестве продуктов от Microsoft, основанных на
технологии Tabular. При этом некоторые специфические возможности доступны только в определенных версиях или при особых условиях лицензирования.
Модель Tabular использует в качестве языков запросов как DAX, так и MDX.
В данном разделе мы опишем общую архитектуру модели Tabular без оглядки
на конкретные языки запросов и ограничения тех или иных версий продуктов.
594
1
вижки
При формировании отчета запрос в формате языка DAX или MDX направляется в модель Tabular. Обрабатывая запросы, вне зависимости от используемого языка модель обращается к двум движкам:
„ движку формул
, обрабатывающему поступивший запрос, генерирующему план выполнения запроса и запускающему его;
„ движку хранилища данных
, извлекающему данные из
модели Tabular в ответ на запросы от движка формул. При этом движок
хранилища данных имеет две реализации:
VertiPaq хранит в памяти периодически обновляемую из источника
копию данных;
DirectQuery перенаправляет каждый поступивший запрос источнику
данных. При этом DirectQuery не создает копию данных.
На рис. 17.1 изображена схема обработки запросов DAX или MDX.
VERTIPAQ
ДВИ О
В ЧИСЛ НИ
DAX
ЗАПРОС
ериоди еское
о новление
е ированн е
данн е
сто ник
данн
DIRECTQUER
,
Модель
вижок ор
Рис 17 1
а рос о ра ат ва тс
и движка ранили а данн
л
вижок ранили а данн
ри о о и движка ор
л
Движок формул представляет собой высокоуровневый функ ионал н й мо
дул (execution unit) движка запросов модели Tabular. Он может выполнять все
операции, предписанные функциями DAX и MDX, и вычислять сложные выражения на этих языках запросов. При этом во время извлечения данных из соответствующих таблиц движок формул перенаправляет часть запросов движку
хранилища данных.
Запросы, поступающие на вход движка формул, могут варьироваться от простого извлечения необработанных табличных данных до выполнения сложных
операций с агрегированием данных и объединением таблиц. Движок хранилища данных может взаимодействовать только с движком формул. Возвращает
результат движок хранилища в несжатом формате вне зависимости от исходного формата данных.
Обычно в модели Tabular для хранения данных используется либо движок
VertiPaq, либо DirectQuery. Однако сложные модели данных способны использовать оба этих движка одновременно для одних и тех же таблиц. Выбор
в пользу того или иного типа хранения данных принимается движком на основании запроса.
Эта книга посвящена исключительно языку DAX. Но стоит помнить, что при
запросах к модели Tabular MDX использует ту же самую архитектуру. В данной
главе мы расскажем о разных типах движков хранилища данных, доступных
1
вижки
595
в модели Tabular, но основное внимание уделим движку VertiPaq, поскольку он
является родным для DAX и наиболее быстрым.
Введение в движок формул
Движок формул является базовым звеном выполнения выражений на DAX.
При этом он также умеет работать и с языком MDX. По сути, движок формул
строит на основании запросов на языках DAX и MDX планы их выполнения,
представляющие собой пошаговую инструкцию физических операций с данными. В свою очередь, движок хранилища данных модели Tabular даже не знает о том, что запросы поступили из модели, поддерживающей DAX.
Каждый шаг в плане выполнения запроса соответствует определенной
операции, выполняемой движком формул. Обычно операции, выполняемые
движком формул, включают в себя объединение таблиц, фильтрацию по сложным условиям, агрегацию и поиск. Чаще всего при выполнении этих операций
происходит обращение к данным в столбцах модели. В таких случаях движок
формул перенаправляет часть запроса движку хранилища данных и получает
в ответ ке данн (datacache). Кеш данных представляет собой временную область хранилища, созданную движком хранилища данных и предназначенную
для чтения движком формул.
Примечание
е данн
ранитс в несжато виде о с ти, то о
н е та ли в ати, ран иес в несжато
ор ате вне зависи ости от того, каки движко ранилиа данн
ли создан
Движок формул всегда работает с кешем данных, возвращенным движком
хранилища, или со структурами данных, вычисленными другими операторами
движка формул. Результаты операций, выполненных движком формул, не сохраняются в памяти для других операций даже в рамках одной сессии. Напротив, кеши данных находятся в памяти и могут быть повторно использованы
другими запросами. Движок формул не располагает системой кеширования
для обмена результатами между запросами. DAX целиком и полностью полагается на систему кеширования движка хранилища данных.
Наконец, движок формул является одно ото н м (single-threaded). Это
означает, что операции, выполняемые движком формул, используют только
один поток и одно ядро процессора вне зависимости от того, сколько ядер есть
в наличии. Движок формул посылает запросы движку хранилища данных последовательно, по одному за раз. До некоторой степени принципы аралле
ли ма (parallelism) могут быть использованы внутри запросов, направленных
движку хранилища данных, обладающему другой архитектурой и способному
воспользоваться всеми преимуществами многопроцессорной обработки данных. Об этом мы поговорим в следующих разделах.
Введение в движок хранили а данных
В задачи движка хранилища данных входит сканирование базы данных Tabular
и создание наборов кешированных данных, необходимых для функциониро596
1
вижки
вания движка формул. Движок хранилища данных не зависит от DAX. Например, DirectQuery, работающий с SQL Server, использует SQL в качестве языка
движка хранилища данных. При этом язык SQL появился гораздо раньше DAX.
Хотя это может показаться странным, встроенный в модель Tabular движок
хранилища данных, именуемый VertiPaq, также не зависит от DAX. В целом же
архитектура является очень простой и понятной. Движок хранилища данных
выполняет только те запросы, которые может в соответствии со своим набором операторов. В зависимости от используемого движка хранилища данных
набор доступных операторов может варьироваться от очень ограниченного
(в случае с VertiPaq) до очень богатого (для SQL). Это оказывает влияние на
производительность запросов и выбор оптимизации, применяемой при анализе планов выполнения запросов.
Разработчик волен сам определять, какой движок хранилища данных будет
использоваться для работы с той или иной таблицей, исходя из трех доступных
вариантов:
„ и орт I
также называемый «в памяти» или VertiPaq. Содержимое таблиц сохраняется движком VertiPaq с применением копирования
и реорганизации данных из источника во время обновления данных;
„ D
содержимое таблиц извлекается из источника данных в момент запроса и не сохраняется в памяти локально во время обновления
данных;
„ с е а
D
в этом режиме к таблице можно обращаться как посредством VertiPaq, так и при помощи DirectQuery. Во время обновления
данных таблица загружается в память, а в момент выполнения запроса
может быть также использован режим DirectQuery для загрузки самой актуальной информации.
Кроме того, таблица в модели Tabular может использоваться в качестве источника агрегирования для другой таблицы. Агрегирование позволяет оптимизировать запросы движка хранилища данных, но не помогает при оптимизации слабых мест, характерных для движка формул. Агрегаты могут быть
определены как в VertiPaq, так и в DirectQuery, хотя чаще для повышения производительности они используются именно в VertiPaq.
Движок хранилища данных использует в своей работе принципы параллелизма. При этом от движка формул запросы к нему приходят последовательно,
один за другим. Таким образом, движок формул ожидает окончания выполнения одного запроса движком хранилища данных и только после этого посылает следующий. В связи с этим ограничением использование принципов
параллелизма в движке хранилища данных может быть сведено на нет.
Движок хранили а данных VertiPaq
Движок хранилища данных VertiPaq является родным для DAX низкоуровневым функциональным модулем. В каких-то инструментах он был официально
назван xVelocity In-Memory Analytical Engine. Но его общепринятым названием, использованным еще на этапе разработки, является именно VertiPaq. Движок VertiPaq сохраняет копию данных из источника в памяти в сжатом виде,
основываясь на структуре хранения данных в столбцах.
1
вижки
597
Запросы VertiPaq используют в своей основе язык, напоминающий SQL
и именующийся xmSQL. xmSQL не является по своей сути полноценным языком запросов. Это, скорее, текстовое представление запроса движка хранилища данных. Язык xmSQL призван дать разработчику представление о том, как
именно движок формул обращается к VertiPaq. Сам по себе движок VertiPaq
обладает очень ограниченным набором операторов, и если внутри запроса
сканирования данных необходимо произвести более сложные вычисления,
VertiPaq может повторно обратиться к движку формул.
Движок хранилища данных VertiPaq является многопоточным и очень эффективно оперирует с данными, обладая возможностью задействовать при
этом несколько ядер процессора. При выполнении запроса движок хранилища
данных может использовать разные степени параллелизма вплоть до выделения одного потока на каждый сегмент таблицы. О сегментах мы поговорим
далее в этой главе. В связи с таким подходом к многопоточности со стороны
движка хранилища данных в полной мере воспользоваться преимуществами
параллелизма можно только в случае, если выполнение запроса подразумевает
обращение к разным сегментам. Иначе говоря, если у вас есть восемь запросов к движку хранилища данных, обращающихся к одной небольшой таблице,
состоящей из одного сегмента, они будут запущены последовательно один за
другим, а не параллельно, из-за однопоточной природы взаимодействия между движком формул и движком хранилища данных.
Система кеширования способствует сохранению результатов, произведенных движком VertiPaq. При этом хранится ограниченное количество кешей
данных – обычно по 512 запросов в расчете на одну базу данных, но это число
может меняться в зависимости от версии движка. Когда движок получает запрос на языке xmSQL, результаты которого уже хранятся в кеше, он возвращает рассчитанные ранее данные, не выполняя при этом сканирования данных
в памяти. При этом кеширование никак не связано с вопросами безопасности
данных, поскольку система безопасности уровня строк управляет только поведением движка формул, производя разные запросы xmSQL, в случае если
пользователю ограничен доступ к отдельным строкам в таблице.
Операция сканирования, производимая движком хранилища данных, обычно выполняется быстрее по сравнению с аналогичной операцией от движка
формул даже при наличии доступа только к одному потоку. Причина кроется
в том, что движок хранилища данных лучше оптимизирован для выполнения
подобных операций. Кроме того, он выполняет итерации по сжатым данным,
тогда как движок формул располагает доступом лишь к кешу данных, хранящемуся в несжатом виде.
Движок хранили а данных DirectQuery
Движок хранилища данных DirectQuery описывает общую концепцию, при
которой данные остаются в исходном источнике, а не копируются в память,
как в случае использования движка VertiPaq. Когда движок формул посылает
запрос движку хранилища данных в режиме DirectQuery, тот пересылает его
непосредственно источнику данных на родном для него языке. В большинстве
случаев таким языком является SQL, но могут быть варианты.
598
1
вижки
Движок формул знает о включении режима DirectQuery. В связи с этим он
строит совершенно другие планы выполнения запросов по сравнению с планами для VertiPaq, чтобы движок хранилища данных мог в полной мере воспользоваться преимуществами языка запроса, характерного для источника данных.
Например, в языке SQL предусмотрены функции для работы с текстом вроде
и
, тогда как движок VertiPaq не умеет манипулировать текстом.
Любые оптимизации движка хранилища данных с использованием режима
DirectQuery требуют проведения оптимизации и в самом источнике данных
вроде использования индексов в реляционной базе данных. Подробнее о движке DirectQuery и доступных методах оптимизации можно почитать по адресу
https://www.sqlbi.com/whitepapers/directquery-in-analysi
201 . Рассматриваемые способы оптимизации применимы как к Power BI, так и к Analysis
Services, поскольку в их основе лежит один и тот же движок.
Процедура обновления данных
DAX работает в SQL Server Analysis Services (SSAS) Tabular, Azure Analysis Services (применительно к данной книге это аналог SSAS), службе Power BI (как
на сервере, так и в клиентском приложении Power BI Desktop), а также в надстройке Power Pivot для Microsoft Excel. Чисто технически Power Pivot для Excel и Power BI используют адаптированную версию SSAS Tabular. Так что все
разговоры о разнице в движках в какой-то степени искусственны: Power Pivot
и Power BI являются аналогами SSAS, хотя SSAS работает в скрытом режиме.
В данной книге мы не будем делать различий между этими движками. Таким
образом, когда мы говорим об SSAS, читатель должен понимать, что все то же
самое относится и к Power Pivot с Power BI. Различия, о которых стоит упомянуть, мы перечислим в специальном разделе.
Когда SSAS загружает содержимое таблиц источника данных в память, мы
говорим, что он обрабатывает таблицу. Это происходит во время выполнения
операции обработки в SSAS и обновления данных – в Power Pivot для Excel
и Power BI. Операция обработки таблицы в режиме DirectQuery просто очищает внутренний кеш без обращения к источнику данных. Напротив, когда обработка данных выполняется в режиме VertiPaq, движок осуществляет чтение
данных из источника и преобразовывает их во внутреннюю структуру VertiPaq.
Обработка таблицы в режиме VertiPaq включает в себя следующие шаги:
1) чтение набора данных из источника и преобразование его в столбчатую
структуру VertiPaq с одновременным кодированием и сжатием каждого
столбца;
2 создание словарей и индексов для каждого столбца;
3 создание структур данных для связей;
4 расчет и компрессия значений всех вычисляемых столбцов и вычисляемых таблиц.
Последние два шага совсем не обязательно должны выполняться в такой последовательности. В действительности связь может базироваться на вычисляемом столбце, как и вычисляемый столбец – на связи в случае использования
функций
или
. Таким образом, SSAS создает сложную схему зависимостей, позволяющую выполнить эти шаги в правильном порядке.
1
вижки
599
В следующих разделах мы опишем эти шаги более детально. Также мы поговорим о внутренних структурах, создаваемых SSAS в процессе преобразования
данных из источника в формат модели VertiPaq.
Принципы работы движка хранили а данных
VertiPaq
VertiPaq является наиболее распространенным движком хранилища данных
в моделях Tabular. Именно этот движок используется для таблиц с режимом
хранения Import (Импорт). Во многих моделях данных этот вариант хранения
таблиц является самым распространенным, а в Power Pivot для Excel – единственно возможным. В сложных моделях данных для таблиц или агрегаций со
смешанным режимом хранения также предполагается использование движка
VertiPaq совместно с DirectQuery.
По этой причине понимание принципов работы движка VertiPaq крайне необходимо для проведения оптимизации модели данных в плане используемой
памяти и скорости выполнения запросов. В этом разделе вы узнаете, что из
себя представляет и как работает движок хранилища данных VertiPaq.
Введение в столбчатые базы данных
VertiPaq представляет собой стол ату
а у данн (columnar database), сохраненную в памяти. Это означает, что все данные, которыми оперирует модель, находятся в оперативной памяти компьютера. Но главной особенностью
базы данных VertiPaq является то, что в ее основе лежат не строки, а столбцы, а значит, она является столбчатой по своей природе. И чтобы разобраться в принципах работы VertiPaq, необходимо хорошо понимать концепцию
столбчатых баз данных.
Мы привыкли думать о таблицах как о наборах строк, где каждая строка разбита на столбцы. Для примера рассмотрим таблицу
, представленную
на рис. 17.2.
Думая о таблицах как о наборах строк, мы используем наиболее естественную
визуализацию табличной структуры. Технически такой тип хранения данных
называется стро н м ранили ем (row store). В строчном хранилище данные
организованы по строкам. Если таблица сохранена в памяти, мы смеем предполагать, что значение столбца
из первой строки будет соседствовать со
значениями из столбцов
и
из той же строки. При этом значение
из второй строки будет достаточно далеко отстоять от аналогичного значения
из первой строки. И правда, между ними будут находиться значения столбцов
и
из первой строки, а также из второй. Давайте посмотрим на
схематическое размещение в памяти элементов строчного хранилища:
ID,Name,Color,Unit Price|1,Camcorder,Red,112.25|2,Camera,Red,97.50|3,Smartphone,
White,100.00|4,Console,Black,112.25|5,TV,Blue,1,240.85|6,CD,Red,39.99|7,
Touch screen,Blue,45.12|8,PDA,Black,120.25,9,Keyboard,Black,120.50
600
1
вижки
Product
ID
Name
Color
Unit Price
1
Camcorder
Red
2
Camera
Red
97.50
3
Smartphone
White
100.00
4
Console
Black
112.25
5
TV
Blue
1,240.85
6
CD
Red
39.99
7
Touch screen
Blue
45.12
8
PDA
Black
120.25
9
Keyboard
Black
120.50
112.25
Рис 17 2 а ли а Product,
состо а из ет ре стол ов
и дев ти строк
Допустим, нам необходимо рассчитать сумму по столбцу
. Для этого
движок должен просканировать всю область памяти, отведенную под хранение
этой таблицы, и сложить внешне никак не связанные значения. Представьте,
как будет происходить последовательное сканирование памяти. Чтобы получить первое значение
, движку необходимо прочитать (и пропустить)
поля ,
и
из первой строки, и только затем мы попадаем в нужную
нам ячейку. И то же самое придется повторить для каждой строки. Получается,
что использование такого принципа хранения данных обязует движок читать
и игнорировать множество значений только для того, чтобы подсчитать сумму
по столбцу.
Чтение и пропуск значений занимает немало времени. Если же
Download