新手必读:了解DAX变量的计算规则

原创:      2024-11-05 13:34

本文翻译自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或属性层次结构的存在)而有所不同。作为一条规则,无论变量在计算中是否使用,都应认为它总是被计算的。

请注意,假设因为变量总是被计算,所以它们会对性能产生负面影响是错误的。确实存在一些边缘情况,删除变量实际上会提高性能。但是,与变量产生性能改进的场景数量相比,这些情况非常罕见,因此规则始终如一:当您对是否定义变量感到疑惑时……定义它!


客户案例

按行业 按部门

线上课程

课程直通 讲师介绍

官方微信

联系我们

北京

13811982114
agileex@agileex.com

北京市朝阳区博雅国际中心A座601

上海

13811982114
agileex@agileex.com

上海市静安区沪太路453号302室

Copyright © 2022 北京敏捷艾科数据技术有限公司agileex.com - 京ICP备16008845号-1