在本章中,您将学习DAX中标量函数和表函数之间的区别。表函数对于DAX中的内部计算非常重要,当您为度量值或计算列编写DAX查询而不是DAX表达式时,表函数非常有用。
本章的目标是介绍表函数的概念,而不是对您第一次在这里看到的所有函数提供详细的解释。第9章“高级表函数”对表函数进行更深入分析。这里,我们将解释表函数在DAX中的作用,以及如何在常见场景中使用它们,包括在标量DAX表达式中。
3.1 介绍表函数
DAX是一种函数式语言,您可以在其中编写表达式,在计算后生成结果。到目前为止,您已经看到DAX表达式通常返回单个值,例如字符串或数字。我们将这些表达式称为标量表达式。定义度量值或计算列时,你总是编写标量表达式,如下所示:
= 4 + 3
= "DAX is a beautiful language"
= SUM ( Sales[Quantity] )
但是,您可以编写一个DAX表达式来生成一个表。您不能将表表达式直接分配给度量值或计算列,但表表达式是DAX的重要组成部分。例如,有DAX函数接收表表达式作为参数,并且需要表表达式来编写DAX查询。
表表达式的最简单示例是在DAX表达式中引用表名,例如以下表达式返回Sales表的整个内容(所有列和所有行):
= Sales
但是,如果您试图将上一个表达式分配给度量值或计算列,则会出现错误,因为度量值需要标量值作为结果。您需要操作表表达式以获取标量值。这可以通过使用接受表表达式作为参数的函数来实现。例如,使用COUNTROWS计算表中包含的行数:
= COUNTROWS ( Sales )
COUNTROWS函数具有以下定义:
COUNTROWS ( <table> )
当一个DAX函数接受表表达式作为参数时,您可以在该参数中写入表的名称,或者您可以编写一个返回表的函数。
我们根据DAX函数的返回类型对其进行分类。我们称返回标量值的函数为“标量函数”,称返回表的函数为“表函数”。例如,COUNTROWS是一个标量函数,因为它接受一个表作为参数,并返回一个数字。
许多表函数通常操作表,更改原始表的行或列。例如,您可以使用以下表达式,计算Sales表中价格大于100的行数:
= COUNTROWS (
FILTER (
Sales,
Sales[Unit Price] > 100
))
在上一个表达式中,FILTER返回一个仅包含Sales中单位价格大于100的行的表。在本章后面,您将了解有关FILTER函数的更多信息。
通常,您在代码中使用表表达式来迭代表的行并聚合一些值,以返回标量值作为结果。您不能将表表达式直接分配给度量值和计算列。但是,您可以在计算表中使用表表达式(如果此功能将来可用)或在DAX查询中使用表表达式,从而得到表达式的内容。
例如,您可以通过执行以下表表达式来获取Sales中单位价格大于100的行的表,您可以在图3-1中看到返回的内容。
= FILTER (
Sales,
Sales[Unit Price] > 100
)
DAX还为您提供了EVALUATE语句,您可以使用它来计算表表达式:
EVALUATE
FILTER (
Sales,
Sales[Unit Price] > 100
)
您可以在可以执行DAX查询的任何客户端工具(Microsoft Excel,DAX Studio,SQL Server Management Studio,Reporting Services等)中执行上面的DAX查询。在下一节中,您将看到EVALUATE语法的详细说明。
3.2 EVALUATE 语法
您可以将DAX用作编程语言和查询语言。
DAX查询是一个返回表的DAX表达式,与EVALUATE语句一起使用。完整的DAX查询语法如下:
[DEFINE { MEASURE <tableName>[<name>] = <expression> }]
EVALUATE <table>
[ORDER BY {<expression> [{ASC | DESC}]} [, ...]
[START AT {<value>|<parameter>} [, ...]] ]
初始DEFINE MEASURE部分可以用于定义查询的本地度量(即,它们存在于查询周期中)。当您调试公式时,它变得非常有用,因为您可以定义一个本地度量,对它进行测试,然后在它的行为符合预期时将其放入模型中。您将在第9章中看到此语法的更多示例。
大多数语法都是由可选参数组成的。最简单的查询是从现有表中检索所有列和行:
EVALUATE Product
您可以在图3-2中看到结果。
要控制排序顺序,可以使用ORDER BY子句:
EVALUATE Product
ORDER BY
Product[Color],
Product[Brand] ASC,
Product[Class] DESC
备注:
请注意,模型中定义的“按列排序”属性在DAX查询中不起作用。即使您可能通过根据Sort By Column属性查询单个列而看到排序数据,您也不必依赖此行为,就像您不能依赖SQL查询中的聚簇索引一样。生成动态DAX查询的客户端应读取模型元数据中的“按列排序”属性,然后生成相应的ORDER BY条件。在DAX和SQL中,必须始终使用显式ORDER BY条件来获取排序数据。
ASC和DESC关键字是可选的;如果不填,则默认使用ASC。您可以在图3-3中看到上一个查询的结果,其中数据按颜色、品牌和类排序。
START AT条件也是可选的,并且只能与ORDER BY子句一起使用。您可以在ORDER BY语句中为每列指定起始值。 START AT条件对于无状态应用程序中的分页非常有用,该应用程序仅从查询中获取有限数量的行,然后在用户请求下一页数据时发送另一个查询。例如,查看以下查询:
EVALUATE Product
ORDER BY
Product[Color],
Product[Brand] ASC,
Product[Class] DESC
START AT
"Yellow", "Tailspin Toys"
查询返回图3-4中所示的表,其中仅包含从Yellow,Tailspin Toys开始的行。
请注意,“starting from”的概念取决于ORDER BY子句中指定的顺序方向。如果您为Product[Brand]指定DESC,如以下示例所示,结果中不包括Wide World Importers,其他品牌(如Southridge Video和 Northwind Traders)跟着Tailspin Toys。您可以在图3-5中看到以下查询的结果。
EVALUATE Product
ORDER BY
Product[Color],
Product[Brand] DESC,
Product[Class] DESC
START AT
"Yellow", "Tailspin Toys"
要筛选DAX查询返回的行并更改列,必须使用特定的表函数在EVALUATE关键字之后操作表表达式。本章介绍了一些表表达式,而第9章介绍了其他表表达式。
3.3 使用表表达式
正如您在本章开头所看到的,您经常使用表表达式作为其他DAX函数的参数。典型的用法是在迭代表的函数中,为每一行计算DAX表达式。比如所有以“X”结尾的聚合函数,例如SUMX:
[Sales Amount] :=
SUMX (
Sales,
Sales[Quantity] * Sales[Unit Price]
)
您可以使用表函数替换简单的Sales表引用。例如,您可以使用FILTER函数仅考虑数量大于1的Sales:
[Sales Amount Multiple Items] :=
SUMX (
FILTER (
Sales,
Sales[Quantity] > 1
),
Sales[Quantity] * Sales[Unit Price]
)
在计算列中,还可以使用RELATEDTABLE函数检索一对多关系的多端表的所有行。例如,Product表中的以下计算列计算相应产品的销售额:
Product[Product Sales Amount] =
SUMX (
RELATEDTABLE ( Sales ),
Sales[Quantity] * Sales[Unit Price]
)
您可以在第4章“理解计算上下文”中的“行上下文和关系”这一节中找到RELATEDTABLE表函数的详细说明
您可以在同一DAX表达式中嵌套表函数调用,因为任何表表达式都可以是表函数的调用。例如,Product表中的以下计算列仅考虑数量大于1的销售额来计算产品销售额。
Product[Product Sales Amount Multiple Items] =
SUMX (
FILTER (
RELATEDTABLE ( Sales ),
Sales[Quantity] > 1
),
Sales[Quantity] * Sales[Unit Price]
)
当您对表函数进行嵌套调用时,DAX首先计算最内层函数,然后计算其他函数直到最外层函数。不要将此规则与函数调用参数进行求值的顺序混淆。
备注:
稍后您将看到,嵌套调用的执行顺序可能会造成混淆,因为CALCULATETABLE的计算顺序与FILTER不同。在下一节中,您将学习FILTER的行为,您将在第5章“理解CALCULATE和CALCULATETABLE”中找到CALCULATETABLE的介绍。
2.4 理解FILTER
FILTER函数有一个简单的角色:它获取一个表并返回一个表,这个表的列与原始表中的列相同,但是只包含满足逐行应用的筛选条件的行。
FILTER的语法如下:
···
FILTER ( <table>, <condition> )
···
FILTER迭代<table>,并且对于每一行,计算<condition>,这是一个布尔表达式。当<condition>的计算结果为TRUE时,FILTER返回该行;否则,它会跳过它。
备注:
从逻辑上看,FILTER为<table>中的每一行执行<condition>。但是,DAX中的内部优化可能会将这些计算的数量减少到<condition>表达式中包含的列引用的唯一值的数量。<condition>的实际计算次数对应于FILTER操作的“粒度”。这种粒度决定了FILTER的性能,它是DAX优化的重要元素。
例如,以下查询筛选了Fabrikam品牌的产品,如图3-6所示。
EVALUATE
FILTER (
Product,
Product[Brand] = "Fabrikam"
)
您可以在另一个FILTER函数中嵌套FILTER调用,因为您可以使用任何表表达式作为筛选参数。执行的第一个FILTER是最内层的。通常来说,嵌套两个筛选和在AND函数中使用一组逻辑条件返回的结果相同。换句话说,以下查询产生相同的结果:
FILTER ( <table>, AND ( <condition1>, < condition2> ) )
FILTER ( FILTER ( <table>, < condition1> ), < condition2> ) )
但是,如果<table>有许多行且两个判断条件具有不同的复杂性,您可能会观察到不同的性能。例如,请考虑以下查询,该查询返回Unit Price超过Unit Cost三倍的Fabrikam产品,如图3-7所示。
EVALUATE
FILTER (
Product,
AND (
Product[Brand] = "Fabrikam",
Product[Unit Price] > Product[Unit Cost] * 3
))
此类查询可能会将这两个条件应用于Product表的所有行。如果两者中有一个条件更快或更具选择性,则可以使用嵌套的FILTER函数首先应用它。例如,以下查询将Unit Price和Unit Cost的筛选应用于最里面的FILTER函数,然后按品牌筛选那些满足价格条件的产品。
EVALUATE
FILTER (
FILTER (
Product,
Product[Unit Price] > Product[Unit Cost] * 3
),
Product[Brand] = "Fabrikam"
)
如果反转条件,则还会反转它们的执行顺序。以下查询将价格条件应用于属于Fabrikam品牌的产品:
EVALUATE
FILTER (
FILTER (
Product,
Product[Brand] = "Fabrikam"
),
Product[Unit Price] > Product[Unit Cost] * 3
)
当您优化DAX表达式时,此知识将非常有用。您可以选择执行顺序以有限应用最具选择性的筛选器。但是,如果没有清楚地理解计算上下文,请不要开始优化DAX。您将在第16章“优化DAX”中找到有关查询优化的更完整的讨论。这些示例的目的是让您了解表函数的嵌套调用的执行顺序。
备注:
通常,嵌套调用函数的执行顺序是从最内层到最外层的函数。您将看到CALCULATE和CALCULATETABLE可能是此行为的一个例外,因为用于计算其参数的特定顺序。因为您可能在类似情况下使用FILTER和CALCULATETABLE,所以请注意嵌套调用的这种差异。
2.5 理解ALL、ALLEXCEPT和ALLNOBLANKROW
ALL是一个有用的函数,它返回表的所有行或列的所有值,具体取决于您使用的参数。例如,以下DAX查询返回Product表中的所有行:
EVALUATE
ALL ( Product )
您不能在ALL参数中指定表表达式。您必须指定表名或列名列表。如果使用单个列,则结果是一个只有一列包含其唯一值列表的表,如图3-8所示。
EVALUATE
ALL ( Product[Class] )
您可以在ALL函数的参数中指定同一个表中的更多列。如果使用多列,则结果将是具有等效列数的表,其中包含这些列中现有值组合的列表。例如,以下表达式生成如图3-9所示的结果。
EVALUATE
ALL ( Product[Class], Product[Color] )
ORDER BY Product[Color]
在所有变体中,ALL忽略任何现有的筛选器以产生其结果。您可以使用ALL作为迭代函数的参数,例如SUMX和 FILTER,或者作为CALCULATE函数中的筛选器参数(稍后将会看到)。
如果要在ALL函数调用中包含表的大多数列,则可以使用ALLEXCEPT。ALLEXCEPT的语法需要一个表,后跟要从结果中排除的列。因此,ALLEXCEPT返回一个表,其中包含表的其他列中现有值组合的唯一列表。
实际上,ALLEXCEPT是一种编写DAX表达式的方法,该表达式将自动在ALL结果中包含在将来版本中可能出现在表中的任何其他列。例如,如果您有一个包含五列(ProductKey、Product Name、Brand、Class、Color)的Product表,则以下语法会产生相同的结果:
ALL ( Product[Product Name], Product[Brand], Product[Class] )
ALLEXCEPT ( Product, Product[ProductKey], Product[Color] )
但是,如果您稍后添加两列Product [Unit Cost]和Product [Unit Price],那么ALL的结果将忽略它们,而之前的ALLEXCEPT将返回相当于:
ALL (
Product[Product Name],
Product[Brand],
Product[Class],
Product[Unit Cost],
Product[Unit Price]
)
以下查询返回一个表,该表包含Product表中除Product Code和Color之外的所有列。图3-10中的结果与原始表的行数相同,因为结果包含ProductKey列,每列具有唯一值。结果中列的其他组合可能返回较少的行数,因为ALLEXCEPT会删除返回列中重复的值组合。
EVALUATE ALLEXCEPT ( Product, Product[ProductKey],
Product[Color] )
在前面的示例中,您在EVALUATE语句中看到了ALL,该语句在没有任何现有筛选器的情况下执行DAX表达式。因此,最好查看一个示例,该示例使用度量值来计算数据透视表中ALL返回的行数,其中每个单元格使用不同的筛选器计算度量值。考虑以下度量值:
[Products] := COUNTROWS ( Product )
[All Products] := COUNTROWS ( ALL ( Product ) )
[All Brands] := COUNTROWS ( ALL ( Product[Brand] ) )
您可以在图3-11中看到每个度量值的不同结果的示例。
对于每个产品类别,在“ALL Products”和“ALL Colors”列中始终具有相同的编号。 ALL语句的计算将忽略由数据透视表的每个单元格定义的筛选器。
在关系的父表上调用ALL时,如果子表包含一个或多个与父表中的任何值不匹配的行,则会检索另一个空行。您可以使用ALLNOBLANKROW而不是ALL来从结果中省略此特殊行。
考虑以下度量值:
[All Products] := COUNTROWS ( ALL ( Product ) )
[All NoBlank Products] := COUNTROWS ( ALLNOBLANKROW ( Product
) )
[All Brands] := COUNTROWS ( ALL ( Product[Brand] ) )
[All NoBlank Brands] := COUNTROWS ( ALLNOBLANKROW (
Product[Brand] ) )
[All Sizes] := COUNTROWS ( ALL ( Product[Size] ) )
[All NoBlank Sizes] := COUNTROWS ( ALLNOBLANKROW (
Product[Size] ) )
在图3-12中,您可以看到度量值ALL和ALLNOBLANKROW之间的区别。所有版本的度量值ALL都比ALLNOBLANKROW多一行。原因是Sales表中的部分行在Product表中没有匹配的行,因此在Product表中虚拟添加了一行,您可以在图3-12中的(空白)行中看到该结果。
您应该注意到,All Sizes和All NoBlank Sizes度量值始终返回相同的值。此度量值查询Products [Size]列中的值的计数。在这种情况下,ALL和ALLNOBLANKROW函数返回相同的值,因为Products [Size]列已包含空值。在图3-13中的示例中,有569个产品的Products [Size]是空白,另外还有一类空白产品,包含对Sales表中不匹配产品的引用,总共570个。所有这些行都归为Products [Size]中的空白值。
当你编写一个DAX公式,该公式忽略关系中的不匹配值,你应该使用ALLNOBLANKROW。然而,ALL的使用很常见,而ALLNOBLANKROW很少使用。
2.6 理解VALUES和DISTINCT
在上一节中,您已经看到ALL与一列一起使用会返回一个包含其所有唯一值的表。 DAX提供了另外两个类似的函数,它们返回列的唯一值列表:VALUES和DISTINCT。
如果在没有任何其他筛选器操作的EVALUATE语句中使用,则VALUES和DISTINCT看起来与ALL完全相同。但是,当您将这些函数放在DAX度量值中时,您可以观察到不同的行为,因为计算发生在数据透视表的每个单元格的不同上下文中。
请考虑以下度量值,这些度量值计算“Product”表的“Brand”和“Size”列中的唯一值的数量。
[Products] := COUNTROWS ( Product )
[Values Brands] := COUNTROWS ( VALUES ( Product[Brand] ) )
[Distinct Brands] := COUNTROWS ( DISTINCT ( Product[Brand] ) )
[Values Sizes] := COUNTROWS ( VALUES ( Product[Size] ) )
[Distinct Sizes] := COUNTROWS ( DISTINCT ( Product[Size] ) )
VALUES返回当前单元格中可见的唯一值列表,包括不匹配值的可选空行。 DISTINCT执行相同操作,而不包括不匹配值的可选空行。但是,如果空白值显示为列的有效值,则2个函数都将包含空行。唯一的区别是为了处理关系中的缺失值而添加的可选空行。
一个例子可能有助于理解这种差异。如图3-14所示,每个产品类别都会筛选不同数量的产品。例如,Deluxe类别有360种产品,有11个独特品牌和204种独特尺寸。 VALUES和DISTINCT返回相同的数字,但有一个例外:数据透视表行上的(空白)产品类别。结果包括为显示不匹配产品的销售额金额而添加的虚拟行。
另一个区别在于图3-14的总计。应用于Product [Brand]的VALUES返回的值比应用于同一列的DISTINCT多一个值。但是,应用于Products [Size]的VALUES不会发生这种情况,它返回与相应列上的DISTINCT相同的值。原因是至少一个产品的“Distinct Sizes”列包含空值,因此添加的空白产品不会向“Distinct Sizes”列添加新的唯一值。
当没有筛选器时,DISTINCT的行为对应于ALLNOBLANKROW,而VALUES的行为对应于ALL。
VALUES也接受一个表作为参数。在这种情况下,它返回当前单元格中可见的整个表,可选地包括不匹配关系的空行。例如,在数据模型中考虑以下度量值,其中Sales表与Product具有关系,并且包含具有与任何现有产品不匹配的产品密钥的交易。
[Products] := COUNTROWS ( Product )
[Values Products] := COUNTROWS ( VALUES ( Product ) )
[All NoBlank Products] := COUNTROWS ( ALLNOBLANKROW ( Product
) )
[All Products] := COUNTROWS ( ALL ( Product ) )
您可以在图3-15中看到,在这种情况下,当没有筛选器时,VALUES的结果对应于ALL的行为,包括添加的空行以显示不匹配产品的销售额。在这种情况下,您不能在表上使用DISTINCT;如果存在重复的行,则没有一个DAX函数可以删除重复的行(您必须使用SUMMARIZE,稍后将在第9章中看到)。但是,度量值[Products]计算表中的行数,忽略可能的空行,当没有筛选器时,对应ALLNOBLANKROW。
2.6.3 使用VALUES作为标量值
即使VALUES是表函数,您也经常使用它来计算标量值,因为您将在本节中学习DAX中的特殊功能。例如,您可以在以下表达式中找到VALUES,如果某个选择的所有产品具有相同的颜色,则会显示颜色名称:
[Color Name] :=
IF (
COUNTROWS ( VALUES ( Product[Color] ) ) = 1,
VALUES ( Product[Color] )
)
您可以在图3-16中看到结果。当“[Color Name]”列包含空白时,表示存在两种或更多种不同的颜色。
这里有趣的一点是,我们使用VALUES的结果作为标量值,即使它返回一个表。这不是VALUES的特殊行为,但它是DAX语言的更一般行为:如果表表达式返回具有一行和一列的表,则可以转换为标量值,并在需要时自动完成。
实际上,如果结果只有一行和一列,则可以将任何表表达式用作标量值。当表返回更多行时,您会在执行时收到此错误:“提供了一个包含多个值的表,其中需要单个值。”因此,您应始终使用返回的条件保护转换为标量值如果表表达式返回更多行,则表示不同的结果(在编写DAX表达式时,您应该已经知道表表达式是否只返回一行)。
上一个示例的Color Name度量值使用COUNTROWS来检查Products表的Color列是否只选择了一个值。执行完全相同控件的一种更简单的方法是使用HASONEVALUE,它执行相同的检查,如果列只有VALUES返回的值,则返回TRUE,否则返回FALSE。以下两种语法是等效的:
COUNTROWS ( VALUES ( <column> ) ) = 1
HASONEVALUE ( <column> )
您应该使用HASONEVALUE而不是COUNTROWS有两个原因:它更具可读性,而且可能稍快一些。以下是基于HASONEVALUE的[Color Name]度量值的更好实现:
[Color Name] :=
IF (
HASONEVALUE ( Product[Color] ),
VALUES ( Product[Color] )
)
您经常使用VALUES作为标量表达式的原因是它返回单个列,并且可能返回单个行,具体取决于计算上下文。使用VALUES作为标量表达式在许多DAX模式中很常见,并且在本书中反复出现。