理解 DAX 中的数据沿袭
本文翻译自国际Power BI大师Alberto Ferrari的文章——《Understanding data lineage in DAX》,该文介绍了数据沿袭的定义以及数据沿袭的执行方法。
数据沿袭是DAX的一个功能,但是大多数开发人员在使用它时并不知道它的存在,本文主要讲解数据沿袭及其如何帮助我们生成更优的DAX代码。
什么是数据沿袭?
首先,数据沿袭是分配给表中的每一列的标记,它标识数据模型中产生列值的原始列。例如,下面的查询返回Product表中的不同类别:
EVALUATE VALUES ( 'Product'[Category] )
结果包含8行,每个类别一行:
通过VALUES函数返回包含8个字符串的表。然而,它们不只是字符串。DAX知道这些字符串源自Product[Category]列。因此,作为Product表的列,它们继承了在筛选器通过关系传播之后筛选模型中其他表的能力。这就是上下文转换迭代 VALUES ( Product[Category] )筛选Sales表的原因。我们看下以下查询:
EVALUATE
ADDCOLUMNS (
VALUES ( 'Product'[Category] ),"Amt", [Sales Amount]
)
查询结果包含每个产品类别的销售额值:
字符串“Audio”本身不能筛选Sales表。通过运行以下查询你可以很容易地检查这个结果:
EVALUATE
VAR Categories =
DATATABLE (
"Category", STRING,
{
{ "Category" },
{ "Audio" },
{ "TV and Video" },
{ "Computers" },
{ "Cameras and camcorders" },
{ "Cell phones" },
{ "Music, Movies and Audio Books" },
{ "Games and Toys" },
{ "Home Appliances" }
}
)
RETURN
ADDCOLUMNS ( Categories, "Amt",
[Sales Amount] )
后一个查询在Amt列中为所有行返回相同的值:
列名称和列内容都不重要。真正重要的只是列的数据沿袭,它是检索值的原始列。(也就是说,数据沿袭指的是不管列名称和列内容是什么,它的检索值都是原始列。)如果列被重命名,则仍然维护数据沿袭。实际上,下面的查询为每一行返回不同的值:
EVALUATE
ADDCOLUMNS (
SELECTCOLUMNS (
VALUES ( 'Product'[Category] ),
"New name for Category",
'Product'[Category]
),
"Amt", [Sales Amount]
)
“New name for Category”列仍然保持Product[Category]的数据沿袭。因此,尽管结果中的列名与原始列名不同,结果依然显示按类别划分的销售额。
只要表达式仅由列引用生成,就会保持数据沿袭。例如,在前面的表达式中向Product[Category]添加空字符串不会改变列内容,而会破坏数据沿袭。在下面的代码中,Category的新名称的来源是一个表达式,而不仅仅是一个列引用。因此,新列具有与模型的任何源列无关的新数据沿袭。
EVALUATE
ADDCOLUMNS (
SELECTCOLUMNS (
VALUES ( 'Product'[Category] ),
"New name for Category",
'Product'[Category] & ""
),
"Amt", [Sales Amount]
)
结果如预期所示,所有行的Amt值都是相同的:
每个列都有自己的数据沿袭,即使一个表包含来自不同表的列。因此,表的计算结果可以同时对多个表应用筛选器。在下面的查询中可以清楚地看到,该查询包含Product[Category]和Date[Calendar Year]。这两列都通过上下文转换产生的筛选上下文将它们的筛选应用于Sales Amount度量。
EVALUATE
FILTER (
ADDCOLUMNS (
CROSSJOIN (
VALUES ( 'Product'[Category] ),
VALUES ( 'Date'[Calendar Year] )
),
"Amt", [Sales Amount]
),
[Amt] > 0
)
结果显示不同类别和年份的Sales amount值,Product[Category]和Date[Calendar Year]都对于Sales Amount度量起到了筛选作用。
即使数据沿袭是由引擎以完全自动化的方式执行的,开发人员也可以选择更改表的数据沿袭。TREATAS就可以做这项工作,TREATAS接受表作为其第一个参数,第二个参数是表中的列(TREATAS,意思是TREAT AS,可以理解为【对待 参数1 作为 参数2】)。
TREATAS返回相同的输入表,每个列都使用指定为参数的列引用的数据沿袭进行标记。如果表中的某些值包含与用于应用数据沿袭更改的列中的有效值不对应的值,则TREATAS将从输入中剔除这些值。
例如,下面的查询构建了一个包含字符串列表的表,其中一个字符串“Computers and geeky stuff”与模型中的任何类别都不相关。我们使用TREATAS强制表的数据沿袭到Product[Category]。
EVALUATE
VAR Categories =
DATATABLE (
"Category", STRING,
{
{ "Category" },
{ "Audio" },
{ "TV and Video" },
{ "Computers and geeky stuff" },
{ "Cameras and camcorders" },
{ "Cell phones" },
{ "Music, Movies and Audio Books" },
{ "Games and Toys" },
{ "Home Appliances" }
}
)
RETURN
ADDCOLUMNS (
TREATAS (
Categories,
'Product'[Category]
),
"Amt", [Sales Amount]
)
结果包含按类别划分的Sales Amount,但是输出中缺少包含Computers and geeky stuff内容的行。
在数据模型中没有名为“Computers and geeky stuff”的类别时,因此TREATAS必须从输出中剔除该行,以完成数据沿袭转换。
执行数据沿袭
现在我们已经了解了数据沿袭是什么,以及如何使用TREATAS执行数据沿袭。现在我们来看看一个示例,其中由TREATAS和数据沿袭生成的DAX代码,此代码需求是只在每个产品销售的第一天计算Sales Amount。同样的计算对于customer、store或任何其他维度都是有意义的,但是目前只考虑本示例中的产品。
由于每种产品都有不同的首次销售日期,第一种方法是计算在每个产品基础上迭代的第一个销售日期,然后计算该日期的Sales Amount,最后聚合所有产品的结果,代码如下:
FirstDaySales v1 :=
SUMX (
'Product',
VAR FirstSale =
CALCULATE (
MIN ( Sales[Order Date] )
)
RETURN
CALCULATE (
[Sales Amount],
'Date'[Date] = FirstSale
)
)
以下是代码返回结果:
结果是正确的,但是上面的代码不是最优的,因为它迭代Product表,为每个产品生成上下文转换,并且在日期上应用筛选器,而没有利用任何关系,性能相对来讲比较低。下面我们将看到该度量的其他计算方法试图以一种性能比较高的方式获得同样的结果。
第二种方式计算的第一步是构建一个包含product name和首次销售相应日期的表,然后使用这张表对Sales应用筛选器,与前一段代码相比,下面的代码有所改进,但仍然不是最优的,因为SUMX仍然为每个产品生成一个上下文转换:
FirstDaySales v2 :=
VAR ProductsWithSales =
SUMMARIZE (
Sales,
'Product'[Product Name]
)
VAR ProductsAndFirstDate =
ADDCOLUMNS (
ProductsWithSales,
"Date First Sale", CALCULATE (
MIN ( Sales[Order Date] )
)
)
VAR Result =
SUMX (
ProductsAndFirstDate,
VAR DateFirstSale = [Date First Sale]
RETURN CALCULATE (
[Sales Amount],
'Date'[Date] = DateFirstSale
)
)
RETURN Result
但是如果能将ADDCOLUMNS函数生成的变量ProductsAndFirstDate用作CALCULATE的筛选参数,它将筛选一个product表和一个date表的话,这个版本性能会更好一些(不幸的是,它是错误的):
FirstDaySales v3 (wrong) :=
VAR ProductsWithSales =
SUMMARIZE (
Sales,
'Product'[Product Name]
)
VAR ProductsAndFirstDate =
ADDCOLUMNS (
ProductsWithSales,
"Date First Sale", CALCULATE (
MIN ( Sales[Order Date] )
)
)
VAR Result =
CALCULATE (
[Sales Amount],
ProductsAndFirstDate
)
RETURN Result
上述代码没有SUMX的迭代运算,但是这个版本的代码是有错误的,因为它返回相同的Sales Amount值,而没有应用任何是啊下器。
事实上,由ADDCOLUMNS 生成的变量ProductsAndFirstDate中包含一个产品和一个日期,但从数据沿袭方面来看的话产品名称继承了Product[Product Name]的数据沿袭,但是而Date First Sale 列继承 Sales[Order Date]的数据沿袭,而是生成了MIN表达式的结果。Date First Sale列有自己的数据沿袭,与数据模型中的其他表无关。
正确的优化度量如下:
FirstDaySales v4 :=
VAR ProductsWithSales =
SUMMARIZE (
Sales,
'Product'[Product Name]
)
VAR ProductsAndFirstDate =
ADDCOLUMNS (
ProductsWithSales,
"First Sale", CALCULATE (
MIN ( Sales[Order Date] )
)
)
VAR ProductsAndFirstDateWithCorrectLineage =
TREATAS (
ProductsAndFirstDate,
'Product'[Product Name],
'Date'[Date]
)
VAR Result =
CALCULATE (
[Sales Amount],
ProductsAndFirstDateWithCorrectLineage
)
RETURN Result
上述解决方案并非模型的首要解决方案,然而,在性能方面,这段代码几乎是最优的,也许是我们没有找到性能更好的版本——如果您找到了更好的版本,我们希望在评论中看到它。一旦熟悉了数据沿袭,您就可以像上面的解决方案一样思考,理解筛选器如何使用数据沿袭从一个表传递到另一个表。
总结理解数据沿袭对于DAX开发人员至关重要,虽然它不像行上下文、筛选上下文和上下文转换那样直接有关,但是这个无疑是成为DAX专业开发人员必须要理解的一项技能。
* PowerPivot工坊原创文章,转载请注明出处!
如果您想深入学习微软Power BI,欢迎登录网易云课堂试听学习我们的“从Excel到Power BI数据分析可视化”系列课程。或者关注我们的公众号(PowerPivot工坊)后猛戳”在线学习”。
长按下方二维码关注“Power Pivot工坊”获取更多微软Power BI、PowerPivot相关文章、资讯,欢迎小伙伴儿们转发分享~
Power Pivot工坊
自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)