CALCULATE之庖丁解牛系列 - CALCULATE专解 (7)

    第27式  CALCULATE的DAX函数与Excel函数比较

      参考阅读:《DAX圣经第一章、第二章、第四章》。
      为了内容的连贯,本部分(CALCULATE系列(7),主要内容来自DAX圣经的这几章的简体修改。
      只所以将基础的一些内容放在了较为后面的式后,是因为之间的内容衔接。

      为了巩固你对列表引用以及定义值列表有一个全面的认识,我们还将啰嗦一些知识点,来作为后面内容的过渡。

      你可能已经知道Excel公式语言了,DAX与它有点像。毕竟,DAX的根是在Excel的Powerpivot中,后者也试图将这两种语言保持相似,以使得向新语言的过渡更加容易。然而,它们之间存在一些非常重要的区别。

        1、单元格与列表

      在Excel中,你可以对单元格使用其坐标进行引用。因此,你写公式如下:
        = (A1 *1,25) - B2
      通过前面的介绍,你知道DAX函数是不同的。在DAX中,没有单元格(行)的概念以及不存在单元格。DAX在表和列上工作,而不是在单元格上。
      所以,当需要写DAX表达式时,你只能指向表和列。表和列的概念在Excel中并不新鲜。实际上,如果将Excel范围定义格式为表(通过使用表格函数处理的格式。比如Ctel+L将单元格区域转化为列表格式。可以在Excel中编写引用表和列的表达式。如果你看图1 - 5,你会发现列SalesAmount计算在同一个表中引用列的表达式,而不是工作簿中的单元格。

      图中  可以在Excel表中使用列名。

      要在Excel中引用列,可以使用[@ ColumnName]格式引用表中的某个列,其中ColumnName是要引用的列的名称,@ 符号表示“取当前行的值”
      尽管这样的语法不是很直观,通常你不会写这些表达式。它们只是简单地单击一个单元格,Excel自动计算出正确的结果。你知道Excel有两种不同的计算方法:即还可以使用标准的单元格引用(在这种情况下,单元F4的公式应该是E4 * D4),或者可以使用列引用:

    Sales[SalesAmount] =Sales[ProductPrice] * Sales[ProductQuantity]

      在Excel里使用列引用的优点是,列的所有单元格使用的是相同的表达式,Excel运用该公式计算每一行的不同值。DAX在表格模型里工作,所以所有公式都需要引用列。比如,在DAX公式中,之前的乘法运算是这样的:
      1) Excel:IF([@ SalesAmount]>10,1,0)
        2)DAX IF(Sales[SalesAmount]>10,1,0)

        Excel和DAX的语法不同的一个重要方面是对整个列的引用。可能已经注意到,在写入[@ productquantity]时,@ 表示“当前行的值”。当使用DAX时,你不需要指定这个。
      DAX语言的默认行为是获取”当前行”的值。我们可以回忆一下前面第一部分中提到的概念:这种DAX内部引擎自动完成的行为,我们称之为“隐式行行为”。
      通过学习,我们已经可以解释:在前面的公式 2)中,Sales[SalesAmount]>10是一个值列表(具有隐式行行为:自动获取“当前行”),同时它又是IF引用的“列表”(使用整个值列表的值,所以是以列表形态被引用)。

      如果在Excel中,想要引用整个列(即该列中的所有行,那么,你可以删除@符号,如图1 - 6所示。图1-6 Excel列表式数据区域中,可以省略列名前面的@符号来引用整个列。

      [AllSales]的值在所有行中是相同的,因为它计算的是[SalesAmount]列的总数。换句话说,在当前行的列值整个列的值之间存在一个句法差异。
      DAX公式是不同的。在DAX公式中,你可以这样写图1 - 6的AllSales表达式:

      [AllSales] :=SUM( Sales[SalesAmount] )

        使用列来获取(定义)特定行的值和使用(引用)列作为一个整体这两种语义,语法上其实并没有区别。DAX知道这是对该列的所有值求和,因为在聚合函数(SUM)中使用了列名(在这个例子中是SUM(Sales[SalesAmount]),这符合:聚合函数需要一个列名称作为参数传递。因此,尽管Excel检索数据时需要显式的语法来区分这两种类型的数据,但DAX会自动地执行歧义消除。至少在开始的时候,这可能会让人感到困惑。
        从这点上看,Excel和DAX是两种不同的函数式语言。

      这两种语言非常相似的一个方面是:Excel和DAX都是函数式语言。函数式语言是由基本函数调用的表达式构成的。无论是在Excel中还是在DAX中,都没有语句、循环和跳转的概念,但这在许多编程语言中都是常见的。

      在DAX公式中,一切都只是一种函数表达式。

      对于来自不同语言的程序员来说,这一方面通常是一个挑战,但是对于Excel的用户来说,这一点并不奇怪。

    2、使用迭代器(多个筛选)

      第一个DAX的概念就是迭代器。Excel工作时,习惯于一次执行一个步骤。在前面的例子中,已经看到,为了计算销售总额,需要创建一个列,其中包含价格乘以数量,然后,作为第二步,引用它来计算总销售额(当前的总值,总是一个值)。这个数值将是有用的,例如,作为计算每个产品销售占比百分比的分母。
      使用DAX,可以使用迭代器在单个步骤中执行相同的操作。迭代器按照公式指定步骤进行相同的计算。例如:你可能很熟悉的一句DAX计算过程的规则:

      它遍历表,并在表的每一行执行一次计算,聚合结果以生成所需的单个值,并填充在对应的每一行。在前面的示例中,可以使用SUMX迭代器的方法来计算所有销售的和:

      [AllSales] := SUMX(Sales, Sales[ProductQuantity] *Sales[ProductPrice])
      该公式是我们已经很熟悉了的X后缀系列迭代器函数的套路:引用一个表(这里是Sales表),然后遍历(行行为)该表的所有行,第二个参数定义的值列表在该表内计值。

      我们说,这种方法有优点也有缺点。
      一方面,优点是:可以执行许多复杂的列表计算,而不必为了只对某些特定的公式有用时,添加不必要的--占用内存的计算列。
      另一方面,缺点是:使用DAX进行编程的视觉效果比不上Excel。事实上,你看不到价格乘以数量的列计算:它只存在于计算的生命周期中。
      当然,你仍然可以选择创建一个新的计算列,来计算[price ]和 [quantity](即价格和数量)的乘积。然而,正如稍后将学到的,这很少是一个好的实践,因为它使用了宝贵的内存,可能会减慢计算速度。
      我们需要明确一点:这不是编程语言之间的区别,而是一种思维模式的区别。就像这个星球上的任何人类一样:当使用Excel的时候,可能都习惯于在web上搜索试图解决的场景,例如复杂公式或解决方案模式。而且,很有可能你都会找到一个几乎可以实现你需要的公式。你可以复制这个公式、定制它、使用它来满足你的需要,而不必过于担心它是如何工作的。

      这种方法在Excel中有效,但却与DAX没有关系。对于DAX,你将需要学习一些新的DAX工作理论,并彻底了解当前计算列表是如何工作的,然后才能写出好的DAX代码。如果没有适当的理论基础,DAX将会计算出莫名奇怪的字符、或者莫名其妙的数字。出现这种现象,问题不在DAX,而是你还不清楚它是怎样工作的。
      幸运的是,DAX的理论仅限于几个重要的概念,这在我们所有CALCULATE系列中将得到解释,而最最关键的是DAX的列表概念,例如如何引用列表与定义值列表等。

      一旦你掌握了它的内容,DAX对你就没有任何秘密了,学习它,后面的实际运用反而显得相对容易一些,不过是一个获得经验的过程问题。然而,试图更进一步熟悉它将更困难,除非你对DAX的理论基础建立得非常好。记住,了解只是成功的一半。
      所以,了解DAX(CALCULATE)我们需要108式的差不多一半的式。
      很多人一开始学习DAX,就想像学习Excel那样:照搬几个公式,以为理解了,其实不然,这里真正掌握DAX还有很长的一段路......。

      前面提到DAX语言与其他语言的区别,这里简单介绍一下很有必要。

    第28式  CALCULATE的DAX语言与其他语言的比较

    1、DAX针对SQL

      如果习惯于SQL语言,那么,可能已经与许许多多、各种各样的表打过交道,并总是会在表的某些列之间定义关系以创建连接。这与在DAX是相同的。因为,在DAX公式中,计算的实际是一种查询一组由关系连接的表的聚合计算问题。从这点上看,SQL工作者会很熟悉,就像是回到自己的家一样。
        但是,这两者之间有很大的区别:

      1)SQL和DAX之间的第一个区别是模型中关系的工作方式。在SQL中,可以在表之间设置外键来声明关系,但是引擎在查询中并不使用这些外键,除非你声明它们。例如,如果有一个Customer表和一个Sales表,其中的[CustomerKey]是Customer--客户表的主键以及销售表的外键,那么,你可以编写如下的查询:

SELECT
Customers,CustomerName,
SUM( Sales,SalesAmount ) AS SumOfSales
FROM
Sales
INNER JOIN Customers
ON Sales,CustomerKey = Customers,CustomerKey GROUPBY
Customers,CustomerName

      即使使用外键声明了模型表中的关系,你仍然需要显式并声明查询中的联接条件。尽管这样会使查询变得更加冗长,但这是必须的,因为它允许在不同的查询中使用不同的连接条件,以便在查询的表达性方面给予最大的自由。
      但是,在DAX公式中,关系是模型固有的一部分,它们都可以被视为一种左外联结。一旦在模型中创建了关系,当使用与主表相关的列时,不再需要在查询中指定连接类型:DAX会在查询中自动进行左外连接。因此,可以将以上的SQL查询用DAX代替如下:

EVALUATE
SUMMARIZE(Sales,
Customers[CustomerName],"SumOfSales",SUM(Sales[SalesAmount] )

      上述DAX公式中,因为关系是模型的一部分,会始终自动跟随模型(只要你不删除已有的关系),DAX能识别Sales和Customer表之间存在的关系。这里,SUMMARIZE函数只是引用Customers表的[CustomerName]列来自动计算,这个过程里,并没有任何关键字被SUMMARIZE声明。DAX中不需要声明模型表中的关系的特点很重要,这在后面的DAX的列表关系在DAX公式中的运用中将会详说。

      2)DAX是函数性语言

        既然SQL是一种声明性语言。
        一方面,它通过使用SELECT语句声明要检索的数据集,灵活地查询所需的数据,而不必担心引擎如何检索信息。
      另一方面,由于DAX是一种函数性语言。在DAX公式中,每一个表达式其实都是一个函数的调用过程,如果一个函数参数又依次为引用其他函数调用,那么,参数的动态调用(迭代)可能会导致非常复杂的查询过程,为了计算出结果,DAX会执行这些过程。

      例如,如果只想检索居住在Europe欧洲的客户,你可以用SQL编写这个:

SELECT
Customers,CustomerName,SUM( Sales,SalesAmount ) ASSumOfSales
FROM
Sales
INNER JOIN Customer ON Sales,CustomerKey = Customers,CustomerKey
WHERE
Customers,Continent ='Europe'GROUPBY
Customers,CustomerName

      如果使用DAX,在查询中不必声明WHERE条件。相反,使用一个特定的函数(FILTER)来筛选结果:

EVALUATE
SUMMARIZE(
FILTER(Customers,Customers[Continent] ="Europe"),Customers[CustomerName],"SumOfSales",SUM( Sales[SalesAmount] ))

      这里的FILTER(筛选器)是一个函数:它遍历整个Customers,只返回Continent--居住地仅仅为Europe--欧洲的客户的那些行的Customers表,并在该表中得出预期的结果。前面我们已经说明:其中嵌套函数的使用以及使用该函数的顺序对最终结果和引擎的性能有很大的影响。这在SQL中也一样。

        3)DAX作为一种编程和查询语言

      在SQL中,查询语言和编程语言之间有明显的区别:也就是说,编程语言用于在数据库中创建存储过程、视图和其他代码片段的指令集。每个SQL代码都有自己的编程语句,让程序员用代码来丰富数据模型。

      DAX实际上并没有区分查询和编程。一组丰富的函数可以操作一个个列表,也可以返回一个个表。刚才在前面的查询中看到的FILTER函数就是一个很好的例子。
      因此,在这方面,DAX比SQL简单。一旦将其作为一种编程语言(通常是它的第一个用法)学习它,就会知道所有需要使用它作为查询筛选的原理。

        4)DAX和SQL中的子查询和条件

      SQL作为查询语言最强大的特性之一是使用子查询的特点。DAX有一些相似的概念,即在DAX的子查询中,它们自然也是由查询后的列表条件改变引起的。这其实对应于我们所说的DAX中值列表与列表的交替变化,也就是是DAX的列表筛选、列表条件(列表的再次被筛选)。

      例如,为了检索仅购买超过100美元的客户的总销售额,可以将该查询编写为:通过简单的嵌套函数调用,获得相对于SQL要简单的相同的结果:

EVALUATE
FILTER(SUMMARIZE(Customers, Customers[CustomerName],"SumOfSales",
SUM( Sales[SalesAmount] )),[SumOfSales] >100)

      在这段代码中,检索CustomerName以及SumOfSales的子查询稍后被输入到FILTER函数中,该函数只筛选SumOfSales中大于100的行予以保留。现在,这段代码对来说可能会不懂它,但是,一旦开始学习DAX,就会发现子查询的使用比SQL更容易,而且它自然地流动,因为DAX是一种函数性语言。

    2、DAX与MDX

      许多BI专业人员开始学习DAX,因为它是SSAS表格的新语言,在过去,他们使用MDX语言构建和查询SSAS多维模型,这点上,DAX和MDX没有多少区别。但糟糕的是,DAX的一些概念会让你想起MDX中相似的现有概念,即便它们其实非常不同。
      事实上,根据我们的经验,在MDX之后学习DAX是最具挑战性的选择。为了学习DAX,你需要从MDX中解放思想:试着忘记你所知道的关于多维空间的一切,并准备好以全新的认识(列表的概念)来学习这门新语言。我们来了解一下:

      1)多维和表格

      MDX在模型定义的多维空间中工作。该多维空间的形状是基于模型中定义的维度和层次结构,进而定义多维空间的集合。在多维空间中,使用不同维度成员的交集来定义点。所以,我们认为,你需要一些时间才能理解:任何属性层次结构中的[All]成员,实际上都是多维空间中的一个点。
      DAX的工作方式则更简单一些。在类似多维空间中并没有维度,没有成员,没有点。换句话说,根本没有多维空间。你可以在模型中定义层次结构,但是它们与MDX中的层次结构完全不同。
      DAX数据空间是建立在表、列和关系之上的(即表格或列表模型)。表格模型中的每个列表既不是一个度量组,也不是一个维度:它只是一个表,要计算值,你必须扫描它、筛选它,或对它里面的值求和。
      这一切都基于两个简单的概念:列表和关系。

      很快就会发现:从建模的角度来看,数据表提供的选项比多维的要少。这种情况下,拥有较少的选项并不意味着不强大,因为有一种编程语言(即DAX),它可以丰富数据模型。
      因为,DAX使用表、列的真正建模能力是它强大的操作列表的能力。

      实际上,你可能已经习惯避免在模型中使用太多的MDX。因为,一方面优化MDX速度常常比较困难, 另一方面,DAX公式惊人的快。因此,计算的复杂性很大程度并不在数据模型中,而是在DAX公式中。所以,掌握DAX,是建模的一项根本技能。

      2)DAX作为一种编程和查询语言

      DAX和MDX都是编程和查询语言。在MDX中,差异是通过MDX脚本的存在而变得清晰的。在MDX仅在脚本中使用MDX,以及一些特殊的语句(例如SCOPE语句)。当编写SELECT语句来检索数据时,在查询中使用MDX。
      在DAX公式中则有所不同。DAX将作为一种编程语言来定义计算列(在MDX中不存在的一种新概念)和度量(类似于MDX中的成员计算)。你也可以用DAX,例如查询语言可以使用Reporting,Services报表服务从表格模型中检索数据。
      然而,在DAX中并没有特别的函数,只针对这两种语言中的一种有用。此外,还可以使用MDX查询一个表格模型。因此,MDX的查询与表格模型一起工作,而在设计表格模型后,DAX是你唯一的选择。

        3)DAX与SQL中的子查询和条件

      使用MDX,依赖于层次结构来执行大部分计算。例如,如果想计算上一年的销售额,那么,必须在年度层次上检索CurrentMember的PrevMember,并使用它重写MDX筛选器。例如,你可以用这种方式来定义MDX的前一年计算:

CREATE MEMBER CURRENTCUBE,[Measures],[SamePeriodPreviousYearSales]  AS ([Measures],[Sales Amount],ParallelPeriod( [Date],[Calendar],[Calendar Year],1,[Date],[Calendar],CurrentMember))

      该方法使用ParallelPeriod函数,该函数返回日历层次结构中CurrentMember的上一期。因此,它基于模型中定义的层次结构。
      在DAX公式中,使用列表筛选和标准时间智能函数来写相同的计算:[SamePeriodPreviousYearSales] :=
CALCULATE(SUM( Sales[Sales Amount] ),SAMEPERIODLASTYEAR( 'Date'[Date] ))

      在DAX里,你可以用许多其他方法来编写这种计算,例如,使用FILTER和其他DAX计算,但是这种思路仍然是一样的:先筛选表,而不是使用层次结构。这种差异是巨大的,在熟悉DAX之前,先不考虑层次结构的计算。
      另一个重要的区别是,在MDX中,引用该[Measures度量],[Sales Amount]和需要使用的聚合函数需要在模型中事先定义。在DAX公式中,没有预先定义的聚合。事实上,正如可能已经注意到的,计算的表达式是SUM(Sales[SalesAmount])。
      预定义的聚合不是一开始就需要存在于模型中:只有在你想使用它的时候定义它即可(你总是可以创建一个sum of sales度量,但是我们不想不需要时,一开始就使用它)。
      相对于MDX,DAX具有较大的灵活性。

      DAX和MDX之间的另一个重要区别是,后者大量使用数据范围约束语句来实现业务逻辑(这时候,同样使用层次结构),而前者需要完全不同的方法,因为语言中完全缺少这种层次处理。举个例子,如果你想在年的粒度级别上建立一个指标,在MDX中你会写这个声明:

SCOPE ( [Measures],[SamePeriodPreviousYearSales],[Date], [Month],[All] )
THIS = NULL: END SCOPE:

      在DAX公式中,没有这种范围声明。为了获得相同的结果,需要检查筛选器筛选中的筛选器是否存在,并且这种场景要复杂得多:

[SamePeriodPreviousYearSales] :=
IF(ISFILTERED( 'Date'[Month] ), CALCULATE(SUM( Sales[Sales Amount] ),SAMEPERIODLASTYEAR( 'Date'[Date] )),BLANK())

      稍后将详细了解该公式计算的内容,但直观地看,它只返回一个值,如果用户在月级别或下级浏览日历层次结构(从月度级别开始),则返回一个空白。这个公式要比等效的MDX代码更容易出错。也复杂的多。说实话,层次处理是DAX公式中真正缺失的一个特征。

      4)粒度(层级结构)计算

      最后,在使用MDX时,你可能已经习惯了避免粒度计算。在MDX中执行粒度计算是非常慢的,因而总是喜欢预先计算值并利用聚合来返回结果。
      在DAX公式中,粒度计算的速度则非常快,而聚合根本不存在。在构建数据模型的时候,这需要在适当的时候改变度量。
      你可以记住:在大多数情况下,一个完全符合SSAS多维度的数据模型并不适合列表表格,反之亦然。
      既便说到DAX的粒度(层级结构),我们也只有在后续相关的内容中看到有关DAX的粒度计算概念以及运用实例。

    第29式  CALCULATE的涉及不同表之间的筛选

        我们在前面草草的说了一下列表之间的列表关系,并多次提到列表关系:数据模型就是列表+关系构成,因为DAX数据模型中的关系相对来说,一旦建立就比较固定,总是伴随数据模型(只要不删除它),后面也将经常提到列表关系、因此,我们并没有刻意的将它作为一个单独的专题概念,虽然很有必要,但它会贯穿后面的DAX学习。作为CALCULATE系列的知识点,我们需要你有一定的DAX基础将会最好。
      你可以通过一些官方的帮助文档,很容易就了解。

      我们前面开始学习了一些列表筛选概念,这让我们发现了一些有趣的、令人惊讶的一些结果。你可能已经注意到,大多数情况下,我们故意只使用一个表:比如只有一个产品表,只需要面对同一表达式中行筛选和列表之间的交互等等。
      事实上,数据模型很少只包含一个表。很有可能在数据模型中存在许多具有关系的表。因此,一个有趣的问题是:在两个或多个表之间的筛选如何通过关系联系? 此外,因为关系有一个方向,接下来,我们需要了解:关系的一端与多端发生了什么”。

      最后,为了增加点难度,请回想一下:
      1)关系可以是单向的,也可以是双向的,这取决于如何定义交叉筛选器关系本身的关系方向。
      2)如果在关系的多端创建一个行筛选,你希望它能让你使用一端的列吗?
      3)此外,如果在关系的一端创建了行筛选,那么是否希望能够从关系的多端访问表中的列呢?
      4)如果针对的是列表筛选呢?又如何呢? 比如希望在多个表上放置一个筛选器,并将其传播到另一端的表中?
      任何答案都可能是正确的,但我们感兴趣的是学习DAX在这些情况下的表现,也就是说,理解DAX语言是如何通过关系定义筛选的传递。
      正如你将要学习的,在筛选和关系中有一些微妙的相互作用,学习它们,需要一些耐心。
      为了检查这个问题,我们设置一个场景:使用一个包含6个表的数据模型,可以在下图中看到:

        在图中,可以看到用于学习筛选和关系的交互的数据模型。关于这个模型,有几点需要注意:

        1)从销售表到产品类别表,从产品表和产品类别表开始,为一对多的关系链:
        2)唯一的双向关系是销售表和产品表之间的关系:
        3)所有剩余的关系都被设置为单向的交叉筛选方向。

        既然我们已经定义了该模型,那么,让我们开始研究筛选是如何通过一些DAX公式来表现的。

      1、行筛选和关系

      行环境之间的相互作用和关系很容易理解,因为只需要记住:它们不以任何方式交互,至少不会自动交互。只在前面我们已经提过。

        假设希望在Sales表中创建一个计算列,其中包含存储在事实表中的单价和产品表中存储的产品价格之间的差异。你可以试试这个公式:

      Sales[UnitPriceVariance] = Sales[UnitPrice] - Product[UnitPrice]

      你应该能够解释该公式:这个表达式引用来自两个不同表的两列,期望DAX在一个行筛选中对它进行计算,因为在该表中定义了计算列 Sales[UnitPriceVariance] 。产品表是销售表关系的一端(销售表为关系的多端)。所以,可能期望能够获得当前相关行的单价。
      不幸的是,这种情况并没有发生。Sales中的行筛选不会自动传递到Product--产品表,如果试图使用上面的一个公式来创建计算列,将返回一个错误。
      我们再次使用前面讨论的RELATED以及RELATEDTABLE函数。

      如果希望从关系的多端的表中访问关系的一端的列,就像本例中的情况一样,必须使用RELATED函数。RELATED接受一个列名称作为参数,并从当前行筛选开始,从关系的多对一方向的一个或多个关系中找到相应行中的列的值。你可以用下面的代码以正确的替代前面的公式:

      Sales[UnitPriceVariance] = Sales[UnitPrice] - RELATED( Product[UnitPrice] )

        RELATED行为:当行筛选在数据列表关系的多端时使用(即引用一端关系表的列值)。如果主动的行筛选在关系的一端,那么,你不能使用RELATED(从一对多方向引入数据),因为可能会被检测到关系的许多行。
      在这种情况下,需要使用另一个类似的函数:RELATEDTABLE。可以在关系的一端使用RELATEDTABLE,它会返回所有(引用的表在关系的多端)与“当前行”相关的列的值。例如,如果要计算每个产品的销售数量,可以使用以下公式, 在产品表里定义一个计算列:

        Product[NumberOfSales] =COUNTROWS(RELATEDTABLE( Sales ) )
      这个表达式计算对应于当前产品的Sales表中的行数。该模式在处理关系的一端有行筛选时非常有用。

      值得注意的是,RELATED 和RELATEDTABLE函数都可以遍历一长链的关系来收集它们的结果:不限于单节点连接关系(一级关系)。例如,可以使用前面相同的代码来创建一个列,但这一次,我们将在产品类别表里创建(这存在:Sales-->产品表-->产品类别表-->产品子类别表的多级关系):

      ProductCategory'[NumberOfSales] = COUNTROWS(RELATEDTABLE( Sales ) )

      结果是该类别的销售数量,它遍历了从产品类别到产品子类表,然后再到产品表最终回到销售表的关系链。

      值得注意的是:RELATED 和 RELATEDTABLE的一般规则的唯一例外是:一对一关系。如果两个表共享一个1:1的关系,那么,可以使用RELATED和RELATEDTABLE两个中的任何一个,得到一个列值或一个单行的表,这取决于你用的哪一个函数。
      对关系链的唯一限制是:所有的关系都必须是相同的类型(即一对多或多对一),所有的关系都朝着同一个方向。如果有两个表,通过一对多和多对一关联,并有一个中间桥表在中间,既非一对多,也不是多对一。因此,如果使用1:1的关系作为表现方式的同时。可以有一个1:1的一对多的关系而不中断关系链。
      让我们以一个例子来说明这个概念。你可能认为客户与产品有关,因为客户和销售之间存在一对多的关系,然后是销售和产品之间的多对一关系。因此,关系链可将这两个表连接起来。
      然而,仔细一想,这两种关系并不是同一方向的。我们把这种情形称为多对多关系。换句话说,一个客户与许多产品(购买的产品)有关,而一个产品也可以同时与许多客户(购买产品的人)有关。
      稍后将了解如何使多对多关系工作的细节。现在让我们关注行筛选。如果尝试通过多对多关系应用RELATEDTABLE,结果可能不是所期望的。例如,在产品列中考虑一个计算列:

      Product[NumOfBuyingCustomers] =COUNTROWS(RELATEDTABLE( Customer ) )

      我们期望看到每一行里,购买该产品的客户数量。出乎意料的是,结果将永远是18869,即数据库中客户的总数。也就是说,如果你想遍历一个多对多的关系,RELATEDTABLE将不工作RELATEDTABLE无法跟随该关系链(只能在关系链的一个方向上能正确工作),因为他们不是在同一方向:一个是一对多,另一个是多对一。因此,产品无法传递筛选客户。
      值得注意的是,如果公式在相反的方向,也就是说,计算每个客户购买产品的数量,其结果将是正确的:为每一行不同的数字代表每个客户采购的产品的数量。这种行为的原因不是一个筛选器的传递环境,但相反,筛选转换在RELATEDTABLE内隐式计算。为了完整性,我们添加了这最后的注意。还没有时间去详细说明,但当你全面“理解CALCULATE和CALCULATETABLE之后,这将不是问题”。
        在后面的相关函数学习中,一开始,虽然不需要刻意地记住那么多的函数,只少需要记住某一个或某一类函数具有某种功能,在需要的时候能想起它。刚刚讨论的两个函数,你需要记住的是:在关系链中使用行筛选(隐式筛选或计算),需要使用RELATED 或 RELATEDTABLE。

        2、列表筛选与关系

      我们已经了解到:行筛选不与关系交互。如要遍历关系,则有RELATED 或 RELATEDTABLE这两个不同的函数可供使用,这取决于在访问目标表时所使用的关系的哪一方。
      与筛选环境行为方式不同:它们以自动方式相互作用关系,以及它们有不同的行为取决于你如何设置筛选的关系。一般规则是,筛选通过关系传递,如果在关系本身上设置的筛选方向是同一个方向,则使传递可行。
        我们通过使用一个简单的数据透视表,很容易地理解这种行为。在下图中,可以看到一个透视表浏览到目前为止使用的数据模型,其中定义了三个非常简单的度量。

[NumOfSales] :        = COUNTROWS( Sales )
[NumOfProducts] :  = COUNTROWS( Product )
[NumOfCustomers] := COUNTROWS(Customer )

      你可以看到筛选和关系的行为:
      一方面,筛选器在产品的颜色列上(透视表中的行筛选)。Product-产品表与Sales- 销售表之间是一对多关系的来源。因此,筛选器筛选从产品表传递到销售表,你可以看到这一点。因为NumOfSales度量只计算具有特定颜色的产品的销售。NumOfProducts显示了每种颜色的产品数量,并且每一行(颜色)的不同值也是所期望的,因为筛选器位于我们正在计算的同一张表上。

      另一方面,[NumofCustomers]所计算的客户数量,总是显示相同的值,即客户总数。这是因为客户和销售之间的关系,你可以看到如图箭头指向的方向:客户与销售表之间的关系是一种单向关系。

      筛选器从产品表开始,然后传递到销售表(从产品到销售的箭头,这是启用的),但是,当它试图传递给客户表时,它没有找到让它继续的箭头方向。因此,它停了下来。
      单向关系允许在单一方向上传递关系筛选,而不允许两者同时使用。

      你可以认为关系中的箭头就像信号灯。如果它们被激活,那么信号灯为绿色,传递就会发生。相反,如果没有启用箭头,则信号灯为红色,筛选器不能传递。关系箭头总是从一端到任意关系的多端。你可以选择让它从多端到一端。如果你让箭头失效,那么,传播就不会从多端到一端。

      如果需要筛选器从客户表开始,可以传递到销售表。你需要启用在对应关系中的关系箭头。然后,从销售表,它可以进一步传递到产品表,因为销售表和产品表的关系是双向的。
      如果关系是单向的,则客户表不能筛选子类别表。这是因为,产品和产品子类别之间的关系是单向的,即它只能让筛选器在一个方向上传递。只要你启动了从产品开始,然后到产品子类别的箭头,这时,将看到筛选器传递。

    第30式  CALCULATE的引入VALUES(初级)

      前面的例子非常有趣,因为它展示了如何通过使用筛选的方向来计算购买产品的客户数量。然而,如果你有兴趣只计算客户的数量,还有一个有趣的替代,我们作为契机,引入另一个强大的函数功能:VALUES。
      关于VALUES函数,我们在前面介绍CALCULATE的唯一值列表函数时匆匆提过。
      VALUES是一个表函数,它返回一个列表,其中包含当前在列表筛选中可见的列的所有列值。我们将在后面介绍VALUES的许多高级用法。现在,开始使用VALUES来了解它的行为是很有帮助的。
      在前一个示例的透视表中,可以修改NumOfCustomers,定义以下DAX表达式:

      [NumOfCustomers]:= COUNTROWS(VALUES( Sales[CustomerKey] )) 

      这个表达式不在Customer表里计算客户的数量。相反,它在Sales表里计算当前筛选下CustomerKey列的数量值。这时,它只引用了Sales表,因此,表达式并不依赖于销售和客户表之间的关系。当你将筛选器放在产品表里时,它也总是筛选sales表,因为筛选器只从产品表传递到销售表。
      因此,并不是所有CustomerKey的值都是当前可见的,只有在当前行对应的被Sales表筛选的产品(即被Sales表筛选的那部分产品)。
        你可以理解该表达式的含义是:“计算与所选产品相关的有销售的当前客户的数量”。因为CustomerKey的值代表了Customer--顾客数,这个表达式有效地计算了购买了产品的顾客数量(我们经常引用表中某个含有最大唯一值的列表,来计算对应的其他列表)。

      请注意:可以使用DISTINCTCOUNT实现与上个公式相同的结果,它计算一个列的不重复值的数量。一般来说,使用DISTINCTCOUNT比使用COUNTROWS要好,这里使用COUNTROWS和VALUES是作为一种了解方式,因为VALUES是一个非常有用的函数,即使它最常用的用法,也只有在后面的内容中才会进一步了解清楚。

      值得注意的是,使用VALUES而不是利用关系的方向传递作用是有利有弊的。

      当然,使用VALUES在模型中设置筛选要灵活得多,因为它使用关系。因此,不仅可以使用CustomerKey来计算客户,还可以计算客户的任何其他属性值(例如,客户类别的数量等)。
        这样说,可能有一些原因迫使你使用单向筛选,或者可能需要使用VALUES以提高性能等原因。无论进行VALUES计数还是将VALUES用作筛选器,你都将在编写DAX时经常使用它。这将在后面的“高级列表关系处理”中更详细地讨论这些主题。

      第31式  CALCULATE的ISFILTERED、ISCROSSFILTERED函数行为

      在前面学习DAX列表的过程中,我们不知不觉地已经接触到多个DAX函数的功能与用法,这都是在一种很自然的情况下发生的。
        1、本式将要介绍的ISFILTERED、ISCROSSFILTERED这两个函数,它们非常有用:可以帮助我们更好地理解列表筛选的传递以及运用该行为方式。

      也可以很好地了解数据透视表计算中最有用的、以及在书写DAX公式时需要考虑的概念之一:检测正从DAX 内部计算的某个值所在的层级级别。例如,了解在当前筛选中某列的所有值是否都是可见的。

      ISFILTERED,依据参数定义的列是否直接被筛选来返回真或假;也就是说它已经被置于行、列、切片器或筛选器中了。而且,对于当前单元格而言筛选正在发生。   
      ISCROSSFILTERED,根据该列是被某个直接筛选器筛选还是由另一个筛选器自动传递而筛选,返回真或假。
      此外,学习它们是介绍数据透视表计算里最有用的概念之一的好方法,也就是说,检测从DAX内部计算值的单元格。这两个函数的目的是允许检测一个列的所有值是否在当前筛选中可见(是否是引擎需要处理的列表计数)。

      ISFILTERED返回TRUE或FALSE(真或假),这取决于列作为直接筛选的一个参数传递,也就是说,它被放在行、列、切片器、任何筛选器以及筛选对当前发生的行(单元格)。
      ISCROSSFILTERED返回TRUE或FALSE,这取决于该列是否有一个筛选器,该筛选器来自另一个筛选器的自动传递,而不是直接的筛选器。

      本式中,我们感兴趣的是使用该函数来理解筛选的传递。因此,我们创建一个虚构的表达式,它只出于学习目的。度量定义如下:

        [CategoryFilter] := ISFILTERED( 'Product Category'[Category])

      这个简单的度量返回ISFILTERED函数应用于(即引用)'Product Category'[Category]的值。然后我们再创建第二个度量,该度量与产品颜色列进行相同的测试。因此,代码是:
        [ColorFilter] :=ISFILTERED( Product[ColorName] )

      如果将这两种方式都添加到一个透视表中,将Category--类别放在一个切片器中,并且在行上添加Color--颜色,结果将类似于图:

      图中你可以看到,Category--类别从来没有被筛选过,而且Color-颜色在任何地方都被筛选掉了,但是在总量上显示正确。
      这里的重点是:Category--类别从来没有被筛选过。因为,即使添加了一个切片器,我们也没有在它上面做出任何筛选。另一方面,颜色总是在行中进行筛选,因为每一行都是当前特定的颜色,但在总量中没有,因为列表筛选并不包含任何产品的选择。

      请注意总计的这种行为,也就是说,它不是从行和列中筛选得到的,因此,在总计上,它显示了一个不同的值。当你想要修改一个公式的行为时,这是非常有用的。实际上,将检查在透表报告中结果存在的属性,以了解正在计算的单元格是否在数据透视表的内部,或者是在总计上。
      如果现在从类别切片器中选择一些值,结果会发生变化。现在,这个类别总是有一个筛选器,事实上,切片器引入的列表筛选即使在数据透视表的总计上也是有效的。

      当一个直接筛选器在一个列上工作时,ISFILTERED将发挥作用。某些情况下,不显示所有的列值,不是因为筛选了列表,而是因为在列上放置了来自另一个列的筛选器。例如,如果你筛选颜色,并计算产品品牌的值,得到的结果将只有当前特定颜色的产品品牌。
      当一个列的筛选来自另一个列时,我们说这个列被交叉筛选(当一个列上的筛选器‘来自同一个列时,我们说这个列被直接筛选),ISCROSSFILTERED函数用于检测这个交叉筛选的场景。
      如果使用这两个新度量查询数据模型,这一次,我们使用ISCROSSFILTERED筛选了颜色和类别列:

      [CrossCategory] := ISCROSSFILTERED( 'Product Category'[Category] )
      [CrossColor]:      = ISCROSSFILTERED( Product[Color] )

      然后将得到:使用ISCROSSFILTERED可以看到交叉筛选函数结果。

      公式中,Color--颜色是被交叉筛选的,而category类别不是。在这一点上,问题是:“为什么这个类别没有筛选?” 当筛选一种Color--颜色时,你可能只会看到特定Color--颜色的产品的类别。要回答这个问题,需要清楚,类别--[Category]列不属于产品表的列,相反,它只是Product Category--产品类别的一部分,关系不会传递(关系的箭头方向不对)。
        如果更改数据模型--在从产品到产品类别的整个关系链上启用双向筛选,则结果将会不同,因为启用双向筛选后,代表着现在显示这个类别是交叉筛选的,而不是直接筛选。

      你已经看到了一些ISFILTERED 和ISCROSSFILTERED的示例,主要用于理解目的,因为使用它们只是为了更好地理解列表筛选是如何通过关系传递的。在接下这来的内容中,通过编写高级DAX代码,将了解为什么这两个函数是如此有用。

        2、列表筛选知识点回顾

      让我们回顾一下我们所学到的关于列表筛选计算的知识。
      1)DAX计算筛选是通过筛选数据模型并提供当前行的概念(当需要访问列值时)来修改DAX表达式的列表集然后计算;
      2)DAX总是先筛选再计算,一般总是先引用列表,然后定义值列表;
      3)DAX计算环境由两部分组成:行(值列表)筛选和列表筛选。它们在所有的DAX公式中共存。当定义计算列时,DAX会自动创建行筛选,你还可以使用迭代器函数以编程(定义)方式创建行筛选,所有迭代器都用于定义行筛选;
      4)可以嵌套行筛选,在这种情况下,EARLIER函数有助于访问前一行的筛选(指代为:“当前行”);
      5)当使用数据透视表时,通过在其行、列、切片器和筛选器上使用字段来创建每个列表筛选。另一种方法是:可以通过使用CALCULATE来显式创建筛选器筛选;

      6)行筛选不会通过关系自动传递。这可以通过使用RELATED 和 RELATEDTABLE来手动进行传递。正确使用这些函数功能的前提是列表具有一对多关系: RELATED用于关系的多端,RELATEDTABLE用于关系的一端。
      7)与行筛选不同的是,列表筛选总伴随着关系的自动传递,并总是从关系的一端传递到另一端(一端或多端)。此外,关系还可以选择从多端扩展到一端。根据关系数据模型的定义,一切都自动在引擎内部发生,并不是强制传递;

      8)VALUES()返回一个表,其中包含当前筛选下可见的列的所有唯一值。你可以将生成的表用作任何迭代器的参数。                      VALUES()既可以引用列表,也可以定义值列表,这取决于函数的结果。等等。
      至此,你似乎已经了解了DAX语言中最复杂的一些概念主题。这些概念规定了公式的所有评价流,它们是DAX语言的支柱。无论何时,如果遇到表达式计算结果不是自己希望的,很大的原因,是因为你还没有完全理解这些规则。
        正如我们在介绍中所说的,在第一次阅读时,所有这些主题看起来都很简单。事实上,让他们变得复杂的是:DAX总在一个复杂的计算环境中!表达式的不同部分中可能有多个活动的计算筛选。掌握计算筛选是一种你将获得DAX经验的技能,我们将在接下来的章内容中展示许多例子来帮助你。在你自己写出一些DAX公式之后,将直观地知道哪些筛选被使用,以及它们需要哪些功能,这样,你将最终掌握DAX语言。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容