问题背景
本问题来自我的课程:《Power BI 自动化用户运营分析》 中的学生提问。
课程中给出了多种进行新用户数相关的计算方法,发现一个问题:
上图给出了 和日期有关的DAX使用 在不同算法(不妨依次称为1,2,3,4)中的实现。
不必深究算法,这里想强调的是,前两种算法或使用ALL(日期表),或使用FILTER(ALL(日期表))的方式忽略了日期表的影响。而在算法3,4中会发现,即使去掉ALL(日期表),计算依然是正确的,这是为什么?
等价问题
当你觉得问题复杂时,找到最小核心点,分而治之。
解决上述的问题的最佳方式不是去解决它,而是观察,测试去发现问题的范围,进而找到等价的问题,并去研究等价问题。
建立等价问题
如果觉得上述背景复杂,那我们将问题做等价简化,有A,B两个表且满足下面的关系:
A,B的数据以及度量值如下:
A表:
B表:
度量值:
related count in B =
VAR current_id = MIN( A[id] )
RETURN CALCULATE( COUNTROWS( B ) , A[id]< current_id )
诡异的结果
解释:由于计算上下文(本处是筛选上下文)的存在,在第2行中,CALCULATE中存在筛选器A[item]="b",此时在CALCULATE中出现的筛选A[id] < current_id
,按照CALCULATE的执行原则,因为这是两个不同的字段,因此他们将共同起作用,而显而易见的是,他们共同的作用下,对表A的筛选应该是空集,而实际却并非如此。
破解诡异
将A表[id]字段的数据类型改为INTEGER,看看效果。 如下:
正如刚刚分析的,这才是应该呈现的效果。问题来了:为什么改成INTEGER类型就正确了,符合我们对CALCULATE运算逻辑的理解:筛选上下文是当前的A[item]而CALCULATE内部添加了筛选器A[id] < current_id
,它们共同的作用的确应该为空。
探究DAX引擎的实际工作
我们最后给出的任何答案,如果想确保正确,那一定是看到了DAX引擎的实际执行,为了解决这个谜题,我们设计了4次对比。
首先,我们猜测这是与取消筛选有关的,可能会涉及到ALL,于是我们的度量值可以是两种:
第一种
related count in B =
VAR current_id = MIN( A[id] )
RETURN CALCULATE( COUNTROWS( B ) , A[id]< current_id )
第二种
related count in B =
VAR current_id = MIN( A[id] )
RETURN CALCULATE( COUNTROWS( B ) , A[id]< current_id , ALL(A) )
注意在第二种度量值中使用了ALL(A)
。
对比试验设计
试验 | 内容 |
---|---|
试验 1 | 数据类型:integer;度量值:不加ALL版本 |
试验 2 | 数据类型:date;度量值:不加ALL版本 |
试验 3 | 数据类型:integer;度量值:加ALL版本 |
试验 4 | 数据类型:date;度量值:加ALL版本 |
DAX Studio查看物理查询
如果说要理解程序运作的CPU逻辑是查看汇编或二进制代码的话,那么理解DAX执行的最底层逻辑就是查看DAX引擎执行的物理查询。(而不是逻辑查询)
按照试验的对比设计,以及常规的想法,我们应该预期试验1和试验2除了在数据类型上不同,其他应该都一致;试验2和试验4因为有ALL的加入或不加入,应该有较大差别;诸如此类。
在DAX Studio中,按照这个方式来运行:
限于篇幅,不再给出每个试验的物理查询文本,但将运行好的物理查询文本拷贝出来进行对比,我们对不同试验,得到以下对比结果:
试验 | 物理查询行数 | related count in B 结果 |
---|---|---|
试验 1 integer 不加ALL | 23 | 空 |
试验 2 date 不加ALL | 40 | 非空 |
试验 3 integer 加ALL | 40 | 非空 |
试验 4 date 加ALL | 40 | 非空 |
可以逐个将四个试验的文本依次详细对比:
试验 1 对比 试验 2
对比显示:
- 不仅在数据类型处有区别
- 试验2足足比试验1多出了更多的物理查询内容
虽然DAX表达式完全一样,仅仅是数据类型的不同,物理查询居然相差这么大。
试验 2 对比 试验 3
对比显示:仅在数据类型处有区别。
DAX表达式有ALL(A)
的明显不同,而物理查询却仅仅是数据类型的不同。
试验 3 对比 试验 4
对比显示:仅在数据类型处有区别。
DAX表达式有ALL(A)
的明显不同,而物理查询却仅仅是数据类型的不同。
试验 2 对比 试验 4
对比显示:物理查询完全一样。
DAX表达式有ALL(A)
的明显不同,而物理查询却完全一致。
水落石出
通过4个试验以及4组试验对比,我们完全确认: 试验 2 date 不加ALL版本的度量值中,DAX引擎在计算时实际自动添加了ALL(A)。
总结
结合更多试验,将发现以下重要事实:若CALCULATE的筛选器中存在某表的日期字段,如:A[date],且该字段恰为主键并已与其他表建立关系,DAX引擎将在此CALCULATE的筛选器中自动加入ALL(A)。
值得特别注意的是:
- 该表不一定是日期表,只要该字段是日期字段即可。
- 一定是该字段已经作为主键并与其他表建立了关系。
由此,原问题得到了完整的解答。
补充
什么是主键
如果从A[C1]上连接一条线出来,连到B[C1]身上,则A[C1]叫做主键。也就是连线出发的字段。
为什么会有这么诡异的问题
这是Power BI Desktop为了实现时间智能函数而设计的内部机制。例如:
CALCULATE( [销售额] , DATEADD( 日期表[日期] , 1 , MONTH ) )
将在该机制下,不需要考虑加入ALL(日期表)
而得以正确运行。
更多参考:
- 彻底搞懂时间智能
- Time Intelligence in Power BI Desktop