第5章了解 CALCULATE 和 CALCULATETABLE
在本章中,我们将继续探索DAX语言的强大功能,并详细说明一个函数:CALCULATE。相同的注意事项适用于 CALCULATETABLE,它计算并返回表而不是标量值。为了简单起见,我们将在示例中引用 CALCULATE,但是请记住 CALCULATETABLE 显示相同的行为。
CALCULATE 是DAX中最重要,最有用和最复杂的函数,因此值得用一整章来讲解。该函数本身易于学习;它仅执行一些任务。复杂性来自于以下事实:CALCULATE 和 CALCULATETABLE 是DAX中唯一可以创建新筛选上下文的函数。因此,尽管它们是简单的函数,但在公式中使用 CALCULATE 或 CALCULATETABLE 会立即增加其复杂性。
本章与上一章一样艰难。我们建议您仔细阅读它,对 CALCULATE 有一个大致的了解,然后继续学习本书的其余部分。一旦您对某个公式感到迷惑,请回到本章并从头开始重新阅读。每次阅读时,您都会有新的收获。
CALCULATE 和 CALCULATETABLE介绍
上一章描述了两个评估上下文:行上下文和筛选上下文。行上下文自动存在于计算列中,并且可以使用迭代函数以编程方式创建行上下文。另一方面,筛选上下文是由报表创建的,我们还没有描述如何以编程方式创建筛选上下文。CALCULATE 和CALCULATETABLE 是操作筛选上下文所需的唯一函数。确实,CALCULATE 和CALCULATETABLE 是可以通过操作现有筛选上下文创建新筛选上下文的唯一函数。从这里开始,我们将仅显示基于CALCULATE 的示例,但请记住,对于返回表的 CALCULATETABLE DAX表达式可以执行相同的操作。在本书第12章,有更多使用CALCULATETABLE 的示例。
创建筛选上下文
我们将通过一个实际示例介绍为什么要创建新的筛选上下文。如以下各节所述,编写代码而无法创建新的筛选上下文会导致冗长且难以理解的代码。下面的示例,说明创建新的筛选上下文如何极大地改善最初看起来相当复杂的代码。
Contoso 是一家在世界各地销售电子产品的公司。有些产品被冠以 Contoso 品牌,其他产品具有不同的品牌。要求在报告中比较 Contoso 品牌产品和竞争对手的毛利和毛利率。报告的第一部分需要进行以下计算:
Sales Amount := SUMX ( Sales, Sales[Quantity] * Sales[Net Price] )
Gross Margin := SUMX ( Sales, Sales[Quantity] * ( Sales[Net Price] - Sales[Unit Cost] ) )
GM % := DIVIDE ( [Gross Margin], [Sales Amount] )
DAX的一个出色的方面是,可以在现有度量值之上构建更复杂的计算。实际上,您可以在GM%的定义中欣赏到这一点,GM%是 Sales 表一个计算毛利率的度量值。GM%调用两个事先定义的度量值,将它们相除。如果已经有一个计算值的度量值,则可以调用该度量值,而不用重写完整的代码。
使用上面定义的三个度量值,可以构建第一个报告,如图5-1所示。
图5-1 通过这三个度量值可对不同类别毛利的快速了解
构建报告的下一步更加复杂。实际上,我们想要的最终报告是图5-2中的报告,该报告显示了另外两列:Contoso 品牌产品的毛利和毛利率(以数量和百分比表示)。
图5-2 报告的最后两列显示了 Contoso 品牌产品的毛利和毛利率
运用到目前为止所获得的知识,您已经可以为这两个度量值编写代码。因为要求是将计算范围限制为一个品牌,所以一种解决方案是使用 FILTER 将毛利的计算范围限制在Contoso 产品:
Contoso GM :=
VAR ContosoSales = -- Saves the rows of Sales which are related
FILTER ( -- to Contoso-branded products into a variable
Sales,
RELATED ( 'Product'[Brand] ) = "Contoso"
)
VAR ContosoMargin = -- Iterates over ContosoSales
SUMX ( -- to only compute the margin for Contoso
ContosoSales,
Sales[Quantity] * ( Sales[Net Price] - Sales[Unit Cost] )
)
RETURN
ContosoMargin
ContosoSales 变量包含与所有 Contoso 品牌产品相关的 Sales 表的行。一旦计算出变量,SUMX就会在ContosoSales上进行迭代以计算毛利。由于迭代位于 Sales 表上,而筛选位于 Product 表上,因此需要使用 RELATED 来检索 Sales 表中每一行的相关产品。以类似的方式,可以通过重复运用两次 ContosoSales 变量来计算 Contoso 的毛利率:
Contoso GM % :=
VAR ContosoSales = -- Saves the rows of Sales which are related
FILTER ( -- to Contoso-branded products into a variable
Sales,
RELATED ( 'Product'[Brand] ) = "Contoso"
)
VAR ContosoMargin = -- Iterates over ContosoSales
SUMX ( -- to only compute the margin for Contoso
ContosoSales,
Sales[Quantity] * ( Sales[Net Price] - Sales[Unit Cost] )
)
VAR ContosoSalesAmount = -- Iterates over ContosoSales
SUMX ( -- to only compute the sales amount for Contoso
ContosoSales,
Sales[Quantity] * Sales[Net Price]
)
VAR Ratio =
DIVIDE ( ContosoMargin, ContosoSalesAmount )
RETURN
Ratio
Contoso GM% 的代码更长一些,但是,从逻辑的角度来看,遵循的模式与Contoso GM相同。尽管这些方法有效,但DAX最初的优雅明显已经丧失。实际上,该模型已经包含计算毛利和计算毛利率的度量值。但是,由于需要筛选新的数值,因此我们必须重写表达式以添加条件。
值得强调的是,基本度量值 Gross Margin 和GM%已经可以计算 Contoso 的值。从图5-2中可以看到,Contoso 的 Gross Margin (毛利)等于3,877,070.65,GM%(毛利率)等于52.73%。如图5-3所示,通过按品牌划分的基本度量值的 Gross Margin 和 GM% 可以获得相同的数字。
图5-3 按品牌划分时,基本度量值将计算 Contoso 的毛利和毛利率
在突出显示的单元格中,报告创建的筛选上下文正在筛选 Contoso 品牌。筛选上下文筛选模型。由于将Sales 链接到 Product 的关系,Product [Brand] 列上的筛选上下文将筛选 Sales 表。因为筛选上下文对整个模型起作用,所以使用筛选上下文可以间接筛选表。
如果我们可以使DAX通过以编程方式,创建仅筛选 Contoso 品牌产品的筛选上下文,来计算毛利度量值,那么我们对后两个度量值的实现将容易得多。这可以通过 CALCULATE 来实现。
CALCULATE 的完整描述将在本章后面介绍。首先,我们来看 CALCULATE 的语法:
CALCULATE ( Expression, Condition1, ... ConditionN )
CALCULATE 可以接受任意数量的参数。唯一的必需参数是第一个,即要求值的表达式。第一个参数之后的条件称为筛选参数。CALCULATE 基于一组筛选参数创建一个新的筛选上下文。一旦计算出新的筛选上下文,CALCULATE会将其应用于模型,然后继续对表达式进行求值。因此,通过利用CALCULATE,Contoso Margin 和 Contoso GM% 的代码变得更加简单:
Contoso GM :=
CALCULATE (
[Gross Margin], -- Computes the gross margin
'Product'[Brand] = "Contoso" -- In a filter context where brand = Contoso
)
Contoso GM % :=
CALCULATE (
[GM %], -- Computes the gross margin percentage
'Product'[Brand] = "Contoso" -- In a filter context where brand = Contoso
)
欢迎回来,简洁优雅!通过创建一个强制使品牌成为 Contoso 的筛选上下文,人们可以依靠现有的度量值并更改其行为,而不必重写度量值的代码。
CALCULATE 允许您通过在当前上下文中操作筛选,来创建新的筛选上下文。如您所见,这导致了简单而优雅的代码。在下节中,我们将对 CALCULATE 的行为提供完整且更正式的定义,详细描述 CALCULATE 的功能以及如何利用其功能。确实,就目前而言,我们示例有一些难度。Contoso 度量值的初始定义在语义上不等同于最终定义,有一些差异需要很好地理解。
CALCULATE 介绍
既然您已经初步了解了CALCULATE,现在该开始学习此函数的详细信息了。如前所述,CALCULATE 是唯一可以修改筛选上下文的DAX函数。请记住,当我们提到 CALCULATE 时,我们还包括 CALCULATETABLE。CALCULATE 不会修改筛选上下文:它是通过将其筛选参数与现有筛选上下文合并来创建新的筛选上下文。一旦 CALCULATE 结束,它的筛选上下文将被丢弃,先前的筛选上下文将再次生效。
我们介绍了CALCULATE 的语法为:
CALCULATE ( Expression, Condition1, ... ConditionN )
第一个参数是CALCULATE 将计算的表达式。在计算表达式之前,CALCULATE 将计算筛选参数,并使用它们来操纵筛选上下文。
关于CALCULATE 的第一件事要注意的是,筛选参数不是布尔条件:筛选参数是表。当将布尔条件用作 CALCULATE 的筛选参数时,DAX都会将其转换为值表。
在上一节中,我们使用了以下代码:
Contoso GM :=
CALCULATE (
[Gross Margin], -- Computes the gross margin
'Product'[Brand] = "Contoso" -- In a filter context where brand = Contoso
)
使用布尔条件只是完整 CALCULATE 语法的快捷方式。这称为语法糖。它是这样写的:
Contoso GM :=
CALCULATE (
[Gross Margin], -- Computes the gross margin
FILTER ( -- Using as valid values for Product[Brand]
ALL ( 'Product'[Brand] ), -- any value for Product[Brand]
'Product'[Brand] = "Contoso" -- which is equal to "Contoso"
)
)
这两种语法是等效的,在性能或语义上没有区别。话虽这么说,特别是当您第一次学习CALCULATE 时,筛选参数始终用表的形式是很有用的,这样 CALCULATE 的行为更加明显,一旦习惯了CALCULATE 语义,使用语法的精简版本就更加方便了,精简版本更短,更容易阅读。
筛选参数是一个表,即一个值列表。作为筛选参数的表,定义了在表达式求值期间,对于列而言可见的值列表。在上一个示例中,FILTER 返回的表仅一行,该行 Product [Brand] 的值等于 “Contoso”。换句话说,“Contoso” 是 CALCULATE 对 Product [Brand] 列显示的唯一值。因此,CALCULATE 会在模型中筛选仅包含 Contoso品牌的产品。考虑以下两个定义:
Sales Amount :=
SUMX (
Sales,
Sales[Quantity] * Sales[Net Price]
)
Contoso Sales :=
CALCULATE (
[Sales Amount],
FILTER (
ALL ( 'Product'[Brand] ),
'Product'[Brand] = "Contoso"
)
)
Contoso Sales 的 CALCULATE 中的 FILTER 的筛选参数ALL(Product [Brand] )扫描所有产品品牌; 因此,产品品牌上任何以前存在的筛选都会被新筛选覆盖。在按品牌划分的报表中使用度量值时,这一点更加明显。在图5-4中:Contoso Sales列中 的所有行/品牌报告的销售额与Contoso 的 Sales Amount 的价值相同。
图5-4 Contoso Sales 用新的 Contoso 筛选覆盖了现有筛选
在每一行中,报告都会创建一个相关品牌的筛选上下文。例如,在Litware的行中,报告创建的原始筛选上下文仅包含显示Litware产品的筛选。然后,CALCULATE 评估其筛选参数,该参数返回仅包含 Contoso 的表。新创建的筛选将覆盖同一列上先前存在的筛选。图5-5中可看到该过程的图形表示。
图5-5 Litware 的筛选被 CALCULATE 评估的 Contoso 筛选覆盖
CALCULATE不会覆盖整个原始筛选器上下文。它仅替换 filter 参数中包含的列上先前存在的筛选。实际上,如果将报表更改为按 Product [Category] 切片,结果将有所不同,如图5-6所示。
图5-6 如果报表按类别筛选,则品牌上的筛选将被合并并且不会覆盖
现在,报告正在筛选 Product [Category],而 CALCULATE 在 Product [Brand] 上应用筛选以评估 Contoso Sales 度量值。这两个筛选不适用于 Product 的同一列,因此,不会发生覆盖,并且两个筛选可以作为新的筛选上下文一起工作。结果,每个单元格都显示了给定类别的 Contoso 的 Sales Amount 。如图5-7所示。
图5-7 CALCULATE 覆盖同一列上的筛选。如果它们在不同的列上,它将合并筛选
现在您已经了解了CALCULATE的基础知识,我们可以总结一下它的语义:
- CALCULATE 复制当前筛选上下文。
- CALCULATE 评估每个筛选参数,并为每个条件生成指定列的有效值列表。
- 如果两个或多个筛选参数影响同一列,则使用AND运算符(或使用数学术语中的交集)将它们合并在一起。
- CALCULATE 使用新筛选替换模型中列上的现有筛选。如果一列已具有筛选,则新的筛选将替换现有的筛选。另一方面,如果该列没有筛选,则CALCULATE 将新筛选添加到筛选上下文中。
- 准备好新的筛选上下文后,CALCULATE将筛选上下文应用于模型,并计算第一个参数:表达式。最后,CALCULATE还原原始筛选上下文,返回计算结果。
注意
CALCULATE 执行另一项非常重要的任务:将任何现有的行上下文转换为等效的筛选上下文。在本章后面的“了解上下文转换”,您可以找到关于此主题的更详细的讨论。如果您需要重新阅读本节,请记住:CALCULATE 从现有的行上下文中创建一个筛选上下文。
CALCULATE 接受两种类型的筛选:
- 值列表,以表表达式的形式。在这种情况下,提供要在新的筛选上下文中显示的值的确切列表。筛选可以是具有任意数量列的表。筛选中仅考虑不同列中的现有值组合。
- 布尔条件,例如 Product [Color] =“ White”。这些筛选需要在单个列上工作,因为结果需要是单列的值列表。这种类型的筛选参数也称为谓词。
如果您将语法与布尔条件一起使用,则DAX会将其转换为值列表。因此,无论何时编写此代码:
Sales Amount Red Products :=
CALCULATE (
[Sales Amount],
'Product'[Color] = "Red"
)
DAX将表达式转换为:
Sales Amount Red Products :=
CALCULATE (
[Sales Amount],
FILTER (
ALL ( 'Product'[Color] ),
'Product'[Color] = "Red"
)
)
因此,只能在具有布尔条件的筛选参数中引用一列。DAX需要在 FILTER 函数中检测要迭代的列,该函数是在后台自动生成的。如果布尔表达式引用了两列或更多列,则必须显式编写 FILTER 迭代,这将在本章后面学习。
用 CALCULATE 计算百分比
现在我们已经介绍了CALCULATE,我们可以使用它来定义几个计算。本节的目的是使您注意一些有关CALCULATE的细节,这些细节乍一看并不明显。在本章的后面,我们将介绍CALCULATE的更多高级的方面。现在,我们重点介绍使用 CALCULATE 时可能遇到的一些问题。
经常出现的模式是百分比模式。使用百分比时,准确定义所需的计算非常重要。在这组示例中,您将了解CALCULATE 和 ALL 函数的不同用法是如何提供不同的结果的。
从简单的百分比计算开始。要构建以下报告,以显示销售额及其占总计的百分比。可以在图5-8中看到想要获得的结果。
图5-8 销售百分比显示当前类别相对于总计的百分比
为了计算百分比,需要将当前筛选上下文中的 Sales Amount 值除以忽略现有筛选的筛选上下文中的 Sales Amount 的值。实际上,Audio 的1.26%是384,518.16除以30,591,343.98得来的。
在报告的每一行中,筛选上下文已经包含当前类别。因此,对于 Sales Amount ,结果将按给定类别自动筛选。比率的分母需要忽略当前的筛选上下文,以便评估总计。由于 CALCULATE 的筛选参数是表,因此提供一个表函数就足够了,该函数将忽略类别上的当前筛选上下文,并始终返回所有类别,而与任何筛选无关。您先前已了解到此函数为 ALL。查看以下度量值定义:
All Category Sales :=
CALCULATE ( -- Changes the filter context of
[Sales Amount], -- the sales amount
ALL ( 'Product'[Category] ) -- making ALL categories visible
)
ALL从筛选上下文中删除 Product [Category] 列上的筛选。因此,在报告的任何单元格中,它都会忽略 Category 中存在的任何筛选。效果是删除了报告行所应用的 Category 筛选。查看图5-9中的结果。您可以看到,报表的每一行度量值 All Category Sales 一直返回相同的值—Sales Amount 的总计。
图5-9 ALL删除了Category上的筛选,因此CALCULATE定义了一个筛选上下文,而Category上没有任何筛选
度量值 All Category Sales 本身并没有用。用户不太可能希望创建一个在所有行上显示相同值的报告。但是,该值非常适合作为我们要计算的百分比的分母。计算百分比的公式可以这样写:
Sales Pct :=
VAR CurrentCategorySales = -- CurrentCategorySales contains
[Sales Amount] -- the sales in the current context
VAR AllCategoriesSales = -- AllCategoriesSales contains
CALCULATE ( -- the sales amount in a filter context
[Sales Amount], -- where all the product categories
ALL ( 'Product'[Category] ) -- are visible
)
VAR Ratio =
DIVIDE (
CurrentCategorySales,
AllCategoriesSales
)
RETURN
Ratio
如本例所示,表函数和 CALCULATE 结合起来使用可以轻松编写有用的度量值。在本书中我们经常使用这种技术,因为它是DAX中的主要计算工具。
注意
ALL 用作CALCULATE 的筛选参数时,具有特定的语义。实际上,它不会用值替换筛选上下文。而是,CALCULATE 使用 ALL 从筛选上下文中删除类别列上的筛选。此行为有些复杂难以遵循的副作用,不属于本节介绍内容。我们将在本章稍后详细介绍。
在编写百分比计算时,请注意小细节,这一点很重要。实际上,如果按类别对报告进行切片,则百分比效果很好。该代码将筛选从类别中删除,但它不涉及任何其他现有筛选。如果报告中添加了其他筛选,则结果可能不完全是您想要的结果。例如,图5-10中的报告,其中我们在报告的行中添加了 Product [Color] 列作为第二级明细。
图5-10 将 Color 添加到报告中会在 Color 级别上产生意外结果
从百分比来看,类别级别的值是正确的,而颜色级别的值是错误的。事实上,颜色百分比并没有累加起来——既没加入类别级别,也没加入100%。要了解这些值的含义以及如何对其进行评估,始终专注于一个单元格并准确了解筛选上下文发生了什么是很有帮助的。专注于图5-11。
图5-11 Product [Category] 上的 ALL 删除了类别中的筛选,但保留了完整的颜色筛选
该图将代码、报告和符号组合在一起,以阐明 Product [Category] 上的 ALL 删除了 Category 上的筛选,但 Color上的筛选保持不变。
报告创建的原始筛选上下文既包含 Category 筛选,也包含 Color 筛选。 Product[Color] 上的筛选不会被 CALCULATE 覆盖,只会删除 Product[Category] 筛选。结果,最终的筛选上下文仅包含 Color。因此,比率的分母包含任何类别所有给定颜色(黑色)产品的销售额。
计算错误不是 CALCULATE 的意外行为。问题在于公式被设计为专门用于类别中的筛选,而其他任何筛选都保持不变。在另一个报告中,相同的公式非常有意义。看一下如果切换列的顺序,生成一个报告,该报告先按颜色进行切片,然后按类别进行分类,会发生什么情况,如图5-12所示。
图5-12中的报告更有意义。该度量值计算出的结果相同,但是由于报表的布局,它更加直观。显示的百分比是给定颜色内各类别的百分比。一个一个颜色,百分比总和为100%。
换句话说,当要求用户计算百分比时,应特别注意确定百分比的分母。CALCULATE 和 ALL 是要使用的主要工具,但是公式的规范取决于业务需求。
回到示例:目标是修正计算,以便针对类别或颜色的筛选计算百分比。有多种执行操作的方法,这些方案导致的结果略有不同,值得深入研究。
一种解决方案是让CALCULATE 从类别和颜色中删除筛选。向 CALCULATE 添加多个筛选参数可以实现此目标:
Sales Pct :=
VAR CurrentCategorySales =
[Sales Amount]
VAR AllCategoriesAndColorSales =
CALCULATE (
[Sales Amount],
ALL ( 'Product'[Category] ), -- The two ALL conditions could also be replaced
ALL ( 'Product'[Color] ) -- by ALL ( 'Product'[Category], 'Product'[Color] )
)
VAR Ratio =
DIVIDE (
CurrentCategorySales,
AllCategoriesAndColorSales
)
RETURN
Ratio
后一版本的 Sales Pct 与包含颜色和类别的报表配合使用时很好,但是仍然受到与前一版本类似的局限。虽然它会产生正确的颜色和类别百分比(如图5-13所示),但是一旦将其他列添加到报表中,它就会失败。
图5-13 将ALL应用于产品类别和颜色,现在百分比加起来是正确的
在报告中添加另一列,将产生与迄今为止所遇到到的不一致相同的问题。如果用户想要删除Product 表上的所有筛选来创建一个百分比,仍然可以使用 ALL 函数将整个表作为参数传递:
Sales Pct All Products :=
VAR CurrentCategorySales =
[Sales Amount]
VAR AllProductSales =
CALCULATE (
[Sales Amount],
ALL ( 'Product' )
)
VAR Ratio =
DIVIDE (
CurrentCategorySales,
AllProductSales
)
RETURN
Ratio
Product 上的 ALL 将删除 Product 的任何列上的所有筛选。在图5-14中,您可以看到该计算的结果。
图5-14 Product 表上使用的 ALL 将删除 Product 所有列中的筛选
到目前为止,您已经看到,通过将 CALCULATE 和 ALL 一起使用,可以从一列,多个列或整个表中删除筛选。CALCULATE 的真正功能在于它提供了多个操作筛选上下文的选项,并且其功能并不止于此。实际上,可能还希望对不同表中的列进行切片来分析百分比,例如,如果按产品类别和客户所在洲对报告进行了切片,那么我们创建的最后一个度量值还不完美,如图5-15所示。
图5-15对多个表的列进行切片仍然显示意外结果
此时,问题很明显。分母处的度量值从 Product 中删除了所有筛选,但该筛选保留在 Customer [Continent] 上。因此,分母计算给定洲所有产品的总销售额。
与前面的方案一样,可以通过将多个筛选作为 CALCULATE 的参数来从多个表中删除该筛选:
Sales Pct All Products and Customers :=
VAR CurrentCategorySales =
[Sales Amount]
VAR AllProductAndCustomersSales =
CALCULATE (
[Sales Amount],
ALL ( 'Product' ),
ALL ( Customer )
)
VAR Ratio =
DIVIDE (
CurrentCategorySales,
AllProductAndCustomersSales
)
RETURN
Ratio
通过在两个表上使用 ALL,现在 CALCULATE 从两个表中删除了筛选。如预期的那样,结果是一个正确累加的百分比,如图5-16所示。
图5-16 在两个表上使用ALL将同时删除两个表的筛选上下文
与两列一样,两个表也面临相同的挑战。如果用户将第三表中的另一列添加到上下文中,则该度量值将不会从第三表中删除筛选。当他们想要从任何可能影响计算的表中删除筛选时,一种可行的解决方案是从事实表本身中删除任何筛选。在我们的模型中,事实表是 Sales。不管哪个筛选与 Sales 表进行交互,此度量值都可以计算累加百分比:
Pct All Sales :=
VAR CurrentCategorySales =
[Sales Amount]
VAR AllSales =
CALCULATE (
[Sales Amount],
ALL ( Sales )
)
VAR Ratio =
DIVIDE (
CurrentCategorySales,
AllSales
)
RETURN
Ratio
此度量值利用关系从可能筛选 Sales 的任何表中删除筛选。现在,我们无法详细解释其工作方式,因为它利用了扩展表,我们在第14章 “高级DAX概念” 中对此进行了介绍。您可以通过查看图5-17来了解其行为,在图5-17中,我们从报告中删除了金额,并在各列上添加了日历年。请注意,日历年属于日期表,该度量值未使用。但是,删除日期筛选是从 Salse 中删除筛选的一部分。
图5-17 事实表上的 ALL 也会从相关表中删除所有筛选
在结束百分比这一长期练习之前,我们想展示筛选上下文操作的另一个最终示例。正如您在图5-17中看到的那样,百分比始终与合计相对,完全符合预期。如果目标是仅计算占当年总计的百分比,该怎么办?在这种情况下,需要仔细准备由 CALCULATE 创建的新筛选上下文。实际上,分母需要计算销售总额,而不考虑除当前年度以外的任何筛选条件。这需要两个操作:
- 从事实表中删除所有筛选
- 恢复年度筛选
请注意,这两个条件是同时应用的,尽管看起来这两个步骤是一个接一个地进行的。您已经学习了如何从事实表中删除所有筛选。最后一步是学习如何还原现有筛选。
注意
本节的目的是说明操作筛选上下文的基本技术。在本章的后面,您将看到另一种更简单的方法,可以通过使用 ALLSELECTED 来解决这一特定要求—占可见总数的百分比。
在第3章 “使用基本表函数” 中,您学习了 VALUES 函数。VALUES 返回当前筛选上下文中的列的值列表。由于 VALUES 的结果是一个表,因此可以将其用作 CALCULATE 的筛选参数。结果, CALCULATE 在给定列上应用了一个筛选,将其值限制为 VALUES 返回的值。看下面的代码:
Pct All Sales CY :=
VAR CurrentCategorySales =
[Sales Amount]
VAR AllSalesInCurrentYear =
CALCULATE (
[Sales Amount],
ALL ( Sales ),
VALUES ( 'Date'[Calendar Year] )
)
VAR Ratio =
DIVIDE (
CurrentCategorySales,
AllSalesInCurrentYear
)
RETURN
Ratio
一旦在报告中使用该度量值,则该度量值每年占比为100%,并且仍将计算除年份以外其他筛选的百分比。您会在图5-18中看到这一点。
图5-18 通过使用VALUES,可以恢复筛选器上下文的一部分,并从原始筛选器上下文读取它。
图5-19 描述了这个复杂公式的全部行为。
这是该图的回顾:
- 包含4.22%(2007年手机销售量)的单元格具有一个筛选器上下文,该筛选器上下文筛选CY 2007 的手机。
- CALCULATE 有两个筛选参数:ALL(Sales)和 VALUES(Date [Calendar Year])。
- ALL(Sales)从 Sales 表中删除筛选。
- VALUES(Date [Calendar Year])在原始筛选上下文中评估 VALUES 函数,该函数仍受列中 CY 2007 的影响。因此,它返回当前筛选上下文中唯一可见的年份,即 CY 2007。
CALCULATE 的两个筛选参数将应用于当前筛选上下文,从而导致一个筛选上下文仅包含 Calendar Year上的筛选。分母仅在 CY 2007 的筛选上下文中计算总销售额。
清楚理解 CALCULATE 的筛选参数是在调用 CALCULATE 的原始筛选上下文中评估的,这一点至关重要。实际上,CALCULATE 会更改筛选上下文,但这仅在评估筛选参数之后发生。
在表上使用 ALL,紧接着在列上使用 VALUES ,是一种用于用同一列上的筛选替换筛选上下文的技术。
注意
前面的示例也可以通过使用 ALLEXCEPT 获得。ALL / VALUES 的语义与 ALLEXCEPT 不同。在第10章 “使用筛选上下文” 中,您将看到 ALLEXCEPT 与 ALL / VALUES 技术之间差异的完整描述。
如您在这些示例中所见,CALCULATE 本身并不是一个复杂的函数。它的行为很容易描述。同时,一旦您开始使用 CALCULATE,代码的复杂性就会变得很高。确实,您需要专注于筛选上下文并确切了解 CALCULATE 如何生成新的筛选上下文。一个简单的百分比就隐藏了很多复杂性,而复杂性全在细节中。在真正掌握评估上下文的处理之前,DAX有点神秘。释放语言全部功能的关键在于掌握评估上下文。此外,在所有这些示例中,我们只需要管理一个CALCULATE。在复杂的公式中,由于存在许多 CALCULATE 实例,因此在同一代码中具有四个或五个不同的上下文并不常见。
整个关于百分比的本章节至少阅读两次,这是一个好主意。根据我们的经验,二读要容易得多,它使您可以专注于代码的重要方面。我们想展示这个例子,以强调理论的重要性。代码的微小变化对公式计算出的数字有重要影响。在您进行了二读之后,请继续进行下一节,下节我们将重点放在理论上而不是在实际示例上。
KEEPFILTERS简介
在前面的部分中,您了解到 CALCULATE 的筛选参数会覆盖同一列上任何以前存在的筛选。因此,无论 Product [Category] 上是否存在任何先前的筛选,以下度量值都会返回 Audio 的销售:
Audio Sales :=
CALCULATE (
[Sales Amount],
'Product'[Category] = "Audio"
)
正如您在图5-20中看到的那样,Audio 的值在报表的所有行上都重复。
图5-20 Audio Sales 始终显示音频产品的销售。
CALCULATE 会覆盖应用新筛选的列上的现有筛选。筛选上下文的所有其余列均保持不变。如果您不想覆盖现有筛选,则可以使用 KEEPFILTERS 包装筛选参数。例如,如果要在筛选上下文中显示Audio 时显示 Audio 销售量,而在筛选上下文中不显示 Audio 时显示空白值,则可以编写以下度量值:
Audio Sales KeepFilters :=
CALCULATE (
[Sales Amount],
KEEPFILTERS ( 'Product'[Category] = "Audio" )
)
KEEPFILTERS 是您学习的第二个CALCULATE 修饰符,第一个是 ALL。我们将在本章后面进一步介绍 CALCULATE 修饰符。KEEPFILTERS 更改了 Calculate 将筛选应用于新筛选上下文的方式。与其在同一列上覆盖现有筛选,不如将新筛选添加到现有筛选中。因此,只有已筛选类别已包含在筛选上下文中的单元格才会产生可见结果。您会在图5-21中看到这一点。
图5-21 Audio Sales KeepFilters 仅显示 Audio 行和 Total 行的 Audio 销售
KEEPFILTERS 完全按照其名称的含义进行操作。它不会覆盖现有筛选,而是保留现有筛选并将新筛选添加到筛选上下文中。我们可以使用图5-22来描述行为。
图5-22 使用 KEEPFILTERS 生成的筛选上下文同时筛选 Cell 和 Audio
因为 KEEPFILTERS 避免了覆盖,所以将由 CALCULATE 的 filter 参数生成的新筛选添加到上下文中。如果我们在 Cell Phone 行中查看度量值 Audio Sales KeepFilters 的值,则结果筛选上下文包含两个筛选:一个筛选为 Cell Phone ;另一个筛选为 Audio。这两个条件的交集导致一个空集,从而产生一个空白结果。
当在一列中选择了多个元素时,KEEPFILTERS 的行为将更加清晰。例如,考虑以下度量值带有和不带有 KEEPFILTERS 筛选 Audio 和 Computers 的情形:
Always Audio-Computers :=
CALCULATE (
[Sales Amount],
'Product'[Category] IN { "Audio", "Computers" }
)
KeepFilters Audio-Computers :=
CALCULATE (
[Sales Amount],
KEEPFILTERS ( 'Product'[Category] IN { "Audio", "Computers" } )
)
图5-23中的报告显示,带有 KEEPFILTERS 的版本仅计算 Audio 和 Computers 的销售额值,而所有其他类别均留空。总计行仅考虑 Audio 和 Computers。
图5-23 使用KEEPFILTERS 将原始筛选上下文和新筛选上下文合并在一起
KEEPFILTERS可以与谓词或表一起使用。实际上,先前的代码也可以用更冗长的方式编写:
KeepFilters Audio-Computers :=
CALCULATE (
[Sales Amount],
KEEPFILTERS (
FILTER (
ALL ( 'Product'[Category] ),
'Product'[Category] IN { "Audio", "Computers" }
)
)
)
这仅仅是出于教育目的的示例。您应该使用可用于筛选参数的最简单的谓词语法。筛选单个列时,可以避免显式编写 FILTER。但是,稍后您将看到更复杂的筛选条件需要用显式的 FILTER。在那些情况下,可以在显式的 FILTER 函数周围使用 KEEPFILTERS 修饰符,下一节将会看到。
筛选单列
在上一节中,我们介绍了引用 CALCULATE 中单列的筛选参数。重要的是要注意,您可以在一个表达式中具有对同一列的多个引用。例如,以下是有效的语法,因为它两次引用同一列(Sales [Net Price] )。
Sales 10-100 :=
CALCULATE (
[Sales Amount],
Sales[Net Price] >= 10 && Sales[Net Price] <= 100
)
实际上,这被转换为以下语法:
Sales 10-100 :=
CALCULATE (
[Sales Amount],
FILTER (
ALL ( Sales[Net Price] ),
Sales[Net Price] >= 10 && Sales[Net Price] <= 100
)
)
CALCULATE 生成的结果筛选上下文仅在 Sales [Net Price] 列上添加一个筛选。关于谓词在CALCULATE 中作为筛选参数的一个重要说明是,尽管它们看起来像条件,但它们是表。如果您阅读了最后两个代码片段中的第一个,则看起来 CALCULATE 会评估条件。而是 CALCULATE 评估满足条件的所有 Sales [Net Price] 值的列表。然后,CALCULATE 使用此值表将筛选应用于模型。
当两个条件在逻辑 AND 中时,它们可以表示为两个单独的筛选。实际上,上一个表达式等同于以下表达式:
Sales 10-100 :=
CALCULATE (
[Sales Amount],
Sales[Net Price] >= 10,
Sales[Net Price] <= 100
)
但是,请记住,CALCULATE 的多个筛选参数始终与逻辑 AND 合并。因此,对于逻辑 OR 语句,必须使用单个筛选,例如以下措施:
Sales Blue+Red :=
CALCULATE (
[Sales Amount],
'Product'[Color] = "Red" || 'Product'[Color] = "Blue"
)
通过编写多个筛选,您可以在一个筛选上下文中合并两个独立的筛选。由于没有同时具有蓝色和红色的产品,因此以下措施始终会产生空白结果:
Sales Blue and Red :=
CALCULATE (
[Sales Amount],
'Product'[Color] = "Red",
'Product'[Color] = "Blue"
)
实际上,前一个度量值对应于使用单个筛选的以下度量值:
Sales Blue and Red :=
CALCULATE (
[Sales Amount],
'Product'[Color] = "Red" && 'Product'[Color] = "Blue"
)
filter 参数始终返回筛选上下文中允许的颜色空白列表。因此,该度量值始终返回空白值。
只要筛选参数引用单个列,就可以使用谓词。我们建议您这样做,因为生成的代码更容易阅读。您也应该针对逻辑 AND 条件执行此操作。但是,永远不要忘记您仅依赖语法加糖。CALCULATE 始终与表一起使用,尽管紧凑语法可能会另外建议。
另一方面,只要筛选参数中有两个或多个不同的列引用,就必须将 FILTER 条件写为表表达式。您将在下一节中学习。
复杂条件下的筛选
引用多个列的筛选参数需要显式的表表达式。重要的是要了解可用于编写此类筛选的不同技术。请记住,用谓词创建所需列数最少的筛选通常是最佳实践。
考虑一个仅将金额大于或等于1,000的交易的销售额相加的度量值。要获取每笔交易的金额,需要将 Quantity 和 Net Price 列相乘。这是因为您没有在示例 Contoso 数据库中为 Sales 表的每一行存储该金额的列。您可能会想编写类似以下表达式的代码,但不幸的是,该表达式无法使用:
Sales Large Amount :=
CALCULATE (
[Sales Amount],
Sales[Quantity] * Sales[Net Price] >= 1000
)
该代码无效,因为 filter 参数在同一表达式中引用了两个不同的列。因此,DAX无法将其自动转换为合适的 FILTER 条件。编写所需筛选的最佳方法是使用一个表,该表只有谓词中引用列的组合:
Sales Large Amount :=
CALCULATE (
[Sales Amount],
FILTER (
ALL ( Sales[Quantity], Sales[Net Price] ),
Sales[Quantity] * Sales[Net Price] >= 1000
)
)
这将导致一个筛选上下文,该筛选上下文包含一个具有两列和若干行的筛选,它们对应于满足筛选条件的“ Quantity 和Net Price ”的唯一组合。如图5-24所示。
图5-24 多列筛选仅包含 Quantity 和 Net Price 的组合,产生的结果大于或等于1,000。
该筛选产生图5-25的结果。
图5-25 Sales Large Amount 仅显示大额交易
请注意,图5-25中的切片器没有筛选任何值:显示的两个值是 Net Price 的最小值和最大值。下一步是显示度量值如何与切片器交互。在类似 Sales Large Amount 的度量值中,当您覆盖 Quantity 或 Net Price 上的现有筛选时,需要注意。确实,由于 filter 参数在两列上使用 ALL,因此它将忽略同一列上任何先前存在的筛选,在此示例中包括切片器的筛选。图5-26中的报告与图5-25相同,但是这一次,切片器筛选了500到3,000之间的 Net Price 格。结果令人惊讶。
图5-26 当前价格范围内没有 Audio 的销售,仍然显示 Sales Large Amount 结果
Audio 和 Music, Movies and Audio Books 的 Sales Large Amount 的值存在是意外的。实际上,对于这两个类别, Net Price 格范围在500到3,000之间(这是切片器生成的筛选条件)没有销售。尽管如此, Sales Large Amount 度量值仍在显示结果。
原因是切片器创建的 Net Price 筛选上下文被 Sales Large Amount 度量值忽略,它覆盖了 Quantity 和 Net Price 上的现有筛选。如果您仔细比较图5-25和5-26,您会注意到 Sales Large Amount 的值是相同的,就像未将切片器添加到报告中一样。的确,Sales Large Amount 完全忽略了切片器。
如果您专注于某个单元格,例如 Audio 的 Sales Large Amount 的值,则执行以下代码计算其值:
Sales Large Amount :=
CALCULATE (
CALCULATE (
[Sales Amount],
FILTER (
ALL ( Sales[Quantity], Sales[Net Price] ),
Sales[Quantity] * Sales[Net Price] >= 1000
)
),
'Product'[Category] = "Audio",
Sales[Net Price] >= 500
)
从代码中,您可以看到最里面的 ALL 将忽略外部 CALCULATE 设置的 Sales [Net Price] 上的筛选。在那种情况下,您可以使用 KEEPFILTERS 来避免覆盖现有筛选:
Sales Large Amount KeepFilter :=
CALCULATE (
[Sales Amount],
KEEPFILTERS (
FILTER (
ALL ( Sales[Quantity], Sales[Net Price] ),
Sales[Quantity] * Sales[Net Price] >= 1000
)
)
)
新的 Sales Large Amount KeepFilter 度量值产生的结果如图5-27所示。
图5-27 使用 KEEPFILTERS,计算也考虑了外部限幅器
指定复杂筛选的另一种方法是使用表筛选而不是列筛选。这是DAX新手的首选技术之一,尽管使用起来非常危险。实际上,可以使用表筛选来编写先前的度量值:
Sales Large Amount Table :=
CALCULATE (
[Sales Amount],
FILTER (
Sales,
Sales[Quantity] * Sales[Net Price] >= 1000
)
)
您可能还记得,CALCULATE 的所有筛选参数都是在 CALCULATE 本身之外的筛选上下文中求值的。因此,基于 Sales 的迭代仅考虑在现有筛选上下文中筛选的行,其中包含基于 Net Price 的筛选。因此,Sales Large Amount Table 度量值的语义对应于 Sales Large Amount KeepFilter 度量值。
尽管此技术看起来很简单,但您应谨慎使用它,因为它可能会对性能和结果准确性产生严重影响。我们将在第14章中详细介绍这些问题。现在,请记住,最佳方法是始终使用尽可能少的列数筛选。
此外,您应该避免使用表筛选,因为它们通常更“昂贵”。Sales表可能非常大,并且逐行对其进行扫描以评估谓词可能是一项耗时的操作。另一方面,Sales Large Amount KeepFilter 中的筛选仅迭代 Quantity 和 Net Price 的唯一组合的 Quantity 。该数字通常比整个Sales表的行数小得多。
CALCULATE 中的评估顺序
当您查看DAX代码时,评估的自然顺序便是最里面的优先。例如,看下面的表达式:
Sales Amount Large :=
SUMX (
FILTER ( Sales, Sales[Quantity] >= 100 ),
Sales[Quantity] * Sales[Net Price]
)
DAX需要在开始 SUMX 评估之前评估 FILTER 的结果。实际上, SUMX 会迭代一个表。由于该表是 FILTER 的结果,因此 SUMX 无法在 FILTER 完成其工作之前开始执行。除 CALCULATE 和 CALCULATETABLE 外,此规则对所有DAX函数均适用。实际上,CALCULATE 首先评估其筛选参数,并且最后才评估第一个参数,即提供 CALCULATE 结果的表达式。
而且,由于 CALCULATE 更改了筛选上下文,因此事情变得更加复杂。所有筛选参数均在 CALCULATE 之外的筛选上下文中执行,并且每个筛选均独立评估。相同 CALCULATE 中筛选的顺序无关紧要。因此,以下所有措施都是完全等效的:
Sales Red Contoso :=
CALCULATE (
[Sales Amount],
'Product'[Color] = "Red",
KEEPFILTERS ( 'Product'[Brand] = "Contoso" )
)
Sales Red Contoso :=
CALCULATE (
[Sales Amount],
KEEPFILTERS ( 'Product'[Brand] = "Contoso" ),
'Product'[Color] = "Red"
)
Sales Red Contoso :=
VAR ColorRed =
FILTER (
ALL ( 'Product'[Color] ),
'Product'[Color] = "Red"
)
VAR BrandContoso =
FILTER (
ALL ( 'Product'[Brand] ),
'Product'[Brand] = "Contoso"
)
VAR SalesRedContoso =
CALCULATE (
[Sales Amount],
ColorRed,
KEEPFILTERS ( BrandContoso )
)
RETURN
SalesRedContoso
使用变量定义的 Sales Red Contoso 版本比其他版本更冗长,但是如果筛选是带有显式筛选的复杂表达式,则还是要使用它。这样,更容易理解筛选是在“计算”之前评估的。
对于嵌套的 CALCULATE 语句,此规则变得更加重要。实际上,最外面的筛选首先被应用,最里面的筛选随后被应用。了解嵌套 CALCULATE 语句的行为很重要,因为每次嵌套度量值调用时都会遇到这种情况。例如,考虑以下度量值,其中 Sales Green 调用 Sales Red:
Sales Red :=
CALCULATE (
[Sales Amount],
'Product'[Color] = "Red"
)
Green calling Red :=
CALCULATE (
[Sales Red],
'Product'[Color] = "Green"
)
为了使嵌套的度量值调用更加明显,我们可以通过以下方式扩展 Sales Green:
Green calling Red Exp :=
CALCULATE (
CALCULATE (
[Sales Amount],
'Product'[Color] = "Red"
),
'Product'[Color] = "Green"
)
评估顺序如下:
- 首先,外部 CALCULATE 应用筛选,Product [Color] =“ Green”。
- 其次,内部 CALCULATE 应用筛选,Product [Color] =“ Red”。该筛选将覆盖先前的筛选。
- 最后,DAX用 Product [Color] =“ Red” 的筛选计算 [Sales Amount]。
因此,红色和绿色调用红色的结果仍然是红色,如图5-28所示。
该报告显示每个类别的销售额,红色,绿色(红色)和红色(绿色)。我们注意到,最后三个度量值返回相同的结果,即按类别对红色产品的销售额。
注意
我们提供的说明仅用于培训目的。实际上,引擎对筛选上下文使用惰性评估,因此,在存在诸如前面代码的筛选参数覆盖的情况下,可能永远不会对外部筛选进行评估,因为它是无用的。但是,此行为仅仅是优化,它不会以任何方式改变 CALCULATE 的语义。
我们可以通过另一个示例查看评估的顺序以及如何评估筛选上下文。请考虑以下度量值:
Sales YB :=
CALCULATE (
CALCULATE (
[Sales Amount],
'Product'[Color] IN { "Yellow", "Black" }
),
'Product'[Color] IN { "Black", "Blue" }
)
由 Sales YB 生成的筛选上下文的评估在图5-29中可见。
如前所示,Product [Color] 上最里面的筛选会覆盖最外面的筛选。因此,度量值结果显示黄色或黑色的乘积之和。通过在最里面的 CALCULATE 中使用 KEEPFILTERS,通过保留两个筛选而不是覆盖现有筛选来构建筛选上下文:
Sales YB KeepFilters :=
CALCULATE (
CALCULATE (
[Sales Amount],
KEEPFILTERS ( 'Product'[Color] IN { "Yellow", "Black" } )
),
'Product'[Color] IN { "Black", "Blue" }
)
图5-29 最里面的筛选覆盖外面的筛选
由 Sales YB KeepFilters 生成的筛选上下文的评估在图5-30中可见。
图5-30 通过使用 KEEPFILTERS,CALCULATE不会覆盖以前的筛选上下文
因为两个筛选保持在一起,所以它们是相交的。因此,在新的筛选上下文中,唯一可见的颜色是黑色,因为它是两个筛选中唯一的值。
但是,同一 CALCULATE 中的筛选参数的顺序无关紧要,因为它们是独立应用于筛选上下文的。