Uploaded by Саня

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

advertisement
Признанные эксперты в области DAX Альберто Феррари
и Марко Руссо научат вас максимально эффективно
проектировать модели данных
Читая эту книгу, вы:
• освоите базовые концепции моделирования
данных, включая таблицы, связи и ключи;
• познакомитесь с распространенными схемами
данных «звезда» и «снежинка» и общими
техниками моделирования;
• усвоите важность гранулярности;
• узнаете, как использовать несколько таблиц
фактов (например, продажи и закупки) в единой
модели данных;
• научитесь производить расчеты с календарем,
используя таблицы с датами;
• освоите отслеживание исторических атрибутов,
таких как адреса покупателей или привязку
клиентов к менеджерам;
• узнаете, как использовать снимки для подсчета
количества товаров в наличии;
• научитесь эффективно работать с несколькими
валютами одновременно;
• приобретете знания для анализа событий
с определенной длительностью, включая
пересекающиеся интервалы;
• сможете определить, какая модель данных лучше
отвечает вашей специфике работы.
Для пользователей Microsoft Excel среднего и
продвинутого уровня.
О книге:
- для пользователей Excel и
Power BI, желающих максимально повысить эффективность
использования этих средств
разработки;
- для профессионалов в
бизнес-аналитике, ищущих
новые идеи в области
моделирования данных.
Об авторах:
Альберто Феррари и Марко
Руссо – основатели сайта sqlbi.
com, на котором регулярно
публикуются свежие статьи по
Microsoft Power Pivot, Power BI,
DAX и SQL Server Analysis Services. Также Альберто и Марко
сами проводят консультации
и обучение в области бизнесаналитики. Кроме этого, они
регулярно принимают участие
в крупнейших международных
конференциях, включая Microsoft Ignite, PASS Summit и
SQLBits.
Примеры на сайте
издательства
www.dmkpress.com
ISBN 978-5-97060-858-6
Анализ данных
при помощи
Microsoft Power BI и
Power Pivot для Excel
Альберто Феррари и Марко Руссо
Интернетмагазин:
www.dmkpress.com
Оптовая продажа:
КТК «Галактика»
e mail: books@alians-kniga.ru
Анализ данных при помощи
Microsoft Power BI и Power Pivot для Excel
Если вы хотите использовать Power BI или Excel для
анализа данных, реальные примеры из этой книги
позволят вам иначе посмотреть на свои отчеты.
С правильно спроектированной моделью данных
ответы на все вопросы будут предельно простыми!
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
УДК
ББК
Ф43
004.424
32.372
Ф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. Анализ интервалов даты и времени........................ 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
 7
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. Остаток по месяцам формируется путем
сложения недельных остатков, что неверно
Это типичный сценарий для использования полуаддитивной меры (semiadditive 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. Единственная таблица-мост способна объединять несколько измерений
Конечно, сначала вам придется создать такую сверхтаблицу-мост (superbridge 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.
Таблица фактов фильтруется
по 100 счетам
10 покупателям соответствуют
100 счетов
Выбираем 10 покупателей
Рис. 8.22. Если количество фильтруемых счетов небольшое, скорость будет приемлемой
222
 Связи «многие ко многим»
С другой стороны, если селективность фильтрации таблицы-моста будет ниже, производительность запросов может снизиться в зависимости от
количества выбранных счетов. На рис. 8.23 показан пример выбора десяти
покупателей, которым соответствуют 10 000 счетов. В этом случае производительность будет ниже.
Таблица фактов фильтруется
по 10 000 счетов
10 покупателям соответствуют 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 (
INTERSECT ( VALUES (
Store[CountryRegion]
)
);
Budget[Brand] ); VALUES ( 'Product'[Brand] ) );
Budget[CountryRegion] ); VALUES (
) )
Функция 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
 Работа с несколькими валютами
 хранить полученные деньги в евро и выполнять конвертацию
валют в конце месяца (или в любой другой момент, отличный от
даты проведения операции). В этом случае вам придется оперировать несколькими валютами с меняющимися курсами на протяжении
ограниченного периода времени.
Примечание. В условиях той или иной политики некоторые из
перечисленных вариантов обращения с валютными операциями
могут быть недоступны. Мы перечислили все три варианта, чтобы
дать вам понять, как теоретически можно строить работу в отношении конвертации валют.
Определяя точку во времени, когда будет проводиться пересчет валют
для денежных операций, вы заботитесь о хранении информации в базе.
В дальнейшем на основании этих данных вам нужно будет строить отчеты. Если вы изначально приводите все операции к базовой валюте
компании, серьезных проб­лем при формировании отчетов у вас не будет. Но если вам необходимо строить отчеты в разных валютах, вам может понадобиться хранить транзакции в евро, а в отчет включать цифры
в долларах, иенах или любой другой валюте. Таким образом, вы должны
иметь возможность осуществлять конвертацию валют «на лету» – на момент формирования отчета.
При проектировании модели, допускающей учет нескольких валют, вам
нужно заранее продумать все нюансы, поскольку от этого будет зависеть
схема данных. Не существует единой модели, удовлетворяющей всем требованиям. Кроме того, конвертация валют – весьма затратная операция
в плане производительности. Поэтому необходимо стараться максимально
упростить работу с валютами, чтобы сложность реализации модели не превышала ваших реальных требований.
Несколько валют источника, одна валюта отчета
Представьте, что в исходных данных хранятся заказы в разных валютах,
а отчеты вам необходимо формировать в единой фиксированной валюте.
Например, вы заводите заказы в долларах, евро и других валютах, но для
возможности сравнивать продажи хотите конвертировать все операции
в одну валюту.
Начнем с беглого взгляда на модель данных, представленную на рис. 11.1,
которую будем использовать в этом сценарии. Таблицы Sales и Currency
(валюты) объединены связью, что позволяет хранить каждую операцию
в своей валюте.
Несколько валют источника, одна валюта отчета
 259
Рис. 11.1. В этой модели валюта хранится вместе с продажей, и по ней можно
осуществлять срезы
Первый вопрос, возникающий при виде такой модели, заключается
в том, какие именно значения хранятся в Unit Price (цена за единицу), Unit
Discount (скидка за единицу) и других полях, связанных с валютой, в таблице Sales. Если информация хранится в валюте операции, то вам придется
несладко при вычислении простых агрегаций вроде суммы продажи. Например, приведенная ниже формула может выдать не тот результат, который вы ожидали:
Sales Amount := SUMX ( Sales; Sales[Quantity] * Sales[Net Price] )
Мы использовали похожую формулу для вычисления суммы продаж на
протяжении всей книги, но применительно к валютам она не работает.
На рис. 11.2 показан простой отчет по этой мере, в котором итоговые значения по столбцам не несут никакого смысла, поскольку суммируют значения
в разных валютах.
В отчете корректно выводятся значения и итоги по строкам, поскольку
они выражены в одной и той же валюте. Но когда дело доходит до столбцов, цифры теряют всякий смысл. Мы не можем складывать доллары, кроны
и евро без соответствующей конвертации валют.
260
 Работа с несколькими валютами
Рис. 11.2. В итогах по столбцам суммируются цифры в разных валютах
Из-за присутствия в модели информации в разных валютах вам следует предотвратить вывод в отчет цифр, не имеющих практического смысла.
Можно использовать функцию HASONEVALUE для вывода результата только
в случае выбора одной валюты отчета. В формуле ниже показан этот метод:
Sales Amount :=
IF (
HASONEVALUE ( Currency[Currency] );
SUMX ( Sales; Sales[Quantity] * Sales[Net Price] )
)
При выводе этой меры в отчет итоги по столбцам не показываются, как
видно по рис. 11.3.
Рис. 11.3. Мы обезопасили отчет от вывода значений, не имеющих смысла
Но и без итогов по столбцам этот отчет нельзя назвать особенно полезным. Обычно отчет используют для сравнения показателей, а здесь выведены цифры, которые нельзя сравнивать. Будет еще хуже, если пользователь
построит график на основании этого отчета и попытается провести по нему
анализ. Чтобы получить сравнимые данные, необходимо либо вынести валюту в фильтр и сделать срез по другому столбцу, либо привести все значения к единой валюте.
Простейшим способом является создание вычисляемого столбца в таблице Sales, в котором сумма продаж будет храниться в валюте отчета. Пред-
Несколько валют источника, одна валюта отчета
 261
ставим, что мы формируем отчеты в американских долларах. В этом случае
вычисляемый столбец будет хранить сумму продажи в долларах по курсу на
текущий день. Для демонстрации мы использовали следующий код для вычисления курса, который вы можете адаптировать под собственные нужды:
RateToUsd =
LOOKUPVALUE(
ExchangeRate[AverageRate];
ExchangeRate[CurrencyKey]; Sales[CurrencyKey];
ExchangeRate[DateKey]; RELATED ( 'Date'[Date] )
)
Теперь мы можем посчитать сумму продаж в долларах в столбце Sales
Amount USD, перемножив исходную сумму на действующий курс. При расчете меры Sales Amount USD мы использовали следующий код на DAX:
Sales Amount USD =
SUMX (
Sales;
Sales[Quantity] * DIVIDE ( Sales[Net Price]; Sales[RateToUsd] )
)
Теперь вы можете сравнивать продажи по годам в разных валютах, как
показано на рис. 11.4.
Рис. 11.4. После приведения цифр к единой валюте
можно их сравнивать и подводить итоги
Применять такую технику довольно просто. При этом логика получения
даты для выполнения конвертации валют заложена непосредственно в вычисляемом столбце. Если у вас свои требования в этом отношении, то можете изменить расчеты. Например, если вы желаете, чтобы курсы считались
на завт­рашний день, то можете изменить функцию LOOKUPVALUE соответствующим образом. Главным ограничением такого подхода является то, что
он работает только в случае, если у вас есть одна фиксированная валюта от-
262

Работа с несколькими валютами
четов. Если же их несколько, вам придется создавать вычисляемые столбцы
для каждой из них.
Примечание. Мера Sales Amount USD показана в отчете в формате
строки с префиксом «$». Форматирование значений выполняется
один раз и не может меняться динамически. Техника создания дополнительных мер для каждой валюты отчетов довольно распространена, и вы вполне можете предложить такой вариант своим
пользователям.
Перед тем как перейти ко второму сценарию, обратим ваше внимание на
то, что не все цифры в нашем отчете корректны. Если сравнить выводы на
рис. 11.3 и 11.4, можно заметить, что в последнем отчете цифра по 2009 году
оказалась меньше. Это наиболее очевидно в строке по американскому доллару. В случае сложных расчетов, как в этом отчете, ошибки заметить бывает трудно. Фактически эта проблема проявляется на строке с долларом,
хотя курс здесь должен быть один к одному. В строках с другими валютами
ошибки также есть, хоть они и не столь заметны. Всегда дважды проверяйте
цифры в своих отчетах – это очень хорошая практика. Так в чем же проб­
лема? Если вы посмотрите в таблицу Sales, то обнаружите несколько сотен
строк с пустыми значениями в столбце RateToUsd, как показано на рис. 11.5.
Здесь мы отсортировали таблицу по этому полю, чтобы пустые значения
были показаны первыми.
Рис. 11.5. В некоторых строках поле RateToUsd осталось пустым
Проблема в том, что курсы валют заполнены не на каждый день, в связи
с чем функции LOOKUPVALUE не удается получить нужное значение. Как
и в других сценариях с участием валют, вы должны определиться с тем, что
делать в подобных случаях. Если курс валюты на дату не установлен, значение в отчете будет неверным, а вы не можете допустить этого. В следующем
фрагменте кода мы поправили меру, добавив получение курса на последнюю возможную дату:
Одна валюта источника, несколько валют отчета
 263
RateToUsd =
LOOKUPVALUE (
ExchangeRate[AverageRate];
ExchangeRate[CurrencyKey]; Sales[CurrencyKey];
ExchangeRate[DateKey]; CALCULATE (
MAX ( 'ExchangeRate'[DateKey] );
'ExchangeRate'[DateKey] <= EARLIER ( Sales[OrderDateKey] );
ExchangeRate[CurrencyKey] = EARLIER ( Sales[CurrencyKey] );
ALL ( ExchangeRate )
)
)
С обновленной мерой значения в таблице стали правильными, что видно
на рис. 11.6.
Рис. 11.6. Обновленная мера курса позволила исправить цифры в отчете
Одна валюта источника, несколько валют отчета
Теперь, когда вы научились конвертировать разные валюты в единую валюту отчетов, можно пойти дальше и проанализировать сценарий, в котором
данные в базе хранятся в фиксированной валюте, а в отчетах пользователь
может выбрать другую валюту.
Как и в предыдущем примере, здесь вам необходимо принять несколько решений. Допустим, если заказ в долларах был оформлен в январе
2005 года, а отчет в другой валюте формируется в декабре 2006-го, какой
курс использовать? Вы можете выбрать между курсом на дату документа
и текущим. Модель данных в обоих случаях будет одинаковой, изменениям
подвергнется только код на DAX. При этом вы можете хранить оба вычисления в одной модели, как показано на рис. 11.7.
264

Работа с несколькими валютами
Рис. 11.7. Модель данных для конвертирования единой валюты документа в выбранную
валюту отчета
Модель данных похожа на ту, что была показана на рис. 1.11, но в ней
есть существенные отличия. Во-первых, отсутствует связь между таблицами Sales и Currency, поскольку все заказы теперь оформляются в долларах
(мы решили использовать эту валюту для демонстрации, вы можете выбрать любую другую). Таким образом, мы не можем фильтровать таблицу
Sales по валюте. В этом случае таблица Currency служит для выбора валюты
отчета. Иными словами, несмотря на то что все документы в базе хранятся
в долларах, пользователь должен иметь возможность выбрать в отчете любую другую валюту.
При этом мы хотим, чтобы все расчеты осуществлялись динамически,
а значит, у пользователя должна быть возможность осуществить выбор непосредственно при формировании отчета. Здесь мы не можем полагаться
на вычисляемый столбец. Вместо этого придется написать немного кода на
DAX в мере. В этой мере должны выполняться следующие действия:
1) осуществление проверки на выбор единственной валюты отчета, чтобы избежать проблем, о которых мы говорили в предыдущем разделе.
Как вы помните, при множест­венном выборе валют итоги перестают
иметь смысл, и мы их просто не выводили;
Одна валюта источника, несколько валют отчета
 265
2) п
роход по всем датам в текущем выборе, вычисление суммы продаж
и курса валюты на каждую дату и перевод значения в нужную валюту. Перебор дат необходим, поскольку курсы валют меняются каждый
день. Нельзя вычислить курс, не пройдя по дате. Именно для этого
и нужны итерации.
Второй пункт осложняется еще и тем, что курсы могут быть выставлены
не на все даты. Таким образом, нам снова придется искать актуальный курс
для валюты. В большинстве случаев это будет курс на выбранную дату, но
иногда придется брать вчерашнее значение. Код получился не самый прос­
той, но он позволяет выполнить оба пункта:
Sales Converted =
IF (
HASONEVALUE ( 'Currency'[Currency] );
SUMX (
VALUES ( 'Date'[Date] );
VAR CurrentDate = 'Date'[Date]
VAR LastDateAvailable =
CALCULATE (
MAX ( 'ExchangeRate'[DateKey] );
'ExchangeRate'[DateKey] <= CurrentDate;
ALL ( 'Date' )
)
VAR Rate =
CALCULATE (
VALUES ( ExchangeRate[AverageRate] );
ExchangeRate[DateKey] = LastDateAvailable;
ALL ( 'Date' )
)
RETURN
[Sales Amount USD] * Rate
)
)
При помощи этой меры мы можем формировать отчет в любой выбранной валюте, как показано на рис. 11.8.
Рис. 11.8. В столбце Currency показана валюта отчета, и все значения переведены
в эту валюту
266

Работа с несколькими валютами
К сожалению, получившаяся формула оказалась не самой простой для
написания и понимания. И это не очень хорошо, поскольку вам может понадобиться писать что-то подобное и для других финансовых показателей
в отчетах – например, для себестоимости или прибыли.
Наиболее сложная часть формулы отведена под поиск актуального курса
валюты. Но, как и всегда, мы можем здесь немного поработать с моделью
данных. На этот раз мы создадим новую таблицу ExchangeRate, в которой
будут представлены курсы валют на все даты из таблицы Sales. Для этого мы
выполним поиск курсов валют, как делали раньше. Тем самым мы не лишаем сложности модель данных в целом, а просто выносим сложные участки
кода в вычисляемую таблицу, которой будем пользоваться. Одновременно
с этим мы ускоряем расчет меры, поскольку самой длительной операцией
в формуле был поиск нужного курса валюты.
Примечание. Этот вариант доступен только тем, кто работает с SQL
Server Analysis Services 2016 или Power BI, поскольку в нем используется вычисляемая таблица. Если ваша версия DAX не поддерживает этот объект, вам необходимо выполнить эти преобразования на этапе подготовки данных.
Следующий код создает вычисляемую таблицу ExchangeRateFull, в которой хранятся курсы для всех пар даты и валюты:
ExchangeRateFull =
ADDCOLUMNS (
CROSSJOIN (
SELECTCOLUMNS (
CALCULATETABLE ( DISTINCT ( 'Date'[Date] ); Sales );
"DateKey"; 'Date'[Date]
);
CALCULATETABLE ( DISTINCT ( Currency[CurrencyKey] );
ExchangeRate )
);
"AverageRate";
VAR CurrentDate = [DateKey]
VAR CurrentCurrency = [CurrencyKey]
VAR LastDateAvailable =
CALCULATE (
MAX ( 'ExchangeRate'[DateKey] );
'ExchangeRate'[DateKey] <= CurrentDate;
ALLNOBLANKROW ( ExchangeRate[DateKey] )
)
RETURN
CALCULATE (
DISTINCT ( ExchangeRate[AverageRate] );
ExchangeRate[CurrencyKey] = CurrentCurrency;
ExchangeRate[DateKey] = LastDateAvailable
)
)
Одна валюта источника, несколько валют отчета
 267
С новой вычисляемой таблицей модель данных стала похожа на предыдущую, как видно по рис. 11.9.
Рис. 11.9. Новая вычисляемая таблица ExchangeRateFull заняла место прежней таблицы
ExchangeRate
Теперь код меры значительно упростился, как показано ниже:
Sales Converted =
IF (
HASONEVALUE ( 'Currency'[Currency] );
SUMX (
VALUES ( 'Date'[Date] );
[Sales Amount USD] * CALCULATE ( VALUES (
ExchangeRateFull[AverageRate] ) )
)
)
Как мы уже говорили, полностью сложность решения не исчезла. Мы прос­
то локализовали ее в вычисляемой таблице, тем самым изолировав от мер.
Преимущество такого подхода состоит в том, что вам не придется при написании новых мер (а их может быть немало) повторять один и тот же сложный
код, что позволит снизить количество ошибок. Более того, поскольку значения в вычисляемой таблице рассчитываются в момент обновления данных
и сохраняются в модели, быстродействие мер также заметно увеличится.
В рассмотренном примере мы упростили код не за счет корректировки
структуры модели данных, а путем изменения содержимого таблицы, что
позволило установить правильную гранулярность для связи.
268

Работа с несколькими валютами
Несколько валют источника, несколько валют отчета
Если вы хотите хранить заказы и формировать отчеты в разных валютах,
вам подойдет наиболее сложный из рассматриваемых сценариев. На самом
деле он не намного сложнее предыдущего, поскольку сложность, как вы понимаете, здесь в основном связана с необходимостью конвертировать валюты в момент запроса при помощи мер и предварительно вычисленных
таб­лиц. Кроме того, в случае с несколькими валютами хранения и отчета
в таблице с курсами валют должно находиться больше строк (по одной строке для каждой пары валют на каждую дату), иначе придется рассчитывать
курс динамически.
Посмотрите на модель данных, показанную на рис. 11.10.
Что необходимо понимать об этой модели:
 в ней есть две таблицы с валютами: Source Currency (валюты источника) и Report Currency (валюты отчета). Первая используется для среза
данных по валюте хранения, а вторая – по валюте отчета;
 в таблице ExchangeRates теперь хранятся ссылки на валюты источника и цели, чтобы была возможность конвертировать любые валюты. Также стоит отметить, что таблица ExchangeRates может быть
построе­на (при помощи DAX) на основе исходной таблицы, которая
переводила любую валюту в доллары.
Рис. 11.10. Модель включает множество валют источника и целевых валют
Несколько валют источника, несколько валют отчета
 269
Представленный код создает таблицу ExchangeRates:
ExchangeRates =
SELECTCOLUMNS (
GENERATE (
ExchangeRateFull;
VAR SourceCurrencyKey = ExchangeRateFull[CurrencyKey]
VAR SourceDateKey = ExchangeRateFull[DateKey]
VAR SourceAverageRate = ExchangeRateFull[AverageRate]
RETURN
SELECTCOLUMNS (
CALCULATETABLE (
ExchangeRateFull;
ExchangeRateFull[DateKey] =
SourceDateKey;
ALL ( ExchangeRateFull )
);
"TargetCurrencyKey";
ExchangeRateFull[CurrencyKey] + 0;
"TargetExchangeRate";
ExchangeRateFull[AverageRate] + 0
)
);
"DateKey"; ExchangeRateFull[DateKey];
"SourceCurrencyKey"; ExchangeRateFull[CurrencyKey];
"SourceExchangeRate"; ExchangeRateFull[AverageRate];
"TargetCurrencyKey"; [TargetCurrencyKey];
"TargetExchangeRate"; [TargetExchangeRate];
"ExchangeRate"; ExchangeRateFull[AverageRate] * [TargetExchangeRate]
)
По сути, здесь выполняется перекрестное соединение таблицы
ExchangeRateFull самой с собой. Сначала получаем курсы обеих валют относительно доллара на одну и ту же дату. Затем перемножаем эти величины
для получения кросс-курса одной валюты к другой.
В новой таблице гораздо больше строк, если сравнивать с предыдущей
(25 166 строк в ExchangeRateFull против 624 133 строк в новой), но она позволяет нам легко создать связи. Код можно написать и без создания этой
таблицы, но он будет куда сложнее.
Что касается подсчета суммы продаж, то здесь мы, по сути, объединим
код из двух предыдущих примеров. Необходимо сделать срез таблицы Sales
по дате и валюте, чтобы получить подмножество продаж с одним и тем же
курсом. После этого нужно выполнить динамический поиск текущего курса,
принимая во внимание выбранную валюту отчета, как показано ниже:
Sales Amount Converted =
IF (
HASONEVALUE ( 'Report Currency'[Currency] );
SUMX (
SUMMARIZE ( Sales; 'Date'[Date]; 'Source Currency'[Currency]
270
 Работа с несколькими валютами
);
[Sales Amount] * CALCULATE ( VALUES (
ExchangeRates[ExchangeRate] ) )
)
)
Используя эту модель, вы можете, например, построить отчет по заказам, сформированным в евро и долларах, с пересчетом курсов «на лету».
На рис. 11.11 показан отчет, в котором курсы рассчитаны на дату заказа.
Рис. 11.11. Значения из исходных валют конвертируются в евро и доллары
Заключение
Модели данных, включающие учет нескольких валют, отличаются повышенной сложностью в зависимости от ваших требований. Основные моменты, которые вы узнали из этой главы:
 простую конвертацию множества валют в одну (или несколько) можно выполнить при помощи обычных вычисляемых столбцов;
 конвертация в несколько валют отчета может потребовать от вас
написания сложного кода на DAX и изменения модели данных, поскольку вычисляемый столбец вам в этом не поможет. Перевод курсов
в этом случае должен осуществляться динамически;
 код для динамической конвертации валют можно упрос­тить за счет
добавления всех дат в таблицу курсов, чего можно добиться путем
создания вычисляемой таблицы;
 сценарий максимально усложняется, когда у вас есть несколько валют
источника и несколько валют отчета. В таком случае вам придется
сочетать предыдущие техники и создать две новые таблицы валют:
одну – для валют источника, а вторую – для целевых валют.
Приложение A.
Моделирование данных 101
Цель этого приложения состоит в том, чтобы кратко описать концепции моделирования данных, использованные на протяжении всей книги и повсеместно встречающиеся в статьях, блогах и других книгах. Это приложение
не предназначено для чтения от начала до конца. Вместо этого вы можете
обращаться к нему в процессе чтения книги, когда вам непонятен тот или
иной термин или концепция, а также если вы просто хотите освежить память. Именно поэтому в приложении не будет единой связной линии повествования, а каждая тема будет рассмотрена в отдельном разделе. Более
того, мы бы не хотели излишне усложнять приложение. Мы предложим
лишь базовую информацию по темам. Подробное описание концепций выходит за границы данной книги.
Таблицы
Таблица (table) – это своего рода контейнер для информации. В таблицах
есть строки и столбцы. В строке содержится информация об одной сущности, тогда как ячейка является минимально допустимым объемом хранимой информации в базе данных. Например, в таблице Customer может
храниться информация обо всех наших покупателях. При этом одна строка
содержит все данные об одном покупателе, а один столбец – всю информацию об именах или адресах. В ячейке же может храниться адрес конкретного покупателя.
При проектировании модели данных вы должны рассуждать именно такими категориями – в противном случае вы рискуете угодить в одну из
многочисленных ловушек, и анализ вашей модели превратится в настоящий кошмар. Представьте, что вам вздумалось хранить информацию об
одном заказе в двух строках одной и той же таблицы. В одной строке вы
фиксируете сумму заказа с указанием даты заказа, а в другой – сумму доставленных товаров с датой доставки. Таким образом вы фактически разделяете один заказ на две строки одной и той же таблицы. Пример таблицы
показан на рис. A-1.
272
 Приложение A. Моделирование данных 101
Рис. A-1. В таблице информация по заказу разбита на две строки
Это значительно усложнило таблицу. Теперь получение даже простейших
данных вроде суммы заказа сопряжено с большими сложностями, поскольку в одном столбце (Amount) хранится разнородная информация. Так что
мы не можем просто агрегировать значение по столбцу, а должны все время
оперировать фильтрами. Проблема в том, что мы не спроектировали модель данных должным образом. Из-за разбивки одного заказа на несколько
строк производить вычисления любого рода оказалось проблематично. Например, для того чтобы установить процент доставленных товаров по заказам для каждого клиента, необходимо написать код, выполняющий следующие действия:
1) пройти по всем номерам заказов;
2) агрегировать суммы заказов (если они разбиты на несколько строк)
с фильтром по полю Type, равному Order;
3) агрегировать суммы доставки, теперь с фильтром по полю Type, равному Ship;
4) посчитать процент.
Ошибка в этом примере заключается в том, что если заказы – это отдельная сущность, то для них должна быть выделена своя таблица, чтобы все
необходимые значения можно было легко агрегировать. Если вам также необходимо отслеживать транзакции по доставке, вы можете создать отдельную таблицу Shipments, в которой будет храниться только этот тип операций. На рис. A-2 показана корректная модель данных для рассматриваемого
примера.
В этом примере мы отслеживали только доставку. Но вам может понадобиться более общая таблица, в которой будут отслеживаться все транзакции
по заказам (заказ, доставка и возврат). В этом случае допустимо хранить все
типы транзакций в одной таблице с отдельным атрибутом, указывающим
на тип операции. Таким образом, можно учитывать в одной таблице разные
типы одной сущности (транзакции), но нежелательно вместе хранить разные сущности (заказы и транзакции по ним).
Приложение A. Моделирование данных 101
 273
Рис. A-2. Правильное представление заказов и доставок требует наличия двух таблиц
Типы данных
При проектировании модели данных вы должны помнить, что у каждого
столбца есть свой тип данных. Тип данных (data type) устанавливает принадлежность хранящейся в столбце информации к тому или иному типу. Это
может быть целое число, строка, валюта, число с плавающей точкой и т. д.
Разновидностей типов данных великое множество. Тип столбца в таблице
очень важен, поскольку он определяет его использование, а также возможность применения тех или иных функций и форматирования. Фактически
тип данных является внутренней характеристикой модели, определяющей
способ хранения информации. Напротив, формат данных относится исключительно к визуальному представлению информации.
Представьте, что у вас есть столбец в таблице для хранения количества
доставленных товаров. Очевидно, что в этом случае логично будет сделать
его целочисленным. Если же вам необходимо хранить суммы продаж, то
лучше выбрать число с плавающей точкой для учета десятичных разрядов.
Вообще, в этом случае оптимальным выбором будет тип валюты.
В Excel ячейки могут хранить любой тип информации. Однако в табличных моделях данных тип информации задается на уровне столбца. Таким
образом, в столбце должна храниться исключительно однородная информация. Смешанный тип данных в столбцах недопустим.
Связи
Если в вашей модели есть сразу несколько сущностей, как обычно и бывает,
вы храните информацию о них в разных таблицах, которые объединяете
друг с другом посредством связей. В таб­личной модели допустимо связывать две таблицы исключительно по одному столбцу.
274
 Приложение A. Моделирование данных 101
Наиболее часто связи представляют при помощи линий со стрелками, идущими от таблицы-источника к целевой таблице, как показано на
рис. A-3.
Рис. A-3. В этой модели четыре таблицы, объединенные связями
Любая связь характеризуется сторонами «один» и «многие». В простой
модели данных каждому товару может соответствовать много продаж, тогда как в каждой продаже один товар может присутствовать лишь раз. Таким
образом, сторона с таб­лицей Product характеризуется в связи как «один»,
а сторона Sales – как «многие». Стрелка всегда направлена от «многих»
к «одному».
В разных версиях Power Pivot для Excel и в Power BI используется разная
визуализация для связей. В последних релизах Excel и Power BI для идентификации сторон связей используются символы «1» (один) и «*» (звездочка).
В Power BI Desktop у вас также есть возможность создать связь типа «один
к одному». Такой тип связи по природе является двунаправленным, поскольку каждой строке в одной таблице может соответствовать максимум одна
строка в другой. В этом случае сторона «многие» в связях будет отсутствовать.
Фильтрация и перекрестная фильтрация
При анализе модели данных посредством PivotTable или Power BI фильтрация играет очень важную роль. Фактически фильтры представляют собой
основу большинства (если не всех) расчетов в отчетах. Основное правило
фильтрации при использовании языка DAX состоит в том, что фильтры
автоматически распространяются по связи от стороны «один» к стороне
«многие». В пользовательском интерфейсе это свойство обозначено стрелкой посередине связи, показывающей направление распространения связи,
как видно на рис. A-4.
Приложение A. Моделирование данных 101
 275
Рис. A-4. Маленькая стрелка посередине связи демонстрирует направление распространения фильтра
Таким образом, устанавливая фильтр на таблицу Date, вы автоматически
фильтруете и таблицу Sales. Поэтому в сводной таблице вы можете легко
осуществить срез суммы продаж по календарному году – фильтр, наложенный на Date, автоматически распространится на Sales. В обратном направлении фильтр по умолчанию не распространяется. Следовательно, фильтр,
наложенный на таблицу Sales, не будет воздействовать на таб­лицу Date,
если не сделать соответствующую настройку в модели. На рис. A-5 графически
показано распространение фильтра между таблицами.
Распространение фильтра
в однонаправленной связи
Рис. A-5. Большой стрелкой показано направление распространения фильтра
в однонаправленной связи
276

Приложение A. Моделирование данных 101
Вы можете изменить направление распространения фильтра (установив
так называемую перекрестную фильтрацию (cross-filtering)), изменив настройки связи. В Power BI, например, это делается путем двойного щелчка
мышью на связи. При этом откроется диалоговое окно Edit Relationship
(Изменение связи), как показано на рис. A-6.
Рис. A-6. Диалоговое окно Edit Relationship (Изменение связи) позволяет настроить
перекрестную фильтрацию между таблицами
По умолчанию направление фильтрации установлено в значение Однонаправленная, то есть от «одного» ко «многим». Если необходимо, вы
можете изменить значение в выпадающем списке на Двунаправленная,
чтобы развернуть направление действия фильтра. На рис. A-7 графически
показано действие фильтра при выборе двунаправленной связи.
Такой вариант недоступен для пользователей Power Pivot для Excel 2016.
Если вам необходимо включить двунаправленную фильтрацию в этой среде, придется воспользоваться функцией CROSSFILTER, как в следующем
примере, который работает в модели, показанной на рис. A-8:
Приложение A. Моделирование данных 101
 277
Распространение фильтра
в двунаправленной связи
Рис. A-7. В двунаправленной связи действие фильтра распространяется в обоих
направлениях
Num of Customers =
CALCULATE (
COUNTROWS ( Customer );
CROSSFILTER ( Sales[CustomerKey]; Customer[CustomerKey]; BOTH )
)
Рис. A-8. Обе связи в модели данных однонаправленные
278

Приложение A. Моделирование данных 101
Функция CROSSFILTER включает режим двунаправленной связи между
таблицами на время выполнения инструкции CALCULATE. При вычислении выражения COUNTROWS ( Customer ) фильтр распространится с Sales на
Customer, чтобы показать только покупателей, участвовавших в продажах.
Такая техника очень удобна, например, когда нужно подсчитать количество клиентов, купивших определенные товары. Изначально действие
фильтра распространяется с таблицы Product на таблицу Sales. Включая
двунаправленную фильтрацию, вы тем самым позволяете фильтру проникнуть в таблицу Customer через Sales. На рис. A-9 показаны два столбца: один
с включенной двунаправленной фильтрацией, другой – с выключенной.
Рис. A-9. Меры отличаются только типом установленной фильтрации.
Результаты при этом совершенно разные
Формулы для мер приведены ниже:
CustomerCount := COUNTROWS ( Customer )
CustomerFiltered :=
CALCULATE (
COUNTROWS ( Customer );
CROSSFILTER ( Customer[CustomerKey]; Sales[CustomerKey]; BOTH )
)
Как видите, в мере CustomerCount используется фильтрация по умолчанию. Так что в этом случае таблица Product распространяет фильтр на Sales,
но от Sales к Customer фильтр не добирается. Во второй мере фильтр проходит от таблицы Product и через Sales достигает таблицы Customer, что позволяет подсчитать количество покупателей, которые приобретали один из
товаров выбранных брендов.
Приложение A. Моделирование данных 101
 279
Различные типы моделей
Обычно модель данных насчитывает множество таблиц, объединенных связями. Эти таблицы могут быть классифицированы по следующим типам:
 таблица фактов. В таблице фактов содержатся значения для агрегации. Обычно таблицы фактов регистрируют события, произошедшие
в определенный момент времени, которые могут быть измерены. Таб­
лицы фактов традиционно лидируют в моделях данных по количеству
содержащихся в них строк, которое может достигать сотен миллионов.
Чаще всего в таких таблицах хранятся только численные показатели,
будь то ключи к таблицам измерений или значения для агрегации;
 измерение. Измерения удобно использовать для осуществления срезов по таблицам фактов. Обычно в измерениях хранятся однородные
сущности вроде товаров, покупателей, времени или категорий. Чаще
всего объем таких таблиц не превышает нескольких тысяч строк. При
этом многие атрибуты в измерениях содержат текстовые данные, поскольку они часто используются для выполнения срезов в таблицах
фактов;
 таблица-мост. Таблицы-мосты используются в сложных моделях
данных для установки связей типа «многие ко многим». Например,
сценарий, в котором один покупатель может принадлежать к нескольким категориям, может быть смоделирован при помощи таблицы-моста, в которой будет содержаться по одной строке для связки
покупателя с категорией.
Схема «звезда»
Если ваша модель данных состоит исключительно из таблиц фактов и измерений, вы можете визуально окружить таблицы фактов измерениями так,
чтобы получилась схема, похожая на звезду, как показано на рис. A-10.
280

Приложение A. Моделирование данных 101
Рис. A-10. Схема «звезда» характеризуется расположением таблицы фактов
посередине, а измерений – по периметру
У схемы «звезда» есть очень много достоинств, в числе которых высокая
производительность и легкость для понимания и управления. Мы уже писали, что большинство аналитических моделей данных построены именно
по такой схеме. Но иногда бывает лучше спроектировать таблицы и связи
иначе, и об этом мы поговорим в следующих разделах.
Схема «снежинка»
Встречаются модели данных, в которых измерения связаны друг с другом
с целью осуществления определенной классификации. Например, товары
могут относиться к категориям, и вы можете решить хранить последние
в отдельной таблице. Кроме того, магазины могут быть разбиты по структурным подразделениям, которые вы также можете захотеть хранить обособленно. На рис. A-11 показана модель данных, в которой таблица товаров
не содержит столбец с названием категории, а вместо этого хранит только
ссылки на категории, которые хранятся в отдельном измерении.
Рис. A-11. Категории хранятся в отдельной таблице, на которую ссылается таблица
с товарами
Приложение A. Моделирование данных 101
 281
Здесь таблица с категориями товаров также является измерением, но она
связана с таблицей фактов не напрямую, а через промежуточное измерение
(Product). В таблице Sales может быть столбец ProductKey, но чтобы получить
название категории товара, необходимо пройти из таблицы Sales в Product
и только затем в Category. Таким образом, мы получим схему данных, названную «снежинкой», которая показана на рис. A-12.
Рис. A-12. Схема «снежинка» возникает при связи измерений с таблицей фактов
посредством других измерений
Обычно измерения не должны быть связаны друг с другом. Например, вы
можете считать связь между таблицами Business Unit и Sales прямой, но на
самом деле она проходит через таб­лицу Store. При этом нет никаких причин связывать таблицы Store и Geography. В этом случае в модели данных
появится неоднозначность, поскольку из таблицы Sales в Geography можно
будет добраться более чем одним способом.
Схема «снежинка» довольно распространена в мире бизнес-аналитики.
Если не считать незначительного снижения производительности, в целом
эта схема является неплохим выбором. Но всякий раз, когда это возможно, нужно стремиться избегать образования схемы «снежинка» и отдавать
предпочтение более традиционной «звезде» – в этом случае вам будет легче
работать с моделью посредством языка DAX, и количество ошибок снизится.
Модели с таблицами-мостами
Таблица-мост обычно располагается в модели между двумя измерениями,
помогая тем самым построить связь типа «многие ко многим». На рис. A-13
282

Приложение A. Моделирование данных 101
показано, как можно отнести покупателя сразу к нескольким категориям. Марко (Marco) относится сразу к двум категориям – Мужчины (Male)
и Итальянцы (Italien), тогда как Кейт (Kate) принадлежит только к категории Женщины (Female). Если у вас похожий сценарий, то самое время
создать таблицу-мост и направить от нее две связи к измерениям Customer
и Category.
Рис. A-13. Таблица-мост позволила установить соответствие между покупателем и несколькими категориями
Модель, содержащая таблицу-мост, приобретает особенный вид, названия которому в сообществе бизнес-аналитики пока не нашлось. На рис. A-14
представлена модель данных с возможностью присваивать покупателям
несколько категорий.
Рис. A-14. Таблица-мост объединяет два измерения, и в целом схема отличается от
традиционной «снежинки»
Разница между представленной схемой и обычной «снежинкой» состоит
в том, что теперь связи между таблицами Customer, Category и Sales не про-
Приложение A. Моделирование данных 101
 283
ходят напрямую, поскольку связь между мостом и Customer развернута
в обратном направлении. Если бы связь была направлена к мосту, а не от
него, мы получили бы «снежинку». Здесь же мы имеем дело с типичным отношением «многие ко многим».
Меры и аддитивность
Определяя меры в модели данных, важно понимать, будут ли они обладать
свойством аддитивности по тому или иному измерению.
Аддитивные меры
Мера называется аддитивной, если агрегация по ней может быть выполнена
при помощи обычной функции SUM. Например, сумма продаж – аддитивная мера по измерению товаров. Это означает, что общая сумма продаж составляется из продаж отдельных товаров. Также можно отметить, что в таблице Sales меры аддитивны по всем измерениям, поскольку общая сумма
продаж за год равна сумме продаж по отдельным дням.
Неаддитивные меры
Вторая категория мер – неаддитивные меры. Например, мера, подсчитывающая уникальное количество, является неаддитивной по определению.
Если мы будем считать уникальное количество проданных товаров, то этот
показатель за год не будет равняться сумме значений по месяцам. То же
самое, помимо товаров, относится к покупателям, странам и другим измерениям. При расчете неаддитивных мер вам придется проходить по всем
уровням иерархии таблицы, поскольку агрегировать значения с более низких уровней не получится.
Полуаддитивные меры
Третья категория мер – полуаддитивные. Это самый сложный тип мер, поскольку им свойственна аддитивность по одним измерениям и неаддитивность – по другим. Одним из типичных исключений такого рода является
измерение времени. Например, меры на основе функций нарастающего
итога с начала года будут неаддитивными, поскольку значение для конкретного месяца (допустим, для марта) не будет являться суммой значений
по дням. Вместо этого мы получим значение показателя на последний день
месяца. На рис. A-15 показаны все три типа измерений.
284
 Приложение A. Моделирование данных 101
Рис. A-15. В отчете показаны все три свойства мер: аддитивность, неаддитивность
и полуаддитивность
В языке DAX предусмотрен целый ряд функций для работы с полуаддитивными мерами. Функции наподобие DATESYTD, TOTALYTD, LASTDATE
и CLOSINGBALANCEQUARTER помогут вам в написании формул для полуаддитивных мер с отсутствием аддитивности по измерению времени. Оперирование полуаддитивными мерами по другим измерениям потребует
применения сложного сочетания функций FILTER, поскольку функций для
работы с неаддитивными мерами по измерениям, не относящимся ко времени, не существует.
Предметный указатель
A
аддитивная мера 154, 283
активное событие 182
атрибут 34
В
финансовый календарь 101
каскадная связь многие
ко многим 208
коэффициент поправки 215
коэффициент распределения 239
виртуальная связь 251
внешний ключ 27
временная связь
многие ко многим 211
выравнивание 55
вычисляемая физическая связь 243
М
Г
неаддитивная мера 283
неоднозначная модель 69
неоднозначность 37
нормализация 31
главная таблица 45
гранулярность 21
Д
двунаправленная
фильтрация 48, 78, 204
денормализация 31
И
измерение 34
быстро меняющееся
измерение 145
медленно меняющееся
измерение 127
измерение даты 86
автоматическая группировка дат
в Excel 87
автоматическая группировка дат
в Power BI 89
исторический атрибут 127
К
календарь
недельный календарь 119
особые периоды года 111
учет рабочих дней 104
материализация связи 217
матрица переходов 162
мера 34
модель данных 25
Н
П
первичный ключ 26
перекрестная фильтрация 276
подчиненная таблица 45
полуаддитивная мера 283
Р
работа с несколькими валютами 257
расширение таблицы 205
С
связь
многие ко многим 201
один ко многим 41
сегментация данных 243
динамическая 248
статическая 246
снимок 151
естественный снимок 152
производный снимок 152
схема данных
звезда 34
снежинка 38
286

Предметный указатель
Т
таблица 271
таблица-источник 27
таблица-мост 202
тип данных 273
E
EXCEPT (функция) 110
F
FILTER (функция) 247
Ф
H
Ц
I
A
L
факт 34
целевая таблица 27
ABC-анализ 251
ALL (функция) 65
AVERAGEX (функция) 23
C
CALCULATETABLE
(функция) 167
CALCULATE (функция) 62
CONTAINS (функция) 231
COUNTROWS (функция) 108
CROSSFILTER (функция) 69, 204
D
DATESINPERIOD (функция) 100
DATESYTD (функция) 99
DISTINCTCOUNT (функция) 134
DISTINCT (функция) 65
HASONEVALUE (функция) 109
INTERSECT (функция) 61, 167
LASTDATE (функция) 155
LOOKUPVALUE (функция) 115
R
RELATEDTABLE (функция) 91
RELATED (функция) 70
S
SUMMARIZE (функция) 23
T
TREATAS (функция) 232
U
UNION (функция) 65
USERELATIONSHIP (функция) 70
V
VALUES (функция) 247
Книги издательства «ДМК Пресс» можно заказать
в торгово-издательском холдинге «Планета Альянс» наложенным платежом,
выслав открытку или письмо по почтовому адресу:
115487, г. Москва, 2-й Нагатинский пр-д, д. 6А.
При оформлении заказа следует указать адрес (полностью),
по которому должны быть высланы книги;
фамилию, имя и отчество получателя.
Желательно также указать свой телефон и электронный адрес.
Эти книги вы можете заказать и в интернет-магазине: www.a-planeta.ru.
Оптовые закупки: тел. (499) 782-38-89.
Электронный адрес: books@alians-kniga.ru.
Альберто Феррари и Марко Руссо
Анализ данных при помощи Microsoft Power BI
и Power Pivot для Excel
Главный редактор
Мовчан Д. А.
dmkpress@gmail.com
Перевод
Корректор
Верстка
Дизайн обложки
Гинько А. Ю.
Синяева Г. И.
Луценко С. В.
Мовчан А. Г.
Формат 70×90 1/16.
Гарнитура «PT Serif». Печать цифровая.
Усл. печ. л. 21,06. Тираж 200 экз.
Веб-сайт издательства: www.dmkpress.com
Отпечатано в ООО «Принт-М»
142300, Московская обл., Чехов, ул. Полиграфистов, 1
Download