變量
在編寫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中總結:值得注意的是,無窮大和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