从单个 Parquet 文件构建快速下钻仪表板

Hacker News Top 工具

摘要

本文展示了利用 Hyparquet JavaScript 阅读器和 HTTP 范围请求,从单个 Parquet 数据立方体构建快速下钻仪表板的方法,从而无需使用传统数据库。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/08/24 10:46

# 从单个Parquet文件快速构建下钻仪表盘 来源:https://www.hamiltonulmer.com/customer-dashboards-r2-hyparquet ## 从单个Parquet文件快速构建下钻仪表盘 一个40MB的Parquet数据立方体、一个18KB的读取器、一个R2存储桶,以及几个不起眼的HTTP范围请求。2026年8月21日 每个月都会涌现出对象存储的新奇用途,这无疑是当今非AI软件基础设施中最活跃的角落。最新的技术浪潮是Vicent Martí的撰文 (https://cursor.com/blog/git-at-any-scale),介绍了Cursor Origin利用S3 + WAL方案大规模管理Git仓库的方法。那是一篇杰出的技术文章,与本文截然不同。我必须承认,甚至在阅读它之前,我就在幻想对象存储可能能*轻松搞定*的另一种完全不同的任务——这次是面向客户的分析仪表盘。我的一位朋友在R2上的Iceberg格式中存储了客户使用数据,希望向用户展示一些带过滤器的基本图表。他告诉我,他不想增加更多供应商,这排除了我目前工作的DuckDB云端数据库公司MotherDuck。 好吧,在分析领域,当你只有对象存储时,一切看起来都像是范围请求。我们或许可以直接将这种数据汇总到一个Parquet支持的数据立方体中,并使用Hyparquet(一种在浏览器中运行的轻量级JavaScript Parquet读取器)(https://github.com/hyparam/hyparquet),通过非常简单的范围查询来填充仪表盘。*这样,你无需数据库或查询引擎就能搭建一个真正的下钻仪表盘。*立方体甚至可以达到数十(或数百)MB,因为布局正确的文件意味着每次你只读取其中一小部分。你只需要一个数据管道来生成立方体——而讽刺的是,即使你*确实*拥有一个真正的分析数据库,这也同样是大部分实际开销所在。 这个异端想法太诱人了,无法错过,因为如今我认为DuckDB*就是*解决我所有数据问题的轻量级方案。为了测试,我使用了电脑里著名的纽约市311服务请求数据集 (https://opendata.cityofnewyork.us/311-service-requests-from-2010-to-present-updates/)——大约15年的请求级数据,3400万行左右——将其汇总成一个40MB的Parquet立方体,包含城市机构、投诉类型、提交类型和行政区的筛选器,以及用于时间序列的单一创建时间列。然后我把它放到了R2上。40MB足够大,足以让你感受到下载整个文件的痛苦。 下面的演示仪表盘使用Hyparquet直接从该文件读取。字节在传输过程中经过一个小型Cloudflare Worker,因为免费的r2.dev URL有速率限制。Worker代理字节范围请求并在边缘缓存它们,因为文件是不可变的,这样做是安全的。老实说,考虑到它既没有使用真正的数据库,也没有强大的查询引擎,我对新数据加载的速度之快感到惊讶。UI执行了所有实际的读取工作,并且它足够轻量,可以直接嵌入本文而不会影响页面加载速度。真正的复杂性几乎完全被数据立方体布局所承担。试试拖动图表或点击排行榜中的行。 nyc 311 ~ 每日请求数 全部时间 ~0当前视图中的请求数 0 范围请求 s · 0 KB 已获取 · 0\.0 %目前已获取立方体比例 无过滤器 ~ 点击排行榜行,或刷选图表(点击图表清除)无过滤器 ~ 点击行或刷选图表 按机构 按投诉类型 按行政区 按渠道 那么,这个仪表盘是如何工作的呢? 像这样的仪表盘旨在回答一组有界的分析问题——每日请求数、某个机构的每日请求数、按行政区划统计的历年总量。每个问题都可以通过`GROUP BY`查询来回答,因此我们可以提前预计算所有结果,并将每个结果保存为一个自己的小表,称为*分组集*。将所有分组集堆叠在一个Parquet文件中,每个集合一个区域,你就得到了一个*数据立方体*。一个分组集只有在它能够回答问题或降低数据提取延迟时才有用。这个文件两者兼有。历年总量支撑排行榜,而每个筛选器组合对应的每日分组集则为折线图提供数据。每周和每年的分组集减少了因刷选图表而需要扫描的行数。这些总量*本可以*从每日行中汇总得出,但如果我们按周和年预计算,需要获取的行数会更少。 该文件现在包含了渲染仪表盘所需的分组集,但浏览器仍然需要提取它所需的特定行。Parquet格式的两个特性使得这成为可能。一个Parquet文件被划分为多个*行组*,每个行组包含数万行,文件末尾有一个页脚,包含关于行组字节范围和内部每列最小/最大值的元数据。客户端只需读取一次页脚。然后每个查询使用最小/最大值来选择可能匹配的行组,获取这些字节范围,并在浏览器中聚合这些行。 仪表盘请求的低延迟源于Parquet文件中行的排序和扫描方式。如果文件的行是随机排序的,每个行组的最小/最大值几乎会跨越每列的完整范围,查询就必须读取大部分文件才能获取一小部分行。相反,每个分组集的行按照其查询所筛选的列进行排序。因此,匹配的行通常构成文件中的连续片段,而最小/最大统计信息使读取器能够忽略其余的行组。这就是为什么点击机构排行榜中的NYPD只会读取40MB文件中大约260KB的数据,而不是整个文件。以下是该文件按字节和分组集划分的实际布局: 行组 分组集行数大小总计 为“当前视图中的请求数”总计和四个排行榜提供数据 831\.1k 1\.7mb 历年总量1行组 当未刷选日期范围时读取 4\.8k 103kb 按周16行组 刷选时读取:范围边缘剩余的周 796\.6k 1\.3mb 按ISO年2行组 刷选时读取:范围中间的完整年份 29\.7k 272kb 每日 · 无维度1行组 当无筛选器激活时绘制折线图 5\.0k 171kb 每日 · 单维度 当单个筛选器激活时绘制折线图 770\.3k 2\.4mb 渠道1行组 22\.7k 171kb 行政区2行组 30\.0k 330kb 投诉13行组 635\.6k 1\.5mb 机构3行组 82\.0k 383kb 每日 · 双维度 当两个筛选器激活时绘制折线图 4\.5m 10\.6mb 行政区 \+ 渠道3行组 127\.7k 401kb 投诉 \+ 渠道23行组 1\.1m 2\.6mb 投诉 \+ 行政区42行组 2\.1m 4\.5mb 机构 \+ 渠道5行组 198\.0k 640kb 机构 \+ 行政区8行组 358\.5k 997kb 机构 \+ 投诉13行组 643\.0k 1\.5mb 每日 · 三维度 当三个筛选器激活时绘制折线图 7\.3m 15\.8mb 投诉 \+ 行政区 \+ 渠道65行组 3\.3m 6\.8mb 机构 \+ 行政区 \+ 渠道17行组 804\.0k 1\.9mb 机构 \+ 投诉 \+ 渠道22行组 1\.1m 2\.4mb 机构 \+ 投诉 \+ 行政区43行组 2\.1m 4\.6mb 每日 · 全部四维度64行组 当所有四个筛选器激活时绘制折线图 3\.3m 6\.6mb 页脚 · 索引 每个区域的字节范围和最小/最大统计信息;首先读取一次 — 195kb 这种设置在两个条件下有效。你的图表和筛选器的组合必须保持较小规模,并且你的管道必须足够快地重建每个客户的文件以满足更新节奏。大多数使用和计费页面都满足这两个条件。它们是一组固定的图表——随时间变化的事件、按小时或天统计的计数或总和、几个筛选器或排行榜——这些图表基于按粗略计划(而非实时)更新的数据,这对他们自己和你来说都是如此。从延迟的角度来看,立方体大小并不重要,但你可能还是希望它保持较小,因为你是按计划为每个客户重新生成一个立方体的。 在我的示例中,时间粒度显然是主导因素,因为每日部分占了文件的大部分字节。基数是另一个乘数——投诉类型有485个不同的值,并且图表中的每个大型部分都包含它。事实上,为折线图选择每日粒度使得文件比周等效文件(5\.6mb)大了约7倍。不过,每日粒度并未显著影响范围请求的延迟,因为任何交互都只读取几个行组。而且在这种情况下,能看到单日的大幅波动是好事,因为服务请求的大幅增加可能发生在一天之内,例如由于飓风或暴风雪等重大事件。 像上面这样的仪表盘对于*可分配*和*可代数*聚合效果很好,这些聚合可以分块计算,然后在可视化之前组合起来。比如求和、计数、最大值和平均值。使此设置适用于*整体*聚合(需要了解分布才能获得最终筛选聚合的那些聚合)既有精确的解决方案,也有近似的解决方案。我将此作为读者及其最爱智能体的练习。 对布局精心的文件进行范围请求已有丰富的先例。PMTiles (https://protomaps.com/) 将一个瓦片集打包到一个文件中,客户端通过HTTP范围请求读取它。它之所以有效,是因为瓦片在文件中沿希尔伯特曲线排列,因此给定地图视图的瓦片在文件中彼此靠近,可以通过几次合并的范围请求获取。当然,著名的SQLite-over-HTTP文章 (https://phiresky.github.io/blog/2021/hosting-sqlite-databases-on-github-pages/) 证明了这种机制甚至适用于B树。 我最喜欢的部分是,这种方法将复杂性“左移”,完全转移到了数据管道。布局是预先决定的,因此当用户点击排行榜或拖动时间序列图表时,客户端只需要获取正确的行并求和。至于管道,对于大多数面向客户的仪表盘,一个10MB的客户立方体可以通过DuckDB的`GROUP BY GROUPING SETS` (https://duckdb.org/docs/stable/sql/query_syntax/grouping_sets) 语句轻松生成。对于我那位身为数据工程师的朋友来说,这简直是*职业思维定势*的完美体现。 每个客户一个文件也使得认证变得异常简单。访问控制就等同于为该客户文件签署一个URL,或者用一个微小的Worker来检查会话。 鉴于R2的出站流量是免费的,管道反而是花费真金白银的地方。写入费用为每百万次$4\.50 (https://developers.cloudflare.com/r2/pricing/)(是读取价格的12\.5倍),每次重建每个客户支付一次写入费,无论文件大小如何(1MB的立方体和40MB的立方体上传成本相同)。假设10,000个客户。每次重建都会替换文件,因此存储费用是固定的:10MB的立方体是100GB,每月约$1\.50;40MB的立方体是400GB,每月约$6。每天重建所有文件是每月30万次写入,约$1\.35;每小时重建是720万次写入,约$32;每五分钟重建一个月是8600万次写入,约$389。谢天谢地,Iceberg快照差异可以准确告诉你哪些客户有新数据,因此很容易只重建有新活动的立方体。 即便如此,假设你每5分钟更新一次,并且每个客户在这个时间窗口内都有活动(再次说明,这不太可能)。对于像这样的单一用例,10k客户每月$389的总成本可能比搭建新基础设施更便宜,而且几乎可以肯定更简单。而且,无论是经济性还是用户体验,最近都发生了非常大的变化:出站费用并没有完全扼杀这个想法,但可能阻碍了人们以这种方式进行实验。同样的设置在S3上总体只贵了约20%——在百万次查询时出站费用大约每月$20,而百万次查询是大多数客户仪表盘都达不到的流量。 我*另一个*最喜欢的部分是其极致轻量级的实现。一个18KB的JavaScript读取器和一个替你完成数据库工作的字节布局。真是个奇妙的世界!

相似文章

Parquet 中定长列表的快速路径

Lobsters Hottest

这篇博客文章介绍了 Apache Parquet 的一项优化,用于高效存储和解码像向量嵌入这样的定长列表,通过绕过固定大小数据页的 Dremel 重构,实现了与扁平列相当的解码性能。