数据库访问:sqlx 与 Diesel

Rust 数据库访问两大主流方案深度对比:sqlx 的编译期 SQL 校验与异步驱动、Diesel 的 DSL 查询构建器与类型安全、连接池(sqlx::Pool 与 deadpool/r2d2)选型、迁移工具、类型映射与 NULL 处理、事务与批量写入实践,附选型决策表。

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 逃生。

两者对比

维度sqlxDiesel
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)
BIGINTi64BigInt → i64
TEXT / VARCHARString / &strText / Varchar
NUMERICrust_decimal::DecimalNumeric → Decimal
TIMESTAMPTZchrono::DateTime<Utc>Timestamptz → DateTime<Utc>
UUIDuuid::UuidUuid
JSONBserde_json::ValueJsonb<Value>
BOOLboolBool

这些类型映射大多要靠 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 专题 一起看。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「rust」更多文章

  1. 性能剖析与优化
  2. 测试与基准:criterion 与 proptest
  3. Serde 与序列化生态