上下文转换是 DAX 计值过程中一个非常灵活的部分,灵活的同时意味着复杂,DAX 的大部分复杂性都蕴含于此。上下文转换需要在理解 CALCULATE 函数的基础上学习。
初识上下文转换
在理解 CALCULATE 的行为之后,你知道这个函数在计值过程中会执行一项非常重要的任务:将任何现有的行上下文转换为等效的筛选上下文。这就是我们说的上下文转换。
为了演示该行为,我们创建一个包含 CALCULATE 表达式的计算列。由于计算列总是具有行上下文,因此会触发上下文转换。例如,在产品表中定义一个包含以下 DAX 表达式的计算列:
Product[SumOfUnitPrice] = SUM ( Product[Unit Price] )
公式对所有产品的标价求和。表达式在行上下文中计算,没有筛选上下文,因此它返回表中所有产品的单价之和,而不是正在计值的当前行产品的单价。你可以在下图中看到这种行为。
现在,你可以将表达式稍作修改创建一个新的计算列,加入 CALCULATE:
Product[SumOfUnitPriceCalc] = CALCULATE ( SUM ( Product[UnitPrice] ) )
什么?只有一个参数的 CALCULATE? 筛选器去哪儿了?实际上,我们用的是 CALCULATE 的极简形式。我们之前说过,CALCULATE 惟一的必选参数是第一参数,因此在不使用任何筛选器的情况下调用 CALCULATE 是完全可以的。在这种情况下,CALCULATE 不会使用其他条件更改现有的筛选上下文,它仍然执行你现在正在学习的行为:接受现有的行上下文(如果有的话),并将它们转换为等效的筛选上下文。请注意,所有现有的行上下文都合并到新的筛选上下文中,稍后我们会详细阐述。
在本例中,CALCULATE 查找现有的行上下文,并在产品表上发现一个由计算列定义的正在执行的行上下文。CALCULATE 考虑这个行上下文,并用一个筛选上下文取而代之,该筛选上下文只包含行上下文正在迭代的当前行。我们将此行为称为上下文转换。一般来说,我们将以上过程简述为CALCULATE 执行上下文转换,将所有行上下文合并到一个新的等效筛选上下文中。
在 CALCULATE 内部,表达式 SUM ( Product[Unit Price] )在只包含产品表当前行的筛选上下文中计值,由于 CALCULATE 执行了上下文转换。这一次的结果与产品单价(unit price)相同,如图所示。
当你第一次观察到这种行为,会发现很难理解为何 CALCULATE 要执行上下文转换。一旦开始使用之后你就一定会喜欢上该特性,因为多亏了它你才能创建强大的公式。
通过在产品表定义以下两个新的计算列公式,你可以观察到这种行为:
Product[SalesAmount] = SUM ( Sales[SalesAmount] ) Product[SalesAmountCalc] = CALCULATE ( SUM ( Sales[SalesAmount] ) )
如你所见,SalesAmount 列包含所有销售额的总计,而 SalesAmountCalc 只包含当前产品的销售额。CALCULATE 通过转换产品表的行上下文将筛选器传递到销售表,最终显示了当前产品的销售。
请注意,当 CALCULATE 计算时,所有活动的行上下文都会发生上下文转换。实际上,在不同的表上可能有多个行上下文。例如,如果你在产品表创建计算列,使用 AVERAGEX 迭代客户表,那么有两个行上下文(产品和客户)将发生上下文转换,销售表将接收两个筛选器。考虑以下表达式:
Product[SalesWithSUMX] = AVERAGEX ( Customer, CALCULATE ( SUM ( Sales[SalesAmount] ) ) )
公式计算的是消费者购买该产品的平均花费(不是平均价格,而是总花费的平均值)。CALCULATE 中的 SUM 函数在筛选上下文中计值,它只显示当前客户(由 AVERAGEX 迭代)和当前产品(由计算列迭代)的销售额。记住这个规则有一个简单的方法:在 CALCULATE 中没有行上下文,只存在一个筛选上下文。
理解度量值中的上下文转换
理解上下文的转换非常重要,这是因为 DAX 还有另一个隐藏知识。到目前为止,我们一直使用函数和列来编写 CALCULATE 内部的表达式。但是,你还可以编写调用度量值的表达式。如果从计算列内部调用度量值会发生什么?更一般地说法是,如果从行上下文中调用度量值会发生什么?
作为示例,你可以这样定义一个名为 SumOfSalesAmount 的度量值:
[SumOfSalesAmount] := SUM ( Sales[SalesAmount] )
然后,你可以使用以下更简单的代码定义 SalesWithSUMX 计算列:
Product[SalesWithSUMX] = SUMX ( Customer, CALCULATE ( [SumOfSalesAmount] ) )
自动添加的 CALCULATE
使用 CALCULATE 表明公式发生了上下文转换,问题是,每当你从另一个表达式中调用已定义好的度量值时,DAX 都会自动将度量值封装在 CALCULATE 中。因此,前面的表达式具有与以下表达式相同的行为:
Product[SalesWithSUMX] = SUMX ( Customer, [SumOfSalesAmount] )
这个公式没有显式调用 CALCULATE,不过上下文转换依然在发生,因为DAX 自动为度量值添加了 CALCULATE。
在嵌套中使用完整公式
上下文的自动转换使编写通过迭代执行复杂计算的公式变得容易。话虽如此,你仍然需要一些时间才能熟悉和使用这种技术。例如,如果你只想计算购买金额超过总体平均水平的客户的销售额总和,可以按如下方式编写度量值:
[SalesMoreThanAverage] := VAR AverageSales = AVERAGEX ( Customer, [SumOfSalesAmount] ) RETURN SUMX ( Customer, IF ( [SumOfSalesAmount] > AverageSales, [SumOfSalesAmount] ) )
在前面的代码中,我们使用 SumOfSalesAmount 作为在不同行上下文中计值的度量值。在定义变量时,我们使用它来计算客户销售额的平均值,而在 SUMX 的迭代中,我们使用它来检查当前客户的销售额与之前存储在变量中的平均值之间的关系。
在公式内部调用度量值时,上下文转换会自动发生,无法避免。这意味着在调用度量值时避免上下文转换的唯一方法是展开它的代码。例如,假设你用另一种方法编写了前面的代码。不使用变量,而是定义一个称为 AverageSales 的度量值表示客户的平均销售额,如下面的代码所示:
[AverageSales] := AVERAGEX ( Customer, [SumOfSalesAmount] ) [SalesMoreThanAverage] := SUMX ( Customer, IF ( [SumOfSalesAmount] > [AverageSales], [SumOfSalesAmount] ) )
在突出显示的行中,使用了[AverageSales]计算客户的平均销售额。问题是此时你正在迭代(SUMX)中调用度量值,这会使上下文转换发生。因此,[AverageSales]的结果将不是所有客户的平均销售额,而是你正在迭代的客户的平均销售额。因此,测试总是会失败,度量值返回一个空值,因为 IF 的真值分支永远不会执行。如果想避免上下文转换,你需要将调用的度量值写成完整形式:
[SalesMoreThanAverage] := SUMX ( Customer, IF ( [SumOfSalesAmount] > AVERAGEX ( Customer, [SumOfSalesAmount] ), [SumOfSalesAmount] ) )
使用完整形式后, SalesMoreThanAverage 现在返回正确的结果。此外,值得注意的是,在这种情况下整个公式有两个嵌套的行上下文, 三个度量值调用。其中两个计算由 SUMX 迭代的当前客户的销售额, 另一个 (在 AVERAGEX 内部) 计算由 AVERAGEX 迭代的当前客户的销售额。
理解这种特性之后你才能编写复杂的 DAX 代码来解决特定场景的需求。
触发上下文转换的条件
如果用一句话概况,DAX 中只有 CALCULATE 和 CALCULATETABLE 可以触发上下文转换。但在实际应用中,这句话需要你很好的理解,因为它有很多衍生形式,也就是公式中没有可见的 CALCULATE 函数,但上下文转换依然发生。比如:
- 引用度量值,隐式调用的 CALCULATE
- 部分时间智能函数,FIRSTDATE/LASTDATE、FIRSTNONBLANK/LASTNONBLANK 等,它们在内部使用 CALCULATE 函数。
上下文转换之后究竟有多少可见行?
上下文转换是指将行上下文转换为等效的筛选上下文。这个说法需要进一步作些澄清。
小结
- 上下文转换性能开销比较大。如果迭代具有 10 列和 100 万行的表并使用上下文转换,则 CALCULATE 需要应用 10 个筛选器,总共 100 万次。无论如何,这将是一个缓慢的操作。这并不是说应该避免依赖上下文转换。然而,它确实是 CALCULATE 的一个需要小心使用的特性。
- 上下文转换不仅过滤一行。存在于 CALCULATE 外部的原始行上下文始终标识唯一行,因为行上下文逐行迭代。当通过上下文转换将行上下文转换为筛选上下文时,新创建的筛选上下文将筛选具有相同值集的所有行。因此,您不应该假设上下文转换只创建了一个只有一行的筛选上下文,这一点非常重要,需要仔细体会。
- 上下文转换使用公式中不存在的列。尽管筛选器使用的这些列不可见,但它们仍然是表达式的一部分。这使得任何带有 CALCULATE 的公式都比最初看起来复杂得多。如果使用上下文转换,则表的所有列都是表达式的一部分,作为隐藏的筛选器参数,此行为可能会创建意外的依赖关系。
- 上下文转换从行上下文中创建筛选上下文。您可能还记得这段表述:“行上下文迭代表,而筛选上下文筛选整个模型”。一旦上下文转换将行上下文转换为筛选上下文,它将更改筛选器的性质,不再只迭代一行,而是筛选整个模型;关系成为表达式的一部分。换句话说,发生在一个表上的上下文转换可能会将其筛选效果传递到远离行上下文来源的其他表。
- 只要是存在行上下文的环境,上下文转换就会发生。例如,如果在计算列中使用 CALCULATE,会发生上下文转换。计算列中有一个自动生成的行上下文,这足以使转换发生。
- 上下文转换所有的行上下文。当对多个表执行嵌套迭代时,上下文转换会考虑所有行上下文。它会使所有这些列无效,并为当前由所有活动行上下文迭代的所有列添加筛选器参数。
- 上下文转换使行上下文无效。虽然我们已经多次重复这个概念,但它值得再次引起您的注意。CALCULATE计算的表达式中没有任何有效的外部行上下文。所有外部行上下文都被转换为等效的筛选上下文。
理解上下文转换后的计值顺序
结合目前所学,相信你已经了解下面这两个在产品表中创建的计算列之间的区别:
Product[SumOfUnitPrice] = CALCULATE ( SUM ( Product[Unit Price] ) ) Product[SumOfAllUnitPrice] = CALCULATE ( SUM ( Product[Unit Price] ), ALL ( Product ) )
它们都是计算列,并且都使用了 CALCULATE,因此,两者都发生了上下文转换。
SumOfUnitPrice 应该只包含当前行的单价。然而,SumOfAllUnitPrice 的值是多少?出于直觉,因为有 ALL (Product),所以你很可能会期望它包含所有单价的总和。结果确实如此。然而,如果你遵循我们迄今所描述的规则,会发现这其中似乎还有一些问题。
事实上,ALL (Product)从筛选上下文删除了产品表的所有筛选器。然而,与此同时,上下文转换将筛选产品表,筛选后的产品表只有一行。如果上下文转换的优先级更高,结果应该是当前行的单价,而不是所有产品的单价之和,但事实恰恰相反。
小测试
上下文转换并不是孤立的知识,需要掌握 CALCULATE 函数的计值过程才能正确理解,这里我假定你已经有一定的基础,通过下面三个案例,你可以测试一下自己对上下文转换的理解程度。
案例一
xSUM 是 Table1 的计算列,请思考在下图中它应该返回什么结果?公式是如何计值的?
案例二
xRANK 是 Table2 的计算列,请思考在下图中它应该返回什么结果?公式是如何计值的?
案例文件下载(附结果)
案例三
[Test] 度量值的第 9 行 MAX (‘销售明细'[出库日期] ) 是否被第 3 行 VALUES (‘销售明细'[出库日期] ) 影响,为什么?
Test := MAXX ( VALUES ( '销售明细'[出库日期] ), CALCULATE ( SUM ( '销售明细'[下单数量] ), FILTER ( ALL ( '销售明细'[出库日期] ), '销售明细'[出库日期] = MAX ( '销售明细'[出库日期] ) ) ) )
案例解析
案例一结果
xSum 列每行都等于 20。以表的第一行为例,公式计值过程如下:
案例二结果
xRANK 列从上至下是 2,2,2,1,1,1。以表的第一行为例,公式计值过程如下:
案例三结果
不影响。
其实并不能叫上下文转换,calculate只是将,迭代行数,迭代的当前行,用作隐式筛选器,去对模型,进行了筛选,至于,筛选之后 是多少行,那得看当前迭代的行上下文中,有多少行,是满足筛选器要求的, 然后将这个筛选后的模型,也可叫做 新的筛选上下文, 传递到calculate内部 ,供其 内部的参数再次进行 迭代(再次筛选)/计值 使用。
[SalesMoreThanAverage] :=
SUMX (
Customer,
IF (
[SumOfSalesAmount]
> AVERAGEX (
Customer,
[SumOfSalesAmount]
),
[SumOfSalesAmount]
)
)
老师,AVERAGEX里的度量值,只转换了AVERAGEX迭代Customer时候的行上下文,为什么没转换SUMX迭代Customer的上下文呢?
与之相反
另一个例子当中
Product[SalesWithSUMX] =
AVERAGEX (
Customer,
CALCULATE (
SUM ( Sales[SalesAmount] )
)
)
这个calculate是转换了averagex和计算列 两层行上下文
上下文转换不仅过滤一行。存在于 CALCULATE 外部的原始行上下文始终标识唯一行,因为行上下文逐行迭代。当通过上下文转换将行上下文转换为筛选上下文时,新创建的筛选上下文将筛选具有相同值集的所有行。因此,您不应该假设上下文转换只创建了一个只有一行的筛选上下文,这一点非常重要,需要仔细体会。
——————————————————————————————
请问老师,这里是指上下文转换后筛选器存在多行,还是筛选后的内容存在多行,后者我可以理解,但是前者存在多行相同值的筛选器应该没有什么意义吧?
xSum 列每行都等于 20。以表的第一行为例,公式计值过程如下:
1)FILTER 在初始的计值上下文环境中计算,不受上下文转换影响,得到的结果是 Table1 表 A 列等于 a2 的所有行
2)CALCULATE 将当前行的行上下文转换为等价的筛选上下文,以第一行为例,这个筛选上下文是
3)步骤 1 得到筛选器覆盖步骤 2 的筛选器,得到最终的筛选上下文
4)CALCULATE 第一参数在步骤 3 得到的筛选上下文中计值,结果为 20
————————–
高老师,五一快乐,我被整蒙了,请指点,第一步和第二步能理解,第三步中提到的“步骤1得到筛选器”怎么会是[A=a2,B=b2,Value=20】,我的理解这个筛选器应该是[A=a2],筛选后才得到【A=a2,B=b2,Value=20] 这个 一行三列的表啊,怎么这个表变成了筛选器呢,故最终的筛选器【A=a2,B=b1,Value=10], 结果为空,我的理解错在哪里哦?
没看DAX圣经前 我脑子转的很快 看了之后脑袋浆糊一团 不知所谓
高老师好!案例2的第3步,是RANKX的第三参数吧
老师您好,我建立了一个表
测试表 =
SUMMARIZE(
‘生产成本计算表’,
‘生产成本计算表'[期间名称],
‘生产成本计算表'[生产订单(单号)]
)
然后建立了一个计算列
期初在制计入 = CALCULATE(SUM(‘生产成本计算表'[期初在制记入]))
计算列的计算结果是’生产成本计算表'[期初在制合计数],完全没有考虑测试表中的行上下文。
看了很多遍终于理解了。 深度好文。 我现在倒回去看上一章节,看能不能帮助上一章节理解。
老师,有两个问题:
第一:以X结尾的迭代函数,如果在出现在迭代中,也会和聚合函数一样,无视外层的行上下文吗?
比如在第一个例子中的AVGRAGEX函数,他处在的环境是SUMX的行上下文中。但是SUMX的行上下文并未对AVGEAGEX产生影响。
第二:在第二个例子中,AVGRAGEX未以完整形式展示,而是以度量值的形式展示,根据前面的解释,在度量值中嵌套度量值,相当于隐形调用了CALCULATE,导致引入了上下文。
我的理解正确吗?
老师,能帮忙解释下案例二的计算过程中第2步的列表是怎么得出来的吗?一直没想通。
老师, 在案例二中, 计算列我用如下公式:RANKX(ALL(‘Sheet2′[category]),Sheet2[value]), 为u什么结果全是1啊。第一参数创建的行上下文(a,b,c)是怎么确定value的值的?因为a,b,c都各有两个值,是因为结合了rankx的行上下文来确定的吗?
老师,下面的语言,我认为是返回3,为什么会返回4呢?
老师,再请教一个问题,我有一个表
ID VALUE
A 1
B 3
B 3
C 3
现在我新建一个计算列
P =
CALCULATE(
SUM(‘Table'[VALUE]),
‘Table'[ID] = “B”
)
为什么结果会是
ID P
A 空
B 6
B 6
C 空
xSum 列每行都等于 20。以表的第一行为例,公式计值过程如下:
FILTER 在初始的计值上下文环境中计算,不受上下文转换影响,得到的结果是 Table1 表 A 列等于 a2 的所有行
CALCULATE 将当前行的行上下文转换为等价的筛选上下文,以第一行为例,这个筛选上下文是
步骤 1 得到筛选器覆盖步骤 2 的筛选器,得到最终的筛选上下文
CALCULATE 第一参数在步骤 3 得到的筛选上下文中计值,结果为 20
公式每行重复步骤 1-4,完成计值
老师,为什么度量值 和计算列出来的数值不同
老师,我写了一个度量值,SUMX之和=SUMX(‘销售表’,’销售表'[销售额])
SUMX是迭代函数,创建了行上下文。现在我在报表矩阵中以[商品]列作为行标签,然后发现度量值返回的结果是销售表中苹果、西瓜、香蕉各自的销售额之和。
1.我理解的是:在度量值中,会自动将行上下文转换为筛选上下文,那么当外部上下文(行标签)为苹果时,那么会对SUMX的第一个参数销售表进行筛选,返回所有商品为苹果的销售表,该销售表中除了有商品列,还有姓名列。度量值在对这个销售表进行转换上下文的时候,会把商品和姓名都进行转换,那转换完以后,相当于在所有商品为苹果的销售表又按照当前行的姓名(假如当前行姓名是李四)进行了筛选,那么最后第一个参数销售表应该是包含所有姓名列为李四且商品列为评估的一个表,那第二参数应该是在这个表上逐行进行计值,再求和。
但是为啥实际效果,就是返回所有商品为苹果的销售表,然后第二参数对该表逐行计值再求和。。。。
2.SUMX创建的行上下文转换后,是否SUMX对第一参数的迭代效果也消失了?因为我感觉迭代创建了行上下文,既然行上下文被转换了,那应该也不能迭代了吧
上下文转换使行上下文无效。CALCULATE计算的表达式中没有任何有效的外部行上下文。
————————–
高老师,这句话无法理解,1.能不能举个“CALCULATE计算的表达式中有外部上下文”的例子。2.外部行上下文作何理解。
初学者刚看DAX权威指南,一个问题但困扰我很长时间了,关于calculate的上下文转换,比如如下SUMX和度量值(自带calculate),既然CALCULATE计算的表达式中没有任何有效的外部行上下文。所有外部行上下文都被转换为等效的筛选上下文。那么没有行上下文就没有迭代,那么筛选上下文不就取代了SUMX的行上下文了吗?那为什么结尾为X的这些函数,比如SUMX和average X,还是在迭代呢?到底怎么理解外部行上下文无效的概念以及SUMX还在迭代的逻辑呢?求解!!!
Product[SalesWithSUMX] =
SUMX (
Customer,
[SumOfSalesAmount]
老师好,如果在案例二新建一个度量值=RANKX(ALL(Table2[value]),CALCULATE(SUM(Table2[values]))),然后将该度量值拖入到卡片图中,为什么显示成9?难道所有的排名数相加了?这是什么原理?麻烦老师帮忙解释一下,谢谢!
参考小测试中的案例一建立如下表格:
日期 品类 销量
2022年1月1日 苹果 1
2022年1月2日 苹果 1
2022年1月3日 苹果 2
2022年1月4日 苹果 3
新建计算列=CALCULATE(SUM(Sheet1[销量]),FILTER(VALUES(Sheet1[日期]),’Sheet1′[日期]=dt”2021-1-2″))。显示的结果为什么只有2022年1月2日的“列”=1,其他行中的“列”=空??
日期 品类 销量 列
2022年1月1日 苹果 1 空
2022年1月2日 苹果 1 1
2022年1月3日 苹果 2 空
2022年1月4日 苹果 3 空
麻烦老师帮忙解答一下,谢谢!
高老师,请问一下同样的公式,为什么在customer表中使用sumx,没有发生上下文的转换?