CategoryResourceRepost/极客时间专栏/MySQL 必知必会/优化篇/22 | 范式:如何消除冗余,实现高效存取?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

193 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

<audio id="audio" title="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="">
这个表存在数据冗余,应该如何优化设计呢?为什么?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。