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

理解上下文转换

上下文转换是 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会员可享受全站权益,性价比更高。

 

266
说点什么

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

Context transition happens when CALCULATE (or CALCULATETABLE) is executed inside a row context
看了很多国内老师的关于context transition的讲解,还是没有原作者英文原版解释的清晰,上面的Calculate在行上下文中被执行是关键
CALCULATE triggers context transition.
Context transition transforms any existing row context into an equivalent filter context,
which is then used by CALCULATE to evaluation

First, context transition is invoked by CALCULATE. If CALCULATE is not present in your code, then there is no context transition.
Second, context transition requires the presence of a row context. If there is no row context, then no context transition happens, because there would be no row context to transform. The row context can be explicitly introduced with an iterator, but it might also be present because you are writing code in a calculated column – where by default, there is a row context.

成员
1750032

案列2,看的我已经要疯了,看了好多遍,把评论也看了, 还是懵逼.哎,过2天继续理解

成员
1750032

我下载的示列文件,怎么没Customer表?谁给我一个完整的示列文件呀?1750032@qq.com

成员
159****3774

案例1理解,请老师指正

新建列本身就是行上下文,要依次行往下计算。

xSUM=
calculate(
SUM(Table1[value]),FILTER(‘Table1’,’Table1′[A]=”a2″))

步骤如下
1. 先执行新建列第一行,calculate把’Table1′[A]=”a2″ 转化为 filter context
2. 然后, 原始数据集是table1,然后按行依次查看A列=“a2”的集合,最终得到《A=a2,B=b2,value=20》,
3. step 2中的集合作为return view,新增一个列x=table1【value】=20,然后sum(x)=20
4.开始做新建列第二行,开始重复步骤2,3
5.当前行到新建列第三行,开始重复步骤2,3
….依次类推
所以结果都是20.

成员
159****3774

CALCULATE 将这两种行上下文转换成筛选上下文,因为 RANKX 第一参数提供的行上下文位于内层,转换后会替换掉同一列(value 列)的来自外部计算列的筛选条件
calculate的行上下文是什么样子?
RANKX的第一参数的行上下文又是什么样子?为什么会替换掉同一列的外部计算列?外部计算列是在哪里标明?
老师能说清楚点吗?

游客
芒果

在突出显示的行中,使用了[AverageSales]计算客户的平均销售额。问题是此时你正在迭代(SUMX)中调用度量值,这会使上下文转换发生。因此,[AverageSales]的结果将不是所有客户的平均销售额,而是你正在迭代的客户的平均销售额。因此,测试总是会失败,度量值返回一个空值,因为 IF 的真值分支永远不会执行。如果想避免上下文转换,你需要将调用的度量值写成完整形式:

[SalesMoreThanAverage] :=
SUMX (
Customer,
IF (
[SumOfSalesAmount]
> AVERAGEX (
Customer,
[SumOfSalesAmount]
),
[SumOfSalesAmount]
)
)
使用完整形式后, SalesMoreThanAverage 现在返回正确的结果。此外,值得注意的是,在这种情况下整个公式有两个嵌套的行上下文, 三个度量值调用。其中两个计算由 SUMX 迭代的当前客户的销售额, 另一个 (在 AVERAGEX 内部) 计算由 AVERAGEX 迭代的当前

—————————–
前面这个代码描述的上下文的转换,自相矛盾。本来你前面说不应该按照当前客户的销售额进行计算,你修改后的代码后面的这段话,不就是按照当前客户来计算的吗?没有看明白。

成员
139****6804

为什么我下载的文件里没有customer表

成员
185****5613

为什么contoso文件显示没有权限下载

成员
139****3194

1CALCULATE 将当前行的行上下文转换为等价的筛选上下文,以第一行为例,这个筛选上下文是
2.FILTER 在初始的计值上下文环境中计算,不受步骤 1 生成的筛选上下文影响,得到的结果是 Table1 表 A 列

老师,按照计值流,第2步应放在第1步吧?

成员
139****3194

[SalesMoreThanAverage] :=
SUMX (
Customer,
IF (
[SumOfSalesAmount]
> AVERAGEX (
Customer,
[SumOfSalesAmount]
),
[SumOfSalesAmount]
)
)
老师聚合函数可以被筛选上下文筛选,为什么不能被行上下文转换的筛选上下文筛选,它也是筛选上下文有筛选作用啊?

成员
139****3194

Product[SalesAmountCalc] = CALCULATE ( SUM ( Sales[SalesAmount] ) ),添加计算列,外部筛选上下文为整张表,cal内部没有任何筛选器参数,根据cal计值流,这里还要发生第一步吗:初始上下文筛选内参,作用后得什么呢?请高老师解疑,谢谢

成员
139****3194

“公式第九行的 MAX() 在调用 CALCULATE 的外部环境中计值,第三行的 VALUES() 在此环境中提供行上下文,而 MAX 忽略行上下文”,
老师,这里的values(‘销售明细'[出库日期] ),虽然经maxx产生迭代,但行上下文遇到cal转换为筛选上下文,为什么不能作为cal的筛选器的外部环境,它不是还在外部吗?
还是说它发生行上下文转换为筛选上下文,已经进入cal内部,等待计算cal的第一参数,在计算前,可能会受到内部筛选器的影响,同列覆盖,不同列,转换后的筛选上下文直接参与计算cal的第一参数?而cal的内部筛选器在初始筛选环境中计算,所以行转换的筛选上下文不属于max的初始环境。

DAX 圣经

导读

初识 DAX

DAX 基础知识

DAX 原理

DAX 高级原理

基础函数类型

迭代函数

CALCULATE 函数

CALCULATE 调节器

基础表函数

条件判断函数

查找匹配函数

时间智能函数

统计类函数

投影函数

分组/连接函数

集合函数

其他函数