接手旧项目最头疼的事,就是大家给你丢过来一个几百MB的SQL脚本,说“数据库结构都在里面,自己看”。几百张表、几千个字段,堆成一个文件,谁看了都头大。我一般在拿到这类脚本之后,干的第一件事就是把它重新转回ER图——实体关系图。画图不是为了好看,而是为了搞清楚表跟表之间到底怎么勾连,外键落在哪里,字段类型是不是一致,有多少张表其实已经没人用了。SQL 转 ER 图这个需求,本质上就是给数据库做一次逆向工程,把散落的文本还原成可视化的结构。
这篇文章把我这些年做过的方案都整理了一下,包括用 Workbench、Navicat、DBeaver 这种图形化工具直接逆向生成,也包括用 Python 脚本解析 DDL 再交给 Graphviz 渲染的自动化路径。适合后端开发、DBA、数据分析师,甚至正在做毕业设计需要画数据库模型的同学参考。你手里只要有一份建表脚本,哪怕是残缺的、带各种历史包袱的,也能按下面的步骤把它变成一张能拿去评审、能放进设计文档的 ER 图。
1. 为什么要做 SQL 转 ER 图
1.1 没有文档的系统,SQL 脚本就是唯一的真相
很多老系统其实没有像样的数据库设计文档。当时的开发流程可能就是“先建库再写代码”,表结构的演进全靠在测试环境不断执行 ALTER TABLE。到后面交接的时候,源码倒是齐的,数据库却没有人能说清楚有多少张表、哪些字段是冗余的、表之间的关系靠什么维系。这种时候,唯一的真相就是那份 SQL 脚本。
把 SQL 转成 ER 图,首先是给团队补上一份基础设计文档。ER 图上有表名、字段、主键、外键和关联关系,新同学接手时不需要去逐条读 CREATE TABLE 语句,一眼就能理解业务模型的骨架。对于技术管理者来说,审视 ER 图可以快速判断数据库设计是否规范,比如是否存在大量没有外键的“孤儿表”、是否存在字段类型不匹配导致潜在 JOIN 性能问题、是否有些表几年都没被引用过。
1.2 一张图能暴露出来的问题,比想象中多
画 ER 图的过程,其实也是数据库体检的过程。我处理过一个遗留项目,表里有十几个字段叫 name,分别属于不同的表,代码里查起来全凭猜测。把 ER 图画出来之后才发现,很多字段命名根本不一致,比如一张表叫 user_id,另一张表叫 uid,但它们其实指向同一份用户数据。这种问题在 SQL 文本里很难一眼看出来,放在图上挨着排布就很扎眼。
外键缺失的问题也很常见。很多团队为了规避删改约束,在建表时故意不声明 FOREIGN KEY,而是在应用层维护关系。这种设计不能说错,但会给后续数据治理带来很大麻烦。转 ER 图的时候,如果图形工具识别不到外键,关系线就不会出现,这时候就需要人工把“语义关系”补上——这就是 SQL 转 ER 图过程中最有价值的部分,逼着你去理解真实的数据流向。
2. 方案选型:四条路径,按场景挑
2.1 图形化工具的适用边界
最常见的做法是直接用数据库客户端自带的逆向工程功能。MySQL 用户首选 MySQL Workbench,它有一项 Reverse Engineer 功能,连上数据库之后会自动读取表结构、外键和索引,生成一张可编辑的 EER 图。Navicat 也有类似的“逆向数据库到模型”功能,它支持的数据库种类更多,SQL Server、Oracle、PostgreSQL 都能连。如果你用的不是 MySQL 而是 SQL Server,可以直接用 SSMS 的 Database Diagrams;Oracle 则对应 SQL Developer 里的 Data Modeler 视图。
这几种图形化工具的共同点是“所见即所得”,画出来的图可以直接拖动调整布局,导出 PNG、PDF 都很方便。优点是门槛低,几乎没有学习成本;缺点是当表数量超过一两百张时,布局会非常拥挤,工具本身的自动排布算法也不够聪明,需要大量手动调整。
2.2 脚本化方案的独特价值
当你需要批量处理几十个脚本、或者想把 ER 图生成嵌入到自动文档流水线里时,图形工具就不够用了。这时候我更推荐解析 SQL 文件中的建表语句和外键定义,再交给 Graphviz 渲染成图。整个过程可以用 Python 自动化完成,输出的是矢量图,放进技术文档或 Wiki 里非常清楚。
这个方案的另一个好处是可以定制。你可以在节点里只显示关键字段、可以用颜色区分业务域、可以过滤掉日志表和历史表、可以生成 mermaid 格式方便直接在 Markdown 文档里预览。对做数据治理的人来说,这种灵活性远比图形工具重要。
下表把这些方案放在一起做了个对比:
| 方案 | 典型工具 | 适用场景 | 主要局限 |
|---|---|---|---|
| 官方逆向工程 | MySQL Workbench | 单库结构梳理、快速出图 | 大库布局混乱、只擅长 MySQL |
| 通用客户端 | Navicat、DBeaver | 跨数据库、日常运维 | DBeaver 免费版存在局限,Navicat 为商业软件 |
| 数据库自带图表 | SSMS、SQL Developer | SQL Server / Oracle 专项 | 跨库能力弱、样式单一 |
| 脚本自动化 | Python + Graphviz / Mermaid | 批量处理、嵌入文档流水线 | 需要写代码、调整样式有门槛 |
如果只是临时看一眼,我建议直接用 Workbench 或者 DBeaver;如果这个 ER 图是要长期维护的文档产物,脚本化是更稳的路线。
3. 第一件事是整理 SQL:这些预处理决定成败
3.1 从项目脚本里抽离真正的建表语句
拿到手的 SQL 文件往往不是一个纯粹的建表脚本,里面可能混着视图定义、存储过程、触发器、初始数据 INSERT,甚至还有 CREATE DATABASE 和 USE 语句。直接用这种文件导入数据库再逆向,很可能因为语法兼容性问题中途失败。我的习惯是先把文件拆干净,只保留 CREATE TABLE 相关的部分。
拆文件的办法很简单,如果文件不大,用文本编辑器打开,手动删掉非建表语句;如果文件很大,用 grep 或正则批量提取。以 Linux 环境为例,可以先把所有 CREATE TABLE 语句块抽出来存成新文件:
grep -n "CREATE TABLE" schema.sql然后根据行号范围,用 sed 切出建表部分。对于插入数据的段落,直接跳过;对于存储过程和触发器,因为中间有 DELIMITER 和 BEGIN...END 结构,分号判断容易出错,建议整段不要。宁可多花十分钟整理输入,也好过后面在逆向的时候反复报错。
3.2 外键、索引和注释的保留策略
外键是你要转 ER 图的核心依据。很多脚本里外键是写在表定义末尾的 CONSTRAINT ... FOREIGN KEY 子句,Workbench 和 Navicat 都能识别。但也有一部分项目的脚本里根本没有外键声明,这种情况下图形工具生成的关系图就是一堆孤零零的表。我的建议是:优先保留脚本中的外键定义;如果原脚本确实没有,就在整理阶段手动补一份“语义关系说明”,在后续建模时手动连线。
索引信息虽然不影响实体之间的关系,但会影响 ER 图上的辅助展示,建议一并保留。注释也要留着。表注释和字段注释在参与评审时非常有用,Workbench 会把 COMMENT 显示在模型里。遇到中文注释最好确认文件编码为 UTF-8 或 UTF-8MB4,否则导入后就是乱码。
3.3 字符集与分隔符两个坑
字符集不对是导入报错的第一大来源。建表脚本中如果没有显式指定 DEFAULT CHARSET,而文件的保存编码又是 GBK 或 GB2312,导入 MySQL 时中文注释和默认值很容易变成问号。处理方式是在文件头部补充:
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0;SET FOREIGN_KEY_CHECKS = 0 的目的是在外键导入阶段暂时取消约束检查,等所有表建完后再打开。这能避免因为表创建顺序导致的外键找不到参照表的问题。
分隔符的问题主要出现在包含存储过程和触发器的脚本里。这些对象内部会有分号,需要用 DELIMITER 重新定义结束符。如果你截取建表语句时不小心把 DELIMITER 语句也带了出来,导入时就会把后面的内容截断。我一般建议直接过滤掉所有 DELIMITER 行,只处理 CREATE TABLE 和 ALTER TABLE。
4. 实操:用 MySQL Workbench 五分钟生成 EER 图
4.1 准备一个可导入的 MySQL 实例
最稳妥的做法是本地起一个空的 MySQL 实例,新建一个单独的库,然后导入整理好的建表脚本。本机装了 MySQL 就直接用命令行:
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS er_temp DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p er_temp < schema_clean.sql如果没有本地环境,用 Docker 起一个临时容器也行。我用的是官方镜像,映射一个数据目录,用完直接删容器,不会污染日常环境:
docker run --name er-studio -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -d mysql:8.0 docker exec -i er-studio mysql -uroot -proot er_temp < schema_clean.sql导入完成后,用 SHOW TABLES 确认表数量,再用 SHOW CREATE TABLE 抽查几张关键表,确认外键是否真的建上了。
4.2 执行逆向工程,调整布局
打开 MySQL Workbench,在菜单里选择 Database → Reverse Engineer。第一步选择连接方式,连上刚才的实例;第二步勾选目标 schema;之后工具会自动读取表定义、主外键关系,绘制成一张 EER 图。
生成的图默认布局通常比较乱,表之间的连线会交叉。Workbench 顶部工具栏提供了两层自动排序:一层是 Select All 之后用“自动布局”按钮,另一层可以按“外键关系”做垂直或水平分层。我的实操体验是:先用自动布局跑一遍,然后手动把相关的业务表拖到同一区域。如果一个库里表特别多,建议按业务模块分块处理,或者使用 Workbench 的“排列”面板按名称排序后再手动微调。
为了让图更清爽,可以关掉一些干扰信息。在左下角的 Model Overview 面板里取消勾选“索引”,只显示主键和外键标志。右侧的字段区域可以切换到“仅字段名”模式,避免 VARCHAR 长度、默认值这些噪音挤满画面。真正需要看字段属性的时候再临时打开。
4.3 导出图片和 HTML 文档
Workbench 支持直接导出 PNG、SVG、PDF。对于放进 Word 或 PDF 文档的场景,我建议导 SVG,矢量图放大不糊。File → Export → Export as PNG 的时候,注意设置分辨率,比例默认是 100%,如果是大图,调成 200% 导出再放进文档,打印时更清晰。
导出的 PNG 是整张画布,如果只调整了部分表的位置,画布边缘会有大片留白。此时可以用鼠标框选所有表,再选择“适应窗口大小”,让内容撑满画布再导出。Workbench 还支持导出 HTML 格式的模型报告,里面包含所有表和字段的定义,很适合用来做评审材料。
5. 进阶:Python 脚本解析 DDL,交给 Graphviz 渲染
5.1 思路拆解:从 CREATE TABLE 里提取结构
Workbench 适合交互式操作,但如果你的 SQL 脚本来自多种数据库、表数量巨大、或者这张 ER 图需要持续由 CI 流水线更新,就要用脚本化方案。核心思路分三步:第一步用正则表达式和简单文本解析从 SQL 里提取每个表的建表语句;第二步从表体里解析出字段名、类型、主键标记;第三步解析外键约束或人工维护的语义关系。最后把结果写入 DOT 格式,交给 Graphviz 渲染。
Python 没有内置 SQL 解析器,所以第一步可以用正则做粗提取。大多数生产环境中的建表语句结构比较规范,可以按“一个 CREATE TABLE 对应一个分号结束”的规则先做切分,再逐段解析。
5.2 核心代码演示
下面这段代码是去掉注释和空行之后的精简版。它的输入是整理好的纯净建表脚本,输出是一张 ER 图的 PNG。我平时会把这段逻辑封装成一个模块,放在文档生成工具库里复用。
import re from graphviz import Digraph def parse_sql_tables(sql_text): tables = {} patterns = re.finditer( r'CREATE TABLE\s+`?(\w+)`?\s*\((.*?)\)\s*(?:ENGINE|DEFAULT|COLLATE|COMMENT)?.*?;', sql_text, re.S | re.I ) for match in patterns: table_name = match.group(1) body = match.group(2) fields = [] primary_key = [] foreign_keys = [] for line in body.split(','): line = line.strip() if not line: continue # 外键约束行 fk_match = re.search( r'FOREIGN KEY\s*\(`?(\w+)`?\)\s*REFERENCES\s+`?(\w+)`?\s*\(`?(\w+)`?\)', line, re.I ) if fk_match: foreign_keys.append({ 'col': fk_match.group(1), 'ref_table': fk_match.group(2), 'ref_col': fk_match.group(3) }) continue # 主键约束行 pk_match = re.search(r'PRIMARY KEY\s*\((.*?)\)', line, re.I) if pk_match: primary_key = re.findall(r'`?(\w+)`?', pk_match.group(1)) continue # 字段行: ``name`` type ... col_match = re.match(r'`?(\w+)`?\s+(\w+)', line) if col_match: fields.append({'name': col_match.group(1), 'type': col_match.group(2)}) tables[table_name] = { 'fields': fields, 'primary_key': primary_key, 'foreign_keys': foreign_keys } return tables def render_er(tables, output_path='er_diagram'): dot = Digraph(comment='ER Diagram') dot.attr(rankdir='LR', splines='spline') for table_name, info in tables.items(): label_lines = ['<{0}>', f'<b>{table_name}</b>', '<hr/>'] for f in info['fields']: pk_flag = '🔑 ' if f['name'] in info['primary_key'] else '' label_lines.append(f'{pk_flag}{f["name"]} : {f["type"]}') dot.node(table_name, shape='plaintext', label=label_lines) for table_name, info in tables.items(): for fk in info['foreign_keys']: dot.edge(table_name, fk['ref_table'], label=f"{fk['col']} -> {fk['ref_col']}") dot.render(output_path, format='png', view=False)这段代码是纯解析脚本,不连接数据库,也不依赖外部服务。在几十张表这个量级下,解析速度是毫秒级的,Graphviz 渲染也很稳定。如果你要把这张图嵌入 Markdown 文档,可以把 format 改成 svg,或者直接把数据结构输出成 mermaid 的 erDiagram 语法。Tips:Graphviz 里默认的字体对中文不友好,生成前建议在 dot.attr 里加 fontname="Microsoft YaHei",否则表注释中文会变成方块。
5.3 实战:处理复合外键与自关联表
真实业务里没有那么多规规矩矩的单列外键。我遇到过一个订单表,它的关联键是 (user_id, order_seq) 组合,这种复合外键如果按单列解析,生成的关系线就会多出来或者连错位。处理的思路是把复合键当成一个整体节点标识,在 DOT 里把两个字段拼成一个连线标签,同时保持表内字段展示不变:
# 处理复合外键:fk_cols 是列表,ref_cols 是对应列表 label = ', '.join(f"{c}:{r}" for c, r in zip(fk['cols'], fk['ref_cols'])) dot.edge(table_name, fk['ref_table'], label=label, style='dashed')自关联表也很常见,典型场景是部门表的 parent_id 指向同一张表的 id。Graphviz 支持节点指向自身的自环边,在 DOT 里直接写 dot.edge("department", "department") 就行,视觉上会出现一个弯曲的回环。如果希望回环不遮挡其他关系线,可以给这条边单独设置 dir="both" 和 color="gray70"。
对于压根没有外键定义的项目,我通常维护一份 YAML 映射文件,人工把语义关系写清。比如:
relations: - from: user_order to: user on: user_id = id脚本在解析完 SQL 里的真实外键之后,再读取这份 YAML 追加进关系列表。这样即使原脚本没有外键约束,最终生成的 ER 图也能还原真实的逻辑关系。
6. 常见问题与排查实录
6.1 导入时报错:语法错误和字段类型不兼容
我把 SQL Server 的脚本直接往 MySQL 里导的时候踩过不少坑。SQL Server 的建表语句常用 nvarchar、datetime2、IDENTITY 自增,这些都是 MySQL 不认识的关键字,导入时必然报错。解决办法取决于你的目标库。
如果最终 ER 图工具是 MySQL Workbench,就必须先做方言转换。我一般是把 nvarchar 替换成 varchar、把 IDENTITY(1,1) 替换成 AUTO_INCREMENT、把 datetime2 替换成 datetime。这种替换只要写几条正则就能完成,虽然不保证 100% 语义一致,但对画图来说足够了。如果项目本身就是 SQL Server,那就没必要硬转,直接用 SSMS 的 Database Diagrams 更省事。
遇到 Oracle 的脚本更麻烦,NUMBER、DATE、VARCHAR2 这些类型 MySQL 都不认。我的建议是优先用支持 Oracle 的工具做逆向,比如 SQL Developer,或者 DBeaver,而不是转而改脚本。
6.2 外键对不上:引擎不一致、类型不一致、字段不存在
Workbench 逆向之后,如果发现应该有关联的表之间没有连线,绝大多数情况是外键声明本身没建成功。最常见的原因之一是建表时引擎用了 MyISAM,它不支持外键约束,MySQL 会静默忽略 FOREIGN KEY 语法,导致表建完之后根本没有外键记录。可以用下面的语句检查外键是否存在:
SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = 'er_temp' AND REFERENCED_TABLE_NAME IS NOT NULL;如果结果为空,说明外键全部丢失。此时就需要在建表脚本里统一加上 ENGINE=InnoDB,同时确保字段类型一致。外键列和引用列必须同类型同长度,比如 id 是 BIGINT,外键列却定义成 INT,超过一定范围就会失败;再比如 varchar 的长度不一致,也可能导致 Workbench 不认为它们是同一关系。
6.3 表太多、图太乱,怎么梳理
超过一百张表之后,无论 Workbench 还是 Graphviz,默认生成的图都是一团乱麻。我的做法是先做“域拆分”,按业务模块分成多张子图,而不是试图在一张图里展示全部。
在 Workbench 里可以利用 Model 的“Schema Diagram”分图管理,在 Graphviz 里则用 subgraph 把表归组,渲染时组内聚拢、组间用虚线连接主外键。分组信息同样来自那 YAML 文件,维护成本很低。如果非要一张大图,输出时可以调大节点间距、用弧形连线替代直线连线,观感会好很多。
6.4 中文注释乱码与导出图片模糊
中文乱码的原因刚才提过,主要是字符集问题。建表脚本导入之前,务必用支持编码转换的编辑器另存为 UTF-8 格式,并在脚本顶部加上 SET NAMES utf8mb4。如果是从旧系统导出的文件已经乱码,只能回到源头重新导出,没有更好的补救办法。
图片模糊的问题基本上都是导出分辨率太低。Workbench 导出 PNG 时把比例拉到 200% 以上,或者改用 SVG 出矢量图。Graphviz 也同理,生成时把 dpi 调高,比如:
dot.attr(dpi='200')这样出来的图片放进文档里,放大看字段名字依然清晰。
6.5 没有外键的“孤儿表”怎么画关系线
很多生产系统的表之间确实没有外键,但数据分析、后端代码里 JOIN 得飞起。这种表在 ER 图里孤零零地待着,画出来的图信息量不够。我遇到这种情况,会在 Python 脚本里增加一个“字段名启发式映射”的逻辑:如果字段名是 user_id,并且存在名为 user 或 users 的表,就自动猜测这是一条指向用户表的关系线。启发式规则准确率大概有七成,剩下的靠 YAML 人工修正。
这个逻辑听起来粗糙,但配合人工确认,比完全放任不管强得多。ER 图的意义不在于每个关系都完美无缺,而在于把那些代码里潜意识存在的关联显性化,让后来的人不用去一行行读 where 条件。
结尾
最后分享一点个人体会。我做了这么多次 SQL 转 ER 图,最大的收获不是拿到了一张漂亮的图,而是画图过程中对项目理解的一次补全。很多开发者在写代码时其实很清楚数据怎么流转,但从来没有人把这些流转画下来,等代码老到没人记得当初为什么这么设计的时候,ER 图反而是唯一还能翻开的历史。
所以不管是用 Workbench 还是写脚本,我都建议把转出来的 ER 图放进项目文档库,作为数据库设计的一部分长期维护。当表结构出现变化时,顺手更新这张图,成本很低,收益却是长期的。如果你手头也有一个堆了多年没人敢动的老库,不妨今天就试一下,把它的结构完整画出来,说不定会有不少意外发现。