数据Lakehouse的概念是由 Uber 的一个团队于 2016 年首创,当时该团队试图解决存储大量大容量更新插入数据的问题。该项目最终成为Apache Hudi[1] ,然后被描述为“事务数据湖”。随着时间的推移,其他组织创建了项目来解耦昂贵的数据仓库计算和存储,利用基于云对象的系统将数据存储为文件。这些项目成为Apache Iceberg[2] (诞生于 Netflix)和 Linux 基金会的Delta Lake[3] (诞生于 Databricks),最终融合为“数据Lakehouse”的术语。
虽然这三个项目都成功地降低了在数据仓库上运行大规模工作负载的成本,并释放了前所未有的数据规模,但工程师很快发现他们需要密切关注性能调整以维持规模化的 Lakehouse 部署。
Apache Hudi 背后的一些团队最终创建了Onehouse[4] ,我们正在其中构建一个通用且真正可互操作的数据 Lakehouse 平台。在 Onehouse,我们的客户和我们支持的许多开源 Lakehouse 项目都让我们看到了有效表性能的重要性。本博客将深入探讨工程团队可以用来提高 Lakehouse 表的写入和读取性能的许多机制,作为未来工程项目的指南。我将重点关注与基于 Apache Hudi 构建的部署相关的功能,但其中许多功能可以在其他 Lakehouse 表格式中找到,并且此处描述的技术可以适用于 Iceberg 和 Delta Lake。
虽然对于我们许多数据工程师来说,这似乎是一个显而易见的话题,但在我们开发数据系统时,对性能表的重要性进行级别设置非常重要。现在,性能的动机以及工程团队努力确保这一点的水平可能因组织而异——坦率地说,甚至因用例而异——总体原则仍然存在。我们都希望以一种能够在业务应用程序中快速查询和使用的方式写入数据,但又不会在摄取和存储方面花费大量成本。这就是 Lakehouse 的性能调整非常重要的地方。
Lakehouse 和开放表格式可以通过存储和计算的解耦来显着节省成本,并利用云超大规模存储服务实现近乎无限可扩展的存储。然而,如果数据没有适当优化,它们也存在对性能产生负面影响的风险。以下是未优化表的几个极端示例,这些示例导致显着的成本和性能超支,有些甚至导致系统性能下降:
在本节中,我们将讨论 Lakehouse 表实现性能提升的常见方法,以及工程师如何利用这些技术来发挥我们的优势。其中许多功能都应用于 Lakehouse 表格式使用的底层 Parquet 文件,因此在使用Apache XTable[5]等互操作性层时,其中一种表格式所取得的收益会反映在所有三种格式中。
创建 Lakehouse 表时,所有三种格式都提供两种表类型的某些版本:写入时复制 (COW)[6]或读取时合并 (MOR)[7] 。这些表类型中的每一种都提供不同的性能特征,并且应该用于不同类型的工作负载。
表类型 | 它是如何运作的 | 何时使用它 |
---|---|---|
COW | 将数据添加到表中时,将为每个具有传入数据的文件组创建新的文件切片(对于插入,将创建新文件组)。因此,当进行插入时,会创建新的 Parquet 文件,但对于更新,Parquet 文件将被重写为包含更新的文件组中的新文件切片。这意味着这些表具有高效的读取性能,并且对于更新相对较少的工作负载(无论是总量还是记录百分比)具有良好的写入性能。 | + 适合读取优化的工作负载+ 非常适合主要是插入的表(> 90% 的操作)+ 简单的批处理作业+ 如果需要快速 OLAP 查询+ 运维负担低 |
MOR | MOR 表处理更新的方式与 COW 表不同。当更新写入表时,MOR 表会创建一系列日志文件,这些文件比重写基本 parquet 文件更轻量级。插入仍作为新文件组处理并写入基本文件。然而,在读取方面,MOR 表创建了额外的约束。为了获得最准确的数据视图,查询必须读取基础文件 (parquet) 和日志文件。这意味着查询可能需要比以前更长的时间。然而,MOR 表提供读取优化查询,其中引擎仅查询存在的基本文件 - 以牺牲数据新鲜度为代价提供更快的读取速度。 | + 适合写入优化的工作负载+ 使处理更新和删除更加高效+ 非常适合流式工作负载+ 更改数据捕获表+ 批处理+流表 |
如果使用 Iceberg 或 Delta Lake也将有类似的功能可供选择。Iceberg 最近添加了对 COW 和 MOR 表的支持[8],而Delta 的删除向量[9]在删除操作上提供了与 MOR 表类似的功能。
分区是指根据特定键将数据分离到不同的位置。该键根据键的值将数据拆分到不同的文件夹(分区)中。下图展示了按 store_id 分区的零售销售数据表。
正如从图中推断的那样,分区可以提供读取器和写入器集群可以利用的一系列好处。
虽然这些都是使用分区的绝佳案例,但可能会问自己:“我如何才能保证分区带来的性能优势?”让我们谈谈分区是如何发挥作用的。
大多数重要的数据项目都有多个层次的管道。数据是原始数据,然后通过管道根据这些原始数据创建一系列下游表。在为表设计分区时,请检查从表读取的管道。在这些管道中,将会有从表中读取数据并提供给管道的查询。在这里需要确保这些查询中的过滤条件与分区方案匹配。
例子:
SELECT sku,
price,
count(transaction_id),
item_category
FROM transactions.clothing_sales
WHERE item_color = "blue"
GROUP BY sku
在这里,我们可以看到表中的查询是根据 item_color 进行过滤的。我们应该确保该表的分区策略也基于 item_color。因此,此查询不必打开“红色”、“绿色”或除“蓝色”之外的任何其他颜色的项目的任何文件。
从我们上一节有关分区的内容来看,策略似乎应该是将分区拆分得尽可能小,将数据拆分为数千甚至数万个分区。然而,这造成的问题远多于它解决的问题(坦率地说,并没有解决那么多问题)。本节重点讨论粗分区的平衡。
在较高级别上,粗粒度分区表示分区不应超过一定数量,并且分区键应保持相对较低的基数(唯一值的数量)。这样做的原因是双重的。
通常,粗粒度分区应包含至少 1 GB 的数据,但分区可以包含更多数据。Databricks 的分区建议[10]是仅对大于 1 TB 的表进行分区,并保持每个分区大于 1 GB。
在Hudi(1.0.0之前的版本,较新版本的Hudi解决了这个问题)和Delta Lake中,每个新分区都被写入数据湖中的一个文件夹。创建这些分区后,它们就不可更改。可以添加新分区 - 并将作为新分区目录添加到表中。假设有一个按项目类别代码分区的表。如果代表冬季夹克的代码更新,例如从“1XY”(旧代码)到“WNTJ”(新代码),旧分区的名称将不会更改,而是将为任何新到达的数据创建一个新分区,其中包含命名为“WNTJ”,数据将被损坏,因为一半冬季夹克数据将存储在“1XY”中,另一半将存储在“WNTJ”中。正如看到的希望使用不会更改的键作为分区键。
Hudi 1.0.0 发布了一种新的分区思维方式。在这个新愿景中,分区被表示为索引的抽象[11](特别是新的表达式索引)。这意味着 Hudi 1.0 分区现在被视为列值上的粗粒度索引。从这里开始,分区统计索引(内置于新的分区机制中)可以实现数据跳过和更快的读取。
对于使用 Iceberg 和 Delta Lake 的人来说,分区仍然是一个有用的策略,尤其是在规模上。Iceberg 甚至具有先进的分区功能,例如分区演化[12],这使得创建数据分区和更改分区名称变得更加容易。
索引于 2020 年首次在 Apache Hudi 中添加到数据Lakehouse中。从那时起,Hudi 将它们用作其性能加速工具库中的不可或缺的工具,并在Hudi 1.0.0 版本[13]中添加了最新的二级索引[14]。这个多模式索引系统[15]由Hudi 元数据表[16]提供支持。因此,在深入研究 Hudi 的索引功能之前,我们将花一些时间讨论元数据表。
Hudi 元数据表是 .hoodie/ 文件夹中的 MoR 表,可用于加速 Hudi 表性能。它存储以下信息:
文件列表 | + 存储在“files”分区+ 存储文件名、大小、活动状态和表分区+ 读取器和写入器不再需要执行昂贵的文件查找、状态检查或文件列表操作 |
---|---|
列统计 | + 存储在“column_stats”分区中+ 包含所选列的统计信息,例如最小值、最大值、值数、空计数、数据大小等+ 允许在查询期间跳过数据,因为统计信息可用于缩小要查询的选择文件的范围 |
分区统计 | + 存储在“partition_stats”分区中+ 聚合每个表分区的指标,允许分区修剪,其中在查询端跳过整个分区以获得读取性能优势 |
以下是将元数据表添加到作业中的方法
写入器配置
writer_metadata_configs = {
"hoodie.metadata.enable": "true", #enables metadata table
"hoodie.metadata.index.column.stats.enable": "true", # (optional)enables columnstats
"hoodie.metadata.index.column.stats.columns":["column_a","column_b","column_c"], #list of columns to track for columns stats
"hoodie.metadata.index.partition.stats.enable": "true" # (optional) enables tracking partition stats
}
读取器配置
reader_metadata_configs = {
"hoodie.metadata.enable":"true", # tells reader to use metadata table
"hoodie.enable.data.skipping": "true" # enables data skipping using column_stats
}
由于我们刚才讨论的高级元数据功能,Hudi 还能够生成索引——这是市场上第一个也是唯一的格式功能。Iceberg 和 Delta 不具备这一点,因为它们没有采用 Hudi 所采用的结构化元数据系统(尽管 Delta 确实提供了数据跳过[17]功能,该功能也内置于 Hudi 中)。这些索引可用于加速读取和写入性能(读取端与写入端索引)。某些索引更适合加速读取性能,而其他索引则更适合写入性能。在本节中,我们将了解不同的 Hudi 索引属性以及如何在 Lakehouse 部署中设置它们。
2022年,Hudi引入了布隆索引,它采用布隆过滤器[18]数据结构来加速跨数据集的查询。此索引在指定的索引键上创建基于哈希的查找,从而加快在表中查找该键是否存在的速度。布隆过滤器通过插入到哈希中的“无误报”保证为值提供零缺失,从而保证查询将获得所有记录的有效哈希命中。
何时使用:布隆过滤器应用于整个数据集,并将最小值和最大值存储在 Parquet 文件页脚中。因此,由于执行更新时每个文件的页脚都会重新散列,因此当有一个大表(大量 Parquet 文件)且整个表中随机更新时,此过滤器会变得很困难。当有一组已知的稍后到达表的数据并且希望利用显着的数据跳过优势来限制插入时扫描的文件时,应该使用 Bloom 索引。
用于启用 Bloom Index 的配置
CREATE INDEX idx_bloom_driver ON hudi_indexed_table USING bloom_filters(driver);
Hudi的索引也创历史新高[19]。这允许用户对单个记录执行快速写入和查找。Hudi记录索引存储在元数据表的record_index分区中。该索引在表上指定的记录键(表全局唯一)上生成哈希,并使用哈希分片创建单个记录的快速查找。
何时使用:如果有大型表,需要从表中搜索和/或更新单个记录或小批量记录。这与查找单个条目或对表中的单个条目进行上下文感知的更新等用例相关。记录级索引极大地加快了这些记录的搜索过程,减少了查询和写入时间。
启用记录索引的配置
CREATE INDEX record_index ON hudi_indexed_table (record_key_column);
在最新发布的 Hudi (1.0.0) 版本中,Hudi 宣布了第一个在辅助列(非记录键)上建立索引[20]的功能。这意味着用户可以在不是 Hudi 记录键的列上设置索引,从而加快表上的非记录键查找速度。
何时使用:对于加速具有非记录键谓词的查询来说,这是一个很好的选择。为了创建这个索引,可以使用随Hudi 1.0发布的SQL语法。
CREATE INDEX secondary_index ON hudi_indexed_table (index_column);
表服务是维持 Lakehouse 表健康和高性能的重要组成部分(在所有三种表形式中)。在本节中,我们将概述所提供的不同服务,并讨论运行这些服务时可以利用的一些改进。这些表服务中的每一个都可以通过不同表格式中略有不同的术语来引用,但会发现许多核心概念仍然相似。对于本节的上下文,我们将主要使用 Hudi 命名约定,并根据需要引用 Iceberg 和 Delta 的变体。
所有 Lakehouse 表格式都需要清理旧文件版本。当对表执行新的写入(“提交”)时,通常会为这些写入生成一个新的文件组。当这种情况发生时,这个新的提交就会出现在时间线中。当然,我们不能无限期地创建太多的新文件组,因为这会大大增加表的存储成本。因此,清理服务会删除文件的旧版本,仅保留配置中指定的所需版本数量。
运行清理服务[21]时,跟踪一些事情很重要。首先必须跟踪需要存储多少表的历史记录。对表执行时间旅行或增量[22]查询并不罕见,可能需要查询数小时或数天前的表版本。可能还想保留该表的先前版本,以防上游管道中出现错误数据。可以将其配置为保留的小时数或保留的提交数。建议将其保持在最低的安全数字,以便可以确信拥有用例所需的历史记录。
第二个考虑因素是运行清理服务的频率。清理服务(与所有表服务一样)使用计算资源来执行,并且不必在每次提交后运行。因此,谨慎的做法是仅以设定的提交间隔运行服务。此间隔最好根据用例和正在处理的数据量来确定。我在下面提供了一个通过Onehouse 的 Table Optimizer[23]运行的示例清理作业,该作业以每 5 次提交的间隔运行清理服务,并保留 2 天的提交历史记录。
对于 Iceberg 和 Delta 表,存在类似的功能,可用于“清理”旧文件版本并回收存储空间和列出操作。Iceberg 有一系列用户可以运行的维护操作[24]来完成此操作,包括使快照过期、删除孤立文件和删除旧的元数据文件。在 Delta Lake 中,清理[25]和日志保留[26]等操作会删除标记为删除的文件(“逻辑删除文件”),因为会添加新文件来替换这些文件。
在每种不同的表格格式中,可以使用高级空间填充技术对 Parquet 文件进行排序和填充。Hudi 将这种操作称为“聚簇”。这意味着数据将填充到 Parquet 文件中,并根据所选键进行排序。这意味着,如果查询具有与选择用于排序的键相匹配的谓词,则根据类似于 Bloom 索引部分中概述的数据跳过原则,将仅扫描一小部分文件来匹配谓词。
Hudi、Delta 和 Iceberg 都支持多个空间填充曲线,有助于有效地对这些 Parquet 文件内的数据进行排序。使用 3 个主要排序选项 — 线性、Z 顺序和希尔伯特曲线。它们各自根据下述特征对数据进行不同的排序。
线性 | 使用这种聚簇或排序策略,Hudi 表的每个分区中的数据文件(假设它是分区表)将按一列或多列排序,并且这些列的顺序起着至关重要的作用。 |
---|---|
Z-Order | Z 排序是一种数据布局优化技术,通过跨多个列对数据进行排序来增强查询性能。它将多维数据映射到一维空间,同时保留数据局部性,这意味着原始多维空间中接近的数据点在一维空间中保持接近。 |
Hilbert Curves | Hilbert curves 与 Z 顺序相似,它是一种空间填充技术。它在底层使用了不同的空间填充机制,但也保留了将多维空间映射到单个维度的特性 - 保留指定键中不同属性之间的关系。 |
Z 阶与希尔伯特曲线图 - 摘自Shiyan Xu 的 Hudi 电子书[27]
在选择使用哪种排序方法时,可以遵循一些一般的经验规则,以便在写入器工作负载和查询性能加速之间保持正确的平衡。在进行了一系列测试并创建了以下说明性场景。
Table_1 在列(A、B、C)上使用线性排序顺序,而 table_2 在同一列(A、B、C)上使用 Z 顺序。现在考虑以下三个查询:
Q1: Select A, count(*) From table Group By A;
Q2: Select B, count(*) From table Group By B;
Q3: Select C, count(*) From table Group By C;
这些查询的性能如下:Q1 对于线性和 Z 顺序的性能大致相同,但 Q2 和 Q3 在 Z 顺序方面的性能明显优于 Q2 和 Q3。
根据测试 - 如果查询谓词与聚类键的顺序匹配(即谓词的形式为 A、A+B 或 A+B+C),那么性能将会很好用于线性排序。在查询谓词的其他排列(B、B+C、A+C 等)中,空间填充曲线(Z 顺序和希尔伯特)将大大优于线性聚类。此外,他发现选择高基数聚簇键可以提高性能,因为它可以最大限度地提高集群并行性并减少每个查询谓词扫描的数据。
Onehouse Table Optimizer[28]使设置和修改集群变得简单直观,只需点击几下鼠标,如下面的屏幕截图所示。它还使用户可以轻松设置以增量方式执行集群的频率,其中表将根据写入的数据进行聚簇,从而最大限度地提高计算效率。
Iceberg[29]和Delta Lake[30]还提供 Z 顺序空间填充曲线来填充以这些格式编写的 parquet 文件,这在编写表时需要在编写器配置中表示。
不同的表格式对术语“压缩”有不同的含义,但无论格式如何,它们都有助于解决数据湖的关键问题:文件大小调整。当数据被引入数据湖系统时,用户经常会发现生成了许多小文件。这些小文件会给读取器和写入器系统带来 I/O 开销,从而成为管道的瓶颈。事实上在 2024 年 re:Invent S3 表公告中,Amazon Web Services 甚至将维护小文件称为“最常见的问题”,这是他们注意到客户在操作大规模数据湖房部署时面临的问题。压缩服务旨在解决这个问题(在Hudi中,压缩指的是不同的东西,我们也将在本节中介绍)。
在 Iceberg 中,文件大小可以通过表维护中定义的压缩命令来定义。[31] Hudi 在写入操作中内置了自动调整文件大小的功能[32]。应该启用一些编写器配置来设置此功能。
hudi_file_sizing_configs = {
"hoodie.parquet.max.file.size"=125829120 #sets max file size to optimal size of # 120 MB
"hoodie.parquet.small.file.limit"=104857600 # limits files to be above 100 MB in size
}
上述配置组合将告诉 Hudi 编写器尝试将所有文件的大小保持在 100-120 MB 之间。这使每个 Parquet 文件的最佳文件大小保持在 120 MB。
Hudi 用术语“压缩”来指代另一个过程。如果还记得我们之前关于表类型的讨论,在 MOR 中表的更改最初写入日志文件中,而不是写入基本 Parquet 文件中。按照设定的时间间隔,这些日志文件需要合并到基本文件中,以便读者在从 MOR 表读取时不必解析一长组日志文件。这个合并过程是由Hudi中的compaction服务完成的。用户必须设置此压缩服务的频率,因为平衡此合并发生的频率对于保持写入日志文件和读取较大 Parquet 基本文件的效率非常重要。Onehouse Table Optimizer 允许通过一种简单且免提的方法来配置此服务并实现压缩服务的最佳性能。下面显示的是设置压缩服务以通过表优化器执行的屏幕截图。
上面描述的每个表服务(聚簇、清理和压缩)都可以以内联或异步机制执行。在内联执行中,定义的表服务由写入者在写入提交发生后顺序执行。这很容易配置,但增加了限制,即新数据可供处理之前的总写入时间延长,从而减慢了端到端写入操作。
这就是使用 Hudi 的多写入器并发控制[33](CC) 的异步表服务发挥作用的地方。在该方法中,主写入器继续处理数据,而辅助写入器对表执行表服务操作。锁提供程序[34]用于确保不同写入者之间不存在写入冲突。这个过程的图表(如下所示)和详细解释可以在Lin Liu 的博客中找到。[35]
正如所看到的,异步和独立的表服务允许更快的端到端写入操作和表上的数据新鲜度,但在设置方面需要大量的额外工作。这就是Onehouse Table Optimizer[36]发挥作用的地方。通过自动化计算资源和锁提供程序编排,用户无需繁重的工作即可实现高性能异步表服务。
正如我们在本博客中所看到的,在格式和存储层上优化 Lakehouse 表性能的选项非常重要。在 Onehouse,我们认识到这一点,并致力于通过我们的数据平台、表优化器[37]中的独立功能以及免费开放的Onehouse LakeView[38]应用程序中提供的可观测性,让客户的体验更易于管理。随着更多 Lakehouse 格式(例如 Apache Hudi、Apache Iceberg 和 Delta Lake)的采用,我们希望这些部署能够继续减轻全球数据平台组织的规模和成本负担。
[1]
Apache Hudi: https://hudi.apache.org/
[2]
Apache Iceberg: https://iceberg.apache.org/
[3]
Delta Lake: https://delta.io/
[4]
Onehouse: https://www.onehouse.ai/?__hstc=17958374.de3d8547d31e71355dda68c54a3ecefe.1721135242002.1736199423630.1736372767573.26&__hssc=17958374.1.1736372767573&__hsfp=130792607
[5]
Apache XTable: https://xtable.apache.org/
[6]
写入时复制 (COW): https://hudi.apache.org/docs/next/table_types/#copy-on-write-table
[7]
读取时合并 (MOR): https://hudi.apache.org/docs/next/table_types/#merge-on-read-table
[8]
支持: https://bigdataenthusiast.medium.com/apache-iceberg-table-formats-bf0c2c09b389
[9]
Delta 的删除向量: https://docs.delta.io/latest/delta-deletion-vectors.html
[10]
建议: https://docs.databricks.com/en/tables/partitions.html#do-small-tables-need-to-be-partitioned
[11]
索引的抽象: https://hudi.apache.org/blog/2024/12/16/announcing-hudi-1-0-0/#partitioning-replaced-by-expression-indexes
[12]
分区演化: https://iceberg.apache.org/docs/1.5.1/evolution/#partition-evolution
[13]
Hudi 1.0.0 版本: https://hudi.apache.org/releases/release-1.0.0/
[14]
二级索引: https://hudi.apache.org/docs/next/indexes#secondary-index
[15]
多模式索引系统: https://www.onehouse.ai/blog/introducing-multi-modal-index-for-the-lakehouse-in-apache-hudi?__hstc=17958374.de3d8547d31e71355dda68c54a3ecefe.1721135242002.1736199423630.1736372767573.26&__hssc=17958374.1.1736372767573&__hsfp=130792607
[16]
Hudi 元数据表: https://hudi.apache.org/docs/metadata/
[17]
数据跳过: https://docs.delta.io/latest/optimizations-oss.html#data-skipping
[18]
布隆过滤器: https://www.geeksforgeeks.org/bloom-filters-introduction-and-python-implementation/
[19]
也创历史新高: https://hudi.apache.org/docs/indexes#record-indexes
[20]
在辅助列(非记录键)上建立索引: https://hudi.apache.org/docs/indexes#secondary-index
[21]
清理服务: https://hudi.apache.org/docs/next/hoodie_cleaner/
[22]
时间旅行或增量: https://hudi.apache.org/docs/next/table_types/#query-types
[23]
Onehouse 的 Table Optimizer: https://www.onehouse.ai/product/table-optimizer?__hstc=17958374.de3d8547d31e71355dda68c54a3ecefe.1721135242002.1736199423630.1736372767573.26&__hssc=17958374.1.1736372767573&__hsfp=130792607
[24]
维护操作: https://iceberg.apache.org/docs/1.5.1/maintenance/
[25]
清理: https://delta.io/blog/remove-files-delta-lake-vacuum-command/
[26]
日志保留: https://docs.delta.io/latest/delta-batch.html#data-retention
[27]
Shiyan Xu 的 Hudi 电子书: https://www.onehouse.ai/whitepaper/ebook-apache-hudi---zero-to-one
[28]
Onehouse Table Optimizer: https://www.onehouse.ai/product/table-optimizer?__hstc=17958374.de3d8547d31e71355dda68c54a3ecefe.1721135242002.1736199423630.1736372767573.26&__hssc=17958374.1.1736372767573&__hsfp=130792607
[29]
Iceberg: https://iceberg.apache.org/docs/1.6.0/spark-procedures/?h=zorder#options-for-sort-strategy-with-zorder-sort_order
[30]
Delta Lake: https://delta.io/blog/2023-06-03-delta-lake-z-order/
[31]
表维护中定义的压缩命令来定义。: https://iceberg.apache.org/docs/latest/maintenance/#compact-data-files
[32]
自动调整文件大小的功能: https://hudi.apache.org/docs/file_sizing/#auto-sizing-during-writes
[33]
并发控制: https://hudi.apache.org/docs/next/concurrency_control/
[34]
锁提供程序: https://hudi.apache.org/releases/release-0.10.0/#dynamodb-based-lock-provider
[35]
Lin Liu 的博客中找到。: https://www.onehouse.ai/blog/table-optimizer-the-optimal-way-to-execute-table-services?__hstc=17958374.de3d8547d31e71355dda68c54a3ecefe.1721135242002.1736199423630.1736372767573.26&__hssc=17958374.1.1736372767573&__hsfp=130792607
[36]
Onehouse Table Optimizer: https://www.onehouse.ai/product/table-optimizer?__hstc=17958374.de3d8547d31e71355dda68c54a3ecefe.1721135242002.1736199423630.1736372767573.26&__hssc=17958374.1.1736372767573&__hsfp=130792607
[37]
表优化器: https://www.onehouse.ai/product/table-optimizer?__hstc=17958374.de3d8547d31e71355dda68c54a3ecefe.1721135242002.1736199423630.1736372767573.26&__hssc=17958374.1.1736372767573&__hsfp=130792607
[38]
Onehouse LakeView: https://github.com/onehouseinc/LakeView