[知识体系] 理解循环依赖

  [复制链接]
查看244478 | 回复179 | 2021-2-21 19:32:45 | 显示全部楼层 |阅读模式
在设计数据模型时,有一个涉及复杂内容的主题需要注意,即公式中的循环依赖关系。在本节中,你将学习什么是循环依赖以及如何在模型中避开循环依赖。

线性依赖


在讨论循环依赖之前,有必要先讨论简单的线性依赖关系。让我们看一个示例,其中包含以下计算列

  1. Product[Profit] = Product[Unit Price] - Product[Unit Cost]
复制代码


新的计算列依赖于同一表的另外两列。在这种情况下,我们说利润列取决于单位价格和单位成本。然后,你可以用以下公式创建一个名为 ProfitPct 的新列:

  1. Product[ProfitPct] = Product[Profit] / Product[Unit Price]
复制代码


很明显, ProfitPct 的结果取决于利润和单价。因此,当 DAX 计算这两列时,它知道只有在计算利润之后才能计算 ProfitPct。否则就无法计算出 ProfitPct 公式的有效值。


线性依赖通常不需要担心,DAX 会在数据模型刷新期间检测到计算列之间的正确计值顺序。在一个有许多计算列的普通数据模型中,列之间的依赖性形成了一个复杂的关系图,但引擎可以很好地处理这个问题。


当这个关系图中出现循环引用时就会发生循环依赖。例如,如果你试图按以下方式修改 Profit 公式,循环依赖就会发生:

  1. Product[Profit] = Product[ProfitPct] * Product[Unit Price]
复制代码


因为 ProfitPct 依赖于 Profit,,而在这个新公式中,Profit 依赖于 ProfitPct, DAX 拒绝修改公式, 并显示错误 “检测到循环依赖关系”。


到目前为止,你已经从公式的角度了解了什么是循环依赖;也就是说,不需要注意表中的数据,你在查看表达式时已经发现了依赖项的存在。不过,通过 CALCULATE 还可以产生一种更微妙、更复杂的依赖关系。让我们从产品表的子集开始,用一个示例来展示这个场景,请注意在本例中我们只加载了产品表,从模型中删除了所有其他表,以便使场景更加明显。


7144211936391.jpeg

产品表的这个子集对于理解循环依赖关系很有用


我们的兴趣在于了解使用了 CALCULATE 函数的新计算列的依赖关系列表,如下所示:

  1. Product[SumOfUnitPrice] = CALCULATE ( SUM ( Product[Unit Price] ) )
复制代码


乍一看,这一列似乎只取决于单价,因为这是公式中使用的唯一一列。不过,请注意我们使用了 CALCULATE 将当前行上下文转换筛选上下文。因为我们没有定义与其他表的关系,也没有为它设置主键,所以当 CALCULATE 进行上下文转换时,它会筛选表的所有列。如果我们扩展 CALCULATE 调用的含义,公式实际上说的是:


检查产品表中对于 Product key、Product Name、Unit Cost 和 Unit Price 具有相同值的所有行,将它们的 Unit Price 值相加。


如果你以这种方式阅读公式,可以清楚地发现,代码依赖于产品表的所有列,因为新引入的筛选上下文将筛选表的所有列。你可以在图中看到结果。


7144211936392.jpeg

在这里可以看到带有 SumOfUnitPrice 计算列的产品表


你可以尝试使用相同的公式在同一个表中定义一个新的计算列。比如使用以下公式定义 NewSumOfUnitPrice,这个公式与前一个公式相同。

  1. 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 是计算列,这里并没有提示循环依赖。



7144211936393.png



按照我们之前的理解,上下文转换会使得 cc1 和 cc2 互相依赖,而实际上, DAX 引擎会根据公式中使用的函数,判断计算的意图,在某些情况下不执行上下文转换。比如这里的 col1 和 col2,引擎知道你要通过 VALUES 获取当前行的值,而此计算不需要依赖其他列的结果,所以上下文转换不会发生。


需要注意的是,这种由引擎执行的内部优化并没有明确的规则,而且规则可能会不断更新,所以不建议你依靠这种内部优化行为,而是始终使用 ALL 或者 ALLEXCEPT 忽略相应的筛选器。
回复

使用道具 举报

lc3662018 | 2021-4-24 18:48:00 | 显示全部楼层
支持,楼下的跟上哈~
回复

使用道具 举报

孔隆 | 2021-9-9 21:53:35 | 显示全部楼层
看起来不错
回复

使用道具 举报

吉普2020 | 2021-9-20 06:53:00 | 显示全部楼层
珍爱生命,果断回帖。
回复

使用道具 举报

冬日暖阳 | 2021-10-19 17:17:42 来自手机 | 显示全部楼层
占位编辑
回复

使用道具 举报

db7382 | 2021-10-22 13:02:46 | 显示全部楼层
鼎力支持!!
回复

使用道具 举报

九筒 | 2021-10-30 07:42:58 | 显示全部楼层
回个帖子支持一下!
回复

使用道具 举报

liutao8888 | 2021-11-13 18:35:28 | 显示全部楼层
我也顶起出售广告位
回复

使用道具 举报

水泡鱼 | 2021-11-24 14:26:03 来自手机 | 显示全部楼层
前排支持下
回复

使用道具 举报

aaronliu | 2021-11-26 15:37:01 | 显示全部楼层
这个视频很不错,推荐一下
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则