SQLx 源码深度解析

从源码视角剖析 Rust 异步数据库工具包 sqlx。

打开任何一个 Rust 后端项目的数据访问层,你大概率会看到这样的代码:

let pool = PgPoolOptions::new()
    .max_connections(20)
    .connect("postgres://...").await?;

let user: User = sqlx::query_as!(User, "SELECT id, name, email FROM users WHERE id = $1", user_id)
    .fetch_one(&pool).await?;

五行代码,一次带类型检查的数据库查询就完成了。连接池、预处理、参数绑定、行解码、结构体映射——query_as! 把这些全部安排妥当,甚至在编译期就帮你验证了 SQL 的语法和类型是否和数据库 schema 对得上。但当你想给某个字段写自定义的 Decode、想让 query! 在 CI 里离线跑通、想搞清楚 Pool::acquire().await? 背后那个"有时候几微秒、有时候几百毫秒"的延迟抖动从哪来、想在 Transaction Drop 的那一刻决定到底 commit 还是 rollback……你就得往下挖了。

这个专栏的目的,就是把 query_as! 底下的每一层都打开给你看。

你会看到 Database trait 如何用泛型关联类型(GAT)把 Row<'r>ValueRef<'r>Arguments<'q> 三族带生命周期的类型收束成一个 trait;你会看到 Executor 为什么在 sqlx 0.7 之后把 &mut Transaction 的 impl 拿掉了、必须写 &mut *tx 才能通过;你会看到 query! 宏在编译期如何通过 DATABASE_URL 连回真实数据库取回 schema、又如何通过 .sqlx/ 目录的离线缓存让 CI 不需要数据库也能编译;你会看到 Poolidle_conn_queue 为什么是 ArrayQueue 而不是 Mutex<VecDeque>PoolConnectionDrop 实现如何把连接还给池子而不是直接关闭;你会看到 Postgres 驱动里 Extended Query 协议的三个消息(Parse / Bind / Execute)如何被流水线化以压缩 RTT;你会看到 Migrator 的 checksum 字段如何在"迁移已应用但文件被改了"这种事故里保命。

这是《Rust 源码之道》本系列的第七卷,也是"Rust 后端栈"完整闭环的最后一环:

从 Tokio 把线程调度让给用户态,到 Hyper 把字节流翻译成请求响应,到 Axum 把 handler 函数装配成服务,再到 SQLx 把一条 SELECT 语句落到磁盘上的 B+Tree——你在这条链路上的每一次 .await,最后都落到 sqlx 的 Pool::acquireExecutor::fetch。这四卷合起来,就是 Rust 异步后端从 socket 字节流到数据库行的全景。

flowchart TB
  Q["query! / query_as!<br/>编译期校验"] -->|宏展开| EXP[expand_input<br/>在线 DESCRIBE / 离线 .sqlx 缓存]
  Q2["query() / query_as()<br/>运行时"] --> EXE
  EXP --> EXE
  subgraph EXE [Executor trait · 统一入口]
    direction LR
    ARG[Arguments<br/>参数绑定] --> TYP
    subgraph TYP [三位一体]
      T1["Type&lt;DB&gt;<br/>类型声明"] --- T2["Encode<br/>写入"] --- T3["Decode<br/>读取"]
    end
  end
  EXE --> POOL
  subgraph POOL [Pool · 内部五件事]
    direction LR
    IQ[idle_conns<br/>ArrayQueue 无锁队列] --> SEM[AsyncSemaphore<br/>公平信号量]
    SEM --> SM[连接状态机<br/>Live / Idle / Floating]
  end
  POOL --> DRV[驱动<br/>Postgres · MySQL · SQLite · Any]
  EXE -.事务.-> TX[TransactionManager<br/>BEGIN 或 SAVEPOINT]

sqlx 最独特的地方是那条编译期路径。 query! 宏在编译你的代码时连上数据库做 DESCRIBE,把真实 schema 带进类型系统——写错列名、类型对不上,cargo build 就红了,而不是等到线上某个分支才炸。离线模式则把这份 schema 快照进 .sqlx/,让 CI 不需要数据库也能编译。第 11 章拆的就是这套宏。

读完你能做到什么

  1. 说清一个 i32 从 Rust 到数据库要穿过几道关卡。 Type<DB> / Encode / Decode 三位一体的分工、Option<T> 怎么统一承载 SQL NULL,以及自定义类型该实现哪几个 trait(第5章 类型系统第6章 Arguments)。
  2. 看懂 query! 宏。 三个成员的差别、expand_input 这个总入口、在线模式的 DATABASE_URL DESCRIBE 与离线模式的 .sqlx/ 缓存、输出怎么构造成 Rust 类型(第11章 query 宏)。
  3. 把连接池调对。 Pool 必须做的三件事、PoolInner 的 10 个字段、无锁 ArrayQueue 的空闲队列、公平异步信号量、Live/Idle/Floating 三态连接状态机、acquire 的完整实现(第13章 Pool API第14章 Pool 内部)。
  4. 正确使用事务。 Rust async 下事务的真实挑战、TransactionManager 的驱动层抽象、begin 走 ANSI BEGIN 还是 SAVEPOINT、commit/rollback 的完整路径与同步回滚(第15章 事务)。
  5. 在四种驱动之间做选择。 Postgres / MySQL / SQLite 各自的协议实现差异,以及 Any 驱动的代价(第16章 Postgres 驱动第17章 MySQL 驱动第18章 SQLite 驱动第19章 Any)。
  6. 搭起数据访问层。 Row/Column 抽象、FromRow 的派生、动态 SQL 的 QueryBuilder、迁移、日志与 tracing(第7章 Row 与 Column第8章 FromRow第10章 QueryBuilder第20章 迁移第21章 日志第22章 生产)。

适合谁读

前置知识:本专栏假设读者熟悉 Rust 的 trait、泛型、生命周期、PinFutureasync/await。建议先阅读卷四《Tokio》的第 4-7 章(Runtime / Task)——sqlx 的每一个 .await 都跑在 Tokio(或 async-std)的任务调度器上。本专栏不会重新解释 Futureasync

目录

开篇

第一部分:核心抽象

第二部分:类型映射

第三部分:查询 API

第四部分:连接与事务

第五部分:驱动实现

第六部分:工具与工程

第七部分:生产落地

源码版本

本专栏所有源码引用均基于以下版本(2026 年 4 月 24 日锁定):

Crate 版本 Git Tag
sqlx 0.8.6 v0.8.6
sqlx-core 0.8.6 v0.8.6
sqlx-macros 0.8.6 v0.8.6
sqlx-macros-core 0.8.6 v0.8.6
sqlx-postgres 0.8.6 v0.8.6
sqlx-mysql 0.8.6 v0.8.6
sqlx-sqlite 0.8.6 v0.8.6

v0.8.6 对应的 commit 是 bab1b02(2025-05-19 发布)。读者可通过以下命令获取与本专栏完全一致的源码:

git clone https://github.com/launchbadge/sqlx.git
cd sqlx && git checkout v0.8.6

也可以直接使用 cargo 已下载的本地副本(推荐):

# 在任意 sqlx 项目里执行
cargo doc --open
# 或者直接打开 ~/.cargo/registry/src/index.crates.io-*/sqlx-core-0.8.6/

正文每一段源码引用都会标注文件路径和行号(例如 sqlx-core/src/database.rs:74),读者可在对应版本的代码中逐行验证。

与其他专栏的关联

版权声明

本专栏内容为 杨艺韬 版权所有,保留一切权利。未经书面许可,不得全文或大段转载、改编、翻译,或用于任何商业用途(含以本专栏内容训练模型、生成衍生课程或商品)。

欢迎分享本专栏的链接。引用少量内容用于评论、教学或研究时,请署名 杨艺韬 并附上原文链接。