Contact us
Telegram
MDX для анализа времени:
Расчеты "год к дате" и скользящие средние
В мире бизнес-аналитики

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

Представьте, что вы аналитик в AdventureWorks. Ваш руководитель просит вас предоставить отчеты, которые включают:
●       Нарастающие итоги продаж с начала года (YTD), квартала (QTD) и месяца (MTD).
●       Сравнение продаж текущего месяца с тем же месяцем прошлого года.
●       Скользящие средние продаж за последние 3 или 6 месяцев для сглаживания сезонных колебаний.
●       Продажи за предыдущий период (например, предыдущий квартал).

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

Давайте погрузимся в мир MDX-функций для анализа времени и посмотрим, как они помогают извлекать глубокие инсайты из данных AdventureWorks.
Основы работы со временем в MDX

Прежде чем мы перейдем к функциям, важно понимать, что для эффективного анализа времени ваш куб должен иметь правильно спроектированное измерение Времени (Time Dimension). Это измерение должно содержать иерархии, такие как "Календарь" (Год -> Квартал -> Месяц -> День) или "Финансовый год". В AdventureWorks такое измерение обычно называется [Date] и содержит иерархию [Calendar].

Ключевые функции для навигации по времени:
●       CurrentMember: Ссылается на текущий член измерения на оси.
●       Parent: Возвращает родительский член текущего члена.
●       Children: Возвращает дочерние члены текущего члена.
●       PrevMember: Возвращает предыдущий член на том же уровне иерархии.
●       NextMember: Возвращает следующий член на том же уровне иерархии.
●       Lag(N): Возвращает член, который находится на N позиций назад от текущего члена на том же уровне.
●       Lead(N): Возвращает член, который находится на N позиций вперед от текущего члена на том же уровне.

Эти функции позволяют нам перемещаться по иерархии времени и выбирать нужные периоды для сравнений и расчетов.
Расчеты "Год к дате" (YTD, QTD, MTD)

Нарастающие итоги (Year-to-Date, Quarter-to-Date, Month-to-Date) — это одни из самых востребованных аналитических показателей. Они показывают сумму показателя с начала определенного периода (года, квартала, месяца) до текущей даты. MDX предлагает специализированные функции для их вычисления.

PeriodsToDate(): Универсальная функция для нарастающих итогов Функция PeriodsToDate() является наиболее универсальной для вычисления нарастающих итогов. Она суммирует значения меры от начала указанного уровня иерархии времени до текущего члена.

Синтаксис:
PeriodsToDate(Level_Expression, Member_Expression)
●       Level_Expression: Уровень иерархии времени, с которого начинается агрегация (например, уровень года для YTD, уровень квартала для QTD).
●       Member_Expression: Член иерархии времени, до которого производится агрегация (обычно CurrentMember на оси).

Пример 1: Продажи YTD (Year-to-Date) для AdventureWorks
Рассчитаем нарастающие продажи с начала года до текущего месяца.
WITH MEMBER [Measures].[Sales Amount YTD] AS
 Sum(
   PeriodsToDate(
     [Date].[Calendar].[Calendar Year], -- Уровень, скоторогоначинаемагрегацию(год)
[Date].[Calendar].CurrentMember-- Текущий член (месяц, квартал или день)
   ),
   [Measures].[Sales Amount]
 )
 , FORMAT_STRING = "Currency"
SELECT
 {[Measures].[Sales Amount], [Measures].[Sales Amount YTD]} ON COLUMNS,
 NON EMPTY [Date].[Calendar].[Month].Members ON ROWS
FROM
 [Adventure Works]
WHERE
 [Date].[Calendar Year].&[2007]


Разбор:
●       [Date].[Calendar].[Calendar Year]: Указывает, что мы хотим агрегировать с начала каждого календарного года.
●       [Date].[Calendar].CurrentMember: Указывает, что агрегация идет до текущего члена на оси ROWS (в данном случае, до каждого месяца).
●       Sum(...): Суммирует [Sales Amount] по всем членам, возвращенным PeriodsToDate().
●       Мы отображаем это по месяцам 2007 года.

Пример 2: Продажи QTD (Quarter-to-Date) для AdventureWorks
Аналогично, для квартальных итогов:
WITH MEMBER [Measures].[Sales Amount QTD] AS
 Sum(
   PeriodsToDate(
     [Date].[Calendar].[Calendar Quarter], -- Уровень, с которого начинаем агрегацию (квартал)
[Date].[Calendar].CurrentMember-- Текущий член (месяц или день)
   ),
   [Measures].[Sales Amount]
 )
 , FORMAT_STRING = "Currency"
SELECT
 {[Measures].[Sales Amount], [Measures].[Sales Amount QTD]} ON COLUMNS,
 NON EMPTY [Date].[Calendar].[Month].Members ON ROWS
FROM
 [Adventure Works]
WHERE
 [Date].[Calendar Year].&[2007]


Разбор:
●       Здесь Level_Expression — [Date].[Calendar].[Calendar Quarter], что означает агрегацию с начала каждого квартала.

Пример 3: Продажи MTD (Month-to-Date) для AdventureWorks
Для месячных итогов:
WITH MEMBER [Measures].[Sales Amount MTD] AS
 Sum(
   PeriodsToDate(
     [Date].[Calendar].[Date], -- Уровень, с которого начинаем агрегацию (день, если хотим MTD по дням)
     [Date].[Calendar].CurrentMember -- Текущий член (день)
   ),
   [Measures].[Sales Amount]
 )
 , FORMAT_STRING = "Currency"
SELECT
 {[Measures].[Sales Amount], [Measures].[Sales Amount MTD]} ON COLUMNS,
 NON EMPTY [Date].[Calendar].[Date].Members ON ROWS
FROM
 [Adventure Works]
WHERE
[Date].[Calendar].[Month].&[2007]&[7] -- Пример для июля 2007 года


Разбор:
●       Для MTD мы обычно агрегируем с начала месяца до текущего дня. Поэтому Level_Expression может быть уровнем [Date] (если у вас есть иерархия до уровня дня) или уровнем [Month] в зависимости от детализации.
Сокращенные функции для YTD, QTD, MTDMDX также предоставляет более короткие, специфические функции для YTD, QTD, MTD, которые являются синтаксическим сахаром для PeriodsToDate():
●       YTD(): Sum(PeriodsToDate([Date].[Calendar].[Calendar Year], CurrentMember), Measure)
●       QTD(): Sum(PeriodsToDate([Date].[Calendar].[Calendar Quarter], CurrentMember), Measure)
●       MTD(): Sum(PeriodsToDate([Date].[Calendar].[Month], CurrentMember), Measure)

Пример 4: Использование YTD() для продаж AdventureWorks
WITH MEMBER [Measures].[Sales Amount YTD (Short)] AS
 YTD([Date].[Calendar].CurrentMember, [Measures].[Sales Amount])
 , FORMAT_STRING = "Currency"
SELECT
 {[Measures].[Sales Amount], [Measures].[Sales Amount YTD (Short)]} ON COLUMNS,
 NON EMPTY [Date].[Calendar].[Month].Members ON ROWS
FROM
 [Adventure Works]
WHERE
 [Date].[Calendar Year].&[2007]


Эти сокращенные функции удобны, но важно понимать, что за ними стоит логика PeriodsToDate().
Сравнение периодов: ParallelPeriod() и Lag()

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

ParallelPeriod(): Для сравнения с параллельным периодом Функция ParallelPeriod() возвращает член из предыдущего параллельного периода на заданном уровне. Она идеально подходит для сравнений "год к году", "квартал к кварталу" или "месяц к месяцу".

Синтаксис:
ParallelPeriod(Level_Expression, [Lag], Member_Expression)
●       Level_Expression: Уровень, по которому определяется параллельный период (например, [Date].[Calendar].[Calendar Year] для года назад).
●       [Lag]: Необязательный аргумент, указывающий количество периодов для смещения (по умолчанию 1).
●       Member_Expression: Член, относительно которого определяется параллельный период (обычно CurrentMember).

Пример 5: Продажи AdventureWorks за тот же месяц прошлого года (Sales Last Year)
WITH MEMBER [Measures].[Sales Amount Last Year] AS
 ([Measures].[Sales Amount], ParallelPeriod(
   [Date].[Calendar].[Calendar Year], -- Уровень для параллельного периода
   1,                                -- Смещение на 1 год
   [Date].[Calendar].CurrentMember   -- Относительно текущего члена
 ))
 , FORMAT_STRING = "Currency"
MEMBER [Measures].[Sales Growth % YOY] AS
 IIF(
   IsEmpty([Measures].[Sales Amount Last Year]),
   NULL,
   ([Measures].[Sales Amount] - [Measures].[Sales Amount Last Year]) / [Measures].[Sales Amount Last Year]
 )
 , FORMAT_STRING = "Percent"
SELECT
 {[Measures].[Sales Amount], [Measures].[Sales Amount Last Year], [Measures].[Sales Growth % YOY]} ON COLUMNS,
 NON EMPTY [Date].[Calendar].[Month].Members ON ROWS
FROM
 [Adventure Works]
WHERE
 [Date].[Calendar Year].&[2008] -- Анализируем 2008 год, сравнивая с 2007


Разбор:
●       [Measures].[Sales Amount Last Year]: Мы создаем расчетную меру, которая получает [Sales Amount] для параллельного периода (того же месяца/квартала/дня) в предыдущем году.
●       [Sales Growth % YOY]: Затем мы используем эту меру для расчета процентного роста год к году. Обработка IsEmpty важна для корректного отображения, когда данных за прошлый год нет.
Lag(): Для смещения на N позицийФункция Lag(N) возвращает член, который находится на N позиций назад от текущего члена на том же уровне иерархии. Она полезна для сравнения с предыдущим месяцем, кварталом или для построения скользящих средних.
Синтаксис:
Member_Expression.Lag(N)
●       Member_Expression: Член, относительно которого производится смещение (обычно CurrentMember).
●       N: Количество позиций для смещения назад.

Пример 6: Продажи AdventureWorks за предыдущий месяц (Sales Last Month)
WITH MEMBER [Measures].[Sales Amount Last Month] AS
 ([Measures].[Sales Amount], [Date].[Calendar].CurrentMember.Lag(1))
 , FORMAT_STRING = "Currency"
SELECT
 {[Measures].[Sales Amount], [Measures].[Sales Amount Last Month]} ON COLUMNS,
 NON EMPTY [Date].[Calendar].[Month].Members ON ROWS
FROM
 [Adventure Works]
WHERE
 [Date].[Calendar Year].&[2007]


Разбор:
●       [Date].[Calendar].CurrentMember.Lag(1): Возвращает предыдущий месяц относительно текущего месяца на оси.
Построение скользящих средних (Moving Averages)

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

LastPeriods(): Для набора последних N периодов Функция LastPeriods(N, Member_Expression) возвращает набор, содержащий N членов, заканчивающихся на Member_Expression (включая его).

Синтаксис:
LastPeriods(N, Member_Expression)
●       N: Количество периодов для включения в набор. Если N отрицательное, то возвращаются следующие N периодов.
●       Member_Expression: Член, с которого начинается отсчет назад (обычно CurrentMember).

Пример 7: Скользящее среднее продаж AdventureWorks за 3 месяца (3-Month Moving Average)
WITH MEMBER [Measures].[3-Month Moving Average Sales] AS
 IIF(
   Count(LastPeriods(3, [Date].[Calendar].CurrentMember), EXCLUDEEMPTY) < 3,
   NULL, -- Не показывать, если данных меньше чем за 3 полных месяца
   Avg(
     LastPeriods(3, [Date].[Calendar].CurrentMember),
     [Measures].[Sales Amount]
   )
 )
 , FORMAT_STRING = "Currency"
SELECT
 {[Measures].[Sales Amount], [Measures].[3-Month Moving Average Sales]} ON COLUMNS,
 NON EMPTY [Date].[Calendar].[Month].Members ON ROWS
FROM
 [Adventure Works]
WHERE
 [Date].[Calendar Year].&[2007] -- Или более широкий диапазон, чтобы видеть начало MA


Разбор:
●       LastPeriods(3, [Date].[Calendar].CurrentMember): Создает набор из текущего месяца и двух предыдущих.
●       Avg(...): Вычисляет среднее [Sales Amount] по этому набору.
●       IIF(Count(..., EXCLUDEEMPTY) < 3, NULL, ...): Это важная проверка. Мы используем Count(..., EXCLUDEEMPTY) для подсчета непустых месяцев в наборе. Если количество непустых месяцев меньше 3 (т.е. у нас еще нет полных трех месяцев данных), мы возвращаем NULL, чтобы избежать искажения среднего в начале ряда. Это соответствует лучшим практикам, описанным в литературе по MDX, для предотвращения "неполных" скользящих средних.
Использование Range и CurrentMember.Lag() для скользящих средних Альтернативный способ создания набора для скользящего среднего — это использование оператора диапазона : в сочетании с CurrentMember.Lag().

Пример 8: Скользящее среднее продаж AdventureWorks за 6 месяцев (6-Month Moving Average)
WITH MEMBER [Measures].[6-Month Moving Average Sales] AS
 IIF(
   Count(
[Date].[Calendar].CurrentMember.Lag(5) : [Date].[Calendar].CurrentMember,
     EXCLUDEEMPTY
   ) < 6,
   NULL,
   Avg(
[Date].[Calendar].CurrentMember.Lag(5) : [Date].[Calendar].CurrentMember,
     [Measures].[Sales Amount]
   )
 )
 , FORMAT_STRING = "Currency"
SELECT
 {[Measures].[Sales Amount], [Measures].[6-Month Moving Average Sales]} ON COLUMNS,
 NON EMPTY [Date].[Calendar].[Month].Members ON ROWS
FROM
 [Adventure Works]
WHERE
 [Date].[Calendar Year].&[2007] -- Илиболееширокийдиапазон


Разбор:
●       [Date].[Calendar].CurrentMember.Lag(5) : [Date].[Calendar].CurrentMember: Создает набор, начинающийся за 5 месяцев до текущего и заканчивающийся текущим месяцем. Это набор из 6 месяцев.
●       Логика IIF аналогична предыдущему примеру, чтобы гарантировать наличие полных 6 месяцев данных.
Важные соображения и лучшие практики для анализа времени

1.     Календарь: Убедитесь, что ваше измерение времени правильно спроектировано и содержит все необходимые уровни (год, квартал, месяц, день) и иерархии. Для сложных финансовых отчетов может потребоваться несколько иерархий (например, Календарный год и Финансовый год).
2.     Обработка пустых значений: Всегда используйте IIF с IsEmpty() или проверкой на 0 для предотвращения ошибок деления на ноль и некорректных расчетов, особенно при вычислении процентов или средних.
3.     Производительность:
○       NON EMPTY на осях очень важен для производительности, особенно при работе с разреженными данными.
○       Сложные расчеты нарастающих итогов или скользящих средних могут быть ресурсоемкими. Если они используются очень часто, рассмотрите возможность их реализации на уровне куба (в MDX-скрипте) или даже как предварительно агрегированных мер, если это возможно.
○       Избегайте избыточных вычислений внутри циклов или функций, которые итерируют по большим наборам.
4.     Контекст: Всегда помните о контексте вычисления. Функции времени, такие как CurrentMember, ParallelPeriod, Lag, работают относительно текущего контекста ячейки. Понимание этого взаимодействия критически важно для получения правильных результатов.
5.     Форматирование: Используйте FORMAT_STRING для правильного отображения денежных значений, процентов и чисел. Это значительно улучшает читаемость отчетов.
Заключение

MDX предоставляет исключительно мощные и гибкие инструменты для анализа данных во временном измерении. Функции PeriodsToDate(), YTD(), QTD(), MTD(), ParallelPeriod() и Lag() позволяют легко выполнять сложные расчеты нарастающих итогов, сравнения с предыдущими периодами и построение скользящих средних. Эти возможности выходят далеко за рамки того, что легко достижимо с помощью традиционного SQL, и являются одной из ключевых причин использования OLAP-кубов для бизнес-аналитики.

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