# 第一批 DAX 度量值：计算列和度量值怎么选，CALCULATE 又改掉了哪条筛选

搞清计算列与度量值的分界、行上下文与筛选上下文的区别，以及 CALCULATE 的筛选参数为什么有时新增、有时覆盖

> DAX · 数据建模 · Power BI 入门 · 约 7 分钟 · 10 月 03 日

## 本篇要点

1. 计算列和度量值的分界在于答案是否随用户在报表上的点选变化：不随点选变化的逐行计算用计算列，会随切片器变化的聚合值用度量值。
2. 度量值只能放在视觉对象的值里，不能当行标签、图例或切片器选项，所以用来分组、分档、拼接的字段仍然要做成计算列。
3. 计算列在刷新时算好并存入模型，占空间也占刷新时间；度量值不存储，查询时才计算。
4. 行上下文指“当前正在算哪一行”，只在计算列和迭代函数里出现；度量值里没有它，所以直接写列引用会报“A single value for column … cannot be determined”，即使表里只有一行也一样。
5. 在度量值里做逐行运算要用 SUMX 这类迭代函数，它先走一遍表产生行上下文，再把每行结果汇总；行上下文只作用于被迭代的那张表，不沿关系传播。
6. 筛选上下文是 Power BI 在算每个单元格前凑好的一组筛选，作用于整个模型并沿关系从“一”端传向“多”端；它筛的是列的取值，不保证只剩一行，所以矩阵的一行不等于行上下文。
7. COUNTROWS(客户) 在按颜色分组的矩阵里出错，是因为颜色筛选只传到销售表，客户表没有被限制；改成 DISTINCTCOUNT(销售[CustomerCode]) 就对了。
8. CALCULATE 的筛选参数有两种结局：涉及的列本来没有筛选就新增，本来有筛选就覆盖；其他列上的筛选不受影响。
9. 所以写一个“某某口径”的度量值前，要先弄清那一列上是否已经有筛选，否则你以为是在加条件，实际是在替换条件。

---

上一篇结尾留了一个错误的数字：按颜色看客户数，蓝色只有张三买过，结果却算出 2。这不是公式拼错了，而是同一个公式在“什么条件下被计算”上出了问题。要把这件事讲清楚，得先分清两种写法（计算列和度量值）和两套上下文（行上下文和筛选上下文），最后再看 `CALCULATE` 是怎么去动其中一套的。

## 先决定：提前算好，还是每次现算

在 Power BI 里点“新建列”还是“新建度量值”，写出来的都是 DAX，但两者做的是完全不同的事：

| | 计算列 | 度量值 |
| --- | --- | --- |
| 什么时候算 | 刷新数据时算一次 | 图表要数字时现算 |
| 结果存哪 | 存进模型，占空间 | 不存 |
| 算的时候看什么 | 只看当前这一行 | 看这个单元格此刻被哪些条件筛过 |
| 用户点切片器之后 | 不变 | 跟着变 |

判断标准一句话：**这个答案会不会随着用户在报表上点选而变？** 不会，就提前算好；会，就只能现算。

`金额 = 销售[数量] * 销售[单价]` 属于前者：每行自己乘自己，跟用户在切片器上选了什么毫无关系，刷新时算一次存下来最省事。“客户数”“占比”“同比”属于后者：用户把年份一切，答案就变，只能现算。

但也不能全用度量值，因为**度量值只能放进视觉对象的“值”里**，不能当行标签、图例或切片器选项。所以想把数值分档成“0–100、100–500”、想把城市和省份拼成一个“所在地”，这些都得是计算列。代价是计算列的结果要存进模型，10 万行的事实表上多存一列数字，文件就大一圈，刷新也慢一点。

## 行上下文：计算列里有，度量值里没有

计算列里写 `销售[数量] * 销售[单价]` 不报错，是因为引擎会一行一行地执行这段公式，算到哪一行，“当前行”就是哪一行。这个“当前行”就是**行上下文**。

同一个公式写在度量值里会直接报错：

> A single value for column 'Quantity' in table 'Sales' cannot be determined. This can happen when a measure formula refers to a column that contains many values without specifying an aggregation such as min, max, count, or sum to get a single result.

这句提示很容易把人带偏。它不是说“你选中的行太多了”，而是说**压根没有“当前行”这回事**——哪怕销售表里只有一行，照样报同样的错。度量值执行前，引擎手里只有一组筛选，没有哪一行是“正在算的那一行”。

所以度量值必须先用一个聚合函数把一列压成一个数：`SUM(销售[金额])`、`COUNTROWS(销售)`、`DISTINCTCOUNT(销售[CustomerCode])`。这些函数的作用正是“把很多行变成结果”，补上没有行上下文的缺口。

那“一行一行乘完再求和”这种活，在度量值里怎么办？两条路：

- 先建计算列 `金额 = 销售[数量] * 销售[单价]`，再写 `销售额 = SUM(销售[金额])`。代价是那一列要被存下来。
- 用迭代函数把行上下文搬进度量值：`销售额 = SUMX(销售, 销售[数量] * 销售[单价])`。`SUMX` 的第一个参数是要走一遍的表，第二个参数是每行要算的表达式；它一边走一边产生行上下文，走完再把每行的结果加起来。

这里有一条边界：**行上下文只作用于被迭代的那一张表**。在销售表里迭代时，你取不到产品表上的列。

## 筛选上下文：每个单元格被筛的东西不一样

同一个度量值放进矩阵，每一行给出不同的数，靠的是**筛选上下文**：Power BI 在算某个单元格之前，先把它此刻生效的所有筛选凑成一组，再用这组筛选去算。行标签、列标签、切片器、其他视觉对象的交叉筛选，最后都汇进这一个筛选上下文。

它有两个行上下文没有的性质。一是**作用于整个模型**，并且沿关系传播：按上一篇讲的默认方向，从维度表（“一”端）流向事实表（“多”端）。二是**筛的是列上的取值，不一定只剩一行**：矩阵里某一行写着“蓝色”，含义是“产品[颜色] 等于蓝色”，蓝色底下还压着好几行明细。

这也说明，**矩阵里的每一行不是行上下文**。没有迭代，就没有行上下文。

## 用三张小表把两套上下文跑一遍

沿用上一篇那个模型，把大区换成国内的地名方便看。客户表：CUST-01 张三（华东）、CUST-02 李四（华南）。产品表：CL-01 T恤（绿色）、CL-02 牛仔裤（蓝色）。销售表（事实）：

| 日期 | 客户 | 产品 | 数量 |
| --- | --- | --- | --- |
| 1月1日 | CUST-01 | CL-01 | 10 |
| 2月2日 | CUST-01 | CL-02 | 20 |
| 3月3日 | CUST-02 | CL-01 | 30 |

先写 `销量 = SUM(销售[数量])`。行放产品[颜色]：绿色 40、蓝色 20、总计 60，都对，因为颜色筛选沿着“产品 → 销售”传了下去。

再写 `客户数 = COUNTROWS(客户)`。绿色 2、蓝色 2、总计 2。蓝色只有张三买过，应该是 1。原因上一篇已经说明：颜色筛选传到销售表就停了，客户表没被限制，`COUNTROWS(客户)` 在“客户表不受任何约束”的上下文里执行，于是每次返回全部客户数。

修法是换到事实表上去数：`客户数 = DISTINCTCOUNT(销售[CustomerCode])`。绿色那组被筛剩两笔销售（张三、李四），得 2；蓝色那组只剩张三的一笔，得 1；总计 2。注意总计在两种写法下都是 2，所以这个错只有去核对明细才发现得了。

这里的错误只出在“用别的表的筛选反过来限制客户表”。把客户表的字段直接拖到行标签上照样没问题，那时它是筛选的来源，不是被数的对象。

## CALCULATE：只改你要改的那部分筛选

`CALCULATE` 的语法是 `CALCULATE(表达式, 筛选1, 筛选2, …)`，作用是**在改动过的筛选上下文里求值**。它的筛选参数只有两种结局：

- 这个参数涉及的列，在当前筛选上下文里**本来没有筛选** → 新增一条筛选。
- 这个参数涉及的列，**已经有筛选** → 原有筛选被**覆盖**掉。

其他列上的筛选原样保留，这就是“只改一部分”。

看一个具体例子：`华东销量 = CALCULATE([销量], 客户[大区] = "华东")`。把它放进一个按“大区”排的矩阵里：

- 华东那一行：原本的大区筛选就是华东，被换成华东，结果 40。
- 华南那一行：原本的大区筛选是华南，被换掉，结果还是 40。
- 总计那一行：原本没有大区筛选，新增一条，结果 40。

三行都是 40，看起来莫名其妙，但这正是覆盖规则最直白的证据：`CALCULATE` 不是叠加条件，是替换。

换成另一列就能看到“新增”长什么样：`绿色销量 = CALCULATE([销量], 产品[颜色] = "绿色")`。在这个按大区排的矩阵里，颜色列本来没有筛选，所以是新加了一条，两个条件取交集：

- 华东行（张三的销售）：只有 CL-01 那笔是绿色，得 10。
- 华南行（李四的销售）：也只有 CL-01 那笔是绿色，得 30。
- 总计：10 + 30 = 40。

同一个函数，一条公式在替换，一条公式在新增，区别只在于“那条列上本来有没有筛选”。这也是最容易踩的坑：你写“华东销量”时心里想的是“在现有条件里再加一个华东”，实际得到的是“把大区这一列的条件整个换成华东”。

如果你要的确实是取交集，用 `KEEPFILTERS` 包住筛选参数即可。至于做占比——分子算出来了，分母还得把大区筛选去掉才公平，那要用 `REMOVEFILTERS` 或 `ALL`，留到下一篇。

回头总结一下写第一个度量值时的顺序：先问答案会不会随用户点选变化，决定用计算列还是度量值；写度量值时，聚合函数负责把列压成一个数，迭代函数负责提供行上下文；最后用 `CALCULATE` 指出“我想在哪个条件上偏离当前单元格的筛选”，并且清楚这一列原来的筛选会因此被覆盖。

## 术语表

- 计算列：刷新数据时按行算一遍、结果存进模型的列，之后不随用户在报表上的点选变化。
- 度量值：不存储、每次图表要数字时按当前筛选上下文现算的计算，只能放在视觉对象的值里。
- 行上下文：迭代到某一行时“当前行是哪一行”的概念，只在计算列和迭代函数里存在，且只作用于被迭代的那张表。
- 筛选上下文：Power BI 在算一个单元格之前先准备好的一组筛选，作用于整个模型，并沿关系从“一”端传到“多”端。
- 迭代函数：把一张表逐行走一遍再汇总的函数，例如 SUMX，用它在度量值里造出行上下文。
- DISTINCTCOUNT：数一列里不重复的值的个数，用于“在已经被筛过的事实表里数对象”这类需求。
- CALCULATE：在改动过的筛选上下文里求值的函数，筛选参数对没有筛选的列是新增，对已有筛选的列是覆盖。
- KEEPFILTERS：包住 CALCULATE 的筛选参数，把默认的“覆盖”改成“与原有筛选取交集”。

## 来源

1. [CALCULATE function (DAX) — 官方语法说明与筛选参数“新增或覆盖”的规则](https://learn.microsoft.com/en-us/dax/calculate-function-dax)
2. [Modify filter context — CALCULATE 修改筛选上下文的两种结果与示例](https://learn.microsoft.com/en-us/training/modules/dax-power-bi-modify-filter/2-modify-filter-context)
3. [KEEPFILTERS function (DAX) — 把默认的覆盖改为与原有筛选取交集](https://learn.microsoft.com/en-us/dax/keepfilters-function-dax)
4. [Use Calculation Options in Power BI Desktop — 计算列与度量值对比表：何时计算、是否持久化、是否随用户交互变化](https://learn.microsoft.com/en-us/power-bi/transform-model/desktop-calculations-options)
5. [Calculated Columns and Measures in DAX — 计算列在刷新时计算并占用内存，度量值按单元格的筛选上下文求值](https://www.sqlbi.com/articles/calculated-columns-and-measures-in-dax/)
6. [Row Context and Filter Context in DAX — 行上下文只在迭代和计算列中出现且不沿关系传播；筛选上下文作用于模型并沿关系传播](https://www.sqlbi.com/articles/row-context-and-filter-context-in-dax/)
7. [DAX error messages in Power BI — “A single value for column … cannot be determined” 的真实含义是缺少行上下文](https://www.sqlbi.com/articles/dax-error-messages-in-power-bi/)
8. [Filter context in DAX — 矩阵的每一行不是行上下文，单元格的筛选上下文筛的是列的取值](https://www.sqlbi.com/articles/filter-context-in-dax/)

---

原文：https://pangzhengboyin.com/articles/dax-measures-row-vs-filter-context-ea75113f

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具。说出想学什么，它会先了解你的基础，再把主题写成一篇篇 5–10 分钟能读完的文章；边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
