高级聚类
DuckLake的聚类能力非常灵活。您不仅可以按一列或多列排序,还可以按表达式甚至DuckLake宏进行排序。这开启了许多自定义潜力。
Alex在此。聚类是我最喜欢的查询优化技术!
这是我对DuckLake代码库和规范的首个重大贡献。这是我最喜欢的,因为它高度依赖于数据和查询,所以我们可以非常有创意!有无限的可能性,以下是一些拓展您想象力的例子。
这种创造力的大部分在存在各种具有不同类型过滤器的查询工作负载时发挥作用。不可预测的查询模式也需要使用一些这些高级方法,而AI代理产生的正是出了名的不可预测的工作负载。
第一条经验法则是优先考虑最常见的工作负载,并首先按该工作负载排序。在平衡多个工作负载时,按多列排序是一种选择。一个指导原则是首先按最低基数列(唯一值最少的列)排序,这样后续排序仍然有效。如果第一列的唯一值数量超过每个Parquet文件中的行组数量,那么后续聚类列的收益将有限。因此,这种方法仅适用于低基数列。
如果您可以同时近似按两列排序呢?业界一种流行的技术是按空间填充曲线排序,通常使用Morton空间填充曲线(通常称为Z-order)或Hilbert曲线。例如,Spark Iceberg实现就包含了z-order功能。这允许两列都近似排序,因此按任一列过滤的查询都会更快。
这种用例的一个经典例子是地理空间数据:按纬度过滤和按经度过滤通常同等重要。图5-1展示了空间填充曲线。想象x轴是经度,y轴是纬度。数据的顺序沿着曲线排列,使得相邻的方块彼此存储得很近。通常
80 | 第5章:性能优化
地理空间查询寻找距另一位置一定距离内的对象(找我附近的加油站等),因此将物理上邻近的对象存储得彼此靠近是有益的。
图5-1. 空间填充曲线:Z-order(Morton曲线,左)和Hilbert曲线(右)。
要在DuckLake中按空间填充曲线排序,我们利用其对表达式的支持。不是先按一列排序再按另一列排序,而是可以按单个表达式排序,该表达式接受多个列作为输入并返回一个反映它们在空间填充曲线中顺序的数字。
Alex在此。处理任意排序表达式使DuckLake极其灵活,甚至与现有湖仓相比也是如此。例如,Iceberg规范只允许特定的转换,如year或month(https://oreil.ly/J54Wf)。Z-order能力仅在Apache Spark等某些引擎中支持,且不在规范范围内。
DuckDB Spatial扩展包含一个Hilbert函数,即使底层数据不是地理空间数据也可以使用。只需安装并加载spatial扩展。
INSTALL spatial;
LOAD spatial;
CREATE TABLE spatial_sort_test (i DOUBLE, j DOUBLE);
ALTER TABLE dl1.spatial_sort_test SET SORTED BY (st_hilbert(st_point(i, j))
ASC);
聚类 | 81
一般来说,这种方法在超过两列后收益递减,但如果需要,可以嵌套该方法来处理超过两列的情况。
要处理字符串,还需要先将它们转换为数值等价物。这是在DuckLake排序表达式中使用DuckLake宏的绝佳用例。下面的示例函数可以将ASCII字符串转换为整数,然后可以输入到相同的Hilbert函数中。请注意,它最多只能处理八个字符的字符串。然而,对于最小/最大索引良好执行所需的近似排序来说,这通常足够了(例如,DuckDB自己的内部最小/最大索引对字符串也只存储八个字符)。
CREATE OR REPLACE MACRO varchar_to_ubigint(i, num_chars := 8) AS (
/* 此SQL函数可以将包含ASCII字符(最多8个字符)的VARCHAR转换为UBIGINT。它将VARCHAR拆分为单个字符,计算该字符的ASCII编号,将其转换为位,将位连接在一起,然后转换为UBIGINT。*/
list_reduce(
[
ascii(my_letter)::UTINYINT::BIT::VARCHAR
FOR my_letter
IN (i[:num_chars]).rpad(num_chars, ' ').string_split('')
],
lambda x, y: x || y
)::BIT::UBIGINT
);
CREATE OR REPLACE TABLE dl1.spatial_sort_test_strings AS
SELECT 'ABC' AS i, 'ZZZ' AS j
UNION ALL
SELECT 'BBB' AS i, 'ZZZ' AS j;
ALTER TABLE dl1.spatial_sort_test_strings
SET SORTED BY (
st_hilbert(
st_point(
varchar_to_ubigint(i),
varchar_to_ubigint(j)
)
) ASC
);
要使用其他空间填充曲线(如Morton(Z-order)),请考虑lindel(linearization/delinearization的缩写)DuckDB社区扩展,它同时具有Morton和Hilbert实现。
要衡量排序效果如何,请检查DuckLake生成的Parquet文件中常用于过滤的列的最小/最大索引,如我们在第3章行组裁剪部分所示。最小/最大索引之间的重叠越少,对该列的查询选择性就越高。
82 | 第5章:性能优化
如果读取对您的应用程序至关重要,请尝试聚类!这可能是一个很好的代理用例:给您的代理一个目标来改善某些读取查询,并迭代各种排序方法。
结合分区和聚类
此时,您可能想知道“为什么我要选择聚类而不是分区?”好吧,猜怎么着。您可以在DuckLake中两者兼得!这实际上是大型事实时间序列数据集的一种非常常见的方法。想想看——您有一个每日区域销售表,包含10年历史和数百万行。一个常见的查询模式是先过滤特定时间段,然后过滤几个区域。您可以按销售日期分区,然后按区域名称聚类来设计表,如下所示:
CREATE TABLE dl1.sales_dly_region_name_cluster (sls_dt date, region_name varchar, sls_amt double);
ALTER TABLE dl1.sales_dly_region_name_cluster SET PARTITIONED BY (sls_dt);
ALTER TABLE dl1.sales_dly_region_name_cluster SET SORTED BY (region_name ASC);
然后,像这样的查询模式将同时利用表的分区和聚类:
SELECT *
FROM dl1.sales_dly_region_name_cluster
WHERE sls_dt = '2025-01-01'
AND region_name = 'North';
底线:分区有助于从考虑中消除整个文件或目录结构,而聚类有助于消除剩余文件内的行组。它们共同在查询执行过程的多个层面提供裁剪。
Matt在此。我已经使用分区和排序表的双重组合策略超过十年了。我第一次这么做是在SQL Server上。当我们按销售日期对销售表进行分区,然后按区域名称对数据进行聚类时,我们的工作负载实现了多大的性能提升,这让我大为惊叹。查询运行速度快了几个数量级,我们的每日报告有时能提前几个小时发出。这确实是数据库读取查询性能优化中一颗隐藏的宝石。
我强烈推荐对服务于各种分析团队和仪表板的大型事实表使用这一策略。
目录优化
作为友好的提醒,DuckLake目录可以托管在DuckDB、SQLite或Postgres中。每个目录平台都有自己的配置选项,管理内存分配、事务处理和存储行为等方面。
目录优化 | 83
虽然DuckLake开箱即用表现良好,但随着环境增长,有几项优化技术值得了解。
数据内联
您将遇到的第一个目录优化是数据内联,我们在前面章节中讨论过。数据内联通过临时将小插入直接存储在目录中而不是立即写入Parquet文件,帮助解决小文件问题。
数据内联的默认值是10行。包含超过10行的插入直接写入Parquet,而较小的插入则保留在目录中内联,直到稍后被刷新。
虽然数据内联可以显著减少小文件的创建,但过多内联数据会增加目录大小和元数据查找成本。定期将内联数据刷新到DuckLake的永久数据存储层有助于保持目录主要关注元数据而非长期数据存储。
要将内联数据刷新到数据存储,请使用以下命令:
CALL ducklake_flush_inlined_data('my_ducklake');
目录位置和硬件
在评估目录性能时,重要的是要考虑目录相对于计算引擎的物理位置。
值得问的问题包括:
• 计算运行在哪里,相对于目录的位置如何?•
• 目录托管在SSD存储还是传统旋转磁盘上?•
• 网络延迟是否在计算和目录操作之间引入延迟?•
随着元数据操作增加,目录延迟可能变得更加明显。如果您的DuckLake计算运行在一个区域,而目录位于另一个区域,每次元数据查找都会产生额外的网络延迟。在大多数情况下,将计算、目录和对象存储在地理上保持靠近将提供最佳性能。
存储也很重要。SSD支持的目录通常比传统旋转磁盘提供显著更快的元数据访问,特别是在处理大型目录和并发工作负载时。
84 | 第5章:性能优化
目录索引
一个经常被忽视的优化是能够直接向目录数据库添加索引。
在添加索引之前,先测量瓶颈。额外的索引可以改善读取性能,但也会增加写入开销,因为目录在元数据更新期间必须维护这些索引。
在大多数环境中,自定义目录索引是不必要的。DuckLake的元数据表已经设计为在常见工作负载下表现良好。通常只有在性能分析表明存在特定目录瓶颈后,才应考虑添加索引。
一个可能随时间大幅增长的目录表是ducklake_file_column_stats,它存储查询规划和裁剪操作期间使用的文件级统计信息。在某些环境中,向此表添加索引可能会改善元数据查询性能。
USE __ducklake_metadata_dl1;
CREATE INDEX idx_col_stats on ducklake_file_column_stats
(table_id, column_id);
与任何索引策略一样,在实施前后对结果进行基准测试,以确保索引提供可衡量的价值。
Postgres异步提交
对于Postgres托管的目录,另一个值得研究的性能优化是异步提交。
默认情况下,Postgres在确认提交之前等待事务日志记录持久写入磁盘。异步提交改变了这种行为,允许Postgres在写入完全持久化之前确认事务。
这可以降低提交延迟,特别是在写入密集型工作负载中。然而,它引入了一个小窗口,在此期间最近提交的事务可能在崩溃或断电时丢失。
DuckLake使崩溃比通常情况稍微不那么痛苦。由于Parquet文件在与目录通信之前就已写入存储,即使目录崩溃,任何原始数据也已经是持久的。因此原始数据更可恢复。然而,这种类型的恢复需要一些自定义逻辑,并且仍然可能难以准确处理。
对于每个事务都必须完全持久的工作负载,通常首选默认的同步行为。对于不太关键的元数据工作负载,性能权衡
目录优化 | 85
可能是可以接受的。在启用此设置之前,请务必仔细阅读Postgres文档。
内存分配
所有支持的目录平台都提供内存管理的配置选项。DuckDB最初将内存限制默认为机器总容量的80%;但是,可以使用全局memory_limit设置调整内存:
SET memory_limit='4GB';
增加内存限制为DuckDB提供了更多用于查询执行和缓存的工作空间。对于较大的目录,这可以减少必须反复从磁盘读取的数据量,并改善整体元数据查询性能。
尽管现代SSD非常快,但内存仍然明显更快。如果您的目录随时间大幅增长,为DuckDB进程分配足够的内存可以帮助提高响应能力并减少磁盘活动。
有关DuckDB内存管理的更深入讨论,您可以参考这篇优秀的文章。
转载自 CSDN-专业IT技术社区



