返回

文章详情

在 DuckDB 中分页遍历 Parquet 文件:File_row_number 还是 Offset?

Hacker News2026年7月30日 14:58

我有一个大型的 Parquet 文件和一个必须返回其内容的服务。返回两千万行的数据可能会轻松超过 API 响应的最大大小,而且无论你部署在哪里都会有上限:Lambda 为同步请求或响应提供 6 MB,而 Cloud Run 在不进行流式传输的情况下将 HTTP/1 响应限制为 32 MiB。即使没有平台限制,客户端也必须保持您发送的内容。因此,内容一次以一页的方式返回,调用者不断请求,直到获取所有信息。影响其它一切的因素是每个请求必须独立存在。在负载均衡器后面有多个工作者时,没有服务器端的状态可供恢复,因此“下一页”必须通过请求本身可重构,可以由任何工作者每次处理。明显的写法是 LIMIT 和 OFFSET ,而担心的是 OFFSET 19000000 必须超过一千九百万行以找到您的页面,这将使文件的完整访问变为二次方复杂度。DuckDB 的 read_parquet 具有一个 file_row_number 选项,能够提供每行的物理位置,因此我可以根据行范围进行过滤。合同保持不变,因为客户端仍然返回一些小数据,服务器根据它重建页面,但不进行计数。我原本希望证明 OFFSET 会重新读取页面之前的所有内容。结果并非如此,影响因素根本不是速度。简而言之,在一个拥有 163 行组的 2000 万行文件中,行范围版本的处理速度比 OFFSET 快 2.53 倍,而且在 37 次运行中都保持这个结果。-- 而不是 LIMIT $n OFFSET $offset SELECT id, k, name , category, value , payload, ts FROM read_parquet($ path , file_row_number => true) WHERE file_row_number >= $lo AND file_row_number < $hi 这个行范围正在做一些特定的事情。Parquet 文件作为称为行组的块的序列进行存储,而 DuckDB 可以根据文件的页脚推算出给定行号范围所处的块。您的页面之前的所有内容在未解压的情况下被跳过:直接跳转到您需要的行组 WHERE file_row_number >= $lo AND file_row_number < $hi 跳过,未解压您请求的行,每个块是一个行组 · 结构示意图,未按比例缩放 黄金路径是您的谓词。它到达持有您行的块,而无需触碰前面的那些块——这就是为什么页面的成本不会随着您深入分页而增长。在您将这个数字加以利用之前有两个注意事项。它完全依赖于您的文件有许多行组。速度也是行范围的两个论据中较弱的;更强的论点是比性能问题更糟糕。您的文件有多少行组?DuckDB 每次跳过一个行组的工作。作为一个巨大的单行组写入的 Parquet 文件没有任何可以跳过的内容,因此这些帮助不大。在计划之前,请使用 parquet_metadata 检查:SELECT count ( DISTINCT row_group_id) AS row_groups, min (row_group_num_rows) AS smallest, max (row_group_num_rows) AS largest FROM parquet_metadata( 'yourfile.parquet' ); 返回 1 后您可以停止阅读。为了查看这有多重要,我将相同的两百万行写了两遍,模式和数据完全相同,仅更改了 ROW_GROUP_SIZE :更多行组,更大的收益 同样的 200 万行以两种方式写入,加上大文件作为参考。虚线是平局。1 行组 2M 行 1.25× 17 行组 2M 行 1.75× 163 行组 20M 行 2.53× 1 与 17 的比较是诚实的:数据相同,仅更改了 ROW_GROUP_SIZE。163 的柱子来自更大的文件,所以可以理解为“趋势还在持续”,而不是一个曲线上的第三个点。在一个行组时,收益降至 1.25 倍。它并没有消失,因为 DuckDB 也在行组内以 2048 行的批次丢弃行,因此一个狭窄的窗口仍然读取的行数少于整组。您只是失去大部分收益。该图中更有趣的数字是时钟而不是比率。相同的工作从 0.49 秒变为 2.12 秒,而且两种方法的速度都减慢了三到四倍。如果您控制编写者,请在优化其他任何内容之前解决此问题。DuckDB 自身的编写器每组默认为 122,880 行,这是可以接受的。其它工具则不然,DuckDB 文件格式性能指南上也有关于选择大小的提示。无论您编写什么查询,一个巨大的行组都是个坏主意。DuckDB 实际上对您的 OFFSET 做了什么 这是我二次方假设崩溃的地方。对分页查询运行 EXPLAIN,您会得到意外的结果:HASH_JOIN (SEMI),file_index = file_index 和 file_row_number = file_row_number ├─ READ_PARQUET id, k, name, ... └─ STREAMING_LIMIT └─ READ_PARQUET DuckDB 将您的 OFFSET 重写为行号查找,并将其半连接回数据。它代表您请求 file_row_number,并且以与手写版本相同的方式跳过行组。使用 OFFSET 进行分页并不是二次方复杂度。您所付出的代价是额外的读取过程,找出哪些 r

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡