03 开发者第一次真正使用 PostgreSQL: 建模、对象与访问方式

大多数数据库问题,不是从慢 SQL 开始的,而是从第一次建表就埋下了。字段类型乱、约束缺失、主键选择随意、索引靠感觉加,这些问题早期通常不会立刻爆炸,但业务一增长,代价会迅速放大。所以,开发者真正开始使用 PostgreSQL 时,最先该学的不是花哨语法,而是怎样设计一份不会轻易背刺你的模式。

建模时先回答四个问题

在写 create table 之前,先想清楚下面四件事:

  • 这张表记录的业务事实是什么
  • 一行数据由什么标识
  • 哪些字段必须正确,哪些字段只是补充信息
  • 未来最常见的查询路径是什么

如果这四个问题答不出来,后面的主键、约束、索引设计大概率也会漂。

PostgreSQL 建模的几个硬原则

1. 类型表达语义,不要把一切都存成字符串

最常见的低质量设计,就是能偷懒的地方全用 textvarchar。这种设计短期方便,长期会带来三类问题:

  • 校验边界模糊
  • 索引和统计信息质量下降
  • 应用和数据库之间出现大量隐式转换

能用 integernumerictimestampbooleanjsonb 的地方,就尽量别用字符串硬顶。

2. 约束越早加,系统越省心

数据库约束不是“给 DBA 看的装饰品”,而是最便宜的一致性保证。

至少要优先考虑:

  • primary key
  • not null
  • unique
  • 必要的 foreign key
  • 有业务意义的 check

如果这些约束全靠应用层代码约定,最终一定有人绕过去。

3. 主键是标识,不是业务说明书

主键最重要的是稳定、短小、低变更风险。不要把一堆业务字段拼成一个庞大的联合主键,除非你真的非常确定这就是最自然的实体标识。

多数业务表中,一个代理主键加必要的唯一约束,往往比复杂的业务主键更稳。

4. 索引围绕查询路径,而不是围绕“重要字段”

索引设计最容易犯的错,是看到某个字段“经常用”就单独给它建索引。真正应该问的是:

  • 查询条件是单列还是多列
  • 是等值过滤还是范围过滤
  • 是否需要排序
  • 是否经常只取少数列

索引是访问路径设计,不是字段荣誉勋章。

一张比较稳的业务表示例

create table orders (
  id bigserial primary key,
  user_id bigint not null,
  status text not null check (status in ('pending','paid','closed','cancelled')),
  amount numeric(18,2) not null check (amount >= 0),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create index idx_orders_user_created_at
on orders (user_id, created_at desc);

这份定义不复杂,但有几个关键点:

  • 类型明确
  • 状态字段有边界
  • 时间字段默认生成
  • 索引对应典型查询路径

这种表结构比“先都存进去再说”强太多。

jsonb 很有用,但不是万能垃圾桶

PostgreSQL 的 jsonb 很强,但越强越容易被滥用。适合用 jsonb 的场景通常是:

  • 字段形态存在弹性,但核心结构仍稳定
  • 部分扩展属性不值得频繁改表
  • 查询只涉及少量特定键

不适合的场景是:

  • 核心业务字段全塞进去
  • 后续需要频繁 join、排序、聚合
  • 你其实知道结构,只是懒得建列

一句话: jsonb 适合“扩展属性”,不适合“逃避建模”。

应用访问层的几个好习惯

  • 永远使用参数化 SQL,不拼接字符串。
  • 非必要不要 select *
  • 深分页尽量用 keyset 分页,不要无限 offset
  • 幂等写入优先考虑 insert ... on conflict
  • 更新时间字段时保持统一策略,不要一半靠应用、一半靠触发器。

最容易被忽视的设计问题

  • 用软删除代替一切,最后把查询和索引都拖慢。
  • 把状态机逻辑完全放进字符串字段,没有清晰约束。
  • 只关心写入成功,不关心后续怎么查。
  • 每个表都有很多“预留字段”,但没人知道意义。

真正高质量的 PostgreSQL 设计,往往并不复杂。它只是把“数据是什么、如何保证正确、如何被访问”这三件事在一开始想清楚了。