【MySQL—优化】表设计与数据类型优化

良好的逻辑设计和物理设计是高性能的基石,应该根据系统将要执行的查询语句来设计schema,这每每须要权衡各类因素。例如,反范式的设计能够加快某些类型的查询,但同时可能使另外一些类型的查询变慢。好比添加计数表和汇总表是一种很好的优化查询的方式,但这些表的维护成本可能会很高。MySQL独有的特性和实现细节对性能的影响也很大。数据库

选择优化的数据类型

MySQL支持的数据类型很是多,选择正确的数据类型对于得到高性能相当重要。无论存储哪一种类型的数据,下面几个简单的原则都有助于作出更好的选择。缓存

更小的一般更好

通常状况下,应该尽可能使用能够正确存储数据的最小数据类型。更小的数据类型一般更快,由于它们占用更少的磁盘、内存和CPU缓存,而且处理时须要的CPU周期也更少。服务器

简单就好

简单数据类型的操做一般须要更少的CPU周期。例如,整型比字符操做代价更低,由于字符集和校对规则(排序规则)使字符比较比整型比较更复杂。这里有两个例子:一个是应该使用MySQL内建的类型而不是字符串来存储日期和时间,另一个是应该用整型存储IP地址。数据结构

尽可能避免NULL

若是查询中包含可为NULL的列,对MySQL来讲更难优化,不使用NULL的理由有:并发

  1. 全部使用NULL值的状况,均可以经过一个有意义的值的表示,这样有利于代码的可读性和可维护性,并能从约束上加强业务数据的规范性。
  2. NULL值到非NULL的更新没法作到原地更新,更容易发生索引分裂,从而影响性能。(null -> not null性能提高很小,除非肯定它带来了问题,不然不要当成优先的优化措施)
  3. NULL值在timestamp类型下容易出问题,特别是没有启用参数explicit_defaults_for_timestamp。
  4. NOT IN、!= 等负向条件查询在有 NULL 值的状况下返回永远为空结果,查询容易出错。
  5. NULL会使索引、索引统计和值比较都更加复杂,而且在MyISIM中须要额外一个字节的存储空间。

在为列选择数据类型时,第一步须要肯定合适的大类型:数字、字符串、时间等,下一步是选择具体类型。不少MySQL的数据类型能够存储相同类型的数据,只是存储的长度和范围不同、容许的精度不一样,或者须要的物理空间(磁盘和内存空间)不一样。相同大类型的不一样子类型数据有时也有一些特殊的行为和属性。函数

整数类型

若是存储整数,可使用这几种整数类型:TINYINT,SMALLINT,MEDIUMINT,INT,BIGINT。分别使用8,16,24,32,64位存储空间。它们能够存储的值的范围从−2(N−1)到2(N−1)−1,其中N是存储空间的位数。性能

整数类型有可选的UNSIGNED属性,表示不容许负值,这大体可使正数的上限提升一倍。例如TINYINT UNSIGNED能够存储的范围是0~255,而TINYINT的存储范围是−128~127。有符号和无符号类型使用相同的存储空间,并具备相同的性能,所以能够根据实际状况选择合适的类型。优化

注:IP地址其实是32位无符号整数,应该用INT存储,MySQL提供INETATON和INETNTOA两个转换IP地址的函数。网站

实数类型

实数是带有小数部分的数字。然而,它们不仅是为了存储小数部分,也可使用DECIMAL存储比BIGINT还大的整数。MySQL既支持精确类型,也支持不精确类型。FLOAT和DOUBLE类型支持使用标准的浮点运算进行近似计算,若是须要知道浮点运算是怎么计算的,则须要研究所使用的平台的浮点数的具体实现。DECIMAL类型用于存储精确的小数,在MySQL 5.0和更高版本,DECIMAL类型支持精确计算。编码

浮点和DECIMAL类型均可以指定精度。对于DECIMAL列,能够指定小数点先后所容许的最大位数,这会影响列的空间消耗,MySQL 5.0和更高版本将数字打包保存到一个二进制字符串中(每4个字节存9个数字)。例如,DECIMAL(18,9)小数点两边将各存储9个数字,一共使用9个字节:小数点前的数字用4个字节,小数点后的数字用4个字节,小数点自己占1个字节。

浮点类型在存储一样范围的值时,一般比DECIMAL使用更少的空间。FLOAT使用4个字节存储。DOUBLE占用8个字节,相比FLOAT有更高的精度和更大的范围。

由于须要额外的空间和计算开销,因此应该尽可能只在对小数进行精确计算时才使用DECIMAL——例如存储财务数据。但在数据量比较大的时候,能够考虑使用BIGINT代替DECIMAL,将须要存储的货币单位根据小数的位数乘以相应的倍数便可,这样能够同时避免浮点存储计算不精确和DECIMAL精确计算代价高的问题。

字符串类型

VARCHAR和CHAR类型

因为如今基本上全部的MySQL数据库使用的都是InnoDB存储引擎,并且使用的字符集都是utf8或者utf8mb4这样的多字节字符集。在这种状况下,varchar和char类型都须要使用1~2个额外字节去存储字符串的长度,此时char相比varchar已经不具备任何的优点,因此推荐全部的字符串类型都使用varchar

BLOB和TEXT类型

BLOB和TEXT都是为存储很大的数据而设计的字符串数据类型,分别采用二进制和字符方式存储。

MySQL对BLOB和TEXT列进行排序与其余类型是不一样的:它只对每一个列的最前max_sort_length字节而不是整个字符串作排序。若是只须要排序前面一小部分字符,则能够减少max_sort_length的配置,或者使用ORDER BY SUSTRING(column,length)。

MySQL不能将BLOB和TEXT列所有长度的字符串进行索引,也不能使用这些索引消除排序。同时由于Memory引擎不支持BLOB和TEXT类型,因此,若是查询使用了BLOB或TEXT列而且须要使用隐式临时表,将不得不使用磁盘临时表。

最好的解决方案是尽可能避免使用BLOB和TEXT类型。若是实在没法避免,有一个技巧是在全部用到BLOB字段的地方都使用SUBSTRING(column,length)将列值转换为字符串(在ORDER BY子句中也适用),这样就可使用内存临时表了。可是要确保截取的子字符串足够短,不会使临时表的大小超过max_heap_table_size或tmp_table_size,超过之后MySQL会将内存临时表转换为MyISAM磁盘临时表。

日期和时间类型

DATETIME类型能保存1001年到9999年范围的值,精度为秒,与时区无关,使用8个字节的存储空间。TIMESTAAMP类型保存了格林尼治标准时间以来的秒数,只能表示1970年到2038年范围的值,显示的值依赖时区,使用4个字节的存储空间。

一般应该尽可能使用TIMESTAMP,由于它比DATETIME空间效率更高。

注:MySQL5.6.4版本开始支持比秒更小的存储粒度,格式为 时间类型(如timestamp)(n),n最大为6。

标识列数据类型选择

整数一般是标识列最好的选择,由于它们很快而且可使用AUTO_INCREMENT。

若是可能,应该避免使用字符串类型做为标识列,例如MD5()、SHA1()或者UUID()产生的字符串。由于它们很消耗空间,而且一般比数字类型慢。这些函数生成的新值会任意分布在很大的空间内,这会致使INSERT以及一些SELECT语句变得很慢:

  • 由于插入值会随机地写到索引的不一样位置,因此使得INSERT语句更慢。这会致使页分裂、磁盘随机访问,以及对于聚簇存储引擎产生聚簇索引碎片
  • SELECT语句会变得更慢,由于逻辑上相邻的行会分布在磁盘和内存的不一样地方。
  • 随机值致使缓存对全部类型的查询语句效果都不好,由于会使得缓存赖以工做的访问局部性原理失效。若是整个数据集都同样的“热”,那么缓存任何一部分特定数据到内存都没有好处;若是工做集比内存大,缓存将会有不少刷新和不命中。

很差的Schema设计实践

过多的列

MySQL的存储引擎API工做时须要在服务器层和存储引擎层之间经过行缓冲格式拷贝数据,而后在服务器层将缓冲内容解码成各个列。从行缓冲中将编码过的列转换成行数据结构的操做代价是很是高的,转换的代价依赖于列的数量。

过多的关联

若是查询中存在过多的关联,那么解析和优化查询的代价会成为MySQL的问题。一个粗略的经验法则,若是但愿查询执行得快速且并发性好,单个查询最好在12个表之内作关联。

过分使用ENUM

若是列的值可能会在之后扩充,那么就应该避免使用ENUM类型。在MySQL 5.1和更新版本中,若是不是在列表的末尾增长值会须要ALTER TABLE,对于大表来讲会致使严重的性能问题。

范式和反范式

范式的优势:

  1. 范式化的更新操做一般比反范式化要快。
  2. 当数据较好地范式化时,就只有不多或者没有重复数据,因此只须要修改更少的数据。
  3. 范式化的表一般更小,能够更好地放在内存里,因此执行操做会更快。
  4. 不多有多余的数据意味着检索列表数据时更少须要DISTINCT或者GROUP BY语句。

范式的缺点:

  1. 一般须要关联。
  2. 范式化可能将列存放在不一样的表中,这样会使某些索引失效。

混用范式化和反范式化

范式化和反范式化的schema各有优劣,怎么选择最佳的设计?事实是,在实际应用中常常须要混用,可能使用部分范式化的schema、缓存表,以及其余技巧。最多见的反范式化数据的方法是复制或者缓存,在不一样的表中存储相同的特定列。

在某些须要特定的查询条件和排序的状况下,能够在父表中冗余一些字段到子表。例若有user表和message表,要查询付费用户最近10条数据,彻底范式化查询的效率较低下,能够在message表中冗余帐户类型的字段并创建好索引,这将很是高效。不过更新帐户类型的时候须要更新两张表。这时须要考虑更新的频率及时长,来和查询的频率做比较,而后作出取舍

缓存衍生值也是有用的。若是须要显示每一个用户发了多少消息(像不少论坛作的),能够每次执行一个昂贵的子查询来计算并显示它,也能够在user表中建一个num_messages列,每当用户发新消息时更新这个值。

延伸阅读:
对关系型数据库五个范式的理解
如何理解关系型数据库的常见设计范式?

缓存表和汇总表

有时提高性能最好的方法是在同一张表中保存衍生的冗余数据。然而,有时也须要建立一张彻底独立的汇总表或缓存表(特别是为知足检索的需求时)。若是能允许少许的脏数据,这是很是好的方法,可是有时确实没有选择的余地(例如,须要避免复杂、昂贵的实时更新操做)。

术语“缓存表”和“汇总表”没有标准的含义。咱们用术语“缓存表”来表示存储那些能够比较简单地从schema其余表获取(可是每次获取的速度比较慢)数据的表(例如,逻辑上冗余的数据)。而术语“汇总表”时,则保存的是使用GROUP BY语句聚合数据的表(例如,数据不是逻辑上冗余的)。也有人使用术语“累积表(Roll-Up Table)”称呼这些表。由于这些数据被“累积”了。

以网站为例,假设须要计算以前24小时内发送的消息数。在一个很繁忙的网站不可能维护一个实时精确的计数器。做为替代方案,能够每小时生成一张汇总表。这样也许一条简单的查询就能够作到,而且比实时维护计数器要高效得多。缺点是计数器并非100%精确。

若是必须得到过去24小时准确的消息发送数量(没有遗漏),有另一种选择。以每小时汇总表为基础,把前23个完整的小时的统计表中的计数所有加起来,最后再加上开始阶段和结束阶段不完整的小时内的计数。

固然,更好的方法是使用内存数据库来完成这个计数器,例如Redis。

相关文章
相关标签/搜索