E-R图实战:电商购物系统数据库设计全流程解析
2026/9/18 7:41:51 网站建设 项目流程

做数据库相关工作这些年,我面试过不少人,也带过不少新人,发现一个很有意思的现象:很多人张口就能背出三大范式,写SQL也溜得很,但一让他画E-R图,或者根据需求设计表结构,就开始露怯。要么实体找不全,要么联系理不清,画出来的图自己都解释不通。

E-R图这个东西,说难不难,说简单也不简单。它就是数据库设计的“图纸”——你还没开始建表,还没写一行SQL,先用一张图把业务世界里有哪些“东西”、这些“东西”之间什么关系、各自带什么“属性”给描述清楚。图纸画错了,后面建的表、写的查询、做的报表,全都会跟着歪。所以“数据库-E-R图练习”这个看似基础的题目,其实是整个数据库设计能力的分水岭。

这篇博文我就拿“电商购物系统”这个最经典的练手场景,完整走一遍从需求分析、实体识别、联系梳理,到最终画出E-R图并转成建表SQL的全过程。不管你是在准备数据库课程设计,还是在复习面试题,或者单纯想把手上的烂表结构重构一下,这篇内容都能给你一套可以直接照搬的思考方法和实操步骤。

1. 画E-R图前,先搞懂这几件事

1.1 E-R图到底是什么,它解决什么问题

E-R图,全称是实体-联系图(Entity-Relationship Diagram),1976年由Peter Chen提出,到现在快五十年了,依然是数据库设计领域最基础也最实用的建模工具。它解决的核心问题只有一个:把现实世界的业务规则,翻译成计算机能理解的数据结构

打个比方。你开一家小卖部,脑子里很清楚:有顾客来买东西,有商品摆在货架上,顾客买了什么、付了多少钱,这些你心里都有数。但要把这套生意搬进电脑系统里,你就得先把“顾客”“商品”“订单”“付款”这些概念拆清楚。什么是实体(Entity)?顾客、商品就是实体,它们是业务中独立存在的“事物”。什么是属性(Attribute)?顾客有姓名、电话,商品有价格、库存,这些描述实体的特征就是属性。什么是联系(Relationship)?顾客“下”订单,订单“包含”商品,这个“下”和“包含”就是实体之间的联系方式。

E-R图就是把这些要素用统一的图形符号画出来:矩形表示实体,椭圆表示属性,菱形表示联系,线段把三者连起来。一张图画完,整个业务的数据结构就一目了然了,开发、测试、产品都能对着同一张图说话。

1.2 一张E-R图为什么能决定数据库设计的成败

我见过太多“先建表、后补需求”的项目,结果表建了二三十张,字段加了几百个,上线跑了一个月,发现统计报表根本写不出来。为什么?因为底层的数据关系从一开始就是乱的。E-R图的价值恰恰在于,它逼着你在动手建表之前,先把业务逻辑想清楚。

举个例子。一个简单的电商系统,如果没画E-R图就直接建表,你很可能建一张“订单表”,里面塞上用户姓名、用户电话、收货地址、商品名称、商品单价、商品数量、订单总价……所有字段堆在一起。看起来挺省事,一张表搞定所有查询。但真上线了你就会发现:用户改了手机号,订单里的手机号却还是旧的;商品改了个价格,历史订单的金额也变了;想统计“这个用户一共买了多少种商品”,SQL写得像天书。

如果你先画了E-R图,就一定会发现“用户”和“订单”是两个实体,“订单”和“商品”是多对多的联系,自然就会拆成用户表、订单表、商品表、订单明细表四张表。这就是E-R图的威力:它用一张图,把规范化设计的压力前置到了设计阶段,而不是等你写了几十条烂SQL之后再来哭。

1.3 画图前的需求分析:先有需求后有图

很多人拿到题目就急着画矩形,这是最大的误区。E-R图是对需求的图形化表达,需求不清,图画得再漂亮也是空中楼阁。做需求分析最简单有效的方法,就是把自己当成系统的使用者,把核心业务流程走一遍

以电商购物系统为例,你闭上眼睛想象一次完整的购物经历:

  1. 你注册一个账号,登录系统。
  2. 浏览商品列表,点进某个商品详情页看看。
  3. 把商品加入购物车。
  4. 结算下单,填写收货地址。
  5. 支付订单。
  6. 商家发货,你收到货。
  7. 你对商品进行评价。

这七个步骤走完,业务的主干就有了。每走一步,你就问自己三个问题:这个环节里有哪几个“东西”在参与?每个“东西”有哪些关键信息要记录?“东西”和“东西”之间是什么关系?把这三个问题的答案记下来,实体、属性、联系的基本素材就齐了。后面画的每一个矩形、每一个菱形,都是从这个业务流程里“长”出来的,而不是凭空想出来的。

2. 实体、属性、联系:画图的核心三要素

2.1 怎么从业务描述里“抓”出实体

实体识别的核心原则是:实体是业务中独立存在、需要被记录信息的事物。判断标准很简单——如果这个“东西”消失或者不存在了,业务还成立吗?如果业务必需且它有自己独立的信息要记录,那它就是实体。

实操中,我习惯用“名词划线法”。把需求文档或业务流程描述里所有的名词都圈出来,然后逐个过滤:哪些是实打实的业务对象,哪些只是对象的属性,哪些只是修饰词。

比如需求文档里写:“用户在平台注册后,可以浏览商品,将商品加入购物车,生成订单并完成支付,随后商品发货,用户确认收货后可以对订单进行评价。”

圈出来的名词有:用户、平台、商品、购物车、订单、支付、收货、评价。现在开始过滤:

  • “平台”是系统本身,不是系统里要管理的对象,排除。
  • “支付”更像是订单的一个行为或状态,不是独立实体,但有支付单这个概念的话另说。
  • “收货”是发货后的动作,通常归类为订单状态,不单列实体。
  • “评价”是用户对商品或订单的反馈,可以做成独立的“评价”实体,也可以做成订单的附属信息。考虑到一个订单一个评价,而且评价内容、评分、评价时间这些信息都挺独立,我倾向于单列一个“评价”实体。

过完这轮过滤,核心实体就浮出来了:用户、商品、订单、购物车项、评价。注意购物车——很多人纠结它算不算实体。我的建议是:购物车本身不是必须单独成实体,购物车里的每一行(用户、商品、数量、选中状态)才是需要记录的,所以“购物车项”是一个实体。如果你用Redis之类的方式做购物车,那它不进数据库,另说;但如果入库,就按照“购物车项”来设计。

2.2 属性和实体怎么区分,什么时候该拆成实体

属性是描述实体的特征,但“特征”这个词实操起来会有边界模糊的时候。我总结了两条判断规则:

第一,属性必须是原子性的,不可再分。比如“用户地址”,如果只需要存一个字符串,那是属性;但如果你要按省份、城市、详细地址分别统计和筛选,那就该拆开,或者单独建一个“地址”实体,一个用户有多个地址,联系就出来了。再比如“商品分类”,如果只是存个分类名,那是属性;但如果分类分两级甚至三级,还带图标、排序、描述等信息,就该建“分类”实体,商品和分类之间形成多对一或树形关联。

第二,一个属性值变化时,是否会影响其他实体的历史数据。这条是判断“要不要把属性升格为实体/关联”的关键。还是商品和订单的例子:订单里存了商品当时的快照信息(商品名、单价),这就不是“商品”实体的属性,而是“订单明细”这个联系本身的属性。为什么?因为商品的价格会变,订单里记录的是成交那一刻的价格,它跟当前商品表里的价格是两回事。这个细节如果你没想清楚,以后做订单统计一定会对不上账。

2.3 三种联系类型:1对1、1对多、多对多

实体之间的关系就三种:一对一、一对多、多对多。这个知识点看着简单,实际画图时最容易出错。

一对一(1:1):A的一个实例最多对应B的一个实例,反之亦然。比如“用户”和“用户详情”(身份证号、实名认证信息),一个用户只有一份详情,一份详情只属于一个用户。在数据库里,你甚至可以把它们合并成一张表,所以一对一联系在设计中往往会被优化掉。

一对多(1:N):A的一个实例对应B的多个实例,反之B的一个实例只能对应A的一个实例。这是最最常见的联系类型。比如“用户”和“订单”——一个用户可以有多个订单,一个订单只属于一个用户。“分类”和“商品”——一个分类下有很多商品,一个商品只属于一个分类。

多对多(M:N):A的一个实例对应B的多个实例,反之亦然。“商品”和“订单”就是典型——一个订单包含多个商品,一个商品也出现在多个订单里。多对多联系在E-R图上可以直接用菱形表示,但到建表阶段必须拆成一张中间表,也就是订单明细表。这是E-R图转关系模式的核心技巧,后面细说。

判断联系类型时,我常用一个“双向问句话术”:随便拿一个A,它能对应几个B?再随便拿一个B,它能对应几个A?两边各自回答“一”还是“多”,组合起来就是联系类型。比如“用户”和“订单”:一个用户能下多个订单(一→多),一个订单只能属于一个用户(多→一),所以是一对多。

2.4 联系的属性:很多人忽略的关键细节

实体有属性,联系也有属性。这是个非常容易漏掉的点,但它恰恰是E-R图设计中最见功力、最能拉开水平差距的地方。

举个最典型的例子:“订单”和“商品”之间的多对多联系,我之前说了要拆成“订单明细”。那么“订单明细”里有什么?有商品数量、有成交单价、有是否评价。这个“商品数量”“成交单价”是订单的属性吗?不是,因为一个订单包含多个商品,每个商品的数量和价格都不同,这些信息无法直接挂在订单头上。是商品的属性吗?也不是,因为商品表里存的是当前价格,不是每次成交的价格。

所以,数量、成交价这类信息,属于“订单-商品”这个联系本身。在E-R图里,你可以把这些属性画在连接“订单”和“商品”的菱形旁边。到转关系模式时,这些属性自然就落到“订单明细表”里了。

再举个例子。“学生”和“课程”是多对多联系(一个学生选多门课,一门课有多个学生选),那么“选课成绩”是属性还是联系属性?显然,成绩既不能只挂在学生上(学生有多门课,每门课成绩不同),也不能只挂在课程上(课程有多个学生,每个学生成绩不同),它必须挂在“选课”这个联系上。掌握了这个思路,你画E-R图时就不会再把字段塞错位置。

3. 完整实操:给电商购物系统画E-R图

3.1 需求分析:购物系统需要哪些功能

既然题目是经典的“对电商购物系统做需求分析并画出E-R图”,我们把需求再细化一点,基于这个需求来画图。为了练习,我们适度增加一些复杂度,让它更接近真实业务:

  • 用户注册登录,维护个人基本信息,包括用户名、密码、手机号、邮箱、收货地址。一个用户可以有多个收货地址。
  • 商品按分类组织,分类分两级:一级分类(如“数码家电”),二级分类(如“手机”)。商品属于最末级分类。
  • 用户浏览商品,将商品加入购物车。购物车中的每一条记录包含用户、商品、数量、勾选状态。
  • 用户结算下单。一个订单对应一个收货地址,包含多个商品项,每个商品项记录商品快照、数量、成交单价。
  • 订单有状态:待付款、已付款、已发货、已完成、已取消。付款生成支付流水,记录支付方式、支付金额、支付时间。
  • 用户确认收货后,可以对订单中的每个商品进行评价,评价包含评分、内容和图片。

上面这些需求已经足够支撑一张中等复杂度的E-R图练习了。从这个需求描述里走一遍“名词划线法”,实体基本就齐了:用户、收货地址、商品分类、商品、购物车项、订单、订单明细、支付流水、评价。一共9个实体,这个规模用于课程设计或面试手绘图,已经非常合格。

3.2 识别实体与属性,逐个盘点清单

有了实体清单,接下来给每个实体配上属性,同时圈出主键。主键的选择原则:稳定、唯一、不含业务语义——所以实际开发中大家偏爱自增ID或雪花ID,不太建议把手机号、身份证号当主键(一改就崩,还有隐私问题)。

实体的属性列表如下:

用户:用户ID(主键)、用户名、登录密码、手机号、邮箱、注册时间、账号状态。

收货地址:地址ID(主键)、用户ID(外键)、收货人姓名、联系电话、所在省份、城市、详细地址、是否默认地址。

商品分类:分类ID(主键)、父分类ID(一级分类此字段为空)、分类名称、排序值。这里是把两级分类放到一张表里,通过“父分类ID”自关联。这是很常见的树形表设计。

商品:商品ID(主键)、分类ID(外键)、商品名称、商品主图、商品描述、当前单价、总库存、上架状态、创建时间。

购物车项:购物车项ID(主键)、用户ID(外键)、商品ID(外键)、数量、勾选状态、加入时间。

订单:订单号(主键)、用户ID(外键)、收货地址ID(外键)、订单状态、下单时间、支付时间、发货时间、完成时间。注意,订单金额不建议直接存一个总金额字段,可以通过明细算出来;但如果系统对性能要求高,冗余一个总金额也很常见,面试时能说出这个考量,是加分项。

订单明细:明细ID(主键)、订单号(外键)、商品ID(外键)、商品名称快照、商品图片快照、成交单价、购买数量。

支付流水:支付流水号(主键)、订单号(外键)、用户ID(外键)、支付方式、支付金额、支付时间、支付状态。

评价:评价ID(主键)、订单明细ID(外键)、用户ID(外键)、商品ID(外键)、评分(1-5星)、评价内容、评价图片、评价时间。

这里有个细节要说明:为什么评价要关联商品ID?因为评价本质上是对“某次购买中的某个商品”进行反馈,关联订单明细ID可以定位到具体是哪一单哪一件商品,关联商品ID是为了方便商品详情页直接聚合展示所有评论。有的设计里这两者取一个就行,但电商系统里商品详情页和订单页都要展示评价,所以我习惯两个都留。

3.3 确定联系与联系类型,把图画出来

实体和属性都清了,最后一步就是连线。逐个分析两两实体之间的联系,画出菱形,标注1和N或M和N。

先列出所有联系:

  1. 用户-收货地址:一个用户有多个收货地址,一个地址只属于一个用户。1:N。
  2. 商品分类-商品分类(自关联):一个一级分类下有多个二级分类,一个二级分类属于一个一级分类。1:N。二级分类下有多个商品。
  3. 商品分类-商品:一个二级分类下有多个商品,一个商品只属于一个分类。1:N。
  4. 用户-购物车项:一个用户有多条购物车项,一条购物车项只属于一个用户。1:N。
  5. 商品-购物车项:一个商品可以出现在多个用户的购物车里,一条购物车项只对应一个商品。1:N。所以“用户-商品-购物车项”实际是两条1:N,汇合到购物车项这个实体上。
  6. 用户-订单:一个用户有多个订单,一个订单只属于一个用户。1:N。
  7. 订单-收货地址:一个订单对应一个收货地址,一个地址可以被多个订单使用。N:1。这里要特别注意,订单关联的是“下单那一刻”的地址快照还是地址ID?我的做法是订单表存地址ID,同时把收货人、电话、地址冗余一份到订单表,因为用户的地址后来可能修改,历史订单需要保留当时的地址现场。
  8. 订单-订单明细:一个订单有多个明细,一个明细只属于一个订单。1:N。
  9. 商品-订单明细:一个商品可以出现在多个订单明细中,一个明细只对应一个商品。1:N。
  10. 订单-支付流水:一个订单可能有多条支付流水(比如支付失败重试、部分退款),一条流水只属于一个订单。1:N。
  11. 订单明细-评价:一条订单明细对应一个评价(也可能没有),一个评价只对应一条明细。1:1。这里按照业务规则“一个商品一条评价”即可,如果你允许同一个商品评价多次,那就要改成多个评价了。

把所有实体和联系画到一张图上,就是完整的电商购物系统E-R图。画图工具推荐draw.io、ProcessOn、Visio都行,手绘也没问题——重要的是关系理清了,画出来只是表达问题。

3.4 从E-R图导出关系模式(建表SQL)

E-R图画完,数据库设计的重头戏来了:把它翻译成关系模式,也就是表结构。转换规则三条:

  • 每一个实体转成一张表,主键就是实体的主键。
  • 1:N联系:把“1”那一侧的主键作为外键,加到“N”那一侧的表中。比如“用户-订单”,就把用户ID加到订单表里。
  • M:N联系:单独建一张中间表,把两侧的主键都拿过来作为外键,联合起来还能当复合主键。比如“商品-订单”,就是订单明细表。

按照这些规则,上面9个实体的建表SQL长这样(我用MySQL做示例,略去外键约束的繁琐声明,重点是结构):

CREATE TABLE user ( user_id BIGINT PRIMARY KEY, username VARCHAR(50) NOT NULL, password_hash VARCHAR(100) NOT NULL, phone VARCHAR(20), email VARCHAR(100), register_time DATETIME, status TINYINT ); CREATE TABLE user_address ( address_id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, receiver_name VARCHAR(50), receiver_phone VARCHAR(20), province VARCHAR(50), city VARCHAR(50), detail_address VARCHAR(200), is_default TINYINT DEFAULT 0 ); CREATE TABLE category ( category_id BIGINT PRIMARY KEY, parent_id BIGINT DEFAULT NULL, category_name VARCHAR(50) NOT NULL, sort_order INT DEFAULT 0 ); CREATE TABLE product ( product_id BIGINT PRIMARY KEY, category_id BIGINT NOT NULL, product_name VARCHAR(200) NOT NULL, main_image VARCHAR(500), description TEXT, price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, is_on_sale TINYINT DEFAULT 1, create_time DATETIME ); CREATE TABLE cart_item ( cart_item_id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1, is_checked TINYINT DEFAULT 1, add_time DATETIME ); CREATE TABLE orders ( order_id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, address_id BIGINT, order_status TINYINT NOT NULL, order_time DATETIME, pay_time DATETIME, ship_time DATETIME, finish_time DATETIME, receiver_name VARCHAR(50), receiver_phone VARCHAR(20), receiver_address VARCHAR(200) ); CREATE TABLE order_item ( item_id BIGINT PRIMARY KEY, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, product_name_snapshot VARCHAR(200), product_image_snapshot VARCHAR(500), deal_price DECIMAL(10,2), quantity INT ); CREATE TABLE payment ( payment_id BIGINT PRIMARY KEY, order_id BIGINT NOT NULL, user_id BIGINT NOT NULL, pay_method VARCHAR(20), pay_amount DECIMAL(10,2), pay_time DATETIME, pay_status TINYINT ); CREATE TABLE review ( review_id BIGINT PRIMARY KEY, order_item_id BIGINT NOT NULL, user_id BIGINT NOT NULL, product_id BIGINT NOT NULL, rating TINYINT NOT NULL, content TEXT, images VARCHAR(1000), review_time DATETIME );

有用的细节我多两句:订单表里的receiver_name等字段就是地址“快照”,这就是我在3.2里强调的“历史现场保留”的落地写法。“商品-订单”的多对多关系没有直接体现在某一张表里,但order_item表同时持有order_id和product_id,中间表的效果就出来了。

4. 常见问题与排查技巧实录

4.1 实体找多了或找漏了怎么办

练E-R图,最经典的翻车现场就是实体数量不对。有些同学恨不得把“手机号”也画成实体,有些则把“收货地址”直接揉进“用户”实体里了事。前者是找多了,后者是找少了。

找多了的本质是混淆了属性和实体。记住根源性判断方法:如果某信息离开所属者之后没有独立存在的业务意义,那它就是属性,不是实体。比如“手机号”不可能脱离用户单独出现在业务里,所以它是用户属性;“收货地址”可以脱离用户存在吗?不行,但它有多个“实例”,而且订单要引用历史地址现场,所以它成了实体。

找漏了则相反,往往是把“联系属性”藏在了实体属性里。典型错误就是订单表里加一列“商品名”——一看到这种设计,多半就是漏掉了“订单明细”这个表/实体。排查方法从我自己的经验看很有效:画完图后,对着每个菱形问一句:这个联系本身需要记录哪些信息才能支撑业务?如果有信息只能挂在联系上,那你就该新建一个实体(如订单明细、选课成绩表)来承接它。

4.2 多对多联系要不要拆成关联表

这是E-R图转关系模式时绕不开的决策。我的回答非常明确:必须拆,而且拆出来的中间表通常会“升级”成一个正式的实体

还是拿“商品”和“订单”举例。表面上看,它们是多对多,拆出“订单明细”就够了。但你再仔细想,“订单明细”有没有自己的属性?有,数量、成交价、评价状态。当它有了自己的属性之后,它就不再是那个卑微的“中间表”了,而是一个有主键、有业务含义、有独立查询需求的实体了。

同理,“用户”和“商品”之间也是多对多(一个用户买多种商品,一种商品被多个用户买),如果不加约束,你会拆出“购买记录”表。但如果你把“购物车”加进来,“用户-购物车-商品”就拆成了两条1:N,购物车项成了独立实体。所以拆不拆、怎么拆,取决于业务上这个关联本身是否承载额外的信息。承载了,就升格为实体;只是表达关系,就建个轻量中间表。这个判断能力,就是设计经验的体现。

4.3 主键怎么选:自增、业务字段还是唯一标识

E-R图上给定主键很简单,但落到真实系统里,选错主键会坑死人。三个原则分享给大家。

第一,能用自增ID就用自增ID,别拿业务字段硬扛。有同学喜欢拿手机号当用户ID,很爽,但哪天用户注销换号,或者同一手机号注册两个账号,全表外键、日志、流水全乱了。业务字段天然带变数,当唯一标识不稳定。

第二,多对多中间表不要额外搞一个自增ID,用复合主键更合适。比如订单明细表,用(order_id, product_id)做联合主键,天然锁定了“一个订单里同一商品只出现一次”的业务规则。你要是多加一个自增ID,反而让程序猿有机会往里插两行相同商品,脏数据就是这么来的。

第三,分布式场景别用自增ID,用雪花ID或UUID。这个对课程设计来说可能超纲了,但面试问到“表很大要分库分表”的时候,你能答出“自增ID在分布式下无法全局唯一,所以用雪花算法”,会是明显的加分项。E-R图练习本身不涉及这些,但主键思路上提前有这个意识,设计出来的表会健壮很多。

4.4 E-R图自查清单:画完对照一遍

画完E-R图千万别急着提交,先按我的清单过一遍:

  1. 每个实体都有主键吗?主键业务语义是否纯净?
  2. 每个联系的两端都标注了基数(1、N、M)吗?很多图不标基数,看了等于白看。
  3. 所有M:N联系都计划好中间表了吗?
  4. 联系属性有没有归宿?比如数量、成绩、时间这类,有地方落脚吗?
  5. 有没有信息被重复存储且无法解释的?比如订单表和商品表都存了单价——如果是有意做快照,就能解释;如果是无意冗余,就是设计缺陷。
  6. 自关联或层级结构处理了吗?比如商品分类的两级关系。
  7. 从用户角度走一遍核心流程,每个环节是否都有对应的实体和联系?

这套清单我面试的时候也爱让候选人当场对着自己的图走一遍。能走通的人,说明图是真懂了;走不通的人,基本就是背了模板,换个业务场景马上露馅。

E-R图的练习,本质上练的不是画图,而是抽象思维能力。把一团乱麻的业务需求,拆成一个个清晰的实体、一组组明确的关系,这个能力在任何数据相关的岗位上都是硬通货。我自己带项目这么多年,最深的体会就是:凡是前期花时间把E-R图琢磨透的项目,后面建表、写接口、出报表都顺风顺水;凡是图省事跳过这一步的项目,后面无一例外都要返工。

最后再分享一个练习方法。别光在网上搜现成的E-R图看,那跟看别人健身视频一样,看了不会长肌肉。找个你熟悉的场景,比如图书馆借书、医院挂号、学校的选课系统,关掉教程,自己从需求分析开始,一步步画出实体、属性和联系,再转成建表SQL。画完找懂行的朋友帮你过一遍,或者对照自查清单检查一遍。练上五六个案例,你再看任何业务需求,脑子里会自动浮现出一张表关系图,那种感觉,就是真的入门了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询