关于列表筛选
前面我们展示了CALCULATE函数的列表筛选+计算的参数构成图。基于它的最简单定义:CALCULATE定义一个计算列表集,因而进一步可表示为下图(仅仅将CALCULATE的计算列表与筛选列表两部分关联在一起):
第14式:CALCULATE的列表(列值)变化
1、至少,该图示代表了DAX计算式该有的样子,依此,我们简单的先理解其含义:从一个关系型业务数据模型里,引用(查询)出计算需要的列表、并构建一个新的数据模型的子集(筛选),然后完成聚合类计算(计算)。这就是我们前面所说的筛选+计算。我们通常使用CALCULATE来定义这句话。
如果你刚从Excel接触到DAX,则必须要了解:DAX包含了一些和Excel函数类似的函数及用法,但DAX提供了一些专有的函数和用法,而且,DAX的计算原理与Excel是完全不同的,DAX更强调列表(列式)的概念,你应该避免使用表或单元格的思维模式来套用DAX(不止一次地说)。因此,DAX一般使用函数来操作列表。需要强调的是:
1)如果说Excel函数的计算是基于单元格的,那么DAX的计算是基于列进行的;
2)由于Excel与DAX各自的特点和优势(你可以查看官方文档了解),更好的使用方式是,结合两者的优点。例如,DAX的列式特点是它的最大优势,但同时也是它最大的弱点:DAX的行概念始终会让你费解。
基于列式运算,一个100万行的列,DAX也许只需要计算1次!对,1次就行。这对于习惯于Excel普通表的单元格填充,除了不可思议之外,还无法理解。但是,这确是DAX的强大之处。
DAX的另一个让你迷恋的过人之处还在于可以融合来自不同数据源的数据,并构成数据模型计算。这是后面要谈到的主题:DAX能筛选 +计算的幕后功臣:列表关系。这里略。
2、 既然DAX基于列式运算如此的“牛气”,问题是:这是一种粗犷式、强调集团作战式的计算,目的是尽量发挥DAX在大规模数据计算上的优势。
但同时,我们更希望DAX具备像Excel单元格那样的行的行为特点,即具备细腻式、强调游击作战式的按行运算,以确保DAX可以完成更细致的业务逻辑条件计算。
无论采用哪种方式,DAX所能使用的只有列表。因此,要兼顾前面的两种行为优势。需要将列表:列式化处理以及行(单元格)化处理,DAX提供了对应的两种列表形态:列表和值列表。
前面我们说,应该将DAX数据模型理解为由一个个单列表构成。换句话说,列表是DAX数据模型里的唯一单元值(包含列关系、列类型等)或元素,因此,一个单列表才是DAX内部引擎能识别的最小单位。
所以,DAX(往往就是CALCULATE)筛选,通常指得到一个计算子集,DAX计算,则是针对这个子集定义的每一行进行聚合类计算(暂时这样说)。
3、通过前面关于值列表和列表的介绍。不难理解,包含整个列值的列表,还可以使用所包含的列值的拆分组合,来构成不同的变化莫测的值列表(即值列表可以由一个或多个列值、以及它们的组合变化构成)。
因此,你可以将所谓的列表筛选,简单理解为列值的数量大小--列表范围的变化。
当然,这包括最极端的两种筛选结果(比较熟悉的两种情况):一个是:结果为整列的列值的列表(列值没有变化),一个是:列表可能被筛选为只有一行的一列,也就是行列的交叉坐标(单元格)。
DAX计算,需要最后完成这种单元格式的、对应于每一行的处理,才能填充数据作为输出结果。被输出的该单元格结果,一般都不需要过多关注)。我们关注的是DAX运行过程中的所有值列表、列表行为。换句话说,我们不太需要关心数据模型里的物理元列表(它总是列表),以及计算输出的结果(你总会去检查公式)。
4、综合上述两种情况,现在你可能已明白,之前我们有意使用了两个不同的词:引用(显式)列表与定义(隐式)值列表。是的,因为不管是使用了列的某些特定行中的列值,还是使用了完整的列本身(包含全部列值),这其实都是相同的语义。因而,很难区分到底是在使用哪一种。
很显然,DAX中,你经常需要面对:现在使用的是列值还是列本身?这是需要明白清楚的一件事(很多资料上将它模糊化处理了)。但后遗症是明显的,比如在回答某些公式行为时,会显得力不从心。
因此,我们将DAX操作列表的行为,称为引用列表,将操作列值的行为称为定义(偶尔会称为:创建)值列表。这对应于列表的行、列两种行为(行筛选与列表筛选)。
当你能够识别出正在使用的是DAX的列引用(列表),还是定义列的一个或多个列值(值列表)。有了这种思维,你将正式踏入DAX的新阶段。
如果你需要更简单的描述。那么,以一种语言式、文科生、方言式的方式是值得鼓励的:DAX的整个筛选(某个计算单元或整个CALCULATE()),一般是由整个列值(列表)-->一个或多个列值(值列表)-->单个行值(行、单元格)。在还没有讨论DAX内的一些更复杂的、特别的行为前。这已经足够正确。
当我们使用“引用列表”时,引擎能良好识别并自动执行它。但是,当从某个给定列表中检索某些需要的列值时,你需要一种方法来告诉DAX:你使用的是哪些行?以便用于DAX最后计算该值。换句话说,您需要一种方法来定义表的当前的列值(行的行为)。
这就是我们通常所说的“定义当前行,或者称为:“当前行”的值列表(行)筛选。无论是显式、还是隐式(计算列)中行的行为(所谓的遍历),都是在使用值列表筛选。
我们将在后续内容中了解到定义"当前行行为"的方式方法,以及常用的一些定义值列表的函数。
5、最初判定是列表还是值列表的最简单方式是:引用包含整个列的列值的一般为列表,否则,则是值列表。
现在,一个CALCULATE()计算中,我们用“引用”、“定义”来区分它。也就是说,你引用的是列表,定义的是值列表。
你可能已经见过如下两个公式,先看一个DAX表达式对列引用的操作表达式:
毛利额 := SUMX ( Sales,Sales[销售额] – Sales[成本] )
上述表达式中:Sales[销售额] 和 Sales[成本]是列引用。即:引用元列表(整个列的列值),然后由SUMX定义这两个列的行行为(值列表的行筛选),其中这两个列的每个列值都是一个行筛选,直到最后得出只有一行的值(一个值的值列表筛选。
再看一个DAX中使用定义的值列表来计算[产品名称]列数量(计数)的度量:
产品数量 : = COUNTROWS ( VALUES (产品[产品名称] ) )
在该公式中,产品[产品名称]列对于VALUES来说,是列引用--列表引用,VALUES要使用的是产品表中[产品名称] 这个列(引用该列本身的全部列值,不是其他的列表)。换句话说,你引用列表--产品[产品名称]列, VALUES函数针对该列表所有列值计算(结果是一个只包含唯一值的那些值列表)。
对于COUNTROWS函数来说,你定义了一个值列表。因此,COUNTROWS计数的列表是由定义的"VALUESi (产品[产品名称]"部分的值列表,它只包含了那些唯一值的列值,而不是所有的产品[产品名称] 列的列值。该部分值列表充当列表被COUNTR0WS函数引用。
该公式看起来比较简单,假如涉及到更多的列表引用以及值列表定义时,则更复杂。
第15式:CALCULATE值列表与列表的存在方式
前面,我们说过,任何一个DAX计算式(或计算式单元),都需要一个显式的列表筛选和一个隐式的行筛选(计算列中引用一个列表能计算,这可以看成:这时候隐式的行只有一个整列的列值,因而结果可能都是一个相同的值)。
因此,我们可以判断,凡是DAX中的计算式参数,该计算式一定是一个即包含显式列表,又包含隐式行行为的值列表。
我们稍微举两个例子说明,如图(使用的是DAX Studio):
从DAX运行过程中发现,运行被迫中止的位置,也正是这里。
上述公式中,SUMMARIZE的新增计算列“01”,依据的是后面的参数:计算式的值(即函数的参数需要“定义”一个结果为值列表的计算式)。如果直接引用列(这里是订单[销售额]列),则由于缺少行的行为,你会收到一个如上图所示的错误提示)。修正的方法很简单,现在,我们很容易就知道,你需要一个计算式的值列表,比如添加像错误提示里说的:能获取单一列值结果(隐式的行行为)的聚合类函数 MIN、MAX、SUM、COUNT、AVERAGE(根据计算需要)定义的计算式。
在数据模型里,如果有类似的公式,也将收到一条相同内容的提示。
当然,你也可以使用前面本系列中已提到的FILTER,SUMX,AVERAGEX,ADDCOLUMNS等所谓迭代器函数的任何一个DAX函数,来定义一个计算式,其目的都是使行筛选可用(添加行筛选)。
如果行筛选不可用,你试图直接对列引用进行计算则会产生错误。例如,前面多次提到的:在DAX度量值中仅写入列引用,就会导致这种错误,因为不存在行筛选。
我们暂时先总结出如下所示的三种方式,你只要记住这几种列表与值列表的“引用”与“定义”的方式与区别,以及在何种情形(这里是度量与计算列里的区别)下使用何种行为方式。内部的机制暂时不管。
1)= Sales[销售额] -- 这是第一种:最常用的引用列表方式。它在计算列里可被引用,而在度量中不能直接引用。
2)为了使其能工作,你可以对该列进行聚合运算。因此,在度量中正确的定义应该是:
= SUM ( Sales[销售额] ) -- 这是第二种:最常用的值列表计算的定义方式。这时候的列表已不是元列表,而是由SUM函数定义的值列表结果,我们称它这时不再具备显式列表性质。
3)由于公式计算需要,往往需要第 2)项里的值列表方式结果。也就是说,我们即想让它不失去值列表的行行为特征,又要它像一个列表一样能重新被“引用”。
这时候,唯一能做这件事的只有CALCULATE(CALCULATETABLE的参数是列表)。这就是CALCULATE的显式化引用列表的功能:CALCULATE能将值列表转换为等值的列表--即值列表筛选转换为等值的列表筛选,值列表计算转换为等值的列表计算(否则你无法理解,一个包含了很多行的值列表为什么公式引擎只执行一次也能得出正确的结果)。我们将第二个公式再次定义为:
= CALCULATE(SUM ( Sales[销售额] ) ) --即包含“引用”(显式化)列表,又包含隐式行行为的值列表的计算式。
公式中, SUM ( Sales[销售额] 的值列表结果(行的行为),总是被CALCULATE监控、捕获--被显式为列表。这包括SUM ( Sales[销售额] 中每产生一个行的行为的结果(比如每计算一次)时,该结果都会被CALCULATE即时引用为列表,并作为“列表”提供给其他计算,以执行“列引用”。这就是通常理解的所谓行迭代或遍历(计算次数),因为这中间可能还包括一次整列的列值计算。所以,有时候计算结果会包含一个我们并不需要的所谓汇总小计(该结果并不受现有筛选条件限制--例如透视表的筛选元素)。如图:显式化列表处理后,公式正确运行。
相对于上一个使用:SUM ( Sales[销售额] ) 的虽然计算正确的公式(因为从该公式行为上,它缺少的只是一个值列表的行筛选,我们修正了它,从而获得正确的计算。但是,由SUMMARIZE定义的整个公式,要求该位置的参数应该是一个真正的计算式,因此,最后所示的CALCULATE(SUM ( Sales[销售额] ) ) 才是正解,它具备计算需要的显式列表筛选与隐式的行筛选。
如果你还记得的话,之前这样子的例子我们已经列举了好几个(比如求最近时期的当前余额的公式,使用的LASTNONBLANK函数的参数)……。它们有一个共同的特点,某一个参数需要的是:Expression(计算式)。
如果你想有一个能很快记住的、速成的一个结论,那就是:类似上面的Expression参数,使用CALCULATE( )显式化处理,总是一个不错的选择。
6、其实不想拿透视表的筛选来分析,它与DAX查询在内部的行为并不相同(后续谈到列表查询时再说)。
通常,我们知道,在透视表的每个单元格都会有不同的一个或多个列表筛选,这是由用户界面(如Excel中的数据透视表行、列以及切片器)隐式定义的,这里也提到隐式,这并不矛盾。因为,应用于数据透视表的任何用户界面元素,包括Power BI Desktop、Powerpivot的列表筛选,都并不直接影响行(值列表)筛选(隐式行行为总是相同的、或者说是被共享的)。
也就是说,你每次改变的是计算列表所在的列表集的范围(列值数量的减少),而行筛选的行为不变。所以,你完全可以使用CALCULATE通过某些DAX表达式进行显式定义,其行为是一样的。(区别稍后再论)。
而且,任何列表筛选实际上是当前所在数据模型上的一组等效的列表筛选器。任何DAX表达式始终都在列表筛选的结果列表集中执行。如果列表筛选为空白,DAX表达式会在整个数据模型中运行。当列表筛选不为空时,它限制DAX表达式在被筛选后的数据模型中运行。
例如,我们在数据模型里的某个表里定义一个计算式,有时候会出现结果为空白的情况。这并不是公式错误,而是它计算的是当前数据模型表(还没有拖拽到透视表筛选,或还没有定义任何筛选),而当前数据模型里并不一定满足公式中定义的条件(筛选)。
如图,公式条件是:天数=366,它在数据模型的计算结果是空白。因为当前计算的值列表针对的是整个列的列值(时期天数并不是366,而是整个表的列值--一个更大的数)。要让它计算出一个结果值,你需要筛选出那些一年中为366天的年份,你可以在当前数据模型里的年份列里筛选出符合条件的年份,或者在透视表里做同样的筛选。
甚至,偶尔还会出现那些在数据模型里显示为错误的计算式,也并不一定是一个错误的公式的情况。原因也是:数据模型并不总是符合它的条件(这只会出现在定义的公式内部筛选中)。
进一步的理解,你需要使用值列表或列表筛选的特点。根据值列表与列表的特点:它们都是列表,本质上并没有区别,只是用它们代表列表的两种方式行为。我们说,一般地,把含整个列值的称为列表,否则,称为值列表。
那么,从行列值的角度看,一个包含整个列值的值列表也就是通常的列表,只有一个或多个结果的(一个或多个列值)的列表是值列表......。
前面的公式:COUNTROWS ( VALUES (产品[产品名称] ) )中,元列表是产品[产品名称] 列,对于VALUES来说,我们是“引用”该列,而对于COUNTROWS来说,我们是“定义”该列(使用了VALUES函数),引用的是该列的值列表(VALUES函数计算后的唯一值的结果:这时候它不再拥有该列的全部列值的数量,但该结果的数量--甚至有没有值,都已不是重点,重点是,它已经作为列表提供给COUNTROWS引用--或者说是COUNTROWS的“引用列表”)。
所以,作为强调:值列表与列表是可以不断进行角色变化的(比如行筛选与列表筛选转换),最后,就是一个值的“单元格”式的结果。
为什么会感觉如此的拗口?有什么办法?还得继续。
所以,最好不要完全依赖于数据模型表里的度量公式的结果,来检验公式是否正确。由于数据模型表里的计算针对整个列表,那么,凡是值列表的条件(比如定义的布尔值列表)计算,有可能为:空白或不显示。
如图,我们定义两个度量,一个是元度量[销售],一个是基于该元度量定义的布尔值(值列表)筛选下的度量。
(这也是目前为止,我们定义的第一个除元度量之外的稍复杂点的函数)。
它们在相同的数据模型表里的结果,以及给出筛选后的计算结果分别如下图所示:
如果将[客户lD]列筛选为空值,则[销售]度量结果为:空白,[计数_销售]度量结果仍然是2290不变(附图待补,不小心删掉了)。
直接在该数据模型里筛选,也可以在透视表里筛选,效果是一样的:其实际都是列表筛选。上述三个筛选下得出了不同的结果,最好的解释是:两个公式是在不同的数据列表集里运行,相互间是独立的计算。
因此,你可以认为:每个DAX公式,都是在各自引用的列表、以及定义的值列表所创建的列表集(关系列表集)中工作。
官方将DAX表达式应用的任何筛选器都统称为查询列表。实际上,这些筛选器的效果几乎是相同的(不需要研究这种差异),因此,我们简单地将列表筛选定义为限制DAX表达式计算(通常是度量值)的一组筛选器,而不管它们是如何生成的。
例如,透视表里的所有筛选因素的筛选,将造成相同度量值出现不同结果。这与通过使用CALCULATE显式列表筛选具有相同的计算效果。
我们前面说CaⅠculate的最简单理解为:定义一个结果为值列表的关系列表集。问题是,我们如何将这些知识对应于DAX的两个列表状态,以构建DAX公式?
关于DAX列表的理解,到这里己基本完成,接下来将是研究如何使用它们构建公式。重点是完成两个关键:筛选与计算。
未完待续。