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

理解物化 Materialization

Materialization,也就是物化,是列式数据库进行查询解析时的一个步骤,了解它何时发生以及如何发生是优化 DAX 非常重要的一项知识。

理解引擎计值流

为了帮助理解什么是物化,先看这个简单的查询:

EVALUATE
ROW (
"Result", COUNTROWS ( SUMMARIZE ( Sales, Sales[ProductKey] ) )
)

结果是销售表中产品键的不重复计数。即使我们还没有介绍查询引擎(将在圣经 2.0 版本中介绍),你也可以想象 VertiPaq 是如何执行这个查询的。因为唯一被查询的列是 ProductKey,所以它只能扫描该列,在列的压缩结构中查找所有值。在扫描过程中,它跟踪在位图索引中找到的值,在结束时,它只需要对标记的位进行计数。由于组的并行计算特性,这个查询可以在非常大的表上以很快的速度运行,它唯一需要分配的内存是用于对 ProductKey 进行计数的位图索引

上面的查询在压缩之后的列上运行。换句话说,不需要对列进行解压缩,也不需要重新构建原始表来解析它。这个特性可以优化查询时的内存占用,以减少内存读取。

同样的情况也发生在更复杂的查询中。请看以下代码:

EVALUATE
ROW (
    "Result", CALCULATE (
        COUNTROWS ( Sales ),
        Product[Brand] = "Contoso"
    )
)

这一次,我们使用两个不同的表:Sales 和 Product。运行这个查询需要引擎做更多的工作。因为筛选器在产品表上,而要聚合的是销售表,所以不能只扫描单个列。

如果你还不习惯列式数据库,你可能会认为公式将迭代销售表,跟踪与产品的关系,如果产品品牌是 Contoso,则求和时标记为 1,否则标记 0。因此,你可能会想到下面这种类似的算法:

EVALUATE
ROW (
    "Result", SUMX (
        Sales,
        IF (
            RELATED ( Product[Brand] ) = "Contoso",
            1,
            0
        )
    ) 
)

这种算法虽然看上去简单,但它隐藏的复杂性远远超过预期。实际上,如果仔细考虑 VertiPaq 的列式特性,这个查询涉及三个不同的列:

  • Product[Brand]用于筛选产品表。
  • Product[ProductKey]用于跟踪产品表与销售表之间的关系。
  • Sales[ProductKey]用于从销售表一端跟踪关系。

迭代 Sales[ProductKey],每行扫描 Product[ProductKey]检索对应的产品表的行号,最后在 Product[brand]中收集品牌信息,整套操作的开销会非常大,需要对内存进行大量随机读取,从而对性能产生负面影响。实际上,VertiPaq 使用了一种完全不同的算法,针对列式数据库进行了优化。

内部发生了什么?

首先,它扫描 Product[Brand]并检索值为 Contoso 的行号。如下图所示,它扫描 Brand dictionary(步骤 1),检索 Contoso 的编码,最后扫描 segment(步骤 2)搜索 ID = 0 的行号,将索引返回到所找到的行(步骤 3)。

扫描后输出品牌等于 Contoso 的行列表

此时,VertiPaq 知道产品表中的哪些行具有给定的品牌。产品表和销售表之间的关系基于 Products[ProductKey],此时 VertiPaq 只知道行号。此外,重要的是记住筛选器将用于销售表,而不是产品表。因此,实际上 VertiPaq 并不需要 Product [ProductKey]的值,它真正需要的是 Sales[ProductKey]值的集合,即销售表中的 ID,而不是产品表中的 ID。

VertiPaq 在内部将关系存储为产品表中的行号和 Sales[ProductKey]中的数据 ID 之间的配对。事实证明,这是将筛选器从产品表的行号移动到销售表中的 ProductKeys 的完美数据结构。VertiPaq 对所选行号执行查找,以确定这些行对应的 Sales[ProductKey]值:

VertiPaq 扫描 Product Key 以检索 brand 等于 Contoso 的 ID

最后一步是将筛选器应用于销售表。因为我们已经有了 Sales[ProductKey]的值列表,所以扫描 Sales[ProductKey]列就足以将这个值列表转换为行号,并最终对其进行计数。如果不是计算 COUNTROWS,而是对某列求和,那么 VertiPaq 将执行另一个步骤,将行号转换为列值来执行最后一步。

如你所见,这个过程由简单的表扫描组成,在每个步骤中,你访问单个列。然而,由于列中的数据位于相同的内存区域,VertiPaq 顺序读取内存块并对其执行简单操作,每次输出一小片数据结构在接下来的步骤中使用。

在 VertiPaq 中解析查询的过程和我们预想的有很大区别。一开始,你可能不习惯用列式思维而是按表的结构来考虑问题。VertiPaq 的算法针对列扫描进行了优化;表的概念是列式数据库中被淡化

物化的介入

然而在某些情况下,引擎不能使用这些算法,而是需要恢复到表扫描。例如,下面这个查询:

EVALUATE
ROW (
    "Result", COUNTROWS (
        SUMMARIZE (
            Sales,
            Sales[ProductKey],
            Sales[CustomerKey]
        )
    )
) 

这个查询看起来非常简单,但简单的背后也暴露了列式数据库的局限性(行数据库也面临相同挑战)。查询返回产品和客户的唯一配对的计数。此查询无法通过分别扫描 ProductKey 和 CustomerKey 来解决。这里惟一的选项是构建一个包含惟一的 ProductKey 和 CustomerKey 键值对的表,最后计算其中的行数。换句话说,这一次 VertiPaq 必须构建一个表,即使只有这两列,它也不能直接在原始存储上执行查询。

物化的定义和总结

构建一个包含部分结果的表,并在稍后扫描该表以计算最终值,称为物化(Materialization)。物化发生在很多查询中,它本身没有好坏之分。这完全取决于被物化的表的大小

物化发生在查询过程中,此时最重要的因素是时间,这意味着公式必须尽可能快的得出结果,所以物化的临时表不会被压缩,因为压缩它们将花费大量时间

值得注意的是,当你从一个表中访问多列时,不会发生物化。这完全取决于你对这些列做了什么。例如,下面这样的查询不需要任何物化,即使它访问两个不同的列:

EVALUATE
ROW (
    "Result", SUMX (
        Sales,
        Sales[Quantity] * Sales[Net Price]
    )
)

VertiPaq 在扫描两列时执行乘法然后求和,因此不需要物化带有 Quantity 和 NetPrice 的表。然而,如果表达式变得更加复杂,或者需要进一步处理该表(如前一个示例中需要 COUNTROWS 的情况),则可能需要物化。

在极端情况下,物化可能会使用大量内存(有时甚至超过整个数据库的大小)并生成非常慢的查询。当这种情况发生时,你唯一的做法是重写计算或修改模型,使 VertiPaq 不需要生成物化表来应答。在后面的章节中(2.0 版本),你将看到此技术的一些示例。

说点什么

avatar
1000
 
鼓掌微笑开心憧憬爱你色并不觉得吃瓜doge二哈喵喵思考笑哭捂脸悲伤大哭抓狂汗偷笑打脸捂眼黑线问号晕拜拜闭嘴衰咒骂ok作揖
  订阅本文评论  
提醒
DAX 圣经

导读

初识 DAX

DAX 基础知识

DAX 原理

DAX 高级原理

基础函数类型

迭代函数

CALCULATE 函数

CALCULATE 调节器

基础表函数

条件判断函数

查找匹配函数

时间智能函数

统计类函数

投影函数

分组/连接函数

集合函数

其他函数