从单个 Parquet 文件构建快速下钻仪表板
摘要
本文展示了利用 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 中定长列表的快速路径
这篇博客文章介绍了 Apache Parquet 的一项优化,用于高效存储和解码像向量嵌入这样的定长列表,通过绕过固定大小数据页的 Dremel 重构,实现了与扁平列相当的解码性能。
将Postgres数据以Parquet格式存储在S3上:LTAP架构解析
Databricks推出Lakebase LTAP架构,将Postgres数据以Parquet格式存储在S3上,无需CDC或镜像即可在单份数据上实现事务与分析。
一条提示。将任何数据文件转化为功能完整的仪表盘或表格。
一种新工具或功能,允许用户通过单次提示将任何数据文件转换为交互式仪表盘或表格,简化数据分析和可视化。
DuckDB – 笔记本电脑上的数据强力工具,现已支持 Clojure(2023)
TechAscent 展示了如何通过 tech.ml.dataset(TMD)从 Clojure 中利用 DuckDB 的高性能矢量化 SQL 引擎,实现大型内存外连接,以及约两分钟内将 50GB CSV 数据摄入并压缩至 18GB。
完整的 ClickHouse OLAP 引擎,编译为 WebAssembly
chDB 是一个完整的 ClickHouse OLAP 引擎,编译为 WebAssembly,可通过 wasm.chdb.io 的 SQL 终端直接在浏览器中执行 SQL 查询。