本文翻译自 Marco Russo & Alberto Ferrari 的文章《
Context transition in DAX explained visually
》。
在之前的文章中,我们通过可视化的方法介绍了两个重要的 DAX 概念:
筛选上下文
和
行上下文
。本文将通过图形可视化的方式来描述上下文转换,从而完成这一简短的系列。
上下文转换将任何活动的行上下文转换为过滤上下文中的相应过滤器。上下文转换由 CALCULATE 和 CALCULATETABLE 以及任何隐式调用这两个函数的 DAX 语法执行。例如,任何度量值引用都会隐含调用 CALCULATE,这就触发了上下文转换。还有一些其他 DAX 函数也在内部调用 CALCULATE 或 CALCULATETABLE 来执行上下文转换。
要执行上下文转换,必须存在一个活动的行上下文。因此,任何上下文转换都会发生在计算列中,或者在 DAX 迭代器(如 FILTER 和 SUMX)内。
除了正式定义之外,让我们尝试通过一个例子来描述上下文转换。当你迭代产品列表时,每个产品都有一个行上下文;上下文转换会在过滤上下文中过滤“当前”产品,因此任何聚合或表表达式都会被该产品过滤。仍然不太清楚?我们可以尝试用一个可视化表示来解释!
请考虑以下报告,其中显示每个产品品牌的销售额和平均客户销售额指标。
平均客户销售额度量值计算每个客户的销售额并返回非空值的平均值:
Average Customer Sales =
AVERAGEX (
Customer,
[Sales Amount]
)
Contoso 的平均客户销售额值为 768.18,计算方式如下:
-
筛选上下文只有一个针对品牌的筛选器 Contoso。
-
在此筛选上下文中, AVERAGEX 的第一个参数被计算,并且它返回所有 Customer 行,因为没有影响 Customer 的筛选器。
-
AVERAGEX 对 Customer 的每一行计算行上下文中的第二个参数。
-
-
计算销售额的筛选上下文继承了初始筛选上下文(品牌为 Contoso)并添加了通过上下文转换获得的筛选器。
-
-
AVERAGEX 计算所得结果的平均值,忽略空白值。
换句话说,迭代(如 AVERAGEX)为每个客户生成一个行上下文;引用 Sales Amount 度量值时隐含调用 CALCULATE,它将每个行上下文转换为过滤上下文。实际上,每个客户都有一个独立的过滤上下文来计算该客户的 Sales Amount;每个过滤上下文由报表的初始过滤上下文(即调用度量的上下文)和通过上下文转换生成的客户过滤器共同形成。
每个迭代器的行为都是类似的;它们之间的唯一区别是对每行获得的结果应用的最终聚合方式:AVERAGEX 计算结果的平均值,而 SUMX 则对结果求和。
在某些情况下,你可能希望移除上下文转换的影响。例如,考虑以下报表,Sales Top Customers 显示了销售额至少占所选品牌总收入 1% 以上的客户的销售额。
我们编写了以下度量,计算迭代器内所有客户的销售额。这种方法对于性能来说并不理想,但我们希望通过一个相对简单的例子来展示如何在需要时移除上下文转换的影响:
Sales Top Customers =
SUMX (
Customer,
VAR SalesCustomer = [Sales Amount]
VAR SalesAllCustomers =
CALCULATE (
[Sales Amount],
REMOVEFILTERS ( Customer )
)
VAR Result =
IF (
SalesCustomer > SalesAllCustomers * 0.01,
[Sales Amount]
)
RETURN Result
)
SalesAllCustomers 变量必须包含当前筛选上下文中所有客户的总销售额。Sales Amount 度量值执行了上下文转换,这是 SalesCustomer 变量所需要的。对于 SalesAllCustomers,我们应该在执行 Sales Amount 度量值之前移除行上下文——但这无法直接实现。我们可以做的是通过显式的 CALCULATE 执行上下文转换,然后使用 REMOVEFILTERS 移除上下文转换添加的 Customer 筛选器。一旦上下文转换生成的筛选器被移除,我们就不再有行上下文或由上下文转换获得的相应筛选上下文。
REMOVEFILTERS 根据 CALCULATE 的计算顺序,在上下文转换之后且其他筛选器之前执行。正如我们提到的,这个例子是出于教学目的,因为在更复杂的场景中可能会需要移除上下文转换。在这种情况下,更好的解决方案是先在 SUMX 迭代之前计算 SalesAllCustomers 变量:
ales Top Customers Optimized =
VAR SalesAllCustomers = [Sales Amount]
RETURN
SUMX (
Customer,
VAR SalesCustomer = [Sales Amount]
VAR Result =
IF
(
SalesCustomer > SalesAllCustomers * 0.01,
[Sales Amount]
)
RETURN Result
)
上下文转换创建的筛选器会覆盖同一列上的任何现有筛选器。这是过滤上下文的默认行为。当我们希望在 CALCULATE 中保留现有筛选器时,会使用 KEEPFILTERS 包裹不应删除现有筛选器的筛选参数。然而,上下文转换并不对应于 CALCULATE 的筛选参数,而是一种隐藏的行为,影响由迭代器函数生成的行上下文。因此,在这种情况下,我们需要针对 KEEPFILTERS 使用特定的语法,下面我们通过一个例子来介绍。
考虑以下报表,其中切片器选择了 2018 年的 11 月和 12 月,以及 2019 年的 1 月和 2 月。
我们需要编写的度量是销售的月平均值,最初我们写为 Monthly Average Incorrect:
Monthly Average Incorrect =
AVERAGEX (
DISTINCT ( 'Date'[Month] ),
[Sales Amount]
)
该度量值在总计行返回了一个错误的值,本应是这四个月的平均值。但它却显示了一个大于任意显示数值的值,显然有些地方没有按预期工作。通过分析筛选上下文和上下文转换,可以解释这个错误的结果:总计行的筛选上下文有一个包含两列的筛选器,分别是 Year 和 Month。
这些筛选器是年和月之间的任意组合筛选,通常称为“任意形状的筛选器”,因为它们不对应于两列或多列中的所有组合。然而,重要的信息是筛选器涉及两列。
AVERAGEX 函数迭代的表只有一列,即月份。因此,每个月的行上下文仅包含月份列,而通过上下文转换获得并应用于筛选上下文的筛选器也只包含月份列。这个新筛选器移除了月份上的任何现有筛选器。然而,由于现有筛选器有年和月两列,现有筛选器中的年列被保留了下来。
例如,计算10月的 Sales Amount 时,过滤上下文有一个月份(10月)和两个年份(2018 和 2019)。
这一过程对每个月都会重复。因此,尽管迭代了四个月,但对于每个月,Sales Amount 都返回了按该月份筛选的 2018 年和 2019 年的销售额。以下截图中的总列解释了将两个年份的销售额相加后得到的结果;总行显示了对应的月平均值。
要获得正确的结果,我们需要对上下文转换添加的筛选器应用 KEEPFILTERS。语法要求对 DAX 迭代器函数的迭代参数应用 KEEPFILTERS。当 KEEPFILTERS 应用于迭代器时,它会被应用于由该迭代器生成的行上下文产生的任何上下文转换。在此例中,KEEPFILTERS 必须包裹在 DISTINCT ( Date[Month] ) 的参数周围:
Monthly Average =
AVERAGEX (
KEEPFILTERS (
DISTINCT ( 'Date'[Month] )
),
[Sales Amount]
)
KEEPFILTERS 的存在阻止了上下文转换对月份的筛选移除现有的年-月筛选器的一部分。值得注意的是,如果迭代器是 Date[Year Month] 而不是 Date[Month],则无需使用 KEEPFILTERS。
上下文转换将行上下文转换为筛选上下文中的对应筛选器。通过可视化表示可以清楚地看到,行上下文就像一张只有一行的表,移动到筛选上下文中。与其他筛选器一样,行上下文会覆盖相同列上的现有筛选器,除非对迭代器应用了 KEEPFILTERS。