DAX权威指南2:CALCULATE 与 CALCULATETABLE
文章目录
- 一、使用CALCULATE创建新的筛选上下文
- 1.1 不修改筛选上下文计算毛利率
- 1.2 通过CALCULATE修改筛选上下文来计算毛利率
- 二、 CALCULATE 的工作原理
- 2.1 筛选参数是表
- 2.2 同一列的筛选器被覆盖
- 2.3 不同列的筛选器被合并
- 2.4 同一列多次筛选
- 2.5 多列筛选
- 2.6 总结
- 三、 CALCULATE中的计算顺序
- 3.1 DAX函数中的嵌套计算顺序
- 3.2 CALCULATE的计算顺序
- 3.3 使用KEEPFILTERS保留筛选
- 3.3.1 保留某个列中的单个筛选元素
- 3.3.2 保留某个列中的多个筛选元素
- 3.3.3 在内层使用KEEPFILTERS
- 四、上下文转换
- 4.1 行上下文迭代,筛选上下文筛选(待重排)
- 4.2 迭代函数中的上下文转换
- 4.3 计算列中的上下文转换
- 4.4 度量值中的上下文转换
- 4.4.1 计算列中的度量值引用
- 4.4.2 迭代函数中的度量值引用
- 4.4.3 度量值引用误区
- 五、 循环依赖
- 5.1 循环依赖的形成
- 5.2 解决方案
- 5.3 注意事项
- 六、 CALCULATE修改器
- 6.1 ALL修改器:清除筛选
- 6.1.1 清除单列筛选器,计算百分比
- 6.1.2 清除多列筛选器
- 6.1.3 清除表中所有的筛选器
- 6.1.4 跨表筛选 :清除多个表的筛选器
- 6.1.5 移除相关表上所有的筛选器
- 6.1.6 使用VALUES恢复部分筛选器(待续)
- 6.1.7 不使用参数&ALLSELECTED
- 6.1.8 使用ALLEXCEPT移除指定列以外所有筛选器
- 6.1.9 ALL 函数覆盖现有的筛选器
- 6.2 `KEEPFILTERS` :迭代表修改器
- 6.3 USERELATIONSHIP
- 6.3.1 基本用法
- 6.3.2 注意事项
- 6.3.3 CALCULATE中的计算优先级
- 6.4 CROSSFILTER
- 七、 CALCULATE 规则
- 7.1 CALCULATE运行过程
- 7.2 参数分类与计算顺序
一、使用CALCULATE创建新的筛选上下文
DAX中,计算上下文分为行上下文和筛选上下文。行上下文在计算列中自动创建,同时也可以通过迭代器以编程方式创建。 筛选上下文可通过在报表中添加切片器、筛选器等各种可视化对象来创建的,也可以通过编程方式创建,那就是CALCULATE 和 CALCULATETABLE函数。
在DAX中,只有CALCULATE 和 CALCULATETABLE 这两个函数能够修改筛选上下文。它们可以创建新的筛选上下文、覆盖现有筛选上下文,或者移除某些筛选条件。 CALCULATE 用于返回一个标量值(如数字或字符串),而 CALCULATETABLE 则返回一个表。虽然本文主要围绕 CALCULATE 展开,但 CALCULATETABLE 的原理与之类似。
有关计算上下文、DAX和表函数的内容,详见《DAX 权威指南1:DAX计算、表函数与计算上下文》
1.1 不修改筛选上下文计算毛利率
下面通过一个案例介绍为何要修改筛选上下文。Contoso 是一家销售电子产品的公司,它同时销售 Contoso 品牌和其他品牌的产品,现在要求对比 Contoso 品牌与其他品牌的毛利及毛利率。传统方式是先创建三个基础度量值:
Sales Amount := // 销售额 = 销售数量 * 销售净价格
SUMX ( Sales, Sales[Quantity] * Sales[Net Price] )Gross Margin := // 毛利 = 销售数量 *( 销售净价格 - 单位成本 )
SUMX ( Sales, Sales[Quantity] * ( Sales[Net Price] - Sales[Unit Cost] ) ) GM % := // 毛利率 = 毛利 / 销售额
DIVIDE ( [Gross Margin], [Sales Amount] )
如果要计算 Contoso GM 和 Contoso GM %,需要使用 FILTER函数 将计算范围限制在 Contoso 产品上。下面的公式在 Sales 表中迭代计算,在 Product 表筛选出Contoso 产品,所以使用 RELATED 检索 Sales 表中每一行的相关产品。通过两次迭代 ContosoSales 变量,可以计算出 Contoso产品的毛利率:
Contoso GM % :=
VAR ContosoSales = -- 1. 筛选 Sales 表中 Contoso 品牌产品FILTER ( Sales,RELATED ( 'Product'[Brand] ) = "Contoso")
VAR ContosoMargin = --2. 迭代 ContosoSales,计算 Contoso产品 的毛利SUMX ( ContosoSales,Sales[Quantity] * ( Sales[Net Price] - Sales[Unit Cost] ))
VAR ContosoSalesAmount = --3. 迭代 ContosoSales,计算 Contoso产品 的销售额SUMX ( ContosoSales,Sales[Quantity] * Sales[Net Price])
VAR Ratio =DIVIDE ( ContosoMargin, ContosoSalesAmount )
RETURNRatio
尽管这些度量值有效,但这种方式不够优雅。事实上,该模型已经包含计算毛利和毛利率的度量值(Gross Margin
和GM%
),但是因为新的度量值需要增加筛选条件,我们不得不重新写表达式。
1.2 通过CALCULATE修改筛选上下文来计算毛利率
通过 CALCULATE
, CALCULATE
的语法为:
CALCULATE ( Expression, Condition1, … ,ConditionN )
CALCULATE
函数可以接受任意数量的参数,只有第一个参数是必须的,它是一个需要计算的表达式,其它参数都是筛选参数,用于定义筛选条件。CALCULATE
函数通过这些筛选参数创建一个新的筛选上下文,将其应用于数据模型,并在此上下文中计算第一个参数中的表达式。
通过使用 CALCULATE
,我们可以修改筛选上下文,复用之前创建的度量值,而不用重写代码,更轻松地实现同样的功能:
Contoso GM :=
CALCULATE ( [Gross Margin], -- 计算毛利'Product'[Brand] = "Contoso" ) -- 筛选上下文 brand = ContosoContoso GM% :=
CALCULATE ( [GM %], -- 计算毛利率'Product'[Brand] = "Contoso" ) -- 筛选上下文 brand = Contoso
二、 CALCULATE 的工作原理
CALCULATE 的关键特性包括:
- 筛选参数是表:即使使用布尔条件,DAX 也会将其转换为一个值列表,即转换为 FILTER(ALL(列), 布尔条件) 的形式。
- 覆盖或合并原有筛选器:CALCULATE 会覆盖同一列上原有的筛选器,但对不同列上的筛选器则进行合并。
2.1 筛选参数是表
在上一节中,我们使用了以下代码:
Contoso GM :=
CALCULATE ( [Gross Margin], -- 计算毛利'Product'[Brand] = "Contoso" ) -- 筛选上下文 brand = Contoso
这里使用布尔条件只是一种快捷方式,是完整版本的语法糖(一种对语法的简化,其背后执行逻辑要考虑多种情况)。完整的写法是:
Contoso GM :=
CALCULATE ([Gross Margin], -- 计算毛利FILTER ( -- 筛选 Product[Brand]的有效值ALL ( 'Product'[Brand] ), -- 任意 Product[Brand]的值'Product'[Brand] = "Contoso" -- 产品名称等于 "Contoso")
)
这两种语法是等价的,它们之间没有性能或语义上的差异。第二种方式显式地将 CALCULATE 的筛选参数写作值列表(指定列上由可见值组成的列表) 会使得 CALCULATE 的行为更加明显。此例中,FILTER 返回只有一行的表,即包含 *Product[Brand]*等于“Contoso”的表。换句话说,CALCULATE 函数使“Contoso”是 *Product[Brand]*列的唯一可见值。因此,CALCULATE 筛选出的模型只包括 Contoso 品牌的产品。一旦熟悉了 CALCULATE 的语义,第一种方式更加简洁。
由于 DAX 引擎的转换逻辑限制,布尔条件筛选参数只能引用一列,这种类型的筛选参数也称为谓词。如果需要在布尔表达式中引用多列,需显式编写 FILTER 迭代表达式。
2.2 同一列的筛选器被覆盖
首先创建两个度量值,然后将其放入以产品品牌划分的矩阵中:
Sales Amount :=
SUMX ( Sales, Sales[Quantity] * Sales[Net Price] )Contoso Sales :=
CALCULATE ([Sales Amount],FILTER ( ALL ( 'Product'[Brand] ), 'Product'[Brand] = "Contoso" )
)
此示例中,报告按产品品牌划分,对每一行,报告都会创建一个包含相关品牌的筛选上下文。例如,在 Litware 行中,原始筛选上下文包含一个'Product'[Brand]= Litware
的筛选器。当计算 Contoso Sales 时,CALCULATE 首先会复制当前的筛选上下文,然后计算筛选参数。
ALL ( 'Product'[Brand] )
返回产品品牌列中所有可能的值,覆盖了任何在产品品牌列上现有的筛选器,生成一个包含所有品牌值的表。接着 FILTER 函数遍历这个表,筛选出 Product[Brand] = “Contoso”
的值。可以看到,原有品牌列上的任何筛选器都被CALCULATE 中筛选参数所组成的新的筛选器覆盖(或者说替换),确保只计算 Contoso 品牌的值。
上下文转换(后续会重点讲解这个概念):在这个过程中,CALCULATE 还会将任何现有行上下文转换为筛选上下文。例如,当在报告的某个品牌行中计算 Contoso Sales 度量值时,该行的行上下文(即当前品牌)会被转换为筛选上下文,但这个筛选上下文在产品品牌列上会被 CALCULATE 的筛选参数所覆盖,从而确保 [Sales Amount] 的计算是基于 Contoso 品牌的。
2.3 不同列的筛选器被合并
如果将报告更改为按 Product[Category]
列进行划分,两个筛选器位于不同列,不会发生覆盖。相反,CALCULATE会将新的筛选器与原有的筛选器合并,共同作用于数据计算。
2.4 同一列多次筛选
只要筛选参数引用单个列,都可以使用谓词语法(布尔表达式)来写筛选参数,而不必使用显式的 FILTER形式,这样代码容易阅读。比如,在一个筛选表达式中可以多次引用同一列:
Sales 10-100 := CALCULATE ( [Sales Amount], Sales[Net Price] >= 10 && Sales[Net Price] <= 100 )
它等价于:
Sales 10-100 :=
CALCULATE ([Sales Amount],FILTER (ALL ( Sales[Net Price] ),Sales[Net Price] >= 10&& Sales[Net Price] <= 100)
)
尽管第一种写法看起来像条件,但其实它是表,它是按第二种写法 CALCULATE 计算 Sales [Net Price]
中所有满足条件的列表,然后 CALCULATE 将该表应用于模型筛选(用表筛选表)。所以在使用谓语时,永远不要忘记你只是在依赖语法糖。
CALCULATE 的多个筛选参数之间总是逻辑 AND 的关系,所以当两个条件之间本身就是 AND 关系时,可将它们表示为两个单独的筛选器。事实上,前面的表达式与下面的是等价的:
Sales 10-100 := CALCULATE ( [Sales Amount], Sales[Net Price] >= 10, Sales[Net Price] <= 100 )
如果是逻辑 OR 的关系时,必须使用单个筛选参数:
Sales Blue+Red :=
CALCULATE ([Sales Amount],'Product'[Color] = "Red" || 'Product'[Color] = "Blue"
)
两个或多个独立的筛选参数会合并为单一的筛选上下文。下面的度量值总是产生一个空值,因为没有一个产品既是 Blue 同时又是 Red:
Sales Blue and Red :=
CALCULATE ([Sales Amount],'Product'[Color] = "Red",'Product'[Color] = "Blue"
)
这等价于:
Sales Blue and Red :=
CALCULATE ([Sales Amount],'Product'[Color] = "Red" && 'Product'[Color] = "Blue"
)
2.5 多列筛选
当筛选条件涉及多个列时,则必须使用 FILTER 函数明确地创建表筛选器。例如,计算销售金额≥ 1000 的交易总和,以下代码无效,因为筛选参数在一个表达式中引用了两个不同列,DAX 不能自动将其转换成合适的 FILTER 表达式。
Sales Large Amount :=
CALCULATE ( [Sales Amount], Sales[Quantity] * Sales[Net Price] >= 1000 )
正确的方法是使用 FILTER 函数显式定义筛选条件:
Sales Large Amount := CALCULATE ([Sales Amount],FILTER (ALL ( Sales[Quantity], Sales[Net Price] ),Sales[Quantity] * Sales[Net Price] >= 1000)
)
2.6 总结
CALCULATE计算过程总结如下:
- 复制当前筛选上下文 :
CALCULATE
函数在执行时,会首先复制当前的筛选上下文,以便在后续的计算过程中使用。 - 计算筛选参数,生成值列表筛选器 :CALCULATE 计算每个筛选参数,并为每个条件生成指定列的有效值列表。即使使用布尔条件筛选参数,DAX 引擎也会将其转换为值列表的形式。
- 创建新的筛选上下文 :对于同一列上的筛选器,
CALCULATE
会用新的筛选器替换现有筛选器;对于不同列上的筛选器,则会将它们合并到新的筛选上下文中。 - 应用新筛选上下文并计算表达式:新的筛选上下文建立后,
CALCULATE
将其应用于数据模型,并基于此计算第一个参数:表达式。计算完成后,CALCULATE
恢复原始筛选上下文,并返回计算结果。
筛选的核心原理:DAX 引擎对于表的筛选是基于值列表的,或者说值的集合来实现的,本质上是用表筛选表 。DAX基于这个原理对数据模型进行筛选,从而计算出符合筛选条件的结果。
三、 CALCULATE中的计算顺序
3.1 DAX函数中的嵌套计算顺序
DAX函数经常嵌套使用,通常先计算最里面的函数,然后逐层递进到最外层的函数。例如在使用FILTER 函数时,建议将高选择性条件放在内层,以快速过滤数据:
// 比起Fabrikam品牌,高于成三倍的产品更少,所以优先筛选
FabrikamHighMarginProducts =
FILTER (FILTER ('Product','Product'[Unit Price] > 'Product'[Unit Cost] * 3),'Product'[Brand] = "Fabrikam"
)
又例如以下代码中,先计算内层FILTER的结果得到筛选表,再在筛选表中执行SUMX迭代:
Sales Amount Large :=
SUMX (FILTER ( Sales, Sales[Quantity] >= 100 ),Sales[Quantity] * Sales[Net Price]
)
3.2 CALCULATE的计算顺序
以上规则适用于除了 CALCULATE 和 CALCULATETABLE 之外所有的 DAX 函数。对于CALCULATE:
-
先计算所有筛选参数,然后再计算第一个参数(要计算的表达式)。整个过程可参考以下第三段代码——使用变量定义的
Sales Red Contoso
,更容易理解筛选器是在 CALCULATE“之前”被计算的。 -
所有筛选参数在CALCULATE外部的筛选上下文中执行,且同一CALCULATE中的多个筛选器是独立计算的,顺序无关紧要。因此,下列各项度量值完全相同(如果这样看, KEEPFILTERS不是CALCULATE修改器啊):
Sales Red Contoso := CALCULATE ([Sales Amount],'Product'[Color] = "Red",KEEPFILTERS ( 'Product'[Brand] = "Contoso" ) )Sales Red Contoso := CALCULATE ([Sales Amount],KEEPFILTERS ( 'Product'[Brand] = "Contoso" ),'Product'[Color] = "Red" ) // 使用变量定义,更容易理解筛选器是在 CALCULATE“之前”被计算的 Sales Red Contoso := VAR ColorRed =FILTER ( ALL ( 'Product'[Color] ), 'Product'[Color] = "Red" ) VAR BrandContoso =FILTER ( ALL ( 'Product'[Brand] ), 'Product'[Brand] = "Contoso" ) VAR SalesRedContoso =CALCULATE ( [Sales Amount], ColorRed, KEEPFILTERS ( BrandContoso ) ) RETURNSalesRedContos
-
嵌套CALCULATE时,最外层筛选器先应用,最内层最后应用。内层CALCULATE的筛选器会覆盖外层相同列的筛选器,除非使用KEEPFILTERS。
例如,考虑以下度量值,其中 Sales Green 调用 Sales Red:
Sales Red :=CALCULATE ( [Sales Amount], 'Product'[Color] = "Red" )Green calling Red :=CALCULATE ( [Sales Red], 'Product'[Color] = "Green" )
为了使嵌套的度量调用更加明显,我们将其改写为:
Green calling Red Exp := CALCULATE (CALCULATE ( [Sales Amount], 'Product'[Color] = "Red" ),'Product'[Color] = "Green" )
- 首先,外部 CALCULATE 应用筛选器
Product[Color] = "Green"
; - 其次,内部 CALCULATE 应用筛选器
Product[Color] = "Red"
,此筛选器覆盖前一个筛选器; - 最后,DAX 使用
Product[Color] = “Red”
筛选器计算[Sales Amount]
。
- 首先,外部 CALCULATE 应用筛选器
因此,Red 和 Green calling Red 的结果都是 Red,如下图所示:
我们可以通过另一个示例来查看计算的顺序以及筛选上下文如何计算的:
Sales YB :=
CALCULATE (CALCULATE ( [Sales Amount], 'Product'[Color] IN { "Yellow", "Black" } ),'Product'[Color] IN { "Black", "Blue" }
)
如前所述,Product[Color]
上最内层的筛选器覆盖了最外层的筛选器,因此,度量值结果显示的是 Yellow 和 Black 产品的总和。
3.3 使用KEEPFILTERS保留筛选
3.3.1 保留某个列中的单个筛选元素
之前已经介绍过CALCULATE的特性,CALCULATE 会覆盖同一列上原有的筛选器。因此,以下度量值将忽略 Product[Category] 上的已有筛选器,只返回 Audio 的销售额。
Audio Sales := CALCULATE ( [Sales Amount], 'Product'[Category] = "Audio" )
在某些情况下,我们希望保留现有筛选器,而不是覆盖它们。例如我们希望仅在特定类别中显示销售额,可以这么写:
Audio Sales KeepFilters := CALCULATE ( [Sales Amount], KEEPFILTERS ( 'Product'[Category] = "Audio" ) )
正如它名字的含义,KEEPFILTERS 不会覆盖同一列上的现有筛选器,而是将新筛选器追加到现有筛选器中。对于Cell Phones 这一行,追加 Audio筛选条件之后,由于这两个条件的交集是一个空集,所以结果为空值。
3.3.2 保留某个列中的多个筛选元素
我们还可以使用KEEPFILTERS
保留某个列中的多个元素。以下代码分别使用和不使用KEEPFILTERS
同时保留Audio 和 Computers:
Always Audio-Computers :=
CALCULATE ( [Sales Amount], 'Product'[Category] IN { "Audio", "Computers" } )
KeepFilters Audio-Computers :=
CALCULATE ([Sales Amount],KEEPFILTERS ( 'Product'[Category] IN { "Audio", "Computers" } )
)
KEEPFILTERS
既可以与谓词一起使用,也可以与表一起使用。上述代码可以写成:
KeepFilters Audio-Computers :=
CALCULATE ([Sales Amount],KEEPFILTERS (FILTER (ALL ( 'Product'[Category] ),'Product'[Category] IN { "Audio", "Computers" }))
)
3.3.3 在内层使用KEEPFILTERS
接着以5.3.2中最后一个示例为例。如果在最里层的 CALCULATE 函数中使用 KEEPFILTERS
,最终的筛选上下文是将两个筛选器合并,而非用新的筛选器覆盖已有筛选器:
Sales YB KeepFilters :=
CALCULATE (CALCULATE ([Sales Amount],KEEPFILTERS ( 'Product'[Color] IN { "Yellow", "Black" } )),'Product'[Color] IN { "Black", "Blue" }
)
由于这两个筛选器同时保留,它们互相取交集,因此,在新的筛选上下文中,唯一可见的颜色是 Black。
四、上下文转换
DAX中上下文分为行上下文和筛选上下文。当在行上下文中执行CALCULATE 函数时,CALCULATE 会将当前行上下文中的所有列及其值作为筛选条件添加到筛选上下文中,这一过程称为上下文转换。
4.1 行上下文迭代,筛选上下文筛选(待重排)
还是以之前的代码举例,我们使用以下公式计算Sales Amount ,然后创建一个报表,并在Brand列上逐行计算。
Sales Amount := SUMX(Sales, Sales[Quantity] * Sales[Unit Price])
整个计算过程如下:
-
创建筛选上下文:报表基于
Product[Brand]="Contoso"
创建初始筛选上下文。该筛选器通过关系传播影响整个模型,同时筛选Product表和Sales表 -
SUMX迭代表被筛选:筛选上下文减少了SUMX需要迭代的数据量,只处理与
Contoso
产品相关的Sales表行(本例中为2行) -
迭代计算: 第一行行上下文为:Quantity=1, Unit Price=11.00,计算结果为1 × 11.00 = 11.00;第二行以此类推,SUMX 返回迭代结果的合计(11.00 + 21.98 = 32.98)。
计算过程中始终存在两种上下文:
- 筛选上下文:
Product[Brand]="Contoso"
,保持不变。筛选上下文首先减少数据规模,SUMX只处理可见行; - 行上下文:SUMX为每行创建临时行上下文,用于访问当前行各列的值。
可以看出,在迭代过程中,筛选上下文仍然是被激活的,行上下文和筛选上下文共同决定了每行的计算值。行上下文迭代,筛选上下文筛选,后者只是通过筛选条件影响计算结果,而不会主动迭代表,不会逐行处理。为了理解这段话,对比以下两个度量值:
NumOfSales := COUNTROWS ( Sales )Sum Num Of Sales := SUMX ( Sales, COUNTROWS ( Sales ) )
对于Sum Num Of Sales
,外层 SUMX(Sales, ...)
会对 Sales 表的每一行执行一次迭代;内层 COUNTROWS(Sales) 计算当前筛选上下文中的 Sales 表行数。那么COUNTROWS(Sales) 是否受行上下文影响?
答案是否。COUNTROWS 是一个聚合函数,仅依赖筛选上下文,不依赖行上下文。即使 SUMX 在迭代 Sales 表的某一行,COUNTROWS(Sales) 仍然计算的是整个筛选上下文中的行数,不受当前行的影响。所以整个计算过程为:
- SUMX 迭代 Sales 表的每一行(共 37,984 次)
- 对每一行,COUNTROWS(Sales) 返回相同的值( 37,984)
- SUMX 将 37,984 累加 37,984 次,最终结果为 37,984 × 37,984 = 1,442,764,256(即 37,984 的平方)
行上下文 vs 筛选上下文的独立性:SUMX 的迭代是行上下文的行为,但它不会影响 COUNTROWS(Sales) 的计算范围。COUNTROWS(Sales) 始终基于筛选上下文(如品牌 “Contoso”),不关心当前正在迭代的行。
4.2 迭代函数中的上下文转换
当表上发生迭代时,行上下文就存在,迭代内部的表达式依赖于行上下文本身,比如下面这个表达式中,Quantity 和 Unit Price 这两列在当前行上下文中各有一个值:
Sales Amount := SUMX ( Sales, Sales[Quantity] * Sales[Unit Price] )
上一节演示了,如果在迭代函数中使用的表达式(COUNTROWS ( Sales )
)没有明确依赖于行上下文中的列,那么这个表达式不会随着每一行的变化而变化,而是会在筛选上下文中计算一次(即当前筛选上下文中 Sales 表的行数),并在迭代中重复使用这个值(在筛选上下文中计算,而不是在行上下文中计算)。
在行下文中可以使用任何函数,包括 CALCULATE。 但它是特殊的,在行上下文中执行时,CALCULATE 将在计算其表达式前先使行上下文失效,因此,下面的代码会产生语法错误:
Sales Amount :=
SUMX ( Sales, CALCULATE ( Sales[Quantity] ) -- CALCULATE 里面没有行上下文,错误!
)
因为 CALCULATE 会使其外部的行上下文失效,于是不能在 CALCULATE 中检索到 Sales[Quantity]列的值。这只是其一,其二是,CALCULATE 将当前行上下文的所有列及其当前值添加为筛选参数。例如:
Sales Amount :=
SUMX ( Sales, CALCULATE ( SUM ( Sales[Quantity] ) ) ) -- SUM 不需要行上下文
在 CALCULATE 中没有筛选参数,只有一个表达式。但是由于在迭代函数中计算, CALCULATE 会将行上下文自动转换为筛选参数,即它为被迭代表中的每一列创建一个筛选器,筛选出当前行的值,如下图所示:
示例中有三列:Product,Quantity,Net Price
。因此,由上下文转换生成的筛选上下文包含每个列的当前值(A,1,11.00
)。在 SUMX 的迭代过程中,对每一行,这个过程都会继续下去。换句话说,前一个 SUMX 的执行会引起如下三个 CALCULATE 函数的执行:
CALCULATE (SUM ( Sales[Quantity] ),Sales[Product] = "A",Sales[Quantity] = 1,Sales[Net Price] = 11
) +
CALCULATE (SUM ( Sales[Quantity] ),Sales[Product] = "B",Sales[Quantity] = 2,Sales[Net Price] = 25
) +
CALCULATE (SUM ( Sales[Quantity] ),Sales[Product] = "A",Sales[Quantity] = 2,Sales[Net Price] = 10.99
)
这些筛选参数是隐藏的。它们由引擎自动添加,并且没有办法避免,也就是所谓隐式的上下文转换。
总结以上内容:
序号 | 知识点描述 | 详细解释 |
---|---|---|
1 | 上下文转换非常昂贵 | 上下文转换会创建包含所有列的筛选上下文。如果在一个包含10列和100万行的表的迭代过程中使用了上下文转换,那么CALCULATE需要应用10个筛选器,迭代100万次,因此需要谨慎使用。 |
2 | 上下文转换筛选整组匹配行 | 原生行上下文每次只处理一行,但当转换为筛选上下文时,新创建的筛选上下文将筛选具有相同值集的所有行,而不是只创建包含一行的筛选上下文。 |
3 | 隐式筛选所有列 | 即使公式中未显式引用,上下文转换也会将整个表的所有列作为筛选条件,形成隐藏的依赖关系,可能导致意料之外的计算结果。 |
4 | 跨表筛选效应传播 | 上下文转换会改变筛选器的本质特性。DAX 从行上下文的逐行迭代转变为模型范围的筛选,使表间关系成为计算条件的一部分。这种转换可能导致起源于一个表的上下文转换,其影响会传递到模型中其他相关表。 |
5 | 计算列自动触发转换 | 在计算列中使用CALCULATE就会发生上下文转换,因为计算列中存在自动的行上下文。 |
6 | 多表迭代中的上下文处理 | 在跨表嵌套迭代中,上下文转换会合并所有活跃行上下文的列作为筛选条件,并使原始行上下文失效,这一机制确保每个表迭代的上下文都能统一处理。 |
7 | 上下文转换使行上下文无效 | 在CALCULATE内部,所有外部行上下文会被自动转换为等效筛选上下文。这一特性确保了计算的一致性,但也意味着原始行上下文此时已不再有效。这是上下文转换的重要副作用,需要理解其含义。 |
4.3 计算列中的上下文转换
在 Product 表中创建一个计算列,该列将销售额占总销售额的 1% 以上的产品标记为“High Performance(高绩效)”,代码如下:
'Product'[Performance] =
VAR TotalSales = -- 所有产品的销售SUMX ( Sales, -- Sales 表没有被筛选Sales[Quantity] * Sales[Net Price] ) -- 所以计算的是总销售额
VAR CurrentSales =CALCULATE ( -- 执行上下文转换SUMX ( Sales, -- 仅是当前产品的销售Sales[Quantity] * Sales[Net Price] ) -- 这里我们仅计算) -- 当前产品的销售额
VAR Ratio = 0.01 -- 1% 表示一个实数
VAR Result =IF (CurrentSales >= TotalSales * Ratio,"High Performance product","Regular product")
RETURNResult
可以看到这两个变量之间只有一个区别:TotalSales 作为常规迭代执行,而 CurrentSales 在一个 CALCULATE 函数中执行相同的 DAX 代码。因为这是一个计算列,所以将行上下文转换为筛选上下文。筛选上下文通过模型传到 Sales 表,只筛选当前产品的销售额。
最终,TotalSales 计算所有产品的总销售额,因为计算列中的筛选上下文为空,不筛选任何内容。而 CurrentSales 通过 CALCULATE 执行的上下文转换只计算当前产品的销售额。
在使用上下文转换特性之前,必须考虑所有情况:
- 性能:Product 表很小,只包含几千行。因此性能不是问题。
- 非唯一行的问题:如果表中的行不是唯一的,上下文转换可能会筛选出多行,而不是预期的单行。
Product 表有一个唯一键列Productkey,因此每一行都是唯一的,由上下文转换生成的筛选上下文保证只筛选一个产品。但是如果在 Sales 中创建了一个计算列,由于Sales 表中的行不是唯一的,上下文转换可能会筛选出多行,导致计算结果不准确。
在许多实际场景中,表中重复的行数很少,这些不准确的计算很难被发现并改正。从下图可以看到,两个度量值的计算结果差异只存在于汇总层面和 Fabrikam 品牌(只有关于 Fabrikam 产品的行是重复的,继而被重复计算)。另外,Sales 表可能包含数百万行的行,因此,这个计算列不仅是错误的,也是非常缓慢的。Sales[Wrong Amt] = CALCULATE ( SUMX ( Sales, Sales[Quantity] * Sales[Net Price] ) )
上下文转换会将所有的列全部转换,这一特性是一个大坑,请死死地记住,在你使用 CALCULATE中 出错时,很有可能是这里出了问题,理解并可以自然地排除这个问题是晋级 DAX 专家的重要一步。
4.4 度量值中的上下文转换
每个度量值引用总是伴随着一个隐式的 CALCULATE,或者说度量值引用总是调用 CALCULATE。在任何行上下文中,由于此 CALCULATE 的存在,度量值引用都将引起隐式上下文转换。
4.4.1 计算列中的度量值引用
例如在Product表中创建一个计算列Product Sales ,用于计算每一行产品的销售额::
Sales Amount := SUMX ( Sales, Sales[Quantity] * Sales[Net Price] )'Product'[Product Sales] = [Sales Amount]
在 Product Sales 计算列中,[Sales Amount] 是一个度量值引用。根据上下文转换的规则,这个引用会被自动封装在 CALCULATE 函数中,展开后如下:
'Product'[Product Sales] = CALCULATE ( SUMX ( Sales, Sales[Quantity] * Sales[Net Price] ) )
此式中,CALCULATE 将行上下文(当前产品的上下文)转换为筛选上下文,确保 SUMX 只计算当前产品的销售额。如果没有 CALCULATE,SUMX 会计算所有产品的总销售额。
4.4.2 迭代函数中的度量值引用
在迭代函数(如 SUMX、FILTER、AVERAGEX 等)内部引用度量值时,会自动触发隐式的 CALCULATE 调用,继而触发隐式上下文转换。换句话说,总是可以用封装在 CALCULATE 中的度量值表达式替换迭代函数中的度量值引用。比如以下度量值,计算每日 Sales Amount 的最大值:
Max Daily Sales := MAXX ( 'Date', [Sales Amount] )
在内部计算时,DAX 用 CALCULATE 封装公式替换了 Sales Amount 度量值引用:
Max Daily Sales :=
MAXX ('Date',CALCULATE ( SUMX ( Sales, Sales[Quantity] * Sales[Net Price] ) )
)
当我们开始编写复杂的 DAX 代码来解决特定场景时,我们将广泛地使用上下文转换这个特性。总结起来就是:当在任何行上下文中调用 CALCULATE 或 CALCULATETABLE 函数;或者是在任何行上下文中引用度量值时(计算列或者迭代函数),都会触发上下文转换。
4.4.3 度量值引用误区
在行上下文中引用度量值时,不能简单地用度量值的定义替换度量值引用,因为这样做会触发隐式的上下文转换,导致计算结果与预期不符(没有行上下文时可以这样做,比如在度量值中引用度量值)。
比如之前我们使用以下代码,在Product表中创建了一个计算列。通过在Sales 上重复迭代两次,筛选出产品的销售额大于总销售额1%的产品,并将其标记为 “High Performance product”。
'Product'[Performance] =
VAR TotalSales = -- 所有产品的销售SUMX ( Sales, -- Sales 表没有被筛选Sales[Quantity] * Sales[Net Price] ) -- 这里我们计算所有销售额
VAR CurrentSales =CALCULATE ( -- 执行上下文转换SUMX ( Sales, -- 仅是当前产品的销售Sales[Quantity] * Sales[Net Price] ) --这里我们仅计算) -- 当前产品的销售额
VAR Ratio = 0.01 -- 1% 表示一个实数
VAR Result =IF (CurrentSales >= TotalSales * Ratio,"High Performance product","Regular product")
RETURNResult
TotalSales
和CurrentSales
的唯一区别,是一个被 CALCULATE 包围,以在行上下文中筛选出当前行的产品,而另一个没有。一个自然的想法是将Sales[Quantity] * Sales[Net Price]
封装成一个度量值,在两次迭代计算中重复使用,计算越复杂越是可以简化代码。下面我们创建一个Sales Amount度量值,然后再两次迭代中一次使用 CALCULATE,一次不使用 CALCULATE:
Sales Amount :=
SUMX ( Sales, Sales[Quantity] * Sales[Net Price] )'Product'[Performance] =
VAR TotalSales = [Sales Amount]
VAR CurrentSales =CALCULATE ( [Sales Amount] )
VAR Ratio = 0.01
VAR Result =IF (CurrentSales >= TotalSales * Ratio,"High Performance product","Regular product")
RETURNResult
两个公式的计算逻辑完全是一样的,但这个计算列公式并没有计算出预期的结果。原因是在 TotalSales
中,[Sales Amount]
是一个在行上下文中的度量值引用,它会被隐式地封装在 CALCULATE 中。因此,TotalSales
的实际计算如下(展开之后):
TotalSales = CALCULATE ( SUMX ( Sales, Sales[Quantity] * Sales[Net Price] ) )
也就是说TotalSales
同样是计算每一行的销售额,而非当前行的销售额。类似的,CurrentSales
中计算[Sales Amount]
时也会触发隐式上下文转换,其实际计算过程如下,外层的CALCULATE 是多余的。
CurrentSales = CALCULATE ( CALCULATE ( SUMX ( Sales, Sales[Quantity] * Sales[Net Price] ) ) )
总结起来就是:在计算列中,TotalSales
和 CurrentSales
都会触发上下文转换,将行上下文转换为筛选上下文,都会计算当前产品的销售额,这与预期不符。如果需要计算所有产品的总销售额,应该使用第一种方式,避免在行上下文中直接引用度量值。
所以在 DAX 中,当你引用一个度量值时,应该意识到这个引用会隐式地触发一个 CALCULATE 函数。如果处于行上下文中(不管是计算列还是迭代函数中),这种隐式的 CALCULATE 会自动将行上下文转换为筛选上下文,可能导致与预期不符的结果。这也是为什么在DAX的语法中,约定永远在列引用中使用表名(比如Sales[Quantity]),而在度量值引用中不要使用表名(比如[Sales Amount])。因为这样你可以快速识别代码中引用的是列还是度量值。而当你看到度量值引用时,应该立马意识到它会隐式地触发一个 CALCULATE,从而可能改变计算的上下文。 隐式CALCULATE 使得编写使用迭代执行复杂计算的公式变得很容易,后面我们将广泛使用这个特性。
五、 循环依赖
5.1 循环依赖的形成
当两个或多个计算列相互依赖时,将形成循环依赖,例如:
Sales[MarginPct] = DIVIDE ( Sales[Margin], Sales[Unit Cost] )Sales[Margin] = Sales[MarginPct] * Sales[Unit Cost]
在这段代码中,MarginPct
依赖于 Margin
,同时 Margin
也依赖于 MarginPct
,依赖关系链中有一个循环。在这种情况下,DAX 拒绝接受最后一个公式,并显示错误信息:“检测到循环依赖关系”。
正常情况下,我们会自动避免这个问题,但是在在行上下文中使用 CALCULATE时,由于隐性的上下文转换,可能导致循环依赖的发生。考虑以下代码:
Sales[AllSalesQty] = CALCULATE(SUM(Sales[Quantity]))Sales[NewAllSalesQty] = CALCULATE ( SUM ( Sales[Quantity] ) )
第一个计算列可以正常创建,但第二个不行。对于第一个计算列,因为 CALCULATE 在行上下文中运行,所以表中所有列的当前值都隐性地包含在表达式中,它等价于:
Sales[AllSalesQty] =
CALCULATE (SUM ( Sales[Quantity] ),Sales[ProductKey] = <CurrentValueOfProductKey>,Sales[StoreKey] = <CurrentValueOfStoreKey>,...,Sales[Margin] = <CurrentValueOfMargin>
)
如上所见,一旦在行上下文中使用了 CALCULATE,AllSalesQty 依赖的列实际上是表的完整列集。因此如果两个计算列都使用 CALCULATE,那么在这两列都会出现上下文转换,从而形成循环依赖。
5.2 解决方案
如果表中存在唯一值列(如主键列),并且DAX引擎也知道这一点,那么DAX将只依赖该列进行上下文转换,从而避免循环依赖。例如:
'Product'[ProductSales] = CALCULATE(SUM(Sales[Quantity]))
由于Product 表中的 ProductKey
是唯一值列,DAX 引擎也知道这一点(该列位于“一对多”关系中的“一”侧)。因此,当上下文转换发生时,引擎知道向每个列添加筛选器是没有意义的,DAX 会将其自动转换为:
'Product'[ProductSales] = CALCULATE(SUM(Sales[Quantity]), 'Product'[ProductKey] = <CurrentValueOfProductKey>)
实际情况是 ,CALCULATE 将表的所有列添加为筛选参数,即使存在行标识符。不过,内部依赖关系仅在唯一列上创建,这使得 DAX 可以用 CALCULATE 计算多个列。不过,无论有没有唯一列,CALCULATE 的语义都是相同的:添加被迭代表的所有列为筛选参数。
5.3 注意事项
即使表中存在唯一值列,也必须让数据模型知道这一列,才能确保上下文转换只依赖于该列。通知 DAX 引擎的方法包括:
- 主键列:在“一对多”关系中,作为“一”端的列被标记为唯一。这适用于所有工具。
- 标记为日期表:当某列被设置为“Mark as Date Table”时,该列被隐式标记为唯一。
- 手动设置行标识符:通过“Table Behavior(表行为)”手动设置唯一列的行标识符。此方法仅适用于 Excel 和 Analysis Services 中的 Power Pivot,Power BI 暂不支持。
DAX 的语义要求在上下文转换时,依赖于表中的所有列。为了提高性能,DAX 引擎在很早就引入了一种优化。这种优化使得在上下文转换时,只依赖于表的主键列(即唯一值列)。然而,这种优化行为在某些边缘场景下可能不生效(比如使用 USERELATIONSHIP
函数时),从而导致循环依赖问题。
六、 CALCULATE修改器
6.1 ALL修改器:清除筛选
ALL可以作为一个表函数使用,例如ALL ( 'Date'[Year] )
返回一个包含所有年份的单列表。但是在作为 CALCULATE 修改器时,其行为与表函数不同。考虑以下代码:
All Years Sales := CALCULATE ( [Sales Amount], ALL ( 'Date'[Year] ) )
如果将 ALL 都看作一个表函数,那么其行为如左下图所示(先算外层再算内层,且内层覆盖外层)。实际上,ALL 不是返回所有年份,而是清除其引用的表或表列上的所有筛选器,所以实际情况如右下图所示。此时ALL应该称之为REMOVEFILTER。
由于此时ALL函数作为 CALCULATE 修改器,所以它会先于其他显式筛选参数执行。其它以 ALL 为前缀的函数具有相同的优先规则,包括ALL、ALLSELECTED、ALLNOBLANKROW、ALLCROSSFILTERED 和 ALLEXCEPT。所以下面两种写法结果相同:
Sales Red :=
CALCULATE ( [Sales Amount], 'Product'[Color] = "Red" )Sales Red :=
CALCULATE ([Sales Amount],KEEPFILTERS ( 'Product'[Color] = "Red" ),ALL ( 'Product'[Color] )
)
当ALL和KEEPFILTERS作用于同一列时,ALL会先清除所有筛选,所以两个式子中,KEEPFILTERS的都是在"全清除"后的基础上重新应用的。
6.1.1 清除单列筛选器,计算百分比
要计算当前类别占总销售额的百分比,就要用当前类别的销售额除以销售总额。在报告的每一行中,筛选上下文已经包含当前类别。因此,给定的类别会自动对 Sales Amount 进行筛选。比率的分母需要忽略当前筛选上下文来计算总金额,并且总是返回所有类别。因为 CALCULATE 的筛选参数是表,所以提供一个表函数就足够了,这个函数就是 ALL。
All Category Sales :=
CALCULATE ( -- 更改销售额的[Sales Amount], -- 筛选上下文ALL ('Product'[Category] ) -- 使所有类别可见
)
百分比计算公式为:
Sales Pct :=
VAR CurrentCategorySales = -- CurrentCategorySales 包含[Sales Amount] -- 的销售额处于当前筛选上下文
VAR AllCategoriesSales = -- AllCategoriesSales 包含
CALCULATE ( -- 的销售额处于所有类别均可见[Sales Amount], -- 的筛选上下文中ALL ( 'Product'[Category] )
)
VAR Ratio =
DIVIDE ( CurrentCategorySales,AllCategoriesSales
)
RETURN Ratio
当用作 CALCULATE 的筛选参数时,ALL 有特定的语义。事实上,它并没有用某列的所有值替换筛选上下文。相反,CALCULATE 使用 ALL 从筛选上下文清除了某列上的筛选器。混合表函数和 CALCULATE 使得轻松编写有用的度量值成为可能。
6.1.2 清除多列筛选器
如果向报告中添加 Product[Color] 列,作为报告行中的第二级详细信息。查看百分比统计结果,可以看到类别级别的值是正确的,而颜色级别的值看起来是错误的。
可以看到最终的筛选上下文只包含颜色,因此比率的分母是给定颜色所有类别的销售额。如果将颜色切片放在第一级,将类别切片放在第二级,报告是OK的,它展示了在给定的颜色中,某一类别的销售额占所有类别销售额的百分比,也就是计算出了某个类别中,子类别的分布情况。
使得公式既能计算类别也能计算颜色的百分比,有多种方式能够实现此操作,其中一种是使用ALL移除类别和颜色上的筛选器:
Sales Pct :=
VAR CurrentCategorySales = [Sales Amount]
VAR AllCategoriesAndColorSales =CALCULATE ([Sales Amount],ALL ( 'Product'[Category] ), -- 这两个 ALL 条件也可以写为ALL ( 'Product'[Color] ) -- ALL ( 'Product'[Category], ) -- 'Product'[Color] )
VAR Ratio =DIVIDE ( CurrentCategorySales, AllCategoriesAndColorSales )
RETURNRatio
6.1.3 清除表中所有的筛选器
上一节的报告已经用颜色和类别生成了正确的百分比,但是一旦向报告添加其他列,结果就会统计错误。如果想清除 Product 表上任何列的任何筛选器,可以将整张 Product 表作为 ALL 函数的参数:
Sales Pct All Products :=
VAR CurrentCategorySales = [Sales Amount]
VAR AllProductSales =CALCULATE ( [Sales Amount], ALL ( 'Product' ) )
VAR Ratio =DIVIDE ( CurrentCategorySales, AllProductSales )
RETURNRatio
6.1.4 跨表筛选 :清除多个表的筛选器
如果报告是按产品类别(Product[Category])和客户所在大陆(Customer[Continent])划分的,那么就需要 使用ALL移除多个表上的筛选器:
Sales Pct All Products and Customers :=
VAR CurrentCategorySales = [Sales Amount]
VAR AllProductAndCustomersSales =CALCULATE ( [Sales Amount], ALL ( 'Product' ), ALL ( Customer ) )
VAR Ratio =DIVIDE ( CurrentCategorySales, AllProductAndCustomersSales )
RETURNRatio
6.1.5 移除相关表上所有的筛选器
如果用户继续引入新表中的某列进行筛选,统计结果又会出错。一种方法是直接移除事实表(本例中是Sales表)上的所有筛选器,包括通过模型关系传递过来的筛选器,从而确保计算的总销售额是基于整个数据集的。这部分内容涉及扩展表原理,在第14章中会详细讲解。
Pct All Sales :=
VAR CurrentCategorySales = [Sales Amount]
VAR AllSales =CALCULATE ( [Sales Amount], ALL ( Sales ) )
VAR Ratio =DIVIDE ( CurrentCategorySales, AllSales )
RETURNRatio
我们从报告中移除了 Sales Amount,并把 Date 表中的Calendar Year 拖到列上。作为从 Sales 表中移除筛选器的一部分,Date 表上的也筛选器被移除。
6.1.6 使用VALUES恢复部分筛选器(待续)
如果需要计算当前年度的销售额百分比,即某个类别的销售额占当前年度总销售额的比例,CALCULATE 需要做两件事:
- 移除事实表上的所有筛选器
- 恢复年份筛选器
最终分母(总销售额)在当前年份的上下文中计算,并忽略除年份以外的所有其它筛选器,在CALCULATE 函数中结合 ALL 和 VALUES 函数可以实现这一目标。在第 3 章“使用基本表函数”中,我们学习了 VALUES 函数,它可以返回当前筛选上下文中指定列的唯一值列表。
如果当前上下文没有对年份进行任何筛选,那么 VALUES ( ‘Date’[Calendar Year] ) 将返回所有年份的列表。如果当前上下文已经通过筛选器限制了年份为 2023,那么 VALUES ( ‘Date’[Calendar Year] ) 将返回一个表,其中只包含一个值:2023。换句话说,VALUES 函数的结果是一个表,表中包含当前上下文中列的唯一值,这个表可以被用作 CALCULATE 函数的筛选参数。
Pct All Sales CY :=
VAR CurrentCategorySales = [Sales Amount]
VAR AllSalesInCurrentYear =CALCULATE ( [Sales Amount], ALL ( Sales ), VALUES ( 'Date'[Calendar Year] ) )
VAR Ratio =DIVIDE ( CurrentCategorySales, AllSalesInCurrentYear )
RETURNRatio
可以看到报告中每一年的合计都是100%。下图解析了详细的计算过程。
以4.22%的单元格为例,它原来有 CY 2007 和 Cell phones 两个筛选器。CALCULATE 函数使用
ALL (Sales)
从 Sales 表中移除所有筛选器。VALUES (Date[Calendar Year])
在原始筛选上下文中计算, VALUES 函数仍然受列上 CY 2007 的影响,因此,它返回原始筛选上下文中唯一可见的年份:CY 2007。最终,筛选上下文只包含 Calendar Year 的筛选器,分母在仅有 CY 2007 的筛选上下文中计算总销售额。
在调用 CALCULATE 时,CALCULATE 的筛选参数是原始筛选上下文中计算的(第一步复制原筛选器),这一点至关重要。事实上,CALCULATE 是在计算筛选参数之后才改变筛选上下文的。在表上使用 ALL,然后在列上使用 VALUES,是用清除表中所有筛选但保留一个列上筛选上下文的一种常用技巧。
6.1.7 不使用参数&ALLSELECTED
ALLSELECTED函数可用于清除当前可视化范围外的所有活动筛选上下文,只保留外部筛选(如切片器、页面筛选器)。比如以下代码可计算当前可视区域所选颜色占所有颜色的百分比:
SalesPct := DIVIDE ( [Sales], CALCULATE ( [Sales], ALLSELECTED ( 'Product'[Color] ) ) )
使用 ALLSELECTED (Product)
可以得到类似的结果,它在整个表上执行 ALLSELECTED。当用作 CALCULATE 修改器时,ALL 和 ALLSELECTED 甚至可以在没有任何参数的情况下工作,比如:
SalesPct := DIVIDE ( [Sales], CALCULATE ( [Sales], ALLSELECTED () ) )
此式中,ALLSELECTED 将恢复当前可视内容之外的所有活动的筛选上下文。类似地, ALL()
将清除模型中所有表的筛选器上下文,需要谨慎使用。
形式 | 行为 |
---|---|
ALLSELECTED(列/表) | 表函数(返回可见筛选值)或修改器(恢复指定列的可见外筛选) |
ALLSELECTED() | 纯修改器,恢复所有可见范围外的筛选上下文 |
ALL(列/表) | 表函数(返回所有值)或修改器(清除指定列的筛选) |
ALL() | 核弹级清除器,移除模型中所有表的筛选 |
6.1.8 使用ALLEXCEPT移除指定列以外所有筛选器
另一种方式是使用ALLEXCEPT,它可以移除指定列以外的所有筛选器,以下代码可以实现同样的功能。不过ALL/VALUES
的语义与 ALLEXCEPT
不同,在第 10 章“使用筛选上下文”中会详细介绍。
VAR AllSalesInCurrentYear = CALCULATE( [Sales Amount], ALLEXCEPT ( Sales, 'Date'[Calendar Year] ))
6.1.9 ALL 函数覆盖现有的筛选器
还是以5.2.5示例为例,原代码为:
Sales Large Amount := CALCULATE ([Sales Amount],FILTER (ALL ( Sales[Quantity], Sales[Net Price] ),Sales[Quantity] * Sales[Net Price] >= 1000)
)
报表中引入了一个切片器,用于选择Net Price的最小值和最大值。如果我们将Net Price 选择范围设为500到3000,可以看到Sales Large Amount
的结果与之前没有任何变化,甚至此筛选区间内某些类别(如 Audio)没有销售记录,但Sales Large Amount
列仍然显示了值。
原因是Sales Large Amount
度量值使用了 ALL 函数,导致计算时会同时忽略Sales[Quantity]
和Sales[Net Price]
上的所有筛选器,包括报表上的任何筛选器。上述操作等价于:
Sales Large Amount :=
CALCULATE (CALCULATE ([Sales Amount],FILTER (ALL ( Sales[Quantity], Sales[Net Price] ),Sales[Quantity] * Sales[Net Price] >= 1000)),'Product'[Category] = "Audio",Sales[Net Price] >= 500
)
从这段代码中您可以看到,最内层的 ALL 函数覆盖了最外层 CALCULATE 函数所创建的的应用于 Sales[Net Price]
列的筛选器。在这种情况下,可以使用 KEEPFILTERS 来避免现有筛选器被覆盖:
Sales Large Amount KeepFilter :=
CALCULATE ([Sales Amount],KEEPFILTERS (FILTER (ALL ( Sales[Quantity], Sales[Net Price] ),Sales[Quantity] * Sales[Net Price] >= 1000))
)
另一种方法是使用表筛选器而不是列筛选器:
Sales Large Amount Table :=
CALCULATE ([Sales Amount],FILTER ( Sales, Sales[Quantity] * Sales[Net Price] >= 1000 )
)
这种方法虽然写法简洁,但可能会导致性能问题或结果不准确(在第14章将讨论其中的细节),因为这么做会让FILTER 函数逐行扫描整个 Sales 表,这在数据量较大时会非常耗时,所以建议 尽量使用列筛选器而不是表筛选器。
6.2 KEEPFILTERS
:迭代表修改器
在之前的5.3.3章节,已经介绍过KEEPFILTERS。严格意义上来说,KEEPFILTERS 不是 CALCULATE 的修改器,而是一个筛选参数的修改器,其工作机制与常规的CALCULATE修改器有本质区别。KEEPFILTERS不改变CALCULATE的整体计算逻辑,而是专门用来调整筛选参数的应用方式——让CALCULATE 中单个列上的筛选器与原有筛选器合并(求交集),而不是覆盖它。
例如,以下代码计算了品牌为 “Contoso” 的销售额,同时保留了其他已存在的筛选器,不改变整个 CALCULATE 函数的语义。
Contoso Sales :=
CALCULATE ( [Sales Amount], KEEPFILTERS ( 'Product'[Brand] = "Contoso" ) )
KEEPFILTERS
也可以作为迭代表的修改器使用,例如在 SUMX
函数中:
ColorBrandSales :=
SUMX (KEEPFILTERS ( ALL ( 'Product'[Color], 'Product'[Brand] ) ),[Sales Amount]
)
在5.4.4.2章节中介绍过,迭代函数内部引用度量值时,会自动触发隐式的 CALCULATE 调用,继而触发隐式上下文转换,所以以上代码可改写为:
ColorBrandSales :=
SUMX (KEEPFILTERS ( ALL ( 'Product'[Color], 'Product'[Brand] ) ),CALCULATE (SUMX ( Sales, Sales[Quantity] * Sales[Net Price] ))
)
通常,上下文转换会将行上下文转为筛选上下文,CALCULATE的同列筛选会覆盖原有的ALL 函数中的筛选器(Color 和 Brand)。但由于使用KEEPFILTERS
函数包裹了迭代的表,DAX 会强制让上下文转换后的筛选器与外部筛选器求交集(AND 运算),而不是覆盖它。
在迭代中使用 KEEPFILTERS 作为顶级函数是不常见的,我们将在第 10 章后面介绍这种高级用法的一些例子。
见本文3.3章节
6.3 USERELATIONSHIP
6.3.1 基本用法
USERELATIONSHIP
是一个用于激活非活动关系的修改器。在数据模型中,两个表之间可以存在多个关系,但只有一个可以是活动的。USERELATIONSHIP
允许你在计算过程中临时激活一个非活动关系,从而改变数据的筛选逻辑。
假设你有一个 Sales
表,其中包含订单日期(Order Date
)和交付日期(Delivery Date
)。平时你基于订单日期进行常规分析,但在某些情况下,需要基于交付日期进行计算。你可以在 Sales
和 Date
表之间创建两个关系:一个基于订单日期(活动关系),另一个基于交付日期(非活动关系)。然后,在公式中使用 USERELATIONSHIP
激活非活动关系:
Delivered Amount :=
CALCULATE ([Sales Amount],USERELATIONSHIP ( Sales[Delivery Date], 'Date'[Date] )
)
在这个例子中,USERELATIONSHIP
激活了 Sales[Delivery Date]
和 Date[Date]
之间的关系,从而允许基于交付日期计算销售额。下图显示了基于 Order Date 的 Sales Amount 与新的 Delivered Amount 度量值之间的差异:
6.3.2 注意事项
USERELATIONSHIP
激活的关系是基于表引用定义的,而不是在调用RELATED
或其他关系函数时定义的,下面举例说明。假设要计算所有在 2007 年实现的销售额(以发货日期为准),有几种不同的写法:
-
在
CALCULATE
表达式中使用RELATED
函数:这种方式公式将直接报错Delivered Amount 2007 v1 := CALCULATE ([Sales Amount],FILTER (Sales,CALCULATE (RELATED ( 'Date'[Calendar Year] ),USERELATIONSHIP ( Sales[Delivery Date], 'Date'[Date] )) = "CY 2007") )
RELATED
函数只能在行上下文中工作。此式中,FILTER
是迭代计算的,内层的CALCULATE
函数会将行上下文转换为筛选上下文,导致RELATED
因缺乏行上下文无法正确关联关系而报错。因此,在CALCULATE
表达式中,根本不能使用RELATED
函数。 -
嵌套调用,在内层执行USERELATIONSHIP
Delivered Amount 2007 v2 := CALCULATE ([Sales Amount],CALCULATETABLE (FILTER ( Sales, RELATED ( 'Date'[Calendar Year] ) = "CY 2007" ),USERELATIONSHIP ( Sales[Delivery Date], 'Date'[Date] )) )
此公式可以正常书写,因为没有在迭代函数
FILTER
中使用CALCULATE
或CALCULATETABLE
。但这个公式的逻辑是错误的。原因在于USERELATIONSHIP
的作用域只限于CALCULATETABLE
内部。CALCULATETABLE
内部USERELATIONSHIP
临时激活Sales[Delivery Date]
与Date[Date]
的关系,覆盖默认的订单日期关系。FILTER
在修改后的关系下迭代Sales
表,RELATED
函数通过Delivery Date
查找'Date'[Calendar Year]
,筛选出Delivery Date
对应年份为2007
的行。
- 外部
CALCULATE
CALCULATETABLE
返回的是一个表(扩展表),其中包含满足Delivery Date = 2007
的所有销售记录。USERELATIONSHIP
的作用仅限于CALCULATETABLE
内部。当退出CALCULATETABLE
后,关系会自动恢复为默认的Sales[Order Date]
→Date[Date]
。
- 最终结果:最终计算时,
[Sales Amount]
会同时受到外层未修改的关系和内层筛选的表的影响,导致逻辑混乱。实际筛选的是Order Date
和Delivery Date
均为 2007 年且日期相同的订单(通常无意义)。
-
非嵌套调用:
Delivered Amount 2007 v2.1 := CALCULATE ([Sales Amount],FILTER ( Sales, RELATED ( 'Date'[Calendar Year] ) = "CY 2007" ),USERELATIONSHIP ( Sales[Delivery Date], 'Date'[Date] ) )
该度量值计算结果与 Delivered Amount 2007 v2 完全相同,而且语义也相同。FILTER 在 V2.1 中是筛选参数,按理说应该晚于 USERELATIONSHIP(修改器) 执行。但
RELATED
是行上下文函数,它可能在FILTER
执行时就已经锁定了关系状态(基于原始关系)。最终计算步骤为:- 首先计算
USERELATIONSHIP ( )
,将关系修改为发货日期; - 其次计算
FILTER ( )
,但在计算该内容时却使用了未修改关系之前的状态,也就是订单日期;由于返回了 Sales,仍然是扩展表; - 在第 1 步关系修改为发货日期后,第 2 步得到的全部以订单日期为准的 Sales 扩展表,这将筛选出订单日期与发货日期均为 2007 年且日期相同的订单。
- 总结:v2 是按发货日期的扩展表与订单日期再求交集,而 v2.1 是按订单日期的扩展表与发货日期再求交集。
- v2.1 版本的错误更加隐蔽而复杂,它首先更改了关系,而在 FILTER 计算时却使用了原关系,计算完成后又由于扩展表与修改了的关系进行交集。
- 首先计算
-
依赖于默认的筛选上下文:使用默认筛选上下文传播,而不是依赖于 RELATED,此种方式是最优的。
Delivered Amount 2007 v3 := CALCULATE ([Sales Amount],'Date'[Calendar Year] = "CY 2007",USERELATIONSHIP ( Sales[Delivery Date], 'Date'[Date] ) )
总结:
RELATED 是一个 行上下文函数,它依赖当前行的活跃关系来查找相关表的数据。RELATED 的核心用途是 在计算表达式中返回相关表的值,而不是用于筛选条件。比如:
Sales[Delivery Year] := RELATED ( 'Date'[Calendar Year] ) -- 基于 Order Date 返回 Delivery Date 的年份
在实际使用中,应该尽量避免同时在CALCULATE在使用多个不同类型的参数,并使用嵌套以体现明确的逻辑。
6.3.3 CALCULATE中的计算优先级
在 CALCULATE 中,由于修改器(例如USERELATIONSHIP,ALL,REMOVEFILTERS
)优先于所有筛选参数执行,所以无论筛选参数在 CALCULATE 中的声明顺序如何,USERELATIONSHIP
总是先于筛选参数生效(先修改或激活模型关系,再计算筛选上下文)。
在Delivered Amount 2007 v3中,即使 Calendar Year 筛选器写在 USERELATIONSHIP 之前,仍会基于 Delivery Date 的关系计算,而非默认的 Order Date。
另外,USERELATIONSHIP
并不引入任何筛选器,它只是改变关系的激活状态。如果仔细查看 Delivered Amount in 2007 v3 的定义,可能会注意到筛选参数在 2007 年应用了一个筛选器,但是它没有指出要使用哪个关系。是使用 Order Date 还是 Delivery Date?要使用的关系由 USERELATIONSHIP 定义。因此,CALCULATE 首先通过激活关系来修改模型的结构,然后才应用筛选参数。所以说 CALCULATE 修改器总是应用于任何筛选参数之前,筛选参数作用于修改后的模型。
6.4 CROSSFILTER
CROSSFILTER 函数可临时修改两个表之间关系及交叉筛选方向,其语法为:
CROSSFILTER(<columnName1>, <columnName2>, <direction>)
columnName1
和columnName2
:这两个参数代表模型关系两端的列名,必须使用标准DAX语法和完全限定的列名。如果参数顺序相反,函数会自动交换它们。这两个参数不能是表达式。direction
:指定交叉筛选的方向,可以是:None
:此关系不会发生交叉筛选。Both
:双向筛选OneWay
:在一侧或关系查找端的筛选器会筛选另一侧,如果不清楚哪个是查找端,请用下面两个选项。OneWay_LeftFiltersRight
:columnName1
筛选columnName2
OneWay_RightFiltersLeft
:columnName2
筛选columnName1
例如,下面的度量值是在激活 Sales 和 Product 之间的双向关系后,计算产品不重复颜色的数量:
NumOfColors :=
CALCULATE (DISTINCTCOUNT ( 'Product'[Color] ),CROSSFILTER ( Sales[ProductKey], 'Product'[ProductKey], BOTH )
)
与 USERELATIONSHIP 一样,CROSSFILTER 本身并不引入筛选器,而只改变关系的结构。
七、 CALCULATE 规则
7.1 CALCULATE运行过程
- 初始化原始上下文:CALCULATE 在计算上下文中执行,原始计算上下文中包含一个筛选上下文,可能包含一个或多个行上下文;
- 创建新的上下文:复制原始筛选上下文为新筛选上下文
- 上下文转换:如果有行上下文,则进行上下文转换,将当前行上下文的所有列及其当前值添加为筛选参数,此时只有一个筛选上下文。由于这个转换涉及到原始行上下文中所有的列,因此并不能保证转换出的新的筛选上下文的筛选作用与原行上下文完全一致。
- 修改器生效: CALCULATE 修改器的计算发生在上下文转换之后,因此这就有机会清除上下文转换的影响。
- 计算原始筛选:在原始筛选上下文中计算 CALCULATE 的第2到N位的各个显式筛选参数
- 应用筛选:CALCULATE 对不同列上的筛选器则进行合并,对会同一列上筛选器进行覆盖(新上下文覆盖原上下文,内层上下文覆盖外层上下文),除非使用 KEEPFILTERS 进行标记。各个筛选器结果的交集构成新的筛选上下文
- 计算结果:CALCULATE 在新的筛选上下文进行计算
当原始上下文有行上下文时,CALCULATE 会执行上下文转换添加隐式筛选参数,若行上下文是由带有 KEEPFILTERS 标记的迭代表产生,则上下文转换后继续带有该标记(见6.2)。
7.2 参数分类与计算顺序
CALCULATE 的参数分为三类:
- 要计算的 DAX 表达式,最后计算
- 操作原始筛选上下文的显式筛选参数,每个筛选参数都可以带有修改器,例如 KEEPFILTERS;
- CALCULATE 修改器,用于清除部分筛选器或更改关系架构。
执行顺序:总体执行顺序为CALCULATE 修改器→显式筛选参数→计算表达式。在同一个 CALCULATE 中,筛选器之间的顺序并不重要(见3.2)。
CALCULATE 的每次执行都由 DAX 引擎在专门的计算平台完成,底层数据模型会被连成一片。CALCULATE 构建筛选上下文是分层次的,其计算流程并非每次都会全部执行,这取决于是否触发了所有的条件。
因此,一个好的习惯是尽量将 CALCULATE 的计算限制在自己可以完全理解的范围,不要试图建立一个复杂的公式并自己为难自己。有关权威文献,可参考《The Logic behind the Magic of DAX Cross Table Filtering》。