InnoDB逻辑存储结构
段—区—页—行
表空间:
默认情况下InnoDB有一个共享表空间ibdata1,所有数据放入这个表空间,如果开启了innodb_file_per_table(默认ON),每张表都可以放到一个单独的表空间,但只把数据、索引和Insert Buffer Bitmap放入单独表空间,其它数据,如undo信息、插入缓冲索引页、事务信息,二次写缓冲等还是放共享表空间
段:
表空间由各个段组成,数据段、索引段、回滚段等,数据段即B+树叶子节点,索引段即B+树非叶子节点。每个段开始时,先用32个页大小的碎片页存放数据,使用完这些碎片页,再去一个区一个区地申请内存,这保证了小段如undo这样的段可以省空间
区:
区由连续的页组成,一个区固定是1MB,页默认16KB,即一个区有64个连续的页,InnoDB1.0.x引入压缩页,页的大小可以是2K、4K、8K,但不管怎么变,区都是1MB
页:
InnoDB磁盘最小的管理单位,默认16KB,InnoDB1.2.x开始可以更改默认大小innodb_page_size为4K、8K、16K,更改后,不得再次修改,除非mysqldump导入导出产生新的库
常见的页类型:
- 数据页(B-tree Node)
- undo页(undo Log Page)
- 系统页(System Page)
- 索引页(Index Page)
- 事务数据页(Transaction system Page)
- 插入缓冲位图页(Insert Buffer Bitmap)
- 插入缓冲空闲列表页(Insert Buffer Free List)
- 未压缩的二进制大对象页(Uncompresses BLOB Page)
- 压缩的二进制大对象页(compressed BLOB Page)
行:
行存储有四种格式,Redundant、Compact、Dynamic和Compressed。MySQL5.1开始默认使用Compact,MySQL5.7开始默认使用Dynamic
Compact:
变长字段长度列表:当数据表有变长字段时才出现,记录本行中各变长字段实际长度,当长度小于255时用1个字节表 示,大于255时用2个字节表示,不会用3个字节,因为变长字段有长度限制,最多65535字节
NULL标志位:当数据表存在允许NULL的字段时才出现,本行每个字段是否为null用0或1表示,同时必须是整数个字 节大小,即不足8个bit位的高位补0
记录头信息:存储一些信息,固定5个字节,如delete_mask,标识删除位;next_record下条记录的地址; record_type,记录类型,0为普通,1为B+树非叶子节点,2为最小记录,3为最大记录
变长字段长度列表和NULL是逆序存储(方便寻址)
row_id:当建表时没指定主键时,选择第一个非空唯一索引当主键,如果没有,添加该列作为主键,6字节大小
trx_id:事务id,这条数据是哪个事务生成的,6字节大小
roll_ptr:上个版本的指针,7字节大小
一个页最多有16KB,16384字节,而varchar(n)最多可以存储65533字节,那么一个页可能都放不了一条记录,这时就会行溢出,溢出的数据会放到“溢出页”中,原页会保留20个字节指向该溢出页的地址。
Compressed和Dynamic行格式和Compact非常相似,主要是行溢出时的处理,这两个不会在原页保存数据,只用20字节指针指向溢出页,数据全在溢出页,而Compressed还会对BLOB、TEXT、VARCHAR这些大长度类型的数据进行zlib算法压缩
char和varchar?
char存储固定长度字符串,最大长度255字节,当存储长度小于定义的长度时,MySQL在后面补空格(如果本身存储的字符串尾部就有空格,就会丢失空格信息!)
varchar存储可变字符串,读取速度相对更慢一点,因为需要先读长度,再读数据
一般使用varchar存储较好,但考虑到极端情况,varchar因为长度可变,可能出现页分裂的情况
如果是身份证号、订单号、国家编码等这些固定长度的,可以用char
如果是产品描述、用户地址、用户名称这种,可以用varchar
数据页结构:
采用链表的结构是让数据页之间不需要是物理上的连续的,而是逻辑上的连续。
数据页中的记录按照「主键」顺序组成单向链表,单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。
因此,数据页中有一个页目录,起到记录的索引作用,就像我们书那样,针对书中内容的每个章节设立了一个目录,想看某个章节的时候,可以查看目录,快速找到对应的章节的页数,而数据页中的页目录就是为了能快速找到记录。
- 将所有的记录划分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录;
- 每个记录组的最后一条记录就是组内最大的那条记录,并且最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段(上图中粉红色字段)
- 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录。
从图可以看到,页目录就是由多个槽组成的,槽相当于分组记录的索引。然后,因为记录是按照「主键值」从小到大排序的,所以我们通过槽查找记录时,可以使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到对应的记录,无需从最小记录开始遍历整个页中的记录链表。
- 第一个分组中的记录只能有 1 条记录;
- 最后一个分组中的记录条数范围只能在 1-8 条之间;
- 剩下的分组中记录条数范围只能在 4-8 条之间。
索引:
索引页的行记录是指针,指向一个页,索引页的索引键值就是指向的页的最小索引键值
数据页的行记录就是数据(聚簇索引,如果是二级索引,存放的是主键值)
页合并和页分裂:
页合并:
当一个数据页的使用率低于一定阈值(50%)时,MySQL 就会将该页与相邻的空闲页合并成一个页面。
例如,数据页能存放7条数据,有两个相邻的数据页,一个存储 [1, 2, 3, 4],另一个是[5, 6]。当删除4时,会检查 本页,本页数据只有三条,小于阈值,MySQL 就会将两个页面合并成一个页面,存储的数据变成 [1, 2, 3, 5, 6]
页分裂:
当一个数据页已经满了,而有新的数据要插入到该页时,MySQL 就会进行页分裂操作。
例如,数据页能存放5条数据,假设一个数据页已经存储了 [1, 2, 3, 4, 5],而有新的数据6要插入到该页中, MySQL 就会将该页拆分为两个页面,一个页面存储 [1, 2, 3],另一个页面存储 [4, 5, 6]。
页分裂是为了保证插入顺序的同时不大量挪动数据
采用逻辑删除可以减少页合并
采用批量顺序插入可以减少页分裂