从数据到信息
从信息到洞察

理解循环依赖

在设计数据模型时,有一个涉及复杂内容的主题需要注意,即公式中的循环依赖关系。在本节中,你将学习什么是循环依赖以及如何在模型中避开循环依赖。

线性依赖

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

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 暂时不支持表行为属性设置。另外,虽然设置表行为属性是个不错的主意,但并不意味着你需要向所有表都添加一个行标识符,事实上如果这么做,那些未被使用的列会浪费掉宝贵的内存。如果该列已在数据库中且需要用于计算时,再设置行标识符属性,否则跳过此步骤。

6
说点什么

avatar
1000
 
鼓掌微笑开心憧憬爱你色并不觉得吃瓜doge二哈喵喵思考笑哭捂脸悲伤大哭抓狂汗偷笑打脸捂眼黑线问号晕拜拜闭嘴衰咒骂ok作揖
2 评论数
6 被回复的评论
3 订阅评论的人数
 
查看最近回复
查看最热评论
  订阅本文评论  
最新 最旧 得票最多
提醒
liu
游客
liu

”检查产品表中对于Product key、Product Name、Unit Cost和Unit Price具有相同值的所有行,将它们的Unit Price值相加“,这一点似乎有误,存在两行一模一样的数据时,所在行并没有汇总全部,还是等所在行的值。