02数据库5 min

我的第一版数据模型几乎从来撑不过第一个月

第 1 天的模型是对一个你还不理解的领域提出的假设。接受这一点,会改变你怎么写它,以及改它有多疼。

作者 Salva Castells

封面图: Sundaram Perumal, CC BY-SA 4.0.
本文目录

在一个排班表生成应用里,两周内我以互不兼容的方式改了三次模型。我建了一张 worker_verifications 表,用来存模型对每个工人做的检查结果。五天后我把整张表删掉,把它唯一有用的那一列 checked_rules 塞进了 worker_week_rules。又过两天,我删掉了 weeks 表里的 extra_closed_days,而那是上一次重设计时加的。

这些改动没有一个是因为新的产品决策。都是因为我更懂这个领域了。

第 1 天的模型和第 30 天的模型

我删掉的那张表有八列:ai_result、worker_hash、context_hash、verified_at、checked_rules,再加上键。想法是缓存验证结果:如果工人的 hash 和上下文的 hash 没变,就不用再调模型。听起来合理。

实际发生的是,领域里真正的单位不是"一次验证",而是"某个工人在某一周的规则"。而那一行本来就存在。验证只是那一行的一个属性,不是一个实体。修正它的迁移很短,读一遍就懂:

ALTER TABLE "worker_week_rules" ADD COLUMN "checked_rules" text DEFAULT '{}' NOT NULL;

UPDATE "worker_week_rules" wwr
SET "checked_rules" = wv."checked_rules"
FROM "worker_verifications" wv
WHERE wwr."week_id" = wv."week_id"
  AND wwr."worker_id" = wv."worker_id"
  AND wv."checked_rules" != '{}';

DROP TABLE "worker_verifications";
ALTER TABLE "worker_week_rules" DROP COLUMN "created_at";

四条语句。代价不在 SQL 上。

哪些改动说明你做对了,哪些说明没有

模型变更有两类,彼此完全不像。

  • 加一列。 weeks 里的 checked_week_rules,calendars 里的 calendar_rules。就是一条带默认值的 ALTER TABLE ADD COLUMN ... DEFAULT,不停机就能上线,旧代码继续能跑。这是系统在生长。是好信号。
  • 把实体挪个地方。 删一张表、改一个列名、把某个数据挂到别的东西下面。这意味着你第一次把领域建模建错了,而不是领域变了。

在同一个项目里,我把 preferred_day_off integer 改成了 preferred_days_off text,因为后来发现一个工人可能想休两天。这也属于好的那一类:这是关于世界的新信息,不是建模错误。迁移三行就够,用一个 CAST 保住数据。

坏的那一类,特征是 diff 根本不在迁移目录里。

真正的代价在 UI,不在 SQL

在一个 AI 文档工具里,我一开始把用户上传的东西叫 documents。一个月后,正确的概念是 contexts:agent 可以读取、引用和重写的文本片段,而且可能来自一个 URL,不只是文件。名字错了,而它拖着整个模型。

改掉它的那个提交完全没碰模型。它动了聊天页面、输入框,并整个删掉了 context-url-dialog、context-view 和 context-viewer:一口气删掉 1127 行。一次两秒的 RENAME COLUMN,和一整天改组件。

还有一个执行更便宜、扩散更贵的例子:同一个项目里我把 starCount 改名成 favoriteCount。SQL 是两个迁移里的五行。diff 动了模型、两个数据库 action 文件,以及负责渲染它的 UI。

我从中带走的规则是:改一个列名的代价,正比于有多少组件把它字面写死了。 而不是迁移本身的成本。

我今天怎么写第一版模型

从那以后我做了四件不一样的事,没有一件是"设计得更好"。

  • 按它是什么来命名,而不是按它从哪来。 documents 说的是来源(一个文件)。contexts 说的是这份数据在系统里扮演的角色。第二个名字扛得住来源变化。
  • 不为一个属性建一张表。 如果新实体永远和另一张表的每一行一一对应,它就不是实体。它是一组列。worker_verifications 就是这样,我花了五天才看出来。
  • 一开始就给每一列正确的类型。 在那次大重构里,我同时把 created_at 从 text 改成 timestamptz、week_start 从 text 改成 date,把当成文本存的 JSON 改成 jsonb。这些第一天就能以同样的代价做对。之后再修,就是每张表一条 ALTER COLUMN ... USING,外加审查所有把日期当字符串比较的代码。
  • UI 不重复列名。 一个从模型派生的类型,加上几个访问函数。这样一次 RENAME COLUMN 由编译器抓住,而不是靠 grep。

什么时候确实值得停下来想

在一个办公室平面图编辑器里,我从分开的 DualDesk 和 QuadDesk 组件,改成了一个带座位数的 MultiDesk,后来又重构了一次以支持停用的座位。两人桌和四人桌看起来像两种情况;实际上是同一种,带一个参数。这个如果你花十分钟问一句"2 和 4 是常量还是数据",第一天就能看出来。

我用的规则是:如果两个模型之间的差别是一个数字,那它就是数据。这种地方确实值得先想。

其余的,我写模型时就知道它是错的。我优化的不是一次写对,而是让下周修它只花一次短迁移,而不是一整天改组件。