Rust 生态里做数据库访问绕不开两条路线:一条是 sqlx,把 SQL 原样交给数据库,用宏在编译期校验语句;另一条是 Diesel,用 Rust 类型系统构建查询,几乎不写裸 SQL。两者都提供驱动、连接池和迁移工具,但设计哲学完全相反。
选错方向的代价要到项目中期才显现:动态 SQL 密集的系统用 Diesel 会处处别扭;而团队没有可连的数据库实例时,sqlx 的 query! 宏又会直接让 cargo build 失败。下面从编译期检查、异步驱动、连接池、迁移与类型映射五个维度拆开对比。
sqlx:编译期校验的异步驱动
sqlx 的核心卖点是「纯 Rust 异步驱动 + 编译期 SQL 校验」,不引入 DSL,SQL 就是字符串。
编译期查询宏
use sqlx::{PgPool, FromRow};
#[derive(Debug, FromRow)]
struct User {
id: i64,
email: String,
nickname: Option<String>,
}
// 编译期连接数据库,校验列名与类型
let users = sqlx::query_as!(
User,
r#"SELECT id, email, nickname FROM users WHERE created_at > $1"#,
since
)
.fetch_all(&pool)
.await?;
query! 会在编译时连接数据库执行 PREPARE,因此列名拼错、类型不匹配、参数个数不对都会变成编译错误,而不是运行时 panic。代价是编译期必须有可用的数据库。
离线模式
CI 或没有数据库的开发机上,用 .sqlx 缓存目录绕开这个限制:
cargo install sqlx-cli --features postgres
export DATABASE_URL=postgres://app:secret@localhost:5432/app
cargo sqlx prepare --workspace # 生成 .sqlx/*.json 元数据
cargo sqlx prepare --check # CI 中校验缓存是否过期
# CI 里带上缓存即可在无数据库环境编译
export SQLX_OFFLINE=true
cargo build --release
.sqlx目录必须提交进版本库,否则SQLX_OFFLINE=true的构建会因缺少元数据失败。改过任何query!语句后都要重新prepare。
运行时查询
SQL 无法静态确定时(多条件动态拼接、报表工具),退回运行时 API:
use sqlx::{QueryBuilder, Postgres};
let mut qb: QueryBuilder<Postgres> =
QueryBuilder::new("SELECT id, email FROM users WHERE active = true");
if let Some(kw) = keyword {
qb.push(" AND email ILIKE ").push_bind(format!("%{kw}%"));
}
qb.push(" ORDER BY id DESC LIMIT ").push_bind(limit);
let rows = qb.build_query_as::<User>().fetch_all(&pool).await?;
push_bind 走真正的参数绑定,不要用 format! 拼字符串,否则会引入 SQL 注入。
Diesel:类型安全的查询构建器
Diesel 用 table! 宏把 schema 编译成 Rust 类型,查询由 trait 组合而成,SQL 在运行时才生成但类型在编译期校验。
// schema.rs 由 diesel print-schema 生成
diesel::table! {
users (id) {
id -> BigInt,
email -> Varchar,
nickname -> Nullable<Varchar>,
active -> Bool,
}
}
// 查询:类型错误编译期即报
let results = users::table
.filter(users::active.eq(true))
.filter(users::email.like("%@example.com"))
.order(users::id.desc())
.limit(20)
.select((users::id, users::email))
.load::<(i64, String)>(&mut conn)?;
Diesel 的优点是写不出注入、写不出列名错,缺点是 DSL 学习曲线陡、复杂聚合(窗口函数、CTE、LATERAL)表达能力有限,遇到写不出的查询只能用 diesel::sql_query 逃生。
两者对比
| 维度 | sqlx | Diesel |
|---|---|---|
| SQL 写法 | 原生 SQL 字符串 | Rust DSL 组合 |
| 编译期检查 | 宏 + 数据库/.sqlx 缓存 | 纯类型系统,无需数据库 |
| 异步支持 | 原生 async(tokio/async-std) | 以同步为主,2.2+ 有 diesel-async |
| 复杂 SQL | 任意 SQL 都行 | 复杂查询需 sql_query 逃生 |
| 学习曲线 | 会 SQL 即可 | 需掌握 DSL 与 trait 组合 |
| 迁移 | sqlx migrate(纯 SQL 文件) | diesel migration(SQL + schema 重生成) |
| 适合 | 动态查询、报表、已有 SQL 资产 | 领域模型稳定、CRUD 密集的领域层 |
连接池选型
连接池的参数直接决定服务在高并发下的表现,二者的默认值都偏保守。
sqlx::Pool
use sqlx::postgres::PgPoolOptions;
use std::time::Duration;
let pool = PgPoolOptions::new()
.max_connections(20) // 上限:参考数据库 max_connections 均摊
.min_connections(5) // 保活连接,避免冷启动抖动
.acquire_timeout(Duration::from_secs(3)) // 拿不到连接就快速失败
.idle_timeout(Duration::from_secs(600))
.max_lifetime(Duration::from_secs(1800)) // 短于数据库/中间件断连时间
.connect_with(dsn)
.await?;
max_connections 不是越大越好:PostgreSQL 每个连接占一个后端进程,连接数过多会加剧上下文切换。多实例部署时总连接数应小于数据库 max_connections,必要时在中间加一层 PgBouncer,具体参数见 PostgreSQL 连接池
。
Diesel 的池
Diesel 本身不带池,需要搭配 deadpool-diesel(异步友好)或 r2d2(同步):
use deadpool_diesel::postgres::{Manager, Pool};
let manager = Manager::new(dsn, deadpool_diesel::Runtime::Tokio1);
let pool = Pool::builder(manager)
.max_size(16)
.build()?;
同步 Diesel 在 async 上下文里必须用
tokio::task::spawn_blocking包一层,否则会阻塞运行时的工作线程。
迁移工作流
sqlx migrate
迁移是纯 SQL 文件,文件名带时间戳,向前执行、可选回滚:
cargo sqlx migrate add create_users # 生成 migrations/20261007_create_users.sql
cargo sqlx migrate run # 应用到数据库
cargo sqlx migrate revert # 回滚最后一版(需 --down 文件)
cargo sqlx migrate info # 查看已应用/待应用
嵌入到程序里可以让服务启动时自动升级 schema:
sqlx::migrate!("./migrations").run(&pool).await?;
diesel migration
diesel migration generate create_users # up.sql + down.sql
diesel migration run
diesel migration redo # 回滚再重放,用于验证 down.sql
diesel print-schema > src/schema.rs # 重新生成类型
Diesel 的迁移改了表结构后必须重新生成 schema.rs,否则 DSL 与实际列不一致。更完整的版本升级与回滚策略可参考 PostgreSQL 迁移指南
。
类型映射与 NULL 处理
基础映射
| SQL 类型 | Rust(sqlx) | Rust(Diesel) |
|---|---|---|
BIGINT | i64 | BigInt → i64 |
TEXT / VARCHAR | String / &str | Text / Varchar |
NUMERIC | rust_decimal::Decimal | Numeric → Decimal |
TIMESTAMPTZ | chrono::DateTime<Utc> | Timestamptz → DateTime<Utc> |
UUID | uuid::Uuid | Uuid |
JSONB | serde_json::Value | Jsonb<Value> |
BOOL | bool | Bool |
这些类型映射大多要靠 feature 开启,例如 sqlx 需要 features = ["chrono", "uuid", "rust_decimal", "json"]。
NULL 与可空列
可空列一律映射成 Option<T>,FromRow 派生会按字段类型自动处理。查询里要区分「列为 NULL」与「行不存在」:fetch_optional 返回 Option<T> 表示行是否存在,字段级 Option<T> 表示该列是否为空。
let row: Option<(i64, Option<String>)> =
sqlx::query_as("SELECT id, nickname FROM users WHERE id = $1")
.bind(id)
.fetch_optional(&pool)
.await?;
#[derive(FromRow)] 默认按字段名匹配列名,别名不一致时用 #[sqlx(rename = "col")] 或 #[diesel(column_name = ...)] 修正。
事务与批量写入
事务
let mut tx = pool.begin().await?;
sqlx::query("UPDATE accounts SET balance = balance - $1 WHERE id = $2")
.bind(amount).bind(from).execute(&mut *tx).await?;
sqlx::query("UPDATE accounts SET balance = balance + $1 WHERE id = $2")
.bind(amount).bind(to).execute(&mut *tx).await?;
tx.commit().await?; // 未 commit 则 Drop 时自动回滚
Diesel 用 conn.transaction(|conn| { ... }),闭包返回 Result,Err 时自动回滚。
批量写入
逐条 INSERT 在大批量下网络往返是瓶颈,三种优化按收益排序:
// 1. 多值 INSERT:一次往返插入多行
let mut qb = QueryBuilder::new("INSERT INTO events (id, kind, payload) ");
qb.push_values(batch, |mut b, e| {
b.push_bind(e.id).push_bind(&e.kind).push_bind(&e.payload);
});
qb.build().execute(&pool).await?;
// 2. UNNEST:数组参数一次提交,适合超大 batch
// INSERT INTO events SELECT * FROM UNNEST($1::bigint[], $2::text[])
// 3. PostgreSQL COPY:吞吐最高,适合导入场景
let mut copy = pool.copy_in_raw("COPY events (id, kind) FROM STDIN").await?;
copy.send("1,click\n".as_bytes()).await?;
copy.finish().await?;
单条 INSERT 配 ON CONFLICT DO UPDATE 做 upsert,可避免「先查后写」的竞态。
选型与落地建议
| 项目形态 | 建议 |
|---|---|
| 已有大量 SQL / 报表 / 动态条件 | sqlx,直接复用 SQL 资产 |
| 领域模型稳定的 CRUD 服务 | Diesel,DSL 能挡住低级错误 |
| 纯异步高并发(Axum/Tokio) | sqlx 更顺,Diesel 需 diesel-async |
| 团队 SQL 水平强、要极致控制 | sqlx + 手工优化 SQL |
| 追求「改列名即编译失败」 | Diesel 的 schema 重生成链路更彻底 |
与 Web 框架集成时,把 PgPool 作为共享状态注入即可,写法见 Rust Web 框架
。无论选哪个,都建议:连接池参数按数据库容量反推、迁移文件进版本库、批量写入走多值 INSERT 或 COPY、可空列一律显式 Option<T>。数据库侧的索引与查询计划优化同样是性能的一部分,可结合 PostgreSQL 专题
一起看。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。