从数据到信息
从信息到洞察

理解上下文转换

上下文转换是 DAX 计值过程中一个非常灵活的部分,灵活的同时意味着复杂,DAX 的大部分复杂性都蕴含于此。上下文转换需要在理解 CALCULATE 函数的基础上学习。

初识上下文转换

在理解 CALCULATE 的行为之后,你知道这个函数在计值过程中会执行一项非常重要的任务:将任何现有的行上下文转换为等效的筛选上下文。这就是我们说的上下文转换。

为了演示该行为,我们创建一个包含 CALCULATE 表达式的计算列。由于计算列总是具有行上下文,因此会触发上下文转换。例如,在产品表中定义一个包含以下 DAX 表达式的计算列:

Product[SumOfUnitPrice] = SUM ( Product[Unit Price] )

公式对所有产品的标价求和。表达式在行上下文中计算,没有筛选上下文,因此它返回表中所有产品的单价之和,而不是正在计值的当前行产品的单价。你可以在下图中看到这种行为。

SumOfUnitPrice 在计算列中计值,返回所有产品单价的总和

现在,你可以将表达式稍作修改创建一个新的计算列,加入 CALCULATE

Product[SumOfUnitPriceCalc] = CALCULATE ( SUM ( Product[UnitPrice] ) )

什么?只有一个参数的 CALCULATE? 筛选器去哪儿了?实际上,我们用的是 CALCULATE 的极简形式。我们之前说过,CALCULATE 惟一的必选参数是第一参数,因此在不使用任何筛选器的情况下调用 CALCULATE 是完全可以的。在这种情况下,CALCULATE 不会使用其他条件更改现有的筛选上下文,它仍然执行你现在正在学习的行为:接受现有的行上下文(如果有的话),并将它们转换为等效的筛选上下文。请注意,所有现有的行上下文都合并到新的筛选上下文中,稍后我们会详细阐述。

在本例中,CALCULATE 查找现有的行上下文,并在产品表上发现一个由计算列定义的正在执行的行上下文。CALCULATE 考虑这个行上下文,并用一个筛选上下文取而代之,该筛选上下文只包含行上下文正在迭代的当前行。我们将此行为称为上下文转换。一般来说,我们将以上过程简述为CALCULATE 执行上下文转换,将所有行上下文合并到一个新的等效筛选上下文中

在 CALCULATE 内部,表达式 SUM ( Product[Unit Price] )在只包含产品表当前行的筛选上下文中计值,由于 CALCULATE 执行了上下文转换。这一次的结果与产品单价(unit price)相同,如图所示。

通过使用 CALCULATE,行上下文被转换为筛选上下文,改变了结果

当你第一次观察到这种行为,会发现很难理解为何 CALCULATE 要执行上下文转换。一旦开始使用之后你就一定会喜欢上该特性,因为多亏了它你才能创建强大的公式。

此外,上下文转换还有另一个非常重要的作用。你可能还记得,筛选上下文和行上下文以不同方式在关系中运行:行上下文不会自动沿着关系方向传递,而筛选上下文从关系的“一”端传递到“多”端。因此,当发生上下文转换时,筛选上下文会自动传递到相关的表。

通过在产品表定义以下两个新的计算列公式,你可以观察到这种行为:

Product[SalesAmount] = SUM ( Sales[SalesAmount] )

Product[SalesAmountCalc] = CALCULATE ( SUM ( Sales[SalesAmount] ) )

由 CALCULATE 引发的上下文转换影响了对相关表的筛选

如你所见,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

这就是为什么编写代码时要从写法上区分列和度量值的原因,我们遵循的书写标准是对度量值使用不完全限定名,列使用完全限定名。也就是避免将表名放在度量值的前面,但始终在列前面加上表名。
实际上,在前面的公式中,在 SumOfSalesAmount 之前没有表名说明 SumOfSalesAmount 是一个度量值,因此,你知道发生了上下文转换。

在嵌套中使用完整公式

上下文的自动转换使编写通过迭代执行复杂计算的公式变得容易。话虽如此,你仍然需要一些时间才能熟悉和使用这种技术。例如,如果你只想计算购买金额超过总体平均水平的客户的销售额总和,可以按如下方式编写度量值:

[SalesMoreThanAverage] :=
VAR AverageSales =
    AVERAGEX (
        Customer,
        [SumOfSalesAmount]
    )
RETURN
    SUMX (
        Customer,
        IF (
            [SumOfSalesAmount] > AverageSales,
            [SumOfSalesAmount]
        )
    )

在前面的代码中,我们使用 SumOfSalesAmount 作为在不同行上下文中计值的度量值。在定义变量时,我们使用它来计算客户销售额的平均值,而在 SUMX 的迭代中,我们使用它来检查当前客户的销售额与之前存储在变量中的平均值之间的关系。

基于 VAR 的语法更易于阅读和维护(公式的计算也可能更快)。然而,本质在于理解不同语法背后不同的公式计值流,可以不用 VAR,也无论你使用的是哪种 DAX 版本。如果没有真正理解和掌握这种上下文自动转换的机制,你可能花了大量时间阅读公式,但依然无法理解它的计算结果。

在公式内部调用度量值时,上下文转换会自动发生,无法避免。这意味着在调用度量值时避免上下文转换的唯一方法是展开它的代码。例如,假设你用另一种方法编写了前面的代码。不使用变量,而是定义一个称为 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 函数。

上下文转换之后究竟有多少可见行?

上下文转换是指将行上下文转换为等效的筛选上下文。这个说法需要进一步作些澄清。

本文隐藏内容查看价格为5G币,请先
单独购买的内容长期有效,不受时间限制(购买前先刷新当前页面)。加入VIP会员可享受全站权益,性价比更高。

小结

  • 上下文转换性能开销比较大如果迭代具有 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)从筛选上下文删除了产品表的所有筛选器。然而,与此同时,上下文转换将筛选产品表,筛选后的产品表只有一行。如果上下文转换的优先级更高,结果应该是当前行的单价,而不是所有产品的单价之和,但事实恰恰相反。

本文隐藏内容查看价格为5G币,请先
单独购买的内容长期有效,不受时间限制(购买前先刷新当前页面)。加入VIP会员可享受全站权益,性价比更高。

小测试

上下文转换并不是孤立的知识,需要掌握 CALCULATE 函数的计值过程才能正确理解,这里我假定你已经有一定的基础,通过下面三个案例,你可以测试一下自己对上下文转换的理解程度。

案例一

xSUM 是 Table1 的计算列,请思考在下图中它应该返回什么结果?公式是如何计值的?

当前表为 Table1

案例二

xRANK 是 Table2 的计算列,请思考在下图中它应该返回什么结果?公式是如何计值的?

当前表为 Table2

案例文件下载(附结果)

下载链接

案例三

[Test] 度量值的第 9 行 MAX (‘销售明细'[出库日期] ) 是否被第 3 行 VALUES (‘销售明细'[出库日期] ) 影响,为什么?

Test :=
MAXX (
    VALUES ( '销售明细'[出库日期] ),
    CALCULATE (
        SUM ( '销售明细'[下单数量] ),
        FILTER (
            ALL ( '销售明细'[出库日期] ),
            '销售明细'[出库日期]
                = MAX ( '销售明细'[出库日期] )
        )
    )
)

案例解析

案例一结果

xSum 列每行都等于 20。以表的第一行为例,公式计值过程如下:

本文隐藏内容查看价格为5G币,请先
单独购买的内容长期有效,不受时间限制(购买前先刷新当前页面)。加入VIP会员可享受全站权益,性价比更高。

案例二结果

xRANK 列从上至下是 2,2,2,1,1,1。以表的第一行为例,公式计值过程如下:

本文隐藏内容查看价格为5G币,请先
单独购买的内容长期有效,不受时间限制(购买前先刷新当前页面)。加入VIP会员可享受全站权益,性价比更高。

案例三结果

不影响。

本文隐藏内容查看价格为5G币,请先
单独购买的内容长期有效,不受时间限制(购买前先刷新当前页面)。加入VIP会员可享受全站权益,性价比更高。

 

305
说点什么

1000
 
鼓掌微笑开心憧憬爱你色并不觉得吃瓜doge二哈喵喵思考笑哭捂脸悲伤大哭抓狂汗偷笑打脸捂眼黑线问号晕拜拜闭嘴衰咒骂ok作揖
77 评论数
249 被回复的评论
55 订阅评论的人数
 
查看最近回复
查看最热评论
  订阅本文评论  
最新 最旧 得票最多
提醒
成员
18718594655

没看DAX圣经前 我脑子转的很快 看了之后脑袋浆糊一团 不知所谓

成员
188****1692

高老师好!案例2的第3步,是RANKX的第三参数吧

WWW.png
成员
hellolsdp

老师您好,我建立了一个表
测试表 =
SUMMARIZE(
‘生产成本计算表’,
‘生产成本计算表'[期间名称],
‘生产成本计算表'[生产订单(单号)]
)
然后建立了一个计算列
期初在制计入 = CALCULATE(SUM(‘生产成本计算表'[期初在制记入]))
计算列的计算结果是’生产成本计算表'[期初在制合计数],完全没有考虑测试表中的行上下文。

游客
jeremy

看了很多遍终于理解了。 深度好文。 我现在倒回去看上一章节,看能不能帮助上一章节理解。

成员
你猜

老师,有两个问题:
第一:以X结尾的迭代函数,如果在出现在迭代中,也会和聚合函数一样,无视外层的行上下文吗?
比如在第一个例子中的AVGRAGEX函数,他处在的环境是SUMX的行上下文中。但是SUMX的行上下文并未对AVGEAGEX产生影响。
第二:在第二个例子中,AVGRAGEX未以完整形式展示,而是以度量值的形式展示,根据前面的解释,在度量值中嵌套度量值,相当于隐形调用了CALCULATE,导致引入了上下文。
我的理解正确吗?

AVG2.png
AVG.png
成员
此夕洛溯

老师,能帮忙解释下案例二的计算过程中第2步的列表是怎么得出来的吗?一直没想通。

游客
jcjc

老师, 在案例二中, 计算列我用如下公式:RANKX(ALL(‘Sheet2′[category]),Sheet2[value]), 为u什么结果全是1啊。第一参数创建的行上下文(a,b,c)是怎么确定value的值的?因为a,b,c都各有两个值,是因为结合了rankx的行上下文来确定的吗?

成员
你猜

老师,下面的语言,我认为是返回3,为什么会返回4呢?

rankx.png
成员
zjhred

老师,再请教一个问题,我有一个表
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 空

成员
zjhred

老师,请教一个问题。我有一个表
ID VALUE
A 1
B 2
C 3
现在我新建一个计算列
P =
CALCULATE (
SUM(‘Table'[VALUE]),
ALL(‘Table'[ID])
)
为什么结果会是
ID P
A 1
B 6
C 6

成员
Clarlechen

xSum 列每行都等于 20。以表的第一行为例,公式计值过程如下:

FILTER 在初始的计值上下文环境中计算,不受上下文转换影响,得到的结果是 Table1 表 A 列等于 a2 的所有行
CALCULATE 将当前行的行上下文转换为等价的筛选上下文,以第一行为例,这个筛选上下文是
步骤 1 得到筛选器覆盖步骤 2 的筛选器,得到最终的筛选上下文
CALCULATE 第一参数在步骤 3 得到的筛选上下文中计值,结果为 20
公式每行重复步骤 1-4,完成计值

老师,为什么度量值 和计算列出来的数值不同

成员
做一名学霸

老师,我写了一个度量值,SUMX之和=SUMX(‘销售表’,’销售表'[销售额])

SUMX是迭代函数,创建了行上下文。现在我在报表矩阵中以[商品]列作为行标签,然后发现度量值返回的结果是销售表中苹果、西瓜、香蕉各自的销售额之和。

1.我理解的是:在度量值中,会自动将行上下文转换为筛选上下文,那么当外部上下文(行标签)为苹果时,那么会对SUMX的第一个参数销售表进行筛选,返回所有商品为苹果的销售表,该销售表中除了有商品列,还有姓名列。度量值在对这个销售表进行转换上下文的时候,会把商品和姓名都进行转换,那转换完以后,相当于在所有商品为苹果的销售表又按照当前行的姓名(假如当前行姓名是李四)进行了筛选,那么最后第一个参数销售表应该是包含所有姓名列为李四且商品列为评估的一个表,那第二参数应该是在这个表上逐行进行计值,再求和。
但是为啥实际效果,就是返回所有商品为苹果的销售表,然后第二参数对该表逐行计值再求和。。。。

2.SUMX创建的行上下文转换后,是否SUMX对第一参数的迭代效果也消失了?因为我感觉迭代创建了行上下文,既然行上下文被转换了,那应该也不能迭代了吧 捂脸

表1.jpg
源表.jpg
成员
151****9937

上下文转换使行上下文无效。CALCULATE计算的表达式中没有任何有效的外部行上下文。

————————–
高老师,这句话无法理解,1.能不能举个“CALCULATE计算的表达式中有外部上下文”的例子。2.外部行上下文作何理解。

成员
TEAR313

初学者刚看DAX权威指南,一个问题但困扰我很长时间了,关于calculate的上下文转换,比如如下SUMX和度量值(自带calculate),既然CALCULATE计算的表达式中没有任何有效的外部行上下文。所有外部行上下文都被转换为等效的筛选上下文。那么没有行上下文就没有迭代,那么筛选上下文不就取代了SUMX的行上下文了吗?那为什么结尾为X的这些函数,比如SUMX和average X,还是在迭代呢?到底怎么理解外部行上下文无效的概念以及SUMX还在迭代的逻辑呢?求解!!!

Product[SalesWithSUMX] =
SUMX (
Customer,
[SumOfSalesAmount]

游客
oz

老师好,如果在案例二新建一个度量值=RANKX(ALL(Table2[value]),CALCULATE(SUM(Table2[values]))),然后将该度量值拖入到卡片图中,为什么显示成9?难道所有的排名数相加了?这是什么原理?麻烦老师帮忙解释一下,谢谢!

游客
oz

参考小测试中的案例一建立如下表格:
日期 品类 销量
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 空
麻烦老师帮忙解答一下,谢谢!

成员
Daniel

高老师,请问一下同样的公式,为什么在customer表中使用sumx,没有发生上下文的转换?

微信截图_20220805180448.png
微信截图_20220805180336.png
成员
839838408

老师您的作品真是常看常新,我又有一个问题关于如图两个度量值。
在文章中您提到:左边是错误写法,右边是正确写法。愿意在于“如果想避免上下文转换,你需要将调用的度量值写成完整形式”。
但是右边的度量值引用了度量值SumOfSalesAmount。
我的问题是:同样都是调用度量值,调用[AverageSales]是错误,而[SumOfSalesAmount]是对的?难道不是“在公式内部调用度量值时,上下文转换会自动发生,无法避免”吗?
感谢老师!

Capture.PNG
成员
839838408

老师好:在我做的测试中有一个表’Test’,计算列Column。分别在原表、新增一个列(非计算列),新增记录之后,出现了如图三个结果。有两个问题如图所示,谢谢老师。

Capture.PNG
成员
暮色

老师帮忙看下这个问题 这个多层行上下文转换 是先转哪个??

1657007466151.jpg
DAX 圣经

导读

初识 DAX

DAX 基础知识

DAX 原理

DAX 高级原理

基础函数类型

迭代函数

CALCULATE 函数

CALCULATE 调节器

基础表函数

条件判断函数

查找匹配函数

时间智能函数

统计类函数

投影函数

分组/连接函数

集合函数

其他函数