ERD是数据库设计的数学草稿:从需求到建表全流程指南
1. 为什么说ERD是数据库设计的“数学草稿”而不是交付文档先说个真实经历。上个月接了个二手项目老板扔过来一个几十MB的SQL备份文件说“表都在里面了你自己捋一捋”。我打开一看46张表没外键约束没注释没ERD没有任何设计文档。我花了两个晚上从代码里反查表关系边看边骂为什么用户表里有个depart_id为什么订单表里有个parent_order_id为什么两张表字段长得几乎一样却一个叫detail一个叫info这种痛苦做过数据库设计的人都懂。但更痛苦的是——这种局面往往不是某个人故意埋雷而是团队从一开始就默认“ERD只是画给外人看的交付文档”画完往共享盘一扔从此再没人打开过。我后来花了三天把46张表的关系图补了出来。补完那一刻整个项目像从马赛克变成了高清照片很多积压已久的疑问瞬间有了答案。从那以后我养成一个习惯任何一个库第一件事不是写SQL是把ERD画出来。ERD的全称是Entity Relationship Diagram中文叫实体关系图。它用图形化方式描述业务中有哪些实体比如用户、订单、商品、实体有哪些属性比如用户有姓名、手机号、实体之间有什么关系比如一个用户可以下多个订单。听起来很简单但我见过太多人把这项基本功课做成了“画图表演”——节点画得圆润漂亮、配色花哨一细看关系线全是错的甚至漏掉了一半表。真正的ERD不是写完代码之后的补充说明而是动工之前的数学草稿。就像解一道复杂的大题你可以直接一顿操作写答案但错了只能整页划掉重来如果你先打草稿、推演思路中间过程出了小问题改起来成本极低。数据库设计是改动延迟极高的工作——表一旦上了生产环境里面有数据、有权限、有上下游依赖想改结构就是牵一发而动全身。ERD的职责就是把这些“牵一发”的代价前移到写代码之前消化掉。所以这篇内容我不会只教你ERD怎么画而是讲清楚一套我从需求分析、到表结构落地、再到长期维护ERD的方法。内容包括怎么从一句含糊的业务描述里拆出实体和关系、怎么判断一对一和一对多、ERD上有哪些细节坑会影响建表正确性、不同阶段用什么工具最顺手、以及一份可以直接拿去当评审checklist的东西。适合正在学数据库设计的新人也适合被各种“历史包袱”折磨的各位开发。看完你应该能直接上手把你手里那张“马赛克库”变成能让后面接手的人少骂两句的清晰地图。2. 从一句业务需求到一张合格的ERD实体识别与关系拆解2.1 用名词抓实体用动词抓关系很多新手拿到业务文档第一反应是懵这么多字我哪知道哪个该画成方块哪个该画成连线有一个非常朴素且好用的方法把需求描述当成一篇阅读理解先从里面抓名词再从里面抓动词。名词大概率是实体Entity。比如“用户可以注册多个收货地址”“用户”是一个实体“收货地址”是另一个实体。再比如“订单包含多个商品”“订单”和“商品”都是实体。动词和量词则指向关系Relationship。“多个”“每个”“包含”“属于”“拥有”这些词通常就是关系的信号。但这里有一个高频误区不是所有名词都能成为实体也不是所有名词都该单独建表。比如用户“地址”如果需求只是记录一个“地址字符串”那它就是用户表里的一个字段但如果是“一个用户可以维护多个收货地址下单时选择其中一个”那它就必须抽出来成为独立实体。判断标准就是一句话——这个名词是否需要作为独立主体被引用、被统计、被关联。如果它只需要依附于某个实体存在那就先老老实实当属性。2.2 一个电商订单场景的完整拆解假设产品经理丢给你一句话“做一个商城系统用户可以浏览商品、把商品加入购物车然后下单购买。订单生成后用户可以查看订单详情后台管理员可以统一管理商品分类和库存。”就这么短短一句我们动手拆。第一轮圈名词用户商品购物车订单订单详情管理员商品分类库存八个名词。但购物车和库存先打个问号购物车在当前表述里是一个需要持久保存的独立对象还是仅仅属于用户的一个临时状态库存是商品的一个属性字段还是需要独立为一个实体做进销存管理在没有更多信息前购物车可以先不落表很多系统确实用Redis临时存库存先不立项商品表加一个stock字段就够了。第二轮圈动词和量词用户“浏览”商品商品“加入”购物车用户“下”订单订单“包含”详情管理员“管理”分类、“管理”库存到这里核心实体基本浮出水面用户、商品、商品分类、订单、订单详情、管理员。第三轮把关系理清楚并标注基数用户 — 订单一个用户可以下多个订单多个订单也可以归属同一个用户。这是典型的一对多1:N外键放在“多”的一方也就是订单表存user_id。商品分类 — 商品一个分类下可以有多个商品一个商品只属于一个分类。这个也是一对多。需要注意分类一般还要考虑层级一级分类、二级分类如果只有两级用parent_id就能解决如果层级不确定可能需要更复杂的设计。这里我们按挂在“商品表存category_id”处理。订单 — 订单详情一个订单可以包含多个商品明细同一商品买了两件明细是两条记录还是数量2的一条记录这属于业务策略。通常一个订单对应多个明细所以是一对多。这里出现了第一个容易混淆的点订单明细虽然通常不叫“订单商品表”但它在ERD里是一个独立实体叫订单详情。订单 — 商品订单详情这个中间实体把订单和商品解耦了。那问题来了——一个订单能不能包含多个商品能。一个商品能不能出现在多个订单里也能。这是多对多M:N。在多对多建模里必须拆出中间实体也就是订单详情。所以订单和商品之间不存在直接依赖它们依赖的是订单详情这条“桥”。第四轮补齐主键和外键就形成了下面这个核心ERD我用的文本化ERD语法后面讲工具时会细说Table users { id int [pk, increment] name varchar email varchar created_at timestamp } Table categories { id int [pk, increment] name varchar parent_id int } Table products { id int [pk, increment] category_id int [ref: categories.id] name varchar price decimal stock int } Table orders { id int [pk, increment] user_id int [ref: users.id] amount decimal status varchar created_at timestamp } Table order_items { id int [pk, increment] order_id int [ref: orders.id] product_id int [ref: products.id] quantity int price decimal }这就是一个能直接指导建表的ERD雏形。你发现了吗中间的每一步都是在用需求原文的线索做判断而不是凭感觉拍脑袋。2.3 基数判断1:1、1:N、M:N分别怎么定基数cardinality是ERD里最核心的约束。没有基数的关系线等于没画——你画了一根线说“用户和订单有关系”可到底一个用户能下几个订单、一串订单能否属于多个用户不写清楚建表时外键放哪边都无从谈起。一对一1:1A的一条记录最多对应B的一条记录反过来也一样。比如一个用户对应一个身份证实名认证信息。这种关系在建库时最简单的是直接合成一张表如果确实要拆表外键放哪边都行但建议放相对不常用、或者扩展可能性更大的一方。一对多1:NA的一条记录可对应B的多条记录但B的每条记录只能归属一个A。这是最常见的关系外键永远放“多”的一侧。比如用户和订单订单表存user_id。多对多M:NA的多条记录可对应B的多条记录反之亦然。比如订单和商品。这种关系在关系型数据库里必须通过中间表来拆解中间表存双方的ID让原本的M:N退化成两个1:N。遗漏中间表的后果在后面的“踩坑”部分我会单独说。判断基数的核心技巧就一句话拿着业务方向的两个例子去问自己反向的问题。你问“一个分类下能放几个商品”答案是多个再问“一个商品能属于几个分类”答案是只能一个。于是你拿到1:N。你问“一个用户能有多少个订单”多个再问“一个订单能属于几个用户”只能一个——又是1:N。你问“一个订单元有多少个商品”多个“一个商品能出现在多少订单里”也多个——那好M:N拆表。这比背任何规则都管用。3. ERD里三个最容易被忽略的建模细节主键、弱实体与继承关系3.1 主键选择业务主键是定时炸弹很多人初学ERD时把“主键”想得很简单有ID列设成自增主键完事。但实际建模时主键设计的争吵能成团队里最耗时的话题之一。我见过最典型的错误是拿业务字段当主键。比如用户表觉得手机号是唯一的就把手机号做成主键订单表觉得订单编号是唯一的就设成主键。短期看没问题长期呢手机号可能换、系统可能需要支持一个手机号对应多个账户、订单编号的生成规则一变主键跟着变。主键一旦变了外键全部要跟着拆迁那是一场灾难。更稳妥的规则是只要不是小到不行的管理系统一律用代理主键surrogate key也就是无业务含义的自增ID或UUID同时用唯一索引去约束业务字段的唯一性。把“唯一性”和“主键身份”两件事分开你才能在高性能和业务柔性之间找到平衡。在ERD上主键标记为PK外键标记为FK最好在图上就直接标清楚。一张ERD如果连主外键都不标那它只能算“业务草图”不能落地。3.2 弱实体依赖父实体而存在的数据弱实体Weak Entity这个概念说实话很多干了三五年的人也没搞清。但它在实际设计里特别常见。弱实体的特征是离开了它的父实体单独存在没有意义。最典型的例子就是订单明细。一个订单明细一旦去掉它归属的订单它甚至连“属于谁”都不知道更不用说实体的完整主键了。它的Key往往需要包含父实体的主键或者依赖父实体才能唯一标识。你在ERD里面画弱实体时规范做法是用双线边框或圆角矩形把弱实体标出来并让它的主键里带上父实体的外键。比如order_items的主键可以设计成自增id但必须在逻辑上用(order_id, id)组合才能唯一确定某条明细。这个细节决定了开发阶段对“这条数据到底怎么找”的理解是否一致。你可以在ERD上用注释文字标注“该实体主键依赖父实体存在”不用画得非常学术但意思必须传达到位。3.3 继承关系超类与子类的三种落地方案业务里常常出现“都是用户但有的是个人用户有的是企业用户字段差异还挺大”的场景。这种抽象叫“继承”在UML里有专门画法但在ERD里你需要落地成具体的表结构方案。三种经典方案单表继承Single Table将所有父类和子类的字段全部塞进一张表用type字段区分。简单、查询快但空列多表越宽越难维护。类表继承Class Table父类一张表存公共字段子类分别一张表存差异化字段子表通过外键和父表关联。查询需要JOIN但字段清晰、无冗余是实际项目里最常推荐的方案。具体表继承Concrete Table每个子类各建一张完整表包含公共字段和各自字段。查询最直接但公共字段逻辑重复改动后要同时改多张表。你在ERD上画这类关系时比较实用的做法是画一张“用户”超类表下面画“个人用户”和“企业用户”两张子类表然后用一根带空心箭头的线连接并标注采用哪种方案。我在后面接手的系统里看到最多的是“一个用户表挂了十几个可空字段每个业务线只用其中三四个”这就是典型的没有先画ERD、最后被单表继承拖垮的例子。4. ERD工具选型从免费画图到企业级建模我怎么选工具这个话题往往是被忽略但影响体验极大的环节。很多人第一反应是“拿个万能画板凑合画”结果画到一半连对齐都费劲关系线一多就成了蜘蛛网。我根据项目阶段和协作方式把常用工具分成了四类大家按需取用。工具典型用途优势不足上手难度draw.io (diagrams.net)快速绘制、方案讨论免费、浏览器可用、导出方便无数据库逆向、自动布局弱低dbdiagram.io文本化ERD、代码生成语法简单、易版本管理、共享方便免费版协作有限低ERDPlus教学、小型项目操作直观、支持弱实体和关系标注导出格式少、无代码生成低Navicat / DBeaver已有库逆向成ERD直接连库、真实反映表结构偏向可视化不适合从零设计中PowerDesigner大型企业项目数据字典、模型驱动、物理模型转换重、贵、学习曲线陡高我先聊聊自己真实的工作流。方案讨论阶段我基本用draw.io或白板快方便修改边画边撕。等需求方向收敛了到了要“出图给人看、按图开发”的阶段我会转成dbdiagram.io用文本把表定义、主键外键、索引全部写出来。原因很实在——dbdiagram.io生成的ERD可以导出成图片但它本身是文本源文件可以放进git仓库参与版本管理。这一点对长期项目太重要了你用Visio画的图改没改、哪一版是当前最新的全看团队自觉但文本文件纳入git后每一次改动的历史记录、谁改的、为什么改全都清清楚楚。再补一句逆向工程如果接手老项目、手头只有数据库没有文档用Navicat或DBeaver逆向出ERD是最高效的方式。逆向出的图能帮你快速看清表字段和索引现状但它只能反映“代码里有什么”反映不了“业务为什么这么设计”。等你看懂之后还是值得用正向的方式手工重画一版逻辑ERD那张才是能指导后续维护的东西。关于工具我的观点是别崇拜某个软件也别一味追求“免费”。ERD工具的核心指标只有三个——改起来方不方便、协作起来清不清楚、历史版本查不查得到。任何工具只要能同时满足这三点哪怕是记事本加手绘也足够专业。5. 实操ERD从0到落地我的五步走法工具说完了下面是一套我现在每个项目都会走的完整流程。我不保证这是唯一正确的方式但至少是踩过一堆坑之后沉淀下来的、每一步都有明确产出的方法。第一步收集业务素材拒绝凭空画图。先找业务方聊一轮拿到你要建模的业务范围把产品文档、原型图、现有系统输出的报表结构都过一遍。特别注意三样东西一是高频查询单据订单、报表上的字段二是业务方口中反复出现的“编号”“单号”“名称”这种业务关键词三是描述里出现的条件分支比如“如果未支付可以取消支付后只能退款”。这些都是后续实体的重要线索。不要跳过这步直接开始画图你对业务的理解决定了ERD的上限。第二步画实体与属性先不画关系。把第一步收集到的名词初步整理成实体列表给每个实体列属性。这一步你要问自己三件事每个实体靠什么唯一标识也就是主键哪些属性是它的非空必需字段哪些属性在当前阶段可以不纳入范围同时把不确定的字段做上标记比如“支付时间——需确认是否单独成表”。这一步的产出是一张张“实体卡片”不要急着连线。第三步用业务规则确定关系和基数。开始连线时每画一条线都强迫自己用前文那句反问去验证基数。同时确认关系的可选性这个关系是必须存在还是可存在可不存在的比如“用户必须登录后才能下单”——用户和订单的关系是可选对可选还是强制对可选ERD上一般会用圆圈和竖线表示可选性虽然不同工具表示法不一样但你在讨论时要明确这个信息。因为可选性直接影响建表时外键能否为NULL这是个硬约束。第四步规范化检查。建表之前用简化版的第三范式过一遍有没有完全重复的字段组没抽出来有没有字段依赖非主键列多对多关系是不是都拆了中间表中间表的主键和唯一索引设计好了没有这一步是“低成本修改的最后机会”过了这个村后面全是高成本修改。第五步评审与迭代。把ERD拉个评审会与会者至少包含业务方、后端开发、测试。评审时要求他们看着图指出不合理的地方而不是让他们“回去再看看文档”。我在每个项目里都有一份固定评审清单列出最常见的十个问题只要有一个回答不上来这张ERD就不允许进入实施阶段每张表的主键是什么是代理主键还是业务主键每个外键对应到哪张表的哪个字段依赖关系是否清晰是否存在姓名/电话/金额这类高频字段是否在索引或查询上有优化空间状态字段的取值范围是什么初始状态和终态分别是什么有没有哪些字段是用来做“记录审计”的created_at、updated_at、创建人哪些表的数据量会很大ERD上是否需要分区、归档策略的标注有没有循环依赖A依赖BB又依赖A如果有归档和删除时怎么处理历史数据迁移要不要纳入设计ERD上有没有规划临时表或过度字段权限相关字段是否和用户/角色表正确关联有没有被业务方遗漏、但逻辑上必然存在的表这套清单不长但每一条都在实际项目里被验证过。比如循环依赖一旦建表建错了后续删除数据特别麻烦再比如状态字段没有定义取值范围开发写代码时各写各的上线后统计口径就乱套了。画完ERD停下来做一轮清单检查最多多花两三个小时省下的是后面数倍的返工时间。6. 我在ERD上踩过的5个坑以及它们如何改变我的画图习惯最后这部分是心血之谈。以下五个坑每一个都是我拿真实项目的代价换来的。写出来能帮后来人绕开也算值了。坑一多对多关系不建中间表。有一次做会员营销系统我在ERD上直接画了“用户——优惠券”的多对多关系但没有拆中间表觉得“两个表之间加个逗号分隔的券ID字段不也够了”开发也图省事照做了。结果上线两个月后产品要做“用户获取优惠券的来源分析”我望着那张表里逗号分隔的ID写SQL写得想抽自己。从那之后我立了规矩ERD上不允许出现M:N的直连线必须画到一张中间表上中间表的主键、唯一索引、额外冗余字段如领取时间、使用时间、来源渠道一并设计好。坑二弱实体的主键策略没和开发对齐。以前画订单明细我在图上就标了个“id int pk”没写清楚这个自增id在全表范围内唯一即可。结果开发老兄理解成“每个订单下明细的id都从1开始”于是order_items表的主键用的是(order_id 1, 2, 3)代码里手拼业务规则后来做大促时疯狂撞主键。这让我意识到ERD上的主键标记不只是“画个钥匙符号”那么简单你必须把主键生成策略自增、UUID、雪花ID、还是组合键写在图上或字段描述里。画得越细开发歧义越少。坑三ERD不做版本管理。前面讲工具时多次提到这一点因为它坑得太惨了。当时项目用Visio画ERD设计V2版本时直接在旧文件上改了结果开发一半发现改动有问题想回退到V1大家对着共享盘里的文件翻记录只找到“上次覆盖时间”。后来我全面转向文本化ERD管理每次修改生成一份diff回滚就是一条git revert命令的事。这件事让我彻底坚定了一个信念ERD的源文件必须是文本图片只是它的渲染产物。凡是源文件是图形格式、没法diff的ERD都不适合严肃的软件项目。坑四逆向生成的ERD不等于设计文档。有一次接手老系统用的DBeaver自动逆向ERD图倒是生成得很漂亮几十张表的关系线铺满屏。但你盯着它看完全不知道当初为什么这样设计——为什么要给订单表加一个“snapshot_data”的JSON字段为什么用户地址和订单地址是两个表这些“为什么”在逆向图上永远找不到答案。所以我后来的做法是逆向ERD用来快速了解现状但一定会在上面叠加业务注释把不确定的设计用问号标出来再手工补一张逻辑层的ERD。只有逻辑层那张才是能指导后续维护的活文档。坑五画图时为了“规范”牺牲了“可用”。这话听起来有点反常识但我知道不少同行都有类似经历为了追求完美范式把一张用户表拆成了十几个小表每个字段都抽模型结果业务方和开发都被这张ERD吓退了——太复杂没法用。规范是工具不是目的。一层薄薄的“冗余”有时候反而能救性能——例如订单快照字段它违反了第三范式但大幅简化了售后的查询逻辑那就是合理的设计。ERD不是课本作业没有“标准答案”只有“适合当前业务的方案”。还有一个坑其实不该叫坑而是一条经验ERD永远不要拖到“完全想清楚”才开始画。第一版两小时画完拿着去问业务方“这是你想要的吗”比一个人憋一天憋出一张精修大图有用得多。迭代永远比一步到位更能逼近真实需求。我最近给团队定了个不成文的规矩拿到任何新需求先问“有没有ERD”如果没有先花半天画出来再讨论排期。效果立竿见影——开发评审会上的无效争论变少了测试拿到的不是一句模糊的“按文档来”而是一张带着字段、关系、主键和状态定义的地图。如果非要说一句给后来人的建议那就是把ERD当成一个伴随项目成长的活文档而不是开工前的一个仪式。每次表结构发生变化顺手把ERD更新一次花不了你十分钟却能让一年后的自己和你的同事们少挠几天头。