多数应用开发者并不想成为 DBA,只希望数据库稳定地待在后台。但数据库并不是一个可以完全隐藏的实现细节:数据模型、约束、事务和索引会直接决定应用能否保持正确,以及它在数据增长后是否仍然可用。
真正需要掌握的不是每一种数据库内部算法,而是一组能够支撑日常开发决策的核心概念。ER 图是很好的起点,因为它迫使我们明确对象、属性、关系和基数,再把这些业务规则落实成数据库能够执行的约束。
先把业务关系画清楚
乌鸦脚表示法可以用很少的线条表达大量信息。例如,“一个部门拥有一名或多名员工,每名员工只属于一个部门”至少包含三条规则:
department与employee是两个独立实体。- 一个部门可以关联多名员工。
- 每名员工必须通过外键关联一个部门。
图画得漂亮并不是最终目的。ER 图真正的价值在于暴露尚未回答的问题:员工能否暂时没有部门?删除部门时员工怎么办?是否需要保留员工调动部门的历史?
这些问题会改变表结构。如果只关心当前部门,可以在员工表中保存 department_id;如果需要追踪调动历史,就可能需要单独的任职关系表,并记录生效时间。数据库设计本质上是在决定哪些事实需要保存,以及这些事实随时间如何变化。
把规则交给数据库执行
应用层校验可以提供友好的错误信息,但它不能代替数据库约束。多个服务、后台任务和人工脚本可能同时写入同一套数据,只有数据库处于所有写入路径的共同终点。
应用开发者至少应该熟悉这些机制:
- 主键:稳定地标识一行数据。
- 外键:防止关系指向不存在的记录。
NOT NULL:区分必填数据与未知数据。UNIQUE:保护邮箱、订单号等业务唯一性。CHECK:限制状态、数量和日期范围。- 事务:让一组相关变更一起成功或一起失败。
范式不必成为饭桌上的背诵题,但开发者需要识别明显的数据重复。如果部门名称被复制到每一名员工记录中,部门改名就会变成一次容易遗漏的批量更新。拆分实体并用外键连接,通常能让事实只有一个权威位置。
可以这样实践:运行一个最小 PostgreSQL 实验
下面的示例把部门与员工关系、约束、事务和索引放进同一个小实验。运行前需要安装 Docker;端口 5432 如果已被占用,可以把命令中的 5432:5432 改成 55432:5432,并在后续 psql 命令中增加 -p 55432。
docker run --name db-basics \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=company \
-p 5432:5432 \
-d postgres:17
until docker exec db-basics pg_isready -U postgres -d company >/dev/null 2>&1; do
sleep 1
done
docker exec -i db-basics psql -v ON_ERROR_STOP=1 -U postgres -d company <<'SQL'
CREATE TABLE department (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL UNIQUE
);
CREATE TABLE employee (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE,
display_name text NOT NULL,
department_id bigint NOT NULL
REFERENCES department(id) ON DELETE RESTRICT,
salary numeric(12, 2) NOT NULL CHECK (salary >= 0),
hired_at date NOT NULL DEFAULT CURRENT_DATE
);
CREATE INDEX employee_department_idx
ON employee (department_id);
BEGIN;
INSERT INTO department (name) VALUES ('Platform');
INSERT INTO employee (email, display_name, department_id, salary)
VALUES (
'mei@example.com',
'Mei',
(SELECT id FROM department WHERE name = 'Platform'),
72000.00
);
COMMIT;
EXPLAIN (ANALYZE, BUFFERS)
SELECT e.display_name, e.email
FROM employee AS e
JOIN department AS d ON d.id = e.department_id
WHERE d.name = 'Platform';
SQL
这里有几个值得观察的设计决定:
ON DELETE RESTRICT阻止删除仍有员工的部门,避免留下孤儿记录。- 两次插入位于同一个事务中,任意一条失败都不会留下半套数据。
- 外键不会自动保证所有查询都高效,因此为常用连接列显式创建了索引。
EXPLAIN (ANALYZE, BUFFERS)会实际执行查询,只应对安全的只读语句使用;分析修改语句时应放在可回滚的测试事务中。
测试结束后可以删除容器:
docker rm -f db-basics
ORM 不能替你做完这些决定
ORM 可以生成 SQL、映射对象并管理迁移,但它无法替应用团队回答业务问题。关系是一对一、一对多还是多对多?删除采用限制、级联还是软删除?一次请求中的多次写入是否必须原子完成?这些都需要开发者明确决定。
索引也不是“越多越好”。索引通常能加快过滤、连接和排序,却会占用空间,并增加插入、更新和删除的成本。更稳妥的流程是根据真实查询创建候选索引,再通过执行计划和生产指标验证效果。
事务隔离同样存在边界。知道 ACID 的缩写还不够,开发者至少要理解并发请求可能引发丢失更新、不可重复读或序列化失败。遇到库存扣减、账户余额和任务认领等场景时,应明确使用条件更新、行锁、版本号或重试,而不是假设 ORM 会自动解决竞争条件。
数据库托管之后,责任并没有消失
托管 PostgreSQL 可以减少备份、补丁、高可用和部分监控工作,这对不想长期照看数据库的应用团队很有价值。但托管服务不会修复错误的数据模型、缺失的约束、无限增长的查询结果或跨度过大的事务。
团队应明确责任边界:平台负责哪些备份与恢复目标,应用负责哪些迁移和查询,谁监控慢查询、连接数与存储增长,以及故障发生时如何验证恢复结果。扩展和湖仓集成等能力可以扩大 PostgreSQL 的使用范围,但引入前仍要评估兼容性、升级路径、可观测性和供应商边界。
一份够用的学习清单
应用开发者不必先成为数据库管理员,但在独立负责生产功能前,最好能够完成以下事情:
- 从业务描述中识别实体、属性、关系和基数。
- 用主键、外键、唯一约束和检查约束保护核心事实。
- 为跨多条语句的业务操作划定事务边界。
- 看懂基本执行计划,并依据查询模式设计索引。
- 使用可回滚、可审查的迁移修改结构。
- 知道连接池、备份、恢复目标和监控由谁负责。
- 对并发写入、历史数据和删除语义提出明确问题。
学习数据库的目标不是记住所有 PostgreSQL 对象,也不是把自己训练成兼职 DBA。目标是在代码提交之前看见数据风险,在性能退化时提出正确问题,并让数据库承担它最擅长的工作:长期、一致地保护事实。