变量(4)
使用变量优化重复的DAX表达式
通过前面关于变量的介绍,知道了它的几个特点:
1、变量实际是一个由存储引擎加载在内存里的存储数据。在这点上,其实际与Powerqurey里的Buffer函数相似。
比如我们在Powerqurey里求累计占比时,事先使用List.Buffer函数将计算列缓存起来,以便用于该列的累计计算时的分子、分母。
然后根据该缓存的列,定义每行的值与该列的总值(绝对值)的计算(作为累计聚合计算时的分子、分母值)
2、由于变量实际上更像一个“常量”,这与DAX的值列表类似,因而常用于构建DAX的逻辑条件(替代难于理解的“当前行”,充当行筛选等);还因为变量在存储引擎里作为“固定的存储数据(更像“常量”)”被公式引擎引用,而且是一次性计算,从而使公式不必直接从数据模型请求引用数据的迭代。
3、正确使用变量也有助于提高查询性能。这是本节需要讨论的问题。
(以下内容有参考官方的简体笔记部分)
本节内容介绍如何使用变量来优化包含同一度量或同一子表达式的多个引用实例的DAX表达式的性能。通过前面的介绍,我们已经知道:在 DAX 中, 应该考虑使用度量作为提高表达式可读性的一种方法。但是, 正确使用变量也有助于提高查询性能。
实际上, 这意味着更好的执行计划:因为这样可以避免同一变量或子表达式在同一筛选器中的多次引用。例如, 在Power BI 数据模型中考虑此简单动态分割模式的实现。
Segments表定义了三个细分市场的划分范围,这些将用于根据客户的支出情况对其细分市场进行聚类(群组)。
在使用变量之前,我们先编写原始的动态分割模式。因此, 第一次实施分段的Customer Sales-- Customer Sales度量如下:
Sales[Customers Sales] :=
IF ( ISCROSSFILTERED ( Segments ),
SUMX ( Segments,
SUMX ( Customer,
IF ( [Sales Amount] >= Segments[MinValue] //上边界
&& [Sales Amount] < Segments[MaxValue],//下边界
[Sales Amount]))), [Sales Amount])
使用Customer Sales表获得的报告显示了相应的结果。
但是, 在Customer--客户表的 SUMX 迭代中,在相同的列表筛选和行筛选中, Sales Amount度量值有三次被引用 :
理想状态下, 查询引擎应该能够识别到这三个引用的结果对于给定的客户定义来说总是相同的。这在现在的最新Powerpivot或Power BI里是可以的, 但2018年1月前的情况并非如此。
使用 DAX STUDIO, 可以看到以前版本的报告中由 Customer Sales度量生成的存储引擎查询。
两个突出显示的行显示了由存储引擎查询返回的数据缓存:两者行数相同、大小相同、查询的初始部分看起来也相同。不过,两个存储引擎查询又有所不同。第一个引擎查询如下:
WITH
$Expr0 := ( PFCAST ( 'Sales'[Quantity] AS INT ) * PFCAST ( 'Sales'[Net Price] AS INT ) ) SELECT
'Customer'[CustomerKey], SUM ( @$Expr0 )
FROM 'Sales'
LEFT OUTER JOIN 'Customer' ON
'Sales'[CustomerKey]='Customer'[CustomerKey];
第二个引擎查询提供了附加的过滤条件(在前一个代码的后面加上WHERE过滤条件):
WHERE 'Customer'[CustomerKey] IN ( 13407, 9266, 17548, 7787, 3646, 17055, 12914, 18041, 7294,3153… [18869 total values, not all displayed] ) ;
请记住,存储引擎查询显示了每个数据缓存的行的估计大小值。实际数值在物理查询计划的Spool _ Iterator事件中可用,它对应于结果为18,869,这是实际的客户数,且两个数据缓存的结果相同。
由于查询优化器假设IF函数的两个参数可能有不同的结果,因而它对存储引擎会提出两个相同结果的数据引用请求。如果对查询语义进行更深入的分析,应该允许让查询优化器这样做,这将更好。但我们已说过,这在2018年1月前的引擎版本中还没有该性能优化。
作为实验,使用变量,可以参考IF函数在不同参数中的变量值,针对每个客户只评估一次Sales Amount---销售额度量。
以下是 Customer Sales度量的优化版本:
Sales[Customers Sales Optimized] :=
IF ( ISCROSSFILTERED ( Segments ),
SUMX ( Segments,
SUMX ( Customer,
VAR SalesAmount = [Sales Amount]
RETURN
IF ( SalesAmount >= Segments[MinValue]
&& SalesAmount < Segments[MaxValue],
SalesAmount))),[Sales Amount])
使用这种方法的度量,物理查询计划更短,这时候,每个客户的销售金额结果都将分别对应一个数据缓存(加粗的三个位置已被同一变量替代):
上述公式里,最后一个[Sales Amount] 为什么不能使用由该度量定义的变量,前一篇文里已讲过。这里从略。
加粗显示的行是用于计算所有客户的销售金额的单个 datacache--数据缓存, 它不包含任何筛选条件(更像一个“常量”)。
WITH $Expr0 := ( PFCAST ( 'Sales'[Quantity] AS INT )
* PFCAST ( 'Sales'[Net Price] AS INT ) )
SELECT 'Customer'[CustomerKey],
SUM ( @ $Expr0 )FROM 'Sales'
LEFT OUTER JOIN 'Customer' ON
'Sales'[CustomerKey]='Customer'[CustomerKey];
如果物化越小,则查询性能越好。但这在小数据模型中几乎无法测量,只有在拥有大量客户的数据模型中,才会很容易看到这种差异。
当相同的子表达式在相同的计算筛选条件下多次被引用时, 也可以进行类似的性能优化。
例如,不包含度量值引用的 Customer Sales版本, 它重复相同的计算次数。 优化的版本则提供了以前显示的相同方法。有一个CALCULATE语句的单一评估, 其结果将多次引用 SalesAmount 变量。
在 DAX 中, 变量提供了更好的代码可读性, 并通过减少同一子表达式多次出现的计算几率, 以促进更优化的查询计划的生成。
未完待续