43-数据库与 SQL
17 分钟阅读
数据库与 SQL (Database and SQL)
面向 Rust 1.97.1 (stable, 2026-07)。本篇假设你熟悉 Go,凡有可比性处均给出 🐹 Go 对比。
热度:
hot高频 |common常见 |occasional偶尔 |advanced进阶少见
本篇能解决什么:
- 你会不会像找
database/sql一样在 std 里搜 DB API,结果一无所获? - 你是否分不清 sqlx / diesel / SeaORM 该选哪个,以及和 Go 的 database/sql + ORM 怎么对照?
- 你是否想知道:连接串放哪、
query!和运行时查询差在哪、行怎么映射成 struct、事务与连接池怎么写? - 你是否担心:在 async 里阻塞、SQL 注入、本地怎么起测试库、迁移(migration)放哪跑?
术语速查表:
| 术语 / 缩略词 | 全称 / 读法 | 中文 | 一句话解释 | Go 里的近亲 |
|---|---|---|---|---|
| SQL | Structured Query Language | 结构化查询语言 | 操作关系库的声明式语言 | 同名 |
| sqlx | — | 异步 SQL 工具箱 | 写 SQL + 编译期/运行时检查 + FromRow | database/sql + 扫描库 |
| diesel | — | 同步 ORM/DSL | 用 Rust DSL 拼查询,偏同步 | GORM / 强类型查询层 |
| SeaORM | — | 异步 ORM | Entity + ActiveModel,建在 sqlx 等之上 | GORM / ent 一类 |
| DSN | Data Source Name | 数据源名/连接串 | 如 postgres://user:pass@host/db | 同概念 / sql.Open 第二参 |
| pool | connection pool | 连接池 | 复用到数据库的 TCP/会话连接 | sql.DB 内置池 |
| transaction | — | 事务 | 一组语句要么全成要么全撤 | Begin / Tx |
| migration | — | 迁移 | 版本化改表结构的脚本/工具 | goose / migrate / golang-migrate |
| prepared statement | — | 预编译语句 | 参数与 SQL 分离,防注入并复用计划 | Prepare / ? 占位 |
query! | sqlx 宏 | 编译期查询宏 | 编译时连库校验 SQL/列类型 | 无直接对应(偏 codegen) |
| FromRow | — | 行映射 trait | 把查询结果行填进 struct | Scan / sqlx-go |
| ORM | Object-Relational Mapping | 对象关系映射 | 用对象/Entity 表达表与关系 | GORM 等 |
| async | asynchronous | 异步 | Future/.await 做 I/O;见 31-async | goroutine + 阻塞驱动为主 |
| env | environment variable | 环境变量 | 运行时注入连接串等配置 | os.Getenv |
| NULL | — | SQL 空值 | 列无值;映射到 Rust 常用 Option<T> | sql.Null* / 指针 / sql.NullString |
| batch insert | — | 批量插入 | 一次插入多行,减少往返 | 多值 INSERT / CopyIn |
说明:本表覆盖本篇出现的所有专业名词与缩略词;正文首次出现时仍会就地解释一次。
热度索引:
| 热度 | 题目 |
|---|---|
hot | Q1, Q2, Q3, Q4, Q5, Q6, Q7, Q10, Q15 |
common | Q8, Q9, Q11, Q12, Q13, Q14 |
occasional | — |
advanced | — |
Q1. 为什么 Rust 标准库没有 database/sql?
Tags: hot beginner stdlib ecosystem
适用版本: Rust 1.0+(生态选型;与具体 crate 版本无关)
一句话答案:
Rust std 不提供通用 SQL / 数据库客户端;连接、驱动、池、ORM 全在 crates.io。对标 Go 的 database/sql 时,想的是 sqlx(或 diesel / SeaORM),不是 std::。
解答:
Go 把「驱动接口 + 连接池类型」放进标准库:database/sql + 第三方 driver。Rust 的 std 刻意停在文件系统、套接字、并发原语——数据库协议、TLS 细节、异步模型不绑死进语言发行版。
你能在 std 里做的,只是「自己读配置、自己开 TCP」这类底层事,例如确认环境变量存在(配置细节见 44-configuration 与 04-running):
| |
真正连库要加依赖(示意,版本随你锁定):
| |
| |
Go 对比:
| |
- Go 怎么做:stdlib 定义
DB/Tx/Rows;驱动用 blank import 注册。 - Rust 为什么不同:避免把驱动生态与 async 运行时钉进 std;用 Cargo feature 选 Postgres/MySQL/SQLite。
- Go 程序员易踩的坑:搜
std database无果就以为 Rust「不能写 SQL」——其实是cargo add sqlx。
记忆点:
- 无 std SQL;生态默认从 sqlx 起步。
DATABASE_URL+ 驱动 feature,对标sql.Open。
Q2. sqlx、diesel、SeaORM 怎么选?
Tags: hot beginner sqlx diesel SeaORM ORM
适用版本: 选型原则;具体以各 crate 当前文档为准
一句话答案: 想「手写 SQL、异步、编译期校验」→ sqlx;想「Rust DSL / 同步 ORM 感」→ diesel;想「Entity + 异步 ORM、少写 SQL」→ SeaORM。多数 Go 用户从 sqlx 最顺。
解答:
| 需求 | 更合适 | 不太合适 |
|---|---|---|
熟悉 QueryRow / 手写 SQL | sqlx | 硬上 ORM 只会更绕 |
| 编译期校验 SQL/列 | sqlx query!(需连库) | 纯运行时字符串永远无此保障 |
| 强 DSL、迁移工具链一体 | diesel | 只要几条 SQL 的小服务 |
| async + ActiveRecord/Entity 风格 | SeaORM | 想完全掌控每条 SQL 文本 |
| 嵌入式/同步工具 | diesel 或 sqlx(SQLite) | 无脑上重 ORM |
依赖形态对比(示意):
| |
| |
选型口诀:团队已写惯 SQL、要 async → sqlx;要「表即类型、少碰 SQL」→ SeaORM;偏同步服务/经典 ORM → diesel。
Go 对比:
- Go 怎么做:
database/sql手写 SQL;要 ORM 再选 GORM / ent / sqlc。 - Rust 为什么不同:没有「官方 database/sql」,三大路线都是社区主流,选错的成本是心智模型而不是「违规」。
- Go 程序员易踩的坑:把 SeaORM 当成唯一答案,或把 diesel 的同步 DSL 硬塞进全 async 服务。
记忆点:
- sqlx ≈ database/sql + 更好的扫描/宏。
- SeaORM / diesel ≈ ORM 层;先问「要不要手写 SQL」。
Q3. 连接串和 env 怎么管?
Tags: hot beginner DSN DATABASE_URL env
适用版本: 与具体驱动无关;配置习惯见 44-configuration
一句话答案:
用 DSN(Data Source Name,数据源名/连接串,常叫 DATABASE_URL)放环境变量;代码里 std::env::var 读取,不要把密码写进仓库。本地可用 dotenvy,生产靠编排注入。
解答:
约定俗成的变量名是 DATABASE_URL:
| |
标准库只负责读字符串:
| |
| |
| |
密钥分层、文件与 CLI 覆盖顺序见 44-configuration;进程如何带着 env 启动见 04-running。
Go 对比:
| |
- Go 怎么做:同样推荐 env 注入 DSN,而不是写死在源码。
- Rust 为什么不同:读 env 的 API 更「显式 Err」;其余习惯几乎一样。
- Go 程序员易踩的坑:把
.env提交进 git,或在库代码里expect掉缺失配置却不在二进制入口统一处理。
记忆点:
DATABASE_URL进 env,不进仓库。- dotenvy 仅开发;生产靠平台注入。
Q4. query! 和运行时查询差在哪?
Tags: hot sqlx query! macros compile-time
适用版本: sqlx 0.7+/0.8.x(宏需对应 features 与可连库或 offline 数据)
一句话答案:
运行时 sqlx::query / query_as:启动后才发现 SQL/列错;query! / query_as!:编译期(或 offline 元数据)校验 SQL 与类型。动态 SQL、无 DB 的 CI 常用运行时;固定查询优先宏。
解答:
| |
运行时查询(灵活,错在运行时暴露):
| |
编译期宏(示意;需 DATABASE_URL 或 SQLX_OFFLINE=true + .sqlx 数据):
| |
运行时 query | 宏 query! | |
|---|---|---|
| SQL 拼错 | 运行时 Err | 编译失败(有元数据时) |
| 动态表名/片段 | 容易 | 基本不适合 |
| CI 无数据库 | 自然 | 需 offline 或可连库 |
Go 对比:
- Go 怎么做:默认全是运行时;要编译期安心多用 sqlc 等生成代码。
- Rust 为什么不同:sqlx 把「连库校验」收进宏,接近 sqlc,但仍可手写运行时 API。
- Go 程序员易踩的坑:CI 没库又开了
query!,构建直接挂——准备 offline 或改运行时查询。
记忆点:
- 固定 SQL →
query!;动态/脚本 → 运行时。 - 宏不是魔法:背后要 schema 元数据。
Q5. 行怎么映射到 struct?
Tags: hot FromRow query_as mapping
适用版本: sqlx 0.7+/0.8.x
一句话答案:
给 struct 派 sqlx::FromRow,用 query_as::<_, T> 或 query_as! 一次取出;列名默认蛇形对齐字段,可用属性改名。对标 Go 的 Scan 进结构体字段。
解答:
| |
| |
列名不一致时:
| |
只要一列时可用 query_scalar;只要「有没有行」用 fetch_optional 得到 Option<T>。
序列化到 JSON 再出 API 时,同一 struct 可再派 serde——见 36-serde-and-serialization。
Go 对比:
| |
- Go 怎么做:按位置
Scan进变量;或用扫描辅助库。 - Rust 为什么不同:
FromRow把列→字段变成类型驱动,少写一长串&mut。 - Go 程序员易踩的坑:字段顺序/可空性(
Option<T>↔ NULL)没对齐就decode失败。
记忆点:
FromRow+query_as= 结构化取行。- NULL 列用
Option<T>。
Q6. 事务怎么写?
Tags: hot transaction commit rollback
适用版本: sqlx 0.7+/0.8.x(其它 crate 有各自 Tx API)
一句话答案:
pool.begin().await? 拿到事务,在其上 execute/fetch,成功 commit,失败丢弃(或 rollback)。对标 Go 的 db.Begin() + tx.Commit()。
解答:
| |
错误路径:? 传播时 tx 被 drop,连接归还且未提交 → 回滚。也可显式:
| |
注意:事务对象要互斥使用;不要把同一个 Tx 并发 .await 两处。
Go 对比:
| |
- Go 怎么做:
Begin/Commit/Rollback;常defer Rollback。 - Rust 为什么不同:所有权 +
Drop帮你在未 commit 时回滚;但仍要自己设计错误传播。 - Go 程序员易踩的坑:在 async 里把
&mut tx跨 await 乱用,或 commit 前就结束作用域。
记忆点:
begin→ 干活 →commit;否则 drop = 回滚。- 事务内查询也绑在
tx上,不要绑回pool。
Q7. 连接池怎么对标 Go 的 sql.DB?
Tags: hot pool PgPool sql.DB
适用版本: sqlx 0.7+/0.8.x
一句话答案:
sqlx 的 Pool(如 PgPool)≈ Go 的 sql.DB:都是带池的句柄,Clone 共享,不是「一条连接」。用 PoolOptions 设最大连接、超时;不要为每个请求 connect 一次。
解答:
| |
对照表:
Go sql.DB | sqlx |
|---|---|
Open + 懒连接 | PoolOptions::connect |
SetMaxOpenConns | max_connections |
SetConnMaxLifetime | 空闲/寿命相关选项(见文档) |
到处传 *sql.DB | 到处 Clone Pool |
Ping | pool.acquire / SELECT 1 |
| |
Go 对比:
| |
- Go 怎么做:
sql.DB本身就是池;长期持有、全局或注入。 - Rust 为什么不同:池类型按驱动区分(
PgPool/MySqlPool/SqlitePool),但用法同一心智。 - Go 程序员易踩的坑:每个 handler
connect新池;或把max_connections开到比 DBmax_connections还大。
记忆点:
Pool≈sql.DB;Clone共享。- 启动建一次池,请求里只用引用/克隆。
Q8. 迁移(migration)怎么做?
Tags: common migration schema
适用版本: 工具选型;sqlx-cli / diesel_cli / 手写 SQL 均可
一句话答案: 用版本化 SQL(或 ORM 迁移工具)管理 schema:本地/CI 跑 migrate,应用启动可选择自动 migrate 或只读校验。对标 golang-migrate / goose。
解答: 常见三路:
| 工具 | 特点 |
|---|---|
| sqlx-cli | sqlx migrate add/run,与 sqlx 搭配自然 |
| diesel_cli | 与 diesel schema 生成一体 |
| 独立 migrate 二进制 | 多语言团队统一跑同一套 SQL |
| |
迁移文件示意:
| |
应用内可选(示意):
| |
生产建议:迁移与应用发布解耦(Job/Init Container),避免多副本同时 migrate 无锁策略。
Go 对比:
- Go 怎么做:golang-migrate、goose、pressly/goose、或 ORM AutoMigrate(慎用生产)。
- Rust 为什么不同:同样「SQL 文件 + 版本表」;没有唯一官方工具。
- Go 程序员易踩的坑:在每个实例启动时无条件 AutoMigrate,造成竞态。
记忆点:
- schema 进
migrations/,不靠口头改表。 - 生产迁移要串行、可回滚策略要事先想清。
Q9. async 里为什么不能阻塞跑同步 DB?
Tags: common async blocking runtime
适用版本: Tokio 等异步运行时;见 31-async-programming
一句话答案:
异步运行时的工作线程在等 .await;若在其上调用 同步阻塞 的 DB API(如部分 diesel 用法),会卡住整条 worker,拖死其它任务。要么用 async 驱动(sqlx/SeaORM),要么把阻塞调用丢进 spawn_blocking。
解答: 错误心智(示意,不要照做):
| |
正确方向 A——全程 async:
| |
正确方向 B——必须调用同步库时:
| |
| 场景 | 做法 |
|---|---|
| 新项目 async 服务 | sqlx / SeaORM |
| 遗留同步 ORM | spawn_blocking 隔离 |
| 纯同步 CLI | 同步客户端即可,不必强行 async |
Go 对比:
- Go 怎么做:goroutine 里阻塞
database/sql是常态;runtime 用更多线程调度。 - Rust 为什么不同:Tokio 默认工人线程少,阻塞 ≠ 可接受;要显式隔离。
- Go 程序员易踩的坑:把「在 goroutine 里随便阻塞」的习惯搬进
async fn。
记忆点:
- async 路径用 async 驱动。
- 同步 DB →
spawn_blocking,别冻住 runtime。
Q10. 怎么防 SQL 注入?
Tags: hot security injection bind
适用版本: 所有通过占位符绑参的客户端
一句话答案:
永远用 绑定参数($1 / ?)传用户输入;禁止把字符串拼进 SQL。预编译/参数化对标 Go 的 ?/$1 + Exec 参数。
解答:
| |
| |
动态 IN 列表 也不要手拼未转义内容:用官方宏/按占位符生成,或先校验为合法 ID 集合再绑参。表名/列名不能绑定的,必须用允许列表(allowlist),不能信客户端字符串。
| |
Go 对比:
| |
- Go 怎么做:同样靠占位符;拼接即风险。
- Rust 为什么不同:原则完全一致;sqlx 宏也不会把「绑参」变成「可拼表名」。
- Go 程序员易踩的坑:以为「Rust 类型安全」等于「SQL 自动安全」——类型不管你有没有
format!SQL。
记忆点:
- 值 → bind;标识符 → allowlist。
- 类型安全 ≠ 注入免疫。
Q11. 测试库怎么搞?
Tags: common testing testcontainers sqlite
适用版本: 测试策略;见 24-testing
一句话答案: 单测可用 SQLite 内存库 或 mock;集成测用真实引擎(Docker / testcontainers / CI 服务)。每个测试隔离事务或独立 schema,避免共享脏数据。
解答: 轻量:SQLite(注意与 Postgres 方言差异):
| |
| |
更贴近生产:
| 策略 | 何时 |
|---|---|
sqlite::memory: | 逻辑快测、方言可接受 |
| Docker Compose / CI service | 集成测 Postgres/MySQL |
| testcontainers | 测试代码里拉容器 |
| 事务回滚夹具 | 每个用例结束撤数据 |
测试组织(单元/集成目录)见 24-testing;运行 cargo test 见 04-running。
Go 对比:
- Go 怎么做:
testing+ dockertest / testcontainers-go / SQLite。 - Rust 为什么不同:工具名不同,隔离原则相同。
- Go 程序员易踩的坑:所有包共享一个长期库却不清理,测试顺序一变就红。
记忆点:
- 快测可 SQLite;合同测要对齐真实引擎。
- 隔离数据比「能跑通」更重要。
Q12. 和 Go database/sql 总对照表?
Tags: common cheatsheet database/sql
适用版本: 心智对照;实现以所选 crate 为准
一句话答案:
把 Go 的 database/sql 心智映射到 sqlx Pool + query/bind + Tx;ORM 另选 SeaORM/diesel。没有 std 替代品,但 API 角色一一可对上。
解答:
| Go | Rust(sqlx 为主) |
|---|---|
database/sql | 无 std;用 sqlx(或其它) |
sql.Open | PoolOptions::connect |
*sql.DB | Pool(可 Clone) |
Query / QueryRow / Exec | query / query_as / execute |
Scan | FromRow / get / query_scalar |
Begin / Tx | pool.begin / Transaction |
Prepare | 驱动侧预处理;优先 bind,勿拼接 |
| 驱动 blank import | Cargo features 选 postgres/mysql/sqlite |
| GORM 等 | SeaORM / diesel |
| sqlc | sqlx query! / 其它 codegen |
| goose / migrate | sqlx migrate / diesel_cli / 独立工具 |
最小「Go 用户落地清单」:
| |
| |
Go 对比:
- Go 怎么做:stdlib 统一接口,驱动可插拔。
- Rust 为什么不同:接口在社区 crate;换驱动常换类型/feature,而不是换
driver.Driver注册。 - Go 程序员易踩的坑:死等「官方 database/sql」,耽误选型;或混用同步 ORM 与 async Web 不隔离。
记忆点:
Pool=sql.DB;bind = 防注入;migrate = schema 真相。- 先 sqlx,再按需上 ORM。
Q13. SQL NULL 怎么映射到 Rust Option?
Tags: common NULL Option FromRow
适用版本: sqlx 0.7+/0.8.x(其它 crate 同思路:可空 → Option)
一句话答案:
列声明可空(SQL NULL)→ Rust 字段用 Option<T>;NOT NULL → 直接 T。解码时 NULL 变 None,有值变 Some;别用「零值」假装空,也别对可空列写非 Option 然后指望静默成功。
解答:
和 Q5 的 FromRow 同一套:
| |
| |
写入时同样用 Option:
| |
三层别混:
| 概念 | 含义 |
|---|---|
| 行不存在 | fetch_optional → Option<User> 外层是 None |
| 行在、列 NULL | User { nickname: None, ... } |
空字符串 '' | 仍是 Some(""),不是 NULL |
query! 宏也会按 schema 空性生成 Option;手写 struct 时空性必须与表一致,否则 decode 报错。
Go 对比:
| |
- Go 怎么做:
sql.Null*、指针或驱动专用可空类型。 - Rust 为什么不同:
Option<T>就是惯用可空表示,和所有权/模式匹配一条路。 - Go 程序员易踩的坑:字段写成
String却查可空列——运行时解码失败,而不是得到""。
记忆点:
- 可空列 →
Option<T>;非空 →T。 - 「没这行」和「列是 NULL」是两层
Option语义。
Q14. 批量插入 / 多行执行常见写法?(text 示意 sqlx)
Tags: common batch insert QueryBuilder
适用版本: sqlx 0.7+/0.8.x(示意;方言按 postgres/mysql/sqlite 调整)
一句话答案:
小批量可以循环 execute;更常见是一条多值 INSERT 或 sqlx QueryBuilder 拼 bind;超大批量再考虑 Postgres COPY / 驱动批量 API。始终 .bind,禁止 format! 拼值(见 Q10)。
解答: 循环(简单,往返多):
| |
多值插入(示意固定两行;动态行数用 QueryBuilder):
| |
动态行数用 QueryBuilder(text 示意):
| |
事务里批量(要么全进要么全撤,见 Q6):
| |
选型直觉:几十行 → 多值/QueryBuilder;成千上万行 → 看引擎的 COPY/load;需要原子性 → 包事务。ORM(SeaORM/diesel)各有 insert_many,语义类似,API 不同。
Go 对比:
- Go 怎么做:循环
Exec、多值 SQL、或CopyIn/ bulk API。 - Rust 为什么不同:sqlx 把「安全拼多行」收成
QueryBuilder+ bind。 - Go 程序员易踩的坑:用字符串拼接邮箱列表——注入与转义灾难。
记忆点:
- 批量优先多值 /
QueryBuilder,别傻循环除非行数极小。 - 值全部 bind;大体积再上 COPY 类工具。
Q15. 连接池参数怎么调?(max / acquire / 与 PG 上限)
Tags: hot pool max_connections tuning
适用版本: sqlx PoolOptions 一类;其它池同思路
一句话答案:
池的 max_connections 必须小于数据库 max_connections,并给管理会话/其它服务留余量;再设 acquire_timeout、空闲回收,避免请求无限等连接。多副本时用「每实例上限 × 副本数」算总连接。
解答:
| |
| |
| |
「❌ 错误写法」——每个实例 max_connections = 100,K8s 扩到 10 副本:数据库直接拒连。
异步里还要避免「持有连接过久不做查询」。慢 SQL 先优化或加语句超时,而不是只靠加大池。
Go 对比:
| |
- Go 怎么做:
sql.DB池参数。 - Rust 为什么不同:名字在 PoolOptions,公式相同。
- Go 程序员易踩的坑:按「机器核数」设池却忽略 DB 全局上限。
记忆点:
- 先算全局连接预算,再设每进程池。
acquire_timeout防止无限排队。