Skip to content

前言

五行代码与一座冰山

你写了一个 Rust 后端服务。查询用户信息的代码是这样的:

rust
let pool = PgPoolOptions::new()
    .max_connections(20)
    .connect(&database_url).await?;

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

cargo check 通过了。你改了 SQL 里的 email 拼成 emial——编译器立刻在 query_as! 这一行报错:"column 'emial' does not exist"。你把 user_id 的 Rust 类型从 i32 改成 String——编译器又报错:"expected i32, found String"。整个过程里你根本没有跑过这条 SQL,但 Rust 编译器硬是替你把 SQL 的语法、列名、类型都核了一遍。

直到某一天,你想给一个 enum 类型的字段加自定义 EncodeDecode,发现派生宏的属性和 sqlx::Type 的 trait 约束缠绕在一起;你想让 CI 在没有 DATABASE_URL 的情况下也能编译,发现 .sqlx/ 目录里那一堆 JSON 是 cargo sqlx prepare 生成的离线缓存;你想搞清楚 Pool::acquire().await? 为什么在高并发下偶尔要花上几百毫秒(而你明明设了 max_connections = 50),翻到 sqlx-core/src/pool/inner.rs 发现空闲连接被 ArrayQueueAsyncSemaphore 联合守卫,fair 模式下排队会阻塞前面的获取者;你想在一个 Transaction 的作用域快结束时根据业务结果决定 commit 还是 rollback,发现 Transaction<'c>Drop 实现默认会尝试 rollback 但不能保证——必须显式调用 tx.commit().await?

五行代码底下藏着一整座冰山。

为什么读源码

Rust 数据库生态有一个鲜明的特点:生态没有 ORM 的话语权,驱动才是

sqlx 不是 ORM。它不帮你做 User.save()、不帮你做 has_many 关联、不帮你做查询 DSL。它只做一件事:把一条 SQL 和一次连接池调度,变成一次类型安全的 .await。在这个极简目标下,sqlx 把工业界这一层做到了极致:编译期校验、异步优先、零拷贝解码、运行时多态 Any、离线缓存、迁移系统、连接池、事务保护、tracing 集成——每一项都经过了最坏情况的打磨。

这意味着两件事。

第一件,你必须理解底层才能用好 sqlx。你在 sqlx::query! 里写的每一条 SQL、每一次 .bind()、每一个 #[derive(FromRow)],最终都会变成 Executor::fetchExecutor::execute 的一次调用。PoolPoolConnection 的转换不是"拿一个连接对象"——它是 AsyncSemaphore::acquire 后拿到 permit、再从 ArrayQueue 弹出 Idle<DB>、再包成 PoolConnection 并设置 Drop 回调归还的一条流水线。理解这些,你才能在看到 PoolTimedOut 错误时准确判断是"连接池满了"还是"连接建立超时",而不是靠盲目调大 max_connections

第二件,sqlx 是 Rust "编译期连接外部世界"的范本query! 是 Rust 生态里最早也是最成功的把真实数据库 schema 带进类型系统的过程宏之一。它在 cargo check 阶段通过 DATABASE_URL 连回 Postgres 或 MySQL、发一个 DESCRIBE 消息、拿到列类型、然后把这些信息注入编译器的类型推导链。这条从"SQL 字符串字面量"到"带类型的 Future<Output = Result<User, Error>>"的路径,设计细节值得每一个想做过程宏的 Rust 作者仔细读一遍。

这本书不是什么

这本书不是 sqlx 使用手册。 sqlx::query! 怎么写、PgPoolOptions 有哪些方法、FromRow#[sqlx(rename = "...")] 怎么用——这些官方文档和 examples 已经覆盖得很好。本书的每一章都在回答"为什么这样设计"和"底层怎么实现",而不是"怎么用"。

这本书不是 Tokio / async 入门。 本书假设你已经读过《Tokio 源码深度解析》,或者至少熟悉 FutureWakertokio::spawn 这些概念。第 13 章 Pool 会直接解释 AsyncSemaphore 与 Tokio 的 Semaphore 在 fair 语义上的差异,不会从零讲 Poll::Pending 是什么。

这本书不是 SQL 教程。 本书不会教你什么是 JOIN、什么是索引、什么是事务隔离级别。如果你对这些概念还感到陌生,建议先补齐 SQL 基础再回来。

这本书不是数据库内核书。 本书关心的是 sqlx 客户端的实现:协议编码、消息收发、预处理、连接池。数据库服务端的存储引擎、查询优化器、WAL——这些是 PostgreSQL / MySQL / SQLite 自己的内部,不是 sqlx 的关注点。

这本书是什么

这本书是 sqlx 0.8.6 的一份"读码日记"。 每一章都像是你和我对着屏幕,一起读 sqlx-core/src/pool/inner.rs 的 619 行、一起 grep impl ExecutorPool 的 fetch 实现、一起 git log 那个把 &mut TransactionExecutor impl 删掉的 commit(那正是 0.7 到 0.8 破坏性最大的变更之一)。2026 年 4 月锁定的 sqlx 0.8.6(tag v0.8.6、commit bab1b02)是我们共同的地图——每一处源码引用都会标注 文件:行号,你可以随时在自己的 clone 里对照验证。

这本书是一份"编译期触达外部"的教材。 sqlx::query!sqlx-macros 里约 100 行宏入口加上 sqlx-macros-core 里 3500+ 行实现,整个流程涉及:连接数据库、发 Describe 消息、解析 ParameterDescriptionRowDescription、把 PostgreSQL 的 OID 映射到 Rust 类型、按 CARGO_MANIFEST_DIR 的路径规则决定是用离线 JSON 还是连在线数据库。读通之后,你会建立起"过程宏如何与真实世界打交道"的直觉。

这本书是一份 trait 家族设计的教材。 Database trait 通过泛型关联类型(GAT)定义了 11 个关联类型,把 Row<'r>ValueRef<'r>Arguments<'q> 这些带生命周期的子类型绑在同一个 trait 下(见 sqlx-core/src/database.rs:72)。Executor trait 通过 default method 把 fetch / fetch_all / fetch_one 都建立在一个 fetch_many 抽象上(见 sqlx-core/src/executor.rs:33)。Encode / Decode / Type 三件套共同构成双向类型映射,每一面都要实现、每一面都有生命周期。这些 trait 的分裂和组合是 Rust 类型系统用于"数据形态各异的驱动"的经典案例。

读者画像

  • Rust 后端开发者:天天写 sqlx::query!,想理解派生宏属性到底做了什么、为什么 &mut Transaction 不再自动是 ExecutorPoolTimedOut 错误怎么诊断。
  • 数据库驱动作者:准备给 TiDB / ClickHouse / DuckDB 写 sqlx 驱动,想从 sqlx-core 的 trait 家族理解最小实现集合。
  • 性能工程师:在生产遇到连接池排队、PREPARE 缓存失效、慢查询诊断,需要从 sqlx 源码找根因。
  • Rust 元编程爱好者:对"过程宏怎么连数据库"感兴趣——sqlx::query! 是经典案例。
  • 《Tokio》《Axum》读者:想把异步后端栈的最后一环(持久层)源码心智补齐。

前置知识

必须掌握:

  • trait 与泛型impl Trait for Type、关联类型、where 子句、生命周期约束。sqlx 的核心抽象全部建立在 trait 家族上。
  • 泛型关联类型(GAT)type Row<'r> 这样的写法。Database trait 重度依赖 GAT。
  • FuturePin:知道 async fn 返回值是一个状态机。sqlx 的 Executor::fetch 返回 BoxStreamPool::acquire 返回 impl Future
  • async/await 语法:会写 async fn,理解 .await 在运行时的含义。
  • Tokio 基础:知道 SemaphoreRwLocktokio::spawn。第 13-14 章 Pool 会直接讨论这些原语。

最好掌握:

  • SQL 基础SELECT / INSERT / UPDATE、事务、索引。
  • PostgreSQL 或 MySQL 的线路协议(wire protocol)概念:知道有 Parse / Bind / Execute 这样的消息。第 16-17 章会详细拆。
  • 过程宏基础:知道 proc_macro::TokenStreamsynquote。第 11 章 query! 宏会用到。

完全不需要:

  • 能手写过程宏。第 11 章会讲 sqlx-macros 的实现,但你不需要自己写过 proc macro。
  • Diesel 使用经验。本书不会以 Diesel 为基准做使用对比,只在设计决策点(比如"为什么不做 DSL")偶尔对照。
  • 数据库内核经验。sqlx 是客户端,存储引擎与查询优化是服务端的事。

本书的阅读路径

本书共 23 章(含前言),按六条主线展开。章节之间的依赖关系如下:

两条典型的阅读路径:

  1. 顺序阅读(推荐):从前言到第 22 章一路读下来。第 3 章 Database trait 和第 4 章 Executor trait 是整本书的枢纽——如果没读它们就跳到 Pool 或驱动,会产生"为什么这里要这么写"的疑惑。

  2. 实战路径:第 1 → 3 → 4 → 9 → 11 → 13 → 14 → 15 → 22。这条路径先建立核心心智模型(数据库抽象 → 执行器 → 查询 API → query! 宏 → 连接池 → 事务),然后直接跳到生产实战。中间跳过的章节可以按需回看。

  3. 驱动作者路径:第 1 → 3 → 5 → 6 → 7 → 8 → 12 → 16(以 Postgres 为模板)。读完这条路径,你就拥有了给任意数据库写 sqlx 驱动所需的全部抽象理解。

每一章的结构保持一致:

  • 一个真实场景:从一行业务代码或一个编译错误切入。
  • 问题背景:为什么这个问题不是显然的。
  • 候选方案对比:其他库(Diesel、tokio-postgres、deadpool)的做法、每种的 trade-off。
  • sqlx 的选择:直接对着源码读决定。
  • 工程落地:怎么把这一章的理解用到你的日常工作里。

关于工具

本书在引用源码时会频繁用到以下命令:

bash
# 获取与本书一致的源码
git clone https://github.com/launchbadge/sqlx.git
cd sqlx && git checkout v0.8.6

# 展开宏(query! 展开后的 Future 类型才看得清)
cargo install cargo-expand
cargo expand --bin my_app

# ripgrep 用来快速定位符号
cargo install ripgrep
rg "impl Executor" sqlx-core/src/

# cargo sqlx CLI(准备 offline 缓存、迁移管理)
cargo install sqlx-cli --version 0.8.6

sqlx 源码里大量使用 crate::declare_driver_with_optional_migrate! 之类的宏来为一个数据库 crate 一次性声明所有 Database 关联类型,还有 sqlx-macros-core/src/query/input.rs 里的自定义解析器来消化 query!("SELECT ...", $1) 这种语法。cargo expand 是读懂这些代码的必备工具——第 11 章 query! 宏会直接对着展开后的代码讲解。

关于版本

本书所引用的源码,全部锁定于 2026 年 4 月 24 日的版本快照(sqlx 0.8.6 / sqlx-core 0.8.6 / sqlx-macros 0.8.6 / sqlx-postgres 0.8.6 / sqlx-mysql 0.8.6 / sqlx-sqlite 0.8.6,tag v0.8.6、commit bab1b02)。sqlx 是仍在积极演进的项目——2025 年下半年发布了 0.9.0-alpha.1,未来的正式版可能会在 ExecutorPool 的 API 上带来较大变化。但本书强调的是设计决策工程思路,这些在未来的版本里大概率依然成立。

好了,序言结束。翻到第 1 章——为什么需要 SQLx——我们开始这场读码之旅。

基于 VitePress 构建