对数据库影响最大的实际上是数据库的结构,良好的数据库逻辑设计和物理设计是数据库获得高性能的基础,在设计数据库的时候不能只考虑业务需要,还要考虑将来如何使用这个数据库编写什么样的查询语句才能得到想要的数据,应该根据系统将要执行的查询语句来设计数据库的结构。
数据库结构设计最好使得查询语句尽量简单,为了达到这个目的通常需要考虑很多因素,例如为了加快查询加入了反范式设计,但同时可能影响其他语句性能,所以再设计数据库的结构的时候要平衡各方面的利益,已达到最好的效果。
(1)减少数据的冗余(数据冗余是指在数据库中存在相同的数据,或者某些数据可以由其他数据计算得到,注意尽量减少不代表完全避免数据冗余)
(2)尽量避免数据维护中出现更新、插入和删除异常
插入异常:如果表中的某个实体随着另一个实体而存在,即如果缺少某个实体就无法表示另一个实体
更新异常:如果更改表中的某个实体的独立属性时,需要对多行进行更新
删除异常:如果删除表中的某一实体则会导致其他实体的消失
总结:要避免异常,就需要对数据库结构进行范式化设计。
(3)节约数据存储空间
(4)提高查询的效率
(1)需求分析:全面了解产品设计的存储需求、数据处理需求、数据安全性与完整性;
(2)逻辑设计:设计数据的逻辑存储结构。数据实体之间的逻辑关系,解决数据冗余和数据维护异常。(数据库设计范式)
(3)物理设计:根据所使用的数据库特点进行表结构设计
(4)维护优化:根据实际情况对索引、存储结构等进行优化
数据库设计范式是指设计出没有数据冗余和数据维护异常的数据结构,一般满足数据库三范式的要求即可达到这个要求,但是这样的数据库不一定是性能最好的设计,在设计数据库的时候要考虑实际的业务使用情况,使用一些反范式。
数据库设计三范式
1NF:数据库中的所有字段都只具有单一属性;单一属性的列是基本的数据类型所构成的;设计出来的表都是简单的二维表(即属性具有原子性,不可再分解)
2NF:要求一个表中只具有一个业务主键,非主键字段必须完全函数依赖于主键(即不能存在非主键列对只对部分主键的依赖关系)
3NF:
指每一个非主属性既不依赖也不传递依赖于业务主键,也就是在第二范式的基础上消除了非主属性对主键的传递依赖(不存在非主键属性传递依赖于主键属性)
注意:完全符合范式化的设计有时并不能得到良好的SQL查询性能。
反范式化是针对范式化而言的,在前面介绍了数据库设计的范式,所谓的反范式化就是为了性能和读取效率的考虑而适当的对数据库设计范式的要求进行违反,而允许存在少量的数据冗余,换句话说反范式化就是使用空间换取时间。
不能完全按照范式化的要求进行设计考虑如何使用表,在范式化的基础上结合查询语句的效率,利用经常使用的查询语句来对数据库结构进行反范式化优化。
(1)范式化设计
优点:
缺点:
(2)反范式设计
优点:
缺点:
总结:完全的范式化和反范式化对于数据库设计都不是最好的,在设计数据库时要结合使用范式化和反范式化设计,才能设计出符合需求的高性能数据库结构。
根据所选择的的关系型数据库的特点对逻辑模型进行存储结构设计,物理设计涉及的内容如下:
(1)定义数据库、表及字段的命名规范
(2)选择合适的存储引擎
可以针对特定的应用场景来选择存储引擎,可以参考我的另一篇博客:MySQL逻辑架构以及常用存储引擎
(3)为表中的字段选择合适的数据类型(后面会介绍)
(4)建立数据结构
MySQL支持的数据类型非常多,选择正确的数据类型对于获得高性能至关重要。不管存储哪种数据类型,下面几个简单的原则都有助于做出更好的选择。
(1)更小的通常更好
一般情况下,应该尽量使用可以正确存储数据的最小数据类型。更小的数据类型通常更快。因为它们占用更少的磁盘、内存和CPU缓存,并且处理时需要的CPU周期也更少。但是要确保没有低估需要存储的值的范围,因为在schema中的增加数据类型的范围是一个非常耗时和痛苦的操作。如果无法确定哪个数据类型是最好的,就选择不会超过范围的最小类型。
(2)简单就好
简单数据类型的操作通常需要更少的CPU周期。例如,整型比字符操作代价更低,因为字符集和校对规则(排序规则)使字符比较比整型比较更复杂。这里有两个例子:一个是应该使用MySQL内建的类型而不是字符串来存储时间和日期,另一个是应该用整型存储IP地址。
(3)尽量避免使用NULL
很多表都包含可为NULL的列,即使应用程序并不需要保存NULL也是如此,这是因为可为NULL是列的默认属性(TIMESTAMP除外),然而通常情况下最好指定列为NOT NULL,除非真的需要存储NULL值。
如果查询中包含可为NULL的列,对MySQL来说更难优化,因为可为NULL的列使得索引统计和值比较更加复杂。可为NULL的列会使用更多的存储空间,在MySQL里也需要特殊的处理。当可为NULL的字段被索引时,每个索引记录需要一个额外的字节,在MyASIM里甚至还可能导致固定大小的索引(例如只有一个整数列的索引)变成可变大小的索引。
通常把可为NULL的列改为NOT NULL 带来的性能提升比较小,所以(调优时)没有必要首先在现有schema中查找并修改这种情况,除非确定这会导致问题。但是,如果计划在列上建索引,就应该尽量避免设计为NULL的列。当然也有一些例外,例如值得一提的是,InnoDB使用单独的位(bit)存储NULL值,所以对于稀疏数据(很多值为NULL,只有少数行是非NULL)有很好的空间效率。但这一点不适用于MyISAM。
在为列选择数据类型时,第一步要确定适合得大类型:数字、字符串、时间等。下一步是选择具体类型,很多MySQL的数据类型可以存储相同类型的数据,只是存储的长度和范围不一样,允许的精度不同,或者需要的物理空间(磁盘和内存空间)不同。
字段类型选择的原则:当一个列可以选择多种数据类型时,应该优先考虑数字类型,其次是日期或二进制类型,最后是字符类型。对于相同级别的数据类型,应该优先选择占用空间小的数据类型。
主要原因如下:
(1)对数据进行比较,比如查询条件,设置条件或者关联排序的时候,字符类型处理与使用的字符集和排序规则是有关的,而数字和二进制不能参照字典的排序规则,其处理是按照二进制大小来进行的,同样的数据字符串处理比数字处理要慢。
(2)数据库中数据处理是以页为单位的,每一页能存储的数据量的一定的,在innodb中页的大小是16k,页的长度越小,在页中能容纳的数据行就越多,在加载同样的数据时,使用宽度小的类型要比使用宽度大的类型所需要加载的数据页就越小,从而减少磁盘IO。
有两种类型的数字:整数和实数。如果存储整数,可以使用这几种整数类型:TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT。分别使用8、16、24、32、64位存储空间。它们可以存储的值的范围从-2 ^ (N-1)到 2 ^ (N-1) -1,其中N是存储空间的位数。整数类型有可选UNSIGNED属性,表示不允许有负值,这大致可以使正数的上限提高一倍。有符号和无符号类型使用相同的存储空间,并具有相同的性能,因此可以根据实际情况选择合适的类型。
其存储空间和范围如下表所示:
需要注意的是:MySQL可以为整数类型指定宽度,例如INT(11),对大多数应用这是没有意义的:它不会限制值的合法范围,只是规定了MySQL的一些交互工具用来显示字符的个数。对于存储和计算来说,INT(1)和INT(20)是相同的。
实数是带有小数部分的数字。然而,它们不只是为了存储小数部分;也可以使用DEMICAL存储比BIGINT还要大的整数。MySQL既支持精确类型,也支持不精确类型。浮点数类型在存储同样范围的值时,通常比DEMICAL使用更少的空间。FLOAT使用4个字节,DOUBLE使用8个字节,DOUBLE比FLOAT有更高的精度和更大的范围。和整数类型一样,能选择的只是存储类型,MySQL使用DOUBLE作为内部浮点计算的类型。
因为需要额外的空间和计算开销,所以应该尽量只在对小数进行精确计算时才使用DEMICAL——例如财务数据。但在数据量比较大的时候,可以考虑使用BIGINT代替DEMICAL,将需要存储的货币单位根据小数的位数乘以相应的倍数即可。假设要存储财务数据精确到万分之一分,则可以把所有的金额乘以一万,然后将结果存储在BIGINT中,这样可以同时避免浮点存储计算不精确和DEMICAL精确计算代价高的问题。
!!需要注意的是:VARCHAR(n)和CHAR(n)中的n是以字符为单位而不是字节,以utf-8为例一个中文字符占用三个字节
(1)VARCHAR
特点:
适用场景:字符串列的最大长度比平均长度大很多;字符串列很少被更新(VARCHAR类型的更新由于字符串的长度会发生变化,会引起存储页的分裂,会造成很多的存储碎片);使用了多字节字符集存储字符串
(2)CHAR
特点:
CHAR类型的适用场景:CHAR类型适合存储所长度近似的值;CHAR类型适合存储短字符串;CHAR类型适合存储经常更新的字符串列,因为定长的CHAR类型不容易产生碎片
使用VARCHAR(5)和CHAR(200)存储’hello’的空间开销是一样的,那么使用更短的列有什么优势?
大量的实时证明更长的列会消耗更多的内存,因为MySQL通常会分配固定大小的内存来保存内部值,尤其是使用内存临时表进行排序或操作时会特别糟糕。在利用磁盘临时表进行排序时也同样糟糕。所以最好的策略是只分配真正需要的空间。
BLOB和TEXT都是为了存储很大的数据而设计的字符串数据类型,分别采用二进制和字符方式存储。BLOB和TEXT之间仅有的不同是BLOB类型存储的是二进制数据,没有排序规则或字符集,而TEXT类型有字符集和排序规则。
与其他类型不同,MySQL把每个BLOB和TEXT值当作一个独立的对象处理。存储引擎在存储时通常会做特殊处理。当BLOB和TEXT值太大时,InnoDB会使用专门的“外部”存储区域来进行存储,此时每个值在行内需要1~4个字节存储一个指针,然后在外部存储区域存储实际的值。
MySQL对BLOB和TEXT列进行排序与其他类型是不同的:它只对每个列的最前max_sort_length字节而不是整个字符串做排序(即前缀索引)。如果只需要排序前面一小部分字符,则可以减小max_sort_length的配置,或者用ORDER BY SUSTRING(column, length)。MySQL不能将BLOB和TEXT列全部长度的字符串进行索引,也不能使用这些索引消除排序。
在介绍Memory存储引擎时,说过Memory不支持BLOB和TEXT类型的列,并且每一行的长度是固定的,所以即使制定了VARCHAR列,实际存储的时候也会转换成CHAR存储。如果MySQL在执行查询过程中需要使用临时表来保存中间结果,内部使用的临时表就是Memory表,如果列中包含了BLOB或者TEXT的话就会转换成MyISAM存储引擎。会造成严重的性能开销。
有时候可以使用枚举列代替常用的字符串类型。枚举列可以把一些不重复的字符串存储成一个预定义的集合。MySQL在存储枚举时非常紧凑,会根据列表值的数量压缩到一个或者两个字节中。MySQL在内部会将每个值在列表中的位置保存为整数,并且在表的.fm文件中保存“数字——字符串”映射关系的“查找表”,如果使用数字作为ENUM枚举常量,这种双重性很容易导致混乱。
枚举最不好的地方是,字符串列表是固定的,添加或删除字符串必须使用ALTER TABLE。因此,对于一系列未来可能会改变的字符串,使用枚举不是一个好主意,除非能接受只在列表末尾添加元素。于MySQL把每个枚举值保存为整数,并且必须进行查找才能转换成字符串,所以枚举列有些开销。
(1)DATETIME
这个类型能保存大范围的值,从1001年到9999年,精度为秒,当然也可以datetime(6)的形式显示微妙。它把日期和时间封装到格式为YYYYMMDDHHMMSS的整数中,与时区无关。使用8个字节的存储空间。
(2)TIMESTAMP
存储了由格林尼治时间1970年1月1日到当前时间的秒数(存储时间戳)以YYYY-MM-DD HH:MM:SS[.fraction] 的格式显示,占用4个字节,时间范围从 1970-01-01到2038-01-19,timestamp类型显示依赖于所指定时区。
默认情况下,如果插入时没有指定第一个TIMESTAMP列的值,MySQL则设置这个列的值为当前时间(除非在UPDATE语句中明确指定了值)。你可以配置任何TIMESTAMP列的插入和更新行为。最后,TIMESTAMP列默认为NOT NULL,这也和其他的数据类型不一样,使用这个功能来标识每行数据的最后修改时间,数据分析类应用就可以根据这个列来抽取数据到数据仓库环境中。
(3)DATE类型和TIME类型(MySQL5.6之后新增)
DATE类型的优点:占用的字节数比使用字符串、datetime、int存储要少,使用date类型只需要3个字节;使用DATE类型还可以利用日期时间函数进行日期之间的计算date类型用于保存1000-01-01到9999-12-31之间的日期
TIME类型用于存储时间数据,格式为HH:MM:SS
存储日期时间数据的注意事项:
不要使用字符串类型来存储日期时间数据,日期时间类型通常比字符串占用的存储空间小,日期时间类型在进行查找过滤时可以利用日期来进行对比,日期事件类型还有丰富的处理函数,可以方便的对时期类型进行日期计算;使用Int存储日期时间不如使用Timestamp类型
可以使用BIT列存储ture/false 值。BIT(1)定义一个包含单个位字段,BIT(2)存储2个位,BIT列最大长度是64个位。BIT的行为因存储引擎而异。MyISAM会打包存储所有的BIT列,所以17个单独的BIT列只需要17个BIT个位存储(假设没有可为NULL的列),这样MyISAM只使用3个字节就能存储这17个BIT列。其他存储引擎例如Memory和InnoDB,为每个BIT列使用一个足够存储的最小整数类型来存放,所以不能节省存储空间。MySQL把BIT当做字符串类型,而不是数字类型。
谨慎使用BIT类型。对于大部分应用,最好避免使用这种类型。如果想存储true/false值,另一个方法是创建一个可以为空的CHAR(0)列,该列可以保存空值(NULL)或者长度为零的字符串(空字符串)
其他的类型可以参考博客:Schema与数据类型优化
使用MySQL一般选择InnoDB存储引擎,除了为每个列选择合适的类型之外,还要为表选择合适的主键,如何为InnoDB选择主键?
设计出符合查询性能要求和范式化要求的数据库结构。