小伙伴催更了。準備放大招,所以,很多內容停滯了。如果本文的技巧讓您震撼,那如果告訴您,本文僅僅是開胃菜呢。開始吹吧。
此前,有很多伙伴反映 PowerBI DAX 在進行某種運算時,速度隨元素的增長會變得很慢,這個問題在很多重要的模型中都存在,包括了:
- 帕累托分析,當要計算的元素很多時;
- 累計百分比分析,當要計算的元素很多時;
- 其他模型。
本文先立足給出一種對比,后續文章再研究其他案例。
問題重述
已知用戶列表,以及用戶所產生的明細數據。顯示:在動態篩選的界面中,給出用戶列表以及對應的指標積累百分比。
先從效果看來理解這個問題,如下:
這非常容易理解,對于每個用戶,用戶Id表示該用戶的唯一性;KPI表示該用戶的某種指標;KPI%(≤Current)表示比當前元素(包括當前元素)的KPI值低的所有元素的KPI的積累百分比。
這非常像帕累托分析,在帕累托分析中,只不過是≥當前元素的積累百分比。
數據模型
用以下精簡的數據模型來表示這個問題,有:
這一模型非常容易構建或模擬。
如果模擬這一數據模型,可以這樣操作:
Item =
SELECTCOLUMNS( GENERATESERIES( 1 , 10 ) , "Id" , [Value] )
其中,10表示元素個人,也可以替換為100,1000,10000,100000,1000000來逐步觀察隨著元素個數的增加,算法的用時成本。
對于明細數據,可以這樣虛擬如下:
Detail =
GENERATEALL( 'Item' , VAR X = RANDBETWEEN( 100 , 200 ) RETURN GENERATESERIES( X , X + RANDBETWEEN( 10 , 50 ) ) )
該模擬生成算法的意圖為,對于每個元素,都從100到200之間隨機給定一個數,并生成以該種子為起點的50個隨機條目。
常規算法
熟悉 DAX 的伙伴或分析師很快就可以寫出該問題的解法,如下:
Item.Percent%.ModelMethod =
VAR vCurr = [Item.Value]
VAR tItemsAllSelected = ALLSELECTED( 'Item'[Id] )
VAR tItemsFiltered = FILTER( tItemsAllSelected , [Item.Value] <= vCurr )
RETURN COUNTROWS( tItemsFiltered ) / COUNTROWS( tItemsAllSelected )
我們稱這一算法為模型算法,而其中的[Item.Value]
度量值可以認為是通用指標計算的邏輯。
模型算法用時分析
對元素個數不斷增加,可以發現在元素個數為8000的時候,算法需要消耗約15秒時間。(會因硬件配置有所不同)
這可以理解為:
如果有8000個用戶以及明細數據,需要得到這樣的積累占比分析,需要等待至少15秒鐘,考慮到其他相關可視化圖表的用時,這是無法接受的。
但這個算法,似乎已經是最好的了。在模型算法中,它使用了 VAR 暫存了數據,但似乎沒有什么卵用啊。
于是,要如何進行優化呢?
答案是:在模型層面,是無法優化的。
該算法已經使用了相當正確的寫法,并沒有明顯的問題,無法得到優化。
另辟蹊徑:視圖層算法
這里先給出結果,后面再做分析。
前面之所以叫模型算法,是針對這里要提出的視圖(層)算法相對而言的。
什么是視圖層算法?
如果計算不需要觸碰底層數據模型,而僅僅需要在視圖層面計算,我們說,這就叫視圖算法。本例中,可以這樣構造:
Item.Percent%.ViewMethod =
VAR vCurr = [Item.Value]
VAR tView = CALCULATETABLE(
ADDCOLUMNS(
VALUES( 'Item'[Id] ) ,
"Value" , [Item.Value]
)
, ALLSELECTED( )
)
RETURN COUNTROWS( FILTER( tView , [Value] <= vCurr ) ) / COUNTROWS( tView )
關于視圖層算法,我們已經在此前文章中給出過詳細說明,這里不再贅述其原理。
值得一提的是,PowerBI 并不內置支持視圖層計算,而由 SQLBI 發起的針對此特性的 PowerBI 社區投票得到非常多支持,但這個特性是否支持,以及如果支持后如何實現,對于微軟的 PowerBI 團隊,其實是一個難題。
但不管 PowerBI 是否原生支持,通過我們給出的幾個案例,具有舉一反三能力的伙伴應該已經發現自助實現視圖層可視化計算的要領,這個要領幾乎是呼之欲出的,我們將在后續文章給出開創性的實現思路以及通用做法。
在這里,我們稱此處算法為視圖層算法,我們檢測其時間消耗成本,從實驗看出,它比模型算法提升了性能,但并不顯著。
視圖層索引表算法
由于我們發現視圖層算法相比模型層算法有加速的效用,我們只需要舉一反三地構建所有可能的算法并進行比對就可以選擇最優的模式。索引,顯然是一個方向,這里直接給出其實現,如下:
Item.Percent%.IndexedViewMethod =
VAR vCurr = [Item.Value]
VAR tView = CALCULATETABLE(
ADDCOLUMNS(
VALUES( 'Item'[Id] ) ,
"Value" , [Item.Value]
)
, ALLSELECTED( )
)
VAR tViewWithIndex = ADDCOLUMNS( tView , "Index1" , [Value] , "Index2" , [Id] )
VAR tIndexTable = DISTINCT( SELECTCOLUMNS( tViewWithIndex , "Index1" , [Index1] , "Index2" , [Index2] ) )
VAR tIndexedView = SUBSTITUTEWITHINDEX( tViewWithIndex , "Index" , tIndexTable , [Index1] , ASC , [Index2] , ASC )
RETURN ( MAXX( FILTER( tIndexedView , [Value] = vCurr ) , [Index] ) + 1 ) / COUNTROWS( tView )
可以看出,該算法是基于視圖層算法改進而來的,對于 DAX 經驗有限的伙伴,可能有理解的難度,但這并不是本文的重點,為了全面,我們把這一算法記錄在案。通過對比,我們發現,該算法可以顯著提升性能,如下:
隨著元素數據的增加,IndexedView 算法可以有非常明顯的性能改善,計算 10000 元素僅需 0.5 秒,比普通的模型算法提升了近 50 倍性能,這太神奇了。更值得驚訝的是,視圖層索引表算法的編寫比常規模型算法復雜得多,但卻有超過 50 倍的性能提升,不可謂不兇殘。
如果觀察性能趨勢圖,普通的模型算法和視圖層算法都近乎是指數級時間增加,這是我們不希望的。而視圖層索引算法相對而言就平緩得多了。
視圖層排序算法
是否還可以更進一步來加速這個算法呢?答案是肯定的。
我們巧妙地利用排序的性質,為每一個元素都進行排序,那么排序的序號正是它超過元素的個數。而這個排序僅僅需要在視圖層完成計算,根本不需要觸碰模型層,給出算法如下:
Item.Percent%.RankedViewMethod =
VAR vCurr = [Item.Value]
VAR tView = CALCULATETABLE(
ADDCOLUMNS(
VALUES( 'Item'[Id] ) ,
"Value" , [Item.Value]
)
, ALLSELECTED( )
)
RETURN RANKX( tView , [Value] , vCurr , ASC , Skip ) / COUNTROWS( tView )
該算法非常簡單,可以看出這也是基于視圖層計算而進行的改進。我們來看看這個算法的時間消耗趨勢,如下:
其效果是驚人的震撼,它面對100萬元素的300萬明細數據,僅僅需要不到3秒就可以計算完畢,在計算2萬元素節點時,其性能是經典算法的上千倍。經過測試,對100萬元素以及25億明細數據,其計算用時約為3秒。
這可以從性能面板得到各種算法時間的對比,如下:
總結
本文拋開了傳統的模型層算法,對于同一問題的解決,給出了視圖層的等效算法,并將性能提升上千倍,這幾乎是不可想象的。你如果問為什么會提升這么多性能,這里當然是觸發了 DAX 最快計算的竅門,限于篇幅和復雜性,就不再展開,畢竟對于 99% 的伙伴,需要的永遠是復雜和粘貼。如果你要問這是如何想到的,那必須歸功于兩點:其一,是對 DAX 本質的理解;其二,是發散思維。
然而,即使是提升了數千倍的性能,本文卻還只是開胃菜,大餐正在烹飪中。
對于希望徹底理解 DAX 本質精髓的伙伴,羅叔準備了前所未有的 VIP 線下課程,徹底揭示 PowerBI 尤其是 DAX 的本質精髓。