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

Если вы хотите использовать Power BI или Excel для анализа данных, реальные примеры из этой книги позволят вам иначе пос

188 56 23MB

Russian Pages 288 [287] Year 2020

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

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

Table of contents :
Оглавление
Предисловие от издательства
Отзывы и пожелания
Список опечаток
Нарушение авторских прав
Рецензия
Введение
Для кого предназначена эта книга?
Как мы представляем себе нашего читателя?
Структура книги
Условные обозначения
Сопутствующий контент
Благодарности
Список опечаток и поддержка
Обратная связь
Оставайтесь с нами
Глава 1. Введение в моделирование данных
Работа с одной таблицей
Введение в модель данных
Введение в схему «звезда»
Понимание важности именования объектов
Заключение
Глава 2. Использование главной/подчиненной таблицы
Введение в модель данных с главной и подчиненной таблицам
Агрегирование мер из главной таблицы
Выравнивание главной и подчиненной таблиц
Заключение
Глава 3. Использование множественных таблиц фактов
Использование денормализованных таблиц фактов
Фильтрация через измерения
Понимание неоднозначности модели данных
Работа с заказами и счетами
Расчет полной суммы по счетам для покупателя
Расчет суммы по счетам, включающим данный заказ от конкретного покупателя
Расчет суммы заказов, включенных в счета
Заключение
Глава 4. Работа с датой и временем
Создание измерения даты и времени
Понятие автоматических измерений времени
Автоматическая группировка дат в Excel
Автоматическая группировка дат в Power BI Desktop
Использование нескольких измерений даты и времени
Обращение с датой и временем
Функции для работы с датой и временем
Работа с финансовыми календарями
Расчет рабочих дней
Учет рабочих дней в рамках одной страны или региона
Учет рабочих дней в разных странах
Работа с особыми периодами года
Работа с непересекающимися периодами
Периоды, связанные с текущим днем
Работа с пересекающимися периодами
Работа с недельными календарями
Заключение
Глава 5. Отслеживание исторических атрибутов
Введение в медленно меняющиеся измерения
Использование медленно меняющихся измерений
Загрузка медленно меняющихся измерений
Исправление гранулярности в измерении
Исправление гранулярности в таблице фактов
Быстро меняющиеся измерения
Выбор оптимальной техники моделирования
Заключение
Глава 6. Использование снимков
Данные, которые нельзя агрегировать по времени
Агрегирование снимков
Понятие производных снимков
Понятие матрицы переходов
Заключение
Глава 7. Анализ интервалов даты и времени
Введение во временные данные
Агрегирование простых интервалов
Интервалы с переходом дат
Моделирование рабочих смен и временных сдвигов
Анализ активных событий
Смешивание разных интервалов
Заключение
Глава 8. Связи «многие ко многим»
Введение в связи «многие ко многим»
Понятие шаблона двунаправленной фильтрации
Понятие неаддитивности
Каскадные связи «многие ко многим»
Временные связи «многие ко многим»
Факторы перераспределения и процентные соотношения
Материализация связей «многие ко многим»
Использование таблицы фактов в качестве моста
Вопросы производительности
Заключение
Глава 9. Работа с разными гранулярностями
Введение в гранулярности
Связи на разных уровнях гранулярности
Анализ данных о бюджетировании
Использование DAX для распространения фильтра
Фильтрация при помощи связей
Скрытие значений на недопустимых уровнях гранулярности
Распределение значений по уровням с большей гранулярностью
Заключение
Глава 10. Сегментация данных в модели
Вычисление связей по нескольким столбцам
Вычисление статической сегментации
Использование динамической сегментации
Понимание потенциала вычисляемых столбцов: ABC анализ
Заключение
Глава 11. Работа с несколькими валютами
Введение в различные сценарии
Несколько валют источника, одна валюта отчета
Одна валюта источника, несколько валют отчета
Несколько валют источника, несколько валют отчета
Заключение
Приложение A. Моделирование данных 101
Таблицы
Типы данных
Связи
Фильтрация и перекрестная фильтрация
Различные типы моделей
Схема «звезда»
Схема «снежинка»
Модели с таблицами-мостами
Меры и аддитивность
Аддитивные меры
Неаддитивные меры
Полуаддитивные меры
Предметный указатель

Citation preview

Analyzing Data with Microsoft Power BI and Power Pivot for Excel Alberto Ferrari and Marco Russo

Анализ данных при помощи Microsoft Power BI и Power Pivot для Excel Альберто Феррари и Марко Руссо

Москва, 2020

УДК 004.424 ББК 32.372 Ф43

Ф43

Альберто Феррари и Марко Руссо Анализ данных при помощи Microsoft Power BI и Power Pivot для Excel / пер. с анг. А. Ю. Гинько. – М.: ДМК Пресс, 2020. – 288 с.: ил. ISBN 978-5-97060-858-6

УДК 004.424 ББК 32.372 В этой книге представлены базовые техники моделирования данных в Excel и Power BI. Авторы, специалисты в области бизнес-аналитики, делают акцент на реальных ситуациях, с которыми регулярно сталкиваются как консультанты. Они продемонстрируют общие техники моделирования, научат читателя производить расчеты с календарем, расскажут об использовании снимков для подсчета количества товаров в наличии, о том, как работать с несколькими валютами одновременно, и подробно объяснят на примерах многие другие полезные операции. Издание предназначено как для новичков, так и для специалистов в области моделирования данных, желающих получить советы экспертов. Для изучения материала требуется владение Excel на среднем или продвинутом уровне. Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. Copyright Authorized translation from the English language edition, entitled ANALYZING DATA WITH POWER BI AND POWER PIVOT FOR EXCEL, 1st Edition by ALBERTO FERRARI; MARCO RUSSO, published by Pearson Education, Inc, publishing as Microsoft Press, Copyright ©2017 RUSSIAN language edition published by DMK PRESS PUBLISHING LTD., Copyright © [2020].

ISBN (анг.) 978-1-5093-0276-5 © 2017 by Alberto Ferrari and Marco Russo ISBN (рус.) 978-5-97060-858-6 © О формление, издание, перевод, ДМК Пресс, 2020

Оглавление

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

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

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

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

6 

Оглавление

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

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

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

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

Оглавление 

Глава 7. Анализ интервалов даты и времени ........................ 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, зайдя на  страницу книги, и  оставить комментарий в  разделе «Отзывы и рецен зии». Также можно послать письмо главному редактору по адресу [email protected], при этом напишите название книги в теме письма. Если есть тема, в  которой вы квалифицированы, и  вы заинтересованы в написании новой книги, заполните форму на нашем сайте по адресу http://dmkpress.com/authors/publish_book/ или напишите в издательство по адресу [email protected].

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

нарушение автОрСких прав Пиратство в интернете по-прежнему остается насущной проблемой. Издательство «ДМК Пресс» очень серьезно относится к вопросам защиты авторских прав и лицензирования. Если вы столкнетесь в интернете с незаконно выполненной копией любой нашей книги, пожалуйста, сообщите нам адрес копии или веб-сайта, чтобы мы могли применить санкции. Пожалуйста, свяжитесь с нами по адресу электронной почты 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 по адресу: [email protected]. Отметим, что услуги по поддержке программного обеспечения 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] = 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] = [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 ) ); '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] 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] 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] ) ) ) 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] MIN ( Segments[MinSale] ); [Sales Amount] Segments[MinSale];

Понимание потенциала вычисляемых столбцов: ABC-анализ 

251

[Sales Amount] = 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]