开发者生态
morning
光速编程:构建重力查询引擎
摘要
With the release of pgrust v0.4-beta, our new query engine, Gravity, is now as much as 3x faster than ClickHouse on ClickBench , the benchmark that ClickHouse themselves designed to show off how fast ...
the
and
that
Gravity
how
speed
our
query
from
pgrust
2026-10-31
1 阅读
约7分钟阅读
poly2it
字号:
随着 pgrust v0.4-beta 的发布,我们的新查询引擎 Gravity 现在在 ClickBench 上的速度比 ClickHouse 快 3 倍,ClickBench 是 ClickHouse 自己设计的基准,用于展示 ClickHouse 的速度有多快。更好的是,Gravity 实现了这样的速度,同时支持完整的 Postgres SQL 方言和类型系统。在之前的几篇文章中,我写了如何使用批处理、运算符融合和 SIMD 使 pgrust 的分析查询速度比 Postgres 快数百倍,以及如何构建一个复制和修补 JIT 编译器,在 5 微秒内编译模板。所有这些工作,经过多个版本的积累,最终形成了《Gravity》。复制和修补是一个很好的起点,但将预编译的模板粘合在一起只能让我们到目前为止。因为我们在整个循环中没有真正的中间表示或寄存器分配,所以与 rustc 或 Clang 等优化编译器相比,我们最终留下了很多性能。对于 pgrust v0.4,我们希望全力构建一个查询编译器,为任何 SQL 查询生成最佳机器代码。这就是我们构建 Gravity 的原因。通常,当工程师试图让软件变得更快时,他们会自下而上地工作。他们运行基准测试,查看火焰图,找到热点,并找出如何将代码速度提高 15% 或 20%。自下而上工作的问题是你不知道上限是什么。您距离硬件可以运行的最快代码可能还差 20%,也可能差 100 倍。当我们开始构建 Gravity 时,我们扭转了这个问题。我们问自己:数据库实际上能有多快?我们不想逐步加速现有的查询引擎,而是希望从硬件和文件格式的物理速度限制开始,然后向后进行。我喜欢称之为“光速编程”。为了找出“光速”是什么样子,我们让 Claude 直接针对分析查询语料库的列式存储箱编写独立的、高度优化的 Rust 程序。这些程序绕过了整个 SQL 执行器,只是直接在我们的磁盘格式上使用 SIMD 运行手动调整的多线程 Rust 循环。当我们运行这些手写的 Rust 程序时,它们执行查询的速度大约比 ClickHouse 等现有分析数据库快 4 倍。然后,我们挖掘这些手工调整的 Rust 循环,以进行优化,以便将其合并到 Gravity 中。我们的 Gravity 目标变得很简单:对于任何 SQL 查询,JIT 在运行时编译机器代码,其精度与 Claude 直接针对我们的文件格式编写的手写 Rust 代码的 20% 以内。 Gravity 的总体架构 我们在设计 Gravity 架构时的主要目标是在生成的输出中为我们自己提供大量的灵活性。我们知道,如果我们能够轻松地更改 Gravity 以发出查询所需的任何低级优化,那么我们将更容易缩小差距并接近光速。我们没有将执行模式硬编码或将固定模板拼接在一起,而是将 Gravity 构建为多阶段编译器管道,逐步将高级 Postgres 计划降低为原始汇编: SQL 查询 -> Postgres 计划 -> 管道图 -> Lisp IR -> 汇编 要查看编译过程每个阶段的查询是什么样子,让我们执行以下查询: SELECT UserID, extract(min FROM EventTime) AS m, MIN(URL), COUNT(*) FROM 点击 WHERE URL LIKE '%google%' GROUP BY UserID, m ORDER BY COUNT(*) DESC LIMIT 10;第一阶段:Postgres Plan Gravity 不会取代 Postgres 的解析器或查询规划器。当查询到达时,pgrust planner 分析表统计信息并生成标准查询计划。 Limit (cost=... rows=10) -> Sort (Sort Key: count(*) DESC) -> HashAggregate (Group Key: userid, extract(min FROM eventtime)) -> Seq Scan on attempts (Filter: (url ~~ '%google%')) 第 2 阶段:管道图 接下来,Gravity 获取查询计划并将其转换为“管道图”。这是一种中间表示形式,表示数据在整个查询执行过程中如何流动。每个管道代表在整个查询过程中如何转换和操作行。下面是我们查询的管道图的草图: graph pipelines=3 Breakers=2 root=p2 p0 Eligible end=breaker(slot=0 HashAgg) source range rel=hits am=pgrc Lanes Attr(UserID):byval8 Attr(EventTime):byval8 Attr(URL):dict4 filter like(URL, param0) project (key0=UserID, key1=extract_minute(EventTime), agg0=min(URL), agg1=count_star()) p1 Serial end=breaker(slot=1 TopK) Source Breaker slot=0 topk k=10 order=agg1:DESC p2 serial end=sink Source Breaker slot=1 project (UserID, m, min_url, count) ->wire 在这种情况下,查询被分成三个管道:p0 从命中表中取出行,过滤 url 包含“google”的行,然后将行插入哈希表以计算按 user_id 分组的 COUNT(*) 和 MIN(url) 以及事件时间 p1 的分钟数
这篇文章对您有帮助吗?
订阅66必读
每日精选科技资讯,直达你的邮箱