实体关系图:数据库设计实用指南
学习如何为数据库建模创建 ER 图。涵盖实体、属性、关系、基数、陈氏符号与鸦脚符号对比,以及真实模式示例。
没有图表就直接编写的数据库模式是一种猜测。你可能第一次就把表格设计对了,但你几乎肯定会漏掉一个关系、将外键放错位置,或者构建出一个抵制你的应用程序实际所需查询的结构。实体关系图——ER 图——是在写第一行 SQL 之前设计和传达数据库结构的标准工具。
ER 图不仅仅在初始设计时有用,在系统整个生命周期中都有价值:为需要理解现有模式的工程师提供入职指导、规划迁移、识别规范化问题,以及为非技术利益相关者记录数据模型。
什么是 ER 图?
ER 图(实体关系图)是数据库中实体及其关系的视觉表示。它显示系统存储哪些数据、每个实体有哪些属性,以及实体之间如何相互关联。
该概念由 Peter Chen 于 1976 年提出,并被数据库工具和从业者不断扩展和改进。现代实践中主导的两种符号风格:陈氏符号(概念设计)和鸦脚符号(以实现为重点,被大多数现代工具使用)。
ER 图在不同的抽象层次运作:
| 层次 | 目的 | 受众 |
|---|---|---|
| 概念层 | 高级别实体和关系 | 业务利益相关者、产品经理 |
| 逻辑层 | 添加属性、键、基数 | 数据库架构师 |
| 物理层 | 数据类型、索引、外键、SQL 就绪 | 开发人员、数据库管理员 |
关键概念
实体
实体是数据库存储信息的事物。它映射到实现数据库中的一个表。
- 强实体:独立存在。有自己的主键。示例:
Customer、Product、Order - 弱实体:依赖另一个实体而存在。示例:
OrderLineItem依赖于Order
实体命名规范:
- 使用单数名词(
Order,而非Orders) - PascalCase 在实体名称中很常见
- 命名行所代表的内容,而非集合
属性
属性是实体的属性。它们映射到实现表中的列。
| 属性类型 | 描述 | 示例 |
|---|---|---|
| 简单 | 单一原子值 | firstName、price |
| 复合 | 由子属性组成 | address = 街道 + 城市 + 邮编 |
| 派生 | 从其他属性计算 | age 从 birthDate 派生 |
| 多值 | 可以有多个值 | phoneNumbers(一个人可以有多个) |
| 键属性 | 唯一标识实体实例 | customerId、email |
在物理 ER 图中,在属性名称旁边指定数据类型:
Customer
├── customerId INT PRIMARY KEY
├── email VARCHAR(255) UNIQUE NOT NULL
├── firstName VARCHAR(100) NOT NULL
├── lastName VARCHAR(100) NOT NULL
├── createdAt TIMESTAMP DEFAULT NOW()
└── status ENUM('active', 'inactive', 'banned')
关系
关系定义两个实体之间的关联方式。它映射到实现模式中的外键约束(或多对多关系的连接表)。
关系具有:
- 名称:描述关联(
places、contains、belongs to) - 基数:每个实体可以参与多少个实例
- 参与度:参与是强制的(全部)还是可选的(部分)
基数
基数是 ER 建模中最重要的概念。它定义了实体实例之间的数量关系。
| 基数 | 含义 | 示例 |
|---|---|---|
| 一对一 | 一个 A 实例恰好关联一个 B 实例 | User 有一个 Profile |
| 一对多 | 一个 A 实例关联多个 B 实例 | Customer 下了多个 Orders |
| 多对一 | 多个 A 实例关联一个 B 实例 | 多个 Orders 属于一个 Customer |
| 多对多 | 多个 A 实例关联多个 B 实例 | Students 注册多个 Courses |
多对多关系不能直接在关系数据库中实现。它们需要一个连接表(也称为关联表或联结实体):
Student ──< Enrollment >── Course
Enrollment 表保存指向 Student 和 Course 的外键,以及注册本身的任何属性(如 enrollmentDate 或 grade)。
符号风格
陈氏符号
陈氏符号是原始的学术符号。它使用几何形状:
- 矩形:实体
- 椭圆:属性
- 菱形:关系
- 直线:连接
- 双椭圆:多值属性
- 虚线椭圆:派生属性
- 双矩形:弱实体
- 双菱形:标识关系(用于弱实体)
陈氏符号 ASCII 示例:
(customerId) (email) (firstName)
| | |
└─────────────┴───────────┘
|
┌─────────┐
│Customer │
└────┬────┘
│
< places >
│
┌─────────┐
│ Order │
└─────────┘
陈氏符号对概念建模很有用,因为它明确显示关系和属性。对于大型模式来说会显得混乱。
鸦脚符号
鸦脚符号是现代数据库工具(MySQL Workbench、dbdiagram.io、ERDPlus)中的主流风格。它使用关系线两端的符号直接编码基数。
鸦脚符号:
线端符号 含义
───────────── ──────────────────────────────
──| 恰好一个(强制)
──o 零或一个(可选)
──< 多个(一或多)
──o< 零或多个(可选多)
──|< 一或多个(强制多)
──|| 恰好一个(两端)
鸦脚符号示例——电商关系:
Customer ||──o< Order ||──|< OrderLineItem >|──|| Product
解读:一个 Customer 有零或多个 Orders;每个 Order 有一或多个 OrderLineItems;每个 OrderLineItem 恰好引用一个 Product。
带鸦脚符号的完整实体:
┌─────────────┐ ┌─────────────┐ ┌──────────────────┐
│ Customer │ │ Order │ │ OrderLineItem │
│─────────────│ │─────────────│ │──────────────────│
│ PK id │||──o<─│ PK id │||──|<─│ PK id │
│ email │ │ FK customerId│ │ FK orderId │
│ name │ │ status │ │ FK productId │
│ createdAt│ │ total │ │ quantity │
└─────────────┘ │ createdAt│ │ unitPrice │
└─────────────┘ └──────────────────┘
鸦脚符号适合逻辑和物理 ER 图,因为它比陈氏符号更紧凑,并直接反映 SQL 模式结构。
数据库设计过程中的 ER 图
ER 图在多个阶段都融入数据库设计:
1. 需求收集 与利益相关者合作,确定系统需要跟踪的事物。需求中的每个名词都是潜在实体。关于名词的每个事实都是潜在属性。
2. 概念设计 绘制实体和关系,不用担心实现细节。专注于数据的含义,而非存储方式。
3. 逻辑设计 添加属性、定义主键、将多对多关系解析为连接表、指定基数。
4. 物理设计 添加数据类型、索引、约束和 NULL/NOT NULL 规范。此阶段的 ER 图应该直接映射到 SQL DDL。
5. 审查和精化 针对实际查询验证模式:你的应用程序能否高效检索所需的数据?模式是否支持所有必需的操作?
分步创建指南
第一步:识别实体
通读需求或用户故事,高亮代表系统跟踪的不同概念的名词。从显而易见的开始。
对于电商系统:
- Customer、Product、Order、Category、Review、Address、Payment
经验法则:
- 如果有多个实例,可能是实体(而非属性)
- 如果有自己的属性,可能是实体
- 如果其他实体独立引用它,那就是实体
第二步:为每个实体定义属性
对于每个实体,列出其属性。识别:
- 主键(唯一标识符,系统生成或自然键)
- 必需属性(SQL 中的 NOT NULL)
- 可选属性
- 可能是复合的属性(address)或多值的属性(tags)
避免存储派生数据,除非性能要求如此。age 可以从 birthDate 派生;两者都存储会产生一致性问题。
第三步:映射关系
对于相互交互的每对实体,定义关系:
- 命名关系(从一个实体的角度使用动词短语)
- 确定基数(一对一、一对多、多对多)
- 确定参与度(每一侧是强制的还是可选的?)
用句子写出来并验证两个方向都说得通:
- "一个 Customer 下了零或多个 Orders"
- "一个 Order 恰好由一个 Customer 下单"
第四步:解析多对多关系
每个多对多关系都变成一个连接表。识别关系本身的属性(如有)——这些成为连接表中的列。
Student ──< Enrollment >── Course
Enrollment 属性:
- enrollmentDate
- grade
- status (active, withdrawn, completed)
第五步:分配数据类型和约束
对于每个属性,指定:
- 数据类型(
INT、VARCHAR(n)、DECIMAL(10,2)、TIMESTAMP、BOOLEAN) - NULL / NOT NULL
- 默认值
- 唯一约束
- 检查约束(例如
price > 0)
第六步:针对查询验证
写出应用程序将运行的关键查询并通过模式追踪它们。如果一个查询需要多个本可以通过更好的设计避免的连接,那就重新审视模式。
检查:
- 你能检索客户的完整订单历史记录吗?
- 你能找到某个类别中的所有产品吗?
- 你能从行项目计算订单总额吗?
- 你能按日期范围生成收入报告吗?
规范化注意事项
规范化是对模式进行结构化以减少数据冗余并提高完整性的过程。最常应用的三种范式:
第一范式(1NF): 每列包含原子值。没有重复组。每行可唯一标识。
第二范式(2NF): 满足 1NF。每个非键属性完全依赖于整个主键(对复合键相关)。
第三范式(3NF): 满足 2NF。没有传递依赖——非键属性只依赖于主键,而非其他非键属性。
3NF 违规示例:在 Order 表中存储 customerCity。城市依赖于客户,而非订单。如果客户搬家,你必须更新每个订单记录。
何时去规范化: 规范化优化写入一致性。读取密集的分析工作负载有时受益于去规范化的结构(物化视图、报告表),这些结构可避免昂贵的连接。这应该是有意为之的决定,而非意外。
常见错误
将自然键用作主键。 电子邮件地址会变更。不应存储社会安全号码。名字不是唯一的。使用代理键(自增整数或 UUID)作为主键,并在自然标识符上添加唯一约束。
在单列中存储列表。 包含逗号分隔值的 tags 列违反 1NF。使用连接表(ProductTag)代替。
缺少连接表属性。 解析多对多关系时,团队通常只创建一个只有两个外键的裸连接表。考虑关系本身的属性——注册日期、订单数量、权限级别。
混淆实体和属性。 在简单系统中,Address 可能是 Customer 上的属性。在多个客户共享地址,或地址有自己生命周期的系统中,Address 应该是单独的实体。
不考虑软删除。 许多系统出于审计或监管原因需要在"删除"后保留记录。提前决定是使用 deletedAt 时间戳还是 isActive 标志,并一致应用。
对小数据集过度规范化。 将每个重复字符串提取到查找表中,对于永远不会超过数千行的数据集来说,增加了复杂性而没有实际好处。运用判断,而非机械地遵循规则。
忽视索引设计。 ER 图显示结构,而非性能。确定模式后,识别将在 WHERE 子句和 JOIN 条件中使用的列,并为其添加索引。
真实案例:电商模式
以下模式涵盖了典型电商系统的核心实体。
┌──────────────┐ ┌──────────────┐ ┌───────────────────┐
│ Customer │ │ Order │ │ OrderLineItem │
│──────────────│ │──────────────│ │───────────────────│
│ PK id │ │ PK id │ │ PK id │
│ email │||─o<│ FK customerId│||─|<│ FK orderId │
│ firstName │ │ FK addressId │ │ FK productId │
│ lastName │ │ status │ │ quantity │
│ createdAt │ │ subtotal │ │ unitPrice │
│ status │ │ tax │ └────────┬──────────┘
└──────────────┘ │ shipping │ │
│ total │ 恰好一个
│ createdAt │ │
└──────────────┘ ┌────────┴──────────┐
│ Product │
┌──────────────┐ │───────────────────│
│ Address │ │ PK id │
│──────────────│ │ FK categoryId │
│ PK id │ │ name │
│ FK customerId│ │ description │
│ line1 │ │ sku │
│ line2 │ │ price │
│ city │ │ stockQty │
│ state │ │ isActive │
│ zip │ └────────┬──────────┘
│ country │ │
│ isDefault │ 多个│属于
└──────────────┘ │一个
┌────────┴──────────┐
┌──────────────┐ │ Category │
│ Payment │ │───────────────────│
│──────────────│ │ PK id │
│ PK id │ │ FK parentId │
│ FK orderId │ │ name │
│ method │ │ slug │
│ amount │ └───────────────────┘
│ status │
│ processedAt│
└──────────────┘
此模式中的关键设计决策:
Address是单独的实体(客户可以有多个地址;订单引用购买时使用的地址)OrderLineItem.unitPrice与Product.price分开存储(价格会变化;订单历史必须反映实际收费)Category有自引用parentId用于层次化类别Payment与Order分开,因为一个订单可能涉及多次支付尝试
使用 Flowova 创建 ER 图
手动或在纯文本编辑器中设计模式很慢。Flowova 的 ER 图生成器让你用纯文本描述数据模型,并生成可以精修的结构化图表。从实体列表开始,描述它们之间的关系,即可获得可与团队共享审查的可视化模式。
结论
ER 图是良好数据库设计的基础。它们迫使你明确说明系统存储哪些数据、什么使每条记录唯一,以及实体之间如何相互关联——在这些决策在 SQL 中变得昂贵之前。从包含实体和关系的概念层开始,通过添加属性和解析多对多连接进入逻辑层,然后为物理设计添加数据类型和约束。针对真实查询验证,在重要的地方应用规范化,并记录模式,让下一位工程师无需阅读迁移文件就能理解它。
