正本清源:从业务规则到关系模型
3 正本清源:从业务规则到关系模型
表不是字段清单的容器,关系模型也不是把接口 JSON 原样搬进数据库。建模首先要辨认系统承诺保存哪些事实、谁拥有这些事实、哪些组合状态绝不允许出现;表、键和外键只是把这些判断变成可验证结构。
本章建立 pg36_shop 逻辑模型 v0。它会真实部署到 PostgreSQL,并用正反样例审查,但它有意保留四项未决:金额、时间、状态和标识的可靠物理表达。v0 是通往 ch04 的设计证据,不是可以复制进生产的最终 DDL。
本章目标
完成本章后,读者应当能够:
- 从业务语言区分实体、事件、状态、命令与不变量;
- 识别一项事实的权威所有者,而不是让多个服务共享写入责任;
- 分开内部主键、业务键、外部标识、幂等键和追踪标识;
- 用主键、唯一约束与外键表达关系的最小完整性;
- 根据父子生命周期选择
RESTRICT、CASCADE等外键动作; - 识别简单约束能维护的行内/引用规则,以及需要事务或流程维护的跨表规则;
- 用模式、owner 与 runtime role 建立对象边界,不依赖不受控
search_path; - 区分重复事实、历史快照、派生结果与缓存;
- 部署五表逻辑模型 v0,生成关系图、状态摘要和四项未决清单。
开始之前
本章假设已经完成 ch01 的 pg36_shop 基线和 ch02 的可重跑工作流。实验仍使用 L1 的 Pigsty default 服务进入当前主库,但建模能力完全属于 PostgreSQL;Pigsty 只提供统一端点、运行环境和后续观测载体。
下载资产:
本章案例
范围限定为五个概念:
erDiagram
CUSTOMER ||--o{ SALES_ORDER : places
SALES_ORDER ||--|{ SALES_ORDER_ITEM : contains
PRODUCT ||--o{ SALES_ORDER_ITEM : snapshotted_as
SALES_ORDER ||--o{ PAYMENT : receivescustomer保存当前客户档案;product保存当前商品目录事实;sales_order保存被接受的下单命令及买家快照;sales_order_item保存订单组成与购买时商品快照;payment保存支付尝试与外部提供方标识。
发货、库存、退款、优惠、税务和多币种不是被遗忘,而是明确排除在 v0 之外。一个小而闭合的模型比一个列很多却没有边界的“万能订单表”更适合演进。
教学路径
flowchart LR
A["业务语言<br/>事实与不变量"] --> B["标识<br/>主键与业务键"]
B --> C["关系<br/>外键与生命周期"]
C --> D["对象边界<br/>schema 与 owner"]
D --> E["规范化<br/>快照与冗余"]
E --> F["v0 实战<br/>部署 + 反例"]
F --> G["ch04<br/>可靠物理模式"]本章目录
3.1 从业务语言提取数据库事实
先写事实句、所有权和失败条件,再决定表。
3.2 标识、主键与业务键
同一行可以同时拥有多个不同目的的标识;只有一个承担内部引用主键。
3.3 关系与引用完整性
把基数与生命周期写进外键,同时承认普通 CHECK 无法维护跨行、跨表真相。
3.4 模式、所有权与对象边界
shop、shop_api、shop_private 分别承载规范事实、查询接口与内部实现;schema 名称本身不自动构成安全边界。
3.5 规范化与有意识的冗余
商品当前名称只保存一次,订单行上的名称则是购买时快照;两者字面重复,事实语义不同。
3.6 实战:建立逻辑模型 v0
- 3.6.1 用户、商品、订单、订单项与支付
- 3.6.2 在 Pigsty L1 的真实数据库中部署并用样例规则审查
- 3.6.3 产出金额、时间、状态、标识四项未决清单
- 3.6.4 生成逻辑关系图并链接 ch04 的可靠版本
v0 强制无争议的键、引用和正值规则,同时用事务内反例证明任意状态、任意金额小数位和“paid 但没有行/付款”仍可能穿透。
章节产物
运行 task.sh all 后,证据目录至少包括:
| 文件 | 证明什么 |
|---|---|
manifest.txt |
客户端/服务端版本与十个输入文件哈希 |
setup.stdout |
五表、两个辅助 schema 与一个查询 view 成功建立 |
verify.txt |
行数、权限、引用与关系摘要符合 v0 |
review.txt |
应拒绝的三类错误被拒绝,三项开放规则仍可穿透 |
review.stderr |
预期 unique、foreign key、check violation 被明确捕获 |
基线数据摘要为:
章节验收
- 能把“用户下单”改写成至少五条可判断真假的事实;
- 能解释
order_id、order_no、request_key与trace_id为什么不能互换; - 能为每条外键说明父子生命周期与删除动作;
- 能指出“订单至少一行”“paid 必须足额付款”为什么不是普通行级
CHECK; - 能区分订单行商品名称快照与无依据缓存;
- 能证明 runtime role 不是对象 owner,且无权使用
shop_private; - 能从负向实验读出 v0 的已闭合与未闭合边界;
- 不把 v0 宣称为可靠物理模式。
下一章 ch04《量体裁衣:数据类型、约束与可靠数据表达》 将逐项关闭金额、时间、状态和标识决策,并用类型、约束、反例与分区 ADR 产出可靠 DDL。
参考资料
- PostgreSQL 18:表约束
- PostgreSQL 18:权限
- PostgreSQL 18:模式与搜索路径
- PostgreSQL 18:CREATE VIEW
- PostgreSQL 18:系统目录
pg_constraint
上一章:手到擒来:psql 与可复现工作流 · 返回上卷导读 · 下一章:量体裁衣:数据类型、约束与可靠数据表达 · 查看全书目录 · 查看索引中心
3.1 从业务语言提取数据库事实
建模会议最容易从“订单表有哪些字段”开始,最后得到一个容纳所有词汇的大表,却没人能说明哪种状态算错。本节反过来:先把业务陈述改写成可判断真假的事实,再找出必须永久成立的不变量。
3.1.1 实体、事件、状态与业务不变量
四类词回答不同问题:
| 概念 | 问题 | pg36_shop 例子 |
常见误区 |
|---|---|---|---|
| 实体 | 哪个事物拥有持续身份? | customer、product、sales order | 看到名词就机械建表 |
| 事件 | 在什么时刻发生了什么? | order placed、payment attempted | 只保留当前状态,失去历史事实 |
| 状态 | 某实体当前处于什么条件? | order is placed/paid/cancelled | 把一个 text 字段当成完整状态机 |
| 不变量 | 哪些命题在每次提交后都必须为真? | order 指向已有 customer | 只写“通常”“应该”,没有失败语义 |
表与业务概念不是一一映射。一个实体可能需要多个关系保存当前事实和历史;多个小值对象也可能嵌入同一关系。判断依据是身份、生命周期、基数、更新原子性和查询责任,而不是面向对象类图。
把叙事改写成事实句
“Alice 买了咖啡和杯子并支付成功”至少包含:
- customer
CUST-ALICE在下单时存在; - order
ORD-20260729-0001属于该 customer; - order 接受了两个不同 line;
- 每个 line 记录数量与购买时单价;
- line 引用的 product 在接受订单时存在;
- payment provider 接受了一个金额为 167.80 的尝试;
- provider reference 在该 provider 范围内唯一;
- captured payment 总额与订单接受金额相符;
- “paid” 状态只有在满足支付规则后才能出现。
前七项可由本章五个关系直接表达;第八、九项是跨表和状态转换规则,v0 会故意暴露它们尚未关闭。
不变量要写出四个维度
不要只写“订单号唯一”,而要写:
再看“订单至少有一行”:
这条规则不能要求每次刚插入 order 行后就成立,否则同一事务尚未来得及插入第一个 line。时点是模型的一部分。
事件和状态不要互相伪装
payment 在 v0 中代表一次有身份的支付尝试,包含 provider reference、request fingerprint、状态和发生时间。它不是完整的支付事件流;若未来要回答每次授权、捕获、撤销和退款的顺序,需要追加事件关系或对账记录,而不是在一行上反复覆盖后声称历史仍然存在。
同样,order_status = 'paid' 只是一个断言。只有定义允许值、转换、终态、并发规则和金额条件后,它才成为可依赖状态机。ch04 负责值域表达,ch10 处理并发转换,ch13 讨论数据库逻辑边界。
3.1.2 命令模型、查询模型与数据所有权
命令模型回答“怎样接受一个合法变化”,查询模型回答“消费者怎样读取所需形状”。它们可以共享一套规范事实,但不必共享一张宽表。
规范事实与读取形状
v0 的命令侧关系是:
customer:当前客户档案;product:当前商品目录;sales_order:订单头、客户引用、幂等输入和下单快照;sales_order_item:组成关系与购买时商品快照;payment:支付尝试与外部标识。
查询侧提供 shop_api.order_summary:
item_count 与金额汇总按需计算,不在订单头重复保存。现在两行三项数据,普通 view 足够;未来是否物化、缓存或拆到读取服务,要由查询量、延迟和新鲜度目标证明。
这不是要求每个系统都采用 CQRS。核心原则更朴素:写模型首先维护事实与不变量,读接口可以投影、连接和聚合;不要为了一个列表页面,让五处写入共同维护一张含所有派生字段的表。
数据所有权不是数据库 owner
“谁拥有数据”至少有三层含义:
| 层次 | pg36_shop 例子 |
责任 |
|---|---|---|
| 业务权威 | 订单域拥有已接受订单事实 | 决定语义、修改接口与生命周期 |
| PostgreSQL 对象 owner | pg36_owner |
执行 DDL、授权、迁移 |
| 运行角色 | pg36_app、pg36_ro |
按最小权限执行命令或读取 |
三者不能混为“这个服务有数据库密码,所以它拥有一切”。
业务事实清单把本章范围写成:
| 事实 | 本章权威 | 外部依赖 |
|---|---|---|
| 当前客户档案 | pg36_shop |
身份系统可能提供外部标识 |
| 当前商品目录 | pg36_shop 教学范围 |
真实组织可能有独立商品服务 |
| 已接受订单与购买快照 | pg36_shop |
下单客户端只提交命令 |
| 本地支付尝试记录 | 支付边界 | provider 才是资金处理外部权威 |
数据库能保证 provider reference 在本地不重复,却不能证明第三方真的扣款。外部响应必须经过认证、重试、对账与补偿;跨系统一致性不能伪装成一个本地外键。
一个事实只应有一个写入责任
多个服务可以消费订单摘要,但不应绕过订单命令随意更新 order_status。否则每个写者都带着不同规则,数据库最后只能保存“谁最后提交”的结果。
若组织确实需要多写者,必须共享同一数据库不变量、并发协议与发布契约。更常见的选择是一个权威写入口,其他服务通过 API、消息或受控数据库接口提出命令。
所有权还包括删除与保留。客户档案删除不意味着历史订单必须消失;商品下架不意味着订单快照应级联删除。关系动作要服从业务生命周期,而不是代码生成器默认值。
3.1.3 哪些规则必须由数据库兜底
“都放应用”与“都放数据库”同样偷懒。选择执行层时逐条问:
- 违反后是否会形成永久无效数据?
- 是否可能由并发写者同时触发?
- 是否存在多个写入入口、批处理或人工 SQL?
- PostgreSQL 能否在正确时点原子判断?
- 规则是否依赖外部系统或人工裁决?
- 错误需要怎样映射给调用方?
数据库应兜底的最小集合
| 规则 | v0 PostgreSQL 表达 | 原因 |
|---|---|---|
| 每行有稳定身份 | PRIMARY KEY |
所有写入口共享 |
| SKU/order_no 等业务键不重复 | UNIQUE |
并发下应用“先查再插”会竞态 |
| order 必须有 customer | FOREIGN KEY |
防止孤儿引用 |
| line 必须有 order 与 product | 两条 FK | 关系事实必须真实 |
| line_no、quantity 为正 | CHECK |
单行、确定、无外部依赖 |
| 关键列必须存在 | NOT NULL |
让“未知”成为显式建模决定 |
应用仍应提前验证并返回友好错误,但数据库约束是最后防线。它覆盖后台任务、迁移脚本、并发请求和未来尚未出现的写者。
普通约束不适合什么
PostgreSQL 不支持让 CHECK 引用该行之外的表数据并承诺持续一致。下面的想法是错误方向:
CHECK 按新行或更新行验证,并假设表达式对同一行输入保持不变。其他表以后变化时,它不会自动重检;dump/restore 顺序也可能让这种伪约束失败。
跨表规则的候选实现包括:
- 同一事务中的原子命令与显式锁;
- 唯一、外键、排他约束等真正受支持的关系约束;
- 受严格设计的触发器或延迟约束触发器;
- 由状态转换把“草稿不完整”和“已接受必须完整”分开;
- 外部工作流的对账、补偿和人工裁决。
选择触发器不自动让规则正确;并发、递归、批量导入、错误语义和恢复都要验证。ch10 与 ch13 会继续。
v0 的刻意空缺
本章负向实验会证明以下状态仍能提交到事务内:
numeric接受 9 位小数;order_status接受teleported;- 一个 status 为
paid的 order 可以没有 line 和 payment。
实验随后 ROLLBACK,不会污染基线。暴露空缺比用 prose 声称“以后应用会注意”更诚实;它们进入四项未决登记并在 ch04 验收。
本节产物
为每条业务规则建立最小登记:
如果一条规则没有权威 owner 或验证时点,先不要写 DDL。技术不能替组织替你决定事实。
本节验收
- 能把一个业务故事拆成实体、事件、状态和至少五条事实;
- 每条不变量都写出范围、时点、权威与违反结果;
- 命令模型与查询投影职责分开;
- 业务 owner、对象 owner 与 runtime role 不再混用;
- 能解释哪些规则适合约束,哪些需要事务或外部协调;
- 所有未闭合规则进入登记,而不是藏在代码注释。
参考资料
返回本章目录 · 下一节:标识、主键与业务键 · 查看全书目录 · 查看索引中心
3.2 标识、主键与业务键
“这个对象叫什么”没有一个通用答案。一行可能同时需要数据库内部引用、业务沟通、外部系统对账、请求去重和链路追踪标识。把它们都塞进一个 id,会让任何一次格式或业务规则变化沿所有外键扩散。
3.2.1 自然键、代理键与外部标识
三类键各有职责:
| 类型 | 定义 | pg36_shop 例子 |
设计问题 |
|---|---|---|---|
| 自然/业务键 | 业务已经赋予的唯一事实 | SKU、order_no、customer_ref | 作用域、稳定性、大小写、回收规则 |
| 代理键 | 数据库模型人为引入的内部行身份 | customer_id、product_id、order_id | 类型、生成位置、生命周期 |
| 外部标识 | 另一个权威系统赋予 | provider_payment_ref | 必须连同提供方/租户保存作用域 |
使用代理主键不意味着可以丢掉业务唯一约束:
product_id 让内部外键紧凑、稳定;sku 的唯一约束防止数据库保存两个业务上同一商品。若只保留代理键,两行不同 ID、同一 SKU 会同时“技术合法”。
哪些自然属性不适合做主键
客户 email 在 v0 中唯一,但仍不作为主键:
- 用户可能改邮箱;
- 大小写与规范化规则尚未决定;
- 邮箱可能由身份系统合并或重新分配;
- 它包含个人信息,会传播进子表、日志和缓存;
- 所有引用表都被迫携带一个较宽可变字符串。
因此使用 customer_id 作内部身份、customer_ref 作业务引用、email 作当前可联系属性并暂时唯一。ch04 会审查文本比较与大小写语义。
SKU 也可能变更,但订单行需要“购买时看到的 SKU”。v0 同时保存:
商品改 SKU 不应重写历史订单。两个字段字面上曾经相同,语义和生命周期不同。
外部标识必须带命名空间
支付提供方的 pay-ref-1001 只在 provider 自己的命名空间内有意义:
若系统是多租户,还可能需要 (tenant_id, provider, provider_payment_ref)。不要根据测试数据“看起来全局唯一”省掉作用域;权威方没有承诺的唯一性不是事实。
外部标识也不是认证凭据。能够猜到 order_no、bigint ID 或 provider reference,不应赋予读取和修改权限。
3.2.2 主键稳定性、键宽度与传播范围
主键一旦被外键、消息、缓存、URL 和数据仓库引用,就变成传播最广的设计决定之一。评审至少看:
- 稳定性:业务是否会要求修改它?
- 作用域:数据库、租户、服务还是全球唯一?
- 宽度:每个引用、索引和 join 要携带多少字节?
- 生成:数据库、应用还是外部权威负责?
- 顺序性:是否暴露规模,是否影响写入局部性?
- 展示:客户支持与 API 是否需要可读标识?
v0 选择 bigint 只是为了让关系可运行,值由 seed 显式提供。ch04 会在 identity、UUID 与应用生成之间做正式决定。
不把可变业务键传播到所有关系
关系传播图:
| 父关系 | 内部主键 | 业务/外部键 | 子关系保存什么 |
|---|---|---|---|
| customer | customer_id |
customer_ref、email |
order 保存 customer_id,另保存下单 email 快照 |
| product | product_id |
SKU | line 保存 product_id 与 SKU/name 快照 |
| sales_order | order_id |
order_no |
line/payment 保存 order_id |
| payment | payment_id |
provider reference | 对账通过 provider + reference 查找 |
内部关系使用稳定代理键,边界接口仍可用 order_no、customer_ref 等业务标识。这样修改展示格式不会要求重写每个外键。
复合主键何时合理
sales_order_item 使用:
line 的身份只在一张订单内成立,没有脱离 order 的独立生命周期。复合键准确表达“订单中的第 N 行”,并让重复 line_no 直接失败。
若未来 line 需要在多个系统独立引用、跨订单移动或拥有大量子关系,可以再评估独立 order_item_id;不要因为所有表模板都含 id 就提前添加。
复合键也有传播成本。子表若引用 order item,必须携带两列,唯一索引和 join 也更宽。模型应在语义准确与操作成本之间明确取舍。
主键更新通常意味着身份混乱
v0 外键显式使用 ON UPDATE RESTRICT。内部主键原则上不可变;如果业务要求“把 customer_id 从 1 改为 2”,更可能是在合并实体,需要迁移引用、冲突决策、审计和补偿,而不是普通级联更新。
ON UPDATE CASCADE 是可用机制,但不应替代身份语义。一个能级联修改的键仍会影响锁、索引、复制和外部消费者。
键宽度与写入局部性的物理代价在 ch04/ch09 量化,本节先冻结职责。
3.2.3 幂等键、去重键与审计标识
网络超时后,客户端不知道服务器是否提交,最安全的重试依赖幂等协议,而不是“希望第一次没成功”。幂等键表达:
在约定作用域和保留期内,同一个 key 代表同一个逻辑命令。
v0 对下单使用:
对支付使用:
作用域不同,因为两类命令的权威和重试边界不同。
唯一键只挡重复,不验证同一请求
如果攻击者或客户端错误地用相同 key 发送不同商品列表,UNIQUE 只会告诉你已有一行。它不会判断新旧 payload 是否等价。因此 v0 还保存 request_fingerprint:
典型流程:
若没有返回行,在同一事务的后续语句读取既有记录并比较 fingerprint。并发语义、锁等待和 ON CONFLICT 快照细节会在 ch10 展开;本节只确定协议事实。
fingerprint 的序列化必须规范:字段顺序、编码、NULL、数值与 JSON 规范化都要固定。直接 md5(raw_http_body) 可能让语义相同但格式不同的请求被判为冲突,也可能遗漏不应忽略的字段。本章 hash 只是确定性样例。
去重键与幂等键
“去重”常从已有数据推测两条记录像不像,例如相同 email 与时间窗口;“幂等”是调用方和服务事先约定同一命令身份。前者可能需要概率与人工判断,后者应有确定作用域和唯一约束。不要把模糊相似度当支付幂等。
trace ID 不应唯一
created_by_trace_id 和 payment.trace_id 用于把数据库事实关联到日志、消息和调用链。同一个 trace 可能创建 order、多个 line 和 payment,因此它们通常不是唯一键,也不决定重试结果。
审计标识还不能替代审计内容。至少要知道动作、主体、时间、目标和结果;单独一串 trace ID 只有在外部日志仍可用时才有意义。
保留期与删除
幂等键如果被删除并重用,迟到重试可能创建第二笔业务。设计时必须定义:
- key 由谁生成;
- 在什么作用域唯一;
- 保留多久;
- 过期后迟到请求怎样处理;
- 数据归档/分区是否仍保留去重索引;
- 跨地域或多主写入怎样协调。
本章不删除订单和支付,因此 key 与事实同生命周期。
本节验收
- 能为每个标识写出权威、作用域、稳定性、生成方与是否公开;
- 代理主键与业务唯一约束同时存在;
- 外部标识包含 provider/tenant 等真实命名空间;
- 幂等唯一键有 fingerprint 冲突语义;
- trace ID 不被误设为主键或唯一键;
bigint只是 v0 选择,生成策略明确留给 ch04。
参考资料
上一节:从业务语言提取数据库事实 · 返回本章目录 · 下一节:关系与引用完整性 · 查看全书目录 · 查看索引中心
3.3 关系与引用完整性
外键不是为了让 ER 图好看,而是让“这条引用指向真实对象”在并发与所有写入入口下持续成立。建模还要回答可选性、基数、父子生命周期和删除后果;只写两列同名 ID 并没有建立关系。
3.3.1 一对一、一对多与多对多
关系基数要落成列、NOT NULL、UNIQUE 和 foreign key 的组合。
一对多:外键放在“多”侧
一个 customer 可以有零到多个 order,每个 order 必须属于一个 customer:
NOT NULL 关闭“订单暂时没有客户”的可能,FK 关闭“客户 ID 不存在”的可能。父侧仍可以暂时没有订单;普通 FK 不保证至少存在一个子行。
一对一:外键再加唯一
假设每个 customer 至多有一份独立 profile:
让 FK 本身成为子表主键即可保证每个 customer 最多一行 profile。若 profile 与 customer 总是同时创建、同生命周期且没有独立权限/更新原因,拆表反而可能增加 join 与一致性成本;“一对一”不是自动拆表指令。
多对多:关联关系也是事实
订单与商品看似多对多:一个 order 有多个 product,一个 product 出现在多个 order。sales_order_item 不是只有两列的机械桥表,因为这段关系还有自己的事实:
它是有属性的关联实体,主键为 (order_id, line_no),另用 product_id 指向当前商品身份。
如果只是“用户收藏商品”,关联表可能是:
一旦需要收藏时间、来源、排序或状态,这些也是关系本身的属性。
可选性要说出业务含义
可空 FK 表示“关系可能不存在或未知”,两者不是同一语义。例如 payment 可以没有 provider reference 吗?
- 若记录代表已发送给 provider 的尝试,reference 应
NOT NULL; - 若要先创建本地 pending 请求,再异步获得 reference,需要单独状态和可空时点;
- 若 provider 永远不返回 reference,应使用另一稳定外部键,而不是把 NULL 解释成所有情况。
v0 选择 provider reference NOT NULL,把“尚未调用 provider”的命令放在 payment 行创建之前。这是范围选择,不是通用支付设计。
3.3.2 外键动作与生命周期
ON DELETE/ON UPDATE 不是语法偏好,而是父事实改变时子事实怎样存活。
| 动作 | 父行删除时 | 适用直觉 | 风险 |
|---|---|---|---|
NO ACTION |
默认在约束检查时拒绝 | 引用必须先处理;可与 deferred constraint 配合 | 名称易被误读为“什么都不做” |
RESTRICT |
立即拒绝相关删除 | 父子身份都应保留 | 清理必须显式按顺序 |
CASCADE |
自动删除子行 | 子行完全是父的组成部分 | 一次误删放大成整棵树 |
SET NULL |
清空可空 FK | 子事实可独立存活且“原父已无”有意义 | 丢失直接引用,列必须允许 NULL |
SET DEFAULT |
写入默认值 | 存在真实“默认父”且 FK 仍成立 | 默认哨兵行常掩盖业务错误 |
NO ACTION 是默认值,在可延迟约束中可以等到稍后检查;RESTRICT 不允许把该引用动作延后。v0 约束都是非 deferrable,本章仍显式写 RESTRICT,让生命周期决定可见。
v0 的动作说明
| 父 → 子 | 动作 | 业务理由 |
|---|---|---|
| customer → sales_order | ON DELETE RESTRICT |
历史订单不能因档案删除消失 |
| product → sales_order_item | ON DELETE RESTRICT |
订单保留对原商品身份的引用 |
| sales_order → sales_order_item | ON DELETE CASCADE |
line 没有脱离 order 的独立身份 |
| sales_order → payment | ON DELETE RESTRICT |
支付尝试是审计/对账事实,不随订单静默删除 |
这里存在一个值得审查的张力:order line 级联、payment 限制,意味着有 payment 的 order 不能删除;没有 payment 的 order 删除会带走 line。若业务要求订单一经接受永不物理删除,可以把 order 删除权限整体收紧,而不依赖 FK 动作区分。
CASCADE 不能代替授权和保留政策。对根表执行一条 DELETE 前,仍要预览作用域、锁和子行数量。
更新动作
v0 使用 ON UPDATE RESTRICT,因为内部身份不应作为普通业务修改。业务键如 email、SKU、order_no 不承担外键,因此可以在各自规则下演进而不级联所有关系;历史快照保持原值。
FK 的物理边界
PostgreSQL 会为主键和 unique constraint 创建唯一 B-tree 索引,但不会自动为外键的引用列创建索引。删除或更新父行时,数据库需要在子表检查引用;大表缺少合适索引会产生昂贵扫描与锁等待。
本章只冻结逻辑 FK。ch09 会根据查询和父表变更路径设计引用侧索引;生产建模评审不能永远把它留空。
外键也会参与并发锁定。插入子行和删除父行竞争时,正确性由 PostgreSQL 保证,但延迟、死锁顺序和批量操作仍需设计。
3.3.3 聚合边界与跨表不变量
聚合边界回答“哪些事实必须在一个命令和事务里一起保持一致”。这里不要求套用某种领域驱动设计术语,而是给事务边界一个业务理由。
order 与 line
下单命令至少涉及:
- 创建 order 头;
- 创建一到多条 line;
- 固化商品标识、名称和单价快照;
- 计算请求 fingerprint;
- 将 order 置为已接受初始状态。
这些动作应在同一事务内完成。外键保证 line 不会指向不存在的 order,但它不能保证每个 order 至少有一条 line。可行策略是:
- 先以
draft状态创建不完整 order; - 插入 line;
- 在同一事务中验证至少一行;
- 只有验证通过才转为
placed; - 对外查询不把 draft 当已接受订单。
状态值和转换机制在后续闭合。
payment 是相邻边界
支付 provider 是外部系统,网络调用不能加入 PostgreSQL 本地事务。订单与支付记录通过 order_id 关联,但“外部扣款 + 本地状态”需要幂等、重试和对账,而不是保持一个数据库事务数秒等待第三方。
本地可以原子记录一次 provider 响应并更新订单状态;若提交结果未知,依赖 provider reference 与幂等键恢复。资金真相还要与 provider 对账。
跨表不变量要有检测查询
即使暂时没有强制机制,也要能发现违反:
基线应返回零行。review.sql 在事务内插入一个没有 line/payment 的 paid order,上述查询就能发现它,然后回滚。
检测查询不是强制约束。它缩短发现时间,却仍允许错误状态短暂或永久存在。对“绝不能提交”的规则,应设计原子命令、锁与约束/触发器;对外部最终一致规则,则定义容忍窗口、告警和补偿。
不把总额重复写进 order v0
item_subtotal 可由 line 快照的 unit_price * quantity 推导。若 v0 又在 order 保存 total,就产生两个可独立更新的事实。没有性能证据和同步责任前,view 按需计算。
未来若订单金额是法律/支付契约中的独立快照,可能需要保存经明确舍入、币种和折扣规则计算的 accepted total。那时它不是随便的缓存,而是新的权威事实;ch04 的金额决策必须先完成。
聚合评审表
| 规则 | 单表约束 | 本地事务 | 外部协调 |
|---|---|---|---|
| quantity > 0 | 是 | 不需要额外 | 否 |
| line 引用真实 product | FK | 不需要额外 | 否 |
| order 至少一行后才 placed | 否 | 是 | 否 |
| paid 金额等于 accepted total | 否 | 是 | provider 对账 |
| provider 实际扣款一次 | 否 | 本地只能记账 | 是 |
本节验收
- 每条关系都写出基数、可选性和父子生命周期;
- 一对一用 unique FK 表达,而不是双方互相引用;
- 多对多关联的自身属性没有塞回任一父表;
- 每个 FK 动作都有业务理由;
- 知道引用侧索引不会由 FK 自动创建;
- 跨表不变量有检测查询、强制层和外部协调边界。
参考资料
上一节:标识、主键与业务键 · 返回本章目录 · 下一节:模式、所有权与对象边界 · 查看全书目录 · 查看索引中心
3.4 模式、所有权与对象边界
关系模型还需要命名与权限边界。PostgreSQL schema 是数据库内的 namespace,也参与权限解析;它不是独立数据库、租户隔离或微服务边界的自动实现。边界成立要靠所有权、USAGE、对象权限和受控名称解析共同支持。
3.4.1 业务模式、接口模式与内部模式
v0 使用三个 schema:
| schema | 内容 | 谁需要 USAGE |
承诺 |
|---|---|---|---|
shop |
customer、product、order、line、payment 规范关系 | app、只读角色 | 业务事实与命令模型 |
shop_api |
order_summary 等显式查询接口 |
app、只读角色 | 面向消费者的读取形状 |
shop_private |
未来内部 helper、staging 或实现对象 | owner | 无外部兼容承诺 |
public 仍存在,但 ch01 已撤销 PUBLIC 的 CREATE。本书不把业务对象默认堆进 public,以免命名、权限与扩展对象混杂。
schema 是 namespace
同一数据库可以同时存在:
未限定的 customer 由 search_path 决定。显式 shop.customer 同时表达对象和边界,迁移、审计和安全敏感 SQL 应优先使用。
schema 不能提供:
- 独立 WAL、备份或故障域;
- 独立连接、资源隔离或主要版本;
- 自动跨租户行隔离;
- 自动阻止拥有更高数据库权限的角色访问。
需要这些性质时,应评估数据库、集群、RLS、资源治理或服务边界,而不是给 schema 换一个更宏大的名字。
接口 view 不是天然安全 view
shop_api.order_summary 把五表投影为读取形状。v0 的 app 和只读角色本来就有底表 SELECT,因此该 view 只表达接口与派生逻辑,不承担隐藏敏感行的安全责任。
PostgreSQL 普通 view 默认按 view owner 的底层权限检查。若想用 view 作为安全边界,还要审查 security_barrier、security_invoker、函数是否 leakproof、RLS 与调用者可创建对象的权限。ch23 专门处理;本章绝不因为“只 grant 了 view”就宣称数据已隔离。
接口演进需要兼容契约
CREATE OR REPLACE VIEW 不能任意改变既有列名称、顺序和类型;新查询必须保留现有列,最多在末尾增加列。消费者依赖哪些列、空值和排序,应在 ch12 的服务契约中管理。
本章 view 查询不承诺默认排序。调用方必须显式 ORDER BY。
3.4.2 对象所有者与运行角色分离
对象 owner 可以修改、删除对象和转授权限,是结构控制身份;应用只应拥有完成运行任务所需的 DML。v0 角色:
| 角色 | LOGIN | 责任 |
|---|---|---|
pg36_owner |
否 | 拥有 schema、table、view,执行受控迁移 |
pg36_app |
是 | 读取与修改规范业务关系 |
pg36_ro |
是 | 只读规范关系和接口 view |
dbuser_dba |
是 | L1 管理入口;经授权 SET ROLE pg36_owner |
DDL 脚本先以管理员认证,再:
于是新对象直接归 pg36_owner,而不是先由个人管理员拥有再批量改 owner。NOLOGIN owner 没有可泄露的直接登录凭据。
owner 权力不是普通 ACL
对象 owner 隐含拥有改变、删除和授权对象的能力。即使撤销 owner 的普通 SELECT,owner 仍能重新 grant。因此安全设计不能把“owner 账号”当作日常应用角色。
超级用户又能绕过绝大多数权限边界。L1 的 dbuser_dba 只用于教学管理;生产迁移应采用受审计、短时授权和明确发布流程。
显式权限与默认权限
setup 对当前五表执行:
并为 owner 在 shop_api 的未来关系设置默认 SELECT。ALTER DEFAULT PRIVILEGES 只影响以后由指定 owner 创建的对象,不会回填现有对象,也不会自动适用于另一创建角色或 schema。
验证实际边界:
期望 true, true, false, false。权限函数是当前状态证据,仍要结合角色成员关系、owner 和 RLS 解释。
v0 给 app 直接表 DML 是教学范围,未来 ch12 可能把部分命令收敛到更窄接口;权限演进必须与应用发布一起设计。
3.4.3 避免依赖不受控的 search_path
search_path 是名称解析规则,也是信任列表。若某角色能在路径靠前的 schema 中创建对象,未限定函数、操作符或关系名可能解析到攻击者对象。
本书运行角色默认:
迁移脚本每次又显式:
随后验证 current_schemas(false)。双重设置让正常会话有安全默认,也让脚本不依赖数据库外部配置。
为什么 pg_catalog 放在前面
即使没有写入路径,PostgreSQL 也会隐式搜索 pg_catalog;显式把它放在前面,能防止同名用户对象抢在系统函数/操作符之前解析。临时 schema 对关系与类型还有特殊搜索规则,安全敏感代码仍应写出限定名。
v0 DDL 使用:
不是所有普通查询都必须把每个内置函数写全,但迁移、SECURITY DEFINER 代码和生成 SQL 应采用更严格限定。
不把 $user, public 当无害默认
默认 search path 通常包含 "$user", public。若同名用户 schema 存在,或者 PUBLIC 仍能在 public 创建对象,解析结果可能与预期不同。是否危险取决于权限,但最简单的基线是:
- 撤销不必要的 public CREATE;
- runtime 不拥有路径中的 schema;
- 路径只含受信 namespace;
- 关键对象使用显式限定;
- 每个函数设置安全 search path,不继承调用者环境。
函数安全在 ch13/ch23 展开。
schema 权限分两层
拥有 schema USAGE 才能解析其中对象;仍需相应表/view 权限才能访问。反过来,表上有 SELECT 但无 schema USAGE,也不能通过普通限定名访问。
shop_private 对 app 撤销 USAGE,且其中未来对象不授予 runtime。owner 和超级用户仍可访问,所以它是实现边界,不是对管理员的保密区。
本节验收
- 每个 schema 有内容、消费者与兼容承诺;
- 不把 schema 宣称为独立数据库或租户隔离;
- owner 是 NOLOGIN,runtime 不拥有业务对象;
- 当前权限与 default privileges 分开验证;
- app/ro 不能使用
shop_private; - search path 只含受信 schema,迁移对象显式限定;
- 不把普通 view 当成未经审查的安全屏障。
参考资料
- PostgreSQL 18:模式
- PostgreSQL 18:安全使用模式
- PostgreSQL 18:权限
- PostgreSQL 18:默认权限
- PostgreSQL 18:CREATE VIEW
- PostgreSQL 18:规则与 view 权限
上一节:关系与引用完整性 · 返回本章目录 · 下一节:规范化与有意识的冗余 · 查看全书目录 · 查看索引中心
3.5 规范化与有意识的冗余
规范化不是把任何重复字符串都拆掉,而是让每个事实由正确的键决定并只有一个权威写入位置。冗余也不是禁词:历史快照、独立契约事实与有证据的缓存都可能重复字节,但必须有明确语义和一致性责任。
3.5.1 函数依赖与重复事实
函数依赖 X → Y 表示:在模型承诺的范围内,给定 X 就唯一决定 Y。它是业务语义,不是根据当前十行样例猜出的相关性。
v0 中:
这些依赖帮助识别事实应该放在哪里。
一张宽表的异常
假设每个订单行重复:
若这些字段都声称是“当前值”,会产生:
- 更新异常:商品改名必须更新所有历史 order line;
- 插入异常:没有订单时无法保存新 product;
- 删除异常:删除最后一条 line 可能丢掉 product 事实;
- 矛盾状态:同一 product_id 在不同 line 上显示两个当前价格。
拆成 customer、product、order、line 后,当前商品事实只由 product_id 决定并保存在一处。
规范化不是按表数评分
把每个字段放一张表会制造无意义 join;把相关字段放在同一行也不自动违反规范化。评审问:
- 这列描述的是哪一个事实?
- 由哪组键决定?
- 更新它是否需要同步其他行?
- 删除一行会不会意外删除另一类事实?
- 同名字段是同一事实,还是不同时间/契约的快照?
PostgreSQL 数组、JSONB 和复合类型也不是天然“不规范”。若值是一个有边界整体、无需独立引用与约束,嵌入可能正确;若其中元素有独立身份、基数和查询生命周期,把它藏进 JSON 只会把关系规则移到应用。ch04 再讨论半结构化边界。
用唯一和外键验证依赖
关系理论上的依赖需要实际约束支撑。product_id 主键保证一行身份,SKU unique 保证另一业务候选键,order line 复合主键保证每个行号只有一条事实。
但数据库看到的约束只是模型声明。若业务允许 SKU 在多个市场重复,单列 unique 就过强;真正键可能是 (market_id, sku)。建模错误不能靠更快索引修复。
3.5.2 派生数据、快照数据与缓存列
字节重复前先判断它属于哪一类:
| 类别 | 含义 | v0 例子 | 更新责任 |
|---|---|---|---|
| 规范事实 | 当前权威值 | product.product_name |
product 命令 |
| 历史快照 | 某时点被接受的独立事实 | sales_order_item.product_name_snapshot |
下单时写一次,之后不随 product 改 |
| 派生数据 | 可由权威事实确定计算 | item_subtotal |
view 查询时计算 |
| 缓存列 | 为性能复制派生结果 | v0 没有 | 需要同步、重建与校验 |
| 外部事实副本 | 另一系统权威的本地记录 | provider reference/status | webhook、查询和对账 |
快照不是缓存
购买后 product 从 “Coffee Beans” 改名为 “Summer Coffee”,历史 order line 仍应显示用户购买时接受的名称与单价。它们不是等待刷新到最新值的缓存,而是订单契约的一部分。
同理,buyer_email 保存下单时联系快照,customer 表保存当前 email。两者相同只是初始状态:
示例在事务末回滚。查询中历史与当前值分离是预期,不应启动“修复任务”把订单快照更新掉。
派生值先计算
订单行小计:
订单 item subtotal:
v0 由 shop_api.order_summary 计算,不在 order 存第二份。金额舍入和币种尚未决定,因此现在存 total 还会提前固化错误语义。
ch04 可能使用生成列保存纯行内派生结果;跨行聚合无法用普通生成列维护。物化 view、缓存表或应用缓存需要性能证据。
外部副本要承认权威差异
本地 payment_status='captured' 表示系统记录了 provider 响应,不自动等于资金最终结算。对账可能发现撤销、拒付或漏单。字段名称和文档应说明它是本地观察还是最终财务事实。
3.5.3 接受冗余前先定义一致性责任
增加缓存列前,设计文档至少回答:
如果没有 rebuild_procedure 和 reconciliation_query,团队实际上选择了“永远相信它不会错”。
三种一致性责任
同一行、同一事务
例如 line_total = unit_price * quantity。若确有保存价值,可用生成列或同一 SQL 写入并约束;最容易提供强一致。ch04 决定。
跨行、同一数据库
例如 order total 是所有 line 之和。触发器、事务命令或物化汇总都要处理:
- insert/update/delete line;
- 批量语句;
- 并发修改;
- 事务回滚;
- 历史数据回填;
- 触发器禁用与恢复;
- 重新计算和差异检测。
没有性能证据时,查询时聚合通常更简单。
跨系统、最终一致
例如搜索索引、缓存、分析仓库。必须定义:
- 事件或 CDC 的投递语义;
- 可接受延迟;
- 重复、乱序与丢失处理;
- 全量重建;
- 源端删除与保留;
- 差异告警和人工修复。
“异步同步”不是一致性设计的完整句子。
一个冗余决策示例
假设订单列表 P99 因聚合 line 变慢,提出在 order 保存 item_count:
- 先用执行计划和负载证明聚合是瓶颈;
- 定义源为
sales_order_item; - 决定由同一订单命令事务更新;
- 禁止其他入口直接改计数;
- 提供:
- 建立回填与告警;
- 记录若优化收益消失则删除缓存列。
v0 没有 item_count 列,view 已提供正确基线,未来优化才能有对照。
本节验收
- 能为每列写出决定它的键;
- 宽表中的更新、插入、删除异常可以用具体事实解释;
- 快照、派生、缓存和外部副本不再统称“冗余”;
- 历史快照不会随当前主数据刷新;
- 新缓存有权威源、写者、窗口、重建与对账;
- 没有性能证据时不提前存跨行聚合。
参考资料
上一节:模式、所有权与对象边界 · 返回本章目录 · 下一节:实战:建立逻辑模型 v0 · 查看全书目录 · 查看索引中心
3.6 实战:建立逻辑模型 v0
本实验把前五节的判断落入真实 PostgreSQL。它不是“写一遍 CREATE TABLE 就算建模完成”,而是同时交付业务事实、DDL、样例、正反规则、关系图和未决登记。
风险:
setup:R1·可逆变更,创建五表、两个 schema 和一个 view;seed:R1·可逆变更,会清空并重建五表的教学数据;verify:R0·观察;review:R2·破坏性演练,事务内插入反例,最后强制回滚;reset:R2·破坏性演练,删除全部 ch03 对象,要求双重令牌。
3.6.1 用户、商品、订单、订单项与支付
先审阅业务事实清单。五表各自只保存有明确所有权的事实:
| 关系 | 主键 | 业务/外部键 | 关键关系 | 快照或派生 |
|---|---|---|---|---|
customer |
customer_id | customer_ref、email | 无 | 当前档案 |
product |
product_id | SKU | 无 | 当前目录 |
sales_order |
order_id | order_no、customer-scoped request key | customer | buyer email 快照 |
sales_order_item |
order_id + line_no | 无 | order、product | SKU/name/unit price 快照 |
payment |
payment_id | provider reference、provider-scoped idempotency key | order | provider 响应记录 |
完整关系图:
erDiagram
CUSTOMER ||--o{ SALES_ORDER : places
SALES_ORDER ||--|{ SALES_ORDER_ITEM : contains
PRODUCT ||--o{ SALES_ORDER_ITEM : snapshotted_as
SALES_ORDER ||--o{ PAYMENT : receives
CUSTOMER {
bigint customer_id PK
text customer_ref UK
text email UK
}
PRODUCT {
bigint product_id PK
text sku UK
numeric current_unit_price
}
SALES_ORDER {
bigint order_id PK
text order_no UK
bigint customer_id FK
text request_key
text order_status
}
SALES_ORDER_ITEM {
bigint order_id PK,FK
integer line_no PK
bigint product_id FK
text sku_snapshot
numeric unit_price
integer quantity
}
PAYMENT {
bigint payment_id PK
bigint order_id FK
text provider
text provider_payment_ref
text payment_status
numeric amount
}可下载源文件是 model.mmd。
v0 已经决定什么
- 所有关系有主键;
- customer ref、email、SKU、order no 有业务唯一约束;
- order request key 在 customer 范围唯一;
- payment provider ref 与 idempotency key 在 provider 范围唯一;
- 所有引用由 FK 维护;
- line_no、quantity、price/amount 的基本正值规则由 CHECK 维护;
- order line 保存购买时商品快照;
- runtime 与 owner 分离;
- 查询摘要由 view 派生,不重复写入 order。
v0 故意没有决定什么
DDL 使用手工 bigint、无指定 precision/scale 的 numeric、text 状态和 timestamptz。它们让逻辑关系可以运行,但没有回答:
- ID 由 identity、UUID 还是应用生成;
- 金额是否用 minor units、如何表达币种和舍入;
- 状态允许值与转换;
- 业务时间 zone、precision、clock source;
- paid/order-line 等跨表不变量如何原子强制。
因此所有对象 comment 和输出都标记 ch03-v0。
3.6.2 在 Pigsty L1 的真实数据库中部署并用样例规则审查
沿用 ch02 私有 service file:
确认目标是 L1 的 pg36_shop 主库。下载 ch03 文件到同一目录并运行:
综合顺序:
Pigsty 5436 负责把管理连接送到当前主库 PostgreSQL;应用 DDL 由版本化 SQL 迁移负责,不应塞进 Pigsty 集群拓扑配置。Pigsty 可以声明 database、role 和 service,业务表模式仍属于应用发布物。
setup 的漂移保护
setup.sql使用 CREATE ... IF NOT EXISTS 支持重入,但随后从 pg_attribute 与 pg_constraint 验证:
- 五表的列名、类型、NOT NULL 与列集合精确匹配;
- 21 个命名 PK/UK/FK/CHECK 存在、类型正确且已验证;
- 没有额外用户约束;
- 相关对象 owner 是
pg36_owner。
若人为增加 product.drift_probe,setup 在事务中返回:
psql 状态为 3,不会用“relation already exists”掩盖漂移。不要在有价值环境为了测试随意改表;这项负向验证只在可销毁 L1 做。
seed 与状态摘要
seed.sql用固定 ID、金额与 UTC 时间生成:
- 2 customer;
- 3 product;
- 2 order;
- 3 line;
- 2 payment。
verify.sql检查行数、孤儿、order 1001 subtotal/captured amount、app/ro 权限和 private schema 边界。基线输出:
连续执行两次 all,摘要相同。setup 的 NOTICE 属于“对象已存在并经过后续验证”,不是静默成功。
规则审查必须有正反两面
review.sql在一个事务内先尝试三类非法写入:
| 反例 | 预期 SQLSTATE 类别 |
|---|---|
重复 SKU-COFFEE |
unique_violation |
| line 引用不存在 product | foreign_key_violation |
| quantity = 0 | check_violation |
脚本只捕获预期异常,若错误类型不同或写入意外成功,整个 review 失败。
随后插入三项当前 DDL允许、业务尚未批准的状态:
事务末 ROLLBACK,再次 verify 仍得到原校验和。这组输出是 v0 的边界证据:约束有效,但模型尚未完整。
分步运行:
每次给 evidence 新目录,保留失败现场。
3.6.3 产出金额、时间、状态、标识四项未决清单
未决登记不是随手记下的待办列表,而是 ch04 的输入合同:
| 决策域 | 已知事实 | 仍需决定 | 关闭证据 |
|---|---|---|---|
| 金额 | line 保存购买价;payment 保存尝试金额 | 表示、币种、scale、rounding、refund | 非法精度/币种有明确失败 |
| 时间 | 下单与支付发生时间是瞬间 | 业务 zone、precision、clock、范围 | DST/客户端 zone 往返样例 |
| 状态 | order/payment 是不同状态域 | 允许值、转换、终态、实现 | 非法值与非法转换分别失败 |
| 标识 | internal/business/idempotency/provider/trace 含义不同 | 类型、生成方、公开性、顺序 | 并发生成与重复用例 |
决策不是选一个类型名
“金额用 numeric”仍缺少:
- 是否每行携带 currency;
- 同币种 scale;
- 税费、折扣与汇率何时舍入;
- 负数表示退款还是另建事实;
- API/JSON 如何序列化;
- index 和聚合代价。
“时间用 timestamptz”仍缺少:
- 字段表示发生瞬间还是业务日;
- 哪个时钟产生;
- 允许多远未来/过去;
- 展示用哪个 zone;
- 精度与外部系统对齐。
“状态用 enum”也没有定义转换;“ID 用 UUID”也没有定义版本、生成位置和暴露范围。ch04 必须把语义、DDL、错误与验证一起交付。
关闭条件
每项 decision 只有同时具备以下内容才从 open 变成 accepted:
- 业务语义与反例;
- PostgreSQL 表达;
- 约束/生成与并发行为;
- 旧数据迁移;
- API 与错误契约;
- 验证查询;
- 回退或前滚路径;
- 版本适用范围。
只在会议中口头说“应该两位小数”不算关闭。
3.6.4 生成逻辑关系图并链接 ch04 的可靠版本
图必须能与真实目录互证。列出所有 FK:
应得到四条边:
Mermaid 中 SALES_ORDER ||--|{ SALES_ORDER_ITEM 表达已接受订单应至少一行,但当前 FK 目录只能保证每个 line 有 order,不能反向保证 order 有 line。图表达目标模型,目录查询表达现有强制能力;两者差异必须进入未决登记,而不是让图冒充约束。
v0 到可靠版本的交接
ch04 将建立 v1,至少产生:
v1 关系图需要标出类型/状态决策变化,并保留 v0 作为迁移起点。不能直接改写 ch03 文件让读者失去演进过程。
reset 边界
默认 all 不清理。如果必须回到 ch02:
SQL 内部还要求 confirm_reset=RESET_CH03_MODEL。reset 只删除:
- 五个 ch03 表;
shop_api.order_summary;- 空的
shop_api与shop_privateschema。
它不删除 pg36_shop、shop、角色或 ch02 fixture。schema 使用默认 RESTRICT 删除;若出现未知额外对象,事务失败并整体回滚,避免把他人对象级联带走。
本章最终验收
- 业务事实清单有 owner、命令和不变量;
- 五表职责没有重叠的当前权威事实;
- 每类标识的作用域与用途明确;
- 四条 FK 与关系图互证;
- owner/runtime/schema 权限符合预期;
- setup 重跑收敛,额外列会返回状态
3; - seed 摘要与 checksum 匹配;
- 三类非法写入触发准确约束;
- 三项开放规则被实验性证明且完全回滚;
- 四项 decision register 已链接 ch04 验收;
- 团队明确 v0 不是生产 DDL。
满足这些条件后进入 ch04《量体裁衣:数据类型、约束与可靠数据表达》。
参考资料
- PostgreSQL 18:数据定义
- PostgreSQL 18:约束
- PostgreSQL 18:
pg_constraint - PostgreSQL 18:
pg_get_constraintdef - Pigsty v4.5:PostgreSQL 服务
上一节:规范化与有意识的冗余 · 返回本章目录 · 下一章:量体裁衣:数据类型、约束与可靠数据表达 · 查看全书目录 · 查看索引中心