CategoryResourceRepost/极客时间专栏/MySQL 必知必会/优化篇/23 | ER模型:如何理清数据库设计思路?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

419 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="23 | ER模型如何理清数据库设计思路" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fe/18/fe451ec65fcc4317a9c270ca686d9618.mp3"></audio>
你好,我是朱晓峰。
在超市项目的数据库设计阶段超市经营者交给我们一大堆Excel表格。我们需要把这些表格的数据都整理清楚并且按照一定的规则存储起来从而进行高效的管理。
比如,当时我们有这样一张进货表:
<img src="https://static001.geekbang.org/resource/image/b8/7c/b8c9688a5c3d9493840b8f17bf98567c.jpeg" alt="">
为了提高数据存储的效率我们按照第三范式的原则进行拆分这样就得到了4个表分别是供货商表、进货单头表、进货单明细表和商品信息表。
供货商表:
<img src="https://static001.geekbang.org/resource/image/0b/1b/0bea0395a5b596e695178a53fe69ef1b.jpeg" alt="">
进货单头表:
<img src="https://static001.geekbang.org/resource/image/ab/0f/ab5e18a0eafc78955096621a6bc34e0f.jpeg" alt="">
进货单明细表:
<img src="https://static001.geekbang.org/resource/image/44/3e/442cb5a11984011168c733009f57963e.jpeg" alt="">
商品信息表:
<img src="https://static001.geekbang.org/resource/image/39/f5/39d06d2068f111c24021f617db2795f5.jpeg" alt="">
其中,商品信息表、供货商表和进货单头表都满足第三范式的原则,进货单明细表虽然不满足第三范式的原则,但是满足第二范式的要求,而且保留的冗余字段也是基于业务优先的原则保留的。因此,超市经营者给我们提供的进货单表,经过我们的拆解,已经是存取效率最佳的方案了。在进货管理这个局部模块中,是最优的数据库设计方案。
但是,当我们按照这样的方式拆分一连串数据表时,却发现越拆越多,而且支离破碎。事实上,**局部最优的表,不仅有可能存在进一步拆分的情况,还有可能会出现数据缺失**。
毕竟,数据库设计是牵一发而动全身的。那有没有什么办法提前看到数据库的全貌呢?比如需要哪些数据表、数据表中应该有哪些字段,数据表与数据表之间有什么关系、通过什么字段进行连接,等等。这样我们才能进行整体的梳理和设计。
其实ER模型就是一个这样的工具。ER模型也叫作实体关系模型是用来描述现实生活中客观存在的事物、事物的属性以及事物之间关系的一种数据模型。**在开发基于数据库的信息系统的设计阶段通常使用ER模型来描述信息需求和信息特性帮助我们理清业务逻辑从而设计出优秀的数据库**。
今天我还是借助实际案例带你使用ER模型分析一下超市的业务流程具体给你讲一讲怎么通过ER模型来理清数据库设计的思路从而设计出优秀的数据库。
在使用之前咱们得先知道ER模型里都包括啥。
# ER模型包括哪些要素
**在ER模型里面有三个要素分别是实体、属性和关系**
- 实体可以看做是数据对象往往对应于现实生活中的真实存在的个体。比如这个连锁超市就可以看做一个实体。在ER模型中用矩形来表示。实体分为两类分别是**强实体和弱实体。**强实体是指不依赖于其他实体的实体;弱实体是指对另一个实体有很强的依赖关系的实体。
- 属性则是指实体的特性。比如超市的地址、联系电话、员工数等。在ER模型中用椭圆形来表示。
- 关系则是指实体之间的联系。比如超市把商品卖给顾客就是一种超市与顾客之间的联系。在ER模型中用菱形来表示。
需要注意的是,有的时候,实体和属性不容易区分。比如刚刚商品信息表中的商品的单位,到底是实体还是属性呢?如果从进货的角度出发,单位是商品的属性,但是从超市信息系统的整体出发,单位可以看做一个实体。
**那么,该如何区分实体和属性呢?**
我给你提供一个原则:我们要从系统整体的角度出发去看,**可以独立存在的是实体,不可再分的是属性**。也就是说,属性不需要进一步描述,不能包含其他属性。
在ER模型的3个要素中关系又可以分为3种类型分别是**1对1、1对多和多对多**。
1. 1对1指实体之间的关系是一一对应的比如个人与身份证信息之间的关系就是1对1的关系。一个人只能有一个身份证信息一个身份证信息也只属于一个人。
1. 1对多指一边的实体通过关系可以对应多个另外一边的实体。相反另外一边的实体通过这个关系则只能对应唯一的一边的实体。比如超市与超市里的收款机之间的从属关系超市可以拥有多台收款机但是每一条收款机只能从属于一个超市。
1. 多对多:指关系两边的实体都可以通过关系对应多个对方的实体。比如在进货模块中,供货商与超市之间的关系就是多对多的关系,一个供货商可以给多个超市供货,一个超市也可以从多个供货商那里采购商品。
知道了这些要素我们就可以给超市业务创建ER模型了如下图所示
<img src="https://static001.geekbang.org/resource/image/b4/ba/b4d18c4ac6419de7c2acd69ae9b25bba.jpg" alt="">
我来简单解释一下这个图。
在这个图中供货商和超市之间的供货关系两边的数字都不是1表示多对多的关系。同样超市和顾客之间的零售关系也是多对多的关系。
这个ER模型包括了3个实体之间的2种关系
1. 超市从供货商那里采购商品;
1. 超市把商品卖给顾客。
有了这个ER模型我们就可以从整体上理解超市的业务了。但是这里没有包含属性这样就无法体现实体和关系的具体特征。现在我们需要把属性加上用椭圆来表示这样我们得到的ER模型就更加完整了。
# ER模型的细化
刚刚的ER模型展示了超市业务的框架但是只包括了供货商、超市和顾客这三个实体以及它们之间的关系还不能对应到具体的表以及表与表之间的关联。
因此我们需要进一步去设计一下这个ER模型的各个局部也就是细化下超市的具体业务流程然后把它们综合到一起形成一个完整的ER模型。这样可以帮助我们理清数据库的设计思路。
我们刚才的超市业务模型包括了两个模块分别是进货模块和销售模块。下面我们分别对这2个模块进行细化。
首先我们来看一下超市业务中的进货模块的ER模型整理一下其中包含哪些实体、哪些关系和哪些属性。
在我们的进货模块里有5个实体
1. 供货商
1. 商品
1. 门店
1. 仓库
1. 员工
其中,供货商、商品和门店是强实体,因为它们不需要依赖其他任何实体。而仓库和员工是弱实体,因为它们虽然都可以独立存在,但是它们都依赖门店这个实体,因此都是弱实体。
接下来,我们再分析一下各个实体都有哪些属性。
- 供货商:名称、地址、电话、联系人。
- 商品:条码、名称、规格、单位、价格。
- 门店:编号、地址、电话、联系人。
- 仓库:编号、名称。
- 员工:工号、姓名、住址、电话、身份证号、职位。
这样细分之后我们就可以重新设计进货模块了ER模型如下
<img src="https://static001.geekbang.org/resource/image/11/bb/113e674350171792fd7ceb3cd3bc41bb.jpg" alt="">
需要注意的是,这里我是用粗框矩形表示弱实体,用粗框菱形,表示弱实体与它依赖的强实体之间的关系。
第二步,我们再分析一下零售模块。
经过分析,我们发现,在超市的业务流程中,零售业务包括普通零售和会员零售两种模式。普通零售包含的实体,包括门店、商品和收银款台;会员零售包含的实体,包括门店、商品、会员和收银款台。
这样我们就可以提炼出零售业务模块中的实体:
1. 商品
1. 门店
1. 会员
1. 收银款台
其中,商品和门店不依赖于任何其他实体,所以是强实体;会员和收银款台都依赖于门店,所以是弱实体。
有了实体之后,我们就可以确定实体的属性了。
- 商品:条码、名称、规格、单位、价格。
- 会员:卡号、发卡门店、名称、电话、身份证、地址、积分、储值。
- 门店:编号、地址、电话、联系人。
- 收银款台:编号、名称。
现在我们就可以重新设计零售模块的ER模型了
<img src="https://static001.geekbang.org/resource/image/69/17/69ce5eb80e09b77ce13c5ea5bfac2b17.jpg" alt="">
现在我们把这两个图整合到一起形成一个完整的ER模型
<img src="https://static001.geekbang.org/resource/image/e8/85/e8684d383db36a08e137dc2b8f2fb685.jpg" alt="">
# 如何把ER模型图转换成数据表
通过绘制ER模型我们已经理清了业务逻辑现在我们就要进行非常重要的一步了**把绘制好的ER模型转换成具体的数据表。**
**我来介绍下转换的原则**
1. 一个实体通常转换成一个数据表;
1. 一个多对多的关系,通常也转换成一个数据表;
1. 一个1对1或者1对多的关系往往通过表的外键来表达而不是设计一个新的数据表
1. 属性转换成表的字段。
好了下面我就结合前面的表格给你具体讲解一下怎么运用这些转换的原则把ER模型转换成具体的数据表从而把抽象出来的数据模型落实到具体的数据库设计当中。
## 一个实体转换成一个数据表
先来看一下强实体转换成数据表。
供货商实体转换成供货商表demo.supplier的代码如下所示
```
mysql&gt; CREATE TABLE demo.supplier
-&gt; (
-&gt; -- 我们给它添加一个与业务无关的字段“supplierid”为主键并且设置自增约束。
-&gt; supplierid INT PRIMARY KEY AUTO_INCREMENT,
-&gt; suppliername TEXT,
-&gt; address TEXT,
-&gt; phone TEXT
-&gt; );
Query OK, 0 rows affected (0.06 sec)
```
商品实体转换成商品表demo.goodsmaster
```
mysql&gt; CREATE TABLE demo.goodsmaster
-&gt; (
-&gt; --我们给商品信息表添加一个与业务无关的字段“itemnumber”为主键采用手动赋值的方式原因是可能存在多个门店录入新品导致冲突的情况
-&gt; itemnumber INT PRIMARY KEY,
-&gt; barcode TEXT,
-&gt; goodsname TEXT,
-&gt; specification TEXT,
-&gt; unit TEXT,
-&gt; salesprice DECIMAL(10,2)
-&gt; );
Query OK, 0 rows affected (0.07 sec)
```
门店实体转换成门店表demo.branch
```
mysql&gt; CREATE TABLE demo.branch
-&gt; (
-&gt; -- 增加一个与业务无关的字段为主键,并且设置自增约束
-&gt; branchid INT PRIMARY KEY AUTO_INCREMENT,
-&gt; branchno TEXT,
-&gt; branchname TEXT,
-&gt; address TEXT,
-&gt; phone TEXT,
-&gt; contacter TEXT
-&gt; );
Query OK, 0 rows affected (0.06 sec)
```
下面我们再把弱实体转换成数据表。
仓库转换成仓库表demo.stock
```
mysql&gt; CREATE TABLE demo.stock
-&gt; (
-&gt; --添加与业务无关的自增约束字段为主键
-&gt; stockid INT PRIMARY KEY AUTO_INCREMENT,
-&gt; -- 仓库是弱实体,依赖于强实体门店表,所以要把门店表的主键字段包括进来,作为与门店表关联的外键
-&gt; branchid INT NOT NULL,
-&gt; stockno TEXT NOT NULL,
-&gt; stockname TEXT NOT NULL,
-&gt; -- 设置外键约束,与门店表关联
-&gt; CONSTRAINT fk_stock_branch FOREIGN KEY (branchid) REFERENCES branch (branchid)
-&gt; );
Query OK, 0 rows affected (0.07 sec)
```
收银款台实体转换成收银款台表demo.cashier
```
mysql&gt; CREATE TABLE demo.cashier
-&gt; (
-&gt; -- 添加与业务无关的自增字段为主键
-&gt; cashierid INT PRIMARY KEY AUTO_INCREMENT,
-&gt; -- 收银款台是弱实体,依赖于强实体门店表,所以要把门店表的主键字段包括进来,所为与门店表关联的外键
-&gt; branchid INT NOT NULL,
-&gt; cashierno TEXT NOT NULL,
-&gt; cashiername TEXT NOT NULL,
-&gt; -- 设置外键约束,与门店表关联
-&gt; CONSTRAINT fk_cashier_branch FOREIGN KEY (branchid) REFERENCES branch (branchid)
-&gt; );
Query OK, 0 rows affected (0.07 sec)
```
员工实体转换成员工表demo.operator
```
mysql&gt; CREATE TABLE demo.operator
-&gt; (
-&gt; -- 添加与业务无关的自增字段为主键
-&gt; operatorid INT PRIMARY KEY AUTO_INCREMENT,
-&gt; -- 员工是弱实体,依赖于强实体门店表,所以要把门店表的主键字段包括进来,所为与门店表关联的外键
-&gt; branchid INT NOT NULL,
-&gt; workno TEXT NOT NULL,
-&gt; operatorname TEXT NOT NULL,
-&gt; phone TEXT,
-&gt; address TEXT,
-&gt; pid TEXT,
-&gt; duty TEXT,
-&gt; -- 设置外键约束,与门店表关联
-&gt; CONSTRAINT fk_operator_branch FOREIGN KEY (branchid) REFERENCES branch (branchid)
-&gt; );
Query OK, 0 rows affected (0.11 sec)
```
会员实体转换成会员表demo.membermaster
```
mysql&gt; CREATE TABLE demo.membermaster
-&gt; (
-&gt; -- 添加与业务无关的自增字段为主键
-&gt; memberid INT PRIMARY KEY,
-&gt; -- 会员是弱实体,依赖于强实体门店表,所以要把门店表的主键字段包括进来,所为与门店表关联的外键
-&gt; branchid INT NOT NULL,
-&gt; cardno TEXT NOT NULL,
-&gt; membername TEXT,
-&gt; address TEXT,
-&gt; phone TEXT,
-&gt; pid TEXT,
-&gt; -- 设置默认约束积分默认为0
-&gt; memberpoints DECIMAL(10,1) NOT NULL DEFAULT 0,
-&gt; -- 设置默认约束储值默认为0
-&gt; memberdeposit DECIMAL(10,2) NOT NULL DEFAULT 0,
-&gt; -- 设置外键约束,与门店表关联
-&gt; CONSTRAINT fk_member_branch FOREIGN KEY (branchid) REFERENCES branch (branchid)
-&gt; );
Query OK, 0 rows affected (0.07 sec)
```
## 一个多对多的关系转换成一个数据表
这个ER模型中的多对多的关系有2个分别是零售和进货。我们分别设计一个独立的表来表示这种表一般称为中间表。
我们可以设计一个独立的进货单表来代表进货关系。这个表关联到4个实体分别是供货商、商品、仓库、员工所以表中必须要包括这4个实体转换成的表的主键。除此之外我们还要包括进货关系自有的属性进货时间、供货数量和进货价格。
按照数据表设计的第三范式的要求和业务优先的原则我们把这个进货单表拆分成2个表分别是进货单头表和进货单明细表
```
CREATE TABLE demo.importhead
(
importid INT PRIMARY KEY, -- 添加与业务无关的字段为主键
listnumber TEXT NOT NULL,
supplierid INT NOT NULL, -- 供货商表的主键,反映了参与进货关系的供货商信息
stockid INT NOT NULL, -- 仓库表的主键,反映了参与进货关系的仓库信息
operatorid INT NOT NULL, -- 员工表的主键,反映了参与进货关系的员工信息
recordingdate DATETIME NOT NULL,
totalquantity DECIMAL(10,3) NOT NULL DEFAULT 0,
totalvalue DECIMAL(10,3) NOT NULL DEFAULT 0,
CONSTRAINT fk_importhead_supplier FOREIGN KEY (supplierid) REFERENCES supplier (supplierid),
CONSTRAINT fk_importhead_stock FOREIGN KEY (stockid) REFERENCES stock (stockid),
CONSTRAINT fk_importhead_operator FOREIGN KEY (operatorid) REFERENCES operator (operatorid)
);
CREATE TABLE demo.importdetails
(
importid INT,
itemnumber INT, -- 商品表的主键,反映了参与进货关系的商品信息
importquantity DECIMAL(10,3) NOT NULL DEFAULT 0,
importprice DECIMAL(10,2) NOT NULL DEFAULT 0,
importvalue DECIMAL(10,2) NOT NULL DEFAULT 0,
PRIMARY KEY (importid,itemnumber),
CONSTRAINT fk_importdetails_goodsmaster FOREIGN KEY (itemnumber) REFERENCES goodsmaster (itemnumber)
);
```
对于零售关系,我们可以设计一张流水单来表示。
这个表关联4个实体分别是收银款台、商品、会员和员工。所以表中也必须要包括这4个实体转换成的表的主键。除此之外表中还要包括零售关系自有的属性交易时间、数量、价格等。
按照数据表设计的第三范式的要求我们把这个流水单表拆分成2个表分别是流水单头表和流水单明细表
```
CREATE TABLE demo.transactionhead
(
transactionid INT PRIMARY KEY, -- 添加与业务无关的字段为主键
transactionno TEXT NOT NULL,
cashierid INT NOT NULL, -- 收款机表的主键,反映了参与零售关系的收款机信息
memberid INT, -- 会员表的主键,反映了参与零售关系的会员的信息
operatorid INT NOT NULL, -- 员工表的主键,反映了参与零售关系的员工信息
transdate DATETIME NOT NULL,
CONSTRAINT fk_transactionhead_cashier FOREIGN KEY (cashierid) REFERENCES cashier (cashierid),
CONSTRAINT fk_transactionhead_member FOREIGN KEY (memberid) REFERENCES member (memberid),
CONSTRAINT fk_transactionhead_operator FOREIGN KEY (operatorid) REFERENCES operator (operatorid)
);
CREATE TABLE demo.transactiondetails
(
transactionid INT,
itemnumber INT, -- 商品表的主键,反映了参与零售关系的商品信息
quantity DECIMAL(10,3) NOT NULL DEFAULT 0,
price DECIMAL(10,2) NOT NULL DEFAULT 0,
salesvalue DECIMAL(10,2) NOT NULL DEFAULT 0,
PRIMARY KEY (transactionid,itemnumber),
CONSTRAINT fk_transactiondetails_goodsmaster FOREIGN KEY (itemnumber) REFERENCES goodsmaster (itemnumber)
);
```
## 通过外键来表达1对多的关系
在上面的表的设计中我们已经完成了用外键来表达1对多的关系。比如在流水单头表中我们分别把cashierid、memberid和operatorid定义成了外键
```
CONSTRAINT fk_transactionhead_cashier FOREIGN KEY (cashierid) REFERENCES cashier (cashierid),
CONSTRAINT fk_transactionhead_member FOREIGN KEY (memberid) REFERENCES member (memberid),
CONSTRAINT fk_transactionhead_operator FOREIGN KEY (operatorid) REFERENCES operator (operatorid)
```
在流水单明细表中,我们把商品编号定义成了外键:
```
CONSTRAINT fk_transactiondetails_goodsmaster FOREIGN KEY (itemnumber) REFERENCES goodsmaster (itemnumber)
```
## 把属性转换成表的字段
在刚刚的设计中,我们也完成了把属性都转换成了表的字段,比如把商品属性(包括条码、名称、规格、单位、价格)转换成了商品信息表中的字段:
```
mysql&gt; CREATE TABLE demo.goodsmaster
-&gt; (
-&gt; --我们给商品信息表添加一个与业务无关的字段“itemnumber”为主键采用手动赋值的方式原因是可能存在多个门店录入新品导致冲突的情况
-&gt; itemnumber INT PRIMARY KEY,
-&gt; barcode TEXT, -- 条码属性
-&gt; goodsname TEXT, -- 名称属性
-&gt; specification TEXT, -- 规格属性
-&gt; unit TEXT, -- 单位属性
-&gt; salesprice DECIMAL(10,2) -- 价格属性
-&gt; );
Query OK, 0 rows affected (0.07 sec)
```
这样我们就完成了ER模型到MySQL数据表的转换。
到这里我们通过创建超市项目业务流程的ER模型再把ER模型转换成具体的数据表的过程完成了利用ER模型设计超市项目数据库的工作。
其实任何一个基于数据库的应用项目都可以通过这种先建立ER模型再转换成数据表的方式完成数据库的设计工作。ER模型是一种工具。创建ER模型不是目的目的是把业务逻辑梳理清楚设计出优秀的数据库。我建议你不是为了建模而建模要利用创建ER模型的过程来整理思路这样创建ER模型才有意义。
# 总结
今天我们学习了通过绘制ER模型图理清业务逻辑以及怎么把ER模型转换成MySQL数据表最终完成项目数据库设计。
这节课的知识点比较多,我用一张图来帮你回顾下重点。
<img src="https://static001.geekbang.org/resource/image/38/7d/38278a13445a597fe931e9f26852477d.jpg" alt="">
最后我还想再提醒你一下ER模型看起来比较麻烦但是对我们把控项目整体非常重要。如果你只是开发一个小应用或许简单设计几个表够用了一旦要设计有一定规模的应用在项目的初始阶段建立完整的ER模型就非常关键了。开发应用项目的实质其实就是建模。胸中有丘壑才能下笔如有神。道理其实是一样的。
# 思考题
超市经营者每个月都要进行库房盘点,也就是在一个月的最后一天的营业结束之后,所有的员工一起把库房里的货品都数一遍,然后跟电脑上的库存比对,查看库存损耗。
我想请你思考一下在这个业务模块中涉及了哪些实体、属性和关系另外请你设计一下ER模型通过它来整理一下数据库设计的思路。
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你把它分享给你的朋友或同事,我们下节课见。