序
合理的表设计是SQL优化的基础,所以在做SQL优化之前先了解下建表的一些基本原则。
字段类型解析
整数
- 支持非负属性UNSIGNED,大致可以使正数的上限提高一倍。
- 整数类型的宽度对大多应用没有意义,例如int(10)和int(1)对于MySQL存储和计算是没有区别的,只是当指定了zerofiil,返回结果会在值前面填充0返回
- 整数的运算速度快且没有精度问题,在某些时候可以通过适当的转换作为存储浮点数的替代方案
- IPV4本质上是32位无符号整数,利用INET_ATON将IP转化为数字,INET_NTOA将数字转化为IP。使用bigint替代字符串存储IP地址,可以进行对应范围查找。
实数
- FLOAT、DOUBLE在运算时有精度问题,但是由于因为CPU原生支持浮点数运算,速度比DECIMAL更快,大多时候占用空间更小。
- DECIMAL的高精度计算由MySQL服务器完成,相比于浮点运算速度更慢,但精度更高,大多数时候占用空间更大
字符串
VARCHAR(变长字符串)
- 当列的最大长度小于等于255时,需要额外1字节,否则需要额外2字节记录字符串的长度。
- 会保留原数据末尾的空格。
- 由于节省了空间,所以对性能有帮助,但由于行是变长的,当UPDATE为更长的字符串时可能导致InnoDB当前页没有足够的存储空间而发生页分裂。
- 适用场景
- 存储长度波动较大的数据,如:文章
- 字符串很少更新的场景,每次更新后都会重算并使用额外存储空间保存长度
- 适合保存多字节字符,如:汉字,特殊字符等
CHAR(定长字符串)
- 不保留原数据末尾的空格。
- 因为已经预留足够的存储空间,变更时发生页分裂的概率减小。
- 最大长度:255
- 适用场景
- 存储长度波动不大的数据,如:md5摘要
- 存储短字符串、经常更新的字符串
注意:字符串长度定义的是字符数而不是字节数。就是说char(10)同样存储10个字符,10个汉字和10个英文字母占用的存储空间是不同的。所以理论上不管VARCHAR还是CHAR更新后都可能导致需要更大的存储空间而发生页分裂。
慷慨是不明智的:能用varchar(5)则不用varchar(200)存储5个字符,因为更长的列将耗费更大的内存,MySQL会分配固定大小的内存块来保存内补值,特别当使用临时表时。只分配需要的空间。
Text Blog类型
- MySQL 把每个 BLOB 和 TEXT 值当作一个独立的对象处理
- 两者都是为了存储很大数据而设计的字符串类型,分别采用二进制和字符方式存储
- MySQL只对该类型的列的前max_sort_length字节排序而不是整个字符串
- 由于memery不支持blog和text, 如何查询了text列,并且使用临时表,就必须使用MyIsam磁盘临时表,将带来严重的性能开销
Binary
- MySQL使用\0填充binary,而不是空格,在查询时不会去掉填充值
- MySQL把Bit当做字符串类型,而不是数字。在数字运算场景中,结果是位字符串转化成的数字,尽量避免使用位数据类型。
Enum(枚举类型)
- 注意事项
- MySQL内部使用数字存储枚举类型,例如: enum(‘a’, ‘b’, ‘c’) 内部存储是整数 1,2,3
- 所以用枚举型做数学运算或者排序可能会得到"意外"的结果,其内部都是使用数字来做对应的操作而不是表面显示的字符串。
- 使用enum和varchar/char做关联速度会较慢,因为比较时,需要先转化枚举类型为字符串。
- 枚举字符串列表是固定的,每次增减字符串都要执行alter table,如果不是在列表末尾进行变更,还需要重建整个表,非常麻烦且耗时。
- 优势
- 由于枚举类型存储的整数,通过一个映射关系转化为字符串,所以非常节约存储空间。
日期类型
DATETIME
- 存储的是格式为YYYYMMDDHHMMSS的整数
- 存储范围更大,1001年-9999年,精确到毫秒(精度可以到1/ 10 ^ 6 秒)
- 占用空间更大,8字节
- 与时区无关
TIMESTAMP
- 存储的是从1970.01.01 00:00:00(格里尼治标准时间)以来的秒数,与UNIX时间戳相同
- 存储范围较小,1970年-2038年,精确到秒
- 占用空间较小,4字节
- 受时区影响
- 默认not null
- 支持自动更新timestamp列的值
DATE
- 占用的字节数比使用字符串、datetime、int存储要少,使用date类型只需要3个字节
- 使用date类型还可以利用日期时间函数进行日期之间的计算
- date类型用于保存1000-01-01到9999-12-31之间的日期
注意:不要使用字符串存储日期类型,占用空间大,损失日期类型函数的便捷性
建表规约
-
数据类型越小越好
在满足需求的前提下,越小的类型占用空间越小,间接减少磁盘I/O次数。
通常来说,越小的类型运算速度越快。
如果不允许负值,请勾选非负。 -
数据类型越简单越好
越简单的类型,比较或运算速度越快。例如:整型优于字符串。 -
使用Decimal存储小数
在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。 -
如果存储的字符串长度几乎相等,使用char定长字符串类型
-
如果字符串字段过长,超过2000,则考虑使用text类型并且独立一张表来存储该字段,使用主键关联,以减少常用数据所占用的存储空间,这样做的一个明显好处就是每个数据块中可以存储的数据条数可以大大增加,既减少物理 IO 次数,也能大大提高内存中的缓存命中率,避免影响其他字段索引效率。
-
联合查询列选择
- 要选择相同类型的字段,类型之间要精准匹配,包括unsigned,字符集等
- 尽量选择整型作为标识列,速度更快。
-
控制列的数量
MySQL的存储引擎API通过行缓冲格式在引擎层和服务器层之间拷贝数据,然后在服务器层将缓冲内容解码成各个列。从行缓冲中将编码过的列转换成行数据机构的操作代价非常高,转换的代价依赖于列的数量。所以当一个表的列很多,但是查询的时候可能只会用到少部分列时,这时转换的代价过高,会影响性能。 -
适当的冗余字段减少联表,提高查询效率是可行的,但要考虑以下几点
- 数据一致性
- 不是频繁更新的字段
- 不是 varchar 超长字段,更不能是 text 字段
-
单表行数超过 1000 万或者单表容量超过 2GB考虑分库分表
-
所有字段不允许Allow Null,必须有默认值。
NULL在MySQL中是一个特殊的值,在某些场景下可能导致不可预期的结果,例如:
- count(字段) 不会计算值为null的行
- 对null做算术运算的结果都是null
- 不能使用=、<、>这样的运算符
- 需要更多的存储空间
- NPE问题
-
不建议使用外键,外键概念在应用层解决
- 外键更新同时触发关联表更新,即级联更新,在分布式,高并发集群中,级联更新是强阻塞,存在数据库更新风暴的风险。
- 而且外键影响数据库的插入速度,也影响程序的可阅读行,增加维护的难度
-
不建议使用存储过程,存储过程不利于维护,难以调试和扩展,没有移植性
-
不建议使用MySQL分区表
-
注意字符集的选择,尽量选择相同的字符集编码,否则查询时可能出现意料之外的情况(例如联表查询时)。同时如果联表操作关联字段的字符集或者排序方式不同,会导致不能使用索引。字符集的继承规则 数据库服务器配置 >> 库 >> 表 >> 列,生效规则(就近原则) 列 > 表 > 库)
-
MySQL的数据类型可以精确到字段,所以当我们需要大型数据库中存放多字节数据的时候,可以通过对不同表不同字段使用不同的数据类型来较大程度减小数据存储量,进而降低 IO 操作次数并提高缓存命中率。
-
适当的数据冗余
- 被频繁引用且只能通过 Join 2张(或者更多)大表的方式才能得到的独立小字段。
- 这样的场景由于每次Join仅仅只是为了取得某个小字段的值,Join到的记录又大,会造成大量不必要的 IO,完全可以通过空间换取时间的方式来优化。不过,冗余的同时需要确保数据的一致性不会遭到破坏,确保更新的同时冗余字段也被更新。
范式与反范式
范式
优点:
- 范式化的更新通常比反范式要快 | 通常需要进行关联
- 当数据较好的范式化后,很少或者没有重复的数据
- 范式化的数据比较小,可以放在内存中,操作比较快
缺点: - 通常需要进行关联
反范式
优点:
- 所有的数据都在同一张表中,可以避免关联
- 可以设计有效的索引
缺点: - 表格内的冗余较多,删除数据时候会造成表有些有用的信息丢失
注意
在企业中很好能做到严格意义上的范式或者反范式,一般需要混合使用
案例:
- 在一个网站实例中,这个网站,允许用户发送消息,并且一些用户是付费用户。现在想查看付费用户最近的10条信息。 在user表和message表中都存储用户类型(account_type)而不用完全的反范式化。这避免了完全反范式化的插入和删除问题,因为即使没有消息的时候也绝不会丢失用户的信息。这样也不会把user_message表搞得太大,有利于高效地获取数据。
- 另一个从父表冗余一些数据到子表的理由是排序的需要。
- 缓存衍生值也是有用的。如果需要显示每个用户发了多少消息(类似论坛的),可以每次执行一个昂贵的自查询来计算并显示它;也可以在user表中建一个num_messages列,每当用户发新消息时更新这个值。
附录
各数据类型大小及取值范围
分类 | 类型 | 大小(字节) | 范围(有符号) | 范围(无符号) | 说明 |
---|---|---|---|---|---|
整型 | TINYINT | 1 | (-128,127) | (0,255) | |
SMALLINT | 2 | (-32768, 32767) | (0,63535) | ||
MEDIUMINT | 3 | (-8 388 608, 8 388 607) | (0, 16777215) | ||
INT | 4 | (-2147483648, 2147483647) | (0, 4294967295) | ||
BIGINT | 8 | (-9223372036854775808, 9223372036854775807) | (0,18446744073709551615) | ||
小数 | FLOAT | 4 | (-3.402 823 466 E+38, 1.175 494 351 E- 38),0,(1.175 494 351 E-38,3.402 823 466 351 E+38) | 0, (1.175 494 351 E-38, 3.402 823 466 E+38) | 单精度浮点数 |
DOUBLE | 8 | (1.797 693 134 862 315 7 E+308, 2.225 073 858 507 201 4 E-308), 0, (2.225 073 858 507 201 4E-308,1.7976931348623157E+308) | 0, (2.225 073 858 507 201 4 E- 308, 1.797 693 134 862 315 7 E+308) | 双精度浮点数 | |
DECIMAL | DECIMAL(M,D) ,如果M>D,为 M+2否则为D+2 | 依赖于M和D的值 | 依赖于M和D的值 | DECIMAL最适合保存准确度要求高,而且用于计算的数据 | |
日期时间类型 | DATE | 3 | 1000-01-01 到 9999-12-31 | YYYY-MM-DD | 日期值 |
TIME | 3 | '-838:59:59' 到 '838:59:59' | HH:MM:SS | 时间值或持续时间 | |
YEAR | 1 | 1901 到 2155 | YYYY | 年份值 | |
DATETIME | 8 | 1000-01-01 00:00:00 到 9999-12-31 23:59:59 | YYYY-MM-DD HH:MM:SS | 混合日期和时间值,与时间无关 | |
TIMESTAMP | 4 | 1970-01-01 00:00:00 到 2038-01-19 03:14:07 | YYMMDD hhmmss | 存储的是UTC时间戳,与时区有关 | |
字符串 | CHAR | 0-255 | 0-255 | 0-255 | 定长字符串,char(n)当插入的字符串实际长度不足n时, 插入空格进行补充保存。在进行检索时,尾部的空格会被去掉。 |
VARCHAR | 0-65535 | 0-65535 | 0-65535 | 变长字符串,varchar(n)中的n代表最大列长度,插入的 字符串实际长度不足n时不会补充空格。 | |
TINYTEXT | 0-255 | 0-255 | 0-255 | 短文本字符串 | |
TEXT | 0-65535 | 0-65535 | 0-65535 | 较长文本字符串 | |
MEDIUMTEXT | 0-16777215 | 0-16777215 | 0-16777215 | 长文本字符串 | |
LONGTEXT | 0-4294967295 | 0-4294967295 | 0-4294967295 | 极大文本字符串 | |
二进制字符串 | TINYBLOB | 0-255 | 0-255 | 0-255 | 不超过 255 个字符的二进制数据 |
BLOG | 0-65535 | 0-65535 | 0-65535 | 较长文本的二进制数据 | |
MEDIUMBLOG | 0-16777215 | 0-16777215 | 0-16777215 | 长文本的二进制数据 | |
LONGBLOG | 0-4294967295 | 0-4294967295 | 0-4294967295 | 极大的二进制数据 |
系列文章
上一篇:【MySQL优化(三)】性能监控分析 - Performance Schema
下一篇:【MySQL优化(五)】InnoDB索引结构及特点