CategoryResourceRepost/极客时间专栏/MySQL 必知必会/优化篇/25 | 表太大了,如何设计才能提高性能?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

260 lines
15 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="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个字段使用得相对比较少。针对这样的表格你会怎么优化呢
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。