【DAX圣经】第二章:介绍DAX(2)
变量
在编写DAX表达式时,您可以通过使用变量来避免重复相同的表达式。例如,看下面的表达式:
VAR TotalSales = SUM ( Sales[SalesAmount] )
RETURN ( TotalSales - SUM ( Sales[TotalProductCost] ) ) / TotalSales
你可以定义很多变量,它们是你定义它们的表达式的局部变量。变量对于简化代码非常有用,因为您可以避免重复相同的子表达式。变量使用惰性计算,这意味着,如果你定义一个变量,由于任何原因,在你的代码中没有使用,那么这个变量永远不会被计算。如果需要计算它,那么这种情况下计算只发生一次:该变量的后续用法将读取先前计算的值。因此,当您使用多次复杂表达式时,它们作为一种优化技术也很有用。
此外,正如你将在第4章学到的,变量是非常有用的,因为它们使用定义计算上下文而不是已经使用过的变量。
处理DAX表达式中的错误
现在您已经了解了一些基本的语法,您将学习如何优雅地处理无效的计算。DAX表达式可能包含无效的计算,因为它引用的数据对公式无效。例如,您可能有一个除数为0,或一个不是数字的列值,却在算术运算中使用,比如乘法。您必须了解这些错误是如何在默认情况下处理的,以及如果您想要一些特殊的处理,如何拦截这些条件。
在您学习如何处理错误之前,有必要描述一下在DAX公式计算期间可能出现的不同类型的错误。他们是:
-
转换错误
-
算术操作错误
-
空值或缺失值
转换错误
第一类错误是转换错误。正如您在本章中所看到的,当运算需要时,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
因为这个和式的第一个操作数是一个列,在本例中是文本数据类型,你必须确保DAX可以将该列中的所有值转换成数字。如果DAX在转换某些内容以满足运算的需要时失败,那么您将会发生转换错误。这是典型的情况:
"1 + 1" + 0 = Cannot convert value '1+1' of type string to a number
DATEVALUE ("25/14/2010") = Type mismatch
为了避免这些错误,您需要在DAX表达式中添加错误检测逻辑来拦截错误条件,并始终返回一个有意义的结果
算术操作错误
第二类错误是算术运算,例如零的除法或负数的平方根。这些都不是与类型转换相关的错误:当您试图用一个无效值去调用函数或者执行运算时,这类错误就会出现。
0的除法需要特殊的处理,因为它的行为方式不是很直观(也许对数学家来说是这样的)。当你把一个数字除以0时,DAX通常会返回特殊值无穷大。此外,在0除以0或无穷大的特殊情况下,DAX返回特殊的NaN(不是数字)值。
因为这是一种奇怪的行为,值得在表2-2中总结: 表2-2值得注意的是,无穷大和NaN不是错误,而是DAX的特殊值。事实上,如果你把一个数字除以无穷那么这个表达式就不会产生错误而是返回0
除了这种特殊情况之外,当使用错误的参数调用函数时,DAX可以返回算术错误,比如负数的平方根。
SQRT ( -1 ) = An argument of function 'SQRT' has the wrong data type or the result is too large or too small
如果DAX检测到这样的错误,它将阻止任何进一步的表达式计算,并引发错误。你可以使用ISERROR函数来检查表达式是否会导致错误,这是你在本章后面会用到的
最后,请记住,像NaN这样的特殊值在Power Pivot或Visual Studio窗口中以这样的方式显示,但是当由一些客户端工具显示时,它们可以显示为错误,比如Excel Pivot表。此外,这些特殊值将被错误检测函数检测为错误
空值或缺失值
我们检查的第三个类别不是一个特定的错误条件,而是空值的存在,当将这些空值与计算中的其他元素组合在一起时,可能会导致意外的返回值或计算错误。你需要理解DAX是如何对待这些特殊值
DAX以使用空值方式处理缺失的值、空值或空单元格。空值不是真正的值,而是一种识别这些条件的特殊方法。你可以通过调用空值函数来获得DAX表达式中的空值,这与空字符串不同。例如,下列表达式总是返回空值,它将显示为透视表中的空单元格。
= BLANK ()
就其本身而言,这个表达式是无用的,但是每当您想返回一个空值时,空值函数本身就变得有用了。例如,您可能想要显示一个空的单元格而不是0,如下面的表达式计算一个销售事务的总折扣,如果折扣为0,则会显示空白单元格。
= IF ( Sales[DiscountPerc] = 0, BLANK (), Sales[DiscountPerc] * Sales[Amount] )
blank()本身不是一个错误,而是一个空值。因此,包含空值的表达式可能返回一个值或一个空值,这取决于所需的计算。例如,当Sales[Amount]为空时,下列表达式返回空值
= 10 * Sales[Amount]
换句话说,当一个或两个运算值都是空值的时候,运算的结果是空值。在DAX表达式中,空值的传播发生在其他几个算术和逻辑操作中,如下面的例子所示
BLANK () + BLANK () = BLANK ()
10 * BLANK () = BLANK ()
BLANK () / 3 = BLANK ()
BLANK () / BLANK () = BLANK ()
BLANK () || BLANK () = FALSE
BLANK () && BLANK () = FALSE
BLANK () = BLANK () = TRUE
然而,在表达式的结果中,空值的传播不会发生在所有的公式中。有些计算不会传播空值,而是根据公式的其他部分返回一个值。这些例子包括加法、减法、除以空值,以及在空值和有效值之间的逻辑运算。在下面的表达式中,您可以看到这些条件的一些示例,以及它们的结果:
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中,乘法和求和运算中,所有的空值被当做0处理;但是在除法或者是在逻辑运算中,则会返回一个错误。
在SQL中,null值以一种不同的方式在表达式中传播,而不是在DAX中出现空值。正如您在前面的例子中所看到的,在DAX表达式中出现空值并不总是导致空值的结果,而SQL中的NULL通常在整个表达式中均表现为null。
在一个DAX表达式中理解空值或缺失值的行为,以及在计算中使用空值返回空单元格,也是控制DAX表达式结果的重要技能。当您检测到错误的值或其他错误时,您通常可以使用空值,正如您将在下一节中学习的那样
拦截错误
第一个是IFERROR函数,它与IF函数非常相似,但它不是评估布尔条件,而是检查表达式是否返回错误。你可以看到IFERRROR函数的两个典型用法
= IFERROR ( Sales[Quantity] * Sales[Price],
BLANK () ) = IFERROR ( SQRT ( Test[Omega] ), BLANK () )
在第一个表达式中,如果销售数量或销售价格是不能转换成数字的字符串,则返回的表达式是空值;否则,数量和价格的乘积就会返回
在第二个表达式中,每次测试Test[Omega] 列包含一个负数时,结果是空单元格
当你使用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语句的第三个参数有一个默认值为空,您也可以编写与之相同的表达式
= IF ( Test[Omega] >= 0, SQRT ( Test[Omega] ) )
一个特定的例子是对空值的测试。ISBLANK函数检测到空值条件,如果参数为空,则返回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”。
格式化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代码,点击格式,页面刷新就能显示已经被格式化的代码,你可以在原来的窗口中复制粘贴。
这是我们用来格式化DAX的一组规则:
-
像IF,COUNTX,CALCULATE总是用空格来分隔,同时它们总是用大写字母写的
-
所有列引用都以TableName[ColumnName]形式编写,表名和方括号之间没有空格。同时表名总是存在
-
所有的度量引用都是用 [MeasureName]形式编写,没有任何表名
-
逗号总是跟着空格,而不会在空格之前
-
如果这个公式适用于一行,那么就不需要应用其他规则了
-
如果这个公式不符合在一行上面,那么:
-
函数名本身就站在一行上,包含开括号
-
所有的参数都是分开的,用四个空格缩进,在表达式的末尾加上逗号。
-
结束括号与函数调用对齐,并单独站在一行中
如果你找到一种方法来表达最适合你的阅读方法的公式,那就用它吧。格式化的目的是让公式更容易阅读,所以使用对你更好的方法。在定义您的个人格式规则时,要记住的最重要的事情是,您总是需要尽快看到错误。如果在之前显示的未格式化的代码中,DAX会提示一个缺失的括号,那么就很难发现错误所在的位置。在格式化的公式中,可以更容易地看到闭合括号与打开的函数调用相匹配
格式化DAX帮助
格式化DAX不是一件容易的事情,因为你需要在文本框中使用一个小字体来编写它。不幸的是,在撰写本文时,无论是Excel还是Visual Studio都没有为DAX提供一个好的文本编辑器。尽管如此,一些提示可能有助于编写您的DAX代码:
- 如果您想要增加字体大小,您可以按住Ctrl键,同时在鼠标上旋转滚轮按钮,这样就可以更容易地查看代码了。
- 如果你想在公式中添加新的一行,你可以按Shift+Enter
- 如果在文本框中编辑真的很痛苦,你可以在另一个编辑器中复制代码,比如记事本,然后在文本框中再次粘贴这个公式
最后,当你看一个DAX的表达式时,乍一看,你很难理解它是一个计算的列还是一个度量。因此,我们定义一个计算列时我们用等号(=),用运算符(:=)来定义度量:
CalcCol = SUM (Sales[SalesAmount]) is a calculated column
CalcFld := SUM (Sales[SalesAmount]) is a measure