第 1 章 与时间相关的计算
本章为您介绍在接了下来的章节中逐一呈现的四种与时间相关的计算模式。目的是帮助您根据自己的特定需求选择正确的模式。确实,在进行时间相关的计算时,模式的选择是个难题。
首先,什么是与时间相关的计算呢?与时间相关的计算是指任何涉及时间的计算。比如期初至今相关的一组计算,例如年初至今、季初至今或月初至今。这些计算从一个时间段(年、季度、月)的开始累积值,并返回从该时间段开始到报告中所示日期的度量值上一个值。时间段的定义会根据您使用的是公历日历还是会计日历而存在差异。在图 1-1 中,您可以看到一个期初至今计算的示例,其中,YTD 代表年初至今,QTD 代表季初至今。

图 1-1 期初至今计算的示例
这些模式中还包括将特定时间段内的参数和另一时间段内的参数进行比较。例如,您可以将当月的销售额与去年同月的销售额进行比较。与时间相关的计算的另一个示例是一段时期内的移动平均,例如,12 个月的移动平均,它可以平滑折线图并消除对计算结果的季节性影响。四个与时间相关的模式会执行相同的一组计算。
对日历的不同定义会使模式产生不同的结果。通过查看图 1-1,您已经可以理解年初至今计算的不同定义。
根据使用的是公历日历还是会计日历,数字是不同的。提及日历,由于日历的定义,事情很容易变得非常复杂。
例如,您可能有遵循 ISO 标准或您自己定义的基于周的日历。在基于周的日历中,每个月从一周的同一天开始,这一年也是如此。因此,基于周的日历中的一年可能在上一个公历年开始,也可能在下一个公历年结束。此外,出于会计目的,某些日历会将一年分为 13 个时间段,而不是 12 个月。不同的日历需求是导致对与时间相关的模式进行选择的主要原因。
四个与时间相关的模式(依复杂程度的增顺排序):
- 标准时间相关的计算
- 与月相关的计算
- 与周相关的计算
- 自定义时间相关的计算
标准时间相关的计算模式是利用常规的 DAX 时间智能函数来实现。该模式的实现基于假设您的日历是常规公历日历,并且您的会计日历起始于公历季度。例如,如果您的会计日历从 7 月 1 日(公历日历第三季度的开始日期)开始,则 DAX 时间智能函数可以正常工作。但是,如果您的会计日历从 3 月 1 日开始,则可能会出现出乎意料的计算结果,这既因为 3 月不是一个公历季度的开始,又因为会计日历在处理闰年方面的历史缺陷。尽管有这些限制,该模式还是易于使用和执行的,因为它依赖于标准 DAX 函数,并且可以与常规日期表配合使用,几乎没有要求。
其他的三个模式不使用 DAX 时间智能计算。它们使用基本的 DAX 函数进行编写,这就给以季度、月份和周定义日历提供了更大的灵活性。这些模式需要您创建一个 Date 表,DAX 度量值需要通过该表中的列来识别该年度的时间段。例如,您需要一个包含年份的列、一个包含季度的列、一个包含月份的列,以及其他用于简化计算的列。
此外,在检测和筛选时间段时需要考虑许多细节。事实证明,许多看起来对于人类而言很容易的计算,对于计算机,就非常复杂。当您将一个季度与上一个季度进行比较时,您需要为两个季度选择不同的天数:1 月至 3 月的季度比 4 月至 6 月的季度短。月份也是如此:1 月比 2 月长,但是如果要逐月进行比较,则您所需要的两个日期选择会具有不同的长度。
如果标准时间智能函数不能满足您的需求,则您需要执行其他三种模式之一。这三种模式都需要创建您自己的 Date 表。
标准时间相关的计算模式是利用常规的 DAX 时间智能函数来实现。该模式的实现基于假设您的日历是常规公历日历,并且您的会计日历起始于公历季度。例如,如果您的会计日历从 7 月 1 日(公历日历第三季度的开始日期)开始,则 DAX 时间智能函数可以正常工作。但是,如果您的会计日历从 3 月 1 日开始,则可能会出现出乎意料的计算结果,这既因为 3 月不是一个公历季度的开始,又因为会计日历在处理闰年方面的历史缺陷。尽管有这些限制,该模式还是易于使用和执行的,因为它依赖于标准 DAX 函数,并且可以与常规日期表配合使用,几乎没有要求。
其他的三个模式不使用 DAX 时间智能计算。它们使用基本的 DAX 函数进行编写,这就给以季度、月份和周定义日历提供了更大的灵活性。这些模式需要您创建一个 Date 表,DAX 度量值需要通过该表中的列来识别该年度的时间段。例如,您需要一个包含年份的列、一个包含季度的列、一个包含月份的列,以及其他用于简化计算的列。
此外,在检测和筛选时间段时需要考虑许多细节。事实证明,许多看起来对于人类而言很容易的计算,对于计算机,就非常复杂。当您将一个季度与上一个季度进行比较时,您需要为两个季度选择不同的天数:1 月至 3 月的季度比 4 月至 6 月的季度短。月份也是如此:1 月比 2 月长,但是如果要逐月进行比较,则您所需要的两个日期选择会具有不同的长度。
如果标准时间智能函数不能满足您的需求,则您需要执行其他三种模式之一。这三种模式都需要创建您自己的 Date 表。
与月相关的计算模式是最简单的一种。该模式所执行的所有计算均假设您并不在意日常细节。例如,如果您需要生成两个月份之间进行比较后的计算和报告,该模式非常适用。该模式不支持选择子月份。如果您要比较一个季度中的三天与上一季度中相同的三天,这就超出了这个模式的能力:它不起作用。尽管其分析能力存在很大局限性(仅限于月粒度),但与月相关的模式仍非常快速且易于执行。此外,对于数据超过 12 个月的情况,它也可以无缝处理。它具有定制模式的灵活性,并且比标准时间相关模式简单。如果月粒度这个局限不会影响您的计算,那么非常建议选择使用这个模式。
在与周相关的计算模式中,星期是日历的基础。尽管很多国家采用不同的国家标准来标识年份、季度和星期,但 ISO 8601 是提供星期日期系统定义的标准之一。一年有 52 或 53 个星期,每个季度有 13 个 星期,每个季度又分为 5+4+4 个星期、4+5+4 个星期或 4+4+5 个星期。如果一年中有 53 个星期,则其中一个季度有 14 个 星期。因为一个星期不一定完整地包含在一个月份中,所以应将一个季度中的一组星期称为 “时间段”,虽然我们通常称之为 “月份”。因此,在以下描述中,我们会将月份名称称为 “时间段”。
由于星期是主要单位,因此公历日历中的一年与基于周的日历中的一年之间没有对应关系。基于周的日历始终在相同的星期几开始,例如星期一或星期日。因此,偶尔才会在 1 月 1 日发生这种情况。对于基于周的年,一年起始于上一年的 12 月 29 日,还是当前年份的 1 月 3 日,都没有关系。尽管有些不同寻常,但是基于周的日历具有其显著的特点:一个季度中的每个 “月” 都包含相同数量的星期几。将一个季度与另一个季度进行比较,意味着比较相同的天数和相同的星期几分布。
基于周的日历需要一个专用的 Date 表,该表包含用于进行 DAX 计算的若干列。此外,由于没有现成的可用于对基于周的日历进行计算的 DAX 函数,所以使用自定义 DAX 代码来执行此类计算。与周相关的模式的复杂程度要高于与月相关的模式,因为与周相关的模式可让您实现筛选任何时间段,直到 “天” 的级别。如果您使用基于周的日历,则必须采用基于周的计算模式。
自定义时间相关的计算模式是最灵活(且最复杂)的。作为最后一个模式,它提供与标准时间相关模式相同的计算。区别在于整个模式是使用基本的 DAX 函数编写的,而不使用任何 DAX 时间智能函数。因此,该模式极为灵活,因为您可以随意变动计算的执行方式。更高的灵活性会带来更高的复杂性。
自定义时间相关的计算模式是最灵活(且最复杂)的。作为最后一个模式,它提供了与标准时间相关模式相同的计算。区别在于整个模式是使用基本的 DAX 函数编写的:我们不使用任何 DAX 时间智能函数。因此,该模式极为灵活,因为您可以随意变动计算的方式。更高的灵活性会带来更高的复杂性。
最终,应该选择哪种模式呢?
- 如果常规公历日历满足您的需求,那么显而易见,选择标准时间相关的计算模式。
- 如果月粒度足以满足您的报告需求(通常是这种情况,而且比预期的要多得多),那么与月相关的计算模式就是最佳选择:快速而简便。
- 如果您使用基于周的日历,则需要与周相关的计算模式。
如果以上情况均不足以满足需求,并且您确实需要完全的灵活性,请准备加入一场漫长而有趣的旅程,深入了解复杂的筛选上下文,然后直接进入自定义时间相关的计算模式。
切记:对于商业智能项目,越简单越好。选择最能直接满足您需求的模式。不用多说,如果您对各种模式之间的差异感到好奇,那么在做出选择之前快速通读所有四个章节可能会很有帮助。
第二章 标准时间相关计算
下载样本文件:https://sql.bi/dax-201
在此模式中,我们向您展示如何使用标准日历来实现与时间相关的计算,例如年初至今、上年同期,以及百分比增长。标准日历的最大优势是,您可以依赖几个内置的时间智能函数。内置函数可以为最常见的需求提供正确的结果。
如果内置函数无法满足您的需求,或者如果您使用的是非标准日历,则可以使用常规的(与时间无关的)DAX 函数来达到相同的目标。以这种方式,您可以随意自定义代码的结果。也就是说,如果您需要自定义计算,则还需要使用 DAX 公式操作筛选器所需的一组列来丰富日期表。这些自定义计算包含在 “自定义时间相关的计算” 模式中。
如果您使用常规的公历日历,则此模式中的公式是生成时间智能计算的最简单、最有效的方法。请记住,标准的 DAX 时间智能函数仅支持常规的公历日历 —— 即由 12 个月组成、每个月都有其公历天数、三个月组成一个季度、具备人们普遍惯用的所有常规日历特征的日历。
2.1 时间智能计算介绍
为了使用所有时间智能计算,您需要一个格式正确的日期表。该 Date 表必须满足以下要求:
- 必须具备年份中的所有日期。该 Date 表必须始终始于 1 月 1 日、终于 12 月 31 日,并包括此日期范围内的所有天。如果报告仅采用会计年度,则日期表必须包含从会计年度的第一天到最后一天的所有日期。例如,如果 2008 会计年度始于 2007 年 7 月 1 日,则 Date 表必须包括从 2007 年 7 月 1 日到 2008 年 6 月 30 日的所有天。
- 必须具有包含数据类型为 “日期 / 时间” 或 “日期” 的不重复值的列。该列通常命名为 Date。尽管 Date 列通常用于定义与其他表的关系,但这不是强制的。不过,Date 列必须包含不重复值,并应为其应用 “标记为日期表” 功能。如果该列还包含时间部分,则不应使用时间 —— 例如,时间应始终为 12:00 am。
- 模型中的 Date 表必须标记为日期表,以防没有基于 Date 而与其他表(如示例中的 Sales)建立联系。(译者注:对于符合日期表要求且位于一对多关系一端的日期表,不标记为日期表,也会自动具备日期表的特性。)
有几种创建 Date 表的方法。只要日期表满足要求,创建 Date 表的方式就不会影响您使用标准时间智能计算。如果您已经有一份符合报告需要的 Date 表,只需在确保该表可以满足最低要求后,便可将其导入并将其标记为日期表。如果没有 Date 表,则可以使用 DAX 计算表来创建(后面会做介绍)。
最佳做法是对用于时间智能计算的 Date 表应用 “标记为日期表” 设置。“标记为日期表” 设置会在每次对 Date 列应用筛选器时,在 Date 表上增加一个 REMOVEFILTERS 修饰符。用于 CALCULATE 的所有时间智能函数都会执行此操作(对 Date 列应用筛选器)。如果您使用 Date 列定义 Sales 表和 Date 表之间的关系,则 DAX 会执行相同的操作。尽管如此,将 “标记为日期表” 设置应用于日期表依然是最佳做法。如果您有多个日期表,则可以将它们全部标记为日期表。
如果您没有采用 “标记为日期表” 设置,而且也没有使用日期列来建立关系,那么每当您在 CALCULATE 中使用时间智能函数时,都必须在 Date 表上增加一个 REMOVEFILTERS。《Power BI Desktop 中的时间智能》(https://sql.bi/28211)一文对此进行了详细描述。
2.2 什么是标准 DAX 时间智能函数
标准时间智能函数是表函数,该函数返回在 CALCULATE 中用作筛选器的日期列表。通过编写更复杂的筛选器表达式可以获得时间智能函数的结果。例如,DATESYTD 函数可以返回筛选上下文中从显示的日期所在年份的第一天到显示的最后一天之间的所有日期。下方的表达式:
DATESYTD ( 'Date'[Date] )
对应下方的 FILTER 表达式:
VAR LastDateAvailable = MAX ( 'Date'[Date] )
VAR FirstJanuaryOfLastDate = DATE ( YEAR ( LastDateAvailable ), 1, 1 )
RETURN
FILTER (
ALL ( 'Date'[Date] ),
AND (
'Date'[Date] >= FirstJanuaryOfLastDate,
'Date'[Date] <= LastDateAvailable
)
)
时间智能函数有很多,大多数时间智能函数是以这种方式呈现。请注意:时间智能函数应用作 CALCULATE 的筛选器参数,有时您会通过使用变量来实现这一点。在迭代器中使用时间智能函数是很危险的,因为会触发隐式上下文转换,从而导致从筛选上下文中检索有效日期。DAX 指南文档中提供了更多详细信息,例如 https://dax.guide/datesytd/。
以下是使用时间智能函数时最佳做法的快速指南:
- 仅在 CALCULATE / CALCULATETABLE 的筛选器参数中使用诸如 DATESYTD 之类的时间智能函数,或给变量分配筛选器。
- 在返回值的 DAX 公式中使用 EDATE 和 EOMONTH 之类的标量函数(也称为标量表达式)。这些函数不是时间智能函数,可以用于以行上下文执行的表达式中。
- 使用 CONVERT 将日期转换为数字,反之亦然。
- 有关时间智能函数的完整最新列表,请访问 https://dax.guide/。
DAX 初学者经常将时间智能函数与常规(标量)时间函数混淆。这种混淆导致出现常见错误,可以通过遵循以下建议来避免:
- 不要使用 DATEADD 来返回前一天或后一天。可以使用简单的数学运算符来做到这一点。
- 不要使用 PREVIOUSDAY 来计算标量表达式中的前一天。从日期中减一,即可获得标量表达式中的前一天。
- 不要将 EOMONTH 用作筛选器,而应使用 ENDOFMONTH。EOMONTH 是标量表达式。ENDOFMONTH 是时间智能函数。请始终注意函数的返回类型:只有表函数是时间智能函数,因此不应当用于标量表达式中。
2.2.1 禁用自动日期 / 时间
Power BI 可以自动将 Date 表添加到数据模型。但是,强烈建议禁用 Power BI 创建的自动 *Date* 表,而采用导入或创建的方式生成显式 Date 表。更多详细信息,请参见《Power BI 中的自动时间智能》一文(https://sql.bi/137706)。
自动 Date 表还会启用一种特定语法,称为列变化。列变化的表达方式会在日期列的后面跟着一个点,点的后面跟着自动创建的日期表的列:
Sales[Order Date].[Date]
Power BI 快速度量值用于自动 Date 表时,会大量使用列变化。我们不要依赖 Power BI 中 自动创建的日期表,因为我们希望保持最大的灵活性和对模型的最大控制。列变化的语法不适用于作为模型的一部分的日期表,因此不会自动创建。
2.2.2 标准时间智能函数的局限性
标准时间智能函数可以在常规的公历日历上使用。本节中会列出它们存在的几个局限性。当您的需求与这些局限性不兼容时,您需要采用其他模式(请参见 “自定义时间相关的计算” 和 “与周相关的计算”)。
- 年度起始于 1 月 1 日。对于从不同日期开始的会计日历,该函数起不到太大作用。但是,由于会计日历在处理闰年方面的时间处理缺陷,每个会计年度的第一天必须是相同的,且不能是 3 月 1 日。
- 季度总是起始于一月、四月、七月和十月的第一天。季度的范围不能修改。
- 月份始终是一个公历月。
- 标准时间智能函数可能不正确支持其他列的筛选器,例如 Day of Week 或 Working Day。有关可能的解决方法的更多详细信息,请参见《筛选其他日期属性》一节。
因此,标准时间智能计算不支持许多高阶计算,如,对数周的计算。这些高阶计算需要自定义日历。
2.2.3 创建 Date 表
DAX 时间智能函数可在任何标准公历日历表上使用。如果您已经有一份 Date 表,则可以将其导入并使用,而不会出现任何问题。如果没有 Date 表,则可以使用 DAX 计算表来创建。例如,以下 DAX 表达式定义了本章中使用的简单 Date 表:
计算表
Date =
VAR FirstFiscalMonth = 7
-- 会计年度的第一个月
VAR FirstDayOfWeek = 0
-- 0 = 周日, 1 = 周一, ...
VAR FirstYear =
-- 自定义使用的第一个年份
YEAR ( MIN ( Sales[Order Date] ))
RETURN
GENERATE (
FILTER (
CALENDARAUTO (),
YEAR ( [Date] ) >= FirstYear
),
VAR Yr = YEAR ( [Date] )
-- 年份编号
VAR Mn = MONTH ( [Date] )
-- 月份编号(1-12)
VAR Qr = QUARTER ( [Date] )
-- 季度编号(1-4)
VAR MnQ = Mn - 3 * (Qr - 1)
-- 季度中的月份编号(1-3)
VAR Wd = WEEKDAY ( [Date], 1 ) – 1
-- 星期几(0 = 周日,1 = 周一,...)
VAR Fyr =
-- 会计年度编号
Yr + 1 * ( FirstFiscalMonth > 1 && Mn >= FirstFiscalMonth )
VAR Fqr =
-- 会计季度(字符串)
FORMAT ( EOMONTH ( [Date], 1 - FirstFiscalMonth ), "\QQ" )
RETURN ROW (
"Year", DATE ( Yr, 12, 31 ),
"Year Quarter", FORMAT ( [Date], "\QQ-YYYY" ),
"Year Quarter Date", EOMONTH ( [Date], 3 - MnQ ),
"Quarter", FORMAT ( [Date], "\QQ" ),
"Year Month", EOMONTH ( [Date], 0 ),
"Month", DATE ( 1900, MONTH ( [Date] ), 1 ),
"Day of Week", DATE ( 1900, 1, 7 + Wd + (7 * (Wd < FirstDayOfWeek)) ),
"Fiscal Year", DATE ( Fyr + (FirstFiscalMonth = 1), FirstFiscalMonth, 1 ) - 1,
"Fiscal Year Quarter", "F" & Fqr & "-" & Fyr,
"Fiscal Year Quarter Date", EOMONTH ( [Date], 3 - MnQ ),
"Fiscal Quarter", "F" & Fqr
)
)
您可以自定义前三个变量以创建满足特定业务需求的 Date 表。为了获得正确的结果,当列不是文本格式,而是具有标准或自定义格式的 “日期” 数据类型时,必须按以下方式在数据模型中对列进行设置:
- Date:m/dd/yyyy(8/14/2007), 用作标记为日期表的列
- Year:yyyy(2007)
- Year Quarter:文本(Q3-2008)
- Year Quarter Date:隐藏(9/30/2008)
- Quarter:文本(Q1)
- Year Month:mmm yyyy(Aug 2007)
- Month:mmm(Aug)
- Day of Week:ddd(Tue)
- Fiscal Year:\F\Y yyyy(FY 2008)
- Fiscal Year Quarter:文本(FQ1-2008)
- Fiscal Year Quarter Date:隐藏(9/30/2008)
- Fiscal Quarter:文本(FQ1)
此模式中的 Date 表具有两个层次结构:
- 日历:年(Year),季度(Year Quarter),月(Year Month)
- 会计:年(Fiscal Year),季度(Fiscal Year Quarter),月(Year Month)
不管来源如何,若要使用此模式的公式,Date 表都还必须包括一个隐藏的 DateWithSales 计算列:
Date 表中的计算列
DateWithSales =
'Date'[Date] <= MAX ( Sales[Order Date] )
如果日期早于或等于 Sales 表中的最后交易日期,则 Date[DateWithSales] 列是 TRUE,否则为 FALSE。换句话说,“过去” 日期的 DateWithSales 为 TRUE,“未来” 日期为 FALSE,这里的 “过去” 和 “未来” 均相对于 Sales 中的最后交易日期。
2.2.4 控制未来日期中可视化
大多数时间智能计算都不应显示最后有效日期之后的日期值。例如,年初至今的计算也可以显示未来日期的值,但是我们想要将其隐藏。这些示例中使用的数据集在 2009 年 8 月 15 日结束。因此,我们将月份 “August 2009”、2009 年第三季度 “Q3-2009” 和年份 “2009” 视为数据的最后时间段。2009 年 8 月 15 日以后的任何日期都被视为未来,我们想要隐藏未来值。
为了避免显示未来日期的结果,我们使用 ShowValueForDates 度量值。
如果所选的时间段不是在数据的最后一个时间段之后,则 ShowValueForDates 返回 TRUE:
Date 表中度量值(隐藏)
ShowValueForDates :=
VAR LastDateWithData =
CALCULATE (
MAX ( 'Sales'[Order Date] ),
REMOVEFILTERS ()
)
VAR FirstDateVisible =
MIN ( 'Date'[Date] )
VAR Result =
FirstDateVisible <= LastDateWithData
RETURN
Result
ShowValueForDates 度量值是隐藏的。这是一项技术措施,目的是在许多与时间相关的不同计算中实现重复使用同一逻辑,并且用户不应直接在报告中使用 ShowValueForDates。
2.2.5 命名约定
本节介绍用来引入时间智能计算的命名约定。一个简单的分类显示了一个计算是否:
- 推移一段时间,例如上一年的同一时间段;
- 执行聚合,例如年初至今;或
- 比较两个时间段,例如今年与去年比较。
| 首字母缩写 | 描述 | 推移 | 聚合 | 比较 |
|---|---|---|---|---|
| YTD | 年初至今 | X | ||
| QTD | 季初至今 | X | ||
| MTD | 月初至今 | X | ||
| MAT | 移动年度总计 | X | ||
| PY | 上一年 | X | ||
| PQ | 上一季 | X | ||
| PM | 上一月 | X | ||
| PYC | 上一整年 | X | ||
| PQC | 上一整季 | X | ||
| PMC | 上一整月 | X | ||
| PP | 上一时间段 (自动选择年、季或月) | X | ||
| PYMAT | 上一年移动年度总计 | X | X |
| 首字母缩写 | 描述 | 推移 | 聚合 | 比较 |
|---|---|---|---|---|
| YOY | 比上个年度的差异 | X | ||
| QOQ | 比上个季度的差异 | X | ||
| MOM | 比上个月度的差异 | X | ||
| MATG | 移动年度总计增长 | X | X | X |
| POP | 比上个时间段的差异 (自动选择年、季或月) | X | ||
| PYTD | 上个年度的年初至今 | X | X | |
| PQTD | 上个季度的季初至今 | X | X | |
| PMTD | 上个月度的月初至今 | X | X | |
| YOYTD | 比上个年度年初至今的差异 | X | X | X |
| QOQTD | 比上个季度季初至今的差异 | X | X | X |
| MOMTD | 比上个月度月初至今的差异 | X | X | X |
| YTDOPY | 年初至今比上个年度 | X | X | X |
| QTDOPQ | 季初至今比上个季度 | X | X | X |
| MTDOPM | 月初至今比上个月度 | X | X | X |
2.3 期初至今总计
年初至今、季初至今和月初至今的计算会修改 Date 表的筛选上下文,将一系列日期用作筛选器,覆盖所选时间段的筛选器。
所有这些计算都可以使用带有时间智能函数的常规 CALCULATE 或某一个 TOTAL 函数(例如 TOTALYTD)来实现。TOTAL 函数只是 CALCULATE 版本的语法糖(译者注:程序自动替换 DAX 语法的行为,这种语法对 DAX 语言的功能没有影响,但是能方便用户输入更少的代码实现同样的计算目的)。尽管我们更喜欢 CALCULATE 版本, CALCULATE 会使公式逻辑更明显,并且比 TOTAL 函数具有更大的灵活性,但我们仍将 TOTAL 函数作为参考。在以下示例中,使用 TOTAL 函数的公式会被标记为 (2)。展示 TOTAL 函数的目的仅仅是为了显示它们可以返回与 CALCULATE 版本相同的值。
2.3.1 年初至今总计
“年初至今” 聚合始于该年度 1 月 1 日的数据,如图 2-1 所示。

图 2-1 Sales YTD (simple)* 显示了任何时间段的值,而 *Sales YTD 和 * Sales YTD (2)* 隐藏了数据的最后一个时间段之后的数据
年初至今总计的度量值可以通过 DATESYTD 函数实现,如下所示:
Sales 表中的度量值
Sales YTD (simple) :=
CALCULATE (
[Sales Amount],
DATESYTD ( 'Date'[Date] )
)
DATESYTD 返回一个日期集,该日期集包含筛选上下文中从显示的日期所在年份的第一天到显示的最后一天之间的所有日期。因此,Sales YTD (simple)* 度量值甚至会显示该年份未来日期的数据。通过仅当 *ShowValueForDates 度量值返回 TRUE 时才返回结果,就可以避免在 Sales YTD 度量值中出现这种情况:
Sales 表中的度量值
Sales YTD :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales Amount],
DATESYTD ( 'Date'[Date] )
)
)
如果报告不是基于公历年度,而是会计年度,则 DATESYTD 会需要增加一个参数来识别会计年度的最后一天。以图 2-2 中的报告为例。

图 2-2 Sales Fiscal YTD 和 Sales Fiscal YTD (2) 显示了基于会计年度的年初至今
Sales Fiscal YTD 度量值在 DATESYTD 的第二个参数中指定了会计年度的最后一天和月份。以下度量值将 6 月 30 日用作会计年度的最后一天。DATESYTD 的第二个参数必须是与 Date 表中会计年度的定义相对应的固定值(也称为常量)。它不能动态计算:
Sales 表中的度量值
Sales Fiscal YTD :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales Amount],
DATESYTD ( 'Date'[Date], "6-30" )
)
)
TOTALYTD 函数可以替代 DATESYTD:
Sales 表中的度量值
Sales YTD (2) :=
IF (
[ShowValueForDates],
TOTALYTD (
[Sales Amount],
'Date'[Date]
)
)
Sales 表中的度量值
Sales Fiscal YTD (2) :=
IF (
[ShowValueForDates],
TOTALYTD (
[Sales Amount],
'Date'[Date],
"6-30"
)
)
2.3.2 季度初至今总计
“季初至今” 聚合始于该季度第一天的数据,如图 2-3 所示。

图 2-3 Sales QTD 显示了季初至今的金额,2009 年显示为空白,因为 2009 年 4 季度没有数据
季初至今总计的度量值可以通过 DATESQTD 函数来计算,如下所示:
Sales 表中的度量值
Sales QTD :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales Amount],
DATESQTD ( 'Date'[Date] )
)
)
TOTALQTD 函数可以替代 DATESQTD:
Sales 表中的度量值
Sales QTD (2) :=
IF (
[ShowValueForDates],
TOTALQTD (
[Sales Amount],
'Date'[Date]
)
)
2.3.3 月初至今总计
“月初至今” 聚合始于该月度第一天的数据,如图 2-4 所示。

图 2-4 Sales MTD 显示了月初至今的金额,CY 2009 和 Q3-2009 显示为空白,是因为 2009 年 8 月 15 日之后没有数据
月初至今总计的度量值可以通过 DATESMTD 函数来计算,如下所示:
Sales 表中的度量值
Sales MTD :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales Amount],
DATESMTD ( 'Date'[Date] )
)
)
TOTALMTD 可以替代 DATESMTD:
Sales 表中的度量值
Sales MTD (2) :=
IF (
[ShowValueForDates],
TOTALMTD (
[Sales Amount],
'Date'[Date]
)
)
2.4 比上个时间段增长的计算
一个常见的需求是将一个时间段与上一年、上一个季度或上一个月的相同时间段进行比较。上一个年、季、月可能不完整,因此,为了实现合理的比较,比较时应考虑一个等效时间段。由于这些原因,本节中显示的计算会使用 Date [DateWithSales] 计算列,如《隐藏 DAX 计 算中的未来日期》(https://sql.bi/78171)一文所述。
2.4.1 比上个年度增长
“比上个年度” 是将一个时间段与上一年的等效时间段进行比较。在此示例中,有效数据截止到 2009 年 8 月 15 日。因此,Sales PY 显示的 2008 年 数字,仅考虑 2008 年 8 月 15 日之前的交易。图 2-5 显示,2008 年 8 月的 Sales Amount 为 721,560.95,而 2009 年 8 月的 Sales PY 返回值为 296,529.51,是因为度量值仅考虑了截至 2008 年 8 月 15 日的销售额。

图 2-5 2009 年 8 月的 * Sales PY* 显示的是 2008 年 8 月 1 日 - 15 日的金额,因为在 2009 年 8 月 15 日之后没有数据
Sales PY 使用 DATEADD 并筛选 Date [DateWithSales] 列,以确保与上一时间段的数据进行合理比较。“比上个年度增长” 的计算,以数字形式表现为 Sales YOY,以百分比形式表现为 Sales YOY %*。这两个度量值都使用* Sales PY 来确保仅计算截至 2009 年 8 月 15 日的数据:
Sales 表中的度量值
Sales PY :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales Amount],
CALCULATETABLE (
DATEADD ( 'Date'[Date], -1, YEAR ),
'Date'[DateWithSales] = TRUE
)
)
)
Sales 表中的度量值
Sales YOY :=
VAR ValueCurrentPeriod = [Sales Amount]
VAR ValuePreviousPeriod = [Sales PY]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod ) && NOT ISBLANK ( ValuePreviousPeriod ),
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Sales 表中的度量值
Sales YOY % :=
DIVIDE (
[Sales YOY],
[Sales PY]
)
也可以使用 SAMEPERIODLASTYEAR 编写 Sales PY。SAMEPERIODLASTYEAR 更易于阅读,但没有任何性能上的优势。这是因为,从本质上讲,该函数在前面的公式中已被翻译为 DATEADD 函数:
Sales 表中的度量值
Sales PY (2) :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales Amount],
CALCULATETABLE (
SAMEPERIODLASTYEAR ( 'Date'[Date] ),
'Date'[DateWithSales] = TRUE
)
)
)
2.4.2 比上个季度增长
“比上个季度” 是将一个时间段与上一季度的等效时间段进行比较。在此示例中,有效数据截止到 2009 年 8 月 15 日(即 2009 年第三季度的前半部分)。因此,2009 年 8 月(即第三季度的第二个月)的 Sales PQ 显示截至 2009 年 5 月 15 日(即上一季度第二个月的前半部分)的销售情况。图 2-6 显示,2009 年 5 月的 Sales Amount 为 1,067,165.23,而 2009 年 8 月的 Sales PQ 返回值为 435,306.10,是因为仅考虑截至 2009 年 8 月 15 日的销售额。

图 2-6 2009 年 8 月的 Sales PQ 显示的是 2009 年 5 月 1 日 - 15 日的金额,因为在 2009 年 8 月 15 日之后没有数据
Sales PQ 使用 DATEADD 并筛选 Date [DateWithSales] 列,以确保与上一时间段的数据进行合理比较。“比上个季度增长” 的计算,以数字形式表现为 Sales QOQ,以百分比形式表现为 Sales QOQ %*。这两个度量值都使用 *Sales PQ 来确保对相同时间段进行合理比较:
Sales 表中的度量值
Sales PQ :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales Amount],
CALCULATETABLE (
DATEADD ( 'Date'[Date], -1, QUARTER ),
'Date'[DateWithSales] = TRUE
)
)
)
Sales 表中的度量值
Sales QOQ :=
VAR ValueCurrentPeriod = [Sales Amount]
VAR ValuePreviousPeriod = [Sales PQ]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod ) && NOT ISBLANK ( ValuePreviousPeriod ),
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Sales 表中的度量值
Sales QOQ % :=
DIVIDE (
[Sales QOQ],
[Sales PQ]
)
2.4.3 比上个月度增长
“比上个月度” 是将一个时间段与上一月度的等效时间段进行比较。在此示例中,有效数据截止到 2009 年 8 月 15 日。因此,Sales PM 仅考虑 2009 年 7 月 1 日 - 15 日这一时期的销售额,以便返回与 2009 年 8 月进行比较的值。以这种方式,它仅返回上一个月相应时间段的数据。图 2-7 显示,2009 年 7 月的 Sales Amount 为 1,068,396.58,而 2009 年 8 月的 Sales PM 返回值为 584,212.78,是因为仅考虑了截至 2009 年 7 月 15 日的销售额。

图 2-7 2009 年 8 月的 Sales PM 显示的是 2009 年 7 月 1 日 - 15 日的金额,因为在 2009 年 8 月 15 日之后没有数据
Sales PM 使用 DATEADD 并筛选 Date [DateWithSales] 列,以确保与上一时间段的数据进行合理比较。“比上个月度增长” 的计算,以数字形式表现为 Sales MOM,以百分比形式表现为 Sales MOM %*。这两个度量值都使用 *Sales PM 来确保对相同时间段进行合理比较:
Sales 表中的度量值
Sales PM :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales Amount],
CALCULATETABLE (
DATEADD ( 'Date'[Date], -1, MONTH ),
'Date'[DateWithSales] = TRUE
)
)
)
Sales 表中的度量值
Sales MOM :=
VAR ValueCurrentPeriod = [Sales Amount]
VAR ValuePreviousPeriod = [Sales PM]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod ),
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Sales 表中的度量值
Sales MOM % :=
DIVIDE (
[Sales MOM],
[Sales PM]
)
2.4.4 比上个时间段增长
“比上个时间段增长” 会根据当前的可视化选择,自动选择本节中先前介绍的度量值之一。例如,如果可视化显示月度级别的数据,则会返回 “比上个月度增长” 的度量值;如果可视化显示年度级别的总计,则返回 “比上个年度增长” 的度量值。预期结果如图 2-8 所示。

图 2-8 Sales PP 在月度级别上显示上个月的值,在季度级别上显示上一个季度的值,在年度级别上显示上一年的值
Sales PP、Sales POP 和 Sales POP% 这三个度量值,会根据报告中选择的级别,定向到相应的年、季度和月的度量值,来进行计算。ISINSCOPE 函数会检测报告中所使用的级别。传递给 ISINSCOPE 的参数是图 2-8 的矩阵视图的行所使用的属性。度量值的定义方法如下:
Sales 表中的度量值
Sales POP % :=
SWITCH (
TRUE,
ISINSCOPE ( 'Date'[Year Month] ), [Sales MOM %],
ISINSCOPE ( 'Date'[Year Quarter] ), [Sales QOQ %],
ISINSCOPE ( 'Date'[Year] ), [Sales YOY %]
)
Sales 表中的度量值
Sales POP :=
SWITCH (
TRUE,
ISINSCOPE ( 'Date'[Year Month] ), [Sales MOM],
ISINSCOPE ( 'Date'[Year Quarter] ), [Sales QOQ],
ISINSCOPE ( 'Date'[Year] ), [Sales YOY]
)
Sales 表中的度量值
Sales PP :=
SWITCH (
TRUE,
ISINSCOPE ( 'Date'[Year Month] ), [Sales PM],
ISINSCOPE ( 'Date'[Year Quarter] ), [Sales PQ],
ISINSCOPE ( 'Date'[Year] ), [Sales PY]
)
2.5 期初至今增长的计算
“期初至今” 增长的度量值是指将 “期初至今” 度量值与基于特定偏移量的等效时间段的同一度量值进行比较。例如,您可以将年初至今的聚合结果与上一年的年初至今进行比较,偏移量为一年。
这组计算中的所有度量值均需考虑不完整时间段。示例中有效数据截止到 2009 年 8 月 15 日,因此这些度量值可确保上一年的计算不包括 2009 年 8 月 15 日之后的日期。
2.5.1 比上个年度年初至今增长
“比上个年度年初至今增长” 将特定日期的年初至今与上一年等效日期的年初至今进行比较。图 2-9 显示,2009 年的 Sales PYTD 仅考虑截至 2008 年 8 月 15 日的交易。因此,Q3-2008 的 Sales YTD 为 7,129,971.53,而 Q3-2009 的 Sales PYTD 较低,为 5,741,502.86。

图 2-9 Q3-2009 的 Sales PYTD 显示的是 2008 年 1 月 1 日至 8 月 15 日的金额,因为在 2009 年 8 月 15 日之后没有数据
Sales PYTD 使用 DATEADD 并筛选 Date [DateWithSales] 列,以确保与上一时间段的数据进行合理比较。Sales YOYTD 和 Sales YOYTD % 通过 Sales PYTD 来确保对相同时间段进行合理比较:
Sales 表中的度量值
Sales PYTD :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales YTD],
CALCULATETABLE (
DATEADD ( 'Date'[Date], -1, YEAR ),
'Date'[DateWithSales] = TRUE
)
)
)
Sales 表中的度量值
Sales YOYTD :=
VAR ValueCurrentPeriod = [Sales YTD]
VAR ValuePreviousPeriod = [Sales PYTD]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod ),
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Sales 表中的度量值
Sales YOYTD % :=
DIVIDE (
[Sales YOYTD],
[Sales PYTD]
)
Sales PYTD 使用 DATEADD 将日期筛选器回移一年。使用 DATEADD 可以轻松地推移两年或两年以上。但是,也可以使用 SAMEPERIODLASTYEAR 编写 Sales PYTD 将日期回移一年。如下面这个示例,其本质用法就是上例中的 DATEADD。
Sales 表中的度量值
Sales PYTD (2) :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales YTD],
CALCULATETABLE (
SAMEPERIODLASTYEAR ( 'Date'[Date] ),
'Date'[DateWithSales] = TRUE
)
)
)
2.5.2 比上个季度季初至今增长
“比上个季度季初至今增长” 将特定日期的季初至今与上一季度等效日期的季初至今进行比较。图 2-10 显示,2009 年 8 月的 Sales PQ 仅考虑截至 2008 年 5 月 15 日 的交易,仅获得上一季度的前半部分的金额。因此,2009 年 5 月的 Sales QTD 为 1,746,058.45,而 2009 年 8 月的 Sales PQTD 较低,为 1,114,199.32。

图 2-10 2009 年 8 月的 Sales PQTD 显示的是 2009 年 4 月 1 日至 5 月 15 日的金额,因为在 2009 年 8 月 15 日之后没有数据
Sales PQTD 使用 DATEADD 并筛选 Date [DateWithSales] 列,以确保与上一时间段的数据进行合理比较。Sales QOQTD 和 Sales QOQTD % 通过 Sales PQTD 来确保对相同时间段进行合理比较:
Sales 表中的度量值
Sales PQTD :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales QTD],
CALCULATETABLE (
DATEADD ( 'Date'[Date], -1, QUARTER ),
'Date'[DateWithSales] = TRUE
)
)
)
Sales 表中的度量值
Sales QOQTD :=
VAR ValueCurrentPeriod = [Sales QTD]
VAR ValuePreviousPeriod = [Sales PQTD]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod ),
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Sales 表中的度量值
Sales QOQTD % :=
DIVIDE (
[Sales QOQTD],
[Sales PQTD]
)
2.5.3 比上个月度月初至今增长
“比上个月度月初至今增长” 将特定日期的月初至今与上一个月等效日期的月初至今进行比较。图 2-11 显示,2009 年 8 月的 Sales PMTD 仅考虑截至 2009 年 7 月 15 日的交易,以获得上一个月的相应时间段。因此,2009 年 7 月的 Sales MTD 为 1,068,396.58,而 2009 年 8 月的 Sales PMTD 较低,为 584,212.78。

图 2-11 2009 年 8 月 的 Sales PQTD 显示的是 2009 年 7 月 1 日 - 15 日这一时期的金额,因为在 2009 年 8 月 15 日之后没有数据
Sales PMTD 使用 DATEADD 并筛选 Date [DateWithSales] 列,以确保与上一时间段的数据进行合理比较。Sales MOMTD 和 Sales MOMTD % 通过 Sales PMTD 度量值来确保对相同时间段进行合理比较:
Sales 表中的度量值
Sales PMTD :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales MTD],
CALCULATETABLE (
DATEADD ( 'Date'[Date], -1, MONTH ),
'Date'[DateWithSales] = TRUE
)
)
)
Sales 表中的度量值
Sales MOMTD :=
VAR ValueCurrentPeriod = [Sales MTD]
VAR ValuePreviousPeriod = [Sales PMTD]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod ),
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Sales 表中的度量值
Sales MOMTD % :=
DIVIDE (
[Sales MOMTD],
[Sales PMTD]
)
2.6 期初至今与上一个完整时间段的比较
当将上一个完整时间段作为基准时,将期初至今聚合结果与上一个完整时间段进行比较是很有用的。一旦当前年初至今达到上一完整年度的 100%,这意味着我们已经或有望以更少的天数达到与上一完整时间段相同的绩效。
2.6.1 年初至今比上个完整年度
“年初至今比上个完整年度” 是将年初至今与上一个完整年度进行比较。图 2-12 显示,2008 年 11 月的 Sales YTD 几乎达到 2007 年全年的 Sales Amount。Sales YTDOPY% 体现年初至今与上个年度总额的直接比较;当该百分比为正数时,表示增长超过上一个会计年。本案例中,从 2008 年 12 月 1 日起实现超过上一年度的增长。

图 2-12 Sales YTDOPY % 从 2008 年 12 月 1 日起显示为正数,表示 Sales YTD 开始大于 2007 年的 Sales Amount
“年初至今比上个年度增长” 是通过 Sales YTDOPY 和 Sales YTDOPY % 度量值来计算的;分别通过 Sales YTD 度量值来计算年初至今的值,通过 Sales PYC 度量值来获得上一完整年度的销售额:
Sales 表中的度量值
Sales PYC :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales Amount],
PARALLELPERIOD ( 'Date'[Date], -1, YEAR )
)
)
Sales 表中的度量值
Sales YTDOPY :=
VAR ValueCurrentPeriod = [Sales YTD]
VAR ValuePreviousPeriod = [Sales PYC]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod ),
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Sales 表中的度量值
Sales YTDOPY % :=
DIVIDE (
[Sales YTDOPY],
[Sales PYC]
)
也可以使用 PREVIOUSYEAR 来编写 Sales PYC 度量值,其执行方式类似于 PARALLELPERIOD(此示例不涉及二者差异):
Sales 表中的度量值
Sales PYC (2) :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales Amount],
PREVIOUSYEAR ( 'Date'[Date] )
)
)
如果进行比较时使用的是会计年度,则必须使用 PREVIOUSYEAR,因为 PREVIOUSYEAR 可以接受用于指定会计年度最后一天的第二个参数。参见下方图 2-13 中的报告,该报告按会计期对度量值进行了分割。

图 2-13 Sales Fiscal YTDOPY % 将 Sales YTD 与上一会计年度的 Sales Amount 进行了比较
报告中使用的度量值定义如下。请注意 Sales Fiscal PYC 中 PREVIOUSYEAR 的第二个参数:
Sales 表中的度量值
Sales Fiscal PYC :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales Amount],
PREVIOUSYEAR ( 'Date'[Date], "06-30" )
)
)
Sales 表中的度量值
Sales Fiscal YTDOPY :=
VAR ValueCurrentPeriod = [Sales Fiscal YTD]
VAR ValuePreviousPeriod = [Sales Fiscal PYC]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod ),
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Sales 表中的度量值
Sales Fiscal YTDOPY % :=
DIVIDE (
[Sales Fiscal YTDOPY],
[Sales Fiscal PYC]
)
2.6.2 季初至今比上个完整季度
“季初至今比上个完整季度” 是将季初至今与上一个完整季度进行比较。图 2-14 显示,2008 年 5 月的 Sales QTD 超过了 Q1-2008 的 Sales Amount 总额。Sales QTDOPQ% 体现季初至今与上个季度总额的直接比较;当该百分比为正数时,表示增长超过上一季度。本案例中,从 2008 年 5 月起实现超过上一季度的增长。

图 2-14 Sales QTDOPQ % 从 2008 年 5 月起显示为正数,表示 Sales QTD 开始大于 Q1-2008 的 Sales Amount
“季初至今比上个季度增长” 是通过 Sales QTDOPQ 和 Sales QTDOPQ % 度量值来计算的;分别通过 Sales QTD 度量值来计算季初至今的值,通过 Sales PQC 度量值来获得上一完整季度的销售额:
Sales 表中的度量值
Sales PQC :=
IF (
[ShowValueForDates] && HASONEVALUE ( 'Date'[Year Quarter] ),
CALCULATE (
[Sales Amount],
PARALLELPERIOD ( 'Date'[Date], -1, QUARTER )
)
)
Sales 表中的度量值
Sales QTDOPQ :=
VAR ValueCurrentPeriod = [Sales QTD]
VAR ValuePreviousPeriod = [Sales PQC]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod ),
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Sales 表中的度量值
Sales QTDOPQ % :=
DIVIDE (
[Sales QTDOPQ],
[Sales PQC]
)
也可以使用 PREVIOUSQUARTER 来编写 Sales PQC 度量值,只要不是用在超过一个季度的年度级别即可:
Sales 表中的度量值
Sales PQC (2) :=
IF (
[ShowValueForDates] && HASONEVALUE ( 'Date'[Year Quarter] ),
CALCULATE (
[Sales Amount],
PREVIOUSQUARTER ( 'Date'[Date] )
)
)
2.6.3 月初至今比上个完整月度
“月初至今比上个完整月度” 是将月初至今与上一个完整月度进行比较。图 2-15 显示,2008 年 4 月 期间的总 Sales MTD 超过了 2008 年 3 月的 Sales Amount。Sales MTDOPM% 体现月初至今与上个月度总额的直接比较;当该百分比为正数时,表示增长超过上一月度。本案例中,从 2008 年 4 月 19 日起实现超过上一月度的增长。

图 2-15 Sales MTDOPM % 从 2008 年 4 月 19 日起显示为正数,表示 Sales MTD 开始大于 2008 年 3 月的 Sales Amount
“月初至今比上个月度增长” 是通过 Sales MTDOPM % * 和 *Sales MTDOPM 度量值来计算的;分别通过 Sales MTD 度量值来计算月初至今的值,通过 Sales PMC 度量值来获得上一完整月度的销售额:
Sales 表中的度量值
Sales PMC :=
IF (
[ShowValueForDates] && HASONEVALUE ( 'Date'[Year Month] ),
CALCULATE (
[Sales Amount],
PARALLELPERIOD ( 'Date'[Date], -1, MONTH )
)
)
Sales 表中的度量值
Sales MTDOPM :=
VAR ValueCurrentPeriod = [Sales MTD]
VAR ValuePreviousPeriod = [Sales PMC]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod ),
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Sales 表中的度量值
Sales MTDOPM % :=
DIVIDE (
[Sales MTDOPM],
[Sales PMC]
)
也可以使用 PREVIOUSMONTH 来编写 Sales PMC 度量值,只要不是用在超过一个月的季度或年度级别即可:
Sales 表中的度量值
Sales PMC (2) :=
IF (
[ShowValueForDates] && HASONEVALUE ( 'Date'[Year Month] ),
CALCULATE (
[Sales Amount],
PREVIOUSMONTH ( 'Date'[Date] )
)
)
2.7 使用移动年度总计计算
聚合几个月数据的一种常用方法是使用移动年度总计,而不是年初至今。移动年度总计包括过去 12 个月的数据。例如,2008 年 3 月的移动年度总计包括从 2007 年 4 月至 2008 年 3 月的数据。
2.7.1 移动年度总计
Sales MAT 度量值用于计算移动年度总计,如图 2-16 所示。

图 2-16 2008 年 3 月的 Sales MAT 聚合了 2007 年 4 月至 2008 年 3 月的 Sales Amount
移动年度总计使用 DATESINPERIOD 来选择上一年:
Sales 表中的度量值
Sales MAT :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales Amount],
DATESINPERIOD (
'Date'[Date],
MAX ( 'Date'[Date] ),
-1,
YEAR
)
)
)
DATESINPERIOD 返回一个日期集,该日期集包含从第二个参数开始传递的日期,同时应用在第三个参数和第四个参数中指定的偏移。例如,Sales MAT 度量值返回筛选上下文中最后有效日期之前的全年日期。通过分别给第三个参数和第四个参数赋值 -12 和 MONTH,可以获得相同的结果。
2.7.2 移动年度总计增长
“移动年度总计增长” 借由 Sales MAT 度量值,通过 Sales PYMAT、Sales MATG 和 Sales MATG % 度量值来计算。Sales MAT 度量值提供首次销售后一年的正确值(当它收集一整年的数据时);但如果当前时间段不足一整年,则不受保护。例如,2009 年全年的 Sales PYMAT 为 9,927,582.99,这与 2008 年的 Sales Amount 相对应,如图 2-17 所示。将 2009 年销售额与 2008 年全年销售额进行比较,只能比较不足 8 个月的数据,是因为有效数据截止到 2009 年 8 月 15 日。类似地,您可以看到 2008 年开始出现的 Sales MATG % 数值非常高,一年后稳定下来。前几个数值非常高,是由于上一年没有销售。设计原理是:移动年度总计通常是以月粒度或日粒度进行计算,以便在图表中显示趋势。

图 2-17 Sales MATG% 显示了 Sales MAT 和 Sales PYMAT 之间增长的百分比
度量值的定义方法如下:
Sales 表中的度量值
Sales PYMAT :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales MAT],
DATEADD ( 'Date'[Date], -1, YEAR )
)
)
Sales 表中的度量值
Sales MATG :=
VAR ValueCurrentPeriod = [Sales MAT]
VAR ValuePreviousPeriod = [Sales PYMAT]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod ),
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Sales 表中的度量值
Sales MATG % :=
DIVIDE (
[Sales MATG],
[Sales PYMAT]
)
也可以使用 SAMEPERIODLASTYEAR 来编写 Sales PYMAT 度量值。如下面的示例,其本质用法就是上例中的 DATEADD:
Sales 表中的度量值
Sales PYMAT (2) :=
IF (
[ShowValueForDates],
CALCULATE (
[Sales MAT],
SAMEPERIODLASTYEAR ( 'Date'[Date] )
)
)
2.8 移动平均
移动平均通常用在折线图中显示趋势。图 2-18 包括 30 天(Sales AVG 30D)、三个月(Sales AVG 3M)和一年(Sales AVG 1Y)的 Sales Amount 的移动平均。

图 2-18 Sales AVG 30D、Sales AVG 3M 和 Sales AVG 1Y 分别显示了 30 天、三个月和一年的移动平均
2.8.1 移动平均 30 天
Sales AVG 30D 度量值通过遍历借由 DATESINPERIOD 获得的过去 30 天的日期列表,来计算 30 天 的移动平均:
Sales 表中的度量值
Sales AVG 30D :=
VAR Period30D =
CALCULATETABLE (
DATESINPERIOD (
'Date'[Date],
MAX ( 'Date'[Date] ),
-30,
DAY
),
'Date'[DateWithSales] = TRUE
)
VAR FirstDayWithData =
CALCULATE (
MIN ( Sales[Order Date] ),
REMOVEFILTERS ()
)
VAR FirstDayInPeriod =
MINX ( Period30D, 'Date'[Date] )
VAR Result =
IF (
FirstDayWithData <= FirstDayInPeriod,
AVERAGEX (
Period30D,
[Sales Amount]
)
)
RETURN
Result
这种模式非常灵活。但是对于常规的累加计算,可以使用另一个更快的公式来实现 Result:
VAR Result =
IF (
FirstDayWithData <= FirstDayInPeriod,
CALCULATE (
DIVIDE (
[Sales Amount],
DISTINCTCOUNT ( Sales[Order Date] )
),
Period30D
)
)
2.8.2 移动平均 3 个月
Sales AVG 3M 度量值通过遍历借由 DATESINPERIOD 获得的过去 3 个月的日期列表,来计算 3 个月的移动平均:
Sales 表中的度量值
Sales AVG 3M :=
VAR Period3M =
CALCULATETABLE (
DATESINPERIOD (
'Date'[Date],
MAX ( 'Date'[Date] ),
-3,
MONTH
),
'Date'[DateWithSales] = TRUE
)
VAR FirstDayWithData =
CALCULATE (
MIN ( Sales[Order Date] ),
REMOVEFILTERS ()
)
VAR FirstDayInPeriod =
MINX ( Period3M, 'Date'[Date] )
VAR Result =
IF (
FirstDayWithData <= FirstDayInPeriod,
AVERAGEX (
Period3M,
[Sales Amount]
)
)
RETURN
Result
对于简单的累加度量值,在 “移动平均 30 天” 中介绍的基于 DIVIDE 的模式,也可以用于计算 3 个月的移动平均。
2.8.3 移动平均 1 年
Sales AVG 1Y 度量值通过遍历借由 DATESINPERIOD 获得的过去 1 年的日期列表,来计算 1 年的移动平均:
Sales 表中的度量值
Sales AVG 1Y :=
VAR Period1Y =
CALCULATETABLE (
DATESINPERIOD (
'Date'[Date],
MAX ( 'Date'[Date] ),
-1,
YEAR
),
'Date'[DateWithSales] = TRUE
)
VAR FirstDayWithData =
CALCULATE (
MIN ( Sales[Order Date] ),
REMOVEFILTERS ()
)
VAR FirstDayInPeriod =
MINX ( Period1Y, 'Date'[Date] )
VAR Result =
IF (
FirstDayWithData <= FirstDayInPeriod,
AVERAGEX (
Period1Y,
[Sales Amount]
)
)
RETURN
Result
对于简单的累加度量值,在 “移动平均 30 天” 中介绍的基于 DIVIDE 的模式,也可以用于计算 1 年的移动平均。
2.9 筛选其他日期属性
一旦将 Date 表标记为日期表,每当 CALCULATE 筛选 Date 表的日期列时,DAX 会自动删除 Date 表中的所有筛选器。设计这样的执行方式,其目的是简化时间智能计算的编写。的确,如果 DAX 不删除筛选器,则每次使用 DAX 时间智能函数时,需要在 Date 表上手动添加 REMOVEFILTERS,这会带来负面的开发体验。
自动删除 Date 表中的筛选器可能会给某些特定报告带来问题。例如,如果一份报告计算年初至今销售情况的方式是按星期几对销售额进行切分,则仅通过使用时间智能函数 DATESYTD 获得的结果会是错误的。图 2-19 显示了一周中所有日期的值,可以看出,一周中每一天的 Sales YTD 结果略小于或等于行总计。

图 2-19 按星期几切分的度量值 Sales YTD 产生了不正确的结果
值不正确的原因是 DATESYTD 在 Date[Date] 列上应用了一个筛选器。因为 Date 被标记为日期表,所以 DAX 会自动将 REMOVEFILTERS(‘Date’) 修饰符应用于筛选器参数中,使得 DATESYTD 在 CALCULATE 筛选作用中删除星期几上的筛选器。因此,显示的数字是年初至今,与星期几上的任何筛选器无关。星期几筛选器仅影响报告行中指定的时间段(年或季度)的最后一天。正确的结果(如图 2-20 所示)需要使用另一种方法。

图 2-20 按星期几切分的 Sales YTD (day of week) 产生了正确的结果
有两个方法可以获取正确的值:要么以 CALCULATE 语句重新遍历星期几上的筛选器,要么更新数据模型。
仅在对列进行筛选的情况下,恢复星期几上的筛选器需要添加 VALUES(Date[Day of Week]),如下方代码:
Sales 表中的度量值
Sales YTD (day of week) :=
IF (
[ShowValueForDates],
IF (
ISFILTERED ( 'Date'[Day of Week] ),
CALCULATE (
[Sales Amount],
DATESYTD ( 'Date'[Date] ),
VALUES ( 'Date'[Day of Week] )
),
CALCULATE (
[Sales Amount],
DATESYTD ( 'Date'[Date] )
)
)
)
第一种解决方案效果很好,但存在一个重大缺点:根据是否筛选了 Date[Day of Week] 列,会有两种不同版本的计算。在大型模型上,这可能会对性能产生明显影响。
针对这种情况,还有另一种解决方案,需要更新数据模型。不通过使用 Date 表选择星期几,我们可以将星期几存储在一个单独的表中,用该表筛选 Sales,与 Date 无关。这样,自动删除 Date 上的筛选器不会影响现有的星期几筛选器。例如,可以将 Day of Week 表创建为计算表:
计算表
Day of Week =
SELECTCOLUMNS (
'Date',
"Date", 'Date'[Date],
"Day of Week", 'Date'[Day of Week]
)
Day of week 表必须通过 ‘Day of week’[Date] 关联到 Sales[Order Date] ,这意味着模型必须像图 2-21 所示。

图 2-21 新建的 Day of Week 表关联到 Date 使用的同一个 Order Date 列
请注意,我们使用 Date 中的所有日期新建了 Day of Week 表,以与现有 Sales[Order Date] 列建立联系。通过创建仅包含七个值的表(周日至周六),可以获得相同的结果,但是这种选择需要在 Sales 表中增加一列,这样会使数据模型消耗更多内存。
在新建的表中按 Day of Week 进行切分,可以兼容任何时间智能计算,并且兼容 Day of Week 表上的任何筛选器。这是因为两个筛选器(Date 和 Day of the week)属于两个不同的表。
依据特定业务规则的需求,其他表可以合并任何属性集。我们使用星期几为您做了一个示例,您可以使用任何其他属性集(例如工作日、假期、季节),前提是这些属性取决于 Order Date。