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)。
此时,VertiPaq 知道产品表中的哪些行具有给定的品牌。产品表和销售表之间的关系基于 Products[ProductKey],此时 VertiPaq 只知道行号。此外,重要的是记住筛选器将用于销售表,而不是产品表。因此,实际上 VertiPaq 并不需要 Product [ProductKey]的值,它真正需要的是 Sales[ProductKey]值的集合,即销售表中的 ID,而不是产品表中的 ID。
VertiPaq 在内部将关系存储为产品表中的行号和 Sales[ProductKey]中的数据 ID 之间的配对。事实证明,这是将筛选器从产品表的行号移动到销售表中的 ProductKeys 的完美数据结构。VertiPaq 对所选行号执行查找,以确定这些行对应的 Sales[ProductKey]值:
最后一步是将筛选器应用于销售表。因为我们已经有了 Sales[ProductKey]的值列表,所以扫描 Sales[ProductKey]列就足以将这个值列表转换为行号,并最终对其进行计数。如果不是计算 COUNTROWS,而是对某列求和,那么 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 版本),你将看到此技术的一些示例。
老师,Vertipaq对于查询的解析优化规则和细节,有没有办法系统了解和学习啊,感觉和SQL查询引擎对语句的解析优化 两者之间有很相似的背景机制,千丝万缕的联系,让我很感兴趣。
在后面的章节中(2.0版本) 现在出来了吗? 在哪儿可以看到呀?
请教老师,“扫描 segment(步骤 2)搜索 ID = 0 的行号,将索引返回到所找到的行(步骤 3)”,这一步行号是自动升序排序的吗,如果是的话,我理解的应该是图片中所示的
老师你好,看完后我大概了解了一些情况下可以避免物化,一些情况下需要进行物化,物化简单来说就是创建中间表,这样理解对吗?
另外我看不懂这两个图,图1中,字典是品牌索引与品牌的键值对,而DataRows中的ID和Count是什么呢?它们DataRows表是存储什么呢?最后那个表中的from和to又表示什么呢?
学习这一章需要勇气。这一章看起来真的挺头痛的,也可能是自己对于这个原理理解不到位。我自己也根据文章中的例子写了一个table1和table2。和文章中一开始提到的自我感知一样,很明显会觉得table1肯定要比table2快很多,毕竟table2又是迭代又是if,怎么也快不起来的感觉。但是经个人测试,答案确实出乎意料,table2还要更快一些。
所以就是有可能就是我们在理解迭代函数的时候,总是会想象查询引擎会真的去在整张表里去逐行迭代,判断,然后求和?实际上查询引擎是会优化这种迭代函数的计算过程,计算比我们想象的要快?
因为之前一直被告诫说是迭代函数不如calculate的简单条件模式快,所以其实在平时的应用中,能够不使用迭代函数的都会用calculate替代,但是这个例子是个例外。
我个人感觉是不是对整个表进行计数是比较影响效率的?因为我自己改成table3里只对某一列计算又会快一些。对算是做个笔记吧,因为没看的太懂,也提不出比较深刻的问题,高飞老师见笑了。
这个图看好多次没明白。请高老师指导。
感觉原理部分特别难懂