在迭代函数中使用行上下文
您了解到,无论何时定义计算列或使用X函数开始迭代时,DAX都会创建行上下文。当我们使用计算列时,行上下文的存在很容易使用和理解。实际上,我们甚至可以在不知道行上下文存在的情况下创建简单的计算列。原因是行上下文是由引擎自动创建的。因此,我们不必担心行上下文的存在。另一方面,在使用迭代函数时,我们负责创建和处理行上下文。此外,通过使用迭代函数,我们可以创建多个嵌套行上下文;这增加了代码的复杂性。因此,重要的是要更准确地了解带有迭代函数的行上下文的行为。
例如,查看以下DAX度量值:
IncreasedSales := SUMX ( Sales, Sales[Net Price] * 1.1 )
由于 SUMX 是迭代函数,因此 SUMX 在 Sales 表上创建一个行上下文,并在迭代过程中使用它。行上下文迭代 Sales 表(第一个参数),并在迭代过程中将当前行提供给第二个参数。换句话说,DAX在包含第一个参数上当前迭代的行的行上下文中计算内部表达式( SUMX 的第二个参数)。
请注意, SUMX 的两个参数使用不同的上下文。实际上,任何DAX代码段都可以在调用它的上下文中工作。因此,执行表达式时,可能已经有一个过滤器上下文和一个或多个行上下文处于活动状态。查看带有注释的相同表达式:
SUMX (
Sales, -- External filter and row contexts
Sales[Net Price] * 1.1 -- External filter and row contexts + new row context
)
使用来自调用方的上下文评估第一个参数Sales。使用外部上下文加上新创建的行上下文来评估第二个参数(表达式)。
所有迭代函数的行为均相同:
- 在现有上下文中评估第一个参数,以确定要扫描的行。
- 为在上一步中评估的表的每一行创建一个新的行上下文。
- 迭代表并在现有评估上下文中评估第二个参数,包括新创建的行上下文。
- 汇总上一步中计算的值。
请注意,原始上下文在表达式内仍然有效。迭代函数添加新的行上下文;它们不会修改现有的筛选上下文。例如,如果外部筛选上下文包含针对红色的筛选,则该筛选在整个迭代过程中仍处于活动状态。此外,请记住,行上下文是迭代的,它不会筛选。因此,无论如何,我们不能使用迭代器覆盖外部筛选上下文。
该规则始终有效,但是有一个重要的细节并不重要。如果先前的上下文已经包含同一表的行上下文,则新创建的行上下文将在同一表上隐藏先前的现有行上下文。对于DAX新手,这可能是错误的来源。因此,我们将在接下来的两节中详细讨论行上下文隐藏。
不同表上的嵌套行上下文
由迭代函数求值的表达式可能非常复杂。而且,表达式可以单独包含其他迭代。乍一看,在另一个迭代中开始一个迭代可能看起来很奇怪。不过,这是DAX的一种常见做法,因为嵌套迭代器会生成功能强大的表达式。
例如,以下代码包含三个嵌套的迭代函数,并扫描三个表:Categories 类别,Products 产品和 Sales 销售。
SUMX (
'Product Category', -- Scans the Product Category table
SUMX ( -- For each category
RELATEDTABLE ( 'Product' ), -- Scans the category products
SUMX ( -- For each product
RELATEDTABLE ( Sales ) -- Scans the sales of that product
Sales[Quantity] --
* 'Product'[Unit Price] -- Computes the sales amount of that sale
* 'Product Category'[Discount]
)
)
)
最里面的表达式(三个因子的乘积)引用了三个表。实际上,在该表达式求值期间打开了三行上下文:当前正在迭代的三个表中的每一个都一个。还值得注意的是,两个 RELATEDTABLE 函数从当前行上下文开始返回相关表的行。因此,在 Categories 表的行上下文中执行的 RELATEDTABLE(Product)返回给定类别的产品。相同的推理适用于RELATEDTABLE(Sales),该函数返回给定产品的销售额。
先前的代码在性能和可读性方面都不理想。通常,如果要扫描的行数不是太大,则嵌套迭代函数是很好的:数百行是好消息,数千行是好消息,数百万行是坏消息。否则,我们很容易遇到性能问题。我们使用前面的代码演示了可以创建多个嵌套行上下文。我们将在本书后面的部分中看到更多有用的嵌套迭代函数示例。通过使用以下代码,可以依靠一种单独的行上下文和RELATED函数,以一种更快,更易读的方式表示相同的计算:
SUMX (
Sales,
Sales[Quantity]
* RELATED ( 'Product'[Unit Price] )
* RELATED ( 'Product Category'[Discount] )
)
只要不同表上有多个行上下文,就可以使用它们在单个DAX表达式中引用迭代表。但是,有一种情况证明具有挑战性。那就是当我们在同一张表上嵌套多个行上下文时,这是下一节所讨论的主题。
同一表上的嵌套行上下文
在同一表上嵌套行上下文的场景似乎很少见。但是,它确实经常发生,并且在计算列中更频繁地发生。假设我们要根据标价对产品进行排名。最昂贵的产品应排在第1位,第二贵的产品应排在第2位,依此类推。我们可以使用RANKX函数解决该方案。但是出于教育目的,我们展示了如何使用更简单的DAX函数来解决它。
为了计算排名,对于每种产品,我们可以计算价格高于当前产品的产品数量。如果没有价格高于当前产品价格的产品,那么当前产品是最昂贵的,其排名为1。如果只有一种价格更高的产品,则排名为2。实际上,我们正在通过计算价格较高的产品数量并将结果加1来计算产品的排名。 因此,可以使用此代码编写计算列,其中我们使用 PriceOfCurrentProduct 作为占位符以指示当前产品的价格。
'Product'[UnitPriceRank] =
COUNTROWS (
FILTER (
'Product',
'Product'[Unit Price] > PriceOfCurrentProduct
)
) + 1
FILTER 返回价格高于当前产品价格的产品,COUNTROWS 对 FILTER 结果的行进行计数。唯一剩下的问题是找到一种方法来表达当前产品的价格,用有效的DAX语法替换 PriceOfCurrentProduct 。“当前”是指DAX计算列时当前行中列的值。这比您预期的要难。
将注意力集中在先前代码的第5行上。此处,对 Product [Unit Price] 的引用是指当前行上下文中的 Unit Price 的值。DAX执行第5行时活动的行上下文是什么?有两行上下文。因为代码是写在计算所得的列中的,所以引擎会自动创建一个默认的行上下文,该上下文会扫描 Product 表。此外,由于 FILTER 是一个迭代器,因此由 FILTER 生成的行上下文将再次扫描 Product。如图4-9所示。
外部框包括正在对 Product 进行迭代的计算列的行上下文。但是,内部框显示 FILTER 函数的行上下文,该行上下文也遍历 Product 。表达式 Product [Unit Price] 取决于上下文。因此,在内部框中对 Product [Unit Price] 的引用只能引用 FILTER 当前迭代的行。问题在于,在该框中,我们需要评估由计算列的行上下文所引用的单价的值,该值现已隐藏。
确实,当不使用迭代函数创建新的行上下文时,Product [Unit Price] 的值就是所需的值,它是所计算列的当前行上下文中的值,如以下简单代码所示:
Product[Test] = Product[Unit Price]
为了进一步说明这一点,让我们在两个框中使用一些伪代码评估 Product [Unit Price] 。结果是不同的结果,如图4-10所示,我们在 COUNTROWS 之前添加了对 Product [Unit Price] 的评估,仅出于教育目的。
这是到目前为止的情况的回顾:
- 由 FILTER 生成的内部行上下文隐藏了外部行上下文。
- 我们需要将内部 Product [Unit Price] 与外部 Product [Unit Price] 的值进行比较。
- 如果我们在内部表达式中编写比较,则无法访问外部 Product [Unit Price] 。
因为我们可以检索当前单价,所以如果在 FILTER 的行上下文之外进行评估,则解决此问题的最佳方法是将 Product [Unit Price] 的值保存在变量中。确实,可以使用以下代码在计算列的行上下文中评估变量:
'Product'[UnitPriceRank] =
VAR
PriceOfCurrentProduct = 'Product'[Unit Price]
RETURN
COUNTROWS (
FILTER (
'Product',
'Product'[Unit Price] > PriceOfCurrentProduct
)
) + 1
此外,最好通过使用更多变量来分隔计算的不同步骤,以更具描述性的方式编写代码。这样,代码也更容易遵循:
'Product'[UnitPriceRank] =
VAR PriceOfCurrentProduct = 'Product'[Unit Price]
VAR MoreExpensiveProducts =
FILTER (
'Product',
'Product'[Unit Price] > PriceOfCurrentProduct
)
RETURN
COUNTROWS ( MoreExpensiveProducts ) + 1
图4-11显示了后一种代码形式的行上下文的图形表示,它使您更容易理解DAX在哪个行上下文中计算公式的每个部分。
图4-12显示了此计算列的结果。
因为有14个产品的单价相同,所以它们的排名始终为1;第15个产品的排名为15,与其他价格相同的产品共享。如果我们能像图中那样将1、2、3而不是1、15、19排好,那将是很好的。我们将尽快解决此问题,但在此之前,重要的是要先说个小题外话。
为了解决提出类似的问题,必须对行上下文有扎实的了解,以便能够检测到公式的不同部分中哪个行上下文是活动的,并且最重要的是,要了解行上下文如何影响DAX表达式返回的值。值得强调的是,在公式的两个不同部分中求值的同一表达式 Product [Unit Price] 返回不同的值,这是因为要对其求值的上下文不同。当人们对评估上下文缺乏扎实的了解时,处理这样复杂的代码将非常困难。
如您所见,具有两个行上下文的简单排名表达式被证明是一个挑战。在第5章的稍后部分,您将学习如何创建多个过滤器上下文。那时,代码的复杂性增加了很多。但是,如果您了解评估上下文,那么这些方案很简单。在进入DAX的下一个层次之前,您需要充分了解评估上下文。这就是为什么我们敦促您再次阅读这节(甚至到目前为止的本章),直到这些概念明确为止。这将使阅读下一章变得更加容易,您的学习体验也会更加顺畅。
在离开此示例之前,我们需要解决最后一个细节——即使用1、2、3的序列而不是到目前为止获得的序列进行排名。该解决方案比预期的要容易。实际上,在前面的代码中,我们着重于对价格较高的产品计数。如是,该公式对14个排名第1的产品进行了计数,并将15分配给了第二排名级别。但是,对产品进行计数不是很有用。如果公式计算的价格高于当前价格,而不是产品价格,则所有14个产品都将折叠为一个价格。
'Product'[UnitPriceRankDense] =
VAR PriceOfCurrentProduct = 'Product'[Unit Price]
VAR HigherPrices =
FILTER (
VALUES ( 'Product'[Unit Price] ),
'Product'[Unit Price] > PriceOfCurrentProduct
)
RETURN
COUNTROWS ( HigherPrices ) + 1
图4-13显示了新的计算列以及UnitPriceRank。
最后的一小步是计算价格而不是产品计数,这似乎比预期的要难。使用DAX的工作越多,就越容易开始考虑为计算目的而创建的临时表。
在此示例中,您了解了处理同一表上多个行上下文的最佳技术是使用变量。请记住,变量是DAX语言于2015年晚些时候引入的。您可能会发现现有的DAX代码(在变量时代之前编写)使用了另一种技术来访问外部行上下文: EARLIER 函数,我们将在下一节介绍
使用 EARLIER 功能
DAX提供了访问外部行上下文的功能: EARLIER 。 EARLIER 通过使用上一个行上下文而不是最后一个行上下文来检索列的值。因此,我们可以使用 EARLIER(Product [UnitPrice])来表示 PriceOfCurrentProduct 的值。
许多DAX新手对 EARLIER 感到恐惧,因为他们对行上下文的了解不够充分,并且他们没有意识到可以通过在同一张表上创建多个迭代来嵌套行上下文。理解行上下文和嵌套的概念后, EARLIER 是一个简单的函数。例如,以下代码不使用变量即可解决以前的情况:
'Product'[UnitPriceRankDense] =
COUNTROWS (
FILTER (
VALUES ( 'Product'[Unit Price] ),
'Product'[UnitPrice] > EARLIER ( 'Product'[UnitPrice] )
)
) + 1
注意 EARLIER 接受第二个参数,这是要跳过的步骤数,以便可以跳过两个或多个行上下文。此外,还有一个名为 EARLIEST 的函数,该函数使开发人员可以访问为表定义的最外面的行上下文。在现实世界中,既不经常使用EARLIEST 也不使用 EARLIER 的第二个参数。尽管在计算列中通常有两个嵌套行上下文,但很少有三个或更多嵌套行上下文。此外,自变量问世以来, EARLIER 实际上已变得毫无用处,因为变量用法取代了 EARLIER 。
学习 EARLIER 的唯一原因是能够读取现有的DAX代码。没有其他理由在较新的DAX代码中使用 EARLIER ,因为在访问行上下文时,变量是保存所需值的更好方法。为此目的使用变量是一种最佳方法,并且可以使代码更具可读性。