在迭代函數中使用行上下文
您了解到,無論何時定義計算列或使用X函數開始迭代時,DAX都會創建行上下文。當我們使用計算列時,行上下文的存在很容易使用和理解。實際上,我們甚至可以在不知道行上下文存在的情況下創建簡單的計算列。原因是行上下文是由引擎自動創建的。因此,我們不必擔心行上下文的存在。另一方面,在使用迭代函數時,我們負責創建和處理行上下文。此外,通過使用迭代函數,我們可以創建多個嵌套行上下文;這增加了代碼的復雜性。因此,重要的是要更準確地了解帶有迭代函數的行上下文的行為。
例如,查看以下DAX度量值:
IncreasedSales := SUMX ( Sales, Sales[Net Price] * 1.1 )
由于 SUMX 是迭代函數,因此 SUMX 在 Sales 表上創建一個行上下文,并在迭代過程中使用它。行上下文迭代 Sales 表(第一個參數),并在迭代過程中將當前行提供給第二個參數。換句話說,DAX在包含第一個參數上當前迭代的行的行上下文中計算內部表達式( SUMX 的第二個參數)。
請注意, SUMX 的兩個參數使用不同的上下文。實際上,任何DAX代碼段都可以在調用它的上下文中工作。因此,執行表達式時,可能已經有一個過濾器上下文和一個或多個行上下文處于活動狀態。查看帶有注釋的相同表達式:
SUMX (
Sales, -- External filter and row contexts
Sales[Net Price] * 1.1 -- External filter and row contexts + new row context
)
使用來自調用方的上下文評估第一個參數Sales。使用外部上下文加上新創建的行上下文來評估第二個參數(表達式)。
所有迭代函數的行為均相同:
- 在現有上下文中評估第一個參數,以確定要掃描的行。
- 為在上一步中評估的表的每一行創建一個新的行上下文。
- 迭代表并在現有評估上下文中評估第二個參數,包括新創建的行上下文。
- 匯總上一步中計算的值。
請注意,原始上下文在表達式內仍然有效。迭代函數添加新的行上下文;它們不會修改現有的篩選上下文。例如,如果外部篩選上下文包含針對紅色的篩選,則該篩選在整個迭代過程中仍處于活動狀態。此外,請記住,行上下文是迭代的,它不會篩選。因此,無論如何,我們不能使用迭代器覆蓋外部篩選上下文。
該規則始終有效,但是有一個重要的細節并不重要。如果先前的上下文已經包含同一表的行上下文,則新創建的行上下文將在同一表上隱藏先前的現有行上下文。對于DAX新手,這可能是錯誤的來源。因此,我們將在接下來的兩節中詳細討論行上下文隱藏。
不同表上的嵌套行上下文
由迭代函數求值的表達式可能非常復雜。而且,表達式可以單獨包含其他迭代。乍一看,在另一個迭代中開始一個迭代可能看起來很奇怪。不過,這是DAX的一種常見做法,因為嵌套迭代器會生成功能強大的表達式。
例如,以下代碼包含三個嵌套的迭代函數,并掃描三個表:Categories 類別,Products 產品和 Sales 銷售。
SUMX (
'Product Category', -- Scans the Product Category table
SUMX ( -- For each category
RELATEDTABLE ( 'Product' ), -- Scans the category products
SUMX ( -- For each product
RELATEDTABLE ( Sales ) -- Scans the sales of that product
Sales[Quantity] --
* 'Product'[Unit Price] -- Computes the sales amount of that sale
* 'Product Category'[Discount]
)
)
)
最里面的表達式(三個因子的乘積)引用了三個表。實際上,在該表達式求值期間打開了三行上下文:當前正在迭代的三個表中的每一個都一個。還值得注意的是,兩個 RELATEDTABLE 函數從當前行上下文開始返回相關表的行。因此,在 Categories 表的行上下文中執行的 RELATEDTABLE(Product)返回給定類別的產品。相同的推理適用于RELATEDTABLE(Sales),該函數返回給定產品的銷售額。
先前的代碼在性能和可讀性方面都不理想。通常,如果要掃描的行數不是太大,則嵌套迭代函數是很好的:數百行是好消息,數千行是好消息,數百萬行是壞消息。否則,我們很容易遇到性能問題。我們使用前面的代碼演示了可以創建多個嵌套行上下文。我們將在本書后面的部分中看到更多有用的嵌套迭代函數示例。通過使用以下代碼,可以依靠一種單獨的行上下文和RELATED函數,以一種更快,更易讀的方式表示相同的計算:
SUMX (
Sales,
Sales[Quantity]
* RELATED ( 'Product'[Unit Price] )
* RELATED ( 'Product Category'[Discount] )
)
只要不同表上有多個行上下文,就可以使用它們在單個DAX表達式中引用迭代表。但是,有一種情況證明具有挑戰性。那就是當我們在同一張表上嵌套多個行上下文時,這是下一節所討論的主題。
同一表上的嵌套行上下文
在同一表上嵌套行上下文的場景似乎很少見。但是,它確實經常發生,并且在計算列中更頻繁地發生。假設我們要根據標價對產品進行排名。最昂貴的產品應排在第1位,第二貴的產品應排在第2位,依此類推。我們可以使用RANKX函數解決該方案。但是出于教育目的,我們展示了如何使用更簡單的DAX函數來解決它。
為了計算排名,對于每種產品,我們可以計算價格高于當前產品的產品數量。如果沒有價格高于當前產品價格的產品,那么當前產品是最昂貴的,其排名為1。如果只有一種價格更高的產品,則排名為2。實際上,我們正在通過計算價格較高的產品數量并將結果加1來計算產品的排名。 因此,可以使用此代碼編寫計算列,其中我們使用 PriceOfCurrentProduct 作為占位符以指示當前產品的價格。
'Product'[UnitPriceRank] =
COUNTROWS (
FILTER (
'Product',
'Product'[Unit Price] > PriceOfCurrentProduct
)
) + 1
FILTER 返回價格高于當前產品價格的產品,COUNTROWS 對 FILTER 結果的行進行計數。唯一剩下的問題是找到一種方法來表達當前產品的價格,用有效的DAX語法替換 PriceOfCurrentProduct ?!爱斍啊笔侵窪AX計算列時當前行中列的值。這比您預期的要難。
將注意力集中在先前代碼的第5行上。此處,對 Product [Unit Price] 的引用是指當前行上下文中的 Unit Price 的值。DAX執行第5行時活動的行上下文是什么?有兩行上下文。因為代碼是寫在計算所得的列中的,所以引擎會自動創建一個默認的行上下文,該上下文會掃描 Product 表。此外,由于 FILTER 是一個迭代器,因此由 FILTER 生成的行上下文將再次掃描 Product。如圖4-9所示。
外部框包括正在對 Product 進行迭代的計算列的行上下文。但是,內部框顯示 FILTER 函數的行上下文,該行上下文也遍歷 Product 。表達式 Product [Unit Price] 取決于上下文。因此,在內部框中對 Product [Unit Price] 的引用只能引用 FILTER 當前迭代的行。問題在于,在該框中,我們需要評估由計算列的行上下文所引用的單價的值,該值現已隱藏。
確實,當不使用迭代函數創建新的行上下文時,Product [Unit Price] 的值就是所需的值,它是所計算列的當前行上下文中的值,如以下簡單代碼所示:
Product[Test] = Product[Unit Price]
為了進一步說明這一點,讓我們在兩個框中使用一些偽代碼評估 Product [Unit Price] 。結果是不同的結果,如圖4-10所示,我們在 COUNTROWS 之前添加了對 Product [Unit Price] 的評估,僅出于教育目的。
這是到目前為止的情況的回顧:
- 由 FILTER 生成的內部行上下文隱藏了外部行上下文。
- 我們需要將內部 Product [Unit Price] 與外部 Product [Unit Price] 的值進行比較。
- 如果我們在內部表達式中編寫比較,則無法訪問外部 Product [Unit Price] 。
因為我們可以檢索當前單價,所以如果在 FILTER 的行上下文之外進行評估,則解決此問題的最佳方法是將 Product [Unit Price] 的值保存在變量中。確實,可以使用以下代碼在計算列的行上下文中評估變量:
'Product'[UnitPriceRank] =
VAR
PriceOfCurrentProduct = 'Product'[Unit Price]
RETURN
COUNTROWS (
FILTER (
'Product',
'Product'[Unit Price] > PriceOfCurrentProduct
)
) + 1
此外,最好通過使用更多變量來分隔計算的不同步驟,以更具描述性的方式編寫代碼。這樣,代碼也更容易遵循:
'Product'[UnitPriceRank] =
VAR PriceOfCurrentProduct = 'Product'[Unit Price]
VAR MoreExpensiveProducts =
FILTER (
'Product',
'Product'[Unit Price] > PriceOfCurrentProduct
)
RETURN
COUNTROWS ( MoreExpensiveProducts ) + 1
圖4-11顯示了后一種代碼形式的行上下文的圖形表示,它使您更容易理解DAX在哪個行上下文中計算公式的每個部分。
圖4-12顯示了此計算列的結果。
因為有14個產品的單價相同,所以它們的排名始終為1;第15個產品的排名為15,與其他價格相同的產品共享。如果我們能像圖中那樣將1、2、3而不是1、15、19排好,那將是很好的。我們將盡快解決此問題,但在此之前,重要的是要先說個小題外話。
為了解決提出類似的問題,必須對行上下文有扎實的了解,以便能夠檢測到公式的不同部分中哪個行上下文是活動的,并且最重要的是,要了解行上下文如何影響DAX表達式返回的值。值得強調的是,在公式的兩個不同部分中求值的同一表達式 Product [Unit Price] 返回不同的值,這是因為要對其求值的上下文不同。當人們對評估上下文缺乏扎實的了解時,處理這樣復雜的代碼將非常困難。
如您所見,具有兩個行上下文的簡單排名表達式被證明是一個挑戰。在第5章的稍后部分,您將學習如何創建多個過濾器上下文。那時,代碼的復雜性增加了很多。但是,如果您了解評估上下文,那么這些方案很簡單。在進入DAX的下一個層次之前,您需要充分了解評估上下文。這就是為什么我們敦促您再次閱讀這節(甚至到目前為止的本章),直到這些概念明確為止。這將使閱讀下一章變得更加容易,您的學習體驗也會更加順暢。
在離開此示例之前,我們需要解決最后一個細節——即使用1、2、3的序列而不是到目前為止獲得的序列進行排名。該解決方案比預期的要容易。實際上,在前面的代碼中,我們著重于對價格較高的產品計數。如是,該公式對14個排名第1的產品進行了計數,并將15分配給了第二排名級別。但是,對產品進行計數不是很有用。如果公式計算的價格高于當前價格,而不是產品價格,則所有14個產品都將折疊為一個價格。
'Product'[UnitPriceRankDense] =
VAR PriceOfCurrentProduct = 'Product'[Unit Price]
VAR HigherPrices =
FILTER (
VALUES ( 'Product'[Unit Price] ),
'Product'[Unit Price] > PriceOfCurrentProduct
)
RETURN
COUNTROWS ( HigherPrices ) + 1
圖4-13顯示了新的計算列以及UnitPriceRank。
最后的一小步是計算價格而不是產品計數,這似乎比預期的要難。使用DAX的工作越多,就越容易開始考慮為計算目的而創建的臨時表。
在此示例中,您了解了處理同一表上多個行上下文的最佳技術是使用變量。請記住,變量是DAX語言于2015年晚些時候引入的。您可能會發現現有的DAX代碼(在變量時代之前編寫)使用了另一種技術來訪問外部行上下文: EARLIER 函數,我們將在下一節介紹
使用 EARLIER 功能
DAX提供了訪問外部行上下文的功能: EARLIER 。 EARLIER 通過使用上一個行上下文而不是最后一個行上下文來檢索列的值。因此,我們可以使用 EARLIER(Product [UnitPrice])來表示 PriceOfCurrentProduct 的值。
許多DAX新手對 EARLIER 感到恐懼,因為他們對行上下文的了解不夠充分,并且他們沒有意識到可以通過在同一張表上創建多個迭代來嵌套行上下文。理解行上下文和嵌套的概念后, EARLIER 是一個簡單的函數。例如,以下代碼不使用變量即可解決以前的情況:
'Product'[UnitPriceRankDense] =
COUNTROWS (
FILTER (
VALUES ( 'Product'[Unit Price] ),
'Product'[UnitPrice] > EARLIER ( 'Product'[UnitPrice] )
)
) + 1
注意 EARLIER 接受第二個參數,這是要跳過的步驟數,以便可以跳過兩個或多個行上下文。此外,還有一個名為 EARLIEST 的函數,該函數使開發人員可以訪問為表定義的最外面的行上下文。在現實世界中,既不經常使用EARLIEST 也不使用 EARLIER 的第二個參數。盡管在計算列中通常有兩個嵌套行上下文,但很少有三個或更多嵌套行上下文。此外,自變量問世以來, EARLIER 實際上已變得毫無用處,因為變量用法取代了 EARLIER 。
學習 EARLIER 的唯一原因是能夠讀取現有的DAX代碼。沒有其他理由在較新的DAX代碼中使用 EARLIER ,因為在訪問行上下文時,變量是保存所需值的更好方法。為此目的使用變量是一種最佳方法,并且可以使代碼更具可讀性。