# 日期表与时间智能：YTD 和同比到底改掉了哪条筛选

搞清为什么算 YTD 和同比必须先建一张独立日期表，以及 TOTALYTD、SAMEPERIODLASTYEAR 是怎么覆盖日期筛选的

> DAX · 时间智能 · 数据建模 · 约 7 分钟 · 10 月 27 日

## 本篇要点

1. 时间智能函数（DATESYTD、SAMEPERIODLASTYEAR 这一类）返回的不是数字，而是一列日期，作用相当于告诉 CALCULATE“保留哪一段日子”。
2. 事实表的日期列重复，而且没有成交的日子不在列里，凑不出连续区间，所以不能当时间智能函数的参数；否则要么报“需要连续日期区间”，要么在看着正常的数字下悄悄算错。
3. 日期表要满足四条：日期连续且唯一、不含时间部分、覆盖所需年份、在模型里标记为日期表；它还能让多张事实表共用同一套年 / 月轴。
4. TOTALYTD 等价于 CALCULATE(表达式, DATESYTD(日期列))：它把日期表 Date 列上原有的筛选覆盖成“年初到上下文最后一天”，其他列（产品、客户、切片器）的筛选全部保留。
5. SAMEPERIODLASTYEAR 把当前上下文的日期整体平移一年，同样覆盖 Date 列上的筛选，所以同比算的是去年同一段日子。
6. 单月同比和年初至今同比可以符号相反，两者回答的是不同问题，不是有一个算错了。

---

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

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

| 年 | 月 | 金额合计 |
| --- | --- | --- |
| 2023 | 1 | 1000 |
| 2023 | 2 | 1200 |
| 2023 | 3 | 1500 |
| 2024 | 1 | 1100 |
| 2024 | 2 | 1400 |
| 2024 | 3 | 1300 |

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

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

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

坏在两处：

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

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

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

## 第二步：建一张日期表

在“建模”里点“新建表”，粘贴：

```dax
日期表 =
ADDCOLUMNS (
    CALENDAR ( DATE ( 2023, 1, 1 ), DATE ( 2024, 12, 31 ) ),
    "年", YEAR ( [Date] ),
    "月", MONTH ( [Date] )
)
```

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

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

## 第三步：标记成日期表，连上关系

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

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

## 第四步：写 YTD，看它改掉了哪条筛选

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

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

| 月 | 销售额 | YTD销售额 |
| --- | --- | --- |
| 1 | 1100 | 1100 |
| 2 | 1400 | 2500 |
| 3 | 1300 | 3800 |
| 总计 | 3800 | 3800 |

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

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

这列日期接着作为 `CALCULATE` 的筛选参数，落到 `日期表[Date]` 上。按上一篇讲过的规则（[CALCULATE 的筛选参数是新增还是覆盖](https://pangzhengboyin.com/articles/dax-measures-row-vs-filter-context-ea75113f)），这列原本就有筛选，所以是**覆盖**，不是叠加；其他列上的筛选原样保留——把产品拖到行上，每个产品就会各算自己的年初至今。

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

## 第五步：写同比

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

同比 % = DIVIDE ( [销售额] - [去年同期销售额], [去年同期销售额] )
```

| 月 | 销售额 | YTD销售额 | 去年同期销售额 | 同比 % |
| --- | --- | --- | --- | --- |
| 1 | 1100 | 1100 | 1000 | +10.0% |
| 2 | 1400 | 2500 | 1200 | +16.7% |
| 3 | 1300 | 3800 | 1500 | -13.3% |
| 总计 | 3800 | 3800 | 3700 | +2.7% |

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

两个函数还能叠：

```dax
去年同期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` 分母为空）、当月没过完就比整个去年同期，这些是下一个话题，跟“日期表怎么建”是两件事，别混在一起改公式。

## 思维导图

```mermaid
flowchart LR
  A["算 YTD 和同比"] --> B["时间智能函数返回一列日期"]
  B --> C["作为 CALCULATE 的筛选参数"]
  C --> D["覆盖日期表 Date 列的筛选"]
  C --> E["其他列筛选保留：按产品各算各的"]
  A --> F["前提：独立的日期表"]
  F --> F1["连续、唯一、覆盖全年"]
  F --> F2["标记为日期表"]
  F --> F3["一对多连到事实表"]
  A --> G["TOTALYTD = CALCULATE + DATESYTD"]
  A --> H["SAMEPERIODLASTYEAR 平移一年"]
  H --> I["可叠加：去年同期 YTD"]
```

## 术语表

- 日期表：每天一行、日期连续且唯一的日历表，提供年 / 月等字段，供所有事实表共用
- 标记为日期表：在模型里声明“这张表的这一列就是日历”，时间智能函数靠这个声明确定日期基准
- 时间智能函数：返回一列日期而不是数字的函数，用来指定“要保留哪段时间”，通常作为 CALCULATE 的筛选参数
- YTD（年初至今）：从当年 1 月 1 日累计到当前上下文最后一天的汇总值，第一个月的 YTD 等于当月值

## 来源

1. [Microsoft Learn: Specify Mark as Date Table for use with time intelligence — 日期表的要求与标记方法](https://learn.microsoft.com/en-us/analysis-services/tabular-models/specify-mark-as-date-table-for-use-with-time-intelligence-ssas-tabular?view=sql-analysis-services-2025)
2. [DAX Guide: TOTALYTD — 语法与「等价于 CALCULATE + DATESYTD」的对照](https://dax.guide/totalytd/)
3. [DAX Guide: SAMEPERIODLASTYEAR — 参数说明与日期表要求](https://dax.guide/sameperiodlastyear/)
4. [Microsoft Support: Time Intelligence in Power Pivot — 返回日期表的函数如何作为 CALCULATE 的筛选参数，SAMEPERIODLASTYEAR 要求连续日期](https://support.microsoft.com/en-us/excel/time-intelligence-in-power-pivot-in-excel)

---

原文：https://pangzhengboyin.com/articles/power-bi-date-table-time-intelligence-90315887

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具：每篇 5–10 分钟，边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
