本文翻译自Marco Russo & Alberto Ferrari的文章—《ALLSELECTED best practices》 来源:SQLBI ALLSELECTED 是一个强大但危险的函数。本文介绍了应遵循的最佳实践,以避免陷入 ALLSELECTED 可能带来的陷阱。
ALLSELECTED 是整个 DAX 语言中最复杂的函数之一。它是唯一一个利用影子筛选上下文(shadow filter contexts)的 DAX 函数。此外,当在 SUMMARIZECOLUMNS 中使用或在迭代器内部使用时,ALLSELECTED 的行为会略有不同。在 SUMMARIZECOLUMNS 中使用 ALLSELECTED 通常能得到预期结果,而在迭代器内部使用时却可能产生异常结果。混合使用这两种方式,很可能会导致报表出现问题!
本文简要介绍了 ALLSELECTED 在两种场景(SUMMARIZECOLUMNS 和迭代器)中的特性,并提供该函数的最佳实践指南,同时通过示例展示混合使用两种行为如何导致意外结果。
ALLSELECTED介绍
ALLSELECTED 的使用方式可以非常直观。例如,考虑以下报表的需求场景:
该报表使用切片器来筛选特定品牌。它显示每个品牌的销售额,以及每个选定品牌占所有已选品牌总销售额的百分比。计算百分比的公式非常简单:
Pct =DIVIDE ([],CALCULATE ([],ALLSELECTED ( 'Product'[Brand] )))
从直观上看,ALLSELECTED 会返回可视化筛选上下文中选定的品牌值——即用户在Adventure Works和Proseware之间选择的品牌。然而,Power BI向DAX引擎发送的查询并不包含"当前可视化"这一概念。因此,DAX度量值中根本不存在所谓的"可视化筛选上下文"(虽然视觉计算是另一个话题,但它与常规度量值无关)。
那么DAX是如何知道切片器和矩阵中选择了什么呢?答案是:它其实并不知道。ALLSELECTED并不会返回可视化外部筛选过的列(或表)的值。它的作用机制完全不同,只是在大多数情况下会"碰巧"产生相同的结果。
ALLSELECTED的行为表现会因其使用场景不同而变化,主要取决于:是否在SUMMARIZECOLUMNS中使用,以及是否存在影子筛选上下文。由于SUMMARIZECOLUMNS是Power BI查询语义模型的主要函数,我们先从ALLSELECTED在SUMMARIZECOLUMNS中的运作机制开始说明。
SUMMARIZECOLUMNS 中的 ALLSELECTED
当在 SUMMARIZECOLUMNS 中使用时,ALLSELECTED 会移除当前迭代分组列的筛选器,恢复由 SUMMARIZECOLUMNS 自身设置的原始筛选条件。举例来说,让我们看一个填充矩阵的简化查询版本:
EVALUATESUMMARIZECOLUMNS ('Product'[Brand],TREATAS ({"Adventure Works","Contoso","Fabrikam","Litware","Northwind Traders","Proseware"},'Product'[Brand]),"Sales_Amount", 'Sales'[Sales Amount],"Pct",DIVIDE ([Sales Amount],CALCULATE ([Sales Amount],ALLSELECTED ( 'Product'[Brand] ))))
执行该查询后,将生成以下结果:
从查询中可以看出,这里并不存在所谓的"可视化筛选上下文"概念。该查询通过SUMMARIZECOLUMNS同时实现筛选和分组操作:首先预设筛选条件,然后在每行处理时为当前品牌施加筛选,使得[销售额]仅计算当前品牌的数据。当使用ALLSELECTED时,DAX会移除当前品牌的筛选,从而恢复包含所有选定品牌的原始筛选上下文。
值得注意的是,即使筛选并非由SUMMARIZECOLUMNS直接创建,这一机制仍能正常工作。以下查询与我们刚才分析的案例效果等同,尽管其筛选是通过CALCULATETABLE而非SUMMARIZECOLUMNS设置的:
EVALUATECALCULATETABLE (SUMMARIZECOLUMNS ('Product'[Brand],"Sales_Amount", 'Sales'[Sales Amount],"Pct",DIVIDE ([Sales Amount],CALCULATE ( [Sales Amount], ALLSELECTED ( 'Product'[Brand] ) ))),TREATAS ({"Adventure Works","Contoso","Fabrikam","Litware","Northwind Traders","Proseware"},'Product'[Brand]))
由于 Power BI 几乎在所有可视化中都使用 SUMMARIZECOLUMNS,因此这种机制是最常用的。然而,ALLSELECTED 在非 SUMMARIZECOLUMNS 场景下同样适用,此时它会基于影子筛选上下文采用另一种不同的技术原理。
引入影子筛选上下文
影子筛选上下文是一种由迭代器创建的特殊筛选上下文。每当迭代开始时,迭代器会创建一个包含被迭代表行的影子筛选上下文。该筛选上下文处于非活跃状态,这意味着它不会主动筛选任何内容。它将保持休眠状态,直到可能被ALLSELECTED调用。
现在让我们重写上一节中使用过的查询,这次不使用SUMMARIZECOLUMNS:
EVALUATECALCULATETABLE (ADDCOLUMNS (VALUES ( 'Product'[Brand] ),"Sales_Amount", [Sales Amount],"Pct",DIVIDE ([Sales Amount],CALCULATE ([Sales Amount],ALLSELECTED ( 'Product'[Brand] )))),TREATAS ({"Adventure Works","Contoso","Fabrikam","Litware","Northwind Traders","Proseware"},'Product'[Brand]))
这个后续查询的结果与我们之前分析的完全一致。但此时ALLSELECTED之所以能从所有品牌中返回六个品牌,是因为作为迭代器的ADDCOLUMNS创建了影子筛选上下文,而ALLSELECTED激活了这个上下文。
以下是查询执行的详细步骤说明(我们在第三步引入影子筛选上下文概念):
外层的CALCULATETABLE创建包含六个品牌的筛选上下文
VALUES函数返回六个可见品牌,并将结果传递给ADDCOLUMNS
作为迭代器,ADDCOLUMNS在开始迭代前会创建一个包含VALUES结果的影子筛选上下文
· 影子筛选上下文类似于普通筛选上下文,但处于休眠状态,不会影响任何计算
· 影子筛选上下文只能被ALLSELECTED激活(稍后解释)。现在只需记住:该上下文包含六个被迭代品牌
· 我们将影子筛选上下文与常规筛选上下文区分,称后者为"显式筛选上下文"
迭代过程中,上下文转换会针对当前行创建仅包含该品牌的显式筛选
当Pct度量值调用ALLSELECTED时,它会执行以下操作:恢复参数指定列/表上的最后一个影子筛选上下文(若无参数则恢复所有列)。由于最后的影子筛选上下文包含六个品牌,这些品牌会重新可见
这个简单示例帮助我们理解了影子筛选上下文的概念。上述查询展示了ALLSELECTED如何利用影子筛选上下文来获取当前迭代外的筛选上下文。在SUMMARIZECOLUMNS出现前的早期Power BI版本中,系统使用ADDCOLUMNS等迭代器并依赖影子筛选上下文机制,而非SUMMARIZECOLUMNS的现行方案。时至今日,迭代器仍会生成影子筛选上下文,但度量值主要基于SUMMARIZECOLUMN算法运行。必须注意:这两种机制仍然共存,它们协同工作,但若使用不当就可能导致错误结果。
掌握 ALLSELECTED 的最佳实践
避免 ALLSELECTED 引发问题其实很简单:永远不要在迭代中使用包含 ALLSELECTED 的度量值,并尽可能减少使用含 ALLSELECTED 的度量值。随着度量值复杂度增加,ALLSELECTED 的引用终将难以追踪——最终你会不小心在迭代中调用某个度量值,而该度量值又通过 ALLSELECTED 调用了其他度量值。简言之:只要存在活跃迭代器,就应避免使用 ALLSELECTED。这样能确保只有 Power BI 在查询初始阶段通过 SUMMARIZECOLUMNS 触发的机制会生效。
错误案例演示
下面我们通过示例展示:如果在迭代中混用 ALLSELECTED 而忽视其机制差异,将如何导致数据计算错误。
业务需求
仅计算"重要产品"的销售额。判定标准为:当前筛选上下文中,产品销售额占比超过总销售额的 0.5%。
方案设计
基础度量值:用 ALLSELECTED 创建动态总销售额计算
筛选度量值:迭代产品并应用条件逻辑,仅累计占比 >0.5% 的产品
Total Sales = CALCULATE ( [Sales Amount], ALLSELECTED () )Relevant Sales Wrong =SUMX ('Product',IF ( DIVIDE ( [Sales Amount], [Total Sales] ) >= 0.005, [Sales Amount] ))
正如其名称所示,"错误相关销售额"(Relevant Sales Wrong)这一度量值的计算结果并不准确。问题根源在于:该度量值对[产品]进行迭代时,在其内部调用了另一个使用 ALLSELECTED 的度量值。下图展示了该度量值在矩阵视觉对象中的计算结果:
矩阵中的多行数据值都超过了总计行的数值。在深入分析该度量值错误原因之前,我们先展示其正确版本:
Relevant Sales Correct =VAR TotalSales = [Total Sales]RETURNSUMX ('Product',IF ( DIVIDE ( [Sales Amount], TotalSales ) >= 0.005, [Sales Amount] ))
错误版本与正确版本度量值的核心差异
"错误相关销售额"(Relevant Sales Wrong)与"正确相关销售额"(Relevant Sales Correct)的唯一区别在于:[总销售额]度量值的调用位置被移至迭代外部。虽然该度量值通过ALLSELECTED改变了筛选上下文,理论上每行迭代应获得相同结果(即不受上下文转换影响),但由于ALLSELECTED的特殊机制,实际差异非常显著。下图展示了使用正确度量值后的矩阵结果:
为清晰对比差异,我们使用简化版查询(包含两个度量值的完整代码):
DEFINEMEASURE Sales[TotalSales] =CALCULATE ([Sales Amount],ALLSELECTED ())EVALUATESUMMARIZECOLUMNS (ROLLUPADDISSUBTOTAL ('Product'[Brand],"Total"),"Sales Amount", [Sales Amount],"Wrong",SUMX ('Product',---- Inside the iteration, ALLSELECTED restores the shadow filter context-- created by SUMX, which is iterating over the products of the given brand---- This is the shadow filter context behavior--IF (DIVIDE ([Sales Amount],[TotalSales]) >= 0.005,[Sales Amount])),"Correct",---- Outside of the iteration, ALLSELECTED removes the filter over Product[Brand]-- and makes all the selected products visible---- This is the SUMMARIZECOLUMNS/ALLSELECTED behavior--VAR TotalSales = [TotalSales]RETURNSUMX ('Product',IF (DIVIDE ([Sales Amount],TotalSales) >= 0.005,[Sales Amount])))
从DAX查询中的注释可见,"错误计算"在迭代产品时从循环内部调用TotalSales度量值,因此它使用了影子筛选上下文机制。由于SUMX在迭代指定品牌的产品,TotalSales计算的是当前显示品牌中所有产品的销售额。
而正确表达式将TotalSales度量值放在迭代外部调用,因此不存在影子筛选上下文,此时ALLSELECTED会采用SUMMARIZECOLUMNS机制——它会移除Product[Brand]上的筛选器,使所有产品可见(若存在切片器筛选,则显示所有被选产品)。
这个示例特意保持简单:仅展示一个在迭代中直接调用含ALLSELECTED度量值的情况。实际场景中,面对包含数百个度量值的语义模型,几乎不可能追踪每个度量值在迭代内外的调用情况。因此规则很简单:ALLSELECTED只能安全用于直接放置在报表中的度量值。您应当避免调用包含ALLSELECTED的度量值,因为其他度量值可能会在不知情的情况下调用您的度量值(而该度量值内部使用了ALLSELECTED)。
结论
ALLSELECTED强大且实用,无需畏惧
影子筛选上下文机制在SUMMARIZECOLUMNS出现前确实适用
在SUMMARIZECOLUMNS内部使用时,ALLSELECTED结果可靠
但若在迭代中调用含ALLSELECTED的度量值,就会触发旧机制,导致难以调试的结果
解决方案很明确:确保迭代开始时绝不使用ALLSELECTED。这需要严格把控度量值调用链,从而保证代码始终输出预期结果。
延伸阅读:
END
长按下方二维码关注“Power Pivot工坊”获取更多微软Power BI、PowerPivot相关文章、资讯,欢迎小伙伴儿们转发分享~


