文章目录
MySQL性能调优
MySQL 性能调优是一个复杂且多维度的过程,下面从数据库设计、查询优化、配置参数调整、硬件优化几个方面为你介绍相关的调优方法。
数据库设计优化
- 合理设计表结构:确保表结构遵循数据库设计范式,减少数据冗余,同时要根据实际业务需求灵活调整,避免过度范式化导致的查询复杂度过高。
- 选择合适的数据类型:使用合适的数据类型可以减少存储空间,提高查询性能。例如,对于固定长度的字符串使用
CHAR
,对于可变长度的字符串使用VARCHAR
;对于整数类型,根据取值范围选择合适的类型,如TINYINT
、SMALLINT
等。 - 建立适当的索引:索引可以加快数据的查找速度,但过多的索引会增加写操作的开销,因此需要根据查询需求建立适当的索引。例如,对于经常用于
WHERE
子句、JOIN
条件和ORDER BY
子句的列,可以考虑创建索引。
查询优化
- 避免全表扫描:尽量使用索引来避免全表扫描,例如在
WHERE
子句中使用索引列进行过滤。 - 优化子查询:子查询可能会导致性能问题,可以考虑使用
JOIN
来替代子查询。 - 减少不必要的列:在查询时只选择需要的列,避免使用
SELECT *
。
配置参数调整
- 调整内存分配:根据服务器的硬件资源和业务需求,调整
innodb_buffer_pool_size
、key_buffer_size
等参数,以提高缓存命中率。 - 调整日志参数:根据业务需求调整
log_bin
、innodb_log_file_size
等参数,以平衡数据安全性和性能。
硬件优化
- 使用高速存储设备:如 SSD 可以显著提高磁盘 I/O 性能。
- 增加内存:足够的内存可以减少磁盘 I/O,提高查询性能。
1.InnoDB引擎底层解析
InnoDB的三大特性:
- 双写机制
- Buffer Pool
- 自适应Hash索引
自适应Hash索引在之前的索引课中已经讲到了,这节课不再做陈述。同时我们对InnoDB不能只是光看亮点,还是要体系化的去学习。
InnoDB的内存结构和磁盘存储结构图总结如下:
看这种结构图大家肯定是比较晕的,所以我们用需求来驱动进行讲解。
1、InnoDB对于我们来说还是一个黑盒,我们只负责使用客户端发送请求并等待服务器返回结果,表中的数据到底存到了哪里?
2、表中的数据以什么格式存放的?
3、InnoDB是以什么方式来访问的这些数据?
4、InnoDB中的事务、锁等的原理是怎样?
1.1.InnoDB记录存储结构和索引页结构
InnoDB是一个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。而真正处理数据的过程是发生在内存中的,所以需要把磁盘中的数据加载到内存中,如果是处理写入或修改请求的话,还需要把内存中的内容刷新到磁盘上。而我们知道读写磁盘的速度非常慢,和内存读写差了几个数量级,所以当我们想从表中获取某些记录时,InnoDB存储引擎需要一条一条的把记录从磁盘上读出来么?
InnoDB采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。
我们平时是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式。
1.1.1.行格式
InnoDB存储引擎设计了4种不同类型的行格式,分别是Compact、Redundant、Dynamic和Compressed行格式。我们可以查看默认值:
show variables like 'innodb_default_row_format'
我们可以在创建或修改表的语句中指定行格式:
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
1.1.1.1.COMPACT
create table text(c1 VARCHAR(10)) ROW_FORMAT=COMPACT;
COMPACT行格式示意图如下:
变长字段长度列表
我们知道MySQL支持一些变长的数据类型,比如VARCHAR(M)、VARBINARY(M)、各种TEXT类型,各种BLOB类型,我们也可以把拥有这些数据类型的列称为变长字段,变长字段中存储多少字节的数据是不固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。如果该可变字段允许存储的最大字节数(M×W)超过255字节并且真实存储的字节数(L)超过127字节,则使用2个字节,否则使用1个字节。
NULL值列表
表中的某些列可能存储NULL值,如果把这些NULL值都放到记录的真实数据中存储会很占地方,所以Compact行格式把这些值为NULL的列统一管理起来,存储到NULL值列表。每个允许存储NULL的列对应一个二进制位,二进制位的值为1时,代表该列的值为NULL。二进制位的值为0时,代表该列的值不为NULL。
记录头信息
它是由固定的5个字节组成。5个字节也就是40个二进制位,不同的位代表不同的意思。
二进制位数 | 解释 | |
---|---|---|
预留位1 | 1 | 没有使用 |
预留位2 | 1 | 没有使用 |
delete_mask | 1 | 标记该记录是否被删除 |
min_rec_mask | 1 | B+树的每层非叶子节点中的最小记录都会添加该标记 |
n_owned | 4 | 表示当前记录拥有的记录数 |
heap_no | 13 | 表示当前记录在页的位置信息 |
record_type | 3 | 表示当前记录的类型, 0表示普通记录, 1表示B+树非叶子节点记录, 2表示最小记录, 3表示最大记录 |
next_record | 16 | 表示下一条记录的相对位置 |
隐藏列信息
MySQL会为每个记录默认的添加一些列(也称为隐藏列),包括:
DB_ROW_ID(row_id):非必须,6字节,表示行ID,唯一标识一条记录
InnoDB表对主键的生成策略是:优先使用用户自定义主键作为主键,如果用户没有定义主键,则选取一个Unique键作为主键,如果表中连Unique键都没有定义的话,则InnoDB会为表默认添加一个名为row_id的隐藏列作为主键。
DB_TRX_ID:必须,6字节,表示事务ID
DB_ROLL_PTR:必须,7字节,表示回滚指
其他的行格式和Compact行格式差别不大。
1.1.1.2.Redundant行格式
Redundant行格式是MySQL5.0之前用的一种行格式,不予深究。
1.1.1.3.Dynamic和Compressed行格式
MySQL5.7的默认行格式就是Dynamic,Dynamic和Compressed行格式和Compact行格式挺像,只不过在处理行溢出数据时有所不同。Compressed行格式和Dynamic不同的一点是,Compressed行格式会采用压缩算法对页面进行压缩,以节省空间。
1.1.1.4. 数据溢出
如果我们定义一个表,表中只有一个VARCHAR字段,如下:
CREATE TABLE test_varchar( c VARCHAR(60000) )
然后往这个字段插入60000个字符,会发生什么?
前边说过,MySQL中磁盘和内存交互的基本单位是页,也就是说MySQL是以页为基本单位来管理存储空间的,我们的记录都会被分配到某个页中存储。而一个页的大小一般是16KB,也就是16384字节,而一个VARCHAR(M)类型的列就最多可以存储65532个字节,这样就可能造成一个页存放不了一条记录的情况。
在Compact和Redundant行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的该列的前768个字节的数据,然后把剩余的数据分散存储在几个其他的页中,记录的真实数据处用20个字节存储指向这些页的地址。这个过程也叫做行溢出,存储超出768字节的那些页面也被称为溢出页。
Dynamic和Compressed行格式,不会在记录的真实数据处存储字段真实数据的前768个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址。
1.1.2.索引页格式
前边我们简单提了一下页的概念,它是InnoDB管理存储空间的基本单位,一个页的大小一般是16KB。
InnoDB为了不同的目的而设计了许多种不同类型的页,存放我们表中记录的那种类型的页自然也是其中的一员,官方称这种存放记录的页为索引(INDEX)页,不过要理解成数据页也没问题,毕竟存在着聚簇索引这种索引和数据混合的东西。
1.1.2.1.数据页结构
一个InnoDB数据页的存储空间大致被划分成了7个部分:
name | 名称 | 长度 | 备注 |
---|---|---|---|
File Header | 文件头部 | 38字节 | 页的一些通用信息 |
Page Header | 页面头部 | 56字节 | 数据页专有的一些信息 |
Infimum + Supremum | 最小记录和最大记录 | 26字节 | 两个虚拟的行记录 |
User Records | 用户记录 | 大小不确定 | 实际存储的行记录内容 |
Free Space | 空闲空间 | 大小不确定 | 页中尚未使用的空间 |
Page Directory | 页面目录 | 大小不确定 | 页中的某些记录的相对位置 |
File Trailer | 文件尾部 | 8字节 | 校验页是否完整 |
User Records
我们自己存储的记录会按照我们指定的行格式存储到User Records部分。但是在一开始生成页的时候,其实并没有User Records这个部分,每当我们插入一条记录,都会从Free Space部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records部分,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。
当前记录被删除时,则会修改记录头信息中的delete_mask为1,也就是说被删除的记录还在页中,还在真实的磁盘上。这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后把其他的记录在磁盘上重新排列需要性能消耗。
所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的垃圾链表,在这个链表中的记录占用的空间称之为所谓的可重用空间,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。
同时我们插入的记录在会记录自己在本页中的位置,写入了记录头信息中heap_no部分。heap_no值为0和1的记录是InnoDB自动给每个页增加的两个记录,称为伪记录或者虚拟记录。这两个伪记录一个代表最小记录,一个代表最大记录,这两条存放在页的User Records部分,他们被单独放在一个称为Infimum + Supremum的部分。
记录头信息中next_record记录了从当前记录的真实数据到下一条记录的真实数据的地址偏移量。这其实是个链表,可以通过一条记录找到它的下一条记录。但是需要注意注意再注意的一点是,下一条记录指得并不是按照我们插入顺序的下一条记录,而是按照主键值由小到大的顺序的下一条记录。而且规定 Infimum记录(也就是最小记录) 的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录)
我们的记录按照主键从小到大的顺序形成了一个单链表,记录被删除,则从这个链表上摘除。
Page Directory
Page Directory主要是解决记录链表的查找问题。如果我们想根据主键值查找页中的某条记录该咋办?按链表查找的办法:从Infimum记录(最小记录)开始,沿着链表一直往后找,总会找到或者找不到。
InnoDB的改进是,为页中的记录再制作了一个目录,他们的制作过程是这样的:
1、将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
2、每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示该记录拥有多少条记录,也就是该组内共有几条记录。
3、将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory,也就是页目录页面目录中的这些地址偏移量被称为槽(英文名:Slot),所以这个页面目录就是由槽组成的。
4、每个分组中的记录条数是有规定的:对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在1到8 条之间,剩下的分组中记录的条数范围只能在是 4到8 条之间,如下图:
这样,一个数据页中查找指定主键值的记录的过程分为两步:
通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。
通过记录的next_record属性遍历该槽所在的组中的各个记录。
Page Header
InnoDB为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫Page Header的部分,它是页结构的第二部分,这个部分占用固定的56个字节,专门存储各种状态信息。
File Header
File Header针对各种类型的页都通用,也就是说不同类型的页都会以File Header作为第一个组成部分,它描述了一些针对各种页都通用的一些信息,比方说页的类型,这个页的编号是多少,它的上一个页、下一个页是谁,页的校验和等等,这个部分占用固定的38个字节。
页的类型,包括Undo日志页、段信息节点、InsertBuffer空闲列表、Insert Buffer位图、系统页、事务系统数据、表空间头部信息、扩展描述页、溢出页、索引页,有些页会在后面的课程看到。
同时通过上一个页、下一个页建立一个双向链表把许许多多的页就串联起来,而无需这些页在物理上真正连着。但是并不是所有类型的页都有上一个和下一个页的属性,数据页是有这两个属性的,所以所有的数据页其实是一个双向链表。
File Trailer
我们知道InnoDB存储引擎会把数据存储到磁盘上,但是磁盘速度太慢,需要以页为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间需要把数据同步到磁盘中。但是在同步了一半的时候中断电了咋办?
为了检测一个页是否完整(也就是在同步的时候有没有发生只同步一半的尴尬情况),InnoDB每个页的尾部都加了一个File Trailer部分,这个部分由8个字节组成,可以分成2个小部分:
前4个字节代表页的校验和
这个部分是和File Header中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为File Header在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,那么在File Header中的校验和就代表着已经修改过的页,而在File Trailer中的校验和代表着原先的页,二者不同则意味着同步中间出了错。
后4个字节代表页面被最后修改时对应的日志序列位置(LSN),这个也和校验页的完整性有关。
这个File Trailer与File Header类似,都是所有类型的页通用的。