编程 pgrust 深度拆解:用 Rust 从零重写 PostgreSQL,一次通过 46066 条回归测试的数据库革命实验

2026-07-14 18:16:23 +0800 CST views 14

pgrust 深度拆解:用 Rust 从零重写 PostgreSQL,一次通过 46066 条回归测试的数据库革命实验

写在前面

2026年7月,一个名叫 pgrust 的项目空降 GitHub Trending 第二名。它的 slogan 很直接——"postgres, rewritten in rust"——PostgreSQL,用 Rust 重写。

这句话的分量有多重,稍微了解 PostgreSQL 的人都能掂量出来。PostgreSQL 不是一个小项目:它有超过 30 年的历史、两千多万行 C 代码、46,066 条回归测试用例、以及一个几乎涵盖了关系型数据库所有功能边界的完整 SQL 方言。把这样一套系统用另一种语言重写,而且还不是为了替代它,而是为了验证「能不能做到」,这件事本身就是一个工程学意义上的奇观。

但更令人惊讶的还在后面:pgrust 做到了 100% 回归测试通过——46,066 条查询,一条不落,全部通过。与此同时,性能还提升了 50%。这不是一个 demo,不是一个 PoC(概念验证),而是一个可以直接在浏览器里跑起来的、PostgreSQL 18.3 兼容的、生产级数据库引擎。

本文从技术架构、内存管理、并发模型、WebAssembly 编译目标、以及 AI 辅助开发五个维度,把 pgrust 的设计与实现彻底拆解一次。


一、为什么用 Rust 重写 PostgreSQL?

这个问题是理解 pgrust 的起点。不理解动机,就无法理解它做出的每一个技术选择。

1.1 C 语言的「历史包袱」

PostgreSQL 的核心代码诞生于 1996 年,彼时 Rust 还不存在。三十年的演进让它成为功能最完整的开源关系型数据库,但与此同时,它的 C 语言根基也带来了几类根深蒂固的问题:

内存安全问题:PostgreSQL 的历史上出现过大量与内存相关的 CVEs(Common Vulnerabilities and Exposures)。use-after-free、buffer overflow、double-free……这些漏洞的根源不在于开发者的疏忽,而在于 C 语言本身对内存操作的「信任」机制——编译器相信程序员是正确的,但人不是神。根据 CVE 数据库的统计,PostgreSQL 历史上约 40% 的安全漏洞源于内存安全问题。

并发模型的局限:PostgreSQL 使用进程模型(每条连接一个进程),而 Rust 的并发原语基于轻量级线程(async/await + tokio),可以更高效地处理大规模并发连接。在现代云原生环境下,连接池动不动就是几千条,C 语言的进程模型会带来可观的上下文切换开销。

现代工具链缺失:C 语言的构建系统(Makefile + Autoconf)虽然在当时是标准,但在 2026 年看来已经相当古老。Rust 的 Cargo 提供了统一的依赖管理、单元测试、文档生成、性能分析(profiling),以及一个活跃的 crates.io 生态。

1.2 Rust 的「承诺」

Rust 解决上述问题的路径很清晰:

  • 所有权系统(Ownership):编译期消除 data races 和 use-after-free,不需要 GC,不需要运行时检查。
  • async/await 生态:tokio 生态已经成熟到可以支撑生产级数据库,在 hyper、Tokio Postgres 等项目中得到了验证。
  • fearless concurrency:在 Rust 中写出正确的并发代码,比在任何其他系统语言中都更简单——编译器会阻止你犯错。

但这并不是说 Rust 重写 PostgreSQL 是一条平坦的路。事实上,这个项目最大的技术挑战,恰恰来自于 PostgreSQL 的「正确性」本身。

1.3 为什么是「实验」而非「替代」

pgrust 的作者非常清醒地定位了项目的性质:这是一个实验,不是要取代 PostgreSQL。它的目标是:

"track Postgres behavior closely enough to become a base for deeper experiments."

翻译过来就是:它的首要目标是行为一致性,而不是功能创新。46,066 条回归测试用例通过率 100%——这个数字背后是 PostgreSQL 三十年来积累的所有边界情况、特殊行为、以及用户依赖的隐式约定。做到这一点,比写一个新的数据库要难得多。


二、核心架构:Rust 里的 PostgreSQL 是什么样子的?

2.1 项目结构概览

从 GitHub 仓库的结构来看,pgrust 保持了与原生 PostgreSQL 类似的模块划分:

src/
├── executor/      # 查询执行器(对应 PostgreSQL 的 executor/)
├── optimizer/     # 查询优化器(对应 PostgreSQL 的 optimizer/)
├── parser/       # SQL 解析器(对应 PostgreSQL 的 parser/)
├── planner/      # 规划器(对应 PostgreSQL 的 planner/)
├── storage/      # 存储引擎(对应 PostgreSQL 的 storage/)
├── utils/        # 工具函数
└── main.rs       # 入口

这是一个很重要的设计决策:pgrust 不是从零设计一个新架构,而是在 Rust 语言中重新实现 PostgreSQL 的既有架构。这样做的好处是:

  1. 可以直接参考 PostgreSQL 的测试用例来验证正确性
  2. 降低了与其他 PostgreSQL 生态工具(如 pgAdmin、psql、psycopg2)的兼容成本
  3. 开发者可以借助已有的 PostgreSQL 知识理解 pgrust 代码

2.2 类型系统:Rust 的 Datum vs PostgreSQL 的 Datum

PostgreSQL 内部使用一种叫 Datum 的类型来表示所有数据值。Datum 是一个 usize 大小的无类型指针——它可以指向一个整数、一个字符串、也可以指向一个复杂结构。这在 C 中是合法的(通过 void*),但在 Rust 中需要非常小心地处理。

pgrust 对 Datum 的 Rust 实现如下(简化版):

/// PostgreSQL Datum 的 Rust 表示
/// 
/// Datum 是一个无类型的值容器,可以装任何 PostgreSQL 支持的数据类型。
/// 在 C 中这是 void*;在 Rust 中我们用 enum + Box 来实现类型安全。
pub enum Datum {
    // 整数家族
    Int16(i16),
    Int32(i32),
    Int64(i64),
    Oid(u32),  // 对象标识符
    
    // 浮点家族
    Float4(f32),
    Float8(f64),
    
    // 字符串
    Text(String),       // varlena 类型,Rust String 自然对应
    BpChar(String),    // 定长字符
    Name(String),      // PostgreSQL Name 类型(63字节)
    
    // 时间日期
    Timestamp(i64),
    Date(i32),
    Interval(i64, i32, i32),  // 微秒、天数、月数
    
    // 复杂类型
    Array(Vec<Datum>),
    Jsonb(Vec<u8>),
    Record(Vec<Datum>),
    Null,
}

impl Datum {
    /// 获取实际的类型 OID
    pub fn type_oid(&self) -> Oid { ... }
    
    /// 转换为 PostgreSQL 内部二进制格式
    pub fn to_pg_binary(&self) -> Vec<u8> { ... }
}

这里的 Oid(对象标识符)是 PostgreSQL 类型系统的核心:每一种数据类型(integertexttimestamp 等)都有一个唯一的 OID,PostgreSQL 靠 OID 来判断一个 Datum 到底装的是什么。在 Rust 中,我们通过 enum 变体在编译期约束了类型的合法性,同时在运行期保留了 OID 信息用于 PostgreSQL 生态的互操作。

2.3 存储引擎:MVCC 与 Rust 的借用检查

PostgreSQL 的多版本并发控制(MVCC)是通过维护多个版本的行数据来实现的。每次 UPDATE 不是原地修改,而是插入新版本、标记旧版本为「已删除」。这个机制是 PostgreSQL 实现事务隔离的基础,但也带来了一个核心问题:垃圾回收(VACUUM)

在 C 语言的 PostgreSQL 中,VACUUM 需要手动管理内存,程序员需要确保在清理旧版本时没有任何正在使用这些数据的并发事务。Rust 的所有权系统在这里提供了一个独特的优势——编译期就能发现潜在的数据竞争

use std::sync::Arc;
use parking_lot::RwLock;

/// PostgreSQL tuple(行)的 Rust 表示
/// 
/// 使用 Arc<RwLock<T>> 包装:Arc 允许多个版本共享底层数据,
/// RwLock 控制读写并发。这与 PostgreSQL 的 MVCC 模型天然对应。
#[derive(Clone)]
pub struct HeapTuple {
    /// 指向实际数据的指针
    data: Arc<HeapTupleData>,
    /// 事务信息(xmin/xmax)
    xmin: TransactionId,
    xmax: TransactionId,
}

pub struct HeapTupleData {
    /// 行头(t_xmin, t_xmax, t_ctid 等)
    header: TupleHeader,
    /// 行数据(Datum 的二进制表示)
    data: Vec<u8>,
    /// 字段值
    fields: Vec<Datum>,
}

/// BufferPool 的 Rust 实现
/// 
/// PostgreSQL 的共享缓冲池管理着磁盘页和内存页的映射关系。
/// Rust 实现中使用了更严格的生命周期管理来保证内存安全。
pub struct BufferPool {
    /// 缓冲区描述符数组
    descriptors: RwLock<HashMap<BufferTag, BufferDescriptor>>,
    /// 实际的数据页
    pages: RwLock<HashMap<BufferId, BufferFrame>>,
    /// LRU 替换策略
    lru: RwLock<LRUCache<BufferId>>,
    /// 缓冲区数量配置
    capacity: usize,
}

impl BufferPool {
    pub fn new(capacity: usize) -> Self {
        Self {
            descriptors: RwLock::new(HashMap::new()),
            pages: RwLock::new(HashMap::with_capacity(capacity)),
            lru: RwLock::new(LRUCache::new(capacity)),
            capacity,
        }
    }
    
    /// 获取或加载一个页面到缓冲池
    pub fn get_page(&self, tag: BufferTag) -> Result<BufferId, BufferError> {
        // 先在缓冲池中查找
        {
            let pages = self.pages.read();
            if let Some(&id) = pages.get(&tag) {
                // 命中,更新 LRU
                drop(pages);
                let mut lru = self.lru.write();
                lru.access(id);
                return Ok(id);
            }
        }
        
        // 未命中,从磁盘加载
        let frame = self.load_from_disk(&tag)?;
        
        // LRU 淘汰
        {
            let mut lru = self.lru.write();
            let mut pages = self.pages.write();
            
            if pages.len() >= self.capacity {
                if let Some(evict_id) = lru.evict() {
                    self.flush_to_disk(evict_id)?;
                    pages.remove(&evict_id);
                }
            }
            
            // 插入新页
            let id = BufferId::new(pages.len());
            pages.insert(tag.clone(), frame);
            lru.insert(id);
            
            Ok(id)
        }
    }
}

2.4 SQL 解析器:PGParser 的 Rust 移植

PostgreSQL 的 SQL 解析器使用的是基于 Yacc 的 gram.y,这是一个超过两万行的语法文件,定义了 PostgreSQL 的完整 SQL 方言。pgrust 的实现同样面临一个艰难的选择:是移植 gram.y 到 LALRPOP 或 pest,还是完全重写一个解析器?

从实际代码来看,pgrust 选择了一条务实的路线:尽量复用 PostgreSQL 的 parser 基础设施,而不是从零构建。这里面包含几层考量:

  1. SQL 语法的边界情况(corner cases)太多,完全重写几乎不可能覆盖所有行为
  2. PostgreSQL 的词法分析器(scan.l)和语法分析器(gram.y)经过了三十年的生产验证
  3. 保持与 PostgreSQL 解析器的行为一致性,是通过回归测试的前提

在 Rust 层面,pgrust 使用了类似的 AST 表示:

/// SELECT 语句的 AST 节点
pub struct SelectStmt {
    /// 目标列列表
    pub target_list: Vec<TargetEntry>,
    /// FROM 子句
    pub from_clause: Vec<RangeVar>,
    /// WHERE 子句
    pub where_clause: Option<Node>,
    /// GROUP BY 子句
    pub group_by: Vec<Node>,
    /// HAVING 子句
    pub having_clause: Option<Node>,
    /// ORDER BY 子句
    pub sort_clause: Vec<SortBy>,
    /// LIMIT 子句
    pub limit_count: Option<Node>,
    /// FOR UPDATE 子句(行级锁)
    pub locking_clause: Vec<LockingClause>,
}

/// 表达式节点的枚举表示
pub enum Node {
    // 表达式
    AExpr(AExpr),
    ColumnRef(ColumnRef),
    Param(Param),
    Const(Const),
    FuncCall(FuncCall),
    
    // 表达式列表
    List(Vec<Node>),
    
    // 子查询
    SubLink(Box<SubLink>),
    
    // 布尔表达式
    BoolExpr(BoolExpr),
    
    // NULL 测试
    NullTest(NullTest),
}

这个 AST 结构与 PostgreSQL 的内部表示高度对应,使得后续的查询规划和执行可以直接参考 PostgreSQL 的实现。


三、并发与事务:Rust 如何驯服 PostgreSQL 的并发模型?

3.1 进程模型 vs 线程模型

PostgreSQL 原生使用进程模型:每一条客户端连接对应一个独立的操作系统进程。父进程(postgres)通过 fork() 创建子进程来处理连接。这个模型的优势是进程间隔离性好(一个进程崩溃不影响其他进程),劣势是:

  • 进程创建和上下文切换的开销在大规模并发下不可忽视
  • 进程间通信(IPC)依赖操作系统原语,效率低
  • 共享缓冲池需要依赖共享内存(System V Shm),增加了复杂度

pgrust 的 Rust 实现转向了线程模型

use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

/// PostgreSQL 连接处理器(Rust 异步版本)
pub struct PgConnection {
    /// 客户端连接
    socket: TcpStream,
    /// 连接状态
    state: ConnectionState,
    /// 当前事务
    transaction: Mutex<Option<Transaction>>,
    /// 参数化查询缓存
    prepared_statements: HashMap<String, PreparedStatement>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum ConnectionState {
    Idle,                  // 等待新命令
    InTransaction,         // 事务中
    InTransactionBlock,    // 显式 BEGIN ... END 块
    CopyIn,                // COPY FROM STDIN 模式
    CopyOut,               // COPY TO STDOUT 模式
}

impl PgConnection {
    /// 处理一条 PostgreSQL 协议消息
    pub async fn handle_message(&mut self, msg: PgMessage) -> Result<(), PgError> {
        match msg.tag() {
            b'Q' => {
                // SimpleQuery:直接执行 SQL 字符串
                let sql = msg.decode_query()?;
                self.execute_simple_query(&sql).await
            }
            b'P' => {
                // Parse:解析 SQL 为prepared statement
                let stmt = msg.decode_parse()?;
                self.prepared_statements.insert(stmt.name.clone(), stmt);
                self.send_ready_for_query().await
            }
            b'B' => {
                // Bind:将参数绑定到 prepared statement
                let binding = msg.decode_bind()?;
                self.bind_statement(&binding)
            }
            b'X' | b'S' => {
                // X: Terminate, S: Sync
                if self.transaction.lock().is_some() {
                    self.rollback().await?;
                }
                Ok(())
            }
            _ => Err(PgError::ProtocolViolation(format!(
                "unexpected message tag: {:?}", msg.tag()
            ))),
        }
    }
}

3.2 事务与 MVCC 的 Rust 实现

PostgreSQL 的 MVCC 实现有两个核心概念:xmin(创建行的事务ID)和 xmax(删除行的事务ID)。判断一行在某个时刻是否「可见」,需要查看当前事务快照(snapshot)。

/// PostgreSQL 事务快照
/// 
/// Snapshot 决定了在某个时间点,哪些行版本对当前事务「可见」。
/// 这是 MVCC 正确性的核心数据结构。
#[derive(Clone)]
pub struct Snapshot {
    /// 当前事务ID
    pub xmin: TransactionId,
    /// 尚在运行的最老事务ID(任何 xid >= xmin 的事务都视为「未来」)
    pub xmax: TransactionId,
    /// 当前事务启动时,已经提交的事务ID集合
    pub xip_list: Vec<TransactionId>,
    /// 子事务列表
    pub subxip_list: Vec<TransactionId>,
}

impl Snapshot {
    /// 判断一个 HeapTuple 是否对当前快照可见
    pub fn is_visible(&self, tuple: &HeapTuple) -> bool {
        let xmin = tuple.xmin;
        let xmax = tuple.xmax;
        
        // 规则1:如果 xmin 尚未提交,不可见
        if !self.is_committed(xmin) && xmin != self.xmin {
            return false;
        }
        
        // 规则2:如果 xmin == 当前事务,拥有者可以看到自己的插入
        if xmin == self.xmin {
            // 除非这是一个 DELETE 导致的行版本(由当前事务删除)
            return xmax == InvalidTransactionId || xmax == self.xmin && self.is_subtransaction();
        }
        
        // 规则3:如果 xmin 在当前快照的 running 范围内,不可见
        if xmin >= self.xmax {
            return false;
        }
        
        // 规则4:如果 xmax == InvalidTransactionId,尚未删除,可见
        if xmax == InvalidTransactionId {
            return true;
        }
        
        // 规则5:如果 xmax 尚未提交,不可见(其他事务的删除尚未生效)
        if !self.is_committed(xmax) {
            return true;
        }
        
        // 规则6:如果 xmax 在当前快照的 running 范围内
        // (其他并发事务正在删除),需看该事务是否已提交
        false
    }
    
    fn is_committed(&self, xid: TransactionId) -> bool {
        // 简单实现:检查是否在已提交列表中
        // 实际 PostgreSQL 使用 pg_clog(提交日志)的位图结构
        self.xip_list.contains(&xid)
    }
}

3.3 WAL(预写日志):Rust 的 crash-safe 保证

PostgreSQL 的 crash-safe 特性依赖于 WAL(Write-Ahead Logging):在修改数据页之前,先将操作记录写入日志。如果系统崩溃,重启时从 WAL 恢复即可。

/// WAL 日志记录
/// 
/// 每个 WAL record 包含:
/// - record header(长度、prev 指针、xlog record magic)
/// - record body(具体操作类型 + 数据)
/// - backup block(如果涉及页面修改)
#[repr(C)]
pub struct XLogRecord {
    /// 记录总长度
    pub xl_tot_len: u32,
    /// 前一条 WAL record 的地址
    pub xl_prev: XLogRecPtr,
    /// 事务ID(0 表示不属于任何事务)
    pub xl_xid: TransactionId,
    /// 记录类型
    pub xl_info: u8,
    /// 资源管理器ID
    pub xl_rmid: u8,
}

/// WAL 写入器(Rust 异步实现)
pub struct XLogWriter {
    /// WAL 文件路径
    path: PathBuf,
    /// 当前写入文件
    current_file: RwLock<Option<File>>,
    /// WAL 缓冲区
    buffer: Vec<u8>,
    /// WAL segement 大小(默认 16MB)
    segment_size: usize,
    /// 写入锁
    write_lock: Mutex<()>,
}

impl XLogWriter {
    /// 原子性地写入一条 WAL 记录
    /// 
    /// Rust 实现利用了 std::fs::sync_all() 的跨平台保证,
    /// 确保数据真正落盘后才返回。
    pub async fn insert(&self, record: &XLogRecord) -> Result<XLogRecPtr, XLogError> {
        let _lock = self.write_lock.lock().await;
        
        // 1. 序列化记录
        let bytes = record.serialize();
        
        // 2. 检查是否需要切换 segment
        if self.buffer.len() + bytes.len() > self.segment_size {
            self.switch_segment().await?;
        }
        
        // 3. 追加到缓冲区
        let offset = self.buffer.len();
        self.buffer.extend_from_slice(&bytes);
        
        // 4. 强制刷盘(fsync)
        let fp = self.current_file.write().await;
        if let Some(ref mut file) = *fp {
            file.sync_all().map_err(|e| XLogError::Io(e))?;
        }
        
        Ok(XLogRecPtr {
            // 实际的实现会返回 WAL 位置(file_number, offset)
            file: self.current_segment,
            offset,
        })
    }
}

四、WebAssembly 编译:把 PostgreSQL 装进浏览器

这是 pgrust 最具想象力的一面:它编译到 WebAssembly(wasm32 目标),可以在浏览器里跑起来。

访问 pgrust.com,你就会看到一个交互式 SQL 会话——这背后是一个完整的 PostgreSQL 引擎,运行在浏览器的 WebAssembly 沙箱里。

4.1 为什么编译到 WASM?

WASM 编译的价值不在于「炫技」,而在于以下几个真实的应用场景:

1. 无服务器 SQL:想象一个场景——你可以在 Cloudflare Workers 或 Deno Deploy 上运行一个完整的 PostgreSQL 实例(虽然目前是单节点),完全不需要管理服务器。WASM 天然支持这种「到哪里都能跑」的部署模型。

2. 隐私保护的本地查询:医疗或金融数据不能上传到云端?没问题,WASM 在浏览器本地运行,数据不离开设备。这意味着可以在完全隔离的环境中执行 SQL 查询。

3. 嵌入式数据库:把 PostgreSQL 的能力嵌入到任何 WASM 宿主环境中——桌面应用、物联网设备、IDE 插件……这是一个真正的「数据库即库」模型。

4. AI Agent 的本地工具:在我们今天看到的 AI Agent 生态中,一个最核心的挑战是「Agent 如何可靠地操作数据」。如果一个 Agent 可以调用一个本地的、确定性的数据库引擎,而不是依赖外部 API,那就能解锁大量的数据密集型 Agent 场景。

4.2 WASM 编译的技术挑战

把一个完整的 PostgreSQL 编译到 WASM,面临的挑战是多方面的:

浮点数精度问题pgrust.com 的 Safari 预览版明确提到 double precision / float8 is a known preview limitation。这是因为 WASM 的浮点数实现与原生 x86 存在微小的精度差异(IEEE 754 标准允许这种差异)。PostgreSQL 的回归测试对 float8 类型有严格的一致性要求,WASM 环境下的精度问题会导致测试失败。

系统调用:PostgreSQL 大量使用 POSIX 系统调用(read/write/open/close/mmap 等)来管理文件 I/O。在 WASM 环境中,这些系统调用被 WASI(WebAssembly System Interface)替代。pgrust 实现了 WASI Preview 2 接口来桥接这一层:

// WASI 文件 I/O 适配层(简化)
use std::io::{self, Read, Write, Seek, SeekFrom};

/// 包装 WASI 文件描述符为 std::io 的 Read/Write 接口
pub struct WasiFile {
    fd: wasi::Fd,
}

impl Read for WasiFile {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        wasi::fd_read(self.fd, iovec(buf)?)
            .map(|(n, _)| n)
            .map_err(|e| io::Error::from_raw_os_error(e.raw_error()))
    }
}

impl Write for WasiFile {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        wasi::fd_write(self.fd, &[iovec(buf)?])
            .map(|(n, _)| n)
            .map_err(|e| io::Error::from_raw_os_error(e.raw_error()))
    }
}

内存限制:WASM 实例的线性内存有一个软上限(通常 2GB),而 PostgreSQL 的共享缓冲池可以配置到数百 GB。pgrust 采用了动态缓冲池策略:在 WASM 环境中使用较小的缓冲池(以适配 WASM 内存限制),通过更激进的页面淘汰算法来弥补容量不足。

4.3 浏览器内的 SQL 体验

在 pgrust.com 的交互式演示中,用户可以直接运行 SQL 语句:

-- PostgreSQL 18.3 兼容的 SQL(pgrust wasm32 版本)

-- 基础查询
SELECT 'pg' || 'rust' AS result;

-- 窗口函数
SELECT 
    department,
    employee_name,
    salary,
    RANK() OVER (PARTITION BY department ORDER BY salary DESC) as rank,
    AVG(salary) OVER (PARTITION BY department) as dept_avg
FROM employees;

-- CTE(公共表表达式)
WITH regional_sales AS (
    SELECT region, SUM(amount) AS total_sales
    FROM orders
    GROUP BY region
), top_regions AS (
    SELECT region
    FROM regional_sales
    WHERE total_sales > (SELECT SUM(total_sales)/10 FROM regional_sales)
)
SELECT region, product, SUM(quantity) AS product_units, SUM(amount) AS product_sales
FROM orders
WHERE region IN (SELECT region FROM top_regions)
GROUP BY region, product;

这些语句在浏览器内的 PostgreSQL 引擎中执行——无需服务器,无需网络往返,每一次查询都在本地完成。


五、性能:50% 提升从何而来?

pgrust 声称带来了 50% 的性能提升。这个数字需要拆解来看。

5.1 性能提升的来源

1. 内存分配优化

PostgreSQL 的 C 实现大量使用 palloc() 系列函数,这些函数在 PostgreSQL 的内存上下文(MemoryContext)系统中运作。虽然这个系统已经很高效,但它仍然受限于 C 语言的 allocator(默认是 glibc 的 malloc)。

Rust 的内存分配有几个优势:

  • jemalloc / mimalloc:Rust 默认使用 jemalloc(Linux)或系统 allocator,提供了更好的多线程分配性能
  • arena allocation:Rust 的 bumpalo crate 提供了高效的区域分配器,与 PostgreSQL 的 MemoryContext 概念相似,但由编译器强制保证释放时机
  • 栈分配优化:Rust 的 SmallVec 等类型可以在栈上存储小数据,避免堆分配
use bumpalo::{Bump, ArenaVec};

/// 使用 bumpalo arena 优化查询执行期间的内存分配
pub struct QueryExecutor<'arena> {
    /// 查询专用的内存 arena,查询结束后整体释放
    exec_arena: &'arena Bump,
    /// 用于存储较大数据的堆分配向量
    large_buffers: Vec<Vec<u8>>,
}

impl<'arena> QueryExecutor<'arena> {
    /// 在 arena 上分配字符串(零分配,如果字符串已经在 arena 中)
    pub fn alloc_string(&mut self, s: &str) -> &'arena str {
        let bytes = self.exec_arena.alloc_slice_copy(s.as_bytes());
        // SAFETY: 字节数组是有效的 UTF-8
        unsafe { std::str::from_utf8_unchecked(bytes) }
    }
    
    /// 执行 Hash Join,使用 arena 优化哈希表分配
    pub fn hash_join(&mut self, build: &[HeapTuple], probe: &[HeapTuple]) -> Vec<HeapTuple> {
        let mut hash_table: HashMap<DatumHashKey, Vec<HeapTuple>, _> = 
            HashMap::with_hasher(S default());  // 自定义 hasher 可复用 arena
        
        // Build phase
        for tuple in build {
            let key = self.compute_hash_key(tuple);
            hash_table.entry(key).or_insert_with(|| Vec::new()).push(tuple.clone());
        }
        
        // Probe phase
        let mut results = Vec::new();
        for tuple in probe {
            let key = self.compute_hash_key(tuple);
            if let Some(matches) = hash_table.get(&key) {
                for m in matches {
                    if self.match_qualification(m, tuple) {
                        results.push(m.clone());
                    }
                }
            }
        }
        
        results
    }
}

2. SIMD 向量化

Rust 的 std::simdpacked_simd crate 支持 AVX-512、AVX2 等 SIMD 指令集。在查询执行器的关键路径(如 LIKE 匹配、WHERE 过滤、JOIN 探测)上使用 SIMD,可以显著提升吞吐量。

use std::arch::x86_64::*;

#[cfg(target_feature = "avx2")]
pub fn filter_numeric_avx2(values: &[i64], predicate: i64) -> Vec<usize> {
    // 使用 AVX2 SIMD 并行比较
    let mut results = Vec::with_capacity(values.len() / 8);
    let predicate_vec: __m256i = unsafe { _mm256_set1_epi64x(predicate) };
    
    for chunk in values.chunks(8) {
        let values_vec: __m256i = unsafe { _mm256_loadu_si256(chunk.as_ptr() as *const _) };
        let cmp_result: __m256i = unsafe { _mm256_cmpgt_epi64(values_vec, predicate_vec) };
        
        // 提取结果并收集匹配的索引
        let mask = unsafe { _mm256_movemask_epi8(cmp_result) };
        // ... 展开 mask 为索引
    }
    
    results
}

3. 零拷贝架构

PostgreSQL 的 C 实现中,数据在多个模块之间传递时经常涉及内存拷贝(例如从存储层读取页面后,拷贝到缓冲池,再拷贝到执行器)。Rust 的生命周期和引用系统可以在编译期保证「没有不必要的引用」,从而减少拷贝:

/// 使用引用而非拷贝来传递行数据
/// 
/// 在 C 中通常需要 Clone 整个 HeapTuple;
/// Rust 的生命周期系统允许我们安全地共享数据。
pub struct HashJoinExecutor {
    // 使用 Arc 来共享跨线程的数据
    build_side: Arc<Vec<HeapTuple>>,
}

impl HashJoinExecutor {
    /// 引用返回,不拷贝数据
    pub fn probe(&self, key: &Datum) -> Option<&[HeapTuple]> {
        self.hash_table.get(key).map(|v| v.as_slice())
    }
}

5.2 基准测试的注意事项

需要指出的是,pgrust 公布的 50% 性能提升是在特定工作负载下的结果。PostgreSQL 的性能高度依赖于具体的查询类型、数据分布、索引策略等因素。Rust 的优势主要体现在:

  • CPU 密集型查询(如复杂 JOIN、聚合计算)
  • 大规模并发场景(减少上下文切换)
  • 长时间运行的服务进程(无 GC 停顿)

但是在 I/O 密集型场景下(受限于磁盘或网络),Rust 和 C 的性能差距可能并不显著。


六、AI 辅助开发:Cursor + Claude Code 如何改变数据库开发

pgrust 的另一个值得关注的维度是它的开发方式。根据公开信息,这个项目大量借助了 AI 辅助编码工具:Cursor 和 Claude Code。

6.1 为什么数据库开发特别适合 AI 辅助?

传统的 AI 编程辅助在数据库开发中往往表现不佳,原因是:

  1. 上下文窗口限制:PostgreSQL 单个模块可能包含数万行 C 代码,AI 无法一次性「消化」
  2. 类型系统缺失:C 没有类型系统,AI 难以理解数据结构的关系和约束
  3. 行为依赖历史:PostgreSQL 的很多代码是为了「兼容 1998 年的某个行为」而存在的,AI 不知道这些背景

Rust 恰好解决了这些问题:

  • 显式类型系统:Rust 的 enum、struct、trait 系统让代码结构清晰可解析
  • 模块化设计:Rust 鼓励小模块、明确接口,AI 可以逐个击破
  • 文档文化:Rust 社区有良好的文档传统,doc-comment 是标准实践

6.2 AI 辅助数据库开发的边界

尽管如此,AI 辅助开发 pgrust 仍然面临一些独特挑战:

正确性验证无法自动化:Rust 编译器可以验证内存安全和类型安全,但 PostgreSQL 的行为正确性(例如「这条 UPDATE 语句是否满足 MVCC 的隔离级别」)需要通过回归测试来验证。46,066 条测试用例——这是 AI 无法替代的。

性能调优的工程直觉:Rust 编译器生成的代码已经很快,但 PostgreSQL 几十年的性能调优经验积累(如 B-Tree 页分裂策略、缓冲区替换算法)需要深厚的数据库理论基础,不是简单 prompt 就能复现的。


七、生态影响:PostgreSQL 生态的新可能

7.1 pgrust 对现有生态的兼容

pgrust 兼容 PostgreSQL 18.3 的 SQL 方言,这意味着它可以直接使用:

  • psql:PostgreSQL 的标准 CLI 客户端
  • psycopg2/psycopg3:Python 的 PostgreSQL 驱动
  • pgAdmin:PostgreSQL 的官方管理工具
  • 所有 PostgreSQL 生态的 ORMs:Prisma、SQLAlchemy、Diesel 等

兼容性不是偶然的,而是 pgrust 团队有意识的设计目标。

7.2 WASM 数据库的新想象空间

pgrust + WebAssembly 的组合,打开了一些非常有趣的可能性:

浏览器端数据分析:用户可以在浏览器内直接查询本地文件(CSV、JSON)——不需要上传到服务器。这对于隐私敏感的数据分析场景是革命性的。

IDE 内置数据库:VS Code、WebStorm 等 IDE 可以嵌入一个完整的 PostgreSQL 实例,用于本地开发,无需安装和配置 Docker 或本地 PostgreSQL 服务。

边缘计算数据库:Cloudflare Workers 现在可以运行 WASM 模块,未来可以在边缘节点运行轻量级 PostgreSQL 实例,实现真正的「边缘数据库」。

离线优先应用:Progressive Web App(PWA)可以在本地浏览器中运行一个完整的关系数据库引擎,实现真正的离线优先架构。

7.3 局限性:这不是银弹

我们需要清醒地看待 pgrust 的局限性:

  1. 单节点:目前 pgrust 不支持分布式,不支持流复制(streaming replication),不适合生产环境的大规模数据场景
  2. WASM 内存上限:受限于 WASM 的线性内存(通常 2GB),无法承载大规模数据
  3. 生态成熟度:PostgreSQL 有数千个扩展(extension),pgrust 目前支持的寥寥无几
  4. 维护成本:追踪 PostgreSQL 每一次更新是一个巨大的工程挑战

八、技术细节:pgrust 的代码组织哲学

8.1 从 C 到 Rust 的迁移策略

将一个大型 C 代码库迁移到 Rust,通常有两条路:

路径一:渐进式迁移

  • 保留原有的 C 代码,通过 Rust/C FFI 暴露 Rust 模块
  • 逐步用 Rust 重写核心模块,替换对应的 C 模块
  • 优势:风险可控;劣势:长期维护两套代码

路径二:完全重写

  • 不参考 C 代码,从 PostgreSQL 的行为规范出发,用 Rust 重新实现
  • 通过对比测试用例来验证正确性
  • 优势:代码纯净;劣势:需要完全理解原有行为

pgrust 走的是路径二,但并非盲目重写。它大量参考了 PostgreSQL 的代码结构、命名约定和算法设计,只是用 Rust 的方式重新表达。这种「尊重行为、不尊重实现」的态度,是这个项目能够快速达到 100% 回归测试通过率的关键。

8.2 错误处理:Rust 的 Result vs C 的返回值

PostgreSQL 的 C 代码大量使用错误码(ERRCODE_*)和 elog() 宏来处理错误:

// PostgreSQL C 代码中的错误处理
if (node == NULL) {
    ereport(ERROR, 
        (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
         errmsg("cannot extract array element from null array")));
}

pgrust 用 Rust 的 Result 类型做了对应封装:

pub type PgResult<T> = Result<T, PgError>;

#[derive(Debug)]
pub enum PgError {
    NullValueNotAllowed(String),
    DivisionByZero,
    InvalidInputSyntax { typename: String, input: String },
    UniqueViolation { constraint_name: String },
    ForeignKeyViolation { constraint_name: String },
    SerializationFailure { message: String },
    IoError(std::io::Error),
    // ... 其他错误类型
}

impl From<std::io::Error> for PgError {
    fn from(e: std::io::Error) -> Self {
        PgError::IoError(e)
    }
}

这种设计保持了 Rust 的错误处理风格,同时保留了 PostgreSQL 的错误码语义——上层代码(如 psycopg2 驱动)仍然可以收到熟悉的 PostgreSQL 错误码。


九、总结:数据库领域的 Rust 时刻

pgrust 不是一个普通的开源项目。它代表了一种范式转变的开始:用 Rust 重新实现那些我们以为「已经足够好」的核心基础设施

过去几年,我们已经看到了类似的趋势:

  • Bun:用 Zig 重写 Node.js 生态
  • ripgrep / fd / exa:用 Rust 重写 Unix 命令行工具
  • Tauri:用 Rust 重写 Electron 的后端
  • litepdf / liteparse:用 Rust 重写 Python 文档处理

pgrust 不过是将这个趋势推进到了数据库的最深处。

但更值得关注的不是「Rust 能不能做到」——46,066 条测试用例已经回答了这个问题。真正值得思考的是:当我们用 Rust 重写了 PostgreSQL 之后,数据库的进化方向会是什么?

有了内存安全的保证,开发者可以把更多精力放在功能创新上,而不是修补 CVE。有了 WASM 的编译目标,数据库可以无处不在地运行。有了 AI 辅助开发,数据库的核心模块可以被更快地原型化和迭代。

pgrust 的故事,才刚刚开始。


项目信息

  • GitHub:malisper/pgrust
  • 官网:pgrust.com
  • 声明兼容性:PostgreSQL 18.3
  • 回归测试:46,066/46,066 通过(100%)
  • 性能提升:50%(特定工作负载)
  • 编译目标:原生(Linux/macOS/Windows)+ wasm32(浏览器)
  • 开源协议:PostgreSQL License(与 PostgreSQL 本身相同)
  • 开发辅助:Cursor + Claude Code(AI-assisted)

本文写于 2026 年 7 月,pgrust 仍处于活跃开发中。最新状态请关注 GitHub 仓库

推荐文章

百度开源压测工具 dperf
2024-11-18 16:50:58 +0800 CST
Go语言中的`Ring`循环链表结构
2024-11-19 00:00:46 +0800 CST
什么是Vue实例(Vue Instance)?
2024-11-19 06:04:20 +0800 CST
随机分数html
2025-01-25 10:56:34 +0800 CST
Vue中如何使用API发送异步请求?
2024-11-19 10:04:27 +0800 CST
38个实用的JavaScript技巧
2024-11-19 07:42:44 +0800 CST
一个收银台的HTML
2025-01-17 16:15:32 +0800 CST
Vue 3 中的 Fragments 是什么?
2024-11-17 17:05:46 +0800 CST
程序员茄子在线接单