人生没有彩排
每一天都是现场直播

33、循环依赖和主键

某个表达式在计算时如果引用了其他对象,则称之为依赖,有线性依赖与循环依赖两种。其中,循环依赖是不正常的依赖关系,当检测到循环依赖时会报错,而线性依赖则是正常的行为,并不会报错。

循环依赖指的是两个对象在计算时互相依赖,比如想要计算A就要先得到B的结果,但想计算B却要先得到A的结果,两者互相依赖陷入循环。

循环依赖是一个很常见且很容易出现错误的知识点,其按照类别可以分为公式依赖与空行依赖,本篇文章将详细介绍循环依赖的原理与解决方案。


线性依赖

在介绍循环依赖之前,先来看一下线性依赖。如下图所示,通过添加计算列来计算销售成本、销售金额、销售利润的场景中,销售成本的计算依赖于成本价格和销售数量,而销售金额的计算则依赖于零售单价和销售数量,最后的销售利润的计算则依赖于前面计算出的销售成本和销售金额。

很明显,想要计算销售利润,就要先计算销售金额和销售成本,否则无法得到销售利润的值。

像这种有先后顺序的依赖关系就称之为线性依赖,线性依赖是很正常的依赖关系,不需要我们进行额外的处理,因为DAX引擎会自动确定各个表达式之间的正确计值顺序。


循环依赖中的公式依赖

通常我们遇到的循环依赖一般都是公式依赖,公式依赖指的是在两个表达式的计算中互相涉及到了对方,导致在计算时陷入循环的一种场景。

原理:

比如上面介绍线性依赖时提到的场景中,如果将销售金额的表达式改为以下公式,那么循环依赖就会发生:

此时的销售金额的计算依赖于销售利润的结果,而想要得到销售利润却要先得到销售成本,所以两者互相依赖形成循环,因此会报循环依赖错误。

以上案例介绍的是最简单易懂的公式依赖的场景,它可以直接通过检查表达式的方式来发现依赖项,因此是比较容易发现和修复的。

除此之外,还有一种更加微妙、复杂且隐蔽的公式依赖场景,在该场景中,通过检查表达式的方式并不能直接发现依赖项,因为两个表达式都没有直接引用对方。比如以下场景:

首先添加一个SumOfCost计算列,它的表达式与结果如下图:

然后再添加一个SumOfSales计算列,它的表达式与结果如下图:

从上图中可以看到,SumOfSales计算列并没有成功计算,而是被检测出了循环依赖错误,而从两个计算列的表达式来看,也并没有引用对方。

那么循环依赖出现在哪个环节呢?答案就是CALCULATE函数使行上下文转换成筛选上下文的这个过程,某个计算列中出现的行上下文转换会涉及到除当前计算列外的所有其他列。假设SumOfCost和SumOfSales这两个计算列都能添加到表中,那么这两个计算列的行上下文转换的依赖范围如下:

  • SumOfCost:日期、产品、成本价格、零售价格、销售数量、SumOfSales
  • SumOfSales:日期、产品、成本价格、零售价格、销售数量、SumOfCost

行上下文转换是指为某个行上下文里包含的所有列设置筛选器,筛选的内容为各个列在此行上下文里的索引标记所对应的值。由于行上下文转换是需要各个列对应的值的,所以想要成功计算SumOfCost则需要先知道SumOfSales的值,而计算SumOfSales时却需要先知道SumOfCost的值,否则无法进行行上下文转换,所以两者互相依赖,陷入循环。

解决方案:

上面介绍了两种公式依赖的场景,其中如果是由表达式中直接可见的互相引用而导致的循环依赖,那么修改表达式,解开循环即可。如果是由行上下文转换导致的循环依赖,那么既可以修改表达式,改用不涉及行上下文转换的其他方法来实现所需的结果,也可以不修改表达式但创建主键字段,从而让行上下文转换时只对主键字段创建依赖关系。

修改表达式的方式比较简单,就不过多赘述。下面重点来看一下使用主键字段来解决循环依赖的方式。

首先,主键字段的作用如下:

  • 如果表没有主键,某个计算列发生行上下文转换时,会对表中除该计算列外的所有列创建依赖关系
  • 如果表存在主键,那么某个计算列发生行上下文转换时只会对主键列创建依赖关系

主键字段的指定方式如下:

  • 某个表与其它表建立关系时,若该表为一端,则该表中用于连接关系的字段会自动成为主键
  • 在模型视图的属性面板里的“主键”选项中指定的字段
  • 在“标记日期表”选项中指定的日期字段,该方法不适用于非日期类型的主键字段

下面通过指定产品主键的方式,来解决上面场景中由行上下文转换导致的循环依赖问题。由于本案例中的模型只有单个表,所以采用第二种方法来指定主键,如下图所示:

指定完主键字段后,刷新一下,SumOfSales计算列就可以解开循环依赖并成功计算,如下图所示:

此时由于存在主键字段,所以行上下文转换时只会对主键字段创建依赖关系,从而解开了循环依赖。

需要注意的是,需要把依赖关系与筛选器区分开来,即使表存在主键,行上下文转换也会对所有列创建筛选器,而不仅仅只是对主键字段创建筛选器。比如下图所示的,使用ALL函数移除了行上下文转换后产生的产品筛选器,但获得的销售数量仍然是当前行的,而不是总销售数量。这是因为除了产品筛选器外,还有其他字段上的筛选器,比如日期、成本价格、零售价格等。

因此,这也充分说明了,即使表存在主键,行上下文转换也会对所有列创建筛选器,而不仅仅只是对主键字段创建筛选器。

另外补充一下,主键字段如果恰好是日期类型的字段,那么就是日期主键,而日期主键存在一个特殊行为,即:日期主键字段上的筛选器可以覆盖日期主键所处的基础表的扩展表上的所有字段的筛选器。关于该特殊行为的介绍可以参考我的另一篇文章:32、应用日期表时的注意事项

特例:

另外要特别注意的是,由于DAX引擎的底层是有优化机制的,所以行上下文转换并不一定总是会导致循环依赖的出现,在某些特殊场景中DAX引擎会进行优化以避免发生这种依赖,比如将SumOfSales计算列的表达式修改为以下就不会报错(未设置主键):

这种特殊例子了解下即可,无需深究。


循环依赖中的空行依赖

空行依赖是非常隐蔽的一种循环依赖,虽然它的解决方法非常简单,但如果不理解其中的原理,那么大多数人都会摸不着头脑,不清楚具体是哪里出现的问题。

原理:

两张表建立关系时,当存在参照完整性不匹配时(上级表的数据无法完全涵盖下级表的数据),DAX引擎会自动在上级表中添加一个空行来对应那些不匹配的数据。因此,上级表总是会依赖于下级表的数据,看看是否需要添加空行,此依赖即为空行依赖,只要两表之间连接了关系就会存在。

另外,还有一些函数也是能够返回参照完整性不匹配时所添加的空行的,比如VALUES函数和ALL函数。这两个函数都会依赖于其参数所属的表的数据,看看是否存在参照完整性不匹配时所添加的空行,看看是否需要返回该空行,所以此依赖也是空行依赖。

因此,如果在下级表的计算中出现了VALUES和ALL这两个函数,并且其参数刚好是上级表,那么此时就会出现空行依赖导致的循环依赖,比如下面这个案例。

首先,模型中存在两个产品表,一个是从数据源里导入的“产品表-RawData”,另一个则是通过计算表/新建表所创建的“产品表-ALL”,该计算表使用了ALL函数去复制了一个相同的产品表:

然后,在这两个表之间连接关系,其中“产品表-RawData”为多端,“产品表-ALL”为一端,此时是可以正常创建关系的:

然后,尝试更改上面关系的类型,让“产品表-RawData”为一端,“产品表-ALL”为多端,那么将会报循环依赖错误:

该循环依赖出现的原因就是因为两边都出现了空行依赖。假设上面的关系类型能够成功更改,即让“产品表-RawData”为一端,“产品表-ALL”为多端,那么此时的依赖关系为:

  • “产品表-RawData”处于关系的一端,因此会依赖于多端的“产品表-ALL”的数据,看看是否存在参照完整性不匹配,以决定是否添加空行,故存在一个空行依赖。
  • “产品表-ALL”是由ALL('产品表-RawData')所计算得到的,因此首先存在一个公式依赖,此外ALL函数会返回参照完整性不匹配时所添加的空行,所以也存在一个空行依赖。

虽然这里出现了一个公式依赖,但只是单方面的并没有形成循环,而空行依赖却是两边都出现了,所以出现循环依赖是由于空行依赖所导致的。

上面的描述可能不太好理解,那么下面用简单的话来说就是,当“产品表-ALL”的值发生变化时,“产品表-RawData”的值可能也会发生变化(DAX引擎会自动在一端添加参照完整性不匹配时的空行)。反过来,当“产品表-RawData”的值发生变化时,那么“产品表-ALL”的值可能也需要更新(ALL函数会返回参照完整性不匹配的空行)。这就是检测到循环依赖的原因,而且是由非常隐蔽的空行依赖导致的。

解决方案:

经过上面的分析可以知道,只要连接了关系,那么上级表总是会空行依赖于下级表。因此想要解开循环,则只能从VALUES和ALL这两个函数入手,因为这两个函数会返回参照完整性不匹配时的空行。所以,我们只需要将其替换成不会返回参照完整性不匹配的空行的函数即可,比如DISTINCT和ALLNOBLANKROW这两个函数。

对于上面的案例,将“产品表-ALL”的计算表达式中的ALL函数改为ALLNOBLANKROW函数即可解开循环依赖,成功创建关系,如下图所示:

截止本文撰写时间,最新版本的PowerBI Desktop已经能够有效处理空行依赖的问题,即以上案例在PowerBI Desktop中并不会报循环依赖错误。但仍不太完善,当关系连接字段是计算列,并且计算列里使用了VALUES等函数时仍然会出现空行依赖导致的循环依赖问题。

总结

循环依赖几乎是所有人都会遇到的问题,因此了解循环依赖出现的原因,并掌握有效的解决方法是非常重要的。

未经允许不得转载:夕枫 » 33、循环依赖和主键