你的位置:首页 > 数据库

[数据库]【mysql】数据库Schema的优化

由于MySQL数据库是基于行(Row)存储的数据库,而数据库操作 IO 的时候是以 page(block)的方式,也就是说,如果我们每条记录所占用的空间量减小,就会使每个page中可存放的数据行数增大,那么每次 IO 可访问的行数也就增多了。反过来说,处理相同行数的数据,需要访问的 page 就会减少,也就是 IO 操作次数降低,直接提升性能。此外,由于我们的内存是有限的,增加每个page中存放的数据行数,就等于增加每个内存块的缓存数据量,同时还会提升内存换中数据命中的几率,也就是缓存命中率。

选择优化的数据类型

MySQL支持很多种不同的数据类型,并且选择正确的数据类型对于获得高性能至关重要。不管选择何种类型,下面的简单原则都会有助于做出更好的选择:

1、越简单越小越好

更小的数据类型通常更快,因为它们使用了更少的磁盘空间、内存和CPU缓存,而且需要的CPU周期也更少。越简单的数据类型,需要的CPU周期就越少。例如:比较整数的代价小于比较字符,因为字符集和排序规则使字符比较更复杂。

2、避免空(NULL)

要尽可地把字段定义为NOT NULL ,如果计划对列进行索引,就要尽量避免把它设置为可为空(NULL),NULL 类型比较特殊,SQL 难优化。虽然 MySQL NULL类型和 Oracle 的NULL 有差异,会进入索引中,但如果是一个组合索引,那么这个NULL 类型的字段会极大影响整个索引的效率。此外,NULL 在索引中的处理也是特殊的,也会占用额外的存放空间。很多人觉得 NULL 会节省一些空间,所以尽量让NULL来达到节省IO的目的,但是大部分时候这会适得其反,虽然空间上可能确实有一定节省,倒是带来了很多其他的优化问题,不但没有将IO量省下来,反而加大了SQL的IO量。所以尽量确保 DEFAULT 值不是 NULL,也是一个很好的表结构设计优化习惯。

  • 难以优化了使用了可空列的查询,它会使索引、索引统计和值更加复杂
  • 可空列需要更多的存储空间,还需要在内部进行特殊处理
  • 当可空列被索引的时候,每条记录都需要一个额外的字节,还能导致MyISAM中固定大小的索引(例如:一个整数列上的索引)变成可变大小的索引
  • 把NULL列改为NOT NULL 带来的性能提升很小,所以除非确定它引入了问题,否则就不要把它当成优先的优化措施
  • 即使要在表中存储可为空的字段,也是有办法不使用NULL的,可以考虑使用0,特殊值或字符串来代替它

3、尽可能不要直接 SELECT * 读取全部字段,尤其是表中存在 TEXT/BLOB 大列的时候。可能本来不需要读取这些列,但因为偷懒写成 SELECT * 导致内存buffer pool被这些“垃圾”数据把真正需要缓冲起来的热点数据给洗出去了。

4、超过20个长度的字符串列,最好创建前缀索引而非整列索引(例如:ALTER TABLE t1 ADD INDEX(user(20))),可以有效提高索引利用率,不过它的缺点是对这个列排序时用不到前缀索引。前缀索引的长度可以基于对该字段的统计得出,一般略大于平均长度一点就可以了。

5、定期用 pt-duplicate-key-checker 工具检查并删除重复的索引。比如 index idx1(a, b) 索引已经涵盖了 index idx2(a),就可以删除 idx2 索引了。

6、有多字段联合索引时,WHERE中过滤条件的字段顺序无需和索引一致,但如果有排序、分组则就必须一致了。

数据库类型

"TINYINT"=>1,"SMALLINT"=>2,"MEDIUMINT"=>3,"INT"=>4,"BIGINT"=>8,"FLOAT"=>'if ($M <= 24) {return 4;} else {return 8;}',"DOUBLE"=>8,"DECIMAL"=>'if ($M < $D) {return $D + 2;} elsif ($D > 0) {return $M + 2;} else {return $M + 1;}',"NUMERIC"=>'if ($M < $D) {return $D + 2;} elsif ($D > 0) {return $M + 2;} else {return $M + 1;}',"DATE"=>3,"DATETIME"=>8,"TIMESTAMP"=>4,"TIME"=>3,"YEAR"=>1,"CHAR"=>'$M',"VARCHAR"=>'$M+1',"TINYBLOB"=>'$M+1',"TINYTEXT"=>'$M+1',"BLOB"=>'$M+2',"TEXT"=>'$M+2',"MEDIUMBLOB"=>'$M+3',"MEDIUMTEXT"=>'$M+3',"LONGBLOB"=>'$M+4',"LONGTEXT"=>'$M+4'

1、数字类型

整数

如果存储整数,就可以使用这几种整数类型,如下所示

还有一个,BIT(M) approximately (M+7)/8 bytes

  • SIGNED 和 UNSIGNED 占用的存储空间是一样的,性能也一样,如果确定没有负数,那就是采用UNSIGNED吧,比如作为主键的ID
  • 整数运算通常使用64位的BINGINT整数
  • 你可以对整数类型定义宽度,比如INT(11),这对于大在多数应用程序是没有意义的,它不限制值的范围,只规定了的交互工具(例如命令客户端)用来显示字符的个数,对于存储计算,INT(1)和INT(20)是一样的
  • 用INT UNSIGNED 存储IPV4地址,用INET_ATON()、INET_NTOA()进行转换,基本上没必要使用CHAR(15)来存储,或者使用程序转换之后存入数据库,因为IP地址本身就是32大小位数字

实数

  • 实数有分数部分,然而,它们并不仅仅是分数,可以使用DECIMAL保存比出BIGINT还大的整数
  • 同时支持精确与非精确类型
  • FLOAT和DOUBLE类型支持使用标准的浮点运算进行近似计算
  • 比较起DECIMAL类型,浮点类型保存同样大小的值使用的空间通常更小,而且精度更大,范围更广,和整数一样,你选择的仅仅是存储类型
  • MYSQL在内部对浮点类型使用DOUBLE进行计算
  • 由于需要额外的空间和计算开销,只有在需要对小数进行精确的时候才使用DECIMAL,比如保存金融数据
  • 一般不要使用DOUBLE,不仅仅只是存储长度的问题,同时还会存在精确性的问题。同样,固定精度的小数,也不建议使用DECIMAL,建议乘以固定倍数转换成整数存储,可以大大节省存储空间,且不会带来任何附加维护成本

2、字符串类型

VARCHAR和CHAR

varchar:保存了可变长度的字符串,是使用得最多的字符串类型,它能比固定类型占用更少的存储空间,因为它只占用了自已需要的空间(也就是说较短的值占用的空间更小)。它使用额外的1-2个字节来存储值的长度。varchar能节约空间,所以对性能有帮助。然而,由于行的长度是可变的,它们在更新的时候可能会发生变化,这会引起额外的工作。当最大长度远大于平均长度,并且很少发生更新的时候,通常适合用varchar。这时候碎片就不会成为问题,还有你使用复杂的字符集,如utf-8时,它的每个字符都可能会占用不同的存储空间。varchar存取值时候,MySQL不会去掉字符串末尾的空格。

char:固定长度,char存取值时候,MySQL会去掉末尾的空格。Char在存储很短的字符串或长度近似相同的字符的时候很有用。例如,char适用于存储密码的MD5哈希值,它的长度总是一样的。对于经常改变的值,char也好于varchar,因为固定长度的行不容易产生碎片,对于很短的列,char的效率也高于varchar。Char(1)字符串对于单字节字符集只会占用1个字节,而varchar(1)则会占用2个字节,因为有一个字节用来存储其长度。

Char和varchar的兄弟类型为binary和varbinary,它们用于保存二进制的字符串,二进制字符串的传统的字符串很类似,但是它们保存的是字节而不是字符。填充也有所不同,MySQL使用\0(0字节)填充binary值,而不是空格,并且不会在获取数据的时候把填充的值截掉。

使用varchar(5)和varchar(200)保存“hello”占用的空间是一样的,但是使用较短的列有很大的优势,较大的列会使用更多的内存,因为MySQL通常会分配固定大小的内存块来保存值。这对排序或使用基于内存的临时表尤其不好。同样的事情也会发生在使用文件排序或基于磁盘的临时表的时候。

一般不要使用 TEXT 数据类型,其处理方式决定了他的性能要低于char或者是varchar类型的处理。定长字段,建议使用 CHAR 类型,不定长字段尽量使用 VARCHAR,且仅仅设定适当的最大长度,而不是非常随意的给一个很大的最大长度限定,因为不同的长度范围,MySQL也会有不一样的存储处理 

BLOB和TEXT

BLOB和TEXT分别用二进制和字符形式保存大量数据。

事实在,它们各有自的数据类型家族:

字符类型有TINYTEXT, SMALLTEXT, TEXT, MEDIUMTEXT和LONGTEXT,

二进制类型有TINYBLOB, SMALLBLOB, BLOB, MEDICMBLOB, LONGBLOB,BLOB 等同于SMALLBLOB, TEXT等同于SMALLTEXT

和其它类型不同,MYSQL把BLOB, TEXT当成有实体的对象来处理,存储引擎通常会特别地保存它们。INNODB在它们较大的时候会使用单独的“外部”存储来进行保存,每个值在行里面都需要1-4字节,并且还需要足够的外部存储空间来保存实际的值。

BLOB和TEXT唯一的区别就是BLOB保存的是二进制数据,没有字符集和排序规则,TEXT保存的是字符数据,有字符集和排序规则。

MYSQL对BLOB、TEXT列的排序方式和其它类型不同,它不会按照字符串的长度进行排序,而只是按照MAX_SORT_LENGTH规定的前若干个字节进行排序,如果只按照开始的几个字符排序,就可以减少MAX_SORT_LENGTH的值或使用ORDER BY SUBSTRING(COLUMN, LENGTH)。MYSQL不能索引这些数据类型的完整长度,也不能为排序而使用索引。

强烈反对在数据库中存放 BLOB 类型数据,虽然数据库提供了这样的功能,但这不是他所擅长的,我们更应该让合适的工具做他擅长的事情,才能将其发挥到极致

ENUM和SET

ENUM的内部存储机制是采用TINYINT或SMALLINT(并非CHAR/VARCHAR),而且即使需要增加新的类型,只要增加于末尾,修改结构也不需要重建表数据,ENUM列可以存储65535个不同的字符串,MYSQL以非常紧凑的方式保存了它们,根据列表中值的数量,MYSQL会把它们压缩到1-2个字节中,MYSQL在内部会把每个值都保存为整数,以表示值在列表中的位置,并且还保留了一份“查找表”来表示整数和字符串在表的.FRM文件中的映射关系。

ENUM最不好的一面是字符串是固定的,如果需要添加或者删除字符串必须使用ALTER TABLE,因此,对于一系列未知可能会改变的字符串,使用ENUM就不是一个好主意,MYSQL在内部的权限表中使用ENUM来保存Y值和N值。

由于MYSQL把每个值保存为整数,并且须进行查找才能把它转换成字符串形式,所以ENUM有一些开销。这通常可以由它们较小的大小进行弥补,但不总是这样,在特定情况下,把CHAR或VARCHAR列和ENUM列进行联接,可能会比联接另一个CHARA或VARCHAR列慢。

对于确定属性的字段,可以尝试使用SET类型,即使存在多种属性,同样可以游刃有余,可以结合FIND_IN_SET来使用,记住千万别用CHAR/VARCHAR来存储枚举数据

3、日期和时间类型

For example, TIME(0)TIME(2)TIME(4), and TIME(6) use 3, 4, 5, and 6 bytes, respectively. TIME andTIME(0) are equivalent and require the same storage

可以使用多种类型来保存各种日期和时间值,比中year和date,MySQL能存储的最细的时间粒度是秒,然而它可以用毫秒的粒度进行暂时的运算。

有几种相似的数据类型:DATETIME、TIMESTAMP和INT

  • DATETIME:范围是:1000-01-01 00:00:00 到 9999-12-31 23:59:59,与时区无关。使用了8个字节存储空间,可以使用NOW()变量来自动插入系统的当前时间
  • TIMESTAMP:保持了自1970年1月1日午夜(格林尼治标准时间)以来的秒数,和UNIX的时间戳相同。只使用了4个字节存储空间。可以用UNIX_TIMESTAMP()函数把日期转换为UNIX时间戳。显示的值依赖于时区,MYSQL服务器、操作系统及客户端连接都有时区设置。因此,保存0值的TIMESTAMP实际显示的时间是美国东部的时间1969-12-31 19:00:00,与格林尼治标准时间(GMT)相差5小时。最后,TIMESTAMP默认是NOT NULL,这也和其它的数据类型不一样
  • INT:占用4个字节;建立索引之后,查询速度快;条件范围搜索可以使用使用BETWEEN;可以使用FORM_UNIXTIME进行格式化

结论

  • INT适合需要进行大量时间范围查询的数据表
  • DATETIME类型适合用来记录数据的原始的创建时间,因为无论你怎么更改记录中其他字段的值,datetime字段的值都不会改变,除非你手动更改它
  • TIMESTAMP类型适合用来记录数据的最后修改时间,因为只要你更改了记录中其他字段的值,timestamp字段的值都会被自动更新

4、选择标识符

为标识列选择好的数据类型非常重要,你可能会更多地用它们和其他列做比较,还可能把它们用作其它表的外键,因为选择标识符列选择数据类型的时候,你也可能是在为相关的表选择数据类型。

当为标识符列选择数据类型的时候,不仅要考虑存储类型,还要考虑MYSQL如何对它们进行计算和比较。例如:MYSQL会在内部把ENUM和SET类型保存为整数,但是在比较的时候把它们转换为字符串。

一旦选择了数据类型,要确保在相关表中使用同样的类型。类型之前要精确匹配,包括诸如UNSIGNED这样的属性。混合不同的数据类型会导致性能问题,即使没有性能问题,隐式的类型转换也能导致难以察觉的错误,在你已经忘记了自己是在对不同类型做比较的时候,这些错误就会突然出现。

选择最小的数据类型能表明所需值的范围,并且为将来留出增长的空间。例如,如果用PORVINCE_ID来表示中国的省份,那么我们知道它不会产成千上万个值,因类就没有必要使用INT,用TINYINT就足够了,它比INT小3个节字,如果把一个表的主键是TINYINT,而另一个表以INT作为外键,那么就会造成较大的性能差距。

整数通常是标识符的最佳选择,因为它速度快,并且能使用AUTO_INCREMENT。

ENUM和SET通常不合适用作标识符,尽管它适合用来做静态的,包含了状态和“类型”和值的“定义表”。

ENUM和SET列适合用来性别、国家、省份这些固定不变的信息。

要尽可能的避免使用字符串来做标识符,因为它们占用了很多空间并且通常比整数类型要慢,特别注意不要在MYISAM表上使用字符串标识符。MYISAM默认情况下为字符串使用了压缩索引,这使查找更为缓慢。

MYISAM使用前缀压缩来减小索引大小,默认情况下会压缩字符串,也可以压缩整数

可以使用CREATE TABLE时用PACK_KEYS控制索引压缩的方式。

PACK_KEYS在MYSQL手册中如下描述:

如果您希望索引更小,则把此选项设置为1。这样做通常使更新速度变慢,同时阅读速度加快。把选项设置为0可以取消所有的关键字压缩。把此选项设置为DEFAULT时,存储引擎只压缩长的CHAR或VARCHAR列(仅限于MYISAM)。

如果您不使用PACK_KEYS,则默认操作是只压缩字符串,但不压缩数字。如果您使用PACK_KEYS=1,则对数字也进行压缩。

5、特殊类型的数据

一些数据类型没有直接对应的内建数据类型,精度低于秒的时间戳就是一个例子,另一个例子就是IP地址,人们通常使用varchar(15)来保存IP地址。但是,IP地址实际上是无符号的32位整数,而不是字符串。使用小数点来进行分纯粹是为了增加它的可读性。在实际使用时应用用无符号整数来存储IP地址。MySQL提供了INET_ATON()和INET_NTOA()函数在IP地址和整数之前转换。

数据库操作中最为耗时的操作就是 IO 处理,大部分数据库操作 90% 以上的时间都花在了 IO 读写上面。所以尽可能减少 IO 读写量,可以在很大程度上提高数据库操作的性能。

我们无法改变数据库中需要存储的数据,但是我们可以在这些数据的存储方式方面花一些心思。下面的这些关于字段类型的优化建议主要适用于记录条数较多,数据量较大的场景,因为精细化的数据类型设置可能带来维护成本的提高,过度优化也可能会带来其他的问题 

字符编码

字符集直接决定了数据在MySQL中的存储编码方式,由于同样的内容使用不同字符集表示所占用的空间大小会有较大的差异,所以通过使用合适的字符集,可以帮助我们尽可能减少数据量,进而减少IO操作次数。

纯拉丁字符能表示的内容,没必要选择 latin1 之外的其他字符编码,因为这会节省大量的存储空间
如果我们可以确定不需要存放多种语言,就没必要非得使用UTF8或者其他UNICODE字符类型,这回造成大量的存储空间浪费
MySQL的数据类型可以精确到字段,所以当我们需要大型数据库中存放多字节数据的时候,可以通过对不同表不同字段使用不同的数据类型来较大程度减小数据存储量,进而降低 IO 操作次数并提高缓存命中率
适当拆分
有些时候,我们可能会希望将一个完整的对象对应于一张数据库表,这对于应用程序开发来说是很有好的,但是有些时候可能会在性能上带来较大的问题。

当我们的表中存在类似于 TEXT 或者是很大的 VARCHAR类型的大字段的时候,如果我们大部分访问这张表的时候都不需要这个字段,我们就该义无反顾的将其拆分到另外的独立表中,以减少常用数据所占用的存储空间。这样做的一个明显好处就是每个数据块中可以存储的数据条数可以大大增加,既减少物理 IO 次数,也能大大提高内存中的缓存命中率。

上面几点的优化都是为了减少每条记录的存储空间大小,让每个数据库中能够存储更多的记录条数,以达到减少 IO 操作次数,提高缓存命中率。下面这个优化建议可能很多开发人员都会觉得不太理解,因为这是典型的反范式设计,而且也和上面的几点优化建议的目标相违背。

适度冗余

为什么我们要冗余?这不是增加了每条数据的大小,减少了每个数据块可存放记录条数吗?

确实,这样做是会增大每条记录的大小,降低每条记录中可存放数据的条数,但是在有些场景下我们仍然还是不得不这样做:

被频繁引用且只能通过 Join 2张(或者更多)大表的方式才能得到的独立小字段

这样的场景由于每次Join仅仅只是为了取得某个小字段的值,Join到的记录又大,会造成大量不必要的 IO,完全可以通过空间换取时间的方式来优化。不过,冗余的同时需要确保数据的一致性不会遭到破坏,确保更新的同时冗余字段也被更新