上一篇找到了触发 SUMMARIZECOLUMNS
bug 的充分条件,这一篇尝试探讨修正理论对其进行解释,在这一篇中,我以为找到了一个合理的理论假设,但随后一个新的发现给了我当头一棒,这一篇应该是本系列的最后一篇,我必须直面现实了。
SUMMARIZECOLUMNS 的疑似 bug(1)引言
SUMMARIZECOLUMNS 的疑似 bug(2)SUMX 在总计行
SUMMARIZECOLUMNS 的疑似 bug(3)总计行是否关键点
SUMMARIZECOLUMNS 的疑似 bug(4)验证触发条件
SUMMARIZECOLUMNS 的疑似 bug(5)最终结论
回顾一下触发条件,同时满足以下3种情况:
<groupby>
参数上包含 [关键列]- 使用的固化筛选器中包含有 [关键列] ,且还有其他列(也许需要来自同一个表,待验证)
- 度量值中存在对 [关键列] 进行迭代的迭代器
上述情况转换到 PBI 的报表中则是以下3种等效情况:
- 矩阵(或表格)的行标题使用【关键列】
- 报表中存在切片器,切片器包含多个列构成层级,并包含有【关键列】
- 度量值中存在对【关键列】进行迭代的迭代器
在 DAX 中,迭代器函数有很多种,以数值为结果的有 SUMX、MAXX、MINX、AVERAGEX、RANKX 等,以文本为结果的有 CONCATENATEX,以表为结果的有 FILTER、ADDCOLUMNS、TOPN等,在实践对比中,发现并不是所有迭代器并会触发 SUMMARIZECOLUMNS 的bug。
经实验,可以触发 bug 的迭代器函数有部分数值类迭代器 SUMX、MAXX、MINX、AVERAGEX,此外还有 FILTER、
1 数值运算类迭代器函数的计算结果
SUMX、MAXX、MINX、AVERAGEX 是常用的数值运算类迭代器,我们根据上一篇中总结出的3个条件,仿照之前的 [SumxBug] 度量值,写出如下度量值
Maxx = MAXX(VALUES(Dates[Year]), [SumAmount])
Minx = MINX(VALUES(Dates[Year]), [SumAmount])
Averagex = AVERAGEX(VALUES(Dates[Year]), [SumAmount])
它们在 PBI 报表中的计算结果如下图所示,红框处标记了上述几个度量值的计算结果。同时,图中也添加了使用 KEEPFILTER 进行修正过的度量值计算出来的正确结果,用做比较。
按照 DAX 理论,报表中红框处的计算结果应该如下图所示:
按照理论分析,Year = 2019 行全部度量值计算结果应为 3,Year = 2020 行全部度量值计算结果应为 47,与实际计算出的结果不一致。
总计行不符合触发 Bug 条件中的条件1,计算结果与理论相符。
2 其他迭代器
除了上述几个数值类迭代器,FILTER 也可以触发 bug :
我尝试了 CONCATENATEX、RANKX、TOPN、ADDCOLUMNS、GENERATE、SUMMARIZE 这些可以产生行上下文的函数,都没有出现 Bug。
3 解释 SUMMARIZECOLUMNS 出现 BUG 的假设理论
为了解释SUMMARIZECOLUMNS
的实际计算结果,我们大胆地作一些假设,先凑出能计算出实际结果的中间过程来,再想办法看能否调和这个解释。
只需修改一处,将迭代对象 VALUES(Dates[Year])
的计算上下文强行从 筛选器C
更换到 筛选器A
作用下,便能推出实际计算结果。
与报表实际计算得到的结果完全一致
把刚才的假设详细描述下,在特定条件下,度量值中的顶层(数值运算类)迭代函数在计算其一参迭代对象时,其计算上下文仅受切片器影响,不受行标题影响。 特定条件即本篇开头提到的触发条件。
该假设也能够很好地解释为什么需要这个触发条件:
- 层级切片器中包含有 [关键列] (以及其他来自同一表的其他列),这样才能触发 Auto-Exist 机制,使后续的筛选器交互结果行数缩减。
- 矩阵(或表格)行标题包含 [关键列], 在理论上,行标题构成的 筛选器B 与切片器构成的 筛选器A 在 Auto-Exist 机制激活的情况下,交互产生出行数被缩减的 筛选器C, 并影响度量值中迭代器的一参计算,产生一个理论结果作为对比
- 度量值中存在对 [关键列] 进行迭代的迭代器,当 Bug 发生时,迭代对象的计算发生在 筛选器A(切片器) 而不是 筛选器C 中,最终产生与理论不符的计算结果。
3 个条件缺一不可,由此可见,该假设在逻辑上是自洽的,在未找到能够推翻该假设的实例之前,姑且认为它是正确的。
这样也就可以解答开篇条件2括号里的待验证内容了:
使用的固化筛选器中包含有 [关键列],且还有其他列(也许需要来自同一个表,待验证)
是的,固化筛选器,或者说切片器中必须存在来自同一个表的其他列,这样才能触发 Auto-Exist 机制,并诱发 SUMMARIZECOLUMNS
的 Bug。
验证一下,修改切片器,删除 [Month Num] 添加 DFact[Date],现在切片器使用的列一个来自 Dates,另一个来自 DFact,两列不再是来自同一张表,Auto-Exist 机制不介入。勾选 2019年1月、2月和 2020年11月、12月的日期,观察计算结果,Bug 未发生。
最后,把解释 SUMMARIZECOLUMNS
出现 Bug 的假设理论再写一遍:
在特定条件下,度量值中的顶层(数值运算类)迭代函数在计算其一参迭代对象时,其计算上下文仅受切片器影响,不受行标题影响。
这个假设理论显然很难让人接受,因为它暗示了在同一个计算环境中,迭代器的迭代对象在计算时却发生在不同的计算上下文中!
但以此假设出发,下面两个度量值得到的计算结果就不意外了。
Validate_MaxYear = MAXX(VALUES(Dates[Year]), CALCULATE(max(Dates[Year])))
Validate_MinYear = MINX(VALUES(Dates[Year]), CALCULATE(max(Dates[Year])))
4 这个假设理论真的能用吗
答案很令人失望,并不能用,因为很快我就找到一个反例。
下面这两个度量值有差别吗?
SumxBug = SUMX(VALUES(Dates[Year]),[SumAmount])
SumxBug2 = SUMX(VALUES(Dates[Year]),[SumAmount] + 0)
从代码来看,后者SumxBug2
只是在 SUMX 的迭代计算参数 [SumAmount] 加了一个 0 ,依然符合触发 SUMMARIZECOLUMNS
bug 的条件,按照假设理论,它的计算结果应该与前者 SumxBug
一致。
但是实际情况,SumxBug2
计算出了正确的结果。
SumxBug2
的物理查询计划完整的流程结构图如下:
对上图作一些简化,并按照它的结构把底层计算过程梳理如下:
上图中 L5
是关键(红框标记处),也是令我困惑的地方。
官方没有提供关于物理查询计划的详细资料,大多数时候,我要结合计算结果、物理查询计划代码去猜测哪些操作符具体得到了什么结果。L5 的代码是:
Spool_Iterator: IterPhyOp LogOp=Scan_Vertipaq IterCols(0, 3)(‘Dates’[Year], ‘Dates’[Year]) #Records=2 #KeyCols=10 #ValueCols=0
上面的粗体代码表明 ,L5 包含有 Year(0) 和 Year(3),且只有两行数据。根据最终计算结果和物理查询计划代码中展示的操作符倒推,L5 的数据表必须是下面这样才能得到报表中的计算结果:
根据其他操作符的代码可推知,Year(0)
来自 TreatAs,而 Year(3)
来自 VQ2。L5 的数据来自 L6←L7←VQ1,而 VQ1 查询结果只有 Year(0)={2019,2020} 一列两行,L5 这里 Year(3)
从哪来的,为什么其值正好与 Year(0)
的值正好相同,我完全不能从物理查询计划的代码中找到任何依据。
这种虽然知道这里必须是这样的结果,但从查询计划代码中完全找不出线索的情况,让我感到很是难受。而且 L5 是使得最终计算结果正确的关键因素,这是它与另一个度量值 SumxBug
底层执行过程(详见这篇文章)的不同之处,正是由于 L5 这两行数据,使得在下一步从 L14 中进行查询操作时,准确地匹配到了正确的数据。
将 SumxBug
与 SumxBug2
两者的底层计算过程放一起作比较:
5 最终结论
距离上一篇文章的发布已经有一段时间,在这段时间里我以为找到了一种 DAX 理论层面的假设解释来说明 SUMMARIZECOLUMNS
为什么会计算出出乎意料的结果,寄希望于可以用这个假设解释来帮助预测该条件下的计算结果,随后在实践测试过程中最终发现通过上面例子中一个极其简单的改动,只是在原来的度量值中加上了一个 0,就使得结果完全不同,DAX 底层对两个度量值的处理方式不同,而这种差异完全无法从 DAX 理论层面去调和。
这也就宣告了之前努力找到的假设解释完全不能用于预测计算结果,虽然也许可以通过继续添加限定条件使这个假设在较狭小的范围内适用,但在写本系列文章的这段时间里,反复阅读和对比各种物理查询计划,我认识到一个事实:由于 DAX 底层优化器的存在,引擎并不会按照某种明显的规则去理解 DAX 代码。
现在我最大的感受就像三体里的杨冬,“物理学不存在了”,DAX 的底层就像一个有脾气的聪明人,它有自己对代码的认识,比如是否 +0 对它来说,就是要按照不同的方式去执行,虽然它通过展示物理查询计划让你可以了解它是怎么计算的,但你无法摸清它的脾气,也就无法找到某种规则用来提前判断它会怎么去看待和执行其他 DAX 代码。
可以猜测,开发组对 SUMMARIZECOLUMNS 进行了大量的优化,尤其是计算数值性迭代器的场景。但某些优化措施造成了新的 bug,所以 Marco Russo 说这是 a bug on top of another bug,背后的原因没什么好说的,使用 KEEPFILTERS 包裹迭代器的迭代对象就好。
开发组仍在不断完善和改进 DAX 底层引擎,希望 SUMMARIZECOLUMNS
的 bug 早日被彻底修复吧。