通过初识筛选上下文和进阶筛选上下文,你已经对筛选上下文有了一定的理解,本文将继续深入阐述筛选上下文,在开始之前,让我们先对之前的内容做一个简单的回顾:
- 筛选上下文为公式提供计值环境,无论度量值还是计算列,都在各自的筛选上下文环境中进行计算
- 计值环境由透视表行、列标签,筛选器、切片器、图表坐标轴等因素共同决定,其中最重要的是由 CALCUALTE 以编程方式创建的筛选上下文,它可以覆盖外部环境中相同的筛选条件。
- 当使用列作为筛选器时,DAX 不会对包含这一列的整张表应用筛选器。它只对列应用筛选器。然后,因为列是表的一部分,因此表也会有一个筛选器。然而,筛选器一次只对一列生效。
理解元组
现在你已经理解了扩展表和它在 DAX 中的工作方式,现在是时候对计值上下文交互进行更深入的分析,并给出计值上下文的最终定义了。让我们从定义一个元组(tuple)开始。元组是一组列值。例如,在日期表中,一个元组可能是这样的:
« Year = 2007, Month = January »
你可以将元组看作单行表,元组中的列可以属于不同的表,元组为模型的列定义一个值。因此,直观地说,元组的行为就像数据模型中的筛选器。例如将元组 «2007,January » 应用于包含销售表的模型,它将筛选 2007 年 1 月的销售数据。
元组(tuple)是关系数据库中的基本概念,关系是一张表,表中的每行(即数据库中的每条记录)就是一个元组,每列就是一个属性
要查看用作筛选器的元组示例,请查看以下表达式:
- CALCULATE (
- ……
- FILTER (
- CROSSJOIN ( ALL ( Date[Year] ), ALL ( Date[Month] ) ),
- OR (
- AND ( Date[Year] = 2007, Date[Month] = "January" ),
- AND ( Date[Year] = 2006, Date[Month] = "December" )
- )
- )
- )
复制代码
CALCULATE 使用以下元组列表作为筛选器参数:
« Year=2006, Month=December »
« Year=2007, Month=January »
元组的列表组成了筛选器。单个筛选器通常不构成最终的筛选上下文,筛选上下文实际上是一组筛选器。事实上,如果你观察下面的代码:
- CALCULATE(
- ……
- FILTER (
- CROSSJOIN ( ALL ( Date[Year] ), ALL ( Date[Month] ) ),
- OR (
- AND ( Date[Year] = 2007, Date[Month] = "January" ),
- AND ( Date[Year] = 2006, Date[Month] = "December" )
- )
- ),
- OR ( Product[Color] = "Black", Product[Color] = "Yellow" )
- )
复制代码
最终的筛选上下文包含两组不同的元组:一组筛选日期表中的两列,另一组筛选产品颜色。从视觉上看,筛选器与表是相同的。为了使两者的区别更加明显,当图中的表用来表示筛选器时会增加一个筛选器图标。前一个公式生成的筛选器如下图所示
左侧筛选器图标将筛选器与普通的表区分开
类似上图中的一组筛选器构成了筛选上下文。这让筛选上下文看上去像是多个表。如果某列位于筛选器中,我们就说这个列被筛选上下文过滤了。因此,示例中的年份、月份和颜色这三列都属于筛选上下文。
乍一看,元组看起来像一个普通的行,筛选器看起来像一个表。这两个概念非常相似,但考虑到使用场景我们使用了不同的名称。例如,在之前的代码中,FILTER 的结果是一个表。仅当你将其用作 CALCULATE 的筛选器参数时,它才会成为筛选上下文。在此过程中 DAX 将表转换为筛选上下文,表的各个行成为元组。两者的差别很小,但在描述筛选上下文行为时很有用。
表筛选器和列筛选器的区别
例如,当你将物理表或列做为筛选器时,这两种结果存在很大的区别,当上文中的公式使用 CROSSJOIN 的结果作为筛选上下文时,包含由 CROSSJOIN 生成的各个列,而当你使用完整的物理表作为筛选上下文时,将包含原始表扩展版本的所有列。如果查看以下两个表达式,它们使用的筛选器参数具有不同的元组:
- CALCULATE (
- SUM ( Sales[Quantity] ),
- 'Product Subcategory'[Subcategory] = "Radio"
- )
- CALCULATE (
- SUM ( Sales[Quantity] ),
- FILTER (
- 'Product Subcategory',
- 'Product Subcategory'[Subcategory] = "Radio"
- )
- )
复制代码
实际上,第一个筛选器参数生成一个由单列元组组成的筛选上下文:
« ‘Product Subcategory'[Subcategory] = Radio »
第二个表达式对表设置了筛选器。这样一来,得到的筛选上下文将包含 Product Subcategory 表的扩展版本的所有列构成的元组
« ‘Product Subcategory'[Subcategory] = Radio,
‘Product Subcategory'[ProductSubcategoryKey] = 3,
‘Product Subcategory'[ProductCategoryKey] = 1,
‘Product Category'[ProductCategoryKey] = 1,
‘Product Category'[Category] = Audio »
正如你在扩展表中了解到的,这种差异非常重要,如果忽略这种差异,将表用作筛选器时很可能会得到意料之外的结果。
列筛选器和表筛选器对比,黄色区域为扩展表
理解筛选上下文的运算符
现在你已经了解了什么是元组和筛选上下文,现在我们将定义筛选上下文中唯二的两种CALCULATE 需要合并筛选器参数时使用。以下示例对于澄清这一概念非常有用:
- CALCULATETABLE (
- ……
- Date[Year] = 2008,
- OR (
- Product[Color] = "Black",
- Product[Color] = "Yellow"
- )
- )
复制代码
公式有两个筛选器,一个是关于年份的,另一个是关于颜色的。我们一直以来的描述口径是 CALCULATE 的所有筛选器参数以 AND 逻辑组合到一起,但现在讨论已经深入到元组,我们可以更具体地说,它们以相交(INTERSECTION)的方式组合到一起。实际上,最终的筛选上下文如图所示
两个筛选器的交集是将两者合并,彼此之间是 AND 关系
交集运算是一种非常简单的操作,实际上,你可以将交集看作是由筛选器定义的集合之间的一种简单的 AND 运算。此外,交集运算具有一种优雅的特性,即它可以很好地在复杂筛选器中生效。比如,如果观察下面的表达式,你将注意到日期表使用两个条件进行筛选。第一个筛选条件得到 2007 年 1 月和 2006 年 12 月,第二个筛选条件得到销售量大于 100 的日期(无论年份和月份)。最终,交集运算得到了正确的结果(日期在 2007 年 1 月和 2006 年 12 月,且销售数量大于给定值 100 的日期)。
- CALCULATE (
- ……
- FILTER (
- CROSSJOIN ( ALL ( Date[Year] ), ALL ( Date[Month] ) ),
- OR (
- AND ( Date[Year] = 2007, Date[Month] = "January" ),
- AND ( Date[Year] = 2006, Date[Month] = "December" )
- )
- ),
- FILTER ( ALL ( Date[Date] ),
- CALCULATE ( SUM ( Sales[Quantity] ) ) > 100
- )
- )
复制代码
最后,交集运算具备对称性:A 与 B 相交得到的结果和 B 与 A 相交得到的结果一致。这与预期吻合,但马上你会了解到,覆盖运算是不对称的,你需要更加小心地使用它。
覆盖运算
现在你已经学习了交集运算,接下来介绍 DAX 在筛选上下文中执行的第二种运算:覆盖(OVERWRITE)。覆盖是 CALCULATE 在合并生成新的筛选上下文时使用的运算方式。例如,查看以下表达式:
- CALCULATE (
- CALCULATE (
- ……
- Product[Color] = "Yellow"
- ),
- Product[Color] = "Black"
- )
复制代码
内层的 CALCULATE 覆盖外层使用的筛选器,公式计算黄色产品的结果,忽略黑色筛选器。这在预料之中,但它表明,在这种情况下,两个筛选上下文没有相交,而是使用了覆盖。覆盖的定义很简单,不过一旦涉及复杂筛选器,会导致非常棘手的局面。让我们从覆盖运算的定义开始:
在计算 A 覆盖 B 时,我们将 B 定义为之前的筛选上下文,将 A 作为覆盖 B 的筛选上下文。这样我们在阅读“A 覆盖 B”时可以获得统一的理解,因为新筛选器(A)覆盖了旧筛选器(B)。在上一个示例中,A 是黄色筛选器,B 是黑色筛选器。
为了计算 A 覆盖 B 这一过程,DAX 执行两步操作:
- 从 B 的所有筛选器中移除在 A 中被筛选的列,从而生成一个新的筛选上下文,我们称之为 B – Cleaned。
- A 与 B – Cleaned 进行合并
在之前的示例中,筛选器 B 包含黑色。B – Cleaned 变为空因为 DAX 移除了其唯一的颜色列,并最终生成只包含黄色筛选器的新筛选上下文。通过下图你可以看到覆盖操作的图形表示。
从语义角度,覆盖移除了 B 中的列,然后将得到的空筛选器与 A 相交
覆盖是一种强大的运算,但使用它需要特别注意。事实上,对于标准形态的筛选器(Well-Shaped Filter),它以一种直观的方式工作,而对于固化形态的筛选器,它开始变得复杂得多。在继续下面的内容之前,是时候介绍固化形态筛选器了,因为它们对于解释覆盖运算起着重要作用。
理解固化筛选器
标准形态的筛选器可以表示为单列筛选器之间的笛卡儿积(CROSSJOIN)。示例如下:
- CALCULATETABLE (
- ……
- OR (
- Date[Year] = 2007,
- Date[Year] = 2006
- ),
- OR (
- Product[Color] = "Black",
- Product[Color] = "Yellow"
- )
- )
复制代码
实际上,这个示例与下面的写法等价:
- CALCULATETABLE (
- ……
- CROSSJOIN (
- FILTER (
- ALL ( Date[Year] ),
- OR (
- Date[Year] = 2007,
- Date[Year] = 2006
- )
- ),
- FILTER (
- ALL ( Product[Color] ),
- OR (
- Product[Color] = "Black",
- Product[Color] = "Yellow"
- )
- )
- )
- )
复制代码
你可以很容易地将标准形态的筛选器图形化表示为作用于单列的一组筛选器,如图所示(左上角漏斗图标表示这里展示的筛选条件的值,并非普通的表格)。
标准形态的筛选器可视为为来自单列的筛选器
这种筛选器总是指向简单的表达式,无论交集还是覆盖在这种状态下都以直观的方式工作。
另一方面,固化的筛选器是非标准形态的筛选器。换句话说,你不能将它表示为单列筛选器之间的笛卡儿积,比如你在下面这个表达式中看到的:
- CALCULATE(
- ……
- FILTER (
- CROSSJOIN ( VALUES ( Date[Year] ),
- VALUES ( Date[Month] )
- ),
- OR (
- AND ( Date[Year] = 2007, Date[Month] = "January" ),
- AND ( Date[Year] = 2006, Date[Month] = "December" )
- )
- )
- )
复制代码
此筛选器的图形化结果如下图所示。正如你所看到的,你不能将这类筛选器显示为两个单独的列。相反,你需要将列放在同一个表中,因为它们具有存储在筛选器内部的关系。
固化筛选器不等于简单的列筛选器的笛卡尔积
不能将包含 2007 年一月份和 2006 年十二月份的筛选器表示为简单筛选器的交叉相乘。实际上,DAX 必须同时考虑这两列才能准确计值筛选器,也就是说,固化筛选器定义了自身列之间的关系。事实上,在学术文献中,我们称筛选器(或者更笼统地称为表)是一种关系。我们倾向于在书中避免使用这个术语,因为它与表间关系这个更广泛使用的概念相冲突。然而,表(在本例中是筛选器)定义了列之间的关系。
固化筛选器和上下文运算
当需要对筛选上下文执行覆盖运算时,固化筛选器可能会带来问题。事实上,虽然交集运算可以很好地与固化筛选器一起工作,但是覆盖运算会导致更复杂的局面。例如,让我们以前一个表达式为例,补充一个关于年份的筛选条件,如:
- CALCULATE(
- ……
- FILTER (
- CROSSJOIN ( VALUES ( Date[Year] ), VALUES ( Date[Month] ) ),
- OR (
- AND ( Date[Year] = 2007, Date[Month] = "January" ),
- AND ( Date[Year] = 2006, Date[Month] = "December" )
- )
- ),
- Date[Year] = 2007
- )
复制代码
正如你所预期的,这个表达式计算 2007 年 1 月的值,因为第一个筛选器返回 2007 年 1 月和 2006 年 12 月,与第二个筛选器相交,只返回 2007 年 1 月。在图 10-29 中可以看到交集运算的图形表示。
固化筛选器可以安全的执行交集运算
但如果你用这种方式改写表达式,情况就会有所不同:
- CALCULATE (
- CALCULATE (
- ……
- Date[Year] = 2007
- ),
- FILTER (
- CROSSJOIN ( VALUES ( Date[Year] ),
- VALUES ( Date[Month] ) ),
- OR (
- AND ( Date[Year] = 2007, Date[Month] = "January" ),
- AND ( Date[Year] = 2006, Date[Month] = "December" )
- )
- )
- )
复制代码
在这个表达式中,我们把年份筛选器移动到了内层 CALCULATE。两个版本的区别是,现在 CALCULATE 使用覆盖运算合并两个筛选器,从第一个筛选器中删除日历年,只留下月份标签上的筛选器,然后将这个筛选器与年份上的筛选器相交,如图:
对固化筛选器集合应用覆盖运算可能会导致意外的结果
生成的筛选器包含 2007 年 1 月和 12 月,因为年份列的新筛选器从之前筛选器中删除了这一列。这样做后,它破坏了存储在筛选器中的关系,并将其替换为标准筛选器,这可能不是你在编写公式时所期望的。
解决方案
在 CALCULATE 调节器章节,你已经学习了 KEEPFILTERS 的行为。它可以很好的解决固化筛选器在覆盖运算时遇到的问题
理解 KEEPFILTERS在DAX的复杂函数排行榜上,KEEPFILTERS有一个醒目的位置。某种程度上,它的行为比较容易学习和记忆,但是你很难精确掌握何时使用它以及使用它会产生什么结果。类似于ALLSELECTED,KEEPFILTERS要求你准确地理解它的语义,然后才能安全地使用它。
现在你已经对筛选上下文和筛选上下文的KEEPFILTERS:当你使用 KEEPFILTERS 时,CALCULATE 通过交集运算(而不是覆盖)将新的筛选上下文与之前的筛选上下文合并。这使得你可以保持固化形态筛选器集合的完整性,因为相交会保留关系,而覆盖可能会破坏它们。
总结
我们可以将这种规则总结如下:对标准筛选器上执行上下文运算时,交集和覆盖都以一种直观的方式工作。当使用固化筛选器时,交集通过与结果相交来保持筛选器的形态,而覆盖可能会中断筛选,从而丢失存储在原始筛选器中的某些关系,产生错误结果。
本文的重点是理解筛选上下文的运算符,它们完整的描述了筛选上下文之间的运算方式,只有准确的计算出最终的筛选上下文,公式才能在这个环境中执行最终计算,由此可见,对筛选上下文的计算将贯穿整个 DAX 使用过程。请记住,无论多么复杂的公式,它所依赖的筛选上下文都离不开这两种运算方式,并且也只有这两种运算方式。
关于筛选上下文的介绍到这里告一段落,目前的知识已经足以解决实践中的所有问题,但是从技术角度,对筛选上下文的底层解释仍然缺失,在完成 DAX 圣经第二版的翻译之后,我将继续补充这部分内容,请继续关注。 |