This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,285 @@
<audio id="audio" title="27 | 手把手带你设计一个完整的连锁超市信息系统数据库(上)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/73/88/73efa8a13f586f9c03e34cc814e3f288.mp3"></audio>
你好,我是朱晓峰。
从创建第一个MySQL数据库开始到现在我们已经学完了MySQL的核心操作。最后这两节课我想带你实际设计一个超市信息系统的数据库。毕竟设计数据库很考验我们综合运用单个技术点的能力。所以通过这个项目我不仅会带你把前面的内容串联起来而且还会教你设计数据库的通用思路。
为什么选择超市项目呢?一方面呢,超市的场景与我们的日常生活密切相关,你比较容易理解其中的业务逻辑,另一方面,超市的业务又相当复杂,几乎能用到我们学到的所有知识点,利于我们对前面学过的内容进行整合。
今天我就带你一起从需求分析开始入手一直到容灾和备份完成一个全流程的连锁超市数据库设计。这节课我会主要给你讲解需求分析、ER模型、分库分表和数据库设计方案。我们做项目首先要从大处着眼把握方向这样才不容易出现大的偏差。下节课我们再设计具体的细节比如创建数据表、外键约束设计灾备方案和备份数据等。
在开始设计之前,咱们得先了解一下项目背景。
随着互联网使用的日益广泛,传统的桌面管理系统的数据不能互通、资源利用率低等弊端越来越明显,显然不能满足用户的需求了。因此,我们需要开发一款基于云服务的连锁超市管理信息系统。具体的要求是:
1. 基于浏览器,无需安装,账号开通即可使用,方便快捷;
1. 数据部署在云端,由运营商负责维护,安全可靠;
1. 用户无需自备服务器,只需租用信息服务,资源利用率高。
知道了具体要求,那该怎么进行设计呢?下面我带你来分析一下。
## 如何设计数据结构?
用户账号开通即可使用所以必然要设计分层的数据结构数据要部署在云端所以必然要使用云。根据这些要求我们可以设计一个基于云服务的2层数据结构这个结构的示意图如下所示
<img src="https://static001.geekbang.org/resource/image/38/9d/38e25fa9d5e2b449yy498a60ef874e9d.jpg" alt="">
我来解释一下图中展示的内容。
首先,你可以看到,所有的系统资源和服务都部署在云端。
其次我们来看下数据结构层面主要有2层。
1. 第一层是商户。每个入驻的商户都有一个组号,所有与这个商户有关的数据,通过这个组号进行识别。
1. 第二层是分支机构。分支机构从属于商户,相同商户的分支机构有相同的组号。分支机构分为几种,包括总部、门店、配送中心等。门店又分为直营店和加盟店。每个分支机构有一个机构编号,同一分支机构的数据,有相同的组号和机构编号。
这样一来,新商户只需要开通账号,分配一个新的组号,就可以使用了。组号用于隔离商户之间的数据,使商户之间互相不影响。
最后,数据由我们统一进行运维,安全性有保障。商户自己不需要采购服务器,只需租用服务,资源的利用率更高。
系统的整体结构设计思路有了,那具体在应用层面如何实现呢?
我先用一张图来展示具体的应用构成:
<img src="https://static001.geekbang.org/resource/image/59/4c/5985a0c712296421bf7b36d50e4d2b4c.jpg" alt="">
这个图展示了应用的3个层级。
1. 展现层包括门店收款机App、移动端的手机App、小程序以及通过浏览器访问的后台管理部分。
1. 服务层:包括云端的销售模块、库存模块、营运模块、会员模块等。
1. 数据层MySQL数据库。
门店收款App、移动端的手机App小程序等与数据库设计无关我就不多说了。下面我重点介绍一下后台管理部分下面的服务层和数据层的相关内容。
服务层包括了销售、库存、营运、会员等管理模块。下面我就以库存管理中的盘点模块为例,详细介绍一下。因为这个模块比较简单,容易理解。
盘点,简单来说,就是把超市仓库里的商品都数一遍,然后跟电脑里的库存数比对,看看有没有不对的地方。实际数出来的库存数量叫做盘存数量,电脑中的库存数量叫做结存数量,比对的结果叫做盈亏数量。要是盘存数量比结存数量多,叫盘盈,否则叫做盘亏。
盘点操作是超市库存管理模块中的一个重要环节,是掌握实际库存的唯一办法。盘点盈亏数据也是衡量超市管理水平的重要指标。
盘点作业一般都在晚上门店营业结束以后进行。这也很好理解,毕竟,在白天营业的过程中,商品不断被顾客取走,又不断得到补充,库存处于一种变化状态,无法获取准确数据。
下面我来介绍下盘点的步骤。
1. 先生成一张盘点表,把当前系统中的库存数量提取出来,获得结存数量;
1. 把员工实际数出来的库存数据录入盘点表中,获得盘存数量;
1. 计算盈亏数量,公式是“盈亏数量=盘存数量-结存数量”;
1. 数据确认无误后,验收盘点表,并调整当前库存:库存数量=库存数量+盈亏数量。
经过这些操作,系统中的库存数量与实际的库存数量就一致了,盘点盈亏也被记录下来了,体现在日报等报表中,超市经营者可以进行查看,为经营决策提供依据。
介绍完了盘点业务,现在回到数据库设计的主题上来,看看如何把盘点业务用数据表的形式表现出来。
盘点业务都是在门店进行由员工实际操作对仓库中的商品进行清点。因此盘点业务模块中肯定要包含员工、门店、仓库、商品等实体。这个时候我们就可以使用ER模型这个工具来理清盘点模块的业务逻辑。
## 盘点模块的ER模型
我先把模型直接展示给你,一会儿我再带你具体分析一下。
<img src="https://static001.geekbang.org/resource/image/c2/93/c2a44370af7ba1538169bf82e78c5993.jpg" alt="">
首先,我们来分析下模型中的实体和关系。
这个ER模型中包括了5个实体分别是
1. 商户
1. 门店
1. 员工
1. 商品
1. 仓库
其中,商户和商品是强实体,门店、仓库和员工是弱实体。
这个ER模型中还包含了5个关系我们按照1对多和多对多来分下类。
1对多
1. 商户与门店的从属关系
1. 门店与员工的雇佣关系
1. 门店与仓库的拥有关系
多对多:
1. 仓库与商品的库存关系
1. 仓库、商品和员工参与的盘点关系
接下来我们再分析一下这5个实体各自的属性。
1. 商户:组号、名称、地址、电话、联系人。
1. 门店:组号、门店编号、名称、地址、电话、类别。
1. 员工:组号、门店编号、工号、名称、身份证、电话、职责。
1. 仓库:组号、门店编号、仓库编号、类别。
1. 商品:组号、条码、名称、规格、单位、价格。
除此之外还有2个多对多关系的属性。
1. 仓库与商品的库存关系:库存数量。
1. 仓库、商品和员工参与的盘点关系:盘存数量、结存数量和盈亏数量。
通过建立ER模型我们理清了业务逻辑。接下来我们就可以把盘点业务中这些实体和关系落实到实际的数据表了。
### ER模型转换成数据表
你还记得在[第23讲](https://time.geekbang.org/column/article/369434)里学的转换规则吗强实体和弱实体转换成独立的数据表多对多的关系转换成独立的数据表1对多的关系转换成外键约束。
首先,我们把强实体转换成独立的数据表。
商户表demo.enterprice
<img src="https://static001.geekbang.org/resource/image/bf/8f/bf5228f8513beb3bd9ab61747c8d5c8f.jpeg" alt="">
商品信息表demo.goodsmaster
<img src="https://static001.geekbang.org/resource/image/65/a5/65d72ff47b7210968026ee4eba33fba5.jpeg" alt="">
接着,我们把弱实体转换成独立的数据表。
门店表demo.branch
<img src="https://static001.geekbang.org/resource/image/89/31/8908eaa15ba0ef46f6a5346005087631.jpeg" alt="">
员工表demo.employee
<img src="https://static001.geekbang.org/resource/image/54/87/54d3662c47d68443c468884099d6aa87.jpeg" alt="">
仓库表demo.stockmaster
<img src="https://static001.geekbang.org/resource/image/44/a2/448b405b670498b70b4a088214f932a2.jpg" alt="">
第三步,把多对多的关系转换成独立的数据表。
库存表demo.inventory
<img src="https://static001.geekbang.org/resource/image/b0/0d/b0dd7aff562eccb063ee81f44981f80d.jpeg" alt="">
盘点关系可以转换成2个表分别是盘点单头表和盘点单明细表这样做是为了满足第三范式的要求防止冗余。
盘点单头表demo.invcounthead
<img src="https://static001.geekbang.org/resource/image/68/ab/6884c011854524ae68898109eb2fccab.jpeg" alt="">
盘点单明细表demo.invcountdetails
<img src="https://static001.geekbang.org/resource/image/0f/ba/0f357a14eed20a91980fe5b4a36f92ba.jpeg" alt="">
这样一来我们就把ER模型中的实体和多对多的关系转换成了独立的数据表。
这里你要注意的是我在盘点单明细表中保留了组号和门店编号。这是因为虽然这2个字段是冗余数据但是可以加快查询的速度经过权衡利弊最后决定还是加上。
在我把1对多的关系转换成外键约束之前我们还要进行一项重要的工作分库分表。因为外键约束与数据表以及表中的字段有关分库分表会影响到表和表中的字段。
而且外键约束需要在创建数据表的时候创建,所以咱们下节课和创建数据表一起讲,这节课我们先学习下分库分表。
在前面的课程中,每节课我们都是以具体技术点为核心展开的。而分库分表,只有在进行数据库系统整体设计的阶段才会用到。所以,今天我就结合咱们这个项目的系统设计,来给你具体讲一讲如何进行分库分表。
为什么要分库分表呢当数据量足够大的时候即便我们把索引都建好系统资源调优到极致仍然有可能遇到运行缓慢、CPU使用率居高不下的情况。因为单个数据库中单个表的数据量高到一定程度超过了系统的承载能力。
面对这种情况我们有2种选择一种是购买更多的资源增加内存增加CPU的算力但是这样会增加系统的成本。这个时候我们就可以用另一种方法也就是接下来我要讲的分库分表。
## 如何进行分库分表?
所谓的分库分表,其实就是把大的数据库拆成小数据库,把大表拆成小表,让单一数据库、单一数据表的数据量变小。这样每次查询时,需要扫描的数据量减少了,也就达到了提升查询执行效率的目的。
分库分表又可以分成垂直分表、垂直分库、水平分库和水平分表。
### 垂直分表
**所谓垂直分表就是把一个有很多字段的表按照使用频率的不同拆分成2个或多个表**
为了帮助你理解,还是用我们的盘点模块中的表来演示说明一下。
每个商户都有一个自己的商品信息表数据量比较大。所以我们可以拆分下这个表。我们把经常使用的字段条码、名称和价格拆分成商品常用信息表demo.goods_o把剩下的字段也就是规格和单位拆分成商品不常用信息表demo.goods_f
商品常用信息表:
<img src="https://static001.geekbang.org/resource/image/0c/5d/0c4bea15a6339f79f000429f4065985d.jpeg" alt="">
商品不常用信息表:
<img src="https://static001.geekbang.org/resource/image/a4/36/a4e5260d7e28fbeabf632edf3510cb36.jpeg" alt="">
至于商户表、门店表、 员工表、仓库表,这些表的数据量有限,不需要拆分。库存表、盘点单头表和盘点单明细表,虽然数据量大,但是评估之后,我们发现字段的使用频率都很高,拆分的价值不大,所以也不需要拆分。
下面我再介绍一下什么是垂直分库。
### 垂直分库
**垂直分库的意思是,把不同模块的数据表分别存放到不同的数据库中**。这样做的好处是,每个数据库只保存特定模块的数据,系统只有用到特定模块的数据时,才会访问这个数据库。这样就减少了数据库访问的次数,就相当于是把数据访问的流量分散了。
这个可能不太好理解,我来画一个简单的示意图:
<img src="https://static001.geekbang.org/resource/image/44/43/44ea44e705c890cbcd93eac6ae6a1043.jpg" alt="">
在这个图中,数据不再存储在一个数据库中,而是根据业务模块的不同,分别存储在不同的数据库中,比如销售数据库、库存数据库、营运数据库、会员数据库等。这样一来,业务模块可以主要与自己的数据库进行数据交互。业务内的数据交互多了,业务与业务之间的数据交互就可以大大减少了。
这样做的好处主要有3个
1. 单个数据库的数据量减小了;
1. 单个数据库的访问流量分散了;
1. 系统整体的故障风险减小了。
下面我再介绍一下什么是水平分库和水平分表。
### 水平分库和水平分表
当垂直分表已经穷尽,垂直分库也不能再拆分的时候,我们还可以做水平分库和水平分表。
**水平分表的意思是,把数据表的内容,按照一定的规则拆分出去**
盘点数据会不断累积,数据量越来越大。为了提升系统效率,我们制定了水平分表的策略。
第一步,我们把盘点单头表和盘点单明细表水平拆分:把验收处理过的盘点单头表和盘点单明细表拆分到盘点单头历史表和盘点单明细历史表。
这样做的好处是盘点单头表和盘点单明细表经常需要进行插入、删除和修改操作只保留当前正在处理的数据可以提升效率避免在一个不断增长的大表中进行DML操作。
而盘点单头历史表和盘点单明细历史表中的数据虽然不断增长,但数据不会修改,只进行查询操作。用经常作为筛选条件的字段创建索引,可以大大加快查询的速度。
拆分出来的盘点单头历史表demo.invcountheadhist与盘点单头表类似不同之处是增加了验收人编号confirmer和验收日期confirmationdate
盘点单头表历史表:
<img src="https://static001.geekbang.org/resource/image/0a/90/0aa2010fcaabc0782f02c6d1823d0f90.jpeg" alt="">
拆分出来的盘点单明细历史表demo.invcountdetailshist的字段则与盘点单明细表一样。
盘点单明细历史表:
<img src="https://static001.geekbang.org/resource/image/51/e7/51e7b78c773310c54322071ac1aebfe7.jpeg" alt="">
第二步我们把组号大于500、小于1000的商户数据拆分到另外的数据表里进行保存。这里的数字是我们根据对入驻平台商户的数据量进行评估之后得出的在实际工作中你可以根据实际情况来决定。**原则是:确保单个数据表中的数据量适中,不会成为操作的瓶颈**。
这样,我们就完成了对盘点模块中数据表的水平拆分。
接下来,我们来进行水平分库。水平分库的目的是使单个数据库中的数据量不会太大。这样可以确保我们设计出来的数据库,在大数据环境下,也能高效运行。
**水平分库的意思与水平分表类似,就是按照一定的规则,<strong><strong>把数据库中的数据**</strong>拆分出去,保存在新的数据库当中</strong>
新的数据库可以在相同的服务器上也可以在不同的服务器上。比如我们可以把组号大于500、小于1000的用户数据拆分出来保存到新的服务器的数据库中。不过保存到新的服务器也就意味着增加系统的开销。
因此我们可以以500个商户为单位每500个商户在相同的服务器上创建一套新的数据库每5000个商户购置新的服务器。这样我们就完成了对数据库进行分库的设计。
## 总结
这节课,我们一起设计了一个基于云服务的连锁超市管理系统数据库。你要重点掌握如何进行需求分析、如何把分析的结果转换成数据库的设计,以及如何在总体设计的阶段通过使用分库分表使设计出来的数据库能够处理大量数据。
在实际项目中你要重点关注3个方面。
- 第一,要充分理解项目需求。有的时候,客户自己也不清楚自己的需求。这个时候,就需要你帮助客户理清思路。你可以把客户的需求用图表等方式整理出来,再跟客户一起讨论。在这个阶段,投入较多的时间是值得的,如果等系统开发完成之后再改,成本就很高了。
- 第二使用ER模型工具来整理思路可以提高效率提高设计的质量。
- 第三,要充分考虑到系统投入运行之后的承载能力。如果有可能处理大量数据,就需要考虑分库分表的策略。
最后,还有几点你需要注意下。
1. 分库分表的策略,需要在设计阶段完成。如果缺乏整体分库分表的策略而匆忙上线,遇到瓶颈时再解决,花费的成本要远远高于在设计阶段投入的时间成本。
1. 分库分表的策略,必然带来开发和运维方面成本的提升,因此,你需要在设计阶段就有一个整体规划。比如,在类的设计里面,用正则表达式计算访问的服务器、数据库和数据表的名称。
1. 分库分表一般适用于比较大的项目,如果你开发的应用数据量小,系统规模有限,团队成员不多,不如就用一个服务器、一个数据库。因为分库分表对数据量小的项目没有什么作用,却会大大提升开发的复杂度,增加开发、运维和项目管理的成本。所以,你要综合考虑利与弊。
## 思考题
假设我有一个数据库demo当中有一个商品流水表demo.trans示例如下
<img src="https://static001.geekbang.org/resource/image/1e/cc/1ecd89a9d71e7ayye43ac9e85b8e2bcc.jpg" alt="">
在设计阶段,如果预见到未来数据量会非常庞大,你会如何制定分库分表策略?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,725 @@
<audio id="audio" title="28 | 手把手带你设计一个完整的连锁超市信息系统数据库(下)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f4/91/f4c85c085e494f53dac71ca3105fc691.mp3"></audio>
你好,我是朱晓峰。
上节课,我们完成了项目的需求分析和业务流程的梳理,为设计数据库做好了准备工作,接下来我们就可以开始具体的设计了。所以,今天,我就带你来建库建表、创建外键约束、视图、存储过程和触发器,最后制定容灾和备份的策略,从而完成一个完整的连锁超市项目数据库的设计,帮助你提高设计高效可靠的数据库的能力。
首先,我们一起来创建数据库和数据表。
## 如何创建数据库和数据表?
经过上节课的分库分表操作我们把数据库按照业务模块拆分成了多个数据库。其中盘点模块中的数据表分别被拆分到了营运数据库operation和库存数据库inventory中。
下面我们就按照上节课的分库策略,分别创建营运数据库和库存数据库:
```
mysql&gt; CREATE DATABASE operation;
Query OK, 1 row affected (0.03 sec)
mysql&gt; CREATE DATABASE inventory;
Query OK, 1 row affected (0.02 sec)
```
接下来,我们来分别创建下这两个数据库中的表。
商户表、门店表、员工表、商品常用信息表和商品不常用信息表从属于营运数据库我们先把这5个表创建出来。
商户表operation.enterprice
```
mysql&gt; CREATE TABLE operation.enterprice
-&gt; (
-&gt; groupnumber SMALLINT PRIMARY KEY, -- 组号
-&gt; groupname VARCHAR(100) NOT NULL, -- 名称
-&gt; address TEXT NOT NULL, -- 地址
-&gt; phone VARCHAR(20) NOT NULL, -- 电话
-&gt; contactor VARCHAR(50) NOT NULL -- 联系人
-&gt; );
Query OK, 0 rows affected (0.05 sec)
```
门店表operation.branch
```
mysql&gt; CREATE TABLE operation.branch
-&gt; (
-&gt; branchid SMALLINT PRIMARY KEY, -- 门店编号
-&gt; groupnumber SMALLINT NOT NULL, -- 组号
-&gt; branchname VARCHAR(100) NOT NULL, -- 门店名称
-&gt; address TEXT NOT NULL, -- 地址
-&gt; phone VARCHAR(20) NOT NULL, -- 电话
-&gt; branchtype VARCHAR(20) NOT NULL, -- 门店类别
-&gt; CONSTRAINT fk_branch_enterprice FOREIGN KEY (groupnumber) REFERENCES operation.enterprice(groupnumber) -- 外键约束,组号是外键
-&gt; );
Query OK, 0 rows affected (0.07 sec)
```
员工表operation.employee
```
mysql&gt; CREATE TABLE operation.employee
-&gt; (
-&gt; employeeid SMALLINT PRIMARY KEY, -- 员工编号
-&gt; groupnumber SMALLINT NOT NULL, -- 组号
-&gt; branchid SMALLINT NOT NULL, -- 门店编号
-&gt; workno VARCHAR(20) NOT NULL, -- 工号
-&gt; employeename VARCHAR(100) NOT NULL, -- 员工名称
-&gt; pid VARCHAR(20) NOT NULL, -- 身份证
-&gt; address VARCHAR(100) NOT NULL, -- 地址
-&gt; phone VARCHAR(20) NOT NULL, -- 电话
-&gt; employeeduty VARCHAR(20) NOT NULL, -- 职责
-&gt; CONSTRAINT fk_employee_branch FOREIGN KEY (branchid) REFERENCES operation.branch(branchid)
-&gt; );
Query OK, 0 rows affected (0.07 sec)
```
商品常用信息表operation.goods_o
```
mysql&gt; CREATE TABLE operation.goods_o
-&gt; (
-&gt; itemnumber MEDIUMINT PRIMARY KEY, -- 商品编号
-&gt; groupnumber SMALLINT NOT NULL, -- 组号
-&gt; barcode VARCHAR(50) NOT NULL, -- 条码
-&gt; goodsname TEXT NOT NULL, -- 名称
-&gt; salesprice DECIMAL(10,2) NOT NULL, -- 售价
-&gt; PRIMARY KEY (groupnumber,itemnumber)-- 主键
-&gt; );
Query OK, 0 rows affected (0.06 sec)
```
商品不常用信息表operation.goods_f
```
mysql&gt; CREATE TABLE operation.goods_f
-&gt; (
-&gt; itemnumber MEDIUMINT PRIMARY KEY, -- 商品编号
-&gt; groupnumber SMALLINT NOT NULL, -- 组号
-&gt; specification TEXT NOT NULL, -- 规格
-&gt; unit VARCHAR(20) NOT NULL, -- 单位
-&gt; PRIMARY KEY (groupnumber,itemnumber) -- 主键
-&gt; );
Query OK, 0 rows affected (0.06 sec)
```
好了,现在我们来创建库存数据库中的表。仓库表、库存表、盘点单头表、盘点单明细表、盘点单头历史表和盘点单明细历史表,从属于库存数据库。
仓库表inventory.stockmaster
```
mysql&gt; CREATE TABLE inventory.stockmaster
-&gt; (
-&gt; stockid SMALLINT PRIMARY KEY, -- 仓库编号
-&gt; groupnumber SMALLINT NOT NULL, -- 组号
-&gt; branchid SMALLINT NOT NULL, -- 门店编号
-&gt; stockname VARCHAR(100) NOT NULL, -- 仓库名称
-&gt; stockkind VARCHAR(20) NOT NULL, -- 仓库种类
-&gt; CONSTRAINT fk_stock_branch FOREIGN KEY (branchid) REFERENCES operation.branch(branchid) -- 外键约束,门店编号是外键
-&gt; );
Query OK, 0 rows affected (0.07 sec)
```
库存表inventory.inventory
```
mysql&gt; CREATE TABLE inventory.inventory
-&gt; (
-&gt; id INT PRIMARY KEY AUTO_INCREMENT, -- 库存编号
-&gt; groupnumber SMALLINT NOT NULL, -- 组号
-&gt; branchid SMALLINT NOT NULL, -- 门店编号
-&gt; stockid SMALLINT NOT NULL, -- 仓库编号
-&gt; itemnumber MEDIUMINT NOT NULL, -- 商品编号
-&gt; itemquantity DECIMAL(10,3) NOT NULL -- 商品数量
-&gt; );
Query OK, 0 rows affected (0.06 sec)
```
盘点单头表inventory.invcounthead
```
mysql&gt; CREATE TABLE inventory.invcounthead
-&gt; (
-&gt; listnumber INT PRIMARY KEY, -- 单号
-&gt; groupnumber SMALLINT NOT NULL, -- 组号
-&gt; branchid SMALLINT NOT NULL, -- 门店编号
-&gt; stockid SMALLINT NOT NULL, -- 仓库编号
-&gt; recorder SMALLINT NOT NULL, -- 录入人编号
-&gt; recordingdate DATETIME NOT NULL -- 录入时间
-&gt; );
Query OK, 0 rows affected (0.07 sec)
```
盘点单明细表inventorry.invcountdetails
```
mysql&gt; CREATE TABLE inventory.invcountdetails
-&gt; (
-&gt; id INT PRIMARY KEY AUTO_INCREMENT, -- 明细编号
-&gt; listnumber INT NOT NULL, -- 单号
-&gt; groupnumber SMALLINT NOT NULL, -- 组号
-&gt; branchid SMALLINT NOT NULL, -- 门店编号
-&gt; stockid SMALLINT NOT NULL, -- 仓库编号
-&gt; itemnumber MEDIUMINT NOT NULL, -- 商品编号
-&gt; accquant DECIMAL(10,3) NOT NULL, -- 结存数量
-&gt; invquant DECIMAL(10,3) NOT NULL, -- 盘存数量
-&gt; plquant DECIMAL(10,3) NOT NULL -- 盈亏数量
-&gt; );
Query OK, 0 rows affected (0.07 sec)
```
盘点单头历史表inventory.invcountheadhist
```
mysql&gt; CREATE TABLE inventory.invcountheadhist
-&gt; (
-&gt; listnumber INT PRIMARY KEY, -- 单号
-&gt; groupnumber SMALLINT NOT NULL, -- 组号
-&gt; branchid SMALLINT NOT NULL, -- 门店编号
-&gt; stockid SMALLINT NOT NULL, -- 仓库编号
-&gt; recorder SMALLINT NOT NULL, -- 录入人编号
-&gt; recordingdate DATETIME NOT NULL, -- 录入时间
-&gt; confirmer SMALLINT NOT NULL, -- 验收人编号
-&gt; confirmationdate DATETIME NOT NULL -- 验收时间
-&gt; );
Query OK, 0 rows affected (0.10 sec)
```
盘点单明细历史表inventorry.invcountdetailshist
```
mysql&gt; CREATE TABLE inventory.invcountdetailshist
-&gt; (
-&gt; id INT PRIMARY KEY AUTO_INCREMENT, -- 明细编号
-&gt; listnumber INT NOT NULL, -- 单号
-&gt; groupnumber SMALLINT NOT NULL, -- 组号
-&gt; branchid SMALLINT NOT NULL, -- 门店编号
-&gt; stockid SMALLINT NOT NULL, -- 仓库编号
-&gt; itemnumber MEDIUMINT NOT NULL, -- 商品编号
-&gt; accquant DECIMAL(10,3) NOT NULL, -- 结存数量
-&gt; invquant DECIMAL(10,3) NOT NULL, -- 盘存数量
-&gt; plquant DECIMAL(10,3) NOT NULL -- 盈亏数量
-&gt; );
Query OK, 0 rows affected (1.62 sec)
```
至此,我们完成了创建数据库和数据表的工作。为了提高查询的速度,我们还要为数据表创建索引。下面我们就来实际操作一下。
### 如何创建索引?
索引对提升数据查询的效率作用最大,没有之一。我们创建索引的策略是:
1. 所有的数据表都必须创建索引;
1. 只要是有可能成为查询筛选条件的字段,都必须创建索引。
这样做的原因是当数据量特别大的时候如果没有索引一旦出现大并发没有索引的表很可能会成为访问的瓶颈。而且这个问题十分隐蔽不容易察觉系统也不会报错但是却会消耗大量的CPU资源导致系统事实上的崩溃。
在之前的操作中我们一共创建了11个数据表下面我们就来一一为这些表创建索引。
商户表中的组号字段,常被用于筛选条件。我们用商户表的组号字段为商户表创建索引,代码如下所示:
```
mysql&gt; CREATE INDEX index_enterprice_groupname ON operation.enterprice (groupname);
Query OK, 0 rows affected (0.06 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
门店表的组号字段也常被用作筛选条件,所以,我们用组号字段为门店表创建索引,代码如下所示:
```
mysql&gt; CREATE INDEX index_branch_groupnumber ON operation.branch (groupnumber);
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
除了组号字段,门店名称字段也常用在查询的筛选条件中,下面我们就用门店名称字段为门店表创建索引:
```
mysql&gt; CREATE INDEX index_branch_branchname ON operation.branch (branchname);
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
门店类别字段也是用作筛选条件的字段之一,我们可以用门店类别字段为门店表创建索引,如下所示:
```
mysql&gt; CREATE INDEX index_branch_branchtype ON operation.branch (branchtype);
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
在员工表中,组号、门店编号、身份证号、电话和职责字段常被用作查询的筛选条件,下面我们就分别用这几个字段为员工表创建索引。
第一步,用组号字段为员工表创建索引:
```
mysql&gt; CREATE INDEX index_employee_groupnumer ON operation.employee (groupnumber);
Query OK, 0 rows affected (0.06 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
第二步,用门店编号字段为员工表创建索引:
```
mysql&gt; CREATE INDEX index_employee_branchid ON operation.employee (branchid);
Query OK, 0 rows affected (0.07 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
第三步,用身份证字段为员工表创建索引:
```
mysql&gt; CREATE INDEX index_employee_pid ON operation.employee (pid);
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
第四步,用电话字段为员工表创建索引:
```
mysql&gt; CREATE INDEX index_employee_phone ON operation.employee (phone);
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
最后,我们用职责字段为员工表创建索引:
```
mysql&gt; CREATE INDEX index_employee_duty ON operation.employee (employeeduty);
Query OK, 0 rows affected (0.09 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
对于商品常用信息表operation.goods_o我们发现组号和售价字段常被用在查询筛选条件语句中所以我们分别用这两个字段为商品常用信息表创建索引。
首先,用组号字段为商品常用信息表创建索引,如下所示:
```
mysql&gt; CREATE INDEX index_goodso_groupnumber ON operation.goods_o (groupnumber);
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
然后,用价格字段为商品常用信息表创建索引,如下所示:
```
mysql&gt; CREATE INDEX index_goodso_salesprice ON operation.goods_o (salesprice);
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
对于商品不常用信息表,只有组号字段经常用在查询的筛选条件中,所以,我们只需要用组号字段为商品不常用信息表创建索引。代码如下:
```
mysql&gt; CREATE INDEX index_goodsf_groupnumber ON operation.goods_f (groupnumber);
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
到这里,我们就完成了为营运数据库中的表创建索引的工作,下面我们来为库存数据库中的表创建索引。
首先是仓库表。这个表中经常被用做筛选条件的字段,是组号和门店编号字段。
我们先用组号字段为仓库表创建索引,代码如下:
```
mysql&gt; CREATE INDEX index_stock_groupnumber ON inventory.stockmaster (groupnumber);
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
然后我们用门店编号字段为仓库表创建索引,代码如下:
```
mysql&gt; CREATE INDEX index_stock_branchid ON inventory.stockmaster (branchid);
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
接下来,我们为库存表创建索引。库存表中常用于筛选的字段有组号、门店编号和商品编号字段。
我们先用组号字段来创建索引,代码如下:
```
mysql&gt; CREATE INDEX index_inventory_groupnumber ON inventory.inventory (groupnumber);
Query OK, 0 rows affected (0.11 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
然后,我们用门店编号字段创建索引,代码如下:
```
mysql&gt; CREATE INDEX index_inventory_branchid ON inventory.inventory (branchid);
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
最后用商品编号字段创建索引,代码如下:
```
mysql&gt; CREATE INDEX index_inventory_itemnumber ON inventory.inventory (itemnumber);
Query OK, 0 rows affected (0.07 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
盘点单头表也需要创建索引,常用的筛选字段是门店编号。那么,我们就用门店编号为盘点单头表创建索引,代码如下:
```
mysql&gt; CREATE INDEX index_invcounthead_branchid ON inventory.invcounthead (branchid);
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
盘点单头明细表中常用于筛选的字段有门店编号和商品编号我们分别用这2个字段创建索引。
首先是用门店编号字段创建索引,代码如下:
```
mysql&gt; CREATE INDEX index_invcountdetails_branchid ON inventory.invcountdetails (branchid);
Query OK, 0 rows affected (0.08 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
然后是用商品编号字段创建索引,代码如下:
```
mysql&gt; CREATE INDEX index_invcountdetails_itemnumber ON inventory.invcountdetails (itemnumber);
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
盘点单头历史表数据量比较大,主要用于查询,常用的筛选字段有门店编号和验收时间。我们先用门店编号字段创建索引,代码如下:
```
mysql&gt; CREATE INDEX index_invcounthaedhist_branchid ON inventory.invcountheadhist (branchid);
Query OK, 0 rows affected (0.06 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
然后用验收时间字段创建索引,代码如下:
```
mysql&gt; CREATE INDEX index_invcounthaedhist_confirmationdate ON inventory.invcountheadhist (confirmationdate);
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
盘点单明细历史表是整个盘点模块中数据量最大的表,主要用于查询,索引对提升查询效率来说非常重要。要是忘了创建索引,很可能成为整个系统的瓶颈。
这个表中用作筛选条件的字段主要有门店编号和商品编号,我们分别用它们创建索引。首先是门店编号字段,创建索引的代码如下:
```
mysql&gt; CREATE INDEX index_invcountdetailshist_branchid ON inventory.invcountdetailshist (branchid);
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
然后是商品编号字段,创建索引的代码如下:
```
mysql&gt; CREATE INDEX index_invcountdetailshist_itemnumber ON inventory.invcountdetailshist (itemnumber);
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
到这里,索引就全部创建完成了。
由于我们把盘点单拆分成了单头和明细两个表,在应用中,经常要用到单头和明细的全部信息,所以,为了使代码更加简洁,查询更加方便,我们要为这两个表创建视图。
### 如何创建视图?
首先,我们为盘点单创建视图,代码如下:
```
mysql&gt; CREATE VIEW view_invcount
-&gt; AS
-&gt; SELECT a.*,b.itemnumber,b.accquant,b.invquant,b.plquant
-&gt; FROM inventory.invcounthead AS a
-&gt; JOIN
-&gt; inventory.invcountdetails AS b
-&gt; ON (a.listnumber=b.listnumber);
Query OK, 0 rows affected (0.04 sec)
```
然后,我们为盘点单历史表创建视图,代码如下:
```
mysql&gt; CREATE VIEW view_invcounthist
-&gt; AS
-&gt; SELECT a.*,b.itemnumber,b.accquant,b.invquant,b.plquant
-&gt; FROM inventory.invcountheadhist AS a
-&gt; JOIN inventory.invcountdetailshist AS b
-&gt; ON (a.listnumber=b.listnumber);
Query OK, 0 rows affected (0.02 sec)
```
### 如何创建存储过程?
在整个盘点模块中,有一个核心的计算模块,就是盘点单验收模块。这个计算模块,每次盘点结束都会被调用。为了提升执行效率,让代码更加模块化,使代码的可读性更好,我们可以把盘点表验收这个模块的数据处理逻辑,用存储过程的方式保存在服务器上,以方便应用程序进行调用。
下面我具体介绍存储过程的入口参数和数据处理逻辑。
存储过程的入口参数是单号和验收人的员工编号。存储过程的数据处理逻辑是:先用盈亏数量调整库存,计算方式是新库存 = 老库存 + 盈亏数量;然后把盘点单数据移到盘点单历史中去。
- 把盘点单明细表中的数据插入到盘点单明细历史表中;
- 把盘点单头表中的数据,插入到盘点单头历史表中;
- 删除盘点单明细表中的数据;
- 删除盘点单头表中的数据。
按照这个参数定义和计算逻辑,我们就可以用下面的代码来创建存储过程了:
```
CREATE DEFINER=`root`@`localhost` PROCEDURE `invcountconfirm`(mylistnumber INT,myconfirmer SMALLINT)
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE mybranchid INT;
DECLARE myitemnumber INT;
DECLARE myplquant DECIMAL(10,3);
DECLARE cursor_invcount CURSOR FOR
SELECT branchid,itemnumber,plquant
FROM inventory.invcountdetails
WHERE listnumber = mylistnumber;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK;
START TRANSACTION;
OPEN cursor_invcount; -- 打开游标
FETCH cursor_invcount INTO mybranchid,myitemnumber,myplquant; -- 读入第一条记录
REPEAT
UPDATE inventory.inventory
SET itemquantity = itemquantity + myplquant -- 更新库存
WHERE itemnumber = myitemnumber
AND branchid = mybranchid;
FETCH cursor_invcount INTO mybranchid,myitemnumber,myplquant; -- 读入下一条记录
UNTIL done END REPEAT;
CLOSE cursor_invcount;
INSERT INTO inventory.invcountdetailshist
(listnumber,groupnumber,branchid,stockid,itemnumber,accquant,invquant,plquant)
SELECT listnumber,groupnumber,branchid,stockid,itemnumber,accquant,invquant,plquant
FROM inventory.invcountdetails
WHERE listnumber=mylistnumber; -- 把这一单的盘点单明细插入历史表
INSERT INTO inventory.invcountheadhist
(listnumber,groupnumber,branchid,stockid,recorder,recordingdate,confirmer,confirmationdate)
SELECT listnumber,groupnumber,branchid,stockid,recorder,recordingdate,myconfirmer,now()
FROM inventory.invcounthead
WHERE listnumber=mylistnumber; -- 把这一单的盘点单头插入历史
DELETE FROM inventory.invcountdetails WHERE listnumber = mylistnumber; -- 删除这一单的盘点单明细表数据
DELETE FROM inventory.invcounthead WHERE listnumber = mylistnumber; -- 删除这一单的盘点单头表数据
COMMIT;
END
```
具体的操作我都标注在代码里面了,你可以看一下。
### 如何创建触发器?
创建完了存储过程,我们已经完成了一大半,但是别急,还有一步工作没有做,就是创建触发器。
由于我们根据分库分表的策略把商品信息表拆分成了商品常用信息表和商品不常用信息表这样就很容易产生数据不一致的情况。为了确保商品常用信息表和商品不常用信息表中的数据保持一致我们可以创建触发器保证这2个表中删除其中一个表的一条记录的操作自动触发删除另外一个表中对应的记录的操作。这样一来就防止了一个表中的记录在另外一个表中不存在的情况也就确保了数据的一致性。
创建触发器的代码如下所示:
```
DELIMITER //
CREATE TRIGGER operation.del_goodso BEFORE DELETE -- 在删除前触发
ON operation.goods_o
FOR EACH ROW -- 表示每删除一条记录,触发一次
BEGIN -- 开始程序体
DELETE FROM operation.goods_f
WHERE groupnumber=OLD.groupnumber
AND itemnumber=OLD.itemnumber;
END
//
DELIMITER ;
DELIMITER //
CREATE TRIGGER operation.del_goodsf BEFORE DELETE -- 在删除前触发
ON operation.goods_f
FOR EACH ROW -- 表示每删除一条记录,触发一次
BEGIN -- 开始程序体
DELETE FROM operation.goods_o
WHERE groupnumber=OLD.groupnumber
AND itemnumber=OLD.itemnumber;
END
//
DELIMITER ;
```
到这儿呢,数据库数据表以及相关的索引、存储过程和触发器就都创建完了。可以说,我们已经完成了数据库的设计。
但是,在实际工作中,如果你只进行到这一步就打住了,那就还不能算是一个优秀的开发者。因为你考虑问题还不够全面。一个合格的系统设计者,不但要准确把握客户的需求,预见项目实施的前景,还要准备好对任何可能意外的应对方案。实际做项目时,不是纸上谈兵,什么情况都可能发生,我们需要未雨绸缪。所以,下面我们就来设计下项目的容灾和备份策略。
## 如何制定容灾和备份策略?
为了防止灾害出现,我设置了主从架构。为了方便你理解,我采用的是一主一从的架构,你也可以搭建一主多从的架构,原理都是一样的。
主从架构的核心是,从服务器实时自动同步主服务器的数据,一旦主服务器宕机,可以切换到从服务器继续使用。这样就可以把灾害损失降到最低。
下面我就和你一起,搭建一下主从服务器。
### 如何搭建主从服务器?
第一步确保从服务器可以访问主服务器在同一网段例如可以把主服务器的IP地址设置为
```
主服务器IP192.168.1.100
```
需要注意的是主服务器入口方向的3306号端口需要打开否则从服务器无法访问主服务器的MySQL服务器。
同时我们把从服务器的IP地址设置为
```
从服务器IP 192.168.1.110
```
第二步修改主从服务器的系统配置文件my.ini使主从服务器有不同的ID编号并且指定需要同步的数据库。
在主服务器的配置文件中我们把主服务器的编号修改为server-id = 1。
```
# ***** Group Replication Related *****
# Specifies the base name to use for binary log files. With binary logging
# enabled, the server logs all statements that change data to the binary
# log, which is used for backup and replication.
log-bin=mysql-bin -- 二进制日志名称
binlog-do-db = operation -- 需要同步的数据库:营运数据库
binlog-do-db = inventory -- 需要同步的数据库:库存数据库
# ***** Group Replication Related *****
# Specifies the server ID. For servers that are used in a replication topology,
# you must specify a unique server ID for each replication server, in the
# range from 1 to 2^32 1. “Unique” means that each ID must be different
# from every other ID in use by any other source or replica.
server-id=1 -- 主服务器的ID设为1
```
然后我们来修改从服务器的配置文件my.ini把从服务器的编号设置为server-id = 2。
```
# ***** Group Replication Related *****
# Specifies the base name to use for binary log files. With binary logging
# enabled, the server logs all statements that change data to the binary
# log, which is used for backup and replication.
log-bin=mysql-bin -- 二进制日志名称
replicate_do_db = operation -- 需要同步过来的数据库:营运数据库
replicate_do_db = inventory -- 需要同步过来的数据库:库存数据库
# ***** Group Replication Related *****
# Specifies the server ID. For servers that are used in a replication topology,
# you must specify a unique server ID for each replication server, in the
# range from 1 to 2^32 1. “Unique” means that each ID must be different
# from every other ID in use by any other source or replica.
server-id=2 -- 从服务器的编号为2
```
第三步在主从服务器上都保存配置文件然后分别重启主从服务器上的MySQL服务器。
第四步,为了使从服务器可以访问主服务器,在主服务器上创建数据同步用户,并赋予所有权限。这样,从服务器就可以实时读取主服务器的数据了。
```
mysql&gt; CREATE USER 'myreplica'@'%' IDENTIFIED BY 'mysql';
Query OK, 0 rows affected (0.02 sec)
mysql&gt; GRANT ALL ON *.* TO 'myreplica'@'%';
Query OK, 0 rows affected (0.99 sec)
```
第五步,在从服务器上启动数据同步,开始从主服务器中同步数据。
```
mysql&gt;change master to master_host='192.168.1.100',master_port=3306,master_user='myreplica',master_password='mysql,master_log_file='mysql-bin.000001',master_log_pos=535;
Query OK, 0 rows affected (0.02 sec)
```
启动同步的时候你需要注意的是必须指明主服务器上二进制日志中的位置master_log_pos。也就是说你准备从主服务器的二进制日志的哪个位置开始同步数据。你可以通过在主服务器上用SQL语句“SHOW BINLOG EVENTS IN 二进制日志名” 获取这个值。下面的代码可以启动同步:
```
mysql&gt;start slave;
Query OK, 0 rows affected (0.02 sec)
```
### 如何制定数据备份策略?
设置了主从服务器,也不是万无一失。
我曾经就遇到过这样一件事:我们把主从服务器搭在了某大厂几台不同的云服务器上,自以为没问题了,没想到大厂也有失手的时候,居然整个地区全部宕机,导致我们的主从服务器同时无法使用,近千家商户无法开展业务,损失惨重。
所以,无论系统的架构多么可靠,我们也不能大意。备份仍然是必不可少的步骤。我们可以在应用层面调用类似下面的命令进行备份:
```
H:\&gt;mysqldump -u root -p --databases
inventory operation &gt; H:\backup\Monday\mybackup.sql
```
我在项目中设定的策略是每天晚上12:00做一个自动备份循环备份7天创建7个文件夹从Monday到Sunday每个文件夹中保存对应的备份文件新的覆盖旧的。
这个逻辑也很简单,你很容易理解,我就不多解释了,你不要忘了做这一步工作就可以了。
## 总结
今天这节课,我给你详细讲解了建库建表、创建索引、存储过程、触发器,以及容灾和备份策略。有几点你需要格外重视一下。
索引是提升查询执行速度的关键,创建的原则是:所有的数据表都要创建索引;有可能作为筛选条件的字段,都要用来创建索引。
另外,容灾和备份是数据库系统设计中必不可少的部分。因为在现实生活中,什么情况都可能发生,我们无法预见,但是可以尽量避免。在设计阶段的未雨绸缪,可以帮助我们减少很多损失。
最后我要提醒你的是MySQL的相关知识实践性非常强决不能停留在纸面上。我在课中演示的的代码都是在实际环境中运行过的你课下一定要跟着实际操作一下。毕竟学习知识最好的办法就是在解决实际问题中学习。
## 思考题
在今天的课程中我演示了搭建主从服务器的过程。其中在第四步我专门创建了一个用来同步数据的账号“myreplica”。我想请你思考一下我为什么要这样做直接用“root”账号不行吗
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你把它分享给你的朋友或同事,我们下节课见。