DAX高级:GROUPBY 和 SUMMARIZE 之间的区别

PowerBI战友联盟

共 10434字,需浏览 21分钟

 ·

2023-08-19 10:04

本文翻译自 Marco Russo & Alberto Ferrari 的文章《Differences between GROUPBY and SUMMARIZE》。

GROUPBY 和 SUMMARIZE 都是用于按列进行分组的函数。然而,它们在性能和功能上存在差异。了解详细信息可以帮助开发人员在特定的场景下选择正确函数。

DAX 提供了丰富的函数集,其中一些函数的功能是重叠的。在众多函数中,有两个函数执行分组操作:SUMMARIZE 和 GROUPBY。这并不是仅有的两个执行分组操作的函数:SUMMARIZECOLUMNS 和GROUPCROSSAPPLY 也执行类似的操作。然而,本文主要关注 SUMMARIZE 和 GROUPBY,因为其他函数拥有更多功能,所以进行比较可能不公平。

现在让我们详细介绍这些函数的工作方式,以提供关于前面陈述的更多技术信息。


SUMMARIZE 函数



SUMMARIZE 函数执行两个操作:按列进行分组和添加新的本地列。我们之前已经在一篇详细且技术性较强的文章中介绍过 SUMMARIZE 函数:《SUMMARIZE 的所有秘密》。在那篇文章中,我们描述了 SUMMARIZE 的行为以及为什么不应该使用它来计算新的本地列。具体而言,SUMMARIZE 实现了聚类,这是一种虽然非常强大但可能导致错误结果和较差性能的分组技术。

然而,为了进行比较,我们将使用 SUMMARIZE 函数来计算新的列,以描述其独特的行为。

在简单示例中使用 SUMMARIZE 函数时,它表现得很好,将分组操作下推到存储引擎。例如,以下代码可以正常工作,并产生预期的存储引擎查询:

EVALUATE
SUMMARIZE (
Sales,
'Product'[Brand],
"Sales Amount", [Sales Amount]

)

SUMMARIZE 函数会扫描 Sales,按 Product[Brand] 进行分组,并按 brand 生成销售额。存储引擎查询如下所示:

WITH
$Expr0 := ( PFCAST ( 'Sales'[Quantity] AS INT ) * PFCAST ( 'Sales'[Net Price] AS INT ) )
SELECT
'Product'[Brand],
SUM ( @$Expr0 )
FROM 'Sales'
LEFT OUTER JOIN 'Product'
ON 'Sales'[ProductKey]='Product'[ProductKey];
错误去粘贴吧

然而,一旦执行的度量值稍微复杂起来,这种简单的行为很容易丧失。实际上,SUMMARIZE 函数使用一种名为“聚类”的特殊技术来进行计算,请看以下代码:

EVALUATE
SUMMARIZE (
Sales,
'Product'[Brand],
"Sales Amount", [Sales Amount],
"Sales All Brands",
CALCULATE (
[Sales Amount],
REMOVEFILTERS ( Product[Brand] )
)
)
错误去粘贴吧

可以预计到 Sales All Brands 会计算出销售总额,因为 CALCULATE 函数会移除过滤器上下文中的唯一过滤器。然而,这种推测并未考虑聚类。由于聚类的存在,SUMMARIZE 函数所放置的过滤器会影响扩展的 Sales 表的所有列,导致了这种奇怪的结果。

正如你所看到的,Sales All Brands 与 Sales Amount 的值相同。不同的数据分布或重复的行可能会导致不同的值。此外,由于聚类的存在,一旦要进行聚合的度量值是很重要的,SUMMARIZE 需要实例化整个表。计算 Sales All Brands,以下是正在执行的 VertiPaq 查询之一:

WITH
$Expr0 := ( PFCAST ( 'Sales'[Quantity] AS INT ) * PFCAST ( 'Sales'[Net Price] AS INT ) )
SELECT
'Sales'[Order Number],
'Sales'[Line Number],
'Sales'[Order Date],
'Sales'[Delivery Date],
'Sales'[CustomerKey],
'Sales'[StoreKey],
'Sales'[ProductKey],
'Sales'[Quantity],
'Sales'[Unit Price],
'Sales'[Net Price],
'Sales'[Unit Cost],
'Sales'[Currency Code],
'Sales'[Exchange Rate],
SUM ( @$Expr0 )
FROM 'Sales';
错误去粘贴吧

请注意,RowNumber 不是查询的一部分,因此数据缓存的粒度与 Sales 的粒度不完全相同,就像使用 GROUPBY 一样。然而,由于表的所有列都被用作分组依据列,因此大小通常非常相关。

使用 SUMMARIZE 和 ADDCOLUMNS 的相同查询会产生预期的结果:

EVALUATE
ADDCOLUMNS (
SUMMARIZE (
Sales,
'Product'[Brand]
),
"Sales Amount", [Sales Amount],
"Sales All Brands",
CALCULATE (
[Sales Amount],
ALL ( Product[Brand] )
)
)
错误去粘贴吧

结果如下。

得益于聚类,SUMMARIZE 也可以对本地列进行分组。尽管按照本地列进行分组,以下查询仍然可以正常工作:

EVALUATE
SUMMARIZE (
ADDCOLUMNS (
Sales,
"Transaction Size",
IF (
Sales[Quantity] > 3,
"Large",
"Small"
)
),
[Transaction Size],
"Sales Amount", [Sales Amount]
)
错误去粘贴吧

结果显示了按交易规模分组的销售金额。

然而,请记住,尽管从句法和语义的角度来看,这个查询是有效的,但其结果是通过使用聚类来计算的。聚类在多种情况下会产生令人惊讶的结果,并且它带来的问题多于解决方案。此外,在这种情况下,计算也需要实例化整个 Sales 表。


GROUPBY 函数


GROUPBY 按表的一列进行分组。该列可以是模型列或本地列。然而,它的行为与 SUMMARIZE 的行为非常不同。GROUPBY 甚至不会将计算下推到存储引擎:整个计算在表被实例化后在公式引擎中进行。GROUPBY 还可以为其结果添加新列。但是,由于其特性,新列需要被计算为使用CURRENTGROUP 特殊函数对正在进行分组的表中的列的简单聚合。

举个例子,让我们看一下以下代码:

EVALUATE
GROUPBY (
Sales,
'Product'[Brand],
"Sales Amount",
SUMX (
CURRENTGROUP (),
Sales[Quantity] * Sales[Net Price]
)
)
错误去粘贴吧

GROUPBY 扫描 Sales 并按 Product[Brand] 进行分组。为了执行分组,DAX会将 Sales 中所需的列实例化到一个数据缓存中,然后由公式引擎进行处理。实际上,查询执行了以下代码:

SELECT
'Product'[Brand],
'Sales'[RowNumber],
'Sales'[Quantity],
'Sales'[Net Price]
FROM 'Sales'
LEFT OUTER JOIN 'Product'
ON 'Sales'[ProductKey]='Product'[ProductKey];
错误去粘贴吧

在 Sales 表中,DAX 检索了 Sales[Quantity]、Sales[Net Price] 和 Product[Brand]。Sales[RowNumber] 的存在保证了所有行都被检索出来,否则 VertiPaq 本身会执行分组操作。

结果是一个与 Sales 具有相同行数的表,因此可能非常大。公式引擎会扫描该表,根据 Product[Brand] 将其分成不同的群组,然后针对每个群组计算 Sales[Quantity] 与 Sales[Net Price]的乘积之和。

GROUPBY 的一个明显限制是,在迭代 CURRENTGROUP 时所使用的表达式不能涉及上下文转换。这个限制使得在迭代中使用现有的度量是不可能的。正如你可能注意到的,我们在示例中不得不重新编写了 Sales Amount 的代码。

尽管看起来较慢,GROUPBY 是唯一能够对没有继承关系的表执行分组和计算的 DAX 函数。例如,以下查询通过表的一列对本地表进行分组,而 GROUPBY 是唯一能够执行此操作的函数:

EVALUATE
VAR TableToGroup =
SELECTCOLUMNS (
{
( "A", 1 ),
( "A", 2 ),
( "B", 3 ),
( "B", 4 )
},
"Group", [Value1],
"Value", [Value2]
)
RETURN
GROUPBY (
TableToGroup,
[Group],
"Result",
SUMX (
CURRENTGROUP (),
[Value]
)
)
错误去粘贴吧

当使用其他 DAX 函数生成一个小表,然后需要对其中的一列进行分组,并对每一行进行简单的聚合时,GROUPBY 是正确的函数选择。



选择正确的函数函数



正如你所见,当需要按模型进行列分组时,SUMMARIZE 表现良好。尽管它具有按本地列分组的能力,但它使用了聚类,其结果通常是意想不到的。GROUPBY 不使用聚类。然而,它具有一个非常强大的限制:它总是需要将需要分组的表实例化。因此,在执行按模型列分组时,它可能不是最佳选择,而 ADDCOLUMNS/SUMMARIZE 效率通常更高。

然而,当需要对一个小型临时表按本地列进行分组时,GROUPBY 是最佳的函数,因为它可以在不依赖聚类的情况下完成工作。

明智的 DAX 开发人员会根据工作的需求选择合适的函数,通常会混合使用 SUMMARIZE、ADDCOLUMNS 和 GROUPBY,以获得最佳的性能和正确的结果。让我们通过一个例子来详细说明。

EVALUATE
SUMMARIZE (
ADDCOLUMNS (
Sales,
"Transaction Size",
IF (
Sales[Quantity] > 3,
"Large",
"Small"
)
),
[Transaction Size],
"Sales Amount", [Sales Amount]
)
错误去粘贴吧

这个查询使用了 SUMMARIZE,因此涉及到聚类。它执行了两个 VertiPaq 查询。第一个查询基本上将 Sales 进行了实例化:

SELECT
'Sales'[Order Number],
'Sales'[Line Number],
'Sales'[Order Date],
'Sales'[Delivery Date],
'Sales'[CustomerKey],
'Sales'[StoreKey],
'Sales'[ProductKey],
'Sales'[Quantity],
'Sales'[Unit Price],
'Sales'[Net Price],
'Sales'[Unit Cost],
'Sales'[Currency Code],
'Sales'[Exchange Rate]
FROM 'Sales';
误去粘贴吧

第二个存储引擎查询使用了第一个查询的结果来构建对 Sales 的大规模过滤器:

WITH
$Expr0 := ( PFCAST ( 'Sales'[Quantity] AS INT ) * PFCAST ( 'Sales'[Net Price] AS INT ) )
SELECT
'Sales'[Order Number],
'Sales'[Line Number],
'Sales'[Order Date],
'Sales'[Delivery Date],
'Sales'[CustomerKey],
'Sales'[StoreKey],
'Sales'[ProductKey],
'Sales'[Quantity],
'Sales'[Unit Price],
'Sales'[Net Price],
'Sales'[Unit Cost],
'Sales'[Currency Code],
'Sales'[Exchange Rate],
SUM ( @$Expr0 )
FROM 'Sales'
WHERE
( 'Sales'[Exchange Rate], 'Sales'[Currency Code], 'Sales'[Unit Cost], 'Sales'[Net Price], 'Sales'[Unit Price], 'Sales'[Quantity],
'Sales'[ProductKey], 'Sales'[StoreKey], 'Sales'[CustomerKey], 'Sales'[Delivery Date], 'Sales'[Order Date],
'Sales'[Line Number], 'Sales'[Order Number] )
IN { ( 1.000000, 'USD', 1227800, 2536500, 2670000, 1, 1507, 999999, 1573592, 43818.000000, 43816.000000, 1, 363800 ) ,
( 0.914500, 'EUR', 1677300, 2928100, 3290000, 2, 241, 999999, 587554, 43739.000000, 43736.000000, 2, 355804 ) ,
( 0.902900, 'EUR', 676000, 1470000, 1470000, 1, 668, 340, 884269, 43693.000000, 43693.000000, 1, 351503 ) ,
( 1.335200, 'CAD', 322500, 701300, 701300, 3, 1707, 999999, 278457, 43473.000000, 43472.000000, 1, 329404 ) ,
( 1.000000, 'USD', 1480780, 3220000, 3220000, 3, 1410, 999999, 1582937, 43095.000000, 43090.000000, 0, 291214 ) ,
( 1.297600, 'CAD', 3214400, 6990000, 6990000, 1, 405, 80, 326829, 43836.000000, 43836.000000, 2, 365800 ) ,
( 1.000000, 'USD', 300800, 513300, 590000, 2, 501, 999999, 1540547, 43818.000000, 43813.000000, 1, 363503 ) ,
( 1.000000, 'USD', 186500, 364950, 405500, 6, 79, 450, 1665181, 43239.000000, 43239.000000, 0, 306110 ) ,
( 1.310000, 'CAD', 1520800, 4590000, 4590000, 4, 569, 100, 384389, 43407.000000, 43407.000000, 0, 322905 ) ,
( 0.875900, 'EUR', 1379600, 3000000, 3000000, 1, 1449, 999999, 590077, 43410.000000, 43406.000000, 0, 322800 )
..[13,915 total tuples, not all displayed]};
错误去粘贴吧

尽管在我们的示例模型上工作速度很快,但在实际情况下,如果 Sales 表中有数千万行数据,这两个查询可能会非常繁重和缓慢。

用 GROUPBY 表达的相同查询可能会更高效:

EVALUATE
GROUPBY (
ADDCOLUMNS (
Sales,
"Transaction Size",
IF (
Sales[Quantity] > 3,
"Large",
"Small"
)
),
[Transaction Size],
"Sales Amount",
SUMX (
CURRENTGROUP (),
Sales[Quantity] * Sales[Net Price]
)
)
错误去粘贴吧

尽管我们无法使用基本度量“Sales Amount”,但实例化级别较小。仅执行了以下 VertiPaq 查询:

SELECT
'Sales'[RowNumber],
'Sales'[Quantity],
'Sales'[Net Price]
FROM 'Sales';
错误去粘贴吧

然而,这个数据缓存的粒度与 Sales 相同,在大型模型中可能会成为一个严重的问题。

要获得更好的性能,需要结合这两个函数并改变我们的观点。我们首先按 Sales[Quantity] 进行分组,使用 ADDCOLUMNS 和 SUMMARIZE 生成一个非常小的表。该表只包含 10 行。然后我们添加 Transaction Size 列,最后使用 GROUPBY 将这个只有 10 行的表按照 Transaction Size 分组成两个类别:

EVALUATE
GROUPBY (
ADDCOLUMNS (
SUMMARIZE (
Sales,
Sales[Quantity]
),
"@Sales", [Sales Amount],
"Transaction Size",
IF (
Sales[Quantity] > 3,
"Large",
"Small"
)
),
[Transaction Size],
"Sales Amount",
SUMX (
CURRENTGROUP (),
[@Sales]
)
)
误去粘贴吧

这个DAX 查询只执行了两个存储引擎查询。第一个查询按 quantity 对 sales amount 进行分组:

WITH
$Expr0 := ( PFCAST ( 'Sales'[Quantity] AS INT ) * PFCAST ( 'Sales'[Net Price] AS INT ) )
SELECT
'Sales'[Quantity],
SUM ( @$Expr0 )
FROM 'Sales';
错误去粘贴吧

第二个 VertiPaq 查询只检索了 Sales[Quantity] 的不同值:

SELECT
'Sales'[Quantity]
FROM 'Sales';
错误去粘贴吧

大部分计算已经下推到了存储引擎,实例化级别可以忽略不计,即使在大型数据库上,这最后一个 DAX 查询也非常快速。



结论



了解函数的细节、它们的实现方式以及用法是任何研究 DAX 的人都应具备的重要技能。在本文中,我们介绍了 GROUPBY 和 SUMMARIZE 之间的区别。然而,DAX 还有许多其他值得学习的隐藏细节。

使用错误的函数可能会产生错误的结果或低效的查询。你对 DAX 的了解越多,你的代码就会变得更好。


我就此问题与BI佐罗讨论,并询问:作为业务人员,应该怎样使用这些函数的最佳实践是什么?此前的这篇文章几乎完美地回答了我的问题。

强烈推荐

PowerBI DAX 表连续运算及上下文转换失效

通过学习 SQLBI 的专业技术文章再加上BI佐罗老师的最佳实践,这已经可以解决好实际的各种问题了。

希望大家也可以从这些技术文章和实践总结中获益,并直接套用。

Power BI 终极系列课程《BI真经》


BI真经 - 让数据真正成为你的力量

扫码与精英一起讨论 Power BI,验证码:data2023

点击“阅读原文”进入学习中心

浏览 1072
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报