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,192 @@
<audio id="audio" title="22 | 范式:如何消除冗余,实现高效存取?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a2/0b/a269fbf5d02952cde52cfb2f1a31370b.mp3"></audio>
你好,我是朱晓峰。今天,我们来聊一聊数据表设计的范式。
在超市项目的设计阶段超市经营者把他们正在用的Excel表格给到我们要求我们把这些数据存储到超市管理系统的数据库中。为了方便你理解我挑选了1个有代表性的表来举例说明。
进货单表import
<img src="https://static001.geekbang.org/resource/image/be/59/be46a5c19ace34dbe7a2409cb5b31459.jpeg" alt="">
这个表中的字段很多,包含了各种信息,表里的数据量也很惊人。我们刚拿到这个表的时候,光是打开表这个操作,就需要等十几秒。
仔细一看发现表里重复的数据非常多比如第一行和第二行的数据同样是3478号单据供货商编号、供货商名称和仓库这3个字段的信息完全相同。可是这2条数据的后半部分又不相同因此并不能认为它们是冗余数据而删除。
其实,造成这种问题的原因是这张表的设计非常不合理,大量重复导致表变得庞大,效率极低。
在我们的工作场景中这种由于数据表结构设计不合理而导致的数据重复的现象并不少见往往是系统虽然能够运行承载能力却很差稍微有点流量就会出现内存不足、CUP使用率飙升的情况甚至会导致整个项目失败。
所以,**高效可靠的设计是提升数据库工作效率的关键**。那该怎么设计呢?有没有什么可以参考的设计规范呢?自然是有了。
接下来我就带你重新设计一下刚刚的进货单表在这个过程中给你具体介绍一下数据表设计的三大范式分别是第一范式1NF、第二范式2NF和第三范式3NF这些范式可以帮助我们设计出简洁高效的数据表进而提高系统的效率。
我先来介绍一下最基本的第一范式。
## 第一范式
我们对这张进货单表重新设计的第一步,就是要把所有的列,也就是字段,都确认一遍,确保每个字段只包含一种数据。如果各种数据都混合在一起,就无法通过后面的拆解,把重复的数据去掉。
其实,这就是第一范式所要求的:**所有的字段都是基本数据字段,不可进一步拆分**。
在我们的这张表里“property”这一字段可以继续拆分。其他字段已经都是基本数据字段不能再拆了。
经过优化我们把“property”这一字段拆分成“specification规格”和“unit单位这2个字段如下
<img src="https://static001.geekbang.org/resource/image/0f/00/0f25120109301511aa5b2919ac47f000.jpeg" alt="">
这样处理之后,字段多了一个,但是每一个字段都成了不可拆分的最小信息单元,我们就可以在这个表的基础之上,着手进行进一步的优化了。这就要用到数据表设计的第二范式了。
## 第二范式
通过观察我们可以发现这个表的前2条记录的前4个字段完全一样。那可不可以通过拆分把它们变成一条记录呢当然是可以的而且为了优化必须要进行拆分。
具体怎么拆分呢?第二范式就告诉了我们拆分的原则。
第二范式要求,在满足第一范式的基础上,**还要满足数据表里的每一条数据记录,都是可唯一标识的。而且所有字段,都必须完全依赖主键,不能只依赖主键的一部分**。
根据这个要求,我们可以对表进行重新设计。
重新设计的第一步就是要确定这个表的主键。通过观察发现字段“listnumber”+“barcode”可以唯一标识每一条记录可以作为主键。确定好了主键以后我们判断一下哪些字段完全依赖主键哪些字段只依赖于主键的一部分。同时把只依赖于主键一部分的字段拆分出去形成新的数据表。
首先进货单明细表里面的“goodsname”“specification”“unit”这些信息是商品的属性只依赖于“barcode”不完全依赖主键可以拆分出去。我们把这3个字段加上它们所依赖的字段“barcode”拆分形成一个新的数据表“**商品信息表**”。
这样一来,原来的数据表就被拆分成了两个表。
商品信息表:
<img src="https://static001.geekbang.org/resource/image/yy/73/yy2d107bb318daac0e5d9d14d73d4273.jpeg" alt="">
进货单表:
<img src="https://static001.geekbang.org/resource/image/b0/e1/b0479052aa9ccae47a925yy9b4aecae1.jpeg" alt="">
同样道理字段“supplierid”“suppliername”“stock”只依赖于“listnumber”不完全依赖于主键所以我们可以把“supplierid”“suppliername”“stock”这3个字段拆出去再加上它们依赖的字段“listnumber”就形成了一个新的表“进货单头表”。剩下的字段会组成新的表我们叫它“**进货单明细表**”。
这样一来原来的数据表就拆分成了3个表。
进货单头表:
<img src="https://static001.geekbang.org/resource/image/e9/88/e967b2a4a47ec6673f511eb0542de988.jpeg" alt="">
进货单明细表:
<img src="https://static001.geekbang.org/resource/image/6e/54/6e76c1306bf9e42a9d5ff4aa9b066854.jpeg" alt="">
商品信息表:
<img src="https://static001.geekbang.org/resource/image/79/3e/79dcf3c98afb5366c79741b594821a3e.jpeg" alt="">
到这里我们就按照第二范式的要求把原先的一个数据表拆分成了3个数据表。
现在我们再来分析一下拆分后的3个表保证这3个表都满足第二范式的要求。
在“商品信息表”中字段“barcode”是有可能存在重复的比如用户门店可能有散装称重商品和自产商品会存在条码共用的情况。所以所有的字段都不能唯一标识表里的记录。这个时候我们必须给这个表加上一个主键比如说是自增字段“itemnumber”。
现在我们就可以把进货单明细表里面的字段“barcode”都替换成字段“itemnumber”这就得到了新的进货单明细表和商品信息表。
进货单明细表:
<img src="https://static001.geekbang.org/resource/image/0e/ae/0e458237156ce1149683d2cdeb209eae.jpeg" alt="">
商品信息表:
<img src="https://static001.geekbang.org/resource/image/b1/b1/b1c38921793650c7e258bb5db1382ab1.jpeg" alt="">
这样一来我们拆分后的3个数据表中的数据都不存在重复可以唯一标识。而且表中的其他字段都完全依赖于表的主键不存在部分依赖的情况。所以拆分后的3个数据表就全部满足了第二范式的要求。
## 第三范式
如果你仔细看的话会发现我们的进货单头表还有数据冗余的可能。因为“suppliername”依赖“supplierid”。那么这个时候就可以按照第三范式的原则进行拆分了。
第三范式要求数据表**在满足第二范式的基础上,不能包含那些可以由非主键字段派生出来的字段,或者说,不能存在依赖于非主键字段的字段**。
在刚刚的进货单头表中字段“suppliername”依赖于非主键字段“supplierid”。因此这个表不满足第三范式的要求。
那接下来,我们就进一步拆分下进货单头表,把它拆解成供货商表和进货单头表。
供货商表:
<img src="https://static001.geekbang.org/resource/image/d5/49/d5f848542334024aa9ea77d27652ce49.jpeg" alt="">
进货单头表:
<img src="https://static001.geekbang.org/resource/image/bb/y4/bb4c264221a8bc6c8949f3c435ec8yy4.jpeg" alt="">
这样一来供货商表和进货单头表中的所有字段都完全依赖于主键不存在任何一个字段依赖于非主键字段的情况了。所以这2个表就都满足第三范式的要求了。
但是在进货单明细表中quantity * importprice = importvalue“importprice”“quantity”和“importvalue”这3个字段可以通过任意两个计算出第三个来这就存在冗余字段。如果严格按照第三范式的要求现在我们应该进行进一步优化。优化的办法是删除其中一个字段只保留另外2个这样就没有冗余数据了。
可是,真的可以这样做吗?要回答这个问题,我们就要先了解下实际工作中的业务优先原则。
## 业务优先的原则
所谓的业务优先原则,就是指一切以业务需求为主,技术服务于业务。**完全按照理论的设计不一定就是最优,还要根据实际情况来决定**。这里我们就来分析一下不同选择的利与弊。
对于quantity * importprice = importvalue看起来“importvalue”似乎是冗余字段但并不会导致数据不一致。可是如果我们把这个字段取消是会影响业务的。
因为有的时候供货商会经常进行一些促销活动按金额促销那他们拿来的进货单只有金额没有价格。而“importprice”反而是通过“importvalue”÷“quantity”计算出来的。因此如果不保留“importvalue”字段只有“importprice”和“quantity”的话经过四舍五入会产生较大的误差。这样日积月累最终会导致查询结果出现较大偏差影响系统的可靠性。
我借助一个例子来说明下为什么会有偏差。
假设进货金额是25.5元数量是34那么进货价格就等于25.5÷34=0.74元但是如果用这个计算出来的进货价格来计算进货金额那么进货金额就等于0.74×34=25.16元其中相差了25.5-25.16=0.34元。代码如下所示:
```
“importvalue”=25.5元“quantity”=34“importprice”=25.5÷34=0.74
“importprice”=0.74元“quantity”=34“importvalue”=0.74*34=25.16
误差 = 25.5 - 25.16 = 0.34
```
现在你知道了在我们这个场景下“importvalue”是必须要保留的。
那么换一种思路如果我们保留“quantity”和“importvalue”取消“importprice”这样不是既能节省存储空间又不会影响精确度吗
其实不是的。“importprice”是系统的核心指标涉及成本核算。几乎所有的财务、营运和决策支持模块都要涉及到成本问题如果取消“importprice”这个字段那么系统的运算成本、开发和运维成本都会大大提高得不偿失。
所以本着业务优先的原则在不影响系统可靠性的前提下可以容忍一定程度的数据冗余保留“importvalue”“importprice”和“quantity"。
因此最后的结果是我们可以把进货单表拆分成下面的4个表
供货商表:
<img src="https://static001.geekbang.org/resource/image/0e/8a/0e9f3b57c06a35e3ea504946076f918a.jpeg" alt="">
进货单头表:
<img src="https://static001.geekbang.org/resource/image/16/a5/16441678d5728eb13703a99df88a7da5.jpeg" alt="">
进货单明细表:
<img src="https://static001.geekbang.org/resource/image/1a/a0/1a9ea53a76ce6ed2c022578516f875a0.jpeg" alt="">
商品信息表:
<img src="https://static001.geekbang.org/resource/image/46/eb/46f4fdbd9f2362b783ca4d2f8b8e84eb.jpeg" alt="">
这样一来,我们就避免了冗余数据,而且还能够满足业务的需求,这样的数据表设计,才是合格的设计。
## 总结
今天我们通过具体案例的分析学习了MySQL数据库设计的范式规范包括第一范式、第二范式和第三范式。
我再给你汇总下MySQL数据库规范化设计的三个范式。
- 第一范式:数据表中所有字段都是不可拆分的基本数据项。
- 第二范式:在满足第一范式的基础上,数据表中所有非主键字段,必须完全依赖全部主键字段,不能存在部分依赖主键字段的字段。
- 第三范式:在满足第二范式的基础上,数据表中不能存在可以被其他非主键字段派生出来的字段,或者说,不能存在依赖于非主键字段的字段。
遵循范式的要求,可以减少冗余,结合外键约束,可以防止添加、删除、修改数据时产生数据的不一致问题。
除此之外,我还给你解释了为什么有的时候不能简单按照规范要求设计数据表,因为有的数据看似冗余,其实对业务来说十分重要。这个时候,我们就要遵循业务优先的原则,首先满足业务需求,在这个前提下,再尽量减少冗余。
一般来说MySQL的数据库设计满足第三范式就足够了。不过第三范式并不是终极范式还有[BCNF范式也叫BC范式](https://baike.baidu.com/item/BC%E8%8C%83%E5%BC%8F/3193909?fr=aladdin)、[第四范式](https://baike.baidu.com/item/%E7%AC%AC%E5%9B%9B%E8%8C%83%E5%BC%8F)和[第五范式](https://baike.baidu.com/item/%E7%AC%AC%E4%BA%94%E8%8C%83%E5%BC%8F)。如果你想进一步研究数据库设计,课下可以看下我分享的链接,拓展下思路。
## 思考题
假设有这样一个销售流水表,如下所示:
<img src="https://static001.geekbang.org/resource/image/e0/78/e0c9dc47d2c199fb0078fcd9ba49bb78.jpeg" alt="">
这个表存在数据冗余,应该如何优化设计呢?为什么?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,418 @@
<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模型通过它来整理一下数据库设计的思路。
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,377 @@
<audio id="audio" title="24 | 查询有点慢,语句该如何写?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/11/76/11d59a2959ff7e65e29796f18205c876.mp3"></audio>
你好,我是朱晓峰。这节课,我想和你聊一聊怎么对查询语句进行调优。
你肯定遇到过这样的情况你写的SQL语句执行起来特别慢要等好久才出结果或者是干脆就“死”在那里一点反应也没有。一旦遇到这种问题你就要考虑进行优化了。
如果你开发过数据库应用,肯定会有这样的体会:**让应用运行起来不难,但是要运行得又快又好,就没那么不容易了**。这很考验我们的内功。
而要想提高应用的运行效率你就必须掌握优化查询的方法。今天我就给你讲一下MySQL的查询分析语句和2种优化查询的方法。
## 查询分析语句
虽然MySQL的查询分析语句并不能直接优化查询但是却可以帮助你了解SQL语句的执行计划有助于你分析查询效率低下的原因进而有针对性地进行优化。查询分析语句的语法结构是
```
{ EXPLAIN | DESCRIBE | DESC }查询语句;
```
下面我借助一个小例子,给你详细地讲解一下,怎么使用查询分析语句,来分析一个查询的执行计划。
假设有一个销售流水表demo.trans里面有400万条数据如下所示
<img src="https://static001.geekbang.org/resource/image/07/bf/0771b07c86d2ebc355788e606f176cbf.jpeg" alt="">
现在我要查询一下商品编号是1的商品在2020年6月18日上午9点到12点之间的销售明细。代码如下所示
```
mysql&gt; SELECT itemnumber,quantity,price,transdate
-&gt; FROM demo.trans
-&gt; WHERE itemnumber=1
-&gt; AND transdate&gt;'2020-06-18 09:00:00'
-&gt; AND transdate&lt;'2020-06-18 12:00:00';
+------------+----------+-------+---------------------+
| itemnumber | quantity | price | transdate |
+------------+----------+-------+---------------------+
| 1 | 0.276 | 70.00 | 2020-06-18 11:04:00 |
| 1 | 1.404 | 70.00 | 2020-06-18 11:10:57 |
| 1 | 0.554 | 70.00 | 2020-06-18 11:18:12 |
| 1 | 0.431 | 70.00 | 2020-06-18 11:27:39 |
| 1 | 0.446 | 70.00 | 2020-06-18 11:42:08 |
| 1 | 0.510 | 70.00 | 2020-06-18 11:56:43 |
+------------+----------+-------+---------------------+
6 rows in set (6.54 sec)
```
结果显示有6条记录符合条件。这个简单的查询一共花去了6.54秒,这个速度显然太慢了。
为了找到查询运行慢的原因,咱们来分析一下它,看看为什么会用这么多时间,有没有办法优化。现在,我们用下面的语句分析一下这个查询的具体细节:
```
mysql&gt; EXPLAIN SELECT itemnumber,quantity,price,transdate -- 分析查询执行情况
-&gt; FROM demo.trans
-&gt; WHERE itemnumber=1 -- 通过商品编号筛选
-&gt; AND transdate&gt;'2020-06-18 09:00:00' -- 通过交易时间筛选
-&gt; AND transdate&lt;'2020-06-18 12:00:00';
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key |key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| 1 | SIMPLE | trans | NULL | ALL | NULL | NULL | NULL | NULL | 4157166 | 1.11 | Using where | -- 没有索引扫描4157166条记录
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
```
这个结果集中包含了很多个字段,理解这些字段的意思,是我们优化查询的关键。
字段比较多,我先说说简单的字段,你只要知道概念就可以了。另外一类比较复杂的字段,一会儿我再具体讲。
1. id是一个查询序列号。
1. table表示与查询结果相关的表的名称。
1. partition表示查询访问的分区。
1. key表示优化器最终决定使用的索引是什么。
1. key_len表示优化器选择的索引字段按字节计算的长度。如果没有使用索引这个值就是空。
1. ref表示哪个字段或者常量被用来与索引字段比对以读取表中的记录。如果这个值是“func”就表示用函数的值与索引字段进行比对。
1. rows表示为了得到查询结果必须扫描多少行记录。
1. filtered表示查询筛选出的记录占全部表记录数的百分比。
1. possible_key表示MySQL可以通过哪些索引找到查询的结果记录。如果这里的值是空就说明没有合适的索引可用。你可以通过查看WHERE条件语句中使用的字段来决定是否可以通过创建索引提高查询的效率
1. Extra表示MySQL执行查询中的附加信息。你可以点击这个[链接](https://dev.mysql.com/doc/refman/8.0/en/explain-output.html#explain-extra-information)查询详细信息。
1. type表示表是如何连接的。至于具体的内容你可以参考下[查询分析语句输出内容说明](https://dev.mysql.com/doc/refman/8.0/en/explain-output.html)。
除了刚刚这些字段还有1个比较重要那就是select_type。它表示查询的类型主要有4种取值。
- SIMPLE表示简单查询不包含子查询和联合查询。
- PRIMARY表示是最外层的查询。
- UNION表示联合查询中的第二个或者之后的查询。
- DEPENDENTUNION表示联合查询中的第二个或者之后的查询而且这个查询受外查询的影响。
关于这个DEPENDENTUNION取值不是很好理解我举个小例子。
假设我们有下面的查询语句:
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster a
-&gt; WHERE itemnumber in
-&gt; (
-&gt; SELECTb.itemnumber
-&gt; FROM demo.goodsmaster b
-&gt; WHERE b.goodsname = '书'
-&gt; UNION
-&gt; SELECTc.itemnumber
-&gt; FROM demo.goodsmaster c
-&gt; WHERE c.goodsname = '笔'
-&gt; );
+------------+---------+-----------+---------------+------+------------+----------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice | avgimportprice |
+------------+---------+-----------+---------------+------+------------+----------------+
| 1 | 0001 | 书 | 16开 | 本 | 89.00 | 31.00 |
| 2 | 0002 | 笔 | NULL | 包 | 5.00 | 2.87 |
+------------+---------+-----------+---------------+------+------------+----------------+
2 rows in set (0.00 sec)
```
对这个语句的执行进行分析,得到如下的结果:
```
mysql&gt; EXPLAIN SELECT *
-&gt; FROM demo.goodsmaster a
-&gt; WHERE a.itemnumber in
-&gt; (
-&gt; SELECTb.itemnumber
-&gt; FROM demo.goodsmaster b
-&gt; WHERE b.goodsname = '书'
-&gt; UNION
-&gt; SELECTc.itemnumber
-&gt; FROM demo.goodsmaster c
-&gt; WHERE c.goodsname = '笔'
-&gt; );
+----+--------------------+------------+------------+--------+---------------+---------+---------+------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------------+------------+------------+--------+---------------+---------+---------+------+------+----------+-----------------+
| 1 | PRIMARY | a | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100.00 | Using where |
| 2 | DEPENDENT SUBQUERY | b | NULL | eq_ref | PRIMARY | PRIMARY | 4 | func | 1 | 50.00 | Using where |
| 3 | DEPENDENT UNION | c | NULL | eq_ref | PRIMARY | PRIMARY | 4 | func | 1 | 50.00 | Using where |
| NULL | UNION RESULT | &lt;union2,3&gt; | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary |
+----+--------------------+------------+------------+--------+---------------+---------+---------+------+------+----------+-----------------+
4 rows in set, 1 warning (0.00 sec)
```
MySQL在执行的时候会把这个语句进行优化重新写成下面的语句
```
SELECT *
FROM demo.goodsmaster a
WHERE EXISTS
(
SELECT b.id
FROM demo.goodsmaster b
WHERE b.goodsname = '书' ANDa.itemnumber=b.itemnumber
UNION
SELECT c.id
FROM demo.goodsmaster c
WHERE c.goodsname = '笔' AND a.itemnumber=c.itemnumber
);
```
在这里,子查询中的联合查询是:
```
SELECT c.id
FROM demo.goodsmaster c
WHERE c.goodsname = '笔' AND a.itemnumber=c.itemnumber
```
这个查询就用到了与外部查询相关的条件a.itemnumber=c.itemnumber因此查询类别就变成了“UNION DEPENDENT”。
好了,现在,我们已经知道了查询分析语句的结果集中各个字段的含义。现在来分析一下刚刚的查询语句。
我们发现这个查询是一个简单查询涉及的表是demo.trans没有分区连接类型是扫描全表没有索引一共要扫描的记录数是4157166。因此查询速度慢的主要原因是没有索引导致必须要对全表进行扫描才能完成查询。所以针对这个问题可以通过创建索引的办法来提高查询的速度。
下面我们用条件语句中的筛选字段itemnumber和transdate分别创建索引
```
mysql&gt; CREATE INDEX itemnumber_trans ON demo.trans(itemnumber);
Query OK, 0 rows affected (59.86 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql&gt; CREATE INDEX transdate_trans ON demo.trans(transdate);
Query OK, 0 rows affected (56.75 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
然后我们再次运行刚才的查询,看看优化有没有起作用:
```
mysql&gt; SELECT itemnumber,quantity,price,transdate
-&gt; FROM demo.trans
-&gt; WHERE itemnumber=1
-&gt; AND transdate&gt;'2020-06-18 09:00:00'
-&gt; AND transdate&lt;'2020-06-18 12:00:00';
+------------+----------+-------+---------------------+
| itemnumber | quantity | price | transdate |
+------------+----------+-------+---------------------+
| 1 | 0.276 | 70.00 | 2020-06-18 11:04:00 |
| 1 | 1.404 | 70.00 | 2020-06-18 11:10:57 |
| 1 | 0.554 | 70.00 | 2020-06-18 11:18:12 |
| 1 | 0.431 | 70.00 | 2020-06-18 11:27:39 |
| 1 | 0.446 | 70.00 | 2020-06-18 11:42:08 |
| 1 | 0.510 | 70.00 | 2020-06-18 11:56:43 |
+------------+----------+-------+---------------------+
6 rows in set (0.09 sec)
```
结果显示查询只运行了0.09秒跟之前的6.54秒相比,快了很多。这说明我们的优化措施起了作用。下面我们再次运行查询分析语句,查看一下现在的查询执行计划。
```
mysql&gt; EXPLAIN SELECT itemnumber,quantity,price,transdate
-&gt; FROM demo.trans
-&gt; WHERE itemnumber=1 -- 按商品编号筛选
-&gt; AND transdate&gt;'2020-06-18 09:00:00'-- 按照交易时间筛选
-&gt; AND transdate&lt;'2020-06-18 12:00:00';
+----+-------------+-------+------------+-------+----------------------------------+-----------------+---------+------+------+----------+------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+----------------------------------+-----------------+---------+------+------+----------+------------------------------------+
| 1 | SIMPLE | trans | NULL | range | itemnumber_trans,transdate_trans | transdate_trans | 6 | NULL | 552 | 1.92 | Using index condition; Using where | -- 使用了索引
+----+-------------+-------+------------+-------+----------------------------------+-----------------+---------+------+------+----------+------------------------------------+
1 row in set, 1 warning (0.01 sec)
```
结果显示这一次的查询执行计划有了改变。系统发现有2个索引itemnumber_trans和transdate_trans可以使用并且最终选择了使用交易时间字段创建的索引transdate_trans来执行查询。扫描的方式也不再是全表扫描而是改为了区域扫描实际的扫描记录数也减少到了552个。这样一来查询更加精准查询的速度自然也就大幅提高了。
至此我给你介绍了查询分析语句并且演示了如何通过使用查询分析语句对慢查询的执行计划进行分析并且利用分析的结果对慢查询进行优化。接下来我再给你介绍2种优化查询的方法。
## 2种查询优化的方法
优化查询最有效的方法就是创建索引。关于如何创建索引,我已经在[第11讲](https://time.geekbang.org/column/article/357312)中介绍过了这里就不多说了。下面我来讲讲怎么在包含关键字“LIKE”和“OR”的条件语句中利用索引提高查询效率。
### 使用关键字“LIKE”
“LIKE”经常被用在查询的限定条件中通过通配符“%”来筛选符合条件的记录。比如,
1. `WHERE字段 LIKE %aa`表示筛选出所有以字段以“aa”结尾的记录
1. `WHERE字段 LIKE aa%`表示筛选出所有以“aa”开始的记录
1. `WHERE字段 LIKE %aa%`表示所有字段中包含“aa”的记录。
这里你要注意的是,通配符在前面的筛选条件是不能用索引的。也就是说,`WHERE字段LIKE%aa``WHERE字段 LIKE %aa%`都不能使用索引,但是通配符在后面的筛选条件,就可以使用索引。
下面,我举个小例子,通过查询分析语句来验证一下索引的使用情况。
假设我用商品流水表的字段商品条码“barcode”创建了一个索引
```
mysql&gt; CREATE INDEX trans_barcode ON demo.trans(barcode);
Query OK, 0 rows affected (1 min 20.78 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
我们来看看如果把通配符放在前面,能不能用到索引:
```
mysql&gt; EXPLAIN SELECT * FROM demo.trans
-&gt; WHERE barcode LIKE '%182505';
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key |key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| 1 | SIMPLE | trans | NULL | ALL | NULL | NULL | NULL | NULL | 4144028 | 11.11 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
```
查询分析的结果显示type的值是“ALL”key是空表示需要进行全表扫描没有索引可用rows的值是4144028基本上要检索全部记录效率非常低。
再看看通配符在后面的情况:
```
mysql&gt; EXPLAIN SELECT * FROM demo.trans
-&gt; WHERE barcode LIKE '6953150%';
+----+-------------+-------+------------+-------+---------------+---------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type |possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | trans | NULL | range | trans_barcode | trans_barcode | 803 | NULL | 563 | 100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+---------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)
```
type的值是“range”意思是使用索引检索一个给定范围的记录。rows的值是563也就是只需要扫描563条记录就行了这样效率就高多了。
### 使用关键字“OR”
关键字“OR”表示“或”的关系“WHERE表达式1 OR 表达式2”就表示表达式1或者表达式2中只要有一个成立整个WHERE条件就是成立的。
需要注意的是只有当条件语句中只有关键字“OR”并且“OR”前后的表达式中的字段都建有索引的时候查询才能用到索引。
同样,为了方便你理解,我还是举个小例子,通过查询分析语句来实际验证一下。
我刚才已经用字段条码给商品流水表创建了一个索引现在我再用商品编号“itemnumber”创建一个索引
```
mysql&gt; CREATE INDEX trans_itemnumber ON demo.trans(itemnumber);
Query OK, 0 rows affected (20.24 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
我们先看一下关键字“OR”前后的表达式中的字段都创建了索引的情况
```
mysql&gt; EXPLAIN SELECT * FROM demo.trans
-&gt; WHERE barcode LIKE '6953150%'
-&gt; OR itemnumber = 1;
+----+-------------+-------+------------+-------------+--------------------------------+--------------------------------+---------+------+-------+----------+---------------------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------------+--------------------------------+--------------------------------+---------+------+-------+----------+---------------------------------------------------------------+
| 1 | SIMPLE | trans | NULL | index_merge | trans_barcode,trans_itemnumber | trans_barcode,trans_itemnumber | 803,5 | NULL | 81099 | 100.00 | Using sort_union(trans_barcode,trans_itemnumber); Using where |
+----+-------------+-------+------------+-------------+--------------------------------+--------------------------------+---------+------+-------+----------+---------------------------------------------------------------+
1 row in set, 1 warning (0.01 sec)
```
查询分析结果显示有2个索引可以使用分别是“trans_barcode”和“trans_itemnumber”。key=index_merge就说明优化器选择了合并索引的方式。因此这个关键字“OR”前后的表达式中的字段都创建了索引的查询是可以用到索引的。
在下面的例子中,表达式`goodsname LIKE '%海鲜菇%'`中的字段goodsname没有创建索引我们来验证一下查询是否能够利用索引
```
mysql&gt; EXPLAIN SELECT * FROM demo.trans
-&gt; WHERE barcode LIKE '6953150%'
-&gt; OR goodsname LIKE '%海鲜菇%';
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key |key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| 1 | SIMPLE | trans | NULL | ALL | trans_barcode | NULL | NULL | NULL | 4144028 | 20.99 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
```
查询分析的结果显示type=ALL是全表扫描不能用索引。
## 总结
今天,我们学习了优化查询的方法,包括用来分析查询执行情况的查询分析语句,语法结构是:
```
{ EXPLAIN | DESCRIBE | DESC }查询语句;
```
同时我们还学习了在使用关键字“LIKE”和“OR”的情况下用索引来提高查询效率的方法。
特别需要提醒你注意的是在使用“LIKE”关键字的条件语句中通配符“%”在前面的筛选条件不能使用索引,通配符“%”在后面的筛选条件可以使用索引。在使用“OR”关键字的条件语句中只有关键字“OR”前后的表达式中的字段都创建了索引条件语句才能使用索引。
关于优化查询还有一个值得关注的点就是子查询。这是MySQL的一项重要的功能可以帮助我们通过一个SQL语句实现比较复杂的查询。但是**子查询的执行效率不高**。因为MySQL会用临时表把子查询的结果保存起来然后再使用临时表的内容完成查询。这样一来查询就多了一个创建临时表的过程执行效率没有连接查询高。针对这种情况建议你把子查询转换成连接查询这样可以进一步提高查询的效率。
## 思考题
假设现在有一个这样的查询请你把这个包含子查询的查询改成2个表的连接查询。
```
mysql&gt; EXPLAIN SELECT * FROM demo.trans
-&gt; WHERE itemnumber IN
-&gt; (
-&gt; SELECTitemnumber FROM demo.goodsmaster
-&gt; WHERE goodsname LIKE '%书%'
-&gt; );
+----+-------------+-------------+------------+------+------------------+------------------+---------+-----------------------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+------------------+------------------+---------+-----------------------------+------+----------+-------------+
| 1 | SIMPLE | goodsmaster | NULL | ALL | PRIMARY | NULL | NULL | NULL | 2 | 50.00 | Using where |
| 1 | SIMPLE | trans | NULL | ref | trans_itemnumber | trans_itemnumber | 5 | demo.goodsmaster.itemnumber | 889 | 100.00 | NULL |
+----+-------------+-------------+------------+------+------------------+------------------+---------+-----------------------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
```
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,259 @@
<audio id="audio" title="25 | 表太大了,如何设计才能提高性能?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/be/92/be95e664079be22d3795034e84219092.mp3"></audio>
你好,我是朱晓峰。
随着数据量的不断增加,表会变得越来越大,查询的速度也会越来越慢。针对这种情况,该怎么处理呢?
咱们上节课学习的优化查询语句是一种方法,不过它并不足以解决所有问题。如果表的设计不合理,会导致数据记录占用不必要的存储空间。
MySQL在存取数据时并不是一条条去处理的而是会按照固定大小的页进行处理如果数据记录占用了不必要的存储空间就会导致一次读入的有效数据很少。那么无论怎么改写语句都无法提升这步操作的效率。这个时候对表的设计进行优化就是必不可少的了。
所以,今天,我就给你介绍一下怎么通过优化数据类型、合理增加冗余字段、拆分表和使用非空约束等方法,来改进表的设计,从而提高查询性能。
## 数据类型优化
**在改进表的设计时,首先可以考虑优化字段的数据类型**。下面我就来讲解2种方法一种是针对整数类型数据尽量使用小的整数类型来定义另外一种是如果字段既可以用文本类型也可以用整数类型尽量使用整数类型。
**先说第一种方法,对整数类型数据进行优化。**
在[第2讲](https://time.geekbang.org/column/article/350470)中我建议你遇到整数类型的字段可以用INT型。这样做的理由是INT型数据有足够大的取值范围不用担心数据超出取值范围的问题。刚开始做项目的时候首先要保证系统的稳定性这样设计字段类型是可以的。
但是,随着你的经验越来越丰富,参与的项目越来越大,数据量也越来越多的时候,你就不能只从系统稳定性的角度来思考问题了,**还要考虑到系统整体的效率**。
这是因为,在数据量很大的时候,数据类型的定义,在很大程度上会影响到系统整体的执行效率。这个时候,你就必须同时考虑稳定性和效率。
**第2种优化方法就是既可以使用文本类型也可以使用整数类型的字段要使用整数类型而不要用文本类型**
跟文本类型数据相比,大整数往往占用更少的存储空间,因此,在存取和比对的时候,可以占用更少的内存。所以,遇到既可以使用文本类型,又可以使用整数类型来定义的字段,尽量使用整数类型,这样可以提高查询的效率。
接下来,我就结合超市项目的案例来讲解下具体的优化方法。
在这个项目中我们有一个400万条记录的流水数据。为了方便你理解这里我只保留2个字段分别是商品编号字段itemnumber和流水唯一编号字段transuniqueid。流水唯一编号用于在系统中唯一标识一条流水。
为了对比方便我创建了2个表demo.test和demo.test1
- 在demo.test的表中我给商品编号设定的数据类型是INT给流水唯一编号设定的数据类型是TEXT
- 在demo.test1中我给商品编号设定的数据类型是MEDIUMINT给流水唯一编号设定的数据类型是BIGINT。
这样设定的原因是MEDIUMINT类型的取值范围是“无符号数0 16777215”。对于商品编号来说其实够用了。我的400万条数据中没有超过这个范围的值。而流水唯一编号是一个长度为18位的数字用字符串数据类型TEXT肯定是可以的大整数类型BIGINT的取值范围是“无符号数0 18446744083709551616”有20位所以用大整数类型数据来定义流水唯一编号也是可以的。
创建表demo.test和demo.test1的语句如下所示
```
mysql&gt; CREATE TABLE demo.test (itemnumber INT,transuniqueid TEXT);
Query OK, 0 rows affected (0.23 sec)
mysql&gt; CREATE TABLE demo.test1 (itemnumber MEDIUMINT,transuniqueid BIGINT);
Query OK, 0 rows affected (0.25 sec)
```
然后,我们来对这两个表进行数据导入和查询操作,看看哪个效率更高:
```
mysql&gt; LOAD DATA INFILE 'C:\\ProgramData\\MySQL\\MySQL Server 8.0\\Uploads\\trans.txt' INTO TABLE demo.test FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n';
Query OK, 4328021 rows affected (3 min 23.47 sec)
Records: 4328021 Deleted: 0 Skipped: 0 Warnings: 0
mysql&gt; LOAD DATA INFILE 'C:\\ProgramData\\MySQL\\MySQL Server 8.0\\Uploads\\trans.txt' INTO TABLE demo.test1 FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n';
Query OK, 4328021 rows affected (3 min 1.84 sec)
Records: 4328021 Deleted: 0 Skipped: 0 Warnings: 0
```
结果显示同样导入400万条数据demo.test用时3分23.47秒而demo.test1用时3分1.84秒。显然demo.test1的数据导入速度比demo.test快了将近21秒。
在保存相同数量的数据记录的情况下,优化过的表的查询的效率也更高一些。下面我们来验证一下:
```
mysql&gt; SELECT COUNT(*)
-&gt; FROM demo.test
-&gt; WHERE itemnumber = 1;
+----------+
| COUNT(*) |
+----------+
| 40742 |
+----------+
1 row in set (5.18 sec)
mysql&gt; SELECT COUNT(*)
-&gt; FROM demo.test1
-&gt; WHERE itemnumber = 1;
+----------+
| COUNT(*) |
+----------+
| 40742 |
+----------+
1 row in set (3.86 sec)
```
结果显示这个差别更大。demo.test用了5.18秒而demo.test1只用了3.86秒,速度提升得非常明显。
这是为啥呢?我们来分析下。
原来INT类型占用4个字节存储空间而MEDIUMINT类型只占用3个字节的存储空间比INT类型节省了25%的存储空间。demo.test1的第一个字段的数据类型是MEDIUMINTdemo.test的第一个字段的数据类型是INT。因此我们来对比下两个表的第一个字段 demo.test1占用的存储空间就比demo.test节省了25%。
再来看看这两个表的第二个字段流水唯一编号transuniqueid。
在demo.test中这个字段的类型是TEXT而TEXT类型占用的字节数等于“实际字符串长度+ 2”在咱们的这个场景中流水唯一编号的长度是18所占用的存储空间就是20个字节。在demo.test1中流水唯一编号的数据类型是BIGINT占用的存储空间就是8个字节。这样一来demo.test1在第二个字段上面占用的存储空间就比demo.test节省了20-8÷20=60%。很明显对于流水唯一编号字段demo.test1比demo.test更加节省空间。
因此,我建议你,**遇到数据量大的项目时,一定要在充分了解业务需求的前提下,合理优化数据类型,这样才能充分发挥资源的效率,使系统达到最优**。
## 合理增加冗余字段以提高效率
在数据量大,而且需要频繁进行连接的时候,为了提升效率,我们也可以考虑增加冗余字段来减少连接。
为了方便你理解,我举个小例子。
假如我们有2个表分别是商品流水表demo.trans和商品信息表demo.goodsmaster。商品流水表里有400万条流水记录商品信息表里有2000条商品记录。
商品流水表:
<img src="https://static001.geekbang.org/resource/image/99/3f/99a5f06a6dc2db78fdbd7aa46d68523f.jpeg" alt="">
商品信息表:
<img src="https://static001.geekbang.org/resource/image/9a/be/9a25517af6178903b57597f95b0326be.jpeg" alt="">
可以看到,这两个表中不存在冗余数据,都是符合第三范式的要求的。但是,在我们项目的实施过程中,对流水的查询频率很高,而且为了获取商品名称,基本都会用到与商品信息表的连接查询。
假设我现在要查询一下2020年04月11日上午9:00到中午12:00、商品编号是355的商品的销售数量明细就必须要使用连接查询
```
mysql&gt; SELECT b.goodsname,a.quantity
-&gt; FROM demo.trans AS a
-&gt; JOIN demo.goodsmaster AS b
-&gt; ON (a.itemnumber=b.itemnumber)
-&gt; WHERE a.transdate&gt;'2021-04-11 09:00:00'
-&gt; AND a.transdate&lt;'2021-04-11 12:00:00'
-&gt; AND a.itemnumber = 355;
+--------------+----------+
| goodsname | quantity |
+--------------+----------+
| 贵烟(跨越) | 1.000 |
| 贵烟(跨越) | 1.000 |
| 贵烟(跨越) | 1.000 |
| 贵烟(跨越) | 1.000 |
| 贵烟(跨越) | 1.000 |
+--------------+----------+
5 rows in set (6.64 sec)
```
结果显示一共有5笔销售各卖了1个商品名称是一种叫做“贵烟跨越”的香烟。这个查询用掉了6.64秒。
为了减少连接,我们可以直接把商品名称字段加到流水表里面。这样一来,我们就可以直接从流水表中获取商品名称字段了。虽然增加了冗余字段,但是避免了连接,这样对提高查询效率有没有什么影响呢?我们来验证一下。
我们给商品流水表添加一个字段商品名称goodsname。新的商品流水表如下所示
<img src="https://static001.geekbang.org/resource/image/f6/a6/f6927aab99ab49f0790f5867a7dc84a6.jpeg" alt="">
修改完表的结构之后,我们把商品名称数据填入新加的字段中,这样一来,流水表中就有了商品名称信息,不用再通过与商品信息表进行连接来获取了。
现在,如果我们要再次进行刚刚的查询,就不需要再做关联查询了,因为商品流水表里面已经有了商品名称字段:
```
mysql&gt; SELECT a.goodsname,a.quantity
-&gt; FROM demo.trans AS a
-&gt; WHERE a.transdate&gt;'2021-04-11 09:00:00'
-&gt; AND a.transdate&lt;'2021-04-11 12:00:00'
-&gt; AND a.itemnumber = 355;
+--------------+----------+
| goodsname | quantity |
+--------------+----------+
| 贵烟(跨越) | 1.000 |
| 贵烟(跨越) | 1.000 |
| 贵烟(跨越) | 1.000 |
| 贵烟(跨越) | 1.000 |
| 贵烟(跨越) | 1.000 |
+--------------+----------+
5 rows in set (6.38 sec)
```
结果显示卖了5个“贵烟跨越花费了6.38秒。查询的结果与之前相同,但是速度更快了。而且,这个查询变成了单表查询,语句也更加简单了。
不过,你要注意的一点是,这样一来,商品流水表中包含了一个冗余字段“商品名称”,不但存储空间变大了,而且,如果某个商品名称做了修改,一定要对应修改流水表里的商品名称。否则,就会出现两个表里的商品名称不一致的情况。
所以在实际的工作场景中你需要权衡增加冗余字段的利与弊。这里给你一个建议增加冗余字段一定要符合2个条件第一个是这个冗余字段不需要经常进行修改第二个是这个冗余字段查询的时候不可或缺。只有满足这两个条件才可以考虑增加冗余字段否则就不值得增加这个冗余字段了。
除了优化数据类型与合理增加冗余字段之外,我们还可以通过对大表进行拆分的方法优化查询。
## 拆分表
跟刚刚的在表中增加冗余字段的方法相反拆分表的思路是把1个包含很多字段的表拆分成2个或者多个相对较小的表。
这样做的原因是,这些表中某些字段的操作频率很高,经常要进行查询或者更新操作,而另外一些字段的使用频率却很低,如果放在一个表里面,每次查询都要读取大记录,会消耗较多的资源。
这个时候,如果把这个大表拆分开,把使用频率高的字段放在一起形成一个表,把剩下的使用频率低的字段放在一起形成一个表,这样查询操作每次读取的记录比较小,查询效率自然也就提高了。
举个小例子,比如流水单头表中包含流水单号、会员编号、收款机编号、整单折让、整单折扣、微信收款金额、支付宝收款金额、现金金额等字段。
流水单头表:
<img src="https://static001.geekbang.org/resource/image/aa/49/aa1a1e7c7711fa235b0699f307ecf949.jpeg" alt="">
我们来分析下这个表中的字段。
在这个表中,会员编号涉及会员销售,会被经常查询。收款机信息经常用于销售统计,整单折让和整单折扣用于优惠计算,也经常要被引用。
其他信息,包括微信金额、支付宝金额和现金金额,只有在财务统计收款方式的时候,才会用到。
所以我们可以把这个表拆分成2个独立的表这个表中常用的字段也就是会员编号、收款机编号、整单折扣和整单折让字段加上流水单号就是流水单头表1剩下的字段加上流水单号字段就是流水单头表2。
流水单头表1
<img src="https://static001.geekbang.org/resource/image/c0/f1/c0b37dce3c59587ffb98dc3c276758f1.jpeg" alt="">
流水单头表2
<img src="https://static001.geekbang.org/resource/image/22/c0/220a7d1a6c40c1922d1c8711fc8272c0.jpeg" alt="">
这样一来在查询的时候只需要访问比较小的流水单头表1或流水单头表2这就提高了查询的效率。
## 使用非空约束
**在设计字段的时候,如果业务允许,我建议你尽量使用非空约束**。这样做的好处是可以省去判断是否为空的开销提高存储效率。而且非空字段也容易创建索引。使用非空约束甚至可以节省存储空间每个字段1个比特
以商品信息表为例,我们可以设定所有的字段满足非空约束,如下所示:
```
mysql&gt; DESCRIBE demo.goodsmaster;
+---------------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+---------------+------+-----+---------+-------+
| itemnumber | int | NO | PRI | NULL | |
| barcode | text | NO | | NULL | |
| goodsname | text | NO | | NULL | |
| specification | text | NO | | NULL | |
| unit | text | NO | | NULL | |
| salesprice | decimal(10,2) | NO | UNI | 0.00 | |
+---------------+---------------+------+-----+---------+-------+
6 rows in set (0.01 sec)
```
这样一来,我们就省去了判断空值的开销,还能够节省一些存储空间。
## 总结
这节课,我给你介绍了几个从设计角度提升查询性能的方法:
- 修改数据类型以节省存储空间;
- 在利大于弊的情况下增加冗余字段;
- 把大表中查询频率高的字段和查询频率低的字段拆分成不同的表;
- 尽量使用非空约束。
这些都可以帮助你进一步提升系统的查询效率,让你开发的应用更加简洁高效。
但是,我要提醒你的是,这些方法都是有利有弊的,比如,修改数据类型,节省存储空间的同时,你要考虑到数据不能超过取值范围;增加冗余字段的时候,不要忘了确保数据一致性;把大表拆分,也意味着你的查询会增加新的连接,从而增加额外的开销和运维的成本。因此,你一定要结合实际的业务需求进行权衡。
## 思考题
假设我们有一个这样的订单表,如下所示:
<img src="https://static001.geekbang.org/resource/image/92/2f/92c6c071182cfb3113ae91bf586cfa2f.jpeg" alt="">
经过统计发现商品信息、订货数量和交易时间这3个字段使用得很频繁地址和电话这2个字段使用得相对比较少。针对这样的表格你会怎么优化呢
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,288 @@
<audio id="audio" title="26 | 如何充分利用系统资源?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9d/7b/9d01c6892b26ab1305ba12b63ca6607b.mp3"></audio>
你好,我是朱晓峰。
内存和CPU都是有限的资源因此把它们的作用发挥到极致对提高应用的承载能力来说至关重要。
磁盘读写需要计算位置、发出读写指令等这些都要消耗CPU资源很容易成为提升系统效能的瓶颈。
如果采取“先把数据放在内存然后集中写入磁盘”的办法可以节省CPU资源和磁盘读取的时间但是也会面临系统故障时会丢失数据的风险相反如果每次都写入磁盘数据最安全但是频繁的磁盘读写会导致系统效率低下。这就需要我们提升优化资源配置的能力。
今天,我就给你介绍一下优化系统配置的方法,同时还会讲解系统自带的监控工具,从而帮助你合理配置系统资源,精准发现系统资源的瓶颈,进一步提升你处理大并发、大数据的能力。
## 优化系统资源配置
对CPU资源的掌控关系到系统整体的成败。因为CPU资源是系统最核心的资源无可替代而且获取成本高。如果应用无法控制CPU的使用率就有可能是失败的不管你的界面多么人性化功能多么强大。
因此,我们需要管理好系统配置,把资源效率提升到极致。**系统参数控制着资源的配置,调整系统参数的值,可以帮助我们提升资源的利用效率**。
我来借助一个小例子,给你介绍下怎么通过对系统变量进行调优,来提升系统的整体效率。
我曾参与过一个点餐系统应用的开发其实就是一个为客户提供点餐服务的应用类似于美团。商家购买服务入住平台开通之后商家可以在系统中录入自己能够提供的各类餐食品种客户通过手机App、微信小程序等点餐商家接到订单以后进行制作并根据客户需求提供堂食或者送餐服务。
系统中包括订单表orderlist、客户信息表clientlist和客户储值表clientdeposit
订单表:
<img src="https://static001.geekbang.org/resource/image/94/da/946066e2e0124ba52788bb4cb31261da.jpeg" alt="">
客户信息表:
<img src="https://static001.geekbang.org/resource/image/31/72/31f03880ac8f5c3d4e048828c7240672.jpeg" alt="">
客户储值表:
<img src="https://static001.geekbang.org/resource/image/2d/5f/2dcbe35e96f520673018826ba590cd5f.jpeg" alt="">
刚刚上线的时候系统运行状态良好。但是随着入住的商家不断增多使用系统的用户量越来越多每天的订单数据达到了2万条以上。这个时候系统开始出现问题CPU使用率不断飙升。终于有一天午餐高峰的时候CPU使用率达到99%,这实际上就意味着,系统的计算资源已经耗尽,再也无法处理任何新的订单了。换句话说,系统已经崩溃了。
这个时候我们想到了对系统参数进行调整因为参数的值决定了资源配置的方式和投放的程度。为了解决这个问题我们一共调整了3个系统参数分别是**InnoDB_flush_log_at_trx_commit、InnoDB_buffer_pool_size**、**InnoDB_buffer_pool_instances。**
接下来,我就给你讲一讲怎么对这三个参数进行调整。
### 1. 调整系统参数InnoDB_flush_log_at_trx_commit
这个参数适用于InnoDB存储引擎。因为刚刚的表用的存储引擎都是InnoDB因此这个参数对我们系统的效能就有影响。需要注意的是如果你用的存储引擎不是InnoDB调整这个参数对系统性能的提升就没有什么用了。
这个参数存储在MySQL的配置文件my.ini里面默认的值是1意思是每次提交事务的时候都把数据写入日志并把日志写入磁盘。**这样做的好处是数据安全性最佳,不足之处在于每次提交事务,都要进行磁盘写入的操作**。在大并发的场景下过于频繁的磁盘读写会导致CPU资源浪费系统效率变低。
这个参数的值还有2个可能的选项分别是0和2。其中0表示每隔1秒将数据写入日志并将日志写入磁盘2表示每次提交事务的时候都将数据写入日志但是日志每间隔1秒写入磁盘。
最后我们把这个参数的值改成了2。这样一来就不用每次提交事务的时候都启动磁盘读写了在大并发的场景下可以改善系统效率降低CPU使用率。即便出现故障损失的数据也比较小。0虽然效率更高一些但是数据安全性方面不如2。
### 2. 调整系统参数InnoDB_buffer_pool_size
这个参数的意思是InnoDB存储引擎使用缓存来存储索引和数据。这个值越大可以加载到缓存区的索引和数据量就越多需要的磁盘读写就越少。
因为我们的MySQL服务器是数据库专属服务器只用来运行MySQL数据库服务没有其他应用了而我们的计算机是64位机内存也有128G。于是我们把这个参数的值调整为64G。这样一来磁盘读写次数可以大幅降低我们就可以充分利用内存释放出一些CPU的资源。
### 3. 调整系统参数InnoDB_buffer_pool_instances
这个参数的意思是将InnoDB的缓存区分成几个部分这样一来就可以提高系统的并行处理能力因为可以允许多个进程同时处理不同部分的缓存区。这就好比买电影票如果大家都挤在一个窗口、一个接一个地进行交易效率肯定是很慢的。如果一次开很多售票窗口多笔交易同时进行那速度就快得多了。
我们把InnoDB_buffer_pool_instances的值修改为64意思就是把InnoDB的缓存区分成64个分区这样就可以同时有多个进程进行数据操作CPU的效率就高多了。
修改好了系统参数的值我们需要重新保存MySQL的配置文件my.ini并且重启MySQL数据库服务器。
这里有个坑你要注意由于my.ini文件是文本格式文件你完全可以用记事本对文件进行修改操作。但是如果你只是简单地进行保存就会发现MySQL服务器停止之后再次启动时没有响应服务器起不来了。其实这就是文件的码制出了问题。
**记事本保存文件默认的码制是UTF-8但配置文件的码制必须是ANSI才行**。所以,当**你修改完MySQL的配置文件my.ini之后保存的时候记得用ANSI的格式**。如下图所示:
<img src="https://static001.geekbang.org/resource/image/d3/9e/d315593c7d73ccc706fd0ccbc2977a9e.png" alt="">
经过我们对系统参数的调整重启MySQL服务器之后系统效率提高了CPU资源的使用率下来了系统得以正常运行。
咱们来小结下。CPU资源是系统的核心资源获取成本非常高。CPU的特点就是阻塞只要CPU一开始计算就意味着等待。遇到CPU资源不足的问题可以从2个思路去解决
1. 疏通拥堵路段,消除瓶颈,让等待的时间更短;
1. 开拓新的通道,增加并行处理能力。
刚刚的调优思路其实就是围绕着这2个点展开的。如果遇到CPU资源不足的问题我建议你也从这2个角度出发去思考解决办法。
## 如何利用系统资源来诊断问题?
在刚刚的例子中我提到了解决CPU资源不足需要消除瓶颈。而消除瓶颈的第一步就是要发现它。如何发现呢幸运的是MySQL提供了很好的工具**Performance Schema**。
这是一种专门用来监控服务器执行情况的存储引擎它会把监控服务器执行情况的数据记录在系统自带的数据库performance_schema中。我们可以利用监控的数据对服务器中执行查询的问题进行诊断。
我还是以刚刚的那个点餐系统为例,来解释一下。
当我们调整完系统参数之后系统恢复了运行。可是随着数据量的不断增大单日订单量超过20万我们再次遇到了问题CPU飙升到99%系统无法工作了。这个时候我们就可以利用performance_schema记录的监控数据来发现问题。
我先讲一讲怎么让Performance Schema监控查询执行事件并且把我们需要的监控数据记录下来。
### 如何启用系统监控?
系统数据库performance_schema中的表setup_instruments和setup_consumers中的数据是启用监控的关键。
setup_instruments保存的数据表示哪些对象发生的事件可以被系统捕获在MySQL中把这些事件称作信息生产者
我们可以通过下面的代码来查看一下当前MySQL会监控哪些事件的信息
```
mysql&gt; SELECT NAME,ENABLED,TIMED
-&gt; FROM performance_schema.setup_instruments
-&gt; LIMIT 1,10;
+---------------------------------------------------------+---------+-------+
| NAME | ENABLED | TIMED |
+---------------------------------------------------------+---------+-------+
| wait/synch/mutex/sql/TC_LOG_MMAP::LOCK_tc | YES | YES |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_commit | YES | YES |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_commit_queue | YES | YES |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_done | YES | YES |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_flush_queue | YES | YES |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_index | YES | YES |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_log | YES | YES |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_binlog_end_pos | YES | YES |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_sync | YES | YES |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_sync_queue | YES | YES |
+---------------------------------------------------------+---------+-------+
10 rows in set (0.00 sec)
```
表的内容有很多为了方便演示这里我只查询了前10条数据。在这个表中你要注意3个字段。
- NAME表示事件的名称
- ENABLED表示是否启用了对这个事件的监控
- TIMED表示是否收集事件的时间信息。
这里我们启用所有事件的监控,以便诊断问题:
```
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES';
```
setup_instruments表中的数据指定了是否对某一事件进行监控而setup_consumers这个表保存的数据则指定了是否保存监控事件发生的信息。在MySQL中Performance Schema把监控到的事件信息存入performance_schema中的数据表中这些数据表保存了事件的监控信息扮演了事件监控信息消费者的角色被称为消费者。
下面我们来修改设置使系统保存所有事件信息并了解一下setup_consumers这个表里都能够控制保存哪些事件的信息。我在数据旁边加了注释来说明保存的事件信息的种类。
```
UPDATE performance_schema.setup_consumers
SET ENABLED = 'YES'
mysql&gt; SELECT *
-&gt; FROM performance_schema.setup_consumers
-&gt; ;
+----------------------------------+---------+
| NAME | ENABLED |
+----------------------------------+---------+
| events_stages_current | YES | -- 当前阶段
| events_stages_history | YES | -- 阶段历史
| events_stages_history_long | YES | -- 阶段长历史
| events_statements_current | YES | -- 当前语句
| events_statements_history | YES | -- 历史语句
| events_statements_history_long | YES | -- 长历史语句
| events_transactions_current | YES | -- 当前事务
| events_transactions_history | YES | -- 历史事务
| events_transactions_history_long | YES | -- 长历史事务
| events_waits_current | YES | -- 当前等待
| events_waits_history | YES | -- 等待历史
| events_waits_history_long | YES | -- 等待长历史
| global_instrumentation | YES |
| thread_instrumentation | YES |
| statements_digest | YES |
+----------------------------------+---------+
15 rows in set (0.01 sec)
```
更新之后,所有种类事件信息的保存都已经启用。这样一来,所有查询事件都能够被监控,事件的相关信息也都能够被记录下来了。如果查询中出现问题,那么,异常信息就会被记录下来。接下来,我们就利用系统监控到的信息来诊断一下问题。
### 利用监控信息诊断问题
为了利用保存下来的监控事件信息来诊断系统问题,我先介绍一下几个保存监控信息数据的系统数据表。
**第1个表是performance_schema.events_statements_current。**
这个表中记录的是当前系统中的查询事件。表中的每一行对应一个进程,一个进程只有一行数据,显示的是每个进程中被监控到的查询事件。
**第2个表是performance_schema.events_statements_history。**
这个表中记录了系统中所有进程中最近发生的查询事件。这个表中包含的查询事件都是已经完成了的。另外,表中可以为每个进程保存的最大记录数由系统变量决定。
下面的代码可以查询当前系统中可以为每个进程保存的最大记录数的值:
```
mysql&gt; show variables like '%performance_schema_events_statements_history_size%'
-&gt; ;
+---------------------------------------------------+-------+
| Variable_name | Value |
+---------------------------------------------------+-------+
| performance_schema_events_statements_history_size | 10 | -- 最多10条
+---------------------------------------------------+-------+
1 row in set, 1 warning (0.01 sec)
```
结果显示当前的设定是这个表中为每个进程保留最多10条记录。
**我要说的第3个表是performance_schema.events_statements_history_long。**
这个表中记录了系统中所有进程中最近发生的查询事件,表中包含的查询事件都是已经完成了的。同时,这个表中可以保存的记录数由系统变量决定。下面的代码用来查询当前系统设置的这个表可以保存的最大记录数。
```
mysql&gt; show variables like '%performance_schema_events_statements_history_long_size%'
-&gt; ;
+--------------------------------------------------------+-------+
| Variable_name | Value |
+--------------------------------------------------------+-------+
| performance_schema_events_statements_history_long_size | 10000 |
+--------------------------------------------------------+-------+
1 row in set, 1 warning (0.00 sec)
```
结果显示当前系统的设定是这个表中最多保留10000条记录。
知道了这几个保存事件信息的系统数据表,我们重新回到项目中来,看看如何利用这几个系统数据表中的信息,来发现问题。
我们先用下面的语句来看一下哪些查询消耗的时间多。这个表的数据量很大为了方便你查看我只让它显示最初的2行数据。
```
mysql&gt; SELECT
-&gt; TRUNCATE(TIMER_WAIT / 1000000000000, 6) AS duration, -- 计算查询的时长,单位是微微秒,需要转换成秒
-&gt; sql_text,
-&gt; EVENT_ID
-&gt; FROM
-&gt; performance_schema.events_statements_history_long
-&gt; WHERE
-&gt; TRUNCATE(TIMER_WAIT / 1000000000000, 6) &lt;&gt; 0
-&gt; AND sql_text IS NOT NULL
-&gt; ORDER BY TRUNCATE(TIMER_WAIT / 1000000000000, 6) DESC
-&gt; LIMIT 1,2;
+----------+---------------------------------+----------+
| duration | sql_text | EVENT_ID |
+----------+---------------------------------+----------+
| 137.2529 | select count(*) from demo.trans | 17 |
| 137.2420 | select count(*) from demo.trans | 907 |
+----------+---------------------------------+----------+
2 rows in set (0.00 sec)
```
结果显示持续时间最长的2个事件分别是事件编号为17和907的事件。这样一来我们就可以发现到底是哪些查询消耗了最多的CPU资源就可以有针对性地进行优化了。
下面我具体给你解释一下这个查询。
1. 字段TIMER_WAIT表示这个查询消耗了多少时间单位是微微秒也就是万亿分之一秒。
1. TRUNCATE(X,D)函数表示给X保留D位小数注意这个函数是直接截取没有四舍五入。
1. 字段sql_text表示执行的SQL语句的内容。
1. EVENT_ID表示事件编号。
这个查询的意思是按照查询花费时间多少排序查询出花费时间最多的2个查询的事件编号、执行时长和查询的内容。
通过查询这个表的内容我们发现有大量的查询出现在执行时间较长的列表中这些查询都涉及数据表demo.clientdeposit。
为了支持顾客储值消费我们新加了这个表但是这个表的使用频率很高数据量较大我们却忘记了创建索引这就导致了CPU资源不足。确定了问题解决起来就很容易了创建了索引之后CPU资源消耗直接下降到了20%左右。
总之Performance Schema是一种很好的工具可以监控到MySQL服务器内部执行的信息。如果你遇到查询中出现难以解决的问题就可以调取数据库performance_shema中的监控数据分析服务器中的执行情况从而定位和解决问题。
## 总结
这节课,我们学习了通过系统参数来配置资源、提高查询效率的方法。
- 系统参数InnoDB_flush_log_at_trx_commit适用于InnoDB存储引擎。默认的值是1意思是每次提交事务的时候都把数据写入日志并把日志写入磁盘。0表示每隔1秒将数据写入日志并将日志写入磁盘。2表示每次事务提交的时候将数据写入日志但是日志每隔1秒写入磁盘。
- 系统参数InnoDB_buffer_pool_size表示InnoDB存储引擎使用多少缓存来存储索引和数据。这个值越大可以加载到缓存区的索引和数据量就越多需要的磁盘读写就越少。
- 系统参数InnoDB_buffer_pool_instances的意思是将InnoDB的缓存区分成几个部分可以提高系统的并行处理能力。
而且我还给你介绍了怎么通过使用Performance Schema来监控服务器运行中的事件。系统数据库performance_schema中的表setup_instruments和setup_consumers中的数据是启用监控的关键。
- setup_instruments保存的数据表示哪些对象发生的事件可以被系统捕获。
- setup_consumers保存的数据用来控制保存哪些事件的信息。
我们可以通过修改这两个表的内容来开启系统监控,并且把监控数据保存到相应的数据表中,方便我们对查询的执行情况进行诊断。
最后我还想额外给你一个建议如果你遇到了因为无法控制CPU的使用率而导致系统崩溃的情况首先应该想到的**是应用本身有缺陷**然后找到自身的问题并加以解决而不是增加系统资源的投入采购功能强大的CPU和扩大内存等。
原因有2个资源永远是有限的如何在有限的资源前提下提高系统的承载能力是我们应该首先考虑的资源的投入都是经过谨慎评估的除非你有充足的理由确信同等条件下你开发的应用承载能力已经超过了业界最高水平否则就说明应用本身还有提升的空间。
所以课下你一定要把今天的内容多学几遍一定要掌握通过MySQL自身的事件监控机制来诊断问题的方法它可以帮助你找到真正的瓶颈。
## 思考题
前面提到我把InnoDB_flush_log_at_trx_commit的值改成了2因为0虽然效率更高一些但是在数据安全性方面不如2。你知道为什么0的效率更高一些但是数据安全性却不如2吗
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,327 @@
<audio id="audio" title="01 | 存储:一个完整的数据存储过程是怎样的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/72/42/72210e06a140aa00b4d20fb60dd45542.mp3"></audio>
你好我是朱晓峰。今天我想跟你聊一聊MySQL是怎么存储数据的。
存储数据是处理数据的第一步。在咱们的超市项目中,每天都要处理大量的商品,比如说进货、卖货、盘点库存,商品的种类很多,而且数量也比较大。只有正确地把数据存储起来,我们才能进行有效的处理和分析,进而对经营情况进行科学的评估,超市负责人在做决策时,就能够拿到数据支持。否则,只能是一团乱麻,没有头绪,也无从着手。
那么,怎样才能把用户各种经营相关的、纷繁复杂的数据,有序和高效地存储起来呢?
在MySQL中一个完整的**数据存储过程总共有4步分别是创建数据库、确认字段、创建数据表、插入数据。**
<img src="https://static001.geekbang.org/resource/image/8b/b1/8b8b594631a175e3016686da88d569b1.jpg" alt="">
接下来我就给你详细讲解一下这个过程的每一步帮你掌握MySQL的数据存储机制。
先提醒你一句,这节课最后有一个视频,我在视频里演示了今天讲到的所有操作。我建议你学完文字以后,跟着视频实操一下。
好了,话不多说,我们现在开始。
## 创建MySQL数据库
**数据存储的第一步,就是创建数据库**
你可能会问,为啥我们要先创建一个数据库,而不是直接创建数据表呢?
这是个很好的问题。其实啊这是因为从系统架构的层次上看MySQL数据库系统从大到小依次是数据库服务器、数据库、数据表、数据表的行与列。
安装程序已经帮我们安装了MySQL数据库服务器所以我们必须从创建数据库开始。
**数据库是MySQL里面最大的存储单元**。数据表、数据表里的数据,以及我们以后会学到的表与表之间的关系,还有在它们的基础上衍生出来的各种工具,都存储在数据库里面。**没有数据库,数据表就没有载体,也就无法存储数据。**
下面我就来给你具体介绍下怎么在我们安装的MySQL服务器里面创建、删除和查看数据库。
### 1.如何创建数据库?
创建数据库我们已经在上节课介绍过了你可以在Workbench的工作区通过下面的SQL语句创建数据库“demo”
```
CREATE DATABASE demo
```
### 2.如何查看数据库?
下面我们来看一下,如何查看数据库。
在Workbench的导航栏我们可以看到数据库服务器里的所有数据库如下图所示
<img src="https://static001.geekbang.org/resource/image/da/9e/da02212629a3084da3ee3d67d982fa9e.png" alt="">
你也可以在Workbench右边的工作区通过查询语句查看所有的数据库
```
mysql&gt; SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| demo |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.00 sec)
```
看到这儿你是不是觉得很奇怪为什么Workbench导航栏里面的数据库只有两个我们创建的数据库“demo”和安装完MySQL就有的数据库“sys”
换句话说为什么有的数据库我们可以在Workbench里面看到有的数据库却必须通过查询语句才可以看到呢要弄明白这个问题你必须要知道这些数据库都是干什么的。
- “demo”是我们通过SQL语句创建的数据库是我们用来存储用户数据的也是我们使用的主要数据库。
- “information_schema”是MySQL系统自带的数据库主要保存MySQL数据库服务器的系统信息比如数据库的名称、数据表的名称、字段名称、存取权限、数据文件所在的文件夹和系统使用的文件夹等等。
- “performance_schema”是MySQL系统自带的数据库可以用来监控MySQL的各类性能指标。
- “sys”数据库是MySQL系统自带的数据库主要作用是以一种更容易被理解的方式展示MySQL数据库服务器的各类性能指标帮助系统管理员和开发人员监控MySQL的技术性能。
- “mysql”数据库保存了MySQL数据库服务器运行时需要的系统信息比如数据文件夹、当前使用的字符集、约束检查信息等等。
如果你是DBA或者是MySQL数据库程序员想深入了解MySQL数据库系统的相关信息可以看下[官方文档](https://dev.mysql.com/doc/refman/8.0/en/system-schema.html)。
话说回来为什么Workbench里面我们只能看到“demo”和“sys”这2个数据库呢其实啊这是因为Workbench是图形化的管理工具主要面向开发人员“demo”和“sys”这2个数据库已经够用了。如果有特殊需求比如需要监控MySQL数据库各项性能指标、直接操作MySQL数据库系统文件等可以由DBA通过SQL语句查看其它的系统数据库。
## 确认字段
**数据存储流程的第二步是确认表的字段**
创建好数据库之后我们选择要导入的Excel数据文件MySQL会让我们确认新表中有哪些列以及它们的数据类型。这些列就是MySQL数据表的字段。
MySQL数据表由行与列组成一行就是一条数据记录每一条数据记录都被分成许多列一列就叫一个字段。每个字段都需要定义数据类型这个数据类型叫做字段类型。
<img src="https://static001.geekbang.org/resource/image/d1/b0/d17b6e27ce7e64a7c9e7c9c6938c50b0.png" alt="">
这样一来每一条数据记录的每一个片段就按照字段的定义被严格地管理起来了从而使数据有序而且可靠。MySQL支持多种字段类型字段的定义会影响数据的取值范围、精度以及系统的可靠性下节课我会重点给你讲一讲字段的定义。这里你只要选择系统默认的字段类型就可以了。
## 创建数据表
**数据存储流程的第三步,是创建数据表。**
当我们确认好了表的字段点击下一步Workbench就帮助我们创建了一张表。
MySQL中的数据表是什么呢**你可以把它看成用来存储数据的最主要工具**。数据表对存储在里面的数据进行组织和管理,使数据变得有序,并且能够实现高效查询和处理。
虽然Workbench帮助我们创建了一个表但大多数情况下我们是不会先准备一个Excel文件再通过Workbench的数据导入来创建表的这样太麻烦了。**我们可以通过SQL语句自己来创建表。**
具体咋做呢?我来介绍一下。
首先在Workbench的工作区输入以下SQL语句
```
CREATE TABLE demo.test
(
barcode text,
goodsname text,
price int
);
```
执行这个SQL语句之后就能创建出一个与导入的Excel表一样的MySQL数据表了。
这里有2点需要你格外注意一下。
- **创建表的时候,最好指明数据库**。否则如果你没有选中数据库Workbench会提示错误要是你当前选中的数据库不对还可能把表创建到错误的数据库中。
- **不要在最后一个字段的后面加逗号“,”,这也是初学者容易犯的错误**。
下面我们就来聊一聊,查看数据表的结构、查看数据库中的表的方法。
### 1.如何查看表的结构?
创建好了表,再来看看如何查看表的结构。
我们运行下面的SQL语句
```
DESCRIBE demo.test;
```
运行结果如下:
```
mysql&gt; DESCRIBE demo.test;
+-----------+------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+------+------+-----+---------+-------+
| barcode | text | YES | | NULL | |
| goodsname | text | YES | | NULL | |
| price | int | YES | | NULL | |
+-----------+------+------+-----+---------+-------+
3 rows in set (0.00 sec)
```
我来解释下这些信息都代表什么意思。
- Field表示字段名称。
- Type表示字段类型这里barcode、goodsname是文本型的price是整数类型的。
- Null表示这个字段是否允许是空值NULL。这里你一定要注意在MySQL里面空值不等于空字符串。一个空字符串的长度是0而一个空值的长度是空。而且在MySQL里面空值是占用空间的。
- Key我们暂时把它叫做键。
- Default表示默认值。我们导入的表的所有的字段都允许是空默认值都是NULL。
- Extra表示附加信息。
关于字段类型和Key后面我会具体讲解这里你只需要知道它们的含义就可以了。
### 2.如何查看数据库中的表?
创建完成后,怎么查看表有没有真的被创建出来呢?
我们可以通过Workbench的导航栏点击数据库下面的“Tables”找到这个数据库中的所有数据表。另外我们也可以在工作区通过SQL语句查询某个数据库中的数据表。
先用USE语句选择数据库
```
USE demo;
```
运行后进入demo数据库然后用SHOW语句查询这个数据库中所有的表
```
SHOW TABLES;
```
运行后结果如下:
```
mysql&gt; SHOW TABLES;
+----------------+
| Tables_in_demo |
+----------------+
| test |
+----------------+
1 row in set (0.00 sec)
```
这样我们就可以看到数据库“demo”里面只有一个数据表“test”。
### 3.如何设置主键?
讲到了数据表,我就一定要给你讲一讲主键。**因为主键可以确保数据的唯一性,而且能够减少数据错误**。
举个简单的小例子。主键就像是咱们的身份证号码,它是唯一的。每个身份证号码只对应唯一的一个人。同样,每一个人只有唯一的身份证号码。
MySQL中数据表的主键是表中的一个字段或者几个字段的组合。它主要有3个特征
- 必须唯一,不能重复;
- 不能是空;
- 必须可以唯一标识数据表中的记录。
一个MySQL数据表中只能有一个主键。虽然MySQL也允许创建没有主键的表但是**我建议你一定要给表定义主键,并且养成习惯。因为主键可以帮助你减少错误数据,并且提高查询的速度**(后面我会专门用一节课的时间介绍“怎么用好主键”)。
我来举个例子,假设我们有这样一张表:
<img src="https://static001.geekbang.org/resource/image/ca/0e/ca3b0c3f2e7b1de8065a5efb427bc70e.jpg" alt="">
我们给它起个名字叫“goodsmaster”意思是商品表。说到这儿你可能注意到了我的表名、字段名都用的是英文。其实MySQL也允许数据表名称、字段名称用中文但我还是**建议你用英文原因有2个一是书写方便二是不容易出错**。如果用汉字,涉及到编码问题,就会容易出现错误。
那么,在这个表里,哪个字段是主键呢?
商品名称“goodsname”行不行呢不行原因是重名的商品太多了比如“笔”大家都可以生产一种叫“笔”的商品各种各样的笔不同规格的比如长的、短的不同材料的比如墨水的、铅芯的……由于可能重复商品名称和数据记录之间不能形成一一对应的关系所以“goodsname”不能作为主键。同样价格“price”重复的可能性很大也不能做主键。
商品条码“barcode”能不能成为这个数据表的主键呢
好像可以。商品的条码都是由中国物品编码中心统一编制的,一种商品对应一个条码,一个条码对应一种商品。这不就是一一对应的关系吗?
在实际操作中,有例外的情况。比较典型的就是用户的门店里面有很多自己生产或者加工的商品。比如,馒头、面条等自产食品,散装的糕点、糖果等称重商品,等等。为了管理方便,门店往往会自己给它们设置条码。这样,很容易产生重复、重用的现象。
<img src="https://static001.geekbang.org/resource/image/06/38/061b762a72bb2999ac3ae3da54e54638.jpg" alt="">
这么说商品条码“barcode”也有重复的可能也不能用做主键。
那么,如果数据表中所有的字段都有重复的可能,我们怎么设置主键呢?答案是**我们可以自己添加一个不会重复的字段来做主键**。
比如在上面的例子中我们就可以添加一个字段字段类型是整数我们给它取个名字叫商品编号“itemnumber”。而且我们可以每次增加一条新数据的时候让这个字段的值自动加1这样就永远不会重复了如下表所示
<img src="https://static001.geekbang.org/resource/image/84/4c/849157b071fcc6d8c7d28c9a1aaf1b4c.jpg" alt="">
我们添加字段商品编号“itemnumber”为主键这样我们的商品表“goodsmaster”就有了主键。
在Workbench中我们可以通过修改表结构来增加一个主键字段
<img src="https://static001.geekbang.org/resource/image/fe/a7/febd6a85383f2b7de02b59eb4fe3f6a7.png" alt="">
你也可以通过一条SQL语句修改表的结构来增加一个主键字段
```
ALTER TABLE demo.test
ADD COLUMN itemnumber int PRIMARY KEY AUTO_INCREMENT;
```
我简单解释下这些关键字的含义。
- ALTER TABLE表示修改表
- ADD COLUMN表示增加一列
- PRIMARY KEY表示这一列是主键
- AUTO_INCREMENT表示每增加一条记录这个值自动增加。一会儿讲到添加数据的时候我还会详细介绍一下它。
## 插入数据
**数据存储流程的第四步,也是最后一步,是把数据插入到表当中去。**
Workbench的数据导入功能可以帮助我们把Excel的数据导入到表里面那么我们自己怎么向数据表中插入一条数据呢我们可以借助SQL语句。
```
INSERT INTO demo.test
(barcode,goodsname,price)
VALUES ('0001','本',3);
```
这里的INSERT INTO表示向demo.test中插入数据后面是要插入数据的字段名VALUES表示对应的值。
在添加数据的时候有2点需要你格外注意一下。
1. 要插入数据的字段名也可以不写,但是我建议你不要怕麻烦,**一定要每次都写**。这样做的好处是可读性好,不易出错,而且容易修改。否则,如果你记不住表的字段,就只能去查表的结构,才能知道值所对应的字段了。
1. 由于字段itemnumber定义了AUTO_INCREMENT所以我们插入一条记录的时候不给它赋值系统也会自动给它赋值。而且每次赋值都会在上次的赋值基础上自动增加1。你也可以在插入一条记录的时候给itemnumber 赋值由于它是主键新的值必须与已有记录的itemnumber值不同否则系统会提示错误。
最后,我还专门录制了一段视频,把今天讲到的实际操作演示了一遍,你可以跟着视频再演练下。
<video poster="https://media001.geekbang.org/4b8954c165a84537b7cdbd5f54d23755/snapshots/322a5fe009b34275989e4f0682bebd58-00005.jpg" preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/7e27d07d27d407ebcc195a0e78395f55/4d6e2c6f-17810d406a4-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src=" https://media001.geekbang.org/f3b7a5474307482d8387645e348dccf9/4342ca9da931447eae4723d1901e9913-d1b210472a78fe4f269073cc7af8891a-sd.m3u8" type="application/x-mpegURL"></video>
## 总结
今天,我们学习了数据存储的完整过程,包括创建数据库、创建数据表、确认字段和插入数据。建议你跟着文字和视频实际操作一下,熟练掌握存储数据的方法。
在进行具体操作的时候我们会用到8种SQL语句我再给你汇总下。
```
-- 创建数据库
CREATE DATABASE demo
-- 删除数据库
DROP DATABASE demo
-- 查看数据库
SHOW DATABASES;
-- 创建数据表:
CREATE TABLE demo.test
(
barcode text,
goodsname text,
price int
);
-- 查看表结构
DESCRIBE demo.test;
-- 查看所有表
SHOW TABLES;
-- 添加主键
ALTER TABLE demo.test
ADD COLUMN itemnumber int PRIMARY KEY AUTO_INCREMENT;
-- 向表中添加数据
INSERT INTO demo.test
(barcode,goodsname,price)
VALUES ('0001','本',3);
```
最后我还想再给你讲一讲MySQL中SQL语句的书写规范。
MySQL以分号来识别一条SQL语句结束所以**你写的每一条SQL语句的最后都必须有一个分号否则MySQL会认为这条语句没有完成提示语法错误**。
所以我建议你写在SQL语句时遵循统一的样式以增加可读性减少错误。如果你不是很清楚具体的规范可以点击这个[链接](https://www.sqlstyle.guide/zh/)学习下。
## 思考题
我想请你思考一下,假设用户现在要销售商品,你能不能帮它设计一个销售表,把销售信息(商品名称、价格、数量、金额等)都保存起来?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,295 @@
<audio id="audio" title="02 | 字段:这么多字段类型,该怎么定义?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/67/7c/673b3e489c777yy4b193b61fedeb727c.mp3"></audio>
你好,我是朱晓峰。
MySQL中有很多字段类型比如整数、文本、浮点数等等。如果类型定义合理就能节省存储空间提升数据查询和处理的速度相反如果数据类型定义不合理就有可能会导致数据超出取值范围引发系统报错甚至可能会出现计算错误的情况进而影响到整个系统。
之前我们就遇到过这样一个问题在销售流水表中需要定义商品销售的数量。由于有称重商品不能用整数我们想当然地用了浮点数为了确保精度我们还用了DOUBLE类型。结果却造成了在没有找零的情况下客人无法结账的重大错误。经过排查我们才发现原来DOUBLE类型是不精准的不能使用。
你看,准确地定义字段类型,不但关系到数据存储的效率,而且会影响整个信息系统的可靠性。所以,我们必须要掌握不同字段的类型,包括它们的适用场景、定义方法,这节课,我们就聊一聊这个问题。
首先我要说的是MySQL中最简单的数据类型整数类型。
## 整数类型
整数类型一共有5种包括TINYINT、SMALLINT、MEDIUMINT、INTINTEGER和BIGINT它们的区别如下表所示
<img src="https://static001.geekbang.org/resource/image/dd/68/dd11099e29ae339f605a222b5b194368.jpg" alt="">
这么多整数类型,咱们该怎么选择呢?
其实,在评估用哪种整数类型的时候,你**需要考虑存储空间和可靠性的平衡问题**:一方面,用占用字节数少的整数类型可以节省存储空间;另一方面,要是为了节省存储空间,使用的整数类型取值范围太小,一旦遇到超出取值范围的情况,就可能引起系统错误,影响可靠性。
举个例子在我们的项目中商品编号采用的数据类型是INT。
我们之所以没有采用占用字节更少的SMALLINT类型整数原因就在于客户门店中流通的商品种类较多而且每天都有旧商品下架新商品上架这样不断迭代日积月累。如果使用SMALLINT类型虽然占用字节数比INT类型的整数少但是却不能保证数据不会超出范围65535。相反使用INT就能确保有足够大的取值范围不用担心数据超出范围影响可靠性的问题。
你要注意的是,在实际工作中,系统故障产生的成本远远超过增加几个字段存储空间所产生的成本。因此,我建议你首先**确保数据不会超过取值范围**,在这个前提之下,再去考虑如何节省存储空间。
接下来,我再给你介绍下浮点数类型和定点数类型。
## 浮点数类型和定点数类型
浮点数和定点数类型的特点是可以处理小数,你可以把整数看成小数的一个特例。因此,浮点数和定点数的使用场景,就比整数大多了。
我们先来了解下MySQL支持的浮点数类型分别是FLOAT、DOUBLE、REAL。
- FLOAT表示单精度浮点数
- DOUBLE表示双精度浮点数
- REAL默认就是DOUBLE。如果你把SQL模式设定为启用“REAL_AS_FLOAT”那么MySQL就认为REAL是FLOAT。如果要启用“REAL_AS_FLOAT”就可以通过以下SQL语句实现
```
SET sql_mode = “REAL_AS_FLOAT”;
```
FLOAT和DOUBLE这两种数据类型的区别是啥呢其实就是FLOAT占用字节数少取值范围小DOUBLE占用字节数多取值范围也大。
<img src="https://static001.geekbang.org/resource/image/13/64/13d20b6f3a8a8d7ed4274d9b7a251c64.jpg" alt="">
看到这儿,你有没有发现一个问题:为什么浮点数类型的无符号数取值范围,只相当于有符号数取值范围的一半,也就是只相当于有符号数取值范围大于等于零的部分呢?
其实这里的原因是MySQL是按照这个格式存储浮点数的符号S、尾数M和阶码E。因此无论有没有符号MySQL的浮点数都会存储表示符号的部分。因此所谓的无符号数取值范围其实就是有符号数取值范围大于等于零的部分。
不过,我要提醒你的是,**浮点数类型有个缺陷,就是不精准**。因此在一些对精确度要求较高的项目中千万不要使用浮点数不然会导致结果错误甚至是造成不可挽回的损失。下面我来重点解释一下为什么MySQL的浮点数不够精准。
为了方便你理解,我来借助一个实际的例子演示下。
我们先创建一个表,如下所示:
```
CREATE TABLE demo.goodsmaster
(
barcode TEXT,
goodsname TEXT,
price DOUBLE,
itemnumber INT PRIMARY KEY AUTO_INCREMENT
);
```
运行这个语句我们就创建了一个表其中的字段“price”就是浮点数类型。我们再通过下面的SQL语句给这个表插入几条数据
```
-- 第一条
INSERT INTO demo.goodsmaster
(
barcode,
goodsname,
price
)
VALUES
(
'0001',
'书',
0.47
);
-- 第二条
INSERT INTO demo.goodsmaster
(
barcode,
goodsname,
price
)
VALUES
(
'0002',
'笔',
0.44
);
-- 第三条
INSERT INTO demo.goodsmaster
(
barcode,
goodsname,
price
)
VALUES
(
'0002',
'胶水',
0.19
);
```
现在,我们运行一个查询语句,看看现在表里数据的情况:
```
SELECT * from demo.goodsmaster;
```
这个时候,我们可以得到下面的结果:
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+---------+-----------+-------+------------+
| barcode | goodsname | price | itemnumber |
+---------+-----------+-------+------------+
| 0001 | 书 | 0.47 | 1 |
| 0002 | 笔 | 0.44 | 2 |
| 0002 | 胶水 | 0.19 | 3 |
+---------+-----------+-------+------------+
3 rows in set (0.00 sec)
```
然后我们用下面的SQL语句把这3个价格加在一起看看得到了什么
```
SELECT SUM(price)
FROM demo.goodsmaster;
```
这里我们又用到一个关键字SUM这是MySQL中的求和函数是MySQL聚合函数的一种你只要知道这个函数表示计算字段值的和就可以了。
我们期待的运行结果是0.47 + 0.44 + 0.19 = 1.1,可是,我们得到的是:
```
mysql&gt; SELECT SUM(price)
-&gt; FROM demo.goodsmaster;
+--------------------+
| SUM(price) |
+--------------------+
| 1.0999999999999999 |
+--------------------+
```
查询结果是1.0999999999999999。看到了吗?虽然误差很小,但确实有误差。
你也可以尝试把数据类型改成FLOAT然后运行求和查询得到的是1.0999999940395355。显然,误差更大了。
虽然1.10和1.0999999999999999好像差不多,但是我们有时候需要以通过数值对比为条件进行查询,一旦出现误差,就查不出需要的结果了:
```
SELECT *
FROM demo.goodsmaster
WHERE SUM(price)=1.1
```
那么,为什么会存在这样的误差呢?问题还是**出在MySQL对浮点类型数据的存储方式上**。
MySQL用4个字节存储FLOAT类型数据用8个字节来存储DOUBLE类型数据。无论哪个都是采用二进制的方式来进行存储的。比如9.625用二进制来表达就是1001.101或者表达成1.001101×2^3。看到了吗如果尾数不是0或5比如9.624),你就无法用一个二进制数来精确表达。怎么办呢?就只好在取值允许的范围内进行近似(四舍五入)。
现在你一定明白了为什么数据类型是DOUBLE的时候我们得到的结果误差更小一些而数据类型是FLOAT的时候误差会更大一下。原因就是DOUBLE有8位字节精度更高。
说到这里,我想你已经彻底理解了浮点数据类型不精准的原因了。
那么MySQL有没有精准的数据类型呢当然有这就是**定点数类型DECIMAL**。
就像浮点数类型的存储方式决定了它不可能精准一样DECIMAL的存储方式决定了它一定是精准的。
浮点数类型是把十进制数转换成二进制数存储DECIMAL则不同它是把十进制数的整数部分和小数部分拆开分别转换成十六进制数进行存储。这样所有的数值就都可以精准表达了不会存在因为无法表达而损失精度的问题。
MySQL用DECIMALM,D的方式表示高精度小数。其中M表示整数部分加小数部分一共有多少位M&lt;=65。D表示小数部分位数D&lt;M。
下面我们就用刚才的表demo.goodsmaster验证一下。
首先我们运行下面的语句把字段“price”的数据类型修改为DECIMAL(5,2)
```
ALTER TABLE demo.goodsmaster
MODIFY COLUMN price DECIMAL(5,2);
```
然后,我们再一次运行求和语句:
```
SELECT SUM(price)
FROM demo.goodsmaster;
```
这次我们得到了完美的结果1.10。
由于DECIMAL数据类型的精准性在我们的项目中除了极少数比如商品编号用到整数类型外其他的数值都用的是DECIMAL原因就是这个项目所处的零售行业要求精准一分钱也不能差。
当然,在一些对精度要求不高的场景下,比起占用同样的字节长度的定点数,浮点数表达的数值范围可以更大一些。
简单小结下浮点数和定点数的特点:浮点类型取值范围大,但是不精准,适用于需要取值范围大,又可以容忍微小误差的科学计算场景(比如计算化学、分子建模、流体动力学等);定点数类型取值范围相对小,但是精准,没有误差,适合于对精度要求极高的场景(比如涉及金额计算的场景)。
## 文本类型
在实际的项目中我们还经常遇到一种数据就是字符串数据。比如刚刚那个简单的表demo.goodsmaster中有两个字段“barcode”和“goodsname”。它们当中存储的条码、商品名称都是字符串数据。这两个字段的数据类型我们都选择了TEXT类型。
TEXT类型是MySQL支持的文本类型的一种。此外MySQL还支持CHAR、VARCHAR、ENUM和SET等文本类型。我们来看看它们的区别。
- CHAR(M)固定长度字符串。CHAR(M)类型必须预先定义字符串长度。如果太短,数据可能会超出范围;如果太长,又浪费存储空间。
- VARCHAR(M) 可变长度字符串。VARCHAR(M)也需要预先知道字符串的最大长度,不过只要不超过这个最大长度,具体存储的时候,是按照实际字符串长度存储的。
- TEXT字符串。系统自动按照实际长度存储不需要预先定义长度。
- ENUM 枚举类型,取值必须是预先设定的一组字符串值范围之内的一个,必须要知道字符串所有可能的取值。
- SET是一个字符串对象取值必须是在预先设定的字符串值范围之内的0个或多个也必须知道字符串所有可能的取值。
对于ENUM类型和SET类型来说你必须知道所有可能的取值所以只能用在某些特定场合比如某个参数设定的取值范围只有几个固定值的场景。
因为不需要预先知道字符串的长度系统会按照实际的数据长度进行存储所以TEXT类型最为灵活方便所以下面我们重点学习一下它。
TEXT类型也有4种它们的区别就是最大长度不同。
- TINYTEXT255字符这里假设字符是ASCII码一个字符占用一个字节下同
- TEXT 65535字符。
- MEDIUMTEXT16777215字符。
- LONGTEXT 4294967295字符相当于4GB
不过需要注意的是TEXT也有一个问题**由于实际存储的长度不确定MySQL不允许TEXT类型的字段做主键。遇到这种情况你只能采用CHAR(M)或者VARCHAR(M)。**
所以我建议你在你的项目中只要不是主键字段就可以按照数据可能的最大长度选择这几种TEXT类型中的的一种作为存储字符串的数据类型。
## 日期与时间类型
除了刚刚说的这3种类型还有一类也是经常用到的那就是日期与时间类型。
日期与时间是重要的信息,在我们的系统中,几乎所有的数据表都用得到。原因是客户需要知道数据的时间标签,从而进行数据查询、统计和处理。
**用得最多的日期时间类型就是DATETIME**。虽然MySQL也支持YEAR、TIME时间、DATE日期以及TIMESTAMP类型但是**我建议你在实际项目中尽量用DATETIME类型**。因为这个数据类型包括了完整的日期和时间信息使用起来比较方便。毕竟如果日期时间信息分散在好几个字段就会很不容易记而且查询的时候SQL语句也会更加复杂。
这里我也给你列出了MySQL支持的其他日期时间类型的一些参数
<img src="https://static001.geekbang.org/resource/image/5d/d5/5dde7b30c14147bd88eacff77e5892d5.jpg" alt="">
可以看到,不同数据类型表示的时间内容不同、取值范围不同,而且占用的字节数也不一样,你要根据实际需要灵活选取。
不过我也给你一条小建议为了确保数据的完整性和系统的稳定性优先考虑使用DATETIME类型。因为虽然DATETIME类型占用的存储空间最多但是它表达的时间最为完整取值范围也最大。
另外这里还有个问题为什么时间类型TIME的取值范围不是-23:59:5923:59:59呢原因是MySQL设计的TIME类型不光表示一天之内的时间而且可以用来表示一个时间间隔这个时间间隔可以超过24小时。
时间类型的应用场景还是比较广的后面我会单独用一节课来讲在数据库中处理时间的问题。这节课你一定要知道MySQL支持哪几种时间类型它们的区别是什么这样在学后面的内容时才能游刃有余。
## 总结
今天,我给你介绍了几个常用的字段数据类型,包括整数类型、浮点数类型、定点数类型、文本类型和日期时间类型。同时,我们还清楚了为什么整数类型用得少,浮点数为什么不精准,以及常用的日期时间类型。
另外我们还学习了几个新的SQL语句。尤其是第2条我们在项目中会经常用到你一定要重点牢记。
```
-- 修改字段类型语句
ALTER TABLE demo.goodsmaster
MODIFY COLUMN price DOUBLE;
-- 计算字段合计函数:
SELECT SUM(price)
FROM demo.goodsmaster;
```
最后我还想再给你分享1个小技巧。在定义数据类型时如果确定是整数就用INT如果是小数一定用定点数类型DECIMAL如果是字符串只要不是主键就用TEXT如果是日期与时间就用DATETIME。
- 整数INT。
- 小数DECIMAL。
- 字符串TEXT。
- 日期与时间DATETIME。
这样做的好处是,首先确保你的系统不会因为数据类型定义出错。
不过凡事都是有两面的可靠性好并不意味着高效。比如TEXT虽然使用方便但是效率不如CHAR(M)和VARCHAR(M)。如果你有进一步优化的需求,我再给你分享一份[文档](https://dev.mysql.com/doc/refman/8.0/en/data-types.html),你可以对照着看下。
## 思考题
假设用户需要一个表来记录会员信息,会员信息包括会员卡编号、会员名称、会员电话、积分值。如果要你为这些字段定义数据类型,你会如何选择呢?为什么?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,491 @@
<audio id="audio" title="03 | 表:怎么创建和修改表?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7d/4f/7d74a01588b6a686fa489e7b7d1c144f.mp3"></audio>
你好,我是朱晓峰。今天,我们来聊一聊怎么创建和修改数据表。
创建和修改数据表,是数据存储过程中的重要一环。我们不仅需要把表创建出来,还需要正确地设置限定条件,这样才能确保数据的一致性和完整性。同时,表中的数据会随着业务需求的变化而变化,添加和修改相应的字段也是常见的操作。这节课,我们就来学习下具体的方法。
在我们的超市项目里客户经常需要进货这就需要在MySQL数据库里面创建一个表来管理进货相关的数据。我们先看看这个表里有什么内容。
假设这个表叫做进货单头表importhead如下图所示
<img src="https://static001.geekbang.org/resource/image/83/ff/83c593cb7679ed5f99c29d937712c8ff.jpg" alt="">
这里的1、2、3表示门店的3种进货方式分别是配送中心配送、门店采买和供货商直供。
其中“1配送中心配送”是标准进货方式。因为超市是连锁经营为了确保商品质量和品类一致超过9成的门店进货是通过配送中心进行配送的。因此我们希望这个字段的值能够默认是1这样一来除非有特别的指定否则门店进货单的进货方式就自动设置成“1”了。
现在客户需要一个类似的表来存储进货数据而且进货方式还有3个可能的取值范围需要设置默认值那么应该怎么创建这个表呢另外创建好表以后又该怎么进行修改呢
## 如何创建数据表?
首先我们要知道MySQL创建表的语法结构
```
CREATE TABLE &lt;表名&gt;
{
字段名1 数据类型 [字段级别约束] [默认值]
字段名2 数据类型 [字段级别约束] [默认值]
......
[表级别约束]
};
```
在这里,我们通过定义表名、表中的字段、表的属性等,把一张表创建出来。
你可能注意到了在MySQL创建表的语法结构里面有一个词叫做“约束”。**“约束”限定了表中数据应该满足的条件**。MySQL会根据这些限定条件对表的操作进行监控阻止破坏约束条件的操作执行并提示错误从而确保表中数据的唯一性、合法性和完整性。这是创建表时不可缺少的一部分。
下面我来带你创建刚刚提到的进货单表。需要注意的是,这里我们需要定义默认值,也就是要定义默认值约束,除此之外,还有很多种约束,一会儿我再细讲。
我们先来看基本的数据表创建流程,创建代码如下:
```
CREATE TABLE demo.importhead
(
listnumber INT,
supplierid INT,
stocknumber INT,
--我们在字段importype定义为INT类型的后面按照MySQL创建表的语法加了默认值1。
importtype INT DEFAULT 1,
quantity DECIMAL(10,3),
importvalue DECIMAL(10,2),
recorder INT,
recordingdate DATETIME
);
```
运行这个SQL语句表demo.importhead就按照我们的要求被创建出来了。
在创建表的时候字段名称要避开MySQL的[系统关键字](https://dev.mysql.com/doc/refman/8.0/en/keywords.html#keywords-removed-in-current-series)原因是MySQL系统保留的关键字都有特定的含义如果作为字段名称出现在SQL语句中MySQL会把这个字段名称理解为系统关键字从而导致SQL语句无法正常运行。比如刚刚我们把进货金额设置为“importvalue”而不是“value”就是因为“value”是MySQL的系统关键字。
好了现在我们尝试往刚刚创建的表里插入一条记录来验证一下对字段“importtype”定义的默认值约束是否起了作用。
```
INSERT INTO demo.importhead
(
listnumber,
supplierid,
stocknumber,
-- 这里我们没有插入字段importtype的值
quantity,
importvalue,
recorder,
recordingdate
)
VALUES
(
3456,
1,
1,
10,
100,
1,
'2020-12-10'
);
```
插入完成后,我们来查询一下表的内容:
```
SELECT *
FROM demo.importhead
```
运行结果如下:
```
mysql&gt; select * from demo.importhead;
+------------+------------+-------------+------------+----------+-------------+----------+---------------------+
| listnumber | supplierid | stocknumber | importtype | quantity | importvalue | recorder | recordingdate |
+------------+------------+-------------+------------+----------+-------------+----------+---------------------+
| 1234 | 1 | 1 | 1 | 10.000 | 100.00 | 1 | 2020-12-10 00:00:00 |
| 2345 | 1 | 1 | 2 | 20.000 | 2000.00 | 1 | 2020-12-10 00:00:00 |
| 3456 | 1 | 1 | 1 | 20.000 | 2000.00 | 1 | 2020-12-10 00:00:00 |
+------------+------------+-------------+------------+----------+-------------+----------+---------------------+
3 rows in set (0.00 sec)
```
你会发现字段importtype的值已经是1了。这样通过在创建表的时候设置默认值我们就实现了将字段的默认值定义为1的目的。
到这里,表就被创建出来了。
### 都有哪些约束?
刚刚这种给字段设置默认值的做法,就是默认约束。设置了默认约束,插入数据的时候,如果不明确给字段赋值,那么系统会把设置的默认值自动赋值给字段。
除了默认约束,还有主键约束、外键约束、非空约束、唯一性约束和自增约束。
我们在[上节课](https://time.geekbang.org/column/article/350470)里学的主键,其实就是主键约束,我就不多说了。外键约束涉及表与表之间的关联,以及确保表的数据一致性的问题,内容比较多,后面我在讲“关联表”的时候,再给你具体解释。
现在,我来重点给你介绍一下非空约束、唯一性约束和自增约束。
**1.非空约束**
非空约束表示字段值不能为空,如果创建表的时候,指明某个字段非空,那么添加数据的时候,这个字段必须有值,否则系统就会提示错误。
**2.唯一性约束**
唯一性约束表示这个字段的值不能重复,否则系统会提示错误。跟主键约束相比,唯一性约束要更加弱一些。
在一个表中我们可以指定多个字段满足唯一性约束而主键约束则只能有一个这也是MySQL系统决定的。另外**满足主键约束的字段,自动满足非空约束,但是满足唯一性约束的字段,则可以是空值**。
为了方便你理解我来举个例子。比如我们有个商品信息表goodsmaster如下所示
<img src="https://static001.geekbang.org/resource/image/5f/93/5f73f86c51f7b259fef9d52eaf5da893.jpg" alt="">
barcode代表条码goodsname代表名称。为了防止条码重复我们可以定义字段“barcode”满足唯一性约束。这样一来条码就不能重复但是可以为空而且只能有一条记录条码为空。
同样道理为了防止名称重复我们也可以定义字段“goodsname”满足唯一性约束。但是由于无论名称和条码都可能重用或者可能为空都不适合做主键。因此对这个表来说可以添加一个满足唯一性要求的新字段来做主键。
**3.自增约束**
自增约束可以让MySQL自动给字段赋值且保证不会重复非常有用只是不容易用好。所以我借助一个例子来给你具体讲一讲。
假如我们有这样一个商品信息表:
<img src="https://static001.geekbang.org/resource/image/52/c2/524bfc84ab3555283dd981a14f2a06c2.jpg" alt="">
从这个表中我们可以看到barcode、goodsname和price都不能确保唯一性所以没有任何一个字段可以做主键因此我们可以自己添加一个字段itemnumber并且每次添加一条数据的时候要给值增加1。怎么实现呢我们就可以通过定义自增约束的方式让系统自动帮我们赋值从而满足唯一性这样就可以做主键了。
<img src="https://static001.geekbang.org/resource/image/e0/b1/e004230d1b626e17c3822c678cb1e5b1.jpg" alt="">
这里有2个问题需要你注意一下。
第一在数据表中只有整数类型的字段包括TINYINT、SMALLINT、MEDIUMINT、INT和BIGINT才可以定义自增约束。自增约束的字段每增加一条数据值自动增加1。
第二你可以给自增约束的字段赋值这个时候MySQL会重置自增约束字段的自增基数下次添加数据的时候自动以自增约束字段的最大值加1为新的字段值。
举个例子我们通过Workbench把数据表demo.goodsmaster中的字段itemnumber定义为满足自增约束如下图所示
<img src="https://static001.geekbang.org/resource/image/4b/22/4bb1264e88ee558e51061e0956d13f22.png" alt="">
然后,我们插入一条测试记录:
```
INSERT INTO demo.goodsmaster
(
itemnumber,
barcode,
goodsname,
specification,
unit,
price
)
VALUES
(
-- 指定商品编号为100
100,
'0003',
'测试1',
'',
'个',
10
);
```
运行这条语句之后,查看表的内容,我们得到:
```
mysql&gt; select * from demo.goodsmaster;
+------------+---------+-----------+---------------+------+----+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 1 | 0001 | 书 | 16开 | 本 | 89.00 |
| 2 | 0002 | 地图 | NULL | 张 | 9.90 |
| 3 | 0003 | 笔 | 10支 | 包 | 3.00 |
| 100 | 0003 | 测试1 | | 个 | 10.00 |
+------------+---------+-----------+---------------+------+-------+
4 rows in set (0.02 sec)
```
这个时候字段“itemnumber”的值不连续最大值是我们刚刚插入的100。
接着,我们再插入一条数据:
```
INSERT INTO demo.goodsmaster
(
-- 不指定自增字段itemnumber的值
barcode,
goodsname,
specification,
unit,
supplierid,
price
)
VALUES
(
'0004',
'测试2',
'',
'个',
1,
20
);
```
运行这条语句之后,我们再查看表的内容:
```
mysql&gt; select * from demo.goodsmaster;
+------------+---------+-----------+---------------+------+-------+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 1 | 0001 | 书 | 16开 | 本 | 89.00 |
| 2 | 0002 | 地图 | NULL | 张 | 9.90 |
| 3 | 0003 | 笔 | 10支 | 包 | 3.00 |
| 100 | 0003 | 测试1 | | 个 | 10.00 |
| 101 | 0004 | 测试2 | | 个 | 20.00 |
+------------+---------+-----------+---------------+------+-------+
5 rows in set (0.00 sec)
```
可以看到系统自动给自增字段“itemnumber”在最大值的基础之上加了1赋值为101。
好了,到这里,我们就学会了创建表和定义约束的方法。约束要根据业务需要定义在相应的字段上,这样才能保证数据是准确的,你一定要注意它的使用方法。
## 如何修改表?
创建完表以后,我们经常还需要修改表,下面我们就来学习下修改表的方法。
在咱们的超市项目中当我们创建新表的时候会出现这样的情况我们前面创建的进货单表是用来存储进货数据的。但是我们还要创建一个进货单历史表importheadhist用来存储验收过的进货数据。这个表的结构跟进货单表类似只是多了2个字段分别是验收人confirmer和验收时间confirmdate。针对这种情况我们很容易想到可以通过复制表结构然后在这个基础上通过修改表结构来创建新的表。具体怎么实现呢接下来我就给你讲解一下。
首先,我们要把原来的表结构复制一下,代码如下:
```
CREATE TABLE demo.importheadhist
LIKE demo.importhead;
```
运行这个语句之后一个跟demo.importhead有相同表结构的空表demo.importheadhist就被创建出来了。
这个新创建出来的表,还不是我们需要的表,我们需要对这个表进行修改,通过添加字段和修改字段,来获得我们需要的“进货单历史表”。
### 添加字段
你可能会想到我们可以通过Workbench用可视化操作来修改表的结构如下图所示
<img src="https://static001.geekbang.org/resource/image/53/3f/537d01d3c33f8c9f804056e7795d1e3f.png" alt="">
这样当然没问题,但是我想给你讲一个更方便灵活的方式:**用SQL语句来修改表的结构**。
现在我要给这个新的表增加2个字段confirmer和confirmdate就可以用下面的代码
```
mysql&gt; ALTER TABLE demo.importheadhist
-&gt; ADD confirmer INT; -- 添加一个字段confirmer类型INT
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql&gt; ALTER TABLE demo.importheadhist
-&gt; ADD confirmdate DATETIME; -- 添加一个字段confirmdate类型是DATETIME
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
运行这个SQL语句查看表的结构你会发现在字段的最后多了两个字段
- “confirmer”数据类型是INT
- “confirmdate"类型是DATETIME。
我们来查看一下表结构:
```
mysql&gt; DESCRIBE demo.importheadhist;
+----------------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------------+---------------+------+-----+---------+-------+
| listnumber | int | NO | PRI | NULL | |
| supplierid | int | NO | | NULL | |
| stocknumber | int | NO | | NULL | |
| importtype | int | YES | | 1 | |
| quantity | decimal(10,3) | YES | | NULL | |
| importvalue | decimal(10,2) | YES | | NULL | |
| recorder | int | YES | | NULL | |
| recordingdate | datetime | YES | | NULL | |
| confirmer | int | YES | | NULL | |
| confirmdate | datetime | YES | | NULL | |
+----------------+---------------+------+-----+---------+-------+
10 rows in set (0.02 sec)
```
这样通过简单增加2个字段我们就获得了进货单历史表。
### 修改字段
除了添加字段我们可能还要修改字段比如我们要把字段名称“quantity”改成“importquantity”并且把字段类型改为DOUBLE该怎么操作呢
我们可以通过修改表结构语句ALTER TABLE来修改字段
```
mysql&gt; ALTER TABLE demo.importheadhist
-&gt; CHANGE quantity importquantity DOUBLE;
Query OK, 0 rows affected (0.15 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
运行这个SQL语句查看表的结构我们会得到下面的结果
```
mysql&gt; DESCRIBE demo.importheadhist;
+----------------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------------+---------------+------+-----+---------+-------+
| listnumber | int | NO | PRI | NULL | |
| supplierid | int | NO | | NULL | |
| stocknumber | int | NO | | NULL | |
| importtype | int | YES | | 1 | |
| importquantity | double | YES | | NULL | |
| importvalue | decimal(10,2) | YES | | NULL | |
| recorder | int | YES | | NULL | |
| recordingdate | datetime | YES | | NULL | |
| confirmer | int | YES | | NULL | |
| confirmdate | datetime | YES | | NULL | |
+----------------+---------------+------+-----+---------+-------+
10 rows in set (0.02 sec)
```
可以看到,字段名称和字段类型全部都改过来了。
如果你不想改变字段名称只想改变字段类型例如把字段“importquantity”类型改成DECIMAL(10,3),你可以这样写:
```
ALTER TABLE demo.importheadhist
MODIFY importquantity DECIMAL(10,3);
```
运行SQL语句查看表结构你会发现已经改过来了。
```
mysql&gt; DESCRIBE demo.importheadhist;
+----------------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------------+---------------+------+-----+---------+-------+
| listnumber | int | NO | PRI | NULL | |
| supplierid | int | NO | | NULL | |
| stocknumber | int | NO | | NULL | |
| importtype | int | YES | | 1 | |
| importquantity | decimal(10,3) | YES | | NULL | |
| importvalue | decimal(10,2) | YES | | NULL | |
| recorder | int | YES | | NULL | |
| recordingdate | datetime | YES | | NULL | |
| confirmer | int | YES | | NULL | |
| confirmdate | datetime | YES | | NULL | |
+----------------+---------------+------+-----+---------+-------+
10 rows in set (0.02 sec)
```
我们还可以通过SQL语句**向表中添加一个字段**,我们甚至可以指定添加字段在表中的位置。
比如说在字段supplierid之后添加一个字段suppliername数据类型是TEXT。我们可以这样写SQL语句
```
ALTER TABLE demo.importheadhist
ADD suppliername TEXT AFTER supplierid;
```
运行SQL语句查看表结构
```
mysql&gt; DESCRIBE demo.importheadhist;
+----------------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------------+---------------+------+-----+---------+-------+
| listnumber | int | NO | PRI | NULL | |
| supplierid | int | NO | | NULL | |
| suppliername | text | YES | | NULL | |
| stocknumber | int | NO | | NULL | |
| importtype | int | YES | | 1 | |
| importquantity | decimal(10,3) | YES | | NULL | |
| importvalue | decimal(10,2) | YES | | NULL | |
| recorder | int | YES | | NULL | |
| recordingdate | datetime | YES | | NULL | |
| confirmer | int | YES | | NULL | |
| confirmdate | datetime | YES | | NULL | |
+----------------+---------------+------+-----+---------+-------+
11 rows in set (0.02 sec)
```
到这里,我们就完成了修改字段在表中位置的操作。
## 总结
这节课,我们学习了创建和修改数据表的具体方法。在讲创建表时,我讲到了一个重要的概念,就是约束,包括默认约束、非空约束、唯一性约束和自增约束等。
- 默认值约束:就是给字段设置一个默认值。
- 非空约束:就是声明字段不能为空值。
- 唯一性约束:就是声明字段不能重复。
- 自增约束就是声明字段值能够自动加1且不会重复。
在修改表时,我们可以通过修改已经存在的表创建新表,也可以通过添加字段、修改字段的方式来修改数据表。
最后我给你汇总了一些常用的创建表的SQL语句你一定要牢记。
```
CREATE TABLE
(
字段名 字段类型 PRIMARY KEY
);
CREATE TABLE
(
字段名 字段类型 NOT NULL
);
CREATE TABLE
(
字段名 字段类型 UNIQUE
);
CREATE TABLE
(
字段名 字段类型 DEFAULT 值
);
-- 这里要注意自增类型的条件,字段类型必须是整数类型。
CREATE TABLE
(
字段名 字段类型 AUTO_INCREMENT
);
-- 在一个已经存在的表基础上,创建一个新表
CREATE demo.importheadhist LIKE demo.importhead;
-- 修改表的相关语句
ALTER TABLE 表名 CHANGE 旧字段名 新字段名 数据类型;
ALTER TABLE 表名 ADD COLUMN 字段名 字段类型 FIRST|AFTER 字段名;
ALTER TABLE 表名 MODIFY 字段名 字段类型 FIRST|AFTER 字段名;
```
对于初学者来说掌握了今天的内容就足够对数据表进行操作了。不过MySQL支持的数据表操作不只这些我来举几个简单的小例子你可以了解一下有个印象。
比如,你可以在表一级指定表的存储引擎:
```
ALTER TABLE 表名 ENGINE=INNODB;
```
你还可以通过指定关键字AUTO_EXTENDSIZE来指定存储文件自增空间的大小从而提高存储空间的利用率。
在MySQL 8.0.23之后的版本中你甚至还可以通过INVISIBLE关键字使字段不可见但却可以使用。
如果你想了解更多有关数据表的操作,我也给你提供两份资料:[MySQL创建表文档](https://dev.mysql.com/doc/refman/8.0/en/create-table.html)和[MySQL修改表文档](https://dev.mysql.com/doc/refman/8.0/en/alter-table.html)。这些都是MySQL的官方文档相信会对你有所帮助。
## 思考题
请你写一个SQL语句将表demo.goodsmaster中的字段“salesprice”改成不能重复并且不能为空。
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,554 @@
<audio id="audio" title="04 | 增删改查:如何操作表中的数据?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/19/f5/199d6yyyy3f3da59fb1ccb6394ab8cf5.mp3"></audio>
你好,我是朱晓峰。今天,我们来聊一聊如何操作数据表里的数据。
在咱们的超市项目中,我们给用户设计好了一个数据表 demo.goodsmaster定义好了里面的字段以及各种约束如下所示
```
mysql&gt; DESCRIBE demo.goodsmaster;
+---------------+--------------+------+-----+---------+--+
| Field | Type | Null | Key | Default |Extra |
+---------------+------------+------+-----+---------+------------+
| itemnumber | int | NO | PRI | NULL |auto_increment |
| barcode | text | NO | | NULL | |
| goodsname | text | NO | | NULL | |
| specification | text | YES | | NULL | |
| unit | text | YES | | NULL | |
| price | decimal(10,2)| NO | | NULL | |
+---------------+------------+------+-----+---------+----------------+
6 rows in set (0.02 sec)
```
现在,我们需要使用这个表来存取数据。那么,如何对表里面的数据进行操作呢?接下来,我就给你讲讲操作表中的数据的方法,也就是常说的“增删改查”。
## 添加数据
我们先来看一下添加数据的语法结构:
```
INSERT INTO 表名 [(字段名 [,字段名] ...)] VALUES (值的列表);
```
这里方括号“[]”表示里面的内容可选也就是说根据MySQL的语法要求写不写都可以。
添加数据分为两种情况:插入数据记录和插入查询结果。下面我分别来介绍下。
### 插入数据记录
其实MySQL支持的数据插入操作十分灵活。你既可以通过给表里面所有的字段赋值完整地插入一条数据记录也可以在插入记录的时候只给部分字段赋值。
这是什么意思呢?我借助一个例子来给你讲解下。
比如我们有一个叫demo.goodsmaster的表包括了itemnumber、barcode、goodsname、specification、unit和price共6个字段。我想要插入一条数据记录其中包含了所有字段的值就可以这样操作
```
INSERT INTO demo.goodsmaster
(
itemnumber,
barcode,
goodsname,
specification,
unit,
price
)
VALUES
(
4,
'0003',
'尺子',
'三角型',
'把',
5
);
```
运行这个SQL语句然后对数据表进行查询可以得到下面的结果
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+-------+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 4 | 0003 | 尺子 | 三角型 | 把 | 5.00 |
+------------+---------+-----------+---------------+------+-------+
1 row in set (0.01 sec)
```
如果我想插入一条记录,但是只给部分字段赋值,可不可以呢?比如说,客户有个商品,需要马上上线销售,目前只知道条码、名称和价格,其它的信息先不录入,等之后再补,可以实现吗?
我们来尝试一下只给3个字段赋值看看实际操作的结果
```
INSERT INTO demo.goodsmaster
(
-- 这里只给3个字段赋值itemnumber、specification、unit不赋值
barcode,
goodsname,
price
)
VALUES
(
'0004',
'测试',
10
);
```
运行这条SQL语句我们来查询表的内容就会发现显然是可以的。
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+-------+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 4 | 0003 | 尺子 | 三角型 | 把 | 5.00 |
| 5 | 0004 | 测试 | NULL | NULL | 10.00 |
+------------+---------+-----------+---------------+------+-------+
2 rows in set (0.00 sec)
```
我们之所以能够在插入一条数据记录的时候,只给部分字段赋值,原因就在于我们对字段的定义方式。
那字段是怎么定义的呢?我们来查看一下表的结构,看看各个字段的定义:
```
mysql&gt; DESCRIBE demo.goodsmaster;
+---------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+---------------+------+-----+---------+----------------+
| itemnumber | int | NO | PRI | NULL | auto_increment |
| barcode | text | NO | | NULL | |
| goodsname | text | NO | | NULL | |
| specification | text | YES | | NULL | |
| unit | text | YES | | NULL | |
| price | decimal(10,2) | NO | | NULL | |
+---------------+---------------+------+-----+---------+----------------+
6 rows in set (0.01 sec)
```
可以看到我们在插入数据时没有明确赋值的3个字段都有着各自的特点。“specification”和“unit”都可以是空值而itemnumber则定义了自增约束。
我们在插入一条数据记录的时候必须要考虑字段约束的3种情况。
第一种情况是如果字段允许为空而我们没有给它赋值那么MySQL会自动给它们赋予空值。在刚刚的代码中“specification”“unit”字段都允许为空因此如果数据插入语句没有指定这几个字段的值MySQL会自动插入空值。
第二种情况是如果字段是主键就不能为空这个时候MySQL会按照我们添加的约束进行处理。比如字段“itemnumber”是主键不能为空而我们定义了自增约束所以MySQL自动在之前的最大值基础上加了1。因此“itemnumber”也有了自己该有的值。
第三种情况是,如果有一个字段定义不能为空,又不是主键,当你插入一条数据记录的时候,就必须给这个记录赋值。
如果我们的操作违反了字段的约束限制,会出现什么情况呢?
比如说我们尝试把表demo.goodsmaster的字段“speicification”改为不能为空
```
ALTER TABLE demo.goodsmaster
MODIFY specification TEXT NOT NULL;
```
运行这个SQL语句系统会提示错误原因就是我们刚才部分插入了一条数据记录没有给字段“specification”赋值这跟我们给字段“specification”添加非空约束的操作冲突了。
因此我们要把字段“speicification”的值为空的数据记录删除然后再修改字段约束
```
DELETE
FROM demo.goodsmaster
WHERE itemnumber=5;
```
删除数据记录之后再运行上面的语句给字段“specification”添加非空约束就成功了。
现在我们来验证一下非空约束。我们尝试部分插入一条数据记录不给字段“specification”赋值
```
INSERT INTO demo.goodsmaster
(
barcode,
goodsname,
price
)
VALUES
(
'0004',
'测试',
10
);
```
运行这个SQL语句MySQL报告错误提示字段“specification”没有默认值。也就是说这个字段不能为空如果插入数据时不给它赋值就必须给它一个默认值。
现在,你一定清楚了,**部分插入一条数据记录是可以的但前提是没有赋值的字段一定要让MySQL知道如何处理比如可以为空、有默认值或者是自增约束字段等等否则MySQL会提示错误的**。
好了到这里我们就学会了给MySQL的数据表插入一条数据记录。但是在实际工作中一次只插入一条数据有时候会不够用。
我们的项目就有这样的场景门店每天的销售流水都很多日积月累流水表变得越来越大。如果就让它这么不断地增长数据量甚至会达到数亿条占据的存储空间达到几十个G。虽然MySQL可以处理这样比较大的数据表但是每次操作的响应时间就会延长这会导致系统的整体效率下降。
刚开始的时候我们开发了一个日结处理当天算清所有账目。其中一个步骤就是把当天流水表的数据全部转到历史流水表当中去。现在我们就可以用上数据插入语句了。具体有2步
1. 从流水表中取出一条数据;
1. 把这条数据插入到历史流水表中。
然后不断重复这两个步骤,一直到把今天流水表的数据全部插入到历史流水表当中去。
你肯定会说,这种做法的效率也太低了吧?有没有更好的办法呢?
当然是有的。这个时候我们就要用到MySQL的另一种数据插入操作了把查询结果插入到数据表中。
### 插入查询结果
MySQL支持把查询的结果插入到数据表中我们可以指定字段甚至是数值插入到数据表中。语法结构如下
```
INSERT INTO 表名 (字段名)
SELECT 字段名或值
FROM 表名
WHERE 条件
```
举个例子在我们的超市信息系统的MySQL数据库中历史流水表设计与流水表非常类似。不同的是历史流水表增加了一些字段来标识历史流水的状态比如日结时间字段是用来记录日结操作是什么时候进行的。
用INSERT语句实现起来也很简单
```
INSERT INTO 历史流水表 (日结时间字段,其他字段)
SELECT 获取当前时间函数,其他字段
FROM 流水表
```
好了,添加数据的操作就讲完了,现在你知道了,我们给一张数据表插入一条数据记录的时候,可以给所有的字段赋值,也可以给部分字段赋值。这取决于字段的定义。如果字段不能为空并且没有默认值,就必须赋值。另外,我们还可以通过把一个查询结果插入数据表中的方式,提高添加数据的效率。
接下来,我们再来看看如何删除数据。
## 删除数据
数据删除的语法很简单,如下所示:
```
DELETE FROM 表名
WHERE 条件
```
如果我们现在想把刚才用过的表demo.goodsmaster里的内容清理一下删除全部数据可以通过下面的SQL语句来实现
```
DELETE FROM demo.goodsmaster;
```
因为我们的查询主要是在MySQL的图形化管理工具中进行现在我们在Workbench中运行上面的SQL语句。这个时候Workbench会提示错误。
这是因为Workbench自动处于安全模式它要求对数据的删除或修改操作中必须包含WHERE条件。而且这个WHERE条件中必须用到主键约束或者唯一性约束的字段。MySQL的这种安全性设置主要就是为了防止删除或者修改数据时出现误操作导致删除或修改了不相关的数据。
因此我们要习惯在删除数据的时候添加条件语句WHERE防止误操作。
假设我们的数据表demo.goodsmaster中有如下数据记录
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+-------+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 4 | 0003 | 尺子 | 三角型 | 把 | 5.00 |
| 5 | 0004 | 测试 | NULL | NULL | 10.00 |
+------------+---------+-----------+---------------+------+-------+
2 rows in set (0.00 sec)
```
如果我们要删除所有数据,可以这样写:
```
DELETE FROM demo.goodsmaster
WHERE itemnumber &gt; 1;
```
好了,下面我们来学习一下修改数据。
## 修改数据
我们来看一下MySQL的数据修改语法
```
UPDATE 表名
SET 字段名=值
WHERE 条件
```
语法很简单,需要注意的一点是,**不要修改主键字段的值**。因为主键是数据记录的唯一标识,如果修改了主键的值,就有可能会破坏数据的完整性。
下面我们来看一个通过主键查询商品信息的例子,看看修改主键的值,会产生什么样的结果。
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster
-&gt; WHERE itemnumber = 3;
+------------+---------+-----------+---------------+------+-------+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 3 | 0003 | 尺子 | 三角型 | 把 | 5.00 |
+------------+---------+-----------+---------------+------+-------+
1 row in set (0.00 sec)
```
我们能查询到一条商品编号是3的数据记录条码是“0003”名称是“尺子”价格是5元。
如果我们修改了主键的值,就可能会改变刚才的查询结果:
```
mysql&gt; UPDATE demo.goodsmaster
-&gt; SET itemnumber=2
-&gt; WHERE itemnumber = 3;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster
-&gt; WHERE itemnumber = 3;
Empty set (0.00 sec)
```
可以看到查询结果是空因为商品编号是3的数据记录已经不存在了。
当然,如果你必须要修改主键的值,那有可能就是主键设置得不合理。关于主键的设置问题,我会在下节课给你详细介绍。这里你只需要知道,不要随便修改表的主键值,就可以了。
## 查询数据
经过了前面的操作之后,现在,我们知道了把数据存入数据表里,以及删除和修改表里数据的方法。那么如何知道数据操作的结果呢?这就要用到数据查询了。
我们先来看下查询语句的语法结构:
```
SELECT *|字段列表
FROM 数据源
WHERE 条件
GROUP BY 字段
HAVING 条件
ORDER BY 字段
LIMIT 起始点,行数
```
在这些字段中SELECT、WHERE、GROUP BY和HAVING比较好理解你只要知道它们的含义就可以了。
- SELECT是查询关键字表示我们要做一个查询。“*”是一个通配符,表示我们要查询表中所有的字段。你也可以把要查询的字段罗列出来,这样,查询的结果可以只显示你想要查询的字段内容。
- WHERE表示查询条件。你可以把你要查询的数据所要满足的条件放在WHERE关键字之后。
- GROUP BY作用是告诉MySQL查询结果要如何分组经常与MySQL的聚合函数一起使用。
- HAVING用于筛选查询结果跟WHERE类似。
FROM、ORDER BY和LIMIT相对来说比较复杂需要注意的地方比较多我来具体给你解释一下。
### FROM
FROM关键字表示查询的数据源。我们现在只学习了单个数据表你可以把你要查询的数据表名直接写在FROM关键字之后。以后我们在学到关联表的时候你就会知道FROM关键字后面还可以跟着更复杂的数据表联接。
这里需要你要注意的是,数据源也不一定是表,也可以是一个查询的结果。比如下面的查询:
<img src="https://static001.geekbang.org/resource/image/c8/b0/c88513447b9yydbb6f983603ffba19b0.png" alt="">
这里你要注意的是红色框里的部分叫做派生表derived table或者子查询subquery意思是我们把一个查询结果数据集当做一个虚拟的数据表来看待。MySQL规定必须要用AS关键字给这个派生表起一个别名。在这张图中我给这个派生表起了个名字叫做“a”。
### ORDER BY
ORDER BY的作用是告诉MySQL查询结果如何排序。**ASC表示升序DESC表示降序**。
我来举个简单的小例子带你看看ORDER BY是怎么使用的这里我们仍然假设字段“specification”和“unit”允许为空。我们向表demo.goodsmaster中插入2条数据
```
INSERT INTO demo.goodsmaster
(
barcode,
goodsname,
price
)
VALUES
(
'0003',
'尺子1',
15
);
INSERT INTO demo.goodsmaster
(
barcode,
goodsname,
price
)
VALUES
(
'0004',
'测试1',
20
);
```
如果我们不控制查询结果的顺序,就会得到这样的结果:
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+-------+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 4 | 0003 | 尺子 | 三角型 | 把 | 5.00 |
| 5 | 0004 | 测试 | NULL | NULL | 10.00 |
| 6 | 0003 | 尺子1 | NULL | NULL | 15.00 |
| 7 | 0004 | 测试1 | NULL | NULL | 20.00 |
+------------+---------+-----------+---------------+------+-------+
4 rows in set (0.00 sec)
```
如果我们使用ORDER BY对查询结果进行控制结果就不一样了
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster
-&gt; ORDER BY barcode ASC,price DESC;
+------------+---------+-----------+---------------+------+-------+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 6 | 0003 | 尺子1 | NULL | NULL | 15.00 |
| 4 | 0003 | 尺子 | 三角型 | 把 | 5.00 |
| 7 | 0004 | 测试1 | NULL | NULL | 20.00 |
| 5 | 0004 | 测试 | NULL | NULL | 10.00 |
+------------+---------+-----------+---------------+------+-------+
4 rows in set (0.00 sec)
```
可以看到查询结果会先按照字段barcode的升序排序相同barcode里面的字段按照price的降序排序。
### LIMIT
LIMIT的作用是告诉MySQL只显示部分查询的结果。比如现在我们的数据表demo.goodsmaster中有4条数据我们只想要显示第2、3条数据就可以用LIMIT关键字来实现
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster
-&gt; LIMIT 1,2;
+------------+---------+-----------+---------------+------+-------+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 5 | 0004 | 测试 | NULL | NULL | 10.00 |
| 6 | 0003 | 尺子1 | NULL | NULL | 15.00 |
+------------+---------+-----------+---------------+------+-------+
2 rows in set (0.00 sec)
```
这里的“LIMIT 1,2”中“1”表示起始位置MySQL中起始位置的起点是01表示从第2条记录开始“2”表示2条数据。因此“LIMIT 1,2”就表示从第2条数据开始显示2条数据也就是显示了第2、3条数据。
## 总结
今天我们学习了添加、删除、修改和查询数据的方法这些都是我们经常遇到的操作你一定要好好学习。下面的这些SQL语句是我总结的数据操作语法你一定要重点掌握。
```
INSERT INTO 表名 [(字段名 [,字段名] ...)] VALUES (值的列表);
INSERT INTO 表名 (字段名)
SELECT 字段名或值
FROM 表名
WHERE 条件
DELETE FROM 表名
WHERE 条件
UPDATE 表名
SET 字段名=值
WHERE 条件
SELECT *|字段列表
FROM 数据源
WHERE 条件
GROUP BY 字段
HAVING 条件
ORDER BY 字段
LIMIT 起始点,行数
```
最后我再补充一点。如果我们把查询的结果插入到表中时导致主键约束或者唯一性约束被破坏了就可以用“ON DUPLICATE”关键字进行处理。这个关键字的作用是告诉MySQL如果遇到重复的数据该如何处理。
我来给你举个例子。
假设用户有2个各自独立的门店分别有自己的系统。现在需要引入连锁经营的模式把2个店用一套系统统一管理。那么首先遇到的问题就是需要进行数据整合。下面我们就以商品信息表为例来说明如何通过使用“ON DUPLICATE”关键字把两个门店的商品信息数据整合到一起。
假设门店A的商品信息表是“demo.goodsmaster”代码如下
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice |
+------------+---------+-----------+---------------+------+------------+
| 1 | 0001 | 书 | 16开 | 本 | 89.00 |
| 2 | 0002 | 笔 | 10支装 | 包 | 5.00 |
| 3 | 0003 | 橡皮 | NULL | 个 | 3.00 |
+------------+---------+-----------+---------------+------+------------+
3 rows in set (0.00 sec)
```
门店B的商品信息表是“demo.goodsmaster1”
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster1;
+------------+---------+-----------+---------------+------+------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice |
+------------+---------+-----------+---------------+------+------------+
| 1 | 0001 | 教科书 | NULL | NULL | 89.00 |
| 4 | 0004 | 馒头 | | | 1.50 |
+------------+---------+-----------+---------------+------+------------+
2 rows in set (0.00 sec)
```
假设我们要把门店B的商品数据插入到门店A的商品表中去如果有重复的商品编号就用门店B的条码替换门店A的条码用门店B的商品名称替换门店A的商品名称如果没有重复的编号就直接把门店B的商品数据插入到门店A的商品表中。这个操作就可以用下面的SQL语句实现
```
INSERT INTO demo.goodsmaster
SELECT *
FROM demo.goodsmaster1 as a
ON DUPLICATE KEY UPDATE barcode = a.barcode,goodsname=a.goodsname;
-- 运行结果如下
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice |
+------------+---------+-----------+---------------+------+------------+
| 1 | 0001 | 教科书 | 16开 | 本 | 89.00 |
| 2 | 0002 | 笔 | 10支装 | 包 | 5.00 |
| 3 | 0003 | 橡皮 | NULL | 个 | 3.00 |
| 4 | 0004 | 馒头 | | | 1.50 |
+------------+---------+-----------+---------------+------+------------+
4 rows in set (0.00 sec)
```
最后我再跟你分享3份资料分别是[MySQL数据插入](https://dev.mysql.com/doc/refman/8.0/en/insert.html)、[MySQL数据更新](https://dev.mysql.com/doc/refman/8.0/en/update.html)和[MySQL数据查询](https://dev.mysql.com/doc/refman/8.0/en/select.html),如果你在工作中遇到了更加复杂的操作需求,就可以参考一下。
## 思考题
我想请你思考一个问题商品表demo.goodsmaster中字段“itemnumber”是主键而且满足自增约束如果我删除了一条记录再次插入数据的时候就会出现字段“itemnumber”的值不连续的情况。请你想一想如何插入数据才能防止这种情况的发生呢
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,436 @@
<audio id="audio" title="05 | 主键:如何正确设置主键?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7c/ff/7c72bfee0a63yy0fdac7199217a257ff.mp3"></audio>
你好我是朱晓峰今天我们来聊一聊如何用好MySQL的主键。
前面在讲存储的时候,我提到过主键,它可以唯一标识表中的某一条记录,对数据表来说非常重要。当我们需要查询和引用表中的一条记录的时候,最好的办法就是通过主键。只有合理地设置主键,才能确保我们准确、快速地找到所需要的数据记录。今天我就借助咱们的超市项目的实际需求,来给你讲一讲怎么正确设置主键。
在我们的项目中客户要进行会员营销相应的我们就需要处理会员信息。会员信息表demo.membermaster的设计大体如下
<img src="https://static001.geekbang.org/resource/image/87/c0/87977152197dbaa92d6a86cc9911c1c0.jpg" alt="">
为了能够唯一地标识一个会员的信息,我们需要为会员信息表设置一个主键。那么,怎么为这个表设置主键,才能达到我们理想的目标呢?
今天,我就带你在解决这个实际问题的过程中,学习下三种设置主键的思路:**业务字段做主键**、**自增字段做主键**和**手动赋值字段做主键**。
## 业务字段做主键
针对这个需求,我们最容易想到的,是选择表中已有的字段,也就是跟业务相关的字段做主键。那么,在这个表里,哪个字段比较合适呢?我们来分析一下。
会员卡号cardno看起来比较合适因为会员卡号不能为空而且有唯一性可以用来标识一条会员记录。我们来尝试一下用会员卡号做主键。
我们可以用下面的代码在创建表的时候设置字段cardno为主键
```
mysql&gt; CREATE TABLE demo.membermaster
-&gt; (
-&gt; cardno CHAR(8) PRIMARY KEY, -- 会员卡号为主键
-&gt; membername TEXT,
-&gt; memberphone TEXT,
-&gt; memberpid TEXT,
-&gt; memberaddress TEXT,
-&gt; sex TEXT,
-&gt; birthday DATETIME
-&gt; );
Query OK, 0 rows affected (0.06 sec)
```
我们来查询一下表的结构,确认下主键是否创建成功了:
```
mysql&gt; DESCRIBE demo.membermaster;
+---------------+----------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+----------+------+-----+---------+-------+
| cardno | char(8) | NO | PRI | NULL | |
| membername | text | YES | | NULL | |
| memberphone | text | YES | | NULL | |
| memberpid | text | YES | | NULL | |
| memberaddress | text | YES | | NULL | |
| sex | text | YES | | NULL | |
| birthday | datetime | YES | | NULL | |
+---------------+----------+------+-----+---------+-------+
7 rows in set (0.02 sec)
```
可以看到字段cardno在表示键值的key这一列的值是“PRI”意思是PRIMARY KEY这就表示它已经被设置成主键了。这里需要注意的一点是除了字段cardno所有的字段都允许为空。这是因为这些信息有可能当时不知道要稍后补齐。
会员卡号做主键有没有什么问题呢我们插入2条数据来验证下
```
mysql&gt; INSERT INTO demo.membermaster
-&gt; (
-&gt; cardno,
-&gt; membername,
-&gt; memberphone,
-&gt; memberpid,
-&gt; memberaddress,
-&gt; sex,
-&gt; birthday
-&gt; )
-&gt; VALUES
-&gt; (
-&gt; '10000001',
-&gt; '张三',
-&gt; '13812345678',
-&gt; '110123200001017890',
-&gt; '北京',
-&gt; '男',
-&gt; '2000-01-01'
-&gt; );
Query OK, 1 row affected (0.01 sec)
mysql&gt; INSERT INTO demo.membermaster
-&gt; (
-&gt; cardno,
-&gt; membername,
-&gt; memberphone,
-&gt; memberpid,
-&gt; memberaddress,
-&gt; sex,
-&gt; birthday
-&gt; )
-&gt; VALUES
-&gt; (
-&gt; '10000002',
-&gt; '李四',
-&gt; '13512345678',
-&gt; '123123199001012356',
-&gt; '上海',
-&gt; '女',
-&gt; '1990-01-01'
-&gt; );
Query OK, 1 row affected (0.01 sec)
```
插入成功后,我们来看一下表的内容:
```
mysql&gt; SELECT *
-&gt; FROM demo.membermaster;
+----------+------------+-------------+--------------------+---------------+------+---------------------+
| cardno | membername | memberphone | memberpid | memberaddress | sex | birthday |
+----------+------------+-------------+--------------------+---------------+------+---------------------+
| 10000001 | 张三 | 13812345678 | 110123200001017890 | 北京 | 男 | 2000-01-01 00:00:00 |
| 10000002 | 李四 | 13512345678 | 123123199001012356 | 上海 | 女 | 1990-01-01 00:00:00 |
+----------+------------+-------------+--------------------+---------------+------+---------------------+
2 rows in set (0.00 sec)
```
我们发现不同的会员卡号对应不同的会员字段“cardno”唯一地标识某一个会员。如果都是这样会员卡号与会员一一对应系统是可以正常运行的。
但是实际情况是上线不到一周就发生了“cardno”无法唯一识别某一个会员的问题。原来会员卡号存在重复使用的情况。
这也很好理解比如张三因为工作变动搬离了原来的地址不再到商家的门店消费了退还了会员卡于是张三就不再是这个商家门店的会员了。但是商家不想让这个会员卡空着就把卡号是“10000001”的会员卡发给了王五。
从系统设计的角度看这个变化只是修改了会员信息表中的卡号是“10000001”这个会员信息并不会影响到数据一致性。也就是说修改会员卡号是“10000001”的会员信息系统的各个模块都会获取到修改后的会员信息不会出现“有的模块获取到修改之前的会员信息有的模块获取到修改后的会员信息而导致系统内部数据不一致”的情况。因此从信息系统层面上看是没问题的。但是从使用系统的业务层面来看就有很大的问题了会对商家造成影响。
下面,我们就来看看这种修改,是如何影响到商家的。
比如我们有一个销售流水表记录了所有的销售流水明细。2020年12月01日张三在门店购买了一本书消费了89元。那么系统中就有了张三买书的流水记录如下所示
<img src="https://static001.geekbang.org/resource/image/86/a4/864b283a81320351ccdeaf24be558aa4.jpg" alt="">
我们可以用下面的代码创建销售流水表。因为需要引用会员信息和商品信息,所以表中要包括商品编号字段和会员卡号字段。
```
mysql&gt; CREATE table demo.trans
-&gt; (
-&gt; transactionno INT,
-&gt; itemnumber INT, -- 为了引用商品信息
-&gt; quantity DECIMAL(10,3),
-&gt; price DECIMAL(10,2),
-&gt; salesvalue DECIMAL(10,2),
-&gt; cardno CHAR(8), -- 为了引用会员信息
-&gt; transdate DATETIME
-&gt; );
Query OK, 0 rows affected (0.10 sec)
```
创建好表以后,我们来插入一条销售流水:
```
mysql&gt; INSERT INTO demo.trans
-&gt; (
-&gt; transactionno,
-&gt; itemnumber,
-&gt; quantity,
-&gt; price,
-&gt; salesvalue,
-&gt; cardno,
-&gt; transdate
-&gt; )
-&gt; VALUES
-&gt; (
-&gt; 1,
-&gt; 1,
-&gt; 1,
-&gt; 89,
-&gt; 89,
-&gt; '10000001',
-&gt; '2020-12-01'
-&gt; );
Query OK, 1 row affected (0.01 sec)
```
接着我们查询一下2020年12月01日的会员销售记录
```
mysql&gt; SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
-&gt; FROM demo.trans AS a
-&gt; JOIN demo.membermaster AS b
-&gt; JOIN demo.goodsmaster AS c
-&gt; ON (a.cardno = b.cardno AND a.itemnumber=c.itemnumber);
+------------+-----------+----------+------------+---------------------+
| membername | goodsname | quantity | salesvalue | transdate |
+------------+-----------+----------+------------+---------------------+
| 张三 | 书 | 1.000 | 89.00 | 2020-12-01 00:00:00 |
+------------+-----------+----------+------------+---------------------+
1 row in set (0.00 sec)
```
我们得到的查询结果是张三在2020年12月01日买了一本书花了89元。
需要注意的是这里我用到了JOIN也就是表的关联目的是为了引用其他表的信息包括会员信息表demo.membermaster和商品信息表demo.goodsmaster。有关关联表查询的具体细节我会在下节课讲到这里你只要知道通过关联查询可以从会员信息表中获取会员信息从商品信息表中获取商品信息就可以了。
下面我们假设会员卡“10000001”又发给了王五我们需要更改会员信息表
```
mysql&gt; UPDATE demo.membermaster
-&gt; SET membername = '王五',
-&gt; memberphone = '13698765432',
-&gt; memberpid = '475145197001012356',
-&gt; memberaddress='天津',
-&gt; sex='女',
-&gt; birthday = '1970-01-01'
-&gt; WHERE cardno = '10000001';
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
会员记录改好了,我们再次运行之前的会员消费流水查询:
```
mysql&gt; SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
-&gt; FROM demo.trans AS a
-&gt; JOIN demo.membermaster AS b
-&gt; JOIN demo.goodsmaster AS c
-&gt; ON (a.cardno = b.cardno AND a.itemnumber=c.itemnumber);
+------------+-----------+----------+------------+---------------------+
| membername | goodsname | quantity | salesvalue | transdate |
+------------+-----------+----------+------------+---------------------+
| 王五 | 书 | 1.000 | 89.00 | 2020-12-01 00:00:00 |
+------------+-----------+----------+------------+---------------------+
1 row in set (0.01 sec)
```
这次得到的结果是王五在2020年12月01日买了一本书消费89元。
很明显这个结果把张三的消费行为放到王五身上去了肯定是不对的。这里的原因就是我们把会员卡号是“10000001”的会员信息改了而会员卡号是主键会员消费查询通过会员卡号关联到会员信息得到了完全错误的结果。
现在你知道了吧,千万不能把会员卡号当做主键。
那么,会员电话可以做主键吗?不行的。在实际操作中,手机号也存在被运营商收回,重新发给别人用的情况。
那身份证号行不行呢?好像可以。因为身份证决不会重复,身份证号与一个人存在一一对应的关系。可问题是,身份证号属于个人隐私,顾客不一定愿意给你。对门店来说,顾客就是上帝,要是强制要求会员必须登记身份证号,会把很多客人赶跑的。其实,客户电话也有这个问题,这也是我们在设计会员信息表的时候,允许身份证号和电话都为空的原因。
这样看来,任何一个现有的字段都不适合做主键。
所以,我建议你**尽量不要用业务字段,也就是跟业务有关的字段做主键**。毕竟,作为项目设计的技术人员,我们谁也无法预测在项目的整个生命周期中,哪个业务字段会因为项目的业务需求而有重复,或者重用之类的情况出现。
既然业务字段不可以,那我们再来试试自增字段。
## 使用自增字段做主键
我们来给会员信息表添加一个字段比如叫id给这个字段定义自增约束这样我们就有了一个具备唯一性的而且不为空的字段来做主键了。
接下来,我们就来修改一下会员信息表的结构,添加一个自增字段做主键。
第一步,修改会员信息表,删除表的主键约束,这样,原来的主键字段,就不再是主键了。不过需要注意的是,删除主键约束,并不会删除字段。
```
mysql&gt; ALTER TABLE demo.membermaster
-&gt; DROP PRIMARY KEY;
Query OK, 2 rows affected (0.12 sec)
Records: 2 Duplicates: 0 Warnings: 0
```
第二步修改会员信息表添加字段“id”为主键并且给它定义自增约束
```
mysql&gt; ALTER TABLE demo.membermaster
-&gt; ADD id INT PRIMARY KEY AUTO_INCREMENT;
Query OK, 0 rows affected (0.12 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
第三步修改销售流水表添加新的字段memberid对应会员信息表中的主键
```
mysql&gt; ALTER TABLE demo.trans
-&gt; ADD memberid INT;
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
第四步我们更新一下销售流水表给新添加的字段“memberid”赋值让它指向对应的会员信息
```
mysql&gt; UPDATE demo.trans AS a,demo.membermaster AS b
-&gt; SET a.memberid=b.id
-&gt; WHERE a.transactionno &gt; 0
--&gt; AND a.cardno = b.cardno; -- 这样操作可以不用删除trans的内容在实际工作中更适合
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
这个更新语句包含了2个关联的表看上去有点复杂。其实你完全可以通过删除表demo.trans、重建表再插入一条数据的操作来达到同样的目的但是我不建议你这么做。
在实际操作中你不一定能删掉demo.trans这个表因为这个表里面可能已经有了很多重要的数据。所以你一定要认真学习一下我给你介绍的这个更新数据的方法这种复杂一点的更新语句在实战中更有用。
好了到这里我们就完成了数据表的重新设计让我们看一下新的数据表demo.membermaster和demo.trans的结构
```
mysql&gt; DESCRIBE demo.membermaster;
+---------------+----------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+----------+------+-----+---------+----------------+
| cardno | char(8) | NO | | NULL | |
| membername | text | YES | | NULL | |
| memberphone | text | YES | | NULL | |
| memberpid | text | YES | | NULL | |
| memberaddress | text | YES | | NULL | |
| sex | text | YES | | NULL | |
| birthday | datetime | YES | | NULL | |
| id | int | NO | PRI | NULL | auto_increment |
+---------------+----------+------+-----+---------+----------------+
8 rows in set (0.02 sec)
mysql&gt; DESCRIBE demo.trans;
+---------------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+---------------+------+-----+---------+-------+
| transactionno | int | NO | PRI | NULL | |
| itemnumber | int | YES | | NULL | |
| quantity | decimal(10,3) | YES | | NULL | |
| price | decimal(10,2) | YES | | NULL | |
| salesvalue | decimal(10,2) | YES | | NULL | |
| cardno | char(8) | YES | | NULL | |
| transdate | datetime | YES | | NULL | |
| memberid | int | YES | | NULL | |
+---------------+---------------+------+-----+---------+-------+
8 rows in set (0.00 sec)
```
现在如果我们再次面对卡号重用的情况该如何应对呢这里我们假设回到修改会员卡10000001为王五之前的状态
如果张三的会员卡“10000001”不再使用发给了王五我们就在会员信息表里面增加一条记录
```
mysql&gt; INSERT INTO demo.membermaster
-&gt; (
-&gt; cardno,
-&gt; membername,
-&gt; memberphone,
-&gt; memberpid,
-&gt; memberaddress,
-&gt; sex,
-&gt; birthday
-&gt; )
-&gt; VALUES
-&gt; (
-&gt; '10000001',
-&gt; '王五',
-&gt; '13698765432',
-&gt; '475145197001012356',
-&gt; '天津',
-&gt; '女',
-&gt; '1970-01-01'
-&gt; );
Query OK, 1 row affected (0.02 sec)
```
下面我们看看现在的会员信息:
```
mysql&gt; SELECT *
-&gt; FROM demo.membermaster;
+----------+------------+-------------+--------------------+---------------+------+---------------------+----+
| cardno | membername | memberphone | memberpid | memberaddress | sex | birthday | id |
+----------+------------+-------------+--------------------+---------------+------+---------------------+----+
| 10000001 | 张三 | 13812345678 | 110123200001017890 | 北京 | 男 | 2000-01-01 00:00:00 | 1 |
| 10000002 | 李四 | 13512345678 | 123123199001012356 | 上海 | 女 | 1990-01-01 00:00:00 | 2 |
| 10000001 | 王五 | 13698765432 | 475145197001012356 | 天津 | 女 | 1970-01-01 00:00:00 | 3 |
+----------+------------+-------------+--------------------+---------------+------+---------------------+----+
3 rows in set (0.00 sec)
```
由于字段“cardno”不再是主键可以允许重复因此我们可以在保留会员“张三”信息的同时添加使用同一会员卡号的“王五”的信息。
现在再来查会员消费,就不会出问题了:
```
mysql&gt; SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
-&gt; FROM demo.trans AS a
-&gt; JOIN demo.membermaster AS b
-&gt; JOIN demo.goodsmaster AS c
-&gt; ON (a.memberid = b.id AND a.itemnumber=c.itemnumber);
+------------+-----------+----------+------------+---------------------+
| membername | goodsname | quantity | salesvalue | transdate |
+------------+-----------+----------+------------+---------------------+
| 张三 | 书 | 1.000 | 89.00 | 2020-12-01 00:00:00 |
+------------+-----------+----------+------------+---------------------+
1 row in set (0.01 sec)
```
可以看到结果是2020年12月01日张三买了一本书消费89元是正确的。
如果是一个小项目只有一个MySQL数据库服务器用添加自增字段作为主键的办法是可以的。不过这并不意味着在任何情况下你都可以这么做。
举个例子用户要求把增加新会员的工作放到门店进行因为发展新会员的工作一般是在门店进行的毕竟人们一般都是在购物的同时申请会员。解决的办法是门店的信息系统添加新增会员的功能把新的会员信息先存放到本地MySQL数据库中再上传到总部进行汇总。
可是问题来了如果会员信息表的主键是自增的那么各个门店新加的会员就会出现“id”冲突的可能。
比如A店的MySQL数据库中的demo.membermaster中字段“id”的值是100这个时候新增了一个会员“id”是101。同时B店的字段“id”值也是100要加一个新会员“id”也是101毕竟B店的MySQL数据库与A店相互独立。等A店与B店都把新的会员上传到总部之后就会出现两个“id”是101但却是不同会员的情况这该如何处理呢
## 手动赋值字段做主键
为了解决这个问题我们想了一个办法取消字段“id”的自增属性改成信息系统在添加会员的时候对“id”进行赋值。
具体的操作是这样的在总部MySQL数据库中有一个管理信息表里面的信息包括成本核算策略支付方式等还有总部的系统参数我们可以在这个表中添加一个字段专门用来记录当前会员编号的最大值。
门店在添加会员的时候先到总部MySQL数据库中获取这个最大值在这个基础上加1然后用这个值作为新会员的“id”同时更新总部MySQL数据库管理信息表中的当前会员编号的最大值。
这样一来各个门店添加会员的时候都对同一个总部MySQL数据库中的数据表字段进行操作就解决了各门店添加会员时会员编号冲突的问题同时也避免了使用业务字段导致数据错误的问题。
## 总结
今天,我给你介绍了设置数据表主键的三种方式:数据表的业务字段做主键、添加自增字段做主键,以及添加手动赋值字段做主键。
- 用业务字段做主键,看起来很简单,但是我们应该尽量避免这样做。因为我们无法预测未来会不会因为业务需要,而出现业务字段重复或者重用的情况。
- 自增字段做主键,对于单机系统来说是没问题的。但是,如果有多台服务器,各自都可以录入数据,那就不一定适用了。因为如果每台机器各自产生的数据需要合并,就可能会出现主键重复的问题。
- 我们可以采用手动赋值的办法,通过一定的逻辑,确保字段值在全系统的唯一性,这样就可以规避主键重复的问题了。
刚开始使用MySQL时很多人都很容易犯的错误是喜欢用业务字段做主键想当然地认为了解业务需求但实际情况往往出乎意料而更改主键设置的成本非常高。所以如果你的系统比较复杂尽量给表加一个字段做主键采用手动赋值的办法虽然系统开发的时候麻烦一点却可以避免后面出大问题。
## 思考题
在刚刚的例子中如果我想把销售流水表demo.trans中所有单位是“包”的商品的价格改成原来价格的80%,该怎么实现呢?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,426 @@
<audio id="audio" title="06 | 外键和连接:如何做关联查询?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0c/99/0cbabcfb798777a941e3095790yybe99.mp3"></audio>
你好,我是朱晓峰。今天我来和你聊一聊关联查询的问题。
在实际的数据库应用开发过程中我们经常需要把2个或2个以上的表进行关联以获取需要的数据。这是因为为了提高存取效率我们会把不同业务模块的信息分别存放在不同的表里面。但是从业务层面上看我们需要完整全面的信息为经营决策提供数据支撑。
就拿咱们的超市项目来说,数据库里面的销售流水表一般只保存销售必需的信息(比如商品编号、数量、价格、金额和会员卡号等)。但是,在呈现给超市经营者的统计报表里面,只包括这些信息是不够的,比如商品编号、会员卡号,这些数字经营者就看不懂。因此,必须要从商品信息表提取出商品信息,从会员表中提取出会员的相关信息,这样才能形成一个完整的报表。**这种把分散在多个不同的表里的数据查询出来的操作,就是多表查询**。
不过,这种查询可不简单,我们需要建立起多个表之间的关联,然后才能去查询,同时还需要规避关联表查询中的常见错误。具体怎么做呢?我来借助实际的项目给你讲一讲。
在我们项目的进货模块有这样2个数据表分别是进货单头表importhead和进货单明细表importdetails我们每天都要对这两个表进行增删改查的操作。
进货单头表记录的是整个进货单的总体信息:
<img src="https://static001.geekbang.org/resource/image/43/11/43d3b085096efc3a6111a89bbe0e7911.jpg" alt="">
进货单明细表记录了每次进货的商品明细信息。一条进货单头数据记录,对应多条进货商品的明细数据,也就是所谓的一对多的关系。具体信息如下表所示:
<img src="https://static001.geekbang.org/resource/image/36/44/369981d9a37a38fb65c0981a0544bc44.jpg" alt="">
现在我们需要查询一次进货的所有相关数据包括进货单的总体信息和进货商品的明细这样一来我们就需要把2个表关联起来那么该怎么操作呢
在MySQL中为了把2个表关联起来会用到2个重要的功能外键FOREIGN KEY和连接JOIN。外键需要在创建表的阶段就定义连接可以通过相同意义的字段把2个表连接起来用在查询阶段。
接下来,我就先和你聊聊外键。
## 如何创建外键?
我先来解释一下什么是外键。
假设我们有2个表分别是表A和表B它们通过一个公共字段“id”发生关联关系我们把这个关联关系叫做R。如果“id”在表A中是主键那么表A就是这个关系R中的主表。相应的表B就是这个关系中的从表表B中的“id”就是表B用来引用表A中数据的叫外键。所以**外键就是从表中用来引用主表中数据的那个公共字段**。
为了方便你理解,我画了一张图来展示:
<img src="https://static001.geekbang.org/resource/image/68/ae/68836a01eb1d667dea93ceda8e5714ae.jpg" alt="">
如图所示在关联关系R中公众字段字段A是表A的主键所以表A是主表表B是从表。表B中的公共字段字段A是外键。
在MySQL中外键是通过外键约束来定义的。外键约束就是约束的一种它必须在从表中定义包括指明哪个是外键字段以及外键字段所引用的主表中的主键字段是什么。MySQL系统会根据外键约束的定义监控对主表中数据的删除操作。如果发现要删除的主表记录正在被从表中某条记录的外键字段所引用MySQL就会提示错误从而确保了关联数据不会缺失。
外键约束可以在创建表的时候定义,也可以通过修改表来定义。我们先来看看外键约束定义的语法结构:
```
[CONSTRAINT &lt;外键约束名称&gt;] FOREIGN KEY 字段名
REFERENCES &lt;主表名&gt; 字段名
```
你可以在创建表的时候定义外键约束:
```
CREATE TABLE 从表名
(
字段名 类型,
...
-- 定义外键约束,指出外键字段和参照的主表字段
CONSTRAINT 外键约束名
FOREIGN KEY (字段名) REFERENCES 主表名 (字段名)
)
```
当然,你也可以通过修改表来定义外键约束:
```
ALTER TABLE 从表名 ADD CONSTRAINT 约束名 FOREIGN KEY 字段名 REFERENCES 主表名 (字段名);
```
一般情况下,表与表的关联都是提前设计好了的,因此,会在创建表的时候就把外键约束定义好。不过,如果需要修改表的设计(比如添加新的字段,增加新的关联关系),但没有预先定义外键约束,那么,就要用修改表的方式来补充定义。
下面,我就来讲一讲怎么创建外键约束。
先创建主表demo.importhead
```
CREATE TABLE demo.importhead (
listnumber INT PRIMARY KEY,
supplierid INT,
stocknumber INT,
importtype INT,
importquantity DECIMAL(10 , 3 ),
importvalue DECIMAL(10 , 2 ),
recorder INT,
recordingdate DATETIME
);
```
然后创建从表demo.importdetails并且给它定义外键约束
```
CREATE TABLE demo.importdetails
(
listnumber INT,
itemnumber INT,
quantity DECIMAL(10,3),
importprice DECIMAL(10,2),
importvalue DECIMAL(10,2),
-- 定义外键约束,指出外键字段和参照的主表字段
CONSTRAINT fk_importdetails_importhead
FOREIGN KEY (listnumber) REFERENCES importhead (listnumber)
);
```
运行这个SQL语句我们就在创建表的同时定义了一个名字叫“fk_importdetails_importhead”的外键约束。同时我们声明这个外键约束的字段“listnumber”引用的是表importhead里面的字段“listnumber”。
外键约束创建好之后我们可以通过Workbench来查看外键约束是不是创建成功了
<img src="https://static001.geekbang.org/resource/image/8f/9b/8f7154a32943699113a308b62ea5979b.png" alt="">
当然我们也可以通过SQL语句来查看这里我们要用到MySQL自带的、用于存储系统信息的数据库information_schema。我们可以查看外键约束的相关信息
```
mysql&gt; SELECT
-&gt; constraint_name, -- 表示外键约束名称
-&gt; table_name, -- 表示外键约束所属数据表的名称
-&gt; column_name, -- 表示外键约束的字段名称
-&gt; referenced_table_name, -- 表示外键约束所参照的数据表名称
-&gt; referenced_column_name -- 表示外键约束所参照的字段名称
-&gt; FROM
-&gt; information_schema.KEY_COLUMN_USAGE
-&gt; WHERE
-&gt; constraint_name = 'fk_importdetails_importhead';
+-----------------------------+---------------+-------------+-----------------------+------------------------+
| CONSTRAINT_NAME | TABLE_NAME | COLUMN_NAME | REFERENCED_TABLE_NAME | REFERENCED_COLUMN_NAME |
+-----------------------------+---------------+-------------+-----------------------+------------------------+
| fk_importdetails_importhead | importdetails | listnumber | importhead | listnumber |
+-----------------------------+---------------+-------------+-----------------------+------------------------+
1 row in set (0.05 sec)
```
通过查询我们可以看到外键约束所在的表是“importdetails”外键字段是“listnumber”参照的主表是“importhead”参照的主表字段是“listnumber”。这样通过定义外键约束我们已经建立起了2个表之间的关联关系。
关联关系建立起来之后,如何才能获取我们需要的数据呢?这时,我们就需要用到连接查询了。
## 连接
在MySQL中有2种类型的连接分别是内连接INNER JOIN和外连接OUTER JOIN
- 内连接表示查询结果只返回符合连接条件的记录,这种连接方式比较常用;
- 外连接则不同,表示查询结果返回某一个表中的所有记录,以及另一个表中满足连接条件的记录。
下面我先来讲一下内连接。
在MySQL里面关键字JOIN、INNER JOIN、CROSS JOIN的含义是一样的都表示内连接。我们可以通过JOIN把两个表关联起来来查询两个表中的数据。
我借助一个小例子,来帮助你理解。
咱们的项目中有会员销售的需求,所以,我们的流水表中的数据记录,既包括非会员的普通销售,又包括会员销售。它们的区别是,会员销售的数据记录包括会员编号,而在非会员销售的数据记录中,会员编号为空。
来看一下项目中的销售表demo.trans)。实际的销售表比较复杂为了方便你理解我把表进行了简化并且假设业务字段cardno是会员信息表的主键。简化以后的结构如下所示
<img src="https://static001.geekbang.org/resource/image/97/73/97b818520dc0de32c5c8e17181746673.jpg" alt="">
再看下简化后的会员信息表demo.membermaster
<img src="https://static001.geekbang.org/resource/image/02/c7/023f62732eede3ee7c14cebd689770c7.jpg" alt="">
这两个表之间存在关联关系表demo.trans中的字段“cardno”是这个关联关系中的外键。
我们可以通过内连接,查询所有会员销售的流水记录:
```
mysql&gt; SELECT
-&gt; a.transactionno,
-&gt; a.itemnumber,
-&gt; a.quantity,
-&gt; a.price,
-&gt; a.transdate,
-&gt; b.membername
-&gt; FROM
-&gt; demo.trans AS a
-&gt; JOIN
-&gt; demo.membermaster AS b ON (a.cardno = b.cardno);
+---------------+------------+----------+-------+---------------------+------------+
| transactionno | itemnumber | quantity | price | transdate | membername |
+---------------+------------+----------+-------+---------------------+------------+
| 1 | 1 | 1.000 | 89.00 | 2020-12-01 00:00:00 | 张三 |
+---------------+------------+----------+-------+---------------------+------------+
1 row in set (0.00 sec)
```
可以看到我们通过公共字段“cardno”把两个表关联到了一起查询出了会员消费的数据。
在这里关键字JOIN与关键字ON配对使用意思是查询满足关联条件“demo.trans表中cardno的值与demo.membermaster表中的cardno值相等”的两个表中的所有记录。
知道了内连接,我们再来学习下外连接。跟内连接只返回符合连接条件的记录不同的是,外连接还可以返回表中的所有记录,它包括两类,分别是左连接和右连接。
- 左连接一般简写成LEFT JOIN返回左边表中的所有记录以及右表中符合连接条件的记录。
- 右连接一般简写成RIGHT JOIN返回右边表中的所有记录以及左表中符合连接条件的记录。
当我们需要查询全部流水信息的时候,就会用到外连接,代码如下:
```
SELECT
a.transactionno,
a.itemnumber,
a.quantity,
a.price,
a.transdate,
b.membername
FROM demo.trans AS a
LEFT JOIN demo.membermaster AS b -- LEFT JOIN以demo.transaction为主
ON (a.cardno = b.cardno);
```
可以看到我用到了LEFT JOIN意思是以表demo.trans中的数据记录为主这个表中的数据记录要全部出现在结果集中同时给出符合连接条件a.cardno=b.cardno)的表demo.membermaster中的字段membername的值。
我们也可以使用RIGHT JOIN实现同样的效果代码如下
```
SELECT
a.transactionno,
a.itemnumber,
a.quantity,
a.price,
a.transdate,
b.membername
FROM
demo.membermaster AS b
RIGHT JOIN
demo.trans AS a ON (a.cardno = b.cardno); -- RIGHT JOIN顺序颠倒了还是以demo.trans为主
```
其实,这里就是把顺序颠倒了一下,意思是一样的。运行之后,我们都能得到下面的结果:
```
mysql&gt; SELECT
-&gt; a.transactionno,
-&gt; a.itemnumber,
-&gt; a.quantity,
-&gt; a.price,
-&gt; a.transdate,
-&gt; b.membername
-&gt; FROM
-&gt; demo.trans AS a
-&gt; LEFT JOIN -- 左连接
-&gt; demo.membermaster AS b ON (a.cardno = b.cardno);
+---------------+------------+----------+-------+---------------------+------------+
| transactionno | itemnumber | quantity | price | transdate | membername |
+---------------+------------+----------+-------+---------------------+------------+
| 1 | 1 | 1.000 | 89.00 | 2020-12-01 00:00:00 | 张三 |
| 2 | 2 | 1.000 | 12.00 | 2020-12-02 00:00:00 | NULL |
+---------------+------------+----------+-------+---------------------+------------+
2 rows in set (0.00 sec)
mysql&gt; SELECT
-&gt; a.transactionno,
-&gt; a.itemnumber,
-&gt; a.quantity,
-&gt; a.price,
-&gt; a.transdate,
-&gt; b.membername
-&gt; FROM
-&gt; demo.membermaster AS b
-&gt; RIGHT JOIN -- 右连接
-&gt; demo.trans AS a
-&gt; ON (a.cardno = b.cardno);
+---------------+------------+----------+-------+---------------------+------------+
| transactionno | itemnumber | quantity | price | transdate | membername |
+---------------+------------+----------+-------+---------------------+------------+
| 1 | 1 | 1.000 | 89.00 | 2020-12-01 00:00:00 | 张三 |
| 2 | 2 | 1.000 | 12.00 | 2020-12-02 00:00:00 | NULL |
+---------------+------------+----------+-------+---------------------+------------+
2 rows in set (0.00 sec)
```
通过关联查询,销售流水数据里就补齐了会员的名称,我们也就获取到了需要的数据。
## 关联查询的误区
有了连接我们就可以进行2个表的关联查询了。你可能会有疑问关联查询必须在外键约束的基础上才可以吗
其实在MySQL中外键约束不是关联查询的必要条件。很多人往往在设计表的时候觉得只要连接查询就可以搞定一切了外键约束太麻烦没有必要。如果你这么想就进入了一个误区。
下面我就以超市进货的例子,来实际说明一下,为什么这种思路不对。
假设一次进货数据是这样的供货商编号是1进货仓库编号是1。我们进货的商品编号是1234进货数量是1进货价格是10进货金额是10。
我先插入单头数据:
```
INSERT INTO demo.importhead
(
listnumber,
supplierid,
stocknumber
)
VALUES
(
1234,
1,
1
);
```
运行成功后,查看一下表的内容:
```
mysql&gt; SELECT *
-&gt; FROM demo.importhead;
+------------+------------+-------------+------------+----------+-------------+-------------+
| listnumber | supplierid | stocknumber | importtype | quantity | importprice | importvalue |
+------------+------------+-------------+------------+----------+-------------+-------------+
| 1234 | 1 | 1 | 1 | NULL | NULL | NULL |
+------------+------------+-------------+------------+----------+-------------+-------------+
1 row in set (0.00 sec)
```
可以看到我们有了一个进货单头单号是1234供货商是1号供货商进货仓库是1号仓库。
接着,我们向进货单明细表中插入进货明细数据:
```
INSERT INTO demo.importdetails
(
listnumber,
itemnumber,
quantity,
importprice,
importvalue
)
VALUES
(
1234,
1,
1,
10,
10
);
```
运行成功,查看一下表的内容:
```
mysql&gt; SELECT *
-&gt; FROM demo.importdetails;
+------------+------------+----------+-------------+-------------+
| listnumber | itemnumber | quantity | importprice | importvalue |
+------------+------------+----------+-------------+-------------+
| 1234 | 1 | 1.000 | 10.00 | 10.00 |
+------------+------------+----------+-------------+-------------+
1 row in set (0.00 sec)
```
这样我们就有了1234号进货单的明细数据进货商品是1号商品进货数量是1个进货价格是10元进货金额是10元。
这个时候,如果我删除进货单头表的数据,就会出现只有明细、没有单头的数据缺失情况。我们来看看会发生什么:
```
DELETE FROM demo.importhead
WHERE listnumbere = 1234;
```
运行这条语句MySQL会提示错误因为数据删除违反了外键约束。看到了吗MySQL阻止了数据不一致的情况出现。
不知道你有没有注意我插入数据的顺序为什么我要先插入进货单头表的数据再插入进货单明细表的数据呢其实这是因为如果我先插入数据到从表也就是进货单明细表会导致MySQL找不到参照的主表信息会提示错误因为添加数据违反了外键约束。
你可能会不以为然,觉得按照信息系统的操作逻辑,生成一张进货单的时候,一定是先生成单头,再插入明细。同样,删除一张进货单的时候,一定是先删除明细,再删除单头。
要是你这么想可能就会“中招”了。原因很简单既然我们把进货数据拆成了2个表这就决定了无论是数据添加还是数据删除都不能通过一条SQL语句实现。实际工作中什么突发情况都是有可能发生的。你认为一定会完成的操作完全有可能只执行了一部分。
我们曾经就遇到过这么一个问题:用户月底盘点的时候,盘点单无法生成,系统提示“有未处理的进货单”。经过排查,发现是进货单数据发生了数据缺失,明细数据还在,对应的单头数据却被删除了。我们反复排查之后,才发现是缺少了防止数据缺失的机制。最后通过定义外键约束,解决了这个问题。
所以虽然你不用外键约束也可以进行关联查询但是有了它MySQL系统才会保护你的数据避免出现误删的情况从而提高系统整体的可靠性。
现在来回答另外一个问题为什么在MySQL里没有外键约束也可以进行关联查询呢原因是外键约束是有成本的需要消耗系统资源。对于大并发的SQL操作有可能会不适合。比如大型网站的中央数据库可能会因为外键约束的系统开销而变得非常慢。所以MySQL允许你不使用系统自带的外键约束在应用层面完成检查数据一致性的逻辑。也就是说即使你不用外键约束也要想办法通过应用层面的附加逻辑来实现外键约束的功能确保数据的一致性。
## 总结
这节课,我给你介绍了如何进行多表查询,我们重点学习了外键和连接。
外键约束可以帮助我们确定从表中的外键字段与主表中的主键字段之间的引用关系还可以确保从表中数据所引用的主表数据不会被删除从而保证了2个表中数据的一致性。
连接可以帮助我们对2个相关的表进行连接查询从2个表中获取需要的信息。左连接表示连接以左边的表为主结果集中要包括左边表中的所有记录右连接表示连接以右边的表为主结果集中要包括右边表中的所有记录。
我汇总了常用的SQL语句你一定要重点掌握。
```
-- 定义外键约束:
CREATE TABLE 从表名
(
字段 字段类型
....
CONSTRAINT 外键约束名称
FOREIGN KEY (字段名) REFERENCES 主表名 (字段名称)
);
ALTER TABLE 从表名 ADD CONSTRAINT 约束名 FOREIGN KEY 字段名 REFERENCES 主表名 (字段名);
-- 连接查询
SELECT 字段名
FROM 表名 AS a
JOIN 表名 AS b
ON (a.字段名称=b.字段名称);
SELECT 字段名
FROM 表名 AS a
LEFT JOIN 表名 AS b
ON (a.字段名称=b.字段名称);
SELECT 字段名
FROM 表名 AS a
RIGHT JOIN 表名 AS b
ON (a.字段名称=b.字段名称);
```
刚开始学习MySQL的同学很容易忽略在关联表中定义外键约束的重要性从而导致数据缺失影响系统的可靠性。我建议你尽量养成在关联表中定义外键约束的习惯。不过如果你的业务场景因为高并发等原因无法承担外键约束的成本也可以不定义外键约束但是一定要在应用层面实现外键约束的逻辑功能这样才能确保系统的正确可靠。
## 思考题
如果你的业务场景因高并发等原因,不能使用外键约束,在这种情况下,你怎么在应用层面确保数据的一致性呢?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,503 @@
<audio id="audio" title="07 | 条件语句WHERE 与 HAVING有什么不同?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b7/6f/b7c2af698e89790c1500c1a9f964856f.mp3"></audio>
你好,我是朱晓峰。
我们在进行查询的时候经常需要按条件对查询结果进行筛选这就要用到条件语句WHERE和HAVING了。
WHERE是直接对表中的字段进行限定来筛选结果HAVING则需要跟分组关键字GROUP BY一起使用通过对分组字段或分组计算函数进行限定来筛选结果。虽然它们都是对查询进行限定却有着各自的特点和适用场景。很多时候我们会遇到2个都可以用的情况。一旦用错就很容易出现执行效率低下、查询结果错误甚至是查询无法运行的情况。
下面我就借助项目实施过程中的实际需求给你讲讲WHERE和HAVING分别是如何对查询结果进行筛选的以及它们各自的优缺点来帮助你正确地使用它们使你的查询不仅能够得到正确的结果还能占用更少的资源并且速度更快。
## 一个实际查询需求
超市的经营者提出要查单笔销售金额超过50元的商品。我们来分析一下这个需求需要查询出一个商品记录集限定条件是单笔销售金额超过50元。这个时候我们就需要用到WHERE和HAVING了。
这个问题的条件很明确,查询的结果也只有“商品”一个字段,好像很容易实现。
假设我们有一个这样的商品信息表demo.goodsmaster里面有2种商品书和笔。
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice |
+------------+---------+-----------+---------------+------+------------+
| 1 | 0001 | 书 | | 本 | 89.00 |
| 2 | 0002 | 笔 | | 支 | 5.00 |
+------------+---------+-----------+---------------+------+------------+
2 rows in set (0.00 sec)
```
同时我们还有一个商品销售明细表demo.transactiondetails里面有4条销售记录
```
mysql&gt; SELECT *
-&gt; FROM demo.transactiondetails;
+---------------+------------+----------+-------+------------+
| transactionid | itemnumber | quantity | price | salesvalue |
+---------------+------------+----------+-------+------------+
| 1 | 1 | 1.000 | 89.00 | 89.00 |
| 1 | 2 | 2.000 | 5.00 | 10.00 |
| 2 | 1 | 2.000 | 89.00 | 178.00 |
| 3 | 2 | 10.000 | 5.00 | 50.00 |
+---------------+------------+----------+-------+------------+
4 rows in set (0.01 sec)
```
接下来我们分别用WHERE和HAVING进行查询看看它们各自是如何查询的是否能够得到正确的结果。
第一步用WHERE关键字进行查询
```
mysql&gt; SELECT DISTINCT b.goodsname
-&gt; FROM demo.transactiondetails AS a
-&gt; JOIN demo.goodsmaster AS b
-&gt; ON (a.itemnumber=b.itemnumber)
-&gt; WHERE a.salesvalue &gt; 50;
+-----------+
| goodsname |
+-----------+
| 书 |
+-----------+
1 row in set (0.00 sec)
```
第二步用HAVING关键字进行查询
```
mysql&gt; SELECT b.goodsname
-&gt; FROM demo.transactiondetails AS a
-&gt; JOIN demo.goodsmaster AS b
-&gt; ON (a.itemnumber=b.itemnumber)
-&gt; GROUP BY b.goodsname
-&gt; HAVING max(a.salesvalue)&gt;50;
+-----------+
| goodsname |
+-----------+
| 书 |
+-----------+
1 row in set (0.00 sec)
```
可以发现两次查询的结果是一样的。那么这两种查询到底有什么区别哪个更好呢要弄明白这个问题我们要先学习下WHERE和HAVING的执行过程。
## WHERE
我们先来分析一下刚才使用WHERE条件的查询语句来看看MySQL是如何执行这个查询的。
首先MySQL从数据表demo.transactiondetails中抽取满足条件“a.salesvalue&gt;50”的记录
```
mysql&gt; SELECT *
-&gt; FROM demo.transactiondetails AS a
-&gt; WHERE a.salesvalue &gt; 50;
+---------------+------------+----------+-------+------------+
| transactionid | itemnumber | quantity | price | salesvalue |
+---------------+------------+----------+-------+------------+
| 1 | 1 | 1.000 | 89.00 | 89.00 |
| 2 | 1 | 2.000 | 89.00 | 178.00 |
+---------------+------------+----------+-------+------------+
2 rows in set (0.00 sec)
```
为了获取到销售信息所对应的商品名称我们需要通过公共字段“itemnumber”与数据表demo.goodsmaster进行关联从demo.goodsmaster中获取商品名称
```
mysql&gt; SELECT
-&gt; a.*, b.goodsname
-&gt; FROM
-&gt; demo.transactiondetails a
-&gt; JOIN
-&gt; demo.goodsmaster b ON (a.itemnumber = b.itemnumber)
-&gt; WHERE
-&gt; a.salesvalue &gt; 50;
+---------------+------------+----------+-------+------------+-----------+
| transactionid | itemnumber | quantity | price | salesvalue | goodsname |
+---------------+------------+----------+-------+------------+-----------+
| 1 | 1 | 1.000 | 89.00 | 89.00 | 书 |
| 2 | 1 | 2.000 | 89.00 | 178.00 | 书 |
+---------------+------------+----------+-------+------------+-----------+
2 rows in set (0.00 sec)
```
这个时候,如果查询商品名称,就会出现两个重复的记录:
```
mysql&gt; SELECT
-&gt; b.goodsname
-&gt; FROM
-&gt; demo.transactiondetails AS a
-&gt; JOIN
-&gt; demo.goodsmaster AS b ON (a.itemnumber = b.itemnumber)
-&gt; WHERE
-&gt; a.salesvalue &gt; 50;
+-----------+
| goodsname |
+-----------+
| 书 |
| 书 |
+-----------+
2 rows in set (0.00 sec)
```
需要注意的是为了消除重复的语句这里我们需要用到一个关键字DISTINCT它的作用是返回唯一不同的值。比如DISTINCT 字段1就表示返回所有字段1的不同的值。
下面我们尝试一下加上DISTINCT关键字的查询
```
mysql&gt; SELECT
-&gt; DISTINCT(b.goodsname) -- 返回唯一不同的值
-&gt; FROM
-&gt; demo.transactiondetails AS a
-&gt; JOIN
-&gt; demo.goodsmaster AS b ON (a.itemnumber = b.itemnumber)
-&gt; WHERE
-&gt; a.salesvalue &gt; 50;
+-----------+
| goodsname |
+-----------+
| 书 |
+-----------+
1 row in set (0.00 sec)
```
这样我们就得到了需要的结果单笔销售金额超过50元的商品就是“书”。
总之WHERE关键字的特点是直接用表的字段对数据集进行筛选。如果需要通过关联查询从其他的表获取需要的信息那么执行的时候也是先通过WHERE条件进行筛选用筛选后的比较小的数据集进行连接。这样一来连接过程中占用的资源比较少执行效率也比较高。
## HAVING
讲完了WHERE我们再说说HAVING是如何执行的。不过在这之前我要先给你介绍一下GROUP BY因为HAVING不能单独使用必须要跟GROUP BY一起使用。
我们可以把GROUP BY理解成对数据进行分组方便我们对组内的数据进行统计计算。
下面我举个小例子具体讲一讲GROUP BY如何使用以及如何在分组里面进行统计计算。
假设现在有一组销售数据,我们需要从里面查询每天、每个收银员的销售数量和销售金额。我们通过下面的代码,来查看一下数据的内容:
```
mysql&gt; SELECT *
-&gt; FROM demo.transactionhead;
+---------------+------------------+------------+---------------------+
| transactionid | transactionno | operatorid | transdate |
+---------------+------------------+------------+---------------------+
| 1 | 0120201201000001 | 1 | 2020-12-10 00:00:00 |
| 2 | 0120201202000001 | 2 | 2020-12-11 00:00:00 |
| 3 | 0120201202000002 | 2 | 2020-12-12 00:00:00 |
+---------------+------------------+------------+---------------------+
3 rows in set (0.00 sec)
mysql&gt; SELECT *
-&gt; FROM demo.transactiondetails;
+---------------+------------+----------+-------+------------+
| transactionid | itemnumber | quantity | price | salesvalue |
+---------------+------------+----------+-------+------------+
| 1 | 1 | 1.000 | 89.00 | 89.00 |
| 1 | 2 | 2.000 | 5.00 | 10.00 |
| 2 | 1 | 2.000 | 89.00 | 178.00 |
| 3 | 2 | 10.000 | 5.00 | 50.00 |
+---------------+------------+----------+-------+------------+
4 rows in set (0.01 sec)
mysql&gt; SELECT *
-&gt; FROM demo.operator;
+------------+----------+--------+--------------+-------------+---------+--------------------+--------+
| operatorid | branchid | workno | operatorname | phone | address | pid | duty |
+------------+----------+--------+--------------+-------------+---------+--------------------+--------+
| 1 | 1 | 001 | 张静 | 18612345678 | 北京 | 110392197501012332 | 店长 |
| 2 | 1 | 002 | 李强 | 13312345678 | 北京 | 110222199501012332 | 收银员 |
+------------+----------+--------+--------------+-------------+---------+--------------------+--------+
2 rows in set (0.01 sec)
mysql&gt; SELECT
-&gt; a.transdate, -- 交易时间
-&gt; c.operatorname,-- 操作员
-&gt; d.goodsname, -- 商品名称
-&gt; b.quantity, -- 销售数量
-&gt; b.price, -- 价格
-&gt; b.salesvalue -- 销售金额
-&gt; FROM
-&gt; demo.transactionhead AS a
-&gt; JOIN
-&gt; demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.operator AS c ON (a.operatorid = c.operatorid)
-&gt; JOIN
-&gt; demo.goodsmaster AS d ON (b.itemnumber = d.itemnumber);
+---------------------+--------------+-----------+----------+-------+------------+
| transdate | operatorname | goodsname | quantity | price | salesvalue |
+---------------------+--------------+-----------+----------+-------+------------+
| 2020-12-10 00:00:00 | 张静 | 书 | 1.000 | 89.00 | 89.00 |
| 2020-12-10 00:00:00 | 张静 | 笔 | 2.000 | 5.00 | 10.00 |
| 2020-12-11 00:00:00 | 李强 | 书 | 2.000 | 89.00 | 178.00 |
| 2020-12-12 00:00:00 | 李强 | 笔 | 10.000 | 5.00 | 50.00 |
+---------------------+--------------+-----------+----------+-------+------------+
4 rows in set (0.00 sec)
```
如果我想看看每天的销售数量和销售金额可以按照一个字段“transdate”对数据进行分组和统计
```
mysql&gt; SELECT
-&gt; a.transdate,
-&gt; SUM(b.quantity), -- 统计分组的总计销售数量
-&gt; SUM(b.salesvalue) -- 统计分组的总计销售金额
-&gt; FROM
-&gt; demo.transactionhead AS a
-&gt; JOIN
-&gt; demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
-&gt; GROUP BY a.transdate;
+---------------------+-----------------+-------------------+
| transdate | SUM(b.quantity) | SUM(b.salesvalue) |
+---------------------+-----------------+-------------------+
| 2020-12-10 00:00:00 | 3.000 | 99.00 |
| 2020-12-11 00:00:00 | 2.000 | 178.00 |
| 2020-12-12 00:00:00 | 10.000 | 50.00 |
+---------------------+-----------------+-------------------+
3 rows in set (0.00 sec)
```
如果我想看每天、每个收银员的销售数量和销售金额就可以按2个字段进行分组和统计分别是“transdate”和“operatorname”
```
mysql&gt; SELECT
-&gt; a.transdate,
-&gt; c.operatorname,
-&gt; SUM(b.quantity), -- 数量求和
-&gt; SUM(b.salesvalue)-- 金额求和
-&gt; FROM
-&gt; demo.transactionhead AS a
-&gt; JOIN
-&gt; demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.operator AS C ON (a.operatorid = c.operatorid)
-&gt; GROUP BY a.transdate , c.operatorname; -- 按照交易日期和操作员分组
+---------------------+--------------+-----------------+-------------------+
| transdate | operatorname | SUM(b.quantity) | SUM(b.salesvalue) |
+---------------------+--------------+-----------------+-------------------+
| 2020-12-10 00:00:00 | 张静 | 3.000 | 99.00 |
| 2020-12-11 00:00:00 | 李强 | 2.000 | 178.00 |
| 2020-12-12 00:00:00 | 李强 | 10.000 | 50.00 |
+---------------------+--------------+-----------------+-------------------+
3 rows in set (0.00 sec)
```
可以看到,通过对销售数据按照交易日期和收银员进行分组,再对组内数据进行求和统计,就实现了对每天、每个收银员的销售数量和销售金额的查询。
好了知道了GROUP BY的使用方法我们就来学习下HAVING。
回到开头的超市经营者的需求查询单笔销售金额超过50元的商品。现在我们来使用HAVING来实现代码如下
```
mysql&gt; SELECT b.goodsname
-&gt; FROM demo.transactiondetails AS a
-&gt; JOIN demo.goodsmaster AS b
-&gt; ON (a.itemnumber=b.itemnumber)
-&gt; GROUP BY b.goodsname
-&gt; HAVING max(a.salesvalue)&gt;50;
+-----------+
| goodsname |
+-----------+
| 书 |
+-----------+
1 row in set (0.00 sec)
```
这种查询方式在MySQL里面是分四步实现的。
第一步把流水明细表和商品信息表通过公共字段“itemnumber”连接起来从2个表中获取数据
```
mysql&gt; SELECT
-&gt; a.*, b.*
-&gt; FROM
-&gt; demo.transactiondetails a
-&gt; JOIN
-&gt; demo.goodsmaster b ON (a.itemnumber = b.itemnumber);
+---------------+------------+----------+-------+------------+------------+---------+-----------+---------------+------+------------+
| transactionid | itemnumber | quantity | price | salesvalue | itemnumber | barcode | goodsname | specification | unit | salesprice |
+---------------+------------+----------+-------+------------+------------+---------+-----------+---------------+------+------------+
| 1 | 1 | 1.000 | 89.00 | 89.00 | 1 | 0001 | 书 | NULL | 本 | 89.00 |
| 1 | 2 | 2.000 | 5.00 | 10.00 | 2 | 0002 | 笔 | NULL | 支 | 5.00 |
| 2 | 1 | 2.000 | 89.00 | 178.00 | 1 | 0001 | 书 | NULL | 本 | 89.00 |
| 3 | 2 | 10.000 | 5.00 | 50.00 | 2 | 0002 | 笔 | NULL | 支 | 5.00 |
+---------------+------------+----------+-------+------------+------------+---------+-----------+---------------+------+------------+
4 rows in set (0.00 sec)
```
查询的结果有点复杂,为了方便你理解,我对结果进行了分类,并加了注释,如下图所示:
<img src="https://static001.geekbang.org/resource/image/5a/33/5a65e30972010a72576d4008fb0b9333.jpg" alt="">
第二步,把结果集按照商品名称分组,分组的示意图如下所示:
组1
<img src="https://static001.geekbang.org/resource/image/23/10/239766d1849b25d03049be4f21c95510.jpg" alt="">
组2
<img src="https://static001.geekbang.org/resource/image/2d/ca/2d4b2fec2c3b84a25928f21353727eca.jpeg" alt="">
第三步对分组后的数据集进行筛选把组中字段“salesvalue”的最大值&gt;50的组筛选出来。筛选后的结果集如下所示
<img src="https://static001.geekbang.org/resource/image/96/a6/96bdae61f5924a9118071294cab86ba6.jpeg" alt="">
第四步返回商品名称。这时我们就得到了需要的结果单笔销售金额超过50元的商品就是“书”。
现在我们来简单小结下使用HAVING的查询过程。首先我们要把所有的信息都准备好包括从关联表中获取需要的信息对数据集进行分组形成一个包含所有需要的信息的数据集合。接着再通过HAVING条件的筛选得到需要的数据。
## 怎么正确地使用WHERE和HAVING
现在你已经知道了WHERE和HAVING的具体使用方法。那么在查询时我们怎样才能正确地使用它们呢
首先你要知道它们的2个典型区别。
第一个区别是,**如果需要通过连接从关联表中获取需要的数据WHERE是先筛选后连接而HAVING是先连接后筛选**。
这一点就决定了在关联查询中WHERE比HAVING更高效。因为WHERE可以先筛选用一个筛选后的较小数据集和关联表进行连接这样占用的资源比较少执行效率也就比较高。HAVING则需要先把结果集准备好也就是用未被筛选的数据集进行关联然后对这个大的数据集进行筛选这样占用的资源就比较多执行效率也较低。
第二个区别是WHERE可以直接使用表中的字段作为筛选条件但不能使用分组中的计算函数作为筛选条件HAVING必须要与GROUP BY配合使用可以把分组计算的函数和分组字段作为筛选条件。
这决定了,**在需要对数据进行分组统计的时候HAVING可以完成WHERE不能完成的任务**。这是因为在查询语法结构中WHERE在GROUP BY之前所以无法对分组结果进行筛选。HAVING在GROUP BY之后可以使用分组字段和分组中的计算函数对分组的结果集进行筛选这个功能是WHERE无法完成的。
这么说你可能不太好理解我来举个小例子。假如超市经营者提出要查询一下是哪个收银员、在哪天卖了2单商品。这种必须先分组才能筛选的查询用WHERE语句实现就比较难我们可能要分好几步通过把中间结果存储起来才能搞定。但是用HAVING则很轻松代码如下
```
mysql&gt; SELECT
-&gt; a.transdate, c.operatorname
-&gt; FROM
-&gt; demo.transactionhead AS a
-&gt; JOIN
-&gt; demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.operator AS c ON (a.operatorid = c.operatorid)
-&gt; GROUP BY a.transdate,c.operatorname
-&gt; HAVING count(*)=2; -- 销售了2单
+---------------------+--------------+
| transdate | operatorname |
+---------------------+--------------+
| 2020-12-10 00:00:00 | 张静 |
+---------------------+--------------+
1 row in set (0.01 sec)
```
我汇总了WHERE和HAVING各自的优缺点如下图所示
<img src="https://static001.geekbang.org/resource/image/24/50/2423421554df9a7dfd15495beb850150.jpg" alt="">
不过需要注意的是WHERE和HAVING也不是互相排斥的我们可以在一个查询里面同时使用WHERE和HAVING。
举个例子假设现在我们有一组销售数据包括交易时间、收银员、商品名称、销售数量、价格和销售金额等信息超市的经营者要查询“2020-12-10”和“2020-12-11”这两天收银金额超过100元的销售日期、收银员名称、销售数量和销售金额。
```
mysql&gt; SELECT
-&gt; a.transdate,
-&gt; c.operatorname,
-&gt; d.goodsname,
-&gt; b.quantity,
-&gt; b.price,
-&gt; b.salesvalue
-&gt; FROM
-&gt; demo.transactionhead AS a
-&gt; JOIN
-&gt; demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.operator AS c ON (a.operatorid = c.operatorid)
-&gt; JOIN
-&gt; demo.goodsmaster as d on (b.itemnumber=d.itemnumber);
+---------------------+--------------+-----------+----------+-------+------------+
| transdate | operatorname | goodsname | quantity | price | salesvalue |
+---------------------+--------------+-----------+----------+-------+------------+
| 2020-12-10 00:00:00 | 张静 | 书 | 1.000 | 89.00 | 89.00 |
| 2020-12-10 00:00:00 | 张静 | 笔 | 2.000 | 5.00 | 10.00 |
| 2020-12-11 00:00:00 | 李强 | 书 | 2.000 | 89.00 | 178.00 |
| 2020-12-12 00:00:00 | 李强 | 笔 | 10.000 | 5.00 | 50.00 |
+---------------------+--------------+-----------+----------+-------+------------+
4 rows in set (0.00 sec)
```
我们来分析一下这个需求由于是要按照销售日期和收银员进行统计所以必须按照销售日期和收银员进行分组因此我们可以通过使用GROUP BY和HAVING进行查询
```
mysql&gt; SELECT
-&gt; a.transdate,
-&gt; c.operatorname,
-&gt; SUM(b.quantity), -- 销售数量求和
-&gt; SUM(b.salesvalue)-- 销售金额求和
-&gt; FROM
-&gt; demo.transactionhead AS a
-&gt; JOIN
-&gt; demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.operator AS c ON (a.operatorid = c.operatorid)
-&gt; GROUP BY a.transdate , operatorname -- 按照日期、收银员分组
-&gt; HAVING a.transdate IN ('2020-12-10' , '2020-12-11')
-&gt; AND SUM(b.salesvalue) &gt; 100; -- 最后筛选数据
+---------------------+--------------+-----------------+-------------------+
| transdate | operatorname | SUM(b.quantity) | SUM(b.salesvalue) |
+---------------------+--------------+-----------------+-------------------+
| 2020-12-11 00:00:00 | 李强 | 2.000 | 178.00 |
+---------------------+--------------+-----------------+-------------------+
1 row in set (0.00 sec)
```
如果你仔细看HAVING后面的筛选条件就会发现条件a.transdate IN ('2020-12-10' , '2020-12-11')其实可以用WHERE来限定。我们把查询改一下试试
```
mysql&gt; SELECT
-&gt; a.transdate,
-&gt; c.operatorname,
-&gt; SUM(b.quantity),
-&gt; SUM(b.salesvalue)
-&gt; FROM
-&gt; demo.transactionhead AS a
-&gt; JOIN
-&gt; demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.operator AS c ON (a.operatorid = c.operatorid)
-&gt; WHERE a.transdate in ('2020-12-12','2020-12-11') -- 先按日期筛选
-&gt; GROUP BY a.transdate , operatorname
-&gt; HAVING SUM(b.salesvalue)&gt;100; -- 后按金额筛选
+---------------------+--------------+-----------------+-------------------+
| transdate | operatorname | SUM(b.quantity) | SUM(b.salesvalue) |
+---------------------+--------------+-----------------+-------------------+
| 2020-12-11 00:00:00 | 李强 | 2.000 | 178.00 |
+---------------------+--------------+-----------------+-------------------+
1 row in set (0.00 sec)
```
很显然我们同样得到了需要的结果。这是因为我们把条件拆分开包含分组统计函数的条件用HAVING普通条件用WHERE。这样我们就既利用了WHERE条件的高效快速又发挥了HAVING可以使用包含分组统计函数的查询条件的优点。当数据量特别大的时候运行效率会有很大的差别。
## 总结
今天我给你介绍了条件语句WHERE和HAVING在MySQL中的执行原理。WHERE可以先按照条件对数据进行筛选然后进行数据连接所以效率更高。HAVING可以在分组之后通过使用分组中的计算函数实现WHERE难以完成的数据筛选。
了解了WHERE和HAVING各自的特点我们就可以在查询中充分利用它们的优势更高效地实现我们的查询目标。
最后我想提醒你的是很多人刚开始学习MySQL的时候不太喜欢用HAVING一提到条件语句就想当然地用WHERE。其实HAVING是非常有用的特别是在做一些复杂的统计查询的时候经常要用到分组这个时候HAVING就派上用场了。
当然你也可以不用HAVING而是把查询分成几步把中间结果存起来再用WHERE筛选或者干脆把这部分筛选功能放在应用层面用代码来实现。但是这样做的效率很低而且会增加工作量加大维护成本。所以学会使用HAVING对你完成复杂的查询任务非常有帮助。
## 思考题
有这样一种说法HAVING后面的条件必须是包含分组中的计算函数的条件你觉得对吗为什么
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,345 @@
<audio id="audio" title="08 | 聚合函数:怎么高效地进行分组统计?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3d/c2/3df24cf968e41eed5dcd06c9e1e9eec2.mp3"></audio>
你好,我是朱晓峰。今天,我来和你聊一聊聚合函数。
MySQL中有5种聚合函数较为常用分别是求和函数SUM()、求平均函数AVG()、最大值函数MAX()、最小值函数MIN()和计数函数COUNT()。接下来,我就结合超市项目的真实需求,来带你掌握聚合函数的用法,帮你实现高效的分组统计。
咱们的项目需求是这样的超市经营者提出他们需要统计某个门店每天、每个单品的销售情况包括销售数量和销售金额等。这里涉及3个数据表具体信息如下所示
销售明细表demo.transactiondetails)
<img src="https://static001.geekbang.org/resource/image/ba/4e/ba86b64760c96caf85872f362790534e.jpeg" alt="">
销售单头表demo.transactionhead)
<img src="https://static001.geekbang.org/resource/image/32/50/3242decd05814f16479f2e6edb5fd050.jpeg" alt="">
商品信息表demo.goodsmaster
<img src="https://static001.geekbang.org/resource/image/d7/1a/d72f0fb930280cb611d8f31aed98bf1a.jpeg" alt="">
要统计销售就要用到数据求和那么我们就先来学习下求和函数SUM()。
## SUM
SUM函数可以返回指定字段值的和。我们可以用它来获得用户某个门店每天每种商品的销售总计数据
```
mysql&gt; SELECT
-&gt; LEFT(b.transdate, 10), -- 从关联表获取交易时间并且通过LEFT函数获取交易时间字符串的左边10个字符得到年月日的数据
-&gt; c.goodsname, -- 从关联表获取商品名称
-&gt; SUM(a.quantity), -- 数量求和
-&gt; SUM(a.salesvalue) -- 金额求和
-&gt; FROM
-&gt; demo.transactiondetails a
-&gt; JOIN
-&gt; demo.transactionhead b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.goodsmaster c ON (a.itemnumber = c.itemnumber)
-&gt; GROUP BY LEFT(b.transdate, 10) , c.goodsname -- 分组
-&gt; ORDER BY LEFT(b.transdate, 10) , c.goodsname; -- 排序
+-----------------------+-----------+-----------------+-------------------+
| LEFT(b.transdate, 10) | goodsname | SUM(a.quantity) | SUM(a.salesvalue) |
+-----------------------+-----------+-----------------+-------------------+
| 2020-12-01 | 书 | 2.000 | 178.00 |
| 2020-12-01 | 笔 | 5.000 | 25.00 |
| 2020-12-02 | 书 | 4.000 | 356.00 |
| 2020-12-02 | 笔 | 16.000 | 80.00 |
+-----------------------+-----------+-----------------+-------------------+
4 rows in set (0.01 sec)
```
可以看到我们引入了2个关键字LEFT 和 ORDER BY你可能对它们不熟悉我来具体解释下。
**LEFT(strn)**表示返回字符串str最左边的n个字符。我们这里的LEFTa.transdate,10表示返回交易时间字符串最左边的10个字符。在MySQL中DATETIME类型的默认格式是YYYY-MM-DD也就是说年份4个字符之后是“-”然后是月份2个字符之后又是“-”然后是日2个字符所以完整的年月日是10个字符。用户要求按照日期统计所以我们需要从日期时间数据中把年月日的部分截取出来。
**ORDER BY**:表示按照指定的字段排序。超市经营者指定按照日期和单品统计,那么,统计的结果按照交易日期和商品名称的顺序排序,会更加清晰。
知道了2个关键字之后刚刚的查询就容易理解了。接下来我们就再拆解一下看看这个查询是如何执行的。我用图表来直观地演示一下各个步骤。
第一步完成3个表的连接由于字段比较多为了你理解我省略了一些在这一步不重要的字段
<img src="https://static001.geekbang.org/resource/image/95/a7/953cd3d7199a36bf070e1a481a852da7.jpeg" alt="">
第二步对结果集按照交易时间和商品名称进行分组我们可以分成下面4组。
第一组:
<img src="https://static001.geekbang.org/resource/image/3a/96/3a9ee51c76802f3dd204c4b680548096.jpeg" alt="">
第二组
<img src="https://static001.geekbang.org/resource/image/1c/e5/1c99979d20f62a22265bd479365b91e5.jpeg" alt="">
第三组
<img src="https://static001.geekbang.org/resource/image/61/85/61c37518b1a8dac6d33e6e85bdc53385.jpeg" alt="">
第四组
<img src="https://static001.geekbang.org/resource/image/52/96/52b8bebfb9bd9ed4e866ceb2a9cef796.jpeg" alt="">
第三步,对各组的销售数量和销售金额进行统计,并且按照交易日期和商品名称排序。这样就得到了我们需要的结果,如下所示:
```
+-----------------------+-----------+-----------------+-------------------+
| LEFT(b.transdate, 10) | goodsname | SUM(a.quantity) | SUM(a.salesvalue) |
+-----------------------+-----------+-----------------+-------------------+
| 2020-12-01 | 书 | 2.000 | 178.00 |
| 2020-12-01 | 笔 | 5.000 | 25.00 |
| 2020-12-02 | 书 | 4.000 | 356.00 |
| 2020-12-02 | 笔 | 16.000 | 80.00 |
+-----------------------+-----------+-----------------+-------------------+
4 rows in set (0.01 sec)
```
如果用户需要知道全部商品销售的总计数量和总计金额我们也可以把数据集的整体看作一个分组进行计算。这样就不需要分组关键字GROUP BY以及排序关键字ORDER BY了。你甚至不需要从关联表中获取数据也就不需要连接了。就像下面这样
```
mysql&gt; SELECT
-&gt; SUM(quantity), -- 总计数量
-&gt; SUM(salesvalue)-- 总计金额
-&gt; FROM
-&gt; demo.transactiondetails;
+---------------+-----------------+
| SUM(quantity) | SUM(salesvalue) |
+---------------+-----------------+
| 27.000 | 639.00 |
+---------------+-----------------+
1 row in set (0.05 sec)
```
到这里呢求和函数SUM()的使用方法我就讲完了。需要提醒你的是,求和函数获取的是分组中的合计数据,所以你要对分组的结果有准确的把握,否则就很容易搞错。这也就是说,你要知道是按什么字段进行分组的。如果是按多个字段分组,你要知道字段之间有什么样的层次关系;如果是按照以字段作为变量的某个函数进行分组的,你要知道这个函数的返回值是什么,返回值又是如何影响分组的等。
## AVG、MAX和MIN
接下来我们来计算一下分组中数据的平均值、最大值和最小值。这个时候就要用到AVG()、MAX()和MIN()了。
1.AVG
首先我们来学习下计算平均值的函数AVG。它的作用是通过计算分组内指定字段值的和以及分组内的记录数算出分组内指定字段的平均值。
举个例子如果用户需要计算每天、每种商品平均一次卖出多少个、多少钱这个时候我们就可以用到AVG函数了如下所示
```
mysql&gt; SELECT
-&gt; LEFT(a.transdate, 10),
-&gt; c.goodsname,
-&gt; AVG(b.quantity), -- 平均数量
-&gt; AVG(b.salesvalue) -- 平均金额
-&gt; FROM
-&gt; demo.transactionhead a
-&gt; JOIN
-&gt; demo.transactiondetails b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.goodsmaster c ON (b.itemnumber = c.itemnumber)
-&gt; GROUP BY LEFT(a.transdate,10),c.goodsname
-&gt; ORDER BY LEFT(a.transdate,10),c.goodsname;
+-----------------------+-----------+-----------------+-------------------+
| LEFT(a.transdate, 10) | goodsname | AVG(b.quantity) | AVG(b.salesvalue) |
+-----------------------+-----------+-----------------+-------------------+
| 2020-12-01 | 书 | 2.0000000 | 178.000000 |
| 2020-12-01 | 笔 | 5.0000000 | 25.000000 |
| 2020-12-02 | 书 | 2.0000000 | 178.000000 |
| 2020-12-02 | 笔 | 8.0000000 | 40.000000 |
+-----------------------+-----------+-----------------+-------------------+
4 rows in set (0.00 sec)
```
2.MAX和MIN
MAX()表示获取指定字段在分组中的最大值MIN()表示获取指定字段在分组中的最小值。它们的实现原理差不多下面我就重点讲一下MAX()知道了它的用法MIN()也就很好理解了。
我们还是来看具体的例子。假如用户要求计算每天里的一次销售的最大数量和最大金额,就可以用下面的代码,得到我们需要的结果:
```
mysql&gt; SELECT
-&gt; LEFT(a.transdate, 10),
-&gt; MAX(b.quantity), -- 数量最大值
-&gt; MAX(b.salesvalue) -- 金额最大值
-&gt; FROM
-&gt; demo.transactionhead a
-&gt; JOIN
-&gt; demo.transactiondetails b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.goodsmaster c ON (b.itemnumber = c.itemnumber)
-&gt; GROUP BY LEFT(a.transdate,10)
-&gt; ORDER BY LEFT(a.transdate,10);
+-----------------------+-----------------+-------------------+
| LEFT(a.transdate, 10) | MAX(b.quantity) | MAX(b.salesvalue) |
+-----------------------+-----------------+-------------------+
| 2020-12-01 | 5.000 | 178.00 |
| 2020-12-02 | 10.000 | 267.00 |
+-----------------------+-----------------+-------------------+
2 rows in set (0.00 sec)
```
代码很简单你一看就明白了。但是这里有个问题你要注意千万不要以为MAXb.quantity和MAXb.salesvalue算出的结果一定是同一条记录的数据。实际上MySQL是分别计算的。下面我们就来分析一下刚刚的查询。
查询中用到3个相互关联的表销售流水明细表、销售流水单头表和商品信息表。这3个表连接完成之后MySQL进行了分组。我用图示的办法给你展示出来
第一组
<img src="https://static001.geekbang.org/resource/image/3b/8a/3ba226e73b81a02a294ab83c7yy0d68a.jpeg" alt="">
第二组
<img src="https://static001.geekbang.org/resource/image/6e/2d/6e50732ff2c59199abbea6381d083a2d.jpeg" alt="">
在第一组中最大数量出现在第2条记录是5最大金额出现在第1条记录是178。同样道理在第二组中最大数量出现在第4条记录是10最大金额则出现在第1条记录是267。
所以MAX字段这个函数返回分组集中最大的那个值。如果你要查询 MAX字段1和MAX字段2而它们是相互独立、分别计算的你千万不要想当然地认为结果在同一条记录上。那样的话你就掉坑里了。
## COUNT
通过**COUNT**,我们可以了解数据集的大小,这对系统优化十分重要。
举个小例子,在项目实施的过程中,我们遇到了这么一个问题:由于用户的销售数据很多,而且每天都在增长,因此,在做销售查询的时候,经常会遇到卡顿的问题。这是因为,查询的数据量太大了,导致系统不得不花很多时间来处理数据,并给数据集分配资源,比如内存什么的。
怎么解决卡顿的问题呢?我们想到了一个分页的策略。
所谓的分页策略,其实就是,不把查询的结果一次性全部返回给客户端,而是根据用户电脑屏幕的大小,计算一屏可以显示的记录数,每次只返回用户电脑屏幕可以显示的数据集。接着,再通过翻页、跳转等功能按钮,实现查询目标的精准锁定。这样一来,每次查询的数据量较少,也就大大提高了系统响应速度。
这个策略能够实现的一个关键,就是要**计算出符合条件的记录一共有多少条**,之后才能计算出一共有几页、能不能翻页或跳转。
要计算记录数就要用到COUNT()函数了。这个函数有两种情况。
- COUNT*):统计一共有多少条记录;
- COUNT字段统计有多少个不为空的字段值。
1.COUNT(*)
如果COUNT*与GROUP BY一起使用就表示统计分组内有多少条数据。它也可以单独使用这就相当于数据集全体是一个分组统计全部数据集的记录数。
我举个小例子,假设我有个销售流水明细表如下:
```
mysql&gt; SELECT *
-&gt; FROM demo.transactiondetails;
+---------------+------------+----------+-------+------------+
| transactionid | itemnumber | quantity | price | salesvalue |
+---------------+------------+----------+-------+------------+
| 1 | 1 | 2.000 | 89.00 | 178.00 |
| 1 | 2 | 5.000 | 5.00 | 25.00 |
| 2 | 1 | 3.000 | 89.00 | 267.00 |
| 2 | 2 | 6.000 | 5.00 | 30.00 |
| 3 | 1 | 1.000 | 89.00 | 89.00 |
| 3 | 2 | 10.000 | 5.00 | 50.00 |
+---------------+------------+----------+-------+------------+
6 rows in set (0.00 sec)
```
如果我们一屏可以显示30行需要多少页才能显示完这个表的全部数据呢
```
mysql&gt; SELECT COUNT(*)
-&gt; FROM demo.transactiondetails;
+----------+
| COUNT(*) |
+----------+
| 6 |
+----------+
1 row in set (0.03 sec)
```
我们这里只有6条数据一屏就可以显示了所以一共1页。
那么,如果超市经营者想知道,每天、每种商品都有几次销售,我们就需要按天、按商品名称,进行分组查询:
```
mysql&gt; SELECT
-&gt; LEFT(a.transdate, 10), c.goodsname, COUNT(*) -- 统计销售次数
-&gt; FROM
-&gt; demo.transactionhead a
-&gt; JOIN
-&gt; demo.transactiondetails b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.goodsmaster c ON (b.itemnumber = c.itemnumber)
-&gt; GROUP BY LEFT(a.transdate, 10) , c.goodsname
-&gt; ORDER BY LEFT(a.transdate, 10) , c.goodsname;
+-----------------------+-----------+----------+
| LEFT(a.transdate, 10) | goodsname | COUNT(*) |
+-----------------------+-----------+----------+
| 2020-12-01 | 书 | 1 |
| 2020-12-01 | 笔 | 1 |
| 2020-12-02 | 书 | 2 |
| 2020-12-02 | 笔 | 2 |
+-----------------------+-----------+----------+
4 rows in set (0.00 sec)
```
运行这段代码,我们就得到了每天、每种商品有几次销售的全部结果。
2.COUNT字段
COUNT字段用来统计分组内这个字段的值出现了多少次。如果字段值是空就不统计。
为了说明它们的区别,我举个小例子。假设我们有这样的一个商品信息表,里面包括了商品编号、条码、名称、规格、单位和售价的信息。
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice |
+------------+---------+-----------+---------------+------+------------+
| 1 | 0001 | 书 | 16开 | 本 | 89.00 |
| 2 | 0002 | 笔 | NULL | 支 | 5.00 |
| 3 | 0002 | 笔 | NULL | 支 | 10.00 |
+------------+---------+-----------+---------------+------+------------+
3 rows in set (0.01 sec)
```
如果我们要统计字段“goodsname”出现了多少次就要用到函数COUNTgoodsname结果是3次
```
mysql&gt; SELECT COUNT(goodsname) -- 统计商品名称字段
-&gt; FROM demo.goodsmaster;
+------------------+
| COUNT(goodsname) |
+------------------+
| 3 |
+------------------+
1 row in set (0.00 sec)
```
如果我们统计字段“specification”用COUNT(specification)结果是1次
```
mysql&gt; SELECT COUNT(specification) -- 统计规格字段
-&gt; FROM demo.goodsmaster;
+----------------------+
| COUNT(specification) |
+----------------------+
| 1 |
+----------------------+
1 row in set (0.00 sec)
```
你可能会问为啥计数字段“goodsname”的结果是3计数字段“specification”却只有1呢其实这里的原因就是3条记录里面的字段“goodsname”没有空值因此被统计了3次而字段“specification”有2个空值因此只统计了1次。
理解了这一点,你就可以利用计数函数对某个字段计数时,不统计空值的特点,对表中字段的非空值进行计数了。
## 总结
今天我们学习了聚合函数SUM、AVG、MAX、MIN和COUNT。我们在对分组数据进行统计的时候可以用这些函数来对分组数据求和、求平均值、最大值、最小值以及统计分组内的记录数或者分组内字段的值不为空的次数。
这些函数,为我们对数据库中的数据进行统计和计算提供了方便。因为计算直接在数据库中执行,比在应用层面完成相同的工作,效率高很多。
最后我还想多说一句不知道你注意到没有这节课我还提到了LEFT和ORDER BY。其实聚合函数可以和其他关键字、函数一起使用这样会拓展它的使用场景让原本复杂的计算变简单。所以我建议你不仅要认真学习这节课的聚合函数还要掌握MySQL的各种关键字的功能和用法并且根据实际工作的需要尝试把它们组合在一起使用这样就能利用好数据库的强大功能更好地满足用户的需求。
## 思考题
如果用户想要查询一下,在商品信息表中,到底是哪种商品的商品名称有重复,分别重复了几次,该如何查询呢?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,343 @@
<audio id="audio" title="09 | 时间函数时间类数据MySQL是怎么处理的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c3/98/c38dd356368fdd7ecec6b5579f447298.mp3"></audio>
你好我是朱晓峰。今天咱们来聊一聊MySQL的时间函数。
顾名思义,时间函数就是用来处理时间的函数。时间,几乎可以说是各类项目中都会存在的数据,项目需求不同,我们需要的时间函数也不一样,比如:
- 如果我们要统计一天之中不同时间段的销售情况就要获取时间值中的小时值这就会用到函数HOUR()
- 要计算与去年同期相比的增长率这就要计算去年同期的日期时间会用到函数DATE_ADD()
- 要计算今天是周几、有没有优惠活动这就要用到函数DAYOFWEEK()了;
- ……
这么多不同类型的时间函数,该怎么选择呢?这节课,我就结合不同的项目需求,来讲一讲不同的时间函数的使用方法,帮助你轻松地处理各类时间数据。
## 获取日期时间数据中部分信息的函数
我先举个小例子。超市的经营者提出,他们希望通过实际的销售数据,了解到一天当中什么时间段卖得好,什么时间段卖得不好,这样他们就可以根据不同时间的销售情况,合理安排商品陈列和人员促销,以实现收益最大化。
要达到这个目标,我们就需要统计一天中每小时的销售数量和销售金额。
这里涉及3组数据分别是销售单头表demo.transactionhead)、销售单明细表 (demo.transactiondetails)和商品信息表demo.goodsmaster为了便于你理解表的结构和表里的记录都是经过简化的
销售单头表包含了销售单的整体信息,包括流水单号、交易时间、收款机编号、会员编号和收银员编号等。
<img src="https://static001.geekbang.org/resource/image/92/ca/925490737131d75b38fe6af861c645ca.jpeg" alt="">
销售单明细表中保存的是交易明细数据,包括商品编号、销售数量、价格、销售金额等。
<img src="https://static001.geekbang.org/resource/image/fd/0b/fdfbf05544e36251b2784259fb3ca00b.jpeg" alt="">
商品信息表主要包括商品编号、条码、商品名称、规格、单位和售价。
<img src="https://static001.geekbang.org/resource/image/44/01/44f29d06fc689edd79e3fe81a39e2d01.jpeg" alt="">
需要注意的是,销售单明细表通过流水编号与销售单头表关联,其中流水编号是外键。通过流水编号,销售单明细表引用销售单头表里的交易时间、会员编号等信息,同时,通过商品编号与商品信息表关联,引用商品信息表里的商品名称等信息。
首先,我们来分析一下“统计一天中每小时的销售数量和销售金额”的这个需求。
要统计一天中每小时的销售情况实际上就是要把销售数据按照小时进行分组统计。那么解决问题的关键就是把交易时间的小时部分提取出来。这就要用到MySQL的日期时间处理函数EXTRACT和HOUR了。
为了获取小时的值我们要用到EXTRACT()函数。**EXTRACTtype FROM date表示从日期时间数据“date”中抽取“type”指定的部分**。
有了这个函数,我们就可以获取到交易时间的小时部分,从而完成一天中每小时的销售数量和销售金额的查询:
```
mysql&gt; SELECT
-&gt; EXTRACT(HOUR FROM b.transdate) AS 时段,
-&gt; SUM(a.quantity) AS 数量,
-&gt; SUM(a.salesvalue) AS 金额
-&gt; FROM
-&gt; demo.transactiondetails a
-&gt; JOIN
-&gt; demo.transactionhead b ON (a.transactionid = b.transactionid)
-&gt; GROUP BY EXTRACT(HOUR FROM b.transdate)
-&gt; ORDER BY EXTRACT(HOUR FROM b.transdate);
+------+--------+--------+
| 时段 | 数量 | 金额 |
+------+--------+--------+
| 9 | 16.000 | 500.00 |
| 10 | 11.000 | 139.00 |
| 11 | 10.000 | 30.00 |
| 12 | 40.000 | 200.00 |
| 13 | 5.000 | 445.00 |
| 15 | 6.000 | 30.00 |
| 17 | 1.000 | 3.00 |
| 18 | 2.000 | 178.00 |
| 19 | 2.000 | 6.00 |
+------+--------+--------+
9 rows in set (0.00 sec)
```
查询的过程是这样的:
1. 从交易时间中抽取小时信息EXTRACT(HOUR FROM b.transdate)
1. 按交易的小时信息分组;
1. 按分组统计销售数量和销售金额的和;
1. 按交易的小时信息排序。
这里我是用“HOUR”提取时间类型DATETIME中的小时信息同样道理你可以用“YEAR”获取年度信息用“MONTH”获取月份信息用“DAY”获取日的信息。如果你需要获取其他时间部分的信息可以参考下[时间单位](https://dev.mysql.com/doc/refman/8.0/en/expressions.html#temporal-intervals)。
这个查询我们也可以通过使用日期时间函数HOUR()来达到同样的效果。**HOURtime表示从日期时间“time”中获取小时部分信息**。
需要注意的是EXTRACT()函数中的“HOUR”表示要获取时间的类型而HOUR()是一个函数HOUR(time)可以单独使用表示返回time的小时部分信息。
我们可以通过在代码中把EXTRACT函数改成HOUR函数来实现相同的功能如下所示
```
mysql&gt; SELECT
-&gt; HOUR(b.transdate) AS 时段, -- 改为使用HOUR函数
-&gt; SUM(a.quantity) AS 数量,
-&gt; SUM(a.salesvalue) AS 金额
-&gt; FROM
-&gt; demo.transactiondetails a
-&gt; JOIN
-&gt; demo.transactionhead b ON (a.transactionid = b.transactionid)
-&gt; GROUP BY HOUR(b.transdate) -- 改写为HOUR函数
-&gt; ORDER BY HOUR(b.transdate);-- 改写为HOUR函数
+------+--------+--------+
| 时段 | 数量 | 金额 |
+------+--------+--------+
| 9 | 16.000 | 500.00 |
| 10 | 11.000 | 139.00 |
| 11 | 10.000 | 30.00 |
| 12 | 40.000 | 200.00 |
| 13 | 5.000 | 445.00 |
| 15 | 6.000 | 30.00 |
| 17 | 1.000 | 3.00 |
| 18 | 2.000 | 178.00 |
| 19 | 2.000 | 6.00 |
+------+--------+--------+
9 rows in set (0.00 sec)
```
除了获取小时信息我们往往还会遇到要统计年度信息、月度信息等情况MySQL也提供了支持的函数。
- YEARdate获取date中的年。
- MONTHdate获取date中的月。
- DAYdate获取date中的日。
- HOURdate获取date中的小时。
- MINUTEdate获取date中的分。
- SECONDdate获取date中的秒。
这些函数的使用方法和提取小时信息的方法一样,我就不多说了,你只要知道这些函数的含义就可以了,下面我再讲一讲计算日期时间的函数。
## 计算日期时间的函数
我先来介绍2个常用的MySQL的日期时间计算函数。
- DATE_ADDdate, INTERVAL 表达式 type表示计算从时间点“date”开始向前或者向后一段时间间隔的时间。“表达式”的值为时间间隔数正数表示向后负数表示向前“type”表示时间间隔的单位比如年、月、日等
- LAST_DAYdate表示获取日期时间“date”所在月份的最后一天的日期。
这两个函数怎么用呢接下来我还是借助咱们项目的实际需求来给你讲解下。假设今天是2020年12月10日超市经营者提出他们需要计算这个月单品销售金额的统计以及与去年同期相比的增长率。
这里的关键点是需要获取2019年12月的销售数据。因此计算2019年12月的起始和截止时间点就是查询的关键。这个时候就要用到计算日期时间函数了。
下面我重点讲解一下如何通过2个计算日期时间函数来计算2019年12月的起始时间和截止时间。
我们先来尝试获取2019年12月份的起始时间。
第一步用DATE_ADD函数获取到2020年12月10日上一年的日期2019年12月10日。
```
mysql&gt; SELECT DATE_ADD('2020-12-10', INTERVAL - 1 YEAR);
+-------------------------------------------+
| DATE_ADD('2020-12-10', INTERVAL - 1 YEAR) |
+-------------------------------------------+
| 2019-12-10 |
+-------------------------------------------+
1 row in set (0.00 sec)
```
第二步获取2019年12月10日这个时间节点开始上个月的日期这样做的目的是方便获取月份的起始时间
```
mysql&gt; SELECT DATE_ADD(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR),INTERVAL - 1 MONTH);
+------------------------------------------------------------------------+
| DATE_ADD(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR),INTERVAL - 1 MONTH) |
+------------------------------------------------------------------------+
| 2019-11-10 |
+------------------------------------------------------------------------+
1 row in set (0.00 sec)
```
第三步获取2019年11月10日这个时间点月份的最后一天继续接近我们的目标2019年12月01日。
```
mysql&gt; SELECT LAST_DAY(DATE_ADD(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR),INTERVAL - 1 MONTH));
+----------------------------------------------------------------------------------+
| LAST_DAY(DATE_ADD(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR),INTERVAL - 1 MONTH)) |
+----------------------------------------------------------------------------------+
| 2019-11-30 |
+----------------------------------------------------------------------------------+
1 row in set (0.00 sec)
```
到这里我们获得了2019年11月30日这个日期。你是不是觉得我们已经达到目的了呢要是这样的话你就错了。因为2019年11月30日可能会有销售的。如果用这个日期作为统计销售额的起始日期你就多算了这一天的销售。怎么办呢我们还要进行下一步。
第四步计算2019年11月30日后一天的日期
```
mysql&gt; SELECT DATE_ADD(LAST_DAY(DATE_ADD(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR),INTERVAL - 1 MONTH)),INTERVAL 1 DAY);
+-----------------------------------------------------------------------------------------------------------+
| DATE_ADD(LAST_DAY(DATE_ADD(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR),INTERVAL - 1 MONTH)),INTERVAL 1 DAY) |
+-----------------------------------------------------------------------------------------------------------+
| 2019-12-01 |
+-----------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
```
你看我们终于获得了正确的起始日期2019年12月01日。
同样,我们可以用下面的方法,获得截止日期:
```
mysql&gt; SELECT DATE_ADD(LAST_DAY(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR)),INTERVAL 1 DAY);
+------------------------------------------------------------------------------+
| DATE_ADD(LAST_DAY(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR)),INTERVAL 1 DAY) |
+------------------------------------------------------------------------------+
| 2020-01-01 |
+------------------------------------------------------------------------------+
1 row in set (0.00 sec)
```
简单小结下我们可以用DATE_ADD()来计算从某个时间点开始过去或者未来一个时间间隔的时间通过LAST_DAY()函数,获得某个时间节点当月的最后一天的日期。借助它们,我们就可以获取从某个时间节点出发的指定月份的起始日期和截止日期。
除了DATE_ADD()ADDDATE()、DATE_SUB()和SUBDATE()也能达到同样的效果。
- ADDDATE()跟DATE_ADD()用法一致;
- DATE_SUB()SUBDATE()与DATE_ADD()用法类似,方向相反,执行日期的减操作。
## 其他日期时间函数
学习了刚刚的时间函数,我们已经可以应对大部分有关时间的场景了。但是这还不够,有的时候,我们还需要其他的日期时间信息,比如:
- 今天是几月几号,星期几;
- 两个时间点之间隔了几天;
- 把时间按照一定的格式显示出来;
- ……
这时就要用到其他日期时间函数了主要包括CURDATE()、DAYOFWEEK()、DATE_FORMAT和DATEDIFF()。
我来借助一个例子,具体解释下这些函数怎么用。
超市经营者为了吸引顾客经常要进行一些促销活动。具体来讲就是以周为单位按照周中不同的日期进行促销比如周一如何打折、周二如何打折、周末如何打折等。那么如何计算当天的价格呢我们来看下单品促销信息demo.discountrule
<img src="https://static001.geekbang.org/resource/image/cd/cf/cdc62927a60978e2128616fab3697fcf.jpeg" alt="">
这个表中的信息表示单品打折的时间和折扣率:
- 编号是1的商品周一、周三和周五打折折扣率分别是9折、75折和88折
- 编号是2的商品周二、周四和周六打折折扣率分别是5折、65折和8折。
- 周日所有商品打5折。
如果我们想要查到具体的价格我们首先要知道当前的日期以及今天是星期几。这就要用到2个MySQL的时间函数CURDATE和DAYOFWEEK
- CURDATE获取当前的日期。日期格式为“YYYY-MM-DD”也就是年月日的格式。
- DAYOFWEEKdate获取日期“date”是周几。1表示周日2表示周一以此类推直到7表示周六。
假设今天是2021年02月06日通过下面的代码我们就可以查到今天商品的全部折后价格了
```
mysql&gt; SELECT
-&gt; CURDATE() AS 日期,
-&gt; CASE DAYOFWEEK(CURDATE()) - 1 WHEN 0 THEN 7 ELSE DAYOFWEEK(CURDATE()) - 1 END AS 周几,
-&gt; a.goodsname AS 商品名称,
-&gt; a.salesprice AS 价格,
-&gt; IFNULL(b.discountrate,1) AS 折扣率,
-&gt; a.salesprice * IFNULL(b.discountrate, 1) AS 折后价格
-&gt; FROM
-&gt; demo.goodsmaster a
-&gt; LEFT JOIN
-&gt; demo.discountrule b ON (a.itemnumber = b.itemnumber
-&gt; AND CASE DAYOFWEEK(CURDATE()) - 1 WHEN 0 THEN 7 ELSE DAYOFWEEK(CURDATE()) - 1 END = b.weekday);
+------------+------+----------+-------+--------+----------+
| 日期 | 周几 | 商品名称 | 价格 | 折扣率 | 折后价格 |
+------------+------+----------+-------+--------+----------+
| 2021-02-06 | 6 | 书 | 89.00 | 1.00 | 89.0000 |
| 2021-02-06 | 6 | 笔 | 5.00 | 0.80 | 4.0000 |
| 2021-02-06 | 6 | 橡皮 | 3.00 | 1.00 | 3.0000 |
+------------+------+----------+-------+--------+----------+
3 rows in set (0.00 sec)
```
这个查询我们用到了CURDATE函数来获取当前日期也用到了DAYOFWEEK函数来获取当前是周几的信息。由于DAYOFWEEK()函数以周日为1开始计周一是2……周六是7而数据表中是从周一为1开始计算为了对齐我用到了条件判断函数CASE我来解释下这个函数。
MySQL中CASE函数的语法如下
```
CASE 表达式 WHEN 值1 THEN 表达式1 [ WHEN 值2 THEN 表达式2] ELSE 表达式m END
```
在我们这个查询中“表达式”有7种可能的值。通过CASE函数我们可以根据DAYOFWEEK()函数返回的值对每个返回值进行处理从而跟促销信息表中的字段weekday对应。
除了获取特定的日期,咱们还经常需要把日期按照一定的格式显示出来,这就要用到日期时间格式化的函数**DATE_FORMAT()它表示将日期时间“date”按照指定格式显示**。
举个小例子张三希望用24小时制来查看时间那么他就可以通过使用DATE_FORMAT()函数,指定格式“%T”来实现
```
mysql&gt; SELECT DATE_FORMAT(&quot;2020-12-01 13:25:50&quot;,&quot;%T&quot;);
+-----------------------------------------+
| DATE_FORMAT(&quot;2020-12-01 13:25:50&quot;,&quot;%T&quot;) |
+-----------------------------------------+
| 13:25:50 |
+-----------------------------------------+
1 row in set (0.00 sec)
```
李四习惯按照上下午的方式来查看时间同样他可以使用DATE_FORMAT()函数,通过指定格式“%r”来实现
```
mysql&gt; SELECT DATE_FORMAT(&quot;2020-12-01 13:25:50&quot;,&quot;%r&quot;);
+-----------------------------------------+
| DATE_FORMAT(&quot;2020-12-01 13:25:50&quot;,&quot;%r&quot;) |
+-----------------------------------------+
| 01:25:50 PM |
+-----------------------------------------+
1 row in set (0.00 sec
```
格式的详细内容非常丰富,我就不一一介绍了,我给你分享一个[链接](https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_date-format),你可以随时查看一下。
另外一个重要的时间函数是DATEDIFFdate1,date2表示日期“date1”与日期“date2”之间差几天。假如你要计算某段时间的每天交易金额的平均值只需要把起始日期和截止日期传给这个函数就可以得到中间隔了几天。再用总计金额除以这个天数就可以算出来了
```
mysql&gt; SELECT DATEDIFF(&quot;2021-02-01&quot;,&quot;2020-12-01&quot;);
+-------------------------------------+
| DATEDIFF(&quot;2021-02-01&quot;,&quot;2020-12-01&quot;) |
+-------------------------------------+
| 62 |
+-------------------------------------+
1 row in set (0.00 sec)
```
## 总结
今天我们学习了MySQL的时间处理函数包括获取日期时间类型数据中部分信息的函数、计算日期时间的函数和获取特定日期的函数我用图片来帮你汇总了下。
<img src="https://static001.geekbang.org/resource/image/6f/49/6f03f8e150e324101b20acf0c61be649.jpg" alt="">
最后我还想多说一句MySQL中获取的时间其实就是MySQL服务器计算机的系统时间。如果你的系统有一定规模需要在多台计算机上运行就要注意时间校准的问题。比如我们的信息系统受门店经营环境和操作人员的素质所限有时会遇到误操作、停电等故障而导致的计算机系统时间失准问题。这对整个信息系统的可靠性影响非常大。
针对这个问题有2种解决办法。
第一种方法是可以利用Windows系统自带的网络同步的方式来校准系统时间。
另一种办法就是门店统一从总部MySQL服务器获取时间。由于总部的服务器的配置和运维状况一般要好于门店所以系统时间出现误差的可能性也较小。如果采用云服务器系统时间的可靠性会更高。
## 思考题
假如用户想查一下今天是星期几(不能用数值,要用英文显示),你可以写一个简单的查询语句吗?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,365 @@
<audio id="audio" title="10 | 如何进行数学计算、字符串处理和条件判断?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a3/af/a3b33304a92fc8e444335a190fdc7aaf.mp3"></audio>
你好,我是朱晓峰。
MySQL提供了很多功能强大而且使用起来非常方便的函数包括数学函数、字符串处理函数和条件判断函数等。
在很多场景中 ,我们都会用到这些函数,比如说,在超市项目的实际开发过程中,会有这样的需求:
- 会员积分的规则是一元积一分不满一元不积分这就要用到向下取整的数学函数FLOOR()
- 在打印小票的时候收银纸的宽度是固定的怎么才能让打印的结果清晰而整齐呢这个时候就要用到CONCAT()等字符串处理函数;
- 不同数据的处理方式不同怎么选择正确的处理方式呢这就会用到IF(表达式V1V2)这样的条件判断函数;
- ……
这些函数对我们管理数据库、提高数据处理的效率有很大的帮助。接下来,我就带你在解决实际问题的过程中,帮你掌握使用这些函数的方法。
## 数学函数
我们先来学习下数学函数它主要用来处理数值数据常用的主要有3类分别是取整函数ROUND()、CEIL()、FLOOR()绝对值函数ABS()和求余函数MOD()。
知道了这些函数,我们来看看超市经营者的具体需求。他们提出,为了提升销量,要进行会员营销,主要是给会员积分,并以积分数量为基础,给会员一定的优惠。
积分的规则也很简单,就是消费一元积一分,不满一元不积分,那我们就需要对销售金额的数值进行取整。
这里主要用到四个表,分别是销售单明细表、销售单头表、商品信息表和会员信息表。为了方便你理解,我对表结构和数据进行了简化。
销售单明细表:
<img src="https://static001.geekbang.org/resource/image/54/93/543b4ce8c0c8b1f3bb7028c911213f93.jpeg" alt="">
销售单头表:
<img src="https://static001.geekbang.org/resource/image/a4/33/a4df12d3469aaf2f770fbfa8fb842c33.jpeg" alt="">
商品信息表:
<img src="https://static001.geekbang.org/resource/image/26/03/262121b96d4ce48e310cdff37d536203.jpeg" alt="">
会员信息表:
<img src="https://static001.geekbang.org/resource/image/10/8e/105d12853e4b09aefd96f2423613648e.jpeg" alt="">
这个场景下可以用到MySQL数学函数中的取整函数主要有3种。
- 向上取整CEIL(X)和CEILING(X)返回大于等于X的最小INT型整数。
- 向下取整FLOOR(X)返回小于等于X的最大INT型整数。
- 舍入函数ROUND(X,D)X表示要处理的数D表示保留的小数位数处理的方式是四舍五入。ROUND(X)表示保留0位小数。
现在积分的规则是一元积一分不满一元不积分显然是向下取整那就可以用FLOOR函数。
首先,我们要通过关联查询,获得会员消费的相关信息:
```
mysql&gt; SELECT
-&gt; c.membername AS '会员', -- 从会员表获取会员名称
-&gt; b.transactionno AS '单号',-- 从销售单头表获取单号
-&gt; b.transdate AS '交易时间', -- 从销售单头表获取交易时间
-&gt; d.goodsname AS '商品名称', -- 从商品信息表获取商品名称
-&gt; a.salesvalue AS '交易金额'
-&gt; FROM
-&gt; demo.transactiondetails a
-&gt; JOIN
-&gt; demo.transactionhead b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.membermaster c ON (b.memberid = c.memberid)
-&gt; JOIN
-&gt; demo.goodsmaster d ON (a.itemnumber = d.itemnumber);
+------+------------------+---------------------+----------+----------+
| 会员 | 单号 | 交易时间 | 商品名称 | 交易金额 |
+------+------------------+---------------------+----------+----------+
| 张三 | 0120201201000001 | 2020-12-01 14:25:56 | 书 | 176.22 |
| 张三 | 0120201201000001 | 2020-12-01 14:25:56 | 笔 | 24.75 |
| 李四 | 0120201202000001 | 2020-12-02 10:50:50 | 书 | 234.96 |
| 李四 | 0120201202000001 | 2020-12-02 10:50:50 | 笔 | 26.40 |
+------+------------------+---------------------+----------+----------+
4 rows in set (0.01 sec)
```
接着我们用FLOORa.salesvalue对销售金额向下取整获取会员积分值代码如下
```
mysql&gt; SELECT
-&gt; c.membername AS '会员',
-&gt; b.transactionno AS '单号',
-&gt; b.transdate AS '交易时间',
-&gt; d.goodsname AS '商品名称',
-&gt; a.salesvalue AS '交易金额',
-&gt; FLOOR(a.salesvalue) AS '积分' -- 使用FLOOR函数向下取整
-&gt; FROM
-&gt; demo.transactiondetails a
-&gt; JOIN
-&gt; demo.transactionhead b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.membermaster c ON (b.memberid = c.memberid)
-&gt; JOIN
-&gt; demo.goodsmaster d ON (a.itemnumber = d.itemnumber);
+------+------------------+---------------------+----------+----------+------+
| 会员 | 单号 | 交易时间 | 商品名称 | 交易金额 | 积分 |
+------+------------------+---------------------+----------+----------+------+
| 张三 | 0120201201000001 | 2020-12-01 14:25:56 | 书 | 176.22 | 176 |
| 张三 | 0120201201000001 | 2020-12-01 14:25:56 | 笔 | 24.75 | 24 |
| 李四 | 0120201202000001 | 2020-12-02 10:50:50 | 书 | 234.96 | 234 |
| 李四 | 0120201202000001 | 2020-12-02 10:50:50 | 笔 | 26.40 | 26 |
+------+------------------+---------------------+----------+----------+------+
4 rows in set (0.01 sec)
```
你看通过FLOOR(),我们轻松地获得了超市经营者需要的积分数据。
类似的如果用户的积分规则改为“不满一元积一分”其实就是对金额数值向上取整这个时候我们就可以用CEIL()函数。操作方法和前面是一样的,我就不具体解释了。
最后我再来讲一讲舍入函数ROUND的使用方法。
超市经营者提出,收银的时候,应收金额可以被设定四舍五入到哪一位。比如,可以设定四舍五入到元、到角,或者到分。
按照指定的位数对小数进行四舍五入计算这样的场景就要用到ROUNDX,D了。它的作用是通过四舍五入对数值X保留D位小数。
根据超市经营者的要求我们把函数ROUND(X,D)中的保留小数的位数D设置成0、1和2。
如果要精确到分我们可以设置保留2位小数
```
mysql&gt; SELECT ROUND(salesvalue,2) -- D设置成2表示保留2位小数也就是精确到分
-&gt; FROM demo.transactiondetails
-&gt; WHERE transactionid=1 AND itemnumber=1;
+---------------------+
| ROUND(salesvalue,2) |
+---------------------+
| 176.22 |
+---------------------+
1rows in set (0.00 sec)
```
如果要精确到角可以设置保留1位小数
```
mysql&gt; SELECT ROUND(salesvalue,1) -- D设置成1表示保留1位小数也就是精确到角
-&gt; FROM demo.transactiondetails
-&gt; WHERE transactionid=1 AND itemnumber=1;
+---------------------+
| ROUND(salesvalue,1) |
+---------------------+
| 176.2 |
+---------------------+
1 rows in set (0.00 sec
```
如果要精确到元可以设置保留0位小数
```
mysql&gt; SELECT ROUND(salesvalue,0)-- D设置成0表示保留0位小数也就是精确到元
-&gt; FROM demo.transactiondetails
-&gt; WHERE transactionid=1 AND itemnumber=1;
+---------------------+
| ROUND(salesvalue,0) |
+---------------------+
| 176 |
+---------------------+
1 rows in set (0.00 se
```
除了刚刚我们所学习的函数MySQL还支持绝对值函数ABS和求余函数MODABSX表示获取X的绝对值MODXY表示获取X被Y除后的余数。
这些函数使用起来都比较简单,你重点掌握它们的含义就可以了,下面我再带你学习下字符串函数。
## 字符串函数
除了数学计算,我们还经常会遇到需要对字符串进行处理的场景,比如我们想要在金额前面加一个“¥”的符号,就会用到字符串拼接函数;再比如,我们需要把一组数字以字符串的形式在网上传输,就要用到类型转换函数。
常用的字符串函数有4个。
- CONCATs1,s2,...表示把字符串s1、s2……拼接起来组成一个字符串。
- CAST表达式 AS CHAR表示将表达式的值转换成字符串。
- CHAR_LENGTH字符串表示获取字符串的长度。
- SPACEn表示获取一个由n个空格组成的字符串。
接下来我还是借助超市项目中的实际应用场景,来说明一下怎么使用这些字符串函数。
顾客交了钱完成交易之后系统必须要打出一张小票。打印小票时对格式有很多要求。比如说一张小票纸57毫米宽大概可以打32个字符也就是16个汉字。用户要求一条流水打2行第一行是商品信息第二行要包括数量、价格、折扣和金额4种信息。那么怎么才能清晰地在小票上打印出这些信息并且打印得整齐漂亮呢这就涉及对字符串的处理了。
首先,我们来看一下如何打印第一行的商品信息。商品信息包括:商品名称和商品规格,而且商品规格要包含在括号里面。这样就必须把商品名称和商品规格拼接起来,变成一个字符串。
这时我们就可以用合并字符串函数CONCAT如下所示
```
mysql&gt; SELECT
-&gt; CONCAT(goodsname, '(', specification, ')') AS 商品信息 -- 这里把商品名称、括号和规格拼接起来
-&gt; FROM
-&gt; demo.goodsmaster
-&gt; WHERE itemnumber = 1;
+----------+
| 商品信息 |
+----------+
| 书(16开) |
+----------+
1 row in set (0.00 sec)
```
这样我们就得到了商品编号是1的商品它的商品信息是“书16开”。
第二步我们来看一下如何打印第二行。第二行包括数量、价格、折扣和金额一共是4种信息。
因为一行最多是32个字符我们给数量分配7个字符价格分配7个字符折扣分配6个字符金额分配9个字符加上中间3个空格正好是32个字符。
为啥这么分配呢?我简单解释下。
- 数量7个字符就是小数点前面给3位小数点后面给3位外加小数点1位最大999.999,基本满足零售的需求了。
- 同样道理价格给7位意思是小数点前面4位小数点后面2位外加小数点这样最大可以表示9999.99。
- 折扣6位小数点后面2位小数点前面2位加上小数点和“%”,这样是够用的。
- 金额9位最大可以显示到999999.99,也够用了。
分配好了各部分信息的字符串大小,我再讲一下格式处理,因为数据的取值每次都会不同,如果直接打印,会参差不齐。这里我以数量为例,来具体说明一下。因为数量比较有代表性,而且比较简单,不像金额或者折扣率那样,有时还要根据用户的需求,加上“¥”或者“%”。
第一步把数量转换成字符串。这里我们需要用到把数值转换成字符串的CAST函数如下所示
```
mysql&gt; SELECT
-&gt; CAST(quantity AS CHAR) -- 把decimal类型转换成字符串
-&gt; FROM
-&gt; demo.transactiondetails
-&gt; WHERE
-&gt; transactionid = 1 AND itemnumber =1;
+---------------------+
| CAST(price AS CHAR) |
+---------------------+
| 2.000 |
+---------------------+
1 rows in set (0.00 sec)
```
第二步计算字符串的长度这里我们要用到CHAR_LENGTH函数。
需要注意的是虽然每个汉字打印的时候占2个字符长度但是这个函数获取的是汉字的个数。因此如果字符串中有汉字函数获取的字符串长度跟实际打印的长度是不一样的需要用空格来补齐。
我们可以通过下面的查询,获取数量字段转换成字符串后的字符串长度:
```
mysql&gt; SELECT
-&gt; CHAR_LENGTH(CAST(quantity AS CHAR)) AS 长度
-&gt; FROM
-&gt; demo.transactiondetails
-&gt; WHERE
-&gt; transactionid = 1 AND itemnumber =1;
+---------------------+
| 长度 |
+---------------------+
| 5 |
+---------------------+
1 rows in set (0.00 sec)
```
第三步用空格补齐7位长度。这时我们要用到SPACE函数。
因为我们采用左对齐的方式打印(左对齐表示字符串从左边开始,右边空余的位置用空格补齐),所以就需要先拼接字符串,再在字符串的后面补齐空格:
```
mysql&gt; SELECT
-&gt; CONCAT(CAST(quantity AS CHAR),
-&gt; SPACE(7 - CHAR_LENGTH(CAST(quantity AS CHAR)))) AS 数量
-&gt; FROM
-&gt; demo.transactiondetails
-&gt; WHERE
-&gt; transactionid = 1 AND itemnumber = 1;
+----------+
| 数量 |
+----------+
| 2.000 |
+----------+
1 row in set (0.00 sec)
```
除此以外MySQL还支持SUBSTR、MID、TRIM、LTRIM、RTRIM。我画了一张图来展示它们的含义你可以了解一下。
<img src="https://static001.geekbang.org/resource/image/86/d9/86f0f4ebe3055db5c112784d86aa07d9.jpg" alt="">
一般来说关于字符串函数你掌握这些就足够了。不过MySQL支持的字符串函数还有很多如果你在实际工作中遇到了更复杂的情况可以参考MySQL官方的[文档](https://dev.mysql.com/doc/refman/8.0/en/string-functions.html)。
## 条件判断函数
我们刚才在对商品信息字符串进行拼接的时候会有一种例外的情况那就是当规格为空的时候商品信息会变成“NULL”。这个结果显然不是我们想要的因为名称变成NULL顾客会觉得奇怪也不知道买了什么商品。我们希望如果规格是空值就不用加规格了。怎么实现呢这就要用到条件判断函数了。
条件判断函数的主要作用,就是根据特定的条件返回不同的值,常用的有两种。
- IFNULLV1V2表示如果V1的值不为空值则返回V1否则返回V2。
- IF表达式V1V2如果表达式为真TRUE则返回V1否则返回V2。
我们希望规格是空的商品,拼接商品信息字符串的时候,规格不要是空。这个问题,可以通过 IFNULL(specification, '')函数来解决。具体点说就是对字段“specification”是否为空进行判断如果为空就返回空字符串否则就返回商品规格specification的值。代码如下所示
```
mysql&gt; SELECT
-&gt; goodsname,
-&gt; specification,
-&gt; CONCAT(goodsname,'(', IFNULL(specification, ''),')') AS 拼接 -- 用条件判断函数,如果规格是空,则括号中是空字符串
-&gt; FROM
-&gt; demo.goodsmaster;
+-----------+---------------+----------+
| goodsname | specification | 拼接 |
+-----------+---------------+----------+
| 书 | 16开 | 书(16开) |
| 笔 | NULL | 笔() |
+-----------+---------------+----------+
2 rows in set (0.00 sec)
```
结果是,如果规格为空,商品信息就变成了“商品信息()”,好像还不错。但是也存在一点问题:商品名称后面的那个空括号“()”会让客人觉得奇怪,能不能去掉呢?
如果用IFNULLV1V2函数就不容易做到但是没关系我们可以尝试用另一个条件判断函数IF表达式V1V2来解决。这里表达式是ISNULL(specification),这个函数用来判断字段"specificaiton"是否为空V1是返回商品名称V2是返回商品名称拼接规格。代码如下所示
```
mysql&gt; SELECT
-&gt; goodsname,
-&gt; specification,
-&gt; -- 这里做判断,如果是空值,返回商品名称,否则就拼接规格
-&gt; IF(ISNULL(specification),
-&gt; goodsname,
-&gt; CONCAT(goodsname, '(', specification, ')')) AS 拼接
-&gt; FROM
-&gt; demo.goodsmaster;
+-----------+---------------+----------+
| goodsname | specification | 拼接 |
+-----------+---------------+----------+
| 书 | 16开 | 书(16开) |
| 笔 | NULL | 笔 |
+-----------+---------------+----------+
2 rows in set (0.02 sec)
```
这个结果就是,如果规格为空,商品信息就是商品名称;如果规格不为空,商品信息是商品名称拼接商品规格,这就达到了我们的目的。
## 总结
今天,我们学习了用于提升数据处理效率的数学函数、字符串函数和条件判断函数。
<img src="https://static001.geekbang.org/resource/image/06/f7/06f0cb9251af48e626b81016630f9ff7.jpg" alt="">
这些函数看起来很容易掌握但是有很多坑。比如说ROUNDX是对X小数部分四舍五入那么在“五入”的时候返回的值是不是一定比X大呢其实不一定因为当X为负数时五入的值会更小。你可以看看下面的代码
```
mysql&gt; SELECT ROUND(-1.5);
+-------------+
| ROUND(-1.5) |
+-------------+
| -2 |
+-------------+
1 row in set (0.00 sec)
```
所以,我建议你在学习的时候,**多考虑边界条件的场景,实际测试一下**。就像这个问题对于ROUND(X,0)并没有指定X是正数那如果是负数会怎样呢你去测试一下就明白了。
## 思考题
这节课我介绍了如何用FLOOR函数来计算会员积分那么如果不用FLOOR有没有其他办法来实现会员积分的计算呢
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,446 @@
<audio id="audio" title="11 | 索引:怎么提高查询的速度?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d0/d8/d0b32336271fe5a52410da5a19ee7fd8.mp3"></audio>
你好,我是朱晓峰。
在我们的超市信息系统刚刚开始运营的时候因为数据量很少每一次的查询都能很快拿到结果。但是系统运转时间长了以后数据量不断地累积变得越来越庞大很多查询的速度就变得特别慢。这个时候我们就采用了MySQL 提供的高效访问数据的方法—— 索引有效地解决了这个问题甚至之前的一个需要8秒钟才能完成的查询现在只用0.3秒就搞定了速度提升了20多倍。
那么,索引到底是啥呢?该怎么使用呢?这节课,我们就来聊一聊。
# 索引是什么?
如果你去过图书馆,应该会知道图书馆的检索系统。图书馆为图书准备了检索目录,包括书名、书号、对应的位置信息,包括在哪个区、哪个书架、哪一层。我们可以通过书名或书号,快速获知书的位置,拿到需要的书。
MySQL中的索引就相当于图书馆的检索目录它是帮助MySQL系统快速检索数据的一种存储结构。我们可以在索引中按照查询条件检索索引字段的值然后快速定位数据记录的位置这样就不需要遍历整个数据表了。而且数据表中的字段越多表中数据记录越多速度提升越是明显。
我来举个例子进一步解释下索引的作用。这里要用到销售流水表demo.trans表结构如下
```
mysql&gt; describe demo.trans;
+---------------+----------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+----------+------+-----+---------+-------+
| itemnumber | int | YES | MUL | NULL | |
| quantity | text | YES | | NULL | |
| price | text | YES | | NULL | |
| transdate | datetime | YES | MUL | NULL | |
| actualvalue | text | YES | | NULL | |
| barcode | text | YES | | NULL | |
| cashiernumber | int | YES | MUL | NULL | |
| branchnumber | int | YES | MUL | NULL | |
| transuniqueid | text | YES | | NULL | |
+---------------+----------+------+-----+---------+-------+
9 rows in set (0.02 sec)
```
某个门店的销售流水表有400万条数据现在我要查看一下商品编号是100的商品在2020-12-12这一天的销售情况查询代码如下
```
mysql&gt; SELECT
-&gt; quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; transdate &gt; '2020-12-12'
-&gt; AND transdate &lt; '2020-12-13'
-&gt; AND itemnumber = 100;
+----------+--------+---------------------+
| quantity | price | transdate |
+----------+--------+---------------------+
| 1.000 | 220.00 | 2020-12-12 19:45:36 |
| 1.000 | 220.00 | 2020-12-12 08:56:37 |
+----------+--------+---------------------+
2 rows in set (8.08 sec)
```
可以看到结果总共有2条记录可是却花了8秒钟非常慢。同时这里我没有做表的关联这只是单表的查询而且只是一个门店几个月的数据而已。而总部是把所有门店的数据都汇总到一起查询速度更慢这样的查询效率我们肯定是不能接受的。
怎么解决这个问题呢?这时,我们就可以给数据表添加索引。
# 单字段索引
MySQL支持单字段索引和组合索引而单字段索引比较常用我们先来学习下创建单字段索引的方法。
## 如何创建单字段索引?
创建单字段索引一般有3种方式
1. 你可以通过CREATE语句直接给已经存在的表创建索引这种方式比较简单我就不多解释了
1. 可以在创建表的同时创建索引;
1. 可以通过修改表来创建索引。
直接给数据表创建索引的语法如下:
```
CREATE INDEX 索引名 ON TABLE 表名 (字段);
```
创建表的同时创建索引的语法如下所示:
```
CREATE TABLE 表名
(
字段 数据类型,
….
{ INDEX | KEY } 索引名(字段)
)
```
修改表时创建索引的语法如下所示:
```
ALTER TABLE 表名 ADD { INDEX | KEY } 索引名 (字段);
```
这里有个小问题要提醒你一下给表设定主键约束或者唯一性约束的时候MySQL会自动创建主键索引或唯一性索引。这也是我建议你在创建表的时候一定要定义主键的原因之一。
举个小例子我们可以给表demo.trans创建索引如下
```
mysql&gt; CREATE INDEX index_trans ON demo.trans (transdate(10));
Query OK, 0 rows affected (1 min 8.71 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql&gt; SELECT
-&gt; quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; transdate &gt; '2020-12-12'
-&gt; AND transdate &lt; '2020-12-13'
-&gt; AND itemnumber = 100;
+----------+--------+---------------------+
| quantity | price | transdate |
+----------+--------+---------------------+
| 1.000 | 220.00 | 2020-12-12 19:45:36 |
| 1.000 | 220.00 | 2020-12-12 08:56:37 |
+----------+--------+---------------------+
2 rows in set (0.30 sec)
```
可以看到加了索引之后这一次我们只用了0.3秒比没有索引的时候快了20多倍。这么大的差距说明索引对提高查询的速度确实很有帮助。那么索引是如何做到这一点的呢下面我们来学习下单字段索引的作用原理。
## 单字段索引的作用原理
要知道索引是怎么起作用的我们需要借助MySQL中的EXPLAIN 这个关键字。
EXPLAIN关键字能够查看SQL语句的执行细节包括表的加载顺序表是如何连接的以及索引使用情况等。
```
mysql&gt; EXPLAIN SELECT
-&gt; quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; transdate &gt; '2020-12-12'
-&gt; AND transdate &lt; '2020-12-13'
-&gt; AND itemnumber = 100;
+----+-------------+-------------+------------+-------+-------------------+-------------------+---------+------+------+----------+-----------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+-------+-------------------+-------------------+---------+------+------+----------+-----------------------------------------------+
| 1 | SIMPLE | trans | NULL | range | index_trans | index_trans | 6 | NULL | 5411 | 10.00 | Using index condition; Using where; Using MRR |
+----+-------------+-------------+------------+-------+-------------------+-------------------+---------+------+------+----------+-----------------------------------------------+
1 row in set, 1 warning (0.00 sec)
```
我来解释下代码里的关键内容。
- type=range表示使用索引查询特定范围的数据记录。
- rows=5411表示需要读取的记录数。
- possible_keys=index_trans表示可以选择的索引是index_trans。
- key=index_trans表示实际选择的索引是index_trans。
- extra=Using index condition;Using where;Using MRR这里面的信息对SQL语句的执行细节做了进一步的解释包含了3层含义第一个是执行时使用了索引第二个是执行时通过WHERE条件进行了筛选第三个是使用了顺序磁盘读取的策略。
通过这个小例子我们可以发现有了索引之后MySQL在执行SQL语句的时候多了一种优化的手段。也就是说在查询的时候可以先通过查询索引快速定位然后再找到对应的数据进行读取这样就大大提高了查询的速度。
## 如何选择索引字段?
在刚刚的查询中我们是选择transdate交易时间字段来当索引字段你可能会问为啥不选别的字段呢这是因为交易时间是查询条件。MySQL可以按照交易时间的限定“2020年12月12日”在索引中而不是数据表中寻找满足条件的索引记录再通过索引记录中的指针来定位数据表中的数据。这样索引就能发挥作用了。
不过你有没有想过itemnumber字段也是查询条件能不能用itemnumber来创建一个索引呢我们来试一试
```
mysql&gt; CREATE INDEX index_trans_itemnumber ON demo.trans (itemnumber);
Query OK, 0 rows affected (43.88 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
然后看看效果:
```
mysql&gt; SELECT
-&gt; quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; transdate &gt; '2020-12-12' -- 对交易时间的筛选可以在transdate的索引中定位
-&gt; AND transdate &lt; '2020-12-13'
-&gt; AND itemnumber = 100; -- 对商品编号的筛选可以在itemnumber的索引中定位
+----------+--------+---------------------+
| quantity | price | transdate |
+----------+--------+---------------------+
| 1.000 | 220.00 | 2020-12-12 19:45:36 |
| 1.000 | 220.00 | 2020-12-12 08:56:37 |
+----------+--------+---------------------+
2 rows in set (0.38 sec)
```
我们发现用itemnumber创建索引之后查询速度跟之前差不多基本在同一个数量级。
这是为啥呢我们来看看MySQL的运行计划
```
mysql&gt; EXPLAIN SELECT
-&gt; quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; transdate &gt; '2020-12-12'
-&gt; AND transdate &lt; '2020-12-13'
-&gt; AND itemnumber = 100; -- 对itemnumber 进行限定
+----+-------------+-------------+------------+------+------------------------------------------------+------------------------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+------------------------------------------------+------------------------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | trans | NULL | ref | index_trans,index_trans_itemnumber | index_trans_itemnumber | 5 | const | 1192 | 0.14 | Using where |
+----+-------------+-------------+------------+------+------------------------------------------------+------------------------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)
```
我们发现“possible_keys= index_trans,index_trans_itemnumber ”就是说MySQL认为可以选择的索引确实有2个一个是用transdate字段创建的索引index_trans另一个是用itemnumber字段创建的索引index_trans_itemnumber。
key= index_trans_itemnumber 说明MySQL实际选择使用的索引是itemnumber字段创建的索引index_trans_itemnumber。而rows=1192就表示实际读取的数据记录数只有1192个比用transdate创建的索引index_trans的实际读取记录数要少这就是MySQL选择使用itemnumber索引的原因。
**所以,我建议你在选择索引字段的时候,要选择那些经常被用做筛选条件的字段**。这样才能发挥索引的作用,提升检索的效率。
# 组合索引
在实际工作中有时会遇到比较复杂的数据表这种表包括的字段比较多经常需要通过不同的字段筛选数据特别是数据表中包含多个层级信息。比如我们的销售流水表就包含了门店信息、收款机信息和商品信息这3个层级信息。门店对应多个门店里的收款机每个收款机对应多个从这台收款机销售出去的商品。我们经常要把这些层次信息作为筛选条件来进行查询。这个时候单字段的索引往往不容易发挥出索引的最大功效可以使用组合索引。
现在先看看单字段索引的效果我们分别用branchnumber和cashiernumber来创建索引
```
mysql&gt; CREATE INDEX index_trans_branchnumber ON demo.trans (branchnumber);
Query OK, 0 rows affected (41.49 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql&gt; CREATE INDEX index_trans_cashiernumber ON demo.trans (cashiernumber);
Query OK, 0 rows affected (41.95 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
有了门店编号和收款机编号的索引,现在我们就尝试一下以门店编号、收款机编号和商品编号为查询条件,来验证一下索引是不是起了作用。
```
mysql&gt; SELECT
-&gt; itemnumber,quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; branchnumber = 11 AND cashiernumber = 1 -- 门店编号和收款机号为筛选条件
-&gt; AND itemnumber = 100; -- 商品编号为筛选条件
+------------+----------+--------+---------------------+
| itemnumber | quantity | price | transdate |
+------------+----------+--------+---------------------+
| 100 | 1.000 | 220.00 | 2020-07-11 09:18:35 |
| 100 | 1.000 | 220.00 | 2020-09-06 21:21:58 |
| 100 | 1.000 | 220.00 | 2020-11-10 15:00:11 |
| 100 | 1.000 | 220.00 | 2020-12-25 14:28:06 |
| 100 | 1.000 | 220.00 | 2021-01-09 20:21:44 |
| 100 | 1.000 | 220.00 | 2021-02-08 10:45:05 |
+------------+----------+--------+---------------------+
6 rows in set (0.31 sec)
```
结果有6条记录查询时间是0.31秒,跟只创建商品编号索引差不多。下面我们就来查看一下执行计划,看看新建的索引有没有起作用。
```
mysql&gt; EXPLAIN SELECT
-&gt; itemnumber,quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; branchnumber = 11 AND cashiernumber = 1
-&gt; AND itemnumber = 100;
+----+-------------+-------+------------+------+---------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | trans | NULL | ref | index_trans_itemnumber,index_trans_branchnumber,index_trans_cashiernumber | index_trans_itemnumber | 5 | const | 1192 | 20.50 | Using where |
+----+-------------+-------+------------+------+---------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)
```
MySQL有3个索引可以用分别是用branchnumber创建的index_trans_branchnumber、用cashiernumber创建的index_trans_cashiernumber和用itemnumber创建的index_trans_itemnumber。
最后MySQL还是选择了index_trans_itemnumber实际筛选的记录数是1192花费了0.31秒。
为什么MySQL会这样选呢这是因为优化器现在有3种索引可以用分别是商品编号索引、门店编号索引和收款机号索引。优化器发现商品编号索引实际搜索的记录数最少所以最后就选择了这种索引。
所以,**如果有多个索引而这些索引的字段同时作为筛选字段出现在查询中的时候MySQL会选择使用最优的索引来执行查询操作**。
能不能让这几个筛选字段同时发挥作用呢这就用到组合索引了。组合索引就是包含多个字段的索引。MySQL最多支持由16个字段组成的组合索引。
## 如何创建组合索引?
创建组合索引的语法结构与创建单字段索引相同,不同的是相比单字段索引,组合索引使用了多个字段。
直接给数据表创建索引的语法如下:
```
CREATE INDEX 索引名 ON TABLE 表名 (字段1字段2...);
```
创建表的同时创建索引:
```
CREATE TABLE 表名
(
字段 数据类型,
….
{ INDEX | KEY } 索引名(字段1字段2...)
)
```
修改表时创建索引:
```
ALTER TABLE 表名 ADD { INDEX | KEY } 索引名 (字段1字段2...);
```
现在,针对刚刚的查询场景,我们就可以通过创建组合索引,发挥多个字段的筛选作用。
具体做法是我们给销售流水表创建一个由3个字段branchnumber、cashiernumber、itemnumber组成的组合索引如下所示
```
mysql&gt; CREATE INDEX Index_branchnumber_cashiernumber_itemnumber ON demo.trans (branchnumber,cashiernumber,itemnumber);
Query OK, 0 rows affected (59.26 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
有了组合索引,刚刚的查询速度就更快了:
```
mysql&gt; SELECT
-&gt; itemnumber,quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; branchnumber = 11 AND cashiernumber = 1
-&gt; AND itemnumber = 100;
+------------+----------+--------+---------------------+
| itemnumber | quantity | price | transdate |
+------------+----------+--------+---------------------+
| 100 | 1.000 | 220.00 | 2020-07-11 09:18:35 |
| 100 | 1.000 | 220.00 | 2020-09-06 21:21:58 |
| 100 | 1.000 | 220.00 | 2020-11-10 15:00:11 |
| 100 | 1.000 | 220.00 | 2020-12-25 14:28:06 |
| 100 | 1.000 | 220.00 | 2021-01-09 20:21:44 |
| 100 | 1.000 | 220.00 | 2021-02-08 10:45:05 |
+------------+----------+--------+---------------------+
6 rows in set (0.00 sec)
```
几乎是瞬间就完成了不超过10毫秒。我们看看MySQL的执行计划
```
mysql&gt; EXPLAIN SELECT
-&gt; itemnumber,quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE -- 同时筛选门店编号、收款机号和商品编号
-&gt; branchnumber = 11 AND cashiernumber = 1
-&gt; AND itemnumber = 100;
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+---------------------------------------------+---------+-------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+---------------------------------------------+---------+-------------------+------+----------+-------+
| 1 | SIMPLE | trans | NULL | ref | index_trans_itemnumber,index_trans_branchnumber,index_trans_cashiernumber,index_branchnumber_cashiernumber_itemnumber | index_branchnumber_cashiernumber_itemnumber | 15 | const,const,const | 6 | 100.00 | NULL |
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+---------------------------------------------+---------+-------------------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)
```
这个查询MySQL可以用到的索引有4个
- index_trans_itemnumber
- index_trans_branchnumber
- index_trans_cashiernumber
- 我们刚才用branchnumber、cashiernumber和itemnumber创建的组合索引Index_branchnumber_cashiernumber_itemnumber。
MySQL选择了组合索引筛选后读取的记录只有6条。组合索引被充分利用筛选更加精准所以非常快。
## 组合索引的原理
下面我就来讲讲组合索引的工作原理。
**组合索引的多个字段是有序的,遵循左对齐的原则**。比如我们创建的组合索引排序的方式是branchnumber、cashiernumber和itemnumber。因此筛选的条件也要遵循从左向右的原则如果中断那么断点后面的条件就没有办法利用索引了。
比如说我们刚才的条件branchnumber = 11 AND cashiernumber = 1 AND itemnumber = 100包含了从左到右的所有字段所以可以最大限度使用全部组合索引。
假如把条件换成“cashiernumber = 1 AND itemnumber = 100”由于我们的组合索引是按照branchnumber、cashiernumber和itemnumber的顺序建立的最左边的字段branchnumber没有包含到条件当中中断了所以这个条件完全不能使用组合索引。
类似的如果筛选的是一个范围如果没有办法无法精确定位也相当于中断。比如“branchnumber &gt; 10 AND cashiernumber = 1 AND itemnumber = 100”这个条件只能用到组合索引中branchnumber&gt;10的部分后面的索引就都用不上了。我们来看看MySQL的运行计划
```
mysql&gt; EXPLAIN SELECT
-&gt; itemnumber,quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; branchnumber &gt; 10 AND cashiernumber = 1 AND itemnumber = 100;
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | trans | NULL | ref | index_trans_itemnumber,index_trans_branchnumber,index_trans_cashiernumber,index_branchnumber_cashiernumber_itemnumber | index_trans_itemnumber | 5 | const | 1192 | 20.50 | Using where |
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.02 sec)
```
果然MySQL没有选择组合索引而是选择了用itemnumber创建的普通索引index_trans_itemnumber。因为**如果只用组合索引的一部分,效果没有单字段索引那么好**。
# 总结
这节课,我们学习了什么是索引、如何创建和使用索引。索引可以非常显著地提高数据查询的速度,数据表里包含的数据越多,效果越显著。我们应该选择经常被用做筛选条件的字段来创建索引,这样才能通过索引缩小实际读取数据表中数据的范围,发挥出索引的优势。如果有多个筛选的字段,而且经常一起出现,也可以用多个字段来创建组合索引。
如果你要删除索引,就可以用:
```
DROP INDEX 索引名 ON 表名;
```
当然, 有的索引不能用这种方法删除,比如主键索引,你就必须通过修改表来删除索引。语法如下:
```
ALTER TABLE 表名 DROP PRIMARY KEY
```
最后我来跟你说说索引的成本。索引能够提升查询的效率但是建索引也是有成本的主要有2个方面一个存储空间的开销还有一个是数据操作上的开销。
- 存储空间的开销,是指索引需要单独占用存储空间。
- 数据操作上的开销,是指一旦数据表有变动,无论是插入一条新数据,还是删除一条旧的数据,甚至是修改数据,如果涉及索引字段,都需要对索引本身进行修改,以确保索引能够指向正确的记录。
因此,索引也不是越多越好,创建索引有存储开销和操作开销,需要综合考虑。
# 思考题
假如我有一个单品销售统计表,包括门店编号、销售日期(年月日)、商品编号、销售数量、销售金额、成本、毛利,而用户经常需要对销售情况进行查询,你会对这个表建什么样的索引呢?为什么?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,239 @@
<audio id="audio" title="12 | 事务:怎么确保关联操作正确执行?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a6/53/a6329fcd8cd566a1a2d772c9468f7753.mp3"></audio>
你好,我是朱晓峰。
我们经常会遇到这样的场景几个相互关联的数据操作必须是全部执行或者全部不执行不可以出现部分执行的情况。比如说你从微信账号里提现100元到银行卡上这个动作就包括了相互关联的2个步骤首先是微信账号减100元然后是银行卡账号加100元这里假设没有手续费。假如因为某种异常这2个操作只执行了一个另外一个没有执行就会出现你的钱少了100元或者你的钱多了100元的情况这肯定是不能接受的。
如何才能确保多个关联操作全部执行呢?这时就要用到事务了。接下来我就重点讲一讲什么是事务,以及如何正确使用事务。
## 什么是事务?
事务是MySQL的一项功能它可以使一组数据操作也叫DML操作是英文Data Manipulation Language的缩写包括SELECT、INSERT、UPDATE和DELETE要么全部执行要么全部不执行不会因为某种异常情况比如硬件故障、停电、网络中断等出现只执行一部分操作的情况。
事务的语法结构如下所示:
```
START TRANSACTION 或者 BEGIN (开始事务)
一组DML语句
COMMIT提交事务
ROLLBACK事务回滚
```
我解释一下这几个关键字。
- **START TRANSACTION和BEGIN**表示开始事务意思是通知MySQL后面的DML操作都是当前事务的一部分。
- **COMMIT**:表示提交事务,意思是执行当前事务的全部操作,让数据更改永久有效。
- **ROLLBACK**:表示回滚当前事务的操作,取消对数据的更改。
事务有4个主要特征分别是原子性atomicity、一致性consistency、持久性durability和隔离性isolation
- 原子性:表示事务中的操作要么全部执行,要么全部不执行,像一个整体,不能从中间打断。
- 一致性:表示数据的完整性不会因为事务的执行而受到破坏。
- 隔离性:表示多个事务同时执行的时候,不互相干扰。不同的隔离级别,相互独立的程度不同。
- 持久性:表示事务对数据的修改是永久有效的,不会因为系统故障而失效。
持久性非常好理解,我就不多说了,接下来我重点讲一讲事务的原子性、一致性和隔离性,这是确保关联操作正确执行的关键。
## 如何确保操作的原子性和数据的一致性?
我借助一个超市的收银员帮顾客结账的简单场景来讲解。在系统中,结算的动作主要就是销售流水的产生和库存的消减。这里会涉及销售流水表和库存表,如下所示:
销售流水表demo.mytrans
<img src="https://static001.geekbang.org/resource/image/bd/1e/bd6a537byy788646d1202167245c1c1e.jpeg" alt="">
库存表demo.inventory
<img src="https://static001.geekbang.org/resource/image/f1/98/f17cfe65a02f4a5a54e4a49e63a35998.jpeg" alt="">
现在假设门店销售了5个商品编号是1的商品这个动作实际上包括了2个相互关联的数据库操作
1. 向流水表中插入一条“1号商品卖了5个”的销售流水
1. 把库存表中的1号商品的库存减5。
这里包含了2个DML操作为了避免意外事件导致的一个操作执行了而另一个没有执行的情况我把它们放到一个事务里面利用事务中数据操作的原子性来确保数据的一致性。
```
mysql&gt; START TRANSACTION; -- 开始事务
Query OK, 0 rows affected (0.00 sec)
mysql&gt; INSERT INTO demo.mytrans VALUES (1,1,5); -- 插入流水
Query OK, 1 row affected (0.00 sec)
mysql&gt; UPDATE demo.inventory SET invquantity = invquantity - 5 WHERE itemnumber = 1; -- 更新库存
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql&gt; COMMIT; -- 提交事务
Query OK, 0 rows affected (0.06 sec)
```
然后我们查询一下结果:
```
mysql&gt; SELECT * FROM demo.mytrans; -- 流水插入成功了
+---------+------------+----------+
| transid | itemnumber | quantity |
+---------+------------+----------+
| 1 | 1 | 5.000 |
+---------+------------+----------+
1 row in set (0.00 sec)
mysql&gt; SELECT * FROM demo.inventory; -- 库存消减成功了
+------------+-------------+
| itemnumber | invquantity |
+------------+-------------+
| 1 | 5.000 |
+------------+-------------+
1 row in set (0.00 sec)
```
这样通过把2个相关操作放到事务里面我们就实现了一个事务操作。
这里有一个坑,我要提醒你一下。**事务并不会自动帮你处理SQL语句执行中的错误**,如果你对事务中的某一步数据操作发生的错误不做处理,继续提交的话,仍然会导致数据不一致。
为了方便你理解,我举个小例子。
假如我们的插入一条销售流水的语句少了一个字段,执行的时候出现错误了,如果我们不对这个错误做回滚处理,继续执行后面的操作,最后提交事务,结果就会出现没有流水但库存消减了的情况:
```
mysql&gt; START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql&gt; INSERT INTO demo.mytrans VALUES (1,5); -- 这个插入语句出错了
ERROR 1136 (21S01): Column count doesn't match value count at row 1
mysql&gt; UPDATE demo.inventory SET invquantity = invquantity - 5 WHERE itemnumber = 1;
Query OK, 1 row affected (0.00 sec) -- 后面的更新语句仍然执行成功了
Rows matched: 1 Changed: 1 Warnings: 0
mysql&gt; COMMIT;
Query OK, 0 rows affected (0.03 sec) -- 事务提交成功了
```
我们查一下表的内容:
```
mysql&gt; SELECT * FROM demo.mytrans; -- 流水没有插入成功
Empty set (0.16 sec)
mysql&gt; SELECT * FROM demo.inventory; -- 库存消减成功了
+------------+-------------+
| itemnumber | invquantity |
+------------+-------------+
| 1 | 5.000 |
+------------+-------------+
1 row in set (0.00 sec)
```
结果显示,流水插入失败了,但是库存更新成功了,这时候没有销售流水,但是库存却被消减了。
这就是因为没有正确使用事务导致的数据不完整问题。那么,如何使用事务,才能避免这种由于事务中的某一步或者几步操作出现错误,而导致数据不完整的情况发生呢?这就要用到事务中错误处理和回滚了:
- 如果发现事务中的某个操作发生错误,要及时使用回滚;
- 只有事务中的所有操作都可以正常执行,才进行提交。
那这里的关键就是判断操作是不是发生了错误。我们可以通过MySQL的函数ROW_COUNT()的返回来判断一个DML操作是否失败-1表示操作失败否则就表示影响的记录数。
```
mysql&gt; INSERT INTO demo.mytrans VALUES (1,5);
ERROR 1136 (21S01): Column count doesn't match value count at row 1
mysql&gt; SELECT ROW_COUNT();
+-------------+
| ROW_COUNT() |
+-------------+
| -1 |
+-------------+
1 row in set (0.00 sec)
```
另外一个经常会用到事务的地方是存储过程。由于存储过程中包含很多相互关联的数据操作所以会大量使用事务。我们可以在MySQL的存储过程中通过获取SQL错误来决定事务是提交还是回滚
```
mysql&gt; DELIMITER // -- 修改分隔符为 //
mysql&gt; CREATE PROCEDURE demo.mytest() -- 创建存储过程
-&gt; BEGIN -- 开始程序体
-&gt; DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK; -- 定义SQL操作发生错误是自动回滚
-&gt; START TRANSACTION; -- 开始事务
-&gt; INSERT INTO demo.mytrans VALUES (1,5);
-&gt; UPDATE demo.inventory SET invquantity = invquantity - 5;
-&gt; COMMIT; -- 提交事务
-&gt; END
-&gt; // -- 完成创建存储过程
Query OK, 0 rows affected (0.05 sec)
mysql&gt; DELIMITER ; -- 恢复分隔符为;
mysql&gt; CALL demo.mytest(); -- 调用存储过程
Query OK, 0 rows affected (0.00 sec)
mysql&gt; SELECT * FROM demo.mytrans; -- 销售流水没有插入
Empty set (0.00 sec)
mysql&gt; SELECT * FROM demo.inventory; -- 库存也没有消减,说明事务回滚了
+------------+-------------+
| itemnumber | invquantity |
+------------+-------------+
| 1 | 10.000 |
+------------+-------------+
1 row in set (0.00 sec)
```
这里我们要先通过“DELIMITER //”语句把MySQL语句的结束标识改为“//”(默认语句的结束标识是“;”。这样做的目的是告诉MySQL一直到“//”才是语句的结束否则MySQL会在遇到第一个“;”的时候认为语句已经结束,并且执行。这样就会报错,自然也就没办法创建存储过程了。
创建结束以后,我们还要录入“//”告诉MySQL存储过程创建完成了并且通过“DELIMITER ;”,再把语句结束标识改回到“;”。
关于存储过程我会在后面的课程里给你详细介绍。这里你只需要知道在这个存储过程中我使用了“DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK;”这个语句来监控SQL语句的执行结果一旦发发生错误就自动回滚并退出。通过这个机制我们就实现了对事务中的SQL操作进行监控如果发现事务中的任何SQL操作发生错误就自动回滚。
总之,**我们要把重要的关联操作放在事务中,确保操作的原子性,并且对失败的操作进行回滚处理**。只有这样,才能真正发挥事务的作用,保证关联操作全部成功或全部失败,最终确保数据的一致性。
## 如何用好事务的隔离性?
接下来,我们再学习下如何用好事务的隔离性。
超市经营者提出门店要支持网上会员销售现在我们假设会员张三是储值会员他的会员卡里有100元。张三用会员卡到门店消费100元他爱人用他的会员卡在网上消费100元。
张三在门店消费结算的时候开启了一个事务A包括这样3个操作
1. 读取卡内金额为100
1. 更新卡内金额为0
1. 插入一条销售流水。
张三的爱人在网上购物开启了一个事务B也来读取卡内金额。如果B读取卡内金额的操作发生在A更新卡内金额之后并且在插入销售流水之前那么B读出的金额应该是多少呢如果B读出0元那么A有可能由于后面的操作失败而回滚。因此B可能会读到一条错误信息而导致本来可以成功的交易失败。有什么办法可以解决这个问题呢
这个时候就会用到MySQL的另外一种机制“锁”。MySQL可以把A中被修改过而且还没有提交的数据锁住让B处于等待状态一直到A提交完成或者失败回滚再释放锁允许B读取这个数据。这样就可以防止因为A回滚而导致B读取错误的可能了。
MySQL中的锁有很多种功能也十分强大。咱们这门课里不要求你掌握锁你只要知道MySQL可以用锁来控制事务对数据的操作就可以了。
通过对锁的使用,可以实现事务之间的相互隔离。**锁的使用方式不同,隔离的程度也不同**。
MySQL支持4种事务隔离等级。
1. READ UNCOMMITTED可以读取事务中还未提交的被更改的数据。
1. READ COMMITTED只能读取事务中已经提交的被更改的数据。
1. REPEATABLE READ表示一个事务中对一个数据读取的值永远跟第一次读取的值一致不受其他事务中数据操作的影响。这也是MySQL的默认选项。
1. SERIALIZABLE表示任何一个事务一旦对某一个数据进行了任何操作那么一直到这个事务结束MySQL都会把这个数据锁住禁止其他事务对这个数据进行任何操作。
一般来讲使用MySQL默认的隔离等级REPEATABLE READ就已经够了。不过也不排除需要对一些关键的数据操作使用最高的隔离等级SERIALIZABLE。
举个例子,在我们的超市项目中,就对每天的日结操作设置了最高的隔离等级。因为日结要进行大量的核心数据计算,包括成本、毛利、毛利率、周转率,等等,并把结果保存起来,作为各类查询、报表系统、决策支持模块的基础,绝对不能出现数据错误。
当然,**计算完成之后,你也不要忘记把隔离等级恢复到系统默认的状态**,否则,会对日常的系统营运效率产生比较大的影响。
事务的隔离性对并发操作非常有用。当许多用户同时操作数据库的时候,隔离性可以确保各个连接之间互相不影响。这里我要提醒你的是,正确设置事务的隔离等级很重要。
一方面,**对于一些核心的数据更改操作,你可能需要较高的隔离等级**,比如涉及金额的修改;另一方面,**你要考虑资源的消耗,不能使系统整体的效率受到太大的影响**。所以,要根据具体的应用场景,正确地使用事务。
## 总结
事务可以确保事务中的一系列操作全部被执行不会被打断或者全部不被执行等待再次执行。事务中的操作具有原子性、一致性、永久性和隔离性的特征。但是这并不意味着被事务包裹起来的一系列DML数据操作就一定会全部成功或者全部失败。你需要对操作是否成功的结果进行判断并通知MySQL针对不同情况分别完成事务提交或者回滚操作才能最终确保事务中的操作全部成功或全部失败。
MySQL支持4种不同的事务隔离等级等级越高消耗的系统资源也越多你要根据实际情况进行设定。
在MySQL中并不是所有的操作都可以回滚。比如创建数据库、创建数据表、删除数据库、删除数据表等这些操作是不可以回滚的所以你在操作的时候要特别小心特别是在删除数据库、数据表时最好先做备份防止误操作。
## 思考题
学完了这节课以后,如果现在有人对你说,事务就是确保事务中的数据操作,要么全部正确执行,要么全部失败,你觉得这句话对吗?为什么?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,267 @@
<audio id="audio" title="13 | 临时表:复杂查询,如何保存中间结果?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a4/63/a45504b0c686ff7962783841892f3263.mp3"></audio>
你好,我是朱晓峰。今天,我来和你聊一聊临时表。
当我们遇到一些复杂查询的时候,经常无法一步到位,或者是一步到位会导致查询语句太过复杂,开发和维护的成本过高。这个时候,就可以使用临时表。
下面,我就结合实际的项目来讲解一下,怎么拆解一个复杂的查询,通过临时表来保存中间结果,从而把一个复杂查询变得简单而且容易实现。
## 临时表是什么?
临时表是一种特殊的表,用来存储查询的中间结果,并且会随着当前连接的结束而自动删除。**MySQL中有2种临时表分别是内部临时表和外部临时表**
- 内部临时表主要用于性能优化,由系统自动产生,我们无法看到;
- 外部临时表通过SQL语句创建我们可以使用。
因为我们不能使用内部临时表,所以我就不多讲了。今天,我来重点讲一讲我们可以创建和使用的外部临时表。
首先,你要知道临时表的创建语法结构:
```
CREATE TEMPORARY TABLE 表名
(
字段名 字段类型,
...
);
```
跟普通表相比临时表有3个不同的特征
1. 临时表的创建语法需要用到关键字TEMPORARY
1. 临时表创建完成之后,只有当前连接可见,其他连接是看不到的,具有连接隔离性;
1. 临时表在当前连接结束之后,会被自动删除。
因为临时表有连接隔离性不同连接创建相同名称的临时表也不会产生冲突适合并发程序的运行。而且连接结束之后临时表会自动删除也不用担心大量无用的中间数据会残留在数据库中。因此我们就可以利用这些特点用临时表来存储SQL查询的中间结果。
## 如何用临时表简化复杂查询?
刚刚提到,临时表可以简化复杂查询,具体是怎么实现的呢?我来介绍一下。
举个例子超市经营者想要查询2020年12月的一些特定商品销售数量、进货数量、返厂数量那么我们就要先把销售、进货、返厂这3个模块分开计算用临时表来存储中间计算的结果最后合并在一起形成超市经营者想要的结果集。
首先我们统计一下在2020年12月的商品销售数据。
假设我们的销售流水表mysales如下所示
<img src="https://static001.geekbang.org/resource/image/ay/f5/ayy269bb7dd210a1f1716ebd2947e9f5.jpeg" alt="">
我们可以用下面的SQL语句查询出每个单品的销售数量和销售金额并存入临时表
```
mysql&gt; CREATE TEMPORARY TABLE demo.mysales
-&gt; SELECT -- 用查询的结果直接生成临时表
-&gt; itemnumber,
-&gt; SUM(quantity) AS QUANTITY,
-&gt; SUM(salesvalue) AS salesvalue
-&gt; FROM
-&gt; demo.transactiondetails
-&gt; GROUP BY itemnumber
-&gt; ORDER BY itemnumber;
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql&gt; SELECT * FROM demo.mysales;
+------------+----------+------------+
| itemnumber | QUANTITY | salesvalue |
+------------+----------+------------+
| 1 | 5.000 | 411.18 |
| 2 | 5.000 | 24.75 |
+------------+----------+------------+
2 rows in set (0.01 sec)
```
需要注意的是,这里我是直接用查询结果来创建的临时表。因为创建临时表就是为了存放某个查询的中间结果。直接用查询语句创建临时表比较快捷,而且连接结束后临时表就会被自动删除,不需要过多考虑表的结构设计问题(比如冗余、效率等)。
到这里我们就有了一个存储单品销售统计的临时表。接下来我们计算一下2020年12月的进货信息。
我们的进货数据包括进货单头表importhead和进货单明细表importdetails
进货单头表包括进货单编号、供货商编号、仓库编号、操作员编号和验收日期:
<img src="https://static001.geekbang.org/resource/image/0a/80/0acf5b6ee4f154414fefd543b33f7180.jpeg" alt="">
进货单明细表包括进货单编号、商品编号、进货数量、进货价格和进货金额:
<img src="https://static001.geekbang.org/resource/image/36/36/368d0f558d497be064d264baaf99e636.jpeg" alt="">
我们用下面的SQL语句计算进货数据并且保存在临时表里面
```
mysql&gt; CREATE TEMPORARY TABLE demo.myimport
-&gt; SELECT b.itemnumber,SUM(b.quantity) AS quantity,SUM(b.importvalue) AS importvalue
-&gt; FROM demo.importhead a JOIN demo.importdetails b
-&gt; ON (a.listnumber=b.listnumber)
-&gt; GROUP BY b.itemnumber;
Query OK, 3 rows affected (0.01 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql&gt; SELECT * FROM demo.myimport;
+------------+----------+-------------+
| itemnumber | quantity | importvalue |
+------------+----------+-------------+
| 1 | 5.000 | 290.00 |
| 2 | 5.000 | 15.00 |
| 3 | 8.000 | 40.00 |
+------------+----------+-------------+
3 rows in set (0.00 sec)
```
这样我们又得到了一个临时表demo.myimport里面保存了我们需要的进货数据。
接着,我们来查询单品返厂数据,并且保存到临时表。
我们的返厂数据表有2个分别是返厂单头表returnhead和返厂单明细表returndetails
返厂单头表包括返厂单编号、供货商编号、仓库编号、操作员编号和验收日期:
<img src="https://static001.geekbang.org/resource/image/20/3e/206c5ba8bdcb13c55c78a1ec1f4aab3e.jpeg" alt="">
返厂单明细表包括返厂单编号、商品编号、返厂数量、返厂价格和返厂金额:
<img src="https://static001.geekbang.org/resource/image/a9/4b/a9bda457d8f15e8f87fc5d0f4e2ee24b.jpeg" alt="">
我们可以使用下面的SQL语句计算返厂信息并且保存到临时表中。
```
mysql&gt; CREATE TEMPORARY TABLE demo.myreturn
-&gt; SELECT b.itemnumber,SUM(b.quantity) AS quantity,SUM(b.returnvalue) AS returnvalue
-&gt; FROM demo.returnhead a JOIN demo.returndetails b
-&gt; ON (a.listnumber=b.listnumber)
-&gt; GROUP BY b.itemnumber;
Query OK, 3 rows affected (0.01 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql&gt; SELECT * FROM demo.myreturn;
+------------+----------+-------------+
| itemnumber | quantity | returnvalue |
+------------+----------+-------------+
| 1 | 2.000 | 115.00 |
| 2 | 1.000 | 3.00 |
| 3 | 1.000 | 5.00 |
+------------+----------+-------------+
3 rows in set (0.00 sec)
```
这样,我们就获得了单品的返厂信息。
有了前面计算出来的数据,现在,我们就可以把单品的销售信息、进货信息和返厂信息汇总到一起了。
如果你跟着实际操作的话你可能会有这样一个问题我们现在有3个临时表分别存储单品的销售信息、进货信息和返厂信息。那么能不能把这3个表相互关联起来把这些信息都汇总到对应的单品呢
答案是不行不管是用内连接、还是用外连接都不可以。因为无论是销售信息、进货信息还是返厂信息都存在商品信息缺失的情况。换句话说就是在指定时间段内某些商品可能没有销售某些商品可能没有进货某些商品可能没有返厂。如果仅仅通过这3个表之间的连接进行查询我们可能会丢失某些数据。
为了解决这个问题我们可以引入商品信息表。因为商品信息表包含所有的商品因此把商品信息表放在左边与其他的表进行左连接就可以确保所有的商品都包含在结果集中。凡是不存在的数值都设置为0然后再筛选一下把销售、进货、返厂都是0的商品去掉这样就能得到我们最终希望的查询结果2020年12月的商品销售数量、进货数量和返厂数量。
代码如下所示:
```
mysql&gt; SELECT
-&gt; a.itemnumber,
-&gt; a.goodsname,
-&gt; ifnull(b.quantity,0) as salesquantity, -- 如果没有销售记录销售数量设置为0
-&gt; ifnull(c.quantity,0) as importquantity, -- 如果没有进货进货数量设为0
-&gt; ifnull(d.quantity,0) as returnquantity -- 如果没有返厂返厂数量设为0
-&gt; FROM
-&gt; demo.goodsmaster a -- 商品信息表放在左边进行左连接,确保所有的商品都包含在结果集中
-&gt; LEFT JOIN demo.mysales b
-&gt; ON (a.itemnumber=b.itemnumber)
-&gt; LEFT JOIN demo.myimport c
-&gt; ON (a.itemnumber=c.itemnumber)
-&gt; LEFT JOIN demo.myreturn d
-&gt; ON (a.itemnumber=d.itemnumber)
-&gt; HAVING salesquantity&gt;0 OR importquantity&gt;0 OR returnquantity&gt;0; -- 在结果集中剔除没有销售,没有进货,也没有返厂的商品
+------------+-----------+---------------+----------------+----------------+
| itemnumber | goodsname | salesquantity | importquantity | returnquantity |
+------------+-----------+---------------+----------------+----------------+
| 1 | 书 | 5.000 | 5.000 | 2.000 |
| 2 | 笔 | 5.000 | 5.000 | 1.000 |
| 3 | 橡皮 | 0.000 | 8.000 | 1.000 |
+------------+-----------+---------------+----------------+----------------+
3 rows in set (0.00 sec)
```
总之通过临时表我们就可以把一个复杂的问题拆分成很多个前后关联的步骤把中间的运行结果存储起来用于之后的查询。这样一来就把面向集合的SQL查询变成了面向过程的编程模式大大降低了难度。
## 内存临时表和磁盘临时表
由于采用的存储方式不同,临时表也可分为内存临时表和磁盘临时表,它们有着各自的优缺点,下面我来解释下。
关于内存临时表有一点你要注意的是你可以通过指定引擎类型比如ENGINE=MEMORY来告诉MySQL临时表存储在内存中。
好了,现在我们先来创建一个内存中的临时表:
```
mysql&gt; CREATE TEMPORARY TABLE demo.mytrans
-&gt; (
-&gt; itemnumber int,
-&gt; groupnumber int,
-&gt; branchnumber int
-&gt; ) ENGINE = MEMORY; (临时表数据存在内存中)
Query OK, 0 rows affected (0.00 sec)
```
接下来我们在磁盘上创建一个同样结构的临时表。在磁盘上创建临时表时只要我们不指定存储引擎MySQL会默认存储引擎是InnoDB并且把表存放在磁盘上。
```
mysql&gt; CREATE TEMPORARY TABLE demo.mytransdisk
-&gt; (
-&gt; itemnumber int,
-&gt; groupnumber int,
-&gt; branchnumber int
-&gt; );
Query OK, 0 rows affected (0.00 sec)
```
现在,我们向刚刚的两张表里都插入同样数量的记录,然后再分别做一个查询:
```
mysql&gt; SELECT COUNT(*) FROM demo.mytrans;
+----------+
| count(*) |
+----------+
| 4355 |
+----------+
1 row in set (0.00 sec)
mysql&gt; SELECT COUNT(*) FROM demo.mytransdisk;
+----------+
| count(*) |
+----------+
| 4355 |
+----------+
1 row in set (0.21 sec)
```
可以看到区别是比较明显的。对于同一条查询内存中的临时表执行时间不到10毫秒而磁盘上的表却用掉了210毫秒。显然内存中的临时表查询速度更快。
不过,内存中的临时表也有缺陷。因为数据完全在内存中,所以,一旦断电,数据就消失了,无法找回。不过临时表只保存中间结果,所以还是可以用的。
我画了一张图,汇总了内存临时表和磁盘临时表的优缺点:
<img src="https://static001.geekbang.org/resource/image/c5/bf/c5f3d549f5f0fd72e74ec9c5441467bf.jpeg" alt="">
## 总结
这节课,我们学习了临时表的概念,以及使用临时表来存储中间结果以拆分复杂查询的方法。临时表可以存储在磁盘中,也可以通过指定引擎的办法存储在内存中,以加快存取速度。
其实临时表有很多好处除了可以帮助我们把复杂的SQL查询拆分成多个简单的SQL查询而且因为临时表是连接隔离的不同的连接可以使用相同的临时表名称相互之间不会受到影响。除此之外临时表会在连接结束的时候自动删除不会占用磁盘空间。
当然,临时表也有不足,比如会挤占空间。我建议你,**在使用临时表的时候,要从简化查询和挤占资源两个方面综合考虑,既不能过度加重系统的负担,同时又能够通过存储中间结果,最大限度地简化查询**。
## 思考题
我们有这样的一个销售流水表:
<img src="https://static001.geekbang.org/resource/image/a9/b6/a970618a3807cyyeeaf28ac57fa034b6.jpeg" alt="">
假设有多个门店,每个门店有多台收款机,每台收款机销售多种商品,请问如何查询每个门店、每台收款机的销售金额占所属门店的销售金额的比率呢?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。

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”账号不行吗
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,11 @@
<audio id="audio" title="期末测试 | 一套习题,测出你的掌握程度" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fb/9f/fbc8c67187c5553c52dbfeefee55cf9f.mp3"></audio>
你好,我是朱晓峰。
咱们课程的核心内容都已经更新完了在临近告别前我还给你准备了一份期末测试题这套测试题共有12道单选题和8道多选题满分100核心考点都出自前面讲到的所有重要知识希望可以帮助你进行一场自测。
[<img src="https://static001.geekbang.org/resource/image/28/a4/28d1be62669b4f3cc01c36466bf811a4.png" alt="">](http://time.geekbang.org/quiz/intro?act_id=444&amp;exam_id=1513)
另外,我还为你准备了一份[结课问卷](https://jinshuju.net/f/H556x1)希望听一听你对这门课的反馈。只要填写就有机会获得一个手绘护腕垫或者是价值99元的课程阅码。欢迎你花1分钟时间填写一下期待你的畅所欲言。
[<img src="https://static001.geekbang.org/resource/image/2c/34/2c9d5fc92a31e86a25f68bab00dc4734.jpg" alt="">](https://jinshuju.net/f/H556x1)

View File

@@ -0,0 +1,252 @@
<audio id="audio" title="特别发送(一) | 经典面试题讲解第一弹" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/df/83/df7d7cff7a07c33c5bbf098c40ff1283.mp3"></audio>
你好,我是朱晓峰。
到这里“实践篇”的内容咱们就学完了。今天我们来学点儿不一样的——5道经典面试题。这些都是在实际面试中的原题当然我没有完全照搬而是结合咱们课程的具体情况有针对性地进行了调整。我不仅会给你提供答案还会和你一起分析让你能够灵活地吃透这些题目并能举一反三。
话不多说,我们现在开始。我先带你从一道简单的关于“索引”的面试题入手,索引在面试题里经常出现,来看看这一道你能不能做对。
## 第一题
下面关于索引的描述,正确的是:
1. 建立索引的主要目的是减少冗余数据,使数据表占用更少的空间,并且提高查询的速度
1. 一个表上可以建立一个或者多个索引
1. 组合索引可以有效提高查询的速度,比单字段索引更高效,所以,我们应该创建一个由所有的字段组成的组合索引,这样就可以解决所有问题了
1. 因为索引可以提高查询效率,所以索引建得越多越好
解析这道题的正确答案是选项2我们来分析一下其他选项。
- 选项1说对了一半索引可以提高查询效率但是创建索引不能减少冗余数据而且索引还要占用额外的存储空间所以选项1不对。
- 选项3不对的原因有2个。第一组合索引不一定比单字段索引高效因为组合索引的字段是有序的遵循左对齐的原则。如果查询的筛选条件不包含组合索引中最左边的字段那么组合索引就完全不能用。第二创建索引也是有成本的需要占用额外的存储空间。用所有的字段创建组合索引的存储成本比较高而且利用率比较低完全用上的可能性几乎不存在所以很少有人会这样做。而且一旦更改任何一个字段的数据就必须要改索引这样操作成本也比较高。
- 选项4错误因为索引有成本很少作为筛选条件的字段没有必要创建索引。
如果这道题你回答错了,一定要回去复习下[第11讲](https://time.geekbang.org/column/article/357312)的内容。
## 第二题
假设我们有这样一份学生成绩单所有同学的成绩都各不相同请编写一个简单的SQL语句查询分数排在第三名的同学的成绩
<img src="https://static001.geekbang.org/resource/image/36/d1/3618ee4c82a592bb7954c4d63d9c1dd1.jpeg" alt="">
解析:这道题考查的是我们对查询语句的掌握情况。针对题目中的场景,可以分两步来进行查询。
第一步,按照成绩高低进行排序:
```
mysql&gt; SELECT *
-&gt; FROM demo.test1
-&gt; ORDER BY score DESC; -- DESC表示降序排列
+----+------+-------+
| id | name | score |
+----+------+-------+
| 2 | 李四 | 90.00 |
| 4 | 赵六 | 88.00 |
| 1 | 张三 | 80.00 |
| 3 | 王五 | 76.00 |
| 5 | 孙七 | 67.00 |
+----+------+-------+
5 rows in set (0.00 sec)
```
第二步,找出排名第三的同学和对应的成绩。我们可以用[第4讲](https://time.geekbang.org/column/article/351225)里学过的对返回记录进行限定的关键字LIMIT
```
mysql&gt; SELECT *
-&gt; FROM demo.test1
-&gt; ORDER BY score DESC
-&gt; LIMIT 2,1;
+----+------+-------+
| id | name | score |
+----+------+-------+
| 1 | 张三 | 80.00 |
+----+------+-------+
1 row in set (0.00 sec)
```
在MySQL中LIMIT后面可以跟2个参数第一个参数表示记录的起始位置第一个记录的位置是0第二个参数表示返回几条记录。因此“LIMIT 2,1”就表示从第3条记录开始返回1条记录。这样就可以查出排名第三的同学的成绩了。
## 第三题
现在我们有两个表分别是人员表demo.person和地址表demo.address要求你使用关联查询查出完整信息。无论有没有地址信息人员的信息必须全部包含在结果集中。
人员表:
<img src="https://static001.geekbang.org/resource/image/b6/90/b6ed67787cebbdb0786a42c47326a390.jpeg" alt="">
地址表:
<img src="https://static001.geekbang.org/resource/image/57/ea/5758763942c2a0dc59125bd73f3134ea.jpeg" alt="">
解析: 这个是典型的外查询,咱们在[第6讲](https://time.geekbang.org/column/article/353464)里学过。题目要求我们查出人员表中的全部信息,而地址表中信息则可以为空,就可以用下面的查询代码:
```
mysql&gt; SELECT *
-&gt; FROM demo.person AS a
-&gt; LEFT JOIN demo.address AS b -- 左连接确保demo.person中的记录全部包括在结果集中
-&gt; ON (a.id=b.id);
+----+-------+-------+------+---------+------+-----------+
| id | fname | lname | id | country | city | address |
+----+-------+-------+------+---------+------+-----------+
| 1 | 张 | 三 | 1 | 中国 | 北京 | 海淀123 |
| 2 | 李 | 四 | 2 | 美国 | 纽约 | 奥巴尼333 |
| 3 | 王 | 五 | NULL | NULL | NULL | NULL |
+----+-------+-------+------+---------+------+-----------+
3 rows in set (0.02 sec)
```
## 第四题
假设有这样一个教学表demo.teach)里面包含了人员编号id、姓名fname和对应的老师的人员编号teacherid。如果一个人是学生那么他一定有对应的老师编号通过这个编号就可以找到老师的信息如果一个人是老师那么他对应的老师编号就是空。比如说下表中李四的老师编号是101我们就可以通过搜索人员编号找到101的名称是张三那么李四的老师就是张三而张三自己就是老师所以他对应的老师编号是空。
<img src="https://static001.geekbang.org/resource/image/8d/7c/8d9de75354c0af5bc99e01767b87a57c.jpeg" alt="">
要求请写一个SQL语句查询出至少有2名学生的老师姓名。
说明一下在刚刚的数据表中张三有3名学生分别是李四、王五和周八。赵六有一名学生是孙七。因此正确的SQL语句的查询结果应该是
<img src="https://static001.geekbang.org/resource/image/e5/40/e515868404495d2cea1c271b0d0ec440.jpeg" alt="">
解析:
针对这道题,我们可以按照这样的思路去做:
1. 通过统计学生对应的老师编号就可以获取至少有2个学生的老师的编号。
1. 通过关联查询和自连接获取需要的信息。所谓的自连接就是数据表与自身进行连接。你可以认为是把数据表复制成一模一样的2个表通过给数据表起不同的名字来区分它们这样方便对表进行操作然后就可以对这2个表进行连接操作了。
1. 通过使用条件语句WHERE和HAVING对数据进行筛选先用WHERE筛选出所有的老师编号再用HAVING筛选出有2个以上学生的老师编号。
首先,我们来获取老师编号,如下:
```
mysql&gt; SELECT teacherid
-&gt; FROM demo.teach
-&gt; WHERE teacherid is not NULL -- 用WHERE筛选出所有的老师编号
-&gt; GROUP BY teacherid
-&gt; HAVING COUNT(*)&gt;=2; -- 用HAVING筛选出有2个以上学生的老师编号
+-----------+
| teacherid |
+-----------+
| 101 |
+-----------+
1 row in set (0.00 sec)
```
接着,通过自连接,来获取老师的姓名:
```
mysql&gt; SELECT a.id,a.fname
-&gt; FROM demo.teach AS a
-&gt; JOIN
-&gt; (
-&gt; SELECT teacherid
-&gt; FROM demo.teach
-&gt; WHERE teacherid IS NOT NULL
-&gt; GROUP BY teacherid
-&gt; HAVING COUNT(*)&gt;=2
-&gt; ) AS b
-&gt; ON (a.id = b.teacherid);
+-----+-------+
| id | fname |
+-----+-------+
| 101 | 张三 |
+-----+-------+
1 row in set (0.00 sec)
```
## 第五题
假设某中学高三年级有多位同学分成多个班我们有统一记录学生成绩的表demo.student)和班级信息表demo.class具体信息如下所示
学生成绩表:
<img src="https://static001.geekbang.org/resource/image/9a/20/9ae0eyy03386f24d568b8507d2dd6f20.jpeg" alt="">
班级信息表:
<img src="https://static001.geekbang.org/resource/image/6c/b9/6c4d85c4dff2c626d55fbaf9671bccb9.jpeg" alt="">
要求写一个SQL查询语句查出每个班级前三名的同学。
说明一下针对上面的数据正确的SQL查询应该得出下面的结果
<img src="https://static001.geekbang.org/resource/image/5c/cd/5c6fc34826c367f5a0cdf38610b26ecd.jpeg" alt="">
解析:
1. 从题目给出的查询结果看不需要考虑并列的情况。那么现在要选出分数排名前三的同学其实只要找出3个最好的分数以及对应的同学就可以了。
1. 这道题需要用到我们讲过的关联查询和子查询的知识。
1. WHERE语句的筛选条件表达式中也可以包括一个子查询的结果。
第一步我们假设有一个分数X就是那个第N好的分数算一下有多少个同学的成绩优于这个分数
```
SELECT COUNT(DISTINCT b.points)
FROM demo.student AS b
WHERE b.points &gt; X;
```
这个查询的结果小于3的话就代表这个分数X是排名第三的分数了。
第二步,查询出哪些同学满足成绩排名前三的这个档次:
```
mysql&gt; SELECT a.stdname,a.points
-&gt; FROM demo.student AS a
-&gt; WHERE 3 &gt; -- 比这个成绩好的不超过3说明这是第三好的成绩
-&gt; (
-&gt; SELECT COUNT(DISTINCT b.points) -- 统计一下有几个成绩
-&gt; FROM demo.student AS b
-&gt; WHERE b.points &gt; a.points -- 比这个成绩好
-&gt; );
+---------+--------+
| stdname | points |
+---------+--------+
| 张三 | 85 |
| 李四 | 80 |
| 赵六 | 90 |
| 周八 | 85 |
+---------+--------+
4 rows in set (0.00 sec)
```
第三步,与班级表关联,按班级统计前三名同学的成绩,并且获取班级信息:
```
mysql&gt; SELECT c.classname,a.stdname,a.points
-&gt; FROM demo.student AS a
-&gt; JOIN demo.class AS c
-&gt; ON (a.classid = c.id) -- 关联班级信息
-&gt; WHERE 3 &gt;
-&gt; (
-&gt; SELECT COUNT(DISTINCT b.points)
-&gt; FROM demo.student AS b
-&gt; WHERE b.points &gt; a.points
-&gt; AND b.classid = a.classid -- 按班级分别查询
-&gt; )
-&gt; ORDER BY c.classname,a.points DESC;
+-----------+---------+--------+
| classname | stdname | points |
+-----------+---------+--------+
| 创新班 | 赵六 | 90 |
| 创新班 | 张三 | 85 |
| 创新班 | 周八 | 85 |
| 创新班 | 郑九 | 70 |
| 普通班 | 李四 | 80 |
| 普通班 | 王五 | 65 |
+-----------+---------+--------+
6 rows in set (0.00 sec)
```
## 总结
今天我们借助几个面试题回顾了索引的概念、查询、子查询和关联查询的知识以及条件语句WHERE和HAVING的不同使用方法。如果你发现哪些内容掌握得还没有那么牢固一定要及时回去复习一下。
在真正的面试中,很少有单纯考查知识点本身的题目,更多的是考查你在解决实际问题的过程中,对知识的灵活运用能力。所以,在学习每一节课时,你一定要结合我给出的实际项目,去真正实践一下,这样才能以不变应万变,在面试中有好的表现。

View File

@@ -0,0 +1,356 @@
<audio id="audio" title="特别放送(三)| MySQL 8 都有哪些新特征?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/02/6e/02ca96e1f5961b2f740a41bf2557126e.mp3"></audio>
你好我是朱晓峰。今天我来和你聊一聊MySQL 8的新特征。
作为应用最广泛的三大关系型数据库之一MySQL的背后有一个强大的开发团队使MySQL能够持续迭代和创新满足不断变化的用户需求。在MySQL 8中就有很多新特征。
今天我就给你介绍两个重要的新特征窗口函数和公用表表达式Common Table Expressions简称CTE。它们可以帮助我们用相对简单的查询语句实现更加强大的查询功能。
## 什么是窗口函数?
窗口函数的作用类似于在查询中对数据进行分组,不同的是,分组操作会把分组的结果聚合成一条记录,而窗口函数是将结果置于每一条数据记录中。一会儿我会借助一个例子来对比下,在此之前,你要先掌握窗口函数的语法结构。
窗口函数的语法结构是:
```
函数 OVER[PARTITION BY 字段]
```
或者是:
```
函数 OVER 窗口名 … WINDOW 窗口名 AS [PARTITION BY 字段名]
```
现在,我借助一个小例子来解释一下窗口函数的用法。
假设我现在有这样一个数据表,它显示了某购物网站在每个城市每个区的销售额:
```
mysql&gt; SELECT * FROM demo.test1;
+----+------+--------+------------+
| id | city | county | salesvalue |
+----+------+--------+------------+
| 1 | 北京 | 海淀 | 10.00 |
| 2 | 北京 | 朝阳 | 20.00 |
| 3 | 上海 | 黄埔 | 30.00 |
| 4 | 上海 | 长宁 | 10.00 |
+----+------+--------+------------+
4 rows in set (0.00 sec)
```
现在我想计算一下,这个网站在每个城市的销售总额、在全国的销售总额、每个区的销售额占所在城市销售额中的比率,以及占总销售额中的比率。
如果用分组和聚合函数,就需要分好几步来计算。
第一步计算总销售金额并存入临时表demo.a
```
mysql&gt; CREATE TEMPORARY TABLE demo.a -- 创建临时表
-&gt; SELECT SUM(salesvalue) AS salesvalue -- 计算总计金额
-&gt; FROM demo.test1;
Query OK, 1 row affected (0.02 sec)
Records: 1 Duplicates: 0 Warnings: 0
```
我们查看一下临时表demo.a的内容来验证一下计算结果
```
mysql&gt; SELECT * FROM demo.a;
+------------+
| salesvalue |
+------------+
| 70.00 |
+------------+
1 row in set (0.00 sec)
```
结果显示总计金额已经存入临时表demo.a中了。
第二步计算每个城市的销售总额并存入临时表demo.b
```
mysql&gt; CREATE TEMPORARY TABLE demo.b -- 创建临时表
-&gt; SELECT city,SUM(salesvalue) AS salesvalue -- 计算城市销售合计
-&gt; FROM demo.test1
-&gt; GROUP BY city;
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0
```
我们查看一下临时表demo.b的内容验证一下计算的结果
```
mysql&gt; SELECT * FROM demo.b;
+------+------------+
| city | salesvalue |
+------+------------+
| 北京 | 30.00 |
| 上海 | 40.00 |
+------+------------+
2 rows in set (0.00 sec)
```
结果显示,每个城市的销售总计金额已经计算成功了。
第三步,计算各区的销售占所在城市的总计金额的比例,和占全部销售总计金额的比例。我们可以通过下面的连接查询获得需要的结果:
```
mysql&gt; SELECT a.city AS 城市,a.county AS 区,a.salesvalue AS 区销售额,
-&gt; b.salesvalue AS 市销售额,a.salesvalue/b.salesvalue AS 市比率,
-&gt; c.salesvalue AS 总销售额,a.salesvalue/c.salesvalue AS 总比率
-&gt; FROM demo.test1 AS a
-&gt; JOIN demo.b AS b ON (a.city=b.city) -- 连接市统计结果临时表
-&gt; JOIN demo.a AS c -- 连接总计金额临时表
-&gt; ORDER BY a.city,a.county;
+------+------+----------+----------+----------+----------+----------+
| 城市 | 区 | 区销售额 | 市销售额 | 市比率 | 总销售额 | 总比率 |
+------+------+----------+----------+----------+----------+----------+
| 上海 | 长宁 | 10.00 | 40.00 | 0.250000 | 70.00 | 0.142857 |
| 上海 | 黄埔 | 30.00 | 40.00 | 0.750000 | 70.00 | 0.428571 |
| 北京 | 朝阳 | 20.00 | 30.00 | 0.666667 | 70.00 | 0.285714 |
| 北京 | 海淀 | 10.00 | 30.00 | 0.333333 | 70.00 | 0.142857 |
+------+------+----------+----------+----------+----------+----------+
4 rows in set (0.01 sec)
```
结果显示:市销售金额、市销售占比、总销售金额、总销售占比都计算出来了。
同样的查询,如果用窗口函数,就简单多了。我们可以用下面的代码来实现:
```
mysql&gt; SELECT city AS 城市,county AS 区,salesvalue AS 区销售额,
-&gt; SUM(salesvalue) OVER(PARTITION BY city) AS 市销售额, -- 计算市销售额
-&gt; salesvalue/SUM(salesvalue) OVER(PARTITION BY city) AS 市比率,
-&gt; SUM(salesvalue) OVER() AS 总销售额, -- 计算总销售额
-&gt; salesvalue/SUM(salesvalue) OVER() AS 总比率
-&gt; FROM demo.test1
-&gt; ORDER BY city,county;
+------+------+----------+----------+----------+----------+----------+
| 城市 | 区 | 区销售额 | 市销售额 | 市比率 | 总销售额 | 总比率 |
+------+------+----------+----------+----------+----------+----------+
| 上海 | 长宁 | 10.00 | 40.00 | 0.250000 | 70.00 | 0.142857 |
| 上海 | 黄埔 | 30.00 | 40.00 | 0.750000 | 70.00 | 0.428571 |
| 北京 | 朝阳 | 20.00 | 30.00 | 0.666667 | 70.00 | 0.285714 |
| 北京 | 海淀 | 10.00 | 30.00 | 0.333333 | 70.00 | 0.142857 |
+------+------+----------+----------+----------+----------+----------+
4 rows in set (0.00 sec)
```
结果显示,我们得到了与上面那种查询同样的结果。
你看,使用窗口函数,我们只用了一步就完成了查询,过程简单多了。而且,由于没有用到临时表,执行的效率也更高了。很显然,**在这种需要用到分组统计的结果对每一条记录进行计算的场景下,使用窗口函数更好**。
除了可以进行分组统计窗口函数还有一些自己独有的函数可以对分组内的数据进行处理比较常用的就是排序函数RANK()、DENSE_RANK()和ROW_NUMBER()。
为了帮助你理解这几个函数的作用,我举个小例子。
假设我们有这样一张学生成绩表:
```
mysql&gt; SELECT * FROM demo.test2;
+----+---------+--------+
| id | student | points |
+----+---------+--------+
| 1 | 张三 | 89 |
| 2 | 李四 | 77 |
| 3 | 王五 | 88 |
| 4 | 赵六 | 90 |
| 5 | 孙七 | 90 |
| 6 | 周八 | 88 |
+----+---------+--------+
6 rows in set (0.00 sec)
```
如果我们需要对表中的数据进行排序,就可以使用排序函数,代码如下所示:
```
mysql&gt; SELECT student,points,
-&gt; RANK() OVER w AS 排序1,
-&gt; DENSE_RANK() OVER w AS 排序2,
-&gt; ROW_NUMBER() OVER w AS 排序3
-&gt; FROM demo.test2
-&gt; WINDOW w AS (ORDER BY points DESC);
+---------+--------+-------+-------+-------+
| student | points | 排序1 | 排序2 | 排序3 |
+---------+--------+-------+-------+-------+
| 赵六 | 90 | 1 | 1 | 1 |
| 孙七 | 90 | 1 | 1 | 2 |
| 张三 | 89 | 3 | 2 | 3 |
| 王五 | 88 | 4 | 3 | 4 |
| 周八 | 88 | 4 | 3 | 5 |
| 李四 | 77 | 6 | 4 | 6 |
+---------+--------+-------+-------+-------+
6 rows in set (0.01 sec)
```
结果显示:
- RANK()函数把并列计算在内,并且并列影响排位;
- DENSE_RANK()函数也计算并列,但是并列不影响排位;
- ROW_NUMBER()函数不计算并列,只是简单排序。
因此我们就可以根据这些函数的特点计算分组中的排位信息。如果不计算并列就用ROW_NUMBER()函数计算并列但不占用位次就用DENSE_RANK()函数计算并列且占用位次就用RANK()函数。
接下来我们再来学习MySQL 8的另一个重要新特征公用表表达式。
## 什么是公用表表达式?
公用表表达式是一个命名的临时结果集。它存在于单个查询语句中,主要作用就是可以代替子查询,并且可以被后面的查询多次引用。
依据语法结构和执行方式的不同公用表表达式分为普通公用表表达式和递归公用表表达式2种。
### 什么是普通公用表表达式?
普通公用表表达式的语法结构是:
```
WITH
CTE名称 AS (子查询)
SELECT|DELETE|UPDATE 语句;
```
普通公用表表达式类似于子查询,不过,跟子查询不同的是,它可以被多次引用,而且可以被其他的普通公用表表达式所引用。
举个小例子假设我们有一个商品信息表demo.goodsmaster它保存的是商品信息还有一个每日销售统计表demo.dailystatistics保存的是每日的销售统计信息。现在超市经营者想要查出都卖了什么商品我们就可以先通过子查询查出所有销售过的商品的商品编号然后查出这些商品的商品信息代码如下所示
```
mysql&gt; SELECT * FROM demo.goodsmaster
-&gt; WHERE itemnumber IN
-&gt; (SELECT DISTINCT itemnumber -- 子查询,查出所有销售过的商品的商品编号
-&gt; FROM demo.dailystatistics);
+------------+---------+-----------+---------------+------+------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice |
+------------+---------+-----------+---------------+------+------------+
| 1 | 0001 | 书 | 16开 | 本 | 89.00 |
| 2 | 0002 | 笔 | 黑色 | 支 | 3.00 |
| 3 | 0003 | 胶水 | 无机 | 瓶 | 15.00 |
+------------+---------+-----------+---------------+------+------------+
3 rows in set (0.01 sec)
```
这个查询也可以用普通公用表表达式的方式完成:
```
mysql&gt; WITH
-&gt; cte AS (SELECT DISTINCT itemnumber FROM demo.dailystatistics)
-&gt; SELECT *
-&gt; FROM demo.goodsmaster a JOIN cte
-&gt; ON (a.itemnumber = cte.itemnumber);
+------------+---------+-----------+---------------+------+------------+------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice | itemnumber |
+------------+---------+-----------+---------------+------+------------+------------+
| 1 | 0001 | 书 | 16开 | 本 | 89.00 | 1 |
| 2 | 0002 | 笔 | 黑色 | 支 | 3.00 | 2 |
| 3 | 0003 | 胶水 | 无机 | 瓶 | 15.00 | 3 |
+------------+---------+-----------+---------------+------+------------+------------+
3 rows in set (0.00 sec)
```
可以看到,普通公用表表达式代替了第一种查询方式中的子查询,并且得到了同样的结果。
这个例子说明,公用表表达式可以起到子查询的作用。以后如果遇到需要使用子查询的场景,你可以在查询之前,先定义公用表表达式,然后在查询中用它来代替子查询。而且,跟子查询相比,公用表表达式有一个优点,就是定义过公用表表达式之后的查询,可以像一个表一样多次引用公用表表达式,而子查询则不能。
好了,我们再来学习下递归公用表表达式。
### 什么是递归公用表表达式?
递归公用表表达式也是一种公用表表达式,只不过,除了普通公用表表达式的特点以外,它还有自己的特点,就是**可以调用自己**。它的语法结构是:
```
WITH RECURSIVE
CTE名称 AS (子查询)
SELECT|DELETE|UPDATE 语句;
```
递归公用表表达式由2部分组成分别是种子查询和递归查询中间通过关键字UION [ALL]进行连接。这里的**种子查询,意思就是获得递归的初始值**。这个查询只会运行一次,以创建初始数据集,之后递归查询会一直执行,直到没有任何新的查询数据产生,递归返回。
同样,为了帮助你理解递归公用表表达式的工作原理,我来举个小例子。
假设我们有这样一张人员信息表demo.teach里面包含人员编号、名称和老师编号。
<img src="https://static001.geekbang.org/resource/image/a0/ba/a033830c05999ed5c3b5aa2f56710aba.jpeg" alt="">
如果甲是乙的老师,那么,我们可以把乙叫做甲的徒子,如果同时乙又是丙的老师,那么丙就是乙的徒子,是甲的徒孙。
下面我们尝试用查询语句列出所有具有徒孙身份的人员信息。
如果用我们之前学过的知识来解决会比较复杂至少要进行4次查询才能搞定
- 第一步,先找出初代老师,就是不以任何人为老师的人,把结果存入临时表;
- 第二步,找出所有以初代老师为师的人,得到一个徒子集,把结果存入临时表;
- 第三步,找出所有以徒子为师的人,得到一个徒孙集,把结果存入临时表。
- 第四步,找出所有以徒孙为师的人,得到一个结果集。
如果第四步的结果集为空,则计算结束,第三步的结果集就是我们需要的徒孙集了,否则就必须继续进行第四步,一直到结果集为空为止。比如上面的这个数据表,就需要到第五步,才能得到空结果集。而且,最后还要进行第六步:把第三步和第四步的结果集合并,这样才能最终获得我们需要的结果集。
如果用递归公用表表达式,就非常简单了。我介绍下具体的思路。
- 用递归公用表表达式中的种子查询找出初代老师。字段n表示代次初始值为1表示是第一代老师。
- 用递归公用表表达式中的递归查询查出以这个递归公用表表达式中的人为老师的人并且代次的值加1。直到没有人以这个递归公用表表达式中的人为老师了递归返回。
- 在最后的查询中选出所有代次大于等于3的人他们肯定是第三代及以上代次的学生了也就是徒孙了。这样就得到了我们需要的结果集。
这里看似也是3步实际上是一个查询的3个部分只需要执行一次就可以了。而且也不需要用临时表保存中间结果比刚刚的方法简单多了。
下面是具体的代码:
```
mysql&gt; WITH RECURSIVE
-&gt; cte AS (
-&gt; SELECT id,fname,teacherid,1 AS n FROM demo.teach WHERE id = 101 -- 种子查询,找到第一代老师
-&gt; UNION ALL
-&gt; SELECT a.id,a.fname,a.teacherid,n+1 FROM demo.teach AS a JOIN cte
-&gt; ON (a.teacherid = cte.id) -- 递归查询,找出以递归公用表表达式的人为老师的人
-&gt; )
-&gt; SELECT id,fname FROM cte WHERE n&gt;=3; -- 从结果集中筛选代次大于等于3的得到所有徒孙集
+------+-------+
| id | fname |
+------+-------+
| 103 | 王五 |
| 105 | 孙七 |
| 106 | 周八 |
+------+-------+
3 rows in set (0.00 sec)
```
结果显示,王五、孙七和周八是徒孙。结果显然是正确的。
总之,递归公用表表达式对于查询一个有共同的根节点的树形结构数据,非常有用。它可以不受层级的限制,轻松查出所有节点的数据。如果用其他的查询方式,就比较复杂了。
## 总结
这节课我们学习了MySQL 8的2个重要新功能窗口函数和公用表表达式。
窗口函数的特点是可以分组,而且可以在分组内排序。另外,窗口函数不会因为分组而减少原表中的行数,这对我们在原表数据的基础上进行统计和排序非常有用。
公用表表达式的作用是可以替代子查询,而且可以被多次引用。递归公用表表达式对查询有一个共同根节点的树形结构数据非常高效,可以轻松搞定其他查询方式难以处理的查询。
当然除了今天学习的窗口函数和公用表表达式MySQL 8还有许多其他的新特征比如完善了对空间位置信息的处理支持对表的DDL操作创建、修改和删除表的原子性使得`CREATE TABLE ...SELECT`语句能够成为一个原子操作,提高了数据安全性,等等。
如果你想要从旧版本切换到MySQL 8课下可以点击这个[链接](https://dev.mysql.com/doc/refman/8.0/en/mysql-nutshell.html)进一步了解一下。
## 思考题
假设我有一个会员销售统计表demo.memtrans其中包括会员名称、商品名称和销售金额具体数据如下
<img src="https://static001.geekbang.org/resource/image/f8/53/f8d3900a108cff5680e5c7b9d9ff0e53.jpeg" alt="">
请使用窗口函数查询会员名称、商品名称、销售金额、总计金额和销售占比。
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,176 @@
<audio id="audio" title="特别放送(二)| 经典面试题讲解第二弹" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7f/bc/7f9585f59083444c3c3c044c7210ebbc.mp3"></audio>
你好,我是朱晓峰。
到这里“进阶篇”的内容咱们就学完了。今天我给你准备了7道面试题。这些面试题涵盖了这个模块的核心内容我们一起借助面试题来复习一下。我不仅会给你提供正确答案还会带你深入分析这些问题让你真正能够做到举一反三。
好了,话不多说,我们现在开始。
## 第一题
**日志文件对数据库的故障恢复至关重要。下面这些关于日志文件的描述,正确的是:**
1. MySQL日志记录的顺序可以与并发操作的执行顺序不一致
1. 为了确保数据库是可恢复的,必须确保先写数据库后写日志
1. 日志文件中检查点的主要作用是提高系统出现故障后恢复的效率
1. 系统故障恢复必须使用日志文件以保证数据库系统重启时能正常恢复,事务故障恢复不一定需要使用日志文件
答案选项3。
解析:
选项1是错误的。MySQL日志记录的顺序是严格按照操作执行的顺序记录的不会出现日志记录的顺序与并发操作执行的顺序不一致的情况。
选项2也是错误的。MySQL的日志系统遵循WALWrite-Ahead Logging机制也就是所谓的先写日志后写数据库的机制。由于记录日志是顺序写入磁盘而写入数据库的磁盘操作需要对磁头定位因而写入日志的速度要远比写入数据快。为了提高执行的效率MySQL执行的是先写日志日志写入成功之后就回复客户端操作成功对数据库的磁盘写入则在之后的某个阶段执行。这样即便遇到系统故障由于有了日志记录就可以通过日志对数据库进行恢复了。WAL机制包括3个规则
- 对数据的修改操作必须在写入数据库之前写入到日志文件中;
- 日志必须严格按序记录就是说如果A操作发生在B操作之前那么在日志中A操作的记录必须在B操作的记录之前
- 事务提交之后,必须在日志写入成功之后,才能回复事务处理成功。
选项3是正确的检查点的作用是加快数据恢复的效率。当修改数据时为了提高存取效率采用的策略是先记录日志实际的数据修改则发生在内存中这些数据修改是没有写入数据库的叫做脏页。MySQL把这些脏页分成小的批次逐步写入磁盘中。因为如果把所有的脏页都一次性写入磁盘会导致磁盘写入时间过长影响到用户SQL操作的执行。检查点的作用就是标记哪些脏页已经写入磁盘。这样当遇到故障时MySQL可以从检查点的位置开始执行日志记录的SQL操作而不是把整个日志都检查一遍所以大大提高了故障恢复的效率。
选项4也是错误的。系统故障恢复必须使用日志文件以保证数据库系统重启时能正常恢复这个表述是正确的但后面的表述“事务故障恢复不一定需要使用日志文件”则是错误的。事务故障的恢复也必须要用到日志文件事务故障恢复需要用到的日志文件有3个分别是回滚日志、重做日志和二进制日志。
- 回滚日志:如果事务发生故障,可以借助回滚日志,恢复到故障前的状态,所以回滚日志对事务故障恢复有用。
- 重做日志:事务中的操作对数据更新进行提交时,会记录到重做日志,对数据的更新则只会发生在内存中,实际的数据更新写入磁盘,则会由后台的其他进程异步执行。如果这个时候事务故障,内存中的数据丢失,就必须要借助重做日志来找回。所以,重做日志对事务故障恢复有用。
- 二进制日志:在主从服务器架构的模式下,从服务器完全依赖二进制日志同步主服务器的操作,如果事务发生故障,从服务器只能依靠主服务器的二进制日志恢复。
## 第二题
**MySQL支持事务处理吗**
参考答案:
这个问题跟数据表用的是什么存储引擎有关。如果用的是Innodb则支持事务处理如果用的是MyISAM则不支持事务处理。
MySQL 8.0 默认的存储引擎是InnodbInnodb是支持事务处理的。在默认的情况下MySQL启用AUTOCOMMIT模式也就是每一个SQL操作都是一个事务操作如果操作执行没有返回错误MySQL会提交事务如果操作返回错误MySQL会执行事务回滚。
你也可以通过执行“START TRANSACTION”或者“BEGIN”来开始一个事务这种情况下就可以在事务处理中包含多个SQL操作语句一直到“COMMIT”语句提交事务或者是“ROLLBACK”语句回滚事务来结束一个事务操作。
MyISAM存储引擎是不支持事务操作的如果你对一个存储引擎是MyISAM的数据表执行事务操作不管你是否执行“COMMIT”或者是“ROLLBACK”都不会影响操作的结果。你可以通过下面的SQL语句来查看表的存储引擎
```
mysql&gt; SHOW CREATE TABLE demo.test;
+-------+------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+-------+------------------------------------------------------------------------------------------------------------------+
| test | CREATE TABLE `test` (
`aa` int DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |
+-------+------------------------------------------------------------------------------------------------------------------+
1 row in set (0.02 sec)
```
## 第三题
**下面这些关于MySQL视图的描述中错误的是**
1. 视图是MySQL提供给用户以多种角度观察数据库中数据的重要机制
1. 视图可以对重构数据库提供一定程度的逻辑独立性
1. 所有视图都是可查询和可更新的
1. 对视图的一切操作都会转换成对数据表的操作
答案选项3。
解析:
选项1的描述是正确的。视图其实就是存储在服务器中的查询语句可以通过编写不同的查询语句从数据库中抽取需要的数据。也就相当于从多种角度观察数据库中的数据。
选项2的描述是正确的。视图通过查询语句从数据库中抽取需要的数据如果遇到数据表结构的变更可以通过修改查询语句使视图返回的结果集中的字段保持不变。因此视图可以一定程度不受实际数据表结构的变化的影响具备一定的逻辑独立性。
选项3的描述是错误的所有的视图都可以查询但不是所有的视图都可以更新。比如定义视图的查询语句如果包含分组由于无法对实际数据表中的记录定位所以这样的视图是不支持数据更新操作的。
选项4的描述也是正确的。视图不过是一段查询语句视图并不保存实际数据对视图的任何操作最终都会转换到实际数据表。
## 第四题
**下面关于存储过程的描述中,不正确的是:**
1. 存储过程实际上是一组SQL语句
1. 存储过程预先被编译存放在数据库服务器上
1. 存储过程与数据库无关
1. 存储过程可以完成某一特定的业务逻辑
答案选项3。
解析:
选项1的描述正确存储过程的程序体就是一组SQL语句调用存储过程实际上就是执行了一组SQL操作。
选项2的描述正确存储过程创建完成之后就是经过了编译的过程以服务可以执行的形式存放在服务器中。
选项3的描述是错误的存储过程是数据库的一部分不能独立于数据库而存在。
选项4的描述是正确的创建存储过程的目的就是为了高效地完成某一特定的业务逻辑。
## 第五题
在MySQL中有学生表学号姓名年龄所在系其中学号是主键。在这个表上建有视图V1V1的定义语句是
```
CREATE VIEW V1 AS SELECT 姓名,年龄,所在系
FROM 学生表 WHERE 年龄&gt;=(SELECT AVG(年龄) FROM 学生表);
```
有下列操作语句:
```
A.UPDATE V1 SET 年龄=年龄-1;
B.SELECT * FROM V1 WHERE 年龄&gt;20;
C.DELETE FROM V1 WHERE 年龄&gt;20;
D.INSERT INTO V1 VALUES (‘张三’,20,’计算机系’);
```
以上语句能够正确执行的是:
1. A、B和D
1. 仅B
1. 仅B和C
1. 仅A、B和C
答案选项2。
解析这道题考核的要点是如果定义视图的SQL语句中包含了聚合函数就不能对视图中的数据进行DML操作。
## 第六题
**什么是游标?如何知道游标已经走到了最后?**
参考答案:
由SELECT语句返回的完整行集包括满足WHERE语句中条件的所有行称为结果集。MySQL中的SQL操作会对整个结果集起作用。应用程序特别是交互式的联机应用程序并不总能将整个结果集作为一个单元来进行有效的处理这些应用程序需要一种机制以便每次处理一行或者一部分行。游标就是提供这种机制的是对结果集的一种扩展。
具体而言MySQL的游标有以下特点
- 可以逐条查看数据集中的记录;
- 一次只能查看一条记录;
- 只能向一个方向扫描数据,并且不能跳跃;
- 游标是只读的。
可以使用条件处理语句“DECLARE CONTINUE HANDLER FOR NOT FOUND”来判断游标到达了结果集的最后。
## 第七题
下面关于触发器的叙述中,错误的是:
1. 触发器是一种特殊的存储程序
1. 触发器必须创建在一个特殊的表中
1. 触发器通过CALL调用
1. 触发器有助于实现数据库的完整性、安全性和主动性
答案选项3。
解析:
触发器也是存储在服务器端是一种存储程序触发器监控的对象是一个特定的表触发器可以记录日志、进行安全性检查并且无需应用程序的控制可以由数据操作的事件驱动所以选项1、2、4都是正确的。
触发器不能由CALL调用而是由事件驱动所以3是错误的。
## 总结
今天,我们借助几个面试题,回顾了日志、游标、视图、存储过程和触发器的相关内容。如果你发现回答这些试题有困难,一定要及时回去复习一下。
刷题不是目的,真正的目的是对你掌握知识的熟练程度进行检验,发现问题,查漏补缺。只有深入了解相关的知识背景、工具的运行机制和原理,才能牢固掌握,在实践中灵活运用。

View File

@@ -0,0 +1,246 @@
<audio id="audio" title="特别放送(四)| 位置信息:如何进行空间定位?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/75/db/7584e459a5404f7b86e93147e08273db.mp3"></audio>
你好,我是朱晓峰。今天,我来和你聊一聊怎么进行空间定位。
我们每天都会用到空间数据比如你在网上购买一件商品你手机上的App就能够算出你是不是需要负担运费负担多少运费。这其实就是因为手机获取到了你的空间位置信息发送到网购平台然后根据你所在的位置是否属于偏远地区来决定你是否需要负担运费如果需要的话应该负担多少。
而从应用开发者的角度出发,我们需要知道怎么进行空间定位,获取用户的空间位置信息,以及如何计算发货点与客户地址的距离,这些都要借助与空间数据相关的技术才能解决。
今天,我还是借助一个真实的项目,来给你介绍下空间数据类型、空间数据处理函数,以及如何为空间数据创建索引,帮助你提升开发基于空间数据应用的能力。
在我们超市项目的实施过程中超市经营者提出了这样一个要求要给距离门店5公里范围内的、从线上下单的客户提供送货上门的服务。要想解决这个问题就需要用到空间数据了。
## 空间数据类型与空间函数
我先给你介绍下空间数据类型和空间函数。
MySQL支持的空间数据类型分为2类
- 一类是包含单个值的几何类型GEOMETRY、点类型POINT、线类型LINESTRINIG和多边形类型POLYGON
- 另一类是包含多个值的多点类型MULTIPOINT、多线类型MULTILINESTRING、多多边形类型MULTIPOLYGON和几何集类型GEOMETRYCOLLECTION
我简单说明一下这几种空间数据类型的特点。
几何类型是一个通用的空间数据类型,你可以把点类型、线类型和多边形类型数据的值赋予几何类型数据。但是点类型、线类型和多边形类型数据则不具备这种通用性,你只能赋予它们各自类型数据的值。
几何集类型数据可以保存点类型数据、线类型数据和多边形类型数据值的集合。多点类型、多线类型和多多边形类型则分别只能保存点类型数据、线类型数据和多边形类型数据值的集合。
下面我们重点介绍一下点类型,因为这种类型是最简单、最基础的空间类型,也最常用。
### 点类型POINT
点类型是最简单的空间数据类型,代表了坐标空间中的单个位置。在不同比例尺的坐标空间中,一个点可以有不同的含义。例如,在较大比例尺的世界地图中,一个点可能代表一座城市;而在较小比例尺的城市地图中,一个点可能只代表一个车站。
点类型数据的属性有2种
- 坐标空间中的X轴的值比如地理坐标中的经度值
- 坐标空间中的Y轴的值比如地理坐标中的纬度值
点类型数据的维度是0边界为空。
### 空间函数
MySQL支持的空间函数有一百多种我们没有必要全部都掌握。所以我给你重点介绍几个比较常用的空间函数ST_Distance_Sphere()、MBRContains()、MBRWithin()和ST_GeomFromText()。
**1.ST_Distance_Sphere()函数**
我们先从计算空间距离的函数ST_Distance_Sphere()说起,这个函数的语法结构和功能如下所示:
- ST_Distance_Sphere(g1,g2)g1与g2为2个点函数返回球体上2个点g1与g2之间的最小球面距离。
**2.MBRContains()和MBRWithin()函数**
在学习MBRContains()和MBRWithin()函数之前我们要先了解一个概念也就是最小边界矩形MBRMinimum Bounding Rectangle )。
最小边界矩形是指以二维坐标表示的若干二维形状(例如点、直线、多边形)的最大范围,即以给定的二维形状各顶点中的最大横坐标、最小横坐标、最大纵坐标、最小纵坐标决定的边界的矩形。
知道了这个概念,你就能更好地理解这两个函数了。
- MBRContains(g1,g2)如果几何图形g1的最小边界矩形包含了几何图形g2的最小边界矩形则返回1否则返回0。
- MBRWithin(g1,g2)与MBRContains(g1,g2)函数正好相反MBRWithin(g1,g2)表示如果几何图形g1的最小边界矩形包含在几何图形g2的最小边界矩形之内则返回1否则返回0。
**3.ST_GeomFromText()**
这个函数的作用是通过WKT形式创建几何图形。而ST_GeomFromText(WKT,SRID)就表示返回用WKT形式和SRID指定的参照系表达的几何图形。
这里的WKT是一种文本标记语言用来表示几何对象。SRIDSpatial Reference Identifier是空间参照标识符默认是0表示平面坐标系。我们平时常用的SRID是4326是目前世界通用的以地球质心为原点的地心坐标系。
知道了这些基础知识,我们就可以着手解决超市经营者提出的需求了。
这家超市有很多门店,该怎么计算是否应该送货上门呢?如果应该送货上门,应该从哪家门店送货呢?我带你分析下具体的思路。
- 第一步,把门店的位置信息录入数据表中;
- 第二步,根据下单客户的送货地址,获取到地理位置信息;
- 第三步计算各门店位置与送货地址的距离找出最近的门店安排送货如果没有一家门店与客户的距离在5公里以内则提示不能送货。
下面我们就来实际操作一下。
首先我们创建一个门店表demo.mybranch包含门店编号、名称、位置等信息。
```
mysql&gt; CREATE TABLE demo.mybranch
-&gt; (
-&gt; branchid SMALLINT PRIMARY KEY,
-&gt; branchname VARCHAR(50) NOT NULL,
-&gt; address GEOMETRY NOT NULL SRID 4326
-&gt; );
Query OK, 0 rows affected (0.07 sec)
```
这里需要注意一下我这里的address字段定义的空间数据类型是GEOMETRYSRID是4326。因为GEOMETRY类型比较通用可以赋予任何类型的空间数据值而且方便后面创建索引。SRID值为4326表示采用地心坐标系这样计算出来的距离才比较准确。当然你完全可以使用空间数据类型POINT也能达到同样的效果。
现在,我们把门店位置信息录入表中:
```
mysql&gt; INSERT INTO demo.mybranch VALUES
-&gt; (1,'西直门店',ST_GeomFromText('POINT(39.938099 116.350266)', 4326)), -- 西直门店的经纬度信息
-&gt; (2,'东直门店',ST_GeomFromText('POINT(39.941143 116.433769)', 4326)),
-&gt; (3,'崇文门店',ST_GeomFromText('POINT(39.896877 116.416977)', 4326)),
-&gt; (4,'五道口店',ST_GeomFromText('POINT(39.9921 116.34584)', 4326)),
-&gt; (5,'清河店',ST_GeomFromText('POINT(39.743378 116.332878)', 4326));
Query OK, 5 rows affected (0.03 sec)
Records: 5 Duplicates: 0 Warnings: 0
```
结果显示数据插入成功了。这里有2个问题需要你注意。
第一我是用门店的经度和纬度值来表示门店的地理位置。要获得门店的地理位置你可以通过地图数据获得但是这样做成本比较高。还有一种办法就是通过大厂提供的免费的API接口获取比如百度地图API这样比较简单。
第二WKT格式表达一个点的时候在关键字POINT后面的括号中要先写这个点的纬度后写这个点的经度。这与一般的习惯相反不要搞错。而且经度值与纬度值之间用空格隔开而不是用逗号。
准备好各门店的位置信息之后,我们就可以通过空间函数来计算距离了。
假设我们获取到客户所在位置的地理坐标为纬度是39.994671经度是116.330788那么我们就可以通过下面的SQL语句查询到这个位置与各个门店的距离
```
mysql&gt; SELECT branchid,branchname,st_distance_sphere(ST_GeomFromText('POINT(39.994671 116.330788)',4326),address) AS distance
-&gt; FROM demo.mybranch;
+----------+------------+--------------------+
| branchid | branchname | distance |
+----------+------------+--------------------+
| 1 | 西直门店 | 6505.859589677078 |
| 2 | 东直门店 | 10604.07854447186 |
| 3 | 崇文门店 | 13123.76779555601 |
| 4 | 五道口店 | 1313.741752971374 |
| 5 | 清河店 | 27943.114458834025 |
+----------+------------+--------------------+
5 rows in set (0.00 sec)
```
结果显示,所有门店与客户位置之间的距离,都已经计算出来了。
需要注意的是这个结果中查出来的距离是以米为单位的。根据这个查询的结果五道口店的球面最短距离只有1313米也就是1.3公里满足送货上门的条件。其他门店的最短距离都在5公里以上。因此应该从五道口店送货上门。到这里超市经营者的要求就得到了满足。
好了到这里我们已经知道了如何定位一个空间位置以及如何计算2个位置之间的距离。接下来我们就再来了解下如何通过创建索引来提升空间数据的查询效率。
## 用空间数据创建索引
对于空间数据的查询一般分为2种一种是查询包括一个点的空间对象另外一种是查询与某一个区域有交集的空间对象。
为了提高查询的速度就可以用空间数据字段创建空间索引。MySQL支持使用InnoDB存储引擎或者是MyISAM存储引擎的数据表创建空间索引。
我们有三种创建空间索引的方式。
第一,我们可以在创建数据表时创建空间索引,语法结构是:
```
CREATE TABLE 表名 (字段名 GEOMETY NOT NULL SRID 4326SPATIAL INDEX(空间数据字段名));
```
第二种是在修改表时创建空间索引,语法结构是:
```
ALTER TABLE 表名 ADD SPATIAL INDEX (空间数据字段名);
```
第三种是单独创建空间索引,语法结构是:
```
CREATE SPATIAL INDEX 索引名 ON 表名(空间数据字段名);
```
这里要提醒你注意的是空间索引与普通索引不同必须要用关键字SPATIAL而且创建空间索引的空间数据字段不能为空。空间索引创建一个R树索引支持区域扫描对提升空间数据查询的效率很有帮助。
我还是以刚才的超市门店位置数据为例,来简单说明一下如何用空间类型字段创建空间索引。我我们先用下面的代码,单独创建一下空间索引:
```
mysql&gt; CREATE SPATIAL INDEX index_address ON demo.mybranch(address);
Query OK, 0 rows affected, 1 warning (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 1
```
结果显示,创建成功了。现在我们来确认一下,刚才创建的空间索引能不能起到优化查询的作用。
在MySQL中只有在WHERE条件筛选语句中包含类似MBRContains()和MBRWithin()这样的函数,空间索引才会起作用。
现在,我们来借助一个小例子验证一下,我们创建的空间索引能不能对空间数据的查询起到优化的作用。
假设我们创建了一个多边形的地理区域,代码如下所示:
```
mysql&gt; SET @poly =
-&gt; 'Polygon((
'&gt; 40.016712 116.319618,
'&gt; 40.016712 116.412773,
'&gt; 39.907024 116.412773,
'&gt; 39.907024 116.319618,
'&gt; 40.016712 116.319618))';
Query OK, 0 rows affected (0.00 sec)
```
这里有个坑你一定要注意多边形的区域起点和终点一定要一致否则就不是一个封闭的区域MySQL就会提示非法的地理位置数据。
然后,我们查询下有多少门店在这个区域中。你可以用下面的代码来实现:
```
mysql&gt; SELECT branchid,branchname FROM demo.mybranch
-&gt; WHERE MBRContains(ST_GeomFromText(@poly,4326),address);
+----------+------------+
| branchid | branchname |
+----------+------------+
| 1 | 西直门店 |
| 4 | 五道口店 |
+----------+------------+
2 rows in set (0.00 sec)
```
结果显示有2个门店在这个地理区域范围内。下面我们用查询分析语句来分析一下这个查询看看有没有用到空间索引
```
mysql&gt; EXPLAIN SELECT * FROM demo.mybranch
-&gt; WHERE MBRContains(ST_GeomFromText(@poly,4326),address);
+----+-------------+----------+------------+-------+---------------+---------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+---------------+---------+------+------+----------+-------------+
| 1 | SIMPLE | mybranch | NULL | range | index_address | index_address | 34 | NULL | 5 | 100.00 | Using where |
+----+-------------+----------+------------+-------+---------------+---------------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
```
结果显示我们创建的索引起了作用MySQL优化器使用空间索引进行了区域扫描提高了查询的效率。
总之MySQL为空间数据提供了一套完整的解决方案。从空间数据类型到空间函数再到空间索引可以让我们像处理普通数据那样来存储、处理和查询空间数据。这样一来开发基于空间数据的应用就十分方便了。
## 总结
这节课我给你介绍了MySQL的空间数据包括空间数据类型POINT空间数据处理函数ST_Distance_Sphere()、MBRContains()、MBRWithin()和ST_GeomFromText(),以及创建空间索引的方法。
MySQL的空间数据是非常有用的数据类型通过各种空间数据处理函数可以开发出路径规划、线路导航、自动驾驶等各种应用。虽然现在还存在数据量大、查询效率比较低等问题但是通过不断使用新的技术比如空间索引中引入R树索引等进步是非常明显的。
如果你在实际工作中,需要开发基于空间数据的应用,课下可以再参考下[链接](https://dev.mysql.com/doc/refman/8.0/en/spatial-types.html)中的内容。
## 思考题
在这节课中我定义的门店表demo.mybranch地址的空间数据类型是几何类型GEOMETRY请你改用点类型POINT完成从创建表到查询最近门店的全部操作。
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,81 @@
<audio id="audio" title="结束语 | 不盲从于群体思维,走一条适合自己的路" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/eb/d2/ebc2c592b4eb5b180e66a5e2ef6181d2.mp3"></audio>
你好,我是朱晓峰。
到这里咱们的课程就全部结束了。不过课程结束更新并不意味着MySQL的学习结束了。相反也许对于你来说这是一个新的开始。
我一直强调在真实项目中进行学习是掌握MySQL的最好方法。所以在课程结束以后你一定要把学到的内容真正应用到实际工作中反复实践这样才能真正成为优秀的数据库开发人员。
那在今天这篇结束语里呢我想暂且抛开MySQL和你分享一个我的人生准则**不盲从于群体思维,按自己的意愿和个人的特质去做选择,走一条适合自己的路**。
为什么在这告别之际,我最想给你分享这一点呢?
因为我们每个人都生活在群体之中,每天都在和不同的人打交道,尤其是现在信息极度丰富,每天都有人在表达着自己的观点,试图成为“意见领袖”,一不小心,别人的想法就会左右我们的行为。
但每个个体都是与众不同的,有的人擅长把细节做到极致,有的人擅长把控全局;有的人对数字特别敏感,有的人长于艺术想象……无论是学习技术,还是选择职业路径,只有适合自己的,才是我们成为人生赢家的捷径。
说到这儿,我想和你分享一个我的真实经历。
大学毕业以后,我进入一家外企工作,从普通程序员做起,一直做到构架师。工作很稳定,待遇也好,家人觉得很不错,希望我买套房子,赶紧结婚生子。
可是,我对这样的生活一点也不满意。因为我发现,在熟悉了工作内容之后,日常的一切对我已经没有什么挑战了,每天不过是重复熟悉的流程,感觉一年跟一天没有什么差别。**这样的人生,不是我想要的**。
我决定辞职,去美国留学,这件事情花光了我多年的积蓄。当时,很多人都在质疑我,觉得我是放弃了光明的前途,走向了未知的恐怖。但是,未知并不意味着恐怖,甚至可能是出乎预料的精彩。
事实证明,那段经历彻底改变了我。我学到了崭新的知识和不同的思维方式,也正因此,我又有机会进入到世界顶级的企业工作,成为了摩根大通银行东京分行副总裁,保证了一系列重大项目的落地和实施。
试想一下,假使我当时听从别人的劝导,选择过安安稳稳的日子,现在可能还在公司里面打工,计算着还有多少年可以退休。而现在的我,在开拓职业新赛道,同时也有机会写下这门课。
其实,人类的一个特点就是很容易接受暗示,尤其是在你犹豫不决的时候,往往一个简单暗示,就会影响你的决定。所以,你一定要慎重对待群体建议,在做每一个重要的决定前,都要先想想这个决定是为自己而做,还是为迎合群体而做。
你可能会说我知道不要盲从群体思维但是怎么按照自己的意愿去生活呢接下来我就结合我的人生经历给你分享3条建议。
**首先,你要制定长期规划,形成自己的一个稳定的内在架构。**
**有句话叫做:“谋定而后动,知止而有得**。”如果做什么都没有规划,凡事跟着感觉走,是很难有作为的。而所谓的长期规划,我认为可以容纳进一套架构里。实际上,不只企业有架构,我们每个人都要形成自己的一个架构。
什么意思呢?我给你举个简单的小例子,你一听就明白了。
记得在银行工作的时候,有一次跟我们部门的一位传奇的交易员聊天,他每年为公司创造的利润超过你的想象。我问他怎么知道一笔交易能够盈利呢,他的回答出乎我的意料,他说:“我不知道这笔交易能不能赢,但是我的计算模型告诉我,我赢的可能性大于输的可能性。我一直这样做,只要交易足够多,我就不可能输。”
你看,我们眼中的一笔笔独立的交易,都是他长线规划中的一环。他实际上是预先做了计算的,一切都是按照计划进行的。虽然他不知道下一笔交易能不能赢,但按照计划行事,赢的可能性就比较大,只要时间足够长,交易量足够多,那么在最终的结果里,他肯定是赢家。
所以,要想走在自己期待的道路上,就要把视角放在更广阔的空间维度和更长久的时间维度上,建立起自己的一套架构,并不断用经验和经历去修正它,让它尽可能稳固,这样你才能在做决定时,有个指导,至少不会让你跑偏。
**其次,对待任何结果,不过多计较一时的得失。**
正确的决定,并不一定会带来好的结果;错误的决定,也可能会侥幸成功,重要的是客观地分析造成这个结果的原因,而不是简单地以结果的好坏,来判断决定的正确性。
我还是想分享一个我经历过的一件事。
记得云服务器刚开始流行起来的时候,因为我们觉得云服务器比自己搭建服务器,在可靠性、资源利用率和成本等方面都很有优势,于是,我们就给客户推荐了大厂的云服务器。可是,没想到大厂也有失手的时候,整个地区全部宕机,所有的客户都没法访问云服务器了。
这个时候,如果是你,你会怎么做呢?放弃云服务器,重新回归本地服务器,肯定会防止类似的事件再次发生,但问题是,这样的选择真的是正确的吗?
当时,云服务器确实比较新,大厂也可能会失手,但是,一次失败并不意味着云服务器就不能用了。从长远来说,云服务器比本地服务器的优势多太多了,大厂的技术也在不断成熟,给客户推荐云服务器肯定仍然是正确的选择。
实际上,任何事情都有成功和失败的概率。正确的选择是成功概率大的那个方案,不要因为一次偶然的失败,就轻易改变我们的选择。
总之,一定要学会不简单地以结果来评判对错,不妨问问自己,当初决定这样做的理由是什么?为什么会导致这个结果?是必然的呢?还是偶然因素造成的?哪个方案成功的概率最大?等等,在回答这些问题的过程中理清思路,努力做出全面客观的判断。
**最后,正确的要坚持,但如果发现错了,果断放弃非常重要。**
几乎所有人都在强调,做事要坚持不懈,这当然是非常重要的,毕竟不能忽视时间的力量。但是,如果你已经意识到方向错了,就一定要果断放弃。
我曾经非常迷信某大厂的软件开发生态,觉得它们能够提供全套的开发环境,稳定性好,特别人性化。自以为站在巨人的肩膀上,就可以借到力了。可是有一次,我们给客户定制的软件在客户的机器上无法启动,由于大厂的开发环境都是封闭的,我们无法了解内部到底发生了什么。这样一来,问题就一直得不到解决。最后,我们不得不回到几个月前的版本,重新编写相应的模块。
这件事使我意识到,要开发出真正满足客户需求的软件,必须要了解底层,开源的开发环境,才是最佳的选择。因为开源软件可以通过阅读源代码,了解所有的技术细节。
这个时候,我意识到,如果明知现在的方向不对,还要一直走下去,虽然比较省力,但是很多项目就不能做了,那样就会把路越走越窄。
最后,我们果断放弃了某大厂的软件,逐步切换到开源体系。刚开始的时候,因为要熟悉新的操作系统、新的编程语言、新的数据库软件,要学的东西太多,每一步都十分痛苦,很多人离开了。
但是,随着对开源体系越来越熟悉,发现原来的项目可以做,甚至很多原来不能做的项目也可以做了,这样一来,路就越走越宽了。
放弃正在走的错误的道路就意味着改变,而人十分容易产生惰性,一旦适应了现在的环境,就不愿意改变,这样就很容易忽视正确的东西。**微小积累会引发持续改变这件事,不仅会在正确的地方得到验证,更会在错误的道路上体现得淋漓尽致。因此,在错误处调头,虽难,但意义重大。**
好了,这些就是我在这几十年中,踩过无数坑、走了无数弯路后提炼出来的生活准则,很高兴有机会分享给你,但愿对你有所帮助。希望你不仅成为一个具有很强技术能力的人,同时还能真正走出一条适合自己的路,成为人生赢家。
课程的最后,我准备了一份[调研问卷](https://jinshuju.net/f/H556x1)希望你能花1分钟填写下聊聊你对这门课的想法。同时我也给你准备了礼物只要填写问卷就有机会获得一个手绘护腕垫或者是价值99元的课程阅码。期待你的畅所欲言。
[<img src="https://static001.geekbang.org/resource/image/2c/34/2c9d5fc92a31e86a25f68bab00dc4734.jpg" alt="">](https://jinshuju.net/f/H556x1)

View File

@@ -0,0 +1,73 @@
<audio id="audio" title="开篇词 | 在实战中学习是解锁MySQL技能的最佳方法" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bf/0b/bfa0368abe182886fe2476911033390b.mp3"></audio>
你好,我是朱晓峰。
工作二十多年来我一直在和MySQL打交道。我曾任摩根大通银行技术部副总裁带领团队为纽约、东京等分支银行提供数据存储和安全服务。目前正致力于开发基于MySQL的管理信息系统率领团队为包括国家开发银行、百度在线、北京西站等大型企业在内的客户提供了信息服务并获得了11项软件著作权比如商业数据管理系统、云POS系统等
因为具备丰富的MySQL开发经验从2015年起我受聘担任数据应用学院客座讲师开始制作职业技术培训课程。我和团队开发了一个为期2周、30个课时的MySQL入门集训课。我们打破了传统的教学模式不去讲零碎的知识点而是借助一个实际项目去讲必备技能帮助数百名初学者迅速掌握了MySQL的基本操作和核心技能。经过2周的集中培训有几十位学生顺利进入谷歌、苹果和亚马逊等公司。
多年的项目开发以及培训经历让我深刻地认识到熟练使用MySQL对技术人来说变得越来越重要是我们拿到心仪Offer的敲门砖。
要知道MySQL的入门门槛非常低还具有免费、开放源码等优势可以满足我们的多样化需求是目前被广泛使用的数据库之一。
看到这里你可能会问“我知道学习MySQL很重要也花了很多时间去学习可是学来学去还是连最简单的实际问题都无法解决该怎么办呢
别着急接下来我们就来聊聊高效的MySQL学习方法。
## 为什么学了很多知识你依然不会用MySQL解决实际问题?
很多人刚开始学习MySQL时都会面临一系列问题。
- 市面上的MySQL资料这么多该怎么挑选呢
- 我花了很多时间学习MysQL但是最后真的遇到问题时我发现我根本不知道怎么解决。
- 我会一些基本的操作,但还是很容易踩到坑里。比如,我曾经把字段设置成浮点数,但我不知道它是不精准的,幸亏领导发现了,不然很可能会给项目带来损失。
- 我储备了不少面试题,为什么一到面试就卡壳呢?
- ……
其实,这些问题,本质上都是一个原因导致的:传统的资料都是在讲授一个个零碎的知识点,最多给出一些基础的小练习,让你进行一些简单的训练。所以,很多人花了很多时间去学习,好像懂得了很多东西,但是一遇到真实的项目问题,就会一头雾水,不知道如何用所学的知识去解决实际问题,更没有能力给出完整的解决方案。
我做过项目主管,也长期带团队,**深知在工作中,最重要的绝对不是你的知识储备量,而是你解决实际问题的能力**。但不幸的是,我见过太多面试时表现优异的人,最终却连试用期都过不了。
说到这里,我特别想和你分享一下我曾面试过的一个应届生的故事。
他是一名计算机专业的研究生讲起MySQL数据库的相关知识他说得非常清楚也很有条理所以我对他的期望值特别高。但是等他真正上手做项目时我才发现他的知识都停留在理论层面。
举个小例子,一次,我们需要开发一个餐厅的点餐系统,我就请他做数据库设计。没想到他设计出来的订单表,居然没有包含客户编号,这就导致无法通过关联查询获取客户信息。这样的数据库根本不满足业务的需求,自然是不能用的。
这并不是个例,很多人在谈到某些知识时可以出口成章,但是一遇到真实的商业环境,就会毫无头绪,或者是犯这样那样的错误。
在我看来,**正确的学习方法,远比你投入的时间更重要**。而实战,就是最高效的方法。
为此,**我特意选择了一个连锁超市的实战项目手把手带你从0到1走完项目全流程不仅帮你掌握核心操作还能让你真正拥有实战能力能够迅速上手任何一个项目**。无论你是刚刚走入职场想要迅速解锁MySQL这项技能还是对它感兴趣想要转岗到MySQL开发都可以在这个课程中达到你理想的目标。
之所以选择采用连锁超市的项目,有两个原因。
1. 你对超市这个场景足够熟悉。我们都有去超市购物的经历,会看到货架上摆着玲琅满目的商品,各种各样的促销招牌,还有忙着扫码收银的店员……借助熟悉的场景来讲解,可以最大程度减轻你的理解成本。
1. 超市背后的业务环节非常复杂产生的数据也多种多样而MySQL是处理这类业务的利器。当我们完整地解决了超市项目中所遇见的复杂数据问题时你再去做其他任何业务就可以更游刃有余一些。
总之,我会从实际问题出发,带你学习技术点,让你能举一反三,快速应用在实战项目中。如果用一个公式去概括,就是:**项目的实际需求--&gt;解决问题所需的知识点--&gt;用好这些知识的实战经验**。
举个例子超市的商品非常多这些商品的名称、数量等必须要被准确地存储、及时地更新才能保证正常地售卖。这就是真实需求。要解决这个问题就要用到MySQL的数据存储功能我们就要掌握设计数据表、定义字段等知识确保数据的存储效率最高以及数据的唯一性同时减少错误。
不只是数据存储我会带着你解决连锁超市所面临的一系列实际问题从商品进货到库存查验再到店面售卖、会员营销等等。在这个过程中我会给你讲解MySQL是怎样存储数据的、如何才能高效查询、如何提供经营决策的依据、如何确保数据的可靠性和安全性……
即使你没有数据库的知识基础也完全不用担心只要你跟着我的思路就一定能真正地在短时间内入门MySQL拥有解决问题的能力。
## 这门课是怎么设计的?
说了这么多,课程的具体设计是怎样的呢?我来介绍一下。
课程总共有四个核心模块。
- **实践篇**:我会从项目最基本的数据存储和操作开始讲起,包括创建数据库、数据表、对表中的数据进行增删改查操作、使用函数、表与表之间的关联操作等,帮你快速掌握最基本的用法。
- **进阶篇**随着用户管理水平的不断提升对系统的要求也越来越多越来越复杂会用到MySQL的许多高级功能。我会手把手带你实现这些功能包括把程序存储在服务器上、利用突发事件来调用程序、在不改变存储结构的前提下创建虚拟表以方便查询等等。
- **优化篇**:项目投入运营以后,随着数据的积累,性能优化的问题逐步凸显。在这个模块呢,我会给你讲一讲数据库的设计规范,还会带你创建数据模型,帮助你来理清设计思路。同时,我还会讲到提升性能的具体方法。
- **案例篇**在课程的最后我会手把手带你从0到1设计一个连锁超市的信息系统数据库把前面讲到的知识点都融入到项目设计中不仅帮你巩固所学的知识更教会你如何灵活使用。
<img src="https://static001.geekbang.org/resource/image/73/30/737ae08d546d03a49d4f93a844a76f30.png" alt="">
除此之外,在课程正式开始之前,我会用图片+音频+视频的形式带着你安装MySQL及必备的图形化管理工具Workbench。同时我还特意设置了一个特别放送模块给你讲解MySQL 8.0的新特性、空间定位的方法,以及大厂的高频面试题,帮你轻松拿下面试。
最后我还想说MySQL是一个非常优秀的数据库里面包含了很多经典的设计思想。虽然现在你不需要掌握得这么深但是我还是建议你多多体会这些思想这会让你提前建立起大局观还可以帮助你从更高的层面去看待所遇见的实际问题。
在这门课程里,我会把我这么多年的经验毫无保留地分享给你,欢迎你来学习这门课,也欢迎你把咱们的课程分享给你的朋友或同事,邀请他们和你一起学习,共同成长。

View File

@@ -0,0 +1,192 @@
<audio id="audio" title="环境准备 | 带你安装MySQL和图形化管理工具Workbench" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8c/09/8c3e876ba8a2ae87e5fab3e6d85dcb09.mp3"></audio>
你好我是朱晓峰。这节课我来手把手带你安装和配置MySQL。
俗话说“巧妇难为无米之炊”我们不能凭空学习理论知识必须要在实际的MySQL环境中进行操作这是一切操作的基础。同时我还会带你安装MySQL自带的图形化管理工具Workbench我们之后要学习的表的关联、聚合、事务以及存储过程等都会用到它。
我会借助图文和音频给你介绍知识重点和操作要领,同时,我还录制了相应的视频,来展示具体的操作细节。你可以根据自己的习惯和需求,选择喜欢的形式来学习。
<video poster="https://media001.geekbang.org/599736e1aad64df7aa8ffa746ebaa675/snapshots/ab93681c22c74e0399a77a31f98af18b-00005.jpg" preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/7e27d07d27d407ebcc195a0e78395f55/488aa8b2-17810dc979a-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src=" https://media001.geekbang.org/0bb39f75acf9400390ba54e0b99ebed2/5246c421f99f45329b88030a012a440a-3c473d8f9e4e8965bf525b201e121eba-sd.m3u8" type="application/x-mpegURL"></video>
好了话不多说我们先来安装MySQL。
## 安装与配置
首先我们要下载MySQL的安装包具体做法是打开浏览器输入网址[https://dev.mysql.com](https://dev.mysql.com/)进入MySQL的开发者专区进行下载。
在下载界面你会看到需要选择操作系统。这是因为MySQL可以在多种操作系统平台上运行包括Windows、Linux、macOS等因此MySQL准备了针对不同操作系统平台的安装程序。这里我们主要介绍MySQL在Windows操作系统上的安装。因为Windows平台应用得最广泛而且图形化操作也比较简单。
当然如果你想了解Linux平台和macOS平台上的安装和配置也可以通过官网[https://dev.mysql.com/doc/refman/8.0/en/linux-installation.html](https://dev.mysql.com/doc/refman/8.0/en/linux-installation.html) 和[https://dev.mysql.com/doc/refman/8.0/en/osx-installation.html](https://dev.mysql.com/doc/refman/8.0/en/osx-installation.html) 来进行查看。不同平台上的MySQL会略有不同比如同样的机器配置Linux上的MySQL运行速度就比Windows快一些不过它们支持的功能和SQL语法都是一样的即使你使用的是其他系统也不会影响到我们的学习。
好了,下载完成之后,我们就可以开始安装了。接下来我给你介绍下安装步骤。
**第一步点击运行下载的安装程序安装MySQL数据库服务器及相关组件**
<img src="https://static001.geekbang.org/resource/image/4c/3b/4cd932563b3c91fac740154bd68e093b.png" alt="">
我给你介绍下这些关键组件的作用。
1. MySQL Server是MySQL数据库服务器这是MySQL的核心组件。
1. MySQL Workbench是一个管理MySQL的图形工具一会儿我还会带你安装它。
1. MySQL Shell是一个命令行工具。除了支持SQL语句它还支持JavaScript和Python脚本并且支持调用MySQL API接口。
1. MySQL Router是一个轻量级的插件可以在应用和数据库服务器之间起到路由和负载均衡的作用。听起来有点复杂我们来想象一个场景假设你有多个MySQL数据库服务器而前端的应用同时产生了很多数据库访问请求这时MySQL Router就可以对这些请求进行调度把访问均衡地分配给每个数据库服务器而不是集中在一个或几个数据库服务器上。
1. Connector/ODBC是MySQL数据库的ODBC驱动程序。ODBC是微软的一套数据库连接标准微软的产品比如Excel就可以通过ODBC驱动与MySQL数据库连接。
其他的组件主要用来支持各种开发环境与MySQL的连接还有MySQL帮助文档和示例。你一看就明白了我就不多说了。
好了知道这些作用下面我们来点击“Execute”运行安装程序把这些组件安装到电脑上。
**第二步:配置服务器**
等所有组件安装完成之后安装程序会提示配置服务器的类型Config Type、连接Connectivity以及高级选项Advanced Configuration如下图所示。这里我重点讲一下配置方法。
<img src="https://static001.geekbang.org/resource/image/f3/07/f3a872586836d57355a022bc3852a107.png" alt="">
我们主要有2个部分需要配置分别是**服务器类别**和**服务器连接**。
先说服务器类别配置。我们有3个选项分别是开发计算机Development Computer、服务器计算机Sever Computer和专属计算机Dedicated Computer。它们的区别在于**MySQL数据库服务器会占用多大的内存**。
- 如果选择开发计算机MySQL数据库服务会占用所需最小的内存以便其他应用可以正常运行。
- 服务器计算机是假设在这台计算机上有多个MySQL数据库服务器实例在运行因此会占用中等程度的内存。
- 专属计算机则会占用计算机的全部内存资源。
这里我们选择配置成“开发计算机”因为我们安装MySQL是为了学习它因此只需要MySQL占有运行所必需的最小资源就可以了。如果你要把它作为项目中的数据库服务器使用就应该配置成服务器计算机或者专属计算机。
**再来说说MySQL数据库的连接方式配置**。我们也有3个选项**网络通讯协议TCP/IP、命名管道Named Pipe和共享内存Shared Memory**。命名管道和共享内存的优势是速度很快但是它们都有一个局限那就是只能从本机访问MySQL数据库服务器。所以**这里我们选择默认的网络通讯协议方式这样的话MySQL数据库服务就可以通过网络进行访问了**。
MySQL默认的TCP/IP协议访问端口是3306后面的X协议端口默认是33060这里我们都不做修改。MySQL的X插件会用到X协议主要是用来实现类似MongoDB 的文件存储服务。这方面的知识,我会在课程后面具体讲解,这里就不多说了。
高级配置Show Advanced和日志配置Logging Options在咱们的课程中用不到这里不用勾选系统会按照默认值进行配置。
**第三步:身份验证配置**
关于MySQL的身份验证的方式我们选择系统推荐的基于SHA256的新加密算法caching_sha2_password。因为跟老版本的加密算法相比新的加密算法具有相同的密码也不会生成相同的加密结果的特点因此更加安全。
**第四步:设置密码和用户权限**
接着我们要设置Root用户的密码。Root是MySQL的超级用户拥有MySQL数据库访问的最高权限。这个密码很重要我们之后会经常用到你一定要牢记。
**第五步配置Windows服务**
最后我们要把MySQL服务器配置成Windows服务。Windows服务的好处在于可以让MySQL数据库服务器一直在Windows环境中运行。而且我们可以让MySQL数据库服务器随着Windows系统的启动而自动启动。
## 图形化管理工具Workbench
安装完成之后我再给你介绍一下MySQL自带的图形化管理工具Workbench。同时我还会用Workbench的数据导入功能带你导入一个Excel数据文件创建出我们的第一个数据库和数据表。
首先我们点击Windows左下角的“开始”按钮如果你是Win10系统可以直接看到所有程序如果你是Win7系统需要找到“所有程序”按钮点击它就可以看到所有程序了。
接着找到“MySQL”点开找到“MySQL Workbench 8.0 CE”。点击打开Workbench如下图所示
<img src="https://static001.geekbang.org/resource/image/ed/c2/ed6288dec0a1899bcb1b6c367aaf53c2.png" alt="">
左下角有个本地连接点击录入Root的密码登录本地MySQL数据库服务器如下图所示
<img src="https://static001.geekbang.org/resource/image/d4/66/d4fb370ed80689384ccfa93267996766.png" alt="">
这是一个图形化的界面,我来给你介绍下这个界面。
- 上方是菜单。左上方是**导航栏**这里我们可以看到MySQL数据库服务器里面的数据库包括数据表、视图、存储过程和函数左下方是**信息栏**,可以显示上方选中的数据库、数据表等对象的信息。
- 中间上方是工作区你可以在这里写SQL语句点击上方菜单栏左边的第三个运行按钮就可以执行工作区的SQL语句了。
- 中间下方是输出区用来显示SQL语句的运行情况包括什么时间开始运行的、运行的内容、运行的输出以及所花费的时长等信息。
好了下面我们就用Workbench实际创建一个数据库并且导入一个Excel数据文件来生成一个数据表。**数据表是存储数据的载体,有了数据表以后,我们就能对数据进行操作了**。
### 创建数据表
**第一步录入Excel数据**
我们打开Excel在工作簿里面录入数据。
我们这个工作表包括3列分别是barcode、goodsname、price代表商品条码、商品名称和售价。然后我们再录入2条数据。
- 0001book3表示条码为“0001”商品名称是“book”价格是3元。
- 0002pen2表示条码是“0002”商品名称是“pen”价格是2元。
注意我在录入商品条码的时候打头用了一个单引号这是为了告诉Excel后面是文本这样系统就不会把0001识别为数字了。
录入完成之后我们把这个文件存起来名称是test格式采用“CSVUTF-8逗号分隔”。这样我们就有了一个CSV文件test.csv。
**第二步:编码转换**
用记事本打开文件再用UTF-8格式保存一次这是为了让Workbench能够识别文件的编码。
**第三步:数据导入**
准备好数据文件以后我们回到Workbench在工作区录入命令`create database demo;`,在工作区的上方,有一排按钮,找到闪电标识的运行按钮,点击运行。
这时下方的输出区域的运行结果会提示“OK”表示运行成功。此时把光标放到左边的导航区点击鼠标右键刷新全部新创建的数据库“demo”就出现了。
点击数据库demo左边的向右箭头就可以看到数据库下面的数据表、视图、存储过程和函数。当然现在都是空的。光标选中数据表鼠标右键选择“Table Data Import Wizard”这时会弹出数据文件选择界面。选中刚才准备的test.csv文件点击下一步Workbench会提示导入目标数据表我们现在什么表也没有所以要选择创建新表“test”。点击下一步Workbench会提示配置表的字段其实它已经按照数据的类别帮我们配置好了。
这时候再次点击下一步点击运行完成数据表导入。光标放到左边的导航区选中我们刚刚创建的数据库“demo”中的数据表鼠标右键点击刷新全部刚刚导入的数据表“test”就显示出来了。
### 进行查询
现在我们已经有了数据库,也有了数据表,下面让我们尝试一个简单的查询。
在工作区,录入`SELECT * FROM demo.test;`这里的demo是数据库名称test是数据表名称*表示全部字段)。
用鼠标选中这行查询命令点击运行。工作区的下半部分会显示查询的结果。我们录入的2条数据都可以看到了。
再尝试插入一条语句:
```
INSERT INTO demo.test
VALUES ('0003','橡皮',5);
```
鼠标选中这条语句点击运行。看到了吗输出区提示“OK”运行成功了。现在回过头来选中上面那条查询语句“SELECT * FROM demo.test;”,点击运行,刚才我们插入的那条记录也查询出来了。
<img src="https://static001.geekbang.org/resource/image/75/79/755b0dfc9f16a598f6270eb2fdb26079.png" alt="">
到这里,我们就完成了数据库创建、数据表导入和简单的查询。是不是觉得很简单呢?
最后,我还想再讲一下源码获取方法。咱们的课程不要求你阅读源码,但是你可以先学会获取源码的方法,毕竟,这是帮助你提升的重要工具。
## MySQL源代码获取
首先你要进入MySQL[下载界面](https://dev.mysql.com/downloads/mysql/8.0.html)。 这里你不要选择用默认的“Microsoft Windows”而是要通过下拉栏找到“Source Code”在下面的操作系统版本里面选择WindowsArchitecture Independent然后点击下载。
接下来把下载下来的压缩文件解压我们就得到了MySQL的源代码。
MySQL是用C++开发而成的,我简单介绍一下源代码的组成。
mysql-8.0.22目录下的各个子目录包含了MySQL各部分组件的源代码
<img src="https://static001.geekbang.org/resource/image/e7/7d/e780e83b296aafd348b3a71948dd4a7d.png" alt="">
- sql子目录是MySQL核心代码
- libmysql子目录是客户端程序API
- mysql-test子目录是测试工具
- mysys子目录是操作系统相关函数和辅助函数
- ……
源代码可以用记事本打开查看如果你有C++的开发环境,也可以在开发环境中打开查看。
<img src="https://static001.geekbang.org/resource/image/72/0b/72180866ddd34c512be6acea95822d0b.png" alt="">
如上图所示源代码并不神秘就是普通的C++代码跟你熟悉的一样而且有很多注释可以帮助你理解。阅读源代码就像在跟MySQL的开发人员对话一样十分有趣。
## 小结
好了,我们来小结下今天的内容。
这节课我带你完成了MySQL的安装和配置同时我还介绍了图形化管理工具Workbench的使用并且创建了第一个数据库、数据表也尝试了初步的SQL语句查询。
我建议你用自己的电脑按照这节课的内容实际操作一下MySQL的安装、配置并尝试不同的配置看看有什么不同体会课程的内容加深理解。
最后,还有几点我要着重提醒你一下。
1. 我们的MySQL是按照开发计算机进行的最小配置实际做项目的时候如果MySQL是核心数据库你要给MySQL配置更多的资源就要选择服务器计算机甚至是专属计算机。
1. Root超级用户的密码你不要忘了否则只好卸载重新安装。
1. 你还可以在Workbench中尝试一下不同的SQL语句同时看看不同的工作区、菜单栏的各种按钮看看它们都是做什么用的。熟悉Workbench对理解我们后面的知识点会很有帮助。
课程的最后我还要给你推荐一下MySQL的[官方论坛](https://forums.mysql.com)。这里面有很多主题比如新产品的发布、各种工具的使用、MySQL各部分组件的介绍等等。如果你有不清楚的内容也可以在里面提问和大家交流建议你好好利用起来。
## 思考题
在导入数据的时候如果不采用MySQL默认的表名而是把导入之后的表改个名字比如说叫demo.sample该如何操作呢
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,505 @@
<audio id="audio" title="14 | 视图:如何简化查询?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7a/a6/7a31d6f3b98195b015ca698da7b135a6.mp3"></audio>
你好,我是朱晓峰。今天,我们来聊一聊视图。
视图是一种虚拟表,我们可以把一段查询语句作为视图存储在数据库中,在需要的时候,可以把视图看做一个表,对里面的数据进行查询。
举个小例子,在学校的信息系统里面,为了减少冗余数据,学生档案(包括姓名、年龄等)和考试成绩(包括考试时间、科目、分数等)是分别存放在不同的数据表里面的,但是,我们经常需要查询学生的考试成绩(包括学生姓名、科目、分数)。这个时候,我们就可以把查询学生考试成绩的这个关联查询,用视图的形式保存起来。这样一来,我们不仅可以从视图中直接查询学生考试成绩,让查询变得简单,而且,视图没有实际存储数据,还避免了数据存储过程中可能产生的冗余,提高了存储的效率。
今天,我就结合超市的项目,来具体讲解一下怎么创建和操作视图,来帮助你提高查询效率。
## 视图的创建及其好处
首先,我们来学习下创建视图的方法,以及使用视图的一些好处。
创建视图的语法结构:
```
CREATE [OR REPLACE]
VIEW 视图名称 [(字段列表)]
AS 查询语句
```
现在假设我们要查询一下商品的每日销售明细这就要从销售流水表demo.trans和商品信息表demo.goodsmaster中获取到销售数据和对应的商品信息数据。
销售流水表包含流水单号、商品编号、销售数量、销售金额和交易时间等信息:
<img src="https://static001.geekbang.org/resource/image/65/56/65168f6e248fcc848e8b9968a712c956.jpeg" alt="">
商品信息表包含商品编号、条码、名称和售价等信息:
<img src="https://static001.geekbang.org/resource/image/73/4b/73836142bbfd6cc0f69de418acb7444b.jpeg" alt="">
在不使用视图的情况下,我们可以通过对销售流水表和商品信息表进行关联查询,得到每天商品销售统计的结果,包括销售日期、商品名称、每天销售数量的合计和每天销售金额的合计,如下所示:
```
mysql&gt; SELECT
-&gt; a.transdate,
-&gt; a.itemnumber,
-&gt; b.goodsname,
-&gt; SUM(a.quantity) AS quantity, -- 统计销售数量
-&gt; SUM(a.salesvalue) AS salesvalue -- 统计销售金额
-&gt; FROM
-&gt; demo.trans AS a
-&gt; LEFT JOIN -- 连接查询
-&gt; demo.goodsmaster AS b ON (a.itemnumber = b.itemnumber)
-&gt; GROUP BY a.transdate , a.itemnumber;
+---------------------+------------+-----------+----------+------------+
| transdate | itemnumber | goodsname | quantity | salesvalue |
+---------------------+------------+-----------+----------+------------+
| 2020-12-01 00:00:00 | 1 | 本 | 1.000 | 89.00 |
| 2020-12-01 00:00:00 | 2 | 笔 | 1.000 | 5.00 |
| 2020-12-02 00:00:00 | 3 | 胶水 | 2.000 | 20.00 |
+---------------------+------------+-----------+----------+------------+
3 rows in set (0.00 sec)
```
在实际项目中,我们发现,每日商品销售查询使用的频次很高,而且经常需要以这个查询的结果为基础,进行更进一步的统计。
举个例子超市经营者要查一下“每天商品的销售数量和当天库存数量的对比”如果用一个SQL语句查询就会比较复杂。历史库存表demo.inventoryhist如下所示
<img src="https://static001.geekbang.org/resource/image/ac/bc/ac3f59ba59e460bb4962391b20a8c1bc.jpeg" alt="">
接下来我们的查询步骤会使用到子查询和派生表,很容易理解,你知道含义就行了。
- 子查询:就是嵌套在另一个查询中的查询。
- 派生表:如果我们在查询中把子查询的结果作为一个表来使用,这个表就是派生表。
这个查询的具体步骤是:
1. 通过子查询获得单品销售统计的查询结果;
1. 把第一步中的查询结果作为一个派生表,跟历史库存表进行连接,查询获得包括销售日期、商品名称、销售数量和历史库存数量在内的最终结果。
```
mysql&gt; SELECT
-&gt; a.transdate,
-&gt; a.itemnumber,
-&gt; a.goodsname,
-&gt; a.quantity, -- 获取单品销售数量
-&gt; b.invquantity -- 获取历史库存数量
-&gt; FROM
-&gt; (SELECT -- 子查询,统计单品销售
-&gt; a.transdate,
-&gt; a.itemnumber,
-&gt; b.goodsname,
-&gt; SUM(a.quantity) AS quantity,
-&gt; SUM(a.salesvalue) AS salesvalue
-&gt; FROM
-&gt; demo.trans AS a
-&gt; LEFT JOIN demo.goodsmaster AS b ON (a.itemnumber = b.itemnumber)
-&gt; GROUP BY a.transdate , a.itemnumber
-&gt; ) AS a -- 派生表,与历史库存进行连接
-&gt; LEFT JOIN
-&gt; demo.inventoryhist AS b
-&gt; ON (a.transdate = b.invdate
-&gt; AND a.itemnumber = b.itemnumber);
+---------------------+------------+-----------+----------+-------------+
| transdate | itemnumber | goodsname | quantity | invquantity |
+---------------------+------------+-----------+----------+-------------+
| 2020-12-01 00:00:00 | 1 | 本 | 1.000 | 100.000 |
| 2020-12-01 00:00:00 | 2 | 笔 | 1.000 | 99.000 |
| 2020-12-02 00:00:00 | 3 | 胶水 | 2.000 | 200.000 |
+---------------------+------------+-----------+----------+-------------+
3 rows in set (0.00 sec)
```
可以看到,这个查询语句是比较复杂的,可读性和可维护性都比较差。那该怎么办呢?其实,针对这种情况,我们就可以使用视图。
我们可以把商品的每日销售统计查询做成一个视图,存储在数据库里,代码如下所示:
```
mysql&gt; CREATE VIEW demo.trans_goodsmaster AS -- 创建视图
-&gt; SELECT
-&gt; a.transdate,
-&gt; a.itemnumber,
-&gt; b.goodsname, -- 从商品信息表中获取名称
-&gt; SUM(a.quantity) AS quantity, -- 统计销售数量
-&gt; SUM(a.salesvalue) AS salesvalue -- 统计销售金额
-&gt; FROM
-&gt; demo.trans AS a
-&gt; LEFT JOIN
-&gt; demo.goodsmaster AS b ON (a.itemnumber = b.itemnumber) -- 与商品信息表关联
-&gt; GROUP BY a.transdate , a.itemnumber; -- 按照销售日期和商品编号分组
Query OK, 0 rows affected (0.01 sec)
```
这样一来,我们每次需要查询每日商品销售数据的时候,就可以直接查询视图,不需要再写一个复杂的关联查询语句了。
我们来试试用一个查询语句直接从视图中进行查询:
```
mysql&gt; SELECT * -- 直接查询
-&gt; FROM demo.trans_goodsmaster; -- 视图
+---------------------+------------+-----------+----------+------------+
| transdate | itemnumber | goodsname | quantity | salesvalue |
+---------------------+------------+-----------+----------+------------+
| 2020-12-01 00:00:00 | 1 | 本 | 1.000 | 89.00 |
| 2020-12-01 00:00:00 | 2 | 笔 | 1.000 | 5.00 |
| 2020-12-02 00:00:00 | 3 | 胶水 | 2.000 | 20.00 |
+---------------------+------------+-----------+----------+------------+
3 rows in set (0.01 sec)
```
结果显示,这两种查询方式得到的结果是一样的。
如果我们要进一步查询“每日单品销售的数量与当日的库存数量的对比”,就可以把刚刚定义的视图作为一个数据表来使用。我们把它跟历史库存表连接起来,来获取销售数量和历史库存数量。就像下面的代码这样,查询就简单多了:
```
mysql&gt; SELECT
-&gt; a.transdate, -- 从视图中获取销售日期
-&gt; a.itemnumber, -- 从视图中获取商品编号
-&gt; a.goodsname, -- 从视图中获取商品名称
-&gt; a.quantity, -- 从视图中获取销售数量
-&gt; b.invquantity -- 从历史库存表中获取历史库存数量
-&gt; FROM
-&gt; demo.trans_goodsmaster AS a -- 视图
-&gt; LEFT JOIN
-&gt; demo.inventoryhist AS b ON (a.transdate = b.invdate
-&gt; AND a.itemnumber = b.itemnumber); -- 直接连接库存历史表
+---------------------+------------+-----------+----------+-------------+
| transdate | itemnumber | goodsname | quantity | invquantity |
+---------------------+------------+-----------+----------+-------------+
| 2020-12-01 00:00:00 | 1 | 本 | 1.000 | 100.000 |
| 2020-12-01 00:00:00 | 2 | 笔 | 1.000 | 99.000 |
| 2020-12-02 00:00:00 | 3 | 胶水 | 2.000 | 200.000 |
+---------------------+------------+-----------+----------+-------------+
3 rows in set (0.00 sec)
```
结果显示,这里的查询结果和我们刚刚使用派生表的查询结果是一样的。但是,**使用视图的查询语句明显简单多了,可读性更好,也更容易维护**。
## 如何操作视图和视图中的数据?
创建完了视图,我们还经常需要对视图进行一些操作,比如修改、查看和删除视图。同时,我们可能还需要修改视图中的数据。具体咋操作呢?我来介绍下。
### 如何操作视图?
修改、查看、删除视图的操作比较简单,你只要掌握具体的语法就行了。
修改视图的语法如下所示:
```
ALTER VIEW 视图名
AS 查询语句;
```
查看视图的语法是:
```
查看视图:
DESCRIBE 视图名;
```
删除视图要使用DROP关键词具体方法如下
```
删除视图:
DROP VIEW 视图名;
```
好了,到这里,对视图的操作我就介绍完了,下面我再讲讲怎么操作视图中的数据。
### 如何操作视图中的数据?
刚刚说过,视图本身是一个虚拟表,所以,对视图中的数据进行插入、修改和删除操作,实际都是通过对实际数据表的操作来实现的。
**1.在视图中插入数据**
我借用刚刚的视图demo.view_goodsmaster来给你解释下。假设商品信息表中的规格字段specification被删除了当我们尝试用INSERT INTO 语句向视图中插入一条记录的时候,就会提示错误了:
```
mysql&gt; INSERT INTO demo.view_goodsmaster
-&gt; (itemnumber,barcode,goodsname,salesprice)
-&gt; VALUES
-&gt; (5,'0005','测试',100);
ERROR 1471 (HY000): The target table view_goodsmaster of the INSERT is not insertable-into
```
这是因为,**只有视图中的字段跟实际数据表中的字段完全一样MySQL才允许通过视图插入数据**。刚刚的视图中包含了实际数据表所没有的字段“specification”所以在插入数据时系统就会提示错误。
为了解决这个问题,我们来修改一下视图,让它只包含实际数据表中有的字段,也就是商品编号、条码、名称和售价。代码如下:
```
mysql&gt; ALTER VIEW demo.view_goodsmaster
-&gt; AS
-&gt; SELECT itemnumber,barcode,goodsname,salesprice -- 只包含实际表中存在的字段
-&gt; FROM demo.goodsmaster
-&gt; WHERE salesprice &gt; 50;
Query OK, 0 rows affected (0.01 sec)
```
对视图进行修改之后,我们重新尝试向视图中插入一条记录:
```
mysql&gt; INSERT INTO demo.view_goodsmaster
-&gt; (itemnumber,barcode,goodsname,salesprice)
-&gt; VALUES
-&gt; (5,'0005','测试',100);
Query OK, 1 row affected (0.02 sec)
```
结果显示,插入成功了。
现在我们来查看一下视图中的数据:
```
mysql&gt; SELECT *
-&gt; FROM demo.view_goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 本 | 89.00 |
| 5 | 0005 | 测试 | 100.00 | -- 通过视图插入的数据
+------------+---------+-----------+------------+
2 rows in set (0.01 sec)
```
结果显示表中确实包含了我们插入的商品编号是5的商品信息。
现在,视图中已经包括了刚才插入的数据,那么,实际数据表中的数据情况又是怎样的呢?我们再来看一下:
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 本 | 89.00 |
| 2 | 0002 | 笔 | 5.00 |
| 3 | 0003 | 胶水 | 10.00 |
| 5 | 0005 | 测试 | 100.00 | -- 通过视图插入的数据
+------------+---------+-----------+------------+
4 rows in set (0.00 sec)
```
可以看到实际数据表demo.goodsmaster中也已经包含通过视图插入的商品编号是5的商品数据了。
**2.删除视图中的数据**
我们可以通过DELETE语句删除视图中的数据
```
mysql&gt; DELETE FROM demo.view_goodsmaster -- 直接在视图中删除数据
-&gt; WHERE itemnumber = 5;
Query OK, 1 row affected (0.02 sec)
```
现在我们来查看视图和实际数据表的内容会发现商品编号是5的商品都已经被删除了。
```
mysql&gt; SELECT *
-&gt; FROM demo.view_goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 本 | 89.00 | -- 视图中已经没有商品编号是5的商品了
+------------+---------+-----------+------------+
1 row in set (0.00 sec)
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 本 | 89.00 |
| 2 | 0002 | 笔 | 5.00 |
| 3 | 0003 | 胶水 | 10.00 | -- 实际表中也已经没有商品编号是5的商品了
+------------+---------+-----------+------------+
3 rows in set (0.00 sec)
```
**3.修改视图中的数据**
我们可以通过UPDATE语句对视图中的数据进行修改
```
mysql&gt; UPDATE demo.view_goodsmaster -- 更新视图中的数据
-&gt; SET salesprice = 100
-&gt; WHERE itemnumber = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
结果显示,更新成功了。现在我们来查看一下视图和实际数据表,代码如下所示:
```
mysql&gt; SELECT *
-&gt; FROM demo.view_goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 本 | 100.00 | -- 视图中的售价改过了
+------------+---------+-----------+------------+
1 row in set (0.01 sec)
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 本 | 100.00 | -- 实际数据表中的售价也改过了
| 2 | 0002 | 笔 | 5.00 |
| 3 | 0003 | 胶水 | 10.00 |
+------------+---------+-----------+------------+
3 rows in set (0.00 sec)
```
可以发现,视图和原来的数据表都已经改过来了。
需要注意的是,**我不建议你对视图的数据进行更新操作**因为MySQL允许用比较复杂的SQL查询语句来创建视图比如SQL查询语句中使用了分组和聚合函数或者是UION和DISTINCT关键字所以要通过对这个结果集的更新来更新实际数据表有可能不被允许因为MySQL没办法精确定位实际数据表中的记录。就比如刚刚讲到的那个“每日销售统计查询”视图就没办法更改因为创建视图的SQL语句是一个包含了分组函数GROUP BY的查询。
## 视图有哪些优缺点?
到这里,视图的操作我就讲完了,现在我们把视线拔高一点,来看看视图都有哪些优缺点。只有全面掌握视图的特点,我们才能充分享受它的高效,避免踩坑。
首先,我来介绍下视图的优点。
第一,因为我们可以把视图看成一张表来进行查询,所以在使用视图的时候,我们不用考虑视图本身是如何获取数据的,里面有什么逻辑,包括了多少个表,有哪些关联操作,而是可以直接使用。这样一来,实际上就把查询模块化了,查询变得更加简单,提高了开发和维护的效率。所以,你可以把那些经常会用到的查询和复杂查询的子查询定义成视图,存储到数据库中,这样可以为你以后的使用提供方便。
第二,视图跟实际数据表不一样,它存储的是查询语句。所以,在使用的时候,我们要通过定义视图的查询语句来获取结果集。而视图本身不存储数据,不占用数据存储的资源。
第三,视图具有隔离性。视图相当于在用户和实际的数据表之间加了一层虚拟表。也就是说,**用户不需要查询数据表,可以直接通过视图获取数据表中的信息**。这样既提高了数据表的安全性,同时也通过视图把用户实际需要的信息汇总在了一起,查询起来很轻松。
<img src="https://static001.geekbang.org/resource/image/a2/45/a2ca36c520ed6a6fee5f788d5dcb2845.jpg" alt="">
第四,**视图的数据结构相对独立,即便实际数据表的结构发生变化,我们也可以通过修改定义视图的查询语句,让查询结果集里的字段保持不变**。这样一来,针对视图的查询就不受实际数据表结构变化的影响了。
这一点不容易理解,我举个小例子来说明一下。
假设我们有一个实际的数据表demo.goodsmaster包括商品编号、条码、名称、规格和售价等信息
<img src="https://static001.geekbang.org/resource/image/63/8f/63f46f64a5007278b97235a00633d58f.jpeg" alt="">
在这个表的基础上我们建一个视图查询所有价格超过50元的商品
```
mysql&gt; CREATE VIEW demo.view_goodsmaster AS
-&gt; SELECT *
-&gt; FROM demo.goodsmaster
-&gt; WHERE salesprice &gt; 50;
Query OK, 0 rows affected (0.03 sec)
```
接着,我们在这个视图的基础上做一个查询,来验证一下视图的内容:
```
mysql&gt; SELECT barcode,goodsname,specification
-&gt; FROM demo.view_goodsmaster;
+---------+-----------+---------------+
| barcode | goodsname | specification |
+---------+-----------+---------------+
| 0001 | 本 | 16开 |
+---------+-----------+---------------+
1 row in set (0.00 sec)
```
结果显示我们得到了商品信息表中售价大于50元的商品16开
假设现在我们需要把数据表demo.goodsmaster中的字段“specification”删掉就可以用下面的代码
```
mysql&gt; ALTER TABLE demo.goodsmaster DROP COLUMN specification;
Query OK, 0 rows affected (0.13 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
这样一来,因为少了一个字段,而我们的语句又是直接查询数据表的,代码就会提示错误:
```
mysql&gt; SELECT barcode,goodsname,specification
-&gt; FROM demo.goodsmaster;
ERROR 1054 (42S22): Unknown column 'specification' in 'field list'
```
你看代码提示字段“specification”不存在。
但是,如果查询的是视图,就可以通过修改视图来规避这个问题。我们可以用下面的代码把刚才的视图修改一下:
```
mysql&gt; ALTER VIEW demo.view_goodsmaster
-&gt; AS
-&gt; SELECT
-&gt; itemnumber,
-&gt; barcode,
-&gt; goodsname,
-&gt; '' as specification, -- 由于字段不存在插入一个长度是0的空字符串作为这个字段的值
-&gt; salesprice
-&gt; FROM demo.goodsmaster
-&gt; WHERE salesprice &gt; 50;
Query OK, 0 rows affected (0.02 sec)
```
你看虽然实际数据表中已经没有字段“specification”了但是视图中却保留了这个字段而且字段值始终是空字符串。所以我们不用修改原有视图的查询语句它也会正常运行。下面的代码查询的结果中就包括了实际数据表没有的字段“specification”。
```
mysql&gt; SELECT barcode,goodsname,specification
-&gt; FROM demo.view_goodsmaster;
+---------+-----------+---------------+
| barcode | goodsname | specification |
+---------+-----------+---------------+
| 0001 | 本 | |
+---------+-----------+---------------+
1 row in set (0.00 sec)
```
结果显示,运行成功了。这个视图查询,就没有受到实际数据表中删除字段的影响。
看到这儿,你可能会说,视图有这么多好处,那我以后都用视图可以吗?其实不是的,视图也有自身的不足。
如果我们在实际数据表的基础上创建了视图,那么,**如果实际数据表的结构变更了,我们就需要及时对相关的视图进行相应的维护**。特别是当视图是由视图生成的时候维护会变得比较复杂。因为创建视图的SQL查询可能会对字段重命名也可能包含复杂的逻辑这些都会增加维护的成本。
所以,在创建视图的时候,你要结合实际项目需求,综合考虑视图的优点和不足,这样才能正确使用视图,使系统整体达到最优。
为了方便你掌握,我用一张图来汇总下视图的优缺点:
<img src="https://static001.geekbang.org/resource/image/d4/12/d4cb502799c8328d22774907d7b97212.jpeg" alt="">
## 总结
今天,我给你介绍了简化查询的工具:视图,我们学习了创建视图、操作视图和视图中的数据的方法以及视图的优缺点。你要重点掌握操作的语法结构。
最后,我还是想提醒你一下,虽然可以更新视图数据,但总的来说,视图作为虚拟表,主要用于方便查询。我不建议你更新视图的数据,因为对视图数据的更改,都是通过对实际数据表里数据的操作来完成的,而且有很多限制条件。
视图虽然有很多优点。但是在创建视图、简化查询的同时,也要考虑到视图太多而导致的数据库维护成本的问题。
视图不是越多越好,特别是嵌套的视图(就是在视图的基础上创建视图),我不建议你使用,因为逻辑复杂,可读性不好,容易变成系统的潜在隐患。
## 思考题
假设某公园售票系统包括门票信息表和类别信息表,这两个表之间通过类别编号相关联。
门票信息表包含门票编号、名称、类别编号和剩余数量等信息。
<img src="https://static001.geekbang.org/resource/image/e1/6a/e1d07bcc301ec2929ff50d601b3c5a6a.jpeg" alt="">
类别信息表包含类别编号、开门时间和闭馆时间。
<img src="https://static001.geekbang.org/resource/image/8f/42/8f21b2f64c2bb51ab7719f27d40ed942.jpeg" alt="">
请编写一个视图视图返回的结果集包括当前时间可以卖的门票名称和剩余数量说明开门前30分钟开始售票结束前30分钟停止售票
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,328 @@
<audio id="audio" title="15 | 存储过程:如何提高程序的性能和安全性?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7d/b2/7db3784962df3021f43c8c37d58dbfb2.mp3"></audio>
你好我是朱晓峰。今天呢我们来聊一聊MySQL的存储过程。
在我们的超市项目中,每天营业结束后,超市经营者都要计算当日的销量,核算成本和毛利等营业数据,这也就意味着每天都要做重复的数据统计工作。其实,这种数据量大,而且计算过程复杂的场景,就非常适合使用存储过程。
简单来说呢存储过程就是把一系列SQL语句预先存储在MySQL服务器上需要执行的时候客户端只需要向服务器端发出调用存储过程的命令服务器端就可以把预先存储好的这一系列SQL语句全部执行。
这样一来不仅执行效率非常高而且客户端不需要把所有的SQL语句通过网络发给服务器减少了SQL语句暴露在网上的风险也提高了数据查询的安全性。
今天,我就借助真实的超市项目,给你介绍一下如何创建和使用存储过程,帮助你提升查询的效率,并且让你开发的应用更加简洁安全。
## 如何创建存储过程?
在创建存储过程的时候我们需要用到关键字CREATE PROCEDURE。具体的语法结构如下
```
CREATE PROCEDURE 存储过程名 [ IN | OUT | INOUT] 参数名称 类型)程序体
```
接下来,我以超市的日结计算为例,给你讲一讲怎么创建存储过程。当然,为了方便你理解,我对计算的过程进行了简化。
假设在日结计算中,我们需要统计每天的单品销售,包括销售数量、销售金额、成本、毛利、毛利率等。同时,我们还要把计算出来的结果存入单品统计表中。
这个计算需要用到几个数据表,我分别来展示下这些表的基本信息。
销售单明细表demo.transactiondetails中包括了每笔销售中的商品编号、销售数量、销售价格和销售金额。
```
mysql&gt; SELECT *
-&gt; FROM demo.transactiondetails;
+---------------+------------+----------+------------+------------+
| transactionid | itemnumber | quantity | salesprice | salesvalue |
+---------------+------------+----------+------------+------------+
| 1 | 1 | 1.000 | 89.00 | 89.00 |
| 1 | 2 | 2.000 | 5.00 | 10.00 |
| 2 | 1 | 2.000 | 89.00 | 178.00 |
| 3 | 2 | 10.000 | 5.00 | 50.00 |
| 3 | 3 | 3.000 | 15.00 | 45.00 |
+---------------+------------+----------+------------+------------+
5 rows in set (0.00 sec)
```
销售单头表demo.transactionhead中包括流水单号、收款机编号、会员编号、操作员编号、交易时间。
```
mysql&gt; SELECT *
-&gt; FROM demo.transactionhead;
+---------------+------------------+-----------+----------+------------+---------------------+
| transactionid | transactionno | cashierid | memberid | operatorid | transdate |
+---------------+------------------+-----------+----------+------------+---------------------+
| 1 | 0120201201000001 | 1 | 1 | 1 | 2020-12-01 00:00:00 |
| 2 | 0120201201000002 | 1 | NULL | 1 | 2020-12-01 00:00:00 |
| 3 | 0120201202000001 | 1 | NULL | 1 | 2020-12-02 00:00:00 |
+---------------+------------------+-----------+----------+------------+---------------------+
3 rows in set (0.00 sec)
```
商品信息表demo.goodsmaster中包括商品编号、商品条码、商品名称、规格、单位、售价和平均进价。
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+------------+----------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice | avgimportprice |
+------------+---------+-----------+---------------+------+------------+----------------+
| 1 | 0001 | 书 | NULL | 本 | 89.00 | 33.50 |
| 2 | 0002 | 笔 | NULL | 支 | 5.00 | 3.50 |
| 3 | 0003 | 胶水 | NULL | 瓶 | 15.00 | 11.00 |
+------------+---------+-----------+---------------+------+------------+----------------+
3 rows in set (0.00 sec)
```
存储过程会用刚刚的三个表中的数据进行计算,并且把计算的结果存储到下面的这个单品统计表中。
```
mysql&gt; DESCRIBE demo.dailystatistics;
+-------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| itemnumber | int | YES | MUL | NULL | |
| quantity | decimal(10,3) | YES | | NULL | |
| actualvalue | decimal(10,2) | YES | | NULL | |
| cost | decimal(10,2) | YES | | NULL | |
| profit | decimal(10,2) | YES | | NULL | |
| profitratio | decimal(10,4) | YES | | NULL | |
| salesdate | datetime | YES | MUL | NULL | |
+-------------+---------------+------+-----+---------+----------------+
8 rows in set (0.01 sec)
```
我们现在就来创建一个存储过程,完成单品销售统计的计算。我来讲一讲具体的思路。
第一步我们把SQL语句的分隔符改为“//”。因为存储过程中包含很多SQL语句如果不修改分隔符的话MySQL会在读到第一个SQL语句的分隔符“;”的时候,认为语句结束并且执行,这样就会导致错误。
第二步我们来创建存储过程把要处理的日期作为一个参数传入关于参数下面我会具体讲述。同时用BEGIN和END关键字把存储过程中的SQL语句包裹起来形成存储过程的程序体。
第三步在程序体中先定义2个数据类型为DATETIME的变量用来记录要计算数据的起始时间和截止时间。
第四步,删除保存结果数据的单品统计表中相同时间段的数据,目的是防止数据重复。
第五步,计算起始时间和截止时间内单品的销售数量合计、销售金额合计、成本合计、毛利和毛利率,并且把结果存储到单品统计表中。
这五个步骤,我们就可以用下面的代码来实现。
```
mysql&gt; DELIMITER // -- 设置分割符为//
-&gt; CREATE PROCEDURE demo.dailyoperation(transdate TEXT)
-&gt; BEGIN -- 开始程序体
-&gt; DECLARE startdate,enddate DATETIME; -- 定义变量
-&gt; SET startdate = date_format(transdate,'%Y-%m-%d'); -- 给起始时间赋值
-&gt; SET enddate = date_add(startdate,INTERVAL 1 DAY); -- 截止时间赋值为1天以后
-&gt; -- 删除原有数据
-&gt; DELETE FROM demo.dailystatistics
-&gt; WHERE
-&gt; salesdate = startdate;
-&gt; -- 插入新计算的数据
-&gt; INSERT into dailystatistics
-&gt; (
-&gt; salesdate,
-&gt; itemnumber,
-&gt; quantity,
-&gt; actualvalue,
-&gt; cost,
-&gt; profit,
-&gt; profitratio
-&gt; )
-&gt; SELECT
-&gt; LEFT(b.transdate,10),
-&gt; a.itemnumber,
-&gt; SUM(a.quantity), -- 数量总计
-&gt; SUM(a.salesvalue), -- 金额总计
-&gt; SUM(a.quantity*c.avgimportprice), -- 计算成本
-&gt; SUM(a.salesvalue-a.quantity*c.avgimportprice), -- 计算毛利
-&gt; CASE sum(a.salesvalue) WHEN 0 THEN 0
-&gt; ELSE round(sum(a.salesvalue-a.quantity*c.avgimportprice)/sum(a.salesvalue),4) END -- 计算毛利率
-&gt; FROM
-&gt; demo.transactiondetails AS a
-&gt; JOIN
-&gt; demo.transactionhead AS b
-&gt; ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.goodsmaster c
-&gt; ON (a.itemnumber=c.itemnumber)
-&gt; WHERE
-&gt; b.transdate&gt;startdate AND b.transdate&lt;enddate
-&gt; GROUP BY
-&gt; LEFT(b.transdate,10),a.itemnumber
-&gt; ORDER BY
-&gt; LEFT(b.transdate,10),a.itemnumber;
-&gt; END
-&gt; // -- 语句结束,执行语句
Query OK, 0 rows affected (0.01 sec)
-&gt; DELIMITER ; -- 恢复分隔符为;
```
这样,我们的存储过程就创建成功了。
在这个存储过程中,我们用到了存储过程的参数定义和程序体,这些具体是什么意思呢?我们来学习下。
### 存储过程的参数定义
存储过程可以有参数,也可以没有参数。一般来说,当我们通过客户端或者应用程序调用存储过程的时候,如果需要与存储过程进行数据交互,比如,存储过程需要根据输入的数值为基础进行某种数据处理和计算,或者需要把某个计算结果返回给调用它的客户端或者应用程序,就需要设置参数。否则,就不用设置参数。
参数有3种分别是IN、OUT和INOUT。
- IN表示输入的参数存储过程只是读取这个参数的值。如果没有定义参数种类默认就是IN表示输入参数。
- OUT表示输出的参数存储过程在执行的过程中把某个计算结果值赋给这个参数执行完成之后调用这个存储过程的客户端或者应用程序就可以读取这个参数返回的值了。
- INOUT表示这个参数既可以作为输入参数又可以作为输出参数使用。
**除了定义参数种类,还要对参数的数据类型进行定义**。在这个存储过程中我定义了一个参数transdate 的数据类型是TEXT。这个参数的用处是告诉存储过程我要处理的是哪一天的数据。我没有指定参数种类是IN、OUT或者INOUT这是因为在MySQL中**如果不指定参数的种类默认就是IN表示输入参数**。
知道了参数下面我具体讲解一下这个存储过程的程序体。存储过程的具体操作步骤都包含在程序体里面我们来分析一下程序体中SQL操作的内容就可以知道存储过程到底在做什么。
### 存储过程的程序体
程序体中包含的是存储过程需要执行的SQL语句一般通过关键字BEGIN表示SQL语句的开始通过END表示SQL语句的结束。
在程序体的开始部分我定义了2个变量分别是startdate和enddate。它们都是DATETIME类型作用是根据输入参数transdate计算出需要筛选的数据的时间区间。
后面的代码分3步完成起始时间和截止时间的计算并且分别赋值给变量startdate和enddate。
第一步使用DATE_FROMAT函数把输入的参数按照YYYY年MM月DD日的格式转换成了日期时间类型数据比如输入参数是“2020-12-01”那么转换成的日期时间值是“2020-12-01 00:00:00”表示2020年12月01日00点00分00秒。
第二步把第一步中计算出的值作为起始时间赋值给变量startdate。
第三步把第一步中计算出的值通过DATE_ADD函数计算出1天以后的时间赋值给变量enddate。
这样,我就获得了需要计算的销售时段。计算出了起始时间和截止时间之后,我们先删除需要计算日期的单品统计数据,以防止数据重复。接着,我们重新计算单品的销售统计,并且把计算的结果插入到单品统计表。
在计算单品销售统计的时候也分为3步
1. 按照“成本=销售数量×平均价格”的方式计算成本;
1. 按照“毛利=销售金额 - 成本”的方式计算毛利;
1. 按照“毛利率 = 毛利 ÷销售金额”的方式计算毛利率。
需要注意的是这里我使用CASE函数来解决销售金额为0时计算毛利的问题。这是为了防止计算出现被0除而报错的情况。不要以为销售金额就一定大于0在实际项目运行的过程中会出现因为优惠而导致实际销售金额为0的情况。我建议你在实际工作中把这些极端情况都考虑在内提前进行防范这样你的代码才能稳定可靠。
存储过程通过开始时定义的分隔符“//”结束MySQL执行这段SQL语句就创建出了一个存储过程demo.dailyoperation.最后,你不要忘了把分隔符改回到“;”。
创建完之后,怎么知道我们创建的存储过程是否成功了呢?下面我介绍一下查看存储过程的方法。
## 如何查看存储过程?
我们可以通过SHOW CREATE PROCEDURE 存储过程名称,来查看刚刚创建的存储过程:
```
mysql&gt; SHOW CREATE PROCEDURE demo.dailyoperation \G
*************************** 1. row ***************************
Procedure: dailyoperation -- 存储过程名
sql_mode: STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
Create Procedure: CREATE DEFINER=`root`@`localhost` PROCEDURE `dailyoperation`(transdate TEXT) -- 存储过程名
BEGIN -- 开始程序体
DECLARE startdate,enddate DATETIME;
SET startdate = date_format(transdate,'%Y-%m-%d');
SET enddate = date_add(startdate,INTERVAL 1 DAY);
DELETE FROM demo.dailystatistics -- 删除重复
WHERE
salesdate = startdate;
INSERT into dailystatistics -- 将计算结果插入每日统计表
(
salesdate,
itemnumber,
quantity,
actualvalue,
cost,
profit,
profitratio
)
SELECT
LEFT(b.transdate,10),
a.itemnumber,
SUM(a.quantity),
SUM(a.salesvalue),
SUM(a.quantity*c.avgimportprice),
SUM(a.salesvalue-a.quantity*c.avgimportprice),
CASE sum(a.salesvalue) WHEN 0 THEN 0
ELSE round(sum(a.salesvalue-a.quantity*c.avgimportprice)/sum(a.salesvalue),4) END
FROM
demo.transactiondetails AS a
JOIN
demo.transactionhead AS b
ON (a.transactionid = b.transactionid)
JOIN
demo.goodsmaster c
ON (a.itemnumber=c.itemnumber)
WHERE
b.transdate&gt;startdate AND b.transdate&lt;enddate
GROUP BY
LEFT(b.transdate,10),a.itemnumber
ORDER BY
LEFT(b.transdate,10),a.itemnumber;
END -- 结束程序体
character_set_client: gbk -- 采用的字符集gbk
collation_connection: gbk_chinese_ci -- 连接校对采用的字符集
Database Collation: utf8mb4_0900_ai_ci -- 数据校对字符集
1 row in set (0.00 sec)
```
## 如何调用存储过程?
下面我们来尝试调用一下这个存储过程并且给它传递一个参数“2020-12-01”也就是计算2020年12月01日的单品统计数据
```
mysql&gt; CALL demo.dailyoperation('2020-12-01');
Query OK, 2 rows affected (0.03 sec)
```
存储过程执行结果提示“Query OK”表示执行成功了。“2 rows affected”表示执行的结果影响了2条数据记录。
我们用SELECT语句来查看一下单品统计表看看有没有把单品统计的结果存入单品统计表中。
```
mysql&gt; SELECT * -- 查询单品统计表中的数据
-&gt; FROM demo.dailystatistics;
+----+------------+----------+-------------+--------+--------+-------------+---------------------+
| id | itemnumber | quantity | actualvalue | cost | profit | profitratio | salesdate |
+----+------------+----------+-------------+--------+--------+-------------+---------------------+
| 13 | 1 | 3.000 | 267.00 | 100.50 | 166.50 | 0.6236 | 2020-12-01 00:00:00 | -- 编号是1的商品单品统计结果
| 14 | 2 | 2.000 | 10.00 | 7.00 | 3.00 | 0.3000 | 2020-12-01 00:00:00 | -- 编号是2的商品单品统计的结果
+----+------------+----------+-------------+--------+--------+-------------+---------------------+
2 rows in set (0.00 sec)
```
看到了吗我们已经能够在单品统计表中查询到2020年12月01日的单品统计结果了。这也就意味着我们的存储过程被执行了它计算出了我们需要的单品统计结果并且把统计结果存入了单品统计表中。
## 如何修改和删除存储过程?
如果你需要修改存储过程的内容我建议你在Workbench中操作。这是因为你可以在里面直接修改存储过程而如果用SQL命令来修改存储过程就必须删除存储过程再重新创建相比之下在Workbench中修改比较简单。
具体的做法是在左边的导航栏找到数据库demo展开之后找到存储过程stored procedure然后找到我们刚刚创建的dailyoperation点击右边的设计按钮就可以在右边的工作区进行修改了。
<img src="https://static001.geekbang.org/resource/image/98/bb/98058c5a4b4f2517ef83d22895dd96bb.png" alt="">
修改完成之后点击工作区右下方的按钮“Apply”保存修改。
在MySQL中存储过程不像普通的编程语言比如VC++、Java等那样有专门的集成开发环境。因此你可以通过SELECT语句把程序执行的中间结果查询出来来调试一个SQL语句的正确性。调试成功之后把SELECT语句后移到下一个SQL语句之后再调试下一个SQL语句。这样逐步推进就可以完成对存储过程中所有操作的调试了。当然你也可以把存储过程中的SQL语句复制出来逐段单独调试。
删除存储过程很简单,你知道具体的语法就行了:
```
DROP PROCEDURE 存储过程名称;
```
## 总结
这节课,我们学习了创建、查看、修改和删除存储过程的具体方法。
存储过程的优点就是执行效率高,而且更加安全,不过,它也有着自身的缺点,那就是开发和调试的成本比较高,而且不太容易维护。
在存储过程开发的过程中虽然也有一些第三方工具可以对存储过程进行调试但要收费。我建议你通过SELECT语句输出变量值的办法进行调试虽然有点麻烦但是成本低而且简单可靠。如果你的存储过程需要随产品一起分发可以考虑把脚本放在安装程序中在产品安装的过程中创建需要的存储过程。
## 思考题
请写一个简单的存储过程要求是定义2个参数一个输入参数a数据类型是INT另一个输出参数是b类型是INT。程序体完成的操作是b = a + 1。
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,306 @@
<audio id="audio" title="16 | 游标:对于数据集中的记录,该怎么逐条处理?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/da/fd/da01c9fbaa9dbd0c468c9089e7fdf5fd.mp3"></audio>
你好,我是朱晓峰。今天,我来和你聊一聊游标。
咱们前面学习的MySQL数据操作语句都是针对结果集合的。也就是说每次处理的对象都是一个数据集合。如果需要逐一处理结果集中的记录就会非常困难。
虽然我们也可以通过筛选条件WHERE和HAVING或者是限定返回记录的关键字LIMIT返回一条记录但是却无法在结果集中像指针一样向前定位一条记录、向后定位一条记录或者是随意定位到某一条记录并对记录的数据进行处理。
这个时候,就可以用到游标。所谓的游标,也就是能够对结果集中的每一条记录进行定位,并对指向的记录中的数据进行操作的数据结构。
这么说可能有点抽象我举一个生活中的例子你一看就明白了。比如你想去某个城市旅游现在需要订酒店。你打开预订酒店的App设置好价格区间后进行搜索得到了一个酒店列表。接下来你可能要逐条查看列表中每个酒店的客户评价最后选择一个口碑不错的酒店。这个逐条搜索并对选中的数据进行操作的过程就相当于游标对数据记录进行操作的过程。
今天我就来给你讲一讲游标的使用方法,同时还会通过一个案例串讲,帮助你更好地使用游标,让你能够轻松地处理数据集中的记录。
# 游标的使用步骤
游标只能在存储程序内使用,存储程序包括存储过程和存储函数。关于存储过程,我们上节课刚刚学过,这里我简单介绍一下存储函数。创建存储函数的语法是:
```
CREATE FUNCTION 函数名称 参数RETURNS 数据类型 程序体
```
存储函数与存储过程很像,但有几个不同点:
1. 存储函数必须返回一个值或者数据表,存储过程可以不返回。
1. 存储过程可以通过CALL语句调用存储函数不可以。
1. 存储函数可以放在查询语句中使用,存储过程不行。
1. 存储过程的功能更加强大,包括能够执行对表的操作(比如创建表,删除表等)和事务操作,这些功能是存储函数不具备的。
这节课,我们主要学习下游标在存储过程中的使用方法,因为游标在存储过程中更常用。游标在存储函数中的使用方法和在存储过程中的使用方法是一样的。
在使用游标的时候主要有4个步骤。
第一步,定义游标。语法结构如下:
```
DECLARE 游标名 CURSOR FOR 查询语句
```
这里就是声明一个游标,它可以操作的数据集是“查询语句”返回的结果集。
第二步,打开游标。语法结构如下:
```
OPEN 游标名称;
```
打开游标之后,系统会为游标准备好查询的结果集,为后面游标的逐条读取结果集中的记录做准备。
第三步,从游标的数据结果集中读取数据。语法结构是这样的:
```
FETCH 游标名 INTO 变量列表;
```
这里的意思是通过游标,把当前游标指向的结果集中那一条记录的数据,赋值给列表中的变量。
需要注意的是,**游标的查询结果集中的字段数必须跟INTO后面的变量数一致**否则在存储过程执行的时候MySQL会提示错误。
第四步,关闭游标。语法结构如下:
```
CLOSE 游标名;
```
**用完游标之后,你一定要记住及时关闭游标**。因为游标会占用系统资源,如果不及时关闭,游标会一直保持到存储过程结束,影响系统运行的效率。而关闭游标的操作,会释放游标占用的系统资源。
知道了基本步骤,下面我就结合超市项目的实际案例,带你实战一下。
# 案例串讲
在超市项目的进货模块中,有一项功能是对进货单数据进行验收。其实就是在对进货单的数据确认无误后,对进货单的数据进行处理,包括增加进货商品的库存,并修改商品的平均进价。下面我用实际数据来演示一下这个操作流程。
这里我们要用到进货单头表demo.importheadl、进货单明细表demo.importdetails、库存表demo.inventory和商品信息表demo.goodsmaster
进货单头表:
<img src="https://static001.geekbang.org/resource/image/aa/97/aa133989325a1125ff118bce9993ec97.jpeg" alt="">
进货单明细表:
<img src="https://static001.geekbang.org/resource/image/a5/b3/a5b6af4f27a24e003bced20e044336b3.jpeg" alt="">
库存表:
<img src="https://static001.geekbang.org/resource/image/1e/d3/1e1c6d83d75094376bf21e6f4c0572d3.jpeg" alt="">
商品信息表:
<img src="https://static001.geekbang.org/resource/image/cd/8e/cd9832d7cac00446b937462518bf018e.jpeg" alt="">
要验收进货单,我们就需要对每一个进货商品进行两个操作:
1. 在现有库存数量的基础上,加上本次进货的数量;
1. 根据本次进货的价格、数量,现有商品的平均进价和库存,计算新的平均进价:(本次进货价格 * 本次进货数量+现有商品平均进价 * 现有商品库存)/(本次进货数量+现有库存数量)。
针对这个操作,如果只用我们在[第4讲](https://time.geekbang.org/column/article/351225)里学习的SQL语句完成起来就比较困难。
因为我们需要通过应用程序来控制操作流程做成一个循环操作每次只查询一种商品的数据记录并进行处理一直到把进货单中的数据全部处理完。这样一来应用必须发送很多的SQL指令到服务器跟服务器的交互多不仅代码复杂而且也不够安全。
这个时候,如果使用游标,就很容易了。因为所有的操作都可以在服务器端完成,应用程序只需要发送一个命令调用存储过程就可以了。现在,我们就来看看如何用游标来解决这个问题。
我用代码创建了一个存储过程demo.mytest。当然你也完全可以在Workbench中创建存储过程非常简单我就不多说了。创建存储过程的代码如下
```
mysql&gt; DELIMITER //
mysql&gt; CREATE PROCEDURE demo.mytest(mylistnumber INT)
-&gt; BEGIN
-&gt; DECLARE mystockid INT;
-&gt; DECLARE myitemnumber INT;
-&gt; DECLARE myquantity DECIMAL(10,3);
-&gt; DECLARE myprice DECIMAL(10,2);
-&gt; DECLARE done INT DEFAULT FALSE; -- 用来控制循环结束
-&gt; DECLARE cursor_importdata CURSOR FOR -- 定义游标
-&gt; SELECT b.stockid,a.itemnumber,a.quantity,a.importprice
-&gt; FROM demo.importdetails AS a
-&gt; JOIN demo.importhead AS b
-&gt; ON (a.listnumber=b.listnumber)
-&gt; WHERE a.listnumber = mylistnumber;
-&gt; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; -- 条件处理语句
-&gt;
-&gt; OPEN cursor_importdata; -- 打开游标
-&gt; FETCH cursor_importdata INTO mystockid,myitemnumber,myquantity,myprice; -- 读入第一条记录
-&gt; REPEAT
-&gt; -- 更新进价
-&gt; UPDATE demo.goodsmaster AS a,demo.inventory AS b
-&gt; SET a.avgimportprice = (a.avgimportprice*b.invquantity+myprice*myquantity)/(b.invquantity+myquantity)
-&gt; WHERE a.itemnumber=b.itemnumber AND b.stockid=mystockid AND a.itemnumber=myitemnumber;
-&gt; -- 更新库存
-&gt; UPDATE demo.inventory
-&gt; SET invquantity = invquantity + myquantity
-&gt; WHERE stockid = mystockid AND itemnumber=myitemnumber;
-&gt; -- 获取下一条记录
-&gt; FETCH cursor_importdata INTO mystockid,myitemnumber,myquantity,myprice;
-&gt; UNTIL done END REPEAT;
-&gt; CLOSE cursor_importdata;
-&gt; END
-&gt; //
Query OK, 0 rows affected (0.02 sec)
-&gt; DELIMITER ;
```
这段代码比较长核心操作有6步我来给你详细解释下。
1. 把MySQL的分隔符改成“//”。
1. 开始程序体之后我定义了4个变量分别是mystockid、myitemnumber、myquantity和myprice这几个变量的作用是存储游标中读取的仓库编号、商品编号、进货数量和进货价格数据。
1. 定义游标。这里我指定了游标的名称以及游标可以处理的数据集mylistnumber指定的进货单的全部进货商品明细数据
1. 定义条件处理语句“DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;”。
1. 打开游标,读入第一条记录,然后开始执行数据操作。
1. 关闭游标,结束程序。
可以看到,在这个存储过程中,我用到了条件处理语句,它的作用是告诉系统,在存储程序遇到问题的时候,应该如何处理。
## 条件处理语句
条件处理语句的语法结构:
```
DECLARE 处理方式 HANDLER FOR 问题 操作;
```
下面我结合刚刚的“DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;”,来解释一下条件处理语句是如何工作的。
1. **语法结构中的“问题”是指SQL操作中遇到了什么问题**。比如这里的问题是“NOT FOUND”意思就是游标走到结果集的最后没有记录了。也就是说数据集中的所有记录都已经处理完了。
1. 执行的操作是“SET done=TRUE”done是我定义的用来标识数据集中的数据是否已经处理完成的一个标记。done=TRUE意思是数据处理完成了。
1. **处理方式有2种选择分别是“CONTINUE”和“EXIT”**,表示遇到问题,执行了语法结构中的“操作”之后,是选择继续运行程序,还是选择退出,结束程序。
所以,这个条件处理语句的意思就是:当游标读到结果集的最后,没有记录了,设置操作完成标识为真,然后继续运行程序。
在存储过程的第5步为了逐一处理每一条记录我还使用了流程控制语句。
解决复杂问题不可能通过一个SQL语句完成我们需要执行多个SQL操作。流程控制语句的作用就是控制存储过程中SQL语句的执行顺序是我们完成复杂操作必不可少的一部分。下面我就给你具体讲解一下。
## 流程控制语句
MySQL的流程控制语句也只能用于存储程序。主要有3类。
1. 跳转语句ITERATE和LEAVE语句。
1. 循环语句LOOP、WHILE和REPEAT语句。
1. 条件判断语句IF语句和CASE语句。
接下来我依次讲解一下跳转语句、循环语句和条件判断语句。
### 跳转语句
1. ITERATE语句只能用在循环语句内表示重新开始循环。
1. LEAVE语句可以用在循环语句内或者以BEGIN和END包裹起来的程序体内表示跳出循环或者跳出程序体的操作。
### 循环语句
LOOP语句的语法结构是
```
标签LOOP
操作
END LOOP 标签;
```
关于这个语句需要注意的是LOOP循环不能自己结束需要用跳转语句ITERATE或者LEAVE来进行控制。
WHILE语句的语法结构
```
WHILE 条件 DO
操作
END WHILE;
```
WHILE循环通过判断条件是否为真来决定是否继续执行循环中的操作你要注意一点**WHILE循环是先判断条件再执行循环体中的操作**。
REPEAT语句的语法结构
```
REPEAT
操作
UNTIL 条件 END REPEAT
```
REPEAT循环也是通过判断条件是否为真来决定是否继续执行循环内的操作的与WHILE不同的是**REPEAT循环是先执行操作后判断条件**。
最后我来讲讲条件判断语句IF语句和CASE语句。
### 条件判断语句
IF语句的语法结构是
```
IF 表达式1 THEN 操作1
[ELSEIF 表达式2 THEN 操作2]……
[ELSE 操作N]
END IF
```
这里“[]”中的内容是可选的。**IF语句的特点是不同的表达式对应不同的操作**。
CASE语句的语法结构是
```
CASE 表达式
WHEN 值1 THEN 操作1
[WHEN 值2 THEN 操作2]……
[ELSE 操作N]
END CASE;
```
这里“[]”中的内容是可选的。CASE语句的特点是表达式不同的值对应不同的操作。
到这里,我们处理进货单验收的存储过程就创建好了。现在,让我们来运行一下这个存储过程,看看能不能得到我们想要的结果:
```
mysql&gt; CALL demo.mytest(1234); -- 调用存储过程验收单号是1234的进货单
Query OK, 0 rows affected (11.68 sec) -- 执行成功了
mysql&gt; select * from demo.inventory; -- 查看库存,已经改过来了
+---------+------------+-------------+
| stockid | itemnumber | invquantity |
+---------+------------+-------------+
| 1 | 1 | 15.000 |
| 1 | 2 | 23.000 |
+---------+------------+-------------+
2 rows in set (0.00 sec)
mysql&gt; select * from demo.goodsmaster; -- 查看商品信息表,平均进价也改过来了
+------------+---------+-----------+---------------+------+------------+----------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice | avgimportprice |
+------------+---------+-----------+---------------+------+------------+----------------+
| 1 | 0001 | 书 | 16开 | 本 | 89.00 | 31.00 |
| 2 | 0002 | 笔 | NULL | 包 | 5.00 | 2.87 |
+------------+---------+-----------+---------------+------+------------+----------------+
2 rows in set (0.00 sec)
```
很显然,库存和平均价格都被正确地计算出来了。
最后,有个小问题要提醒你注意:**如果一个操作要用到另外一个操作的结果,那我们一定不能搞错操作的顺序**。比如,在刚刚的例子中,我是先计算平均价格,后消减库存数量,这就是因为,计算平均价格的时候会用到库存数量,如果先消减库存数量,平均价格的计算就会不准。
# 总结
这节课我们学习了游标的使用方法包括在存储过程中使用游标的4个步骤分别是定义游标、打开游标、读取游标数据和关闭游标。除此之外我还介绍了经常与游标结合使用的流程控制语句包括循环语句LOOP、WHILE和REPEAT条件判断语句IF和CASE还有跳转语句LEAVE和ITERATE。
游标是MySQL的一个重要的功能为逐条读取结果集中的数据提供了完美的解决方案。跟在应用层面实现相同的功能相比游标可以在存储程序中使用效率高程序也更加简洁。但是游标会消耗系统资源所以我建议你养成用完之后就关闭的习惯这样才能提高系统的整体效率。
# 思考题
假设我有一个数据表demo.test具体信息如下所示
<img src="https://static001.geekbang.org/resource/image/a6/44/a62ccd9ea5e88cfa793dc837a1168f44.jpeg" alt="">
你能自己写一个简单的存储过程,用游标来逐一处理一个数据表中的数据吗?
要求编号为偶数的记录myquant=myquant+1编号是奇数的记录myquant=myquant+2。
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,431 @@
<audio id="audio" title="17 | 触发器:如何让数据修改自动触发关联操作,确保数据一致性?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/97/81/97d0bcc17f014yy02b4e5416ff27e981.mp3"></audio>
你好,我是朱晓峰。今天,我来和你聊一聊触发器。
在实际开发中我们经常会遇到这样的情况有2个或者多个相互关联的表如商品信息和库存信息分别存放在2个不同的数据表中我们在添加一条新商品记录的时候为了保证数据的完整性必须同时在库存表中添加一条库存记录。
这样一来,我们就必须把这两个关联的操作步骤写到程序里面,而且要用事务包裹起来,确保这两个操作成为一个原子操作,要么全部执行,要么全部不执行。要是遇到特殊情况,可能还需要对数据进行手动维护,这样就很容易忘记其中的一步,导致数据缺失。
这个时候,其实咱们可以使用触发器。你可以创建一个触发器,让商品信息数据的插入操作自动触发库存数据的插入操作。这样一来,就不用担心因为忘记添加库存数据而导致的数据缺失了。
听上去好像很不错,那触发器到底怎么使用呢?接下来,我就重点给你聊聊。我会先给你讲解创建、查看和删除触发器的具体操作,然后借助一个案例带你实战一下。
## 如何操作触发器?
首先,咱们来学习下触发器的基本操作。
### 创建触发器
创建触发器的语法结构是:
```
CREATE TRIGGER 触发器名称 {BEFORE|AFTER} {INSERT|UPDATE|DELETE}
ON 表名 FOR EACH ROW 表达式;
```
在创建时,你一定要注意触发器的三个要素。
- 表名:表示触发器监控的对象。
- INSERT|UPDATE|DELETE表示触发的事件。INSERT表示插入记录时触发UPDATE表示更新记录时触发DELETE表示删除记录时触发。
- BEFORE|AFTER表示触发的时间。BEFORE表示在事件之前触发AFTER表示在事件之后触发。
只有把这三个要素定义好,才能正确使用触发器。
创建好触发器之后,咱们还要知道触发器是不是创建成功了。怎么查看呢?我来介绍下。
### 查看触发器
查看触发器的语句是:
```
SHOW TRIGGERS\G;
```
### 删除触发器
删除触发器很简单,你只要知道语法结构就可以了:
```
DROP TRIGGER 触发器名称;
```
知道了触发器的操作方法,接下来咱们就借助超市项目的实际案例,在真实的场景中实战一下,毕竟,实战是掌握操作的最好方法。
## 案例讲解
超市项目实际实施过程中,客户经常要查询储值余额变动的明细,但是,查询会员消费流水时,存在数据汇总不及时、查询速度比较慢的问题。这时,我们就想到用触发器,及时把会员储值金额的变化信息记录到一个专门的表中。
我先用咱们熟悉的SQL语句来实现记录储值金额变动的操作后面再带你使用触发器来操作。通过两种操作的对比你就能更好地理解在什么情况下触发器能够比普通的SQL语句更加简洁高效从而帮助你用好触发器这个工具提高开发的能力。
下面我就借助具体数据来详细说明一下。这里我们需要用到会员信息表demo.membermaster和会员储值历史表demo.deposithist
会员信息表:
<img src="https://static001.geekbang.org/resource/image/26/34/26166082e21c2cbd38e4c21d03e76734.jpeg" alt="">
会员储值历史表:
<img src="https://static001.geekbang.org/resource/image/f0/c5/f0b59f65801543af6c52b208c71925c5.jpeg" alt="">
假如在2020年12月20日这一天会员编号是2的会员李四到超市的某家连锁店购买了一条烟消费了150元。现在我们用之前学过的SQL语句把这个会员储值余额的变动情况记录到会员储值历史表中。
第一步查询出编号是2的会员卡的储值金额是多少。我们可以用下面的代码来实现
```
mysql&gt; SELECT memberdeposit
-&gt; FROM demo.membermaster
-&gt; WHERE memberid = 2;
+---------------+
| memberdeposit |
+---------------+
| 200.00 |
+---------------+
1 row in set (0.00 sec)
```
第二步我们把会员编号是2的会员的储值金额减去150。
```
mysql&gt; UPDATE demo.membermaster
-&gt; SET memberdeposit = memberdeposit - 150
-&gt; WHERE memberid = 2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
第三步读出会员编号是2的会员当前的储值金额。
```
mysql&gt; SELECT memberdeposit
-&gt; FROM demo.membermaster
-&gt; WHERE memberid = 2;
+---------------+
| memberdeposit |
+---------------+
| 50.00 |
+---------------+
1 row in set (0.00 sec)
```
第四步,把会员编号和前面查询中获得的储值起始金额、储值余额和储值金额变化值,写入会员储值历史表。
```
mysql&gt; INSERT INTO demo.deposithist
-&gt; (
-&gt; memberid,
-&gt; transdate,
-&gt; oldvalue,
-&gt; newvalue,
-&gt; changedvalue
-&gt; )
-&gt; SELECT 2,NOW(),200,50,-150;
Query OK, 1 row affected (0.02 sec)
Records: 1 Duplicates: 0 Warnings: 0
```
这样,我们就完成了记录会员储值金额变动的操作。现在,我们来查询一下记录的结果:
```
mysql&gt; SELECT *
-&gt; FROM demo.deposithist;
+----+----------+---------------------+----------+----------+--------------+
| id | memberid | transdate | oldvalue | newvalue | changedvalue |
+----+----------+---------------------+----------+----------+--------------+
| 1 | 2 | 2020-12-20 10:37:51 | 200.00 | 50.00 | -150.00 |
+----+----------+---------------------+----------+----------+--------------+
1 row in set (0.00 sec)
```
结果显示会员编号是2的会员卡储值金额在2020-12-20 10:37:51时有变动变动前是200元变动后是50元减少了150元。
你看这个记录会员储值金额变动的操作非常复杂我们用了4步才完成。而且为了确保数据的一致性我们还要用事务把这几个关联的操作包裹起来这样一来消耗的资源就比较多。
那如果用触发器来实现,效果会怎样呢?我们来实操一下。
首先我们创建一个数据表demo.membermaster的触发器。每当更新表中的数据时先触发触发器如果发现会员储值金额有变化就把会员编号信息、更新的时间、更新前的储值金额、更新后的储值金额和变动金额写入会员储值历史表。然后再执行会员信息表的更新操作。
创建触发器的代码如下所示:
```
DELIMITER //
CREATE TRIGGER demo.upd_membermaster BEFORE UPDATE -- 在更新前触发
ON demo.membermaster
FOR EACH ROW -- 表示每更新一条记录,触发一次
BEGIN -- 开始程序体
IF (new.memberdeposit &lt;&gt; old.memberdeposit) -- 如果储值金额有变化
THEN
INSERT INTO demo.deposithist
(
memberid,
transdate,
oldvalue,
newvalue,
changedvalue
)
SELECT
NEW.memberid,
NOW(),
OLD.memberdeposit, -- 更新前的储值金额
NEW.memberdeposit, -- 更新后的储值金额
NEW.memberdeposit-OLD.memberdeposit; -- 储值金额变化值
END IF;
END
//
DELIMITER ;
```
创建完成之后,我们查看一下触发器,看看是不是真的创建成功了:
```
mysql&gt; SHOW TRIGGERS \G;
*************************** 1. row ***************************
Trigger: upd_membermaster -- 触发器名称
Event: UPDATE -- 触发的事件
Table: membermaster -- 表名称
Statement: BEGIN -- 被触发时要执行的程序体
IF (new.memberdeposit &lt;&gt; old.memberdeposit) -- 储值金额变动时插入历史储值表
THEN
INSERT INTO demo.deposithist
(
memberid,
transdate,
oldvalue,
newvalue,
changedvalue
)
SELECT
NEW.memberid,
NOW(),
OLD.memberdeposit,
NEW.memberdeposit,
NEW.memberdeposit-OLD.memberdeposit;
END IF;
END
Timing: BEFORE
Created: 2021-04-03 15:02:48.18
sql_mode: STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
Definer: root@localhost
character_set_client: utf8mb4
collation_connection: utf8mb4_0900_ai_ci
Database Collation: utf8mb4_0900_ai_ci
1 row in set (0.00 sec)
```
从代码中,我们可以知道触发器的具体信息:
- Trigger表示触发器名称这里是upd_membermaster
- Event表示触发事件这里是UPDATE表示更新触发
- Table表示定义触发器的数据表这里是membermaster
- Statement表示触发时要执行的程序体。
看到这些信息,我们就可以确认,触发器创建成功了。
创建成功以后我们尝试更新一下会员编号是1的会员的储值金额这里假设把会员1的储值金额增加10元。简单说明一下会员也可以把钱存到会员卡里需要的时候进行消费对应的就是储值金额的增加。
我们用下面的代码来完成这个操作:
```
mysql&gt; UPDATE demo.membermaster
-&gt; SET memberdeposit = memberdeposit + 10
-&gt; WHERE memberid = 1;
Query OK, 1 row affected (0.03 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
现在,我们来查看一下会员信息表和会员储值历史表:
```
mysql&gt; SELECT *
-&gt; FROM demo.membermaster;
+----------+----------+------------+---------+-------------+---------------+
| memberid | cardno | membername | address | phone | memberdeposit |
+----------+----------+------------+---------+-------------+---------------+
| 1 | 10000001 | 张三 | 北京 | 13812345678 | 110.00 |
| 2 | 10000002 | 李四 | 天津 | 18512345678 | 50.00 |
+----------+----------+------------+---------+-------------+---------------+
2 rows in set (0.00 sec)
mysql&gt; SELECT *
-&gt; FROM demo.deposithist;
+----+----------+---------------------+----------+----------+--------------+
| id | memberid | transdate | oldvalue | newvalue | changedvalue |
+----+----------+---------------------+----------+----------+--------------+
| 1 | 2 | 2020-12-20 10:37:51 | 200.00 | 50.00 | -150.00 |
| 2 | 1 | 2020-12-20 11:32:09 | 100.00 | 110.00 | 10.00 |
+----+----------+---------------------+----------+----------+--------------+
2 rows in set (0.01 sec)
```
结果显示,触发器正确地记录了修改会员储值金额的操作。
如果你一直跟着我进行操作到这里你可能会有疑问在会员历史储值表中记录修改会员储值金额的操作和实际修改会员信息表是2个操作有没有可能一个成功一个失败比如说记录修改会员储值金额的操作失败了但是会员的储值金额被修改了。
其实在MySQL中如果触发器中的操作失败了那么触发这个触发器的数据操作也会失败不会出现一个成功、一个失败的情况。
我还是借助一个例子来解释下。假设我们创建了一个触发器这个触发器的程序体中的SQL语句有误比如多了一个字段。在这种情况下触发器执行会失败因此数据更新的操作也不会执行。我们用下面的代码来验证一下
```
DELIMITER //
CREATE TRIGGER demo.upd_membermaster BEFORE UPDATE
ON demo.membermaster
FOR EACH ROW
BEGIN
IF (new.memberdeposit &lt;&gt; old.memberdeposit)
THEN
INSERT INTO demo.deposithist
(
aa, -- 不存在的字段
memberid,
transdate,
oldvalue,
newvalue,
changedvalue
)
SELECT
1, -- 给不存在的字段赋值
NEW.memberid,
NOW(),
OLD.memberdeposit,
NEW.memberdeposit,
NEW.memberdeposit-OLD.memberdeposit;
END IF;
END
//
DELIMITER ;
```
现在假设我们要把会员编号是2的会员卡的储值金额更新为20
```
mysql&gt; update demo.membermaster set memberdeposit=20 where memberid = 2;
ERROR 1054 (42S22): Unknown column 'aa' in 'field list'
```
系统提示因为字段“aa”不存在导致触发器执行失败。现在我们来看看会员储值金额有没有被修改
```
mysql&gt; select * from demo.membermaster;
+----------+----------+------------+---------+-------------+---------------+
| memberid | cardno | membername | address | phone | memberdeposit |
+----------+----------+------------+---------+-------------+---------------+
| 1 | 10000001 | 张三 | 北京 | 13812345678 | 110.00 |
| 2 | 10000002 | 李四 | 天津 | 18512345678 | 50.00 |
+----------+----------+------------+---------+-------------+---------------+
2 rows in set (0.00 sec)
```
结果显示会员储值金额不变。这个时候为了让应用程序知道触发器是否执行成功我们可以通过ROW_COUNT()函数来发现错误:
```
mysql&gt; select row_count();
+-------------+
| row_count() |
+-------------+
| -1 |
+-------------+
1 row in set (0.00 sec)
```
结果是-1说明我们可以通过ROW_COUNT()函数捕获到错误。
我们回顾一下对于记录会员储值金额变化的操作可以通过应用层发出SQL语句指令或者用一个存储过程来实现。无论哪种方式都要通过好几步相互关联的操作而且要做成一个事务处理过程复杂消耗的资源也较多。如果用触发器效率就会提高很多消耗的资源也少。同时还可以起到事务的类似功能保证关联操作全部完成或全部失败。
## 触发器的优缺点
通过刚刚的案例,你应该感受到触发器的高效了。现在,咱们把视角拔高一下,来看看触发器具体都有什么优缺点。毕竟,知己知彼,才能百战不殆。只有非常清楚它的优缺点,你才能充分发挥它的作用。
我先来说说触发器的优点。
**首先,触发器可以确保数据的完整性**。这是怎么体现的呢?我来举个小例子。
假设我们用进货单头表demo.importhead来保存进货单的总体信息包括进货单编号、供货商编号、仓库编号、总计进货数量、总计进货金额和验收日期。
<img src="https://static001.geekbang.org/resource/image/5b/e8/5b36cf11405b0159a1388ee23639aee8.jpeg" alt="">
用进货单明细表demo.importdetails来保存进货商品的明细包括进货单编号、商品编号、进货数量、进货价格和进货金额。
<img src="https://static001.geekbang.org/resource/image/b8/0e/b8e3fc8e001da9af2c85c22be5e6780e.jpeg" alt="">
每当我们录入、删除和修改一条进货单明细数据的时候,进货单明细表里的数据就会发生变动。这个时候,在进货单头表中的总计数量和总计金额就必须重新计算,否则,进货单头表中的总计数量和总计金额就不等于进货单明细表中数量合计和金额合计了,这就是数据不一致。
为了解决这个问题我们就可以使用触发器规定每当进货单明细表有数据插入、修改和删除的操作时自动触发2步操作
1. 重新计算进货单明细表中的数量合计和金额合计;
1. 用第一步中计算出来的值更新进货单头表中的合计数量与合计金额。
这样一来,进货单头表中的合计数量与合计金额的值,就始终与进货单明细表中计算出来的合计数量与合计金额的值相同,数据就是一致的,不会互相矛盾。
**其次,触发器可以帮助我们记录操作日志。**
利用触发器,可以具体记录什么时间发生了什么。我们前面的记录修改会员储值金额的触发器,就是一个很好的例子。这对我们还原操作执行时的具体场景,更好地定位问题原因很有帮助。
**另外,触发器还可以用在操作数据前,对数据进行合法性检查。**
举个小例子。超市进货的时候,需要库管录入进货价格。但是,人为操作很容易犯错误,比如说在录入数量的时候,把条形码扫进去了;录入金额的时候,看串了行,录入的价格远超售价,导致账面上的巨亏……这些都可以通过触发器,在实际插入或者更新操作之前,对相应的数据进行检查,及时提示错误,防止错误数据进入系统。
说了这么多触发器的优点,那是不是所有事件可以驱动的操作,都应该用触发器呢?要是你这么想,就掉坑里了。
下面我来说说触发器的缺点。
**触发器最大的一个问题就是可读性差。**
因为触发器存储在数据库中,并且由事件驱动,这就意味着触发器有可能不受应用层的控制。这对系统维护是非常有挑战的。
这是啥意思呢?我举个例子,你一看就明白了。
还是拿我们创建触发器时讲到的修改会员储值操作的那个触发器为例。如果触发器中的操作出了问题,会导致会员储值金额更新失败。我用下面的代码演示一下:
```
mysql&gt; update demo.membermaster set memberdeposit=20 where memberid = 2;
ERROR 1054 (42S22): Unknown column 'aa' in 'field list'
```
结果显示系统提示错误字段“aa”不存在。
这是因为触发器中的数据插入操作多了一个字段系统提示错误。可是如果你不了解这个触发器很可能会认为是更新语句本身的问题或者是会员信息表的结构出了问题。说不定你还会给会员信息表添加一个叫“aa”的字段试图解决这个问题结果只能是白费力。
另外,相关数据的变更,特别是数据表结构的变更,都可能会导致触发器出错,进而影响数据操作的正常运行。这些都会由于触发器本身的隐蔽性,影响到应用中错误原因排查的效率。
## 总结
今天这节课,我给你介绍了如何操作触发器。为了方便你学习,我汇总了相关的语法结构:
```
创建触发器的语法结构是
CREATE TRIGGER 触发器名称 {BEFORE|AFTER} {INSERT|UPDATE|DELETE}
ON 表名 FOR EACH ROW 表达式;
查看触发器的语句是:
SHOW TRIGGERS\G;
删除触发器的语法结构是:
DROP TRIGGER 触发器名称;
```
除此之外,我们还学习了触发器的优缺点。它的优点是可以确保数据一致性、记录操作日志和检查数据合法性。不过,它也存在可读性差,会增加系统维护的成本的缺点。在使用触发器的时候,你一定要综合考量。
最后,我还想再给你提一个小建议:**维护一个完整的数据库设计文档**。因为运维人员可能会经常变动,如果有一个完整的数据库设计文档,就可以帮助新人快速了解触发器的设计思路,从而减少错误,降低系统维护的成本。
## 思考题
我在课程中提到,每当在进货单明细表中插入或修改数据的时候,都要更新进货单头表中的总计数量和总计金额,这个问题可以用触发器来解决。你能不能创建一个触发器,要求是当操作人员更新进货单明细表中相关数据的时候,自动触发对进货单头表中相关数据的更改,确保数据的一致性?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,316 @@
<audio id="audio" title="18 | 权限管理:如何控制数据库访问,消除安全隐患?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d4/33/d4c1ce6debd0c28a2272429250a58433.mp3"></audio>
你好,我是朱晓峰,今天,我来和你聊一聊权限管理。
我们在开发应用的时候,经常会遇到一种需求,就是要根据用户的不同,对数据进行横向和纵向的分组。
所谓横向的分组,就是指用户可以接触到的数据的范围,比如可以看到哪些表的数据;所谓纵向的分组,就是指用户对接触到的数据能访问到什么程度,比如能看、能改,甚至是删除。
我们把具有相同数据访问范围和程度的用户归成不同的类别,这种类别就叫角色。通过角色,管理用户对数据库访问的范围和程度就更加方便了。这也就是对用户的数据访问权限的管理。
**恰当的权限设定,可以确保数据的安全性,这是至关重要的**
那么,怎么进行权限管理呢?这节课,我就结合超市项目的实际案例,给你讲一下权限管理的具体操作,包括怎么操作角色和用户,怎么通过角色给用户授权,怎么直接给用户授权,从而帮助你管理好用户的权限,提升数据库的安全性。
下面我就来先讲讲角色。我们可以通过角色对相同权限的用户进行分组管理,这样可以使权限管理更加简单高效。
## 角色的作用
角色是在MySQL 8.0中引入的新功能,相当于一个权限的集合。引入角色的目的是方便管理拥有相同权限的用户。
下面我举个超市项目中的例子,来具体说明一下如何通过角色管理用户权限。
超市项目中有库管、营运和财务等不同的模块它们各自对应不同的数据表。比如库存模块中的盘点表demo.invcount、营运模块中的商品信息表demo.goodsmaster还有财务模块中的应付账款表demo.settlement。下面是这些表的具体信息。
盘点表:
<img src="https://static001.geekbang.org/resource/image/81/28/812f141d6f1f7fb85a2c797a3c666528.jpeg" alt="">
商品信息表:
<img src="https://static001.geekbang.org/resource/image/00/dd/00151b4b837c70fc5d7516b3307cf8dd.jpeg" alt="">
应付账款表:
<img src="https://static001.geekbang.org/resource/image/81/c6/81c3b5cd0937fca32b0a54847170cec6.jpeg" alt="">
在超市项目中,员工的职责不同,包括库管、营运和财务等,不同的职责有不同的数据访问权限。比如:
- 张三是库管,他就可以查询商品信息表,对盘点表有增删改查的权限,但无权访问应付账款表;
- 李四是营运,他就拥有对商品信息表有增删改查的权限,而对库存表和应付账款表,只有查看的权限;
- 王五是财务,他就有对应付账款表有增删改查的权限,对商品信息表和库存表,只有查看的权限。
所以,我们需要为每一个职责创建一个对应的角色,为每个员工创建一个对应的数据库用户。然后通过给角色赋予相关的权限,再把角色赋予用户,实现对超市员工访问数据权限的管理,从而保证数据的安全性。
这样说有点抽象,下面我们具体操作一下角色和用户。
## 如何操作角色?
首先,我们要创建一个角色,为后面的授权做好准备。
### 如何创建角色?
MySQL中的角色名称由角色名称加主机名称组成。创建角色的语法结构如下
```
CREATE ROLE 角色名;
```
假设我们现在需要创建一个经理的角色,就可以用下面的代码:
```
mysql&gt; CREATE ROLE 'manager'@'localhost';
Query OK, 0 rows affected (0.06 sec)
```
这里的意思是创建一个角色角色名称是“manager”角色可以登录的主机是“localhost”意思是只能从数据库服务器运行的这台计算机登录这个账号。你也可以不写主机名直接创建角色“manager”
```
mysql&gt; CREATE ROLE 'manager';
Query OK, 0 rows affected (0.01 sec)
```
如果不写主机名MySQL默认是通配符“%”,意思是这个账号可以从任何一台主机上登录数据库。
同样道理,如果我们要创建库管的角色,就可以用下面的代码:
```
mysql&gt; CREATE ROLE 'stocker';
Query OK, 0 rows affected (0.02 sec)
```
**创建角色之后,默认这个角色是没有任何权限的,我们需要给角色授权**
### 怎么给角色赋予权限?
给角色授权的语法结构是:
```
GRANT 权限 ON 表名 TO 角色名;
```
假设我们现在想给经理角色授予商品信息表、盘点表和应付账款表的只读权限,就可以用下面的代码来实现:
```
mysql&gt; GRANT SELECT ON demo.settlement TO 'manager';
Query OK, 0 rows affected (0.03 sec)
mysql&gt; GRANT SELECT ON demo.goodsmaster TO 'manager';
Query OK, 0 rows affected (0.01 sec)
mysql&gt; GRANT SELECT ON demo.invcount TO 'manager';
Query OK, 0 rows affected (0.01 sec)
```
如果我们需要赋予库管角色盘点表的增删改查权限、商品信息表的只读权限,对应付账款表没有权限,就可以这样:
```
mysql&gt; GRANT SELECT,INSERT,DELETE,UPDATE ON demo.invcount TO 'stocker';
Query OK, 0 rows affected (0.02 sec)
mysql&gt; GRANT SELECT ON demo.goodsmaster TO 'stocker';
Query OK, 0 rows affected (0.02 sec)
```
### 查看角色权限
赋予角色权限之后我们可以通过SHOW GRANTS语句来查看权限是否创建成功了
```
mysql&gt; SHOW GRANTS FOR 'manager';
+-------------------------------------------------------+
| Grants for manager@% |
+-------------------------------------------------------+
| GRANT USAGE ON *.* TO `manager`@`%` |
| GRANT SELECT ON `demo`.`goodsmaster` TO `manager`@`%` |
| GRANT SELECT ON `demo`.`invcount` TO `manager`@`%` |
| GRANT SELECT ON `demo`.`settlement` TO `manager`@`%` |
+-------------------------------------------------------+
4 rows in set (0.00 sec)
```
只要你创建了一个角色系统就会自动给你一个“USAGE”权限意思是连接登录数据库的权限。代码的最后三行代表了我们给角色“manager”赋予的权限也就是对商品信息表、盘点表和应付账款表的只读权限。
再来看看库管角色的权限:
```
mysql&gt; SHOW GRANTS FOR 'stocker';
+----------------------------------------------------------------------------+
| Grants for stocker@% |
+----------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `stocker`@`%` |
| GRANT SELECT ON `demo`.`goodsmaster` TO `stocker`@`%` |
| GRANT SELECT, INSERT, UPDATE, DELETE ON `demo`.`invcount` TO `stocker`@`%` |
+----------------------------------------------------------------------------+
3 rows in set (0.00 sec)
```
结果显示,库管角色拥有商品信息表的只读权限和盘点表的增删改查权限。
当我们需要对业务重新整合的时候,可能就需要对之前创建的角色进行清理,删除一些不会再使用的角色。
### 删除角色
删除角色的操作很简单,你只要掌握语法结构就行了。
```
DROP ROLE 角色名称;
```
这个操作十分简单,我就不多说了。
到这里,关于角色的操作,我就介绍完了,下面我们来看看关于用户的操作。
## 如何操作用户?
我们可以把数据库中的角色看作是一个权限集。角色本身不能操作数据,这个任务还是要靠用户来完成。在操作用户前,我们先要创建一个用户。
### 创建用户
创建用户的语法结构是这样的:
```
CREATE USER 用户名 [IDENTIFIED BY 密码];
```
“[ ]”表示可选,也就是说,可以指定用户登录时需要密码验证,也可以不指定密码验证,这样用户可以直接登录。不过,不指定密码的方式不安全,不推荐使用。
举个例子假设我们要给张三创建一个用户用户名是“zhangsan”密码是“mysql”可以通过下面的代码来实现
```
mysql&gt; CREATE USER 'zhangsan' IDENTIFIED BY 'mysql';
Query OK, 0 rows affected (0.02 sec)
```
这样,张三的用户就创建成功了。
### 给用户授权
给用户授权的方式有2种分别是通过把角色赋予用户给用户授权和直接给用户授权。
通过把角色赋予用户给用户授权的语法结构如下:
```
GRANT 角色名称 TO 用户名称;
```
举个小例子,我们想要给张三赋予库管的角色,可以通过下面的代码实现:
```
mysql&gt; GRANT 'stocker' TO 'zhangsan';
Query OK, 0 rows affected (0.01 sec)
```
我们也可以直接给用户授权,语法结构如下:
```
GRANT 权限 ON 表名 TO 用户名;
```
这种方式简单直接,我就不多说了。下面我们来查看一下这个用户的权限有哪些。
### 查看用户权限
查看用户权限的语法结构是:
```
SHOW GRANTS FOR 用户名;
```
我们可以通过下面的代码来查看张三的权限:
```
mysql&gt; SHOW GRANTS FOR 'zhangsan';
+---------------------------------------+
| Grants for zhangsan@% |
+---------------------------------------+
| GRANT USAGE ON *.* TO `zhangsan`@`%` |
| GRANT `stocker`@`%` TO `zhangsan`@`%` |
+---------------------------------------+
2 rows in set (0.00 sec)
```
结果显示,张三拥有库管角色的权限。
说到这里,我必须要提醒你一个常见的坑。
如果现在你用张三的这个用户去登录你会发现这个账号是没有任何权限的。你是不是觉得很奇怪我不是把角色“stocker”赋予用户“zhangsan”了吗那用户“zhangsan”应该有角色“stocker”的权限啊。其实这是因为**MySQL中创建了角色之后默认都是没有被激活的**,也就是不能用,必须要用下面的语句激活:
```
SET global activate_all_roles_on_login=ON;
```
这条SQL语句的意思是对所有角色永久激活。运行这条语句之后用户“zhangsan”才真正拥有了角色“stocker”的所有权限。
下面我们就用张三的账号登录,确认一下他有没有相应的权限:
```
H:\&gt;mysql -u zhangsan -p
Enter password: *****
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 24
Server version: 8.0.23 MySQL Community Server - GPL
Copyright (c) 2000, 2021, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql&gt; select * from demo.goodsmaster;
+------------+---------+-----------+---------------+------+------------+----------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice | avgimportprice |
+------------+---------+-----------+---------------+------+------------+----------------+
| 1 | 0001 | 书 | 16开 | 本 | 89.00 | 31.00 |
| 2 | 0002 | 笔 | NULL | 包 | 5.00 | 2.87 |
+------------+---------+-----------+---------------+------+------------+----------------+
2 rows in set (0.02 sec)
```
结果显示,我们可以正常登录,并且可以查询商品信息表的内容。
### 删除用户
当用户不再使用的时候,我们也可以删除用户。操作起来很简单,你只要知道语法结构就行了:
```
DROP USER 用户名;
```
## 总结
今天这节课,我们学习了权限管理的方法,包括如何操作角色和用户,如何通过角色给用户授权,如何直接给用户授权等。
角色是权限的集合。你可以直接给用户授予访问数据库的权限,也可以通过把角色授予用户,从而把角色对数据库的访问权限全部授予给用户。而用户是数据库的使用者,我们可以通过给用户授予访问数据库中资源的权限,来控制使用者对数据库的访问,消除安全隐患。
需要注意的是,角色在刚刚创建出来的时候,默认是没有激活的,需要手动激活,才可以使用。如果你把角色赋予了用户,那么用户就拥有了角色的全部权限。但是,如果你删除了角色,那么用户也就失去了通过这个角色所获得的所有权限。
我知道有一些程序员喜欢使用Root超级用户来访问数据库完全把权限控制放在应用层面实现。这样当然也是可以的。不过我建议你尽量使用数据库自己的角色和用户机制来控制访问权限不要轻易用Root账号。因为Root账号密码放在代码里面不安全一旦泄露数据库就会完全失去保护。而且MySQL的权限控制功能十分完善应该尽量利用可以提高效率而且安全可靠。
## 思考题
在今天的课里我举了一个例子提到超市运营中的一个职责“财务”。财务可以对商品信息表、盘点表有只读的权限对应付账款表有增删改查的权限。请你设计一个角色财务“accountant”具备这些权限。给会计“李四”创建一个用户账号“lisi”使李四通过财务的角色获得对应付账款表增删改查的权限和对商品信息表、盘点表有只读的权限。
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,251 @@
<audio id="audio" title="19 | 日志(上):系统出现问题,如何及时发现?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/94/84/940866d2267e4859005225614ae40184.mp3"></audio>
你好,我是朱晓峰。
我们曾经开发过一个数据库应用系统,但是却突然遭遇了数据库宕机。在这种情况下,定位宕机的原因就非常关键,毕竟,知道了问题,才能确定解决方案。
这时我们就想到了查看数据库的错误日志因为日志中记录了数据库运行中的诊断信息包括了错误、警告和注释等信息。从日志中我们发现原来某个连接中的SQL操作发生了死循环导致内存不足被系统强行终止了。知道了原因处理起来也就比较轻松了系统很快就恢复了运行。
除了发现错误,日志在数据复制、数据恢复、操作审计,以及确保数据的永久性和一致性等方面,都有着不可替代的作用,对提升你的数据库应用的开发能力至关重要。
今天,我就结合超市项目的实际案例,给你讲解一下怎么通过查看系统日志,来了解数据库中实际发生了什么,从而快速定位原因。
MySQL的日志种类非常多包括通用查询日志、慢查询日志、错误日志、二进制日志、中继日志、重做日志和回滚日志内容比较多而且都很重要所以我们来花两节课的时间学习一下。
这节课,我会先具体讲一讲通用查询日志、慢查询日志和错误日志。
## 通用查询日志
**通用查询日志记录了所有用户的连接开始时间和截止时间以及发给MySQL数据库服务器的所有SQL指令**。当我们的数据发生异常时,开启通用查询日志,还原操作时的具体场景,可以帮助我们准确定位问题。
举个小例子在超市项目实施的过程中我们曾遇到过这样一件事超市经营者月底查账的时候发现超市的1号门店在12月1日销售了5件化妆品但是当天对应的历史库存并没有减少。化妆品的金额都比较大库存不对的话会在报表查询中产生巨额差异触发到报警机制对超市经营者的决策产生影响。超市经营者找到我们对系统的可靠性提出质疑。
我们对系统进行了仔细检查,没有发现数据问题。可是商品确实卖出去了,当天的历史库存也确实没有消减。这个时候,我们想到了检查通用查询日志,看看当天到底发生了什么。
查看之后我们就复原了当天的情况12月1日下午门店的收银台销售了5件化妆品但是由于网络故障流水没有及时上传到总部。12月1日晚上11:59总部的历史库存被保存下来但是因为没有收到门店的流水所以没有消减库存。12月2日上午门店的网络恢复了流水得以上传总部这个时候对应化妆品的库存才被消减掉。
这样,我们就确定了故障的原因,也就是超市的网络问题,而系统本身是没有问题的。
你看,通用查询日志可以帮助我们了解操作发生的具体时间和操作的细节,对找出异常发生的原因极其关键。
下面我来具体介绍一下控制通用查询日志的系统变量。通过这些变量,你会清楚怎么控制通用查询日志的开启和关闭,以及保存日志的文件是哪个。
```
mysql&gt; SHOW VARIABLES LIKE '%general%';
+------------------+---------------+
| Variable_name | Value |
+------------------+---------------+
| general_log | OFF | -- 通用查询日志处于关闭状态
| general_log_file | GJTECH-PC.log | -- 通用查询日志文件的名称是GJTECH-PC.log
+------------------+---------------+
2 rows in set, 1 warning (0.00 sec)
```
在这个查询的结果中有2点需要我们注意一下。
1. 系统变量general_log的值是OFF表示通用查询日志处于关闭状态。在MySQL中这个参数的默认值是关闭的。因为一旦开启记录通用查询日志MySQL会记录所有的连接起止和相关的SQL操作这样会消耗系统资源并且占用磁盘空间。我们可以通过手动修改变量的值在需要的时候开启日志。
1. 通用查询日志文件的名称是GJTECH-PC.log。这样我们就知道在哪里可以查看通用查询日志的内容了。
下面我们来看看如何开启通用查询日志把所有连接的起止和连接的SQL操作都记录下来。这个操作可以帮助我们追踪SQL操作故障的原因。
### 开启通用查询日志
我们可以通过设置系统变量的值来开启通用查询日志并且指定通用查询日志的文件夹和文件名为“H:\mytest.log”。这个操作如下
```
mysql&gt; SET GLOBAL general_log = 'ON';
Query OK, 0 rows affected (0.00 sec)
mysql&gt; SET @@global.general_log_file = 'H:\mytest.log';
Query OK, 0 rows affected (0.02 sec)
```
为了确认我们的设定是否已经生效,我们再来查询一下通用查询日志的状态:
```
mysql&gt; SHOW VARIABLES LIKE '%general%';
+------------------+--------------+
| Variable_name | Value |
+------------------+--------------+
| general_log | ON | -- 通用查询日志开启
| general_log_file | H:mytest.log | -- 日志名称也改过了
+------------------+--------------+
2 rows in set, 1 warning (0.00 sec)
```
结果显示通用查询日志已经开启文件是“H:\mytest.log”这就意味着我们的操作成功了。
### 查看通用查询日志
通用查询日志都是文本型数据,可以用记事本打开。下面我们就用记事本打开我电脑上的通用查询日志,实际看一看通用查询日志的内容,包括都有哪些连接,什么时候登录了数据库,都做了哪些操作等信息。
```
2021-04-05T06:39:53.621980Z 28 Connect zhangsan@localhost on using SSL/TLS -- 账号zhangsan从本地登录
2021-04-05T06:39:53.622085Z 28 Connect Access denied for user 'zhangsan'@'localhost' (using password: NO) -- 没有使用密码,连接被拒绝了
2021-04-05T06:40:02.522303Z 29 Connect zhangsan@localhost on using SSL/TLS
2021-04-05T06:40:02.522913Z 29 Query select @@version_comment limit 1
2021-04-05T06:40:14.211511Z 29 Query SELECT *
FROM demo.invcount -- 查询数据表demo.invcount内容
2021-04-05T06:40:37.647625Z 29 Query UPDATE demo.invcount
SET plquant = - 5 -- 更新数据表demo.invcount
WHERE itemnumber = 1
2021-04-05T06:41:15.047067Z 29 Query SELECT *
FROM demo.goodsmaster -- 查询数据表demo.goodsmaster
```
在通用查询日志里面我们可以清楚地看到账号“zhangsan”是什么时间登录的服务器登录之后做了什么SQL操作针对的是哪个数据表等信息。
### 删除通用查询日志
当用户对数据库的操作比较频繁时,通用查询日志文件会不断变大。为了节省磁盘空间,我们可以移除旧的日志文件,创建新的日志文件,来对通用查询日志文件进行维护。
第一步,关闭通用查询日志:
```
mysql&gt; SET GLOBAL general_log = 'OFF'; -- 关闭通用查询日志
Query OK, 0 rows affected (0.01 sec)
mysql&gt; SHOW VARIABLES LIKE '%general_log%'; -- 查看通用查询日志状态
+------------------+--------------+
| Variable_name | Value |
+------------------+--------------+
| general_log | OFF |
| general_log_file | H:mytest.log |
+------------------+--------------+
2 rows in set, 1 warning (0.00 sec)
```
第二步把通用查询日志文件“H:\mytest.log”移至备份文件夹空出磁盘H的空间。
第三步,开启通用查询日志:
```
mysql&gt; SET GLOBAL general_log = 'ON';
Query OK, 0 rows affected (0.01 sec)
mysql&gt; SHOW VARIABLES LIKE '%general_log%';
+------------------+--------------+
| Variable_name | Value |
+------------------+--------------+
| general_log | ON |
| general_log_file | H:mytest.log |
+------------------+--------------+
2 rows in set, 1 warning (0.00 sec)
```
这个时候你会发现MySQL已经给我们准备好了一个新的通用查询日志文件“H:\mytest.log”并且记录了我们第一个查询的语句“SHOW VARIABLES LIKE '%general_log%';”。
文件内容如下:
```
C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqld.exe, Version: 8.0.23 (MySQL Community Server - GPL). started with:
TCP Port: 3306, Named Pipe: MySQL
Time Id Command Argument
2021-04-05T07:02:03.007394Z 30 Query SHOW VARIABLES LIKE '%general_log%'
```
总之,开启了通用查询日志之后,如果遇到用户对数据产生质疑的情况,我们就可以通过查看通用查询日志,还原当时的场景,快速定位并解决问题。
## 慢查询日志
**慢查询日志用来记录执行时间超过指定时长的查询**。它的主要作用是帮助我们发现那些执行时间特别长的SQL查询并且有针对性地进行优化从而提高系统的整体效率。当我们的数据库服务器发生阻塞、运行变慢的时候检查一下慢查询日志找到那些慢查询对解决问题很有帮助。
慢查询日志是由MySQL的配置文件进行控制的。下面我先简单介绍一下MySQL的配置文件。
在MySQL的安装目录中C:\ProgramData\MySQL\MySQL Server 8.0我们可以找到MySQL的配置文件“my.ini”。这个文件是一个文本格式的文件可以直接用记事本打开来阅读。
我们来看看配置文件中关于慢查询日志变量的相关设定:
```
slow-query-log=1 -- 表示开启慢查询日志,系统将会对慢查询进行记录。
slow_query_log_file=&quot;GJTECH-PC-slow.log&quot; -- 表示慢查询日志的名称是&quot;GJTECH-PC-slow.log&quot;。这里没有指定文件夹,默认就是数据目录:&quot;C:\ProgramData\MySQL\MySQL Server 8.0\Data&quot;。
long_query_time=10 -- 表示慢查询的标准是查询执行时间超过10秒
```
除了刚刚的这些变量控制慢查询日志的还有一个系统变量min_examined_row_limit。这个变量的意思是查询扫描过的最少记录数。这个变量和查询执行时间共同组成了判别一个查询是否是慢查询的条件。如果查询扫描过的记录数大于等于这个变量的值并且查询执行时间超过long_query_time的值那么这个查询就被记录到慢查询日志中反之则不被记录到慢查询日志中。
如果要查看当前这个系统变量的值,我们就可以用下面的代码:
```
mysql&gt; show variables like 'min%';
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| min_examined_row_limit | 0 |
+------------------------+-------+
1 row in set, 1 warning (0.00 sec)
```
这个值默认是0。与long_query_time=10合在一起表示只要查询的执行时间超过10秒钟哪怕一个记录也没有扫描过都要被记录到慢查询日志中。你也可以根据需要通过修改“my.ini”文件来修改查询时长或者通过SET指令用SQL语句修改“min_examined_row_limit”的值。
只是你要注意如果修改了MySQL的配置文件“my.ini”就需要重启服务器这样才能使修改生效。
来看一个例子之前我运行的一个慢查询被记录到了慢查询日志中。这个例子记录了一个运行时间超过10秒的慢查询的发生时间、连接所属的用户、执行的时长、锁表的时长和扫描过的记录数等相关信息。
```
C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqld.exe, Version: 8.0.23 (MySQL Community Server - GPL). started with:
TCP Port: 3306, Named Pipe: MySQL
Time Id Command Argument
# Time: 2021-03-25T07:20:33.412260Z -- 执行开始时间
# User@Host: root[root] @ localhost [::1] Id: 13 -- 用户
# Query_time: 10.166435 Lock_time: 0.000000 Rows_sent: 0 Rows_examined: 0 -- 执行时长、锁表时长、检查的记录数
use demo;
SET timestamp=1616656823;
```
通过这个慢查询日志的记录,我们就可以发现是哪个查询消耗了大量的系统资源,是哪个连接里面的查询,具体什么时间开始的。有了这些信息,我们就可以对慢查询进行分析,决定优化的方式,避免出现同样的问题。
好了,到这里,通用查询日志和慢查询日志我就讲完了,咱们最后再来学习一种重要的日志:错误日志。
## 错误日志
**错误日志记录了MySQL服务器启动、停止运行的时间以及系统启动、运行和停止过程中的诊断信息包括错误、警告和提示**等。当我们的数据库服务器发生系统故障时,错误日志是发现问题、解决故障的首选。
错误日志默认是开启的。我们可以在MySQL的配置文件“my.ini”中配置它
```
# Error Logging.
log-error=&quot;GJTECH-PC.err&quot;
```
这段代码指定了错误日志的文件名。如果没有指定文件夹默认就是数据目录“C:\ProgramData\MySQL\MySQL Server 8.0\Data”。
下面我们查看一下错误日志的内容:
```
2021-02-28T08:07:07.228880Z 0 [System] [MY-010116] [Server] C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqld.exe (mysqld 8.0.23) starting as process 7652
2021-02-28T08:07:07.270982Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
2021-02-28T08:07:08.116433Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
```
可以看到错误日志文件中记录了服务器启动的时间以及存储引擎InnoDB启动和停止的时间等。
## 总结
今天这节课,我们学习了通用查询日志、慢查询日志和错误日志。
- 通用查询日志:可以记录所有连接的起始时间和终止时间,以及连接发送给数据库服务器的所有指令,对我们复原操作的实际场景、发现问题,甚至是对数据库操作的审计都有很大的帮助。
- 慢查询日志:可以记录运行时间和检查记录数超过指定值的查询,方便我们对查询进行优化。
- 错误日志:它记录了服务器启动、运行和停止过程中的诊断信息,方便我们了解服务器的状态,从而对服务器进行维护。
最后,我还是想提醒你一句,**千万不要小看日志**。很多看似奇怪的问题,答案往往就藏在日志里。很多情况下,只有通过查看日志才能发现问题的原因,真正解决问题。所以,一定要学会查看日志,养成检查日志的习惯。
## 思考题
请你思考一下怎么设置开启慢查询日志并且把慢查询日志的指定时长设置为5秒最少扫描记录数为1
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,472 @@
<audio id="audio" title="20 | 日志(下):系统故障,如何恢复数据?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/92/28/921d3fba4fc361633647b8e679eac928.mp3"></audio>
你好,我是朱晓峰。
上节课咱们学习了通用查询日志、慢查询日志和错误日志它们可以帮助我们快速定位系统问题。但实际上日志也可以帮助我们找回由于误操作而丢失的数据比如二进制日志binary log、中继日志relay log、回滚日志undo log和重做日志redo log
这节课我们就来学习下这4种日志。
## 二进制日志
**二进制日志主要记录数据库的更新事件**,比如创建数据表、更新表中的数据、数据更新所花费的时长等信息。通过这些信息,我们可以再现数据更新操作的全过程。而且,由于日志的延续性和时效性,我们还可以利用日志,完成无损失的数据恢复和主从服务器之间的数据同步。
可以说,二进制日志是进行数据恢复和数据复制的利器。所以,接下来我就结合一个实际案例,重点给你讲一讲怎么操作它。
### 如何操作二进制日志?
操作二进制日志,主要包括查看、刷新二进制日志,用二进制日志恢复数据,以及删除二进制日志。
**1.查看二进制日志**
查看二进制日志主要有3种情况分别是查看当前正在写入的二进制日志、查看所有的二进制日志和查看二进制日志中的所有数据更新事件。
查看当前正在写入的二进制日志的SQL语句是
```
SHOW MASTER STATUS;
```
我们可以通过这条语句,查看当前正在写入的二进制日志的名称和当前写入的位置:
```
mysql&gt; SHOW MASTER STATUS;
+----------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+----------------------+----------+--------------+------------------+-------------------+
| GJTECH-PC-bin.000011 | 2207 | | | |
+----------------------+----------+--------------+------------------+-------------------+ -- 当前正在写入的二进制日志是“GJTECH-PC-bin.000011”当前的位置是2207。
1 row in set (0.00 sec)
```
查看所有的二进制日志的SQL语句是
```
SHOW BINARY LOGS;
```
查看二进制日志中所有数据更新事件的SQL语句是
```
SHOW BINLOG EVENTS IN 二进制文件名;
```
**2.刷新二进制日志**
刷新二进制日志的SQL语句是
```
FLUSH BINARY LOGS;
```
这条语句的意思是关闭服务器正在写入的二进制日志文件并重新打开一个新文件文件名的后缀在现有的基础上加1。
**3.用二进制日志恢复数据**
我们可以用mysqlbinlog工具进行数据恢复
```
mysqlbinlog start-positon=xxx end-position=yyy 二进制文件名 | mysql -u 用户 -p
```
这条命令的意思是执行二进制日志中从位置xxx开始到yyy截止的所有数据更新操作。这里的截止位置也可以不写意思是从位置xxx开始执行二进制文件中的所有数据更新操作。
**4.删除二进制日志**
如果我们已经把日志文件保存到了安全的地方就可以通过下面的SQL语句删除所有二进制日志文件以释放磁盘空间
```
mysql&gt; RESET MASTER;
Query OK, 0 rows affected (0.20 sec)
mysql&gt; SHOW BINARY LOGS;
+----------------------+-----------+-----------+
| Log_name | File_size | Encrypted |
+----------------------+-----------+-----------+
| GJTECH-PC-bin.000001 | 156 | No |
+----------------------+-----------+-----------+
1 row in set (0.00 sec)
```
结果显示所有二进制日志文件都被删除了MySQL从头准备了一个“.000001”为后缀的新的二进制日志文件。
我们也可以通过SQL语句删除比指定二进制日志文件编号小的所有二进制日志文件
```
mysql&gt; PURGE MASTER LOGS TO 'GJTECH-PC-bin.000005';
Query OK, 0 rows affected (0.02 sec)
```
好了,知道了二进制日志的操作方法,下面我们借助一个案例,来实操一下。我们来看看怎么通过二进制日志恢复数据,避免因故障或异常等导致数据损失。
### 案例讲解
假设数据库demo中有一个商品信息表demo.goodsmaster我先对数据库demo做了一个全量备份。所谓的全量备份就是指对数据库中存储的全部数据进行备份。备份完成之后我又在商品信息表中插入了新数据。
这个时候数据库demo出现异常数据全部丢失。现在咱们需要把所有的数据包括备份前的数据和备份之后新插入的数据都恢复回来。我来介绍下具体的操作步骤。
商品信息表的信息如下所示:
<img src="https://static001.geekbang.org/resource/image/98/83/980b731a6ab550894479b1f3382a9683.jpeg" alt="">
可以看到表中有一条记录编号是1的商品名称是“书”售价是89元。
**第一步,做数据库备份。**
你可以用MySQL的数据备份工具mysqldump来备份数据。这个工具的语法结构如下所示
```
mysqldump -u 用户 -p 密码 数据库 &gt; 备份文件
```
在这个场景中我们可以使用mysqldump工具把数据库demo中的全部信息备份到文件“mybackup.sql”中来完成对数据库demo的全量备份
```
H:\&gt;mysqldump -u root -p demo &gt; mybackup.sql
Enter password: *****
```
这个命令的意思是把数据库demo中的全部数据备份到文件mybackup.sql中。
**第二步用“FLUSH BINARY LOGS;”语句刷新一下日志。**
```
mysql&gt; FLUSH BINARY LOGS;
Query OK, 0 rows affected (0.06 sec)
```
这步操作的目的是:产生一个新的二进制日志文件,使这个文件只保存数据备份之后的数据更新事件,这样可以方便我们查看文件的内容。
**第三步,给商品信息表插入一条新的数据记录“笔”。**
```
mysql&gt; INSERT INTO demo.goodsmaster
-&gt; (
-&gt; itemnumber,
-&gt; barcode,
-&gt; goodsname,
-&gt; salesprice
-&gt; )
-&gt; VALUES
-&gt; (
-&gt; 2,
-&gt; '0002',
-&gt; '笔',
-&gt; 3
-&gt; );
Query OK, 1 row affected (0.03 sec)
```
这样我们就增加了一个新的商品“笔”。
现在,我们来查看一下数据表里的内容:
```
mysql&gt; SELECT * FROM demo.goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 书 | 89.00 |
| 2 | 0002 | 笔 | 3.00 |
+------------+---------+-----------+------------+
2 rows in set (0.00 sec)
```
结果显示我们有了2条记录分别是“书”和“笔”。
假设这个时候,系统突然宕机,数据库无法启动,为了使系统恢复正常,我们重启机器,重新创建数据库,并且需要进行数据恢复。
**第四步,准备从备份文件恢复数据。**
每当服务器启动、刷新二进制日志或者二进制日志大小超过系统变量 max_binlog_size指定的值时系统就会生成一个新的二进制日志文件。
我们先查看一下机器上的二进制日志文件,以确定哪个文件是我们正在使用的。
```
mysql&gt; SHOW BINARY LOGS;
+----------------------+-----------+-----------+
| Log_name | File_size | Encrypted |
+----------------------+-----------+-----------+
| GJTECH-PC-bin.000005 | 179 | No |
| GJTECH-PC-bin.000006 | 113316452 | No |
| GJTECH-PC-bin.000007 | 12125 | No |
| GJTECH-PC-bin.000008 | 1544 | No |
| GJTECH-PC-bin.000009 | 207 | No |
| GJTECH-PC-bin.000010 | 1758 | No |
| GJTECH-PC-bin.000011 | 2207 | No |
| GJTECH-PC-bin.000012 | 462 | No |
+----------------------+-----------+-----------+
12 rows in set (0.01 sec)
```
结果显示最新的也就是记录了数据插入操作的二进制日志文件是“GJTECH-PC-bin.000012”,这就是接下来我们要用的日志文件。
接着,我们来刷新一下二进制日志文件,这样做的目的是防止后面数据恢复的事件全都被写入这个二进制日志文件,妨碍我们理解文件的内容。
```
mysql&gt; FLUSH BINARY LOGS;
Query OK, 0 rows affected (0.08 sec)
```
现在,我们查看一下当前正在写入的二进制文件和位置,确认一下系统是否创建了新的二进制日志文件:
```
mysql&gt; SHOW MASTER STATUS;
+----------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+----------------------+----------+--------------+------------------+-------------------+
| GJTECH-PC-bin.000013 | 156 | | | |
+----------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)
```
结果显示当前在使用的二进制日志文件是“GJTECH-PC-bin.000013”,这样保存增量数据的二进制日志文件就不会受到后面操作的影响了。
下面我来删除并重建一个数据库demo给你演示一下如何使用二进制日志恢复数据。
```
mysql&gt; DROP DATABASE demo;
Query OK, 1 row affected (0.07 sec)
mysql&gt; CREATE DATABASE demo;
Query OK, 1 row affected (0.01 sec)
```
通过上面的操作就有了一个空数据库demo。接下来就可以恢复数据了。
**第五步,从备份恢复数据。**
可以通过mysql命令来恢复数据语法结构如下
```
mysql -u 用户 -p 密码 数据库名称 &lt; 备份文件
```
现在我用刚才的备份文件,通过下面的指令来恢复数据:
```
H:\&gt;mysql -u root -p demo&lt;mybackup.sql
Enter password: *****
```
这个指令的意思是在数据库demo中执行备份文件“mybackup.sql”中的所有SQL操作这样就可以把demo中的数据恢复到备份时的状态了。
我们,来看一下现在商品信息表中的数据:
```
mysql&gt; SELECT * FROM demo.goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 书 | 89.00 |
+------------+---------+-----------+------------+
1 row in set (0.00 sec)
```
结果显示,只有一条数据记录“书”,那么,怎么才能把我们备份之后插入的那条数据“笔”给找回呢?这个时候,就要进入下一步,使用二进制日志了。
**第六步从保存增量信息的二进制日志文件“GJTECH-PC-bin.000012”中恢复增量数据。**
<img src="https://static001.geekbang.org/resource/image/c1/64/c1d720d4405e479ba71eca27d5a15e64.png" alt="">
通过查看二进制日志中的事件你会发现写入第二条记录的时候MySQL使用了一个事务操作起始位置是306截止位置是462。这样就可以用mysqlbinlog工具进行数据恢复了。日志名称是“C:\ProgramData\MySQL\MySQL Server 8.0\Data\GJTECH-PC-bin.000012”读取日志的起始位置是306。
```
H:\&gt;mysqlbinlog --start-position=306 &quot;C:\ProgramData\MySQL\MySQL Server 8.0\Data\GJTECH-PC-bin.000012&quot; | mysql -u root -p
Enter password: *****
```
现在我们查看一下商品信息表,确认一下备份之后插入的商品数据记录是不是恢复回来了。
```
mysql&gt; SELECT * FROM demo.goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 书 | 89.00 |
| 2 | 0002 | 笔 | 3.00 |
+------------+---------+-----------+------------+
2 rows in set (0.00 sec)
```
结果显示,备份之后插入的记录“笔”也找回来了。
到这里,二进制日志恢复数据的功能我就介绍完了。需要提醒你注意的是,在实际工作中,用二进制日志文件找回数据时经常会遇到问题,主要就是不容易找准起始位置或者截止位置。找早了,会导致数据冲突、重复;找晚了,又会丢失数据。所以,我建议你在数据备份结束之后,把当前的二进制日志位置记录下来,存放在一个安全的地方,这关系到数据恢复的完整性,一定不要怕麻烦。
二进制日志还有一个重要的功能,就是在主从服务器的架构中,把主服务器的操作复制到从服务器。而这个操作要借助中继日志一起完成。
## 中继日志
**中继日志只在主从服务器架构的从服务器上存在**。从服务器为了与主服务器保持一致,要从主服务器读取二进制日志的内容,并且把读取到的信息写入本地的日志文件中,这个从服务器本地的日志文件就叫中继日志。然后,从服务器读取中继日志,并根据中继日志的内容对从服务器的数据进行更新,完成主从服务器的数据同步。
搭建好主从服务器之后中继日志默认会保存在从服务器的数据目录C:\ProgramData\MySQL\MySQL Server 8.0\Data文件名的格式是从服务器名-relay-bin.序号。
中继日志还有一个索引文件:从服务器名-relay-bin.index用来定位当前正在使用的中继日志。
中继日志与二进制日志的格式相同可以用mysqlbinlog工具进行查看。下面是中继日志的一个片段
```
SET TIMESTAMP=1618558728/*!*/;
BEGIN
/*!*/;
# at 950
#210416 15:38:48 server id 1 end_log_pos 832 CRC32 0xcc16d651 Table_map: `demo`.`test` mapped to number 91
# at 1000
#210416 15:38:48 server id 1 end_log_pos 872 CRC32 0x07e4047c Delete_rows: table id 91 flags: STMT_END_F -- server id 1 是主服务器,意思是主服务器删了一行数据
BINLOG '
CD95YBMBAAAAMgAAAEADAAAAAFsAAAAAAAEABGRlbW8ABHRlc3QAAQMAAQEBAFHWFsw=
CD95YCABAAAAKAAAAGgDAAAAAFsAAAAAAAEAAgAB/wABAAAAfATkBw==
'/*!*/;
# at 1040
```
这一段的意思是主服务器“server id 1”对表demo.test进行了2步操作
1. 定位到表demo.test编号是91的记录日志位置是832
1. 删除编号是91的记录日志位置是872。
关于中继日志,有一个很容易踩到的坑。如果从服务器宕机,有的时候为了系统恢复,你要重装操作系统,这样就可能会导致你的服务器名称与之前不同。而中继日志的名称里面是包含从服务器名的。因此,在这种情况下,就可能导致你恢复从服务器的时候,无法从宕机前的中继日志里读取数据,以为是日志文件损坏了,其实是文件名不对了。解决的方法也很简单,只要把从服务器的名称改回之前的名称就可以了。
下面我再介绍一下回滚日志。
## 回滚日志
**回滚日志的作用是进行事务回滚**
当事务执行的时候,回滚日志中记录了事务中每次数据更新前的状态。当事务需要回滚的时候,可以通过读取回滚日志,恢复到指定的位置。另一方面,回滚日志也可以让其他的事务读取到这个事务对数据更改之前的值,从而确保了其他事务可以不受这个事务修改数据的影响。
回滚日志的设置是啥样的呢?我们来学习下相关变量值,包括文件大小、所在的文件夹、是否加密、是否自动截断回收以及是否有独立的表空间等。这些都是我们了解事务回滚的机制的关键。
```
mysql&gt; SHOW VARIABLES LIKE '%innodb_max_undo_log_size%';
+--------------------------+------------+
| Variable_name | Value |
+--------------------------+------------+
| innodb_max_undo_log_size | 1073741824 |
+--------------------------+------------+
1 row in set, 1 warning (0.00 sec)
```
变量“innodb_max_undo_log_size”的意思是单个回滚日志最大可占用1G字节存储空间。
下面几个变量定义了回滚日志所在的文件夹、是否加密、是否自动截断回收空间和是否有独立的表空间等。
```
mysql&gt; SHOW VARIABLES LIKE '%innodb_undo%';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_undo_directory | .\ | -- 表示回滚日志的存储目录是数据目录。
| innodb_undo_log_encrypt | OFF | -- 表示回滚日志不加密。
| innodb_undo_log_truncate | ON | -- 表示回滚日志是否自动截断回收,前提是设置了独立表空间。
| innodb_undo_tablespaces | 2 | -- 表示回滚日志有自己的独立表空间而不是在共享表空间ibdata文件中。
+--------------------------+-------+
4 rows in set, 1 warning (0.00 sec)
```
这里的结果显示了这4个变量的默认值。下面我来分别解释一下。
- innodb_undo_directory=.\ 表示回滚日志的存储目录是数据目录数据目录的位置可以通过查询变量“datadir”来查看。
- innodb_undo_log_encrypt = OFF表示回滚日志不加密。
- innodb_undo_log_truncate = ON表示回滚日志是否自动截断回收这个变量有效的前提是设置了独立表空间。
- innodb_undo_tablespaces = 2表示回滚日志有自己的独立表空间而不是在共享表空间ibdata文件中。
下面的截图显示了回滚日志的存储目录,以及在文件夹中的名称等信息:
<img src="https://static001.geekbang.org/resource/image/90/38/90ebac7eed19314e6fc5928b92a56b38.png" alt="">
最后我来介绍一下MySQL的重做日志。
## 重做日志
重做日志是存储在磁盘上的一种日志文件主要有2个作用。
1. 在系统遇到故障的恢复过程中,可以修复被未完成的事务修改的数据。
1. MySQL为了提高数据存取的效率减少磁盘操作的频率对数据的更新操作不会立即写到磁盘上而是把数据更新先保存在内存中积累到一定程度再集中进行磁盘读写操作。这样就存在一个问题一旦出现宕机或者停电等异常情况内存中保存的数据更新操作可能会丢失。这个时候就可以通过读取重做日志中记录的数据更新操作把没来得及写到磁盘上的数据更新写到磁盘上确保数据的完整性。
我们可以通过系统变量的值,了解重做日志所在的文件夹和文件的数量。这些是我们进一步了解系统运行机制的必要条件,有助于我们开发出高效的数据库应用。
```
mysql&gt; SHOW VARIABLES LIKE '%innodb_log_files_in_group%';
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| innodb_log_files_in_group | 2 |
+---------------------------+-------+
1 row in set, 1 warning (0.00 sec)
```
结果显示变量innodb_log_files_in_group值是2表示有2个重做日志文件。
需要注意的是变量innodb_log_files_in_group值的取值范围是1~4这四个文件分别用于记录不同的操作.
1. 用户创建表的插入操作;
1. 用户创建表的更新和删除操作;
1. 临时表的插入操作;
1. 临时表的更新和删除操作。
那么为什么在我的电脑上变量innodb_log_files_in_group值是2呢其实这是因为我只执行了对用户创建表的插入操作和更新删除操作所以只用到了2个文件。如果我还执行了临时表的插入和更新删除的操作那么这个变量的值就会变成4也就是会有4个重做日志文件了。
## 总结
这节课,我们学习了二进制日志、中继日志、回滚日志和重做日志。
1. 二进制日志:主要用于主从服务器之间的数据同步,以及服务器遇到故障时数据的无损失恢复。
1. 中继日志:就是主从服务器架构中,从服务器用来存放主服务器二进制日志内容的一个中间文件。从服务器通过读取中继日志的内容,来同步主服务器上的操作。
1. 回滚日志:用来存储事务中数据更新前的状态,以便回滚和保持其他事务的数据一致性。
1. 重做日志:是为了确保数值持久性、防止数据更新丢失的一种日志。
在这几种日志中,你一定要格外注意二进制日志的用法。有了它,我们就可以通过数据库的全量备份和二进制日志中保存的增量信息,完成数据库的无损失恢复。不过,我要提醒你的是,如果你遇到数据量大、数据库和数据表很多(比如分库分表的应用)的场景,用二进制日志进行数据恢复,是很有挑战性的,因为起止位置不容易管理。
在这种情况下,一个有效的解决办法是配置主从数据库服务器,甚至是一主多从的架构,把二进制日志文件的内容通过中继日志,同步到从数据库服务器中,这样就可以有效避免数据库故障导致的数据异常等问题。
## 思考题
下面是一段二进制日志中事件的内容:
```
mysql&gt; SHOW BINLOG EVENTS IN 'GJTECH-PC-bin.000013';
+----------------------+------+----------------+-----------+-------------+--------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+----------------------+------+----------------+-----------+-------------+--------------------------------------+
| GJTECH-PC-bin.000013 | 556 | Query | 1 | 627 | BEGIN |
| GJTECH-PC-bin.000013 | 627 | Table_map | 1 | 696 | table_id: 114 (demo.goodsmaster) |
| GJTECH-PC-bin.000013 | 696 | Delete_rows | 1 | 773 | table_id: 114 flags: STMT_END_F |
| GJTECH-PC-bin.000013 | 773 | Xid | 1 | 804 | COMMIT /* xid=253 */ |
| GJTECH-PC-bin.000013 | 804 | Anonymous_Gtid | 1 | 894 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
| GJTECH-PC-bin.000013 | 894 | Query | 1 | 969 | BEGIN |
| GJTECH-PC-bin.000013 | 969 | Table_map | 1 | 1038 | table_id: 114 (demo.goodsmaster) |
| GJTECH-PC-bin.000013 | 1038 | Write_rows | 1 | 1094 | table_id: 114 flags: STMT_END_F |
| GJTECH-PC-bin.000013 | 1094 | Xid | 1 | 1125 | COMMIT /* xid=259 */ |
```
观察一下其中包括了哪几个SQL数据操作呢为了从二进制日志中恢复这些操作我们应该如何设置起始位置和截止位置呢
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,466 @@
<audio id="audio" title="21 | 数据备份:异常情况下,如何确保数据安全?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/aa/38/aa398d679f7139c51800898e1886a238.mp3"></audio>
你好,我是朱晓峰。今天,我来和你聊一聊数据备份。
数据备份,对咱们技术人员来说十分重要。当成千上万的用户,每天使用我们开发的应用做着他们的日常工作的时候,数据的安全性就不光是你一个人的事了。要是有一天,突然发生了某种意想不到的情况,导致数据库服务器上的数据全部丢失,所有使用这个应用的人都会受到严重影响。
所以,我们必须“未雨绸缪”,及时把数据备份到安全的地方。这样,当突发的异常来临时,我们就能把数据及时恢复回来,就不会造成太大损失。
MySQL的数据备份有2种一种是物理备份通过把数据文件复制出来达到备份的目的另外一种是逻辑备份通过把描述数据库结构和内容的信息保存起来达到备份的目的。逻辑备份这种方式是免费的广泛得到使用而物理备份的方式需要收费用得比较少。所以这节课我重点和你聊聊逻辑备份。
我还会给你介绍一下MySQL中的数据备份工具mysqldump、数据恢复的命令行客户端工具mysql以及数据表中数据导出到文件和从文件导入的SQL语句帮助你提高你所开发的应用中的数据安全性。
## 如何进行数据备份?
首先我们来学习下用于数据备份的工具mysqldump。它总共有三种模式
1. 备份数据库中的表;
1. 备份整个数据库;
1. 备份整个数据库服务器。
接下来我就来介绍下这3种备份的具体方法。
### 如何备份数据库中的表?
mysqldump备份数据库中的表的语法结构是
```
mysqldump -h 服务器 -u 用户 -p 密码 数据库名称 [表名称 … ] &gt; 备份文件名称
```
我简单解释一下这里的核心内容。
- “-h”后面跟的服务器名称如果省略默认是本机“localhost”。
- “-u”后面跟的是用户名。
- “-p”后面跟的是密码如果省略执行的时候系统会提示录入密码。
我举个小例子,带你看看怎么使用这个工具。
假设数据库demo中有2个表分别是商品信息表demo.goodsmaster和会员表demo.membermaster
商品信息表:
<img src="https://static001.geekbang.org/resource/image/9c/45/9cd746385988ee32d8813ffbb12ed645.jpeg" alt="">
会员表:
<img src="https://static001.geekbang.org/resource/image/bd/87/bd0e205b0893773944275861ae9b6e87.jpeg" alt="">
现在我需要把数据库demo备份到文件中就可以用下面的代码实现
```
H:\&gt;mysqldump -u root -p demo goodsmaster membermaster &gt; test.sql
Enter password: *****
```
这个指令的意思就是备份本机数据库服务器上demo数据库中的商品信息表和会员信息表的所有信息。
**备份文件是以文本格式保存的**,我们可以用记事本打开,看一下备份的内容:
```
-- MySQL dump 10.13 Distrib 8.0.23, for Win64 (x86_64)
--
-- Host: localhost Database: demo -- 表示从本地进行备份数据库是demo
-- ------------------------------------------------------
-- Server version 8.0.23
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `goodsmaster` -- 商品信息表的结构
--
DROP TABLE IF EXISTS `goodsmaster`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `goodsmaster` (
`itemnumber` int NOT NULL,
`barcode` text,
`goodsname` text,
`specification` text,
`unit` text,
`salesprice` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`itemnumber`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `goodsmaster` -- 商品信息表的内容
--
LOCK TABLES `goodsmaster` WRITE;
/*!40000 ALTER TABLE `goodsmaster` DISABLE KEYS */;
INSERT INTO `goodsmaster` VALUES (1,'0001','书','16开','本',89.00),(2,'0002','笔','10支装','包',5.00),(3,'0003','橡皮',NULL,'个',3.00);
/*!40000 ALTER TABLE `goodsmaster` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `membermaster` -- 会员表的结构
--
DROP TABLE IF EXISTS `membermaster`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `membermaster` (
`id` int NOT NULL AUTO_INCREMENT,
`cardno` char(8) NOT NULL,
`membername` text,
`memberphone` text,
`memberpid` text,
`memberaddress` text,
`sex` text,
`birthday` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `membermaster` -- 会员表的内容
--
LOCK TABLES `membermaster` WRITE;
/*!40000 ALTER TABLE `membermaster` DISABLE KEYS */;
INSERT INTO `membermaster` VALUES ('10000001','张三','13812345678','110123200001017890','北京','男','2000-01-01 00:00:00',1),('10000002','李四','13512345678','123123199001012356','上海','女','1990-01-01 00:00:00',2);
/*!40000 ALTER TABLE `membermaster` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2021-04-11 10:43:04
```
从这个文件中我们可以看到它相当于一个SQL执行脚本里面包括了创建商品信息表和会员表的SQL语句以及把表里的数据插入到这两个表中的SQL语句。这样一来商品信息表和会员信息表的结构信息和全部数据信息就都备份出来了。
下面我来介绍一下备份整个数据库的方法。
### 如何备份数据库?
mysqldump备份数据库的语法结构是
```
mysqldump -h 服务器 -u 用户 -p 密码 --databases 数据库名称 … &gt; 备份文件名
```
举个小例子假设我现在需要对本机的数据库服务器中的2个数据库demo和demo1进行备份就可以用下面的指令
```
H:\&gt;mysqldump -u root -p --databases demo demo1 &gt; test1.sql
Enter password: *****
```
现在,我们来查看一下备份文件的内容:
```
-- MySQL dump 10.13 Distrib 8.0.23, for Win64 (x86_64)
--
-- Host: localhost Database: demo
-- ------------------------------------------------------
-- Server version 8.0.23
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Current Database: `demo` -- 备份数据库demo
--
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `demo` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `demo`; -- 备份数据库中的表
--
-- Table structure for table `dailystatistics`
--
DROP TABLE IF EXISTS `dailystatistics`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `dailystatistics` (
`id` int NOT NULL AUTO_INCREMENT,
`itemnumber` int DEFAULT NULL,
`quantity` decimal(10,3) DEFAULT NULL,
`actualvalue` decimal(10,2) DEFAULT NULL,
`cost` decimal(10,2) DEFAULT NULL,
`profit` decimal(10,2) DEFAULT NULL,
`profitratio` decimal(10,4) DEFAULT NULL,
`salesdate` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_dailystatistic_salesdate` (`salesdate`),
KEY `index_dailystatistic_itemnumber` (`itemnumber`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `dailystatistics`
--
LOCK TABLES `dailystatistics` WRITE;
/*!40000 ALTER TABLE `dailystatistics` DISABLE KEYS */;
INSERT INTO `dailystatistics` VALUES (15,1,3.000,267.00,100.50,166.50,0.6236,'2020-12-01 00:00:00'),(16,2,2.000,10.00,7.00,3.00,0.3000,'2020-12-01 00:00:00');
/*!40000 ALTER TABLE `dailystatistics` ENABLE KEYS */;
UNLOCK TABLES;
-- 这里省略了其他表的备份语句
--
-- Current Database: `demo1` -- 备份数据库demo1
--
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `demo1` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `demo1`;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2021-04-11 11:02:09
```
可以看到这个文件里面包含了创建数据库demo和demo1的SQL语句以及创建数据库中所有表、插入所有表中原有数据的SQL语句。
### 如何备份整个数据库服务器?
mysqldump备份整个数据库服务器的语法结构是
```
mysqldump -h 服务器 -u 用户 -p 密码 --all-databases &gt; 备份文件名
```
举个小例子假设我要把本机上整个MySQL服务器备份下来可以用下面的代码
```
H:\&gt;mysqldump -u root -p --all-databases &gt; test2.sql
Enter password: *****
```
这个指令表示备份本机上运行的MySQL数据库服务器的全部内容包含系统数据库和用户创建的数据库中的库结构信息、表结构信息和表里的数据。这种备份方式会把系统数据库也全部备份出来而且消耗的资源也比较多一般来说没有必要我就不展开细说了。
备份文件有了,如何用它进行数据恢复呢?下面我就来给你介绍下具体的方法。
## 如何进行数据恢复?
mysqldump的备份文件包含了创建数据库、数据表以及插入数据表里原有数据的SQL语句我们可以直接运行这些SQL语句来进行数据恢复。
数据恢复的方法主要有2种
- 使用“mysql”命令行客户端工具进行数据恢复
- 使用“SOURCE”语句进行数据恢复。
使用“mysql”命令行客户端工具进行数据恢复的命令如下
```
H:\&gt;mysql -u root -p demo &lt; test.sql
Enter password: *****
```
我来简单介绍下这个数据恢复命令。
- mysql是一个命令行客户端工具可以与MySQL服务器之间进行连接执行SQL语句。
- “-u”后面跟的是用户。
- “-p”后面跟的是密码。
在这个命令里面我指定了数据库因为备份文件test.sql里面只有数据表的备份信息需要指定恢复到哪个数据库中。如果使用的备份文件备份的是数据库的信息比如test1.sql或者是整个MySQL数据库服务器的信息比如test2.sql则不需要指定数据库。
第二种数据恢复的方法是使用“SOURCE”语句恢复数据语法结构如下
```
SOURCE 备份文件名
```
举个小例子,刚才我们对商品信息表和会员信息表进行了备份,现在想用备份的文件进行恢复,就可以用下面的语句:
```
mysql&gt; USE demo;
Database changed
mysql&gt; SOURCE H:\\test.sql
Query OK, 0 rows affected (0.00 sec)
```
注意这里需要先用“USE”语句把当前的数据库变更为demo这样商品信息表和会员表才能恢复到正确的数据库里面。否则可能会恢复错误。
除此之外你还可以通过这种方式用整个数据库的备份文件把数据库恢复回来甚至是用整个数据库服务器的备份文件恢复整个MySQL服务器。
到这里我们就掌握了备份和恢复整个数据库服务器、数据库和数据库中的表的方法。不过有的时候我们只关心表里的数据本身希望能够把表里的数据按照一定的格式保存下来。这个时候mysqldump就不够用了。所以接下来我再给你介绍下MySQL数据导出和导入的方法。
## 如何导出和导入表里的数据?
先来学习下怎么把一个表的数据按照一定的格式,导出成一个文件。
### SELECT语句导出数据
使用“SELECT … INTO OUTFILE”语句导出数据表的语法结构是
```
SELECT 字段列表 INTO OUTFILE 文件名称
FIELDS TERMINATED BY 字符
LINES TERMINATED BY 字符
FROM 表名;
```
我来解释下这段代码。
- INTO OUTFILE 文件名称,表示查询的结果保存到文件名称指定的文件中;
- FIELDS TERMINATED BY 字符,表示列之间的分隔符是“字符”;
- LINES TERMINATED BY 字符,表示行之间的分隔符是“字符”。
举个小例子假设我们要把商品信息表导出到文件H:\goodsmaster.txt中该如何实现呢按照我刚刚介绍的语法结构来尝试一下
```
mysql&gt; SELECT * INTO OUTFILE 'H:\goodsmaster.txt'
-&gt; FIELDS TERMINATED BY ','
-&gt; LINES TERMINATED BY '\n'
-&gt; FROM demo.goodsmaster;
ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option so it cannot execute this statement
```
结果,系统提示错误。其实,这是因为**服务器的“secure-file-priv”参数选项不允许把文件写入到H:\goodsmaster.txt中**。那怎么解决这个问题呢?
这个时候我们可以通过MySQL的配置文件my.ini来查看一下“secure-file-priv”参数的设定并且按照这个参数设定的要求准备导入文件。
打开C:\ProgramData\MySQL\MySQL Server 8.0\my.ini找到“secure-file-priv”参数设定如下所示
```
# Secure File Priv.
secure-file-priv=&quot;C:/ProgramData/MySQL/MySQL Server 8.0/Uploads&quot;
```
这个意思是说只能把数据导出到“C:/ProgramData/MySQL/MySQL Server 8.0/Uploads”这个文件夹中所以如果我们把数据导出到H:\goodsmaster.txt中就违反了系统参数的设定导致发生错误。
现在我们来修改一下数据导出的SQL语句把导出文件的路径改到系统要求的文件目录看看结果如何
```
mysql&gt; SELECT * INTO OUTFILE 'C:/ProgramData/MySQL/MySQL Server 8.0/Uploads/goodsmaster.txt'
-&gt; FIELDS TERMINATED BY ','
-&gt; LINES TERMINATED BY '\n'
-&gt; FROM demo.goodsmaster;
Query OK, 3 rows affected (0.00 sec)
```
结果显示,执行成功了。下面我们来看一下结果文件的内容:
```
1,0001,书,16开,本,89.00
2,0002,笔,10支装,包,5.00
3,0003,橡皮,\N,个,3.00
```
很显然,这很符合我们希望的导出格式:行与行之间用回车“\n”分隔列与列之间用逗号“,”分隔。
到这里,我们就知道怎么把数据表中的数据按照一定的格式导出到文件了。那在实际工作中,我们还经常需要把一定格式的数据从文件中导入到数据表中。
“LOAD DATA”语句就是MySQL提供的一种快速数据读入的方法在实际工作中常用于大量数据的导入效率极高。
### 使用“LOAD DATA”语句导入数据
“LOAD DATA”是与“SELECT … INTO OUTFILE”相对应的数据导入语句。语句结构是
```
LOAD DATA INFILE 文件名
INTO TABLE 表名
FIELDS TERMINATED BY 字符
LINES TERMINATED BY 字符;
```
我举个小例子来演示一下“LOAD DATA”语句是如何工作的。
还是以我们刚才导出的那个文件goodsmaster.txt为例现在我们把这个文件内的数据导入到商品信息表demo.goodsmaster中去。
为了演示方便我会先把demo.goodsmaster中的数据先删除然后使用“LOAD DATA”语句把刚才的导出文件goodsmaster.txt的内容导入进来再与删除之前的数据进行对比来验证“LOAD DATA”语句的执行效果。
首先,我们把商品信息表中的数据删除:
```
mysql&gt; DELETE FROM demo.goodsmaster
-&gt; WHERE itemnumber&gt;0;
Query OK, 3 rows affected (0.03 sec)
```
然后我们尝试把文件goodsmaster.txt中的数据导入进来
```
mysql&gt; LOAD DATA INFILE 'C:/ProgramData/MySQL/MySQL Server 8.0/Uploads/goodsmaster.txt'
-&gt; INTO TABLE demo.goodsmaster
-&gt; FIELDS TERMINATED BY ','
-&gt; LINES TERMINATED BY '\n';
Query OK, 3 rows affected (0.02 sec)
Records: 3 Deleted: 0 Skipped: 0 Warnings: 0
```
结果显示,导入成功了。我们再查看一下数据表中的内容:
```
mysql&gt; SELECT * FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice |
+------------+---------+-----------+---------------+------+------------+
| 1 | 0001 | 书 | 16开 | 本 | 89.00 |
| 2 | 0002 | 笔 | 10支装 | 包 | 5.00 |
| 3 | 0003 | 橡皮 | NULL | 个 | 3.00 |
+------------+---------+-----------+---------------+------+------------+
3 rows in set (0.00 sec)
```
结果显示与我们删除之前的数据完全一致。这说明“LOAD DATA”语句成功导入了数据文件goodsmaster.txt中的数据。
## 总结
今天我们重点学习了数据备份包括数据备份的工具mysqldump以及用命令行客户端工具“mysql”和SQL语句“SOURCE”进行数据恢复的方法。同时我还给你介绍了用于导出数据表中数据的语句“SELECT … INTO OUTFILE”和导入的语句“LOAD DATA”。这些都是你在备份数据时必不可少的对确保数据的安全性至关重要。
最后提醒你一点“LOAD DATA”是很好用的工具因为它的数据导入速度是非常惊人的。一个400万条数据的文件用“LOAD DATA”语句只需要几分钟就可以完成而其他的方法比如使用Workbench来导入数据就需要花费好几个小时。
## 思考题
这节课我介绍了数据导出语句“SELECT … INTO OUTFILE”并且在第一次数据导出时遇到了一个系统参数“secure-file-priv”设定的目录与导出文件目录不一致从而导致导出失败的问题。最后我通过修改导出文件的文件夹解决了这个问题。
我想请你思考一下如果还是想把导出文件保存到H:\目录下,有没有办法实现呢?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。