MySQL优化一:数据类型优化

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

选择优化的数据类型

MySQL支持的数据类型非常多,选择正确的数据类型对于获得高性能至关重要。不管存储哪种类型的数据,下面几个简单的原则都有助于做出更好的选择。

① 更小的通常更好:

一般情况下,应该尽量使用可以正确存储数据的最小数据类型。更小的数据类型通常更快,因为它们占用更少的磁盘、内存和CPU缓存,并且处理时需要的CPU周期也更少。

但是要确保没有低估需要存储的值的范围,因为在schema中的多个地方增加数据类型的范围是一个非常耗时的操作。如果无法确定哪个数据类型是最好的,就选择你认为不会超过范围的最小类型。

② 简单就好:

简单数据类型的操作通常需要更少的CPU周期。例如,整形比字符操作代价更低,因为字符集和校对规则(排序规则)使字符比较比整形比更复杂。

③ 尽量避免NULL:

很多表都包含可以为NULL的列,即使应用程序并不需要保存NULL也是如此,这是因为可为NULL是列的 默认属性。通常情况下最好指定为NOT NULL,除非真的需要存储NULL值。

如果查询中包含可为NULL的列,对MySQL来说更难优化,因为NULL的列使得索引、索引统计和值比较都更复杂。可为NULL的列会使用更多的存储空间,在MySQL里也需要特殊处理。当可以为NULL的列被索引时,每个索引记录需要一个额外的字节,在MyISAM里甚至还可能导致固定大小的索引变成可变大小的索引(例如只有一个整数列的索引)。

通常可以把NULL的列改为NOT NULL带带的性能提升比较小,所以调优时没有必要受限在现有schema中查找并修改这种情况,除非确定这会导致问题。但是,如果计划在列上建立索引,就应该尽量避免设计成可为NULL的列。

当然也有例外,例如InnoDB使用单独的位(bit)存储NULL值,所以对于稀疏数据(很多值为NULL,只有少数行非NULL)有很好的空间效率。但这一点不适用与MyISAM。

在为列选择数据类型时,第一步需要确定合适的大类型数字、字符串、时间等。这通常是很简单的,但是我们会提到一些特殊的不是那么直观的例子。

下一步是选择具体类型。很多MySQL的数据类型可以存储相同类型的数据,只是存储的长度和范围不一样、允许的经度不同,或者需要的无力空间(磁盘和内存空间)不同。相同大类型的不同子类型数据有时也有一些特殊的行为和属性。

例如,DATATIME和TIMESAMP列都可以存储相同类型的数据:时间和日期,精确到秒。然而TIMESTAMP只使用DATATIME一半的存储空间,并且会根据时区变化,具有特殊的自动更新能力。另一方面,TIMESTAMP允许的时间范围要小得多,有时候他的特殊能力也会成为障碍。

1 整数类型

有两种类型的数字:整数和实数。如果存储整数,可以使用者集中整数类型: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.

有符号和无符号类型使用相同的存储空间,并具有相同的性能,因此可以根据实际情况选择合适的类型。

你的选择决定MySQL是怎么在内存和磁盘中保存数据的。然而,整数计算一般使用64位的BIGINT整数,即使在32位环境也是如此。(一些聚合函数除外,它们使用DECIMAL或DOUBLE进行计算)

MySQL可以为整数类型指定宽度,例如INT(11),对大多数应用这是没有意义的:它不会限制值的合法范围,只是规定了MySQL的一些交互工具(例如MySQL命令行客户端)用来显示字符的个数。对于存储和计算来说,INT(1)和INT(20)是相同的。

2 实数类型

实数是带有小数部分的数字。然而,它们不只是为了存储小数部分;也可以使用DECIMAL存储比BIGINT还大的整数。MySQL即支持精确类型,也支持不精确类型。

FLOAT和DOUBLE类型支持使用标准的浮点运算进行近似计算。如果需要知道浮点运算是怎么计算的,则需要研究所使用的平台的浮点数的具体实现。

DECIMAL类型用于存储精确的小数。在MySQL5.0和更高版本,DECIMAL类型支持精确计算。MySQL4.1以及更早版本则使用浮点运算来实现DECIAML的计算,这样会因为经度损失导致一些奇怪的结果。在这些版本的MySQL中,DECIMAL只是一个存储类型。

因为CPU不支持对DECIMAL的直接计算,所以在MySQL5.0以及更高版本中,MySQL服务器自身实现了DECIMAL的高经度计算。相对而言,CPU直接支持原生浮点计算,所以浮点运算明显更快。

浮点和DECIMAL类型都可以指定经度。对于DECIMAL列,可以指定小数点前后所允许的最大位数。这会影响列的空间消耗。MySQL5.0和更高版本将数字打包保存到一个二进制字符串中(每4个字节存9个数字)。例如DECIMAL(18,9)小数点两边将各存储9个数字,一共使用9个字节:小数点前的数字用4个字节,小数点后的数字用4个字节,小数点本身占一个字节。

MySQL5.0和更高版本中的DECIMAL类型允许最多65个数字。而更早的版本中这个限制是254个数字,并且保存为未压缩的字符串。然而,这些版本实际上并不能在计算中使用这么大的数字,因为DECIMAL只是一种存储格式:在计算中DECIMAL会转换成DOUBLE类型。

有多重方法可以指定浮点列锁需要的经度,这会使得MySQL悄悄选择不同的数据类型,或者在存储时对值进行取舍。这些经度定义是非标准的,所以我们建议只指定数据类型,不指定精度。

浮点类型在存储同样范围的值时,通常比DECIMAL使用更少的空间。FLOAT使用4个字节存储。DOUBLE占用8个字节,相比FLOAT有更高精度和更大的范围。和整数类型一样,能选择的只是存储类型:MySQL使用DOUBLE作为内部浮点计算的类型。

因为需要额外的空间和计算开销,所以应该尽量只在对小数进行精确计算时才使用DECIMAL。例如存储财务数据。但在数据量比较大的时候,可以考虑使用BIGINT代替DECIMAL,将需要存储的货比单位根据小数的位数乘以响应的倍数即可。假设要存储财务数据精确到万分之一分,则可以吧所有金额乘100W,然后将结果存储在BIGINT里,这样可以同时避免浮点存储计算不精确和DECIMAL精确计算代价高的问题。

3 字符串类型

MySQL支持多种字符串类型,每种类型还有很多变种。这些数据在4.1和5.0版本发生了很大的变化,使得情况更加复杂。从MySQL4.1开始,每个字符串列可以定义自己的字符集和排序规则,或者说校对规则。这些东西会很大程度上影响性能。

VARCHAR和CHAR类型

VARCHAR和CHAR是两种最主要的字符串类型。不幸的是,很难精确的解释这些值是怎么存储在磁盘和内存中的,因为这根存储引擎的具体实现有关。下面假设使用的存储引擎是InnoDB或MyISAM。如果不是这两种引擎,请参考存储引擎相关文档。

VARCHAR

varchar类型用于存储可边长字符串,是最常见的字符串数据类型。它比定长类型更节省空间,因为它仅使用必要的空间。有一种情况例外,如果MySQL表使用ROW_FORMAT=FIXED创建的话,每一行都会使用定长存储,这会很浪费空间。

VARCHAR需要使用1或2个额外字节记录字符串的长度;如果列的最大长度小于或等于255字节,则只使用1个字节表示,否则使用2个字节。

VARCHAR节省了存储空间,所以对性能也有帮厨。但是,由于行是变长的,在UPDATE时可能使行变得比原来更长,这就导致需要做额外的工作。如果一个行占用的空间增长,并且在页内没有更多的空间可以存储,在这种情况下,不同的存储引擎的处理方式是不一样的。例如,MyISAM会将行拆分成不同的片段存储,InnoDB则需要分裂页来使行可以放入页内。其他一些存储引擎也许从不在原数据位置更新数据。

下面这些情况下是很合适使用VARCHAR的:

①  字符串列的最大长度比平均长度大很多

② 列的更新很少,所以碎片不是问题

③ 使用了想UTF-8这样复杂的字符集,每个字符都使用不同的字节数进行存储。

CHAR

CHAR类型是定长的,MySQL总是根据定义的字符串长度分配足够的空间。当存储CHAR值时,MySQL会删除所有的末尾空格。CHAR值会根据需要采用空格进行填充以方便比较。

CHAR适合存储很短的字符串,或者所有值都接近同一个长度。例如存储密码的MD5值,因为这是一个定长的值。对于经常变更的数据,CHAR也比VARCHAR更好,因为定长的CHAR类型不容易产生碎片。对于非常短的列,CHAR比VARCHAR在存储空间上也更有效率。

CHAR类型的这些行为可能有些难以理解,下面通过一个具体的例子来说明。首先我们创建一张只有一个CHAR(10)字段的表并插入几条数据 :

当检索这些值的时候,你会发现str3末尾的空格被截断了;

数据如何存储取决于存储引擎盖,并非所有的存储引擎都会按照相同的方式处理定长和变长的字符串。Memory引擎只支持定长的行,即使有变长字段也会根据最大长度分配空间。不过,田中和截取空格的行为在不同存储引擎都是一样的,因为这是在MySQL服务器层进行处理的。

与CHAR和VARCHAR类似的类型还有BINARY和VARBINARY,它们存储的是二进制字符串。二进制字符串跟常规字符串非常相似,但是二进制字符串存储的是字节码而不是字符。填充也不一样;MySQL填充BINARY采用的是\0 而不是空格,在检索时也不会去掉填充值。

当需要存储二进制数据,并且希望MySQL使用字节码而不是字符进行比较时,这些类型是非常有用的。二进制比较的优势并不仅仅体现在大小写敏感上。MySQL比较BINARY字符串时,每次按一个字节,并且根据该字节的数值进行比较。因此,二进制比较比字符比较简单很多,所以也更快。

需要注意的是:

使用VARCHAR(5)和VARCHAR(100)存储‘hello’的空间开销是一样的。那么使用更短的列有什么优势呢?

事实证明它的优势在于,更长的列会消耗更多的内存,因为MySQL通常会分配固定大小的内存块来保存内部值。尤其是使用内存临时表进行排序或操作时会特别糟糕。在利用磁盘临时表进行排序也很糟糕。

所以最好的策略就是只分配所需空间。

BLOB和TEXT类型

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

实际上,他们分别属于两种不同的数据类型;

字符类型是:TINYTEXT,SMALLTEXT,TEXT,MEDIUMTEXT,LONGTEXT;

二进制类型是:TINYBLOB,SMALLBLOB,BLOB,MEDIUMBLOB,LONGBLOB;

与其他类型不同,MySQL把每个BLOB和TEXT值当作一个独立的对象处理。存储引擎在存储时通常会做特殊处理。当BLOB和TEXT值太大时,InnoDB会使用专门的外部存储区域来进行存储,此时每个值在行内需要1~4个字节存储一个指针,然后在外部存储区域存储实际的值。

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列并且需要使用隐式临时表,将不得不使用MyISAM磁盘临时表,即使只有几行数据也是如此。

这会导致严重的性能开销。即使配置MySQL将临时表存储在内存块设备上,依然需要许多昂贵的系统调用。

最好的解决方案是尽量避免是用BLOB和TEXT类型。如果实在无法避免,有一个技巧是在所有用到BLOB字段的地方都是用SUBSTRING(column,length)将列值转换为字符串,这样就可以使用内存临时表了。但是要确保截取的子串够端,不会使临时表的大小超过max_heap_table_size或tmp_table_size,超过后MySQL会将内存临时表转换为MyISAM磁盘临时表。

最坏情况下的长度分配对于排序的时候也是一样的,所以这一招对内存中创建大临时表和文件排序,以及在磁盘上创建大临时表和文件排序这两种情况都很有帮助。

使用枚举(ENUM)代替字符串类型

有时候可以使用枚举列代替常用的字符串类型。枚举列可以把一些不重复的字符串存储成一个预定含义的集合。MySQL在存储枚举时非常紧凑,会根据列表值的数量压缩到一个或两个字节中。MySQL在内部会将每个值在列表中的位置保存为整数,并且在表的.frm文件中保存“数字 - 字符串”映射关系的查找表。下面有一个例子;

这三行数据实际存储为整数,而不是字符串。可以通过数字上下文环境检索看到这个双重属性:
 

如果使用数字作为ENUM枚举常量,这种双重性很容易导致混乱,所以尽量避免这么使用。

另外一个需要注意的是,枚举字段不是按照字符串进行排序的,而是按照内部存储的整数进行排序的。

一种绕过这种限制的方式是按照需要的顺序来定义枚举列。另外也可以在查询中使用FIELD()函数显示的指定排序顺序,但这回导致MySQL无法利用索引消除排序:

select e form enum_test order by field(e,'pig','fish','dog');

如果在定义时就是按照字母的顺序,就没有必要这么做了。

枚举最不好的地方时,字符串列表时固定的,添加或删除字符串必须使用ALTER TABLE。

因此,对于一系列未来可能会改变的字符串,使用枚举不是一个好主意,触发能接受只在列表末尾添加元素,这样在MySQL5.1中就可以补用重建整个表来完成修改。

由于MySQL把每个枚举值保存为整数,并且必须进行查找才能转换为字符串,所以枚举列有一些开销。通常枚举的列表都比较小,所以开销还可以控制,但也不能保证一直如此。在特定情况下,把CHAR/VARCHAR列与枚举列进行关联可能会比直接关联CHAR/VARCHAR列更慢。

4 日期和时间类型

MySQL可以使用许多类型来保存日期和时间值,例如YEAR和DATE。MySQL能存储的最小时间粒度为妙(MariaDB支持微妙级别的时间类型)。但是MySQL也可以使用微妙级的粒度进行临时运算,我们会展示怎么绕开这种存储限制。

大部分时间类型都没有替代品,因此没有什么是最佳选择的问题。唯一的问题是保存日期和时间的时候需要做什么。MySQL提供两种相似的日期类型;DATETIME和TIMESTAMP。对于很多应用程序,它们都能工作,但是在某些场景,一个比另一个工作的好。

① DATETIME

这个类型能保存大范围的值,从1001年到9999年,经度为秒。它把日期和时间封装到格式为YYYYMMDDHHMMSS的整数中,与时区无关。使用8个字节的存储空间。

默认情况下,MySQL以一种可排序的、无歧义的格式显示DATETO,E值,例如“2019-01-08 14:30:00”。这是ANSI标准定义的日期和时间表示方法。

② TIMESTAMP

就像它的名字一样,TIMETAMP类型保存从1970年1月1日午夜依赖的秒数,它和UNIX时间戳相同。TIMESTAMP只使用4个字节的存储空间,因为它的范围比DATETIME小的多;只能表示从1970年到2038年。MySQL提供了FROM_UNIXTIME()函数把UNIX时间戳转换为日期,并提供了UNIX_TIMESTAMP()函数把日期转换为Unix时间戳。

MySQL4.1以上版本按照DATRETIME的方式格式化TIMESTAMP的值,但是4.0及过去版本不会再各个部分之间显示任何标点符号。这仅仅是显示格式上的区别,TIMESTAMP的存储格式在各个版本都是一样的。

TIMESTAMP显示的值也依赖于时区。MySQL服务器、操作系统,以及客户端连接都有时区设置。

因此,存储值为0的TIMESTAMP在美国东部时区显示为“1969-12-31 19:00:00”,与格林尼时间差5个小时。有必要强调一下这个区别;如果多个时区存储或访问数据,TIMESTAMP和DATETIME的行为将很不一样。前者提供的值与时区有关系,后者则保留文本表示的日期和时间。

除了特殊行为之外,通常也应该尽量使用TIMESTAMP,因为它比DATETIME空间效率更高。有时候人们会将UNIX时间戳存储为整数值,但这不会带来任何收益。用整数保存时间的格式通常不方便处理,所以不推荐这样做。

如果需要存储比秒更小粒度的日期和时间值怎么办?MySQL目前没有提供合适的数据类型,但是可以使用自己的存储格式;可以使用BIGINT类型存储微妙级别的时间戳或者使用DOUBLE存储秒之后的小数部分,这两种方式都可以,或者也可以使用MariaDB替代MySQL。

5 位数据类型

MySQL有少数几种存储类型使用紧凑的位存储数据。所有这些位类型,不管底层存储格式和处理方式如何,从技术上来说都是字符串类型。

① BIT

在MySQL5.0之前,BIT是TINYINT的同义词。但是在5.0以后,这是一个特性完全不同的数据类型。下面我们将讨论下BIT类型新的行为特性。

可以使用BIT列在一列中存储一个或多个true/false值。BIT(1)定义一个包含单个为的字段,BIT(2)存储2个位,以此类推。BIT列的最大长度时64个位。

BIT的行为因存储引擎而异。MyISAM会打包存储所有的BIT列,所以17个单独的BIT列只需要17个位存储(假设没有可为NULL的列),这样MyISAM只使用3个字节就能存储着17个BIT列。其他存储引擎例如Memory和InnoDB,为每个BIT列使用一个足够存储的最小整数类型来存放,所以不能节省存储空间。

MySQL把BIT当作字符串类型,而不是数字类型。当检索BIT(1)的值时,结果是一个包含二进制0或1值的字符串,而不是ASCII码的0 或 1.然而,在数字上下文的场景中检索时,结果将是位字符串转换成的数字。如果需要和另外的值比较结果,一定要记得这一点。例如,如果存储一个值b'00111001'(二进制值等于57)到BIT(8)的列并且检索它,得到的内容时字符码为57的字符串。也就是得到ASCII码为57的字符串‘9’。但是在数字上下文场景中,得到的数字是57.

例:

这是相当令人费解的,所以我们认为应该谨慎使用BIT类型,对于大部分应用,最好避免使用这种类型。

如果想在一个it的存储空间中存储一个true/false值,另一个放马是创建一个可以为空的CHAR(0)列,该列可以保存空值或者长度为零的字符串。

② SET

如果需要保存很多true/false值,可以考虑合并这些列到一个SET数据类型,它在MySQL内部是以一系列打包的位的集合来表示的。这样就有效的利用了存储空间,并且MySQL有想FIND_IN_SET和FIELD()这样的函数,方便的在查询中使用。它的主要缺点是改变列的定义的代价较高,需要alter table。一般来说,也无法在set列上通过索引查找。

③ 在整数列上进行按位操作

一种替代SET的方式是使用一个整数包装一系列的位。例如,可以把8个位包装到一个TINYINT钟,并且按位操作来使用。可以在应用中为每个位定义名称常量来简化这个工作。

比起SET,这种办法主要的好处在于可以不适用alter table改变字段代表的枚举值,缺点是查询语句更难写,并且更难理解。

一个包装位的应用例子是保存权限的访问控制列表。每个位或者SET元素代表一个值,如果CAN_READ、CAN_WRITE,或者CAN_DELETE。如果使用SET列,可以让MySQL在定义里存储位到值的映射关系;如果使用整数列,则可以在应用代码里存储这个对应关系。例:

如果使用整数来存储,则可以参考下面的例子

这里我们使用MySQL变量来定义值,但是也可以在代码中使用常量来代替。

6 选择标识符(identifier)

为标识列选择合适的数据类型非常重要。一般来说更有可能用标识列与其他值进行比较,或者通过标识列寻找其他列。标识列也可能在另外的表中作为外键使用,所以为标识列选择数据类型时,应该选择跟关联表中的对应列一样的类型。

当选择标识列的类型时,不仅仅需要考虑存储类型,还需要考虑MySQL对这种类型怎么执行计算和比较。例如,MySQL在内部使用整数存储ENUM和SET类型,然后在做比较操作时转换为字符串。

一旦选的了一种类型,要确保在所有关联 表中都使用同样的类型。类型之间需要精确匹配,包括想UNSIGNED这样的属性。混用不同数据类型可能导致性能问题,即使没有性能 影响,在比较操作时隐式类型转换也可能导致很难发现的错误。这种错误可能会很久以后才出现,那时候你可能已经忘记是在比较不同的数据类型了。

在可以满足值的范围的需求,并且预留未来增长空间的前提下,应该选择最小的数据类型。例如有一个state_id列存储美国各洲的名字,就不需要几千个值,所以使用TINYINT足够存储,而且比INT少了3个字节。入股哦这个值作为其他表的外键,3个字节可能导致很大的性能差异。

① 整数类型

整数通常是标识类做好的选择,因为它们很快并且十一使用AUTO_INCREMENT.

② ENUM和SET类型

对于标识类来说,EMUM和SET类型通常是一个糟糕的选择,尽管对某些只包含固定状态或者类型的静态定义表来说可能是没有问题的。ENUM和SET列适合存储固定信息,例如有序的状态、产品类型、性别等。

③ 字符串类型

如果可能,应该避免使用字符串类型作为标识列,因为他们很销毁空间,并且通常比数字类型慢。尤其是在MyISAM表里使用字符串作为标识列时要特别小心。MyISAM默认对字符串使用压缩索引,这会导致查询慢得多。在我们的测试中,发现最多会有6倍的性能下降。

对完全随机的字符串也需要多加注意,例如MD5()、SHAL()或UUID()产生的字符串。这些函数生成的新值会任意分布在很大的空间内,这会导致Insert以及一些select语句变得很慢。

[1] 因为插入值会随机的写道索引的不同位置,所以使insert语句变慢。这会导致页分裂、磁盘随机访问,以及对于聚簇存储引擎产生聚簇索引碎片。

[2] select会变的更慢,因为所及上相邻的行辉分布在磁盘和内存的不同地方。

[3] 随机值导致缓存对所有类型的查询语句效果都很差,因为会使缓存依赖的访问局部性原理失效。如果整个数据集都是一样的,那么缓存任何一部分特定数据到内存都没有好处;如果工作集比内存大,缓存将会有很多刷新和不命中。

如果存储UUID值,则应该移除“-”符号,或者最好的做法是用UNHEX()函数换行UUID值为16字节的数字,并且存储在一个BINARY(16)列中。检索时可以通过HEX()函数来格式化为16进制格式。

UUID()生成的值与加密三列函数SHAL()生成的值有不同的特征;UUID值虽然分布也不均匀,但是还是有一定顺序的。尽管如此,还是不如递增的整数好用。

注意:当心自动生成的schema

我们已经介绍了大部分重要的数据类型的考虑,但是还没有提到自动生成的schema设计有多糟糕。

写的很烂的schema迁移程序,或者自动生成schema的程序,都会导致严重的性能问题。有些程序存储任何东西都会使用很大的VARCHAR列,或者对需要在关联时使用不同的数据类型。如果schema是自动生成的,一定要反复检查确认。

ORM系统是另一种常见的性能噩梦,一些ORM系统会存储任意类型的数据到任意类型的后端数据存储中,这通常以为着并没有设计使用更优的数据类型来存储。有时会为每个对象的每个属性使用单独的行,甚至使用基于时间戳的版本控制,导致单个属性会有多个版本存在。

7 特殊类型数据

某些类型的数据并不直接与内置类型一致。低于秒级精度的时间戳就是一个例子;

另一个例子是IPv4地址。人们经常是用VARCHAR(15)列来存储IP地址。然而,它们实际上是32位无符号整数,不是字符串。用小数点将地址分成4端的表示方法只是为了让人们易读。所以应该用无符号整数存储IP地址。MySQL提供INET_ATON()和INET_NTOA()函数在这两种表示方法之间转换。

猜你喜欢

转载自blog.csdn.net/yongqi_wang/article/details/86015706