到本书的这一章时,您已经学习了DAX语言的基础知识。您知道如何创建计算列和度量值,并且您已经很好地理解了DAX中的通用函数。在这一章中,你将进入该语言的下一阶段:在学习了DAX语言坚实的理论背景之后,你将能够成为一个真正的DAX选手。
到目前为止,您已经获得了一些知识,您可以创建许多有趣的报告,为了能创建更复杂的报告,你还需要学习计算上下文。计算上下文是DAX所有高级特性的基础。
我们想对我们的读者说几句话:计算上下文的概念很简单,你很快就会学习和理解它。然而,您需要彻底地理解一些底层的细枝末节;否则,在你的DAX学习路径中,你会感到迷失。我们在公共和私人课程中教许多用户。我们知道这是绝对正常的。在某一时刻,你会觉得公式像魔术一样工作,因为它们是有效的,但你不明白为什么。别担心:许多人也有同样的问题。大多数DAX的学生在学习时能理解,而其他许多学生将来也会理解。这仅仅意味着计算上下文对他们来说不够清晰。在这一点上,解决方案很简单:回到这一章,再读一遍,你可能会发现一些新的东西,这是你在第一次读到的时候漏掉了。
此外,计算上下文在CALCULATE函数的使用中起着重要的作用。CALCULATE可能是DAX中最强大、最难以学习的函数,我们将在第5章“理解 CALCULATE和CALCULATETABLE”中介绍CALCULATE,然后在书的其余部分中使用它。在没有坚实的计算上下文背景的情况下,理解CALCULATE是有问题的。另一方面,在不尝试使用CALCULATE的情况下,理解计算上下文的重要性几乎是不可能的。因此,这一章和随后的一章根据我们之前的经验,总是会被标记并反复查阅。
介绍计算上下文
让我们先了解一下计算上下文是什么。任何DAX表达式都是在上下文中求值的。上下文是计算公式的“环境”。例如,考虑一个非常简单的公式:
[Sales Amount] := SUMX ( Sales, Sales[Quantity] * Sales[UnitPrice] )
您已经知道这个公式计算的是什么:数量乘以价格的总和。您可以将这个度量值放在一个透视表中,并查看结果,如图4-1所示。
这个数字本身看起来一点都不有趣,是吗?但是,如果你仔细想想,这个公式计算出它应该计算的东西:所有销售金额的总和,这是一个没有多少含义的大数字。当我们使用一些列来分开查看这个结果时,这个透视表变得就有趣了。例如,您可以使用产品颜色,将其放在透视表行中,这时透视表突然就显示了一些有趣的业务见解,如图4-2所示
总数仍然在那里,但现在它是小数字的总和,每个值,连同所有其他的值,都有意义。但是,如果你再仔细想想,你应该注意到这里发生了一些奇怪的事情:这个公式并不是计算我们所要求的。
我们认为这个公式的意思是“所有销售金额的总和”。但在透视表的每个单元格中,公式并不是计算所有销售的总和,而是计算具有特定颜色的产品的销售总额。然而,我们从未指定计算必须在数据模型的一个子集上工作。换句话说,这个公式并没有指定它可以在数据子集上工作。
为什么在不同的单元格中,公式计算不同的值?答案是非常简单的:因为在这个计算上下文下,DAX计算公式。你可以把一个公式的计算上下文想象成围绕在单元格周围可以计算DAX公式的区域。
因为产品的颜色在各行中,透视表中的每一行都可以看到,在整个数据库中,只有特定颜色的产品的子集。这是公式的周围区域,也就是在公式计算之前应用到数据库的一组过滤器。当公式计算所有销售金额的总和时,它不会在整个数据库中计算它,因为它无法选择全部行。当DAX计算出与行值为白色的公式时,只有白色的产品是可见的,因此,它只考虑与白色产品相关的销售。因此,当计算仅显示白色产品的透视表中的一行时,销售金额的总和就变成了所有白色产品的销售额之和。
任何DAX公式都确定了一个计算,但是DAX在一个上下文执行计算,它定义了计算的最终值。公式虽然是一样的,但是值是不同的因为DAX根据不同的数据子集对它进行计算。
现在让我们把年份放在列上,使透视表更有趣。现在的报告如图4-3所示。
计算的规则在这一点上应该是清楚的:每个单元格现在都有不同的值,即使公式总是相同的,因为透视表的行和列选择都定义了上下文。事实上,2008年的白色产品销售与2007年的白色产品销售不同。而且,因为您可以在行和列中放置多个字段,所以最好说行上的字段集和列上的字段集定义上下文。图4-4使这个更加明显
每个单元格有不同的值,因为在行、颜色和品牌名称上有两个字段。行和列完整的字段集合定义了上下文。例如,图4-4中突出显示的单元格的上下文对应黑色、品牌Contoso和日历年。
注意
字段是否在行或列上(或在切片器和/或页面过滤器上,或者在任何其他您可以用查询创建的类型过滤器中)并不重要。所有这些过滤器共同定义单个上下文,而DAX利用这个上下文来计算公式。在行或列上放置一个字段会产生一些美观上的影响,但是在DAX计算值的方式上没有任何变化。
现在让我们看完整的场景。在图4-5中,我们在切片器上添加了产品类别,并在过滤器上添加了月份,我们选择了12月。
在这一点上,很明显,每个单元计算的值都有一个由行、列、切片器和过滤器定义的上下文。所有这些过滤器共同定义一个上下文,并且在公式计算之前,DAX将该上下文应用于数据模型上。此外,重要的是要知道,并非所有的单元格都具有相同的过滤器集,不仅在值方面,而且在字段方面也是如此。例如,列上的grand total只包含类别、月份和年份的过滤器,但是它不包含颜色和品牌的过滤器。颜色和品牌的字段在行中,它们不会过滤总数。这同样适用于透视表中颜色的子总数:对于那些单元,制造商没有过滤器,来自行的唯一有效的过滤器是颜色
我们把这个上下文称为筛选上下文,正如它的名字所暗示的那样,它是一个筛选表的上下文。您所编写的任何公式都有不同的值,这取决于执行计算的DAX语句上的筛选上下文。这种行为,虽然非常直观,但需要被充分理解。
现在您已经了解了筛选上下文是什么,您知道下面的DAX表达式应该被理解为“在当前筛选上下文中可见的所有销售金额的总和”:
[Sales Amount] := SUMX ( Sales, Sales[Quantity] * Sales[UnitPrice] )
稍后您将学习如何阅读、修改和清除筛选上下文。到目前为止,我们已经足够深入地了解了这个事实,即筛选上下文总是存在于透视表的任何单元格或报告/查询中的任何值。你总是需要考虑筛选上下文来理解DAX是如何计算公式的。
理解行上下文
筛选上下文是DAX中存在的两个上下文之一。它的搭档是行上下文,在本节中,您将了解它是什么以及它是如何工作的。
这一次,我们使用了一个不同的公式来考虑:
Sales[GrossMargin] = Sales[SalesAmount] - Sales[TotalCost]
您可能会在一个计算列中写出这样的表达式,以便计算毛利润。一旦您在计算列中定义了这个公式,您就会得到结果表,如图4-6所示。
DAX对表中所有行进行公式计算,对于每一行,它都按照预期计算出了不同的值。为了理解行上下文,我们需要有点学究式的阅读公式:我们要求减去两列,但是我们从哪里告诉DAX从哪一行来得到列的值呢?您可能会说,要使用的行是隐式的。因为它是一个计算列,所以DAX计算它一行一行,对于每一行,它都会计算一个不同的结果。这是正确的,但是,从DAX表达式的角度来看,要使用哪一行的信息仍然缺失。
实际上,用于执行计算的行并没有存储在公式中。它是由另一种上下文定义的:行上下文。当你定义计算的列时,DAX从表的第一行开始了一个迭代;它创建了包含该行的行上下文并对表达式求值。然后它移到第二行,再次求出表达式。这发生在表格中所有的行中,如果你有100万行,你可以认为DAX创建了100万行上下文来评估这个公式100万次。显然,为了优化计算,这并不是发生的事情;否则,DAX将是一种非常缓慢的语言。不管怎样,从逻辑的角度来看,这就是它的工作原理。
让我们试着更精确一点。行上下文是一个总是包含一行的上下文,DAX在计算列的创建过程中自动定义它。您可以使用其他技术创建行上下文,这将在本章后面讨论,但是解释行上下文的最简单方法是查看计算过的列,其中引擎总是自动创建它。
总是有两种上下文
到目前为止,您已经了解了行上下文和筛选上下文是什么,它们是DAX中仅有的上下文类型。因此,它们是修改公式结果的唯一方法。任何公式都将在这两个不同的上下文中进行计算:行上下文和筛选上下文。我们将这两种上下文称为“计算上下文”,因为它们是改变公式计算方式的上下文,为相同的公式提供不同的结果。
这是一个非常重要的点,在开始时很难集中注意力:总是有两个上下文,一个公式的结果取决于两者。在你的DAX学习路径的这一点上,你可能会认为这是显而易见的,而且是很自然的。你也许是对的。然而,在书的后面,如果你不记得这两个上下文的共存,你会发现一些公式是很难理解的,每一个都可以改变公式的结果。
测试你的计算上下文理解
在我们继续进行关于计算上下文的更复杂的讨论之前,我们希望通过几个示例来测试您对上下文的理解。请不要立即看这个解释;在这个问题之后停下来,试着回答这个问题。然后阅读解释,并试着去理解它。
在计算列中使用SUM
第一个测试是非常简单的。如果您在销售表中定义了一个计算列,那么会发生什么呢?
Sales[SumOfSalesAmount] = SUM ( Sales[SalesAmount] )
因为它是一个计算列,它将被逐行计算,对于每一行,您将获得一个结果。你希望看到什么数字?从这些选项中选择一个:
这一行的销售额的值,同时也就是每一行的不同值。
所有行的总销售额,也就是说,所有行的值都是一样的。
一个错误;你不能在计算的列中使用求和。
请停止阅读,当我们等待你的有根据的猜测之后再继续。
现在,让我们详细说明当DAX计算公式时发生了什么。您已经了解了公式的含义:“在当前筛选上下文中看到的所有销售金额的总和。“因为在一个计算列中,DAX会逐行计算公式。因此,它为第一行创建一行上下文,然后调用公式计算,并对整个表进行迭代。这个公式计算当前筛选上下文中所有销售金额的总和,所以真正的问题是“当前的筛选上下文是什么?”答案:它是完整的数据库,因为DAX计算这个公式在任何透视表或任何其他类型的筛选条件之外。实际上,当没有筛选条件激活时,DAX将它作为计算列定义的一部分进行处理。
即使有行上下文,SUM也会忽略它。相反,它使用筛选上下文,现在的筛选上下文是完整的数据库。因此,第二种选择是正确的:您将获得总销售额,与所有销售行相同的值,如图4-7所示。
这个例子说明了这两个上下文共存。他们共同在公式的结果上工作,但是用不同的方式。计算列中使用的SUM、MIN和MAX等聚合函数只使用过滤上下文,忽略行上下文,DAX只使用它来确定列值。如果你选择了第一个答案,就像许多学生通常做的那样,这是完全正常的。关键是,您还没有想到这两个上下文正在一起工作,以不同的方式改变公式结果。当使用直观的逻辑时,第一个答案是最常见的,但它是错误的,现在你知道为什么了。
在度量值中使用列
我们想和你们做的第二个测试有点不同。假设你想要在一个度量中定义毛利润的公式,而不是在一个计算的列中。你有一个带有销售金额的列,另一个用于产品成本的列,你可以写下面的表达式:
[GrossMargin] := Sales[SalesAmount] - Sales[ProductCost]
如果你试图定义这样一个度量值,你应该期待什么结果?
1、这个表达式工作正常,我们需要在报告中测试结果。
2、一个错误,你甚至不能写这个公式
3。你可以定义这个公式,但是当它在透视表或查询中使用时,它会给出一个错误
和以前一样,停止阅读,思考答案,然后阅读下面的解释
在这个公式中,我们使用了Sales[SalesAmount],这是一个列名,也就是销售表中销售金额的值。这个定义缺少什么吗?您应该回忆一下,从以前的观点来看,这里缺少的信息是从哪里获取当前销售额的值。当您在计算列中编写这段代码时,DAX知道在计算表达式时要使用的行,这要归功于行上下文。然而,这个度量值会发生什么呢?没有迭代,也没有当前行,也就是说,没有行上下文。
因此,第二个答案是正确的,你甚至不能写公式;它在语法上是错误的。当你试图输入它的时候,你会收到一个错误
记住,列本身没有值。相反,它对于表格的每一行都有不同的值。因此,如果你想要一个单独的值,你需要指定要使用的行。指定要使用的行的惟一方法是行上下文。因为在这个度量中没有行上下文,公式是不正确的,从而DAX会拒绝它。
在一个度量值中指定这个计算的正确方法是使用聚合函数,如下所列
[GrossMargin] := SUM ( Sales[SalesAmount] ) - SUM ( Sales[ProductCost] )
使用这个公式时,您现在要求通过SUM进行聚合。因此,后一个公式并不依赖于行上下文;它只需要一个筛选上下文,同时提供了正确的结果。
利用迭代器创造一个行上下文
您了解到DAX在定义一个计算列时自动创建行上下文。在这种情况下,引擎会逐行计算DAX的表达式。现在,是时候学习如何使用迭代器在DAX表达式中创建行上下文了
您可能还记得,在第2章“引入DAX”中,所有以x结尾的函数都是迭代器,也就是说,它们遍历表,并对每一行进行评估,最后用不同的算法聚合结果。例如,看看下面的DAX表达式:
[IncreasedSales] := SUMX ( Sales, Sales[SalesAmount] * 1.1 )
SUMX是一个迭代器,它迭代销售表,对于表的每一行,它计算销售额增加10%到它的值,最后返回所有这些值的总和。为了计算每一行的表达式,SUMX在Sales表上创建行上下文,并在迭代期间使用它。DAX在包含当前迭代行的行上下文中计算内部表达式(SUMX的第二个参数)
需要注意的是,在完整的计算流程中,SUMX的不同参数使用不同的上下文。让我们更仔细地看一下相同的表达式
= SUMX (
Sales, ← 外部上下文
Sales[SalesAmount] * 1.1 ← 外部上下文 + 新的行上下文
)
第一个参数,Sales,是使用来自调用者的上下文来计算的(例如,它可能是一个透视表单元格,另一个度量值,或者查询的一部分),而第二个参数(表达式)是使用外部上下文加上新创建的行上下文来计算的。
所有迭代器的行为都是一样的:
1、为作为第一个参数接收的表的每一行创建一个新的行上下文
2、在新创建的行上下文中(加上在迭代开始之前存在的任何其他上下文),对表的每一行进行评估。
3、聚合在步骤2中计算的值。
重要的是要记住,原始上下文在表达式中仍然有效:迭代器只添加新的行上下文;它们不会以任何方式修改现有的。这条规则通常是有效的,但是有一个重要的例外:如果前面的上下文已经包含了同一个表的行上下文,那么新创建的行会上下文隐藏了先前存在的行上下文。我们将在下一节中更详细地讨论这个问题。