理解上下文转换

本文翻译自Marco Russo & Alberto Ferrari的文章—《Understanding Context Transition》来源:SQLBI 在DAX中,上下文转换是指将行上下文转换为等效的筛选上下文,这是由CALCULATE和CALCULATETABLE执行的。一旦你理解了行上下文和筛选上下文,管理这种行为是学习DAX的下一步。本文提供了上下文转换的基础知识。

上下文转换是CALCULATE和CALCULATETABLE在定义新的筛选上下文时执行的操作,在该上下文中评估其表达式。上下文转换的正式定义很简单,但它隐藏了一些复杂性。在下面的描述中,您将看到基于CALCULATE的示例,但所有这些概念也可以应用于CALCULATETABLE。

CALCULATE函数会将其所有的现有行上下文转换为等效的筛选上下文,然后再将其筛选参数应用于原始筛选上下文。这是在创建新的筛选上下文以执行其表达式参数时发生的。

通过实例观察上下文转换比通过理论学习更容易。例如,考虑一个只包含产品和销售的数据模型,该模型基于ProductKey建立关系:


你可以在Product中创建一个计算列:

Product[SumOfUnitPrice]=SUM(Product[Unit Price])
作为一个计算列,它是在行上下文中计算的。然而,由于SUM计算当前筛选上下文中可见的所有单价的总和,因此结果是每个产品的单价总和来自整个表。在计算列中没有筛选上下文,只有行上下文。

许多DAX新手错误地认为,计算列的结果应该仅仅是当前行中的单价值。一旦他们掌握了行和筛选上下文,这种行为就变得非常自然。如果你使用以下代码(包括调用CALCULATE)创建一个列,会发生什么?
Product[CalcSumOfUnitPrice]=CALCULATE(SUM(Product[Unit Price]))
这一次,CALCULATE围绕着SUM,因此SUM在不同的筛选器上下文中执行。因为CALCULATE没有过滤器参数,所以它的唯一效果是上下文转换。包含单行的行上下文被转换为包含同一行的筛选器上下文。此时,SUM运行到具有单行的过滤器上下文中,并仅返回该行的Unit Price值:

在使用SUM对Sales列求和时,你可以观察到同样的行为,就像在下面两个计算列中一样:

Product[SumOfSalesQuantity] = SUM ( Sales[Quantity] )
Product[CalcSumOfSalesQuantity] = CALCULATE ( SUM ( Sales[Quantity] ) )

第一个计算列返回Sales[Quantity]的总和,因为没有活动的过滤器上下文,而带有CALCULATE的列只返回当前产品的Sales[Quantity]的总和,因为包含当前产品的过滤器上下文由于两个表之间的关系而自动传播到Sales:

由上下文转换引入的筛选上下文和行上下文之间存在很大的差异。事实上,CALCULATE生成的筛选上下文会在表的所有列上放置一个筛选器,以标识单行,而不是在其行号上。因此,如果你在包含重复行的表中使用上下文转换,CALCULATE生成的筛选上下文将包含所有重复项。因此,当且仅当表中不包含任何重复行时,依靠上下文转换来筛选单行才是安全的。当然,如果表有一个主键保证没有重复,那么这一点也同样适用。

重要的是要注意,上下文转换发生在CALCULATE中进一步筛选之前。因此,CALCULATE中的筛选器可能会覆盖由上下文转换产生的筛选器。在下面的例子中,产品只有三列:产品密钥、单价和颜色。如果你使用以下代码定义了一个计算列:

Product[SumOfUnitPriceAllProductKey]=
CALCULATE (
SUM ( Product[Unit Price] ),
ALL ( Product[ProductKey] )
)

ALL从ProductKey中移除了任何筛选条件,并且因为它是在上下文转换之后应用的,所以它也移除了由上下文转换引入的ProductKey上的筛选条件。因此,计算列会计算具有相同颜色和单价的所有产品的单价总和:

值得记住的是,当您从DAX表达式中调用度量时,它会自动被CALCULATE包围。DAX添加的自动CALCULATE会执行上下文转换,如果您不注意,它可能会成为您代码中的错误源。

例如,如果您想查询一个模型,返回销售额占总销售额1%以上的产品,您可能会想以这种方式编写查询:

DEFINE
MEASURE Product[TotalSales] =
SUM ( Sales[Quantity] )
EVALUATE
FILTER (
ADDCOLUMNS (
VALUES ( Product[Product Code] ),
"SalesOfProduct", CALCULATE ( [TotalSales] )
),
[SalesOfProduct] >= [TotalSales] * 0.01
)

请注意ADDCOLUMNS内部调用TotalSales时围绕的显式CALCULATE(第8行),这表明需要进行上下文转换,以便仅计算当前迭代产品的销售额。实际上,TotalSales是一个度量,因此不需要CALCULATE,因为DAX会自动添加它。更糟糕的是,查询不起作用,因为引擎在FILTER条件中对TotalSales的第二次调用周围添加了一个CALCULATE(第10行),我们在其中将SalesOfProduct与TotalSales的1%进行比较。由于存在CALCULATE,因此查询实际上是检查产品的销售额是否占同一产品销售额的1%。

为了产生正确的结果,你可以以这种方式编写查询:

DEFINE
MEASURE Product[TotalSales] =
SUM ( Sales[Quantity] )
EVALUATE
FILTER (
ADDCOLUMNS (
VALUES ( Product[Product Code] ),
"SalesOfProduct", [TotalSales]
),
[SalesOfProduct] >= SUM ( Sales[Quantity] ) * 0.01
)

如您所见,我们删除了TotalSales第一次调用(第8行)周围无用的(因为隐含的)CALCULATE。然后,我们在FILTER使用的条件(第10行)中使用了SUM(Sales [Quantity]),以避免隐含的上下文转换。

通过利用变量,可以将前面的查询写得更加优雅:

DEFINE
MEASURE Product[TotalSales] = SUM ( Sales[Quantity] )
VAR SalesOfAllProducts = [TotalSales]
EVALUATE
FILTER (
ADDCOLUMNS (
VALUES ( Product[Product Code] ),
"SalesOfProduct", [TotalSales]
),
[SalesOfProduct] >= SalesOfAllProducts * 0.01
)

DAX中的变量有助于您在不同于您想使用其结果的环境中评估表达式。您可以在DAX的变量中找到更多示例。


如果您想深入学习微软Power BI,欢迎登录网易云课堂试听学习我们的“从Excel到Power BI数据分析可视化”系列课程。或者关注我们的公众号(PowerPivot工坊)后猛戳”在线学习”。




长按下方二维码关注“Power Pivot工坊”获取更多微软Power BI、PowerPivot相关文章、资讯,欢迎小伙伴儿们转发分享~


Power Pivot工坊