上一篇结尾留了一个错误的数字:按颜色看客户数,蓝色只有张三买过,结果却算出 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-01CL-0110
2月2日CUST-01CL-0220
3月3日CUST-02CL-0130

先写 销量 = 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 指出“我想在哪个条件上偏离当前单元格的筛选”,并且清楚这一列原来的筛选会因此被覆盖。