ER 图示例:一步步设计一个在线商店的数据库

从需求出发构建的 ER 图示例:实体、键、乌鸦脚基数、通过订单项实现的多对多,以及值得写下来的设计决策。

· 创始人兼开发者

发布于

ER 图示例:一步步设计一个在线商店的数据库

ER 图是数据库设计在变成迁移脚本之前接受检查的地方。表的列表告诉你有哪些东西。图则展示表之间如何相互依赖:哪一行需要哪一行,什么可以缺失,多对多关系藏在哪里。

本文从朴素的需求出发,为一个小型在线商店设计数据库,直到画出完整的实体关系图,并在用到时解释表示法。商店只是例子,这个方法适用于任何 schema。

什么时候需要 ER 图

如果 schema 只有几张表、只有一个团队在维护,表的列表就够了。当团队之外的人需要理解数据、在编写迁移之前评审设计,或者 schema 已经大到没人能记在脑子里时,就该画图了。

常见的表示法有两种。Chen 表示法来自 Peter Chen 1976 年提出实体关系模型的论文,用矩形表示实体,用菱形表示关系,适合概念建模。乌鸦脚表示法(crow's foot)把表画成方框,把基数标在连线的两端。它通常被归功于 1976 年的 Gordon Everest,也是大多数数据库设计工具采用的表示法。本文使用乌鸦脚表示法。

第 1 步:用简单的句子写出需求

先把规则写成句子。每一句都会变成一张表、一列或一条连线。

  1. 客户用邮箱地址注册,并可以保存多个地址。
  2. 客户下订单。每个订单寄送到该客户的某一个地址。
  3. 一个订单包含一个或多个商品,每个商品都有数量和实付价格。
  4. 每个商品属于一个分类。
  5. 一个订单可以有多次支付尝试,大多数订单有一次成功的支付。

第 2 步:实体和键

名词变成实体:客户、地址、订单、订单项、商品、分类和支付。每个实体都有一个主键。大多数表使用代理键(标识列),业务上已有唯一值的地方再使用自然唯一键,例如客户的邮箱、商品的 SKU。

订单项是个例外。订单项没有自己的标识,它是一个订单和一个商品之间的关联。它的主键是 (order_id, product_id) 这一对列。

第 3 步:关系和基数

每个外键都是一条连线。两端的符号说明这一侧可以有多少行:

连线端点 读作 商店中的例子
两条竖线 恰好一个 每个订单恰好由一个客户下单
圆圈加竖线 零个或一个 如果加上礼品留言,一个订单有零条或一条礼品留言
竖线加乌鸦脚 一个或多个 一个订单有一个或多个订单项
圆圈加乌鸦脚 零个或多个 一个客户有零个或多个订单

读关系时,从一张表出发,沿着连线读到另一端的符号:“一个客户下零个或多个订单”;反过来读是“每个订单恰好由一个客户下单”。

NOT NULL 的外键在被引用一侧表示“恰好一个”;允许 NULL 的外键表示“零个或一个”。另一侧的“一个或多个”是业务规则,外键并不会强制它,所以要把它写在图上,让读者看得到。

第 4 步:多对多

订单和商品是多对多关系:一个订单包含多个商品,一个商品出现在多个订单里。关系型数据库用一张中间表来建模。这里这张表的存在理由比单纯的连接更充分:order_items 记录下单时的数量和单价,这些事实属于这一对组合,而不单独属于任何一方。

第 5 步:值得写下来的决策

有些选择在列清单里看不出来,如果没人知道,就会引发 bug:

  • 价格快照。 order_items.unit_price 复制下单时的商品价格。之后修改价格不应改变旧订单。
  • 地址不就地修改。 一旦有订单指向某个地址,修改那一行就等于改写过去订单的寄送地址。修改会新建一行地址。
  • 支付按尝试记录。 每次尝试一行,带状态,这样失败和重试的支付都能看到。
  • 金额用 numeric。 PostgreSQL 文档推荐用 numeric 存储金额,因为它是精确的。

第 6 步:DDL

PostgreSQL 中的两张核心表:

CREATE TABLE orders (
  id                  bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  customer_id         bigint NOT NULL REFERENCES customers (id),
  shipping_address_id bigint NOT NULL REFERENCES addresses (id),
  status              text NOT NULL DEFAULT 'pending'
                      CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')),
  currency            char(3) NOT NULL,
  total               numeric(12,2) NOT NULL CHECK (total >= 0),
  placed_at           timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE order_items (
  order_id   bigint NOT NULL REFERENCES orders (id) ON DELETE CASCADE,
  product_id bigint NOT NULL REFERENCES products (id),
  quantity   integer NOT NULL CHECK (quantity > 0),
  unit_price numeric(12,2) NOT NULL,  -- copied from products.price at checkout
  PRIMARY KEY (order_id, product_id)
);

CREATE INDEX ON orders (customer_id);
CREATE INDEX ON order_items (product_id);

PostgreSQL 不会自动为外键列创建索引,所以这两行 CREATE INDEX 对连接查询以及在被引用表上的删除都很重要。order_items.order_id 由主键覆盖,因为主键索引以这一列开头。ON DELETE CASCADE 表示订单项不能比它所属的订单存在得更久。

图

包含七张表的 ER 图。客户有地址并下订单;每个订单寄送到一个地址,包含一个或多个订单项,并有零个或多个支付。每个订单项指向一个商品,每个商品属于一个分类。一条注释列出了设计决策。

8 个组件 · 7 个连接

在 Archboard 中打开
图表内容
  • customers
  • orders
  • payments
  • addresses
  • order_items
  • products
  • categories
  • belongs to
  • placed by
  • ships to
  • pays for
  • line of
  • for
  • in
本文的在线商店 schema:客户、地址、订单、订单项、支付、商品和分类;每张表都标出了键,每个关系都用乌鸦脚表示基数,设计决策写在相关表的旁边。

这张图的读法如下:

  • 客户(左上)有零个或多个 地址,下零个或多个 订单。
  • 订单(中间)寄送到恰好一个地址,包含一个或多个 订单项,并有零个或多个 支付。
  • 订单项 把每个订单与 商品 关联起来,每个商品恰好属于一个 分类。
  • 注释 把第 5 步的决策写在受影响的表旁边。

如何画出易读的 ER 图

  • 用动词给关系命名。 “下单”“寄送到”“支付”这样的标签让连线读起来像句子。
  • 在属性中标出键。 PK 和 FK 标记让人一眼看出连线属于哪一列。
  • 按领域给表分组。 客户和地址、订单和支付、商品目录。把每组放在一起,连线就能保持短。
  • 去掉噪音。 created_by、updated_at 这样的审计列、每张表上的软删除标记以及技术性的表,只会增加行数,不会增加理解。
  • 画出多对多的中间表。 如果用一条直接连接订单和商品的线把它藏起来,数量和价格快照也会一起被藏起来。
  • 展示 schema 表达不了的业务规则。 每个订单有“一个或多个”订单项、“地址不可修改”,这些都是大家必须知道的规则。

让图与 schema 保持同步

手绘的 ER 图在下一次迁移之后就过时了。不如从 schema 生成第一版草稿。免费的 SQL 转 ER 图 工具可以读取 CREATE TABLE 语句或整个 pg_dump,为每张表画一张带 PK、FK、UK 标记的卡片,为每个外键画一条连线;另外还有适用于 PostgreSQL、Prisma 和 DBML 的工具。生成的乌鸦脚端点跟随列定义:NOT NULL 的外键是必选的,允许 NULL 的外键是可选的。像“一个或多个”订单项这类 SQL 里没有的规则,需要手动补上。

在 Archboard 中构建

  1. 使用 Google 或 GitHub 登录,并新建一张图。
  2. 如果从 schema 开始,把 CREATE TABLE 语句粘贴到“SQL 转 ER 图”工具中,然后选择 在 Archboard 中打开。如果手动绘制,为每张表添加一个实体关系节点包中的 实体,每行写一个属性,并加上 PK 和 FK 标记。
  3. 把每个外键连接到它引用的表,并把连线端点设为乌鸦脚标记:被引用的表一侧为恰好一个,外键一侧为零个或多个,业务规则要求的地方为一个或多个。
  4. 用动词给每条连线加标签,例如“下单”或“寄送到”。
  5. 添加一个基础节点包中的 Markdown 块,写下设计决策。

这个示例共用 8 个图表节点,在 Free 套餐的限制之内。想了解 schema 如何融入系统的其他部分,请参阅系统设计图示例。

常见问题

ER 图和数据库 schema 图有什么区别? 实际上二者有重叠。ER 图可以是概念层面的,在表存在之前就展示实体和关系;schema 图展示实际的表、列和键。本文的乌鸦脚图两者兼备。

每张表都应该有代理键吗? 大多数表都会受益,因为自然键会变。不过像 order_items 这样的关联表,通常改用外键组合作为主键。

如何表示多对多关系? 用中间表加两条一对多的连线。中间表应按它代表的事物命名,例如订单项,而不是用它连接的两张表的名字。

每一列都要画吗? 画出键和能解释设计的列。除非图的主题就是审计列和技术列,否则把它们省略。

用 Chen 表示法还是乌鸦脚表示法? 和不以表为思考单位的人做概念讨论时用 Chen,凡是要变成数据库的都用乌鸦脚。

免费开始 — 1 个席位,无需信用卡。

相关文章

← 返回博客

一分钟打开画布

使用 Google 或 GitHub 登录。单席位免费,团队加入后再升级。