上一篇中我们用 FastAPI 构建了 Todo API,但数据存在 Python 内存列表里——服务重启数据就消失了。真实项目需要把数据存到磁盘上,这就是数据库的职责。
数据库(Database)是一个有组织的数据集合。关系型数据库(Relational Database)把数据组织成一张张二维表格——就像 Excel 表,每张表有行(记录)和列(字段),表与表之间通过外键建立"关系"。
想象一个图书馆:书籍表存所有书的信息,借阅者表存读者信息,借阅记录表通过书 ID 和读者 ID 把两张表关联起来——这就是"关系"的含义。图书管理员能通过借阅记录查出"谁借了哪本书",而不需要在书籍表里重复存读者信息。
| 特性 | 关系型(PostgreSQL) | 非关系型(MongoDB) |
|---|---|---|
| 数据结构 | 表格(行+列),需预定义结构 | 文档(JSON),结构灵活 |
| 查询语言 | SQL(标准化) | 各产品自有 API |
| 事务支持 | 完整 ACID 事务 | 有限事务支持 |
| 适用场景 | 订单系统、用户管理、财务 | 日志、内容管理、实时数据 |
PostgreSQL(简称 Postgres)是世界上最先进的开源关系型数据库,拥有超过 35 年的活跃开发历史。当前稳定版是 PostgreSQL 18,于 2025 年 9 月 25 日发布,引入了异步 I/O 引擎(读取性能最高提升 3 倍)、UUID v7 支持和虚拟生成列等重要特性。
选择 PostgreSQL 而非 MySQL 的核心理由:PostgreSQL 对 SQL 标准的遵守更严格,支持 JSONB 类型(可在关系型表中存储和查询 JSON 数据),拥有强大的扩展生态(PostGIS 地理空间、pgvector 向量搜索),且在数据完整性约束方面表现更优。FastAPI 官方文档的所有数据库示例均以 PostgreSQL 为默认数据库。
了解了 PostgreSQL 的定位,接下来在你的开发机上把它装起来。本系列的四设备协作策略中,macOS 作为主力开发机本地安装 PostgreSQL,树莓派作为后端服务器也运行 PostgreSQL,两者通过 psql 客户端统一连接。
执行 psql --version 后应看到类似输出:
PostgreSQL 安装后会自动创建一个与当前系统用户同名的数据库超级用户。macOS 上你的用户名是 e2do,那么直接输入 psql postgres 即可连接到默认的 postgres 数据库。
psql 提供了一组以反斜杠开头的元命令(meta-command),它们不是 SQL 而是客户端工具命令。最常用的几个:
| 命令 | 功能 |
|---|---|
\l |
列出所有数据库 |
\dt |
列出当前数据库的所有表 |
\d 表名 |
查看表结构(列、类型、约束) |
\du |
列出所有用户和角色 |
\c 数据库名 |
连接到另一个数据库 |
\q |
退出 psql |
brew services start postgresql@18;Linux 执行 sudo systemctl start postgresql
已经能用 psql 连接数据库了,接下来学习 SQL(Structured Query Language,结构化查询语言)。SQL 是与关系型数据库"对话"的标准语言——你用 SQL 告诉数据库"创建一张表"、"插入一条数据"、"查出所有完成的待办事项"。
SQL 语句按功能分为三大类:DDL(数据定义语言,如 CREATE/ALTER/DROP)、DML(数据操作语言,如 INSERT/SELECT/UPDATE/DELETE)和 DCL(数据控制语言,如 GRANT/REVOKE)。本篇重点学习 DDL 和 DML。
建表时需要为每一列指定数据类型,这决定了该列能存什么数据、占多少空间。PostgreSQL 支持数十种数据类型,初学阶段掌握以下几种即可覆盖 90% 场景:
| 类型 | 用途 | 示例 |
|---|---|---|
INTEGER |
整数(4 字节) | 1, 42, -7 |
SERIAL |
自增整数(自动生成 ID) | 1, 2, 3... |
VARCHAR(n) |
定长上限字符串 | 'hello'(最多 n 字符) |
TEXT |
无长度限制字符串 | 长文本、文章内容 |
BOOLEAN |
布尔值 | TRUE, FALSE |
TIMESTAMP |
日期时间 | 2026-08-13 14:30:00 |
UUID |
通用唯一标识符 | a1b2c3d4-... |
JSONB |
二进制 JSON(可查询) | {"key":"value"} |
现在用 SQL 创建上一篇 Todo API 需要的 todos 表。每条待办事项有:ID(自增主键)、标题、描述、优先级、完成状态和创建时间。
逐行解读这条建表语句:
id SERIAL PRIMARY KEY — SERIAL 自动递增,每插入一条记录 id 自动 +1,无需手动指定。PRIMARY KEY 标记为主键,值唯一且不为空
title VARCHAR(100) NOT NULL — 标题最多 100 字符,不允许为空
priority INTEGER DEFAULT 1 CHECK(...) — 优先级默认 1,CHECK 约束确保值在 1-5 之间
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP — 创建时间默认为当前时刻,插入时无需手动填写
建表后用 \d todos 查看表结构,输出如下:
可以看到 PostgreSQL 自动为 SERIAL 创建了一个序列(sequence)来生成自增 ID,并为主键自动创建了 B-tree 索引。CHECK 约束也被记录在表结构中,任何不符合约束的数据都会被拒绝插入。
表结构已经就绪,现在往里面填数据。CRUD 是四个数据操作的首字母缩写:Create(创建/插入)、Read(读取/查询)、Update(更新)、Delete(删除)。这四个操作覆盖了日常 90% 的数据库交互。
id、completed 和 created_at 没有出现在 INSERT 列表中——因为它们都有 DEFAULT 值,PostgreSQL 会自动填充。INSERT 后 psql 会返回 INSERT 0 3,表示成功插入 3 行。
执行 SELECT * FROM todos; 后的输出:
psql 中 BOOLEAN 值显示为 t(true)和 f(false),这是 psql 的简写显示方式,实际存储的值是 TRUE/FALSE。
DELETE 同样需要 WHERE 条件——不带 WHERE 的 DELETE FROM todos; 会清空整张表。生产环境中这条命令的危险程度不亚于 rm -rf /。
掌握了基本的增删改查后,我们来深入表设计。好的表结构能在数据库层面防止脏数据写入,减少应用层代码的校验负担。这一节用博客系统的文章表和标签表来演示多表关系设计。
主键(PRIMARY KEY)是表中每行数据的唯一标识。一张表只能有一个主键,但主键可以包含多列(复合主键)。上一节已用 SERIAL PRIMARY KEY 创建了自增主键。
除了主键,PostgreSQL 还支持以下列级约束:
NOT NULL — 该列不允许空值。用户邮箱、文章标题等必填字段应加此约束
UNIQUE — 该列值不可重复。用户名、邮箱等需要唯一的字段
CHECK(条件) — 自定义校验规则。如 age >= 18、priority BETWEEN 1 AND 5
DEFAULT 值 — 插入时未指定则自动填充默认值
外键(FOREIGN KEY)是关系型数据库的核心特性。它让一张表的某列引用另一张表的主键,从而建立"关系"。比如每篇文章属于一个作者,文章表的 author_id 引用用户表的 id。
下面创建一个博客系统的三张表:users(用户)、posts(文章)、tags(标签),通过外键串联。
关键设计要点:
REFERENCES users(id) — posts.author_id 引用 users.id,插入文章时 author_id 必须是 users 表中已存在的 id
ON DELETE CASCADE — 删除用户时,该用户的所有文章自动级联删除。也可选 ON DELETE SET NULL(设为空)或 RESTRICT(拒绝删除)
PRIMARY KEY (post_id, tag_id) — 复合主键,一篇文章不能有重复标签
ERROR: update or delete on table "users" violates foreign key constraint。这是数据库在帮你的应用兜底——应用层代码写得再差,脏数据也进不了库。当表数据量增大到上万行时,SELECT * FROM posts WHERE author_id = 5 会变慢——数据库需要逐行扫描整张表。索引(INDEX)就像书的目录,让数据库直接定位到目标行而不必全表扫描。
索引不是越多越好——每个索引会占用磁盘空间,且每次 INSERT/UPDATE/DELETE 都需要同步更新索引。经验法则:主键和频繁出现在 WHERE 条件中的列建索引,内容频繁变动的列慎建索引。
基础的 CRUD 已经能完成大部分工作,但真实业务中你还需要排序、分页、统计和跨表关联查询。这一节把 SELECT 的能力再扩展一层。
LIMIT 10 OFFSET 10 表示跳过前 10 条(第 1 页),取接下来 10 条(第 2 页)。这是 FastAPI 分页接口背后最常用的 SQL 模式。
聚合函数把多行数据合并计算为一个结果:COUNT(计数)、SUM(求和)、AVG(平均)、MAX(最大)、MIN(最小)。配合 GROUP BY 可以按维度分组统计。
JOIN 是关系型数据库的杀手锏——它让你在一次查询中把多张表的数据拼在一起。最常用的是 INNER JOIN(内连接),只返回两张表中都有匹配的行。
JOIN 的执行逻辑:取出 posts 表的每行,用 author_id 去 users 表中查找匹配的 id 行,把两张表的指定列拼在一起返回。WHERE 条件在 JOIN 之后过滤,只返回已发布的文章。
| JOIN 类型 | 行为 | 使用场景 |
|---|---|---|
| INNER JOIN | 只返回两边都有匹配的行 | 最常用,文章+作者 |
| LEFT JOIN | 左表全保留,右表无匹配则填 NULL | 查所有文章(含无标签的) |
| RIGHT JOIN | 右表全保留,左表无匹配则填 NULL | 较少使用 |
理论学完了,现在做一件有实际意义的事——把上一篇用内存列表存储的 Todo API 升级为 PostgreSQL 持久化存储。使用 SQLAlchemy 2.0 的异步引擎 + asyncpg 驱动,与 FastAPI 的 async/await 体系完美融合。
SQLAlchemy 2.0 从 2023 年发布以来已成为 Python 生态的 ORM 标准。asyncpg 是一个高性能的 PostgreSQL 异步驱动,比传统的 psycopg2 快 3-5 倍。连接字符串格式为 postgresql+asyncpg://用户:密码@主机:端口/数据库名。
SQLAlchemy 2.0 使用 Mapped[类型] + mapped_column() 声明模型,比旧版的 Column() 语法更类型安全。Python 3.10+ 的 str | None 语法直接对应 SQL 的可空列。
与上一篇内存版的关键区别:
Depends(get_db) — 依赖注入获取数据库会话,每个请求独立会话,请求结束自动关闭
select(Todo) — SQLAlchemy 2.0 的查询语法,替代旧版 query() API
db.commit() — 写操作后必须提交事务,否则数据不落盘
lifespan — 应用启动时自动建表,替代旧版 @app.on_event("startup")(已废弃)
create_all() 自动建表——它只在表不存在时创建,无法修改已有表结构。使用 Alembic(SQLAlchemy 的数据库迁移工具)管理表结构变更,每次修改模型后生成迁移脚本,逐步应用到生产数据库。可搜索关键词 "Alembic SQLAlchemy migration" 了解详情。启动服务:fastapi dev main.py。打开 http://localhost:8000/docs 测试 API,创建几条待办事项。现在关闭终端重启服务——数据还在,因为它们已经持久化到 PostgreSQL 中了。