Skip to content

设计到第三范式,然后适度回退

另有 28 种语言版本

English · Español · 日本語 · Português (Brasil) · Français · Deutsch · Italiano · 한국어 · Português (Portugal) · 繁體中文 · Bahasa Indonesia · Türkçe · Polski · Tiếng Việt · ไทย · Nederlands · العربية · עברית · Українська · हिन्दी · Русский · Svenska · Dansk · Norsk bokmål · Suomi · Español (México) · Français (Canada) · Deutsch (Schweiz)

A park seen from above: paths at right angles, and one diagonal shortcut that people kept walking, now deliberately paved, with the old worn track still faint beside it.
图片由 AI 生成

第 5 个模块,共 6 个,属于 Data Architect 课程第 2 章为含义建模数据。每个模块都可以单独阅读。 上一篇:把数据放在该放的地方 · 下一篇:把模型落到 Salesforce 中

**本模块回答的问题:**什么时候我应该有意偏离一个干净的规范化模型?

在上一个模块里,我告诉你要按第三范式来设计。

现在我要告诉你打破它。

有时候。

这个区别很重要。无意中构建出一个杂乱的数据模型,和有意地对一个干净的模型做反规范化,差别很大。如果你一开始就不知道数据属于哪里,把它复制到别处就不是架构,只是重复。

但一旦理解了干净的逻辑模型,我就可以有意识地决定,为消费数据的人和系统把数据塑造成不同的形状。

在可行的情况下设计到第三范式。然后在必要时适度回退。

注意这句话没有说什么。它没有说第三范式更容易理解。有时它更难:一个完全规范化的模型可能把一个业务概念分散到五个对象里,而使用它的人得在脑子里把它重新拼起来。规范化告诉我每个事实属于哪里。它不保证那是最容易阅读、使用或做报表的形状。这正是为什么适度回退是一种设计技能,而不是一种失败。

先为真相建模。再决定这个真相需要如何交付。

回到狗,这一次我们要做一些有意的选择。

**品种:选项列表,还是独立的表?**用我在本模块后面会给你的那些问题过一遍。信息是不是不止一列?是的:至少还有体型。有多少个取值?各家犬业俱乐部认可的品种有几百个(光是美国养犬俱乐部 American Kennel Club 就列出了 205 个),远远超过我大约 20 个的经验法则。所以品种值得拥有自己的表,每只狗都指向它的品种。这就解决了我们从模块 1 一直带着的问题:品种是一个独立的方框。

**熟练程度:选项列表。**学习中、良好、精通(Learning、Good、Expert)。三个标签,关于它们没有别的可说,而且不会变。这正是选项列表的用途。

每只狗会多少项技能?答案已经在模型里了:数一数这只狗的狗狗技能记录。但训练师希望每份报表上都有这个数字,可以按它排序、按它筛选,而不必跨两张表去建报表。所以我适度回退,在狗身上直接保留一个已会技能数(Tricks Known)。这是一份副本。真相仍然存在于狗狗技能中;这个计数只是它的一种呈现,必须与之保持同步。Salesforce 给了我们一个干净的办法来做到这一点,我们会在模块 6 看到。

现在来个转折:狗狗学校开始收猫了。我们怎么办?一个选项:在狗的表旁边加一张猫的表,在狗狗技能旁边加一个猫咪技能。一切都翻倍:每个界面两份,每份报表两份,等到哪天开始收兔子,就是三份。另一个选项:退一步,注意到一只猫有主人,也许有繁育者,有品种、出生日期,耐心一点的话,还会一两项技能。和狗一样的事实,一样的关系。所以我们做泛化:一个宠物(Pet),带一个物种(Species),狗或猫。每个品种也属于某个物种,那才是物种真正所在的地方:拉布拉多是狗。宠物上有意带着一份副本,这样每个界面和每份报表都可以先问“狗还是猫?”,而不必去查品种。又一份有意为之的副本,到模块 6 它会有自己的用处。

这就是为变化而设计,而且这是一种权衡。泛化得太早,一切都会变成一个含糊的“事物”(Thing),带着一个谁也看不懂的类型字段。只有当业务真的给我带来第二种东西,而且两者几乎所有方面都相同,就像这里的狗和猫一样,我才做泛化。如果猫需要非常不同的事实,我会三思。

权衡之后的宠物模型 作为详细数据模型的宠物模型。宠物位于原来狗的位置:宠物 ID,又以芯片号来标识,主人、繁育者和品种的 ID,物种(一个选项列表:狗或猫)、名称、出生日期,以及为报表保留的已会技能数。品种现在已连线(一只宠物属于一个品种),并有自己的物种、名称和体型。宠物技能位于宠物和技能之间,熟练程度为选项列表,并有学会日期;技能评估位于它的下方,带有日期、熟练程度和评估人。主人拥有宠物,繁育者繁育宠物,与之前相同。 拥有 繁育 属于 会 见于 评估于 主人 主人 ID 记录 ID 名称 电话 ZIP 城市 繁育者 繁育者 ID 记录 ID 名称 城市 宠物 宠物 ID 记录 ID 芯片号 又称 主人 ID 繁育者 ID 品种 ID 物种(选项列表) 名称 出生日期 已会技能数(计数) 品种 品种 ID 记录 ID 物种(选项列表) 名称 体型 宠物技能 宠物技能 ID 记录 ID 宠物 ID 技能 ID 熟练程度(选项列表) 学会日期 技能 技能 ID 记录 ID 名称 难度 技能评估 技能评估 ID 记录 ID 宠物技能 ID 日期 熟练程度(选项列表) 评估人 恰好一个 零个或一个 一个或多个 零个或多个 © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

现在到 Steve Trucking 做同样类型的选择,那里的利害关系更大。

最干净的模型不一定是最好的系统

Section titled “最干净的模型不一定是最好的系统 ”

想象一个配送小组在一个停靠点。配送有它的项目。安装有一份检查清单。每一步都需要照片。收货人要签字。

规范化的模型很漂亮:配送 → 配送项目 → 安装检查清单 → 照片,每样都在该在的地方。

然后我和真正使用它的人坐下来。一个配送小组每天要完成大约十个停靠点,在人家的车道上,常常没有信号。如果我漂亮的模型让他们穿过一个由各种界面和相关列表组成的迷宫才能完成一个停靠点,那我就有一个架构问题。

数据模型并不是我们唯一在设计的东西。

我们在设计一个系统。

这是我有时会偏离一个完全规范化模型的原因之一:用户体验。

也许某个概念在技术上可以成为自己的实体,但把它分出来增加的复杂度几乎没带来什么价值。也许一个带有好几层子记录的父记录在学术上很干净,但录入和维护起来很痛苦。也许这个概念很小、很稳定,主要被用作一个选择项。

在这里,我不再只问**“这个事实理论上属于哪里?”,而是开始问“这个系统实际上会被怎样使用?”**

这并不意味着每一句关于多点一下的抱怨都要把模型做扁平。它意味着可用性是一项正当的架构输入。

报表是另一个常见原因。

每个零售合作伙伴都想要一份记分卡:按区域、按月份统计的准时配送情况。在干净的模型里,一次配送知道它的配送中心,配送中心知道它的区域。技术上,我们可以沿着这条路径找过去。

但报表工具能吗?自己做报表的人能吗?他们能不能不借助一套比问题本身还复杂的报表架构就回答这个问题?

这时我可能会把区域复制或推导到配送上,离它被使用的地方更近一些。

现在我引入了重复。所以我需要另一个问题:谁拥有真相?

如果源值变了,我的副本怎么办?它应该跟着变吗?还是应该保留配送发生时为真的那个值?这是不同的需求。

反规范化并不会消除复杂度。

它只是转移了复杂度。

这里有一个 Salesforce 管理员经常遇到的小例子。用户需要从一组值中选择。我是再创建一个实体并与之关联,还是用一个选项列表?

拿配送的状态来说:已排期、已装车、配送中、即将到达、已送达、已安装、异常、已退回。一份短小、稳定的标签列表。选项列表正是我想要的。

再看停靠点的类型:仅配送、配送加安装、配送加拉走旧货。这个就带有自己的信息:停靠点要花多长时间,配送小组需要什么技能,适用哪份安装检查清单。它独立变化,由业务方管理。这是一个实体,不是一个标签。

如果我为每一个理论上可以规范化的概念都创建一个对象,我就可能构建出一个技术上很漂亮、却没人用得开心的系统。如果我因为省事把每个概念都做成选项列表,我很快就会把自己逼进死角。

所以我会问:它有自己的信息吗?有关系吗?它会独立变化吗?它需要作为数据来管理吗?业务方将来是否可能需要它提供不止一个标签的东西?

下面是我在设计会议上实际会问的问题。

**信息是否不止一列?**如果这个值需要一个描述、一个时长、一份技能列表、一个负责人,它就不再是一个标签了。

**有多少个值?**我的经验法则是大约 20 个。超过 20 个值,选项列表就开始不好用了,是时候考虑用对象了。美国各州有五十个,是我们忍着用的例外,国家就更糟了;说实话,有谁喜欢国家选项列表吗?我觉得它很烦人。

**这些值是否会随时间不断新增,由谁新增?**添加选项列表的值是一次配置变更,这意味着要找管理员。如果业务方每周都在添加值,而添加的人又不是管理员,选项列表就会让每一个新值都得先过一张工单。

**人们需要按它筛选和做报表吗?**选项列表在这方面很出色。查找关系就没那么干净了。每个查找关系都会多一跳:报表类型只能沿着一条很短的相关对象链走下去(自定义报表类型最多允许四层对象关系),界面会变成相关列表套相关列表,代码也会更复杂。

一份值列表往往一开始是选项列表,随着时间推移,**它会挣得一个独立的对象。**让它自己去挣。

标签还是实体?状态与停靠点类型 两张卡片。配送状态,一个选项列表:八个稳定的值,每个都是一个单词或带连字符的单词(Scheduled、Loaded、Out-for-Delivery、Arriving、Delivered、Installed、Exception、Returned),每个值都只是一个标签。停靠点类型,一个实体:二十四个值,还在增加,例如仅配送、配送加安装洗衣机或燃气灶、配送加拉走旧货、仅拉走旧货、退货取件,还有另外十八个;每个值都有自己的停靠时长、所需配送小组技能和安装检查清单。下方的说明:超过大约 20 个值,或者有自己的属性,它就是实体,不是标签。 配送状态 选项列表 值:8 个,且稳定 Scheduled Loaded Out-for-Delivery Arriving Delivered Installed Exception Returned 每个值都是 只是一个标签 停靠点类型 实体 值:24 个,还在增加 仅配送 配送 + 安装:洗衣机 配送 + 安装:燃气灶 配送 + 拉走旧货 仅拉走旧货 退货取件 ……还有 18 个 每个值都有自己的 停靠时长 所需配送小组技能 安装检查清单 超过大约 20 个值,或者有自己的属性:它就是实体,不是标签。 © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

一个在几千条记录下运行得很漂亮的设计,在几百万条记录下可能表现得截然不同。

所以不要只看图。要了解数字。

Steve Trucking 每年大约有一百万次配送,每年增长 30%。每次配送大约收集八次状态更新。今年就是八百万条状态记录,到第三年大约每年 1,350 万条。再加上每年四五百万张照片。

同样的模型,截然不同的架构。

我们会在 Architecting at Scale 中深入得多。现在请记住:**数据量可以改变一个数据建模的决定。**一个逻辑上正确的关系,并不会自动成为每种规模下最好的物理设计。

到目前为止,我们一直在考虑运营系统。调度员给配送排路线。配送小组完成停靠点。系统随着业务运转而变化。

但业务方也想分析多年的历史。哪些合作伙伴和区域经常延误?配送时间有什么变化?安装需要二次上门的频率有多高?

一个分析系统可能希望同样的信息以不同的方式组织。你会听到数据仓库、事实、维度和星型模式这些术语。我们在这里不展开;在 Steve Trucking,那就是 Snowflake 数据仓库,我们会在课程后面回到它。

现在只要记住:不同的消费者可能需要同一个真相的不同形状。

而现在又多了一个消费者:AI。

一位收货人发短信问:“我的货到哪儿了?”一个 AI 助手可能会查到这个:

配送 10427,路线 3312,停靠点 7,卡车 117,时间窗口 13:00–15:00。

传统应用很乐意沿着这些关系走下去。但要回答收货人,助手需要从好几个地方拼出上下文:

你的 Redrock 洗碗机是今天从凤凰城配送中心出发的路线上的第七个停靠点。卡车还有三站就到,仍在你下午 1 点到 3 点的时间窗口内。

我们从好几个相关实体中取出信息,为某个特定的消费者拼出了一个更扁平、上下文更丰富的表示。

这并不意味着我要毁掉我的规范化模型。更不意味着“AI 喜欢扁平的数据,所以把所有东西都放进一个巨大的对象里”。不要那样做。

真相的来源保持干净。AI 得到的是一个为它所做的工作而构建的表示。这和我们刚刚在分析中看到的是同一个思路:

一个真相来源可以支撑多种用于消费的形状。

我们会在课程后面深入讨论落地(grounding)和检索。

这里有一个危险。

如果我为了报表把区域复制到配送上,给 Snowflake 发一个扁平化的版本,再为 AI 助手构建一个上下文丰富的版本,哪一个才是正确的?

这个问题需要一个答案。我想知道权威的值在哪里。其他的一切都是这个真相为了某个目的而做的表示。

那个地方就是记录系统。反规范化的副本可以是好东西:停靠点界面、记分卡、Snowflake 和 AI 上下文,都因为有了为它们构建的形状而把工作做得更好,引用它们也没问题。但当有东西变化时,变化从记录系统开始,通常就是关系模型,然后向外流向每一个副本。没有人去 Snowflake 里修正区域,也没有人手动编辑 AI 的上下文。在源头修正,让变化流出去。

记录系统:变化向外流动 上方一个方框是记录系统,它是关系型的,变化从这里开始,箭头向下流向由它构建的四个副本:配送小组停靠点界面,一个停靠点全在一页上;合作伙伴记分卡,按区域统计的延误配送;Snowflake 数据仓库,多年的历史;以及 AI 配送上下文,用来回答“我的货到哪儿了?”。下方的说明:变化从记录系统开始,流向每一个副本;绝不修正副本。 记录系统 关系型:变化从这里开始 配送小组停靠点界面 一个停靠点,全在一页上 合作伙伴记分卡 按区域统计的延误配送 Snowflake 数据仓库 多年的历史 AI 配送上下文 “我的货到哪儿了?” 变化从记录系统开始,流向每一个副本。绝不修正副本。 © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

而当数据变化时,这一点最为重要。如果某个配送中心今天划到了另一个区域,上个月的配送应该显示新区域吗?也许应该,也许不应该。对于历史,我可能需要的是配送发生时为真的那个区域。这就不再是一个同步问题了,而是一个业务决定。

这就是为什么反规范化绝不应该是意外发生的。每一个被重复的事实,都会带来关于所有权、同步、历史和信任的问题。这是第 1 章里的权衡思维:知道你买到了什么(更简单的界面、更简单的报表、给 AI 的更好上下文),以及你付出了什么(重复的数据、同步、更复杂的真相所有权)。而且因为我们是从一个干净的逻辑模型出发的,我们可以指着每一个副本说:**“我知道它属于那边。因为这项需求,我在这里放了它的一个表示。”**这是一个我们可以评审、质疑并写下来的决定,这样下一位架构师就知道为什么。

在考虑反规范化时,我要决定的是:

  • 每个业务事实的权威来源在哪里。
  • 干净的模型是否给用户、报表、规模或性能带来了真正的问题。
  • 一个更简单的表示是否值得付出重复的代价,以及副本如何保持可信。
  • 当当前的来源变化时,历史值是否应该跟着变。
  • 分析或 AI 是否需要与运营系统不同的数据形状。

起点永远不变:知道真相属于哪里。然后,当需求证明有必要时,为交付去塑造它。

接下来,Salesforce 重新回到画面中。我们要把设计好的东西变成一个物理数据模型。