DAX联接表系列 之五
1、从 SQL 到 DAX: 联接表
在 SQL 中有不同的表联接类型,可用于不同的目的。本文显示了在 DAX中所支持的等效语法,并在2018年5月更新了它。
SQL 语言提供以下类型的联接:
(1)内部联接
(2)外部联接
(3)交叉联接
联接的结果并不取决于数据模型中是否存在关系。你可以使用表的任何列定义联接条件。在 DAX中,有两种方法可以获得联接行为。
首先,可以利用数据模型中的现有关系,以便查询包含在不同表中的数据,就像你在 DAX查询中编写相应的联接条件一样。
其次,你可以编写 DAX表达式,以生成与某些联接类型等效的结果。任何情况下,所有在SQL 中可用的所有联接操作几乎都能在DAX中得到支持。你可以通过下载示例文件来测试本文所示的示例,并使用 DAX studio运行DAX查询。
2、在数据模型中使用关系
在 DAX中获取联接行为的常用方法是隐式使用现有关系。例如,考虑一个具有销售、产品和日期表的简单模型。销售表与其他三个表之间存在关系,如果要查看按年份和产品颜色划分的销售额,可以编写以下查询:
EVALUATE
ADDCOLUMNS (SUMMARIZE (Sales,
'Date'[Year],Product[Color]),"Total Quantity", CALCULATE ( SUM ( Sales[Quantity] ) ) )
这三个表是通过销售表(在Total Quantity列的表达式中使用的表)以及其他两个表:日期和产品表之间的左连接自动连接在一起的:
SELECT d.Year,p.Color,SUM ( s.Quantity ) AS [Total Quantity]
FROM Sales s LEFT JOIN Date d ON d.DateKey = s.DateKey
LEFT JOIN Product p ON p.ProductKey = s.ProductKeyGROUP BYd.Year,p.Color
请注意,LEFT JOIN--左联接的方向是在销售表和日期表之间,所以,销售表中包含的所有对应于日期或产品表中都没有的行,都被分组在一个BLANK--空白的值中(对应于SQL中的NULL的概念)。如果你不想聚合各行,则可以简单地使用RELATED方法通过关系的“一端”来访问lookup--查找表上的列值。例如,在SQL中考虑以下语法:
SELECT s.*,d.Year,p.Color
FROM Sales s
LEFT JOIN Date d ON d.DateKey = s.DateKey
LEFT JOIN Product p ON p.ProductKey = s.ProductKey
对应于以下 DAX查询,可以获得相同的行为:
EVALUATE
ADDCOLUMNS (Sales,"Year",RELATED ( 'Date'[Year] ),
"Color",RELATED ( Product[Color] ) )
通过将筛选器应用到目前为止所运用的 ADDCOLUMNS 的结果里,也可能会获得类似于内部联接的行为,并在查找表中移除具有空值的行,假设空白不是该列数据中可能具有的值。
只有利用数据模型中的关系,才能在 DAX中获取交叉联接行为。
3、使用 NATURALLEFTOUTERJOIN 或NATURALINNERJOIN 关系
在 SQL 中考虑这些语法:
SELECT *FROM a
LEFT OUTER JOIN bON a.key = b.key
SELECT *FROM a
INNER JOIN bON a.key = b.key
(1)如果两个表之间存在连接关系,则可以使用 NATURALLEFTOUTERJOIN或 NATURALINNERJOIN 函数分别在 DAX中编写等效的语法。
例如,此查询返回在产品中有销售额的所有行,其中只包括两个表中的所有列的唯一值。 EVALUATE
NATURALINNERJOIN ( Sales,Product )
以下查询返回产品中的所有行,并显示没有销售的产品。俗称“外联接"。
EVALUATE
NATURALLEFTOUTERJOIN ( Product,Sales )
在这两种情况下,定义关系的列仅在结果中出现一次,其中包括两个表的所有其他列。 (2) NATURALLEFTOUTERJOIN 和 NATURALINNERJOIN 函数也可以与没有关系的表一起使用。但在这种情况下,该列不能存在与数据模型的物理列相对应的数据沿袭关系,正如本文后面所解释的那样。
4、使用 CROSSJOIN定义不带关系的联接表
请在 SQL 中考虑此语法:
SELECT * FROM a CROSS JOIN b
您可以使用 CROSSJOIN 函数在 DAX中编写等效语法:
EVALUATE
CROSSJOIN ( a,b )
这里通过使用CROSSJOIN,实现笛卡尔积的表。相当于提前创建了一个表,然后这个表首先被筛选出符合两个表(a、b)条件的列值,所以CROSSJOIN实现两个表条件的组合联接。
5、附加:使用CALCULATETABLE ,解决多对多关系表的处理
这个多对多的案例,已经有多个解释。本文从关系连接表的概念来加以说明。
上图数据模型中,存在A、B、C、D四个一对多关系连接成的五个表的数据模型。不难理解:
(1)其中,该模型里存在两个事实表01和02:tb订单与tb进货单,并分别由此关系构成另外两个可独立计算的列表子集,01:tb客户→ tb订单 ← tb产品,以及02:tb货源地→ tb进货单 ← tb产品,
(2)01模型的tb订单表里有客户、产品信息记录(因而tb订单表是这两个表的多端表,即事实表),同理,02模型的进货单表对应tb货源地、tb产品表,是这两个表的事实表。其实所谓的多对多(其内部将被压缩引擎处理成特殊的一对一关系:比如多端列表中相同的值被压缩为同一字典符,从而与一端表构成联接,这里从略)并没有那么复杂,你只要记得:
(3)如果要计算的列表为关系列表的一端表时(图中的tb客户、tb产品、tb货源地三个关系的一端表),使用正常的一对多的关系计算将产生错误的结果。 我们先在01所在的列表子集下计算,例如:计算包含订单的产品计数:该计算渉及两个表,有以下两个DAX:请考虑哪个计算正确?
产品数 = CALCULATE( COUNTROWS( 'tb产品' ) )
产品数_连接 = CALCULATE ( COUNTROWS( 'tb产品' ), 'tb定单' )
第一个公式的计算结果总是产品表的计数(结果值相同),因为关系只能从一端传递到多端, 'tb订单' 表属于关系的多端,不能传递到tb产品表的一端。
(4)很显然,第一个公式的错误结果,首先是因为缺少关系的传递(无法从多端传递到一端)。知道了这个原因,那么,我们要做的就是在原公式上加上事实表,来重新定义列表关系,也就是在原有度量后面加上原列表关系多端的事实表。
当然,求另一个一端表:tb产品的计数(tb订单表的产品计数),也是一样:
客户数 = CALCULATE( COUNTROWS( 'tb客户' ) )
客户数_1连接 = CALCULATE( COUNTROWS( 'tb客户' ) , 'tb订单'))
同理,我们针对02所在的列表集,可以采取同样的计算定义。因为这两个数据模型结构相同。 (5)最后,我们还可以针对01和02两个数据集计算,你可以将它们看成一个扩展表。基于前面的分析,这里需要在两个事实表之间建立起关系。也就是,在前面 [客户数_1连接] 度量里再加上一个连接表:
客户数_2连接 = CALCULATE ( COUNTROWS ('tb客户') ,
CALCULATETABLE ( 'tb订单', 'tb进货单') )
该公式使用了 CALCULATETABLE强制DAX连接了两个事实表,与原计算度量构成新的计算列表。( 原理是:内部存储引擎先将'tb订单'、 'tb进货单'缓存在内存,作为存储数据提供给由CALCULATETABLE定义的公式引擎引用,其内部构成新的连接表 ),对于这个如果不好理解,你只要记得以上的连接方式即可。很简单,它适用于那些:计算列表位于关系一端的查询表的情况。
将该公式理解为连接表模式,可能更利于你的了解,我们不妨使用前面使用过的连接表函数: NATURALLEFTOUTERJOIN来替代CALCULATETABLE:
客户数_内连接=CALCULATE( COUNTROWS ('tb客户'),
NATURALLEFTOUTERJOIN ( 'tb订单', 'tb进货单') )
而且,使用NATURALLEFTOUTERJOIN的好处是,无论这两个连接表之间是否存在关系,后面就要讨论。我们将上述的陈述总结出公式模式:
(1)度量值_1个事实表 = CALCULATE( 度量值,事实表1);
(2)度量值_2个事实表 = CALCULATE( 度量值,CALCULATETABLE(表1,表2))
(3)度量值_2个事实表 = CALCULATE( 度量值, NATURALLEFTOUTERJOIN(表1,表2)) 有人可能要问,有三个或更多连接表时,怎么办?理论上是可以的,但同时也要注意:其实关键不是联接表的多少,而是你使用的数据模型结构。因为使用的是整个表的连接,存储数据处理是个不得不考虑的问题。当然,更好的办法是考虑不同的方式来处理处理不同的列表关系问题。
例如,下一部分:DAX 联接表系列(四),我们将讨论使用CONTAINS、INTERSECT、TREATAS等高级列表函数、以及UNIN、EXCEPT等列表集合函数来构建列表关系(联接表)计算。
6、使用没有关系的 NATURALLEFTOUTERJOIN 和 NATURALINNERJOIN
NATURALLEFTOUTERJOIN 和 NATURALINNERJOIN 函数也可以联接没有关系的表。这种情况下,联接条件必须基于所涉及的表中具有相同名称的列,但这些列不能具有与数据模型的物理列相对应的数据沿袭关系(不使用原有物理列表关系)。因为,这会导致在查询数据模型的物理表时产生混淆。
例如,请考虑连接两个称为 P_A (列:ProductKey、Code和Color) 和 P_B (列:ProductKey、Name和Bland) 的物理表,而无需任何关系。
这两个表具有共同的ProductKey列,你不能使用前面的两个连接函数来连接这两个表,因为这样的列在模型中具有相同的名称,以及不同的数据沿袭关系(列表“关系混乱”)。事实上,下面的代码会产生一个错误:
EVALUATE
NATURALLEFTOUTERJOIN( P_A,P_B )
生成的错误显示: "未检测到公用联接列“。join 连接函数: NATURALLEFTOUTERJOIN要求至少有一个”公共联接列 ",否则在执行 NATURALINNERJOIN 时会显示类似的消息。
为了联接两个同名且没有关系的列,很显然,必须先使得这些列没有数据沿袭关系。要达到这个目的,需要使用中断数据沿袭关系的表达式来编写该列,如下面的示例所示:
(1)第一种方法:处理列表关系沿袭
EVALUATE
VAR A = SELECTCOLUMNS (P_A,"ProductKey",P_A[ProductKey]+0,
"Code",P_A[Code],"Color",P_A[Color] )
VAR B = SELECTCOLUMNS ( P_B,"ProductKey",P_B[ProductKey]+0,"Name",P_B[Name],"Brand",P_B[Brand] )
VAR Result = NATURALLEFTOUTERJOIN ( A,B )
RETURN Result
(2)第二种方法: 这里顺便先提一下,从性能的角度来看,还存在更好的解决方案,例如使用 TREATAS的方法:
EVALUATE
VAR B_TreatAs = TREATAS ( P_A,P_B[ProductKey],P_A[Code],P_A[Color] )
-- 第一个参数 :它通过连接初始列表P_A、P_B[ProductKey]筛选出一个唯一值的值列表,然后将此值列表传递给第二个参数 (P_A[Code],P_A[Color]的连接表),结果为一个表:B_TreatAs 。详细解解释见下一部分呢内容。然后将定义的结果表B_TreatAs与P_B联接: VAR Result = NATURALLEFTOUTERJOIN ( B_TreatAs,P_B )
RETURN Result
这两种解决方案都有一个共同的目的: 为 DAX中的JOIN联接函数提供两个表,其中有一个或多个具有相同数据沿袭的列,这样的列将用于联接两个表并生成结果。
在 Excel 2013 和Analysis Services 2012/2014 中有两个DAX表连接函数,以前版本的 DAX没有 NATURALLEFTJOIN 和 NATURALINNERJOIN。但可以通过使用 CROSSJOIN 将表达式嵌入到筛选器中,来可以获取内部的等效项,但如果你必须聚合结果 (稍后将看到),则不建议这样做。在 SQL 中考虑以下内部联接:
SELECT * FROM a
INNER JOIN b ON a.key = b.key
您可以使用以下表达式在 DAX中编写等效语法:
EVALUATE
FILTER ( CROSSJOIN ( a,b ),a[key] = b[key] )
在 DAX的旧版本中,没有简单的获取该语法的方法 (它将最多产生2014个连接值),对应于 SQL 中的左联接。但是,如果您可以假定左侧的表和右侧的表之间存在多对一关系,则可以选择它。这是在 DAX中使用关系的左联接情况。
如果连接的表存在关系,这种情况下,我们已经介绍了使用RELATED.函数的DAX解决方案,如果关系不存在,则还可以改用 LOOKUPVALUE 函数方案。例如,考虑以前看到的那个相同的 SQL 查询:
SELECT s.*,d.Year,p.Color
FROM Sales s
LEFT JOIN Date d ON d.DateKey = s.DateKey
LEFT JOIN Product p ON p.ProductKey = s.ProductKey
你可以使用LOOKUPVALUE 模式的 DAX如下:
EVALUATE
ADDCOLUMNS (Sales,
"Year",LOOKUPVALUE ( 'Date'[Year],'Date'[DateKey],Sales[DateKey]),
"Color",LOOKUPVALUE ( Product[Color],Product[ProductKey],Sales[ProductKey]))
如果存在关系,使用RELATED函数的版本效率更高,但是,如果不存在关系,则LOOKUPVALUE模式可能是一个很好的选择。最后,考虑在 SQL 中聚合左联接结果的查询表,如前面所示 (只添加了 order by 子句):
SELECT d.Year,p.Color,SUM ( s.Quantity ) AS [Total Quantity]
FROM Sales s
LEFT JOIN Date d ON d.DateKey = s.DateKey
LEFT JOIN Product p ON p.ProductKey = s.ProductKey
GROUP BY d.Year,p.Color
ORDER BY d.Year,p.Color
7、目前的两种连接表方法
第一种,利用 LOOKUPVALUE 语法,聚合结果,如下面的 DAX语法所示:
EVALUATE
SUMMARIZE (
ADDCOLUMNS ( Sales,"Sales[Year]",
LOOKUPVALUE ('Date'[Year],'Date'[DateKey],Sales[DateKey]),"Sales[Color]", LOOKUPVALUE (Product[Color],Product[ProductKey],Sales[ProductKey])),
Sales[Year],Sales[Color],"Total Quantity",
CALCULATE ( SUM ( Sales[Quantity] ) ))
ORDER BY Sales[Year],Sales[Color]
但是,如果聚合列的组合数(基数)很小,并且聚合表中的行数很大,则可以在某些条件下考虑以下更详细的方法,该度量速度更快:
DEFINE
MEASURE Sales[Total Quantity] =
CALCULATE ( SUM ( Sales[Quantity] ),
FILTER ( ALL ( Sales[ProductKey] ),
CONTAINS ( VALUES ( Product[ProductKey] ),Product[ProductKey],Sales[ProductKey])), FILTER ( ALL ( Sales[DateKey] ),
CONTAINS ( VALUES ( 'Date'[DateKey] ),'Date'[DateKey],Sales[DateKey] )))
EVALUATE
FILTER ( ADDCOLUMNS (
CROSSJOIN ( ALL('Date'[Year]),ALL ( Product[Color] ) ),
"Total Quantity",[Total Quantity]),NOT ISBLANK ( [Total Quantity] ))
ORDER BY 'Date'[Year],Product[Color]
8、结论
在 DAX中,联接表的最佳方式始终是利用数据模型中的物理关系。因为它使DAX代码更简单、更快速。在 DAX中有几种技术可供创建表。这些可用于在度量值和计算列中使用的复杂表达式中生成计算表或表的子集。但是,从性能角度来看,这些技术更贵,也会导致更复杂的 DAX代码。