
主菜单malisper.me常规关于我TwitterPostgres 文章目录RSS通过电子邮件订阅博客输入你的电子邮件地址订阅此博客并通过电子邮件接收新文章通知。电子邮件地址订阅跳转到内容重构 Postgres让分析速度提升 300 倍批处理、操作符融合和 SIMD上周发布了 pgrust 0.2 版本此版本着重提升性能比之前版本快 10 倍。在 OLTP 基准测试中pgrust 比 Postgres 快 30%在 Clickbench 中pgrust 比 Postgres 快 300 倍甚至超过了 Clickhouse。查询引擎的改进是实现大幅性能提升的关键仅查询引擎的优化就为整体 300 倍的提速贡献了约 10 倍。将从一个简化版的 Postgres 查询引擎入手逐步添加优化让 pgrust 查询引擎达到如此高的性能。要理解为何相比 Postgres 有如此大的提升空间需了解 Postgres 的诞生背景。最初的 Postgres 项目可追溯到 80 年代当时数据库性能的主要瓶颈是磁盘 I/O。但如今有三个趋势改变了这一状况一是许多数据集现在可存于内存大幅减少了磁盘 I/O二是对于无法完全存入内存的数据集工作负载也有所不同数据分析通常是批量扫描数据此时性能瓶颈往往不再是磁盘吞吐量而是 CPU 或内存吞吐量三是近年来磁盘速度大幅提升NVMe 比传统硬盘快数百倍。这三个趋势使得 CPU 和内存速度比以往更为重要许多优化都针对这一点。查询引擎是数据库中 CPU 的主要使用者优化了 pgrust 查询引擎使其在处理相同查询时比 Postgres 消耗更少的 CPU 和内存带宽。对比 Postgres 查询引擎的性能为直观感受 Postgres 查询引擎的速度来看一个简单查询对前 5 亿个数字求和。在 Postgres 中运行此查询在 c8g.4xl 实例上禁用并行查询的情况下大约需要 20 秒。作为对比在 Rust 中实现相同功能这个查询仅需 358 毫秒速度快了约 55 倍而且实际上还能更快。当然这并非完全公平的对比因为 Postgres 底层有更多操作。但优化数据库的关键就在于尽可能减少这些额外开销Postgres 中两个主要的开销来源是锁机制和解析存储格式并提取相关元组。构建简化版 Postgres 查询引擎为聚焦查询引擎的影响构建一个简化版的 Postgres 查询引擎。首先简单介绍一下查询引擎。处理 SQL 查询时Postgres 会先将查询转换为内部表示即“查询计划”它描述了查询的执行方式。以上面的查询为例Postgres 生成的查询计划实际上表示“从 my_table 中获取行并对这些行中的值求和”。由于查询本身较简单此查询计划也相对简单但涉及连接、排序、子查询等操作时查询计划会变得复杂得多。Postgres 总共有 40 多种不同类型的计划节点。生成查询计划后Postgres 将其传递给查询引擎。查询引擎负责根据查询计划检索行并执行聚合操作。Postgres 使用的是“火山模型”执行器。下面是一个简化版的 Postgres 查询引擎实现。火山模型的关键特性是 next() 方法查询计划中的所有节点都支持该方法。next() 的作用是返回一行数据。顺序扫描节点的 next() 方法返回下一行聚合节点的 next() 方法计算整个聚合结果并返回单行结果。执行查询计划只需不断调用根节点的 next() 方法直到没有更多行返回。火山模型的优点是简单为每个计划节点实现一个方法即可。上述代码虽经简化但与 Postgres 内部实现非常接近。然而火山模型虽简单却带来了大量开销。运行上述代码需要 1.3 秒比 Postgres 版本快很多因为去除了许多非查询引擎的部分但仍比原生的 for 循环慢主要是因为火山模型的开销。上述代码的主要性能瓶颈在于 next() 方法每次只处理一行数据没有批处理。SeqScan.next() 函数每行调用一次这增加了显著的开销尤其是在运行时调用未知函数时许多 CPU 优化效果不佳。可以实现的第一个优化是批处理。批处理本身消除了大部分开销将查询运行时间从 1.3 秒缩短至约 480 毫秒虽仍比 for 循环慢但已接近很多。一个重要细节是批处理缓冲区在栈上分配这意味着聚合节点运行时无需分配内存而内存分配通常是较慢的操作因此编写超快速代码时应尽量减少内存分配次数。操作符融合与 JIT 编译对批处理版本进行性能分析后发现热点在于 copy_from_slice。尽管采用了批处理但仍需将数据复制到缓冲区可通过“操作符融合”消除此开销。如果某些操作经常一起执行可以创建一个节点来替代两个节点。在本例中可以创建一个 SumAggregateSequentialScan 节点将顺序扫描和求和逻辑合并。这与直接使用 for 循环的性能相同因为代码本质上是一样的。这看似有些取巧确实如此因为针对特定查询进行了硬编码优化。使用操作符融合时对一些常见情况进行硬编码优化是合理的但很快会遇到未提前考虑的情况。这可以通过 JIT 编译解决。借助 JIT 编译可以生成理想的代码对每个查询都进行“取巧”优化。JIT 编译能为任何查询生成完美代码并始终实现操作符融合。可惜本文篇幅有限后续再介绍 pgrust 如何利用 JIT 编译。SIMD 优化最后一个优化是使用 SIMD单指令多数据。SIMD 是一组 CPU 操作可同时对多个数据执行相同操作。使用 SIMD 同时处理多行数据通常比逐行处理快得多。以下是使用 SIMD 优化后的代码。优化后的代码仅需 135 毫秒比 for 循环快近 3 倍比最初的火山模型代码快 10 倍。虽然编译器通常会将 for 循环替换为 SIMD 等效代码但此例特意避免了这种情况。编译器在处理浮点数时通常会避免引入 SIMD因为浮点数运算不满足结合律改变求和顺序可能会产生略微不同的结果。优化总结通过这三个简单的优化将查询速度提升了 10 倍。以下是不同实现的性能对比实现方式时间提速倍数Postgres~20 s—火山模型1.3 s1× 批处理480 ms2.7× 操作符融合358 ms3.6× SIMD135 ms9.6×这些优化以及更多其他优化使 pgrust 在分析型查询上的性能比 Postgres 快数百倍。基准测试设置- AWS c8g.4xlargeGraviton416 vCPU- PostgreSQL 18.4max_parallel_workers_per_gather 0- 数据预热至共享缓冲区- 每个测试运行 5 次取中位数- Rust 使用 cargo build -release 编译每个实现运行 4 次所有测试在同一台机器的同一个进程中进行感谢阅读如果想支持该项目最好的方式是在 GitHub 上给项目点个星。如果想持续关注可关注 GitHub、Discord、邮件列表、pgrust.com。分享本文在 X 上分享在 LinkedIn 上分享在 Facebook 上分享通过电子邮件分享给朋友打印文章导航上一篇文章Postgres in Rust: three dead ends before we passed 100% of the regression suite发表评论你的电子邮件地址不会被公开。必填字段已标记 *评论 *姓名 *电子邮件 *网站通过电子邮件通知我后续评论。通过电子邮件通知我新文章。