由 John Doe 七月 17, 2026
最近数据库圈最火的话题,莫过于用 Rust 重写 PostgreSQL 的 pgrust 项目。13 天产出 240 万行代码,100% 通过 PostgreSQL 18.3 核心回归测试,甚至能直接挂载原生 PG 的数据目录启动,配上 “AI 重写数据库” 的标签,一时间赚足了眼球。
目录
作者:Michael Malis
Postgres 本身非常优秀,但用户在实际使用中常会遇到一批极易触发宕机的共性问题。这些并非纸上谈兵的理论缺陷 —— 根据我与大量初创团队的交流,它们都是生产环境中真实存在的故障诱因,在没有专职数据库工程师的团队中尤为突出。这也是我在自己重写的 Postgres 项目 pgrust 中,重点想要优化的四大方向。
VACUUM 与事务 ID 回卷
VACUUM 机制大概是 Postgres 相关宕机的头号元凶。在 Postgres 中删除一行数据时,数据库并不会真正将其抹去,而是保留该行并标记为 “已删除”。这么设计有诸多原因,核心目的之一是为事务回滚提供支撑。为了真正清除数据、释放存储空间,Postgres 依赖名为 VACUUM 的后台任务,物理移除所有被标记删除的行。
这一机制带来了两层问题。首先是 VACUUM 本身的资源开销:由于它需要扫描全量数据,会消耗大量磁盘 I/O,与业务查询争抢资源,进而拖慢查询速度。如果为了降低 I/O 占用而调弱 VACUUM 的执行力度,它又可能跟不上数据删除的节奏,导致死元组堆积的速度快于清理速度。最终你不得不陷入两难的权衡:既要让 VACUUM 足够激进,跟得上数据更新删除的频率;又不能让它跑得太快,挤占业务查询的 I/O 资源。
第二个问题是事务 ID 回卷。为了区分数据行的可见性(哪些是有效行、哪些是已失效行),Postgres 会为每一行标记事务 ID,而它采用的是 32 位事务 ID,这意味着最多执行约 40 亿次事务后,就必须开始复用旧的事务 ID。VACUUM 的核心职责之一,就是回收旧事务 ID 以供复用。一旦 VACUUM 回收事务 ID 的进度严重滞后,Postgres 会直接强制关闭数据库,避免复用仍在生效的事务 ID 导致数据损坏。这听起来有些极端,但却是必要的自我保护机制。据我估计,由此引发的 Postgres 宕机事故可达数千甚至数万起。
pgrust 的差异化解法
我正在从两个方向解决这个问题:
- 社区已有为 Postgres 增加 64 位事务 ID 的补丁,但出于多种原因一直未能合入主线:它会破坏与旧版本的兼容性,并且每一行数据都会增加 32 位的存储开销。我个人认为,考虑到该问题引发的大量宕机事故,做出这项改变完全值得。
- 我同时在探索完全无需 VACUUM 的替代架构。Oracle 在数据删除标记上采用了完全不同的思路,通过**回滚日志(Undo Log)**实现,无需依赖 VACUUM 机制。目前也已有相关项目在探索为 Postgres 增加这类能力。
连接数限制与查询并行能力
启动一个 Postgres 实例时,会有一个最大连接数的配置项。一旦并发连接请求超过这个上限,新的连接就会被拒绝,进而导致业务不可用。修改连接数需要重启数据库,因此在创建数据库之初就需要谨慎规划该参数。如果提前没有了解过这一点,很容易设置一个过低的连接数上限。很多团队会部署 PgBouncer 这类连接池工具,能部分缓解问题,但始终是一大痛点。
你可能会问,Postgres 为什么要设置连接数限制?答案很大程度上源于它的查询并行模型。每新建一个数据库连接,Postgres 就会创建一个独立的进程。与其他并行实现方式相比,进程的资源开销极高,既占用更多 CPU 资源,创建启动的耗时也更长。
进程模型不仅限制了 Postgres 的跨查询并行能力,也限制了单条查询内部的并行执行。当 Postgres 需要对单条查询做并行处理时,需要启动多个进程分别执行部分计算;正因为进程创建成本高,Postgres 只会对长耗时查询启用并行执行。
多年来社区一直有声音提议将 Postgres 从进程模型改为线程模型,但始终没有落地成果。最大的挑战之一在于,这是一项规模极大的重构,实施难度极高,且很难保证改造后的安全性与稳定性。
pgrust 的方案
pgrust 从设计之初就基于线程模型架构。Postgres 采用进程模型的一大原因是出于安全考虑:进程之间的隔离性强于线程,单个进程出现故障不会影响其他进程。而 pgrust 采用 Rust 语言编写,Rust 在编译期就能提供内存安全保障,因此线程模型的安全风险对 pgrust 而言并不存在。
糟糕的查询执行计划
Postgres 的查询规划器虽然设计精巧,但实际使用中却很容易出问题。简单来说,Postgres 内置了数十种查询执行算法,每次执行查询时,它会结合表的数据统计信息,估算并选择执行速度最快的算法组合。
多数情况下 Postgres 的选择是正确的,但一旦判断失误,偏差会非常严重。优质执行计划和劣质执行计划的差距,可能是 10 毫秒与 10 分钟的天壤之别。更棘手的是,Postgres 没有提供有效的查询规划器干预手段,这意味着一旦遇上坏的执行计划,用户几乎无计可施。
遇到坏执行计划时,你唯一的办法只能是全局禁用某些执行算法。像 MySQL 这类数据库支持 “规划器提示(Planner Hints)”,可以人为干预特定查询的执行方式,而 Postgres 完全没有类似功能。
我之前任职的公司就曾全局禁用嵌套循环连接(Nested Loop Join),因为 Postgres 总是倾向于使用该算法,导致所有查询都变得异常缓慢。这很大程度上是因为我们大量使用 JSON 类型,Postgres 无法准确感知数据分布 —— 这点我后面会详细展开。
pgrust 的建设方向
这是 pgrust 中偏前瞻性的部分,我的目标是实现一个自适应查询规划器。核心思路是:如果一条原本只需 10 毫秒的查询突然耗时 10 秒,pgrust 会自动检测到性能退化,检查执行计划是否发生变更、底层统计信息是否出现偏差,并自动进行修正,而不是等待 DBA 发现后人工介入。
该功能目前尚未在 pgrust 中实现,但我认为完全具备落地的可行性。
JSON 类型
Postgres 与 JSON 的关系可谓爱恨交织。处理非结构化数据时,JSON 类型让存储和查询都变得非常便捷,但遗憾的是,它也极易引发问题。
我在 Heap 公司任职时,团队重度使用 JSON,也因此深受其扰。我的上级甚至专门写过一篇博客讨论这个问题。最大的痛点在于,Postgres 不会为 JSON 类型收集统计信息。对于大多数数据类型,Postgres 会采集大量统计数据,包括直方图、高频值等等;但对 JSON 类型,却没有任何有效的统计信息。取而代之的是,当你对 JSON 字段做过滤查询时,Postgres 会默认估算过滤结果占 0.1% 的行数。这个 0.1% 是个凭空设定的 “魔法数字”,并非基于实际数据计算得出,真实的匹配率可能是 80%,也可能是 0.0001%。
综合这些因素,只要你开始对 JSON 字段做过滤,几乎必然会得到糟糕的查询执行计划。
这还没提到 JSON 的另一大问题:Postgres 不会对 JSON 数据做任何形式的压缩。也就是说,如果大量数据重复使用相同的键名和键值,每次都会完整存储一遍。通过字典压缩这类技术,JSON 数据的存储空间有非常大的优化空间。
pgrust 的优化计划
首要任务是为 JSON 实现真正的统计信息采集,这能解决 80% 的 JSON 使用痛点。社区已有几个添加 JSON 统计信息的补丁方案,我希望能推动其中一个落地完善。这需要一些实验验证,不过我已经有了几个思路。
同时我也计划引入压缩机制,缓解 JSON 数据的存储空间问题,可能采用跨行压缩或者字典压缩的方式。
pgrust 路线规划
当前项目优先级如下:
- 持续推进 100% Postgres 兼容性 —— 目前已通过 96% 的 Postgres 回归测试用例,仅剩少量问题待解决。
- 提升稳定性与修复缺陷 —— 让 pgrust 达到可被用户信赖的生产可用状态。
- 启动上述架构层面的优化改造 —— 从 64 位事务 ID 开始落地。
你们怎么看 “Rust 重写数据库” 这件事?欢迎在评论区聊聊你的观点。