1.选择优化的数据类型
更小的通常更好
一般情况下选择可以正确存储数据的最小数据类型。更小的类型通常更快,占用更少的硬盘、内存、CPU等。但要确保没有递归需要存储的值的范围。
简单就好
简单数据类型的操作通常需要更少的CPU周期。整形比字符操作代价更低。列子:
1.用MySQL内建的类型而不是字符串来存储日期和h时间
2.另外一个用整型存储IP地址
尽量避免NULL
通常情况下指定列为NULL。如果查询中包含NULL列,MySQL更难优化。因为可为NULL的列被索引时,索引统计和值比较都比较复杂。列被指定为NULL每个索引记录都要一个额外字节。
通常把NULL列改为NOT NULL 带来的提升比较小。所以调优时没有必要在schema中查找并修改这种情况。在设计的时候,避免设计NULL列。
1.1.整数类型
TINYINT : 8位
SMALLINT:16
MEDIUMINT:24
INT: 32
BIGINT: 64
长度 -2(N-1) ~2(N-1)
整数类型有可选的UNSIGNED类型,标识不允许负值,这个大致可以使正数的范围提高一倍。
有符号和无符号类型使用相同的存储空间,并且具有相同的性能。
整数计算一般使用64位的BIGINT整数
1.2.实数类型
DECIMAL存储比BIGINT还大的整数
DECIMAL类型用于储存精确的小数
浮点和DECIMAL类型可以指定精度
在对小数进行精确计算的时候使用DECIMAL---财务数据。但在数据量比较大的时候,可以考虑使用BIGINT代替DECIMAL,将需要的货币单位根据小数乘以相应的倍数即可。避免浮点数不精确和DECIMAL精确计算代价高的问题
1.3.字符串类型
VARCHAR
1.存储可变长字符串。比定长类型更节省空间,因为它仅需要必要的空间。
列外:当表使用ROW_FORMAT= FIXED创建的话,每一行都会使用定长存储。浪费空间。
2.VARCHAR使用1-2个额外字节记录字符串的长度:列的最大长度<=255字节,则只使用1个字节表示,否则使用2个字节。假设使用Latin1字符集,VARCHAR(10)需要11个存储空间,VARCHAR(1001)需要1002个存储空间。
3.Update时可能使行变得比原来更长,导致额外工作。如果行占用的空间增长,并且在页内没有更多的空间可以存储,在这种情况下不同引擎处理方式不一样。MyISAM会将行拆成不同的片段存储,InnoDB则需要分裂页来使得行可以放进页内。
符合使用VARCHAR:
1.字符串的最大长度比平均长度大很多;
2.列的更新很少,碎片不是问题。
3.InnoDB将过长的VARCHAR存储为BLOB
CHAR
CHAR是定长的:MYSQL总是根据定义的字符串长度来分配足够的空间,并且会剔除末尾的空格。
CHAR适合存储很短的字符串,或者所有的字符串都接近于同一个长度。
对于经常改变的数据CHAR也比VARCHAR更好,因为定长CHAR不容易产生碎片。对应非常短的列CHAR比VARCHAR在空间上也更有效率。
二进制数据存储一般使用BINARY和CARBINARY类型。
使用枚举(ENUM)代替字符串类型
枚举是按照内部存储的整数而不是定义的字符串进行排序的。所以我们一般按照需要的顺序来定义枚举序列。另外也可以在查询中使用FIELD()函数显示的指定排序,但这会导致MySQL无法利用索引消除排序。
CREATE TABLE t1 (e ENUM('finsh', 'apple', 'dog') NOT NULL);
INSERT INTO t1 (e) VALUES ('finsh'), ('dog'),('apple');
这三行数据实际存储为整数,而不是字符串
SELECT e+0 FROM t1;
1.4.日期和时间类型
DATETIME
能保存大范围的值,从1001年到9999年,精度为秒。他把时间和日期封装到YYYYMMDDHHMMSS的整数中,与时区无关。使用8个字节的存储空间。
TIMESTAMP
保存了从1970年1月1日午夜以来的秒数,和UNIX时间戳相同。只用4个字节的存储空间,比DATETIME小的多;只能表示从1970到2038年。
TIMESTAMP依赖于时区。MYSQL服务器、操作系统,以及客户端链接都有时区设置。
插入时没有指定第一个TIMESTAMP列的值,MySQL设置这个列的值为当前时间。TIMESTAMP列默认为NULL。
除了特殊行为之外,一般尽量使用TIMESTAMP的值,因为它比DATETIME效率更高。不推荐将其转为Unix时间戳,因为不会带来任何收益。整数格式保存时间的格式通常不方便处理,所以通常不这么做。
如果存储比秒更小的单位,MYSQL目前没有提供合适的数据类型,不过可以使用其他格式:BIGINT存储微妙级别的时间戳,或者使用DOUBLE存储秒之后的小数部分。
4.5.位数据类型
位类型,技术上来说都是字符串类型。
BIT
使用BIT列在一列或多列中存储一个或多个true/false值。BIT(1)定义包含一个位的字段、BIT(2)定义包含2个位,以此类推。最大长度64个位。
MYSQL把BIT当作字符串类型,所以当检索BIT(1)的值的时候,结果时包含二进制0或1的字符串,而不是ASCII码的0或1。
而在数字上下文检索的时候,结果是将字符串转为数字。
如果在一个bit的存储空间保存true/false值,另一个方法是创建一个为空的CHAR(0)列。该列可以保存空值NULL或者长度为0 的字符串(空字符串)
SET
如果需要保存很多true/false值,可以考虑合并这些列到一个SET数据类型,它在MySQL内部是以一系列打包的位的集合来表示的。这样有效地利用了空间。缺点是改变列的成本太高,ALTER TABLE是非常昂贵的操作。一般来说,也无法在SET列上通过索引进行查找。
4.6.选择标识符
标识列又称自增长列,可以不手动插入的列,系统提供默认值。为标识列选择合适的数据非常重要,一般选择时要考虑存储类型,还需要考虑MYSQL对这种类型怎么比较和计算。例如,MySQL在内部使用整数存储ENUM和SET类型,然后在比较时转为字符串。
技巧:
1.选择最小数据类型。TINYINT选INT都满足的情况下选择TINYINT。
2.整数类型时标识列最好的选择,因为他们很快并且可以使用AUTO INCREMENT;
3.避免使用ENUM和SET类型。
4.如果可以,避免使用字符串类型作为标识列,因为他们很消耗空间。并且比数字类型慢。尤其是在MyISAM表使用字符串要小心,因为它默认会对字符串使用压缩引擎,会导致查询变慢。
5.对于一些随机字符串也要注意。UUID() 、MD5等生成的字符串会任意分布在很大的空间内。会导致INSERT以及一些语句变得很慢:
- 因为插入值会随机的写到索引的不同位置,所以insert语句很慢。这回导致页分裂、磁盘随机访问,以及对于聚簇存储引擎产生新的聚簇索引碎片。
- SELECT语句变慢,因为逻辑上相邻的行会分布在磁盘和内存不同的地方
- 随机值导致缓存对所有类型的查询语句效果都很差,因为会使得缓存赖以工作的访问局部性原理失效。整个数据都变热。
如果存储UUID,应该移除“-”号;或者更好的做法是用UNHEX()函数转换UUID为16字节的数字,并存储在一个BINARY(16)列中。检索时可以通过HEX()函数来格式化为十六进制格式。
1.7.特殊类型数据
某些类型的数据类型并不直接与内置类型一致。比如低于秒级精度的时间戳。
另一个例子时一个IPv4地址。人们常使用VARCHAR(15)
来存储IP地址。然而,它们实际上时32位无符号整数,不是字符串。所以应该使用无符号整数存储IP地址。MySQL提供INET_ATON()
和INET_NTOA()
函数来进行转换。
2.注意MySQL schema设计中的陷阱
1.太多的列
MySQL的存储引擎API工作时需要在服务器和存储引擎之间通过缓冲行格式拷贝数据,然后在服务器层将缓冲内容解码成各个列。从缓冲中将编码通过的列转成数据结构的代价是非常高的。
2.太多关联
所谓的 "实体-属性-值"(EVA)设计模式是一个糟糕的设计模式。一般,单个查询在12个表以内做关联。
3.全能的枚举
防止过度使用枚举
4.变相的枚举
枚举列允许在列中存储一组定义值中的单个值,集合(SET)允许在列中存储一组定义值的一个或多个值。有时候这个可能会导致混乱。
ex:
CTEATE TABLE ...(
is default set('Y', 'N') NOT NULL default 'N'
这里真和假两种情况不会同时出现,那么毫无疑问应该使用枚举替代集合列。
5.非此发明的NULL
之前我们提到避免使用NULL,并建议尽可能的采用替代方案。
- 字符串NULL用空字符串代替"NULL"
- 不要走极端,改用NULL还是得用NULL
- MySQL会在所以存储NULL,ORACLE不会。
3.范式和反范式
1.范式的优点和缺点
- 范式化的更新操作通常比反范式化要快
- 当数据较好的范式化时,只有很少或没有重复数据,所以只需要修改很少的数据。
- 范式化的表通常很小,可以更好的放在内存中,所以执行会更快
- 很少有多余的数据意味着检索列表数据时更少需要DISTINCT或者 GROUP BY语句。
范式化的缺点通常是需要关联。通常稍微复杂一些的查询语句在符合范式的schema上都可能需要至少一次关联,也许更多。这不但代价昂贵,也可能使一些索引策略无效。例如,范式化将列存放在不同的表中,而这些列如果在一个表中本可以属于同一个索引。
2.反范式的优点和缺点
反范式化的schema因为所有数据都在一张表中,可以很好的避免关联。如果不需要关联表,则对大部分查询最差的情况--即使表没有使用索引--全表扫描。当数据比内存大时可能比关联要快的多,因为这避免了随机I/O。
3.混用范式和反范式化
常见的反范式化数据的方法时复制或者缓存,在不同的表中存储相同的特列,在5.0的版本更新中,可以使用触发器更新缓存
4.缓存表和汇总表
术语“缓存表”和“汇总表”没有标准的定义。我们用术语“缓存表”来表示存储那些可以比较简单的从schema其他表获取(获取速度慢)数据的表。而术语“汇总表”则保存的是使用“GROUP BY” 语句聚合数据的表。
1.物化视图
预先计算并且存储在磁盘上的表,可以通过各种各样的策略刷新和更新。MySQL并不原生支持物化视图。一般使用Flexviews。
2.计数器表
如果在表中保存计数器,则在更新计算器的时候可能碰到并发问题。计数器在Web应用中很常见。缓存用户朋友数,文件下载次数等。
假设有个计数器表,只有一行数据记录网站的点击数:
CREATE TABLE hit_counter(
cnt int unsigned not null
) ENGINE = InnoDB
网站每次点击都会导致计数器更新
UPDATE hit_counter SET cnt = cnt + 1;
问题在一这个事务只能串行执行。要获得更高的并发性能,也可以将计数器保存在多行中,每次随机选择一行进行更新,这样对计数器表结构进行如下修改:
CREATE TABLE hit_counter (
slot tinyint unsigned not null primary key,
cnt int unsigned not null
) ENGINE = InnoDB
然后预先在这个表增加100行数据。现在选择一个随机的slot进行更新:
UPDATE hit_counter SET cnt = cnt + 1 WHERE slot = RAND() * 100;
要获得统计结果,使用下面的查询:
SELECT SUM(cnt) FROM hit_counter;
一个常见的需求是每隔一段新的时间开始一个新的计数器(每天一个)。如果要这么做,可以修个一个表设计:
CREATE TABLE daily_hit_counter (
day date not null,
slot tinyhit unsigned not null,
cnt int unsigned not null,
primary key(day, slot)
) ENGINE = InnoDB;
在这个场景中,可以不用预先生成行, 而用 ON DUPLICATEKEY UPDATE 代替:
INSERT INTO daily_hit_counter(day, slot, cnt)
VALUES(CURRENT_DATE, RAND() * 100, 1)
ON DUPLICATE KEY UPDATE cnt = cnt + 1
若果希望减少表的行数,避免表变得太大,可以写一个周期行执行任务,将所有结果合并到0号slot,并且删除其他slot。
mysql> UPDATE daily_hit_counter as c INNER JOIN(
SELECT day, SUM(cnt) AS cnt , MIN(slot) AS mslot
FROM daily_hit_counter
GROUP BY day
) AS x USING (day)
SET c.cnt = IF(c.slot = x.mslot, x.cnt, 0),
c.slot = IF (c.slot = x.mslot, 0, c.slot);
mysql> DELETE FROM daily_hit_counter WHERE slot <> 0 AND cnt = 0;
更快的度,更慢的写
提升查询速度,经常会捡一些额外的索引,增加多余列,甚至是创建缓存表和汇总表。这写方法增加了查询的负担,也需要额外的维护任务,但在设计高性能数据库时,这些都是常见技巧:虽然写操作变慢了,但是显著提高了度的性能。但是写操作并不是付出的唯一代价,还可能同时增加读操作和写操作的开发难度。
5.加快ALTER TABLE 的操作的速度
MySQL的ALTER TABLE 操作性能对大表来说是个大问题。MySQL 执行大部分修改表结构的方法时用新的结构创建一个空表,从旧表中查出所有的数据插入新表,然后删除旧表。这样操作可能花费很长时间。常见的场景有两种:一种时现在一台不提供服务的机器上执行ALTER TABLE 操作,然后和提供服务的机器进行主库切换;另外一种是进行:影子拷贝,是用要求的表结构创建一张和源无关的新表,然后通过重命名和删表操作交换两张表。
不是所有的ALTER TABLE 操作都会引起表重建。例如,有两种方法可以修改和删除一个列的默认值。一种是MODIFY COLUMN ,另一种是ALTER COLUMN,这个操作会修改 .frm 文件而不涉及表数据,所以这个操作是非常快的
1.只修改.frm文件
这项操作有一定的风险。有时候MySQL会在没有必要的时候也重建表。下面的一些操作是有可能不需要重建的:
- 移除(不是增加)一个表的AUTO_INCREMENT属性
- 增加、移除,或更改ENUM和SET常量。如果移除的是已经有行数据用到其他值的常量,查询将会返回一个空字符串。
基本的技术是为想要的表结构创建一个新的.frm文件,然后用它替换掉已经存在的那张表的.frm文件
2.快速创建MyISAM索引
为了高效的载入数据到MyISAM,有个常用的技巧是先禁用索引、载入数据,然后重新启用索引。这个技巧能发挥作用是因为构建索引的工作被延迟到数据完全载入之后,这个时候已经可以通过排序来构建索引了。这样做会快很多,并且使得索引树的碎片减少,更加的紧凑。
不过这个方法对唯一性索引无效,因为DISABLE KEYS 只对非唯一性索引有效。MyISAM会在内存中构造唯一性索引,并且位载入的每一行检查唯一性。一旦索引大小超过了有效内存的大小,载入操作将变得越来越慢。
现代版本的InnoDB中,有个类似的技巧,依赖于InnoDB的快速在线索引创建功能。技巧是,先删除所有的非唯一性索引,然后增加新的列,最后重新创建删除掉的索引。Percona可以自动完成这些操作。
1. 用需要的表结构创建一张表,但是不包括索引。
2. 载入数据到表中以构建.MYD文件
3. 按照需要的结构创建另外一个空表,这次包含索引。这回创建需要的.frm和.MYI文件
4. 获取读锁并刷新表
5. 重命名第二张表的.frm和.MYI文件,让MySQL认为这是第一张表的文件
6. 释放读锁
7. 使用REPAIR TABLE 来重建表的索引。该操作会通过排序来构建所有的索引,包括唯一索引。
这些操作步骤对大表来说快很多
6.总结
- 尽量避免过度设计,例如会导致机器复杂查询的schema设计,或者有很多列的表设计(很多的意思是介于有点多和非常多之间)
- 使用小而简单的数据类型,除非真实数据模型中有相关需要,否则应该尽可能避免使用NULL值
- 注意可变长字符串,其在临时表和排序时可能导致悲观的按最大长度分配内存
- 尽量使用整型定义标识列
- 避免使用MySQL已经遗弃的特性,例如浮点数的精度,或者整数的显示宽度
- 小心使用ENUM 和SET。虽然他们用起来很方便,但是不要滥用,否则会变成陷阱。最好避免使用BIT
- 范式是好的,但是反范式有时候也是必需的,并且能够带来好处。预先计算、缓存或生成汇总表也可能获得很大的好处。
- ALTER TABLE 是让人痛苦的操作,因为在大部分情况下,它会锁表并且重建整张表。我们展示了一些特殊场景的方法,但是大部分尝尽,必须使用其他更加常规的方法,例如在备机执行ALTER TABLE 并在完成后把他切为主库。