
如何用数据库设计工具绘制 ER 图并生成建表语句
ER 图(实体关系图)把业务中的对象、属性以及对象之间的关系表达出来。数据库设计工具可以把这张图转成建表语句,减少手写重复工作,也让表结构更容易审阅和修改。它不会替你判断业务规则是否合理:字段类型、唯一性、删除策略和索引仍需要设计者确认。
本文以 MySQL Workbench 为例,演示从业务需求建模、绘制 EER 图,到生成并检查 MySQL 建表语句的流程。示例针对 MySQL 8.0,包含用户、商品、订单和订单明细四张表。不同 Workbench 版本的菜单名称或窗口布局可能略有差异;生成的 SQL 也应以当前版本实际显示的内容为准。
一、什么情况下适合从 ER 图生成 SQL
ER 图适合用于梳理结构化数据,以及表达表之间相对稳定的关联关系。常见用途包括:
- 新项目建库:在开发前统一实体、字段和关系,避免各自按代码习惯创建表。
- 多人评审:让产品、开发和数据人员在同一张图上讨论主键、可空性、唯一性和关联规则。
- 原型验证:先建立一套可执行的基础结构,再根据真实业务数据和查询负载迭代。
- 维护存量数据库:将已有结构逆向导入模型,辅助理解依赖关系或规划改造。
- 生成迁移脚本:把模型变更转换为 SQL 草稿,经审查后交给迁移流程执行。
以下情况需要谨慎:业务规则频繁变化、需要大量数据库专有功能、已有生产库不能随意改动,或目标数据库与建模工具支持的功能不一致。图适合解释结构,不等于完整的业务规格说明,也不能代替代码评审、迁移测试和备份。
二、示例需求与关系设计
假设我们要保存电商订单:
- 一个用户可以有多个订单;每个订单属于一个用户。
- 一个订单有多条订单明细;每条明细属于一个订单。
- 每条订单明细关联一个商品,一个商品可以出现在多条明细中。
- 订单明细保存下单时的商品单价,不能只依赖商品当前价格。
关系可以概括为:用户 1 对多订单,订单 1 对多明细,商品 1 对多明细。订单与商品之间实际是多对多关系,通过 order_items 这张关联表拆成两个一对多关系。
这个例子还体现了一个重要边界:外键只能约束数据库能够直接验证的结构关系,不能自动表达所有业务流程。例如“订单支付成功后才允许发货”通常由应用逻辑、数据库约束或专门的状态流转机制共同实现,不能仅靠 ER 图的一条连线解决。
三、先明确字段和约束
绘图前先列出实体、字段和约束,比打开工具后边画边猜更可靠。下面的设计采用以下约定:
- 主键使用自增的有符号
BIGINT,适合多数常规业务规模;如果系统对 ID 类型、分布式生成或容量有特定要求,应另行评估。 - 金额使用
DECIMAL(12,2),避免用浮点数保存需要精确到分的金额。 - 名称和邮箱使用
VARCHAR;是否可空、是否唯一按业务需求设置。 - 时间采用
DATETIME,示例默认由数据库写入当前时间。项目若采用 UTC、时区转换或统一由应用写入,应明确一致策略。 - 订单明细的商品数量必须大于零,单价不能为负。示例使用 MySQL 8.0.16 及以上版本支持并执行的
CHECK约束。
字段定义不能只看名字。比如用户邮箱是否允许为空、商品删除后历史订单是否保留、订单明细中的单价是否快照,都属于业务选择。下面的示例将邮箱设为非空且唯一,将商品和用户的外键删除策略设为限制删除,并将订单删除时的明细设为级联删除。
四、在 MySQL Workbench 中绘制 EER 图
1. 创建模型和关系图
打开 MySQL Workbench,选择创建新模型,在模型中添加 EER Diagram。可以使用工具栏中的表格工具逐个创建表,也可以先在模型编辑器中创建表,再从画布工具栏选择表放置到图上。新版本界面可能略有不同,关键是编辑模型中的表,而不是直接在查询窗口里执行 SQL。
建议先创建四张表,再补关系。表名使用小写蛇形命名法,例如 order_items。避免使用容易与数据库关键字冲突的名称;确实需要时,确认工具生成的引用符号符合目标数据库语法。
2. 定义列、主键和可空性
双击表格打开编辑面板,在 Columns 区域逐列添加字段。为每列填写名称和数据类型,并根据需求设置 PK(主键)、NN(非空)、UQ(唯一)和自增属性。注意:不同版本的属性名称或位置可能有变化,保存后应检查表格定义,而不是只看画布上的显示。
例如,users.id 是主键并设为自增;users.email 设置非空和唯一;orders.user_id 是非空字段,但它成为外键还需要建立关系或在外键设置中明确关联目标列。
主键、唯一约束和索引不是同一件事。主键用于标识一行,且不能为 NULL;唯一约束保证指定列或列组合不重复。普通索引主要用于查询性能,不代表数据唯一。是否需要索引应结合查询条件、排序、连接和写入成本决定。
3. 建立关系和设置删除规则
使用工具栏中的关系工具连接表。对于本例,分别建立 users 到 orders、orders 到 order_items、products 到 order_items 的一对多关系。通常先选择父表,再选择子表;最终仍应检查外键列是否落在子表,以及方向和基数是否符合预期。
如果 Workbench 在建立关系时自动创建外键列,检查列名、类型和可空性,避免又手动创建一列造成重复。外键两端的数据类型需要兼容;对于整数列,还要留意有符号与无符号属性是否一致。外键所引用的列通常必须是主键或有唯一约束的键。
在外键属性中设置删除和更新规则。示例采用:
- 删除用户时,若仍有订单则拒绝删除,防止历史订单失去归属。
- 删除订单时,级联删除其订单明细。
- 商品有关联订单明细时拒绝删除,以保留历史关系;更常见的产品做法也可能是设置停用状态,而不是物理删除。
ON DELETE CASCADE 会由数据库自动删除子表相关记录,影响范围可能大于操作者预期。不要因为图上关系是“一对多”就默认级联删除。对订单、账务、审计等数据,通常需要先考虑保留要求、软删除和归档方案。
4. 补充唯一约束、检查约束和索引
在表编辑器的 Indexes 区域配置索引及唯一索引;复合唯一约束要按列组合建立,而不是给每列分别加唯一。例如,若一个系统要求同一用户的外部订单号不能重复,应建在 (user_id, external_order_no) 上,而不是单独要求全表的外部订单号唯一。
Workbench 对各种数据库对象的建模和输出能力会随版本及目标数据库而异。若界面没有提供某种约束的编辑方式,不要假设它会自动生成;可以检查 Forward Engineer 预览的 SQL,必要时将该对象通过经过审核的迁移脚本补上。
5. 排版和命名
把父表放在上方或左侧、子表放在下方或右侧,尽量避免关系线交叉。图上的位置不影响数据库执行,但清晰的布局有助于评审。命名应统一表达实体含义,字段名尽量体现单位或语义,例如 unit_price 比 price 更明确。团队还应约定字符集、排序规则、时间处理和主键策略。
五、生成建表语句
模型完成后,使用 MySQL Workbench 的 Database > Forward Engineer 流程生成目标数据库结构。向导通常会让你选择连接、对象和导出选项,并显示即将执行或输出的 SQL。若只想审阅脚本,可在向导中选择查看、保存或导出 SQL 的选项,不要在尚未确认目标环境时直接执行。
生成前后都应确认目标 MySQL 版本和模型使用的语法一致。尤其检查以下内容:
- 是否包含预期的数据库、表、列、主键和外键。
- 字符串字段的字符集和排序规则是否符合项目约定。
- 外键动作、唯一约束、检查约束和索引是否生成。
- 表和列的命名是否需要引用,引用符是否为 MySQL 使用的反引号。
- 是否存在工具自动添加但项目不需要的默认值或索引。
Forward Engineer 是把模型转换为目标数据库操作的过程;它不等同于安全、无风险的发布流程。若目标数据库已有同名对象,向导可能报错、要求调整或根据选项执行改动。生产环境中应在代码审查、备份、测试环境验证和迁移计划完成后,再按团队发布流程执行。
六、可运行的 MySQL 建表示例
以下 SQL 可在新建的 MySQL 8.0 数据库中执行。它表达了上面的核心结构。为避免不同工具生成的约束名称差异,示例显式命名主键和外键。若在已有数据库中运行,应先确认同名表不存在;CREATE TABLE 默认不会替你合并或改造已有表。
CREATE DATABASE IF NOT EXISTS shop_demo
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
USE shop_demo;
CREATE TABLE users (
id BIGINT NOT NULL AUTO_INCREMENT,
email VARCHAR(254) NOT NULL,
display_name VARCHAR(100) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_users_email (email)
) ENGINE=InnoDB;
CREATE TABLE products (
id BIGINT NOT NULL AUTO_INCREMENT,
sku VARCHAR(64) NOT NULL,
name VARCHAR(200) NOT NULL,
current_price DECIMAL(12, 2) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_products_sku (sku),
CONSTRAINT chk_products_current_price
CHECK (current_price >= 0)
) ENGINE=InnoDB;
CREATE TABLE orders (
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
order_no VARCHAR(40) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_orders_order_no (order_no),
KEY ix_orders_user_created (user_id, created_at),
CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES users (id)
ON UPDATE RESTRICT
ON DELETE RESTRICT
) ENGINE=InnoDB;
CREATE TABLE order_items (
id BIGINT NOT NULL AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(12, 2) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uq_order_items_order_product (order_id, product_id),
KEY ix_order_items_product (product_id),
CONSTRAINT chk_order_items_quantity
CHECK (quantity > 0),
CONSTRAINT chk_order_items_unit_price
CHECK (unit_price >= 0),
CONSTRAINT fk_order_items_order
FOREIGN KEY (order_id) REFERENCES orders (id)
ON UPDATE RESTRICT
ON DELETE CASCADE,
CONSTRAINT fk_order_items_product
FOREIGN KEY (product_id) REFERENCES products (id)
ON UPDATE RESTRICT
ON DELETE RESTRICT
) ENGINE=InnoDB;
INSERT INTO users (email, display_name)
VALUES ('[email protected]', '示例用户');
INSERT INTO products (sku, name, current_price)
VALUES ('BOOK-001', '数据库设计入门', 59.90);
INSERT INTO orders (user_id, order_no)
VALUES (1, 'ORD-2025-0001');
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (1, 1, 2, 59.90);
SELECT
o.order_no,
u.email,
p.name,
oi.quantity,
oi.unit_price
FROM orders AS o
JOIN users AS u ON u.id = o.user_id
JOIN order_items AS oi ON oi.order_id = o.id
JOIN products AS p ON p.id = oi.product_id
WHERE o.order_no = 'ORD-2025-0001';
其中 uq_order_items_order_product 限制同一订单中的同一商品只出现一行。如果业务允许同一商品以不同规格、批次或折扣拆成多行,应调整唯一键,例如把规格标识也纳入唯一组合,或者移除该唯一约束并采用合适的明细标识。
ix_orders_user_created 面向按用户查询订单并按创建时间筛选或排序的常见场景。索引列顺序会影响索引可用于哪些查询条件;这不是放上去就一定提升所有查询的通用配置。实际项目应根据查询模式和执行计划决定。
七、用客户端验证结构
执行建表 SQL 后,可通过以下查询检查表、列和约束。不同 MySQL 版本对系统目录视图的具体字段可能略有差异;以下查询使用常见的 information_schema 视图。
USE shop_demo;
SHOW CREATE TABLE order_items;
SELECT
TABLE_NAME,
COLUMN_NAME,
COLUMN_TYPE,
IS_NULLABLE,
COLUMN_KEY,
COLUMN_DEFAULT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
ORDER BY TABLE_NAME, ORDINAL_POSITION;
SELECT
TABLE_NAME,
CONSTRAINT_NAME,
CONSTRAINT_TYPE
FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = DATABASE()
ORDER BY TABLE_NAME, CONSTRAINT_NAME;
检查结果时,不要只确认“表创建成功”。还要验证可空性、默认值、字符集、索引顺序和外键动作。可以尝试插入一条无效明细,确认数据库是否拒绝数量为零;也可以尝试插入不存在的 user_id,确认外键确实生效。
八、关键边界:图能表达什么,不能表达什么
关系图不等于业务规则全集
ER 图擅长描述实体、属性、基数和引用关系。它通常不能完整表达跨多张表的复杂业务规则、审批流程、权限逻辑、状态转换或并发下的操作语义。约束能在数据库层表达的,尽量用主键、唯一键、外键、非空和检查约束保障;复杂规则则需要应用逻辑、事务设计或其他机制,并通过测试验证。
模型与数据库可能不完全同步
开发过程中,团队可能有人直接执行 SQL、有人通过迁移工具改表,也有人只更新 Workbench 模型。时间久了模型就可能和真实数据库分叉。应明确模型是权威来源还是参考文档,并把变更纳入版本控制和代码审查。若模型为权威来源,每次结构变更都应同步更新模型;若迁移脚本为权威来源,则应定期核对模型与实际结构。
生成语句不等于迁移方案
首次建库时,生成完整的 CREATE TABLE 脚本通常比较直接。对已运行的数据库,结构变更涉及旧数据、锁表、回填、回滚和应用兼容性。生成一份新的建表脚本,不会自动替你制定这些步骤。生产变更应采用团队认可的数据库迁移机制,先评估数据影响,再在近似生产的数据环境中演练。
数据库方言和版本会影响结果
MySQL、PostgreSQL、SQL Server 等数据库在类型、引用符、索引、序列、自增列、检查约束和函数语法上并不相同。同一张抽象 ER 图并不保证可无损生成所有数据库的 SQL。切换目标数据库时,要重新审核数据类型、默认值、约束和迁移策略,而不是只替换连接地址。
九、常见错误与排查方式
1. 关系画对了,但外键列类型不匹配
常见现象是生成或执行时提示外键无法建立。检查两侧列的数据类型、长度、符号属性、字符集(字符串外键场景)以及被引用列是否有合适键。父表和子表的整数类型虽都叫 BIGINT,有符号与无符号差异也可能导致不兼容。
2. 只画关系线,没有确认外键是否生成
画布上的连线是模型表示,不足以证明最终 SQL 包含外键。检查表的 Foreign Keys 面板和 Forward Engineer 输出;执行后用 SHOW CREATE TABLE 核对。也要确认模型中的外键没有被导出选项排除。
3. 把所有字段都设为非空,或全部允许为空
过度严格会导致正常流程无法写入;过度宽松则会积累语义不明的空值。逐字段回答两个问题:业务上是否允许未知或暂缺?创建记录时是否一定能提供?注意 SQL 中 NULL 表示缺失或未知,不等同于空字符串、零或“无”。
4. 把级联删除当作默认值
级联能减少清理子记录的代码,但也会让一次父记录删除扩散到大量数据。先判断数据是否应该保留、是否有审计要求、是否需要恢复能力,再配置删除规则。对用户和订单等有历史价值的数据,常见方案是限制物理删除或使用状态字段。
5. 金额使用浮点类型
FLOAT 和 DOUBLE 采用近似表示,不适合要求精确十进制金额计算的场景。示例用 DECIMAL,但精度与小数位仍要根据币种、计价单位和业务规则选择。涉及多币种时,通常还要保存币种代码,并明确定价、舍入和汇率规则。
6. 忽略索引和唯一性设计
没有索引可能让关联查询变慢;索引过多则增加写入和存储成本。唯一性应由数据库约束保障,而不仅是应用先查询再插入,因为并发请求可能同时通过预检查。应根据真实访问模式选索引,并在数据量和查询计划上验证效果。
7. 认为 CHECK 约束在所有 MySQL 版本中行为一致
较早版本的 MySQL 对检查约束可能只解析而不执行;MySQL 8.0.16 起才按文档执行 CHECK 约束。若要兼容较早版本,不能假设示例中的数量和金额检查一定会生效,应确认目标版本并采用可行替代方案。工具的模型里显示了约束,也不代表目标服务器会执行它。
8. 在生产环境直接运行向导生成的脚本
生成 SQL 后先审阅,再在隔离的开发或测试环境执行。检查执行影响、是否存在破坏性语句、是否会改变已有数据,以及失败后的恢复办法。数据库工具不会知道当前库是否有业务高峰、备份是否可恢复或下游服务是否依赖旧结构。
十、安全与操作注意事项
ER 图和建表脚本本身不具备安全认证能力,也不会自动保护数据库。安全性来自数据库账户权限、网络访问控制、凭据管理、备份与审计等整体措施。创建模型时不要把真实密码、访问令牌或其他凭据写进备注、默认值或示例数据。
生成或执行脚本时,优先使用权限最小化的账户:仅授予当前任务需要的建库、建表或变更权限,避免在日常开发和自动化流水线中使用高权限管理员账户。生产环境执行需要遵循审批、备份和变更管理流程。限制权限不能替代 SQL 审查,备份也不能替代恢复演练。
如果工具连接真实数据库,应确认连接的主机、端口、库名和账户,避免把测试操作执行到生产库。不要把连接配置或含敏感数据的模型文件提交到公开仓库。团队共享模型时,可在提交前检查是否包含内部主机名、数据库名称或不必要的样例数据。
十一、推荐的完整工作流
- 整理需求:列实体、字段、关系和业务约束,标记不确定事项,不急于直接画图。
- 建立模型:在 Workbench 中创建表和字段,明确主键、可空性、唯一性及类型。
- 连接关系:建立外键,逐一检查基数、外键位置和删除更新策略。
- 评审结构:检查命名、字段含义、历史数据保留、索引和多对多关系拆分方式。
- 生成 SQL:通过 Forward Engineer 预览或导出脚本,核对目标服务器版本和实际语句。
- 隔离验证:在开发或测试数据库执行,检查约束、插入失败行为和常用查询。
- 版本管理:保存模型和经过审查的迁移脚本,明确哪一个是结构变更的权威记录。
- 发布与核验:按迁移流程实施,确认结果并记录变更;必要时准备回滚或前向修复方案。
结语
数据库设计工具能把表结构和关系直观呈现出来,也能减少重复编写建表语句的工作。有效的流程不是“画完图就上线”,而是先梳理业务约束,再建立模型、生成 SQL、检查数据库方言与版本,并在隔离环境验证。尤其要认真对待外键动作、唯一约束、索引和生产变更:这些选择会影响数据完整性、查询性能和历史记录是否可恢复。把模型、脚本和迁移流程纳入一致的团队规范,ER 图才会成为可维护的设计资产,而不只是一次性的示意图。








