了解 Direct Lake 语义模型的存储
本文介绍 Direct Lake 存储概念。 它介绍了 Delta 表和 Parquet 文件。 它还介绍了如何优化 Direct Lake 语义模型的 Delta 表,以及如何维护这些表来帮助提供可靠、快速的查询性能。
Delta 表
Delta 表位于 OneLake 中。 它们将基于文件的数据组织成行和列,可用于Microsoft Fabric 计算引擎,例如笔记本、Kusto 以及湖屋和仓库。 可以使用 Data Analysis Expressions (DAX)、多维表达式 (MDX)、T-SQL (Transact-SQL)、Spark SQL 甚至 Python 来查询 Delta 表。
注意
Delta - 或 Delta Lake 是开源存储格式。 这意味着 Fabric 还可以查询由其他工具和供应商创建的 Delta 表。
Delta 表将其数据存储在 Parquet 文件中,这些文件通常存储在 Direct Lake 语义模型用于加载数据的湖屋中。 但是,Parquet 文件也可以存储在外部。 可以使用 OneLake 快捷方式来引用外部 Parquet 文件,该快捷方式指向特定存储位置,例如 Azure Data Lake Storage (ADLS) Gen2、Amazon S3 存储帐户或 Dataverse。 在几乎所有情况下,计算引擎都可以通过查询 Delta 表来访问 Parquet 文件。 但是,Direct Lake 语义模型通常使用称为“转码”的进程直接从 OneLake 中优化的 Parquet 文件加载列数据。
数据版本控制
Delta 表包含一个或多个 Parquet 文件。 这些文件附带一组基于 JSON 的链接文件,后者用于跟踪与 Delta 表关联的每个 Parquet 文件的顺序和特性。
请务必注意,基础 Parquet 文件本质上是增量文件。 因此,Delta 这一名称是对增量数据修改的引用。 每当对 Delta 表执行写入操作(例如插入、更新或删除数据)时,都会创建新的 Parquet 文件,它们将数据修改表示为“版本”。 因此,Parquet 文件不可变,这意味着它们永远不会被修改。 因此,可以在 Delta 表的一组 Parquet 文件中多次复制数据。 Delta 框架依赖于链接文件来确定生成正确的查询结果需要哪些物理 Parquet 文件。
请考虑一个简单的 Delta 表示例,本文使用它来解释不同的数据修改操作。 该表有两列,并存储三行内容。
ProductID | StockOnHand |
---|---|
A | 1 |
B | 2 |
C | 3 |
Delta 表数据存储在包含所有数据的单个 Parquet 文件中,并且有一个链接文件包含有关数据插入(追加)时间的元数据。
- Parquet 文件 1:
- ProductID:A, B, C
- StockOnHand:1, 2, 3
- 链接文件 1:
- 包含创建
Parquet file 1
时的时间戳,以及追加数据的记录。
- 包含创建
插入操作
考虑执行插入操作时会发生什么情况:为存货价值为 4
的产品 C
插入新行。 此操作会导致创建新的 Parquet 文件和链接文件,因此现在有两个 Parquet 文件和两个链接文件。
- Parquet 文件 1:
- ProductID:A, B, C
- StockOnHand:1, 2, 3
- Parquet 文件 2:
- ProductID:D
- StockOnHand:4
- 链接文件 1:
- 包含创建
Parquet file 1
时的时间戳,以及追加数据的记录。
- 包含创建
- 链接文件 2:
- 包含创建
Parquet file 2
时的时间戳,以及追加数据的记录。
- 包含创建
此时,Delta 表的查询将返回以下结果。 结果是否源自多个 Parquet 文件并不重要。
ProductID | StockOnHand |
---|---|
A | 1 |
B | 2 |
C | 3 |
D | 4 |
每次后续插入操作都会创建新的 Parquet 文件和链接文件。 这意味着每次插入操作都会增加 Parquet 文件和链接文件的数量。
更新操作
现在,请考虑在执行更新操作时发生的情况:产品 D
的存货价值行已更改为 10
。 此操作会导致创建新的 Parquet 文件和链接文件,因此现在有三个 Parquet 文件和三个链接文件。
- Parquet 文件 1:
- ProductID:A, B, C
- StockOnHand:1, 2, 3
- Parquet 文件 2:
- ProductID:D
- StockOnHand:4
- Parquet 文件 3:
- ProductID:C
- StockOnHand:10
- 链接文件 1:
- 包含创建
Parquet file 1
时的时间戳,以及追加数据的记录。
- 包含创建
- 链接文件 2:
- 包含创建
Parquet file 2
时的时间戳,以及追加数据的记录。
- 包含创建
- 链接文件 3:
- 包含创建
Parquet file 3
时的时间戳,以及更新数据的记录。
- 包含创建
此时,Delta 表的查询将返回以下结果。
ProductID | StockOnHand |
---|---|
A | 1 |
B | 2 |
C | 10 |
D | 4 |
产品 C
的数据现在位于多个 Parquet 文件中。 但是,对 Delta 表的查询会合并链接文件,以确定应使用哪些数据来提供正确结果。
删除操作
现在,请考虑在执行删除操作时发生的情况:删除产品 B
的行。 此操作会导致产生新的 Parquet 文件和链接文件,因此现在有四个 Parquet 文件和四个链接文件。
- Parquet 文件 1:
- ProductID:A, B, C
- StockOnHand:1, 2, 3
- Parquet 文件 2:
- ProductID:D
- StockOnHand:4
- Parquet 文件 3:
- ProductID:C
- StockOnHand:10
- Parquet 文件 4:
- ProductID:A, C, D
- StockOnHand:1, 10, 4
- 链接文件 1:
- 包含创建
Parquet file 1
时的时间戳,以及追加数据的记录。
- 包含创建
- 链接文件 2:
- 包含创建
Parquet file 2
时的时间戳,以及追加数据的记录。
- 包含创建
- 链接文件 3:
- 包含创建
Parquet file 3
时的时间戳,以及更新数据的记录。
- 包含创建
- 链接文件 4:
- 包含创建
Parquet file 4
时的时间戳,以及删除数据的记录。
- 包含创建
请注意,Parquet file 4
不再包含产品 B
的数据,但它包含表中所有其他行的数据。
此时,Delta 表的查询将返回以下结果。
ProductID | StockOnHand |
---|---|
A | 1 |
C | 10 |
D | 4 |
注意
此示例很简单,因为它涉及一个小表,只有几项操作,并且只涉及少量修改。 经历多次写入操作并且包含多行数据的大型表将为每个版本生成多个 Parquet 文件。
重要
根据定义 Delta 表的方式和数据修改操作的频率,可能会产生许多 Parquet 文件。 请注意,每个 Fabric 容量许可证都有护栏。 如果 Delta 表的 Parquet 文件数超过 SKU 的限制,则查询将回退到 DirectQuery,这可能会导致查询性能变慢。
若要管理 Parquet 文件的数量,请参阅本文后面的 Delta 表维护。
Delta 按时间顺序查看
链接文件支持从较早的时间点开始查询数据。 此功能称为“Delta 按时间顺序查看”。 较早的时间点可以是时间戳或版本。
请考虑以下查询示例。
SELECT * FROM Inventory TIMESTAMP AS OF '2024-04-28T09:15:00.000Z';
SELECT * FROM Inventory AS OF VERSION 2;
提示
还可以通过以下方法来查询表,即使用 @
速记语法将时间戳或版本指定为表名称的一部分。 时间戳必须采用 yyyyMMddHHmmssSSS
格式。 你可以通过在版本前附加一个 v
在 @
后指定版本。
下面是使用速记语法重写的前面的查询示例。
SELECT * FROM Inventory@20240428091500000;
SELECT * FROM Inventory@v2;
重要
可通过“按时间顺序查看”访问的表版本取决于事务日志文件的保留期阈值以及 VACUUM 操作的频率和指定保留期(稍后将在 Delta 表维护部分中介绍)。 如果使用默认值每天运行 VACUUM,则 可“按时间顺序查看”7 天的数据。
组帧
组帧是一种 Direct Lake 操作,用于设置将数据加载到语义模型列中的 Delta 表的版本。 同样重要的是,版本还可确定加载数据时应排除的内容。
组帧操作会将每个 Delta 表的时间戳/版本标记到语义模型表中。 从这一点来看,当语义模型需要从 Delta 表加载数据时,与最新组帧操作关联的时间戳/版本可用于确定要加载的数据。 自上次组帧操作以来,Delta 表发生的任何后续数据修改都将被忽略(直到下一次组帧操作)。
重要
由于成帧的语义模型引用了特定的 Delta 表版本,因此源必须确保它保持该 Delta 表版本,直到新版本的组帧完成为止。 否则,如果 Delta 表文件需要由模型访问并且已被生成者工作负载清空或以其他方式删除,则用户将遇到错误。
有关组帧的详细信息,请参阅 Direct Lake 概述。
表分区
可以对 Delta 表进行分区,以便将行的子集存储在一组 Parquet 文件中。 分区可以加速查询和写入操作。
考虑一个 Delta 表,其中包含两年内十亿行的销售数据。 尽管可以将所有数据存储在一组 Parquet 文件中,但对于这个数据量来说,这并不是读写操作的最佳选择。 相反,可以通过将十亿行数据分散在多个序列的 Parquet 中来提高性能。
设置表分区时,必须定义分区键。 分区键确定哪些行存储在哪个序列中。 对于 Delta 表,可以根据某个指定列(或多个列)的不同值(如日期表的月份/年份列)来定义分区键。 在本例中,两年的数据将分布在 24 个分区内(2 年 x 12 个月)。
Fabric 计算引擎不知道表分区。 插入新的分区键值时,系统会自动创建新分区。 在 OneLake 中,每个唯一分区键值都有一个子文件夹,每个子文件夹存储其自己的 Parquet 文件和链接文件集。 必须至少存在一个 Parquet 文件和一个链接文件,但每个子文件夹中的实际文件数可能会有所不同。 当数据修改操作发生时,每个分区都会维护其自己的 Parquet 文件和链接文件集,以跟踪给定时间戳或版本返回的内容。
如果仅在已分区的 Delta 表中查询最近三个月的销售数据,则可以快速识别需要访问的 Parquet 文件和链接文件的子集。 这样就可以跳过许多 Parquet 文件,从而提高读取性能。
但是,不筛选分区键的查询可能并不总是性能更好。 当 Delta 表将所有数据存储在单个大型 Parquet 文件中,并且存在文件或行组碎片时,就会出现这种情况。 尽管可以在多个群集节点之间从多个 Parquet 文件并行进行数据检索,但许多小型 Parquet 文件可能会对文件 I/O 产生不利影响,因此查询性能会受到影响。 因此,在大多数情况下,最好避免对 Delta 表进行分区,除非写入操作或提取、转换和加载 (ETL) 过程可明显从中受益。
分区也有利于插入、更新和删除操作,因为文件活动仅发生在与已修改或已删除行的分区键匹配的子文件夹中。 例如,如果将一批数据插入到分区的 Delta 表中,则会评估数据以确定批处理中存在哪些分区键值。 然后,这些数据将仅定向到分区的相关文件夹。
了解 Delta 表使用分区的方式有助于设计最佳的 ETL 方案,以减少更新大型 Delta 表时所需的写入操作。 通过减少必须创建的任何新 Parquet 文件的数量和大小,可提高写入性能。 对于按月份/年份分区的大型 Delta 表,如上例所述,新数据只会将新的 Parquet 文件添加到最新分区。 前几个日历月的子文件夹保持不变。 如果必须修改前几个日历月的任何数据,则只有相关分区文件夹会接收新的分区和链接文件。
重要
如果 Delta 表的主要用途是用作语义模型(其次是其他查询工作负载)的数据源,则通常最好避免进行分区,而是优化列在内存中的负载。
对于 Direct Lake 语义模型或 SQL 分析终结点,优化 Delta 表分区的最佳方式是让 Fabric 自动管理各个 Delta 表版本的 Parquet 文件。 将管理工作留给 Fabric 应该可通过并行化实现高查询性能,但它不一定能提供最佳的写入性能。
如果必须针对写入操作进行优化,请考虑使用分区根据分区键来优化对 Delta 表的写入操作。 但是,请注意,对 Delta 表进行分区可能会对读取性能产生负面影响。 因此,我们建议你仔细测试读取和写入性能,或许可以通过创建具有不同配置的同一 Delta 表的多个副本来进行比较。
警告
如果在高基数列上进行分区,则可能会导致 Parquet 文件过多。 请注意,每个 Fabric 容量许可证都有护栏。 如果 Delta 表的 Parquet 文件数超过 SKU 的限制,则查询将回退到 DirectQuery,这可能会导致查询性能变慢。
Parquet 文件
Delta 表的基础存储是一个或多个 Parquet 文件。 Parquet 文件格式通常用于“写入一次读取多次”应用程序。 每次通过插入、更新或删除操作修改 Delta 表中的数据时,都会创建新的 Parquet 文件。
注意
可以使用工具(如 OneLake 文件资源管理器)访问与 Delta 表关联的 Parquet 文件。 该文件可以像移动任何其他文件一样轻松地下载、复制或移动到其他目标。 但是,这是 Parquet 文件和基于 JSON 的链接文件的组合,允许计算引擎以 Delta 表的形式对文件发出查询。
Parquet 文件格式
Parquet 文件的内部格式不同于其他常见数据存储格式,例如 CSV、TSV、XMLA 和 JSON。 这些格式按行组织数据,而 Parquet 则按列组织数据。 此外,Parquet 文件格式也与这些格式不同,因为它将数据行组织成一个或多个行组。
Power BI 语义模型的内部数据结构基于列,这意味着 Parquet 文件与 Power BI 有很多共同之处。 这种相似性意味着 Direct Lake 语义模型可以有效地将数据从 Parquet 文件直接加载到内存中。 事实上,可以在几秒钟内加载大量数据。 将此功能与导入语义模型的刷新进行对比,导入语义模型必须检索块或源数据,然后进行处理、编码、存储,最后将其加载到内存中。 导入语义模型刷新操作还会消耗大量的计算(内存和 CPU),并花费相当长的时间来完成。 但是,对于 Delta 表,准备适合直接加载到语义模型的数据的大部分工作都是在生成 Parquet 文件时进行的。
Parquet 文件存储数据的方式
请考虑以下示例数据集。
日期 | ProductID | StockOnHand |
---|---|---|
2024-09-16 | A | 10 |
2024-09-16 | B | 11 |
2024-09-17 | A | 13 |
… |
当以 Parquet 文件格式存储时,从概念上讲,此数据集可能类似于以下文本。
Header:
RowGroup1:
Date: 2024-09-16, 2024-09-16, 2024-09-17…
ProductID: A, B, A…
StockOnHand: 10, 11, 13…
RowGroup2:
…
Footer:
可通过使用字典键替换常用值并应用行程编码 (RLE) 来压缩数据。 RLE 会努力将一系列相同的值压缩为较小的表示形式。 在以下示例中,在标头中创建了一个数字键到值的字典映射,并使用较小的键值来代替数据值。
Header:
Dictionary: [
(1, 2024-09-16), (2, 2024-09-17),
(3, A), (4, B),
(5, 10), (6, 11), (7, 13)
…
]
RowGroup1:
Date: 1, 1, 2…
ProductID: 3, 4, 3…
StockOnHand: 5, 6, 7…
Footer:
当 Direct Lake 语义模型需要数据来计算按 ProductID
分组的 StockOnHand
列的总和时,只需要与这两列关联的字典和数据。 在包含许多列的大型文件中,可以跳过 Parquet 文件的大部分内容,以帮助加快读取过程。
注意
Parquet 文件的内容不可读,因此它不适合在文本编辑器中打开。 但是,有许多开放源代码工具可以打开和显示 Parquet 文件的内容。 这些工具还可以让你检查元数据,例如文件中包含的行数和行组数。
V 顺序
Fabric 支持名为“V 顺序”的其他优化。 V 顺序是 Parquet 文件格式的写入时间优化。 应用 V 顺序后,它会生成更小的文件,因此读取速度更快。 此优化尤其适用于 Direct Lake 语义模型,因为它会为快速加载到内存中准备数据,因此对容量资源的需求更少。 它还会带来更快的查询性能,因为需要扫描的内存更少。
由 Fabric 项(例如数据管道、 数据流和笔记本)创建和加载的 Delta 表会自动应用 V 顺序。 但是,上传到 Fabric 湖屋或由快捷方式引用的 Parquet 文件可能未应用此优化。 虽然仍可以读取未优化的 Parquet 文件,但读取性能可能不如应用了 V 顺序的同等 Parquet 文件那么快。
注意
应用了 V 顺序的 Parquet 文件仍符合开源 Parquet 文件格式。 因此,非 Fabric 工具可以读取它们。
有关详细信息,请参阅 Delta Lake 表优化和 V-Order。
Delta 表优化
本部分介绍优化语义模型的 Delta 表的各种主题。
数据量
虽然 Delta 表可以增长以存储海量数据,但 Fabric 容量护栏对查询它们的语义模型施加了限制。 超出这些限制后,查询将回退到 DirectQuery,这可能会导致查询性能变慢。
因此,请考虑通过提高其粒度(存储汇总数据)、减少维度或存储更少的历史记录来限制大型事实数据表的行计数。
此外,请确保应用 V 顺序,因为它会生成更小的文件,因此读取速度更快。
列数据类型
尽量减少每个 Delta 表每一列中的基数(唯一值的数量)。 这是因为所有列都通过使用哈希编码进行压缩和存储。 哈希编码需要 V 顺序优化才能将数字标识符分配给列中包含的每个唯一值。 它是存储的数字标识符,在存储和查询过程中需要进行哈希查找。
使用近似数字数据类型(如浮点数和实数)时,请考虑舍入值并使用较低的精度。
不需要的列
与任何数据表一样,Delta 表应仅存储所需的列。 在本文的上下文中,这表示语义模型所需的列,尽管可能还有其他需要查询 Delta 表的分析工作负载。
除了支持模型关系的列外,Delta 表还应包括语义模型所需的列,用于筛选、分组、排序和汇总。 虽然不需要的列不会影响语义模型查询性能(因为它们不会加载到内存中),但它们会导致更大的存储大小,因此需要更多的计算资源来加载和维护。
由于 Direct Lake 语义模型不支持计算列,因此应在 Delta 表中具体化此类列。 请注意,此设计方法是导入和 DirectQuery 语义模型的反模式。 例如,如果你有 FirstName
列和 LastName
列,并且需要一个 FullName
列,请在将行插入到 Delta 表中时具体化此列的值。
请注意,某些语义模型摘要可能依赖于多个列。 例如,为了计算销售额,模型中的度量值会求以下两列的乘积:Quantity
和 Price
。 如果这两列都不单独使用,那么将销售计算具体化为单个列比将其组成值存储在单独的列中更有效。
行组大小
在内部,Parquet 文件会将数据行组织到每个文件中的多个行组中。 例如,包含 30,000 行的 Parquet 文件可能会将它们分为三个行组,每个行组包含 10,000 行。
行组中的行数会影响 Direct Lake 读取数据的速度。 由于 I/O 过高,包含更少行数的更高行组数可能不利于将列数据加载到语义模型中。
通常,不建议更改默认行组大小。 但是,可以考虑更改大型 Delta 表的行组大小。 请确保仔细测试读取和写入性能,或许可以通过创建具有不同配置的同一 Delta 表的多个副本来进行比较。
重要
请注意,每个 Fabric 容量许可证都有护栏。 如果 Delta 表的行组数超过 SKU 的限制,则查询将回退到 DirectQuery,这可能会导致查询性能下降。
Delta 表维护
随着时间的推移和写入操作的发生,Delta 表版本会逐渐累积。 最终,可能会达到对读取性能产生明显负面影响的地步。 更糟糕的是,如果每个表的 Parquet 文件数或每个表的行组数超过容量的护栏,则查询将回退到 DirectQuery,这可能会导致查询性能下降。 因此,请务必定期维护 Delta 表。
OPTIMIZE
可以使用 OPTIMIZE 来优化 Delta 表,将较小的文件合并为较大的文件。 还可以将 WHERE
子句设置为仅针对与给定分区谓词匹配的筛选行子集。 仅支持涉及分区键的筛选器。 OPTIMIZE
命令还可以应用 V 顺序来压缩和重写 Parquet 文件。
建议定期对大型、经常更新的 Delta 表运行此命令,也许可在每天 ETL 过程完成后运行此命令。 在更高的查询性能与优化表所需的资源使用成本之间权衡取舍。
VACUUM
可以使用 VACUUM 删除不再引用和/或早于设置保留阈值的文件。 请注意设置适当的保持期,否则可能无法按时间顺序查看早于标记到语义模型表中的帧的版本。
重要
由于成帧的语义模型引用了特定的 Delta 表版本,因此源必须确保它保持该 Delta 表版本,直到新版本的组帧完成为止。 否则,如果 Delta 表文件需要由模型访问并且已被生成者工作负载清空或以其他方式删除,则用户将遇到错误。
REORG TABLE
通过重写文件来清除软删除的数据,例如在使用 ALTER TABLE DROP COLUMN 删除列时,可以使用 REORG TABLE 重新组织 Delta 表。
自动执行表维护
若要自动执行表维护操作,可以使用湖屋 API。 有关详细信息,请参阅使用 Microsoft Fabric REST API 管理湖屋。
提示
还可以在 Fabric 门户中使用湖屋表维护功能来简化 Delta 表的管理。