应用开发者到底需要懂多少数据库:从 ER 图到事务与索引

2026-07-16 27 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

多数应用开发者并不想成为 DBA,只希望数据库稳定地待在后台。但数据库并不是一个可以完全隐藏的实现细节:数据模型、约束、事务和索引会直接决定应用能否保持正确,以及它在数据增长后是否仍然可用。

真正需要掌握的不是每一种数据库内部算法,而是一组能够支撑日常开发决策的核心概念。ER 图是很好的起点,因为它迫使我们明确对象、属性、关系和基数,再把这些业务规则落实成数据库能够执行的约束。

先把业务关系画清楚

乌鸦脚表示法可以用很少的线条表达大量信息。例如,“一个部门拥有一名或多名员工,每名员工只属于一个部门”至少包含三条规则:

  • departmentemployee 是两个独立实体。
  • 一个部门可以关联多名员工。
  • 每名员工必须通过外键关联一个部门。

图画得漂亮并不是最终目的。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。目标是在代码提交之前看见数据风险,在性能退化时提出正确问题,并让数据库承担它最擅长的工作:长期、一致地保护事实。


相关推荐