代码改变世界

MDX Step by Step 读书笔记(九) - Working with Time 处理时间

2013-11-21 17:08  BIWORK  阅读(4268)  评论(1编辑  收藏  举报

开篇介绍

这一章节主要用到的 MDX 函数:

  • PeriodsToDate( [Level , [Member]] ) - 从指定级别的范围内,返回与指定成员同一级别,从第一个期间开始到指定成员结束的期间集。
  • Ytd( [Member] )
  • Qtd( [Member] )
  • Mtd( [Member] )
  • Wtd( [Member] )
  • LastPeriods( n [, Member] )  - 返回从比指定成员表达式滞后 n - 1 个位置的成员开始,到指定成员为止的期间集。
  • ParallelPeriod( [Level [,n [, Member]]] ) - 在指定级别处指定成员的祖先,查找指定滞后的该祖先的同级,返回该同级后代中指定成员的并行期间。
  • OpeningPeriod( [Level [, Member]] ) / ClosingPeriod ( [Level [, Member]] ) - 返回指定级别上的默认成员的后代中的第一个/最后一个同级。

主要实现和讲解的内容:Year to date 的 Running Total, 同比,环比 等计算方式。

时间函数

时间在商业分析过程中是必不可缺的一环,分析人员一般会基于现在这个时间点对业务的分析,或者回顾之前某个时间点然后对未来某个时间点的数据做出预测或者估计。在分析服务中有一系列的时间函数可以帮助大家更好的处理和分析 SSAS 数据库中的数据。

作为时间维度的核心,一个或者多个用户自定义的层次结构对日历做出了分解。在这个日历上你可以从高级别的粒度比如说年钻取到低级别的粒度比如说季度,月和天这些时间点上。下面的这幅图上就显示了基于标准日历的一个层次结构 -

计算累加值

在分析业务中,度量值常用来表示某些累加值。比如说 10月份的 零售额 Reseller Sales,单独来看这个数据可能还是比较有用的,但是更有用的信息可能还是用户想知道从今年年初到10月份 (包括10月份) 的所有零售额是多少,因为他需要知道总零售额和今年的预期目标还有多少差距。

因此,可以通过这样的时间函数来实现这样的累加值的计算。

PeriodsToDate( [Level , [Member]] )

  • 从指定级别的范围内,返回与指定成员处于同一级别,从第一个期间开始到指定成员结束的期间集。
  • 如果指定了级别,则层次结构的当前成员就是指 Hierarchy, CurrentMember, 其中 Hierarchy 是指定级别的层次结构。
  • 如果又没有指定级别又没有指定成员,那么该级别是度量值组中 Time 类型的第一个维度上第一个层次结构的当前成员的父级。

下面这个例子中通过 PeriodsToDate 函数的第一个参数定义了时间的级别是 [Date].[Calendar].[Calendar Year],比如 CY 2002, CY2003,第二个参数的成员必须都是从这个级别往下的范围来取。比如第二个参数是 [Date].[Calendar].[Month].[April 2002] 很显然第二个参数透露出来的它的 CY 是指2002,那么这个 PeriodsToDate 的计算日期范围就是 CY 2002年的第一个月到 CY 2002的第四个月。

SELECT
{([Measures].[Reseller Sales Amount])} ON COLUMNS,
{
  PeriodsToDate(
     [Date].[Calendar].[Calendar Year],
     [Date].[Calendar].[Month].[April 2002]
  )
} ON ROWS
FROM [Step-by-Step]

从分析服务的角度来看这个过程,在这个函数中所使用的级别是 Calendar 层次结构中的 Calendar Year ,因此分析服务从成员 April 2002 开始沿着这个级别朝着它的祖先来移动并到达 CY 2002。这时因为通过 [Date].[Calendar].[Month].[April 2002] 得知要取的成员位于 Month 级别下,因此分析服务通过获取在 Month 级别下 CY 2002 成员的第一个直系亲属再次定义到一个成员 -  January 2002,那么接着把从 January 2002 到 April 2002 这一段成员再通过 [Date].[Calendar].[Month].[January 2002]:[Date].[Calendar].[Month].[April 2002] 得到这样的一个集合。

上面的这个例子演示了  PeriodsToDate 这样的一个函数,但是我们所需要做的是这种 Running Total, Year-To-Date Total 的效果,我们先看看自然月的情况。

SELECT
{([Measures].[Reseller Sales Amount])} ON COLUMNS,
{[Date].[Calendar].[Month].Members} ON ROWS
FROM [Step-by-Step]

我们可以按照年,季度,月等实现 Running total 的计算。

WITH
-- Running total
MEMBER [Measures].[Reseller Running Total]
AS
AGGREGATE(
PERIODSTODATE([Date].[Calendar].[(All)],
  [Date].[Calendar].CurrentMember
),
([Measures].[Reseller Sales Amount])
)
-- Running total for Year
MEMBER [Measures].[Reseller Running Total Year]
AS
AGGREGATE(
PERIODSTODATE([Date].[Calendar].[Calendar Year],
[Date].[Calendar].CurrentMember
),
([Measures].[Reseller Sales Amount])
)
-- Running total for Semester
MEMBER [Measures].[Reseller Running Total Semester]
AS
AGGREGATE(
PERIODSTODATE([Date].[Calendar].[Calendar Semester],
[Date].[Calendar].CurrentMember
),
([Measures].[Reseller Sales Amount])
)
-- Running total for Quarter
MEMBER [Measures].[Reseller Running Total Quarter]
AS
AGGREGATE(
PERIODSTODATE([Date].[Calendar].[Calendar Quarter],
[Date].[Calendar].CurrentMember
),
([Measures].[Reseller Sales Amount])
)
SELECT
{
([Measures].[Reseller Sales Amount]),
([Measures].[Reseller Running Total]),
([Measures].[Reseller Running Total Year]),
([Measures].[Reseller Running Total Semester]),
([Measures].[Reseller Running Total Quarter]) 
} ON COLUMNS,
{[Date].[Calendar].[Month].Members} ON ROWS
FROM [Step-by-Step]

简化了的 Periods To Date 的计算

实际上在大多数的时间维度设计过程中,一些时间属性例如年,季度,月,周都已经被预先定义好了。因此在使用 Periods To Date 计算这些时间点的时候,可以用更加简单的方式来表达 Year to date, quarter to date, month to date 和 week to date 等,这样就不再需要一定要指定 Calendary 级别了。

  • Ytd( [Member] )
  • Qtd( [Member] )
  • Mtd( [Member] )
  • Wtd( [Member] )
WITH
-- Running total
MEMBER [Measures].[Reseller Running Total]
AS
AGGREGATE(
PERIODSTODATE([Date].[Calendar].[(All)],
  [Date].[Calendar].CurrentMember
 ),
([Measures].[Reseller Sales Amount])
)
-- Running total for Year
MEMBER [Measures].[Reseller Running Total Year]
AS
AGGREGATE(
YTD([Date].[Calendar].CurrentMember ),
([Measures].[Reseller Sales Amount])
)
-- Running total for Quarter
MEMBER [Measures].[Reseller Running Total Quarter]
AS
AGGREGATE(
QTD([Date].[Calendar].CurrentMember ),
([Measures].[Reseller Sales Amount])
)
SELECT
{
   ([Measures].[Reseller Sales Amount]),
   ([Measures].[Reseller Running Total]),
   ([Measures].[Reseller Running Total Year]), 
   ([Measures].[Reseller Running Total Quarter]) 
} ON COLUMNS,
{[Date].[Calendar].[Month].Members} ON ROWS
FROM [Step-by-Step]

在上面的两个例子中要注意到这个成员的定义,通过使用 [(All)] 这个虚拟的级别来表示取的是 [Date].[Calendar] 范围下所有的时间,打破了年或者季度级别的限制的一个 Running Total。

WITH
-- Running total
MEMBER [Measures].[Reseller Running Total]
AS
AGGREGATE(
PERIODSTODATE([Date].[Calendar].[(All)],
  [Date].[Calendar].CurrentMember
 ),
([Measures].[Reseller Sales Amount])
)

这种写法本身是合法的没有任何问题,但是很多 MDX 开发者可能会选择另外的一种捷径:

NULL: [Date].[Calendar].CurrentMember

NULL 成员的引用使得分析服务去计算并返回从当前成员所在级别上的第一个成员到当前成员这样的一个范围,这样就省去了 PERIODSTODATE 这个函数,当然效果也完全是一样的。

WITH
-- Running total
MEMBER [Measures].[Reseller Running Total]
AS
AGGREGATE(
PERIODSTODATE([Date].[Calendar].[(All)],
  [Date].[Calendar].CurrentMember
 ),
([Measures].[Reseller Sales Amount])
)
-- Running total From NULL
MEMBER [Measures].[Reseller Running Total From NULL]
AS
AGGREGATE(
(NULL:[Date].[Calendar].CurrentMember),
([Measures].[Reseller Sales Amount])
)
SELECT
{
   ([Measures].[Reseller Sales Amount]),
   ([Measures].[Reseller Running Total]),
   ([Reseller Running Total From NULL]) 
} ON COLUMNS,
{[Date].[Calendar].[Month].Members} ON ROWS
FROM [Step-by-Step]

计算累加平均值

可以使用到这个函数 LastPeriods( n [, Member] )

  • 如果指定的期间数为正数,那么 LastPeriods 返回一个成员集,其中的成员从比指定成员表达式滞后 n - 1 个位置的成员开始,到指定成员为止。返回的成员数就是 n。
  • 如果指定的期间数为负,那么 LastPeriods 返回一个成员集,其中的成员从指定的成员开始,到比指定的成员超前 n - 1 个位置的成员为止。
  • 如果指定的期间数位零,那么 LastPeriods 返回空集。
SELECT
{([Measures].[Reseller Sales Amount])} ON COLUMNS,
{
           LastPeriods(
                             3,
                            [Date].[Calendar].[Month].[January 2002]
                           )
} ON ROWS
FROM [Step-by-Step]

返回 2002年1月份之前前两个月和包含自身的 Reseller Sales Amount 值。

如果返回之后的几个月就是像这样改一下 -

{([Measures].[Reseller Sales Amount])} ON COLUMNS,
{
LastPeriods(
 -3,
 [Date].[Calendar].[Month].[January 2002]
   )
} ON ROWS
FROM [Step-by-Step]

下面这个例子展示了累加平均值,计算含当前月前三个月的平均 Reseller Sales Amount。

WITH MEMBER [Measures].[Running Total Reseller Sales Amount of Previous 3 Month]
AS
AGGREGATE(
 LastPeriods(3, [Date].[Calendar].CurrentMember),
 ([Measures].[Reseller Sales Amount])
)
MEMBER [Measures].[Avg Reseller Sales Amount of Previous 3 Month]
AS
AVG(
 LastPeriods(3, [Date].[Calendar].CurrentMember),
 ([Measures].[Reseller Sales Amount])
)
SELECT
{
  ([Measures].[Reseller Sales Amount]),
  ([Measures].[Running Total Reseller Sales Amount of Previous 3 Month]),
  ([Measures].[Avg Reseller Sales Amount of Previous 3 Month])
} ON COLUMNS,
{
[Date].[Calendar].[Month].MEMBERS
} ON ROWS
FROM [Step-by-Step]

关于上期比(Period Over Period)的分析

在数据分析中历史数据经常拿来和当前数据进行对比分析,在这里先了解两个概念同比和环比。

同比 - 同期相比,比较的是当前时期与比如说上一年同时期的数据对比,这样就能比较一个相对数据变化。比如说今年8月份的销售额是1000万美金,同比上一年的7000W美金增长了1000W美金,或者用百分比表示。

环比 - 与相连的上一期进行对比。如果比较的周期是周,环比就是本周比上周的;如果比较的周期是月,那么环比就是本月比上一个月;同样如果比较的周期是年,那么就是当年比上一年。如果用倍数计算要得到环比增长率,那么就是 (当前周期 - (当前周期 - 1)) / (当前周期 - 1)  最后得到一个结果乘以 100% 就可以了。

环比还是用的非常多的,因为它能够表明某现象的逐期发展速度。比如2月比1月,3月比2月,4月比3月…12月比11月,它能够说明逐月的发展速度。比如在分析抗击非典期间某些经济现象的发展趋势,环比比同比更能说明问题。

我们可以通过 ParallelPeriod 函数来实现同比,它返回上一期间具有与指定成员相同的相对位置的成员。

ParallelPeriod( [Level [,n [, Member]]] )

  • 在指定级别处获取指定成员的祖先,然后查找具有指定滞后的该祖先的同级,最后返回该同级后代中指定成员的并行期间。
  • 如果没有指定级别表达式,也没有指定成员表达式,则默认成员值为度量值组中类型为 Time 的第一个维度上第一个层次结构的当前成员。
  • 如果指定了级别表达式,但未指定成员表达式,则默认成员值为 Level_Expression.Hierarchy.CurrentMember。
  • 默认索引值为1。
  • 默认级别为指定成员的父成员。

先来回顾一下这个例子 - 获取 CY 2003年各个月份的 Reseller Sales Amount。

SELECT
{([Measures].[Reseller Sales Amount])} ON COLUMNS,
{
Descendants(
[Date].[Calendar].[Calendar Year].[CY 2003],
[Date].[Calendar].[Month],
SELF
)
} ON ROWS
FROM [Step-by-Step]

再来通过 ParallelPeriod 获取一个以年为单位的同比对象。

WITH
MEMBER [Measures].[x] AS
ParallelPeriod(
[Date].[Calendar].[Calendar Year],
1,
[Date].[Calendar].CurrentMember
).Name
SELECT
{
([Measures].[Reseller Sales Amount]),
([Measures].[x])
} ON COLUMNS,
{
Descendants(
[Date].[Calendar].[Calendar Year].[CY 2003],
[Date].[Calendar].[Month],
SELF
)
} ON ROWS
FROM [Step-by-Step]

[Date].[Calendar].CurrentMember 在查询中指的就是 CY 2003年的 Month 级别下的对象,以 CY2003 1月为例,首先因为级别定义的是 [Date].[Calendar].[Calendar Year],因此首先要通过 CY 2003 年1月这个对象找到它的年祖先也就是 CY2003。

第二个参数 1 表示往前推 1 个时期单位,这个时期单位在第一个参数也给出了,因此就是 CY 2003 的上一年即 CY 2002。找到这个 CY 2002 之后来取得与原对象相同级别下的成员,即 CY 2002 1月这个对象。

ParallelPeriod(
[Date].[Calendar].[Calendar Year],
1,
[Date].[Calendar].CurrentMember
).Name

理解了这些概念之后,我们就可以基于 CY 2003年中的月来获取它们的同比数据。

WITH
MEMBER [Measures].[Prior Period Reseller Sales Amount] AS
(
ParallelPeriod(
[Date].[Calendar].[Calendar Year],
1,
[Date].[Calendar].CurrentMember
  ),
[Measures].[Reseller Sales Amount]
)
,FORMAT="Currency"
SELECT
{
([Measures].[Reseller Sales Amount]),
([Measures].[Prior Period Reseller Sales Amount])
} ON COLUMNS,
{
Descendants(
 [Date].[Calendar].[Calendar Year].[CY 2003],
 [Date].[Calendar].[Month],
 SELF
)
} ON ROWS
FROM [Step-by-Step]

当然我们更应该计算一下同比率 -  (当前日期数据 - 同比时期数据) / 同比时期数据

WITH
MEMBER [Measures].[Prior Period Reseller Sales Amount] AS
(
ParallelPeriod(
[Date].[Calendar].[Calendar Year],
1,
[Date].[Calendar].CurrentMember
),
[Measures].[Reseller Sales Amount]
)
,FORMAT="Currency"
MEMBER [Measures].[Prior Period Growth] AS
(
([Measures].[Reseller Sales Amount])-([Measures].[Prior Period Reseller Sales Amount])
)
/
([Measures].[Prior Period Reseller Sales Amount])
,FORMAT="Percent"
SELECT
{
([Measures].[Reseller Sales Amount]),
([Measures].[Prior Period Reseller Sales Amount]),
([Measures].[Prior Period Growth])
} ON COLUMNS,
{
Descendants(
[Date].[Calendar].[Calendar Year].[CY 2003],
[Date].[Calendar].[Month],
SELF
)
} ON ROWS
FROM [Step-by-Step]

再来通过前面学习到的一个函数来完成一个按月份的环比查询。

WITH
MEMBER [Measures].[Prior Month Reseller Sales Amount] AS
(
(LASTPERIODS(2,[Date].[Calendar].CurrentMember).Item(0),[Measures].[Reseller Sales Amount])
)
,FORMAT="Currency"
MEMBER [Measures].[Prior Period Growth] AS
(
([Measures].[Reseller Sales Amount] - [Measures].[Prior Month Reseller Sales Amount])
/
([Measures].[Prior Month Reseller Sales Amount])
)
,FORMAT="Percent"
SELECT
{
([Measures].[Reseller Sales Amount]),
([Measures].[Prior Month Reseller Sales Amount]),
([Measures].[Prior Period Growth])
} ON COLUMNS,
{
Descendants(
[Date].[Calendar].[Calendar Year].[CY 2003],
[Date].[Calendar].[Month],
SELF
)
} ON ROWS
FROM [Step-by-Step]

要注意的地方

实际上基于时间的 MDX 函数并不是说它们在使用过程中能够感知和理解时间,实际上也可以通过导航来实现它们的一些功能。例如下面这个逻辑就是取得 2003年所有月份与上一年所有月份的 Reseller Sales Amount,这样就可以很容易的实现同比。

WITH
MEMBER [Measures].[x] AS
Cousin(
[Date].[Calendar].CurrentMember,
Ancestor(
[Date].[Calendar].CurrentMember,
[Date].[Calendar].[Calendar Year]
).Lag(1)
).Name
SELECT
{
([Measures].[Reseller Sales Amount]),
([Measures].[x])
} ON COLUMNS,
{
Descendants(
[Date].[Calendar].[Calendar Year].[CY 2003],
[Date].[Calendar].[Month],
SELF
)
} ON ROWS
FROM [Step-by-Step]

再来看这个例子 -

WITH
MEMBER [Measures].[x] AS
ParallelPeriod(
[Date].[Calendar].[Calendar Year],
1,
[Date].[Calendar].CurrentMember
).Name
SELECT
{
([Measures].[Reseller Sales Amount]),
([Measures].[x])
} ON COLUMNS,
{
Descendants(
[Date].[Calendar].[Calendar Year].[CY 2002],
[Date].[Calendar].[Month],
SELF
)
} ON ROWS
FROM [Step-by-Step]

按照我们的理解 x 中的对象应该是 January 2001 , February 2001 …. 按照这样排下去,正好是一个同比的场景。NULL ? 为什么呢?

但是快速的查询一下 CY 2001 的 Month 成员后就明白了这个原因,因为在 CY 2001 年中, July 2001 是第一个成员。并且到了 December 2001 的时候就结束了,因此这个时期指定了只能在 CY 2002 - 1 即 CY 2001 这个范围来取,这样就会造成后面的成员都是 NULL。当然我们也没有办法直接可以解决这样的一个问题,所以对于这样的问题是在开发过程中更要注意的,要知道我们查询对象的边界范围有多大。

Combining Time-Based Metrics 结合时间指标

还是先写一个比较简单的查询 - CY 2003 年各个月份的 Reseller Sales Amount 。

然后继续看看之前的一个例子 - 根据月份做的一个 Running Total 。

WITH
MEMBER [Measures].[Year to Date Reseller Sales] AS
Aggregate(
PeriodsToDate(
[Date].[Calendar].[Calendar Year],
[Date].[Calendar].CurrentMember
),
([Measures].[Reseller Sales Amount])
)
,FORMAT="Currency"
SELECT
{
([Measures].[Reseller Sales Amount]),
([Measures].[Year to Date Reseller Sales])
} ON COLUMNS,
{
Descendants(
[Date].[Calendar].[CY 2003],
[Date].[Calendar].[Month],
SELF
)
} ON ROWS
FROM [Step-by-Step]

改造一下加上上一年的 Year To Date 的 Running Total。

WITH
MEMBER [Measures].[Prior Period Year to Date Reseller Sales] AS
(
ParallelPeriod(
[Date].[Calendar].[Calendar Year],
1,
[Date].[Calendar].CurrentMember
),
[Measures].[Year to Date Reseller Sales]
)
,FORMAT="Currency"
MEMBER [Measures].[Year to Date Reseller Sales] AS
Aggregate(
PeriodsToDate(
[Date].[Calendar].[Calendar Year],
[Date].[Calendar].CurrentMember
),
([Measures].[Reseller Sales Amount])
)
,FORMAT="Currency"
SELECT
{
([Measures].[Reseller Sales Amount]),
([Measures].[Year to Date Reseller Sales]),
([Measures].[Prior Period Year to Date Reseller Sales])
} ON COLUMNS,
{
Descendants(
[Date].[Calendar].[CY 2003],
[Date].[Calendar].[Month],
SELF
)
} ON ROWS
FROM [Step-by-Step]

看起来可能比较复杂,但是解释一下就全明白了。

MEMBER [Measures].[Year to Date Reseller Sales] AS
Aggregate(
PeriodsToDate(
[Date].[Calendar].[Calendar Year],
[Date].[Calendar].CurrentMember
),
([Measures].[Reseller Sales Amount])
)
,FORMAT="Currency"

这个计算成员求的就是 Year To Date 的 Running Total,那么用在 ROWS 轴上 CY 2003 的时候就是求的 CY 2003 各个月的 Running Total,那么因为这段代码中因为 ParallelPeriod 已经得到 CY 2003 同比的对象了,因此加上 

[Measures].[Year to Date Reseller Sales] 说明的就是 CY 2003 同比对象即 CY 2002 的 Running Total。

ParallelPeriod(
[Date].[Calendar].[Calendar Year],
1,
[Date].[Calendar].CurrentMember
),
[Measures].[Year to Date Reseller Sales]

我们可以验证一下,直接查询 CY 2002 各个月的 Running Total。

WITH
MEMBER [Measures].[Year to Date Reseller Sales] AS
Aggregate(
PeriodsToDate(
[Date].[Calendar].[Calendar Year],
[Date].[Calendar].CurrentMember
),
([Measures].[Reseller Sales Amount])
)
,FORMAT="Currency"
SELECT
{
([Measures].[Reseller Sales Amount]),
([Measures].[Year to Date Reseller Sales]) 
} ON COLUMNS,
{
Descendants(
[Date].[Calendar].[CY 2002],
[Date].[Calendar].[Month],
SELF
)
} ON ROWS
FROM [Step-by-Step]

和上图对比一下,看看 CY 2002 的 Running Total 是否也一样?

 

OpeningPeriod 和ClosingPeriod 函数

最后来看看这两个函数,它们主要用于实践维度,当然也可以用在其它维度,它主要处理半累加 (semi-additive)的度量值。

OpeningPeriod( [Level [, Member]] ) / ClosingPeriod ( [Level [, Member]] )

  • 如果指定了级别表达式,则函数使用包含指定级别的层次结构,返回指定级别上的默认成员的后代中的第一个/最后一个同级。
  • 如果指定了级别表达式和成员表达式,则返回包含指定级别的层次结构中位于指定级别上指定成员后代中的第一个/最后一个同级。
  • 如果既不指定级别表达式也不指定成员表达式,则函数使用默认级别和时间类型维度的成员。

例如,一天结束时的汇率应该取的是这个时期里最后一个非空的对象,那反过来想知道这个时期最开始的一个 End-Of-Day 汇率的话就可以使用 OpeningPeriod 函数来计算了。

WITH
MEMBER [Measures].[First Child Rate] AS
(
OpeningPeriod(
[Date].[Calendar].[Date],
[Date].[Calendar].CurrentMember
),
[Measures].[End of Day Rate]
)
,FORMAT="Standard"
SELECT
{
([Measures].[First Child Rate]),
([Measures].[End of Day Rate])
} ON COLUMNS,
{[Date].[Calendar].Members} ON ROWS
FROM [Step-by-Step]
WHERE ([Destination Currency].[Destination Currency].[Euro])

实际上这个例子中, End Of Day Rate 汇率已经在 Cube 中早就计算好了,但是一段时间内比如说一天之内这个汇率就有可能会发生很多次变化。比如说在上午10点汇率是 1.03 那么在那个时间点 End Of Day Rate 就是1.03。到了下午汇率变成了 1.02,那么 End Of Day Rate 就会变成1.02。

更多 BI 文章请参看 BI 系列随笔列表 (SSIS, SSRS, SSAS, MDX, SQL Server)  如果觉得这篇文章看了对您有帮助,请帮助推荐,以方便他人在 BIWORK 博客推荐栏中快速看到这些文章。