Вы находитесь на странице: 1из 289

Признанные эксперты в области DAX Альберто Феррари

и Марко Руссо научат вас максимально эффективно


проектировать модели данных

Если вы хотите использовать Power BI или Excel для


анализа данных, реальные примеры из этой книги
О книге:
- для пользователей Excel и Анализ данных

Microsoft Power BI и Power Pivot для Excel


Анализ данных при помощи
Power BI, желающих макси-
позволят вам иначе посмотреть на свои отчеты.

при помощи
мально повысить эффективность
С правильно спроектированной моделью данных использования этих средств
ответы на все вопросы будут предельно простыми! разработки;
- для профессионалов в

Microsoft Power BI и
Читая эту книгу, вы:
бизнес-аналитике, ищущих
• освоите базовые концепции моделирования новые идеи в области
данных, включая таблицы, связи и ключи; моделирования данных.
• познакомитесь с распространенными схемами

Power Pivot для Excel


данных «звезда» и «снежинка» и общими Об авторах:
техниками моделирования; Альберто Феррари и Марко
• усвоите важность гранулярности; Руссо – основатели сайта sqlbi.
• узнаете, как использовать несколько таблиц com, на котором регулярно
публикуются свежие статьи по
фактов (например, продажи и закупки) в единой
Microsoft Power Pivot, Power BI,
модели данных; DAX и SQL Server Analysis Ser-
• научитесь производить расчеты с календарем, vices. Также Альберто и Марко
используя таблицы с датами; сами проводят консультации
• освоите отслеживание исторических атрибутов, и обучение в области бизнес-
таких как адреса покупателей или привязку аналитики. Кроме этого, они
клиентов к менеджерам; регулярно принимают участие
• узнаете, как использовать снимки для подсчета в крупнейших международных
конференциях, включая Mi-
количества товаров в наличии;
crosoft Ignite, PASS Summit и
• научитесь эффективно работать с несколькими SQLBits.
валютами одновременно;
• приобретете знания для анализа событий
с определенной длительностью, включая
пересекающиеся интервалы; Примеры на сайте
• сможете определить, какая модель данных лучше издательства
отвечает вашей специфике работы. www.dmkpress.com

Для пользователей Microsoft Excel среднего и ISBN 978-5-97060-858-6


продвинутого уровня.

Интернетмагазин: Альберто Феррари и Марко Руссо


www.dmkpress.com
Оптовая продажа:
КТК «Галактика»
e mail: books@alians-kniga.ru www.дмк.рф 9 785970 608586
Альберто Феррари и Марко Руссо

Анализ данных
при помощи Microsoft
Power BI
и Power Pivot
для Excel
Analyzing Data
with Microsoft Power BI
and
Power Pivot for Excel

Alberto Ferrari and Marco Russo


Анализ данных
при помощи Microsoft
Power BI и Power Pivot
для Excel

Альберто Феррари и Марко Руссо

Москва, 2020
УДК 004.424
ББК 32.372
Ф43

Альберто Феррари и Марко Руссо


Ф43 Анализ данных при помощи Microsoft Power BI и Power Pivot для Excel / пер.
с анг. А. Ю. Гинько. – М.: ДМК Пресс, 2020. – 288 с.: ил.

ISBN 978-5-97060-858-6

УДК  004.424
ББК  32.372

В этой книге представлены базовые техники моделирования данных в Excel


и Power BI. Авторы, специалисты в области бизнес-аналитики, делают акцент
на реальных ситуациях, с которыми регулярно сталкиваются как консультан-
ты. Они продемонстрируют общие техники моделирования, научат читателя
производить расчеты с календарем, расскажут об использовании снимков для
подсчета количества товаров в наличии, о том, как работать с несколькими
валютами одновременно, и подробно объяснят на примерах многие другие
полезные операции.
Издание предназначено как для новичков, так и для специалистов в области
моделирования данных, желающих получить советы экспертов. Для изучения
материала требуется владение Excel на среднем или продвинутом уровне.

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

Copyright Authorized translation from the English language edition, entitled


ANALYZING DATA WITH POWER BI AND
POWER PIVOT FOR EXCEL, 1st Edition by ALBERTO FERRARI; MARCO RUSSO,
published by Pearson Education, Inc, publishing as Microsoft Press, Copyright
©2017
RUSSIAN language edition published by DMK PRESS PUBLISHING LTD.,
Copyright © [2020].

ISBN (анг.) 978-1-5093-0276-5 ©  2017 by Alberto Ferrari and Marco Russo


ISBN (рус.) 978-5-97060-858-6 © Оформление, издание, перевод,
ДМК Пресс, 2020
Оглавление

Рецензия.................................................................................................................. 9
Предисловие от издательства.............................................................. 10
Введение............................................................................................................... 11
Для кого предназначена эта книга?............................................ 11
Как мы представляем себе нашего читателя?............................ 11
Структура книги........................................................................... 12
Условные обозначения................................................................. 14
Сопутствующий контент.............................................................. 14
Благодарности............................................................................... 14
Список опечаток и поддержка..................................................... 14
Обратная связь.............................................................................. 15
Оставайтесь с нами....................................................................... 15

Глава 1. Введение в моделирование данных.......................... 17


Работа с одной таблицей.............................................................. 18
Введение в модель данных.......................................................... 25
Введение в схему «звезда»........................................................... 33
Понимание важности именования объектов............................. 40
Заключение................................................................................... 42

Глава 2. Использование
главной/подчиненной таблицы ......................................................... 45
Введение в модель данных с главной и подчиненной
таблицами..................................................................................... 45
Агрегирование мер из главной таблицы.....................................47
Выравнивание главной и подчиненной таблиц......................... 55
Заключение................................................................................... 58

Глава 3. Использование множественных


таблиц фактов................................................................................................... 59
Использование денормализованных таблиц фактов................. 59
Фильтрация через измерения..................................................... 66
Понимание неоднозначности модели данных........................... 69
6    Оглавление

Работа с заказами и счетами....................................................... 72


Расчет полной суммы по счетам для покупателя...................77
Расчет суммы по счетам, включающим данный
заказ от конкретного покупателя............................................ 78
Расчет суммы заказов, включенных в счета........................... 78
Заключение................................................................................... 81

Глава 4. Работа с датой и временем................................................ 83


Создание измерения даты и времени......................................... 83
Понятие автоматических измерений времени...........................87
Автоматическая группировка дат в Excel................................87
Автоматическая группировка дат в Power BI Desktop........... 89
Использование нескольких измерений даты и времени.......... 90
Обращение с датой и временем.................................................. 96
Функции для работы с датой и временем................................... 99
Работа с финансовыми календарями........................................ 101
Расчет рабочих дней................................................................... 104
Учет рабочих дней в рамках одной страны или региона....... 104
Учет рабочих дней в разных странах..................................... 107
Работа с особыми периодами года............................................ 111
Работа с непересекающимися периодами............................ 111
Периоды, связанные с текущим днем................................... 113
Работа с пересекающимися периодами................................ 116
Работа с недельными календарями.......................................... 118
Заключение................................................................................. 124

Глава 5. Отслеживание исторических атрибутов................ 127


Введение в медленно меняющиеся измерения........................ 127
Использование медленно меняющихся измерений................ 133
Загрузка медленно меняющихся измерений........................... 136
Исправление гранулярности в измерении........................... 140
Исправление гранулярности в таблице фактов.................... 143
Быстро меняющиеся измерения............................................... 145
Выбор оптимальной техники моделирования......................... 149
Заключение................................................................................. 150

Глава 6. Использование снимков.................................................... 151


Данные, которые нельзя агрегировать по времени................. 151
Агрегирование снимков............................................................. 153
Понятие производных снимков................................................ 159
Понятие матрицы переходов..................................................... 162
Заключение................................................................................. 168
Оглавление    7

Глава 7. Анализ интервалов даты и времени........................ 169


Введение во временные данные............................................... 170
Агрегирование простых интервалов......................................... 172
Интервалы с переходом дат....................................................... 175
Моделирование рабочих смен
и временных сдвигов................................................................. 180
Анализ активных событий......................................................... 182
Смешивание разных интервалов.............................................. 192
Заключение................................................................................. 198

Глава 8. Связи «многие ко многим».............................................. 201


Введение в связи «многие ко многим»..................................... 201
Понятие шаблона двунаправленной фильтрации............... 203
Понятие неаддитивности...................................................... 206
Каскадные связи «многие ко многим»...................................... 208
Временные связи «многие ко многим»..................................... 211
Факторы перераспределения
и процентные соотношения.................................................. 215
Материализация связей «многие ко многим»....................... 217
Использование таблицы фактов в качестве моста................... 218
Вопросы производительности................................................... 219
Заключение................................................................................. 223

Глава 9. Работа с разными гранулярностями........................ 225


Введение в гранулярности......................................................... 225
Связи на разных уровнях гранулярности.................................. 227
Анализ данных о бюджетировании...................................... 228
Использование DAX для распространения фильтра............ 230
Фильтрация при помощи связей........................................... 233
Скрытие значений на недопустимых
уровнях гранулярности.......................................................... 235
Распределение значений по уровням
с большей гранулярностью.................................................... 239
Заключение................................................................................. 241

Глава 10. Сегментация данных в модели................................. 243


Вычисление связей по нескольким столбцам.......................... 243
Вычисление статической сегментации..................................... 246
Использование динамической сегментации............................ 248
Понимание потенциала вычисляемых столбцов:
ABC-анализ................................................................................. 251
Заключение................................................................................. 256
8    Оглавление

Глава 11. Работа с несколькими валютами............................. 257


Введение в различные сценарии................................................ 257
Несколько валют источника, одна валюта отчета.................... 258
Одна валюта источника, несколько валют отчета.................... 263
Несколько валют источника, несколько валют отчета............. 268
Заключение................................................................................. 270

Приложение A. Моделирование данных 101....................... 271


Таблицы....................................................................................... 271
Типы данных............................................................................... 273
Связи............................................................................................ 273
Фильтрация и перекрестная фильтрация................................. 274
Различные типы моделей.......................................................... 279
Схема «звезда»........................................................................ 279
Схема «снежинка»................................................................... 280
Модели с таблицами-мостами............................................... 281
Меры и аддитивность................................................................. 283
Аддитивные меры.................................................................. 283
Неаддитивные меры.............................................................. 283
Полуаддитивные меры........................................................... 283

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


Рецензия

Вы держите в руках уникальную по нескольким причинам книгу.


Во-первых, это первая книга на русском языке по системе бизнес-
аналитики Microsoft Power BI. В течение нескольких последних лет, ког-
да слушатели после тренингов по Excel, Power Pivot и Query спрашивали
«что мне почитать про Power BI?», я не знал, что ответить. Англоязычной
литературы написано по этой теме уже много, но на русском  – полный
ноль. Теперь уже нет.
Во-вторых, я очень рад, что в качестве первой ласточки издательство
«ДМК Пресс» решило перевести именно эту книгу. Альберто Феррари и
Марко Руссо однозначно входят в круг самых достойных авторов в этой об-
ласти. Они щедро делятся своими знаниями в книгах и статьях, выступают
на конференциях и проводят тренинги по Power Pivot, DAX и Power BI ещё
с самого начала появления этих технологий и знают о них больше, чем кто
бы то ни было. Отдельно, как тренер, хочу отметить их преподавательский
талант, стройность и логичность объяснений, красоту примеров – это до-
рогого стоит.
Бизнес-аналитика (Business Intelligence, BI) давно уже перестала быть
уделом гиков-айтишников из миллиардных корпораций. Сегодня она спо-
собна принести пользу при принятии управленческих решений в компа-
нии любого калибра, помочь визуализировать результаты и непрерывно
отслеживать их динамику, собирая данные из разных «вселенных»: бухгал-
терских программ, баз данных, файлов, интернета. Сегодня каждый мо-
жет (и должен!) быть «сам себе аналитик». И эта книга – настоящий клад
и огромное подспорье для всех, кто встал на этот путь.

Николай Павлов,
Microsoft Certified Trainer, Microsoft Most Valuable Professional,
автор проекта «Планета Excel», www.planetaexcel.ru
Предисловие от издательства
Отзывы и пожелания
Мы всегда рады отзывам наших читателей. Расскажите нам, что вы думаете
об этой книге – что понравилось или, может быть, не понравилось. Отзывы
важны для нас, чтобы выпускать книги, которые будут для вас максимально
полезны.
Вы можете написать отзыв прямо на нашем сайте www.dmkpress.com,
зайдя на  страницу книги, и  оставить комментарий в  разделе «Отзывы
и рецен­зии». Также можно послать письмо главному редактору по адресу
dmkpress@gmail.com, при этом напишите название книги в теме письма.
Если есть тема, в  которой вы квалифицированы, и  вы заинтересова-
ны в написании новой книги, заполните форму на нашем сайте по адресу
http://dmkpress.com/authors/publish_book/ или напишите в издательство
по адресу dmkpress@gmail.com.

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

Нарушение авторских прав


Пиратство в интернете по-прежнему остается насущной проблемой. Изда-
тельство «ДМК Пресс» очень серьезно относится к вопросам защиты автор-
ских прав и лицензирования. Если вы столкнетесь в интернете с незаконно
выполненной копией любой нашей книги, пожалуйста, сообщите нам адрес
копии или веб-сайта, чтобы мы могли применить санкции.
Пожалуйста, свяжитесь с нами по адресу электронной почты dmkpress@
gmail.com со ссылкой на подозрительные материалы.
Мы высоко ценим любую помощь по  защите наших авторов, помогаю-
щую нам предоставлять вам качественные материалы.
Введение

Пользователи Excel любят цифры. А  может, те, кто любят цифры, любят
Excel. Как бы то ни было, если вам нравится доходить до самой сути при
анализе любых наборов данных, скорее всего, вы провели немало времени,
работая с Excel, сводными таблицами и формулами.
В 2015 году увидел свет программный продукт Power BI. И сегодня спра-
ведливо будет утверждать, что те, кто любят цифры, любят также Power Pivot
для Excel и  Power BI. Эти средства имеют много общего – в частности, их
объединяет движок баз данных VertiPaq, а также язык DAX, унаследованный
от SQL Server Analysis Services.
В прежних версиях Excel процесс анализа информации главным обра-
зом основывался на загрузке наборов данных, расчете значений в столбцах
и написании формул для построения графиков. При этом в своей работе вы
сталкивались с  серьезными ограничениями – начиная с  размера рабочей
книги и  заканчивая тем, что язык формул Excel не лучшим образом под-
ходит для решения числовых задач большого объема. Новый движок, лежа-
щий в основе Power BI и Power Pivot, стал огромным шагом вперед. С ним
в вашем распоряжении оказался полный функционал баз данных, а также
потрясающий язык DAX. Но ведь с большой силой приходит и большая от-
ветственность! И если вы хотите воспользоваться всеми преимуществами
этих новых средств, вам придется многому научиться. В частности, необхо-
димо будет познакомиться с основами моделирования данных.
Моделирование данных  – это отнюдь не  ядерная физика, а  лишь на-
бор базовых знаний, которым должен овладеть всякий, кто заинтересован
в  анализе данных. К тому же если вы любите цифры, то вам непременно
придется по душе моделирование данных. Освоить эту науку будет неслож-
но, а вместе с тем вы получите массу удовольствия.
В этой книге вы познакомитесь с базовыми концепциями моделирования
данных на практических примерах, с которыми наверняка не раз встреча-
лись в жизни. В наши планы не входило написание запутанной книги с под-
робным описанием комплексных решений, необходимых для реализации
сложных систем. Вместо этого мы сосредоточились на реальных ситуациях,
с  которыми ежедневно сталкиваемся в  работе в  качестве консультантов.
Когда к нам обращались за помощью, а мы видели, что имеем дело с типич-
ной задачей, то отправляли ее прямиком в архив. Позже, открыв заветный
ящик, мы получили ценные примеры для книги и расположили их в поряд-
ке, пригодном для обучения моделированию данных.
12    Введение

Прочитав эту книгу, вы вряд ли станете гуру в области создания моде-


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

Для кого предназначена эта книга?


Целевая аудитория книги довольно разнообразна. В нее входят и пользова-
тели Excel, применяющие в своей практике Power Pivot, и специалисты по
анализу данных в  Power BI, и  даже новички в  области бизнес-аналитики,
желающие познакомиться с основами моделирования данных. Все они по-
тенциальные читатели данной книги.
Заметьте, что мы не  включили в  этот список тех, кто целенаправленно
хочет почитать о создании моделей данных. Изначально мы предполагали,
что наш читатель может даже не знать, что ему нужно какое-то моделиро-
вание каких-то данных. Наша цель – дать вам понять, что проектирование
моделей данных – это как раз то, что вам нужно, и познакомить с базовыми
принципами этой прекрасной науки. В общем, если вам интересно, что та-
кое моделирование данных и чем оно так полезно, эта книга для вас.

Как мы представляем себе нашего читателя?


Мы предполагаем, что наш читатель обладает базовыми знаниями в обла-
сти сводных таблиц Excel и/или имеет опыт использования Power BI в ка-
честве средства отчетности и моделирования. Наличие аналитических на-
выков также приветствуется. В  своей книге мы не  затрагиваем вопросы
интерфейса Excel или Power BI. Вмес­то этого мы фокусируем свое внимание
исключительно на моделях данных – как проектировать и модифицировать
их так, чтобы значительно упростить запросы. Так что наша задача – рас-
сказать вам, что делать, а как это делать, вы уж решите сами. Мы не плани-
ровали создавать пошаговое руководство, а хотели максимально простым
языком объяснить достаточно сложную тему.
Также мы намеренно обошли вниманием описание языка DAX. Было
бы невозможно уместить в одной книге и теорию моделирования данных,
и DAX. Если вы уже знакомы с этим языком, вам будет проще разобраться
с многочисленными примерами кода на DAX, представленными в данной
книге. В противном случае советуем вам прочитать книгу «Подробное ру-
ководство по DAX» (The Definitive Guide to DAX), являющуюся полноценным
Введение    13

учебником по этому языку и хорошо сочетающуюся с приведенными в на-


шей книге примерами.

Структура книги
Книга начинается с пары легких вводных глав, за которыми следуют главы,
каждая из которых посвящена отдельному виду модели данных. Предлага-
ем вам краткое описание:
 глава 1 «Введение в  моделирование данных». Является вводной ча-
стью в  базовые принципы моделирования данных. В  ней мы рас-
скажем, что из себя представляет модель данных, начнем говорить
о  понятии гранулярности, определим понятия основных моделей
хранилища данных  – «звезда» и  «снежинка»,  – а также поговорим
о нормализации и денормализации;
 глава 2 «Использование главной/подчиненной таблицы». Описывает
наиболее распространенный сценарий с наличием главной и подчи-
ненной таблиц. В этой главе мы обсудим пример с заказами и строка-
ми заказов, размещенными в двух отдельных таблицах фактов;
 глава 3 «Использование множественных таблиц фактов». Описывает
сценарии, в которых у вас есть множество таб­лиц фактов, на основа-
нии которых необходимо построить единый отчет. В  этой главе мы
подчеркнем важность создания корректной многомерной модели для
облегчения работы с информацией;
 глава 4 «Работа с  датой и  временем». Это одна из самых длинных
глав книги. В  ней затронуты вопросы логики расчетов на основа-
нии временных периодов. Мы  расскажем, как правильно создать
таблицу-календарь и  работать с  функциями времени (YTD, QTA,
PARALLELPERIOD и др.). После этого приведем несколько примеров
расчетов на основании рабочих дней, поработаем с особыми перио-
дами года и поясним в целом, как правильно работать с датами;
 глава 5 «Отслеживание исторических атрибутов». В этой главе опи-
сываются особенности использования в  модели данных медленно
меняющихся измерений. Также представлено детальное описание
трансформаций, которые необходимо выполнить для отслежива-
ния исторических атрибутов, и  даны инструкции по написанию
корректного кода на DAX, учитывающего медленно меняющиеся
измерения;
 глава 6 «Использование снимков». Описывает любопытные аспекты
использования снимков (snapshot). В этой главе вы узнаете, что такое
снимки, когда и  для чего их необходимо использовать, а также как
рассчитывать значения при применении снимков. Кроме того, мы
посмотрим, как можно использовать мощную модель с применением
матрицы переходов;
14    Введение

 глава 7 «Анализ интервалов даты и времени». В этой главе мы пойдем


еще на шаг дальше, чем в  главе 5. Мы  продолжим заниматься вре-
менными вычислениями, но на этот раз обратимся к модели данных,
в которой события, хранящиеся в таблице фактов, обладают опреде-
ленной длительностью, а значит, требуют особого подхода для полу-
чения корректных результатов;
 глава 8 «Связи многие ко многим». Описывает характерные особен-
ности использования связей «многие ко многим». Такой тип связи
играет важную роль в любой модели данных. Мы рассмотрим обыч-
ные связи «многие ко многим», связи с каскадными действиями и их
использование с  учетом факторов перераспределения и  фильтров.
Также обсудим вопросы производительности таких связей и способы
ее улучшения;
 глава 9 «Работа с разными гранулярностями». В этой главе мы углу-
бимся в  работу с таблицами фактов с  разными уровнями грануляр-
ности. Мы рассмотрим примеры из области бюджетирования, в кото-
рых таблицы фактов будут хранить информацию с разной степенью
детализации, и предложим несколько альтернативных способов для
решения этих ситуаций как при помощи языка DAX, так и непосред-
ственно в модели данных;
 глава 10 «Сегментация данных в модели». В этой главе мы рассмот­
рим несколько моделей с  применением техники сегментации. Нач-
нем с простой сегментации по цене, пос­ле чего перейдем к анализу
динамической сегментации с  использованием виртуальных связей.
В конце главы проведем ABC-анализ средствами DAX;
 глава 11 «Работа с несколькими валютами». В этой главе мы рассмот­
рим особенности работы с  несколькими валютами. Взаимодействуя
с  курсами валют, важно понимать их специфику и  в  соответствии
с ней строить модель данных. Мы проанализируем несколько сцена-
риев с разными требованиями и для каждого из них выработаем оп-
тимальное решение;
 приложение A «Моделирование данных 101». Это приложение можно
рассматривать как справочное руководство. Здесь мы кратко опишем
на примерах все базовые концепции, использованные в  этой книге.
При возникновении вопросов вы всегда можете обратиться к приложе-
нию, освежить в памяти соответствующую тему и вернуться к чтению.
Сложность моделей и решений будет возрастать на протяжении всей кни-
ги, так что мы советуем читать ее последовательно, а не прыгать от главы
к главе. Так вы сможете постепенно идти от простого к сложному и осваи­
вать по одной теме за раз. После прочтения книга может стать для вас спра-
вочным руководством, и когда вам потребуется построить ту или иную мо-
дель данных, вы можете смело открыть нужную главу и  воспользоваться
предложенным решением.
Введение    15

Условные обозначения
В этой книге приняты следующие условные обозначения:
 жирным помечен текст, который вводите вы;
 курсив используется для обозначения новых терминов;
 программный код обозначен в книге моноширинным шрифтом;
 первые буквы в  названиях диалоговых окон, их элементов, а также
команд – прописные. Например, в  диалоговом окне Save As... (Со-
хранить как…);
 комбинации нажимаемых клавиш на клавиатуре обозначаются зна-
ком плюс (+) между названиями клавиш. Например, Ctrl+Alt+Delete
означает, что вы должны одновременно нажать клавиши Ctrl, Alt
и Delete.

Сопутствующий контент
Для подкрепления ваших навыков на практике мы снабдили книгу сопут-
ствующим контентом, который можно скачать по ссылке: https://aka.ms/
AnalyzeData/downloads.
Представленный архив содержит файлы в форматах Excel и/или Power BI
Desktop для всех примеров из этой книги. Каждому рисунку соответствует
отдельный файл, чтобы вы имели возможность анализировать разные шаги
и присоединиться к выполнению примера на любой стадии. Для большин-
ства примеров представлены файлы в формате Power BI Desktop, так что мы
настоятельно рекомендуем вам установить этот программный пакет с сай-
та Power BI.

Благодарности
В конце вводной главы мы бы хотели выразить благодарность нашему ре-
дактору Кейт Шуп (Kate Shoup), которая помогала нам на протяжении всей
книги, и техническому редактору Эду Прайсу (Ed Price). Если бы не их до-
тошность, читать эту книгу было бы гораздо труднее. Если книга содержит
меньше ошибок, чем наша первоначальная рукопись, это только их заслуга.
А во всех оставшихся неточностях виноваты лишь мы.

Список опечаток и поддержка


Мы сделали все возможное, чтобы текст и сопутствующий контент к этой
книге не  содержали ошибок. Все неточности, которые были обнаружены
пос­ле публикации издания, перечислены на сайте Microsoft Press по адресу:
https://aka.ms/AnalyzeData/errata.
16    Введение

Если вы нашли опечатку, которая не указана в перечне, вы можете оповес­


тить нас на той же странице.
Если вам требуется дополнительная помощь, направьте письмо в Microsoft
Press Book Support по адресу: mspinput@microsoft.com.
Отметим, что услуги по поддержке программного обеспечения Microsoft
по этому адресу не оказываются.

Обратная связь
Ваше удовлетворение от книги  – главный приоритет для Microsoft Press,
а ваша обратная связь – наш самый ценный актив. Пожалуйста, выскажите
свое мнение об этой книге по адресу: https://aka.ms/tellpress.
Пройдите небольшой опрос, и мы прислушаемся ко всем вашим идеям
и пожеланиям. Заранее благодарим за ваши отзывы!

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

Книга, которую вы держите в  руках, посвящена моделированию данных


(data modeling). Но перед тем как приступать к чтению, неплохо бы по-
нять, зачем вам вообще нужно изучать моделирование данных. В конце
концов, вы можете просто загрузить нужные данные в Excel и построить
на их основе сводную таблицу. Так зачем вам еще что-то знать о модели-
ровании данных?
К нам как к  консультантам в  этой области часто обращаются частные
лица и  компании, которые не  могут рассчитать какие-то нужные им по-
казатели. При этом они понимают, что все исходные данные для расчета
у  них есть, но либо формула получается чересчур сложной и  запутанной,
либо цифры не сходятся. В 99 % случаев причиной является неправильно
спроектированная модель данных (data model). Если ее поправить, формула
станет простой и понятной. Так что вам просто необходимо научиться мо-
делировать данные, если вы хотите улучшить свои аналитические навыки
и  предпочитаете концентрироваться на принятии правильных решений,
а не на поиске замысловатой формулы в справочнике по DAX.
Обычно считается, что моделирование данных  – непростая тема для
изуче­ния. И мы не станем этого отрицать. Это действительно сложная об-
ласть. Она потребует от вас серьезных усилий, к тому же вам нужно будет
постараться перестроить сознание так, чтобы сразу мыслить категориями
модели данных, рассуждая о возможных сценариях. Так что да, моделиро-
вание данных – тема непростая, ресурсоемкая и требующая немалых уси-
лий в освоении. Иными словами, сплошное удовольствие!
В этой главе мы покажем вам несколько примеров того, как правильно
спроектированная модель данных помогает облегчить написание итого-
вых формул. Конечно, это всего лишь примеры, и они могут не относить-
ся напрямую к стоящим перед вами задачам. Но мы надеемся, что их бу-
дет достаточно для понимания того, почему стоит изучать моделирование
данных. Быть хорошим специалистом по моделированию данных – значит
уметь подгонять актуальную модель под шаблоны, изученные и решенные
18    Введение в моделирование данных

другими. Ваша модель данных ничем не отличается от других. Да, в ней есть
свои особенности, но высока вероятность, что до вас с подобными задачами
уже кто-то сталкивался. Научиться выявлять сходства между вашим при-
мером и моделями, описанными в книге, не так просто, но в то же время
очень приятно. Когда вы достигнете успеха в этом, решения задач начнут
появляться перед вами сами, а  большинство проб­лем с  расчетом нужных
вам показателей просто исчезнут.
В основном в  своих примерах мы будем использовать базу данных
Contoso. Это вымышленная компания, торгующая элект­роникой по всему
миру с  использованием различных каналов продаж. Вероятно, вы ведете
совершенно иной бизнес – в этом случае вам придется адаптировать отчеты
под свои нужды.
Поскольку это первая глава, начнем мы с описания общей терминоло-
гии и концепции. Мы расскажем, что такое модель данных и почему в ней
так важны связи. Также мы познакомимся с  понятиями нормализации/
денормализации и схемой «звезда». На протяжении всей книги мы будем
описывать новые концепции на примерах, но в  первой главе это будет
наиболее заметно.
Пристегните ремни! Пришло время узнать все тайны о моделировании
данных.

Работа с одной таблицей


Если вы используете Excel и  сводные таблицы для анализа данных, вели-
ка вероятность, что вы загружаете информацию посредством запроса из
какого-то источника – обычно из базы данных. После этого строите свод-
ную таблицу и приступаете к анализу. Разумеется, при этом вы вынуждены
мириться с некоторыми ограничениями Excel, главным из которых являет-
ся лимит на количество строк в таблице, равный одному миллиону. Больше
записей просто не  поместится на рабочем листе. Честно говоря, в  начале
своего пути мы не рассматривали эту особенность как серьезный сдержива-
ющий фактор. В самом деле, зачем кому-то может понадобиться загружать
в Excel миллион строк, если можно воспользоваться базой данных? Причи-
на может быть в том, что работа с Excel не требует от пользователя знаний
в области моделирования данных, а с базой данных – требует.
Так или иначе, эта особенность Excel является существенным ограниче-
нием. В базе данных Contoso, которую мы используем в примерах, табли-
ца продаж содержит 12 млн записей. Так что мы не  можем просто взять
и поместить их все на лист Excel. Но эта проблема легко решается. Вместо
того чтобы загружать данные целиком, вы можете сгруппировать их, чтобы
сократить количество строк. Если, допустим, вам необходимо проанализи-
ровать продажи в  разрезе категорий и  подкатегорий товаров, вы можете
наложить соответствующие группировки, что существенно снизит объем
загружаемой информации.
Работа с одной таблицей    19

К примеру, разделение исходной таблицы из 12 млн строк на группы по


производителю, бренду, категории и  подкатегории с  сохранением дета-
лизации продаж до дня позволило нам сократить количество записей до
63 984, что вполне приемлемо для загрузки на лист Excel. Написание запро-
са для выполнения подобной группировки – это задача для отдела ИТ или
подходящего редактора запросов, если вы, конечно, не знаете язык SQL. Вы-
полнив получившийся запрос, вы можете приступать к анализу. На рис. 1.1
можно видеть первые несколько строк после импорта данных в Excel.

Рис. 1.1. Данные о продажах, сгруппированные для облегчения анализа

После загрузки таблицы в Excel вы можете наконец почувствовать себя


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

Рис. 1.2. На основании данных в Excel легко можно создать сводную таблицу

Верите вы или нет, но только что вы построили свою первую модель дан-
ных. Да, она состоит всего из одной таблицы, но тем не  менее это модель
данных. А значит, вы можете исследовать ее аналитический потенциал и ис-
кать способы для его повышения. У представленной модели есть одно серь­
езное ограничение – она содержит меньше строк, чем исходная таблица.
20    Введение в моделирование данных

Будучи новичком в  Excel, вы могли бы подумать, что лимит в  миллион


строк распространяется только на исходные данные, которые вы загружае-
те для дальнейшего анализа. И хотя это верно, важно также понимать, что
данное ограничение автоматически переносится и на модель данных, что
негативно сказывается на аналитическом потенциале отчетов. Фактически,
для того чтобы сократить количество строк, вы вынуждены были произво-
дить группировку на уровне исходных данных и извлекать продажи, сгруп-
пированные по определенным столбцам.
Таким образом, вы косвенно ограничили свои аналитические возможно-
сти. К примеру, вы не сможете провести аналитику по цвету товаров на осно-
вании полученной таблицы, поскольку информация об этой характеристи-
ке просто отсутствует. Добавить столбец к таблице – не проблема. Проблема
в  том, что при добавлении столбцов будет автоматически увеличиваться
размер таблицы как в ширину (в количестве столбцов), так и в длину (в ко-
личестве строк). На практике одна строка для отдельной категории – напри-
мер, аудиотехники (Audio) – превратится в  несколько записей, каждая из
которых будет содержать свой цвет для этой категории.
А если вы не сможете заранее решить, какие столбцы вам пригодятся для
выполнения срезов, то вам придется загружать все 12 млн строк, а с таким
объемом Excel не справится. Именно это мы имели в виду, когда говорили,
что потенциал Excel в отношении моделирования данных невелик. Ограни-
чение на количество импортируемых строк делает невозможным проведе-
ние анализа больших объемов данных.
Здесь вам на помощь приходит Power Pivot. Используя Power Pivot, вы
не будете ограничены миллионом строк. Фактически количество записей,
загружаемых в таблицу Power Pivot, ничем не ограничено. А значит, вы лег-
ко сможете импортировать в свою модель все продажи и проводить на их
основании более глубокий анализ.

Примечание. Power Pivot доступен в  Excel с  версии 2010 в  ка-


честве внешней надстройки, а  начиная с  Excel 2013 включен
в  основной пакет. В  Excel 2016 и  следующих версиях Microsoft
ввела новый термин для описания моделей Power Pivot: мо-
дель данных Excel (Excel Data Model). Однако термин Power Pivot
по-прежнему широко используется.

Располагая полной информацией о продажах в одной таблице, вы можете


проводить более детализированный анализ. К примеру, на рис. 1.3 вы види-
те сводную таблицу, построенную на основе модели данных Power Pivot со
всеми загруженными столбцами. Теперь вы можете осуществлять срезы по
категории товара, цвету и году, поскольку вся эта информация находится
в модели. Чем больше столбцов, тем выше аналитический потенциал.
Работа с одной таблицей    21

Рис. 1.3. Если в модель данных загружены все столбцы, можно строить более
интересные сводные таблицы

Этого примера достаточно, чтобы усвоить первый урок, касающийся мо-


дели данных: размер имеет значение, поскольку он напрямую связан с грану-
лярностью. Но что такое гранулярность? Гранулярность (granularity) – одна
из важнейших концепций, описываемых в  этой книге, и  мы постараемся
познакомить вас с  ней как можно раньше. Далее в  книге мы углубимся
в изуче­ние этой концепции, а сейчас позвольте дать простое описание тер-
мина гранулярность. В первом наборе данных вы сгруппировали информа-
цию по категории и подкатегории, пожертвовав детальными данными ради
уменьшения размера таблицы. Говоря техническим языком, вы установили
гранулярность таблицы на уровне категории и  подкатегории. Можете ду-
мать о  гранулярности как об уровне детализации данных. Чем выше гра-
нулярность, тем более детализированная информация будет доступна для
анализа. В последнем рассмотренном наборе данных, загруженном в Power
Pivot, гранулярность установлена на уровне товара (на самом деле она даже
выше  – на уровне каждой отдельной продажи), тогда как в  предыдущем
примере была на уровне категории и подкатегории. Возможности для де-
тального анализа напрямую связаны с  количеством доступных столбцов
в  таблице, а  значит, с  ее гранулярностью. Вы  уже знаете, что увеличение
количества столбцов непременно ведет к увеличению количества строк.
Выбрать правильный уровень гранулярности всегда непрос­то. При невер-
ном выборе практически невозможно будет извлечь нужную информацию
при помощи формул. У вас либо попросту не будет этих данных в таблице
(как в примере с отсутствующим цветом товаров), либо эти данные будут
разбросаны по всему набору. При этом неправильно будет говорить, что бо-
лее высокий уровень гранулярности таблицы – это всегда хорошо. Нужно
стремиться, чтобы гранулярность была установлена на оптимальном уров-
не с учетом ваших требований к дальнейшему анализу данных.
Мы уже рассматривали пример с  потерянными данными. А  что значит
выражение «данные разбросаны по всему набору»? Проиллюстрировать
такое поведение информации несколько сложнее. Представьте, к примеру,
что вам необходимо получить средний годовой доход клиентов, покупаю-
22    Введение в моделирование данных

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


ствует – у нас ведь есть все сведения о наших покупателях. На рис. 1.4 по-
казан фрагмент таблицы с нужными нам столбцами (необходимо открыть
окно Power Pivot, чтобы увидеть содержимое таблицы).

Рис. 1.4. Информация о покупателях и товарах содержится в одной таблице

В каждой строке таблицы продаж в отдельном столбце указывается вели-


чина годового дохода клиента, купившего этот товар. В попытке вычислить
средний годовой доход покупателя мы можем попробовать создать меру
при помощи следующего кода на DAX:
AverageYearlyIncome := AVERAGE ( Sales[YearlyIncome] )

Созданная мера отлично работает, и вы можете использовать ее в свод-


ной таблице, как это показано на рис. 1.5. Здесь мы видим средний годовой
доход покупателей бытовой техники (Home Appliances) разных брендов.

Рис. 1.5. Анализ среднего годового дохода покупателей бытовой техники

Отчет выглядит замечательно, но, к сожалению, цифры в нем не соответ-


ствуют действительности – они чересчур завышены. Фактически вы вычис-
ляете среднее значение по таблице продаж с гранулярностью, установлен-
ной на уровне каждой продажи. Иными словами, в этой таблице содержатся
строки для каждой продажи, а значит, покупатели в ней будут повторяться.
Так, если покупатель приобрел три товара в разные дни, при подсчете сред-
него значения годовой доход для него будет учтен трижды, что приведет
к ошибочным результатам.
Работа с одной таблицей    23

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


величину годового дохода. Но это не совсем так. Для того чтобы рассчитать
средневзвешенное, нам необходимо было бы задать вес для каждой состав-
ляющей, а брать в качестве веса количество покупок было бы неправильно.
Более логично было бы определить как вес количество купленных товаров,
сумму покупки или еще какой-то значимый показатель. Кроме того, в дан-
ном примере мы планировали вычислять обычное среднее значение годо-
вого дохода покупателей, и созданная мера нам в этом ничуть не помогла.
И хотя это не так просто заметить, здесь мы также столкнулись с пробле-
мой некорректно выбранной гранулярности. Получается, что информация,
которая нам нужна, доступна, но не привязана к конкретному покупателю,
а вместо этого разбросана по таблице продаж, что значительно затрудняет
вычисления. Чтобы получить корректный результат, необходимо изменить
гранулярность до уровня покупателя – либо путем повторной загрузки таб­
лицы, либо воспользовавшись сложной формулой на языке DAX.
Если вы решите пойти по пути DAX, можно для вычисления среднего го-
дового дохода воспользоваться следующей формулой, довольно сложной
для понимания:
CorrectAverage := AVERAGEX (
SUMMARIZE (
Sales;
Sales[CustomerKey];
Sales[YearlyIncome]
);
Sales[YearlyIncome]
)

В этой не самой простой формуле мы сначала агрегируем продажи на уров-


не (гранулярности) покупателя, после чего применяем к результирующей та-
блице, в  которой каждый покупатель появляется только один раз, функцию
AVERAGEX. В примере мы применяем функцию SUMMARIZE для предваритель-
ной агрегации на уровне покупателя во временной таб­лице, а затем вычисляем
среднее значение по YearlyIncome. Как видно по рис. 1.6, итоги правильного рас-
чета среднего годового дохода сильно отличаются от наших прежних расчетов.

Рис. 1.6. При взгляде на результаты вычислений видно, как далеки мы были от истины
24    Введение в моделирование данных

Необходимо хорошо усвоить один простой факт: сумма годового дохода –


это величина, обладающая смыслом на уровне гранулярности покупателя.
На уровне конкретной продажи этот показатель совершенно неуместен, хоть
и показывает верные цифры. Иными словами, мы не можем использовать
значение, актуальное на уровне покупателя, с тем же смыслом и на уровне
продажи. Таким образом, чтобы получить верный результат, нам пришлось
понижать гранулярность исходных данных, пусть и во временной таблице.
Из этого примера можно сделать пару важных выводов:
 правильная формула оказалась куда сложнее простого использования
функции AVERAGE. Нам пришлось производить временную агрега-
цию, чтобы скорректировать гранулярность таблицы, поскольку нуж-
ная информация оказалась разбросана по всему набору данных, а не
организована должным образом;
 вероятно, вам было бы непросто понять, что произведенные вами
расчеты неверны. В  нашем примере достаточно одного взгляда на
рис. 1.6, чтобы заподозрить наличие ошибки – вряд ли у всех наших
покупателей средний годовой доход превышает 2 млн долларов. Од-
нако для более сложных расчетов выявить неточность может быть
весьма проблематично, что приведет к  появлению ошибок в  вашей
итоговой отчетности.
Необходимо повышать гранулярность таблицы, чтобы извлекать ин-
формацию нужной вам степени детализации, но если зайти в этом слиш-
ком далеко, могут возникнуть сложности с  вычислением некоторых
показателей. Как же выбрать правильный уровень гранулярности? Это не-
простой вопрос, и ответ на него мы прибережем на потом. Мы надеемся,
что сможем научить вас выбирать оптимальный уровень гранулярности
таб­лиц, но не забывайте, что это действительно сложная задача даже для
опытных специалистов. А пока достаточно вводных слов о том, что из себя
представляет гранулярность и как она важна для каждой таблицы в вашей
модели данных.
На самом деле модели данных, которую мы до сих использовали в наших
примерах, присуща одна серьезная проблема, отчасти связанная с  грану-
лярностью. Основной ее недостаток состоит в том, что все данные у нас со-
браны в одной таблице. Если ваша модель, как в наших примерах, состоит
из одной таблицы, то вам придется выбирать для нее гранулярность с уче-
том всех возможных видов отчетов, которые вы захотите формировать в бу-
дущем. Как бы вы ни старались, выбранная гранулярность никогда не будет
идеально подходить для всех создаваемых вами мер. В следующих разделах
мы рассмотрим вариант использования в модели данных сразу нескольких
таблиц, что даст вам возможность оперировать более чем одним уровнем
гранулярности.
Введение в модель данных    25

Введение в модель данных


Из предыдущей главы вы узнали, что модель данных, состоящая из одной
таблицы, таит в  себе проблему в  отношении определения правильного
уровня гранулярности. Пользователи Excel зачастую применяют такие мо-
дели, поскольку до версии Excel 2013 строить сводные таблицы можно было
только на их основании. В Excel 2013 компания Microsoft ввела понятие мо-
дели данных Excel, чтобы можно было загружать сразу несколько таб­лиц
и создавать связи между ними – это позволило пользователям программы
строить очень мощные модели данных.
Что же такое модель данных? Модель данных – это просто набор таблиц,
объединенных связями (relationships). Модель из одной таблицы – тоже мо-
дель, хоть и не представляющая большого интереса. Именно связи, объеди-
няющие несколько таб­лиц в  составе единой модели данных, и  делают ее
столь мощной и удобной для анализа.
Создание модели данных вполне естественно при загрузке сразу не-
скольких таблиц. Более того, обычно информация импортируется из баз
данных, обслуживаемых специалистами, которые уже создали модель
данных за вас. Это означает, что ваша модель зачастую будет просто
имитировать модель из источника данных. В  таком случае ваша работа
сущест­венно упрощается.
К сожалению – и вы поймете это, читая книгу, – модель данных в источ-
нике очень редко будет отвечать всем вашим требованиям в плане будущего
анализа информации. Наша задача – на примерах с возрастающей сложно-
стью научить вас проектировать собственную модель данных, отталкиваясь
от источника. А  чтобы упростить процесс обучения, мы будем знакомить
вас с имеющимися техниками последовательно – от простого к сложному.
И начнем с самых основ.
Для знакомства с концепцией модели данных загрузите таб­лицы Product
и  Sales из базы данных Contoso в  модель Excel. После этого вы увидите
диаграмму как на рис.  1.7 – с двумя таб­лицами и  содержащимися в  них
столбцами.

Примечание. В Power Pivot вы можете получить доступ к диаграм-


ме связей. Для этого выберите вкладку Power Pivot на ленте Excel
и нажмите Manage (Управление). Далее на вкладке Home (В нача-
ло) окна Power Pivot нажмите Diagram View (Представление диа-
граммы) в группе View (Просмотр).
26    Введение в моделирование данных

Рис. 1.7. В модель данных вы можете загружать несколько таблиц

Две несвязанные таблицы в представленном примере еще не являются


полноценной моделью данных. Пока это просто две таблицы. Чтобы преоб-
разовать их в осмысленную модель, необходимо установить связи между та-
блицами. В нашем примере обе таблицы содержат общее поле ProductKey.
В таблице Product этот столбец представляет собой первичный ключ (primary
key), что предполагает уникальность значений в нем и возможность иден-
тифицировать по ним товары. В  таблице Sales этот столбец служит иной
цели, а именно для идентификации проданного товара.

Информация. В столбце, являющемся первичным ключом табли-


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

Если у вас есть уникальный идентификатор в одной таблице и поле в дру-


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

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


менить модель данных при помощи определенных техник, описываемых
в этой книге. А сейчас давайте на нашем примере поясним некоторые осо-
бенности связей:
 таблица Sales называется таблицей-источником (source table).
Связь берет свое начало из таблицы Sales. Это означает, что для того,
чтобы получить товар, вы всегда начинаете с продажи. Получив зна-
чение ключевого поля товара из таблицы Sales, вы ищете его в табли-
це Product. Теперь вы знаете, с каким товаром имеете дело, а также
получаете доступ ко всем его атрибутам;
 таблица Product называется целевой (target table) для этой связи.
Вы начинаете поиск с таблицы Sales и переходите к Product. Значит,
таблица Product и есть цель устанавливаемой связи;
 связь берет свое начало из таблицы-источника и направляется
к целевой таблице. Иными словами, у связи есть направление. По-
этому на диаграммах связь час­то сопровождает стрелка, идущая от
источника к цели. Но в разных программных продуктах графическое
отображение связи свое;
 таблица-источник также именуется в  связи как «многие». Этим
названием таблица обязана тому, что для каждого товара в таблице
продаж может быть много записей, тогда как каждой продаже соот-
ветствует лишь один товар. По той же причине целевой таблице в свя-
зи отводится название «один». В этой книге мы будем пользоваться
именно этой терминологией;
 столбец ProductKey присутствует в  обеих таблицах. При этом
в таблице Product это ключевое поле, а в таблице Sales – нет. По дан-
ной причине применительно к  таб­лице Product мы называем поле
ProductKey первичным ключом, тогда как в таблице Sales оно имену-
ется внешним ключом. Под внешним ключом (foreign key) подразуме­
вается столбец, указывающий на первичный ключ в другой таблице.
Все эти термины широко используются в  области моделирования дан-
ных, и эта книга не станет исключением. Представив терминологию нашим
читателям, мы будем использовать ее на протяжении всей книги. Но  не
волнуйтесь. В первых главах мы будем напоминать вам значение того или
иного определения, пока вы к ним не привыкнете.
Используя Excel и Power BI, вы имеете возможность создавать связи пу-
тем перетаскивания мышью поля, являющегося внешним ключом (в  на-
шем случае это ProductKey в  таблице Sales), к  первичному ключу (у  нас
это ProductKey в  таблице Product). Сделав это, вы заметите, что ни Excel,
ни Power BI не используют стрелки для обозначения связей. Вместо этого
на концах линии, соединяющей таблицы, вы обнаружите единичку (один)
и звездочку (многие). На рис. 1.8 представлена соответствующая диаграмма
из Power Pivot. Заметьте, что посередине линии все же присутствует стрелка,
28    Введение в моделирование данных

но она не определяет направление связи. Вместо этого она служит совсем


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

Рис. 1.8. Связь между таблицами представлена линией с индикаторами на концах («1»


для одного и «звездочка» для многих)

После связывания таблиц вы можете осуществлять суммирование зна-


чений в таблице Sales, делая срезы по столбцам из таблицы Product. К при-
меру, как показано на рис. 1.9, вы можете использовать цвет товара (стол-
бец Color из таблицы Product, как видно на рис. 1.8) в качестве среза при
суммировании по количеству проданных товаров (столбец Quantity в таб­
лице Sales).

Примечание. Если вы не видите вкладку Power Pivot в Excel, вероят-


но, произошла какая-то ошибка, в результате чего надстройка была
отключена. Чтобы вновь активировать ее, нажмите на вкладке File
(Файл) и выберите пункт Options (Параметры) на левой панели. В ле-
вой части окна Excel Options (Параметры Excel) нажмите на Add-Ins
(Надстройки). После этого раскройте выпадающий список Manage
(Управление), выберите пункт COM Add-Ins (Надстройки COM) и на-
жмите Go  (Перейти). В  окне COM Add-Ins (Надстройки для модели
компонентных объектов (COM)) выберите Microsoft Power Pivot for
Excel. В том случае, если этот пункт выбран, снимите выделение. Пос­
ле этого нажмите OK. Если вы снимали выделение пункта Microsoft
Power Pivot for Excel, вернитесь в окно COM Add-Ins и снова выбери-
те его. Вкладка Power Pivot должна появиться на ленте.
Введение в модель данных    29

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


таблицы, используя столбцы из другой

Это был ваш первый пример модели данных, состоящей из двух таблиц.
Как мы уже сказали, модель данных – это просто набор таблиц (в  нашем
случае Sales и Product), объединенных связями. Перед тем как идти дальше,
давайте уделим еще немного времени гранулярности – на этот раз приме-
нительно к модели из нескольких таблиц.
В первом разделе этой главы вы уяснили, насколько важно (и  сложно)
определить правильный уровень гранулярности для конкретной таблицы.
При неправильном выборе гранулярности дальнейшие расчеты в этой таб­
лице существенно усложнятся. А что можно сказать о гранулярности в но-
вой модели данных, состоящей из двух таблиц? В этом случае вы столкне-
тесь с задачей иного характера, решить которую будет в каком-то смысле
проще, но понять – сложнее.
Поскольку теперь у вас в наличии есть две таблицы, то и гранулярностей
будет две. В таблице Sales гранулярность установлена на уровне продажи,
а в таблице Product – на уровне товара. Фактически гранулярность как кон-
цепция относится к таблице, а не к модели данных в целом. Когда в вашей
модели несколько таблиц, вы должны позаботиться о том, чтобы в каждой
из них была настроена гранулярность. Даже если сценарий с наличием не-
скольких таблиц кажется вам более сложным по сравнению с единственной
таблицей, моделью данных, созданной на их основе, будет гораздо легче
управлять, а гранулярность перестанет быть проблемой.
Более того, в этом случае совершенно естественно будет установить гра-
нулярность в  таблице Sales на уровне продажи, а  в таблице Product  – на
уровне товара. Вспомните первый пример из этой главы. У нас была одна
таблица продаж с  гранулярностью, установленной на уровне категории
и  подкатегории товара. Причиной было то, что информация о  категории
и подкатегории товара хранилась в таблице Sales. Иными словами, вам не-
обходимо было принимать решение по поводу гранулярности, потому что
30    Введение в моделирование данных

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

Информация. В хорошо спроектированной модели данных гра-


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

Если внимательно посмотреть на таблицу Product, можно заме-


тить, что в  ней отсутствуют категория и  подкатегория. Зато есть столбец
ProductSubcategoryKey, название которого говорит о том, что это внешний
ключ, ссылающийся на другую таблицу (где это поле будет первичным клю-
чом) с  перечислением подкатегорий товаров. Фактически в  базе данных
категории и  подкатегории товаров разделены на две таблицы. Загрузив
в модель данных обе таблицы и правильно построив связи, вы увидите на
диаграмме в Power Pivot схему, показанную на рис. 1.10.

Рис. 1.10. Категории и подкатегории товаров хранятся в разных таблицах, к которым


можно обратиться посредством связей

Как видите, информация о  товарах разнесена сразу на три таб­лицы:


Product, Product Subcategory и Product Category. Таким образом, образуется це-
Введение в модель данных    31

лая цепочка связей, начиная с Product, через Product Subcategory и к Product
Category.
Что послужило причиной выбора такого подхода к проектированию мо-
дели? Поначалу кажется, что это чересчур усложненный способ для хране-
ния довольно простой информации. Однако у этой техники есть целый ряд
преимуществ, пусть и не столь очевидных с первого взгляда. Вынос катего-
рии товара из таблицы продаж позволяет хранить название категории, к ко-
торой могут принадлежать сразу несколько товаров, в единственной строке
таблицы Product Category. Это правильный способ хранения информации
сразу по двум причинам. Во-первых, это позволяет сохранить место на дис-
ке из-за отсутствия необходимости хранить дублирующуюся информацию.
Во-вторых, при необходимости изменить название категории товара вам
нужно будет сделать это всего в одной строчке. Все товары автоматически
подхватят новое наименование посредством связи.
У такой техники проектирования модели данных есть свое название – нор-
мализация (normalization). Говорят, что атрибут таблицы (вроде нашей ка-
тегории товара) нормализован, если он вынесен в отдельную таблицу, а на
его место помещен ключ, ссылающийся на эту таблицу. Это широко распро-
страненная техника, которую используют архитекторы баз данных при про-
ектировании моделей. Обратная техника, заключающаяся в хранении атри-
бутов в таблице, которой они принадлежат, носит название денормализация
(denormalization). В денормализованной таблице один и тот же атрибут мо-
жет встречаться множество раз, и при необходимости изменить его название
вам придется корректировать все строки, содержащие этот атрибут. К при-
меру, в нашей модели атрибут цвета товара (Color) денормализован, а значит,
значение Red будет повторяться во всех строках с красными товарами.
Вас, должно быть, интересует, почему разработчик базы данных Contoso
решил хранить атрибуты категории и  подкатегории товаров в  отдельных
таблицах (то есть в нормализованном виде), а цвет, наименование произво-
дителя и бренд – в таблице Product (без применения нормализации). В этом
конкретном случае ответ прост: Contoso – это демонстрационная база дан-
ных, и на ее примере хотелось показать все возможные техники. На прак-
тике вы будете встречаться как с  преимущественно нормализованными,
так и с денормализованными моделями в зависимости от особенностей ис-
пользования базы данных. Будьте готовы к тому, что одни атрибуты будут
нормализованы, а другие – нет. Это вполне приемлемо для моделирования
данных, поскольку здесь есть разные методы и подходы. К тому же вполне
возможно, что разработчик базы данных был вынужден принимать то или
иное решение по структуре модели уже в процессе работы.
Модели с  высокой степенью нормализации обычно используются в  си-
стемах обработки транзакций в  реальном времени (online transactional
processing systems – OLTP). Такие базы данных спроектированы специально
для выполнения ежедневных оперативных действий вроде обслуживания
подготовки счетов, размещения заказов, доставки товаров или создания
32    Введение в моделирование данных

и  удовлетворения заявок. Нормализация здесь используется как способ


сокращения занимаемого на диске места (что обычно ведет к увеличению
быстродействия базы данных) и  повышения эффективности операций
вставки и обновления информации, характерных для OLTP-систем. В еже-
дневной работе компании часто выполняются операции обновления дан-
ных (например, о покупателях), и хочется, чтобы обновленная информация
мгновенно распространялась на все таблицы, связанные с  покупателями.
Этого можно добиться путем нормализации соответствующих атрибутов.
В такой системе все заказы, ссылающиеся на конкретного покупателя, будут
обновлены сразу после изменения информации о нем в базе данных. Если
бы атрибуты были денормализованы, то обновление адреса покупателя по-
влекло бы за собой изменение сотен строк в  базе данных, что негативно
сказалось бы на быстродействии системы.
OLTP-системы зачастую насчитывают сотни таблиц, поскольку почти каж-
дый атрибут хранится в отдельной таблице. Применительно к товарам, допус­
тим, можно было бы завести таблицы для хранения производителей, брендов,
цветов и прочего. В результате хранение простой сущности вроде товаров вы-
лилось бы в 10–20 отдельных таблиц, объединенных связями. Разработчик та-
кой базы данных с гордостью назвал бы свое детище «хорошо спроектирован-
ной моделью данных» и, несмот­ря на некоторые ее странности, был бы прав.
Для OLTP-систем нормализация почти всегда будет оптимальным выбором.
Но во время анализа данных вы не выполняете операции вставки и об-
новления. Вас интересует исключительно чтение информации. И  в этом
случае нормализация таблиц вам ни к  чему. Представьте, что вы строите
сводную таблицу на основании нашей предыдущей модели данных. В этом
случае список полей будет выглядеть примерно так, как на рис. 1.11.

Рис. 1.11. В списке полей сводной таблицы, построенной на основании нормализован-


ной модели данных, слишком много таблиц – легко запутаться

Информация о  товарах хранится в  трех таблицах, и  все они представле-


ны в списке полей сводной таблицы. Хуже того, в таб­лицах Product Category
и Product Subcategory содержится всего по одному столбцу. Так что хоть нор-
Введение в схему «звезда»    33

мализация и является оптимальным выбором для OLTP-систем, для нужд ана-


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

Примечание. В этом примере мы намеренно скрыли некоторые


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

В процессе создания модели данных для нужд аналитики вам необходи-


мо прийти к оптимальному уровню денормализации данных вне зависимо-
сти от того, как информация хранится в базе физически. Как вы уже видели,
излишняя денормализация может привести к проблемам с определением
гранулярности таб­лиц. Позже вы узнаете, какие еще негативные послед-
ствия влечет за собой чрезмерное увлечение денормализацией. Какую же
степень денормализации можно считать оптимальной?
Для ответа на этот вопрос нет какого-то единого правила. Вы  должны
интуитивно дойти до такого уровня денормализации, при котором струк-
тура таблицы станет самодостаточной и будет полностью описывать храня-
щуюся в ней сущность. В нашем примере необходимо перенести столбцы
Product Category и  Product Subcategory в  таблицу Product, поскольку они
являются атрибутами товаров и вам не хотелось бы видеть их в отдельных
таблицах. При этом не следует денормализовывать информацию о товарах
в таблице Sales, поскольку товары и продажи – это разные сущности. Кон-
кретная продажа напрямую связана с товаром, но нельзя сказать, что она
составляет с ним единое целое.
На этом этапе вы можете рассматривать модель данных, состоящую из
единственной таблицы, как чрезмерно денормализованную. Это так и есть.
Вспомните, мы задумывались о  том, чтобы установить гранулярность на
уровне товара в таблице Sales, что изначально неправильно. В  корректно
спроектированной модели данных с  оптимальной степенью денормали-
зации проблемы с гранулярностью решаются сами собой. Если же модель
излишне денормализована, начинаются неприятности с  правильным вы-
бором уровня гранулярности.

Введение в схему «звезда»


До сих пор мы имели дело с очень простыми моделями данных, состоящими
из товаров и продаж. В реальном мире такие модели практически не встре-
34    Введение в моделирование данных

чаются. В  распоряжении типичной компании вроде Contoso будет сразу


несколько информационных активов, в числе которых товары, склады, со-
трудники, покупатели и время. Эти активы взаимодействуют друг с другом
и генерируют события. Например, в определенный день сотрудник, работа-
ющий на складе, продал товар конкретному покупателю.
Конечно, каждый бизнес подразумевает свои информационные активы,
и события у всех разные. Но если мыслить в общем, то почти в любом виде
деятельности будет прослеживаться четкое разделение на активы и собы-
тия. К примеру, в случае с медицинским учреждением активами могут быть
пациенты, заболевания и лекарственные препараты, тогда как к событиям
мы причислим постановку диагноза и прием лекарственного средства па-
циентом. В  системе приема заявок к  активам могут относиться клиенты,
заявки и время, а события генерируются в процессе изменения статуса за-
явок. Подумайте о виде деятельности, которым занимаетесь вы. Наверняка
вам также удастся выделить в своей области активы и события.
Такое разделение делает возможным применение специальной техники
моделирования данных, получившей название схема «звезда» (star schema).
В этой схеме все сущности (таблицы) подразделяются на две категории:
 измерения. Измерение (dimension) является информационным ак-
тивом: товар, покупатель, сотрудник или пациент. Измерения содер-
жат атрибуты (attribute). К примеру, атрибутами товара являются его
цвет, категория, подкатегория, производитель и цена. У пациента это
имя, адрес и дата рождения;
 факты. Факт (fact) – это событие, в которое вовлечено несколько из-
мерений. В базе данных Contoso, например, фактом является продажа
товара. В этом событии участвуют сам товар, покупатель, дата прода-
жи и другие измерения. В фактах также содержатся меры (measures) –
числовые показатели, которые можно агрегировать при анализе со-
стояния бизнеса. Это может быть количество или сумма проданного
товара, размер скидки и прочее.
После мысленного разделения таблиц на две категории становится ясно,
что факты связаны с измерениями. Каждому отдельному товару в таблице
продаж соответствует несколько строк. Иными словами, между таблицами
Sales и Product есть связь, в которой Product соответствует стороне «один»,
а Sales – стороне «многие». Если вы расположите на диаграмме в Power Pivot
все измерения вокруг единственной таблицы фактов, то получите типич-
ную форму звезды, показанную на рис. 1.12.
Схема «звезда» легка для чтения, понимания и использования. Измере-
ния используются для осуществления срезов данных, тогда как сама агре-
гация числовых показателей выполняется в таблице фактов. Удобство этой
модели еще и в том, что в списке полей сводной таблицы будет не так много
сущностей.
Введение в схему «звезда»    35

Рис. 1.12. Схема «звезда» приобретает свои очертания после расположения измерений


вокруг таблицы фактов

Примечание. Схема «звезда» получила широкое распространение


в области хранилищ данных. Сегодня такая модель считается стан-
дартом представления информации для нужд аналитики.

По своей природе таблицы измерений содержат не так много строк – мень-


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

Совет. Прежде чем читать дальше, попробуйте представить, как ваша


собственная бизнес-модель может быть реализована с использовани-
ем схемы «звезда». Не стоит на данном этапе пытаться спроектиро-
вать идеальную модель, но размышление над этой задачей поможет
вам в будущем лучше оперировать таб­лицами измерений и фактов.
36    Введение в моделирование данных

Важно привыкнуть к схеме «звезда». Посредством нее ваши данные будут


представлены в наиболее удобном виде. Кроме того, терминология, приме-
няемая в этой схеме, очень широко используется в сфере бизнес-аналитики
(BI), и  эта книга – не  исключение. Мы  часто употребляем термины изме-
рение и  таблица фактов, чтобы подчеркнуть разницу между маленькими
и большими таблицами. В следующей главе мы будем говорить о главных
и подчиненных таблицах, попутно решая задачу установления связей меж-
ду разными таблицами фактов. И к тому моменту мы будем считать, что вы
уже хорошо усвоили разницу между таблицей фактов и измерением.
Стоит отметить несколько важных особенностей устройства схемы «звез-
да». Одной из них является то, что таблицы фактов могут быть объединены
связями с измерениями, тогда как измерения не должны быть связаны меж-
ду собой. Чтобы проиллюстрировать важность этого правила и показать, что
бывает, если ему не следовать, предположим, что мы добавили в модель но-
вое измерение Geography, содержащее географические данные, такие как го-
род, штат и страну/регион рождения. Оба наших измерения Store и Customer
могут быть объединены связью с Geography. В итоге у нас могла бы получить-
ся модель, представленная на рис. 1.13 в виде диаграммы Power Pivot.

Рис. 1.13. Новое измерение Geography объединено связями с Customer и Store


Введение в схему «звезда»    37

В этой модели нарушено правило, запрещающее наличие связей между


измерениями. По сути, все три таблицы – Customer, Store и Geography – яв-
ляются измерениями, но при этом они связаны. Что плохого в такой моде-
ли? А то, что она вносит неоднозначность (ambiguity).
Представьте, что вы делаете срез данных по городу в надежде посчитать
количество проданных товаров. В результате запрос может пройти по связи
между таблицами Geography и Customer и вернуть количество товаров, про-
данное покупателям из выбранного города. А если пройти по связи между
Geography и Store, то мы получим продажи со склада из этого города. Есть
и третий вариант – использовать обе связи и выяснить, какое количество
товаров было продано покупателю из выбранного города, со склада, рас-
положенного там же. У нас получилась неоднозначная модель данных, и по-
нять, какие цифры она выдает, крайне проблематично. И это не только тех-
ническая проб­лема, но и логическая. Пользователь, который будет работать
с этой моделью, будет сбит с толку и не сможет понять, что значат цифры
в отчетах. И именно по причине ее неоднозначности ни Excel, ни Power BI
не позволят вам создать подобную модель. В следующих главах мы будем
рассматривать вопросы неоднозначности моделей более подробно. Пока же
важно знать, что Excel (а именно в нем создавался этот пример) сделал соз-
данную связь между таблицами Store и Geography неактивной, чтобы не до-
пустить неоднозначности в модели данных.
Как разработчик модели вы должны всеми способами стараться избегать
неоднозначности. Как избавить рассматриваемую нами модель от неодно-
значности? Ответ очень прост. Необходимо провести денормализацию мо-
дели – перенести нужные колонки из таблицы Geography в Store и Customer,
а  само измерение с  географией удалить из модели. Также вы могли бы
включить в  измерения колонку ContinentName с  названием континента,
и получилась бы модель, представленная на рис. 1.14.
Проведя денормализацию модели, мы избавили ее от неоднозначности.
Теперь пользователи смогут осуществлять срезы данных, используя геогра-
фические признаки из таблицы Customer или Store. В итоге Geography – это
то же измерение, но для возможности полноценного использования схемы
«звезда» нам пришлось его денормализовать.
38    Введение в моделирование данных

Рис. 1.14. После денормализации колонок из Geography модель вернулась к схеме «звезда»

Напоследок хотелось бы познакомить вас с  еще одним термином, ко-


торый будет часто использоваться в  книге, – снежинка. Схема «снежинка»
(snowflake schema) является разновидностью «звезды» с тем исключением,
что некоторые измерения не связаны с таблицей фактов напрямую. Вместо
этого они объединены с ней посредством других измерений. Вы уже встре-
чались с такой схемой на страницах этой книги, и мы вновь представим вам
ее на рис. 1.15.
Нарушает ли схема «снежинка» правило, запрещающее установку свя-
зей между измерениями? В  каком-то смысле да, ведь таблицы Product
Subcategory и Product представляют собой измерения, и при этом они объ-
единены связью. Отличие этого примера от предыдущего состоит в  том,
что эта связь является единственной, соединяющей таблицу Product
Subcategory с  другими измерениями, объединенными с таблицей фактов,
или таблицей Product. Так что вы можете рассматривать таб­лицу Product
Subcategory как измерение, объединяющее в группы различные товары, но
при этом не  группирующее содержимое других измерений или таблицы
фактов. То же самое верно и для таблицы Product Category. Таким образом,
Введение в схему «звезда»    39

хотя схема «снежинка» и нарушает указанное выше правило, она не создает


в модели данных неоднозначности, а значит, с ней все в порядке.

Рис. 1.15. Измерения Product Category, Subcategory и Product образуют цепочку связей


в виде снежинки

Примечание. Образования схемы «снежинка» можно избежать


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

Как вы узнаете из этой книги, в большинстве случаев схема «звезда» будет


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

Примечание. В процессе изучения моделирования данных в какой-


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

Понимание важности именования объектов


При построении модели данных вы обычно загружаете информацию из
базы данных SQL Server или других источников данных. Велика вероятность,
что разработчик базы данных в процессе именования объектов пользовался
определенным соглашением. В наше время существует великое множество
соглашений об именовании объектов – мы не сильно ошибемся, если ска-
жем, что свое соглашение есть сегодня буквально у каждого.
Многие разработчики при проектировании модели данных предпочита-
ют использовать префикс Dim для названий измерений и Fact для таблиц
фактов. Так что сегодня зачастую можно встретить таблицы с  названия-
ми DimCustomer и FactSales. Другие предпочитают делать различия между
представлениями и физическими таблицами, используя префиксы Vw и Tbl
соответственно. А  кто-то считает, что буквенного обозначения недоста-
точно для полной ясности и  добавляет цифры – получается что-то вроде
Tbl_190_Sales. Продолжать можно до бесконечности, но суть вы уловили.
Стандартов именования масса, и у каждого есть свои плюсы и минусы.

Примечание. Можно поспорить с уместностью применения подоб-


ных стандартов при именовании объектов в базах данных, но эта
дискуссия выйдет далеко за пределы данной книги. Так что мы
ограничимся обсуждением использования соглашений об имено-
вании в моделях данных, которые вы создаете и просматриваете
в Power BI и Excel.

Вы не  обязаны при именовании объектов следовать каким-либо тех-


ническим стандартам – достаточно будет здравого смысла и обеспечения
легкости использования в  дальнейшем. Например, мало кому доставит
удовольствие работа с моделью данных, в которой таблицы носят назва-
ния VwDimCstmr или Tbl_190_FactShpmt. Это очень странные и  малопо-
нятные наборы символов, но, признаться, мы до сих пор встречаемся с по-
добными именами объектов в моделях данных. И это мы говорим только
о правилах именования таблиц. Когда речь заходит о столбцах, все стано-
вится совсем плохо. Единственный наш совет заключается в том, чтобы
использовать легко читающиеся названия, ясно описывающие измерение
или таблицу фактов.
На протяжении лет мы спроектировали множество аналитических си-
стем и за это время выработали очень простой свод правил по именованию
таблиц и столбцов:
 наименование измерения должно состоять только из названия
актива в единственном или множественном числе. Так, к приме-
ру, таблица со списком покупателей может называться Customer или
Customers. Информация о товарах должна храниться в таблице с на-
Понимание важности именования объектов    41

званием Product или Products. Мы считаем, что единственное число


лучше подходит для именования измерений, поскольку оно идеально
сочетается с запросами на естественном языке в Power BI;
 если название актива состоит из нескольких слов, используйте
для их разделения прописные буквы. К примеру, категории това-
ров могут храниться в таблице с названием ProductCategory, а страна
отгрузки может именоваться CountryShip или CountryShipment. Вмес­
то разделения слов прописными буквами допустимо использовать
обычные пробелы  – например, таблица может называться Product
Category. Здесь есть только один минус – код на языке DAX может не-
много усложниться. Но все это на ваше личное усмотрение;
 для имени таблицы фактов необходимо использовать название
фактической операции и всегда применять множественное чис-
ло. Так, факты продаж можно хранить в таблице с названием Sales,
а факты закупок, как вы уже догадались, – в таблице Purchases. Если
вы будете использовать для фактов исключительно множественное
число, то при взгляде на модель данных вам будет представлять-
ся один покупатель (из таблицы Customer) со множеством продаж
(из таблицы Sales), а природа связи «один ко многим» будет читаться
естест­венным образом;
 избегайте использования слишком длинных имен объектов. На-
звания вроде CountryOfShipmentOfGoodsWhenSoldByReseller могут
приводить в замешательство. Никому не интересно будет читать та-
кие длинные имена. Вместо этого лучше подобрать уместную аббре-
виатуру, попутно исключив лишние слова;
 избегайте использования слишком коротких имен. Все любят
использовать в своей речи сокращения. И если в повседневном об-
щении это приемлемо и забавно, то в отчетах часто бывает неумест-
но и вносит неразбериху. К примеру, вы могли бы использовать для
обозначения страны отгрузки для торговых посредников (country
of shipment for resellers) аббревиатуру CSR, но ее будет очень труд-
но запомнить тем, кто не работает с вами изо дня в день. Помните
о том, что отчеты могут использоваться самыми разными пользо-
вателями, многие из которых не имеют понятия о привычных для
вас сокращениях;
 ключевой атрибут в  измерении должен содержать название
таблицы и  окончание Key. Например, первичный ключ в  табли-
це Customer должен называться CustomerKey. То  же самое касается
и внешних ключей. Так что в будущем вы сможете легко определять
внешние поля по окончанию Key и нахождению в таблице с другим
именем. Допустим, поле CustomerKey в таблице Sales является внеш-
ним ключом, ссылающимся на таблицу Customer, где оно выступает
в качестве первичного ключа.
42    Введение в моделирование данных

Как видите, правил немного. Все остальное – на ваше усмот­рение. При


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

Совет. Если сомневаетесь по поводу именования того или иного


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

Заключение
В этой главе вы познакомились с основами моделирования данных, а именно:
 одна таблица – это уже модель данных, пусть и в ее прос­тейшей форме;
 при наличии единственной таблицы вы должны правильно выбрать
ее гранулярность. Это облегчит написание формул в будущем;
 разница между моделью с  одной таблицей и  несколькими состоит
в  том, что во втором случае таблицы объединены между собой по-
средством связей;
 любая связь характеризуется стороной с одним элементом и многи-
ми – этот показатель говорит о том, сколько строк вы обнаружите, про-
следовав по связи в этом направлении. Поскольку один товар может
присутствовать сразу в нескольких продажах, в соответствующей свя-
зи таблица Product будет представлять один элемент, а Sales – многие;
 в целевой для связи таблице обязательно должен присутствовать
первичный ключ – колонка с уникальными значениями, однозначно
определяющими каждую строку. При отсутствии первичного ключа
связь к этой таблице установить невозможно;
 нормализованной моделью данных называется модель, в  которой
информация хранится в компактном виде, без повторения значений
в  разных строках. Обычно нормализация модели ведет к  образова-
нию большого количест­ва таблиц;
 денормализованная модель данных характеризуется множеством по-
вторений значений в строках (например, слово Red (красный) в такой
модели может встречаться многократно – для каждого товара красного
цвета), но при этом содержит меньшее количество таблиц;
Заключение    43

 нормализованные модели данных обычно используются в  OLTP-


системах, тогда как денормализация зачастую применяется к моде-
лям, предназначенным для анализа информации;
 в типичной аналитической модели можно провести четкие различия
между информационными активами (измерениями) и  событиями
(фактами). Разделяя сущности на измерения и факты, мы в конечном
счете выстраиваем структуру модели в виде звезды. Схема «звезда»
является наиболее распространенной архитектурой аналитических
моделей данных по одной простой причине – она отлично работает
в подавляющем большинстве случаев.
Глава 2
Использование главной/
подчиненной таблицы

Теперь, когда вы знакомы с основами моделирования данных, мы можем


начать обсуждение одного из многочисленных сценариев, который со-
стоит в  использовании главной (header) и  подчиненной (detail) таблиц. Это
довольно распространенная ситуация. Сама по себе такая модель данных
не представляет особой сложности в использовании. Однако в ней есть свои
особенности при формировании отчетов с  агрегированными значениями
сразу с обоих уровней.
Типичными примерами модели данных с  главной и  подчиненной таб­
лицами являются счета или заказы со строками в таб­личной части. Опись
материалов также можно отнести к этому типу модели. Еще один пример –
модель распределения людей по командам. В  этом случае команды и  их
участники будут представлять два разных уровня.
Модель с главной и подчиненной таблицами не стоит путать с обычными
иерархиями измерений. Вспомните нашу иерархическую структуру таблиц
товаров, их категорий и подкатегорий. Несмотря на то что здесь у нас сра-
зу три уровня данных, все же это совсем другой шаблон. Модель с главной
и  подчиненной таблицами базируется на создании иерархий на уровне
собы­тий, то есть таблиц фактов. Заказ и его табличная часть представляют
собой факты, пусть и с разной гранулярностью. В то же время товары, кате-
гории и подкатегории из прошлого примера – это измерения. Таким обра-
зом, можно сделать вывод, что модель с главной и подчиненной таблицами
возникает тогда, когда связью объединяются таблицы фактов.

Введение в модель данных с главной


и подчиненной таблицами
В качестве примера мы создали сценарий для базы данных Contoso, кото-
рый будем использовать для демонстрации концепции модели с  главной
и подчиненной таблицами. Диаграмма модели изображена на рис. 2.1.
46    Использование главной/подчиненной таблицы

Рис. 2.1. SalesHeader и SalesDetail составляют основу модели данных с главной и под-


чиненной таблицами

Вооружившись знаниями, полученными в  предыдущей главе, вы легко


определите в  этой схеме слегка модифицированную «звезду». На  самом
деле здесь даже две «звезды», в  основе каждой из которых лежат таблицы
SalesHeader и SalesDetail соответственно, а также связанные с ними измере-
ния. Но пос­ле объединения этих двух групп таблиц схема «звезда» пропада-
ет – и именно из-за связи, объединяющей SalesHeader и SalesDetail. Эта связь
нарушает правила схемы «звезда», поскольку обе таблицы являются факта-
ми. Одновременно с  этим главная таблица выполняет роль измерения по
отношению к подчиненной.
Сейчас вы могли бы сказать, что если мы рассматриваем таблицу SalesHeader
как измерение, а не факт, то перед нами типичная схема «снежинка». Более
того, если мы денормализуем таблицы Date, Customer и Store в SalesHeader, то
придем к самой настоящей «звезде». Но есть сразу две причины, по которым
мы этого не будем делать. Во-первых, в таблице SalesHeader содержится мера
TotalDiscount. И велика вероятность, что вы захотите агрегировать ее в раз-
резе покупателей. Присутствие меры в таблице является одним из главных
признаков того, что перед вами не измерение, а факт. Вторым, и более важ-
ным, аргументом является то, что денормализовывать измерения Customer,
Date и Store в таблице SalesHeader будет большой ошибкой моделирования
данных. Дело в том, что каждое из этих измерений представляет самостоя­
тельный информационный актив в  бизнес-логике организации, и  если их
атрибуты вынести в отдельное измерение, чтобы прийти к схеме «звезда»,
модель станет излишне сложной для дальнейшего анализа.
Агрегирование мер из главной таблицы    47

Как вы узнаете дальше из этой главы, объединять измерения, связанные


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

Агрегирование мер из главной таблицы


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

DiscountValue := SUM ( SalesHeader[TotalDiscount] )

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


в  момент продажи для всего документа в  целом. Иными словами, скидка
у нас содержится не в каждой отдельной строке заказа, а указывается еди-
ной суммой для всей операции. Именно по этой причине ей отведено место
в главной таблице. Мера показывает правильные цифры, пока вы осущест-
вляете срезы по измерениям, напрямую связанным с  главной таблицей
SalesHeader. К  примеру, со сводной таблицей, показанной на рис.  2.2, все
в порядке. Здесь показаны срезы по континентам (атрибуту таблицы Store,
непосредственно связанной с SalesHeader) и годам (измерение Date также
напрямую объединено с главной таблицей).

Рис. 2.2. Вы можете делать срезы по континентам и годам, как в этой сводной таблице

Но как только вы задействуете в  срезах любой атрибут измерения,


не объединенного с SalesHeader напрямую, мера сломается. Допустим, если
попытаться получить размер скидки по товарам определенного цвета, мы
получим результат, показанный на рис. 2.3. Фильтр по годам по-прежнему
48    Использование главной/подчиненной таблицы

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


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

Рис. 2.3. Если сделать срез по товарам, сумма скидки во всех строках будет
дублироваться

Для определенных сценариев такое поведение меры можно считать впол-


не корректным. Пользователи должны понимать, что нельзя осуществлять
срезы по любым измерениям – в каких-то ситуациях расчеты окажутся не-
верны. Но  в данном конкретном случае мы хотели бы получить среднюю
сумму скидки по каждому товару, которую можно взять только из главной
таблицы. Однако это не так просто, как может показаться на первый взгляд,
и причина этих сложностей кроется в модели данных.
Если вы используете Power BI или Analysis Services Tabular 2016 и выше,
вам будет доступна двунаправленная фильтрация (bidirectional filtering). Это
значит, что вы сможете установить направление распространения филь-
тра от таблицы SalesDetail к SalesHeader. В результате фильтр, наложенный
на товары или их цвет, будет распространяться как на таблицу SalesDetail,
так и на SalesHeader, выбирая только интересующие вас заказы. На рис. 2.4
изобра­жена диаграмма модели с двунаправленной фильтрацией, установ-
ленной для связи между таблицами SalesHeader и SalesDetail.
Агрегирование мер из главной таблицы    49

В Excel двунаправленная фильтрация недоступна в  самой модели, но


у вас есть возможность изменить исходный код меры для применения шаб­
лона двунаправленной фильтрации при расчете, как на примере ниже. Бо-
лее подробно мы будем говорить об этой теме в главе 8 «Связи многие ко
многим».

Рис. 2.4. С включенной двунаправленной фильтрацией вы можете распространять


действие фильтра в обе стороны связи

DiscountValue :=
CALCULATE (
SUM ( SalesHeader[TotalDiscount] );
CROSSFILTER ( SalesDetail[Order Number]; SalesHeader[Order Number]; BOTH )
)

Похоже, что включение двунаправленной фильтрации в  модели или


в  исходном коде меры при помощи шаблона решило проблему. Увы, ре-
зультат вычисления остался неправильным. Точнее говоря, он не такой, как
вы ожидали.
Обе техники позволили нам распространить фильтрацию с  таблицы
SalesDetail на SalesHeader, но агрегация в  итоге выполняется для всех за-
казов, содержащих выбранные товары. Проблема станет очевидна, если по-
строить отчет со срезом по брендам (атрибут измерения Product, косвенно
связанного с  таблицей SalesHeader) и  по годам (атрибут измерения Date,
напрямую связанного с SalesHeader). На рис. 2.5 представлен вывод отчета,
50    Использование главной/подчиненной таблицы

в котором сумма значений подсвеченных ячеек намного превышает итого-


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

Рис. 2.5. В этой сводной таблице сумма значений в подсвеченных ячейках составляет


$458 265.70, что больше, чем в общем итоге

Информация. Это один из примеров ошибки в  расчетах, кото-


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

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

в  цифрах вы будете списывать на ошибки в  формуле. Ошибки, конечно,


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

SalesHeader[DiscountPct] =
DIVIDE (
SalesHeader[TotalDiscount];
SUMX (
RELATEDTABLE ( SalesDetail );
SalesDetail[Unit Price] * SalesDetail[Quantity]
)
)

На рис. 2.6 показан результат вывода таблицы SalesHeader с новым вы-


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

Рис. 2.6. В столбце DiscountPct подсчитан процент скидки по документу

С этой колонкой вы знаете, какой процент скидки действует в  каждой


строке заказа. А значит, можете вычислить размер скидки в каждой из строк
документа путем прохождения по таблице SalesDetail и перемножения сум-
мы заказа на процент скидки. Следующий код DAX можно использовать для
расчета правильной скидки вместо предыдущей меры DiscountValue:

[DiscountValueCorrect] =
SUMX (
SalesDetail;
RELATED ( SalesHeader[DiscountPct] ) * SalesDetail[Unit Price] *
SalesDetail[Quantity]
)

Стоит отметить, что присутствие такой меры не  требует установки


двунаправленной фильтрации для связи между таблицами SalesHeader
и  SalesDetail. Для демонстрации мы оставили фильтрацию включенной,
чтобы показать в таблице обе меры одновременно. На рис. 2.7 можно ви-
деть сводную таблицу с выводом обоих вычисляемых столбцов. Заметьте,
что в колонке DiscountValueCorrect цифры чуть меньше, и теперь их сумма
в точности соответствует строке итогов.
Агрегирование мер из главной таблицы    53

Рис. 2.7. Разница между столбцами очевидна. И сумма по колонке равна итоговому


значению

Еще одним вариантом с  более простыми расчетами является создание


вычисляемого столбца в  таблице SalesDetail, отражающего сумму скидки
для каждой строки в заказе:

SalesDetail[LineDiscount] =
RELATED ( SalesHeader[DiscountPct] ) *
SalesDetail[Unit Price] *
SalesDetail[Quantity]

В этом случае общая сумма скидки по документу может быть рассчитана


путем сложения всех скидок по строкам.
Такой подход может оказаться полезным, поскольку он лучше отража-
ет наши действия. Мы  изменили модель данных путем денормализации
скидки из таблицы SalesHeader в таблицу SalesDetail. На рис. 2.8, представ-
ленном в виде диаграммы, показана структура обеих таблиц фактов после
добавления вычисляемого столбца LineDiscount в  подчиненную таблицу.
В свою очередь, таблица SalesHeader больше не содержит значений, которые
напрямую агрегируются в мере. В SalesHeader остались две колонки, связан-
ные со скидкой, – TotalDiscount и  DiscountPct, которые используются для
расчета LineDiscount в таблице SalesDetail. Оба этих столбца должны быть
скрыты от пользователя, поскольку они не будут использоваться для анали-
за, если вы, конечно, не захотите осуществить срез по DiscountPct. В этом
случае есть смысл оставить эту колонку видимой.
Давайте подведем некоторые итоги по рассмотренной модели данных.
Поразмышляйте о  ней. Теперь, после денормализации меры из таблицы
SalesHeader в таблицу SalesDetail, главную таблицу вполне можно рассмат­
ривать как измерение. А поскольку эта таблица объединена связями с дру-
гими измерениями, мы, по сути, привели нашу модель к схеме «снежинка»,
54    Использование главной/подчиненной таблицы

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


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

Рис. 2.8. Столбцы DiscountPct и TotalDiscount используются для расчета LineDiscount

Перед тем как двинуться дальше, давайте повторим, что мы изучили:


 в модели данных с главной и подчиненной таблицами главная служит
одновременно и измерением, и таблицей фактов. В качестве измере-
ния она может применяться для осуществления срезов в подчинен-
ной таблице, а в качестве таблицы фактов – для подсчета значений на
уровне гранулярности главной таблицы;
 при суммировании значений в  главной таблице фильтры, установ-
ленные в  измерениях, связанных с  подчиненной таблицей, не  при-
меняются, если не активировать двунаправленную фильтрацию или
не использовать шаблон связи «многие ко многим»;
 активирование двунаправленной фильтрации в модели или исполь-
зование соответствующего шаблона в  DAX позволяет суммировать
значения на уровне гранулярности главной таблицы, что ведет к рас-
хождению в расчетах. Это может быть проблемой, а может и не быть.
В нашем случае проблема была, и нам предстояло ее решить;
 для решения проблемы с аддитивностью можно перенес­ти колонки
с итоговыми значениями из главной таблицы в подчиненную, пред-
ставив их при этом в  виде процентов. После этого значения могут
быть агрегированы, и  по ним можно будет делать срезы по любым
измерениям. Иными словами, вы денормализуете столбцы до нуж-
ного уровня гранулярности, чтобы облегчить использование модели.
Выравнивание главной и подчиненной таблиц    55

Опытный специалист в  области моделирования данных смог бы уви-


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

Выравнивание главной и подчиненной таблиц


В предыдущем примере мы денормализовали скидку из главной таб­
лицы в  подчиненную, сперва вычислив ее процент, а  затем перенеся
в SalesDetail. Эту операцию можно проделать и с остальными столбцами
в главной таблице, такими как StoreKey, PromotionKey, CustomerKey и др.
Подобный предельный уровень денормализации данных называется вы-
равниванием (flattening), поскольку вы постепенно переходите от модели
с несколькими таблицами (в нашем случае двумя) к единой таб­лице, со-
держащей всю информацию.
Обычно процесс выравнивания выполняется еще до загрузки данных
в  модель путем выполнения запросов на языках SQL или M при помощи
редактора запросов (Query Editor), Excel или Power BI Desktop. Если вы за-
гружаете информацию из хранилища данных, вполне вероятно, что вырав-
нивание было произведено еще до переноса в хранилище. Мы же считаем
важным продемонстрировать вам различия между использованием выров-
ненных данных и структурированных.

Предупреждение. В  примере из этого раздела мы провели


довольно странные действия. Исходная модель уже была вы-
ровнена. Но  в целях обучения мы построили модель данных
с главной и подчиненной таблицами. После этого запустили код
на языке M в Power BI Desktop, чтобы воссоздать изначальную
выровненную структуру. Мы сделали это с целью демонстрации
процесса выравнивания. Конечно, в обычной жизни мы бы сра-
зу загрузили выровненные данные без выполнения этой слож-
ной процедуры.

Исходная модель была представлена на рис.  2.1. На  рис.  2.9 вы можете
видеть уже выровненную модель, которая приобрела вид «звезды», а  все
столбцы из таблицы SalesHeader были денормализованы в Sales.
56    Использование главной/подчиненной таблицы

Рис. 2.9. После выравнивания модель снова приобрела вид канонической схемы «звезда»

Нам потребовались следующие шаги для создания таблицы Sales.


1. Мы объединили таблицы SalesHeader и  SalesDetail по столбцу Order
Number, после чего добавили связанные столбцы из SalesHeader
в итоговую таблицу Sales.
2. Создали скрытый запрос, который на основании информации из таб­
лицы Sales Detail высчитал общие суммы заказов, и объединили ре-
зультаты запроса с таблицей Sales, чтобы извлечь общие суммы.
3. Мы добавили столбец, в  котором рассчитывается скидка по каждой
строке заказа так же, как в предыдущем примере. На этот раз мы, од-
нако, предпочли DAX язык М.
После выполнения этих трех шагов мы пришли к  классической схеме
«звезда» со всеми ее преимуществами. Выравнивание внешних ключей
вроде CustomerKey и OrderDateKey не представило труда, поскольку оно за-
ключается в  простом копировании информации. А  вот с  выравниванием
мер наподобие скидок обычно приходится потрудиться, что мы и сделали,
распределив их по строкам в равных пропорциях. Иными словами, мы ис-
пользовали сумму продажи в каждой строке в качестве веса при распреде-
лении скидок.
Единственным недостатком такой архитектуры является то, что при рас-
чете значений столбцов, перенесенных из главной таблицы, вам нужно
будет сохранять особую осторожность. Давайте поясним на примере. Если
бы вы хотели узнать количест­во заказов в исходной модели данных, вы бы
могли создать меру со следующей формулой:
Выравнивание главной и подчиненной таблиц    57

NumOfOrders := COUNTROWS ( SalesHeader )

Это очень простая мера. Она показывает, сколько строк представлено


в таблице SalesHeader в рамках выбранного контекста фильтров. Она пре-
красно работала бы, поскольку в SalesHeader наблюдалось четкое соответ-
ствие между количеством заказов и строк в таблице. Каждому заказу соот-
ветствовала ровно одна запись. Так что для определения количества заказов
достаточно было посчитать строки.
В выровненной модели данных это соответствие было утрачено. К  при-
меру, если на схеме, представленной на рис. 2.9, посчитать строки в таблице
Sales, мы получим не количество заказов, а количество строк во всех заказах.
Чтобы вычислить количество заказов, необходимо будет посчитать количест­
во уникальных значений в столбце Order Number следующей формулой:

NumOfOrders := DISTINCTCOUNT ( Sales[Order Number] )

Очевидно, что этот же шаблон вы можете использовать для всех атрибу-


тов, перенесенных из главной таблицы в единую выровненную. Поскольку
функция подсчета уникального количества в  DAX работает очень быстро,
это не проблема для таб­лиц среднего размера. В случае с очень объемными
таблицами могут возникнуть сложности в плане эффективности выполне-
ния запроса, но это нетипичная ситуация для бизнес-логики.
Также мы много говорили о специфике размещения числовых значений
в объединенной таблице. При переносе скидок из главной таблицы в под-
чиненную мы использовали проценты. Это сделано для того, чтобы можно
было агрегировать эти значения по строкам и в итоге все равно выходить
на правильные суммы. При этом методы размещения значений могут раз-
ниться с учетом ваших требований. К примеру, вы могли бы рассчитывать
транспортные расходы исходя из веса проданного товара, вместо того что-
бы равномерно распределять их по таблице. Для этого вам было бы необхо-
димо соответствующим образом изменить запросы.
Завершая тему выравнивания данных, стоит сказать пару слов о произ-
водительности. Большинство аналитических систем, включая SQL Server
Analysis Services, Power BI и Power Pivot, серьезно оптимизированы для ра-
боты со схемой «звезда» с маленькими измерениями и объемными табли-
цами фактов. В  изначальной нормализованной модели мы использовали
главную таблицу – довольно большую по размерам – в качестве измерения
для выполнения срезов в  подчиненной таблице. Можно взять за правило
хранить в таблицах измерений не более ста тысяч строк. В противном слу-
чае вы можете заметить спад эффективности системы. И  выравнивание
главной таблицы с подчиненной может быть хорошей мерой для уменьше-
ния объема измерений. Так что с точки зрения производительности опера-
ция выравнивания почти всегда будет оптимальной.
58    Использование главной/подчиненной таблицы

Заключение
В этой главе мы рассмотрели несколько вариантов архитектуры модели
данных. Как вы узнали, одна и та же информация может храниться в табли-
цах по-разному. Отличаться модели при этом будут лишь количеством таб­
лиц и связей между ними. К тому же неправильный выбор модели хранения
информации может привести к усложнению расчетов и неожиданным ре-
зультатам агрегирования.
Также вы узнали о важности гранулярности. Сумма скидки, выраженная
в абсолютных значениях, не могла быть правильно агрегирована с исполь-
зованием срезов по измерениям, объединенным связями с  подчиненной
таблицей. Но после перевода скидки в проценты мы смогли разместить эти
значения на строках, что позволило нам проводить агрегацию по любым
измерениям.
Глава 3
Использование множественных
таблиц фактов

В предыдущей главе мы обсудили сценарий с  наличием двух связанных


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

Использование денормализованных таблиц фактов


В первом примере мы рассмотрим ситуацию с наличием двух таблиц фак-
тов, которые невозможно объединить связью из-за их чрезмерной денорма-
лизации. Как вы увидите, выход из этого положения будет очень прост – мы
восстановим схему «звезда» на основе разрозненных таблиц и тем самым
вернем модели прежнюю функциональность.
В этом примере мы решили начать с очень простой модели данных, со-
стоящей из двух таблиц: Sales (продажи) и Purchases (закупки). У них очень
похожая структура, и  они обе полностью денормализованы, что означает
хранение всей сопутствующей информации внутри самих таблиц. Никаких
связей с измерениями нет. Получившаяся модель представлена на рис. 3.1.
60    Использование множественных таблиц фактов

Рис. 3.1. Полностью денормализованные таблицы Sales и Purchases без связей

Это очень распространенный сценарий в ситуациях, когда вы хотите объ-


единить два запроса, которые ранее работали обособленно. С каждой из этих
таблиц отдельно можно превосходно работать посредством сводных таблиц
в Excel. Проб­лема появится, когда вы захотите объединить обе таблицы в еди-
ную модель данных и использовать значения из них в общей сводной таблице.
Давайте рассмотрим пример. Предположим, вы определили меры
Purchase Amount (сумма закупок) и Sales Amount (сумма продаж) посред-
ством следующих запросов DAX:
Purchase Amount := SUMX ( Purchases; Purchases[Quantity] * Purchases[Unit
Cost] )
Sales Amount := SUMX ( Sales; Sales[Quantity] * Sales[Unit Price] )

При этом вы хотели бы видеть продажи и закупки в одной таблице. К со-


жалению, это не такая простая задача, как кажется. Например, если на стро-
ки сводной таблицы вы поместите производителя из таблицы Purchases
и обе меры вынесете в область значений, то получите результат, показан-
ный на рис. 3.2. Очевидно, что значения в столбце Sales Amount вывелись
неправильные – они попросту одинаковые.

Рис. 3.2. Вывод мер Sales Amount и Purchase Amount в единой сводной таблице дал
неверные результаты
Использование денормализованных таблиц фактов    61

Дело в том, что фильтр по производителю товаров из таблицы Purchases


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

Sales Amount Filtered :=


CALCULATE (
[Sales Amount];
INTERSECT ( VALUES ( Sales[BrandName] ); VALUES ( Purchases[BrandName]
) )
)

Функция INTERSECT позволяет выбрать значения из столб-


ца Sales[BrandName], содержащиеся в  текущем фильтре по Purchases
[BrandName]. В  результате действие фильтра по Purchases[BrandName] от-
разится на выборе Sales[BrandName], что, в свою очередь, позволит отфильт­
ровать таблицу Sales. На рис. 3.3 показана новая мера в действии.

Рис. 3.3. Поле Sales Amount Filtered использует текущий выбор из таблицы Purchases,
распространяющийся и на таблицу Sales
62    Использование множественных таблиц фактов

Несмотря на то что мера показывает правильные значения, это решение


в целом является далеко не самым оптимальным в этой ситуации по целому
ряду причин:
 написанный нами запрос фильтрует значения исключительно по
производителю, а если вам понадобится отбирать значения по дру-
гим полям, то все их придется перечислять в отдельных инструкциях
INTERSECT внутри функции CALCULATE. Это серьезно усложнит фор-
мулу в целом;
 производительность этой формулы будет невысока, поскольку DAX
гораздо лучше работает со связями, чем с  фильтрами, созданными
посредством функции CALCULATE;
 если у вас не одна мера, агрегирующая данные из таблицы Sales, вам
необходимо будет для каждой из них писать похожую формулу. А это
негативно скажется на удобстве сопровождения системы.
Просто чтобы вы поняли, насколько может усложниться формула при до-
бавлении всех атрибутов товаров в фильтр, посмот­рите на следующее вы-
ражение, предусматривающее расширенную фильтрацию по всем полям:

Sales Amount Filtered :=


CALCULATE (
[Sales Amount];
INTERSECT ( VALUES ( Sales[BrandName] ); VALUES ( Purchases[BrandName]
) );
INTERSECT ( VALUES ( Sales[ColorName] ); VALUES ( Purchases[ColorName]
) );
INTERSECT ( VALUES ( Sales[Manufacturer] ); VALUES (
Purchases[Manufacturer] ) );
INTERSECT (
VALUES ( Sales[ProductCategoryName] );
VALUES ( Purchases[ProductCategoryName] )
);
INTERSECT (
VALUES ( Sales[ProductSubcategoryName] );
VALUES ( Purchases[ProductSubcategoryName] )
)
)

Этот код очень уязвим к ошибкам и потребует немалых сил для поддерж-
ки. Если вы, например, захотите повысить гранулярность таблиц при помо-
щи добавления столбца, то вынуждены будете пройти по всем созданным
мерам и  добавить инструкцию INTERSECT для каждого созданного поля.
Гораздо лучше будет один раз изменить модель данных.
Чтобы упростить код, нам необходимо привести модель данных к схе-
ме «звезда». Все станет значительно проще, если довести структуру мо-
дели до показанной на рис.  3.4 – с добавлением измерения Product, по
которому можно будет осуществлять фильт­рацию обеих таблиц  – Sales
Использование денормализованных таблиц фактов    63

и  Purchases. Даже если внешне это не  слишком заметно, новая схема
представляет собой «звезду» в чистом виде – с двумя таблицами фактов
и одним измерением.

Рис. 3.4. С введением измерения Product модель данных стала значительно проще


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

Примечание. Мы скрыли столбцы, которые были нормализованы


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

При построении такой модели данных вы можете столкнуться со следу-


ющими проблемами:
 вам потребуется источник для измерения Product, но час­то у  вас
не будет доступа к исходным таблицам;
 в таблице Product должен присутствовать первичный ключ, чтобы она
могла выступать в качестве целевой для устанавливаемой связи.
С первой проблемой разобраться очень легко. Если у вас есть доступ к ис-
ходной таблице Product, вы можете просто загрузить информацию из нее
в измерение. В противном случае можно воссоздать эту таблицу, восполь-
зовавшись средством Power Query, путем загрузки таблиц Sales и Purchases,
их объединения и удаления дубликатов. Следующий код на языке M легко
справится с этой задачей:

let
SimplifiedPurchases = Table.RemoveColumns(
Purchases,
{"Quantity", "Unit cost", "Date"}
),
SimplifiedSales = Table.RemoveColumns(
Sales,
{"Quantity", "Unit Price", "Date"}
),
64    Использование множественных таблиц фактов

ProductColumns = Table.Combine ( { SimplifiedPurchases, SimplifiedSales


} ),
Result = Table.Distinct (ProductColumns )
in
Result

Как видите, этот фрагмент кода сначала подготавливает обе локальные


таблицы SimplifiedPurchases и SimplifiedSales, оставляя в них только относя-
щиеся к будущему измерению Product столбцы и избавляясь от остальных.
Затем запрос объединяет две получившиеся таблицы, добавляя строки из
SimplifiedSales к  таблице SimplifiedPurchases. После этого происходит из-
влечение только уникальных значений, что ведет к образованию справоч-
ника товаров.

Примечание. Те же самые результаты вы можете получить, рабо-


тая с редактором запросов в Excel или Power BI Desktop. Сначала
создаете два запроса, удаляющих количество и цену за единицу
из источника, а затем объединяете результаты вместе при помощи
оператора Union. Однако подробности написания этого запроса
выходят за пределы данной книги. Мы больше сосредоточены на
моделировании данных, нежели на деталях интерфейса.

Для создания измерения необходимо объединить результаты запросов


с таблицами Sales и Purchases. Высока вероятность, что некоторые товары
присутствуют только в одной из этих таб­лиц. Таким образом, если извлечь
уникальные значения лишь из одного запроса, в  результате мы получим
частично заполненное измерение, использование которого в модели может
привести к ошибочным результатам.
После загрузки таблицы Product в  модель данных нужно будет создать
связи. В данном случае вы имеете право использовать наименование това-
ра в  качестве ключа, поскольку этот столбец заполнен уникальными зна-
чениями. Если в  вашем промежуточном измерении не  найдется столбца,
подходящего для создания первичного ключа, могут быть неприятности.
При отсутствии наименований товаров в  исходной таблице вам не  удаст-
ся создать связь с  получившимся измерением. Например, если вы распо-
лагаете категорией и  подкатегорией товаров, но наименования у  вас нет,
вам придется создать измерения с доступной вам степенью гранулярности.
Вам, вероятно, понадобятся два измерения для категорий и подкатегорий
товаров, которые вы можете создать, используя описанную выше технику.
Часто подобные преобразования уместно делать еще до загрузки данных
в модель. Допустим, если вы импортируете информацию из SQL Server, вы
можете написать запросы на языке SQL, которые выполнят все необходи-
мые действия за вас, что позволит упростить итоговую модель.
Использование денормализованных таблиц фактов    65

Стоит отметить, что того же результата можно добиться в Power BI, ис-
пользуя вычисляемые таблицы (calculated tables). На момент написания кни-
ги в Excel эта опция была недоступна, а присутствовала только в Power BI
и  SQL Server Analysis Services 2016. Следующий код создает вычисляемую
таблицу для измерения товаров, и  он существенно проще, чем фрагмент
кода на языке M:

Products =
DISTINCT (
UNION (
ALL (
Sales[ProductName];
Sales[ColorName];
Sales[Manufacturer];
Sales[BrandName];
Sales[ProductCategoryName];
Sales[ProductSubcategoryName]
);
ALL (
Purchases[ProductName];
Purchases[ColorName];
Purchases[Manufacturer];
Purchases[BrandName];
Purchases[ProductCategoryName];
Purchases[ProductSubcategoryName]
)
)
)

В этой вычисляемой таблице выполняются два оператора ALL над столб-


цами из таблиц Sales и  Purchases, сокращая количество полей и  оставляя
только уникальные строки. Затем при помощи оператора UNION резуль-
таты объединяются, а  на заключительном этапе посредством оператора
DISTINCT удаляются дубли, которые могли появиться после объединения.

Примечание. Выбор конкретного средства между языками M


и DAX остается полностью на ваше усмот­рение. Между этими ва-
риантами нет существенных отличий.

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

Фильтрация через измерения


В предыдущем примере вы освоили основы обращения с несколькими из-
мерениями. Тогда у нас было два сильно денормализованных измерения,
и с целью улучшения модели мы вернулись к более простой схеме «звезда».
В следующем примере мы рассмотрим другой сценарий, снова с использо-
ванием таблиц Sales и Purchases.
Представьте, что вам нужно проанализировать закупку только тех това-
ров, которые участвовали в продажах в определенный период времени или,
в более широком смысле, товаров, удовлетворяющих определенной выбор-
ке. В  предыдущем разделе мы говорили, что если у  вас есть две таблицы
фактов, лучше всего будет объединить их связями с измерениями. Это по-
зволит вам фильтровать обе таблицы фактов по одному измерению. Итак,
исходный сценарий изображен на рис. 3.5.

Рис. 3.5. В этой модели данных две таблицы фактов связаны с двумя измерениями

Используя представленную модель данных и две простые меры, вы мо-


жете легко построить показанный на рис. 3.6 отчет о продажах и закупках
по брендам и годам.
Более сложные расчеты потребуются, если вы захотите посмот­реть ин-
формацию о закупках только по тем товарам, которые продавались. Иными
словами, необходимо использовать таблицу Sales как фильтр для товаров
таким образом, чтобы все другие фильтры, наложенные на продажи (напри-
мер, по дате), ограничивали итоговый список товаров, по которым выво-
дятся закупки. Есть несколько подходов к решению этого сценария. Мы по-
кажем вам разные варианты и обсудим их преимущества и недостатки.
Фильтрация через измерения    67

Рис. 3.6. В простой схеме «звезда» продажи и закупки по годам и брендам вычисляются


очень легко

Если в  вашем инструменте доступна двунаправленная фильтрация (на


момент написания книги она присутствовала в Power BI и SQL Server Analysis
Services, но не в Excel), вы могли бы задуматься о том, чтобы включить ее
для связи между таблицами Sales и  Product и  таким образом ограничить
выбор товарами, участвовавшими в продажах. К сожалению, для этого вам
пришлось бы отключить связь между таблицами Product и  Purchases, как
показано на рис. 3.7. Если этого не сделать, модель станет неоднозначной,
и движок не позволит сделать все связи двунаправленными.

Информация. Движок DAX не допускает появления неоднознач-


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

Рис. 3.7. Чтобы включить двунаправленную фильтрацию между таблицами Sales


и Product, нужно отключить связь между Product и Purchases

Если вы попытаетесь применить необходимые вам фильтры в такой модели,


то очень быстро поймете, что они работают не так, как вы ожидали. К приме-
ру, если установить фильтр на измерение Date, он распространится на таблицу
Sales, затем на Product (из-за включенной двунаправленной фильтрации), но
дальше остановится и не сможет оказать влияние на таблицу Purchases. Если
включить двунаправленную фильтрацию и в таб­лице Date, в отчете по закуп-
кам будут показаны не те товары, которые участвовали в  продажах. Вместо
этого туда попадут закупки любых товаров, сделанные в те даты, когда какой-
либо из выбранных товаров продавался. Как видите, очень запутанно и мало-
понятно. Двунаправленная фильтрация представляет из себя очень мощный
инструмент, но в этом случае он совершенно не годится, поскольку нам нужен
более четкий контроль за распространением фильтров.
Ключом к  решению этой задачи является понимание распространения
фильтрации в целом. Давайте начнем с измерения Date и вернемся к изна-
чальной схеме, показанной на рис.  3.5. Когда вы накладываете фильтр на
определенный год в измерении Date, он автоматически распространяется
на таблицы Sales и Purchases, но измерения Product не достигает из-за об-
ратной направленности. Вам нужно получить список товаров, участвовав-
ших в продажах (Sales), и использовать его для фильтрации таблицы заку-
пок (Purchases). Правильная формула для этого представлена ниже.

PurchaseOfSoldProducts :=
CALCULATE (
Понимание неоднозначности модели данных    69

[PurchaseAmount];
CROSSFILTER ( Sales[ProductKey]; Product[ProductKey]; BOTH )
)

В этом фрагменте кода мы используем функцию CROSSFILTER для акти-


вации двунаправленной фильтрации между таблицами Products и Sales на
время выполнения запроса. Таким образом, таблица Sales отфильтрует из-
мерение Product, откуда фильтр распространится на таблицу Purchases. Для
дополнительной информации по функции CROSSFILTER см. приложение A
«Моделирование данных 101».
Получается, что для решения этого сценария мы задействовали только код
на языке DAX. Мы не меняли модель. Тогда как это относится к моделирова-
нию данных? Просто мы хотели показать, что в данном конкретном случае
в изменении модели не было необходимости. Зачастую проблемы решаются
именно путем модификации схемы данных, но иногда – как в этом случае –
достаточно написать немного кода на языке DAX, и проб­лемы будут решены.
Это вам поможет обрести понимание того, когда и что лучше использовать.
К тому же модель данных в нашем примере включает в себя сразу две «звез-
ды», и придумать схему лучше было бы очень непросто.

Понимание неоднозначности модели данных


В предыдущем разделе мы разобрали ситуацию, когда включение двуна-
правленной фильтрации для связи не работает, поскольку вносит неодно-
значность в  модель данных. Пришло время немного больше углубиться
в понятие неоднозначности модели и узнать, почему она недопустима в та-
бличных системах моделирования.
Под неоднозначной моделью (ambiguous model) понимается такая модель,
в которой допущено несколько путей объединения двух таблиц посредством
связей. Простейшая форма неоднозначности модели возникает, когда вы
пытаетесь объединить две таблицы более чем одной связью. В таком случае
активна будет только одна из связей – по умолчанию та, которую вы создали
первой. Остальные связи будут помечены как неактивные. На рис. 3.8 пред-
ставлен пример такой модели. Из существующих трех связей между табли-
цами лишь одна обозначена сплошной линией, то есть активна. Оставшиеся
две связи отмечены пунктиром, а значит, являются неактивными.

Рис. 3.8. Две таблицы не могут быть объединены более чем одной активной связью
70    Использование множественных таблиц фактов

С чем связано такое ограничение? Причина очень проста. Язык DAX пред-
лагает богатую функциональность в отношении работы со связями. К при-
меру, из таблицы Sales вы можете легко обратиться к  любому столбцу из
связанной таблицы Date, используя функцию RELATED, как показано ниже:
Sales[Year] = RELATED ( 'Date'[Calendar Year] )
Функция RELATED не предусматривает инструкции о том, какую именно
связь ей использовать для обращения к связанному полю. Язык DAX автома-
тически проходит по единственной активной связи и возвращает значение
года. В данном случае это будет год продажи, поскольку в данный момент
активной является связь, построенная на основании поля OrderDateKey.
Если бы между таблицами могло быть сразу несколько активных связей,
вам пришлось бы использовать указание предполагаемой связи каждый
раз, когда обращаетесь к функции RELATED. Примерно такое же поведение
наблюдается в  отношении автоматического распространения контекста
фильтра при использовании, скажем, функции CALCULATE.
В следующем примере вычисляется сумма продаж за 2009 год:
Sales2009 := CALCULATE ( [Sales Amount]; 'Date'[Calendar Year] = "CY 2009" )
И снова вам нет необходимости указывать, какую именно связь исполь-
зовать для осуществления фильтрации. В данной модели активной по умол-
чанию является связь, основанная на столбце OrderDateKey. В  следующей
главе вы узнаете, как эффективно использовать множественные связи на
примере таблицы Date. Цель этого раздела состоит в том, чтобы вы поняли,
почему в табличных моделях данных недопустима неоднозначность.
У вас есть возможность программно активировать любую из имеющихся
связей в рамках выражения. К примеру, если вам необходимо узнать сумму
продаж по товарам, доставленным в 2009 году, вы можете воспользоваться
функцией USERELATIONSHIP, как показано ниже:
Shipped2009 :=
CALCULATE (
[Sales Amount];
'Date'[Calendar Year] = "CY 2009";
USERELATIONSHIP ( 'Date'[DateKey]; Sales[DeliveryDateKey] )
)

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


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

в  целях повышения скорости специ­фических расчетов. Однако в  приме-


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

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

В данной модели присутствует два столбца с указанием на возраст. Один


из них – Historical Age – находится в таблице фактов, а второй – CurrentAge –
в измерении Customer. Оба поля являются внешними ключами в своих таб­
лицах и ссылаются на таблицу Age Ranges (диапазоны возрастов), но лишь
одна из этих связей может быть активна. Другая связь деактивирована.
В этом случае неоднозначность модели не так очевидна, но все же она есть.
Представьте, что строите сводную таблицу со срезом по таблице Age Ranges.
Так какую информацию вы хотите получить? Во  сколько лет покупатель
приобрел наш товар (поле Historical Age) или сколько ему лет сейчас (поле
CurrentAge)? Если бы обе связи оставались активными, системе не удалось
бы однозначно ответить на этот вопрос. Поэтому движок запрещает наличие
таких вводящих в заблуждение связей. В результате вы должны либо решить,
какую связь оставить активной, либо продублировать таблицу, вносящую
неразбериху. Выбрав второй вариант, вы сможете в  будущем однозначно
указывать, связь с  какой из двух таблиц (Current Age Ranges или Historical
Age Ranges) вы имеете в виду в своих запросах. Модифицированная модель
данных с продублированной таблицей Age Ranges показана на рис. 3.10.
72    Использование множественных таблиц фактов

Рис. 3.10. Теперь в нашей модели две таблицы Age Ranges

Работа с заказами и счетами


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

Рис. 3.11. Модель данных с заказами и счетами в виде простой схемы «звезда»

На этот раз за исходную модель мы возьмем схему «звезда» с двумя таб­


лицами фактов и  одним измерением покупателей (Customer), в  котором
определим две меры:

Amount Ordered := SUM ( Orders[Amount] )


Amount Invoiced:= SUM ( Invoices[Amount] )
Работа с заказами и счетами    73

С этими двумя мерами вы можете формировать отчет с указанием суммы


заказов и суммы выставленных счетов в разрезе покупателей. Пример от-
чета, показывающего разницу между суммой по заказам и по счетам, пред-
ставлен на рис. 3.12.

Рис. 3.12. Простой отчет с суммами по заказам и счетам для каждого покупателя

Если вас интересует только общая картина, этого отчета может быть вам
вполне достаточно. Но если вам понадобятся подробности, увы, вы столкне-
тесь с  серьезными проблемами. К  примеру, как определить, по каким за-
казам еще не были выставлены счета? Перед тем как двигаться дальше, по-
думайте, глядя на модель данных на рис. 3.11, в чем может быть загвоздка.
Поскольку этот пример таит в себе сразу несколько сложностей, мы вместе
с вами пройдем методом проб и ошибок. Мы покажем вам несколько проме-
жуточных ошибочных вариантов и объясним, где в них кроются неточности.
Если вы добавите в  сводную таблицу отчета номер заказа, то получите
сложный для понимания и анализа результат, изображенный на рис. 3.13,
в  котором под каждым покупателем (John, Melanie и  Paul) располагаются
все заказы – и свои, и чужие.

Рис. 3.13. Опустившись до уровня заказов, вы увидите ошибочные цифры в столбце


Amount Invoiced
74    Использование множественных таблиц фактов

Этот сценарий очень похож на тот, что мы рассматривали в начале гла-


вы – с двумя предельно денормализованными таб­лицами фактов. Фильтр
по номеру заказа никак не отражается на выборе счетов, поскольку в табли-
це со счетами нет номера заказа. Так что в столбце Amount Invoiced учиты-
вается только фильтр по покупателю, и во всех строках в рамках покупателя
цифры получаются одинаковые.
Сейчас самое время повторить одну очень важную вещь: цифры, которые
вы видите в  сводной таблице, правильные – в  рамках информации, при-
сутствующей в модели данных. Если подумать, то движку просто неоткуда
взять информацию о том, какие именно заказы включены в счета, а какие –
нет. Эти данные у нас просто отсутствуют. Так что в этом сценарии нам не-
обходимо менять саму модель данных. Помимо общей суммы счетов, нужно
также знать, какие именно заказы были включены в счета, а также перечень
номеров заказов в каждом конкретном счете. Как и всегда, перед тем как
двигаться дальше, потратьте немного времени на поиск решения.
У этого сценария может быть несколько решений в зависимости от сте-
пени сложности модели данных. Но  сначала давайте посмотрим на сами
данные в таблицах, представленные на рис. 3.14.

Рис. 3.14. Актуальные данные, содержащиеся в нашей модели

Как видите, в обеих таблицах – Invoices и Orders – присутствует атрибут


Customer, содержащий имена покупателей. При этом таблица Customer на-
ходится на стороне «один» в связях, берущих свое начало в таблицах Orders
и Invoices. Что нам точно необходимо сделать, так это добавить связь между
таблицами Orders и Invoices, определяющую, какие заказы включены в ка-
кие счета. Здесь есть два возможных сценария:
Работа с заказами и счетами    75

 каждому заказу соответствует один счет. Такой сценарий возмо-


жен в случае, когда заказы включаются в счета только целиком. В этом
варианте счет может содержать в себе множество заказов, но один за-
каз не может быть разбит на несколько счетов. В этом описании вы
можете четко угадать тип связи «один ко многим»;
 заказы могут быть разнесены по нескольким счетам. Если заказ
может быть включен в счет частично, значит, одному заказу в модели
могут соответствовать несколько счетов. В то же время в одном сче-
те могут присутствовать несколько заказов. Здесь мы имеем дело со
связью типа «многие ко многим» между таблицами Orders и Invoices,
что делает этот сценарий чуть сложнее.
Первый сценарий решить очень просто. По  сути, достаточно будет до-
бавить в таблицу Orders поле с номером счета. Модель, которая получится
в результате, показана на рис. 3.15.

Рис. 3.15. В подсвеченном столбце вы видите номера счетов, соответствующих каждому


конкретному заказу

И хотя кажется, что это незначительное изменение модели, сделать его


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

Рис. 3.16. Связь между таблицами Orders и Invoices неактивна

Где же в этой модели неоднозначность? Дело в том, что если бы связь меж-
ду таблицами Orders и Invoices была активна, от таблицы Orders к Customer
можно было бы добраться двумя путями: один из них прямой, с использо-
ванием связи между этими таблицами, а второй – обходной, через вспомо-
гательную таблицу Invoices. Даже если сейчас эти два пути указывают на
одного и того же покупателя, нет никакой гарантии, что так будет всегда –
все зависит от наполнения таблиц. Ничто не может помешать вам ошибоч-
но включить заказ по одному покупателю в счет по другому. В этом случае
модель станет неработоспособной.
Поправить это проще, чем кажется. Если внимательно посмотреть на мо-
дель, можно увидеть, что между таблицами Customer и Invoices есть связь
«один ко многим», так же, как и  между Invoices и  Orders. И  покупатель
из конкретного заказа может быть извлечен с  использованием таблицы
Invoices в  качестве промежуточной. Так что вполне можно избавиться от
связи между Customer и Orders и полагаться только на оставшиеся две. По-
лучившаяся в результате модель показана на рис. 3.17.

Рис. 3.17. После удаления связи между таблицами Orders и Customer модель


существенно упростилась
Работа с заказами и счетами    77

Знакома ли вам модель, изображенная на рис. 3.17? Это ведь тот же са-


мый шаблон с главной и подчиненной таблицами, который мы обсуждали
во второй главе. Теперь у вас есть две таб­лицы фактов: одна содержит счета,
а вторая – заказы. При этом таблица Orders выступает в качестве подчинен-
ной, а Invoices – в качестве главной.
Будучи приведенной к схеме с главной и подчиненной таблицами, такая
модель наследует от этого шаблона все его плюсы и  минусы. В  каком-то
смысле проблему со связями мы решили, но с суммами – пока нет. Если по-
строить сводную таблицу на основании новой модели данных, то вы увиди-
те такую же картину, как на рис. 3.13, – для каждого покупателя будут указа-
ны все заказы из базы, как свои, так и чужие. Проблема в том, что какой бы
заказ мы ни выбрали, сумма по счетам для покупателя остается одной и той
же. Даже если цепочка из связей построе­на правильно, в модели данных по-
прежнему есть проблемы.
На самом деле ситуация здесь еще более запутанная. Анализируя данные
по покупателю и номеру заказа, какую информацию вы хотели бы получить
в отчете? Какие есть варианты?
 общая сумма по счетам для этого покупателя. Это то, что у  нас
есть в отчете сейчас и кажется нам ошибочным;
 общая сумма по счетам, включающим данный заказ от конкрет-
ного покупателя. В этом случае мы хотим, чтобы сумма отражалась
только по тем счетам, в которых присутствует выбранный заказ;
 сумма по заказу, если он включен в счет. Здесь мы хотим видеть
общую сумму по заказу, только если он уже включен в счет. В против-
ном случае должны быть нули. При этом в отчет могут попасть сум-
мы бо́льшие, чем указаны в счетах, поскольку мы учитываем полную
сумму заказа, а не ту часть, которая включена в счет.

Примечание. Список на этом мог бы закончиться, но мы забыли об


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

Расчет полной суммы по счетам для покупателя


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

Расчет суммы по счетам, включающим данный заказ от


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

Amount Invoiced Filtered by Orders :=


CALCULATE (
[Amount Invoiced];
CROSSFILTER ( Orders[Invoice]; Invoices[Invoice]; BOTH )
)

В результате мера будет включать в себя только те счета, которые указа-


ны в выбранном наборе заказов. Результат в виде сводной таблицы можно
видеть на рис. 3.18.

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


в отчете

Расчет суммы заказов, включенных в счета


Последняя мера, как и  ожидалось, будет неаддитивной (non-additive). По-
скольку здесь должны выводиться полные суммы по счетам для каждого
заказа, обычно они будут значительно превышать суммы самих заказов.
Вы можете вспомнить подобное поведение модели из предыдущей главы.
Когда мы агрегируем значения из главной таблицы, применяя при этом
фильтр в подчиненной, получившиеся меры будут неаддитивными.
Работа с заказами и счетами    79

Чтобы сделать их аддитивными (additive), нужно для начала проверить


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

Amount Invoiced Filtered by Orders :=


CALCULATE (
SUMX (
Orders;
IF ( NOT ( ISBLANK ( Orders[Invoice] ) ); Orders[Amount] )
);
CROSSFILTER ( Orders[Invoice]; Invoices[Invoice]; BOTH )
)

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

Рис. 3.19. Если заказ не полностью включен в счет, в последнем столбце будут


выводиться неверные результаты

Не существует простого способа посчитать частичную оплату заказа, по-


скольку в нашей модели нет для этого достаточных данных. В случае час­
тичного включения заказов в  счета нам явно недостает поля актуальной
суммы оплаты. Для вывода правильных результатов необходимо хранить
80    Использование множественных таблиц фактов

эти суммы в модели и использовать их в нашей формуле вместо общей сум-


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

Рис. 3.20. В этой модели появилась возможность включения нескольких заказов в счет


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

Фактически теперь наша модель включает в  себя связь типа «многие ко


многим» между таблицами заказов и счетов. Один заказ может быть включен
в несколько счетов, и в то же время один счет может распространяться на не-
сколько заказов. Для каждого заказа сумма включения в конкретный счет хра-
нится в таблице OrdersInvoices, что позволяет добиться желаемого результата.
Подробнее о связях «многие ко многим» мы расскажем в главе 8. Но уже
на данном этапе полезно посмотреть, как может выглядеть правильная мо-
дель данных для работы с заказами и счетами. Здесь мы намеренно нарушили
каноническое правило схемы «звезда», чтобы построить корректную модель.
По своей сути таблица OrdersInvoices не является ни измерением, ни таблицей
фактов. С фактом ее роднит то, что она содержит меру Amount и объединена
связью с измерением Invoices. В то же время она связана и с таблицей Orders,
которая сама одновременно является измерением и таблицей фактов. Техни-
чески таблицу OrdersInvoices можно назвать таблицей-мостом (bridge table),
поскольку она представляет собой мост между таб­лицами заказов и счетов.
Теперь, когда суммы частичной оплаты заказов хранятся в промежуточ-
ной таблице, формула для расчета суммы заказов, включенных в счета, бу-
дет выглядеть следующим образом:
Заключение    81

Amount Invoiced :=
CALCULATE (
SUM ( OrdersInvoices[Amount] );
CROSSFILTER ( OrdersInvoices[Invoice]; Invoices[Invoice]; BOTH )
)

Мы суммируем столбец Amount в  таблице-мосте, тогда как функция


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

Рис. 3.21. Использование таблицы-моста позволило нам получить полную картину


по заказам и счетам

Заключение
В этой главе вы узнали, как обрабатывать различные сценарии с несколь-
кими таблицами фактов, объединенными посредством измерений или таб­
лиц-мостов. Наиболее важные темы, затронутые в этой главе:
 чрезмерная денормализация модели может привести к невозмож-
ности осуществлять фильтрацию по нескольким таблицам фактов.
В  модели должно присутствовать определенное количество изме-
рений, чтобы вы могли производить фильтрацию нескольких таб­
лиц фактов;
82    Использование множественных таблиц фактов

 хотя вы можете использовать DAX для работы с  излишне денорма-


лизованными таблицами, код на этом языке очень быстро может
оказаться слишком сложным для осуществления его поддержки. Из-
менения в  самой модели данных способны значительно упростить
написание формул;
 установка сложных связей между измерениями и таб­лицами фактов
может внести неоднозначность в вашу модель данных, что недопус­
тимо в движке DAX. Неоднозначность модели можно устранить при
помощи дублирования некоторых таблиц и денормализации отдель-
ных столбцов;
 сложные модели данных, как в нашем случае с заказами и счетами,
включают в себя множество таблиц фактов. Для комфортной работы
с  такими моделями необходимо создавать таблицы-мосты, способ-
ствующие извлечению информации из нужной сущности.
Глава 4
Работа с датой и временем

В бизнес-аналитике часто приходится производить расчеты с начала года


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

Создание измерения даты и времени


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

Рис. 4.1. В таблице Sales находится поле Order Date, отражающее дату заказа
84    Работа с датой и временем

Вы можете использовать столбец Order Date в таблице Sales для осуществ­


ления среза по конкретной дате. Но  если вам понадобится аналитика по
году или месяцу, без дополнительных полей будет не обойтись. Вы можете
выйти из ситуации, создав набор вычисляемых столбцов прямо в таблице
фактов (хотя это и  не  оптимальное решение, поскольку не  позволит вам
использовать специальные функции для работы с датой и временем). На-
пример, можно написать следующие простые формулы для создания трех
столбцов – Year, Month Name и Month Number:

Sales[Year] = YEAR ( Sales[Order Date] )


Sales[Month] = FORMAT ( Sales[Order Date]; "mmmm" )
Sales[MonthNumber] = MONTH ( Sales[Order Date] )

Очевидно, что номер месяца может понадобиться вам для выполнения


сортировки месяцев в правильном порядке. После добавления этих столб-
цов вы можете воспользоваться сортировкой по столбцу, которая доступна
как в Power BI Desktop, так и в модели данных Excel. Как видно по рис. 4.2,
вычисляемые столбцы прекрасно справляются со своей задачей осуществ­
ления среза по дате.
Однако у этой модели есть серьезные недостатки. К примеру, если вам за-
хочется анализировать похожим образом закупки, вам придется создавать
такие же вычисляемые столбцы в таблице Purchases. Поскольку столбцы при-
надлежат таблице Sales, у вас не получится использовать их для осуществле-
ния срезов в Purchases. Как вы помните из третьей главы, чтобы одновремен-
но фильтровать две таблицы фактов, вам необходимо измерение. Кроме того,
в измерении даты и времени обычно содержится большое количество полей,
включая специфику финансового года, информацию о рабочих и праздничных
днях и многое другое. Держать все это в одной таблице было бы очень удобно.

Рис. 4.2. Расчет сумм продаж со срезом по дате с использованием вычисляемых колонок


Создание измерения даты и времени    85

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

Sales YTD :=
VAR CurrentYear = MAX ( Sales[Year] )
VAR CurrentDate = MAX ( Sales[Order Date] )
RETURN
CALCULATE (
[Sales Amount];
Sales[Order Date] <= CurrentDate;
Sales[Year] = CurrentYear;
ALL ( Sales[Month] );
ALL ( Sales[MonthNumber] )
)
Что происходит при выполнении этого кода?
1. Устанавливается фильтр по дням, оставляя только самую позднюю
дату до видимой.
2. Устанавливается фильтр по годам, чтобы остался только последний
год при наличии нескольких лет в контексте фильтра.
3. Удаляются ранее наложенные фильтры по названию месяца (в табли-
це Sales).
4. Удаляются ранее наложенные фильтры по номеру месяца (также
в таб­лице Sales).

Примечание. Если вы незнакомы с  DAX, попытка разобраться


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

Этот код работает превосходно, как видно по рис. 4.3. Однако он получил-


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

Рис. 4.3. В столбце Sales YTD показаны правильные цифры, но формула получилась из-
лишне сложной

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

Рис. 4.4. Добавление в модель измерения даты упростило итоговую формулу

В этот раз вы можете воспользоваться встроенной функцией для написа-


ния формулы, как показано ниже:

Sales YTD :=
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
)

Примечание. Специальные функции для работы с датой и време-


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

Добавив в модель измерение даты, вы:


 упростили написание формулы для меры;
 определили единое место для хранения всех столбцов, связанных
с датой и временем, необходимых для построе­ния отчетов;
 повысили производительность запросов;
 построили модель данных, простую в обслуживании и навигации.
Здесь мы перечислили достоинства такого подхода. А какие у него есть
недостатки? В данном случае никаких. У использования специальных изме-
рений для хранения даты и времени есть только плюсы. Заведите привычку
создавать такие измерения всякий раз, когда проектируете модель данных,
и не идите по простому пути создания вычисляемых столбцов – это ловуш-
ка. Если вы попадете в нее, то рано или поздно пожалее­те об этом.

Понятие автоматических измерений времени


В Excel 2016 и Power BI Desktop компания Microsoft встроила автомати-
ческую систему для работы с  датами и  временем, хотя эти инструменты
и  используют разные механизмы. В данном разделе мы рассмотрим оба
средства.

Примечание. Как вы поймете из этой главы, мы настоятельно


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

Автоматическая группировка дат в Excel


При работе со сводными таблицами на основании модели данных Excel до-
бавление столбца с датами автоматически приведет к созданию набора вспо-
могательных столбцов для оперирования датами и временем. Представьте,
что вы начали работать с моделью, представленной на рис. 4.5, где в табли-
це Sales есть один столбец с датами – Order Date.

Рис. 4.5. В таблице Sales есть один столбец с датами – Order Date, без столбцов
для представления лет и/или месяцев
88    Работа с датой и временем

При создании сводной таблицы с  полем Sales Amount в  области значе-


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

Рис. 4.6. В сводной таблице сделан срез данных по годам и кварталам, несмотря на то


что в модели не было таких столбцов

Чтобы появилась возможность осуществлять срезы по годам, Excel авто-


матически добавил в таблицу Sales необходимые поля. Вы увидите их, если
откроете модель данных. На рис. 4.7 показана диаграмма модели, а столб-
цы, добавленные движком Excel, подсвечены.

Рис. 4.7. В таблице Sales содержатся новые столбцы, которые были автоматически


созданы в Excel

Обратите внимание, что Excel, по сути, сделал то, от чего мы вас со-
всем недавно отговаривали, – создал столбцы для возможности осуществ­
ления срезов по ним. Если вы проделаете те же операции над другой та-
блицей фактов, в ней также будут созданы эти столбцы. При этом новые
столбцы из обеих таблиц не  могут быть использованы в  едином пере-
крестном фильтре. Более того, поскольку эти поля были созданы прямо
в  таблице фактов с  большим количеством строк, эта операция займет
какое-то время и увеличит размер файла Excel. Подробности об этом ин-
струменте можно почитать на сайте https://blogs.office.com/2015/10/13/
time-grouping-enhancements-in-excel-2016/. В  данной статье также есть
информация о том, какие действия необходимо выполнить в системном
регистре, чтобы отключить этот механизм. Если вы работаете с более или
Понятие автоматических измерений времени    89

менее сложными моделями данных, мы советуем вам отключить этот по-


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

Автоматическая группировка дат в Power BI Desktop


Разработчики Power BI Desktop также попытались облегчить нам работу
с датами и временем, автоматизировав некоторые шаги. И хотя здесь это
сделано несколько лучше, чем в Excel, все же это далеко не идеал.
Если вы, используя в Power BI Desktop ту же модель данных, что изобра-
жена на рис. 4.7, попытаетесь построить матрицу (matrix) по колонке Order
Date, то получите вывод, показанный на рис. 4.8.

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


в нашей модели данных

Как и  Excel, Power BI Desktop также автоматически создает календарную


иерар­хию, хоть и  использует при этом другую технику. Если вы заглянете
в таб­лицу Sales, то не обнаружите там никаких вычисляемых столбцов. Вместо
этого Power BI Desktop создает по одной скрытой таблице для каждого столб-
ца с датой и строит все необходимые связи. Когда вы осуществляете срез по
дате, Power BI Desktop использует для визуализации календарную иерархию
из созданной скрытой таблицы. Как видите, здесь это сделано более толково,
чем в Excel. Но все же этот механизм обладает некоторыми ограничениями:
 созданная Power BI Desktop таблица скрыта, что не дает вам возмож-
ности редактировать ее содержимое. К  примеру, вы не  сможете по-
менять названия столбцов или порядок сортировки дат, а также до-
бавить поддержку финансового календаря;
 Power BI Desktop создает по одной таблице на каждый столбец. Так что
если у  вас есть множество таблиц фактов, все они будут привязаны
к разным таблицам с датами, и вы не сможете осуществлять срез по
нескольким таблицам, используя один календарь.
90    Работа с датой и временем

Со временем мы привыкли отключать в  Power BI Desktop опцию авто-


матического создания календарей. Чтобы сделать это, нужно щелкнуть на
вкладке File (Файл), выбрать пункт Options and Settings (Параметры и на-
стройки), в появившемся диалоговом окне перейти на страницу Data Load
(Загрузка данных) и  снять флажок Auto Date/Time (Автоматические дата
и  время). Мы  всегда предпочитаем создавать измерение для календаря
самостоятельно, с  полным контролем над ним и  возможностью с  его по­
мощью осуществлять фильтрацию всех таблиц фактов в модели. Надеемся,
что вы также заведете себе такую привычку.

Использование нескольких измерений даты и времени


В одной таблице фактов может присутствовать сразу несколько полей с да-
тами. Это очень распространенная ситуация. В базе данных Contoso, к при-
меру, в таблице заказов хранятся три даты: дата заказа, дата оплаты и дата
поставки. И поля с датами есть далеко не в одной таблице фактов. В итоге
таких полей в модели насчитывается довольно много. Как правильно спро-
ектировать модель данных, в которой присутствует множество дат? Ответ
очень прост: за редкими исключениями, все даты в  модели должны хра-
ниться в едином измерении. В этом разделе мы расскажем о причинах оп-
тимальности такого подхода.
Как мы уже сказали, в таблице Sales есть три поля с датами. Вы могли бы соз-
дать измерение с названием Date и объединить эти таблицы тремя связями.
В результате, как мы отмечали ранее, активной будет только одна связь, соз-
данная первой. Остальные две будут неактивными, как показано на рис. 4.9.

Рис. 4.9. Из трех связей, созданных между таблицами, активна только одна, обозначен-
ная сплошной линией. Остальные (пунктирные) – неактивные

У вас есть возможность активировать связи прямо в формуле при помо-


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

Поскольку использовать неактивные связи мы не можем, попробуем про-


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

Примечание. Связи нельзя активировать по причине того, что


движок не  поддерживает создание неоднозначных моделей.
Неоднозначности возникают, когда из одной таблицы (в нашем
примере это Sales) в  другую (Date) можно добраться несколь-
кими путями. Представьте, что вам нужно создать вычисляемый
столбец в  таблице Sales с  использованием функции RELATED
(Date[Calendar Year]). В таком случае движок DAX не сможет вы-
брать, какой связью воспользоваться для обращения к таблице
дат. Именно поэтому активной в любой момент времени может
быть лишь одна связь между таблицами, и  это определяет по-
ведение функций RELATED и RELATEDTABLE, а также автомати-
ческое распространение контекста фильтра.

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


92    Работа с датой и временем

Используя получившуюся модель, мы, например, можем построить свод-


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

Рис. 4.11. Отчет показывает заказы с датами продажи и поставки из разных лет

На первый взгляд сводную таблицу, представленную на рис. 4.11, понять


очень сложно. Невозможно сразу определить, куда вынесен год поставки –
в строки или столбцы. Можно лишь догадаться, что он находится в столбцах,
потому что дата заказа всегда предшествует дате поставки. Но формально
это нигде не указано.
В этом случае достаточно установить префиксы для столбцов – OY для за-
каза и DY для поставки. Изменив запрос к таблицам с датами, мы получили
легко читаемый отчет, показанный на рис. 4.12.

Рис. 4.12. Добавление префиксов к годам значительно облегчило чтение отчета

Пока все выглядит так, будто вы можете спокойно создавать столько


дуб­лей таблиц с  датами, сколько вам необходимо,  – достаточно просто
пере­именовывать колонки и добавлять нужные префиксы для облегчения
чтения отчетов. До  некоторой степени вы правы. Но  представьте, во что
превратится ваша модель данных с увеличением количества таблиц фактов.
Если добавить, к примеру, одну таблицу Purchases, сценарий сущест­венно
усложнится, как показано на рис. 4.13.
Появление таблицы Purchases в модели приведет к добавлению еще трех
дат, поскольку у закупки также есть дата заказа, оплаты и поставки. Вам по-
надобится немного умения, чтобы правильно разобраться в этой ситуации.
Вы  можете добавить в  модель три новых измерения с  датами, доведя их
общее количество до шести. Но пользователи окажутся сбиты с толку при
Использование нескольких измерений даты и времени    93

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

Рис. 4.13. В таблице Purchases есть еще три даты

Еще один вариант – использовать для закупок те же измерения дат, ко-


торые использовались для заказов. К  примеру, таб­лица Order Date будет
фильт­ровать одновременно и таблицу Sales, и таблицу Purchases. То же са-
мое и с остальными измерениями. Модель данных в таком случае приоб-
ретет вид, показанный на рис. 4.14.
Модель данных, представленная на рис. 4.14, гораздо легче в использо-
вании, но по-прежнему достаточно сложна. К тому же стоит отметить, что
нам очень повезло с добавляемыми измерениями. В таблице Purchases ока-
зались те же три даты, что и в Sales, а в жизни это будет встречаться нечас­
то. Скорее всего, у  вас будут появляться новые таблицы фактов с  полями
дат, никак не связанными с теми, что уже присутствуют в модели. В таком
случае вам придется решать, создавать ли новое измерение для хранения
дат, усложняя тем самым модель, или пользоваться существующими табли-
цами, что доставит проблемы пользователям, работающим с моделью, по-
скольку названия дат не всегда будут в точности совпадать.
94    Работа с датой и временем

Рис. 4.14. Использование одного измерения дат для фильтрации двух таблиц фактов
облегчит использование модели

Проблема может быть решена, если вы откажетесь от создания отдельно-


го измерения для каждой даты в модели. Иными словами, если вы все даты
будете хранить в едином измерении, модель станет проще для понимания
и работы, что показано на рис. 4.15.
Использование одной таблицы с датами серьезно облегчит работу с мо­
делью данных. Вы интуитивно понимаете, что измерение Date осуществляет
срезы по таблицам Sales и Purchases с использованием их основного столб-
ца с датами – даты заказа. На первый взгляд получившаяся модель обладает
меньшей эффективностью по сравнению с предыдущей, и в какой-то степе-
ни это так и есть. Но перед тем как выносить такой вердикт, стоит потратить
немного времени и узнать, в чем же состоят отличия в аналитическом по-
тенциале между моделью с одним измерением дат и несколькими.
Использование нескольких измерений даты и времени    95

Рис. 4.15. Единое измерение Date, объединенное с таблицами продаж и закупок связью


по полям OrderDate

Наличие множества измерений дат позволит пользователю строить отче-


ты, используя в них сразу несколько календарных полей. Вы видели в пре-
дыдущем примере, как полезно может быть сравнение дат заказов с датами
поставки. Но нужно ли вам присутствие в модели нескольких таблиц с дата-
ми для обеспечения такой функциональности? Ответ – нет. Вы можете легко
справиться с этой проблемой путем создания специальных мер с расчетом
нужных вам показателей без изменения модели.
Если, к  примеру, вам необходимо провести сравнение с  учас­тием даты
заказа и даты поставки, вы можете сохранить в модели неактивную связь
между таблицами Sales и Date по полю DeliveryDateKey и активировать ее
программно для вычисления этой специфической меры. После добавления
этой связи мы получим модель, представленную на рис. 4.16.

Рис. 4.16. Связь между полями DeliveryDateKey и DateKey присутствует, но она не активна


96    Работа с датой и временем

Теперь вы можете использовать формулу, представленную ниже, для соз-


дания меры Delivered Amount:

Delivered Amount :=
CALCULATE (
[Sales Amount];
USERELATIONSHIP ( Sales[DeliveryDateKey]; 'Date'[DateKey] )
)

В этой мере включается неактивная связь между таблицами Sales и Date


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

Рис. 4.17. Мера Delivered Amount использует связь с датой поставки, а логика расчетов
скрыта внутри формулы

Примите это простое правило – создавать единое измерение для хране-


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

Обращение с датой и временем


Измерение дат нужно почти во всех моделях данных. С  другой стороны,
время встречается в  бизнес-аналитике куда реже. Есть и такие сценарии,
в  которых важны и даты, и  время. И  в таких случаях необходимо хорошо
понимать, как с ними обращаться.
Сразу нужно отметить, что таблица, предназначенная для хранения дат,
не может также хранить время. Фактически, для того чтобы сделать таблицу
Обращение с датой и временем    97

хранилищем календарных дат (а это необходимо, если вы хотите воспользо-


ваться специальными функциями для работы с датами и временем), нужно
выполнить некоторые требования со стороны DAX. В частности, столбец для
хранения даты должен иметь гранулярность на уровне дня, без информа-
ции о времени. Нет, вы не получите ошибку, если попытаетесь вместе с да-
той хранить время, но специальные функции могут работать некоррект­но
в случае дублирования дат.
Так что же делать, если вам необходимо вести учет времени? Самое пра-
вильное и простое решение состоит в том, чтобы создать отдельные изме-
рения для хранения даты и  времени. Для создания таблицы со временем
можно использовать несложный код на языке M в Power Query, представ-
ленный ниже:

Let
StartTime = #datetime(1900,1,1,0,0,0),
Increment = #duration(0,0,1,0),
Times = List.DateTimes(StartTime, 24*60, Increment),
TimesAsTable = Table.FromList(Times,Splitter.SplitByNothing()),
RenameTime = Table.RenameColumns(TimesAsTable,{{"Column1", "Time"}}),
ChangedDataType = Table.TransformColumnTypes(RenameTime,{{"Time", type
time}}),
AddHour = Table.AddColumn(
ChangedDataType,
"Hour",
each Text.PadStart(Text.From(Time.Hour([Time])), 2, "0" )
),
AddMinute = Table.AddColumn(
AddHour,
"Minute",
each Text.PadStart(Text.From(Time.Minute([Time])), 2, "0" )
),
AddHourMinute = Table.AddColumn(
AddMinute,
"HourMinute", each [Hour] & ":" & [Minute]
),
AddIndex = Table.AddColumn(
AddHourMinute,
"TimeIndex",
each Time.Hour([Time]) * 60 + Time.Minute([Time])
),
Result = AddIndex
in
Result

Выполнение этого скрипта приведет к созданию таблицы, показанной на


рис.  4.18. В таблице содержится столбец TimeIndex с  номерами по поряд-
ку от 0 до 1439, который вы можете использовать для связи с таблицами
фактов, и  несколько полей для осуществления срезов. Если ваша таблица
98    Работа с датой и временем

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


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

Рис. 4.18. Простая таблица для хранения времени,


сгенерированная при помощи Power Query

Столбец для индекса создан путем умножения количества часов на 60


и добавления минут, так что он легко может быть включен в качестве ключа
в  вашу таблицу фактов. Эти вычисления должны быть произведены в  ис-
точнике данных, откуда информация импортируется в таблицу.
Использование отдельной таблицы для хранения времени позволит вам
осуществлять срезы по часам, минутам и другим столбцам, которые вы до-
бавите в свою таблицу. Часто в таких измерениях можно встретить учет вре-
мени суток или временных диапазонов – к примеру, часовых интервалов,
как показано на рис. 4.19.
Есть сценарии, в которых вам не нужно разбивать показатели по времен-
ным диапазонам. К  примеру, вам может понадобиться проводить вычис-
ления на основании разницы в  часах между двумя событиями. Еще один
вариант – подсчет количества событий между двумя временными отмет-
ками, с гранулярностью ниже дня. Допустим, вы хотели бы знать, сколько
покупателей посетили ваш магазин между восемью часами утра 1 января
и  часом дня 7 января. Это более сложные сценарии, и  их мы рассмотрим
в главе 7 «Анализ интервалов даты и времени».
Функции для работы с датой и временем    99

Рис. 4.19. Измерение времени может быть полезным


для создания отчетов о продажах по часам

Функции для работы с датой и временем


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

Sales YTD :=
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
)

Функция DATESYTD возвращает набор дат с 1 января текущего выбранно-


го периода и до последней даты, включенной в контекст. Другие полезные
функции – SAMEPERIODLASTYEAR, PARALLELPERIOD и LASTDAY. Вы може-
те комбинировать эти функции для получения более сложных агрегаций.
К примеру, если вам нужно провести вычисления нарастающего итога с на-
чала года по предыдущему периоду, вы можете использовать следующую
формулу:
100    Работа с датой и временем

Sales PYTD :=
CALCULATE (
[Sales Amount];
DATESYTD ( SAMEPERIODLASTYEAR ( 'Date'[Date] ) )
)

Еще одной полезной функцией является DATESINPERIOD. Она возвра-


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

Sales Avg12M :=
CALCULATE (
[Sales Amount] / COUNTROWS ( VALUES ( 'Date'[Month] ) );
DATESINPERIOD (
'Date'[Date];
MAX ( 'Date'[Date] );
-12;
MONTH
)
)

Результат расчета средних показателей вы видите на рис. 4.20.

Рис. 4.20. Мера, вычисляющая средние показатели продаж


за 12 месяцев
Работа с финансовыми календарями    101

Работа с финансовыми календарями


Еще одной причиной для создания своей собственной таблицы с  датами
является облегчение работы с  финансовыми календарями (fiscal calendars).
Вы также получите возможность взаимодействовать с более сложными ка-
лендарями, включая недельный и сезонный.
При работе с финансовыми календарями нет необходимости добавлять
специальные столбцы в таблицу фактов. Вместо этого вы вводите новые
поля в ваше измерение с датами, чтобы при желании иметь возможность
осуществлять срезы как по обычному, так и по финансовому календарю.
Представьте, например, что вам необходимо создать финансовый кален-
дарь, в котором первым месяцем будет июль. То есть финансовый год бу-
дет продолжаться с 1 июля по 30 июня. В таком случае вам понадобится
модифицировать свое измерение, чтобы в нем появились финансовые ме-
сяцы, а также изменить некоторые расчеты для работы с альтернативным
календарем.
Первое, что нужно сделать, – добавить набор специальных столбцов в ка-
лендарь для обработки финансовых месяцев (если их еще нет в таблице).
Некоторые предпочитают работать с  названиями месяцев (у  нас первым
месяцем будет июль), другие склоняются к использованию порядковых но-
меров месяцев. Допустим, вместо июля они будут оперировать названием
«Финансовый месяц 01». В нашем примере мы будем использовать тради-
ционные названия месяцев.
Не важно, какую технику именования месяцев вы выберете, в  любом
случае вам понадобится дополнительный столбец для их правильной сор­
тировки. В  обычном календаре у  нас есть столбец Month Name (название
месяца), который сортируется по полю Month Number (номер месяца). Это
позволяет выстроить месяцы в отчетах с января по декабрь. При использо-
вании альтернативного календаря вам необходимо, чтобы июль открывал
год, а  июнь – закрывал. Поскольку вы не  можете сортировать столбец по
нескольким полям, вам придется продублировать столбец с  названиями
месяцев, назвав его Fiscal Month (Финансовый месяц), и создать новое поле
для его сортировки.
Когда это сделано, вы сможете пользоваться финансовым календарем,
и месяцы будут отсортированы в нем правильно. И все же некоторые вычис-
ления будут работать не так, как вы ожидали. Для примера посмотрите на
расчет нарастающего итога продаж с начала года, приведенный на рис. 4.21.
102    Работа с датой и временем

Рис. 4.21. Нарастающий итог с финансовым календарем работает неправильно

Если внимательно посмотреть на вывод, можно обнаружить, что данные


по нарастающему итогу обнуляются в январе 2008 года, а не в июле, как мы
планировали. Причина в том, что специальные функции даты и  времени
ориентированы на работу со стандартным календарем и просто не умеют
работать с альтернативными. Но у некоторых из них есть дополнительный
параметр, позволяющий взаимодействовать с финансовыми календарями.
И  функция DATESYTD для работы с  нарастающими итогами из их числа.
Для осуществления нужного вычисления с финансовым календарем необ-
ходимо передать функции DATESYTD второй параметр, указывающий день
и месяц окончания года, как в примере ниже:
Sales YTD Fiscal :=
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date]; "06/30" )
)

На рис. 4.22 представлен вывод отчета с нарастающим итогом по обыч-


ному и финансовому календарям.
Работа с финансовыми календарями    103

Рис. 4.22. Продажи в новой колонке обнуляются в июле, как мы и ожидали

Разумеется, разные виды вычислений требуют разных подходов, но стан-


дартные функции DAX для работы с  датой и  временем могут быть легко
адаптированы для обработки финансовых календарей. В последнем разде-
ле этой главы мы затронем тему недельного календаря. Это еще одна полез-
ная разновидность альтернативного варианта хранения дат. Если у вас есть
необходимость работать с  более сложными календарями, вы можете об-
ратиться к  соответствующим шаблонам по адресу http://www.daxpatterns.
com/time-patterns/.
В этой книге мы главным образом хотим подчеркнуть, что для работы
с финансовыми календарями вам не придется создавать дополнительных
таблиц. Если ваше измерение дат правильно спроектировано, обработка
с  его помощью альтернативных календарей не  составит труда. Для этого
достаточно будет добавить в него несколько специальных полей.
Если вы позволите Power BI Desktop или Excel добавлять столбцы для об-
работки дат и времени за вас, вы лишитесь возможности использовать эту
простую технику работы с  альтернативными календарями, а  значит, вы-
нуждены будете самостоятельно разбираться с тем, как писать правильные
формулы.
104    Работа с датой и временем

Расчет рабочих дней


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

Учет рабочих дней в рамках одной страны или региона


Начнем с  простой модели данных, включающей таблицы Date, Product
и Sales. Но нас главным образом будет интересовать измерение с датами.
Изначально таблица Date выглядит так, как показано на рис. 4.23.

Рис. 4.23. Отправная точка для анализа рабочих дней в таблице Date

В таблице нет информации о том, является конкретный день рабочим или


выходным. Предположим, что есть две разновидности нерабочих дней: вы-
ходные и праздники. Если в вашем регионе выходные дни – суббота и вос-
кресенье, вы можете легко создать вычисляемый столбец для хранения этой
информации, как в представленном ниже фрагменте кода. Если выходные
в вашей стране в другие дни, вам не составит труда поправить формулу:
Расчет рабочих дней    105

'Date'[IsWorkingDay] =
INT (
AND (
'Date'[Day of Week Number] <> 1;
'Date'[Day of Week Number] <> 7
)
)

Мы перевели булево значение в целое число, чтобы облегчить суммиро-


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

NumOfWorkingDays = SUM ( 'Date'[IsWorkingDay] )

Эта мера вычисляет правильное количество рабочих дней в году, как вид-
но на рис. 4.24.

Рис. 4.24. Мера NumOfWorkingDays подсчитывает количество рабочих дней в любом


периоде

До сих пор мы учитывали только выходные дни, то есть субботу и  вос-


кресенье. Но нужно еще принимать во внимание праздничные дни. Мы со-
брали список государственных праздников в США за 2009 год с сайта www.
timeanddate.com. После этого воспользовались редактором запросов в Power
BI Desktop, чтобы получить таблицу, представленную на рис. 4.25.

Рис. 4.25. В таблице Holidays представлен перечень государственных праздников в США


106    Работа с датой и временем

Теперь у нас есть два варианта в зависимости от того, является ли столбец


Date в таблице Holidays ключевым. Если это так, то мы можем объединить
таблицы Date и Holidays посредством связи, чтобы получилась модель дан-
ных, показанная на рис. 4.26.

Рис. 4.26. Таблица Holidays успешно интегрируется в модель, если поле Date является
ключом

После установки связи мы можем изменить формулу для меры


IsWorkingDay для учета новых обстоятельств. Новое условие добавляет
проверку на вхождение конкретной даты в список праздников. Посмотри-
те на код ниже:

'Date'[IsWorkingDay] =
INT (
AND (
AND (
'Date'[Day of Week Number] <> 1;
'Date'[Day of Week Number] <> 7
);
ISBLANK ( RELATED ( Holidays[Date] ) )
)
)

Получившаяся модель очень схожа со схемой «звезда». На самом деле это


«снежинка», но из-за небольшого объема таблиц Date и Holidays быстродей-
ствие не страдает.
Бывает, что столбец Date в таблице Holidays не является ключом. Это воз-
можно, если несколько праздников выпадают на одни и те же дни – в этом
случае поле даты в таблице не будет уникальным. В таком случае вам не-
обходимо привести связь к типу «один ко многим» с целевой таблицей Date
и источником Holidays. Помните, что поле Date в календаре является пер-
вичным ключом по определению. Теперь наша формула будет выглядеть
так:

'Date'[IsWorkingDay] =
INT (
AND (
Расчет рабочих дней    107

AND (
'Date'[Day of Week Number] <> 1;
'Date'[Day of Week Number] <> 7
);
ISEMPTY ( RELATEDTABLE ( Holidays ) )
)
)

Единственная строка, которая была изменена, – это проверка на вхожде-


ние в перечень праздников. Вместо использования функции RELATED мы
обратились к RELATEDTABLE с проверкой на отсутствие записей. Поскольку
мы имеем дело с вычисляемым столбцом, небольшой спад производитель-
ности здесь вполне приемлем.

Учет рабочих дней в разных странах


Как вы поняли, учет рабочих дней в рамках одной страны или региона в мо-
дели данных не составляет особого труда. Сложности появляются, когда не-
обходимо принимать во внимание праздники и выходные в разных стра-
нах. В этом случае вы более не можете полагаться на вычисляемый столбец.
Фактически в зависимости от выбранной страны столбец IsHoliday может
принимать разные значения.
Если вы должны вести учет всего для пары стран, самым простым реше-
нием будет создать два столбца – предположим, для США и Китая – с назва-
ниями IsHolidayChina и IsHolidayUnitedStates и использовать их в зависи-
мости от страны. Однако если стран в вашей модели данных больше, такой
способ неприменим. Давайте рассмотрим предельно сложный сценарий.
Обратите внимание, что структура и  содержимое таблицы Holidays изме-
нились по сравнению с предыдущим примером, как показано на рис. 4.27.
А именно в таблицу был добавлен новый столбец Country Region с указани-
ем страны или региона. К тому же поле Date больше не является ключевым,
поскольку одни и те же дни в разных странах могут быть праздничными.

Рис. 4.27. Таблица Holidays содержит праздники для разных стран


108    Работа с датой и временем

Модель данных незначительно модифицировалась по сравнению с пре-


дыдущей версией, как видно по рис. 4.28. Главным изменением стала смена
направления связи между таблицами Date и Holidays.

Рис. 4.28. Модель данных с разными странами похожа на модель с одной страной

Особенность учета нескольких регионов состоит в правильном понима-


нии вычисляемых значений. Например, вопрос «Сколько рабочих дней в ян-
варе?» больше не является таким простым. По сути, без указания конкретной
страны подсчет количества рабочих дней в такой модели не имеет смысла.
Чтобы лучше понять проблему, посмотрите на рис. 4.29. Мера в представ-
ленном отчете рассчитана при помощи функции COUNTROWS по таблице
Holidays, а значит, возвращает количест­во праздников для каждой страны.

Рис. 4.29. Цифры означают количество праздников по странам и месяцам


Расчет рабочих дней    109

Количество праздников в отчете правильное по каждой стране, но ито-


ги по месяцу просто суммируют значения в  ячейках и  не  учитывают, что
один и тот же день может быть праздничным в одной стране и рабочим –
в другой. К примеру, в феврале в США один праздничный день, а в Китае
и Германии праздников нет. Так какое общее количество праздничных дней
в феврале? Такая постановка вопроса не имеет никакого смысла, если речь
идет о сравнении количества рабочих дней с выходными. Накопительный
итог по количеству праздников для всех стран никак не поможет нам в ана-
лизе, а ответ зависит только от конкретно выбранной страны.
В этот момент необходимо сделать уточнение в модели данных относи-
тельно того, как считать рабочие дни. Перед вычислением вы, например,
можете проверять, одна ли страна выбрана в отчете, при помощи шаблона
IF ( HASONEVALUE () ) в DAX.
Есть еще один важный момент, который стоит учесть перед написанием
окончательной формулы. Вы могли бы вычислить количество рабочих дней
путем вычитания числа праздников, извлеченных из таблицы Holidays, из
общего количества дней. Но в этом случае вы обойдете вниманием суббо-
ты и воскре­сенья. Более того, если праздник выпадает на выходные, то его
также не стоит учитывать. Можно решить эту задачу путем использования
шаблона с  двунаправленной фильтрацией и  подсчета дней, не  входящих
в таблицу Holidays и не являющихся субботой и воскресеньем. Измененная
формула приобретет следующий вид:

NumOfWorkingDays :=
IF (
OR (
HASONEVALUE ( Holidays[CountryRegion] );
ISEMPTY ( Holidays )
);
CALCULATE (
COUNTROWS ( 'Date' );
AND (
'Date'[Day of Week Number] <> 1;
'Date'[Day of Week Number] <> 7
);
EXCEPT ( VALUES ( 'Date'[Date] ); VALUES ( Holidays[Date] ) )
)
)

В этой формуле есть два интересных момента, выделенных жирным


шрифтом. Рассмотрим каждый из них:
 необходимо убедиться, что в поле CountryRegion выбран только один
регион, чтобы мера не вычисляла значение для множественного вы-
бора. В то же время нужно проверить, что таблица Holidays пуста, по-
скольку для месяцев без праздников в столбце CountryRegion не будет
ни одного значения, а значит, функция HASONEVALUE вернет False;
110    Работа с датой и временем

 в качестве фильтра для CALCULATE можно использовать функцию


EXCEPT для извлечения дней, не  входящих в  список праздничных.
Этот набор будет объединен логическим И  (AND) с  набором дней,
не  являющихся выходными. В  результате мы получим правильный
расчет.
Однако нашу модель данных по-прежнему нельзя назвать универсаль-
ной. Мы допустили предположение, что выходные дни – это суббота и вос-
кресенье, но это верно не для всех стран и регионов. Если вы хотите учесть
это, придется еще немного усложнить модель данных. Нам понадобится
еще одна таблица, в которой будут указаны выходные дни для каждой стра-
ны. А поскольку теперь у нас есть две таблицы, которые необходимо фильт­
ровать по стране, необходимо выделить страны в  отдельное измерение.
Окончательная модель данных представлена на рис. 4.30.

Рис. 4.30. В окончательной модели присутствует таблица для хранения выходных дней


по странам и отдельное измерение для стран и регионов

Итоговая формула, представленная ниже, несколько упрос­тилась, но она


может быть не так проста для понимания:

NumOfWorkingDays :=
IF (
HASONEVALUE ( CountryRegions[CountryRegion] );
CALCULATE (
Работа с особыми периодами года    111

COUNTROWS ( 'Date' );
EXCEPT (
VALUES ( 'Date'[Day of Week Number] );
VALUES ( Weekends[Day of Week Number] )
);
EXCEPT ( VALUES ( 'Date'[Date] ); VALUES ( Holidays[Date] ) )
)
)

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

Примечание. Когда модель данных становится более сложной,


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

Работа с особыми периодами года


Работая с датами и временем, не стоит забывать о существовании так назы-
ваемых особых периодов (special periods) года. К примеру, если вы занимае-
тесь аналитикой бронирования отелей, вам важно помнить про пасхальные
дни и иметь возможность сравнивать такие периоды в разные годы. Проб­
лема в том, что Пасха каждый год выпадает на разные даты. Так что вам
необходимо учитывать эти изменяющиеся периоды с целью их сравнения.
Также очень полезно уметь создавать отчеты и  панели мониторинга
(dashboard), содержимое которых обновляется в зависимости от даты фор-
мирования. К примеру, у вас есть панель мониторинга для сравнения про-
даж в  текущем месяце с  предыдущим. Проблема в  том, что определение
текущего месяца тесно связано с текущим днем. Сегодня текущим месяцем
может быть апрель, а в этот же день в следующем месяце – май, и нам бы
не хотелось обновлять фильтры панели мониторинга каждый месяц.
Как и в случае с учетом рабочих дней, изменения в модели данных будут
зависеть от того, могут ли пересекаться особые периоды.

Работа с непересекающимися периодами


Если периоды времени, которые вы собираетесь анализировать, не  пере-
секаются, построить модель данных не  составит труда. Как и  в  предыду-
щем примере с праздничными днями, вам понадобится конфигурационная
таблица (configuration table) для хранения периодов. Мы создали таблицу
112    Работа с датой и временем

с  пасхальными днями и  кануном Рождества для 2008, 2009 и  2010 годов,


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

Рис. 4.31. В таблице SpecialPeriods собраны особые периоды по годам

Пасхальные дни начинаются за несколько дней до означенной даты и за-


канчиваются спустя несколько дней после нее. Несмотря на то что дата в таб­
лице SpecialPeriods является первичным ключом, нет никакого смысла объ-
единять эту таблицу связью с другими. По  сути, единственным значимым
для нас полем в таблице SpecialPeriods является название периода, который
нам предстоит анализировать. Будет полезно денормализовать его при по-
мощи вычисляемого столбца в нашем измерении дат, как показано ниже:

'Date'[SpecialPeriod] =
CALCULATE (
VALUES ( SpecialPeriods[Description] );
FILTER (
SpecialPeriods;
AND (
SpecialPeriods[Date] - SpecialPeriods[DaysBefore] <=
'Date'[Date];
SpecialPeriods[Date] + SpecialPeriods[DaysAfter] >
'Date'[Date]
)
)
)

В этом столбце будет выводиться название особого периода для дат, ко-
торые попадают в интервал между:
 датой особого периода минус указанное в конфигурационной табли-
це количество дней;
 датой особого периода плюс указанное количество дней.
На рис.  4.32 показано заполнение вычисляемого столбца SpecialPeriod
для пасхальных дней 2008 года.
Наличие этого столбца позволит выполнять фильтрацию по нему в раз-
ные годы. Это даст нам возможность сравнить продажи в  особый период
текущего года и предыдущего, не заботясь о том, на какие именно даты вы-
падал этот период. Вы можете видеть построенный отчет на рис. 4.33.
Работа с особыми периодами года    113

Рис. 4.32. Для дат, входящих в особые периоды, заполнен столбец SpecialPeriod

Рис. 4.33. Отчет показывает продажи в пасхальные и рождественские дни 2008 и 2009


годов

Использованная техника прекрасно справляется со своими задачами.


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

Периоды, связанные с текущим днем


В предыдущем разделе вы научились работать с непересекающимися осо-
быми временными периодами при помощи конфигурационной таблицы.
Похожую технику можно применить и  для создания отчетов, содержимое
которых меняется динамически. Представьте, что вам нужно построить па-
нель мониторинга, показанную на рис.  4.34, которая отображает продажи
товаров в разрезе брендов по различным временным периодам, а рядом вы-
водит радиальный датчик (gauge) со сравнением продаж за сегодня и вчера.
114    Работа с датой и временем

Рис. 4.34. Отчет содержит панель мониторинга с датчиком для сравнения продаж


за сегодня и вчера

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

Рис. 4.35. В таблице RelativePeriods хранятся временные


периоды, связанные с текущим днем

Каждый период характеризует его название, код и  определенное ко-


личество дней до текущей даты. Даты, попадающие в  интервал между
DatesFrom и DaysTo относительно текущего дня, будут помечены названи-
ем периода, хранящимся в поле Description. Столбец с кодом в основном
используется для сор­тировки. После создания конфигурационной табли-
цы необходимо извлечь из нее название периода и код (для сортировки)
и соответствующим образом пометить даты, входящие в эти временные
интервалы. Это можно сделать при помощи двух вычисляемых столбцов
в таблице Date. Первый из них находит код относительного периода сле-
дующим образом:
Работа с особыми периодами года    115

'Date'[RelPeriodCode] =
VAR LastSalesDateKey =
MAX ( Sales[OrderDateKey] )
VAR LastSaleDate =
LOOKUPVALUE( 'Date'[Date]; 'Date'[DateKey]; LastSalesDateKey )
VAR DeltaDays =
INT ( LastSaleDate - 'Date'[Date] )
VAR ValidPeriod =
CALCULATETABLE(
RelativePeriods;
RelativePeriods[DaysTo] >= DeltaDays;
RelativePeriods[DaysFrom] < DeltaDays
)
RETURN
CALCULATE ( VALUES ( RelativePeriods[Code] ); ValidPeriod )

На каждом из шагов в представленном фрагменте кода используются пе-


ременные. Сначала мы получаем последнее доступное значение из столбца
OrderDateKey из таблицы Sales. Далее при помощи функции LOOKUPVALUE
вычисляем дату, ассоциированную с  найденным ключом. Переменной
DeltaDays мы присваиваем разницу между сегодняшней датой и текущей.
На заключительном этапе мы вызываем функцию CALCULATETABLE в по-
иске единственной строки в  таблице RelativePeriods, в  которой значение
переменной DeltaDays входит в интервал между DaysFrom и DaysTo.
В результате этой формулы мы получим код относительного периода,
в интервал которого входит дата. После этого мы можем создать второй вы-
числяемый столбец с названием этого периода, как показано ниже:

'Date'[RelPeriod] =
VAR RelPeriod =
LOOKUPVALUE(
RelativePeriods[Description];
RelativePeriods[Code];
'Date'[RelPeriodCode]
)
RETURN
IF ( ISBLANK ( RelPeriod ); "Future", RelPeriod )

На рис. 4.36 показана таблица Date с двумя новыми вычисляемыми столб-


цами RelPeriodCode и RelPeriod.
Будучи вычисляемыми столбцами, RelPeriodCode и RelPeriod пересчиты-
ваются каждый раз, когда обновляется модель данных. И в этот момент ме-
няются принадлежности дат к тому или иному периоду. Нет необходимости
обновлять отчет, поскольку он всегда будет показывать последнюю обрабо-
танную дату как сегодня, дату перед ней как вчера и т. д.
116    Работа с датой и временем

Рис. 4.36. Последние два столбца вычислены посредством формул, приведенных выше

Работа с пересекающимися периодами


Техники, показанные в предыдущих разделах, прекрасно работают на прак-
тике, но имеют одно существенное ограничение – используемые временные
периоды не должны пересекаться. До этого момента мы хранили указание
на принадлежность даты тому или иному периоду в вычисляемом столбце,
а по своей природе столбец может содержать только одно значение.
Но есть случаи, когда это неприемлемо. Предположим, вы устраиваете
скидки на определенные категории товаров в разные периоды года. Вполне
возможно, что в  один и тот же временной промежуток скидка будет рас-
пространяться сразу на несколько категорий. В то же время одна катего-
рия может продаваться со скидкой в разные промежутки времени. Так что
в представленном сценарии вы не можете хранить информацию о периоде
продаж в таблице Products или Date.
Сценарий, когда несколько строк из одной таблицы (категории) должны
быть объединены с несколькими строками из другой (даты), известен как
модель «многие ко многим». Такими моделями довольно непросто управ-
лять, но они дают возможность проводить очень глубокую аналитику, и мы
не можем обойти их своим вниманием. Больше об этом типе моделей вы
сможете прочитать в главе 8. Здесь же мы хотим показать, что присутствие
в модели связей «многие ко многим» способно значительно усложнить на-
писание формул.
Конфигурационная таблица Discounts из этого примера показана на
рис. 4.37.
Работа с особыми периодами года    117

Рис. 4.37. Временные периоды скидок для разных категорий товаров хранятся


в конфигурационной таблице Discounts

Анализируя таблицу Discounts, можно заметить, что в  первую неделю


января в 2007 и 2008 годах скидки действовали на две категории товаров
(компьютеры и аудиотехнику). То же самое можно сказать и о первых двух
неделях августа (в этот период скидки распространялись на аудиотехнику
и мобильные телефоны). В подобном сценарии вы не можете полагаться на
связи между таблицами, а вынуждены писать код на DAX, который возьмет
текущий фильтр из периодов продаж и объединит его с уже существующим
фильтром из таблицы Sales. Пример такого кода представлен ниже:

SalesInPeriod :=
SUMX (
Discounts;
CALCULATE (
[Sales Amount];
INTERSECT (
VALUES ( 'Date'[Date] );
DATESBETWEEN ( 'Date'[Date]; Discounts[DateStart];
Discounts[DateEnd] )
);
INTERSECT (
VALUES ( 'Product'[Category] );
CALCULATETABLE ( VALUES ( Discounts[Category] ) )
)
)
)

Используя эту формулу, можно получить отчет, представленный на


рис. 4.38.
118    Работа с датой и временем

Рис. 4.38. При использовании пересекающихся периодов можно наблюдать несколько


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

Отчет на рис. 4.38 показывает периоды скидок на различные категории


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

Работа с недельными календарями


Как вы уже знаете, работая со стандартными календарями, вы можете
легко и просто вычислять значения нарастающим итогом с начала года,
месяца или сравнивать сопоставимые периоды, поскольку DAX предо-
ставляет вам ряд специальных функций для работы с датой и временем.
Сложности начинаются, когда вам приходится работать с нестандартны-
ми календарями.
Что такое нестандартные календари? Это календари, в  которых не  вы-
полняются канонические правила деления года на 12 месяцев с различным
количеством дней в  месяцах. Например, специфика некоторых организа-
ций предусматривает оперативную работу с неделями, а не месяцами. К со-
жалению, недели не  объединяются в  месяца или года. В  месяцах бывает
разное количество недель, как и  в  годах. Есть широко распространенные
техники для работы с годами, основанными на неделях, но ни одна из них
не является стандартом, который можно было бы формализовать в языке
DAX. Именно поэтому в DAX не предусмотрены функции для работы с не-
Работа с недельными календарями    119

стандартными календарями. И если вам понадобится работать с ними, вам


придется позаботиться обо всем самим.
К счастью, даже в  отсутствие специальных функций вы можете вос-
пользоваться определенными техниками моделирования для работы с не-
стандартными календарями. Мы не покажем их все в этом разделе. Наша
цель – продемонстрировать вам несколько примеров, которые вам придет-
ся адаптировать к собственным нуждам в случае необходимости. Если вам
понадобится дополнительная информация по работе с похожими шаблона-
ми, обратитесь по адресу http://www.daxpatterns.com/time-patterns/.
В качестве примера работы с  нестандартными календарями мы рас-
смотрим вычисления, основанные на недельном календаре, с соблюдением
стандарта ISO 8601. Если вам нужно больше информации об использовании
недельных календарей, вы можете найти ее по адресу https://en.wikipedia.
org/wiki/ISO_week_date.
На первом шаге мы создадим полноценный календарь ISO. Есть множест­
во способов для выполнения этой задачи. И  даже вполне вероятно, что
в вашей базе данных уже имеется календарь ISO. Для нашего примера мы
построим календарь ISO с  использованием DAX и  таблицы соответствий,
поскольку это позволит вам овладеть полезными навыками моделирования
данных.
В основе календаря, который мы будем использовать, лежат недели. Не-
деля всегда начинается с понедельника, а год – с первой недели. Таким об-
разом, есть большая вероятность, что год начнется, например, с 29 декабря
предыдущего календарного года или со 2 января текущего. Для учета этой
особенности вы можете добавить вычисляемые столбцы в стандартную таб­
лицу с календарем для определения номера недели ISO и года ISO. Следу-
ющие формулы позволят вам прийти к таблице, содержащей дополнитель-
ные столбцы Calendar Week, ISO Week и ISO Year и показанной на рис. 4.39.

'Date'[Calendar Week] = WEEKNUM ( 'Date'[Date]; 2 )


'Date'[ISO Week] = WEEKNUM ('Date'[Date]; 21 )
'Date'[ISO Year] =
CONCATENATE (
"ISO ";
IF (
AND ( 'Date'[ISO Week] < 5; 'Date'[Calendar Week] > 50 );
YEAR ( 'Date'[Date] ) + 1;
IF (
AND ( 'Date'[ISO Week] > 50; 'Date'[Calendar Week] < 5
);
YEAR ( 'Date'[Date] ) – 1;
YEAR ( 'Date'[Date] )
)
)
)
120    Работа с датой и временем

Рис. 4.39. Год ISO отличается от обычного календарного года тем, что всегда начинается
с понедельника

Если традиционные месяц и неделю для конкретной даты определить до-


вольно легко при помощи вычисляемого столбца, с месяцем ISO придется
потрудиться. Есть различные способы для вычисления месяца по стандарту
ISO. Один из них начинается с разбиения года на кварталы. В квартале со-
держится три месяца, каждый из которых соответствует одному из следую-
щих наборов из трех цифр: 445, 454 или 544. Эти цифры говорят о количест­
ве недель в  соответствующем месяце. Например, в  квартале, отмеченном
группой 445, первые два месяца содержат по четыре недели, а последний –
пять. Эта концепция применима и к другим техникам. Вместо того чтобы
писать сложную математическую формулу, вычисляющую месяц, которому
принадлежит та или иная неделя в различных стандартах, лучше один раз
составить таблицу соответствий, представленную на рис. 4.40.
Когда таблица Weeks to Months готова, можно использовать функцию
LOOKUPVALUE, как показано в следующем фрагменте кода:

'Date'[ISO Month] =
CONCATENATE
"ISO M";
RIGHT (
CONCATENATE (
"00";
LOOKUPVALUE(
'Weks To Months'[Period445];
'Weks To Months'[Week];
'Date'[ISO Week]
);
2
)
)
Работа с недельными календарями    121

Рис. 4.40. Weeks to Months – таблица соответствия недель и месяцев соотносит номер


недели с месяцем в трех столбцах (по одному для каждой техники)

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


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

Рис. 4.41. Месяц ISO легко вычисляется при помощи таблицы соответствий

Теперь, когда все столбцы готовы, можно начинать строить иерархии


и разбивать модель по годам, месяцам и неделям ISO. Однако при работе
с такими календарями производить вычисления нарастающего итога с на-
чала года, месяца, а также пользоваться специальными функциями для ра-
боты с датой и временем бывает затруднительно. Фактически стандартный
набор функций DAX в области дат рассчитан на работу с обычным григо-
122    Работа с датой и временем

рианским календарем и оказывается бесполезным при использовании не-


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

Sales ISO YTD :=


IF (
HASONEVALUE ( 'Date'[ISO Year] );
CALCULATE (
[Sales Amount];
ALL ('Date' );
FILTER ( ALL ( 'Date'[Date] ); 'Date'[Date] <= MAX (
'Date'[Date] ) );
VALUES ( 'Date'[ISO Year] )
)
)

Как видите, ключом к  расчету меры является установка фильт­ров на


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

Рис. 4.42. Мера вычисляет сумму продаж нарастающим итогом с начала года ISO

Вычисления нарастающим итогом с начала месяца и недели производят-


ся аналогичным образом и не представляют особой сложности. Более слож-
ные манипуляции потребуются, если вам, к примеру, нужно будет провести
вычисление показателей за аналогичный период прошлого года. Поскольку
воспользоваться функцией SAMEPERIODLASTYEAR вам здесь не  удастся,
придется немного поработать с моделью и языком DAX.
Чтобы определить аналогичный период прошлого года, нужно взять вы-
бранную в  контексте фильтра дату и  найти в  прошлом году набор дат за
Работа с недельными календарями    123

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


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

Date[ISO Day Number] = ( 'Date'[ISO Week] - 1 ) * 7 + WEEKDAY( 'Date'[Date]; 2


)

Результат вы можете видеть на рис. 4.43.

Рис. 4.43. Столбец ISO Day Number заполнен порядковыми номерами дней в году
по стандарту ISO

Этот столбец для нас очень важен, потому что для определения аналогич-
ного интервала дат в прошлом году нам нужно воспользоваться следующей
формулой:

Sales SPLY :=
IF (
HASONEVALUE ( 'Date'[ISO Year Number] );
CALCULATE (
[Sales Amount];
ALL ( 'Date' );
VALUES ( 'Date'[ISO Day Number] );
'Date'[ISO Year Number] = VALUES ( 'Date'[ISO Year Number] ) –
1
)
)

Как видите, мы сняли все фильтры с календаря и заменили их двумя но-


выми условиями:
 в столбце ISO Year Number должен быть указан предыдущий год;
 значения в столбце ISO Day Number должны совпадать.
Таким образом, вне зависимости от того, какой выбор мы сделаем в те-
кущем контексте фильтра (день, неделя или месяц), он будет смещен ровно
на год назад.
124    Работа с датой и временем

На рис. 4.44 показана мера Sales SPLY в отчете со срезами по году и ме-


сяцу.
Используя похожие техники, вы можете вычислять значения предыдуще-
го месяца, рост показателей в процентном отношении по сравнению с ана-
логичным периодом в прошлом году и т. д. В последнем сценарии добавле-
ние в модель вычисляемого столбца значительно упростит процесс расчета.
В то же время попытка произведения подобных вычислений без столбца ISO
Day Number может вылиться в написание крайне сложных формул. Больше
информации по работе в DAX с недельными календарями можно найти по
ссылке http://www.sqlbi.com/articles/week-based-time-intelligence-in-dax/.

Рис. 4.44. На строках 2008 года мера Sales SPLY показывает прошлогодние показатели
продаж по соответствующим месяцам

Заключение
Работа с датами и временем – очень обширная и интересная тема. Практи-
чески каждая модель данных в бизнес-аналитике так или иначе взаимодей-
ствует с календарями. Из этой главы вы узнали следующее:
 большинство вычислений (если не все), связанных с датой и време-
нем, требуют наличия в модели данных таб­лицы с календарем;
 при создании измерения дат необходимо обращать внимание на де-
тали, такие как сортировка месяцев;
Заключение    125

 если в  вашей модели есть сразу несколько столбцов с  датами, это


не значит, что вы не сможете обойтись единственным календарным
измерением. С одним календарем расчеты будет производить гораз-
до легче. Если вам понадобится не  один календарь, вы сможете за-
грузить таблицу Date несколько раз;
 хранение дат и времени необходимо всегда разделять – это полезно
как с  точки зрения легкости моделирования, так и  для повышения
производительности.
Оставшаяся часть главы была посвящена различным сценариям работы
с датой и  временем. Мы  разобрали специфику вычисления рабочих дней
в одной и нескольких странах, поговорили об особых периодах года с ис-
пользованием вычисляемых столбцов в измерении дат и созданием новых
таблиц, а также уделили внимание работе с календарями ISO.
По причине большого разнообразия подходов к работе с датами и вре-
менем велика вероятность, что ни один из рассмотренных нами примеров
не подойдет к вашей специфике работы. Но вы можете использовать пред-
ставленные здесь концепции для разработки собственных сценариев, что
чаще всего требует создания вычисляемых столбцов в измерении дат и на-
писания кода на DAX разной степени сложности.
Глава 5
Отслеживание исторических
атрибутов

Информация имеет свойство меняться со временем. И для определенных


моделей данных и отчетов бывает полезно отслеживать как текущие зна-
чения некоторых атрибутов, так и их содержимое на тот или иной момент
времени. К примеру, компании может понадобиться отслеживать измене-
ние адресов своих клиентов. Есть также товары с изменчивой специфика-
цией, и может быть очень полезно анализировать продажи в зависимости
от тех или иных характеристик. Или у вас может возникнуть необходимость
сравнить итоговые продажи с учетом разных цен на товары и услуги. Все
это довольно распространенные сценарии, и есть устоявшиеся методики по
работе с ними.
Если вам необходимо хранить состояние тех или иных атрибутов в зави-
симости от времени, значит, вы имеете дело с так называемыми историче-
скими атрибутами (historical attributes), или, говоря техническим языком,
медленно меняющимися измерениями (slowly changing dimensions  – SCD).
Это не самая сложная тема в области моделирования данных, но здесь есть
свои скрытые нюансы.
В этой главе мы рассмотрим несколько моделей данных, и вы убедитесь
в том, что фактор наличия медленно меняющихся измерений очень важно
учитывать на этапе проектирования модели. Также мы обсудим, как управ-
лять различными сценариями.

Введение в медленно меняющиеся измерения


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

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


пояснить, когда и зачем нужно использовать медленно меняющиеся измере-
ния. Представьте, что за каждым вашим покупателем закреплен конкретный
менеджер. Простейшим способом хранения этой информации является до-
бавление соответствующего атрибута в таблицу покупателей. Но со временем
у покупателя может смениться менеджер. К примеру, до прошлого года за по-
купателя по имени Николас (Nicholas) мог отвечать менеджер по имени Пол
(Paul), а после этого ответственность за него перешла к Луизе (Louise). Если вы
просто обновите значение в соответствующем столбце измерения Customer,
то при анализе продаж Луизы в будущем увидите и продажи, совершенные
Полом. Это приведет к ошибкам в расчетах. Значит, нам нужна модель данных
с возможностью хранить историю привязки менеджера к покупателю.
В зависимости от того, как вы намерены работать с  обновляющимися
данными, медленно меняющиеся измерения можно разделить на несколь-
ко категорий. Специалисты пока не при­шли к общему мнению в отноше-
нии классификации таких измерений. Сценарии, включающие в себя меня-
ющиеся измерения, очень разнообразны, и когда кто-то находит удобную
методику обращения с тем или иным сложным сценарием, он тут же дает
своему открытию имя. Что касается именования, разработчики моделей
данных обычно не скупятся на поиск новых названий для своих детищ.
В этой книге мы будем придерживаться изначального подхода к класси-
фикации измерений по типам:
 тип 1. В медленно меняющихся измерениях первого типа хранятся
лишь текущие значения атрибутов. В процессе работы старые значе-
ния полей заменяются на новые. Таким образом, из-за невозможно-
сти отследить историю изменения атрибутов первый тип нельзя при-
числить к разряду медленно меняющихся измерений;
 тип 2. Второй тип – полноценное медленно меняющееся измерение.
Информация в этом случае хранится многократно – для каждой от-
дельной версии. Например, если у покупателя сменился адрес, в из-
мерении появится две строки для него  – одна со старым адресом,
а вторая – с новым. В то же время в таблице фактов в строках, относя-
щихся к этому покупателю, будет содержаться ссылка на нужную его
версию в измерении. Если, допустим, сделать срез по имени покупа-
теля, вы увидите только одну строку. В то же время если задать фильтр
по стране, цифры распределятся по странам в зависимости от места
проживания покупателя на момент совершения покупок.

Примечание. Первый тип медленно меняющихся измерений пре-


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

В качестве примера давайте разберем ситуацию с передачей покупателя


от одного менеджера другому, о которой мы говорили ранее, и посмотрим,
как подобная информация хранится в базе данных Contoso. У нас есть при-
вязка менеджеров к странам, за которые они отвечают. Один менеджер мо-
жет вес­ти сразу несколько регионов, и данные о привязках хранятся в таб­
лице CountryManagers в виде соответствия двух столбцов – CountryRegion
и Manager, как показано на рис. 5.1.

Рис. 5.1. В таблице CountryManagers содержатся соответствия между менеджерами


и странами

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


Мы можем объединить связью таблицы Customer и CountryManagers по об-
щему полю CountryRegion. В результате получим модель, представленную
на рис. 5.2.

Рис. 5.2. Можно объединить связью таблицы Customer и CountryManagers

После этого можно построить отчет с  выводом на строки менеджеров


и континентов, как показано на рис. 5.3.
130    Отслеживание исторических атрибутов

Рис. 5.3. В отчете показаны продажи с разбивкой по менеджерам и континентам

Менеджеры, ответственные за продажи в той или иной стране, меняются


с течением времени, но в нашей модели данных эта информация обраба-
тывается некорректно. К примеру, Луиза была ответственна за США (United
States) в 2007 году, в 2008 году ее сменил Пол, а еще годом позже США попа-
ли в зону ответственности Марка (Mark). Однако в представленном отчете
продажи по США за все годы приписаны Марку, поскольку он был послед-
ним, в чьем ведении была эта страна.
Предположим, что мы ввели в  таблице CountryManagers учет соответ-
ствий менеджеров и стран по годам, как показано на рис. 5.4.

Рис. 5.4. Смена менеджеров по США с течением лет


Введение в медленно меняющиеся измерения    131

В каждой строке теперь указаны годы начала и  окончания зоны ответ-


ственности конкретного менеджера в  отношении страны. С  учетом этих
изменений мы больше не  сможем объединить эту таблицу с  измерением
покупателей по полю CountryRegion, поскольку оно теперь не может высту-
пать в качестве ключа. Для каждого региона в обновленной таблице может
быть сразу несколько строк с менеджерами и их годами ответственности.
Сценарий вдруг значительно усложнился, но есть множество способов
справиться с этим. В этой главе мы покажем вам некоторые из них, что по-
зволит вам строить аналитические отчеты, учитывающие историческую
привязку менеджеров к странам. Представьте, что модель данных уже была
создана отделом информационных технологий и  передана вам для обра-
ботки. В правильно спроектированной модели вы должны увидеть в изме-
рении Customer следующие два столбца:
 Historical Manager. Содержит менеджера, который был ответствен-
ным за этого покупателя в момент совершения продажи;
 Current Manager. Указывает на менеджера, прикрепленного к этому
покупателю в данный момент, вне зависимости от того, кто был его
менеджером на момент продажи.
С подобной структурой данных вы можете строить отчеты, подобные
тому, что показан на рис.  5.5. Здесь мы выводим не текущего менеджера
покупателя, а того, кто сопровождал его в момент покупки.

Рис. 5.5. Отчет со срезом по менеджерам на момент покупки корректно отнес Северную


Америку (North America) за 2007 год к Луизе

Более того, вы можете выводить в  отчетах одновременно текущего


и исторического менеджера для покупателя, как показано на рис. 5.6. Здесь
показаны данные по продажам в Северной Америке (США и Канаде) с теку-
щими и историческими менеджерами.
132    Отслеживание исторических атрибутов

Рис. 5.6. Сочетая актуальные и исторические значения атрибутов измерений, вы можете


строить очень подробные отчеты

Совет. Использовать медленно меняющиеся измерения в отчетах


непросто. Мы советуем вам внимательно проанализировать циф-
ры в предыдущем отчете, чтобы лучше понять, какие данные ис-
пользуются в привязке к текущим и историческим атрибутам.

Вы имеете возможность осуществлять срезы как по текущему менеджеру,


так и по тому, кто был ответственным за покупателя на момент продажи.
И цифры вполне ожидаемо будут разными. Например, легко заметить серь­
езное падение продаж в  стране, за которую ныне отвечает Рауль (Raoul).
В 2007 году, когда ей занималась Луиза, показатели Северной Америки были
куда выше.
Срез по текущему менеджеру можно использовать для анализа потенциа-
ла покупателя в ведении разных менеджеров. В то же время фильтр по исто-
рическому атрибуту позволит оценить эффективность работы конкретного
менеджера на протяжении лет. В нашем отчете мы совместили два среза для
анализа продаж по каждому менеджеру.
Используя текущие и  исторические значения атрибутов, можно стро-
ить очень мощные отчеты. Однако визуально они могут восприниматься
не так просто. Для облегчения восприя­тия необходимо тщательно выбирать
столбцы для включения в отчет и использовать форматирование значений.
Также полезно включать в отчет понятное описание столбцов.
На вводных страницах главы мы обсудили следующие наиболее важные
особенности медленно меняющихся измерений:
 большую важность имеют как текущие, так и исторические значения
атрибутов. Вы будете использовать те и другие в зависимости от того,
какие данные необходимо будет показать в  отчете. В  хорошей реа-
лизации медленно меняющегося измерения должны храниться все
текущие и исторические значения атрибутов для каждой записи;
 несмотря на термин медленно меняющееся измерение, само изме-
рение не подвергается изменениям. Вместо этого меняется один или
больше атрибутов.
Теперь, когда вы осознали всю важность умелого обращения с историче-
скими атрибутами и  сложность их отображения в  отчетах, пришло время
Использование медленно меняющихся измерений    133

поработать с построением моделей данных с применением медленно ме-


няющихся измерений.

Использование медленно меняющихся измерений


В предыдущем разделе мы показали вам, что такое медленно меняющие­
ся измерения, а теперь обсудим некоторые особенности их использова-
ния на практике. Наличие таких измерений в модели данных автоматиче-
ски усложняет многие расчеты. В обычных измерениях каждая сущность
хранится на отдельной строке. Например, покупатель всегда содержится
только в одной строке соответствующей таблицы Customer. Но если рас-
сматривать это измерение как медленно меняющееся, то каждому по-
купателю будет соответствовать сразу несколько строк в  таблице в  за-
висимости от количества версий. Таким образом, обычная связь «один
к одному» между клиентом и отдельной строкой больше не просматрива-
ется. А  значит, некогда простые вычисления вроде подсчета количества
покупателей станут сложнее.
В примере, показанном выше, мы решили хранить менеджера в  виде
атрибута в  измерении покупателей. В  результате мы получим несколько
версий каждого покупателя в  зависимости от того, сколько менеджеров
у него сменилось за все время. В нашей базе данных насчитывается ровно
18 869 покупателей. Однако в таблице Customer содержится 43 882 строки.
И если мы создадим простую меру для подсчета количества покупателей,
как в приведенном ниже коде, результат будет неверным:

NumOfCustomers = COUNTROWS ( Customer )

На рис. 5.7 показан вывод этой меры в разрезе менеджеров.

Рис. 5.7. Такой подсчет количества покупателей будет неверным, если речь идет о мед-
ленно меняющемся измерении

В отчете отображается не количество покупателей, а число их версий, что


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

NumOfCustomers := DISTINCTCOUNT ( Customer[Customer Code] )


134    Отслеживание исторических атрибутов

Использование функции DISTINCTCOUNT позволило нам рассчитать


правильное количество покупателей в базе, что показано на рис. 5.8.

Рис. 5.8. Использование функции DISTINCTCOUNT ведет к подсчету уникальных записей


в таблице

Если вам нужно осуществить срез по одному атрибуту таб­лицы, замена


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

NumOfBuyingCustomers := DISTINCTCOUNT ( Sales[CustomerKey] )

В модели с медленно меняющимся измерением покупателей такая мера


покажет похожие на правду результаты, но все же они будут неверными.
Вывод отчета показан на рис. 5.9.

Рис. 5.9. Количество покупателей по каждой категории


товаров кажется похожим на правду, но это не так
Использование медленно меняющихся измерений    135

Агрегируя уникальные ключи CustomerKey, вы получаете лишь количество


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

NumOfBuyingCustomersCorrect :=
CALCULATE (
DISTINCTCOUNT ( Customer[Customer Code] );
Sales
)

На рис. 5.10 показан тот же отчет, что и раньше, но с выводом новой меры.


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

Рис. 5.10. Вывод двух мер показывает, насколько мала разница между правильными
и неправильными результатами

Вы, наверное, заметили, что мы использовали шаблон двунаправленной


фильтрации с таблицей Sales в качестве фильтра, вместо того чтобы явно стро-
ить двунаправленную связь между таблицами Sales и Customer, как мы часто
делали в этой книге. Дело в том, что в этом случае итоговые значения были бы
подсчитаны неверно. Смотрите, если вы напишете формулу так, как показано
ниже, то увидите в отчете (представленном на рис. 5.11), что в итоговых зна-
чениях учтены все покупатели, а не только те, которые что-то приобретали:

NumOfBuyingCustomersCorrectCrossFilter :=
CALCULATE (
DISTINCTCOUNT ( Customer[Customer Code] );
CROSSFILTER ( Sales[CustomerKey]; Customer[CustomerKey]; BOTH )
)
136    Отслеживание исторических атрибутов

Рис. 5.11. С включением в формуле двунаправленной фильтрации итоговые строки


показывают неправильные цифры

Причина таких различий в том, что таблица Sales не проходит фильтра-


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

Загрузка медленно меняющихся измерений


В этом разделе мы обсудим загрузку медленно меняющихся измерений
в модель данных при помощи редактора запросов Power BI Desktop. В изна-
чальной модели данных такие измерения могут не присутствовать вовсе, но
зачастую вам будет требоваться воссоздать их в отдельной модели, с кото-
рой вы работаете. Например, в демонстрационной базе данных, с которой
Загрузка медленно меняющихся измерений    137

мы работаем в этой главе, медленно меняющихся измерений нет. Но нам


необходимо будет создать его для отслеживания изменения привязки к ме-
неджерам – информации, отсутствующей в исходном хранилище данных.
Чтобы справиться с этой задачей, нужно освежить в памяти тему грану-
лярности, с которой мы познакомились в первой главе. В присутствии мед-
ленно меняющегося измерения гранулярность изменится как у  таблицы
фактов, так и у самого измерения.
Без необходимости хранить историю смены менеджеров гранулярность
таблицы фактов в нашей демонстрационной базе данных была установлена
на уровне покупателя. Но с введением медленно меняющегося измерения
уровень гранулярности повысится до версии покупателя. И именно версии
покупателей должны быть связаны с продажами в зависимости от того, ког-
да именно была совершена операция.
Изменение уровня гранулярности потребует от нас определенных дей-
ствий при построении правильной модели данных. Необходимо будет
также изменить запросы на формирование измерения и  таблицы фак-
тов, чтобы их гранулярности совпадали. Нельзя обновить уровень гра-
нулярности только одной таб­лицы – в этом случае связь будет работать
некорректно.
Давайте начнем с  анализа сценария. В  нашей базе данных есть табли-
ца Customer, не  являющаяся медленно меняющимся измерением. Также
присутствует таблица CountryManagers с  распределением менеджеров по
странам с датами начала и окончания зоны ответственности конкретного
менеджера. С годами менеджеры, отвечающие за конкретный регион, мо-
гут меняться. Но эти изменения не обязательно должны проходить каждый
год – именно поэтому мы не  хотим выставлять гранулярность на уровне
покупатель/год, чтобы не  возникали дубли в  таблице. В  нашем сценарии
идеальный уровень гранулярности лежит где-то между покупателем (чего
было бы недостаточно для отслеживания смены менеджеров) и связкой по-
купатель/год (что избыточно в случае, когда менеджер у региона не меня-
ется два года подряд). Гранулярность должна зависеть от того, сколько раз
у конкретной страны сменился менеджер.
Давайте попробуем найти правильный уровень гранулярности. Для
этого сначала нужно выставить гранулярность на уровне хуже оптималь-
ного. После этого мы выясним, какой уровень можно считать правиль-
ным. На рис. 5.12 показана изначальная таблица с менеджерами по стра-
нам и регионам.
Для поиска оптимальной гранулярности необходимо упрос­тить приве-
денную таблицу, сократив два поля с датами до одного. Это приведет к уве-
личению количества строк в таблице, поскольку один и тот же менеджер
будет повторяться несколько раз для каждого года работы (скоро мы пого-
ворим про удаление образовавшихся дублей).
138    Отслеживание исторических атрибутов

Рис. 5.12. В таблице CountryManagers содержатся столбцы FromYear и ToYear, ограничи-


вающие привязку менеджеров к странам

Для начала давайте добавим в таблицу столбец Year, который будет содер-
жать список лет из интервала между FromYear и ToYear, воспользовавшись
функцией List.Numbers, как показано на рис. 5.13.

Рис. 5.13. В столбце Year содержится список лет между FromYear и ToYear

На рис. 5.13 показан как сам столбец в таблице с указанием значения List


(список), так и его содержимое, которое можно увидеть в редакторе запро-
сов, щелкнув на ячейке. Вы видите, например, что Пол отвечал за Велико-
британию (United Kingdom) с 2007 по 2010 год, так что список по нему со-
держит перечисление из трех лет: 2007, 2008 и 2009.
Теперь мы можем расширить нашу таблицу, добавив по отдельной стро-
ке для каждого года из списка. Попутно можно избавиться от столбцов
FromYear и ToYear за ненадобностью. Получившаяся таблица представлена
на рис. 5.14.
Загрузка медленно меняющихся измерений    139

Рис. 5.14. В этой таблице Великобритания встречается три раза с одним


и тем же менеджером

В данный момент в нашей таблице установлен неблагоприятный уровень


гранулярности для страны или региона – с одной версией для каждого года.
Иными словами, по одному менеджеру и  одной стране есть одинаковые
строки, отличающиеся только годом. Но эта таблица нам очень нужна, по-
скольку мы будем использовать ее в качестве таблицы соответствий при из-
менении гранулярности таблицы фактов. Поскольку в таблице содержится
информация о менеджерах, которые были ответственны за страны в разные
годы, сохраним ее под именем Historical Country Managers.
Также нам понадобится таблица с  текущими менеджерами для стран
и регионов. Ее довольно легко построить на основании таблицы Historical
Country Managers. Для этого необходимо просто сгруппировать ее по столб-
цам CountryRegion и Manager, что даст нам уникальные строки по сочета-
нию этих полей. К столбцу Year во время группировки применим агрегиру-
ющую функцию MAX для получения последнего года для каждого сочетания
менеджера и страны. Как видно по рис. 5.15, Великобритания теперь пред-
ставлена в таблице единственной строкой.

Рис. 5.15. После группировки количество элементов в множестве стало оптимальным

Теперь в нашей таблице содержатся уникальные пары значений в полях


CountryRegion и Manager с последним годом привязки менеджера к стране.
140    Отслеживание исторических атрибутов

Для получения списка текущих менеджеров достаточно оставить только те


записи, в  которых значение в  поле LastYear соответствует текущему году.
В нашей модели текущим годом является 2009-й – это последний год в ис-
пользуемой нами базе данных. На рис. 5.16 показана итоговая таблица с те-
кущими менеджерами, которую мы назвали Actual Country Managers.

Рис. 5.16. В таблице Actual Country Managers содержатся только текущие менеджеры по


странам и регионам

На данный момент у нас есть две таблицы:


 Actual Country Managers  – содержит список текущих менеджеров
для каждой страны;
 Historical Country Managers – содержит историю изменений менед-
жеров по странам.
На следующем шаге мы используем эти две таблицы для обновления таб­
лиц Customer и Sales.

Исправление гранулярности в измерении


Таблицы, созданные на предыдущем шаге, мы используем для установки
правильной гранулярности в таблицах Customer и Sales. Сначала займемся
таблицей покупателей. Чтобы увеличить гранулярность измерения, необхо-
димо объединить (Merge) его с таблицей Historical Country Managers. В таб­
лице Customer есть столбец CountryRegion, и если объединить ее с Historical
Country Managers по этому полю, результирующий набор будет содержать
больше строк – по одной для каждого менеджера у  покупателя. При этом
в нем не будет представлена каждая версия покупателя для каждого года,
что было бы признаком неблагоприятного уровня гранулярности. Вместо
этого операция группировки, проведенная с  таблицей Historical Country
Managers, позволила оставить для каждого покупателя необходимое коли-
чество версий.
После выполнения этих двух действий набор данных, отсор­тированный по
полю OriginalCustomerKey, должен приобрести вид, показанный на рис. 5.17.
Загрузка медленно меняющихся измерений    141

Рис. 5.17. Обновленная таблица Customer с измененной гранулярностью и денормали-


зованными текущим и историческими менеджерами

Посмотрите внимательно на первые три строки таблицы. В них представ-


лен Джон Янг (Jon Yang), покупатель из Австралии (Australia), у которого за
все время сменились три менеджера: Пол, Марк и Луиза. Эта информация
корректно отражена в  модели данных, но при этом здесь есть проблема.
Столбец OriginalCustomerKey, содержащий ключ покупателя, более не  мо-
жет являться первичным ключом таблицы. Фактически этот код соответ-
ствует покупателю, тогда как мы подняли таб­лицу на уровень представ-
ления версий покупателей. И  из-за потери своей уникальности это поле
не может быть ключом. Значит, нам нужен новый ключ.
Обычно добавить ключ в таблицу не представляет труда – достаточно соз-
дать новый столбец с индексом, значения которого начинаются с единицы
и  постепенно увеличиваются. Такой техники придерживаются все разра-
ботчики баз данных. В нашем случае гранулярность таблицы установлена на
пересечении покупателя и года, где год представляет собой именно послед-
ний год привязки менеджера к стране. Так что мы безопасно можем создать
новый столбец, в котором соединим значения из полей OriginalCustomerKey
и Year, представляющего год из таблицы Historical Country Managers, денор-
мализованный в обновленном измерении Customer. Получившаяся таблица
с новым ключевым полем показана на рис. 5.18.

Рис. 5.18. OriginalCustomerKey не является ключом. Лучше для этого подходит поле


CustomerKey
142    Отслеживание исторических атрибутов

На данном этапе мы подняли гранулярность в таблице Customer с уровня


покупателя до связки покупателя с годом. Это еще не окончательная табли-
ца, а лишь промежуточная. Сохраним ее под названием CustomerBase.
Впереди заключительный шаг к  правильному уровню гранулярности.
Мы  сделаем что-то похожее на то, что уже делали ранее с  менеджерами
для разных стран. Начнем с  удаления из таблицы CustomerBase всех по-
лей, кроме тех, что связаны с гранулярностью, и выполним группировку по
полям OriginalCustomerKey, Actual Manager и Historical Manager. К столбцу
CustomerKey применяем агрегирующую функцию MAX и  называем новое
поле NewCustomerKey. Результат можно видеть на рис. 5.19.

Рис. 5.19. Временная таблица находится на правильном уровне гранулярности

Операция группировки помогла установить правильную гранулярность


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

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

На заключительном шаге мы объединяем нашу таблицу с  таб­лицей


CustomerBase по ключевому полю и извлекаем все необходимые столбцы.
Результат показан на рис. 5.21, и легко заметить, что по каждому покупате-
лю в итоговой таблице содержится ровно столько строк, сколько раз у него
менялся менеджер за все время.
Загрузка медленно меняющихся измерений    143

Рис. 5.21. Итоговая таблица Customer с правильно установленной гранулярностью


и всеми нужными столбцами

Далее мы проведем похожие операции с  таблицей Sales. Заметьте, что


после изменения ключа в  измерении Customer поле CustomerKey больше
не может служить внешним ключом в таб­лице Sales.

Исправление гранулярности в таблице фактов


В таблице Sales мы не можем создать новый ключ, ссылаясь на год продажи.
По сути, если менеджер у страны или региона не менялся, то новый ключ
не  зависит от года продажи. Значит, нам придется поискать новый ключ.
Состав измерения Customer зависит от покупателя, текущего менеджера
и исторического менеджера. По этим трем полям мы можем осуществ­лять
поиск в таблице Customer, результатом которого будет ключ CustomerKey.
Чтобы исправить гранулярность в таблице фактов Sales, необходимо вы-
полнить следующие действия:
1. В исходной таблице Sales добавить столбец с годом продажи.
2. Выполнить объединение с  таблицей CustomerBase для получения
страны покупателя, а  также текущего и  исторического менеджера.
Мы используем таблицу CustomerBase, поскольку из нее можем полу-
чить год продажи. При этом в CustomerBase гранулярность установле-
на неправильно, но сейчас нам это будет на руку, поскольку поможет
осуществлять поиск в ней по году продажи.
Результат операции объединения хранится в новом столбце, как показа-
но на рис. 5.22.

Рис. 5.22. Нужно провести объединение таблиц Sales и CustomerBase для извлечения


текущих и исторических менеджеров
144    Отслеживание исторических атрибутов

После этого можно развернуть столбцы полученных менеджеров и  ис-


пользовать их для второго объединения с таблицей Customer с правильно
установленной гранулярностью. Далее выполняем поиск покупателя с теми
же кодом и менеджерами. Это позволит нам извлечь новый код покупателя
и тем самым решить проблему с гранулярностью в таблице фактов.
На рис. 5.23 показан фрагмент таблицы Sales после обработки. В первой
выделенной строке мы видим клиента, у которого менеджер изменился –
раньше был Марк, а теперь Луиза. Значит, у этого покупателя будут другие
версии, а эта строка (относящаяся к 2007 году, когда менеджером был Марк)
ссылается на версию покупателя 2007 года. У покупателя из второй выде-
ленной строки менеджер никогда не менялся, так что для него будет только
одна строка в таблице (помеченная 2009 годом). Кроме того, продажа – хотя
она и была в 2007 году – ссылается на версию покупателя 2009 года. В окон-
чательном варианте таб­лицы Sales столбец для поиска нам не понадобится,
он нужен был только в процессе обработки таблицы.

Рис. 5.23. В выделенных строках показана разница между покупателями, у которых


менялся менеджер, и теми, у кого менеджер оставался одним и тем же

Загрузку медленно меняющихся измерений нужно производить очень


внимательно. Перечислим вкратце шаги, которые нам пришлось выполнить.
1. Мы установили новую гранулярность для измерения, которая теперь
зависит от атрибутов, изменяющихся со временем.
2. Мы изменили само измерение, чтобы можно было оперировать с но-
вой гранулярностью. Это потребовало создания сложных запросов и,
что более важно, определения нового кода для покупателей для даль-
нейшего использования в связях.
3. Мы изменили таблицу фактов, чтобы она могла использовать новый
код. Поскольку новый код нельзя было прос­то вычислить, мы вынуж-
дены были осуществлять его поиск в новом измерении. При этом все
медленно меняющиеся атрибуты были задействованы для определе-
ния гранулярности.
Мы прошли через весь процесс описания работы с медленно меняющи-
мися измерениями в редакторе запросов Power BI Desktop (те же действия
Быстро меняющиеся измерения    145

вы можете выполнить и в Excel 2016). Мы хотели показать вам, насколько


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

Быстро меняющиеся измерения


Как мы уже отмечали и как понятно из названия, медленно меняющиеся из-
мерения изменяют свое состояние довольно редко, поэтому количество вер-
сий их элементов бывает невелико. Мы намеренно использовали в качестве
примера смену привязок менеджеров к покупателям, которая может проис-
ходить ежегодно. А по причине того, что меняющиеся атрибуты принадлежат
каждому из покупателей, общее число создаваемых версий в этом случае бу-
дет довольно большим. Более традиционным примером медленно меняюще-
гося измерения была бы смена адресов у покупателей, поскольку они будут
переезжать с места на место гораздо реже, чем раз в год. Но мы выбрали при-
мер с менеджерами, а не с адресами, потому что с помощью Excel или Power
BI Desktop построить модель данных для такого сценария не составляло труда.
Еще один ежегодно меняющийся атрибут, который вам может понадо-
биться отслеживать, – это возраст покупателя. Допус­тим, вам необходимо
проанализировать продажи по диапазону возрастов покупателей. Если
не  рассматривать эту связку как медленно меняющееся измерение, вы
не сможете хранить возраст покупателя в таблице с измерением. Покупа-
тели взрослеют, и вам нужно отслеживать не текущий их возраст, а возраст
на момент совершения покупки. Вы могли бы для хранения этого атрибута
воспользоваться шаблонами из предыдущего раздела. Но сейчас мы пока-
жем вам другой способ отслеживать изменение атрибутов и для этого вве-
дем понятие быстро меняющегося измерения (rapidly changing dimensions).
Предположим, в вашей модели данных хранится информация за десять
лет. Если использовать описанную выше методику обращения с медленно
меняющимся измерением, в  таблице Customer у  вас наберется по десять
версий каждого покупателя. А если меняющихся атрибутов будет больше,
то и количество версий возрастет до таких пределов, что управлять измере-
нием станет очень проблематично. При этом заметим, что само измере­ние
в целом у нас не меняется – изменения касаются лишь отдельных атрибу-
тов. И если значения атрибутов меняются достаточно часто, лучшим вари-
антом будет вынести их в отдельное измерение, тем самым удалив из та-
блицы покупателей.
146    Отслеживание исторических атрибутов

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


Customer, показана на рис. 5.24.

Рис. 5.24. Возраст хранится в качестве атрибута в измерении Customer

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


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

Sales[Historical Age] =
DATEDIFF (
RELATED ( Customer[Birth Date] );
RELATED ( 'Date'[Date] );
YEAR
)
Быстро меняющиеся измерения    147

В момент продажи будет вычисляться разница между днем рождения по-


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

Рис. 5.25. Атрибут с историческим возрастом покупателей хорошо подходит для постро-


ения графиков и гистограмм

Возраст как отдельное число может быть полезен для построе­ния подоб-
ных графиков. Но что, если вас заинтересует объединение возрастов в груп-
пы для проведения более глубокого анализа? В этом случае лучше будет вы-
нести хранение этого атрибута в отдельное измерение, а возраст в таблице
фактов использовать в качестве внешнего ключа. Измененная модель дан-
ных показана на рис. 5.26.
148    Отслеживание исторических атрибутов

Рис. 5.26. Можно преобразовать возраст покупателя во внешний ключ для связи с из-
мерением возрастов

В измерении Historical Age мы можем хранить диапазоны возрастов и дру-


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

Рис. 5.27. С отдельным измерением можно осуществлять срезы по  возрастным группам

Таким образом, можно оптимизировать модель данных, денормализовав


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

Выбор оптимальной техники моделирования


В этой главе мы рассмотрели два подхода к работе с меняющимися изме-
рениями. Классическим способом является построе­ние полноценного мед-
ленно меняющегося измерения с  характерным для этого подхода услож-
нением загрузки данных в модель. Более простым можно считать вариант
хранения меняющегося атрибута в таблице фактов, а при необходимости –
выделения его в обособленное измерение.
Последний способ значительно более прост в разработке, и иногда лучше
будет выбрать его, особенно если вы легко можете выделить конкретный
атрибут, значения которого меняются с  определенной периодичностью.
Однако при наличии нескольких таких атрибутов вам придется создавать
большое количест­во измерений, что усложнит модель данных. Как и всегда,
когда дело касается моделирования данных, вы должны тщательно все об-
думать, прежде чем делать выбор. К примеру, если вам необходимо отсле-
живать изменение нескольких исторических атрибутов покупателей – их
возраст, адрес (страну, штат и  континент), менеджера  и  др., – вы можете
создать для каждого из них отдельное измерение, и все они будут служить
одной цели. Если же вы решите пойти по пути создания полноценного
медленно меняющегося измерения, то сможете обойтись одной таб­лицей,
сколько бы атрибутов вам ни пришлось отслеживать.
Давайте вернемся к примеру, который мы рассматривали в этой главе, –
с  текущими и  историческими менеджерами. Если бы мы сосредоточили
свое внимание не на измерении в целом, а на конкретном атрибуте, то мог-
ли бы решить эту задачу при помощи модели, показанной на рис. 5.28.
Построить такую модель довольно просто. Для этого необходимо вычис-
лить текущего на момент продажи менеджера по стране покупателя и со-
хранять в таблице Sales вместе с продажей. Это можно сделать при помощи
пары объединений таблиц, зато не придется менять уровень гранулярности
измерения и таблицы фактов.
Что касается выбора оптимального подхода, здесь есть одно простое пра-
вило: если это возможно, старайтесь выделять меняющийся атрибут (или
набор атрибутов) в отдельное измерение. В этом случае вам не нужно будет
менять гранулярность таблиц. Однако если таких атрибутов слишком мно-
го, лучше пойти по более сложному пути создания полноценного медленно
меняющегося измерения.
150    Отслеживание исторических атрибутов

Рис. 5.28. Денормализация исторического менеджера в таблице фактов позволила зна-


чительно упростить модель данных

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

Снимком (snapshot) называется один из видов таблиц, час­то применяемых


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

Данные, которые нельзя агрегировать по времени


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

шие события, а информацию, актуальную на определенный момент време-


ни. Иными словами, под фактом мы подразумеваем ответ на вопрос о том,
сколько и каких товаров хранилось в ваших магазинах в конкретный день.
На  следующий месяц в  вашей таблице инвентаризации появится новый
факт с новыми данными. Это и есть снимок – мера того, что было доступно
в магазинах в определенный момент времени. С точки зрения функционала
перед нами таблица фактов в чистом виде, поскольку она связана с изме-
рениями, и вы, вероятно, захотите агрегировать информацию, хранящуюся
в ней. Так что различия между таблицей фактов и снимком заключаются,
скорее, в природе хранимой информации, а не в структуре.
Еще один пример снимка – таблица с курсами валют. Если вам необходи-
мо хранить такую информацию в модели, вы можете создать таблицу с да-
той, наименованием валюты и ее относительным значением в сравнении
с  базовой валютой  – скажем, американским долларом. Это полноценная
таблица фактов, ведь она связана с измерениями и содержит данные, при-
годные для агрегирования. Но никаких событий как таковых наша таблица
не хранит. Вместо этого в ней содержится информация, актуальная на кон-
кретные даты. В  главе 11 мы подробно рассмотрим сценарии для работы
с  курсами валют, а  сейчас вам достаточно знать, что таблица с  подобной
информацией является одной из разновидностей снимков.
Различают следующие разновидности снимков:
 естественный снимок (natural snapshot). Существуют наборы данных,
которые по своей природе образуют снимок. К ним относятся таблицы
фактов, хранящие, например, информацию о температуре воды в двига-
теле на ежедневной основе. Это естественный снимок. Иными словами,
факт – это сохраненная мера, а событие – осуществление измерения;
 производный снимок (derived snapsot). К  этой категории относятся
наборы данных, которые лишь выглядят как снимки, а считаются та-
ковыми только потому, что мы склонны воспринимать их как сним-
ки. Представьте таблицу, в которой на ежемесячной основе хранится
баланс расчетного счета. Мера внутри таблицы отражает состояние
счета на конец каждого месяца, но, по сути, баланс счета является
производной величиной от суммы всех произведенных транзакций
(доходов и расходов) со счетом за предыдущий период. Так что ин-
формация здесь лишь хранится как снимок, но также может быть рас-
считана путем простой агрегации соответствующих транзакций.
Эти различия очень важны. Как вы узнаете из данной главы, работа со
снимками обладает как преимуществами, так и  недостатками. И  необхо-
димо найти правильный баланс для наиболее оптимального представле-
ния данных. Иногда лучше хранить остатки, а иногда – транзакции. В слу-
чае с производ­ными снимками мы располагаем свободой выбора решения
и несем ответственность за свой выбор. Что касается естественных сним-
ков, здесь мы ограничены в выборе, поскольку такие данные по своей при-
роде представлены как снимок.
Агрегирование снимков    153

Агрегирование снимков
Давайте начнем рассмотрение снимков с  того, как правильно проводить
агрегирование хранимых в них данных. В качестве примера возьмем еже-
недельную инвентаризацию товаров в  магазинах. Полная модель данных
представлена на рис. 6.1.
Внешне модель выглядит как схема «звезда» с двумя таблицами фактов
(Sales и  Inventory). Обе таблицы объединены связями с  измерениями дат,
товаров и  магазинов. Серьезным различием между этими таблицами вы-
ступает то, что Inventory выступает снимком, тогда как Sales – традицион-
ная таблица фактов.

Примечание. Как вы узнаете далее в  этом разделе, вычисле-


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

Рис. 6.1. В таблице Inventory содержатся остатки товаров в магазинах по неделям


154    Использование снимков

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


сказали, в  виде снимка находятся данные о  ежедневной инвентаризации
товаров в магазинах. Для начала создадим меру On Hand (в наличии), в ко-
торой проведем агрегацию по полю OnHandQuantity:

On Hand := SUM ( Inventory[OnHandQuantity] )

Используя эту меру, мы можем построить отчет-матрицу в  Power BI


Desktop для анализа остатков по конкретным товарам. На рис. 6.2 показан
отчет по одному виду наушников по магазинам в Германии.

Рис. 6.2. Отчет демонстрирует наличие товаров в магазинах в Германии

Взглянув на итоговые цифры в  отчете, вы сразу заметите проблему.


Итоговые остатки по магазинам не  соответствуют действительности.
К примеру, в магазине в Гибельштадте (Giebelstadt) оставалось 18 единиц
товара в ноябре 2007 года и 0 – в декабре. В то же время итоговые цифры
по 2007 году показывают значение 56. Вполне очевидно, что это не так.
Здесь должен быть ноль, поскольку после продаж ноября эти наушники
в магазин не завозили. Поскольку остатки у нас хранятся по неделям, раз-
вернув уровень месяцев, мы обнаружим, что даже здесь есть ошибки. Это
можно видеть на рис. 6.3, где суммарный остаток товаров за месяц скла-
дывается из остатков по неделям.
Работая со снимками, необходимо помнить, что в них не должны содер-
жаться аддитивные меры. Аддитивная мера (additive measure) представ-
ляет собой меру, которая может агрегироваться с применением функции
SUM по всем измерениям. В нашем случае мы можем пользоваться функ-
цией суммирования для агрегации остатков по магазинам, но не должны
этого делать по измерению времени. Снимки хранят информацию, акту-
Агрегирование снимков    155

альную на конкретный момент времени. Но  при вычислении итоговых


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

Рис. 6.3. Остаток по месяцам формируется путем


сложения недельных остатков, что неверно

Это типичный сценарий для использования полуаддитивной меры (semi-


additive measure), показывающей последние доступные данные по времени.
Если посмотреть на апрель 2007 года, то последней датой в таблице явля-
ется 28-е число. Есть сразу несколько способов провести необходимые нам
вычисления при помощи DAX. Рассмотрим их.
Традиционно в полуаддитивной мере применяется функция LASTDATE,
извлекающая последнюю дату в заданном интервале. Но она будет беспо-
лезна в нашем случае, поскольку для апреля вернет 30-е число, а не 28-е,
а на эту дату информации в таблице нет. И если для меры On Hand исполь-
зовать приведенную ниже формулу, итоги по месяцам просто очистятся:

On Hand :=
CALCULATE (
SUM ( Inventory[OnHandQuantity] );
LASTDATE ( 'Date'[Date] )
)

На рис. 6.4 можно видеть, что месячные итоги не содержат данных.


156    Использование снимков

Рис. 6.4. При использовании функции LASTDATE итоги просто пропадают

Нам необходимо использовать последнюю дату, на которую есть инфор-


мация в таблице, и она не всегда будет совпадать с последней датой меся-
ца. В нашем случае в таблице Inventory есть поле с датой DateKey, которое
можно попробовать использовать в формуле. Таким образом, вместо того
чтобы применять функцию LASTDATE к таблице Date, в которой хранятся
все даты, мы применим ее к таблице Inventory, где содержатся только даты
хранения остатков. Мы  уже видели подобные формулы в  других моделях
данных. К сожалению, и в этот раз результаты оказались ошибочными. При-
чина в том, что мы нарушили одно из главных правил DAX, заключающееся
в том, чтобы применять фильтры к измерениям, а не к таблице фактов, и к
тем полям, которые используются при построении связей. Давайте посмот­
рим на код ниже и проанализируем полученные результаты, представлен-
ные на рис. 6.5:

On Hand :=
CALCULATE (
SUM ( Inventory[OnHandQuantity] );
LASTDATE ( Inventory[DateKey] )
)

Посмотрите на итоги по апрелю. В  Гибельштадте и  Мюнхене (Munich)


итоговые значения берутся по 21 апреля, а  в Бамберге (Bamberg)  – по
28 апреля. Итоговое же значение по всем трем городам составляет 6, что
соответствует суммарному остатку по трем магазинам на 28 апреля. Что
же произошло?
Агрегирование снимков    157

Рис. 6.5. Применение функции LASTDATE к столбцу с датами в таблице Inventory тоже


не помогло

Вместо того чтобы подсчитать итоги, основываясь на последних датах, по


которым были остатки по Мюнхену и Гибельштадту (21 апреля) и Бамбергу
(28 апреля), формула взяла в расчет только 28 апреля, поскольку это послед-
няя дата с остатками по магазинам. Иными словами, вычисленное значе-
ние (6 штук) – это не общий итог, а частичный итог на уровне магазинов.
Фактически, поскольку на 28 апреля остатков по Мюнхену и Гибельштадту
не было, месячные итоги по ним должны быть нулевые, а не содержать по-
следний доступный остаток. Таким образом, в правильной формуле для об-
щих итогов должен осуществляться поиск последней даты, на которую были
остатки как минимум по одному магазину. Традиционное решение такого
сценария показано ниже:

On Hand := CALCULATE (
SUM ( Inventory[OnHandQuantity] );
CALCULATETABLE (
LASTNONBLANK ( 'Date'[Date]; NOT ( ISEMPTY ( Inventory ) ) );
ALL ( Store )
)
)

Или в нашем конкретном случае:

On Hand := CALCULATE (
SUM ( Inventory[OnHandQuantity] );
LASTDATE (
CALCULATETABLE (
VALUES ( Inventory[Date] );
ALL ( Store )
)
)
)
158    Использование снимков

Обе формулы работают правильно. Вы  вольны выбирать, какой из них


пользоваться, в  зависимости от распределения данных и  некоторых осо-
бенностей модели, о чем мы здесь распространяться не будем. Главное, что
теперь итоговые значения правильные, как видно по рис. 6.6.

Рис. 6.6. Последняя формула позволила вычислить правильные итоги по магазинам

Несмотря на правильные расчеты, у представленных выше формул есть один


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

Date[RowsInInventory] := CALCULATE ( NOT ISEMPTY ( Inventory ) )

Столбец имеет логический тип данных и может содержать одно из двух


значений: TRUE или FALSE. Плюсом является то, что эта информация хра-
нится в календаре, который содержит не так много строк. Даже если в мо-
дели будет присутствовать информация за десять лет, количество строк
в  измерении Date будет составлять всего 3650. А  сканировать маленькую
таб­лицу дат гораздо быстрее большой таблицы фактов, в  которой могут
быть миллионы строк. После добавления вычисляемого столбца можно пе-
реписать формулу следующим образом:

On Hand := CALCULATE (
SUM ( Inventory[OnHandQuantity] );
LASTDATE (
CALCULATETABLE (
VALUES ( 'Date'[Date] );
'Date'[RowsInInventory] = TRUE
)
)
)
Понятие производных снимков    159

Пусть код выглядит более сложным, но работать он будет гораздо бы-


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

Понятие производных снимков


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

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

Рис. 6.7. В таблице хранится информация о новых и вернувшихся покупателях в виде


снимка

Эта предварительно агрегированная таблица может быть добавлена


в модель данных и объединена связью с измерением Date. Это позволит вам
строить по ней отчеты. На рис. 6.8 показана получившаяся модель.

Рис. 6.8. NewCustomers – новая таблица, добавленная в модель посредством связи


Понятие производных снимков    161

В снимке хранится всего по одной записи для каждого месяца – в сумме


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

Рис. 6.9. Простой ежемесячный отчет на основании снимка

С точки зрения производительности этот отчет просто великолепен, по-


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

Понятие матрицы переходов


Матрица переходов (transition matrix) является очень полезной техникой
моделирования, позволяющей при помощи снимков создавать очень мощ-
ные аналитические модели. Это не самая простая техника, но нам кажется,
что вы должны познать хотя бы базовые концепции матрицы переходов.
Она может стать очень важным инструментом в вашем арсенале специали-
ста по моделированию данных.
Предположим, вы решили ранжировать своих покупателей на основании
суммы покупок в  месяц. Вы  вводите три статуса – низкий (low), средний
(medium) и высокий (high) – и создае­те для хранения границ статусов кон-
фигурационную таблицу Customer Rankings, показанную на рис. 6.10.

Рис. 6.10. Конфигурационная таблица Customer Rankings для хранения статусов по-


купателей

На основании этой информации вы можете создать вычисляемую табли-


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

CustomerRanked =
SELECTCOLUMNS (
ADDCOLUMNS (
SUMMARIZE ( Sales; 'Date'[Calendar Year]; 'Date'[Month];
Sales[CustomerKey] );
"Sales"; [Sales Amount];
"Rating"; CALCULATE (
VALUES ( 'Ranking Configuration'[Rating] );
FILTER (
'Ranking Configuration';
AND (
'Ranking Configuration'[MinSale] <
[Sales Amount];
'Ranking Configuration'[MaxSale] >=
[Sales Amount]
)
)
);
"DateKey"; CALCULATE ( MAX ( 'Date'[DateKey] ) )
);
"CustomerKey"; [CustomerKey];
"DateKey"; [DateKey];
"Sales"; [Sales];
"Rating"; [Rating]
)
Понятие матрицы переходов    163

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


Сначала мы получаем список месяцев, лет и кодов покупателей. Затем, ос-
новываясь на конфигурационной таб­лице, присваиваем покупателям ме-
сячные статусы. Итоговая таблица CustomerRanked показана на рис. 6.11.

Рис. 6.11. В снимке хранятся статусы покупателей по месяцам

В зависимости от активности у  одного и  того же покупателя могут быть


разные статусы по месяцам. Более того, могут быть месяцы без статуса вовсе,
если он не совершил ни одной покупки. Добавив таблицу в нашу модель дан-
ных и создав соответствующие связи, получим схему, показанную на рис. 6.12.
Если вы думаете, что только что мы создали производный снимок, вы правы.
CustomerRanked как раз и есть производный снимок с предварительными рас-
четами на основании таблицы Sales, в которой хранятся действительные факты.

Рис. 6.12. Как и всегда, наш снимок в модели данных выглядит как обычная таблица фактов
164    Использование снимков

Можно использовать эту таблицу для построения простых отчетов с ко-


личеством покупателей с тем или иным статусом по месяцам и годам. Сто-
ит отметить, что данные из снимка необходимо агрегировать при помощи
функций, работающих с уникальными значениями, чтобы в итоговых стро-
ках каждый покупатель появлялся только один раз. При помощи формулы,
приведенной ниже, создадим меру для формирования отчета, представлен-
ного на рис. 6.13:
NumOfRankedCustomers :=
CALCULATE (
DISTINCTCOUNT ( CustomerRanked[CustomerKey] )
)

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


статусом довольно просто

По сути, мы создали снимок, очень похожий на пример из предыдущего


раздела, в конце которого было решено, что использование снимка в том
сценарии нецелесообразно. В  чем же разница? Вот два важных отличия
между этими сценариями:
 статусы присваиваются на основании того, сколько покупок сделал
клиент – вне зависимости от того, какие товары он приобретал. А по-
скольку статусы не зависят от внешнего выбора, есть смысл сделать их
предварительное агрегирование и хранить без изменений;
 дальнейшие срезы по конкретному дню, например, не несут в себе ни-
какой пользы, поскольку статусы присваиваются по месяцам, и имен-
но месяц лежит в основе концепции проведенного ранжирования.
Этих доводов достаточно, чтобы оправдать создание снимка в этом слу-
чае. Но есть и более весомая причина, чтобы это сделать. Можно трансфор-
мировать этот снимок в матрицу переходов и тем самым создать почву для
проведения более усовершенствованного анализа.
Матрица переходов в  нашем случае может помочь ответить на вопрос
о  том, как, к  примеру, изменился статус покупателей, которые в  январе
2007 года обладали средним статусом. Внешний вид требуемого отчета по-
Понятие матрицы переходов    165

казан на рис. 6.14. В нем учтены только покупатели, у которых в январе был


средний статус, и показано, как их статус менялся с течением времени.

Рис. 6.14. Матрица переходов помогает проводить очень мощный анализ по покупате-


лям

На рис.  6.14 видно, что в  январе 2007 года средним статусом обладали
40 покупателей. В апреле один из них получил высокий статус, в мае у од-
ного клиента из этой группы статус понизился, а  в ноябре также был за-
фиксирован один высокий статус. В июне 2009 года четырем покупателям
из этого перечня был присвоен низкий статус. Как видите, мы установили
фильтр на конкретный статус в  заданном месяце, на основании которого
был выбран набор покупателей. В дальнейшем был проведен анализ того,
как у этой группы людей со временем изменялся статус.
Чтобы построить матрицу переходов, необходимо выполнить следующие
действия:
1) определить список покупателей с интересующим нас статусом в опре-
деленном месяце;
2) проверить их статусы в будущих периодах.
Начнем с  первого пункта. Мы  хотим выделить группу покупателей
с определенным статусом в конкретном месяце. Поскольку мы собираем-
ся использовать срезы для фиксации даты и статуса в снимке, необходимо
создать вспомогательную таб­лицу для дальнейшего использования ее в ка-
честве фильтра. Этот момент очень важно понять. Подумайте о том, что зна-
чит наложить фильтр на снимок по конкретному месяцу. Если использовать
для этого измерение Date, это будет означать, что мы дважды будем обра-
щаться к этой таблице – сначала для фильтрации данных, а затем – для вы-
вода информации по будущим периодам. Иными словами, если установить
фильтр на январь 2007 года в нашем измерении дат, то его действие распро-
странится на всю модель данных, что сделает невозможным (или серьезно
затруднит) построение требуемого отчета из-за недоступности сведений об
изменении статусов, к примеру, к февралю.
166    Использование снимков

А поскольку использовать измерение Date для установки фильтров нель-


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

SnapshotParameters =
SELECTCOLUMNS (
ADDCOLUMNS (
SUMMARIZE (
CustomerRanked;
'Date'[Calendar Year];
'Date'[Month];
CustomerRanked[Rating]
);
"DateKey"; CALCULATE ( MAX ( 'Date'[DateKey] ) )
),
"DateKey"; [DateKey];
"Year Month"; FORMAT (
CALCULATE ( MAX ( 'Date'[Date] ) );
"mmmm YYYY"
);
"Rating"; [Rating]
)

На рис. 6.15 показан внешний вид созданнойтаблицы (SnapshotParameters).


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

Рис. 6.15. В таблице SnapshotParameters представлены три столбца

Таблица SnapshotParameters не должна быть объединена связями с дру-


гими таблицами в  модели. Это просто вспомогательная таблица, которая
нужна для наполнения среза данными по месяцам и статусам. Сама модель
Понятие матрицы переходов    167

данных уже готова. Можно выбирать статус и  заполнять матрицу инфор-


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

Transition Matrix =
CALCULATE (
DISTINCTCOUNT ( CustomerRanked[CustomerKey] );
CALCULATETABLE(
VALUES ( CustomerRanked[CustomerKey] );
INTERSECT (
ALL ( CustomerRanked[Rating] );
VALUES ( SnapshotParameters[Rating] )
);
INTERSECT (
ALL ( CustomerRanked[DateKey] );
VALUES ( SnapshotParameters[DateKey] )
);
ALL ( CustomerRanked[Rating] );
ALL ( 'Date' )
)
)

Этот код понять не так просто, но мы советуем вам уделить ему некоторое
время и попытаться разобраться. Представленный фрагмент из нескольких
строк обладает огромной мощью, и вы сможете почерпнуть из него много
полезного, когда поймете, что он делает.
Основа этого кода заключается в  вызове функции CALCULATETABLE
с двумя встроенными вызовами INTERSECT. Функции INTERSECT исполь-
зуются для применения текущего выбора из SnapshotParameters (табли-
цы в  основе нашего среза) в  качестве фильтра для CustomerRanked. Один
вызов для даты, второй – для статуса. После установки фильтра функция
CALCULATETABLE вернет набор ключей покупателей, у которых в выбран-
ный месяц был указанный в  срезе статус. Внешняя функция CALCULATE,
в свою очередь, рассчитает количество покупателей со статусами в разные
периоды, при этом ограничив выбор лишь теми клиентами, которые были
отфильтрованы на предыдущем шаге функцией CALCULATETABLE. Итого-
вую таблицу мы уже представляли на рис. 6.14.
С точки зрения моделирования данных довольно интересен тот факт, что
для выполнения подобного вида анализа нам понадобилось использовать
снимок. Фактически он выступал здесь только в роли фильтра для выбора
покупателей.
Что еще интересного мы узнали о снимках из этого раздела:
 снимки полезно использовать, когда необходимо «заморозить» вы-
числения. В  этом примере нам нужно было получить перечень по-
купателей с определенным статусом на конкретный месяц. И снимок
легко позволил это сделать;
168    Использование снимков

 если вам необходимо наложить фильтр по дате на снимок, но вы


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

Заключение
Снимки являются полезным инструментом для уменьшения объема таблиц
за счет снижения уровня гранулярности. Предварительное агрегирование
данных позволяет значительно увеличить скорость расчета формул. Кро-
ме того, как мы видели в примере с матрицей переходов, снимки данных
существенно расширяют грани доступной аналитики. Вместе с тем исполь-
зование снимков усложняет саму модель данных. Также из этой главы мы
узнали следующие важные факты о снимках:
 снимки почти всегда требуют агрегации, отличной от привычной
функции SUM. Вам необходимо тщательно продумывать, как будет
проводиться агрегация и будет ли проводиться вообще;
 гранулярность снимков всегда отличается от гранулярности обычных
таблиц фактов. Стоит принимать это во внимание при построении
отчетов, поскольку за скорость их формирования всегда приходится
чем-то платить;
 если ваша модель данных не очень большая, можно избежать созда-
ния в ней производных снимков. Используйте их только как крайнюю
меру – если оптимизация кода DAX не дает ожидаемых результатов;
 как вы видели в примере с матрицей переходов, использование сним-
ков может позволить проводить более глубокий анализ. Есть и другие
аналитические возможности, которые вы сможете для себя открыть
в зависимости от области применения анализа.
Использовать снимки непросто. В этой главе мы рассмотрели как простые,
так и более сложные сценарии. Мы советуем вам досконально разобраться
в легких примерах, внимательно изучить примеры посложнее и двигаться
дальше, используя при необходимости снимки и матрицы переходов. Даже
опытным специалистам в области моделирования данных могут показать-
ся достаточно сложными приведенные здесь примеры. И все же при необхо-
димости использование матрицы переходов позволит вам намного сильнее
углубиться в изучение ваших данных.
Глава 7
Анализ интервалов
даты и времени

В главе 4 мы уже обсуждали особенности работы с датой и времени. В этой


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

Введение во временные данные


Ранее в этой книге мы не раз упоминали возможность осуществ­ления сре-
зов по дате и времени. Это позволяет анализировать факты, изменяющиеся
с  течением времени. Говоря о  фактах, мы обычно имеем в  виду событие
с ассоциированным с ним числовым значением – например, количеством
проданных товаров, их ценой или возрастом покупателя. Но бывает так, что
событие не происходит одномоментно, а начинается в определенной точке
и сохраняет свое действие на протяжении некоторого времени.
Представьте себе учет рабочего времени. Вы можете учесть в модели тот
факт, что в определенный день сотрудник вышел на работу, выполнил свои
функции и заработал какую-то сумму денег. Информация об этом событии
может храниться в качест­ве обычного факта в базе данных. В то же время
мы могли бы хранить количество часов, отработанных сотрудником, чтобы
суммировать эти данные в  конце месяца. Для такого анализа нам подой-
дет схема данных, показанная на рис. 7.1. Здесь у нас есть два измерения
Workers (рабочие) и Date (даты), а также таб­лица фактов Schedule (расписа-
ние) с соответствующими ключами и значениями.

Рис. 7.1. Простая модель данных для отслеживания рабочего расписания

Бывает так, что сумма оплаты зависит от времени суток, в которое работал
сотрудник. Например, ночные смены обычно оплачиваются в большем раз-
мере, чем дневные. Посмотрите на таблицу, показанную на рис. 7.2, – суммы
(Amount) для вечерних смен, начинающихся после шести вечера (6:00 p.m.),
выше по сравнению с утренними. Размер почасовой оплаты можно полу-
чить, поделив столбец Amount на HoursWorked (отработанные часы).
Введение во временные данные    171

Рис. 7.2. Фрагмент содержимого таблицы Schedule

Мы можем использовать этот простой набор данных для формирования


отчета об отработанных сотрудниками часах и полученных деньгах по ме-
сяцам. Матрица представлена на рис. 7.3.

Рис. 7.3. Простая матрица на основании расписания рабочего времени

На первый взгляд цифры выглядят правильно. Но посмотрите еще раз на


таблицу, изображенную на рис. 7.2, обратив внимание на рабочие дни в кон-
це каждого месяца (января и февраля). Вы заметите, что вечерние смены
31 января по причине их продолжительности захватили февраль. Было бы
уместно оставшиеся часы смены учитывать уже в феврале. То же самое ка-
сается и рабочих смен, начавшихся 29 февраля и завершившихся уже в мар-
те. Наша модель данных не позволяет осущест­вить такие переносы часов.
Вместо этого вся смена, начавшаяся в конце месяца, оказывается привязана
к этому месяцу, хотя это и не совсем верно.
Мы находимся в  самом начале раздела, и  поэтому нам не  хотелось бы
сразу погружаться во все подробности решения. Мы  постепенно во всем
разберемся в этой главе. На этом этапе важно понять, что исходная модель
данных не отвечает всем нашим требованиям. Суть проблемы заключается
в том, что события в таблице фактов имеют определенную длительность,
которая может входить в конфликт с уровнем гранулярности этой таблицы.
Иными словами, в  таблице фактов гранулярность установлена на уровне
дня, а сами факты могут содержать информацию сразу о нескольких днях.
Получается, мы снова вернулись к проблеме с гранулярностью. Очень похо-
жий сценарий возникает при необходимости анализировать длительность
172    Анализ интервалов даты и времени

событий. Когда факты обладают определенной продолжительностью, обра-


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

Агрегирование простых интервалов


Перед тем как углубиться в сложный анализ временных интервалов, давай-
те начнем с более простых сценариев. В этом разделе мы покажем, как пра-
вильно включить измерение времени в модель данных. Фактически в боль-
шинстве сценариев, с которыми мы имеем дело, так или иначе необходимо
присутствие измерения времени, и очень важно уметь правильно модели-
ровать работу с ним.
В традиционных базах данных вы зачастую будете сталкиваться со столб-
цом DateTime, хранящим как дату, так и время. Таким образом, информация
о том, что событие началось в 09:30 утра 15 января 2017 года, будет отражена
в таблице в  единственном столбце. Даже если в  вашем источнике данных
так и есть, мы настоятельно советуем при загрузке данных в модель разби-
вать информацию о дате и времени события на два столбца: один для даты,
второй для времени. Причина в том, что табличный движок (Tabular), лежа-
щий в основе Power Pivot и Power BI, гораздо лучше работает с небольшими
измерениями. Если вы решите хранить дату и время в одном столбце, ваше
измерение очень сильно увеличится в  объеме, поскольку для каждого дня
придется хранить часы и  минуты. Разбивая информацию на два столбца,
измерение дат будет хранить данные с гранулярностью до дня, а в измере-
нии будет содержаться только время. Таким образом, для хранения событий
в интервале десяти лет вам понадобится измерение дат объемом 3650 строк,
а в таблице со временем будет находиться 1440 строк, если данные нужны
с детализацией до минуты. Если бы дата и время хранились в одной таблице,
нам бы потребовалось измерение, содержащее 5 256 000 строк (3650 раз по
1440). Разница в скорости обработки запросов будет существенной.
Конечно, необходимо разбивать данные о дате и времени на два столбца
еще до момента загрузки информации в модель. Иначе говоря, вы можете
загрузить столбец с датой и временем в свою модель, а затем сделать два
вычисляемых столбца, которые впоследствии будете использовать для связей.
Агрегирование простых интервалов    173

Но в таком случае хранение исходного объединенного столбца будет пустой


тратой ресурсов, поскольку вы никогда не  воспользуетесь этой информа-
цией. Можно добиться того же результата с гораздо меньшими затратами
памяти, используя для разбиения столбцов Excel или редактор запро­сов
Power BI, а более опытные пользователи могут прибегнуть к помощи пред-
ставления (view) SQL. На рис. 7.4 показано простое измерение времени.

Рис. 7.4. Простое измерение времени с детализацией до минуты

Измерение, в котором хранятся часы и минуты, может оказаться для вас


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

Рис. 7.5. Можно объединять время в интервалы


при помощи обычных вычисляемых столбцов

При анализе временных интервалов необходимо проявлять особую вни-


мательность. Даже если вам кажется вполне естест­венным хранить инфор-
мацию в  измерении с  детализацией до минуты и  затем группировать ее
174    Анализ интервалов даты и времени

в  интервалы, возможно, лучшим решением будет определить грануляр-


ность таблицы на уровне интервалов. Иными словами, если у вас нет необ-
ходимости анализировать данные с точностью до минуты (а чаще всего это
так и будет) и вы можете ограничиться получасовыми интервалами, не сто-
ит тратить драгоценные ресурсы на хранение информации по минутам.
Перевод измерения на интервалы по полчаса позволит сократить объем
таблицы с 1440 до 48 строк – почти на два порядка. В результате мы полу-
чим существенную экономию в  плане расходования оперативной памяти
и  увеличим скорость выполнения запросов при работе с  объемной таб­
лицей фактов. На рис. 7.6 показано то же измерение времени, что и раньше,
но с гранулярностью до получасового интервала.
Разумеется, при таком хранении информации в измерении времени вам
необходимо будет позаботиться о  наличии в  таб­лице фактов ключевого
поля, по которому может осуществ­ляться связь. В  таблице, представлен-
ной на рис.  7.6, мы использовали формулу Hours × 60 + Minutes для клю-
ча (TimeIndex) вместо простого индекса с  автоматическим приращением.
Это облегчило нам задачу расчета значения ключа в таблице фактов. То же
можно сделать путем простых математических вычислений – без необходи-
мости выполнять сложный ранжированный поиск.

Рис. 7.6. Таблица, хранящая информацию по получасовым интервалам, значительно


уменьшилась в объеме

Позвольте нам повторить одну простую истину: дата и  время должны


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

Интервалы с переходом дат


В предыдущем разделе мы научились создавать измерение времени. Теперь
пришло время вернуться к началу главы и провес­ти более детальный ана-
лиз сценария, в котором события могут захватывать часть следующего дня.
Как вы помните, у нас есть таблица Schedule, в которой хранятся отрабо-
танные сотрудниками часы. А трудности анализа состоят в том, что смена
могла начаться вечером одного дня, а завершиться утром другого. Давайте
вспомним нашу исходную модель данных, представленную на рис. 7.7.

Рис. 7.7. Простая модель данных для работы с расписанием

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

Real Working Hours =


--
-- Вычисляем рабочие часы в текущий день
--
SUMX (
Schedule;
IF (
Schedule[TimeStart] + Schedule[HoursWorked] * ( 1 / 24 ) <= 1;
Schedule[HoursWorked];
( 1 - Schedule[TimeStart] ) * 24
)
)
--
-- Проверяем, есть ли часы, перенесенные с предыдущего дня на текущий
--
+ SUMX (
176    Анализ интервалов даты и времени

VALUES ( 'Date'[Date] );
VAR
CurrentDay = 'Date'[Date]
RETURN
CALCULATE (
SUMX (
Schedule;
IF (
Schedule[TimeStart] +
Schedule[HoursWorked] * ( 1 / 24 ) >
1;
Schedule[HoursWorked] - ( 1 -
Schedule[TimeStart] ) * 24
)
);
'Date'[Date] = CurrentDay - 1
)
)

Теперь мера возвращает корректные значения, как показано на рис. 7.8.

Рис. 7.8. Новая мера правильно распределяет рабочие часы по дням

Кажется, проблема решена. Но вы можете задаться вопросом, хотите ли


вы заниматься написанием такого сложного кода. Нам просто пришлось
это сделать, поскольку мы пишем книгу и  должны были показать, каким
сложным может быть код, но у вас всегда есть другие варианты. Шанс допус­
тить ошибку в столь замысловатом коде очень велик. Кроме того, этот код
не универсален, поскольку работает только при условии распространения
смены на два дня. Если дней будет больше, код еще сильнее усложнится,
а вероятность появления ошибок в нем повысится.
Как и всегда в этой книге (и в реальном мире тоже), правильное реше-
ние состоит отнюдь не в написании громоздкого кода на DAX. Необходимо
просто изменить модель данных так, чтобы она наиболее точно отражала
информацию, которая нам нужна. В этом случае код для мер значительно
упростится, а его время выполнения снизится.
Интервалы с переходом дат    177

Есть несколько вариантов изменения модели данных. Как мы выяснили


ранее в этой главе, главная проблема заключается в том, что мы храним дан-
ные на неправильном уровне гранулярности. Вы должны изменить грану-
лярность, если у вас есть необходимость осуществлять срезы по часам, кото-
рые сотрудник отработал за день, и вы хотите относить ночные смены на те
календарные даты, на которые они приходятся. Иными словами, от хране-
ния факта, говорящего о том, что, «приступив к работе такого-то числа, со-
трудник отработал столько-то часов», мы должны перейти к факту о том, что
«в определенный день сотрудник отработал столько-то часов». Например,
если смена для сотрудника началась 1 сентября, а закончилась 2 сентября,
в таблице фактов будут храниться две записи – по одной для каждой даты.
Таким образом, факты, которые в  предыдущей версии таблицы фактов
хранились в одной строке, в обновленной модели данных будут разделены
на две. Если сотрудник начал рабочую смену поздно вечером и  завершил
в следующую календарную дату, то в таблице фактов появятся две записи:
в одной будет отражено количество часов, которое он отработал в день нача-
ла смены, а во второй – остаток часов, начиная с полуночи, в дату окончания
смены. Если смена длится больше двух календарных дней, строк будет боль-
ше. Конечно, в этом случае нам придется потрудиться на этапе подготовки
данных. В  этой книге сам процесс подготовки к  загрузке мы показывать
не будем, поскольку он содержит довольно сложный код на языке M. Но если
вам интересно, вы можете ознакомиться с ним подробно в сопутствующем
контенте. Получившаяся таблица Schedule, в  которой в  целом ряде строк
рабочая смена начинается в полночь, показана на рис. 7.9. Рабочие часы для
каждого дня были подсчитаны на этапе извлечения, преобразования и загруз-
ки данных (Extract, Transform, Load – ETL).

Рис. 7.9. В таблице Schedule гранулярность снижена


178    Анализ интервалов даты и времени

После произведенного изменения модели данных с корректировкой гра-


нулярности мы сможем агрегировать значения при помощи обычной функ-
ции SUM. При этом мы получим правильные суммы и  сможем избежать
сложностей с написанием громоздкого кода на DAX.
Внимательные читатели заметят, что мы изменили значения в столбце
HoursWorked, но не  стали корректировать цифры в  поле Amount. Факти-
чески если сейчас провести агрегацию по этому столбцу, мы получим не-
правильные результаты. Все потому, что могут быть дважды подсчитаны
значения из-за смены календарных дат. Мы  намеренно допустили такую
неточность, чтобы впоследствии на основании этого провести более де-
тальный анализ модели.
Легким способом исправления этой ошибки было бы деление количества
часов, отработанных сотрудником за день, на общую продолжительность
смены. В результате мы получили бы процент от смены, приходящийся на
конкретный день. Это также может быть сделано на этапе предваритель-
ной подготовки данных к загрузке. Однако если вы стремитесь к идеальной
модели, то должны учитывать и то, что рабочие часы могут оплачиваться
по-разному в зависимости от времени суток. Кроме того, некоторые смены
могут захватывать сразу несколько тарифов оплаты. Наша обновленная мо-
дель не подходит для такого сценария.
Если часы могут оплачиваться по-разному, необходимо снизить уровень
гранулярности (то есть повысить детализацию) таблиц фактов до часа. Мож-
но либо хранить информацию в таб­лице фактов по часам, либо выполнять
предварительную агрегацию значений в отрезки, когда тариф не меняется.
В  плане гибкости переход на почасовые факты даст нам больше свободы
и  облегчит формирование отчетов с  сохранением возможности распро-
странять смены на несколько дней. В  варианте с  предварительно агреги-
рованными данными сделать это будет гораздо сложнее. С другой стороны,
при снижении уровня гранулярности в  таблице фактов неминуемо будет
наблюдаться рост количества строк. Как и  всегда, вам необходимо найти
правильный баланс между объемом модели данных и ее аналитическим по-
тенциалом.
В нашем примере мы решили снизить уровень гранулярности таблицы
фактов до часа, что отражено на рис. 7.10.
Интервалы с переходом дат    179

Рис. 7.10. Новая мера относит рабочие часы на правильный день

В обновленной модели данных факт говорит о том, что «в такой-то час


такого-то дня этот сотрудник работал». Мы снизили гранулярность до мак-
симально возможного уровня детализации. К  тому же в  этом случае для
вычисления количества рабочих часов сотрудника нам даже не  придется
пользоваться агрегирующей функцией SUM. Фактически достаточно будет
посчитать строки в таблице Schedule для получения необходимого резуль-
тата, как показано в мере WorkedHours ниже:

WorkedHours := COUNTROWS ( Schedule )

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

Рис. 7.11. Анализ временных периодов, не относящихся к датам

Теперь вы можете задавать определенные тарифы по часам (возможно,


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

Моделирование рабочих смен и временных сдвигов


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

ночи? Хотите ли вы, чтобы аудитория этой программы сравнивалась с ау-


диторией той, которая началась на час раньше? Скорее всего, ответ будет
положительным, ведь не так показательно, когда начались передачи, важно
то, что они идут одновременно. И  высока вероятность, что зрители будут
выбирать между каналами, на которых идут эти шоу.
Для обоих этих сценариев есть одно интересное решение, требующее
от вас расширения понятия времени. В  случае с  рабочими сменами мож-
но полностью игнорировать время. Вмес­то того чтобы хранить в таблице
фактов время начала смены, можно остановиться на номере смены и про-
водить анализ именно по этому параметру. Если же вам нужно анализиро-
вать время, то лучше будет понизить уровень гранулярности, вернувшись
к решению из предыдущего раздела. Но в большинстве подобных случаев
мы просто избавлялись от учета фактического времени в модели данных.
Сценарий со зрительской аудиторией несколько отличается, и решение
здесь будет очень простым, пусть и довольно странным. Вы можете рассмат­
ривать передачи, начавшиеся после полуночи, как продолжение текущего
дня, чтобы при анализе дневной аудитории эти зрители попали в выборку.
Этого можно добиться путем применения простого алгоритма временного
сдвига. Например, вы можете считать, что сутки начинаются не с полуночи,
а с двух часов ночи. Таким образом, к стандартному времени мы добавляем
два часа, и получается, что сутки длятся с 02:00 до 26:00, а не с 00:00 до 24:00.
В  этом случае удобнее будет пользоваться именно 24-часовым форматом
времени, а не признаками A.M. и P.M.
На рис.  7.12 показан типичный отчет, использующий технику времен-
ных сдвигов. Заметьте, что в  столбце CustomPeriod времена ранжируются
от 02:00 до 25:59. Это тот же 24-часовой формат, но со сдвигом на два часа.
Так что при анализе определенного дня вы учитываете также два часа от
следующих за ним календарных суток.

Рис. 7.12. Применение временного сдвига смещает начало дня на два часа вперед

Разумеется, при загрузке данных в модель вам понадобится соответству-


ющим образом преобразовать информацию. При этом вы не  сможете ис-
пользовать привычный формат данных DateTime, поскольку он не поддер-
живает время за пределами 24 часов.
182    Анализ интервалов даты и времени

Анализ активных событий


Как вы заметили, в этой главе мы в основном говорим о таб­лицах фактов
применительно к концепции продолжительности событий. При анализе по-
добных моделей данных часто встает вопрос о количестве событий, актив-
ных в определенный момент времени. Событие считается активным (active
event), если оно началось, но еще не  закончилось. Примеров может быть
масса, и  один из них касается заказов, которые мы рассматривали ранее
в этой книге. Заказы обычно получают, обрабатывают, а затем осуществля-
ют отправку товаров. На протяжении всего времени между моментом полу-
чения и отправкой товаров заказ считается активным. Конечно, при более
подробном анализе этого сценария вы можете полагать, что с момента от-
грузки и до фактического получения посылки адресатом заказ также можно
отнести к активным, но с измененным статусом.
Для простоты анализа мы не будем рассматривать различные статусы
отгрузки, а сосредоточимся на построении модели данных для учета ак-
тивных заказов. Эту модель можно использовать не только в продажах, но
и в других отраслях – например, при оформлении страховых договоров,
у  которых также есть дата начала и  окончания, страховых исков, зака-
зов на выращивание растений или в производстве изделий на станочном
оборудовании. Во всех этих случаях вы фиксируете определенные собы-
тия, такие как размещение заказа или выращивание растений. При этом
само событие характеризуется двумя и  более датами на пути от его на-
чала к завершению.
Перед тем как приступить к  рассмотрению сценария, давайте отметим
один важный момент, актуальный для анализа заказов. В модели данных,
которую мы использовали на протяжении большей части книги, продажи
хранятся на уровнях товара, даты и покупателя. И если в заказе присутству-
ет десять товаров, то столько же строк будет и в таблице Sales. Используемая
модель приведена на рис. 7.13.

Рис. 7.13. В таблице фактов Sales хранятся заказы

Для определения количества заказов вам необходимо будет подсчитать


уникальные значения в  столбце Order Number таблицы Sales, посколь-
ку номер заказа будет дублироваться на нескольких строках. Более того,
Анализ активных событий    183

если заказ состоит из нескольких посылок, то в каждой строке может быть


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

Orders =
SUMMARIZECOLUMNS (
Sales[Order Number];
Sales[CustomerKey];
"OrderDateKey"; MIN ( Sales[OrderDateKey] );
"DeliveryDateKey"; MAX ( Sales[DeliveryDateKey] )
)

В этой таблице меньше строк и  столбцов. Также мы на этапе создания


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

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

Как видите, таблица фактов Orders в обновленной модели не связана с из-


мерением Product. Стоит отметить, что данный сценарий можно решить
и  путем построения модели с  главной и  подчиненной таблицами фактов,
где главной будет Orders, а  подчиненной – Sales. В  этом случае вы долж-
ны учесть все особенности таких моделей, которые мы обсуждали в главе 2.
184    Анализ интервалов даты и времени

В нашем случае мы не будем строить модель с главной и подчиненной таб­


лицами, поскольку нас, по сути, интересует только таблица Orders. Так что
мы остановимся на упрощенной модели данных, показанной на рис. 7.15.
Заметим, что в сопутствующем контенте таблица Sales также присутствует,
поскольку от нее зависит таблица Orders. Но мы сконцентрируемся только
на этих трех таблицах.

Рис. 7.15. Упрощенная модель, которую мы будем использовать в этом сценарии

После построения модели мы можем создать меру для подсчета количест­


ва открытых заказов посредством следующего кода на DAX:

OpenOrders :=
CALCULATE (
COUNTROWS ( Orders );
FILTER (
ALL ( Orders[OrderDateKey] );
Orders[OrderDateKey] <= MIN ( 'Date'[DateKey] )
);
FILTER (
ALL ( Orders[DeliveryDateKey] );
Orders[DeliveryDateKey] > MAX ( 'Date'[DateKey] )
);
ALL ( 'Date' )
)

Сам по себе код довольно прост. Важно лишь отметить, что таб­лицы
Orders и  Date объединены связью по полю OrderDateKey, а  значит, нужно
использовать функцию ALL для таблицы Date для отмены установленных
фильтров. Если этого не сделать, результаты будут неправильными – факти-
чески нам вернутся все заказы, созданные в выбранный период. Созданная
нами мера работает прекрасно – на рис.  7.16 показан отчет, содержащий
количество созданных и открытых заказов.
Анализ активных событий    185

Рис. 7.16. Количество созданных и открытых заказов

Для проверки правильности работы меры было бы полезно вывести в от-


чет еще и количество доставленных заказов. Этого можно добиться путем
использования техники, описанной в главе 3 и состоящей в добавлении еще
одной связи между таб­лицами Orders и Date. Новая связь будет выполнена
по дате поставки и будет неактивна в модели данных, чтобы не вносить не-
однозначность. Используя эту связь в формуле, можно создать новую меру
OrdersDelivered следующим образом:
OrdersDelivered :=
CALCULATE (
COUNTROWS ( Orders );
USERELATIONSHIP ( Orders[DeliveryDateKey]; 'Date'[DateKey] )
)

Новый отчет, показанный на рис. 7.17, гораздо легче читать и проверять.

Рис. 7.17. Добавление меры OrdersDelivered значительно облегчило понимание отчета


186    Анализ интервалов даты и времени

Наша модель правильно обрабатывает отчеты на уровне дня. Однако при


формировании отчета по месяцам или другим периодам, превышающим
один день, начинаются серьезные проблемы. Фактически если убрать из
вывода дни и оставить только месяцы, колонка OpenOrders станет показы-
вать пустые значения, как видно по рис. 7.18.

Рис. 7.18. На уровне месяцев мера выводит неправильные (пустые) значения

Проблема в том, что ни один заказ не доставляется больше месяца, а наша


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

OpenOrders :=
CALCULATE (
CALCULATE (
COUNTROWS ( Orders );
FILTER (
ALL ( Orders[OrderDateKey] );
Orders[OrderDateKey] <= MIN ( 'Date'[DateKey] )
);
FILTER (
ALL ( Orders[DeliveryDateKey] );
Orders[DeliveryDateKey] > MAX ( 'Date'[DateKey] )
);
ALL ( 'Date' )
);
LASTDATE ( 'Date'[Date] )
)
Анализ активных событий    187

Обновленная мера выводит ожидаемое количество открытых заказов на


уровне месяца, как показано на рис. 7.19.
Модель работает правильно, правда, в более старых версиях движка (ко-
торый использовался в Excel 2013 и SQL Server Analysis Services 2012 и 2014)
производительность ее будет не  слишком высока. В  Power BI и  Excel 2016
с обновленным движком дела будут получше, но эту меру все равно не на-
зовешь чемпионом по скорости вычисления. Описание причин такого па-
дения производительности выходит за рамки этой книги, но, в  двух сло-
вах, это происходит из-за того, что условия в фильтре не используют связи.
Вмес­то этого два наложенных фильтра будут вычисляться в наименее про-
изводительной области подсистемы, называемой движком формул. Если
же формулы в своих расчетах опираются исключительно на связи в модели
данных, их эффективность будет довольно высока.

Рис. 7.19. В отчете показано, сколько заказов оставались открытыми на конец месяца

Чтобы добиться этого, потребуется скорректировать модель данных, из-


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

OpenOrders =
SELECTCOLUMNS (
GENERATE (
Orders;
VAR CurrentOrderDateKey = Orders[OrderDateKey]
VAR CurrentDeliverDateKey = Orders[DeliveryDateKey]
RETURN
FILTER (
188    Анализ интервалов даты и времени

ALLNOBLANKROW ( 'Date'[DateKey] );
AND (
'Date'[DateKey] >=
CurrentOrderDateKey;
'Date'[DateKey] <
CurrentDeliverDateKey
)
)
);
"CustomerKey"; [CustomerKey];
"Order Number"; [Order Number];
"DateKey"; [DateKey]
)

Примечание. Хотя мы и представили для построения новой таб­


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

Новую модель данных можно видеть на рис. 7.20.

Рис. 7.20. В новой таблице OpenOrders содержатся только открытые заказы

В обновленной модели данных вся логика, связанная с открытыми зака-


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

Open Orders := DISTINCTCOUNT ( OpenOrders[Order Number] )

Нам по-прежнему приходится подсчитывать количество уникальных но-


меров заказов, поскольку один заказ может появляться в таблице несколько
раз. Но в целом логика ограничивается одной таблицей. Главным преиму-
ществом этой меры является то, что при расчете она использует быстрый
движок DAX с  его мощной системой кеширования (cache system). Табли-
ца OpenOrders будет более объемной по сравнению с  исходной таблицей
Анализ активных событий    189

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


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

Рис. 7.21. В отчете по месяцам показаны заказы, которые были открыты в любой


из дней этого месяца

Вы можете легко изменить способ агрегации, чтобы получить среднее


количество открытых заказов или их число на конец месяца, используя сле-
дующие формулы:

Open Orders EOM := CALCULATE ( [Open Orders]; LASTDATE ( ( 'Date'[Date] ) ) )


Open Orders AVG := AVERAGEX ( VALUES ( 'Date'[DateKey] ); [Open Orders] )

Результирующий вывод можно видеть на рис. 7.22.


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

Рис. 7.22. В отчете показано суммарное количество открытых заказов, их среднее коли-


чество и число на конец месяца

Для создания таблицы с  предварительно подсчитанной информацией


можно воспользоваться следующим кодом:

Aggregated Open Orders =


FILTER (
ADDCOLUMNS (
DISTINCT ( 'Date'[DateKey] );
"OpenOrders", [Open Orders]
);
[Open Orders] > 0
)

Получившаяся в  результате таблица будет небольшой по объему, по-


скольку ее гранулярность будет установлена на уровне дня. Так что в  ней
будет не больше нескольких тысяч записей. Это самая простая из всех рас-
смотренных моделей данных для этого сценария – после отказа от хранения
номеров заказов и  кодов покупателей мы пришли к  единственной связи
с измерением дат, как видно по рис. 7.23. Снова обращаем ваше внимание
на то, что в сопровождающем книгу файле таблиц больше, поскольку там
сохранена исходная модель, что будет показано далее в этом разделе.
В представленной модели данных количество открытых заказов на кон-
кретную дату вычисляется простым агрегированием по столбцу OpenOrders
с применением функции SUM.
Анализ активных событий    191

Рис. 7.23. Предварительная агрегация позволила максимально упростить схему данных

Внимательный читатель в этом месте может упрекнуть нас в том, что мы


сделали шаг назад в  изучении моделирования данных. Ведь в  самом на-
чале книги мы сказали, что использование единственной таблицы фактов
с предварительно рассчитанными данными ведет к ограничению аналити-
ческого потенциала модели. И действительно, если информация не пред-
ставлена в таблице фактов, мы утрачиваем возможность осуществлять сре-
зы по соответствующим атрибутам для более глубокого анализа. Более того,
в главе 6 мы сказали о том, что такая предварительная агрегация в снимках
редко бывает полезной. А сейчас мы вдруг делаем снимок с открытыми за-
казами для повышения скорости выполнения запросов!
В какой-то степени ваша критика оправдана, но мы призываем вас еще
раз подумать об этой модели. Вся необходимая информация по-прежнему
доступна в  исходных таблицах. И  то, что мы сделали, никоим образом
не  ограничивает аналитический потенциал модели. Просто в  стремлении
максимально повысить скорость выполнения запросов мы при помощи
языка DAX создали снимок, вместивший в себя всю вычислительную логику.
Таким образом, мы пришли к  ситуации, когда «тяжелые» вычисления
вроде получения количества открытых заказов мы можем брать из пред-
варительно агрегированной таблицы, тогда как оперативные данные, та-
кие как сумму продажи, продолжаем вычислять на основании исходных
таблиц фактов. В результате наша модель не утратила былой выразитель-
ности (expressivity), а даже приобрела в виде новых таблиц фактов, которые
192    Анализ интервалов даты и времени

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


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

Рис. 7.24. Полная модель данных со всеми таблица-


ми фактов выглядит достаточно сложно

В зависимости от объема ваших данных и требований к анализу вы мо-


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

Смешивание разных интервалов


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

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

Рис. 7.25. Модель данных отражает зарплату сотрудников и их привязку к магазинам

На первый взгляд модель кажется довольно сложной. Вот небольшое опи-


сание к таблицам:
 SalaryEmployee. В этой таблице содержится информация о ежеднев-
ной зарплате сотрудников с указанием начала и окончания действия
ставки;
 StoreEmployee. Эта таблица хранит привязки сотрудников к магази-
нам с датами начала и окончания работы в каждом из них;
 Schedule. Таблица расписания содержит рабочие дни для сотрудников.
Остальные таблицы  – Store (магазины), Employees (сотрудники) и  Date
(даты) – обычные справочники-измерения.
В модели данных представлена вся необходимая информация для по-
строения отчета об изменениях зарплаты сотрудников с течением време-
ни. При этом мы можем осуществлять срезы как по магазинам, так и  по
сотрудникам. Однако формула в мере для подобных вычислений с учетом
наличия дат будет довольно сложной, поскольку вы должны выполнить
следующие действия.
194    Анализ интервалов даты и времени

1. И
 звлечь зарплату, действующую для данного сотрудника на выбран-
ную дату, путем наложения фильтра на столбцы FromDate и  ToDate
в таблице SalaryEmployee. Если в выборке сразу несколько сотрудни-
ков, необходимо пройти по всем и для каждого отдельно выполнить
эту операцию.
2. Получить магазин, в котором сотрудник работал в заданную дату.
Давайте начнем с простого примера прямо в модели данных – сформиру-
ем отчет о количестве рабочих дней сотрудников по годам. Это возможно, по-
скольку установленные связи позволяют осуществлять срезы таблицы Schedule
по календарным годам и имени сотрудника. Остается написать простую меру:

WorkingDays := COUNTROWS ( Schedule )

Эту часть отчета мы получили легко и просто, а вывод показан на рис. 7.26.

Рис. 7.26. В отчете выведено количество рабочих дней сотрудников по годам

Для начала проанализируем зарплату Мишель (Michelle), код которой


в  модели данных равен 2, за 2015 год. Отчет по зарплатам на основании
таблицы SalaryEmployee представлен на рис. 7.27.

Рис. 7.27. В зависимости от даты зарплата сотрудника может меняться

В 2015 году зарплата Мишель менялась один раз. Так что для получения
нужного результата придется проходить по каждому дню и определять зар-
плату сотрудника, после чего суммировать полученные данные. На этот раз
мы не можем полагаться на связи, поскольку связь должна базироваться на
условии вхождения в интервал. В нашей таблице зарплата сотрудника огра-
ничена столбцами FromDate и ToDate включительно.
Код для этой меры написать будет не так просто, как видно из представ-
ленного ниже фрагмента:
Смешивание разных интервалов    195

SalaryPaid =
SUMX (
'Schedule';
VAR SalaryRows =
FILTER (
SalaryEmployee;
AND (
SalaryEmployee[EmployeeId] =
Schedule[EmployeeId];
AND (
SalaryEmployee[FromDate] <=
Schedule[Date];
SalaryEmployee[ToDate] >
Schedule[Date]
)
)
)
RETURN
IF ( COUNTROWS ( SalaryRows ) = 1; MAXX ( SalaryRows;
[DailySalary] ))
)

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

Рис. 7.28. Количество рабочих дней и зарплата сотрудников за период

Сценарий усложнится, если вам понадобится осуществлять срезы по ма-


газинам. В этом случае нужно учитывать только тот период, когда сотруд-
ник числился в этом магазине. Значит, надо добавить к формуле фильтр на
таблицу Schedule, как показано ниже:
196    Анализ интервалов даты и времени

SalaryPaid =
SUMX (
FILTER (
'Schedule';
AND (
Schedule[Date] >= MIN ( StoreEmployee[FromDate] );
Schedule[Date] <= MAX ( StoreEmployee[ToDate] )
)
);
VAR SalaryRows =
FILTER (
SalaryEmployee;
AND (
SalaryEmployee[EmployeeId] =
Schedule[EmployeeId];
AND (
SalaryEmployee[FromDate] <=
Schedule[Date];
SalaryEmployee[ToDate] >
Schedule[Date]
)
)
)
RETURN
IF ( COUNTROWS ( SalaryRows ) = 1; MAXX ( SalaryRows;
[DailySalary] ) )
)

Формула работает корректно, как показано на рис. 7.29, но она достаточ-


но сложна и может выдавать неверные результаты при неправильном ис-
пользовании.

Рис. 7.29. Последняя версия меры SalaryPaid выдает правильные результаты по


магазинам

Проблема с этой моделью заключается в том, что связи между таблицами


магазинов, зарплат и сотрудников довольно сложны. В результате серьезно
усложняется код на DAX для анализа модели, что может вести к появлению
ошибок. Как и раньше, мы пойдем по пути переноса сложности с вычисле-
Смешивание разных интервалов    197

ний DAX на процесс загрузки данных в модель с постепенным переходом


к схеме «звезда».
Для каждой строки в таблице Schedule можно легко вычислить, в каком
магазине в этот день работал сотрудник и какую зарплату получал. Как всег-
да, процесс денормализации позволит упростить формулы агрегации, что
в конечном счете приведет к более понятной модели данных.
Итак, нам необходимо создать два вычисляемых столбца в  таб­лице
Schedule: один для ежедневной зарплаты сотрудника конкретно в этот день,
а второй – для обозначения магазина, в котором он работал. Это можно сде-
лать посредством следующего кода:

Schedule[DailySalary] =
VAR CurrentEmployeeId = Schedule[EmployeeId]
VAR CurrentDate = Schedule[Date]
RETURN
CALCULATE (
VALUES ( SalaryEmployee[DailySalary] );
SalaryEmployee[EmployeeId] = CurrentEmployeeId;
SalaryEmployee[FromDate] <= CurrentDate;
SalaryEmployee[ToDate] > CurrentDate
)

Schedule[StoreId] =
VAR CurrentEmployeeId = Schedule[EmployeeId]
VAR CurrentDate = Schedule[Date]
RETURN
CALCULATE (
VALUES ( StoreEmployee[StoreId] );
StoreEmployee[EmployeeId] = CurrentEmployeeId;
StoreEmployee[FromDate] <= CurrentDate;
StoreEmployee[ToDate] >= CurrentDate
)

Создание вычисляемых столбцов позволит нам избавиться от связей


в  таб­лицах SalaryEmployee и  StoreEmployee и  привес­ти модель данных
к традиционной схеме «звезда», что показано на рис. 7.30.

Примечание. Мы  намеренно оставили таблицы Salary­Employee


и  StoreEmployee, на основании которых создали вычисляемые
столбцы, видимыми, чтобы показать, что они лишились преж-
них связей. В итоговой модели данных вам, возможно, захочется
скрыть эти таблицы от глаз пользователей, поскольку для них они
не должны представлять никакого интереса.
198    Анализ интервалов даты и времени

Рис. 7.30. Денормализованная модель данных в виде схемы «звезда»

В обновленной модели расчет суммарной зарплаты сотрудников может


осуществляться простейшей формулой, приведенной ниже:
SalaryPaid = SUM ( Schedule[DailySalary] )

Еще раз повторим, что проведенная нами денормализация позволила


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

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

 дату и время предпочтительно хранить в разных столбцах;


 агрегирование простых интервалов выполнять довольно легко. Необ-
ходимо только снизить количество строк по соответствующим связям
в фактах до уровня вашей потребности в аналитике и одновременно
с этим уменьшить количество строк в столбце Time;
 когда длительности (или интервалы) в  вашей модели пересекаются
по времени, вы должны очень внимательно подходить к схеме дан-
ных. Здесь есть множество вариантов, и  вы ответственны за выбор
лучшего из них. Хорошая новость заключается в том, что вы можете
переходить от одного решения к другому, просто меняя модель дан-
ных. Таким образом вы сможете определить оптимальную схему для
вашего сценария;
 иногда стоит применять творческий подход в отношении временных
интервалов. Если сутки в  вашей модели данных не  заканчиваются
в полночь, вы можете осуществить временной сдвиг, чтобы день на-
чинался, к примеру, не с 00:00, а с 02:00. Не будьте заложником своей
модели. Наоборот, вы должны смело менять ее в зависимости от ва-
ших требований;
 анализ активных событий  – очень распространенный сценарий во
многих областях бизнеса. И в этой главе вы научились разным под-
ходам к такой разновидности фактов. Как всегда, чем лучше спроек-
тирована модель, тем проще будут формулы на DAX, но тем больше
работы придется проделать на этапе подготовки данных;
 если у вас есть несколько таблиц, в каждой из которых хранятся факты
со своими интервалами, попытка решить сценарий при помощи кода
DAX приведет к значительному усложнению итоговых формул. В то же
время предварительная агрегация данных в  вычисляемых столбцах
и таблицах поможет вам достигнуть требуемого уровня денормализа-
ции, что приведет к упрощению формул и их большей надежности.
Основной вывод всегда один и тот же: если ваш код на DAX становится
излишне сложным, скорее всего, пришло время внести изменения в модель
данных. И хотя модель нельзя подгонять под один отчет, именно она долж-
на служить ключом к эффективным решениям ваших сценариев.
Глава 8
Связи «многие ко многим»

Связи «многие ко многим» (many-to-many relationships) являются важным


инструментом в  арсенале любого аналитика данных. Часто такие связи
считают проблемными из-за тенденции к усложнению модели данных в их
присутствии. Но мы советуем вам относиться к ним как к еще одной анали-
тической возможности. Обращаться с этими связями довольно легко. Нуж-
но просто освоить базовые техники и умело их применять.
Как вы узнаете из этой главы, связи «многие ко многим» обладают огром-
ным потенциалом и  позволяют создавать очень мощные модели данных,
пусть и скрывающие в себе определенную сложность как в построении, так
и  в  интерпретации результатов. Надо сказать, что связи «многие ко мно-
гим» присутствуют почти в каждой модели, включая простую схему «звез-
да». Мы научим вас распознавать такие связи и, что более важно, извлекать
из них максимум пользы при проведении анализа.

Введение в связи «многие ко многим»


Давайте начнем со знакомства со связями типа «многие ко многим». Сущест­
вуют сценарии, в которых невозможно выразить отношение между двумя
сущностями при помощи одной связи. Типичный пример такого сценария –
расчетный счет. Банк накапливает транзакции, относящиеся к расчетному
счету. У счета может быть сразу несколько владельцев, тогда как у каждого
владельца может быть не один расчетный счет. Таким образом, вы не мо-
жете добавить поле с  ключом покупателя в  таблицу Accounts (счета), как
не  можете хранить ссылку на расчетный счет в таблице Customers. Такой
тип отношения по своей природе выражает наличие соответствия многих
записей из одной таблицы многим строкам из другой и не может быть опи-
сан посредством одного столбца.
202    Связи «многие ко многим»

Примечание. Есть множество других сценариев, в  которых по-


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

Типичным способом работы со связями «многие ко многим» является


создание таблицы-моста (с которой мы встречались в главе 3), содержащей
информацию о владельцах счетов. На рис. 8.1 показан пример модели дан-
ных, в  которой реализована связь «многие ко многим» между клиентами
и расчетными счетами.

Рис. 8.1. Связь между таблицами Customers и Accounts осуществляется посредством


таблицы-моста AccountsCustomers

Первое, что необходимо усвоить касательно связей «многие ко многим», –


это то, что связями они называются лишь с точки зрения модели данных, тогда
как на практике представляют собой пару обычных связей «один ко многим».
Так что здесь больше речь идет о концепции, нежели о физической реализа-
ции. Мы рассуждаем и работаем со связью «многие ко многим» как с физиче-
ским отношением между таблицами, хотя на самом деле его не существует.
Также стоит отметить, что связи, идущие от таблиц Customers и Accounts
к мосту, разнонаправленные. Фактически вектор каждой из этих связей на-
правлен от таблицы-моста к измерению. При этом мост всегда будет нахо-
диться на стороне «многих».
Почему связь «многие ко многим» считается более сложной по сравне-
нию с другими типами связей? Вот несколько причин:
 связи «многие ко многим» не работают по умолчанию в моделях
данных. Точнее говоря, они могут работать или нет в зависимости от
Введение в связи «многие ко многим»    203

версии и  настроек использующегося табличного движка. В  Power  BI


вы имеете возможность включать двунаправленную фильтрацию,
тогда как в Microsoft Excel (вплоть до версии Excel 2016 включительно)
у вас нет такой возможности, в связи с чем для правильного выполне-
ния формул в присутствии связей «многие ко многим» вам придется
пользоваться кодом на DAX;
 связи «многие ко многим» обычно способствуют созданию не-
аддитивных мер. Это ведет к затрудненному пониманию некоторых
показателей и усложняет поддержку модели данных;
 может страдать производительность. В  зависимости от объема
фильтруемых данных эффективность прохождения через две связи
в разных направлениях может оказаться не слишком высокой. Так что
при работе со связями «многие ко многим» вы должны уделять повы-
шенное внимание производительности.
Давайте проанализируем эти особенности более детально.

Понятие шаблона двунаправленной фильтрации


По умолчанию фильтр между таблицами распространяется по связи от «од-
ного» ко «многим», но не наоборот. Таким образом, если построить отчет
и осуществить в нем срез по покупателю, фильтр достигнет таблицы-моста
и на этом остановится. А значит, на таблицу Accounts фильтр, установлен-
ный в Customers, не распространится, как показано на рис. 8.2.
Действие фильтра не может
распространиться с таблицы-
моста на Accounts

Фильтр автоматически распространяется


с Customers на таблицу-мост

Рис. 8.2. Фильтр может распространяться от «одного» ко «многим», но не наоборот


204    Связи «многие ко многим»

Если вы в отчете вынесете на строки покупателей, а в единственной ко-


лонке в  значениях примените функцию SUM к  полю Amount из таблицы
Transactions (транзакции), то увидите для всех строк одинаковые суммы.
Это произошло из-за того, что фильтр, наложенный на Customers, не смог
пробиться через таблицу Accounts к транзакциям. Результат вывода пока-
зан на рис. 8.3.

Рис. 8.3. Вы не сможете фильтровать транзакции по покупателям


из-за наличия связи «многие ко многим»

Решить эту проблему можно, включив двунаправленную фильтрацию


(bidirectional filtering) между таблицей-мостом и Accounts. В Power BI такая
возможность заложена в саму модель данных, тогда как в Excel вам придет-
ся воспользоваться помощью DAX.
Если включить двунаправленную фильтрацию в модели, ее действие бу-
дет распространяться на все вычисления. В то же время если активировать
соответствующий шаблон при помощи включения функции CROSSFILTER
в  качестве параметра CALCULATE, его действие будет ограничено только
этой инструкцией. Посмотрите на пример ниже:

SumOfAmount :=
CALCULATE (
SUM ( Transactions[Amount] );
CROSSFILTER ( AccountsCustomers[AccountKey]; Accounts[AccountKey];
BOTH )
)

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


этой меры фильтр сможет распространять свое действие от таблицы-моста
к Accounts, а значит, в вывод попадут только строки, принадлежащие вы-
бранному покупателю.
На рис. 8.4 показаны обе меры рядом – новая и старая, в которой исполь-
зовалась только функция SUM.
Введение в связи «многие ко многим»    205

Рис. 8.4. Мера SumOfAmount показывает правильные значения, тогда как Amount во


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

Существует одно серьезное отличие между установкой двунаправленной


фильтрации в модели данных и при помощи кода на DAX. В первом случае
все остальные меры также смогут воспользоваться в своих расчетах распро-
странением фильтра от «многих» к «одному». В случае с DAX вам придется
повторять этот шаблон для каждой отдельной меры. А если таких мер у вас
много, добавлять одни и те же три строчки кода в каждую из них будет уто-
мительно. С другой стороны, установка двунаправленной фильтрации не-
посредственно для связи может внести неоднозначность в модель данных.
По этой причине не следует увлекаться такой возможностью – лучше напи-
сать несколько строчек кода.
Как мы уже сказали, в  Excel двунаправленную фильтрацию в  модели
включить нельзя, так что в  этом случае у  нас просто не  остается выбора.
В Power BI, напротив, такой выбор есть, и вы вольны выбирать более пред-
почтительный вариант. По  нашему опыту, включение двунаправленной
фильтрации в модели является более удобным вариантом и ведет к сниже-
нию потенциального количества ошибок в коде.
Похожего на использование функции CROSSFILTER эффекта можно до-
биться в DAX при помощи расширения таблицы (table expansion). Детальное
обсуждение этой темы заняло бы целую главу, тем более что мы достаточно
подробно описали ее в нашей книге «Подробное руководство по DAX» («The
Definitive Guide to DAX»). Здесь мы разве что отметим, что с использованием
расширения таблицы код предыдущей меры мог бы выглядеть примерно так:

SumOfAmount :=
CALCULATE (
SUM ( Transactions[Amount] );
AccountsCustomers
)

Результат будет практически таким же, как раньше. Использование рас-


ширения таблицы здесь служило бы той же цели, а именно распростране-
нию фильтра по связи. Главным отличием двунаправленной фильтрации
от расширения является то, что при использовании расширения шаблон
всегда применяет выбранный фильтр, тогда как двунаправленная фильтра-
ция работает, только когда фильтр активен. Чтобы продемонстрировать это
206    Связи «многие ко многим»

отличие, давайте добавим в таблицу Transactions строку, которая не будет


привязана ни к одному расчетному счету. Сумму поставим 5000 долларов,
и поскольку у этой транзакции не будет привязки ни к одному счету, следо-
вательно, и связи с покупателями у нее не будет. Посмотрите на результат
отчета на рис. 8.5.

Рис. 8.5. Применение CROSSFILTER и расширения таблицы дали разные результаты


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

Разница получилась ровно в 5000 долларов, что составляет сумму добав-


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

Понятие неаддитивности
Другой важной особенностью применения связей типа «многие ко многим»
является то, что меры, агрегируемые посредством таких связей, обычно по-
лучаются неаддитивными. Это не ошибка в модели данных, а особенность
таких связей. Чтобы лучше понять, о чем речь, посмотрите на матрицу, по-
казанную на рис. 8.6, в которой собраны одновременно данные по таблицам
Accounts и Customers.
Введение в связи «многие ко многим»    207

Рис. 8.6. Связи «многие ко многие» генерируют неаддитивные меры

Легко видеть, что итоги по колонкам показываются правильные, то есть


составляют суммы значений по строкам. Однако итоги по строкам заполне-
ны неверно. Это происходит из-за того, что суммы по счетам выводятся для
всех их владельцев. Например, счетом Mark-Paul одновременно владеют
Марк и Пол. Для каждого из них индивидуально сумма баланса составляет
1000 долларов, но когда мы рассматриваем их вместе, баланс не меняется
и по-прежнему равен 1000 долларов.
Неаддитивные меры  – это не  ошибка. Это характерное поведение для
мер, когда вы работаете со связями типа «многие ко многим». Просто эту
особенность нужно держать в уме, чтобы она для вас не стала неожиданно-
стью. Допустим, вы можете проходить по всем покупателям и агрегировать
по ним суммы – получившийся результат будет отличаться от ранее рассчи-
танных итогов. На рис. 8.7 показаны выводы следующих двух мер:

Interest := [SumOfAmount] * 0.01


Interest SUMX := SUMX ( Customers; [SumOfAmount] * 0.01 )

В версии с SUMX аддитивность меры достигается за счет вывода опера-


ции суммирования из расчета. В  результате этого итоги посчитались не-
правильно. Работая со связями «многие ко многим», необходимо помнить
об этих особенностях и обрабатывать их соответственно.

Рис. 8.7. Итоги по двум мерам получаются разными из-за присутствия связей «многие
ко многим»
208    Связи «многие ко многим»

Каскадные связи «многие ко многим»


Как вы видели в предыдущем разделе, существуют разные подходы к работе
со связями типа «многие ко многим». Изучив их, вы сможете легко справ-
ляться с подобными сценариями. Чуть больше внимания потребуется уделить
наличию в модели целых цепочек связей «многие ко многим», которые мы
называем каскадными связями «многие ко многим» (cascading many-to-many).
Давайте рассмотрим пример. Предположим, что в  нашем предыдущем
сценарии с расчетными счетами мы хотим добавить категории покупате-
лей, причем каждый покупатель может принадлежать к одной или несколь-
ким категориям, а в одной категории может быть несколько покупателей.
Иными словами, мы получаем еще одну связь «многие ко многим» – на этот
раз между таблицами покупателей и категорий.
Модель данных в  этом случае будет немного отличаться от предыду-
щей. На этот раз в ней будет сразу два моста – между таб­лицами Accounts
и Customers и между Customers и Categories, как показано на рис. 8.8.

Рис. 8.8. В шаблоне с каскадными связями содержится цепочка из двух таблиц-мостов

Можно заставить эту модель работать, установив двунаправленную


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

Рис. 8.9. Меры по каскадным связям «многие ко многим» с двунаправленной фильтра-


цией неаддитивны по строкам и столбцам
Каскадные связи «многие ко многим»    209

Свойство аддитивности теряется у  любого измерения, пропущенного


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

SumOfAmount :=
CALCULATE (
SUM ( Transactions[Amount] );
CROSSFILTER ( AccountsCustomers[AccountKey]; Accounts[AccountKey];
BOTH );
CROSSFILTER ( CustomersCategories[CustomerKey];
Customers[CustomerKey]; BOTH )
)

Если же вы решите воспользоваться вариантом с расширением таблицы,


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

SumOfAmount :=
CALCULATE (
SUM ( Transactions[Amount] );
CALCULATETABLE ( AccountsCustomers; CustomersCategories )
)

Если не  уделить этому должного внимания, может получиться код, по-
добный приведенному ниже:

SumOfAmount :=
CALCULATE (
SUM ( Transactions[Amount] );
AccountsCustomers;
CustomersCategories
)

Из-за неправильного порядка распространения фильтров цифры в отче-


те не будут соответствовать действительности, как показано на рис. 8.10.
210    Связи «многие ко многим»

Рис. 8.10. Если не следовать установленному порядку фильтрации, расширение таблицы


даст неверные результаты

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


связей двунаправленную фильтрацию (когда это возможно). В этом случае
вам не придется при написании кода обращать внимание на такие детали.
Очень легко допустить ошибку в коде, а сложности, связанные с неаддитив-
ностью мер, могут сделать его трудным для проверки и отладки.
Перед тем как двигаться дальше, стоит заметить, что в большинстве слу-
чаев в моделях с каскадными связями «многие ко многим» можно обойтись
единственной таблицей-мостом. В  приведенном выше примере у  нас два
моста, связывающих таблицы Categories с Customers и Customers с Accounts.
В качест­ве альтернативы можно было бы упростить модель данных, оставив
лишь один мост, объединяющий все три таблицы, как показано на рис. 8.11.
В таблице-мосте для трех измерений нет ничего сложного – более того,
модель даже внешне упрощается, особенно если вы привыкли к схемам со
связями «многие ко многим». К  тому же в  этом случае двунаправленную
фильтрацию придется устанавливать только для одной связи. А в вариан-
те с  расширением таблицы или программным включением перекрестной
фильтрации с помощью функции CROSSFILTER потребуется указать только
один параметр, что также снижает шанс появления ошибок.

Примечание. Такая схема данных с единым мостом, связывающим


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

Рис. 8.11. Единственная таблица-мост способна объединять несколько измерений

Конечно, сначала вам придется создать такую сверхтаблицу-мост (super-


bridge table). Мы для этого использовали язык DAX, но, как и всегда, у вас
есть свобода выбора – вы можете прибегнуть к помощи SQL или редактора
запросов.

Временные связи «многие ко многим»


Из прошлого раздела вы узнали, что в моделях со связями «многие ко мно-
гим» таблицы-мосты могут быть объединены сразу с несколькими измере-
ниями. Допустим, если таких связей три, вы можете каждую из трех таблиц
рассматривать как отдельный фильтр и осуществлять по ним срезы инфор-
мации в таблице фактов. Сценарий несколько меняется, когда связь «многие
ко многим» содержит условие, которое не может быть выражено простым
отношением. Вместо этого она описывается определенной длительностью.
Подобные связи называются временными (temporal many-to-many), и обра-
щаться с ними следует, помня о том, что мы изучали в главе 7 («Анализиру-
ем интервалы даты и времени») и уже прошли в настоящей главе.
При помощи такой модели можно описать, например, принадлежность
сотрудников к  командам, которая может меняться с  течением времени.
Сотрудники могут переходить из команды в команду, так что мы должны
хранить историю их принадлежности тому или иному коллективу. Начнем
с модели, представленной на рис. 8.12.
212    Связи «многие ко многим»

Рис. 8.12. Таблица IndividualsTeams описывает принадлежность сотрудников к коман-


дам во времени

Главным в этой модели является не наличие связей «многие ко многим»,


а то, что таблица-мост IndividualsTeams содержит поля FromDate и ToDate,
описывающие принадлежность сотрудника к конкретной команде в опре-
деленный промежуток времени. Если использовать эту модель как есть
и попытаться извлечь количество рабочих часов по командам и сотрудни-
кам, мы получим неверные результаты. Причина в том, что нужно очень
аккуратно использовать временные ограничения для распределения со-
трудников по командам. Обычный фильтр по сотруднику не сработает. Что-
бы лучше понять, что происходит, посмотрите на рис. 8.13, где изображена
таблица-мост с подсвеченными строками по Катерине (Catherine).

Рис. 8.13. Отфильтровав мост по Катерине, мы получим все команды, в которых она


работала за все время

Наложив фильтр по имени сотрудника, можно извлечь все команды,


в  которых когда-либо работала Катерина. Но  если вас интересует только
2015 год, то вы захотите получить лишь первые две строки. Более того, по-
Временные связи «многие ко многим»    213

скольку Катерина за 2015 год успела поработать в двух командах, вы хотели


бы, чтобы в январе вывелась команда разработчиков (Developers), а с фев-
раля по декабрь – команда продаж (Sales).
С моделями, содержащими временные связи «многие ко многим», рабо-
тать бывает нелегко. К тому же они обычно с трудом поддаются оптими-
зации. При работе с ними очень легко угодить в одну из многочисленных
ловушек, которые они скрывают. К  примеру, можно поддаться соблазну
установить временной фильтр к связи «многие ко многим», чтобы посмот­
реть только строки, актуальные для выбранного периода. Но представьте,
что вы ограничили выбор по строкам лишь по Катерине за 2015 год. Вы по-
прежнему будете видеть две команды – Developers и Sales.
Чтобы решить этот сценарий, необходимо выполнить следующие дей-
ствия в правильной последовательности:
1) определить периоды, в течение которых каждый сотрудник работал
в той или иной команде;
2) распространить фильтр с таблицы дат на таблицу фактов, проследив
за тем, чтобы он пересекся со всеми остальными фильтрами, нало-
женными на таблицу фактов.
Эти две операции необходимо выполнить для каждого сотрудника, по-
скольку все они могли работать в разные периоды времени. Это можно сде-
лать при помощи следующего кода:

HoursWorked :=
SUMX (
ADDCOLUMNS (
SUMMARIZE (
IndividualsTeams;
Individuals[IndividualKey];
IndividualsTeams[FromDate];
IndividualsTeams[ToDate]
);
"FirstDate"; [FromDate];
"LastDate"; IF ( ISBLANK ( [ToDate] ); MAX (
WorkingHours[Date] ); [ToDate] )
);
CALCULATE (
SUM ( WorkingHours[Hours] );
DATESBETWEEN ( 'Date'[Date]; [FirstDate]; [LastDate] );
VALUES ( 'Date'[Date] )
)
)

В этом сценарии вы не сможете воспользоваться связью «многие ко мно-


гим» в модели данных, поскольку временной интервал связи вынудит вас
полагаться на код DAX при распространении фильтра с таблицы-моста на
таблицу фактов. Код получился не самым простым, и он требует от вас по-
214    Связи «многие ко многим»

нимания того, как контекст фильтра распространяется через связи. К тому


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

Рис. 8.14. В отчете показано, сколько часов сотрудники отработали в разных командах

Как мы уже сказали, код получился непростым. Стоит также отметить,


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

Рис. 8.15. В августе и сентябре 2015-го Пол был закреплен сразу за двумя командами

Мы воспользуемся этим сценарием в следующем разделе, в котором об-


судим факторы перераспределения в связях «многие ко многим».
Временные связи «многие ко многим»    215

Факторы перераспределения и процентные соотношения


Как видно по рис. 8.15, Пол, похоже, отработал по 62 часа в авгус­те за две ко-
манды: Sales (продажи) и Testers (тестировщики). Ясно, что это не может быть
правдой. Пол не мог работать в двух командах одновременно. В подобных
сценариях, когда в связях «многие ко многим» есть временные перекрытия,
полезно бывает хранить коэффициент поправки (correction factor) – в нашем
случае он будет показывать, какую долю времени Пол отработал в каждой
команде. Посмотрим на данные в таблице более подробно на рис. 8.16.

Рис. 8.16. В августе и сентябре 2015-го Пол числился в командах Testers и Sales

Информация в этой модели не выглядит корректной. И чтобы 100 % рабо-


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

Рис. 8.17. Дублирование строк позволило избежать перекрытия данных, также был


добавлен процент занятости
216    Связи «многие ко многим»

Теперь рабочие смены Пола разбиты на два неперекрывающихся перио­


да. К  тому же мы добавили поле с  указанием доли занятости сотрудника
в командах. Так, 60 % времени Пола в августе и сентябре мы отвели на ко-
манду Testers, а 40 % – на Sales.
Осталось только принять эти показатели в расчет при подсчете рабочих
часов. Для этого достаточно изменить формулу меры, чтобы она включала
проценты. Итоговый код может выглядеть так:

HoursWorked :=
SUMX (
ADDCOLUMNS (
SUMMARIZE (
IndividualsTeams;
Individuals[IndividualKey];
IndividualsTeams[FromDate];
IndividualsTeams[ToDate];
IndividualsTeams[Perc]
);
"FirstDate"; [FromDate];
"LastDate"; IF ( ISBLANK ( [ToDate] ); MAX (
WorkingHours[Date] ); [ToDate] )
);
CALCULATE (
SUM ( WorkingHours[Hours] );
DATESBETWEEN ( 'Date'[Date]; [FirstDate]; [LastDate] );
VALUES ( 'Date'[Date] )
) * IndividualsTeams[Perc]
)

Как видите, мы добавили столбец Perc в  функцию SUMMARIZE. На  за-


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

Рис. 8.18. В отчете рабочее время Пола распределилось между двумя командами


в процентном отношении
Временные связи «многие ко многим»    217

Произведя эту операцию, мы, по сути, изменили нашу модель данных,


избавившись от временных накладок и перейдя на проценты. Мы вынуж-
дены были это сделать, чтобы уйти от неаддитивной меры. И хотя верно то,
что при наличии связей «многие ко многим» меры в основном получаются
неаддитивными, в данном конкретном случае мы хотели сделать нашу меру
аддитивной для лучшего отображения.
С точки зрения концепции этот важный шаг поможет нам перейти к сле-
дующему этапу оптимизации нашей модели  – материализации связей
«многие ко многим».

Материализация связей «многие ко многим»


Как вы видели в предыдущем примере, в связях «многие ко многим» мо-
гут присутствовать временные данные (обычно со сложными фильтра-
ми), процентные соотношения и факторы перераспределения (reallocation
factors). Все это ведет к значительному усложнению кода на DAX. А в мире
DAX сложно – это все равно что медленно. Если у вас не такая большая база
данных, вы вполне можете воспользоваться этими шаблонами, но для на-
боров данных приличного размера они будут слишком медленными. В сле-
дующем разделе мы отдельно поговорим о производительности моделей
со связями «многие ко многим». Сейчас же мы покажем вам способ изба-
виться от таких связей, если вы хотите повысить эффективность модели
и упростить код на DAX.
Как мы уже говорили, в большинстве случаев вы можете уйти от исполь-
зования связи «многие ко многим», заменив ее на таб­лицу фактов, объеди-
ненную с  двумя измерениями. По  сути, в  нашей модели данных есть два
измерения: Teams и Individuals. Они связаны между собой при помощи таб­
лицы-моста, через которую мы вынуждены проходить с фильтром каждый
раз, когда хотим осуществить срез по команде. Более эффективным реше-
нием здесь было бы хранить ключ команды прямо в таблице фактов, тем
самым материализовав связь «многие ко многим».
Материализация связи (materializing) осуществляется путем денормали-
зации столбцов из таблицы-моста в таблицу фактов, тем самым увеличивая
ее в размерах. В случае с рабочими часами Пола, которые должны быть рас-
пределены в августе и сентябре между двумя командами, необходимо бу-
дет создать по одной строке для каждой команды. В результате мы получим
идеальную схему «звезда», показанную на рис. 8.19.
218    Связи «многие ко многим»

Рис. 8.19. Избавившись от связи «многие ко многим»,


мы пришли к обычной схеме «звезда»

Увеличение количества строк в  таблице фактов потребует от вас до-


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

Использование таблицы фактов в качестве моста


Одна любопытная особенность связей типа «многие ко многим» состоит
в том, что они зачастую появляются там, где вы меньше всего ожидаете
их увидеть. По сути, главным признаком таких связей является наличие
таблицы-моста, объединенной с  двумя измерениями. Но  такая модель
данных встречается куда чаще, чем вы могли бы представить. Фактиче-
ски такие признаки присутствуют в любой классической схеме «звезда».
На рис. 8.20 вы видите пример схемы «звезда», который мы уже рассмат­
ривали в этой книге.
На первый взгляд кажется, что в представленной схеме нет связей «мно-
гие ко многим». Но если задуматься о природе таких связей, то можно за-
метить, что таблица Sales связана сразу с несколькими измерениями и по
своей сути является таблицей-мостом.
Вопросы производительности    219

Рис. 8.20. Схема «звезда» содержит типичную связь «многие ко многим»

Формально любая таблица фактов выполняет функции моста для из-


мерений. Ранее в книге мы использовали эту концепцию множество раз,
хоть и не упоминали о ее сходствах со связями «многие ко многим». И если
в качестве примера вы захотите подсчитать количество покупателей, при-
обретавших конкретный товар, то можете сделать это одним из следую-
щих способов:
 включить двунаправленную фильтрацию у  связи между таблицами
Sales и Customer;
 использовать функцию CROSSFILTER для активации двунаправлен-
ной фильтрации «на лету»;
 применить двунаправленный шаблон с  инструкцией CALCULATE
( COUNTROWS ( Customer ), Sales ).
Любой из приведенных шаблонов DAX позволит получить правильный
результат. Во время вычисления мы фильтруем таб­лицу товаров и подсчи-
тываем/выводим список покупателей, приобретавших выбранные товары.
В  этих трех шаблонах вы легко можете узнать ту же технику, которую мы
использовали для решения сценария со связями «многие ко многим».
С приобретением опыта в моделировании данных вы сможете легко рас-
познавать эти шаблоны в  различных моделях и  применять правильную
технику. Связи «многие ко многим» – это очень мощный инструмент при
построении схем данных, и, как вы узнали из этого короткого раздела, они
появляются в самых разных сценариях.

Вопросы производительности
Ранее в этой главе мы говорили о том, как проектировать модели с нали-
чием связей «многие ко многим». Мы пришли к выводу, что если вам не-
220    Связи «многие ко многим»

обходимо будет выполнять сложную фильтрацию или как-то перераспреде-


лять данные, лучшим вариантом как с точки зрения производительности,
так и в плане сложности решения будет материализовать связь «многие ко
многим» в таблице фактов.
К сожалению, объема этой книги не хватит, чтобы подробно остановить-
ся на вопросах производительности связей «многие ко многим». Но основ-
ные идеи мы постараемся проговорить.
Работая со связями «многие ко многим», в  вашем распоряжении будет
три типа таблиц: измерения, таблицы фактов и  мосты. Чтобы выполнить
вычисления в такой модели, движку необходимо просканировать таблицу-
мост, используя измерение в качестве фильтра, а затем на основании вы-
бранных строк пройтись по таблице фактов. Сканирование таблицы фактов
может занимать какое-то время, но эта ситуа­ция не сильно отличается от
той, когда нам необходимо вычислить показатель в таблице фактов с  на-
прямую связанным с  ней измерением. Таким образом, дополнительные
расходы по времени, необходимые для поддержки связи «многие ко мно-
гим», никак не связаны с размером таблицы фактов. Чем больше объем таб­
лицы, тем дольше будут проводиться вычисления, и связи типа «многие ко
многим» здесь мало чем отличаются от всех остальных.
Размер измерения обычно не влияет на скорость выполнения расчетов,
если он не превышает миллиона строк, а в наших бизнес-решениях такие
объемы встречаются редко. Более того, движку все равно придется скани-
ровать измерение, как бы оно ни было связано с таблицей фактов. Так что
и  размер измерения особенно не  влияет на производительность связей
«многие ко многим».
А как насчет таблицы-моста? Объем этой таблицы, в отличие от измере-
ний и  фактов, действительно оказывает влияние на производительность.
Точнее говоря, не объем таблицы в целом, а количество строк, которые ис-
пользуются для фильтра таблицы фактов. Давайте посмотрим на некоторые
примеры. Предположим, у вас есть измерение с 1000 строк, в таблице-мосте
100 000 строк, а во втором измерении – 10 000, как показано на рис. 8.21.
Вопросы производительности    221

Рис. 8.21. Типичная связь «многие ко многим» с указанием количества строк в таблицах

Как мы сказали, размер таблицы фактов не  имеет значения. В  нашем


случае она насчитывает 100 000 000 записей, но это не должно вас пугать.
Что действительно важно, так это селективность (selectivity) таблицы-моста
в отношении измерения Accounts. Если вы просматриваете информацию по
десяти покупателям, в таблице-мосте отфильтруется порядка ста строк со
счетами. Это весьма приемлемое распределение, и  скорость выполнения
запросов будет высокой. Данный сценарий показан на рис. 8.22.
Таблица фактов фильтруется 10 покупателям соответствуют
по 100 счетам 100 счетов

Выбираем 10 покупателей
Рис. 8.22. Если количество фильтруемых счетов небольшое, скорость будет приемлемой
222    Связи «многие ко многим»

С другой стороны, если селективность фильтрации таблицы-моста бу-


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

Выбираем 10 покупателей
Рис. 8.23. При увеличении количества фильтруемых счетов будет страдать
производительность запросов

В общем случае чем выше селективность таблицы-моста, тем выше про-


изводительность. А  поскольку обычно таблицы-мосты стремятся к  нор-
мальной селективности, можно перефразировать предыдущее высказыва-
ние так: чем больше строк в таблице-мосте, там ниже скорость выполнения
запросов. Это не всегда будет так, но такое правило легче запомнить и при-
менять – чаще всего это даст ожидаемые результаты.
По нашему опыту, таблицы-мосты с количеством записей до миллиона
показывают приемлемое быстродействие, тогда как превышение этого по-
рога ведет к  снижению производительности. Так что, вместо того чтобы
пытаться уменьшить количест­во строк в таблице фактов, лучше обратить
более пристальное внимание на объем таблиц-мостов и постараться что-то
сделать в этой области. Тем самым вы сделаете шаг в сторону оптимизации
производительности связей «многие ко многим».
Заключение    223

Заключение
Необходимо учиться извлекать максимум возможного из связей «многие ко
многим», поскольку они обладают огромным аналитическим потенциалом.
При этом изучение такого типа связей предполагает понимание свойствен-
ных им ограничений как в отношении написания кода на DAX, так и в плане
легкости использования. Главные выводы из этой главы:
 связи типа «многие ко многим» можно использовать посредством
трех шаблонов: двунаправленной фильтрации, применения функ-
ции CROSSFILTER или расширения таб­лицы. Выбор зависит от версии
движка DAX, который вы используете, и от результатов, которые хо-
тите получить;
 базовые принципы связей «многие ко многим» довольно просты.
Поняв их неаддитивную природу и  особенности использования, вы
не будете испытывать проблем с ними;
 каскадные и фильтрующиеся связи «многие ко многим» – тема чуть
более сложная, особенно если вы полагаетесь на расширение табли-
цы. Здесь вам может прийти на помощь выравнивание связей в еди-
ной таблице-мосте – это позволит облегчить написание кода;
 временные связи «многие ко многим» и связи с факторами перерас-
пределения являются довольно сложными по своей природе. Они об-
ладают большим аналитическим потенциалом, но использовать их
непросто;
 если вам предстоит использовать очень сложные связи «многие ко
многим», возможно, лучше будет вовсе от них отказаться. Материа-
лизация таких связей в таблице фактов в большинстве случаев помо-
гает избавиться от сложных отношений в модели данных, даже если
получившаяся таблица фактов окажется более сложной, а количество
строк в ней возрастет. Также в этом случае вам, возможно, придется
пересмотреть уже написанный код на DAX;
 говоря о производительности связей «многие ко многим», стоит вы-
делить главную цель – снижение количест­ва записей в таблице-мос­
те. Уменьшая число записей в таблице, вы тем самым повышаете ее
селективность. Если же ваша таблица-мост объемная, но при этом об-
ладает высокой селективностью, вы также на верном пути.
Глава 9
Работа с разными
гранулярностями

В предыдущих главах мы много говорили о  гранулярности, и  вы, должно


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

Введение в гранулярности
Гранулярность представляет собой уровень детализации, на котором хра-
нится информация в таблице. В традиционной схеме «звезда» гранулярно-
сти определяются измерениями, а  не таб­лицами фактов. Чем больше из-
мерений, тем выше гранулярность. Также чем выше уровень детализации
измерений, тем, опять же, выше гранулярность. Посмотрите на модель дан-
ных, представленную на рис. 9.1.
226    Работа с разными гранулярностями

Рис. 9.1. Типичная схема «снежинка» с четырьмя измерениями и одной таблицей фактов

В этой модели данных гранулярность определяется присутствием из-


мерений Date, Store, Customer и  Product. Измерения Product Subcategory
и  Product Category, будучи составляющими элементами луча «снежинки»,
не оказывают влияния на гранулярность таблицы фактов. В таблице Sales
не должно находиться более одной строки с уникальной комбинацией зна-
чений всех измерений. Если в  таблице фактов присутствует две и  более
строки с одинаковым набором значений измерений, их всегда можно объ-
единить в  одну без потери выразительности модели данных. Посмотрите
на таблицу Sales, изображенную на рис. 9.2. Обратите внимание на то, что
сразу несколько строк содержат одинаковые значения измерений.

Рис. 9.2. Первые восемь строк в таблице совершенно идентичны

В отчете вы не увидите никакой разницы между одинаковыми строками


в таблице. Срез по любому измерению неминуемо приведет к вычислению
Связи на разных уровнях гранулярности    227

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


строк в таблице в одну. При этом в столбце Quantity (количество) появится
значение 8, тогда как остальные поля сохранят свои первоначальные значе-
ния. На первый взгляд это кажется странным, но это правильный вариант.
Выразительность модели не изменится, если уменьшить количество строк
в соответствии с наибольшей необходимой степенью гранулярности. Лиш-
ние строки лишь расходуют место на диске.
Конечно, при добавлении нового измерения ситуация тут же изменится.
Допустим, во всех этих восьми строках была своя акционная скидка. Если
добавить измерение Promotion (акции), уровень гранулярности таблицы
повысится. Измерения, расположенные на лучах «снежинки», не участвуют
в определении гранулярности таблицы фактов, поскольку сами находятся
на более низком уровне детализации, чем измерение, с которым они объ-
единены. Фактически можно сказать, что измерение Product находится на
стороне «многие» связи между таблицами Product и  Product Subcategory.
В базе есть много товаров одной категории. И если добавить поле Product
Subcategory в таблицу фактов, количество строк в ней не изменится.
Проектируя модели данных, всегда принимайте во внимание эти об-
стоятельства. После определения измерений постарайтесь максимально
уменьшить количество строк в  таблице фактов, доведя ее гранулярность
до естественной. Для этого необходимо выполнить группировку данных
с предварительной агрегацией на этапе извлечения информации. В резуль-
тате мы получим модель данных меньшего размера. Точнее сказать, опти-
мального размера – ни большую, ни маленькую.
Обратите внимание, что таблица фактов, показанная на рисунке выше,
не  содержит информацию о  номере заказа. Если добавить в  таблицу но-
мер заказа, значения в  строках изменятся даже для одинаковых наборов
измерений. Например, два заказа по одному и тому же покупателю были
бы сгруппированы вместе, если бы не требовалось учитывать номер заказа.
Но как только вы добавите аналитику по заказам, группировка по строкам
не сможет быть выполнена. Таким образом, присутствие в таблице фактов
детализированной информации влияет на уровень гранулярности таблицы.
Вы можете хранить эти данные в таблице фактов, но должны помнить о том,
что за детализацию информации придется платить ресурсами компью­тера.
Наш совет: храните детальную информацию только в случае, если она вам
может понадобиться при формировании отчетов.

Связи на разных уровнях гранулярности


Теперь, когда мы разобрались с терминологией, давайте рассмотрим при-
мер с разными уровнями гранулярности в разных таблицах фактов. Мы об-
ратимся к сценарию бюджетирования (budgeting), который идеально под-
ходит для этой ситуации.
228    Работа с разными гранулярностями

Анализ данных о бюджетировании


При анализе бюджета мы зачастую сравниваем текущие продажи (в  про-
шлом или нынешнем году) с  планируемыми. Это позволяет нам рассчи-
тать ключевые показатели эффективности (key performance indicators – KPI)
и построить необходимые отчеты. Но при этом мы неизбежно столкнемся
с проблемой гранулярности. Очень маловероятно, что в данных о прогно-
зах у вас будет информация с детализацией до товара и дня, тогда как теку-
щие продажи вы храните именно с такой гранулярностью. Рассмотрим этот
пример подробнее. На рис. 9.3 показана модель данных в форме обычной
звезды вокруг таблицы Sales (продажи), а также таблица Budget (бюджет)
с данными на следующий год.

Рис. 9.3. Таблицы по продажам и бюджету располагаются в одной модели данных

Бюджет мы храним на уровне детализации по стране/региону и бренду.


Очевидно, что нет никакого смысла планировать бюджет по дням. Когда вы
что-то прогнозируете, то делаете это в обобщенном масштабе. То же каса-
ется и товаров. Вам не удастся спланировать продажи по конкретной пози-
ции, за исключением случаев, когда у вас очень ограниченный ассортимент.
В модели данных, представленной на рис. 9.3, ответственный за бюджет вы-
делил два атрибута для составления прогноза: страну и бренд.
Если вы попытаетесь объединить в одном отчете данные из таблиц Sales
и Budget, то сразу заметите неточности в выводе из-за отсутствия связи меж-
ду таблицами. На рис. 9.4 показан отчет со срезом по 2009 году с выводом
бренда в строки (столбец Brand в таблице Product). Но в колонке с бюджетом
данные неверны, поскольку таблицы Product и Budget никак не связаны.
Связи на разных уровнях гранулярности    229

Рис. 9.4. Срез по бренду никак не затрагивает таблицу Budget по причине отсутствия связей

Если вы помните, мы сталкивались с подобным сценарием в первой главе.


Но тогда у вас еще не было достаточных знаний, а теперь мы можем рассмот­
реть этот пример подробнее и предложить варианты для решения проблемы.
Важно помнить, что проблемы с гранулярностью не являются ошибкой
в модели данных. Таблица бюджета существует на своем уровне грануляр-
ности, отличающемся от гранулярности таблицы продаж. При этом обе таб­
лицы спроектированы правильно. Однако осуществить срез по ним одно-
временно довольно проблематично.
Первый вариант, который мы рассмотрим, является простейшим спосо-
бом заставить работать нашу модель. Для этого мы просто понизим уровень
гранулярности в таблице Sales, удалив из нее детальную информацию, ко-
торой нет в таблице Budget. Это можно сделать, модифицировав запрос на
загрузку таблицы Sales с отсечением информации на более глубоком уровне
детализации, которой нет в таблице бюджета. Модель данных, которая у нас
получилась, показана на рис. 9.5.

Рис. 9.5. Упростив таблицы, мы пришли к типичной схеме «звезда»


230    Работа с разными гранулярностями

Чтобы получить эту модель, мы понизили гранулярность таб­лицы Sales,


избавившись от лишней детализации. Мы вынуждены были отбросить дату
продажи, код товара (его мы заменили на бренд) и код магазина (StoreKey),
вместо которого появилось поле CountryRegion. Также предварительно под-
считали продажи, выполнив группировку данных. Наши прежние измере-
ния исчезли, а  вместо них появились два простых измерения с  брендами
и странами. Итоговая модель получилась довольно простой, и отчет по ней,
показанный на рис. 9.6, показывает правильные цифры. Теперь мы можем
осуществлять срез по брендам по таб­лицам Sales и Budget, и цифры в отчете
будут верными.

Рис. 9.6. Поскольку в основе модели лежит простая схема «звезда», цифры в отчете со-
ответствуют действительности

Проблема этого решения заключается в том, что ради построе­ния прос­


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

Использование DAX для распространения фильтра


Следующим вариантом решения сценария является метод с применением
языка DAX. Особенностью модели данных, приведенной на рис. 9.3, являет-
ся то, что хоть мы и можем осуществлять фильтрацию по столбцу Brand из
таблицы Product, этот фильтр не распространяется на таблицу Budget из-за
отсутствия связи между ней и Product.
При помощи языка DAX можно принудительно распространить фильтр
по столбцу Brand в таблице Products на таблицу Budget. И сделать это можно
разными способами в зависимости от вашей версии движка DAX. В Power BI
Связи на разных уровнях гранулярности    231

и Excel 2016 и выше можно воспользоваться для этого специальной функ-


цией. Если вы напишете код для меры Budget 2009 следующим образом, то
увидите правильные данные из таблицы бюджета по брендам и странам:

Budget 2009 :=
CALCULATE (
SUM ( Budget[Budget] );
INTERSECT ( VALUES ( Budget[Brand] ); VALUES ( 'Product'[Brand] ) );
INTERSECT ( VALUES ( Budget[CountryRegion] ); VALUES (
Store[CountryRegion] ) )
)

Функция INTERSECT строит набор данных на пересечении значений


Product[Brand] и  Budget[Brand]. Поскольку таблица Budget, не  будучи свя-
занной с Product, не может быть отфильтрована, в результирующем наборе
мы получим пересечение всех значений Brand из таблицы Budget и только
видимых из таблицы Product. Иными словами, фильтр, установленный на
таблицу Product, распространится на таблицу Budget по полю Brand. А по-
скольку у нас в формуле две функции INTERSECT, таблица бюджета получит
также фильтр по полю CountryRegion из измерения Store.
Эта техника похожа на динамическую сегментацию, о которой мы будем
говорить в главе 10. Поскольку у нас нет связи между таблицами и мы не мо-
жем ее создать, мы вынуждены моделировать эту связь посредством языка
DAX – пусть пользователь модели думает, что связь между таблицами есть.
В Excel 2013 функция INTERSECT недоступна, так что нам придется вос-
пользоваться техникой на основе функции CONTAINS, как показано ниже:

Budget 2009 Contains =


CALCULATE (
SUM ( Budget[Budget] );
FILTER (
VALUES ( Budget[Brand] );
CONTAINS (
VALUES ( 'Product'[Brand] );
'Product'[Brand];
Budget[Brand]
)
);
FILTER (
VALUES ( Budget[CountryRegion] );
CONTAINS (
VALUES ( Store[CountryRegion] );
Store[CountryRegion];
Budget[CountryRegion]
)
)
)
232    Работа с разными гранулярностями

Этот код чуть сложнее, чем предыдущий, где была использована функция
INTERSECT, но если вам необходимо реализовать такую функциональность
в Excel 2010 или Excel 2013, это лучший способ. В отчете на рис. 9.7 показано,
что разные техники дали абсолютно идентичный результат.
Техника, описанная в  этом разделе, не требует изменения модели дан-
ных, поскольку целиком полагается на язык DAX. Решение вполне рабочее,
но код на DAX писать бывает довольно трудно, особенно для старых версий
Excel. К тому же формулы могут еще больше усложниться, если вам потре-
буется использовать для фильтра не два, а больше атрибутов. Фактически
вам придется добавлять по целой инструкции с функцией INTERSECT для
каждого столбца, определяющего гранулярность таблицы бюджета.

Рис. 9.7. Столбцы с бюджетами показывают одинаковые данные

Еще одной проблемой такой меры является ее производительность.


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

Budget 2009 :=
CALCULATE (
SUM ( Budget[Budget] );
TREATAS ( VALUES ( Budget[Brand] ); 'Product'[Brand] );
TREATAS ( VALUES ( Budget[CountryRegion] ); Store[CountryRegion] )
)

Функция TREATAS работает очень похоже на INTERSECT. При этом она


быст­рее, чем INTERSECT, но значительно медленнее по сравнению с исполь-
зованием связей. Об этом варианте мы расскажем в следующем разделе.
Связи на разных уровнях гранулярности    233

Фильтрация при помощи связей


В предыдущем разделе мы рассмотрели вариант решения сценария бюджети-
рования при помощи языка DAX. Сейчас мы выполним ту же задачу посред-
ством изменения модели данных и организации необходимых связей. Идея
состоит в  сочетании первой техники со снижением гранулярности таблицы
Sales с созданием двух новых измерений с переходом на схему «звезда».
Для начала используем следующий код на DAX для создания двух изме-
рений: Brands и CountryRegions.

Brands =
DISTINCT (
UNION (
ALLNOBLANKROW ( Product[Brand] );
ALLNOBLANKROW ( Budget[Brand] )
)
)

CountryRegions =
DISTINCT (
UNION (
ALLNOBLANKROW ( Store[CountryRegion] );
ALLNOBLANKROW ( Budget[CountryRegion] )
)
)

После этого мы можем настроить необходимые связи в модели с новыми


измерениями – для таблицы Sales они будут располагаться на лучах «снежин-
ки», а с таблицей Budget будут связаны напрямую, как показано на рис. 9.8.

Рис. 9.8. Дополнительные измерения Brands и CountryRegions позволили решить


проблему с гранулярностью
234    Работа с разными гранулярностями

В обновленной модели данных мы можем использовать столбец Brand


из измерения Brands или CountryRegion из CountryRegions для осуществле-
ния одновременного среза таб­лиц Sales и Budget. Но нужно проявить боль-
шую осторожность, чтобы выбрать правильные поля. Если вы, к  примеру,
выберете столбец Brand из измерения Product, то не сможете сделать срез
по нему в таблице Brands, а следовательно, и в Budget, поскольку для связи
установлено неподходящее направление распространения фильтра. Поэто-
му хорошей практикой является скрытие от пользователя полей, которые
выполняют частичную или нежелательную фильтрацию модели. Если вы
хотите сохранить предыдущую модель данных, то в  ней вам необходимо
скрыть поле CountryRegion в таблицах Budget и Store и столбец Brand в таб­
лицах Product и Budget.
Хорошей новостью является то, что в Power BI у вас есть полный конт­
роль над распространением двунаправленной фильт­рации. Таким обра-
зом, вы можете включить эту опцию для связей между таблицами Product
и Brands, а также между Store и CountryRegions. Получившаяся модель по-
казана на рис. 9.9.
На первый взгляд нет никакой разницы между моделями на последних
двух рисунках. Да, таблицы в  них одинаковые, но связи установлены по-
разному. Связи между таблицами Product и  Brands, а  также между Store
и CountryRegions имеют двунаправленный характер.

Рис. 9.9. В этой модели данных таблицы Brands и CountryRegions скрыты, а их связи
с Product и Store – двунаправленные

Более того, мы скрыли от пользователя таблицы Brands и CountryRegions,


потому что они по своей сути превратились во вспомогательные табли-
Связи на разных уровнях гранулярности    235

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


конечному пользователю. После установки среза по полю Brand в таблице
Product двунаправленный фильтр распространится с  таблицы Product на
Brands, а далее – на Budget. Связь между таблицами Store и CountryRegions
будет вести себя точно так же. Таким образом, мы получили модель данных,
в  которой фильтры, установленные в  измерениях Product или Store, рас-
пространяют свое действие на Budget, а поскольку две технические таблицы
скрыты, пользователь не испытает затруднений при работе с моделью.
Использованная техника может похвастаться приличной производи-
тельностью. Поскольку фильтры здесь основаны на связях, при их рас-
пространении задействуется подсистема DAX, отличающаяся высоким
быстродействием. Кроме того, фильтры применяются только тогда, когда
это необходимо (в  предыдущем примере, где мы использовали функцию
FILTER, это было не так, ведь фильтрация выполнялась вне зависимости от
текущего выбора в измерениях). В результате мы получили решение с хо-
рошей скоростью вычислений. А  поскольку проблема с  гранулярностями
была решена в самой модели, для агрегации мер мы можем использовать
простую функцию SUM, без сложных выражений CALCULATE и вложенных
фильтраций. С точки зрения внедрения и поддержки это очень важно, так
как в  новые формулы вам не  придется вставлять одни и  те же шаблоны
фильтра, как было в предыдущих моделях.

Скрытие значений на недопустимых уровнях гранулярности


В предыдущих разделах мы пытались решить проблемы с гранулярностью
в  модели данных путем снижения гранулярности таблицы Sales до уров-
ня Budget с потерей выразительности модели. После этого мы объединили
две таблицы фактов в одной модели посредством промежуточных скрытых
измерений, что позволило пользователю фильтровать данные в таблицах
Budget и Sales. И хотя он сможет осуществлять срезы в бюджетировании по
бренду, по другим атрибутам, скажем по цвету товара, построить отчет он
не сможет. Фактически цвет товара и бренд размещены на разных уровнях,
и в таблице Budget отсутствует информация на уровне гранулярности цве-
та товара. Позвольте продемонстрировать это на примере. Если построить
простой отчет по таблицам Sales и Budget со срезом по цвету, вы получите
результат, показанный на рис. 9.10.
Вы могли наблюдать похожее поведение мер во многих шаб­лонах «мно-
гие ко многим». Вот что на самом деле происходит. В отчет не выводится
информация по бюджетированию товаров конкретных цветов, поскольку
данные о  товарах в  таблице Budget просто отсутствуют. Там есть только
бренды. Фактически цифры, которые мы видим во второй колонке, отра-
жают бюджет по всем брендам, в  которых есть как минимум один товар
заданного цвета. И  проблем с  этими цифрами ровно две. Во-первых, они
неправильные. А во-вторых, очень трудно понять, что они неправильные.
236    Работа с разными гранулярностями

Рис. 9.10. Мера Sales Amount аддитивная, тогда как Budget 2009 – нет. В результате
сумма значений во втором столбце намного превышает итог

Вам бы очень не хотелось, чтобы такие отчеты формировались на осно-


вании вашей модели данных. В лучшем случае пользователи пожалуются на
полученные цифры, а в худшем – станут принимать какие-то решения на
основании этих показателей. На вас как на специалисте по моделированию
данных лежит ответственность за то, чтобы цифры, которые не могут быть
правильно посчитаны, не  выводились в  отчет. Иными словами, в  вашем
коде должна быть заложена определенная логика, позволяющая проверить,
что результирующие данные соответствуют действительности. Неправиль-
ные цифры выводить просто нельзя.
Как вы, наверное, догадались, следующим вопросом будет такой: а  как
узнать, что ту ли иную цифру выводить нельзя? На самом деле все довольно
просто – от вас потребуется лишь минимальное знание языка DAX. В целом
задача сводится к тому, чтобы понять, показываются ли в  вашей сводной
таблице (или отчете) данные за пределами гранулярности, на которой циф-
ры обладают практическим смыслом. Если речь идет о более высоком уров-
не гранулярности, мы агрегируем значения, что вполне приемлемо. Если
о более низком – разбиваем значения, основываясь на гранулярности, даже
если показываем их на уровне с большей детализацией. В таком случае не-
обходимо выводить значение BLANK (пусто), чтобы уведомить пользовате-
ля о том, что мы не знаем правильного ответа.
Ключом к решению такого сценария является нахождение количества вы-
бранных товаров (или магазинов) на уровне гранулярности таблицы Sales
и сравнение его с количеством выбранных элементов на уровне грануляр-
ности таблицы Budget. Если полученные значения равны, значит, фильтр
Связи на разных уровнях гранулярности    237

по товарам даст достоверную информацию в обеих таблицах фактов. В про-


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

ProductsAtSalesGranularity := COUNTROWS ( Product )

ProductsAtBudgetGranularity :=
CALCULATE (
COUNTROWS ( Product );
ALL ( Product );
VALUES ( Product[Brand] )
)

В мере ProductsAtSalesGranularity подсчитывается количество товаров на


максимальном уровне гранулярности, то есть для конкретных товаров. Таб­
лицы Sales и Product связаны между собой именно на этом уровне грану-
лярности. Мера ProductsAtBudgetGranularity отвечает за количество товаров
с учетом фильтра по полю Brand и удалением всех остальных установлен-
ных фильтров. Это, по сути, и есть гранулярность таблицы Budget. Вы мо-
жете понаблюдать за разницей между значениями этих двух мер, построив
отчет со срезом по бренду и цвету товара, который показан на рис. 9.11.

Рис. 9.11. В отчете показано количество товаров на разных уровнях гранулярности

Одинаковые значения мер наблюдаются только в тех строках, где уста-


новлен единственный фильтр по бренду. Иными словами, когда в таблице
Product сделан срез по гранулярности таб­лицы Budget. То же самое нужно
сделать и с измерением Store по гранулярности страны или региона. Созда-
дим две меры для проверки гранулярности на уровне магазина с использо-
ванием следующего кода:
238    Работа с разными гранулярностями

StoresAtSalesGranularity := COUNTROWS ( Store )

StoresAtBudgetGranularity :=
CALCULATE (
COUNTROWS ( Store );
ALL ( Store );
VALUES ( Store[CountryRegion] )
)

Построив отчет, вы увидите одинаковые значения у мер на уровне грану-


лярности таблицы бюджета и выше, как показано на рис. 9.12.

Рис. 9.12. В отчете показано количество магазинов на разных уровнях гранулярности

Фактически значения мер равны не только для отдельных стран, но и для


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

Budget 2009 :=
IF (
AND (
[ProductsAtBudgetGranularity] = [ProductsAtSalesGranularity];
[StoresAtBudgetGranularity] = [StoresAtSalesGranularity]
);
SUM ( Budget[Budget] )
)
Связи на разных уровнях гранулярности    239

Таким образом, мы удостоверились, что цифры в отчете будут выводить-


ся только в случае, когда мы не опускаемся ниже уровня гранулярности таб­
лицы Budget. Результат показан на рис. 9.13. Бюджет правильно рассчиты-
вается на уровне брендов и не показывается на уровне цвета товаров.

Примечание. Когда вы выводите в отчет таблицы фактов с разны-


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

Рис. 9.13. В отчете показываются пустые значения для уровней гранулярности ниже


корректного

Распределение значений по уровням с большей


гранулярностью
В предыдущем примере мы научились скрывать значения на уровнях грану-
лярности, не поддерживаемых моделью данных. Эта техника помогает избе-
жать показа неправильных цифр в отчетах. Но в некоторых сценариях мож-
но сделать в  этом отношении чуть больше, а  именно вычислять значения
на более высоких уровнях гранулярности с использованием коэффициента
распределения (allocation factor). К примеру, вы не знаете бюджет по синим
товарам от компании Adventure Works, а знае­те только суммарный бюджет
по этому бренду. Но зато вы можете выяснить процент по этим товарам от
итоговых продаж по бренду. Это и будет коэффициент распределения.
К примеру, можно рассчитать этот коэффициент по продажам синих това-
ров в сравнении со всеми товарами за предыдущий год. Вместо того чтобы
говорить об этом, лучше посмот­реть на отчет, представленный на рис. 9.14.
240    Работа с разными гранулярностями

Рис. 9.14. В колонке Allocated Budget значения на более высоком уровне гранулярности


рассчитываются «на лету»

Давайте внимательно присмотримся к этому отчету. До этого мы исполь-


зовали в расчетах поле Sales 2009, а сейчас перешли на Sales 2008, поскольку
решили вычислять коэффициент именно в  сравнении с  предыдущим го-
дом. Сами значения отражают отношение величины продаж по заданному
цвету в 2008 году к общим продажам по всем товарам на уровне грануляр-
ности таблицы Budget.
Мы видим, например, что по синим товарам бренда Adventure Works
в 2008 году продажи составили $8603.64, а при делении на общие продажи
по бренду, составляющие $93 587.00, мы получим 9.19% – это и  есть доля
синих товаров компании в общей массе продаж. Бюджет по синим товарам
на 2009 год нам неизвестен, но мы можем вычислить его путем умножения
общего бюджета по Adventure Works на полученную на предыдущем шаге
долю. В результате получим значение $6168.64.
Вычислять значения просто, когда вы понимаете, что такое грануляр-
ность. Формулы, которые мы использовали в  этом примере, приведены
ниже:

Sales2008AtBudgetGranularity :=
CALCULATE (
[Sales 2008];
ALL ( Store );
VALUES ( Store[CountryRegion] );
ALL ( Product );
VALUES ( Product[Brand] )
)

AllocationFactor := DIVIDE ( [Sales 2008]; [Sales2008AtBudgetGranularity] )

Allocated Budget := SUM ( Budget[Budget] ) * [AllocationFactor]


Заключение    241

Ключ к  вычислениям содержится в  мере Sales2008AtBudgetGranularity,


в которой продажи рассчитываются после удаления всех фильтров с таблиц
Store и Product, кроме столбцов, определяющих гранулярность на уровне
таблицы Budget. Две другие меры содержат простые функции деления
и умножения.
Техника распределения значений на более высоких уровнях гранулярно-
сти представляет большой интерес и дает пользователю ощущение того, что
цифры в модели присутствуют на более высоких уровнях, чем это есть на
самом деле. Однако если вы собираетесь использовать эту технику, то долж-
ны подробно рассказать людям, принимающим решения, как именно рас-
считываются эти значения. В конце концов, эти показатели получены пу-
тем вычислений, а не были введены вручную при планировании бюджета.

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

В предыдущей главе мы научились моделировать данные при помощи


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

Вычисление связей по нескольким столбцам


Первой темой, которую мы рассмотрим, будет создание вычисляемых физи-
ческих связей (calculated physical relationships). Единственным отличием та-
ких связей от обычных является то, что они будут построены на основании
вычисляемых столбцов. Когда связи в модели не могут быть установлены по
причине отсутствия ключевых столбцов или необходимости рассчитывать
их по сложным формулам, на помощь придут вычисляемые столбцы, кото-
рые могут служить основанием для связей. Несмотря на то что базироваться
244    Сегментация данных в модели

связи в  этом случае будут на вычисляемых столбцах, сами они будут при
этом физическими.
Табличный движок позволяет создавать связи только по одному столбцу
и не поддерживает отношения между таблицами сразу по нескольким по-
лям. Но такая возможность очень полезна и может пригодиться вам в самых
разных ситуациях. Если вы хотите применить это на практике, у вас есть два
варианта:
 создать вычисляемый столбец, сочетающий в себе несколько полей,
и использовать его в качестве ключа для связи;
 денормализовать столбцы из целевой таблицы (представляющей
в связи сторону «один») с использованием функции LOOKUPVALUE.
Представьте, что вы вводите акцию «Товар дня», по которой в определен-
ные дни тот или иной товар будет продаваться со скидкой, как показано на
рис. 10.1.

Рис. 10.1. Таблицы SpecialDiscounts и Sales необходимо связать по двум столбцам

В таблице акций (SpecialDiscounts) содержатся три столбца: ProductKey


(код товара), OrderDateKey (дата) и  Discount (скидка). Если вам понадо-
бится использовать эту информацию для вычисления общей скидки, вы
столкнетесь с  проблемой, ведь для каждой конкретной продажи величи-
на скидки зависит сразу от двух полей: ProductKey и  OrderDateKey. Полу-
чается, что стандартными средствами вы не можете связать таблицы Sales
и SpecialDiscounts, поскольку табличный движок не поддерживает связи по
нескольким полям.
Одним из решений этого сценария является создание вычисляемого столб-
ца, на основании которого можно построить связь. Пусть движок не поддер-
живает отношения между таблицами по нескольким полям, но вы всегда
можете объединить эти поля в  один вычисляемый столбец и  использовать
Вычисление связей по нескольким столбцам    245

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

Sales[SpecialDiscountKey] = Sales[ProductKey] & "-" & Sales[OrderDateKey]

Для таблицы SpecialDiscounts столбец создается аналогично. После опре-


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

Рис. 10.2. Вы можете использовать вычисляемый столбец для создания связи

Другим способом достичь того же эффекта является использование функ-


ции LOOKUPVALUE. С ее помощью вы сможете денормализовать скидку не-
посредственно в таблице фактов, определив в таблице Sales вычисляемый
столбец со следующей формулой:

Sales[SpecialDiscount] =
LOOKUPVALUE (
246    Сегментация данных в модели

SpecialDiscounts[Discount];
SpecialDiscounts[ProductKey]; Sales[ProductKey];
SpecialDiscounts[OrderDateKey]; Sales[OrderDateKey]
)

Здесь мы не создаем связь между таблицами. Вместо этого мы переносим


скидку в таблицу фактов, используя поиск. Говоря техническим языком, де-
нормализуем столбец SpecialDiscount из таблицы SpecialDiscounts в Sales.
Оба варианта вполне рабочие, и  выбор между ними зависит от разных
факторов. Если поле со скидкой единственное в таб­лице SpecialDiscounts,
которое вам нужно будет использовать, вариант с денормализацией будет
предпочтительным. В  этом случае будет создан всего один вычисляемый
столбец, в котором будет не так много уникальных значений по сравнению
с двумя и более столбцами. Таким образом вы сможете сэкономить память
и облегчить написание кода.
Если же в таблице SpecialDiscounts содержится много столбцов, которые
вам необходимо использовать, то их денормализация в  таблице фактов
приведет к  чрезмерному использованию памяти и, возможно, снижению
производительности. В  этом случае лучше будет использовать вычисляе-
мый столбец с составным ключом.
Первый пример был очень важен, поскольку позволил нам продемонст­
рировать особенности применения языка DAX для создания вычисляемых
столбцов, которые впоследствии можно использовать в качестве основания
для связи. Таким образом, вы можете создавать любые связи в  своей мо-
дели данных, если ключи для них можно вычислить и  сохранить в  столб-
це. В  следующем примере мы рассмотрим создание связей на основании
статических диапазонов. Расширив эту концепцию, вы сможете создавать
практически любые связи в моделях данных.

Вычисление статической сегментации


Статическая сегментация (static segmentation) представляет собой очень
распространенный сценарий, в  котором вам необходимо анализировать
информацию не по значениям в таблице, поскольку их может быть великое
множество, а с разбивкой по сегментам или группам. В качестве типичных
примеров сегментирования можно привести анализ продаж по возрастным
группам покупателей или по цене из прайса. Нет никакого смысла анали-
зировать продажи по уникальным ценам из прайса, поскольку он содержит
огромное количество уникальных значений. Но если разбить цены по груп-
пам, можно извлечь весьма полезную аналитическую информацию.
В следующем примере мы рассмотрим таблицу PriceRanges, в  которой
хранятся цены из прайса по группам. Для каждой группы обозначены свои
непересекающиеся диапазоны, как показано на рис. 10.3.
Вычисление статической сегментации    247

Рис. 10.3. Конфигурационная таблица с диапазонами цен

Здесь, как и в предыдущем примере, мы не можем создать прямую связь


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

Sales[PriceRange] =
CALCULATE (
VALUES ( PriceRanges[PriceRange] );
FILTER (
PriceRanges;
AND (
PriceRanges[MinPrice] <= Sales[Net Price];
PriceRanges[MaxPrice] > Sales[Net Price]
)
)
)

Стоит отметить, что использование функции VALUES здесь позволяет


извлечь непосредственно значения. В  основном эта функция возвращает
таблицу, но если в результате вычисления остается одна строка и один стол-
бец, результат автоматически преобразуется в  скалярное значение, если
выражение того требует.
Функция FILTER в этой формуле всегда будет возвращать одну строку из
конфигурационной таблицы, а значит, функция VALUES гарантированно бу-
дет ее обрабатывать и возвращать посредством CALCULATE название теку-
щего ценового диапазона. Очевидно, что это решение будет работать только
в случае правильной структуры конфигурационной таблицы. Если же в этой
таблице будут пропуски или пересечения диапазонов, функция VALUES мо-
жет вернуть несколько строк, и результат может оказаться ошибочным.
В связи с  этим лучше будет добавить в  наш код соответствующую про-
верку на правильность конфигурационной таблицы и в случае ошибки вы-
водить соответствующее сообщение, как показано ниже:
248    Сегментация данных в модели

Sales[PriceRange] =
VAR ResultValue =
CALCULATE (
IFERROR (
VALUES ( PriceRanges[PriceRange] );
"Overlapping Configuration"
);
FILTER (
PriceRanges;
AND (
PriceRanges[MinPrice] <= Sales[Net Price];
PriceRanges[MaxPrice] > Sales[Net Price]
)
)
)
RETURN
IF (
ISEMPTY ( ResultValue );
"Wrong Configuration";
ResultValue
)

В представленном коде отлавливаются как пересекающиеся диапазоны


(при помощи внутренней функции IFERROR), так и пропуски в конфигура-
ционной таблице (посредством вызова функции ISEMPTY перед возвраще-
нием результата). Этот код гарантированно вернет адекватное значение,
а  значит, его использование является более безопасным по сравнению
с предыдущим фрагментом.
Вычисляемые физические связи являются мощнейшим инструментом
моделирования в Power BI и Excel, поскольку позволяют строить очень за-
мысловатые отношения между таблицами. К  тому же вычисление связей
в этом случае происходит на этапе обновления данных, а не в момент за-
проса, что положительно сказывается на производительности модели.

Использование динамической сегментации


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

Представьте, что вам необходимо разбить покупателей на группы по объ-


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

Рис. 10.4. Конфигурационная таблица для выполнения динамической сегментации

Теперь нам понадобится мера для вычисления количества покупателей,


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

CustInSegment :=
COUNTROWS (
FILTER (
Customer;
AND (
[Sales Amount] > MIN ( Segments[MinSale] );
[Sales Amount] <= MAX ( Segments[MaxSale] )
)
)
)

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


тором сегменты вынесены в строки, а годы – в столбцы. Отчет представлен
на рис. 10.5.
250    Сегментация данных в модели

Рис. 10.5. Сводная таблица демонстрирует динамическую сегментацию в действии

Обратите внимание на ячейку с цифрой 76, показывающей, сколько по-


купателей в  2008 году относились к  группе Medium (Средняя). Формула
проходит по таблице Customer и  для каждого покупателя проверяет, по-
падает ли значение Sales Amount по нему в интервал между минимальным
значением столбца MinSale и максимальным значением MaxSale. При этом
значение Sales Amount здесь отражает сумму продаж этому конкретному
покупателю из-за перехода между контекстами. Получившаяся мера, как
и ожидалось, будет аддитивной по сегментам и покупателям и неаддитив-
ной по остальным измерениям.
Формула будет работать правильно только в случае выбора всех сегмен-
тов. Если вы, к примеру, выберете только Very Low (Очень низкая) и Very
High (Очень высокая), убрав остальные три сегмента из выбора, то функ-
ции MIN и MAX вернут неправильные результаты. В них будут учитываться
все покупатели, что приведет к ошибочному подсчету итогов, как показано
на рис. 10.6.

Рис. 10.6. В сводной таблице показываются ошибочные результаты при неполном вы-


боре сегментов

Если вы хотите позволить пользователю выбирать определенные сегмен-


ты, то необходимо переписать формулу следующим образом:

CustInSegment :=
SUMX (
Segments;
COUNTROWS (
FILTER (
Customer;
AND (
[Sales Amount] > Segments[MinSale];
Понимание потенциала вычисляемых столбцов: ABC-анализ    251

[Sales Amount] <= Segments[MaxSale]


)
)
)
)

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


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

Рис. 10.7. В итогах две меры выдают разные цифры из-за частичного выбора сегментов

Виртуальные связи (virtual relationships) являются очень мощным


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

Совет. Вы можете попробовать применить показанные концепции


в вашей модели и посмотреть, как эти шаблоны отработают в от-
ношении ваших группировок данных.

Понимание потенциала вычисляемых


столбцов: ABC-анализ
Вычисляемые столбцы хранятся в базе данных. С точки зрения моделиро-
вания это огромный плюс, поскольку открывает перед нами новые возмож-
ности. В этом разделе мы рассмотрим некоторые сценарии, которые можно
успешно решить при помощи вычисляемых столбцов.
В качестве примера эффективного использования вычисляемых столб-
цов рассмотрим построение ABC-анализа (ABC analysis) в  среде Power BI.
Этот вид анализа базируется на законе Парето, и его иногда называют ABC/
Парето-анализ. Это очень распространенная техника выделения ключевых
для компании аспектов деятельности, будь то товары или покупатели. В на-
шем сценарии мы остановимся на товарах.
252    Сегментация данных в модели

Целью ABC-анализа является выявление приоритетных для компании


товаров, чтобы менеджеры могли уделить им больше внимания. Для этого
все товары разделяются на три условные категории A, B и C по следующим
критериям:
 товары из категории A приносят компании 70 % прибыли;
 товары из категории B приносят компании 20 % прибыли;
 товары из категории C приносят компании 10 % прибыли.
Категорию товара мы будем хранить в вычисляемом столбце измерения
Product, поскольку намереваемся использовать его в отчетах в качестве сре-
за. На рис. 10.8 показана простейшая сводная таблица, в которой категория
товаров вынесена в строки.

Рис. 10.8. В отчете представлены категории товаров и суммарная прибыль по ним

Как часто бывает с ABC-анализом, мы видим, что в самую прибыльную


категорию A попала небольшая доля ассортимента. Эти товары представ-
ляют основу бизнеса компании Contoso. Товары из категории B обладают
меньшей значимостью для компании, но все же они важны. В категории C
содержатся основные кандидаты на исключение из ассортимента, посколь-
ку прибыль от них составляет минимальную долю в сравнении с ходовыми
позициями.
Модель данных в этом сценарии крайне проста. Нам нужны только това-
ры и продажи, как показано на рис. 10.9.

Рис. 10.9. Модель данных для ABC-анализа по товарам очень простая


Понимание потенциала вычисляемых столбцов: ABC-анализ    253

Мы изменим модель данных, добавив несколько вычисляемых столбцов.


При этом нам не понадобятся новые таблицы или связи. Чтобы определить
категорию товара, нужно сначала подсчитать полную прибыль по нему
и сравнить ее с итоговой прибылью. Так мы получим долю прибыли по то-
вару. После этого необходимо отсортировать товары по полученной доле
и вычислить прибыль нарастающим итогом. Как только сумма нарастающе-
го итога достигнет отметки в 70 %, категория A заканчивается. Следующие
20 % прибыли (до 90 %) относятся к категории B, а остаток товаров будет
принадлежать категории C. При этом все расчеты будут выполнены исклю-
чительно в вычисляемых столбцах.
Для начала добавим вычисляемый столбец с прибылью по товарам в таб­
лицу Product:

Product[TotalMargin] =
SUMX (
RELATEDTABLE( Sales );
Sales[Quantity] * ( Sales[Net Price] - Sales[Unit Cost] )
)

На рис.  10.10 показана таблица с  новым вычисляемым столбцом


TotalMargin и выполненной по нему сортировкой.

Рис. 10.10. TotalMargin – новый вычисляемый столбец в таблице Product

На следующем шаге создадим вычисляемый столбец с нарастающим ито-


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

Product[MarginRT] =
VAR
CurrentTotalMargin = 'Product'[TotalMargin]
RETURN
SUMX (
FILTER (
'Product';
'Product'[TotalMargin] >= CurrentTotalMargin
);
'Product'[TotalMargin]
)

На рис. 10.11 показан список товаров с новым вычисляемым столбцом.

Рис. 10.11. В столбце MarginRT рассчитывается нарастающий итог по полю TotalMargin

На заключительном шаге мы вычисляем столбец с процентом нарастаю-


щего итога. Формула для него представлена ниже:

Product[MarginPct] = DIVIDE ( 'Product'[MarginRT]; SUM (


'Product'[TotalMargin] ) )

На рис. 10.12 показан новый вычисляемый столбец, отформатированный


в виде процентов для большей ясности.
Понимание потенциала вычисляемых столбцов: ABC-анализ    255

Рис. 10.12. В столбце MarginPct вычисляется процент по нарастающему итогу

Ну и осталось преобразовать проценты в название категории. Если вы ис-


пользуете границы в 70, 20 и 10 %, формула будет довольно простой:
Product[ABC Class] =
IF (
'Product'[MarginPct] <= 0.7;
"A";
IF (
'Product'[MarginPct] <= 0.9;
"B";
"C"
)
)

Результат показан на рис. 10.13.

Рис. 10.13. Результат классификации содержится в вычисляемом столбце ABC Class


256    Сегментация данных в модели

Поскольку столбец ABC Class хранится непосредственно в базе данных,


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

Примечание. Больше информации об ABC-анализе можно найти


по адресу: http://en.wikipedia.org/wiki/ABC_analysis.

Заключение
В этой главе мы пошли чуть дальше использования обычных связей и рас-
смотрели разные техники сегментации данных при помощи языка DAX.
Важные моменты, которые вы должны были усвоить из этой главы:
 вычисляемые столбцы могут быть использованы для создания на их
основании вычисляемых связей. Потенциал вычисляемых столбцов
состоит в возможности строить связи на основании вычислений лю-
бой степени сложности, не ограничиваясь при этом условием равен-
ства значений, доступным в движке по умолчанию;
 если связь не может быть создана по причине зависимости от дина-
мически меняющихся данных, вы можете прибегнуть к помощи вир-
туальных связей. Пользователь не  должен заметить разницу между
обычными связями и виртуальными, при этом последние будут рас-
считываться «на лету». При работе с  виртуальными связями может
пострадать быстродействие системы, но гибкость, которую вы полу-
чите в свое распоряжение, перевесит любые неудобства;
 вычисляемые столбцы являются полезным дополнением к  таблич-
ному движку. Используя их, вы сможете выполнять сложную сегмен-
тацию данных с  несколькими вычисляемыми столбцами, значения
которых будут рассчитываться в момент обновления модели. Таким
образом можно добиться приемлемого компромисса между скоро-
стью и гибкостью, что позволит вам создавать поистине мощнейшие
модели данных.
Надеемся, рассмотренные примеры помогли вам понять, что примене-
ние творческого подхода способно помочь в  построении действительно
первоклассных моделей.
Глава 11
Работа с несколькими валютами

В этой главе мы рассмотрим модели данных, общей особенностью которых


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

Введение в различные сценарии


Как мы уже сказали, необходимость вести учет в нескольких валютах таит
в  себе определенную опасность. Многие крупные компании оперируют
платежами в разных валютах, курсы которых, как мы знаем, меняются изо
дня в день. В результате появляется необходимость конвертировать валю-
ты и сравнивать показатели в разных валютах. Давайте рассмотрим прос­
той пример. Представьте, что 20 января компания Contoso получила от по-
купателя денежный перевод в размере 100 евро. Как нам конвертировать
эту сумму в  доллары, являющиеся для компании базовой валютой? Есть
следующие способы:
 переводить евро в доллары сразу в  момент проведения опера-
ции. Это простейший вариант работы с валютами, поскольку он по-
зволяет нам, по сути, прийти к единой валюте в системе;
 хранить полученные деньги в евро и расплачиваться ими в этой
валюте при необходимости. Это затруднит процесс формирования
отчетов в  разных валютах, поскольку фактическая сумма операции
будет меняться с каждым днем в зависимости от текущего курса;
258    Работа с несколькими валютами

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


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

Примечание. В  условиях той или иной политики некоторые из


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

Определяя точку во времени, когда будет проводиться пересчет валют


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