DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE

在本章中,我们将继续探索DAX语言的强大,并详细解释单个函数:CALCULATE。实际上,相同的注意事项对于CALCULATETABLE也是有效的,它计算并返回表而不是标量值。为简单起见,我们将在示例中引用CALCULATE,但请记住CALCULATETABLE具有相同的行为。

用一整章的篇幅来描述一个函数似乎有些奇怪,但这是必要的,因为它的丰富性和副作用。 CALCULATE是迄今为止DAX语言中最重要、最有用和最复杂的函数。实际上,函数本身很简单。它只执行很少的任务,但是需要计算的场景的数量,以及可以用计算编写的公式的复杂性,使得完整的章节是绝对必要的。

正如前一章所述,这是一个艰难的过程。我们强烈建议您阅读一次,大致了解一下CALCULATE,然后继续阅读该书的剩余部分。然后,当你对一个特定的公式感到困惑时,回到这一章,从头再读一遍。您每次阅读时都可能会发现新信息。

本章的另一个重要方面是我们需要有点迂腐。因此,如果在某个时候,你发现某个部分看起来很无聊,而且它似乎只是在陈述显而易见的东西,那么再仔细阅读一遍,以确保你完全理解了它。

5.1 理解CALCULATE

在上一章中,您已经了解到有两种不同的上下文:行上下文和筛选上下文。您已经了解到可以使用迭代器以编程方式创建行上下文,并且学习了允许忽略筛选上下文的ALL函数。重要的是要记住ALL忽略筛选上下文,它不会改变它。因此,在以下公式中:

[Sales Amount Margin] :=
SUMX (
ALL ( Sales ),
Sales[SalesAmount] * AVERAGE ( Sales[MarginPct] )
)

ALL忽略现有的筛选上下文并始终返回整个表,但它不会以任何方式更改公式的其他部分的计算。实际上,在最内层表达式中,AVERAGE将计算筛选上下文中MarginPct列的平均值,DAX在该上下文中计算整个表达式。 DAX中有一个函数可以更改筛选上下文,它是CALCULATE。

让我们通过查看一个很有用的场景来介绍CALCULATE。想象一下,您想要构建如图5-1所示的报告,其中包含类别,子类别和销售额总和。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第1张图片
图5-1在这里,您可以看到一个简单的报告,显示销售额除以类别和子类别。 SalesPct列显示行总数的百分比。

报告显示了每行占总行的百分比。您可以使用Microsoft Excel数据透视表功能轻松地生成这样的报告,但是我们感兴趣的是计算百分比作为度量值,以便用户可以随时将其添加到数据透视表中。

以下是一个天真的解决方案:

SalesPct :=
DIVIDE (
SUM ( Sales[SalesAmount] ),
SUMX ( ALL ( Sales ), Sales[SalesAmount] )
)

分子是SalesAmount的和。分母忽略筛选上下文,并始终返回SalesAmount的总计,而不管任何筛选器。只要您不从切片器中选择任何内容,此公式就可以使用。例如,如果在切片器中选择黑色,则值为错误;总计的百分比是18.76%而不是100%,因为用于百分比计算的分母是一个更高的数字,如图5-2所示。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第2张图片
图5-2从切片器中选择颜色显示错误的百分比结果。

这里的问题很容易理解。通过使用ALL,我们忽略了筛选上下文。因此,分母始终是所有销售的总计,而如果选择颜色,则希望将筛选器保持为颜色,仅清除类别和子类别上的筛选器。 ALL和迭代器不是这里的正确选择;你需要更强大的东西。换句话说,您需要CALCULATE。

4.1.1 理解筛选上下文

在继续介绍CALCULATE之前,插入一个题外话,这很重要,以加深您对筛选上下文的理解。筛选上下文是一个复杂的概念,只有在第10章“高级计算上下文”的末尾,您才能最终理解它是如何工作的。在此之前,我们按照复杂性的顺序对筛选上下文的进行了不同的描述,以便一步一步地得到最终的解释。

在上一章中,我们给出了是啊选上下文的第一个定义:“应用于模型的一组筛选器,它们更改了整个数据库中可见的行。”即使它是一个正确的定义,它仍然非常幼稚。为了进入下一个级别,您需要记住VertiPaq(DAX工作的数据库)是一个列式数据库。您应该停止考虑表格,而不是考虑列。

例如,您可以将Product视为常规表,如图5-3所示。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第3张图片
图5-3您可以将Product表视为标准表,由行组成,每行分为列。

但是,由于VertiPaq是一个列式数据库,因此表的正确表示形式将是一组列,而不是单个实体。因此,如图5-4所示可以更好的理解同一个表

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第4张图片
图5-4 Product的正确可视化是一组列,每列都分为几行。

当然,内容是相同的,但是现在更容易将不同的列看作存储在内存中的不同项。显然,相同的表示适用于数据模型中的任何表。因此,您应该在思维上将模型中的每个表划分为单独的列,最后将得到一组列,逻辑上划分为表,但每列与其他列分开。

将Color放入切片器时,DAX会对该列应用筛选器。请仔细阅读:它不会对包含该列的表格应用筛选器。它仅将筛选器应用于列。然后,因为该列是表的一部分,因此该表也将具有筛选器。尽管如此,筛选器一次只能处理一列(我们正在描述实际发生的事情的近似;我们将在本书的后面部分对筛选上下文进行最终理解)。

当您仅在切片器筛选中选择红色产品时,模型将具有此筛选器,如图5-5所示。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第5张图片
图5-5过滤红色产品会导致仅应用于“颜色”列的过滤器。

您可以想象将单个列上的筛选器表示为列的值上的位图,或者以一种更易于阅读的方式表示为列的活动值列表。

最后,我们可以更好地定义筛选上下文:筛选器上下文是一组表。每个表都包含一个列,并列出该列的所有值,引擎认为这些值在当前上下文中可见。所有这些筛选器,放在一个逻辑AND中,形成筛选上下文。

在数据透视表的单个单元格中,您有来自切片器和筛选的筛选器;从行和列。每个筛选器都对一组表列进行操作。因此,在前面的示例中,筛选上下文包含三个单独的筛选器:一个用于类别,一个用于子类别(两者都在数据透视表中的行上),另一个用于颜色(来自切片器)。

所有这些题外话使我们更好地理解更改筛选上下文的含义。如果要更改筛选上下文,则需要为模型中的部分或全部筛选列提供新的值列表。 DAX将使用新的值列表替换该列上的筛选器(仅限该列),并以此方式生成新的筛选上下文。

要记住的两个重要方面是:

  • 筛选器是一列的一组活动值。
  • 筛选器始终仅适用于单个列。

请记住,这不是筛选上下文的正确定义。在成为DAX的大师之前,你必须学习许多其他方面的知识。然而,这个定义对于开始使用筛选上下文已经非常有用了。

在结束了题外话之后,您现在对筛选上下文有了更好的理解,我们可以继续介绍CALCULATE是如何修改它的。

4.1.2 介绍CALCULATE

CALCULATE(及其同伴CALCULATETABLE,您将在稍后学习)是唯一可以修改筛选上下文的函数。实际上,CALCULATE会创建一个新的筛选上下文,然后在该新上下文中计算表达式。因为新上下文的起点是现有上下文,我们可以说它修改了计算上下文。

让我们开始检查CALCULATE的语法:

[Measure] := CALCULATE ( Expression, Condition1, ...
ConditionN )

CALCULATE接受任意数量的参数,唯一的强制参数是第一个,即要计算的表达式。我们在第一个参数筛选器参数后面调用条件。 CALCULATE执行以下操作:

  • 它获取当前筛选上下文并将其复制到新的筛选上下文中。
  • 它计算每个筛选器参数,并为每个条件生成该特定列的有效值列表。
  • 如果两个或多个筛选器参数影响同一列,则使用AND运算符将它们合并在一起(或者,在数学术语中,使用集合交集)。
  • 它使用新条件替换模型中列上的现有筛选器。如果列已有过滤器,则新过滤器将替换现有过滤器。另一方面,如果列没有筛选器,则 DAX只是将新筛选器应用于列。
  • 一旦计算了新的筛选上下文,CALCULATE将计算新筛选上下文中的第一个参数(表达式)。最后,它将恢复原始筛选上下文,返回计算机结果。

备注:
CALCULATE执行另一项非常重要的任务:它将任何现有行上下文转换为等效的筛选上下文。本章稍后将对此主题进行更详细的讨论。我们在这里提到它的原因是,如果你对本节进行第二次阅读,最好记住这个非常重要的事实:CALCULATE从现有的行上下文中创建一个筛选上下文。

CALCULATE接受的筛选器可以有两种类型:

  • 值表,以表表达式的形式。在这种情况下,您需要提供要在新筛选上下文中查看的确切值列表。筛选器可以是具有单列或多列的表,就像整个表上的筛选器一样。
  • 布尔条件,例如Product [Color] =“White”。这些筛选器需要在单个列上工作,因为结果必须是单个列的值列表。

如果使用带有布尔条件的语法,DAX会将其转换为值列表。所以每当你写:

[Sales Amount Red Products] :=
CALCULATE (
SUM ( Sales[SalesAmount] ),
Product[Color] = "Red"
)

DAX将表达式转换为以下表达式:

[Sales Amount Red Products] :=
CALCULATE (
SUM ( Sales[SalesAmount] ),
FILTER (
ALL ( Product[Color] ),
Product[Color] = "Red"
))

因此,您只能使用布尔条件引用筛选器参数中的一列。 DAX必须检测要在FILTER表达式中迭代的列,FILTER表达式在后台自动生成。如果布尔表达式引用了更多列,那么您必须以显式方式编写FILTER迭代,稍后您将看到。

在这一点上,我们可以回到计算所有类别和子类别的销售额占总销售额的百分比的示例,仍然要考虑颜色上的过滤器。您可以使用CALCULATE编写该度量值:
'''
[SalesPctWithCalculate] :=
DIVIDE (
SUM ( Sales[SalesAmount] ),
CALCULATE (
SUM ( Sales[SalesAmount] ),
ALL ( 'Product Category' ),
ALL ( 'Product Subcategory' )
))
'''
让我们关注在这个公式的分母中使用的CALCULATE。要计算的表达式始终相同:销售额的总和。但是,我们已经知道公式本身可以有许多不同的值,这取决于DAX计算它的上下文。因为表达式在CALCULATE内,所以表达式的上下文不是原来的上下文。我们只需要理解上下文是如何进行的,这些信息来自于CALCULATE的附加参数。

第一个筛选参数是ALL('产品类别')。 ALL返回表中的所有行,在本例中为所有类。 DAX检索ProductCategory表并将其值用作新筛选器,替换产品类别的任何列上的任何先前存在的筛选器,在此示例中,该列来自数据透视表行。

显然,对于它的第二个筛选器参数也是如此:ALL('Product Subcategory')从Product Subcategory的任何列中删除任何筛选器。

重点是CALCULATE不会替换来自切片器的筛选器,即Color上的筛选器。它仅从Product Category和Product Subcategory表的列中删除筛选器。因此,最终筛选上下文包含所有类别和子类别,但仅包含所选颜色。

在数据透视表中使用这个新公式可以显示正确的值,如图5-6所示。


DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第6张图片
图5-6使用CALCULATE计算的百分比显示正确的值。

这时,非常细心的读者可能会停下来问:嗯,这没有意义。您已从Product Category和Product Subcategory中删除了筛选器,但是您要从Sales中对值进行求和。谁从该表中删除了筛选器?这确实是一个非常好的问题。事实上,我们的描述遗漏了一些非常重要的内容:一旦CALCULATE计算出新的筛选上下文,它就会在计算表达式之前将其应用于数据模型。

当DAX将筛选上下文应用于表时,我们已经从上一章中了解到筛选器通过遵循其定义(单向或双向筛选)的关系出传递。事实证明,我们从Product Category和Product Subcategory中删除了筛选器,并且当DAX应用新的筛选上下文时,它将它传递到事实表,该事实表位于从Product Category开始的关系链的多端,以Sales结束。通过删除Product Category和Product Subcategory中的筛选器,我们还删除了它们在Sales中相应的传递筛选器。

5.2 CALCULATE示例

现在您已经了解了CALCULATE的基础知识,或者至少您已经了解了它为何如此有用,本章接下来的部分将专门介绍它的各种用法示例。它们对于深入学习和理解很重要。实际上,CALCULATE本身就是一个非常简单的函数。复杂性(因此我们给它的重要性)来自这样一个事实,即使用CALCULATE,你必须根据筛选上下文进行思考,并并且您可能会在一个公式中得到多个上下文,这使得代码流程难以理解。根据我们的经验(作为培训师),通过示例学习是理解CALCULATE和筛选上下文的最佳方式。

5.2.1 筛选单个列

使用CALCULATE最简单的方法是筛选单个列。例如,假设您要创建一个始终返回黑色产品销售额的度量值,无论对颜色进行何种选择。该公式非常容易得出:

[SalesAmountBlack] :=
CALCULATE (
SUM ( Sales[SalesAmount] ),
Product[Color] = "Black"
)

如果在数据透视表中使用上一个公式,您将得到如图5-7所示的结果。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第7张图片
图5-7无论当前过筛选上下文如何,SalesAmountBlack始终显示黑色产品的销售额。

您可以看到SalesAmountBlack始终显示黑色产品的销售额,即使在行上筛选上下文选择不同颜色也是如此。

如果您关注第三行(蓝色),则会发生以下情况:公式在筛选上下文中开始计算,其中颜色的唯一值为蓝色。然后,CALCULATE计算了一个新条件(Color = Black),当将其应用于新的筛选器上下文时,它替换了现有条件,删除了Blue上的筛选器并将其替换为Black上的筛选器。这发生在所有行(即所有颜色)上,这就是为什么您看到所有行的相同数字的原因。

显然,因为CALCULATE覆盖选择的唯一列是颜色,其他列保持其筛选器。例如,如果将日历年放在列上,您会看到所有颜色的结果始终相同,但不同年份的结果会发生变化,如图5-8所示。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第8张图片
图5-8 SalesAmountBlack仅覆盖颜色;它仍然遵守其他列(年)的过滤。

过滤单个列很简单。一个不太明显的事实是,如果您使用条件作为CALCULATE的筛选器,则一次只能筛选一列。例如,您可能希望创建一个度量值,仅计算单位价格至少是单位成本两倍的产品的销售额。你可以试试这个公式:

[HighProfitabilitySales]:= CALCULATE(SUM(Sales [SalesAmount]),Product [Unit Price]> = Product [Unit Cost] * 2)

您可以看到,这次,条件涉及两列:Unit Cost和Unit Price。即使DAX可以轻松计算每个产品的条件,它也不允许这种语法。原因在于,在其计算算法中,CALCULATE无法确定条件是否应替换Unit Cost、Unit Price或其中任何一个上的任何现有过滤器。事实上,如果你试着写上面的公式,你会得到一个错误的结果:

Calculation error in measure 'Sales'[HighProfitabilitySales]:
The expression contains
multiple columns, but only a single column can be used in a
True/False expression that is
used as a table filter expression.

没有办法使用布尔语法编写这样的公式。如果需要在条件中使用多个列调用CALCULATE,则需要使用不同的语法,它提供值列表而不是条件。

编写上一个表达式的正确方法是使用以下语法:

[HighProfitabilitySales] :=
CALCULATE (
SUM ( Sales[SalesAmount] ),
FILTER ( Product, Product[Unit Price] >= Product[Unit Cost] *
2 )
)

这次我们没有使用布尔表达式,而是使用Table语法作为CALCULATE的filter参数。而且,我们不只筛选了一列;我们筛选了整个Product表。在图5-9中,您可以看到HighProfitabilitySales度量值的实际应用。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第9张图片
图5-9 HighProfitabilitySales显示与其成本相比价格较高的产品的销售情况。

在本例中,CALCULATE计算条件:FILTER的结果是一个包含多个列的表(它包含Product的所有列)。当新条件被插入到筛选上下文中时,实际上Product上的所有现有条件都将替换为此新筛选器。换句话说,使用实际表作为FILTER函数的第一个参数,可以有效地替换该表所有列上的所有条件。

阅读了前面的解释后,您应该注意到这里有些事情不是很清楚。我们说CALCULATE中的FILTER表达式替换了Product表中所有先前存在的筛选器,因为FILTER返回的表包含Product的所有列。然而,我们的公式返回的值对于每一行是不同的。

例如,在蓝色行上,HighProfitabilitySales返回具有高盈利能力的蓝色产品的销售额,即使,就我们目前所知,它应该返回所有高利润产品的销售额,而不考虑颜色。要么 DAX没有替换颜色上的筛选器,要么发生了更复杂的事情。因为我们已经知道DAX替换了颜色上的筛选器,所以我们需要进行更多的研究来理解正确的计算流程。下面的代码是度量值的公式::我们对行进行了编号,以便更容易引用公式的各个部分。

1. CALCULATE (
2. SUM ( Sales[SalesAmount] ),
3. FILTER (
4. Product,
5. Product[Unit Price] >= Product[Unit Cost] * 2
6. )
7. )

第一个函数是CALCULATE。我们知道CALCULATE作为其第一步,计算筛选器参数。在执行任何其他操作之前,CALCULATE将从第3行开始计算FILTER表达式。

FILTER是一个迭代器,它遍历Product表(参见第4行)。FILTER不会看到所有产品;它只能看到当前筛选上下文中可见的行。现在,问题是DAX在哪个筛选上下文中计算第4行上的Product ?请记住,CALCULATE仍然没有创建新的筛选上下文。稍后,在评估筛选器之后,它将执行此操作。您可以推断出CALCULATE中的筛选是在原始筛选上下文下评估的,而不是在CALCULATE创建的筛选上下文下。虽然这似乎是显而易见的,但在许多DAX公式中,这种简单的考虑是错误的主要来源之一。

第4行的Product是指在原始筛选上下文中可见的产品。对于图5-9中的蓝色行,该上下文仅显示蓝色产品。因此,FILTER将迭代蓝色产品,它将只返回具有高盈利能力的蓝色产品。然后CALCULATE将删除现有的颜色筛选器,但这样的筛选器现在已经包含在FILTER的结果中,从而导致您正在观察的行为。重要的是要正确理解筛选的流程,在CALCULATE计算的表达式内替换颜色上的筛选器,而不是在CALCULATE筛选器内部。换句话说,CALCULATE的筛选器参数在先前的筛选上下文中进行评估。只有稍后的CALCULATE才会创建新的筛选上下文,在该上下文中计算其表达式参数。

要获得完整的图片,您可以查看以下公式:

[HighProfitabilityALLSales] :=
CALCULATE (
SUM ( Sales[SalesAmount] ),
FILTER (
ALL ( Product ),
Product[Unit Price] >= Product[Unit Cost] * 2
))

这一次,我们使用FILTER(ALL(Product))而不是使用FILTER(Product)。 FILTER不会只迭代蓝色产品;它将始终迭代所有产品,并且因为CALCULATE替换颜色上的筛选器,行为如图5-10所示。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第10张图片
图5-10 HighProfitabilityALLSales显示在CALCULATE内有效替换颜色上的过滤器。

HighProfitabilityALLSales始终显示所有高盈利产品的销售额,有效地忽略了先前存在的颜色筛选器。

让我们从第一个例子开始得出一些结论。

  • 您可以在CALCULATE中使用布尔条件,但是为了使用它们,您只能在表达式中引用一列,否则您将收到语法错误。
  • 您可以在CALCULATE中使用FILTER或任何其他表函数作为筛选器参数,在这种情况下,表中的所有列都是新筛选器上下文的一部分。这意味着CALCULATE将替换这些列上的任何现有筛选器。
  • 如果使用FILTER,则CALCULATE将在原始筛选上下文中评估FILTER。另一方面,如果使用布尔条件,则CALCULATE将替换现有的筛选上下文,但仅替换受影响的列。

5.2.2 用复杂条件进行筛选

当您使用多个筛选器时,CALCULATE在创建新筛选上下文时会对其所有筛选条件执行逻辑AND。因此,如果您想筛选Tailspin Toys生产的所有黑色产品,您可以使用如下表达式:

[Calculate Version] :=
CALCULATE (
SUM ( Sales[SalesAmount] ),
Product[Brand] = "Tailspin Toys",
Product[Color] = "Black"
)

因为CALCULATE将这两个条件放在AND中,您可能会认为表达式的这个公式是等价的:

[FILTER Version] :=
CALCULATE (
SUM ( Sales[SalesAmount]),
FILTER (
Product,
AND (
Product[Brand] = "Tailspin Toys",
Product[Color] = "Black"
)))

实际上,这两个表达式是不同的,本节解释了原因。您已经了解了这一点,但由于本主题的复杂性以及本节后面讨论的概念的重要性,值得重复一遍。在带有布尔表达式的公式中,品牌和颜色都会忽略现有的筛选上下文;而在使用FILTER的公式中,两列都考虑了之前存在的筛选上下文(在应用公式之前)。

因此,Calculate Version始终返回黑色Tailspin Toys的销售额,而FILTER Version只返回在之前存在的筛选上下文中已经存在的销售额;否则返回一个空值。您可以在图5-11中观察到此行为。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第11张图片
图5-11两个公式导致不同的计算,即使它们看起来非常相似。

不同之处在于,FILTER会迭代由外部筛选上下文筛选的表。请记住以下公式:

[Sales of Tailspin Toys] :=
CALCULATE (
SUM ( Sales[SalesAmount] ),
Product[Brand] = "Tailspin Toys",
)

相当于下一个:

[Sales of Tailspin Toys] :=
CALCULATE (
SUM ( Sales[SalesAmount] ),
FILTER (
ALL ( Product[Brand] ),
Product[Brand] = "Tailspin Toys",
))

在第二个公式中,通过使用ALL(Product [Brand]),我们明确要求忽略制造商列的当前筛选上下文。我们再怎么强调理解这些公式的重要性也不为过。即使这里使用的表达式用于教育目的,在编写自己的表达式时也会遇到类似的情况,并确保最终会看到奇怪的结果。为了理解公式的行为,您必须了解上下文行为。

在单个列上工作,上述等价可以正常工作。在我们的示例中,我们有两列,您可能会尝试通过以下公式将等效扩展到多列场景:

FilterAll Version :=
CALCULATE (
SUM ( Sales[SalesAmount] ),
FILTER (
ALL ( Product ),
AND (
Product[Brand] = "Tailspin Toys",
Product[Color] = "Black"
)))

此公式不满足您在本节中看到的第一个公式所解决的要求。如果使用ALL(Product),则通过忽略整个表上的筛选上下文来忽略两列上的筛选上下文。但是,通过忽略整个表上的筛选上下文,您仍然会得到不同的行为。为了看到效,我们需要使数据透视表更复杂。在图5-12中,我们在行上添加了类别名称。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第12张图片
图5-12在Product表上使用FILTER和ALL仍然无法解决方案。

如您所见,FilterAll Version忽略整个Product表上的筛选上下文,即使对于Computers类别也显示该值,Calculate Version显示一个空值。原因是Calculate Version仅忽略颜色和品牌名称的筛选上下文,而FilterAll Version忽略整个表上的筛选上下文,从而忽略该类别。

为了找到正确的公式,你必须用列而不是表来考虑。我们既不能将Product表提供给FILTER(因为它包含完整的原始筛选上下文),也不能提供ALL(Product)(因为它忽略了所有筛选器)。相反,我们需要计算一个Product表,我们在其中删除了制造商上的筛选器,但其中任何其他现有筛选器仍处于活动状态。我们已经知道了一个函数,它允许我们以这种非常精细的方式处理筛选上下文:它是CALCULATE。唯一的问题是CALCULATE需要一个返回单个值的表达式,这次我们想要返回一个整表,因为我们需要它作为FILTER函数的参数。幸运的是,有一个CALCULATE的伴侣,而不是返回单个值,返回一个表。它是CALCULATETABLE,将在下一节介绍。

5.2.3 使用CALCULATETABLE

CALCULATETABLE的工作方式与CALCULATE相同。唯一的区别在于结果的类型:CALCULATE计算返回标量值,而CALCULATETABLE计算表表达式并返回表。下一个公式完全按照我们的需要执行:它从Brand和Color中删除筛选上下文,但让其他过滤器在FILTER函数内部流动。

[CalcTable Version] :=
CALCULATE (
SUM ( Sales[SalesAmount] ),
FILTER (
CALCULATETABLE (
Product,
ALL ( Product[Brand] ),
ALL ( Product[Color] )
),
AND (
Product[Brand] = "Tailspin Toys",
Product[Color] = "Black"
)))

如图5-13所示,CalcTable Version中的最后一个公式计算正确的值,该值与Calculate Version返回的值相同。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第13张图片
图5-13使用CALCULATETABLE导致计算正确的值。

这种关于等价结果的题外话很重要,因为了解将布尔筛选器转换为FILTER等效项的正确方法将极大地帮助您处理更复杂的条件。例如,如果您想表达OR条件而不是AND条件,则需要使用此方法。

例如,如果您想计算所有品牌和颜色的总和,其中条件是品牌可以是Tailspin Toys或颜色是黑色(OR条件),那么您需要使用CALCULATETABLE定义它,如下面的代码:

[CalcTable Version OR] :=
CALCULATE (
SUM ( Sales[SalesAmount] ),
FILTER (
CALCULATETABLE (
Product,
ALL ( Product[Brand] ),
ALL ( Product[Color] )
),
OR (
Product[Brand] = "Tailspin Toys",
Product[Color] = "Black"
)))

实际上,使用CALCULATETABLE是一种从Brand和Color中删除筛选器的便捷方法,而不影响其他筛选。因此,使用简单的CALCULATE可以轻松解决具有多列的AND条件,因为CALCULATE会自动将其所有筛选器参数放入AND中。然而,不同列之间的OR条件要复杂得多,因为您不能依赖CALCOULATE的自动AND,并且需要手动编写复杂的DAX代码。

值得注意的是,作为替代公式,您还可以使用以下代码,该代码使用包含两列的ALL函数:

[ALL Version OR] :=
CALCULATE (
SUM ( Sales[SalesAmount] ),
FILTER (
ALL ( Product[Brand], Product[Color] ),
OR (
Product[Brand] = "Tailspin Toys",
Product[Color] = "Black"
)))

后一种方法更加优雅,即使在开始时它也不是很直观。

在本节中,您已经看到,只要您使用多个列,或者在一般情况下使条件更加复杂,结果就会变得难以理解。即使是经验丰富的DAX程序员也经常发现很难把握计算流程。因此,不要被这一节的复杂性所吓倒;只有经验才能引导您走向自然的道路,在那里您将学习如何乍看之下阅读公式。

5.3 理解上下文转换

我们之前曾预料到CALCULATE会执行另一项非常重要的任务:它将任何现有的行上下文转换为等效的筛选上下文。

为了演示该行为,您需要创建一个包含CALCULATE表达式的计算列。计算列始终具有行上下文,因此这将触发上下文转换。例如,您在Product中定义包含以下DAX表达式的计算列:

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

此公式汇总了所有产品的所有清单价格。表达式在行上下文中进行计算,没有筛选上下文,因此它返回表中所有产品的单位总价格,而不是计算它的产品的定价。您可以在图5-14中看到该行为。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第14张图片
图5-14在计算列内计算的SumOfUnitPrice返回单位价格的总计。

现在,您可以使用稍微修改的表达式版本创建新的计算列,这次涉及CALCULATE:

Product[SumOfUnitPriceCalc] = CALCULATE ( SUM ( Product[Unit
Price] ) )

什么?使用单个表达式参数进行CALCULATE?筛选器在哪里?无处。事实上,我们使用最简单的CALCULATE形式。我们之前说过,CALCULATE的唯一强制性参数是第一个,所以在没有任何筛选器的情况下调用CALCULATE是完全可以的。在这种情况下,CALCULATE不会更改现有的筛选上下文与其他条件,但它仍然执行您现在正在学习的行为:它采用现有的行上下文(如果有)并将它们转换为等效的筛选上下文。请注意,所有现有的行上下文都会合并到新的筛选上下文中,我们稍后会详细介绍。

在该示例中,CALCULATE搜索现有行上下文,并发现Product上有一个,因为CALCULATE在定义计算列中运行。 CALCULATE接受此行上下文并将其替换为仅包含当前由行上下文迭代的行的筛选上下文。
我们将此行为称为上下文转换。一般来说,我们说CALCULATE执行上下文转换,将所有行上下文合并为一个新的等效筛选上下文。

在CALCULATE内部,表达式SUM (Product[Unit Price])在只包含当前产品行的筛选上下文中计算其值,这是因为CALCULATE执行了上下文转换。这一次的结果是相同的产品单价,如图5-15所示。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第15张图片
图5-15使用CALCULATE,行上下文已转换为过滤器上下文,从而更改结果。

第一次观察到这种行为时,很难理解为什么CALCULATE执行上下文转换。在您开始使用此特性后,您会爱上它,因为您将能够编写强大的公式。

此外,上下文转换还有另一个非常重要的副作用。您可能还记得筛选上下文和行上下文在关系中的行为方式不同:行上下文不会自动通过关系传递,而筛选上下文会随关系传递。因此,当发生上下文转换时,筛选上下文会自动传递到相关表。

如果您使用以下定义创建两个仍在Product中的新计算列,而不是对列表价格求和,则可以观察到此行为:

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

在图5-16中,您可以看到结果,这显然是不同的。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第16张图片
图5-16 CALCULATE引起的上下文转换会影响相关表的筛选。

如您所见,SalesAmount列包含所有销售的总计,SalesAmountCalc包含当前产品的销售额。 CALCULATE的存在以及Product上行上下文的相关上下文转换将筛选器传播到Sales,仅显示一个产品的销售额。

请注意,执行CALCULATE时,所有活动的上下文都会发生上下文转换。实际上,不同的表上可能有多个行上下文。例如,如果在Product中创建的计算列中使用AVERAGEX迭代客户,则两个行上下文(Product和Customer)都会发生上下文转换,Sales表将接收这两个筛选器。请考虑以下表达式:

Product[SalesWithSUMX] =
AVERAGEX (
Customer,
CALCULATE ( SUM ( Sales[SalesAmount] ) )
)

该公式计算客户购买此产品所花费的平均金额(不是平均价格,而是花费的总金额的平均值)。 CALCULATE中的SUM在筛选上下文中执行,该筛选上下文仅显示当前客户(由AVERAGEX迭代)和当前产品(由计算列评估迭代)的销售额。这里有一个简单的方法来记住这个规则:在CALCULATE内部没有行上下文;仅存在筛选上下文。

5.3.1 理解度量值中的上下文转换

由于DAX的另一个隐藏方面,理解上下文转换非常重要。到目前为止,我们总是使用函数和列在CALCULATE中编写表达式。但是,您也可以编写包含度量值调用的表达式。如果从计算列内部调用度量值,会发生什么?更一般地说,如果从行上下文中调用度量值会发生什么?

例如,您可以通过以下方式定义度量值SumOfSalesAmount:

[SumOfSalesAmount] := SUM ( Sales[SalesAmount] )

然后,您可以使用以下更简单的代码定义SalesWithSUMX计算列:

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

CALCULATE的存在表明发生了上下文转换。问题在于,无论何时从另一个表达式中调用度量值,DAX都会自动将度量值封装在CALCULATE中。因此,前一个表达式的行为与以下表达式相同:

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

这次,公式中没有可见的CALCULATE,但由于DAX添加了自动CALCULATE,因此发生了上下文转换。

这就是为什么总是编写区分列和度量值的代码非常重要的原因。 DAX作者使用的实际标准是避免将表名放在度量值前面,并且总是在列的前面加上表名。实际上,在上一个公式中,SumOfSalesAmount之前缺少表名表明SumOfSalesAmount是一个度量值,因此,您知道上下文转换发生了。

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

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

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

警告
基于VAR的语法更易于阅读和维护(也可能更快)。但是,无论您使用的DAX版本如何,使用不同的语法(也不使用VAR)了解不同的行为非常重要。如果您不理解并掌握自动上下文转换,您可能会花费大量令人沮丧的时间来查看公式,而不能完全理解正在计算的值。

调用度量值时会自动执行上下文转换,并且无法避免它。这意味着在调用度量值时避免上下文转换的唯一方法是扩展其代码。例如,假设您以不同的方式编写了以前的代码,不是使用变量,而是定义一个名为AverageSales的度量值,它表示客户的平均销售额,如下面的代码所示:

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

在突出显示的行中,您使用[AverageSales]计算客户的平均销售额。问题是,这一次您调用的是迭代(SUMX)中的度量值,这使得上下文转换发生。因此,[AverageSales]的结果不是所有客户的平均销售额,而是仅针对您正在迭代的客户的平均销售额。因此,测试将始终失败并且度量值返回BLANK,因为IF永远不会执行真分支。如果要避免上下文转换,则需要扩展度量值的代码,如以下示例所示:

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

用扩展代码替换了度量值调用后,现在SalesMoreThanAverage返回正确的结果。此外,值得注意的是,在这种情况下,在Customer上有两个嵌套的行上下文和三个度量值调用。其中两个通过SUMX计算当前迭代客户的销售额,另一个(AVERAGEX内部)通过AVERAGEX计算当前迭代客户的销售额。

我们将在下一章中广泛使用这些特性,那时我们将开始编写复杂的DAX代码来解决特定的场景。

5.3.2 上下文转换后多少行可见?

上下文转换将行上下文转换为等价的筛选上下文。

这一声明需要澄清。行上下文始终包含单个行,而上下文转换后CALCULATE创建的筛选上下文可能包含多行,而CALCULATE创建的筛选器可能会影响一个或多个列,具体取决于表结构。

如果表具有在模型中定义的主键,则CALCULATE将创建仅筛选主键的筛选上下文。实际上,这样的筛选上下文包含由主键唯一标识的单个行。值得记住的是,可以通过使用表的元数据或通过创建将表作为目标的关系来定义主键。在这两种情况下,上下文转换都将筛选单个列,因为该列是表的标识,只有一行。

如果表没有主键,则上下文转换会在表的所有列上创建一个筛选器。这可能会导致包含一行或多行的筛选器,具体取决于表内容。实际上,如果所有行都不同,则筛选上下文唯一地标识一行。但是,如果表中有相同的行,则所有这些行都将包含在筛选上下文中。

在以下示例中,Wrong Sales和Correct Sales将返回不同的值:

[Sales Amount] := SUMX ( Sales, Sales[Quantity] * Sales[Unit
Price] )
[Wrong Sales] := SUMX ( Sales, [Sales Amount] )
[Correct Sales] := SUMX ( Sales, Sales[Quantity] * Sales[Unit
Price] )

实际上,Wrong Sales会对销售进行迭代,并且对于每一行,它会计算所有相同行的Sales Amount,而Correct Sales则计算每个单独行的数量。因此,如果Sales中存在多个相同的行,则Wrong Sales会产生更高的值。

处理维度表时,这通常不是问题,因为维度总是具有主键。在这种情况下,唯一与当前行相同的行是行本身。在事实表或一般没有主键的表上,您需要考虑您可能有重复的行;否则你可能会得到意想不到的结果。

5.3.3 理解上下文转换的评估顺序

至此,您已经了解了Product表中创建的这两个计算列之间的区别:

Product[SumOfUnitPrice] = CALCULATE ( SUM ( Product[Unit
Price] ) )
Product[SumOfAllUnitPrice] = CALCULATE ( SUM ( Product[Unit
Price] ), ALL ( Product ) )

它们都是计算列,它们都使用CALCULATE函数。因此,两者都发生了上下文转换。

SumOfUnitPrice应仅包含当前行的Unit Price。但是,SumOfAllUnitPrice的值是多少?直观地说,因为有一个ALL(Product),你应该期望该列包含所有单价的总和。这正是它计算的内容。然而,如果您按照我们到目前为止所描述的规则去做,您会发现存在错误。

事实上,ALL(Product)返回整个Product表,有效地从筛选上下文中删除任何筛选器。但是,与此同时,上下文转换应筛选产品并仅显示一行。如果将这两个条件放在AND中,则上下文转换的限制性更大,因此它应该获胜。那么,为什么结果是所有单价的总和,而不是当前的单价?

因为上下文转换创建的筛选上下文与CALCULATE中的条件创建的筛选上下文之间存在优先顺序。CALCULATE先执行上下文转换,稍后应用筛选器。因此,CALCULATE的任何条件都可以覆盖由上下文转换创建的筛选器。

描述这种行为比使用更难。实际上,上面的两个公式可以直观地计算出人们所期望的。然而,重要的是要理解为什么会发生这种情况,最重要的是,CALCULATE中的筛选器会覆盖来自上下文转换的筛选器(换句话说,筛选器参数将在后面应用)。

5.4 变量和计算上下文

在第2章“介绍DAX”中,我们展示了变量的概念,它是通过使用以下表达式创建的:

VAR Denominator = SUMX ( Sales, Sales[SalesAmount] +
Sales[TotalProductCost] )
VAR Numerator = SUM ( Sales[SalesAmount] )
RETURN
DIVIDE ( Numerator, Denominator )

变量可以方便地使代码更容易阅读,并避免多次重复相同的子表达式,但由于它们使用计算上下文的方式,它们具有另一个非常重要的用途。事实上,DAX在您定义它们的计算上下文中计算变量,而不是在使用变量的上下文中。

这个特性对于复杂的公式非常有用,您希望根据以前的计算上下文引用值。让我们看一个例子。想象一下,您希望使用数据透视表来显示类别,并为每个类别显示高销售产品的数量。如果产品的销售额超过该类别总销售额的10%,则将产品定义为高销售额。您可以在图5-17中看到要实现的结果。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第17张图片
图5-17此数据透视表显示每个类别中有多少高销售产品。

使用变量可以非常轻松地计算此度量值。此外,使用变量也使得阅读更简单,如下面的代码所示:

[HighSalesProducts] :=
VAR
TenPercentOfSales = [SalesAmount] * 0.1
RETURN
COUNTROWS (
FILTER (
Product,
[SalesAmount] >= TenPercentOfSales
))

该公式的有趣部分是DAX计算FILTER迭代之外的变量TenPercentOfSales。如果TenPercentOfSales的计算是在迭代内部,那么由于上下文转换,它将计算当前迭代产品的10%的销售额,从而使整个度量值计算失败。相反,DAX计算迭代之外的值,然后在内部使用它,从而可以引用当前筛选上下文之外的表达式的值。如果你要编写没有变量的相同度量值,你应该编写一个表达式,如下所示:

[HighSalesProductsCalculate] :=
COUNTROWS (
FILTER (
Product,
[SalesAmount] >= CALCULATE (
[SalesAmount],
ALL ( Product ),
'Product Category'
) * 0.1
))

后一段代码要难读得多,因为基本上,您需要在迭代器内部重建先前的计算上下文,这不是一件容易的事,即使对于经验丰富的DAX程序员也是如此。实际上,您将仅在第10章中了解前前面表达式的详细信息,其中我们将揭示筛选上下文的所有详细信息。

备注:
如果在阅读上一个示例时,您发现可以使用变量来计算诸如EARLIER等计算上下文之类的内容,那么我们很高兴地欢迎您加入那些理解计算上下文的DAX程序员的精英。如果没有,请不要担心。在第一次阅读本章时,它永远不会发生。书中还有很多内容专门介绍这些复杂的主题,可以帮助您掌握计算上下文所需的技能。

5.5 理解循环依赖

在设计数据模型时,应该注意一个复杂的主题,即公式中的循环依赖关系。在本节中,您将了解循环依赖关系以及如何在模型中避免它们。

在谈到循环依赖之前,有必要讨论简单的线性依赖关系。让我们看一个例子,它包含以下计算列:

Product[Profit] = Product[Unit Price] - Product[Unit Cost]

新计算列取决于同一个表的两列。在这种情况下,我们说列Profit取决于Unit Price和Unit Cost。然后,您可以使用以下公式创建一个新列,例如ProfitPct:

Product[ProfitPct] = Product[Profit] / Product[Unit Price]

很明显,ProfitPct取决于利润和单价。因此,当DAX计算表中的计算列时,它知道它将仅在Profit之后计算ProfitPct。否则,它将无法计算ProfitPct公式的有效值。

线性依赖关系通常不需要担心。在内部,DAX使用它来检测数据模型刷新期间计算列的正确计算顺序。在包含许多计算列的普通数据模型中,计算的依赖性变为复杂的图,同样,引擎可以优雅地处理这个图。

循环依赖是在此图中出现循环时发生的情况。例如,循环依赖关系出现的一个明显情况是,如果您试图将利润的定义修改为这个公式:

Product[Profit] := Product[ProfitPct] * Product[Unit Price]

因为ProfitPct依赖于Profit,并且在这个新公式中,Profit取决于ProfitPct,DAX拒绝修改公式并显示错误“检测到循环依赖”。

到目前为止,您已经从公式的角度了解了什么是循环依赖。也就是说,在不注意表内容的情况下,通过查看表达式检测到依赖项的存在。然而,使用CALCULATE可以引入更微妙和复杂的依赖类型。让我们通过一个示例来展示这个场景,从Product的一列子集开始,如图5-18所示。请注意 ,在本例中 -,我们仅加载了Product表,从模型中删除了所有其他表,以便使场景更加明显。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第18张图片
图5-18 Product表的这一列子集对于理解循环依赖关系非常有用。

我们有兴趣了解使用CALCULATE函数的新计算列的依赖关系列表,如下所示:

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

乍一看,似乎该列仅取决于单价,因为这是公式中使用的唯一列。然而,我们使用CALCULATE将当前行上下文转换为筛选上下文。因为我们没有定义与表的任何关系,并且我们没有为它设置主键,所以当CALCULATE进行上下文转换时,它会筛选表的所有列。如果我们扩展CALCULATE调用的含义,公式实际上是说:对Product表中与ProductKey,Product Name,Unit Cost和Unit Price具有相同值的所有行的单价的值相加。
如果您以这种方式阅读公式,现在很清楚,代码依赖于Product的所有列,因为新引入的筛选上下文将筛选表的所有列。您可以在图5-19中看到结果表。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第19张图片
图5-19您可以在此处看到带有SumOfUnitPrice计算列的Product表。

您可以尝试在同一个表中使用完全相同的公式定义新的计算列。考虑使用以下公式定义NewSumOfUnitPrice,该公式与前一个公式相同。

Product[NewSumOfUnitPrice] = CALCULATE ( SUM ( Product[Unit
Price] ) )

令人惊讶的是,在这一点上,DAX提出了一个错误,称它检测到循环依赖。奇怪的是,它用相同的代码在公式中检测到了以前没有检测到的循环依赖关系。确实发生了一些变化,它是表格中的列数。如果我们能够将NewSumOfUnitPrice添加到表中,我们将达到两个公式具有以下含义的情况:

  • SumOfUnitPrice对Product表中具有相同ProductKey值的所有行的Unit Price值求和,Product Name、Unit Cost、Unit Price和NewSumOfUnitPrice。
  • NewSumOfUnitPrice对Product表中具有相同ProductKey、Product Name、Unit Cost、Unit Price和SumOfUnitPrice值的所有行的Unit Price值求和。

与表中的任何其他列一样,计算列成为CALCULATE引入的筛选上下文的一部分,因此,所有计算列都是依赖项列表的一部分。如果您阅读之前的定义,很明显两个公式之间存在循环依赖关系,这就是为什么DAX拒绝允许创建NewSumOfUnitPrice的原因。

理解这个错误并不容易。然而,找到一个解决方案很简单,即使不是很直观。问题是,如果表没有主键,则任何包含CALCULATE的计算列(或对任何度量值的调用,自动添加CALCULATE)都会从表的所有列(包括计算列)创建依赖关系。如果表具有行标识符(数据库术语中的主键),则情况将有所不同。如果表具有充当行标识符的列,则包含CALCULATE函数的所有列将仅依赖于行标识符,从而将其依赖性列表减少为单个列。

在Product中,ProductKey列可以唯一标识每一行,因为它是主键。为了将Product Key标记为行标识符,有两个选项:

  • 您可以使用ProductKey作为目标列,从任何表创建到Product的关系。执行此操作将确保ProductKey是Product的唯一值。
  • 您可以使用“表行为”属性手动设置ProductKey列的行标识符的属性。

这些操作中的任何一个都告诉DAX引擎该表具有行标识符。在这种情况下,您可以定义NewSumOfUnitPrice列以避免循环依赖,因为使用CALCULATE的两个计算列将仅依赖于新键。

5.6 CALCULATE规则

回顾一下CALCULATE的工作方式很有用。您可以使用这组规则来测试您对CALCULATE的理解:如果您可以阅读并理解所有这些规则,那么您就可以成为真正的DAX大师。

  • CALCULATE和CALCULATETABLE是DAX中唯一直接操作筛选上下文的函数。
  • CALCULATE只有一个必需参数,即要评估的表达式。其他参数(也称为筛选器参数)是用于构建新筛选上下文的筛选器;如果您只想调用CALCULATE来执行上下文转换,则可以省略它们。
  • CALCULATE中的筛选器参数可以有三种形状:
    • (a)布尔条件,例如Product [Color] =“White”。
    • (b)一列的值列表,以单列表的形式,如ALL(产品[颜色])或更复杂的FILTER表达式,如FILTER(ALL(产品[颜色],产品[颜色] = “白色”)。
    • (c)一个表的行列表,如ALL(产品)或更复杂的FILTER条件,如FILTER(ALL(产品),Product [Color] =“White”)。
      使用(a)或(b)编写的条件只能在单个列上运行。
      使用(c)形状写入的条件可以在任意数量的列上工作。
  • CALCULATE的所有筛选器参数都是独立计算的,然后将它们放在逻辑AND中。最后,它们用于确定新创建的筛选上下文。
  • CALCULATE的筛选器参数(从第二个参数开始)在原始筛选上下文中进行计算,它们可以缩小、扩展或替换计算的范围。例如,当使用直接布尔表达式作为参数时,CALCULATE将替换原始筛选上下文,而在传递表上使用FILTER的表达式时,CALCULATE会考虑原始筛选上下文。 CALCULATE的第一个参数(要评估的表达式)在新创建的筛选上下文中进行计算。
  • CALCULATE的上下文转换和筛选器参数之间存在优先顺序。 CALCULATE在执行上下文转换后应用筛选器。因此,筛选器可以覆盖由转换创建的上下文。

5.7 介绍ALLSELECT

当您希望使用数据透视表中的页面过滤器或切片器选择作为参数之一来执行计算时,ALLSELECTED是一个非常有用的函数。例如,想象一下,您想要构建一个报告,如图5-20所示。

DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第20张图片
图5-20数据透视表中显示的百分比是根据显示的总数计算的,而不是所有颜色的总和。

在此报告中,您将显示当前行占列总数的百分比。使这个百分比难以计算的原因是产品颜色既用作切片器(在本例中,我们删除了一些可用的颜色),也用于行。如果你运用目前学到的知识,你可以试试这个公式:

[SalesPct] :=
DIVIDE (
[Sales Amount],
CALCULATE (
[Sales Amount],
ALL ( Product[Color] )
))

使用ALL (Product[Color])可以从Color中删除约束,并尝试在列级别上计算总数。不幸的是,ALL删除了所有约束,包括来自行和来自切片器的约束,从而导致错误的百分比。在图5-21中,您可以在数据透视表中查看公式的结果,其中总计不会显示100%,而是显示较小的值。


DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第21张图片
图5-21使用ALL计算的百分比不正确,因为它是针对所有颜色的百分比。

这里的问题是你计算所有颜色的分母,即使用户只在切片器中选择了一些颜色。对于每一行,您计算该行与分母的百分比,该分母大于数据透视表中显示的总数。

这里需要的是一个函数,它不返回所有颜色,只返回在原始筛选上下文中选择的颜色,即完整的主表之一。我们称这种计算为可视总计,因为它使用的是用户可见的总计,而不是完整数据模型的总计。这里使用的函数是ALLSELECTED。如果你这样写公式:

[SalesPct] :=
DIVIDE (
[Sales],
CALCULATE (
[Sales],
ALLSELECTED ( Product[Color] )
))

结果将是本节开头所示的正确结果。

ALLSELECTED仅返回原始筛选上下文中可见的值,即数据透视表之一。换句话说,ALLSELECTED忽略数据透视表的行和列上的筛选器,并考虑用于计算总计的筛选器。

您可以使用三种不同类型的参数调用ALLSELECTED:

  • 单列 在ALLSELECTED(Product [Color])中,返回最初选择的颜色。
  • 全表 与在ALLSELECTED ( Product )中一样,它将对表的所有列执行ALLSELECTED操作,返回最初选择的所有行。
  • 没有参数 您也可以使用没有参数的ALLSELECTED(),它将对数据模型中的所有表执行ALLSELECTED,从而可以计算数据透视表的总计,行和列上没有筛选器。

您将主要使用ALLSELECTED以非常动态的方式计算百分比和比率。在第10章中,我们将更深入地描述ALLSELECTED,它隐藏了一些复杂性,,我们将在后面讨论高级评估上下文时讨论这些复杂性。

5.8 理解 USERELATIONSHIP

CALCULATE提供的另一个功能是在计算表达式期间激活关系。实际上,正如您所知,数据模型可能包含活动关系和非活动关系。您可能在模型中具有非活动关系,例如,您在两个表之间存在许多关系,并且只能保留一个活动关系。

例如,您可能在销售表中存储了每个订单的订单日期和交货日期。通常,您希望根据订单日期执行销售分析,但是,对于某些特定度量值,您要考虑交货日期。在这种情况下,您在Sales和Date之间创建两个关系:一个基于OrderDateKey,另一个基于DeliveryDateKey。一次只能激活其中一个,因为您通常按订单日期分析销售,所以您保持与OrderDateKey的关系处于激活状态,而另一个处于非激活状态。然后,您要创建一个度量值,该度量值显示给定日期的交付值,以便将其与订单值进行比较。此新度量值(已交付金额)应使用非活动关系计算销售额,同时停用与订单日期的关系。

在这种情况下,您可以将CALCULATE与USERELATIONSHIP关键字一起使用,如下面的代码所示:

[Delivered Amount] :=
CALCULATE (
[Sales Amount],
USERELATIONSHIP ( Sales[DeliveryDateKey], Date[DateKey] )
)

DeliveryDateKey和DateKey之间的关系将在[Sales Amount]的计算期间激活,同时,与OrderDateKey的关系将被停用。在图5-22中,您可以看到一个数据透视表,显示基于OrderDateKey的Sales Amount与新的Delivered Amount度量值之间的不同值。


DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE_第22张图片
图5-22该图说明了有序销售和交付销售之间的差异。

使用USERELATIONSHIP激活关系时,您需要了解一个非常重要的方面:在调用表引用时定义关系,而不是在调用RELATED或其他关系函数时定义关系。

例如,如果要计算2007年交付的所有金额,则此公式将不起作用:

[Delivered Amount in 2007] :=
CALCULATE (
[Sales Amount],
FILTER (
Sales,
CALCULATE (
RELATED ( Date[CalendarYear] ),
USERELATIONSHIP ( Sales[DeliveryDateKey], Date[DateKey] )
) = 2007
)
)

原因是FILTER迭代Sales表(调用Sales表),然后计算条件。在计算条件期间,它会更改活动关系,然后调用RELATED。但是,Sales和Date之间的关系已在调用Sales时定义,而不是在使用RELATED时定义。

如果要执行上一个计算,则需要按以下方式重写度量值:

[Delivered Amount in 2007] :=
CALCULATE (
[Sales Amount],
FILTER (
CALCULATETABLE (
Sales,
USERELATIONSHIP( Sales[DeliveryDateKey], Date[DateKey] )
),
RELATED ( Date[Calendar Year] ) = 2007
))

在后一个公式中,CALCULATE激活所需关系后调用Sales。因此,在FILTER内部调用RELATED会发生与DeliveryDateKey活动的关系。

这种行为使得在计算列表达式中使用非默认关系成为一项复杂的操作,因为表的调用隐含在计算列定义中,因此您无法控制它,并且您无法使用CALCULATE和USERELATIONSHIP更改该行为。

你可能感兴趣的:(DAX 权威指南 | 05 理解CALCULATE 和 CALCULATETABLE)