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 版本),你将看到此技术的一些示例。 |