首页 时政热点 科技头条 智能AI 安全攻防 数码硬件 开发者生态 汽车 游戏 社会热点 开源推荐 医疗健康 归档 标签 关于

使 Postgres 的分析速度提高 300 倍:批处理、运算符融合和 SIMD

2026-08-07 1 阅读 约8分钟阅读 poly2it
分享:
字号:
上周我们发布了 pgrust 0.2 版本。这个版本的重点是性能。它比之前版本的 pgrust 快 10 倍。在 OLTP 基准测试中,pgrust 比 Postgres 快 30%,在 Clickhouse 的分析数据库基准测试中,pgrust 比 Postgres 快 300 倍。甚至领先于Clickhouse!查询引擎是我们为实现更好的性能而做出的最大改变之一。查询引擎本身的性能是 300 倍的约 10 倍。我们将从 Postgres 查询引擎的微型版本开始,然后一一添加我们为使 pgrust 查询引擎如此快速而所做的相同优化。为了说明为什么与 Postgres 相比有如此大的改进空间,Postgres 是在不同的时代创建的。最初的 Postgres 项目可以追溯到 80 年代。它是在数据库性能的主要瓶颈是磁盘 I/O 的时候构建的。三个趋势使这种情况不再存在: 许多数据集现在适合 RAM,消除了大多数磁盘 I/O 对于不适合 RAM 的数据集,工作负载有所不同。数据分析批量扫描数据。瓶颈通常不再是您的磁盘吞吐量,而通常是您的 CPU 吞吐量或内存吞吐量 近年来磁盘变得更快。 NVMe 比硬盘快数百倍。所有这三种趋势都使得 CPU 和内存速度比以往更加重要。我们所做的许多优化都是针对这一点。查询引擎是数据库中CPU的主要使用者。我们优化了 pgrust 查询引擎,使其在处理相同查询时使用比 Postgres 更少的 CPU 和内存带宽。为了让您了解 Postgres 查询引擎有多慢,我们来看一个对前 5 亿个数字求和的简单查询: CREATE TABLE my_table AS select col::float8 fromgenerate_series(1.0, 500000000.0) g(col);从 my_table 中选择 SUM(col);当我在 Postgres 中运行它时,大约需要 20 秒。这是在禁用并行查询的 c8g.4xl 上完成的。为了进行比较,当我在 Rust 中计算等效值时: let table: Vec = (1..=500_000_000usize).map(|i| i as f64).collect();让互和 = 0.0; for &table 中的 &value { sum += value;查询需要 358 毫秒。这大约快了 55 倍,不管你信不信,我们甚至可以比 358 毫秒更快。这个例子并不是同类比较。 Postgres 的底层还有很多事情要做。同时,优化数据库就是要尽可能多地消除这种开销。 (如果您好奇 Postgres 开销的两个最大原因是 1. 锁定和 2. 解析 Postgres 存储格式并提取与查询相关的元组)。为了将我们的注意力集中到查询引擎的影响上,让我们构建一个微型版本的 Postgres 查询引擎。首先,简单解释一下什么是查询引擎。在处理 SQL 查询时,Postgres 首先将查询转换为称为“查询计划”的内部表示形式,它描述了 Postgres *如何*执行查询。在上面的示例中,Postgres 将生成一个可能类似于以下内容的查询计划:这实际上表示“从 my_table 获取行并对这些行中的值求和”。考虑到查询的性质,这个查询计划非常简单,但是当您开始使用连接/排序/子查询等时,它们可能会变得更加复杂。Postgres 总共有 40 多种不同类型的计划节点。生成查询计划后,Postgres 将其传递给查询引擎。 Postgres 查询引擎是 Postgres 的一部分,它接受查询计划并实际检索行并执行聚合。 Postgres 使用一种称为“火山模型”的执行器风格。为了了解它是如何工作的,这里有一个 Postgres 查询引擎的微型实现:use std::hint::black_box;特征节点 { fn next(&mut self) -> Option; } struct SeqScan<'a> { table: &'a [f64], pos: usize, } impl Node for SeqScan<'_> { fn next(&mut self) -> Option { if self.pos >= self.table.len() { return None; } // 表结束 } let value = self.table[self.pos]; self.pos += 1; Some(value) } } struct SumAggregate<'a> { child: Box, Total: f64, did: bool, } 为 SumAggregate<'_> 实现节点 { fn next(&mut self) -> Option { if self.done { return None; } } while let Some(value) = self.child.next() { self.total += value; self.done = true; Some(self.total) } } let table: Vec = (1..=500_000_000usize).map(|i| i as f64).collect();让 mut plan = SumAggregate { child: black_box(Box::new(SeqScan { table: &table, pos: 0 })), 总计: 0.0, 完成: false, };让 sum = plan.next().unwrap(); (需要使用 black_box 来防止编译器优化阻碍我们的基准测试)Volcano 模型的关键特性是“next()”方法,查询计划中的所有节点都支持该方法。 “next()”的工作是返回一行。顺序扫描中的“next()”返回顺序扫描中的下一行。聚合上的“next()”将共同
这篇文章对您有帮助吗?

订阅66必读

每日精选科技资讯,直达你的邮箱