在快速介绍上一章之后,现在是时候开始讨论DAX语言了。在本章中,您将学习语言的语法、计算列和度量值(在Excel术语中也称为计算字段)之间的差异,以及DAX中最常用的函数。
由于这是一个介绍性的章节,所以许多函数没有深入介绍。在本书后面的章节中,我们会更深入地解释它们。现在,只需要介绍这些函数并开始研究DAX语言就足够了。
2.1 了解DAX计算
要使用复杂的公式,您需要学习DAX的基础知识,包括语法、DAX可以处理的不同数据类型、基本运算符以及如何引用列和表。下面几节将讨论这些概念。
使用DAX计算表中列的值。您可以聚合、计算和搜索数字,但是最终,所有的计算都涉及表和列。因此,要学习的第一个语法是如何引用表中的列。
一般格式是写表名,用单引号括起来,后跟列名括在方括号中,如下所示:
'Sales'[Quantity]
如果表名不以数字开头,不包含空格,也不是保留字(如Date或Sum),则可以省略单引号。
注意
最好不要在表名中使用空格。这样,您可以避免在公式中使用引号,因为使用引号会使代码难以阅读。
但是,请记住,表格的名称与使用数据透视表或任何其他客户端工具(如Power View)浏览模型时的名称相同。因此,如果您希望报表中的表名包含空格,则需要在代码中使用单引号。
如果要在定义公式的同一表中引用列或度量值,也可以完全避免编写表名。因此,如果在计算列中或在Sales表中的度量值中写入,[Quantity]是有效的列引用。即使这种方法在语法上是正确的,并且当您选择列而不是编写列时,用户界面可能会建议您使用它,但是我们强烈建议您不要使用它。这样的语法使得代码难以阅读,因此在DAX表达式中引用列时最好始终使用表名。
2.1.1 DAX数据类型
DAX可以使用七种不同的数字类型执行计算。在下面的列表中,我们显示了DAX名称和此数据类型的常用名称。例如,布尔值在DAX术语中称为TRUE / FALSE。我们更喜欢遵循事实上的命名标准,我们将它们称为布尔值。
- 整数(整数)
- 十进制数(小数)
- 货币(货币),内部存储为整数的固定十进制数
- 日期(日期时间)
- 布尔值(TRUE / FALSE)
- 文本(字符串)
- 二进制大对象(二进制)
DAX具有强大的类型处理系统,因此您不必担心数据类型:编写DAX表达式时,结果类型基于表达式中使用的术语类型。如果从DAX表达式返回的类型不是预期的类型,则需要注意这一点:那么您必须检查表达式本身使用的术语的数据类型。
例如,如果求和的一个项是日期,那么结果也是一个日期;而如果相同的运算符与整数一起使用,则结果为整数。这称为运算符重载,您可以在图2-1中看到其行为的示例,其中OrderDatePlusOneWeek列的计算方法是将Order Date列的值加7。正如我们所说,结果是一个日期。
除了运算符重载之外,DAX还会在运算符需要时自动将字符串转换为数字和将数字转换为字符串。例如,如果使用连接字符串的&运算符,DAX会将其参数转换为字符串。以下公式:
= 5 & 4
它返回“54”作为字符串。然而,公式:
= "5" + "4"
返回值为9的整数结果。
结果值取决于运算符,而不是源列,它们根据运算符的要求进行转换。即使这个行为看起来很方便,稍后您将看到在这些自动转换过程中可能发生的错误类型。我们建议您避免自动转换。如果需要进行某种转换,那么明确地控制其转换会好得多。为了更明确,前面的例子应该是:
= VALUE(“5”)+ VALUE(“4”)
对于使用Excel或其他语言的人来说,DAX数据类型可能很熟悉。您可以在http://msdn.microsoft.com/en-us/library/gg492146.aspx上找到DAX数据类型的规范。但是,分享有关每种数据类型的一些注意事项很有用。
整数
DAX只有一个可以存储64位的整数数据类型。 DAX中整数值之间的所有内部计算也使用64位。
十进制数
十进制数始终存储为双精度浮点值。
不要将此DAX数据类型与Transact-SQL的十进制和数字数据类型混淆:SQL中DAX十进制数的相应数据类型是Float。
货币
货币数据类型存储固定的十进制数。它可以表示四个小数点,并在内部存储为64位整数值除以10,000。货币数据类型之间执行的所有计算只保留小数点后四位。如果您需要更高的准确性,则必须转换为十进制数据类型。
货币数据类型的默认格式包括货币符号。您还可以将货币格式应用于整数和十进制数,并且可以使用货币数据类型不带货币符号的格式。
日期(日期时间)
DAX将日期存储在DateTime数据类型中。此格式在内部使用浮点数,其中整数对应于自1899年12月30日以来的天数,小数部分表示时间的占比。小时、分钟和秒转换为一天的小数部分。因此,以下表达式返回当前时间加上一天(恰好24小时):
= NOW()+ 1
其结果是明天的同一时间。如果您只需要使用DateTime的日期部分,请始终记住使用TRUNC来删除小数部分。
闰年错误
Lotus 1-2-3是1983年发布的流行电子表格,它在处理DateTime数据类型时存在错误。它认为1900年是闰年,即使它不是(一个世纪的最后一年只有前两个数字可以除以4而没有余数)。那时,第一版Excel的开发团队故意复制了这个bug,以保持与Lotus 1-2-3的兼容性。从那时起,由于兼容性,每个新版本的Excel都将bug作为一项功能维护。
现在,在2015年,该错误仍然在DAX中,为了向后兼容Excel而引入。错误的存在(我们应该称之为功能吗?)可能会导致1900年3月1日之前的错误。因此,按照设计,DAX的第一个官方支持日期是1900年3月1日。在该日期之前执行的日期计算可能会导致错误,应被视为不准确。
如果需要在1900年之前执行计算,则应使用数学运算1900之后的日期,执行计算,然后将日期移回时间。
布尔值
布尔数据类型用于表示逻辑条件。例如,由以下表达式定义的计算列的类型为布尔值:
= Sales[Unit Price] > Sales[Unit Cost]
您也可以将布尔数据类型看作数字,其中TRUE等于1,FALSE等于0。有时候,这可能用于排序,因为TRUE> FALSE。
文本(字符串)
DAX中的每个字符串都存储为Unicode字符串,其中每个字符以16位存储。默认情况下,字符串之间的比较是不区分大小写的,因此两个字符串“Power Pivot”和“POWER PIVOT”被认为是相等的。
2.1.2 DAX 运算符
了解了运算符在确定表达式类型中的重要性之后,您现在可以在表2-1中看到DAX中可用的运算符列表。
暂略
此外,逻辑运算符也可以作为DAX函数使用,其语法与Excel非常相似。例如,你可以写:
AND ( [CountryRegion] = "USA", [Quantity] > 0 )
OR ( [CountryRegion] = "USA", [Quantity] > 0 )
这相当于:
[CountryRegion] = "USA" && [Quantity] > 0
[CountryRegion] = "USA" || [Quantity] > 0
当您必须编写复杂的条件时,使用函数会优与逻辑运算符。实际上,在格式化大部分代码时,函数比运算符更容易格式化和读取。但是,函数的一个主要缺点是一次只能传入两个参数。如果有两个以上的条件需要计算,则需要嵌套函数。
2.2 理解计算列和度量值
现在您已经了解了DAX语法的基础知识,接下来您需要学习DAX中最重要的概念之一:计算列和度量值之间的差异。即使它们看起来很相似,因为你可以通过两种方式进行一些计算,但实际上它们是非常不同的,理解两者的区别是解开DAX力量的关键。
2.2.1 计算列
例如,如果要在Excel中创建计算列,只需移动到表的最后一列(名为“添加列”),然后开始编写公式。当然,其他实现DAX的工具可能具有不同的用户界面。您可以在公式栏中创建DAX表达式,在编写表达式时智能感知可以帮助您。
计算列与表中的任何其他列一样,可以在数据透视表或任何其他报表的行、列、筛选器或值中使用它。如果需要,还可以使用计算列来定义关系。为计算列定义的DAX表达式在它所属的表的当前行上下文中操作。对列的任何引用都返回当前行的该列的值。您不能直接访问其他行的值。
备注:
正如您稍后将看到的,一些DAX函数聚合整个表的列值。获取行子集值的唯一方法是使用返回表然后对其进行操作的DAX函数。通过这种方式,您可以为一系列行聚合列值,并可能通过筛选仅由一行组成的表对不同的行进行操作。您将在第4章“理解计算上下文”中学习有关此主题的更多信息。
您需要记住有关计算列的一个重要概念是它们在数据库处理期间计算,然后存储在模型中。如果您习惯于SQL计算列(非持久化),这可能看起来很奇怪,这些列在查询时计算并且不使用内存。然而,在Tabular中,所有计算列占用存储器中的空间并且在表处理期间计算。
当您创建非常复杂的计算列时,这种行为非常有用。计算它们所需的时间总是处理时间而不是查询时间,从而带来更好的用户体验。然而,您始终必须记住计算列使用珍贵的RAM。例如,如果您有一个计算列的复杂公式,您可能想要将不同中间列中的计算步骤分开。虽然这种方法在项目开发过程中很有用,但它在生产中是一个坏习惯,因为每个中间计算都存储在RAM中并浪费宝贵的空间。
2.2.2 度量值
在DAX模型中还有另一种定义计算的方法,当您不想为每一行计算值,而是希望聚合表中许多行的值时,这种方法很有用。我们称之为度量值。
例如:您可以在Sales表中定义GrossMargin列以计算毛利:
Sales[GrossMargin] = Sales[SalesAmount] - Sales[TotalProductCost]
但是,如果您想将毛利率显示为销售额的百分比,会发生什么?您可以使用以下公式创建计算列:
Sales[GrossMarginPct] = Sales[GrossMargin] / Sales[SalesAmount]
此公式计算行级别的正确值,如图2-2所示。
然而,当您计算总计的百分比时,您不能依赖计算列。实际上,您需要用毛利总计除以销售额总计。因此,在这种情况下,您需要计算聚合的比率,而不能使用计算列的聚合。换句话说,您计算总计的比率,而不是比率的总计。
GrossMarginPct的正确实现是用度量值:
Sales[GrossMarginPct] :=
SUM ( Sales[GrossMargin] ) / SUM(Sales[SalesAmount] )
但是,正如我们已经说过的,您无法将其输入到计算列中。如果需要对聚合值进行操作而不是逐行操作,则必须创建度量值。您可能已经注意到我们使用:=来定义度量值,而不是等号(=)。这是我们在本书中使用的标准,以便更容易在代码中区分度量值和计算列。
度量值和计算列都使用DAX表达式,不同之处在于上下文。度量值是在数据透视表或DAX查询的单元格上下文中计算的,而计算列是在它所属的表的行级别计算的。单元格的上下文(本书后面,您了解到这是一个筛选器上下文)取决于数据透视表中的用户选择或DAX查询。因此,当您在度量中使用SUM(Sales[SalesAmount])时,表示在此单元格下计算所有单元格的和,而在计算列中使用Sales [SalesAmount]时,则表示当前行的SalesAmount列的值。
需要在表格中定义度量值。这是DAX语言的要求之一。但是,该度量值并不属于该表。实际上,您可以将度量值从一个表移动到另一个表,而不会丢失其功能。
计算列和度量值之间的差异
计算列和度量值之间存在很大差异,即使它们看起来非常相似。计算列在数据刷新期间计算,并将当前行作为上下文,它不依赖于数据透视表上的用户活动。度量值对当前上下文定义的数据进行聚合操作。例如,在数据透视表中,根据单元格的坐标筛选源表,并使用这些筛选器聚合和计算数据。换句话说,度量值总是在计算上下文下对数据进行聚合操作,因此默认执行模式不引用任何单行。计算上下文将在第4章中进一步解释。
在计算列和度量值之间进行选择
现在您已经看到了计算列和度量值之间的差异,您可能想知道何时使用这个而不是另一个。有时两者都是一种选择,但在大多数情况下,您的计算需求决定了您的选择。
只要您想执行以下操作,就必须定义计算列:
- 将计算结果放在Excel切片器中,或在数据透视表中的行或列中显示结果(与“值”区域相对),或将结果用作DAX查询中的筛选条件。
- 定义严格绑定到当前行的表达式。(例如,价格*数量不能平均或两列的总和。)
- 分类文本或数字。(例如,度量值的范围,客户的年龄范围,例如0-18,18-25等。)
但是,每当要显示反映用户选择的结果,并在数据透视表的值区域中查看它们时,必须定义度量值,例如:
- 计算数据透视表选择的利润百分比时。
- 当您计算产品与所有产品的比率,并保持按年份和地区筛选器时。
即使在这些情况下需要使用不同的DAX表达式,也可以使用计算列和度量值表达一些计算。
例如,您可以将GrossMargin定义为计算列:
Sales[GrossMargin] = Sales[SalesAmount] - Sales[TotalProductCost]
但它也可以定义为一个度量值:
[GrossMargin] := SUM ( Sales[SalesAmount] ) – SUM (Sales[TotalProductCost] )
我们建议您在这种情况下使用度量值,因为度量值在查询时进行计算,它不会占用内存和磁盘空间,但这仅在大型数据集中非常重要。当模型的大小不是问题时,您可以使用您更熟悉的方法。
交叉引用
很明显,一个度量值可以引用一个或多个计算列。相反的情况可能不那么直观。计算列可以引用度量值:以这种方式,它强制计算当前行定义的上下文的度量值。此操作将度量结果转换并合并到列中,该列不受用户操作的影响。显然,只有某些操作才能产生有意义的结果,因为通常情况,度量值的计算强烈依赖于用户在数据透视表中所做的选择。
2.3 变量
在编写DAX表达式时,可以通过使用变量避免重复相同的表达式。例如,看以下表达式:
VAR TotalSales = SUM ( Sales[SalesAmount] )
RETURN ( TotalSales - SUM ( Sales[TotalProductCost] ) ) / TotalSales
您可以定义许多变量,它们是您定义它们的表达式的本地变量。变量对于简化代码非常有用,因为您可以避免重复相同的子表达式。变量使用延迟计算来计算。这意味着,如果您定义了一个变量,由于任何原因,该变量在您的代码中没有使用,那么该变量将永远不会被求值。如果需要计算,则只发生一次:变量的后续用法将读取先前计算的值。因此,当您多次使用复杂表达式时,它们也可用作优化技术。
此外,正如您将在第4章中学到的,变量非常有用,因为它们使用定义时的上下文而不是使用变量时的上下文。
2.4 处理DAX表达式中的错误
现在您已经了解了DAX语法的一些基础知识,接下来将学习如何优雅地处理无效计算。DAX表达式可能包含无效计算,因为它引用的数据对公式无效。
例如,您可能有零除,或不是数字的列值,并且在算术运算中使用,例如乘法。您必须了解默认情况下如何处理这些错误,以及如果您需要一些特殊处理,如何拦截这些错误。
在您学习如何处理错误之前,有必要花一些时间来描述在DAX公式计算期间可能出现的各种错误。他们是:
- 转换错误
- 算术运算错误
- 空值或缺少值
2.4.1 转换错误
第一类错误是转换错误。正如您在本章前面看到的,只要运算符需要,DAX就会自动在字符串和数字之间转换值。用例子回顾一下这个概念,下面这些都是有效的DAX表达式:
"10" + 32 = 42
"10" & 32 = "1032"
10 & 32 = "1032"
DATE (2010,3,25) = 3/25/2010
DATE (2010,3,25) + 14 = 4/8/2010
DATE (2010,3,25) & 14 = "3/25/201014"
这些公式总是正确的,因为它们是用常数值运算的。但是,如果VatCode是一个字符串,那么下面的代码是什么呢?
SalesOrders[VatCode] + 100
因为此求和的第一个操作数是一个列,在本列中是一个Text数据类型,您必须确保DAX可以将该列中的所有值转换为数字。如果DAX无法转换某些内容以满足运算符的需求,则会产生转换错误。下面是一些典型的情况:
"1 + 1" + 0 = 无法将字符串类型的值 "1+1" 转换为数值
DATEVALUE ("25/14/2010") = 类型不匹配
要避免这些错误,您需要在DAX表达式中添加错误检测逻辑,以拦截错误并始终返回有意义的结果。
2.4.2 算术运算错误
第二类错误是算术运算错误,例如除以零或负数的平方根。这些不是与转换相关的错误:每当您尝试调用函数或使用具有无效值的运算符时,DAX都会提示这些错误。
除零需要特殊处理,因为它的行为方式不是非常直观(数学家可能除外)。当你用一个数字除以0时,DAX通常会返回特殊值Infinity。此外,当遇到0除以0或无穷大除以无穷大的特殊情况,DAX返回特殊的NaN(非数字)值。
由于这是一种奇怪的行为,所以值得在表2-2中进行总结。
暂略
表2-2除以零的特殊结果值。
值得注意的是,Infinity和NaN不是错误,而是DAX中的特殊值。实际上,如果将数字除以Infinity,则表达式不会生成错误但返回0:
9954 /(7/0)= 0
除了这种特殊情况,当使用错误的参数调用函数时,DAX还会返回算术错误,比如负数的平方根:
SQRT ( -1 ) = 函数'SQRT'的参数具有错误的数据类型或结果太大或太小
如果DAX检测到这样的错误,它会阻止表达式的任何进一步计算,并引发错误。您可以使用ISERROR函数来检查表达式是否会导致错误,您将在本章后面使用它。
最后,请记住,NaN等特殊值在Power Pivot或Visual Studio窗口中以这种方式显示,但是当某些客户端工具(如Excel数据透视表)显示时,它们可能会显示为错误。而且,这些特殊值将被错误检测函数检测为错误。
2.4.3 空值或缺失值
我们检查的第三个类别不是特定的错误条件,而是存在空值,当将这些空值与计算中的其他元素组合时,可能会导致意外返回或计算错误。您需要了解DAX如何处理这些特殊值。
DAX使用BLANK以相同的方式处理缺失值、空值或空单元格。BLANK不是一个真正的值,而是一种识别这些条件的特殊方法。您可以通过调用BLANK函数获取DAX表达式中的BLANK,该函数与空字符串不同。例如,以下表达式始终返回一个空白值,该值将在数据透视表中显示为空单元格:
= BLANK ()
就其本身而言,这个表达式是无用的,但是每当你要返回空值时,BLANK函数就会变得有用了。例如,您可能希望显示一个空单元格而不是0,如下面的表达式计算销售交易的总折扣,如果折扣为0则将单元格留空:
= IF ( Sales[DiscountPerc] = 0, BLANK (), Sales[DiscountPerc] * Sales[Amount] )
BLANK本身不是错误,而是空值。因此,包含BLANK的表达式可能会返回值或空白,具体取决于所需的计算。例如,只要Sales [Amount]为BLANK,以下表达式就会返回BLANK:
= 10 * Sales[Amount]
换句话说,当一项或两项都为空时,算术乘积的结果就是BLANK。 BLANK在DAX表达式中的传递在其他几个算术和逻辑运算中也会发生,如以下示例所示:
BLANK () + BLANK () = BLANK ()
10 * BLANK () = BLANK ()
BLANK () / 3 = BLANK ()
BLANK () / BLANK () = BLANK ()
BLANK () || BLANK () = FALSE
BLANK () && BLANK () = FALSE
BLANK () = BLANK () = TRUE
但是,表达式结果中的BLANK传递并不适用于所有公式。有些计算不会传递BLANK,而是根据公式的其他项返回一个值。这些示例包括加法、减法、BLANK除法以及BLANK和有效值之间的逻辑运算。在以下表达式中,您可以看到这些条件的一些示例及其结果:
BLANK () - 10 = -10
18 + BLANK () = 18
4 / BLANK () = Infinity
0 / BLANK () = NaN
FALSE || BLANK () = FALSE
FALSE && BLANK () = FALSE
TRUE || BLANK () = TRUE
TRUE && BLANK () = FALSE
Excel和SQL Excel中的空值
Excel用不同方法的处理空值。在Excel中,在求和或乘法中使用的所有空值都被认为是0,但如果它们是除法或逻辑表达式的一部分,则返回一个错误。
在SQL中,值在表达式中传递的方式与在DAX中使用空值传递的方式不同。正如您在前面的示例中所看到的,DAX表达式中BLANK的存在并不总是会产生BLANK结果,而SQL中存在的NULL通常会为整个表达式求值为NULL。
了解DAX表达式中空值或缺失值的行为以及使用BLANK在计算中返回空单元格也是控制DAX表达式结果的重要技巧。当您检测到错误的值或其他错误时,通常可以使用BLANK,这将在下一节中介绍。
2.4.4 拦截错误
现在您已经看到了可能发生的各种错误,您将学习拦截错误并纠正错误的方法,或者至少显示包含一些有意义信息的错误消息。 DAX表达式中存在的错误通常取决于表达式本身引用的表和列中包含的值。因此,您可能希望控制这些错误条件的存在并返回错误消息。标准方法是检查表达式是否返回错误,如果是,则将错误替换为消息或默认值。这有几个DAX函数可用于此方法。
第一个是IFERROR函数,它与IF函数非常相似,但它不计算布尔条件,而是检查表达式是否返回错误。您可以在此处看到IFERRROR函数的两种典型用法:
= IFERROR ( Sales[Quantity] * Sales[Price], BLANK () )
= IFERROR ( SQRT ( Test[Omega] ), BLANK () )
在第一个表达式中,如果Sales [Quantity]或Sales [Price]是无法转换为数字的字符串,则返回的表达式为空值;否则返回数量和价格的乘积。
在第二个表达式中,每当TestΩ列包含负数时,结果为空单元格。
当您以这种方式使用IFERROR时,您遵循需要使用ISERROR和IF的更一般的模式:
= IF (
ISERROR ( Sales[Quantity] * Sales[Price] ),
BLANK (),
Sales[Quantity] * Sales[Price]
)
= IF (
ISERROR ( SQRT ( Test[Omega] ) ),
BLANK (),
SQRT ( Test[Omega] )
)
当返回的表达式与测试的错误相同时,应该使用IFERROR;您不必在两个地方复制表达式,并且生成的公式在将来发生更改时更易于阅读,也更安全。但是,当您想要返回不同表达式的结果时,应该使用IF。例如,您可以检测SQRT的参数是否为有效参数,仅计算正数的平方根,并为负数计算BLANK:
= IF ( Test[Omega] >= 0, SQRT ( Test[Omega] ), BLANK () )
考虑到IF语句的第三个参数具有默认值BLANK,您还可以编写相同的表达式:
= IF ( Test[Omega] >= 0, SQRT ( Test[Omega] ) )
一个特例是针对空值的测试。 ISBLANK函数检测空值条件,如果参数为BLANK则返回TRUE。
这一点很重要,尤其是当缺失的值与设置为0的值含义不同时。在以下示例中,我们计算销售交易的运输成本,如果交易没有指定重量,则使用产品的默认运费:
= IF (
ISBLANK ( Sales[Weight] ),
Sales[DefaultShippingCost],
Sales[Weight] * Sales[ShippingPrice]
)
如果我们只是将产品重量和运输价格相乘,那么对于所有缺少重量数据的销售交易,我们将得到一个空成本。
尽量避免使用错误处理函数
即使现在尚未讨论DAX代码优化,您需要注意错误处理函数可能会在代码中产生严重的性能问题。并不是说他们自己运算很慢。问题是当错误发生时,DAX引擎不能在代码中使用优化的路径。在大多数情况下,检查操作数以查找可能的错误比使用错误处理引擎更有效。例如,不写这个:
= IFERROR (
SQRT ( Test[Omega] ),
BLANK ()
)
写这个更好:
= IF (
Test[Omega] >= 0,
SQRT ( Test[Omega] ),
BLANK ()
)
第二个表达式不需要检测错误,它比前一个更快。当然,这是一个通用方法。有关详细说明,请参见第16章“优化DAX”。
2.5 格式化DAX代码
在继续解释DAX语言之前,有必要介绍DAX的一个非常重要的方面,即格式化代码。 DAX是一种函数式语言,这意味着无论它有多复杂,DAX表达式总是带有一些参数的单个函数调用。代码的复杂性转化为您用作最外层函数的参数的表达式的复杂性。
由于这个原因,通常可以看到超过10行或更多的表达式。看到一个20行的DAX表达并不奇怪,你会熟悉它。然而,随着公式的长度和复杂性的增长,学习如何格式化代码,使其变得可读,这是非常重要的。
格式化DAX代码没有“官方”标准,但我们认为描述我们在代码中使用的标准非常重要。它可能不完美,你可能更喜欢不同的东西,我们并不反对。你唯一需要记住的是:“格式化你的代码,永远不要把所有内容写在一行上,否则你马上会遇到麻烦。”
为了理解格式化代码的重要性,我们展示了一个计算时间智能的公式。这是一个有点复杂的公式,但肯定不是您将编写的最复杂的公式。如果不以某种方式对表达式进行格式化,则表达式如下所示:
IF (COUNTX (BalanceDate, CALCULATE (COUNT( Balances[Balance]
), ALLEXCEPT ( Balances,
BalanceDate[Date] ))) > 0, SUMX (ALL ( Balances[Account] ),
CALCULATE (SUM(
Balances[Balance] ), LASTNONBLANK (DATESBETWEEN
(BalanceDate[Date], BLANK(),LASTDATE(
BalanceDate[Date] )), CALCULATE ( COUNT( Balances[Balance]
))))), BLANK ())
试图理解这个公式计算的内容几乎是不可能的,因为你不知道哪个是最外面的函数,以及DAX如何计算不同的参数来创建完整的执行流程。我们已经看到了太多的客户以这种方式编写公式,他们在某些时候寻求帮助,为什么公式会返回不正确的结果。你猜怎么着?我们要做的第一件事就是格式化表达式,然后才开始研究它。
相同的表达式,格式化后,看起来是这样的:
=
IF (
COUNTX (
BalanceDate,
CALCULATE (
COUNT ( Balances[Balance] ),
ALLEXCEPT ( Balances, BalanceDate[Date] )
)
) > 0,
SUMX (
ALL ( Balances[Account] ),
CALCULATE (
SUM ( Balances[Balance] ),
LASTNONBLANK (
DATESBETWEEN ( BalanceDate[Date], BLANK (), LASTDATE ( BalanceDate[Date] ) ),
CALCULATE ( COUNT ( Balances[Balance] ) )
)
)
),
BLANK ()
)
代码是相同的,但这次更容易查看IF的三个参数,最重要的是,遵循缩进行自然产生的块,以及它们如何组成完整的执行流程。是的,代码仍然难以阅读,但现在问题是DAX,而不是格式。
DAXFormatter.com
我们创建了一个专门用于格式化DAX代码的网站。我们自己做了,因为格式化代码是一项耗时的操作,我们不想花时间来格式化我们编写的每个公式。一旦该工具工作,我们决定将其捐赠给公共领域,以便用户可以格式化他们自己的DAX代码(顺便说一句,我们已经能够以这种方式推广我们的格式化规则)。
您可以在www.daxformatter.com上找到它。用户界面很简单:只需复制DAX代码,单击FORMAT,页面刷新即可显示代码格式正确的版本,然后可以在原始窗口中复制和粘贴。
这是我们用来格式化DAX的一组规则:
- IF、COUNTX、CALCULATE等关键字始终使用空格分隔任何其他术语,并且始终以大写字母书写。
- 所有列引用都以TableName [ColumnName]的形式编写,表名和开始方括号之间没有空格。始终包含表名。
- 所有度量引用都以[MeasureName]的形式编写,没有任何表名。
- 逗号总是后面跟着一个空格,前面没有空格。
- 如果公式适合一行,则不需要应用其他规则。
- 如果公式不适合单行,则:
- 函数名称单独显示一行,并带有左括号。
- 所有参数都在单独的行中,用四个空格缩进,并在表达式的末尾加上逗号。
- 右括号与函数调用对齐,并且单独排成一行。
这些是我们使用的基本规则。有关这些规则的更详细列表,请访问http://sql.bi/daxrules。
如果您找到一种最适合您阅读公式的表达方式,那么请使用它。格式化的目标是使公式更易于阅读,因此请使用更适合您的方法。定义个人格式规则时要记住的最重要的事情是,您始终需要尽快查看错误。如果在之前显示的未格式化代码中,DAX提示缺少右括号,则很难找到错误所在的位置。在格式化的公式中,更容易看到右括号如何与开始函数调用匹配。
有关格式化DAX的帮助
格式化DAX不是一件容易的事,因为您需要在文本框中使用小字体来编写它。不幸的是,在撰写本文时,Excel和Visual Studio都没有为DAX提供良好的文本编辑器。
尽管如此,一些提示可能有助于编写DAX代码:
- 如果要增加字体大小,可以在旋转鼠标滚轮按钮的同时按住Ctrl键,以便更容易查看代码。
- 如果要在公式中添加新行,可以按Shift + Enter。
- 如果在文本框中进行编辑确实很麻烦,您可以随时在另一个编辑器(如记事本)中复制代码,然后在文本框中再次粘贴公式。
最后,每当您查看DAX表达式时,乍一看很难理解它是计算列还是度量值。因此,我们使用(=)来定义计算列,使用(:=)来定义度量值:
CalcCol = SUM (Sales[SalesAmount]) 这是一个计算列
CalcFld := SUM (Sales[SalesAmount]) 这是一个度量值
2.6 常用的DAX函数
现在您已经了解了DAX的基本原理以及如何处理错误条件,接下来简要介绍DAX最常用的函数和表达式。在本章的剩余部分中,您将看到一些最常用的DAX函数,您可能会在自己的数据模型中使用这些函数。
2.6.1 聚合函数
几乎每个数据模型都需要对聚合数据进行操作。 DAX提供了一组函数,这些函数聚合表中列的值并返回单个值。我们称这组函数为聚合函数。例如,以下度量值计算Sales表的SalesAmount列中所有数字的总和:
Sales := SUM ( Sales[SalesAmount] )
如果在计算列中使用此表达式(SUM),则它将聚合表的所有行,但在度量值中,它只考虑在数据透视表中使用时,由切片器、行、列和筛选条件筛选的行。
聚合函数(SUM、AVERAGE、MIN、MAX、STDEV和VAR)仅对数值或日期起作用。
备注:
MIN和MAX具有另一个功能:如果使用两个参数,它们将返回两个参数的最小值或最大值。因此,MIN(1,2)将返回1而MAX(1,2)返回2。此功能在2015年引入,在您需要计算复杂表达式的最小值或最大值时非常有用,因为它可以避免在IF语句中多次编写相同的表达式。
与Excel类似,DAX为这些函数提供了另一种语法,用于计算同时包含数值和非数值的列,例如文本列。该语法只是将后缀A添加到函数名称中,只是为了获得与Excel相同的名称和行为。但是,这些函数仅对包含TRUE / FALSE的列有用,因为TRUE计算为1,FALSE计算为0。文本列始终视为0。因此,无论列的内容是什么,如果使用,例如,文本列上的MAXA,结果总是得到0。此外,DAX在执行聚合时从不考虑空单元格。
即使这些函数可以用于非数字列而不会返回错误,它们的结果也没有用处,因为文本列没有自动转换为数字。这些函数分别名为AVERAGEA、COUNTA、MINA和MAXA。
备注:
尽管统计函数的名称相同,但它们在DAX和Excel中的使用方式存在差异,因为在DAX中,列具有类型,其类型决定了聚合函数的行为。Excel为每个单元格处理不同的数据类型,而DAX为每列处理单个数据类型。 DAX以表格形式处理数据,每列都有明确定义的类型,而Excel公式适用于异构单元格值,没有明确定义的类型。如果Power Pivot中的列具有数值数据类型,则所有值都只能是数字或空单元格。如果列是文本类型,则对于这些函数(COUNTA除外)始终为0,即使文本可以转换为数字,而在Excel中,该值也被视为逐个单元格的数字。由于这些原因,这些DAX函数对Text列不是很有用。
前面学习的函数对于执行值的聚合很有用。有时,您对聚合值不感兴趣,而只对计数感兴趣。因此,DAX提供了一组用于对行或值进行计数的函数:
- COUNT 仅对数字列进行操作
- COUNTA 对任何类型的列进行操作
- COUNTBLANK 返回列中空单元格的数量
- COUNTROWS 返回表行数
- DISTINCTCOUNT 返回列中不同值的数量
COUNTA是A后缀函数组中唯一有趣的函数,因为它返回非空列的值的值,适用于任何类型的列。如果要计算包含空值的列中的所有值,可以使用COUNTBLANK函数。最后,如果要计算表的行数,可以使用COUNTROWS函数。请注意,COUNTROWS需要将表作为参数,而不是列。
备注:
对于任何表中的任何列,COUNTA(表[列])+ COUNTBLANK(表[列])的结果将始终与COUNTROWS(表)相同。
最后一个函数DISTINCTCOUNT非常有用,因为它完全按照其名称建议:对列中的不同值计数,它将其作为唯一参数。 DISTINCTCOUNT将BLANK值计为可能值之一。
备注:
DISTINCTCOUNT是2012版DAX中引入的函数。早期版本的DAX不包括DISTINCTCOUNT,为了计算列的不同值的数量,您必须使用COUNTROWS( DISTINCT( table [column] ) )。这两种模式返回的结果完全相同,但DISTINCTCOUNT更容易阅读,只需要一个函数调用。
到目前为止,您学习的所有聚合函数都在列上工作(COUNTROWS除外,它适用于表)。因此,他们只能聚合来自单个列的值。聚合函数可以聚合表达式而不是单个列。这组函数非常有用,尤其是当您想使用不同相关表的列进行计算时。例如,如果Sales表包含所有销售交易,相关Product表包含有关产品的所有信息(包括其成本),那么您可以通过使用此表达式定义度量值来计算销售交易的总内部成本:
Cost := SUMX ( Sales, Sales[Quantity] * RELATED (Product[StandardCost] ) )
此度量计算Sales表中每行的Quantity(来自Sales表)和售出产品的StandardCost(来自相关Product表)的乘积。最后,它返回所有这些计算值的总和。
所有以X后缀结尾的聚合函数都以这种方式运行:它们为表(第一个参数)的每一行计算表达式(第二个参数),并返回由相应的聚合函数(SUM,MIN, MAX或COUNT)应用于这些计算的结果。
您将在第4章中进一步了解这种行为,因为要正确理解它们的行为,我们需要引入计算上下文的概念。可用的X后缀函数是SUMX、AVERAGEX、PRODUCTX、COUNTX、COUNTAX、CONCATENATEX、MINX和MAXX。还有一些没有X后缀的迭代器,如FILTER和ADDCOLUMNS。稍后将详细解释所有这些内容。
2.6.2 逻辑函数
有时您希望在表达式中构建逻辑条件 。例如,根据列的值实现不同的计算或拦截错误。在这些情况下,您可以使用DAX中的逻辑函数。您已经在上一节“处理DAX表达式中的错误”中学习了这个组中最重要的两个函数,即IF和IFERROR。
逻辑函数非常简单,按照它们的名称所暗示的去做,它们是AND、FALSE、IF、IFERROR、NOT、TRUE和OR。例如,如果您希望仅在“价格”列包含正确的数值时将金额计算为数量乘以价格,则可以使用以下模式:
Amount = IFERROR ( Sales[Quantity] * Sales[Price], BLANK ( ) )
如果您未使用IFERROR且价格列包含无效数字,则计算列的结果将是错误,因为如果单行生成计算错误,则错误将传递到整个列。但是,使用IFERROR可以拦截错误并将其替换为空值。
此类别中另一个有趣的函数是SWITCH,当您有一个列包含少量不同的值,并且您希望根据其值获得不同的行为时,SWITCH非常有用。例如,Product表中的Column Size包含L、M、S、XL,您可能希望生成一个更有意义的列。您可以使用嵌套的IF调用获取结果:
SizeDesc =
IF (
DProduct[Size] = "S",
"Small",
IF (
Product[Size] = "M",
"Medium",
IF (
Product[Size] = "L",
"Large",
IF ( Product[Size] = "XL", "Extra Large", "Other" )
)
)
)
一个更方便的方法来表达相同的公式,使用SWITCH,是:
SizeDesc =
SWITCH (
Product[Size],
"S", "Small",
"M", "Medium",
"L", "Large",
"XL", "Extra Large",
"Other"
)
后一个表达式中的代码更易读,即使不是更快,因为在内部,DAX将SWITCH语句转换为一组嵌套的IF函数。
提示
这里有一种有趣的方法,可以使用SWITCH函数检查同一个表达式中的多个条件。因为SWITCH被转换为一组嵌套的IF,其中第一个匹配优先,所以您可以使用此模式测试多个条件:
SWITCH (
TRUE (),
Product[Size] = "XL"
&& Product[Color] = "Red", "Red and XL",
Product[Size] = "XL"
&& Product[Color] = "Blue", "Blue and XL",
Product[Size] = "L"
&& Product[Color] = "Green", "Green and L"
)
实际上,使用TRUE作为第一个参数表示:“返回条件计算为TRUE的第一个结果。”
2.6.3 信息函数
每当需要分析表达式的类型,您都可以使用信息函数。所有信息函数都返回TRUE / FALSE值,可以在任何逻辑表达式中使用。它们是:ISBLANK、ISERROR、ISLOGICAL、ISNONTEXT、ISNUMBER和ISTEXT。
需要注意的是,当一个列(而不是一个表达式)作为参数传递时,函数ISNUMBER,ISTEXT和ISNONTEXT总是返回TRUE或FALSE,这取决于列的数据类型和每个单元格的空条件。
您可能想知道是否可以在文本列中使用ISNUMBER来检查是否可以转换为数字。
不幸的是,你不能使用这种方法;如果要测试文本值是否可转换为数字,则必须尝试转换并在错误时处理错误。例如,要测试列Price(文本类型)是否包含有效数字,您必须编写:
IsPriceCorrect = NOT ( ISERROR ( Sales[Price] + 0 ) )
DAX尝试向Price添加零以强制从Text值转换为数字;如果成功,则返回TRUE(因为ISERROR将返回FALSE),否则返回FALSE(因为ISERROR返回TRUE)。转换将失败,例如,如果某些行的价格具有“N / A”字符串值。
但是,如果您尝试使用ISNUMBER,如下面的表达式中所示,您将始终收到FALSE:
IsPriceCorrect = ISNUMBER ( Sales[Price] )
在这种情况下,ISNUMBER始终返回FALSE,因为根据元数据,Price列不是数字而是字符串,而不管每行的内容如何。
2.6.4 数学函数
DAX中可用的数学函数集与Excel中的相同集非常相似,具有相同的语法和行为。常用的数学函数是ABS、EXP、FACT、LN、LOG、LOG10、MOD、PI、POWER、QUOTIENT、SIGN和SQRT。随机函数是RAND和RANDBETWEEN。 EVEN和ODD让你测试数字。 GCD和LCM可用于计算两个数字的最大公分母和最小公倍数。 QUOTIENT返回两个数字的整数除法。
最后,有几个舍入函数值得举个例子;实际上,您可能会使用多种方法来获得相同的结果。考虑这些计算列,以及图2-3中的结果:
FLOOR = FLOOR ( Tests[Value], 0.01 )
TRUNC = TRUNC ( Tests[Value], 2 )
ROUNDDOWN = ROUNDDOWN ( Tests[Value], 2 )
MROUND = MROUND ( Tests[Value], 0.01 )
ROUND = ROUND ( Tests[Value], 2 )
CEILING = CEILING ( Tests[Value], 0.01 )
ISO.CEILING = ISO.CEILING ( Tests[Value], 0.01 )
ROUNDUP = ROUNDUP ( Tests[Value], 2 )
INT = INT ( Tests[Value] )
FIXED = FIXED ( Tests[Value], 2, TRUE )
如您所见,FLOOR,TRUNC和ROUNDDOWN非常相似,除了您可以指定要舍入的位数的方式。在相反的方向上,CEILING和ROUNDUP的结果非常相似。您可以看到在MROUND和ROUND函数之间进行舍入的方式存在一些差异。
2.6.5 三角函数
DAX提供了一组丰富的三角函数,可用于某些计算。我们不会详细介绍这些函数,因为如果需要,它们的使用很简单。它们是COS、COSH、COT、COTH、SIN、SINH、TAN和TANH。用A对它们进行前缀计算弧形版本(反正弦,反余弦等)。
DEGREES和RADIANS分别执行转换为度和弧度,SQRTPI在乘以π后计算其参数的平方根。
2.6.6 文本函数
几乎所有DAX中可用的文本函数都与Excel中可用的类似,只有少数例外。它们是CONCATENATE,EXACT,FIND,FIXED,FORMAT,LEFT,LEN,LOWER,MID,REPLACE,REPT,RIGHT,SEARCH ,SUBSTITUTE,TRIM,UPPER和VALUE。这些函数对于处理文本和从包含多个值的字符串中提取数据非常有用。例如,在图2-4中,您可以看到从包含以逗号分隔的这些值的字符串中提取名字和姓氏的示例,标题位于中间,我们要删除它们。
我们开始计算两个逗号的位置,然后我们使用这些数字来提取文本的正确部分。如果字符串中的逗号少于两个,则SimpleConversion列实现可能返回错误值的公式(如果根本没有逗号则会引发错误),而FirstLastName列实现一个不会失败的更复杂的表达式丢失逗号的情况:
Comma1 = IFERROR ( FIND ( ",", People[Name] ), BLANK ( ) )
Comma2 = IFERROR ( FIND ( ",", People[Name], People[Comma1] + 1 ), BLANK ( ) )
SimpleConversion = MID ( People[Name], People[Comma2] + 1, LEN
( People[Name] ) )
& " " & LEFT ( People[Name], People[Comma1] - 1 )
FirstLastName = TRIM (
MID (
People[Name],
IF (
ISNUMBER ( People[Comma2] ),
People[Comma2],
People[Comma1]
) + 1,
LEN ( People[Name] )
))&
IF (
ISNUMBER ( People[Comma1] ),
" " & LEFT ( People[Name], People[Comma1] - 1 ),
""
)
如您所见,FirstLastName列由一个长DAX表达式定义,但您必须使用它来避免可能的错误,即使单个值生成错误也会传递到整个列。
2.6.7 转换函数
您之前了解到,DAX执行数据类型的自动转换,以根据运算符的需要进行调整。即使它是自动发生的,一组函数仍然可以执行类型的显式转换。
CURRENCY可以转换货币类型的表达式,而INT将表达式转换为整数。DATE和TIME将日期和时间部分作为参数并返回正确的DATETIME。 VALUE将字符串转换为数字格式,而FORMAT将数值作为第一个参数,将字符串格式作为第二个参数,并将数值转换为字符串。 FORMAT常用于DateTime。例如,以下表达式返回“2015 Jan 12”。
= FORMAT ( DATE ( 2015, 01, 12 ), "yyyy mmm dd" )
通过使用DATEVALUE函数执行相反的操作,即将字符串转换为DateTime值。
2.6.8 日期和时间函数
几乎在所有类型的数据分析中,处理时间和日期都是工作的一个重要部分。 DAX具有大量按日期和时间运行的函数。其中一些函数与Excel中类似的函数相对应,并对DateTime数据类型进行简单的转换。日期和时间函数分别是DATE、DATEVALUE、DAY、EDATE、EOMONTH、HOUR、MINUTE、MONTH、NOW、SECOND、TIME、TIMEVALUE、TODAY、WEEKDAY、WEEKNUM、YEAR和YEARFRAC。
为了对日期进行更复杂的操作,例如逐年比较聚合值或计算度量值的年初至今值,还有另一组时间智能函数,将在第7章“时间智能计算”中介绍。
正如本章前面提到的,DateTime数据类型在内部使用浮点数,其中整数部分对应于1899年12月30日之后的天数,小数部分则表示当天的时间占比。小时、分钟和秒被转换为当天的小数部分。因此,向DateTime值添加整数会使该值增加相应的天数。但是,您可能会发现使用转换函数从日期中提取日、月和年更方便。在图2-5中,您可以看到如何从包含日期列表的表中提取此信息:
Day = DAY ( Calendar[Date] )
Month = FORMAT ( Calendar[Date], "mmmm" )
Year = YEAR ( Calendar[Date] )
2.6.9 关系函数
RELATED和RELATEDTABLE两个函数使您能够在DAX公式内跨越关系。
您已经知道计算列可以引用定义它的表的列值。因此,Sales中定义的计算列可以引用同一表的任何列。但是,如果必须引用另一个表中的列,您可以做什么?通常,您不能在其他表中使用列,除非在两个表之间的模型中定义关系。如果两个表存在一个关系,则RELATED函数允许您访问相关表中的列。
例如,您可能希望在Sales表中创建一个计算列,检查已售出的产品是否属于“手机”类别,并且此类别下,减少标准成本。要计算此列,必须使用检查产品类别的值,该值不在Sales表中。然而,一系列关系从Sales开始,通过Product和Product Subcategory到达Product Category,如图2-6所示。
从原始表到相关表需要多少步骤无关紧要,DAX将遵循完整的关系链并返回相关的列值。因此,AdjustedCost列的公式可以是:
Sales[AdjustedCost] =
IF (
RELATED ( 'Product Category'[Category] ) = "Cell Phone",
Sales[UnitCost] * 0.95,
Sales[UnitCost]
)
在一对多关系中,RELATED可以从多端访问一端,因为在这种情况下,相关表中只有一行存在(如果有)。如果不存在这样的行,RELATED只返回BLANK。
如果您处于关系的一端并且想要访问多端,那么不能用RELATED,因为多端的许多行需要用于单行。在这种情况下,您可以使用RELATEDTABLE。RELATEDTABLE返回一个表,其中包含与当前行相关的所有行。例如,如果您想知道每个类别中有多少产品,可以使用以下公式在Product Category中创建一个列:
= COUNTROWS ( RELATEDTABLE ( Product ) )
对于每个产品类别,此计算列将显示相关产品的数量,如图2-7所示。
与RELATED的情况一样,RELATEDTABLE可以遵循一个关系链,从一端指向多端。