性能优化,在 DAX 中是很重要的问题,对 DAX 的性能优化大致可以归结为针对 SE(存储引擎) 或 FE(公式引擎) 的性能优化。
如果可以确保 SE 和 FE 都在最好的状态下工作,那么 DAX 将得到充分的发挥。而往往分析师会更加关注业务逻辑的表达,但我们开始研究写出更快的 DAX 时,我们将成为会修车的分析师了。我们会通过一系列文章来帮助大家在各个角度来体会 DAX 的性能优化技术。
今天我们来看一个案例。
看一个案例,我们想知道大订单的个数,如下:
代码语言:javascript复制OrderPurchaseNumber =
CALCULATE(
DISTINCTCOUNT( 'Order'[OrderID] ) ,
FILTER(
'Order' ,
'Order'[LinePrice] > 1000 && 'Order'[LineProfit] > 0
)
)
大订单的定义为包含单价大于1000且利润大于0的订单。
这个定义没有问题,放在 PowerBI 中的计算也是正确的,但不久就会发现它的性能问题,于是,通过 DAX Studio 来检查可以看到:
我晕,居然惊现了 779 个查询。
该查询的意义是计算每天的大订单个数。但这种方法显然是不行的。虽然在度量值的定义上非常自然。
我们再来看看从 PowerBI 中拖拽的情况,如下:
如果研究该图表背后的 DAX 查询,其结果和上述内容是一致的。
那么问题来了,我们建立了一个基础度量值叫:OrderPurchaseNumber,其逻辑也很清楚,但却有如此之差的性能,怎么办呢?
原因分析
这里的问题在于发起了对 SE 的多次查询,不难察觉:
代码语言:javascript复制 FILTER(
'Order' ,
'Order'[LinePrice] > 1000 && 'Order'[LineProfit] > 0
)
这里使用了 Order 表作为 FILTER 的参数,而且位于基础度量值的位置,导致在迭代日期时,每次都会做单独计算,导致对 SE 的过度重复访问。
改进措施
有一种做法是,可以将度量值改为:
代码语言:javascript复制OrderPurchaseNumber =
CALCULATE(
DISTINCTCOUNT( 'Order'[OrderID] ) ,
FILTER(
ALL( 'Order' ) ,
'Order'[LinePrice] > 1000 && 'Order'[LineProfit] > 0
)
)
注意,这里用了 ALL( ‘Order’ ) 而非 ‘Order’ ,这显然是不对的,因为它改变了语义。
那么进而想到另一种方式为:
代码语言:javascript复制OrderPurchaseNumber =
CALCULATE(
DISTINCTCOUNT( 'Order'[OrderID] ) ,
FILTER(
ALL( 'Order'[LinePrice] , 'Order'[LineProfit] ) ,
'Order'[LinePrice] > 1000 && 'Order'[LineProfit] > 0
)
)
这样的方式仅仅使用需要用到的两列,而非整个表,来看下效果:
性能得到了非常恐怖的提升。
但细心的伙伴会发现,这种写法的努力方向是对的,但这种写法还是错误的,因为:
代码语言:javascript复制 FILTER(
ALL( 'Order'[LinePrice] , 'Order'[LineProfit] ) ,
'Order'[LinePrice] > 1000 && 'Order'[LineProfit] > 0
)
作为筛选器参数,会覆盖外部的筛选,这也是不正确的逻辑,所以,需要进一步优化为:
代码语言:javascript复制OrderPurchaseNumber =
CALCULATE( DISTINCTCOUNT( 'Order'[OrderID] ) ,
KEEPFILTERS(
FILTER(
ALL( 'Order'[linePrice] , 'Order'[LineProfit] ) ,
'Order'[LinePrice] > 1000 && 'Order'[LineProfit] > 0
)
)
)
性能结果为:
完美。
总结
当需要在基础度量值中使用筛选条件时,必须注意:
- 仅仅使用所必须的列,提升性能
- 使用 KEEPFILTERS 包裹,确保逻辑正确
这样,基础度量值就可以携带复杂的筛选器参数而不影响性能了。