本篇文章将介绍CALCULATE与CALCULATETABLE这两个函数的完整计值流程,这两个函数是DAX中最强大也是最重要的函数,它让我们拥有了任意修改筛选上下文的能力。但是,通常某个东西的功能越强大,也意味着这个东西的使用与学习越困难。对CALCULATE函数的学习与使用可以说是贯穿了DAX的整个知识体系,只有把DAX的所有原理都弄清楚了,才有可能彻底掌握CALCULATE函数。
CALCULATE函数就像航空母舰,其他的各种函数与原理就像战斗机与护航舰队,加载了所有战斗装备的航空母舰自然是所向披靡,但哪怕是裸奔的航空母舰,也是同样强大的。在之前的文章中,我们已经了解了CALCULATE函数的基本语法与初步的计值流程,并对筛选上下文有了深刻的理解,换句话说就是我们已经了解了这艘航空母舰的船体结构。那么在本篇文章,我们就来学习这艘航空母舰的操作方式,先学会如何驾驭裸奔的航空母舰,后续再逐渐学习其它知识,慢慢地给这艘航空母舰加载战斗装备。当这艘航空母舰羽翼丰满、所向披靡之时,就代表着你的DAX水平已臻化境。
之前的文章中已经说过,CALCULATETABLE函数与CALCULATE函数并没有太大的区别,唯一区别就是CALCULATE函数的第一参数要返回标量值,而CALCULATETABLE函数的第一参数要返回表,除此之外的计值流程都是一致的。因此,下面同样是重点介绍CALCULATE函数,CALCULATETABLE函数自行类比一下就好了。
CALCULATE函数的总体计值流程为:先在外部计值环境下计算内部筛选器参数,然后内部筛选器修改外部计值环境,最后计算器参数(第一参数)在修改后的计值环境里计值。其中,内部筛选器参数在CALCULATE函数的整个计值流程中是最先计算的,但却是最后应用的。此外,CALCULATE函数的各个内部筛选器之间互相独立,在计算时互不影响。
更具体一点的描述为:先在外部计值环境下计算内部筛选器参数,但并不应用,然后检查是否存在行上下文,若存在则进行行上下文转换,然后检查是否存在筛选调节器,若存在则执行筛选调节器,之后再应用第一步计算出的内部筛选器,最后计算器参数在修改后的计值环境里计值。
此外,有很多人对内部筛选器先计算后应用这一点不太理解,所以我再稍微解释一下。在之前介绍筛选器的表现形式时,我们已经了解到筛选器是用元组来表示的,元组与表很相似,表可以转化成元组,但元组并不能转化成表。那么对CALCULATE的内部筛选器参数来说,它需要的其实是一个筛选器,但我们却不能直接提供一个筛选器给它,所以只能通过提供一张表并使这张表转化成筛选器元组的方式来间接提供一个筛选器给内部筛选器参数。所以,这就类似于函数嵌套一样,要想执行外层函数,就必须要先得到内层函数的结果,而内部筛选器参数就类似于内层函数,只有等表转化成筛选器元组后,CALCULATE函数才正式开始工作。因此严格来说,CALCULATE函数的计值流程中,第一步并不是先计算内部筛选器参数,所以这就解释了为什么内部筛选器参数会先计算后应用,这并不矛盾。而在一般情况下,先在外部计值环境下计算内部筛选器参数这一步骤是否属于CALCULATE函数的计值流程的界限其实没有那么严格,所以通常也把先在外部计值环境下计算内部筛选器参数这一步骤纳入到CALCULATE函数的计值流程里,方便记忆。
下面给出我自己所理解的CALCULATE函数的完整计值流程:
- 先在外部计值环境里计算各个内部筛选器参数,但并不应用
- 若外部计值环境中存在行上下文则将行上下文转换成筛选上下文,并与外部计值环境中的筛选上下文交互,得到环境1。若不存在行上下文,则环境1等于外部计值环境中的筛选上下文
- 若有筛选调节器,那么先应用筛选调节器,对环境1进行修改,得到环境2。若不存在筛选调节器,则环境2等于环境1
- 将步骤1所计算完成的各个内部筛选器相交,得出环境3(注意:环境3并不是在环境1或环境2的基础上修改得到)
- 环境3再与环境2交互得出最终计值环境,其中环境3后执行
- 计算器参数在最终计值环境里进行计值
- 上述的交互指的是:相同列的筛选器是覆盖行为,非相同列的筛选器是相交行为
上面给出的是完整体的CALCULATE函数的计值流程,当省略了内部筛选器参数或只有调节器而没有内部筛选器,亦或者是其他情况时,需要灵活删减步骤。例如,对一个省略了内部筛选器参数的CALCULATE函数而言,第一步与第三步明显是不需要的,其次,第四步也是不需要的,而第五步也是可以忽略的,因为已经不存在环境3了,所以最终计值环境就是环境1。此外,相同列覆盖,非相同列相交,这种筛选器的交互行为是默认的,但当使用了筛选调节器时很可能就会更改这种默认的交互行为,例如KEEPFILTERS函数就会强制交互方式为相交,这一点需要注意。最后,再注意一下第四步,在第四步里是把所有内部筛选器相交,哪怕是相同列上的筛选器也是相交,这一点需要重点注意,并不是我写错了,千万不要搞混了。
下面来看一些帮助理解的例子,使用到的数据与数据模型如下图:
先来看第一个例子,使用到的度量值表达式如下:
销售金额 = SUM('订单表'[销售额])
度量值 1 = CALCULATE([销售金额],'产品表'[产品名称] IN {"U盘","充电宝","耳机"},'产品表'[产品名称]="U盘")
首先,创建一个矩阵视觉对象,把产品表中的产品名称列放入行字段,并把销售金额与度量值1放入值字段,结果如下图所示:
在这个例子中,矩阵行标签提供的筛选器与两个内部筛选器都是筛选的产品表上的产品名称列,这对初学者来说应该是比较复杂的情况了,那下面就以度量值1在耳机行标签时的结果147为例,逐步介绍CALCULATE函数的完整计值流程。
- 度量值1的外部计值环境存在一个由行标签提供的筛选器,即:’产品表'[产品名称] = "耳机"
-
先在外部计值环境下计算CALCULATE的各个内部筛选器参数,得到两个内部筛选器如下:
'产品表'[产品名称] IN {"U盘","充电宝","耳机"} '产品表'[产品名称] = "U盘"
- 由于不存在行上下文,所以环境1等于外部筛选上下文,即:’产品表'[产品名称] = "耳机"
- 由于CALCULATE函数不包含任何筛选调节器,因此环境2等于环境1,即:’产品表'[产品名称] = "耳机"
- 将CALCULATE函数的各个内部筛选器相交,即把第二步中的两个筛选器相交,得到环境3,可表示成如下:
'产品表'[产品名称] = "U盘" && '产品表'[产品名称] = "U盘" || '产品表'[产品名称] = "充电宝" && '产品表'[产品名称] = "U盘" || '产品表'[产品名称] = "耳机" && '产品表'[产品名称] = "U盘"
- 环境3与环境2交互,环境3是一个固化筛选器,环境2是一个标准筛选器,但它们筛选的列均一致,因此它们的交互方式为覆盖,且环境3后执行,故用环境3去覆盖环境2,得到最终计值环境如下:
'产品表'[产品名称] = "U盘" && '产品表'[产品名称] = "U盘" || '产品表'[产品名称] = "充电宝" && '产品表'[产品名称] = "U盘" || '产品表'[产品名称] = "耳机" && '产品表'[产品名称] = "U盘"
- 最终计值环境所生成的筛选上下文为:
- CALCULATE函数的计算器参数在上述筛选上下文里计值,将把销售额列上的可见值汇总起来,最终得到汇总值:147
下面再来看第二个例子,使用到的度量值表达式如下:
销售金额 = SUM('订单表'[销售额])
度量值 2 = CALCULATE([销售金额],ALL('产品表'[产品名称]))
把度量值2继续放入到上述矩阵中的值字段,得到的结果如下图所示:
由于这里用到的ALL函数还没有介绍,因此我先简单介绍一下这个ALL函数的作用。在度量值2里,ALL函数被用作筛选调节器,作用就是移除其参数所指定的列上的筛选器,即当它工作时,它会移除产品表的产品名称列上的筛选器。关于ALL函数的其他内容将在后续文章中介绍,现在我们只要知道这里的ALL函数是一个筛选调节器以及它会移除产品名称列上的筛选器这两点就够了。
下面以度量值2在耳机行标签时的结果1642为例,逐步介绍度量值2的完整计值流程。
- 度量值2的外部计值环境存在一个由行标签提供的筛选器,即:’产品表'[产品名称] = "耳机"
- 先在外部计值环境下计算CALCULATE的各个内部筛选器参数,得到一个筛选调节器,即:ALL(‘产品表'[产品名称])
- 由于不存在行上下文,所以环境1等于外部筛选上下文,即:’产品表'[产品名称] = "耳机"
- 由于存在筛选调节器,故先应用筛选调节器对环境1进行修改,而ALL函数在这里的作用是移除产品名称列上的筛选器,使得环境1里的唯一一个筛选器被移除,得到环境2,而环境2不存在任何筛选器
- 由于内部筛选器参数只有筛选调节器,而不存在普通的内部筛选器,所以各个内部筛选器的相交行为省略,此时的环境3不存在任何筛选器
- 环境3与环境2交互,由于两个环境都不存在任何筛选器,因此最终计值环境也不存在任何筛选器,所以此时的可见数据为所有数据,即此时的筛选上下文为所有数据
- CALCULATE函数的计算器参数在上述筛选上下文里计值,将把销售额列上的所有值汇总起来,最终得到总计值1642
下面来看第三个例子,使用到的度量值表达式如下:
销售金额 = SUM('订单表'[销售额])
度量值 3 = CALCULATE([销售金额],ALL('产品表'[产品名称]),VALUES('产品表'[产品名称]))
把度量值3继续放入到上述矩阵中的值字段,得到的结果如下图所示:
同样的,先来介绍一下VALUES函数在这里的作用。VALUES函数是一个表函数,它返回其参数所指定列的可见值去重后的单列表,所以VALUES在度量值3里就是一个普通的内部筛选器。同时,我们观察一下度量值3的结果,将发现它与销售金额度量值的结果是一样的,就好像我们添加的那些内部筛选器参数都不起作用了一样,那它们真的是没有起作用吗?带着这个疑问,我们来看一下度量值3的计值流程。下面以度量值3在耳机行标签时的结果218为例,逐步介绍度量值3的完整计值流程。
- 度量值3的外部计值环境存在一个由行标签提供的筛选器,即:’产品表'[产品名称] = "耳机"
-
先在外部计值环境下计算CALCULATE的各个内部筛选器参数,得到一个筛选调节器与一个内部筛选器。在此时,由于产品表被外部筛选器(行标签提供的筛选器)筛选,使得产品名称列的可见值只有一个,即VALUES返回的结果与行标签显示的内容一致。故在这一步得到的内部筛选器与筛选调节器具体如下:
ALL('产品表'[产品名称]) // 筛选调节器 '产品表'[产品名称] = "耳机" // 由VALUES返回的表转化而来的筛选器
- 由于不存在行上下文,所以环境1等于外部筛选上下文,即:’产品表'[产品名称] = "耳机"
- 由于存在筛选调节器,故先应用筛选调节器对环境1进行修改,而ALL函数在这里的作用是移除产品名称列上的筛选器,使得环境1里的唯一一个筛选器被移除,得到环境2,而环境2不存在任何筛选器
- 将CALCULATE函数的各个内部筛选器相交,由于只有一个内部筛选器,所以相交行为可省略,得到环境3,即:’产品表'[产品名称] = "耳机"
- 环境3与环境2交互,由于环境2不存在任何筛选器,因此最终计值环境等于环境3,即:’产品表'[产品名称] = "耳机"
- 最终计值环境所生成的筛选上下文为:
- CALCULATE函数的计算器参数在上述筛选上下文里计值,将把销售额列上的可见值汇总起来,最终得到汇总值:218
我们来回顾一下度量值3的计值流程,首先,由于内部筛选器参数是先在外部计值环境里计值的,使得VALUES函数返回的结果与行标签显示的一致,其次,ALL函数作为筛选调节器是优先应用的,所以行标签提供的筛选器必定被移除,使得最终剩下一个由VALUES转化来的内部筛选器,而这个内部筛选器筛选的内容又与行标签提供的筛选器筛选的内容一致,所以度量值3的结果才会与销售金额度量值的结果一致。
说来话长,其实度量值3里的两个内部筛选器参数就干了两件事,先是移除了行标签提供的筛选器,随后则是增加了一个与行标签提供的筛选器筛选一样内容的筛选器,属于先移除后增加,互相抵消。
现在我们已经学会了如何驾驭CALCULATE这艘航空母舰,接下来就是多去实操练习,等熟悉之后,就是学习更多的知识去武装这艘航空母舰了。
DAX系列文章中涉及到的案例文件,均已上传到QQ群:344353627,若有需要,可自行加群获取。