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