MySQL优化-最佳实践-字段类型篇

一、前言

MySQL支持着很多的数据类型,但是实际上大多数开发者对数据类型并没有一个清晰的认识,因为部分数据类型的兼容性很强,大家觉得能正常存储我的数据就好了,不管三七二十一,字符串我就直接 varchar, 整形我就直接用 int,甚至有些开发者整张表一梭哈全字符串类型。哈哈哈,你别笑,我曾经就干过这种傻事。

作为最佳实践的第一篇,当然要从字段类型开始入手,在合适的时机找到对的人,对后期的维护成本,有着十分重要的作用。

在这里我列出几个问题,大家可以先思考一下,文章末尾我会进行解答

  • 想存储时间,timestamp、datetime、int 如何选择?
  • 主键如何抉择?
  • 业务上的枚举值怎么选型?
  • IP 地址该如何存储?

二、MySQL支持的字段类型有什么?

在此之前,我们需要知道MySQL到底提供了哪些类型给我们使用,我们暂且将其分为三大类,分别是数值类型、时间日期类型、字符串类型。

1.数值类型
字段类型 字节大小 范围(有符号) 范围 (无符号)
TINYINT 1字节 (-128,127) (0,255)
SMALLINT 2字节 (-32768,32767) (0,65535)
MEDIUMINT 3字节 (-8 388 608,8 388 607) (0,16 777 215)
INT 4字节 (-2^31, 2^31 - 1) (0, 2^32 - 1)
BIGINT 8字节 (-2^63, 2^63 - 1) (0, 2^64 - 1)
FLOAT 4字节 -3.402823466E+38~-1.175494351E-38 0 和 -1.175494351E-38~-3.402823466E+38
DOUBLE 8字节 -1.7976931348623157E+308~-2.2250738585072014E-308。 0 和 -2.2250738585072014E-308~-1.7976931348623157E+308
DECIMAL 对DECIMAL(M,D) ,如果M>D,为M+2否则为D+2 (依赖于M和D的值) (依赖于M和D的值)

首先我们先聊聊整数,
对大多数开发者来说,整数使用的并不少。它的家族包括 TINYINT,SMALLINT, MEDIUMINT,BIGINT, INT 这几个兄弟。他们之间的区别就是能存的数值范围不同,我们可以在上面表格清晰的看到其中的数值差别差别。

我们知道一个字节是八个比特,每个比特有0,1两种可能,这样我们可以清楚的算出其取值范围

  • 对于有符号的整数来说(首位当做符号位,有正负之分)数值范围在 -2^(字节数x8/2) 到 2^(字节数x8/2) - 1
  • 无符号整数(正整数)的范围则在 0 到 2^(字节数x8) - 1

如果你想要存整数的话,其实是很好选择的,只需要预估你的数字需要存储什么范围内,理论上就可以选择合适的,但是我们一般来说,会采取性价比最高的存储,例如枚举值一般会选用 TINYINT,如果你的笔记本只有13寸,就没必要买16寸的电脑包了。

但是在实际的开发过程中,大多数人只使用过TINYINT 和 INT,因为这两个基本上已经满足基本的要求,也许这也是一种约定俗成了。我与很多的开发者聊过,大家似乎并不想过多的纠结我的字段值的范围纠结会到哪个程度,INT 不够 BIGINT 来凑。要注意的是,如果能确定字段只有正数,unsigned 一定要带上,一方面是向其他开发者说明业务要求,另一方面是能够利用好存储空间(我们可以看到同样的存储空间无符号的整数可以存储有符号的两倍数值)
还有需要提醒的是,int(10) 其中的 10 只是显示宽度(涉及到客户端能显示的字符的个数),对存储并无影响。

数值类型中,除了整数还有小数,在实际上开发过程中,涉及到金额或者是倍率,我们通常会使用小数来进行存储。float 和 double 类型支持使用标准的浮点运算进行近似的计算。则 decimal 类型则用于存储精确的小数。浮点类型在存储同样范围的值时,float 和 double 通常比 decimal 使用更少的空间。而 double 相比于 float 有更高的精度和更大的范围。实际上,这三种类型都只是存储类型,在MySQL的内部计算中,统一使用double类型进行处理,博主目前接触到的公司很少会使用 float 和 double ,通常是使用 decimal 来进行小数的存储,虽然 decimal 占用的存储更多,但是会更加的准确。如果插入小数的个数比预先设置的多,那么MySQL会自动的四舍五入,涉及到精确的金融计算时,我们还是推荐使用整形进行存储(乘以约定好的倍数)

2.时间日期类型
字段类型 字节大小 范围 格式
DATE 3字节 1000-01-01/9999-12-31 YYYY-MM-DD
TIME 3字节 ‘-838:59:59’/‘838:59:59’ HH:MM:SS
YEAR 1字节 1901/2155 YYYY
DATETIME 8字节 1000-01-01 00:00:00/9999-12-31 23:59:59 YYYY-MM-DD HH:MM:SS
TIMESTAMP 4字节 1970-01-01 00:00:00/2038 YYYY-MM-DD HH-MM-SS

接下来我们来聊聊日期类型, 通常来说,我们数据记录总会有需要存时间的需求。其中 DATE、TIME、YEAR 这三种通常是特殊场景下需要使用,绝大多数的业务场景还是使用 YYYY-MM-DD HH:MM:SS 的格式来进行存储,它可以看做前面三种类型的汇总形式。
那么 datetime 和 timestamp 我们该如何选择呢?
我们先从存储空间来考虑, timestamp 只需要 4 个字节的存储空间,相比于 datetime,有很多的优势,因此如果考虑到数据存储大小的话,同等情况下,选择 timestamp 会更加的节省空间
其次,我们考虑存储的时间范围,timestamp 的时间范围相比 datetime 有一定的限制,它只能存储从 1970年到2038年的时间,如果你要存1970年前的数据,那就只能使用 datetime 了,我曾经和以前的同事调侃过,我们建的表顶多能再活十几年了(选的timestamp,因为是存的日志数据,数据量很大,上千万级别,优先考虑了存储空间),哈哈哈,公司都不见得能活到那时候,十几年的时间早应该被重构了。
而 timestamp 相比于 datetime 还有一个优势,就是 在5.5到5.6.4版本里,支持DEFAULT CURRENT_TIMESTAMP子句
而从5.6.5开始(也包括5.7),这个优势就没了,DEFAULT CURRENT_TIMESTAMP子句可以指定到TIMESTAMP或者DATETIME类型列上

所以综上来说,如果时间范围允许,尽量使用 timestamp,因为它比datetime的空间效率更高,有时候也有人会把 Unix的时间戳存在整数值,但实际上并不推荐这么做。并不会带来多大的收益,除非是想记录到比秒更小的粒度(因为这些时间类型的最小粒度是秒),那么可以使用 bigint 存储微妙级别的时间戳 或者使用 double 存储秒之后的小数部分,再或者也可以使用 MariaDB 来替代 MySQL (mysql 的另一个分支)

3. 字符串类型
字段类型 字节大小 用途
CHAR 0-255字节 定长字符串
VARCHAR 0-65535字节 变长字符串
TINYBLOB 0-255字节 不超过 255 个字符的二进制字符串
TINYTEXT 0-255字节 短文本字符串
BLOB 0-65535字节 二进制形式的长文本数据
TEXT 0-65535字节 长文本数据
MEDIUMBLOB 0-16 777 215字节 二进制形式的中等长度文本数据
MEDIUMTEXT 0-16 777 215字节 中等长度文本数据
LONGBLOB 0-4 294 967 295 字节 二进制形式的极大文本数据
LONGTEXT 0-4 294 967 295 字节 极大文本数据

字符串类型指CHAR、VARCHAR、BINARY、VARBINARY、BLOB、TEXT、ENUM和SET,其中,我们使用的最多的就是 char 和 把 varchar。

  • char 和 varchar 类型

varhcar 类型用于存储可变字符串,它比定长类型的优势在于它仅仅使用必要的空间。这就有点像数组和切片的区别,一般来说,如果是不确定的字符串长度,我们应该优先使用 varchar 类型进行存储,它可以更加有效的利用空间,而 char 类型通常是用来存储指定长度的字符串(如确定长度 uuid 或者 md5 后的密码),在这种情况下, char 类型更不容易产生碎片。

varchar 需要使用1或者2个额外的字节记录字符串的长度,当列的最大长度小于或者等于255字节时,只需要 1个字节表示,否则需要两个字节(因为1个字节八个比特位最大值就是255),假设使用的是 latin1 字符集, varchar(10) 需要11个字节的存储空间,而 varchar(1000) 则需要1002个字节。虽然 varchar 看起来很有优势,但是由于是可变字符,这就是意味的更新的时候会有额外的损耗(例如更新后字符串占的空间变大了,在这种情况下,不同引擎会有不同的行为,例如 myisam 会将行拆成不同的片段存储,innodb 会分裂页来使行可以放进页内),并且因为它需要额外的空间来存储字符串长度,因此 varchar(1) 要比 char(1) 更占空间。

还有一点需要注意的是,varchar(10) 和 varchar(100) 在存储 “mclink” 字符串使用的空间开销虽然是一样的,但是由于 MySQL 通常会分配固定大小的内存块来存储内部值,更长的列会消耗更多的内存,尤其是在使用内存临时表或文件临时表进行排序或者其他操作时,都是十分糟糕的。

  • text 和 blob

text 和 blob 的存在是为了可以存储更大的数据,它们分别有四个自己的成员。分别采用二进制和字符方式存储。与其他类型不同,MySQL 把每个 blob 和 text 值当做一个独立的对象处理,存储引擎在存储时通常会做特殊处理。当 blob 和 text 值太大时,innodb 会使用住啊们的 “外部”存储区域来进行存储,此时每个值在行内需要 1到4个字节存储一个指针,然后在外部区域存储实际的值。

blob 和 text 之间仅有的不同在于 blob 类型存储的是二进制数据,没有排序规则或字符集,而 text 类型有字符集和排序规则。(二进制除了0和1还能有啥呢。)并且在排序的时候,MySQL针对这两种类型只会排序 sort_length 字节而不是整个字符串,同时也无法针对这两种类型进行全长度的索引。因此我们在非必要的情况下,应减少使用这两种类型。

4.其他类型
  • enum 类型
    有的时候可以使用枚举列代替常用的字符串类型,它可以把一些不重复的字符串存储成一个预定义的集合。MySQL在存储枚举时非常的近臭,会根据列表值的数量压缩到一个或者两个字节中。在内部,MySQL会将每个值在列表中的位置保存为整数,并且在.frm 文件中保存 数字-字符串的映射表。
    我们举个列子,
CREATE TABLE `test_enum` (
  `e` enum('woman','man') NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

insert into enum_test(e) values('woman'),('man');

mysql> select e + 0 from test_enum;
+-------+
| e + 0 |
+-------+
|     1 |
|     2 |
+-------+
2 rows in set (0.00 sec)

mysql> select e from test_enum;
+-------+
| e     |
+-------+
| woman |
| man   |
+-------+
2 rows in set (0.00 sec)

通过数字上下文环境检索可以看到这个双重属性。所以我们应该尽量避免使用数字作为枚举值,这样很容易导致混淆。例如 enum(‘1’,‘2’), 在工作中我曾经就看到有同事想这么使用,实际上这是不可取的,另外需要提到的一点时,枚举字段是按照内部存储的整数而不是定义的字符串来进行排序的,这就意味着定义的时候就要考虑好对应的顺序,否则你得使用 field 来指定数据顺序。

枚举最不好的地方就是字符串列表是固定的,如果你想修改列表则必须使用 alter table,所以,如果你的枚举值未来可能会改变,那就不应该使用它,如果你只在列表末尾添加元素,那么MySQL才不会重建整个表来完成修改,大多数情况下,我们的枚举列表都比较小,所以查找转化的成本也比较低。

  • 位类型

在 MySQL 5.0 之前, bit 是 tinyint 的同义词。但是在 MySQL 5.0以及更新版本,这是一个特性完全不同的数据类型。

bit 的最大长度是64个位,bit 的行为跟存储引擎有关,例如 myisam 会打包存储所有的 bit 列,所以 bit(17) 只需要 3个字节(24个位)来存储就可以,而其他的引擎例如 innodb 或者是 memory,它们会为每个bit 列使用一个足够存储的最小整数来存放,所以无法节省空间,例如 同样的 bit(17) 需要使用 至少 17个字节进行存储。

MySQL 把 bit 当做字符串类型,而不是数字类型。例如检索 BIT(1) 的值时,结果是一个包含二进制0或者1值的字符串,并不是 ASCII 码的 “0” 或者 “1”。然而如果是在数字上下文的环境中(包含数字计算),会将位字符串转为对应的数字,例如一个 值为 b “00111001” 的二进制字符串,其十进制值为 57,在数字上下文场景中,它是57,但是在普通场景中,因为转换得到的是 “57”, 对应 ASCII 码来说却是 ‘9’

mysql>  Create Table: CREATE TABLE `test_bit` (
  `b` bit(8) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

mysql>  insert into test_bit(b) values (b'00111001');

mysql> select b, b+0 from test_bit;
+---+-----+
| b | b+0 |
+---+-----+
| 9 |  57 |
+---+-----+
1 row in set (0.00 sec)

实际上,我们应该谨慎使用这种类型,如果想在一个 bit 的存储空间中去存储一个 布尔值,另一个方法就是建一个 char(0)列,这个列可以保存空值(NULL) 或者是空字符串(长度为0),分别去对应 false 和 true 即可,这是一种巧妙的方式,但是并不容易让他人理解。

  • set 类型

如果需要保存很多的布尔值,可以考虑合并这些列到一个set数据类型中,它在 MySQL 中是以一系列打包的集合来进行表示的,因此可以有效的利用空间,并且 MySQL 中有 find_in_set()he filed() 这样的函数来辅助使用。它的缺点和enum 一样,改变列需要使用 alter table,对大表来说是很大的损耗,我们简单说明一下它的使用方式。

mysql> show create table test_set\G;
*************************** 1. row ***************************
       Table: test_set
Create Table: CREATE TABLE `test_set` (
  `s` set('can_edit','can_del','can_read') NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql> insert into test_set(s) values('can_edit,can_read');
Query OK, 1 row affected (0.01 sec)


mysql> select s from test_set where find_in_set('can_read',s);
+-------------------+
| s                 |
+-------------------+
| can_edit,can_read |
+-------------------+
1 row in set (0.00 sec)

对于这种情况,如果使用整数来包装一系列的位进行处理的话,同样可以达到 set 的作用,例如使用 tinyint (8位)来标志每个位的布尔值,针对每个位在代码中标识好含义,通过按位运算来得到结果。这种方式说实话在业务中也很少使用,一般是用于较为底层的开发,需要考虑到高性能存储的情况下才会采取,因为这种方式无疑会增加代码的复杂性,位运算对于很多底层开发来说是一种很好的性能优化方式,毕竟数据存储的最底层结构就是二进制。

三、问题答疑

经过上面知识的洗礼,相信你对这些数值类型有了一定的认识了。至少不会随便一把梭了。通过上面的知识,我们解答一下前言中提到的一些问题。

  • 存储时间,timestamp、datetime、int 如何选择?
    优先使用存储空间较小的 timestamp,其次是选择 datetime , int 类型用作时间戳的存储并没有太大的优势存储,如果考虑说整形的排序和空间利用有优势,我觉得它所增加的复杂性远比这个更加突出

  • 主键如何抉择?
    说起主键,我们都知道,普通索引树的叶子节点就是存储的主键,因此我们应该将主键设置的更加小,自增的整形主键是个很好的选择,整形有着天生的空间紧凑性,不会轻易的引起页分裂,市面上也有采用uuid 的主键。个人觉得这种方式十分不可取(博主现在的公司有些表就是用的uuid),字符串类型的主键对表的损耗是很大的,一方面会增加索引树的存储空间,其次长期的增删改会导致大量的碎片,数据表数据操作的效率也没有自增主键高,如果考虑到分布式,可以采用雪花ID来进行赋值,uuid 作为主键可以用 自增主键+业务code 的方式进行替代。求求大佬们,别再用UUID主键了。

  • 业务上的枚举值怎么选型?
    通常来说,我们业务上的枚举值通常是一些有限的状态值,在前面也有说过,enum 的性能并没有 tinyint 好,而且会有 alter table 的开销,一般情况下,我们会优先使用 tinyint 进行进行状态的存储,在代码中对这些数字进行常量转换,当然为了提高数据表的可读性,应该在注释中标识好其枚举值含义,如果你坚持使用 enum,那么请记住,数字类型枚举尽量不要使用 enum,避免产生不必要的麻烦。

  • IP 地址该如何存储?
    大家知道,ipv4 实际上就是32位无符号的整数,为了让人们阅读方面,才会有所谓的点分十进制写法,因此有些人喜欢使用varchar(15) 来存储IP地址,实际上是不可取的,我们应该使用 4个字节的 int 进行存储,并且MySQL提供了 inet_aton() 和 inet_ntoa() 两个函数来帮助我们进行快速的转换

MySQL的优化包含着一系列的东西,选择好的字段类型是基础,其次还有索引的设置,存储引擎的选择,SQL 语句的优化,配置参数的影响 等等,这些相关的优化我会在后面的文章进行一一讲解,希望能对大家带来帮助。

猜你喜欢

转载自blog.csdn.net/qq_38378384/article/details/114218224