新手必读:了解 DAX 变量的计算规则
本文翻译自Marco Russo&Alberto Ferrari的文章—《When are variables evaluated in DAX?》来源:SQLBI 本文解释了DAX如何计算变量,这对于初次使用DAX时避免常见错误至关重要。
变量是DAX中提高可读性和性能的重要元素。变量通过VAR关键字创建,该关键字可以针对多个变量重复多次,后面跟随RETURN关键字,该关键字定义了表达式的结果。
计算变量
在以下代码中,我们定义了两个变量:SalesAmount(销售额)和NumCustomers(客户数量),最后我们将一个变量除以另一个变量以得到结果:
销售表中的度量值
SalesPerCustomer =
VAR SalesAmount =
SUMX ( Sales, Sales[Quantity] * Sales[Net Price] )
VAR NumCustomers =
DISTINCTCOUNT ( Sales[CustomerKey] )
RETURN
DIVIDE ( SalesAmount, NumCustomers )
我们习惯了一种更强大的模式,该模式要求将Result用作计算表达式结果的最后一个变量,以便最后的RETURN只返回Result变量:
销售表中的度量值
SalesPerCustomer =
VAR SalesAmount =
SUMX ( Sales, Sales[Quantity] * Sales[Net Price] )
VAR NumCustomers =
DISTINCTCOUNT ( Sales[CustomerKey] )
VAR Result =
DIVIDE ( SalesAmount, NumCustomers )
RETURN
Result
这一选择背后的理由是调试的简便性。很多时候,由于代码存在问题,您希望检查变量的内容。在这些情况下,将其中一个变量返回以在报告上下文中可视化其内容是非常有用的。如果您的最后一行始终是RETURN Result,那么在调试完成后,您就会确切知道如何恢复原始代码:它就是RETURN Result。另一方面,如果您在RETURN部分使用了更复杂的表达式,那么您必须记住在开始调试之前度量值是什么样的。
这是一个简单的技巧,但在调试时非常有用。
一个乍一看不明显的方面是,变量可以在DAX代码的任何位置定义。尽管大多数变量都在度量值的开头使用,但在复杂表达式的中间定义变量也是完全可以的。在这个例子中,你可以看到有两个VAR块:外部VAR块从公式的开头开始,内部VAR块从SUMX内部开始:
销售表中的度量值
Discounted Sales =
VAR AverageSales = AVERAGEX( Customer, [Sales Amount] )
VAR Result =
SUMX (
Customer,
VAR CustomerSales = [Sales Amount]
VAR Discount = 0.85
VAR Result =
IF (
CustomerSales >= AverageSales,
CustomerSales * Discount,
CustomerSales
)
RETURN
Result
)
RETURN
Result
因为有两个VAR块,所以也有两个RETURN语句。每个VAR都需要通过其对应的RETURN来关闭。请注意,我们使用了两次Result变量。内层的Result在内层块中有效,而外层的Result在外层框中有效。
Result是一个比较特殊的情况。事实上,外部Result在内部块中是不可见的,因为它被一个同名的更近的变量隐藏了。在外部块中定义的AverageSales变量在内部块中仍然是可见且可用的。
变量在其定义的筛选器上下文中进行计算,且最多计算一次。这正是CustomerSales变量如此有用的原因。在IF语句中,当前客户的销售额值被需要了三次。使用变量可以确保计算仅发生一次,并且DAX引擎不会选择一个需要多次计算销售额的执行计划。另一方面,Discount变量除了提高代码的可读性之外没有其他用途,从性能角度来看它是无用的。但是,由于变量的存在,代码更加易读,因此在代码中使用变量是有意义的。
变量最多计算一次。这很重要。变量不会在每次使用时都重新计算。如果用户错误地在SUMX之前定义了CustomerSales变量,那么结果将是不准确的:
销售表中的度量值
Discounted Sales =
VAR AverageSales = AVERAGEX( Customer, [Sales Amount] )
VAR CustomerSales = [Sales Amount]
VAR Discount = 0.85
VAR Result =
SUMX (
Customer,
IF (
CustomerSales >= AverageSales,
CustomerSales * Discount,
CustomerSales
)
)
RETURN
Result
CustomerSales 不再为每个客户重新计算。CustomerSales 的值在迭代 Customer 之前就已经确定。因此,它的值是总销售额,而不是公式所要求的当前迭代客户的销售额。
另一方面,以下代码运行正常。然而,它之所以有效,是因为 AVERAGEX 在 Customer 迭代内部定义了自己的迭代来分配 AverageSales,因此计算出了正确的平均值。尽管从 DAX 的角度来看这完全没问题,但人类很容易因为这段代码而被误导,并发现错误,尽管实际上并没有错误:
销售表中的度量值
Discounted Sales =
SUMX (
Customer,
VAR AverageSales = AVERAGEX ( Customer, [Sales Amount] )
VAR CustomerSales = [Sales Amount]
VAR Discount = 0.85
VAR Result =
IF (
CustomerSales >= AverageSales,
CustomerSales * Discount,
CustomerSales
)
RETURN
Result
此外,通过在迭代内部定义 AverageSales,存在(幸运的是,在这个简单的场景中,DAX 引擎避免了这种情况)计算每个客户的平均销售额的风险。
选择合适的位置来定义变量是很重要的。在选择定义多少个VAR块时,我们必须实现代码的可读性和正确性这两个目标。
变量是“恒定的”
新手常犯的一个错误是将变量视为定义变量本身所用代码的别名。换句话说,他们将变量视为一种局部度量定义。但事实并非如此:变量是赋给某个值的名称。该值被计算并一次性赋给变量。换句话说,变量——令人惊讶的是——是常量。为什么它们被称为变量而不是常量,这是DAX众多迷思中的一个。这个事实不得而知,已经迷失在DAX开发者早期历史的尘埃之中。
请考虑以下代码,用于计算与前一年的增长率:
销售表中的度量值
Sales Growth (Wrong) =
VAR SalesCY = [Sales Amount]
VAR SalesPY =
CALCULATE (
SalesCY,
SAMEPERIODLASTYEAR ( 'Date'[Date] )
)
VAR Result = SalesCY - SalesPY
RETURN
Result
在代码中,SalesCY变量被赋予了“销售额”度量的值。之后,在定义SalesPY时,CALCULATE函数在不同的筛选上下文中计算SalesCY的值,该上下文通过SAMEPERIODLASTYEAR将日期范围向前推一年。尽管筛选上下文发生了变化,但SalesCY的值并没有改变。它在SalesCY的定义期间就已经被计算出来,并且其值不会改变:它是一个常量。因此,该度量始终返回0。正确的公式如下:
销售表中的度量值
Sales Growth =
VAR SalesCY = [Sales Amount]
VAR SalesPY =
CALCULATE (
[Sales Amount],
SAMEPERIODLASTYEAR ( 'Date'[Date] )
)
VAR Result = SalesCY - SalesPY
RETURN
Result
使用度量而不是变量会完全改变其语义。度量是在其被使用的筛选上下文中进行计算的。每次调用度量时,其值都会根据活动的筛选上下文发生变化。
使用变量时,请始终记住它们是常量。变量永远不会改变,尽管其名称可能暗示了其他含义。
移除未使用的变量
一个有趣的问题是:如果一个变量被定义了但没有被使用,会发生什么?引擎会计算这个变量吗?从语义的角度来看,没有区别。如果一个变量被计算了但没有被使用,公式仍然可以正常工作。然而,从性能的角度来看,创建未使用的变量可能会对代码的性能产生负面影响。
规则很简单:代码中引用的变量总是会被计算,除非DAX引擎通过对代码的静态分析移除了变量引用。简单来说:如果DAX引擎在代码执行之前就能清楚地看到该变量是无用的,那么该变量就会从代码中物理删除,并且永远不会被计算。如果是否使用变量的选择需要在执行时做出,那么该变量将被计算,然后可能不会被使用。
为了获得精确的性能测量,我们需要分析查询计划来检查变量是否被计算。但是,有一个更简单的方法。如果变量产生错误,那么一旦变量被计算,整个公式就会立即返回错误。如果变量没有被计算,那么就不会出现错误。
请看以下代码:
销售表中的度量值
Test 1 =
VAR Err = ERROR ( "I am your favourite and personal error" )
VAR NumCustomers = DISTINCTCOUNT ( Sales[CustomerKey] )
VAR Result = IF ( NumCustomers < 0, Err )
RETURN
Result
Err变量只是抛出一个错误。NumCustomers包含客户数量,然后只有当客户数量小于零时,Result才会返回Err。因为客户数量不可能是一个负数,所以这个公式总是返回BLANK。但是,该变量在代码中已定义并被引用。因此,即使我们从未返回Err变量,DAX也需要计算其值。因此,将此度量添加到任何报告中都会产生错误。
如果变量已定义但未引用,则不会对其进行计算。因此,以下代码可以正常工作并始终返回BLANK:
销售表中的度量值
Test 2 =
VAR Err = ERROR ( "I am your favourite and personal error" )
VAR NumCustomers = DISTINCTCOUNT ( Sales[CustomerKey] )
VAR Result = IF ( NumCustomers < 0, BLANK () )
RETURN
Result
正如我们之前所说,有一个特殊情况,即DAX引擎在仅通过查看代码就知道变量永远不会被使用的情况下,会移除对变量的引用。如果我们用COUNTROWS ( ALL () )替换DISTINCTCOUNT,那么公式将不再返回错误,至少在导入模式下是这样(使用不同的存储引擎时,情况会有所不同):
销售表中的度量值
Test 3 =
VAR Err = ERROR ( "I am your favourite and personal error" )
VAR NumCustomers = COUNTROWS ( ALL ( Sales[CustomerKey] ) )
VAR Result = IF ( NumCustomers < 0, Err )
RETURN
Result
这是因为——作为优化的一部分——在导入模式下,DAX知道每个表和列的值的数量。因此,它知道Sales[CustomerKey]列中有多少个值。这个数字很有用,因为它意味着COUNTROWS ( ALL ( Sales[CustomerKey] ) )表达式可以被已知的值(在我们数据库中为5585)替换,从而避免执行昂贵的DAX代码。换句话说,DAX引擎内部以这种方式重写代码:
Test 3 =
VAR Err = ERROR ( "I am your favourite and personal error" )
VAR NumCustomers = 5585
VAR Result = IF ( NumCustomers < 0, Err )
RETURN
Result
然后它进一步简化它:
Test 3 =
VAR Err = ERROR ( "I am your favourite and personal error" )
VAR Result = IF ( 5585 < 0, Err )
RETURN
Result
最后,因为在运行代码之前已知IF内部的条件,这使得它变得更简单:
Test 3 =
VAR Err = ERROR ( "I am your favourite and personal error" )
VAR Result = IF ( FALSE (), Err )
RETURN
Result
因为IF(FALSE)永远不会执行Err,所以度量可以进一步简化:Test 3 = BLANK ()
如您所见,所有变量都从代码中消失了。但是,这种情况只有在DAX引擎在运行代码之前进行检查时才会发生。如果代码不能静态计算(如DirectQuery模式下),则错误会再次出现。
使用 VALUES 而不是 ALL 会使代码依赖于筛选上下文。因此,在代码运行之前,这些值是未知的,而引擎需要计算所有变量:
销售表中的度量值
Test 4 =
VAR Err = ERROR ( "I am your favourite and personal error" )
VAR NumCustomers = COUNTROWS ( VALUES ( Sales[CustomerKey] ) )
VAR Result = IF ( NumCustomers < 0, Err )
RETURN
Result
结论
如您所见,变量最多只会被计算一次。唯一不计算变量的情况是它根本没有被引用。这可能是因为开发人员忘记删除无用的变量,或者因为DAX引擎确定该变量永远不会被使用。
DAX引擎能够确定变量无用的场景相当罕见,因此对此做出假设是错误的——这也可能根据模型细节(如DirectQuery或属性层次结构的存在)而有所不同。作为一条规则,无论变量在计算中是否使用,都应认为它总是被计算的。
请注意,假设因为变量总是被计算,所以它们会对性能产生负面影响是错误的。确实存在一些边缘情况,删除变量实际上会提高性能。但是,与变量产生性能改进的场景数量相比,这些情况非常罕见,因此规则始终如一:当您对是否定义变量感到疑惑时……定义它!
如果您想深入学习微软Power BI,欢迎登录网易云课堂试听学习我们的“从Excel到Power BI数据分析可视化”系列课程。或者关注我们的公众号(PowerPivot工坊)后猛戳”在线学习”。
长按下方二维码关注“Power Pivot工坊”获取更多微软Power BI、PowerPivot相关文章、资讯,欢迎小伙伴儿们转发分享~
Power Pivot工坊
自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)