在设计数据模型时,有一个涉及复杂内容的主题需要注意,即公式中的循环依赖关系。在本节中,你将学习什么是循环依赖以及如何在模型中避开循环依赖。
线性依赖
在讨论循环依赖之前,有必要先讨论简单的线性依赖关系。让我们看一个示例,其中包含以下计算列:
- Product[Profit] = Product[Unit Price] - Product[Unit Cost]
复制代码
新的计算列依赖于同一表的另外两列。在这种情况下,我们说利润列取决于单位价格和单位成本。然后,你可以用以下公式创建一个名为 ProfitPct 的新列:
- Product[ProfitPct] = Product[Profit] / Product[Unit Price]
复制代码
很明显, ProfitPct 的结果取决于利润和单价。因此,当 DAX 计算这两列时,它知道只有在计算利润之后才能计算 ProfitPct。否则就无法计算出 ProfitPct 公式的有效值。
线性依赖通常不需要担心,DAX 会在数据模型刷新期间检测到计算列之间的正确计值顺序。在一个有许多计算列的普通数据模型中,列之间的依赖性形成了一个复杂的关系图,但引擎可以很好地处理这个问题。
当这个关系图中出现循环引用时就会发生循环依赖。例如,如果你试图按以下方式修改 Profit 公式,循环依赖就会发生:
- Product[Profit] = Product[ProfitPct] * Product[Unit Price]
复制代码
因为 ProfitPct 依赖于 Profit,,而在这个新公式中,Profit 依赖于 ProfitPct, DAX 拒绝修改公式, 并显示错误 “检测到循环依赖关系”。
到目前为止,你已经从公式的角度了解了什么是循环依赖;也就是说,不需要注意表中的数据,你在查看表达式时已经发现了依赖项的存在。不过,通过 CALCULATE 还可以产生一种更微妙、更复杂的依赖关系。让我们从产品表的子集开始,用一个示例来展示这个场景,请注意在本例中我们只加载了产品表,从模型中删除了所有其他表,以便使场景更加明显。
产品表的这个子集对于理解循环依赖关系很有用
我们的兴趣在于了解使用了 CALCULATE 函数的新计算列的依赖关系列表,如下所示:
- Product[SumOfUnitPrice] = CALCULATE ( SUM ( Product[Unit Price] ) )
复制代码
乍一看,这一列似乎只取决于单价,因为这是公式中使用的唯一一列。不过,请注意我们使用了 CALCULATE 将当前行上下文转换为筛选上下文。因为我们没有定义与其他表的关系,也没有为它设置主键,所以当 CALCULATE 进行上下文转换时,它会筛选表的所有列。如果我们扩展 CALCULATE 调用的含义,公式实际上说的是:
检查产品表中对于 Product key、Product Name、Unit Cost 和 Unit Price 具有相同值的所有行,将它们的 Unit Price 值相加。
如果你以这种方式阅读公式,可以清楚地发现,代码依赖于产品表的所有列,因为新引入的筛选上下文将筛选表的所有列。你可以在图中看到结果。
在这里可以看到带有 SumOfUnitPrice 计算列的产品表
你可以尝试使用相同的公式在同一个表中定义一个新的计算列。比如使用以下公式定义 NewSumOfUnitPrice,这个公式与前一个公式相同。
- Product[NewSumOfUnitPrice] = CALCULATE ( SUM ( Product[Unit Price] ) )
复制代码
观察循环依赖现象
令人惊讶的是, 此时 DAX 提示了一个错误, 称它检测到循环依赖关系。这很奇怪, 因为它在相同的的公式中检测到了以前没有发现的循环依赖关系。这其中的原因在于表的列数发生了变化。假设我们能够将 NewSumOfUnitPrice 添加到表中, 这两个公式将分别具有以下含义:
- SumOfListPrice 对产品表中的以下 5 列有相同值的所有行的单价求和,这 5 列分别为 ProductKey, Product Name, Unit Cost, Unit Price 和 NewSumOfListPrice。
- NewSumOfListPrice 对产品表中的以下 5 列有相同值的所有行的单价求和,这 5 列分别为 ProductKey, Product Name, Unit Cost, Unit Price 和 SumOfListPrice。
任何添加到数据模型中的计算列都成为由 CALCULATE 引入的筛选上下文的一部分,因此所有计算列都成为依赖列表的一部分。阅读上述定义,两个公式之间存在着明确地循环依赖,这恰恰是 DAX 拒绝创建 NewSumOfListPrice 列的原因。
理解该错误并不容易,但是找到解决方案很简单,即使这个方案看上去不是很直观:
如果表没有主键,任何包含 CALCULATE(或对任何度量值的调用,这些调用都自动添加一个 CALCULATE)的计算列都会创建对表的所有列(也包括计算列)的依赖关系。如果表中有一个行标识符(用数据库术语来说是主键),情况将会有所不同。当表中含有这个作为行标识符的列时,包含 CALCULATE 的所有列仅依赖于该行标识符,从而将依赖列表的数目降低到单列。
在产品表中,有一列可以唯一地标识出每一行,即 ProductKey。要将 ProductKey 标记为行标识符(主键),你有两种选择:
- 可以使用 ProductKey 作为目标列,来创建任意表和产品表之间的关系。执行此操作将确保 ProductKey 列可以唯一区分产品表。
- 你可以使用表行为属性设置将 ProductKey 列设为行标识符。
以上任一操作都会使得 DAX 知道该表中存在行标识符,避免了在定义 NewSumOfListPrice 列的同时遇到循环依赖,因为使用了 CALCULATE 的两个计算列都只依赖于新设置的主键列。
除设置日期表外,目前 Power BI Desktop 暂时不支持表行为属性设置。另外,虽然设置表行为属性是个不错的主意,但并不意味着你需要向所有表都添加一个行标识符,事实上如果这么做,那些未被使用的列会浪费掉宝贵的内存。如果该列已在数据库中且需要用于计算时,再设置行标识符属性,否则跳过此步骤。
例外情况
通过上面的介绍,我们已经知道 DAX 中的循环依赖绝大部分都是由于 CALCULATE 在执行上下文转换时产生的,但上下文转换并非发生始终发生在所有情境中,在某些特殊情况下,DAX 引擎会避免发生这种转换。观察下面这个案例,c1 列是 Table 表的原始列,cc1 和 cc2 是计算列,这里并没有提示循环依赖。
按照我们之前的理解,上下文转换会使得 cc1 和 cc2 互相依赖,而实际上, DAX 引擎会根据公式中使用的函数,判断计算的意图,在某些情况下不执行上下文转换。比如这里的 col1 和 col2,引擎知道你要通过 VALUES 获取当前行的值,而此计算不需要依赖其他列的结果,所以上下文转换不会发生。
需要注意的是,这种由引擎执行的内部优化并没有明确的规则,而且规则可能会不断更新,所以不建议你依靠这种内部优化行为,而是始终使用 ALL 或者 ALLEXCEPT 忽略相应的筛选器。 |