在快速介绍前一章之后,现在是时候开始讨论DAX语言了。在这一章中,您将学习语言的语法、计算列和度量值(在Excel术语中称为计算字段)之间的区别,以及在DAX中最常用的函数。
由于这是一个介绍性的章节,许多函数没有被深度覆盖。在书的后面部分,我们会更深入地解释它们。现在,已经足够介绍这些函数并开始研究一般的DAX语言了。
理解DAX计算
为了表达复杂的公式,您需要学习DAX的基础知识,包括语法、DAX可以处理的不同数据类型、基本运算符以及如何引用列和表。这些概念将在接下来的几节中讨论。
您可以使用DAX来计算表中列的值。您可以聚集、计算和搜索数字,但是,最后,所有的计算都涉及到表和列。因此,要学习的第一个语法是如何引用表格中的列。
一般的格式是写表名,用单引号括起来,然后是列名,用方括号括起来,如下所示:
'Sales'[Quantity]
如果表名不是以数字开头的,那么您可以省略单引号,它不包含空格,而且它不是一个保留的单词(如 Date 或 Sum)
注意
不要在表名中使用空格是很好的做法。这样,您就避免了公式中的引号,因为这使得代码更难阅读。但是请记住,当使用pivot表或Power View等其他客户端工具浏览模型时,该表的名称与您将看到的名称相同。因此,如果您希望在报表中有空格,那么您需要在代码中使用单引号。
你也可以避免写表名,以防你在已经定义了公式的同一个表中引用一个列或一个度量值。因此,[Quantity]是一个有效的列引用,如果是在计算列中,或者在销售表中的度量值中。即使这种技术在语法上是正确的,而且用户界面可能会建议你选择一个列而不是完整书写它,我们还是强烈建议你不要使用它。这样的语法使得代码难于阅读,所以当您在DAX表达式中引用一个列时,最好总是使用表名。
DAX 数据类型
DAX可以用不同的数字类型进行计算,其中有7个。在接下来的列表中,我们展示了DAX的名称和相同数据类型的更常见的名称。例如,在DAX术语中,布尔值被称为tru/false。我们更倾向于遵循事实上的命名标准,因此我们将它们称为布尔值。
- 整数(Integer)
- 十进制数(Float)
- 货币(Currency),一个固定的十进制数字内部存储为整数
- 日期(DateTime)
- 布尔(tru/false)
- 文本(String)
- 二进制大对象(BLOB)
DAX有一个强大的类型处理系统,这样您就不必担心数据类型:当您编写DAX表达式时,结果类型是基于表达式中使用的术语的类型。您需要注意这一点,以防DAX表达式返回的类型不是预期的那样:然后您必须研究表达式本身中使用的术语的数据类型。
例如,如果一个和式的一个项是日期,结果也是一个日期;然而,如果同一个运算符被用于整数,那么结果就是一个整数。这就是所谓的操作符重载,您可以在图2-1中看到它的行为示例,其中OrderDatePlusOneWeek的列是通过在订单日期列的值中添加7来计算的。结果就像我们说的,是一个日期。除了操作符重载之外,DAX还会在操作符需要时自动将字符串转换成数字和数字。例如,如果您使用连接字符串的&运算符,那么DAX将其参数转换为字符串。如果你看这个公式:
= 5 & 4
它返回“54”作为字符串。另一方面,这个公式:
= "5" + "4"
返回一个值为9的整数结果
产生的值取决于操作符,而不是源列,它们是根据操作符的要求进行转换的。即使这种行为看起来很方便,在本章的后面,您将看到在这些自动转换过程中可能会发生什么类型的错误。我们建议避免自动转换。如果需要进行某种转换,那么如果您控制它并使转换显式,那就更好了。为了更加明确,前面的例子应该是:
= VALUE ( "5" ) + VALUE ( "4" )
对于习惯于使用Excel或其他语言的人来说,DAX数据类型可能很熟悉。您可以在http://msdn.microsoft.com/en-us/librar/gg492146.aspx上找到DAX数据类型的规范。但是,对每种数据类型分享一些考虑是很有用的。
整数(Integer)
DAX只有一个整数数据类型,可以存储64位的值。在DAX的整数值之间的所有内部计算也使用64位的值。
十进制数(Float)
一个十进制数总是作为一个双精度浮点值存储。不要将DAX数据类型与Transact-SQL的十进制和数字数据类型混淆:SQL中的DAX十进制数字的对应数据类型是Float。
货币(Currency)
货币数据类型存储一个固定的十进制数。它可以表示4个小数点,它内部存储为64位整数值,除以10000。在货币数据类型之间执行的所有计算总是忽略4位小数点后的小数。如果你需要更精确的数据,你需要对十进制数据类型进行转换。
货币数据类型的默认格式包括货币符号。您还可以将货币格式化应用到整数和十进值数字,您也可以使用一种没有货币符号的格式来表示货币数据类型。
日期(datetime)
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年,这个bug仍然在DAX中,引入了与Excel的向后兼容性。bug的存在(我们应该称它为特性吗?)可能会在1900年3月1日之前出现错误。因此,根据设计,DAX的第一个官方支持的日期是1900年3月1日。在该日期之前的时间内执行的日期计算可能会导致错误,应该被认为是不准确的。如果你需要在1900年之前进行计算,你应该使用数学手段来移动1900年之后的日期,执行你的计算,然后把日期重新回到过去。
布尔值 (TRUE/FALSE)
布尔数据类型用于表示逻辑条件。例如,由下列表达式定义的计算列是布尔型:
= Sales[Unit Price] > Sales[Unit Cost]
文本 (String)
DAX中的每一个字符串都存储为Unicode字符串,其中每个字符存储在16位中。在默认情况下,字符串之间的比较是不敏感的,所以两个字符串“Power Pivot”和“POWER PIVOT”被认为是相等的。
二进制大对象(BLOB)
在数据模型中使用BLOB数据类型来存储图像,但是在DAX中是不可访问的。它主要用于Power View或其他客户端工具,以显示直接存储在数据模型中的图片。
DAX 运算符
在看到了操作符在确定表达式类型方面的重要性之后,您现在可以看到在表2-1中DAX中可用的操作符列表。此外,逻辑运算符也可用作DAX函数,语法非常类似于Excel。例如,你可以写:
AND ( [CountryRegion] = "USA", [Quantity] > 0 )
OR ( [CountryRegion] = "USA", [Quantity] > 0 )
这是等价的,分别是
[CountryRegion] = "USA" && [Quantity] > 0
[CountryRegion] = "USA" || [Quantity] > 0
当您必须编写复杂的条件时,使用函数而不是布尔逻辑的运算符变得非常有用。事实上,当涉及到格式化大量的代码时,函数的格式和读取要比操作符容易得多。然而,函数的一个主要缺点是每次只能传递两个参数。这就要求你在有两个以上条件的情况下进行嵌套函数。
理解计算列和度量值
现在您已经了解了DAX语法的基础知识,您需要了解DAX中最重要的概念之一:计算列和度量值之间的区别。尽管它们在第一眼看上去可能是相似的,因为你可以通过两种方式进行计算,但它们实际上是非常不同的,理解差异是开启DAX真正力量的关键。
计算列
例如,如果你想在Excel中创建一个计算好的列,你可以简单地移动到表格的最后一列,它被命名为Add Column,并开始编写公式。当然,实现DAX的其他工具可能有不同的用户界面。你在公式栏中创建了DAX表达式,而智能感知在写表达式时帮助你。
一个计算列就像表中的任何其他列一样,您可以在行、列、过滤器或主表或任何其他报告的值中使用它。如果需要的话,您还可以使用一个经过计算列来定义的一段关系。为计算列定义的DAX表达式是在其所属表的当前行的上下文中运行的。任何对列的引用都返回当前行的此列的值。你不能直接访问其他行的值。
注意
正如您稍后将看到的,有DAX函数将整张表的列的值聚合起来。获得行的子集的值的惟一方法是使用DAX函数返回一张表,然后对其进行操作。通过这种方式,您可以对一系列行的列值进行聚合,并可能通过过滤仅由一行组成的表格来操作不同的行。在第4章“理解计算上下文”中,您将了解更多关于这个主题的知识。
您需要记住的关于计算列的一个重要概念是,它们是在数据库处理期间计算出来的,然后存储在模型中。如果您习惯了SQL计算列(不是持久的),这可能看起来很奇怪,因为它是在查询时计算的,并且不使用内存。然而,在表格模型中,所有计算列占用内存空间,并在表处理期间计算。
每当您创建非常复杂的计算列时,这种行为都是有帮助的。计算它们所需的时间总是过程时间而不是查询时间,从而获得更好的用户体验。然而,您必须记住,一个计算列使用的是宝贵的RAM(随机存储器)。例如,如果您有一个计算列的复杂公式,您可能会尝试将不同中间列中的计算步骤分开。尽管这种技术在项目开发过程中很有用,但它在生产中是一个坏习惯,因为每个中间计算都存储在RAM中,浪费了宝贵的空间。
度量值
在DAX模型中有另一种定义计算的方法,当你不想计算每一行的值时,它是有用的,但是,你想要从表格中的许多行中聚合值。我们称这类的计算为度量值。
例如:您可以在销售表中定义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]) 时,你指的是在这个单元格下聚集的所有单元格的总和,而当你在一个计算列中使用销售销售额时,你指的是当前行中销售金额列的值。
需要在表中定义一个度量值。这是DAX语言的一个要求。然而,该度量值并不真正属于这一张表。事实上,你可以将一个度量值从一个表移动到另一个表而不用担心丢失它的功能。
计算列和度量值的区别
尽管它们看起来相似,两者之间却有很大的区别。计算列的值是在数据刷新期间计算的,并使用当前行作为上下文;它不依赖于透视表上的用户的行为。度量值则是根据当前上下文定义的数据进行聚合。例如,在一个透视表中,源表根据单元格进行过滤,度量值则使用这些过滤器对数据进行聚合和计算。换句话说,度量值总是在计算上下文中对数据的聚合进行操作,因此默认的执行模式不引用任何一行。在第4章中进一步解释了计算上下文。
选择度量值和计算列
现在您已经看到了计算的列和度量值之间的区别,您可能想知道什么时候该使用一个。有时这是一种选择,但在大多数情况下,您的计算需要决定您的选择。
当您想要执行以下操作时,您必须定义一个计算过的列:
- 将计算结果放在Excel切片器中,或者在透视表的行或列中看到对应的结果(而非值域),或者在DAX查询中使用结果作为筛选条件。
- 定义一个严格绑定到当前行的表达式。(例如,Price * Quantity 不能通过求平均或求两列的总和上计算得出)。
- 分类文本或数字。(例如,度量值的范围,一个客户的年龄范围,比如0——18,18——25,等等。)
但是,当您想要显示结果的计算值时,您必须定义一个度量值,以反映用户选择,并在透视表的值区域中看到它们,例如:
- 当你计算一个透视表区域的利润百分比
- 当你计算一个产品与所有的产品的比率,同时保持对年份和地区的筛选条件
您可以用计算列和度量值来表示一些计算,即使您需要在这些情况下使用不同的DAX表达式。例如,您可以将 GrossMargin 定义为一个计算列:
Sales[GrossMargin] = Sales[SalesAmount]- Sales[TotalProductCost]
但它也可以被定义为一个度量值:
[GrossMargin] := SUM ( Sales[SalesAmount] ) - SUM ( Sales[TotalProductCost] )
我们建议您在这种情况下使用度量值,因为在查询时进行计算不消耗内存和磁盘空间,这在大型数据集中非常重要。但当模型的大小不是问题时,您可以使用您更熟悉的方法。
交叉引用
很明显,一个度量值可以引用一个或多个计算列。相反的情况可能不那么直观。计算列可以引用一个度量值:在这种情况下,它强制计算当前行的上下文所定义的度量值。这个操作将一个度量的结果转换成一个列,它不会受到用户操作的影响。显然,只有特定的操作下才能产生有意义的结果,通常一个度量值的计算非常依赖于透视表中用户所做的选择。