这篇做完,你会得到一张 2023-01-01 到 2024-12-31 每天一行、中间不缺日子的日期表,和销售表连好关系,再加几个能算年初至今和同比的度量值。每一步都有可以核对的数字。

开始前要有:沿用上一篇的模型——销售表(日期、客户、产品、数量、金额),其中 金额 = 数量 * 单价 是计算列;度量值 销售额 = SUM ( 销售[金额] );客户表、产品表已按一对多连到销售表;日期 列必须是真正的日期类型,不能是文本。数据只有两年的一季度,后面所有数字都对着这张表核:

年月金额合计
202311000
202321200
202331500
202411100
202421400
202431300

第一步:先在事实表的日期列上试一次

1YTD销售额_错误版 = TOTALYTD ( [销售额], 销售[日期] )

矩阵里行放 销售[日期] 展开出来的年、月,值放 销售额 和这个新度量值。你会看到两种结果之一:某几个月看着像对的;或者直接报错,说这个函数需要一个连续的日期区间。前者更危险——它通常只是因为那几个月的每天恰好都有订单(从系统导出的日汇总表就长这样),换一份有缺口的明细就会坏。

坏在两处:

  1. 同一天有很多行。 事实表的日期列里一个日期出现几十上百次,它只是一串重复的时间戳,不是一本日历。
  2. 没成交的日子根本不在列里。 时间智能函数要做的事,是先把当前这段日期扩成“从 1 月 1 日到手上这一天”,或者整体往前挪一年,再拿这组日期去筛模型。要生成这样一段区间,它得知道这一年哪天开始、哪天结束、中间哪些天存在。事实表的日期列只记录发生过订单的日子,回答不了这个问题,只能东拼西凑。

还有一个跟公式无关的理由:时间字段放在独立的一张表里,才能让销售、采购等多张事实表共用同一套年 / 月轴;放在各张事实表里,同比就得一张表写一遍。

官方文档的门槛写得很直接:用时间智能函数时 永远不要指定事实表里的日期列;单独建日期表,日期范围连续,日期列唯一且不含时间部分,并标记为日期表 1。SAMEPERIODLASTYEAR 还额外要求当前上下文是连续的一段日期,不连续直接报错 4。

第二步:建一张日期表

在“建模”里点“新建表”,粘贴:

1日期表 =
2ADDCOLUMNS (
3    CALENDAR ( DATE ( 2023, 1, 1 ), DATE ( 2024, 12, 31 ) ),
4    "年", YEAR ( [Date] ),
5    "月", MONTH ( [Date] )
6)

CALENDAR 生成一列连续日期,列名默认就叫 Date;ADDCOLUMNS 在它旁边加上年和月。要季度、月名之类的字段,照这个写法再加。

选中日期表到数据视图看:731 行,Date 列从 2023-01-01 连续排到 2024-12-31,没有重复、没有空值。

第三步:标记成日期表,连上关系

  1. 模型视图里从 日期表[Date] 拖到 销售[日期]。连线应显示 一对多(日期表是一端),交叉筛选方向保持 单一。
  2. 表视图选中日期表,在“表工具”里点 标记为日期表,日期列选 Date。表名旁边会出现一个小日历图标。

这一步不是装饰。没标记、或者关系没建在 Date 这列上,时间智能函数就不认这张表,典型症状是度量的结果整列空白或直接报错。

第四步:写 YTD,看它改掉了哪条筛选

1YTD销售额 = TOTALYTD ( [销售额], '日期'[Date] )

矩阵行放 日期表[月],值放 销售额 和 YTD销售额,上面加个年份切片器选 2024:

月销售额YTD销售额
111001100
214002500
313003800
总计38003800

1 月两个数相等是对的——累计的第一个月就是当月。

TOTALYTD 只是语法糖,等价于 CALCULATE ( [销售额], DATESYTD ( '日期'[Date] ) ) 2。真正干活的是 DATESYTD,而它 返回的不是数字,是一列日期:从当前上下文最后一天所在那一年的 1 月 1 日,到这个最后一天。在“2024 年 3 月”那一格里,上下文把 Date 筛在 3 月 1 日到 3 月 31 日,它扩成 1 月 1 日到 3 月 31 日。

这列日期接着作为 CALCULATE 的筛选参数,落到 日期表[Date] 上。按上一篇讲过的规则(CALCULATE 的筛选参数是新增还是覆盖),这列原本就有筛选,所以是 覆盖,不是叠加;其他列上的筛选原样保留——把产品拖到行上,每个产品就会各算自己的年初至今。

方向也别搞反:这里筛的是 日期表的 Date 列,不是销售表的日期列。因为关系从日期表指向销售表,这层筛选沿一对多传到每一行订单,才变成“这些天的销售额”。

第五步:写同比

1去年同期销售额 = CALCULATE ( [销售额], SAMEPERIODLASTYEAR ( '日期'[Date] ) )
2
3同比 % = DIVIDE ( [销售额] - [去年同期销售额], [去年同期销售额] )
月销售额YTD销售额去年同期销售额同比 %
1110011001000+10.0%
2140025001200+16.7%
3130038001500-13.3%
总计380038003700+2.7%

SAMEPERIODLASTYEAR 是同一路数:取当前上下文里那组日期,整体往前平移一年,返回一列日期 3。3 月那行进去的是 2024 年 3 月,出来的是 2023 年 3 月,CALCULATE 把 Date 换成去年 3 月,[销售额] 算出 1500。因为它必须“取一段连续日期再整段平移”,事实表那个有缺口的日期列到这里就撑不住。

两个函数还能叠:

1去年同期YTD = CALCULATE ( [YTD销售额], SAMEPERIODLASTYEAR ( '日期'[Date] ) )

选 2024 年 3 月,得到 3700,也就是 2023 年 1 到 3 月的合计。它顺带解释一件容易看漏的事:3 月单月同比是 -13.3%,年初至今却是 +2.7%。两者不矛盾,是两个不同的问题。

做完自查

  1. 日期表行数对得上天数,Date 列连续、唯一、无空值。
  2. 关系是日期表对销售表的一对多,筛选方向单一。
  3. 日期表带日历图标(已标记为日期表)。
  4. 2024 年 1 月的 YTD 等于当月值 1100,不是全年。
  5. 2024 年 3 月的 YTD 等于 1 到 3 月之和 3800,不是 1300。
  6. 日期表范围要盖住事实表里的所有日期。漏了的话,矩阵会冒出空白年份的行,总计也和分项之和对不上。

常见出错对照

你看到的通常是怎么修
提示函数需要一个连续的日期区间传了事实表的日期列,或日期表中间缺日子参数改成 '日期'[Date];用连续 CALENDAR 生成
度量结果整列空白或报错日期表没标记,或关系没建在 Date 上用“标记为日期表”指定 Date,关系挪到这一列
矩阵多出空白年份行日期表范围没盖全事实数据把 CALENDAR 的起止日期改成覆盖全部数据

边界

TOTALYTD 还有第四个参数,可以传财年结束日,处理不按自然年结账的公司;现在没有财年数据就先别碰。另外,看同比时要用年份切片器把年份固定住:让“年”同时出现在行上,总计行会把 2023 和 2024 的销售额加在一起(7500),再和去年全年比,得出 +103% 这种没人看得懂的数——这不是公式错,是总计行的上下文本来就没有年份限制。至于 1 月看同比时去年同期空白(再往前一年没有数据,DIVIDE 分母为空)、当月没过完就比整个去年同期,这些是下一个话题,跟“日期表怎么建”是两件事,别混在一起改公式。