InnoDB存储结构
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了InnoDB存储结构相关的知识,希望对你有一定的参考价值。
参考技术A InnoDB是将表中的数据存储到磁盘中,所以关闭服务器数据不会丢失。而真正发生数据处理是在内存中完成的,这样内存与磁盘的数据交互,实现了对数据的读写,而读写磁盘过程相比内存读写很慢,InnoDB采取的方式为:将数据划分为页,以页为作为交互的数据单元,基本大小为16KB。
InnoDB为了不同的⽬的⽽设计了 许多种不同类型的⻚,⽐如存放表空间头部信息的⻚,存放Insert Buffer信息的⻚,存放INODE信息的⻚,存放undo⽇志信息的⻚等等等等。
SELECT * FROM record_compact WHERE C1=3;
最笨的办法:从Infimum记录(最⼩记录)开始,沿着链表⼀直往 后找,这种暴力查找当然是不可取的,为此InnoDB设计一种目录索引。制作过程如下:
步骤一:将所有的记录(最大最小记录, 不包括已标记删除的记录)分组
步骤二:将每组中的最后一条记录(也就是组内最大的记录那条)的头信息的n_owned属性表示为改组拥有的记录条数
步骤三:将每组的最后一条记录的地址偏移量(槽)单独拿出来放在靠近页尾部的地方即page_directory页目录。所以这个⻚⾯⽬录就是由槽组成的。
我们平时以记录为单元向表中插入数据,这些记录在磁盘上的存储方式称为行格式或者记录格式。目前设计了4中行格式:Compact、Redundant、 dynamic、Compressed
行格式的语法:
CREATE TABLE 表名 (列信息) ROW_FOMAT=行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称
------------------------------------------------------------------------------------
例如:->USE dabao;
->CREATE TABLE record_format(c1 VARCHAR(10),c2 VARCHAR(10),c3 CHAR(10),c4 VARCHAR(10) NOT NULL) CHARSET=ascii ROW_FORMAT=COMPACT;
INSERT INTO record_format(c1,c2,c3,c4)VALUES('AAA','BBBB','CCCC','DDDD'),('EEE','DDDD','DADA'NULL);
然后我们就开始解开每个行格式下的存储方式的神秘面纱。
compact分为记录的额外信息和记录的真实数据。
(1)变长字段长度列表
mysql支持一些变长的数据类型,变长字段占用的存储空间分为两部分:真正的数据内容和占用的字节数。
把真正的数据内容占据的字节长度放在记录的开头,从而形成一个变字长的字段长度列表,各字段占用的字节数按照列的顺序 逆序 存放。根据每条记录中的列数据中的字符串大小,来判断具体使用1字节还是2个字节存储真实数据,InnoDB有自己的一套规则。M:选用的字符集类型中一个字符所占用的大小;W:变长类型的最大存储字符数;L:实际存储的字节大小。
if W*M<255 则采用1个字符存储真正字符串占用的字节数
else if W*M>255 && L<127 则采用1个字符存储真正字符串占用的字节数 else 则采用2个字符存储真正字符串占用的字节数
变长字段长度列表只存储值为非NULL,
(2)NULL值 列表
不让把所有的null值都存储到真实数据中,所以compact列格式把null的列集中管理,存储到null值列表中,处理过程:
1.统计表中允许存储null的列表,如果表中没有可以null的列,则null值列表也不存在。否则每个允许null值的列占用一个位,并且逆序排列,二进制值为1时,该列的值为null。否则不为空。
2.MySQL中规定所有的null列必须存储在整个字节的位中,位数不足则最高位补零。
(3)记录头信息
5个固定字节数。记录当前记录条数,当前堆位置,下一条记录位置等信息。
(1) User Record
(2)记录头信息的秘密
1.delete_mask 这个属性表示当前记录是否被删除 占用一个位 值为1则已经删除。需要注意的是,这里的删除记录,并不是立即从磁盘中清除,是因为如果每次记录删除立即磁盘清除的话,就需要将其他记录在磁盘上重新排序需要消耗性能,所以只是打个标记,将所有的删除记录组成一个垃圾链表,标记的垃圾链表的空间则变为可重用空间,新来的记录就会覆盖标记删除的记录。
!!!删除记录位为1时与该记录假如垃圾链表其实是两个阶段。跟事务的删除操作有关。
2.min_rec_mask B+树的每层非叶子节点的最小记录都会添加该标记。索引的时候会用到。
3.n_owned
4.heap_no 表示当前记录在页中的位置,有趣的是InnoDB会在每页上默认添加最大最小伪纪录作为补充称一条完整记录。
图中可以看出 最⼩记录和最⼤记录的heap_no值分 别是0和1,也就是说它们的位置最靠前。
5.record_type 表示当前记录类型,0:普通记录;1:非叶子节点记录,索引 2:最小记录;3 最大记录
6.next_record 表示从当前记录的真实数据到下⼀条记录的真实数据的 地址偏移量.下⼀条记录指得并不是按照我们插⼊顺序的下⼀条记录, ⽽是按照 主键值 由⼩到⼤的顺序的下⼀条记录.
删除一个记录时
MySQL中除了存储c1,c2,c3,c4用户自定义列数据外,MySQL会自动为每条记录添加三个隐藏列:ROW_id(主键,唯一标识一条记录,6个字节)、 transaction_id( 事务ID 6个字节 )ROLL_id(回滚指针,7个字节)
对于第2条记录中c3和c4列的值都为NULL,它们被存储在了前边的NULL值列表处,在记录的真实数据处就不再冗余存储,从⽽节省存储空间。
最终的compact列格式:
如果想要c3列也变为可变字段长度,则将定长字符集改为可变字符集。
ALTER TABLE record_format MODIFY COLUMN c3 CHAR(10) CHARACTER SET = utf8 ;
对于Compact和Reduntant⾏格式来说,如果 某⼀列中的数据⾮常多的话,在本记录的真实数据处只会存储该列的前768个字节的数据和20个字节存储指向这些⻚的地址,然后把剩下的数据存放到其他⻚中,这个过程也叫做⾏溢出,存储超出768字节的那些⻚⾯也被称为溢出⻚。
MySQL中规定⼀个⻚中⾄少存放两⾏记录,溢出条件:
(1)每个⻚除了存放我们的记录以外,也需要存储⼀些额外的信 息,乱七⼋糟的额外信息加起来需要136个字节的空间。
(2)每个记录需要的额外信息是27字节。
假设⼀个列中存储的数据字节数为n,那么发⽣⾏溢出现象时需要满
⾜这个式⼦:136 + 2×(27 + n) > 16384(页大小16K) 求解这个式⼦得出的解是:n > 8098。
MySQL版本是5.7,它的默认⾏格式就是Dynamic,这俩⾏格式和Compact⾏格式挺像,只不过在处理⾏溢出数据时有点⼉分歧,它们不会在记录的真实数据处存储字段真实数据的前768个字节,⽽是把所有的字节都存储到其他⻚⾯中,只在记录的真实数据处存储其他⻚⾯的地址。
1. ⻚是MySQL中磁盘和内存交互的基本单位,也是MySQL是管理存储空间的基本单位。
2. 指定和修改⾏格式的语法如下:
CREATE TABLE 表名 (列的信息) ROW_FORMAT=⾏格式名称
ALTER TABLE 表名 ROW_FORMAT=⾏格式名称
3. InnoDB⽬前定义了4种⾏格式
* COMPACT⾏格式
*REDUNDANT
*DYNAMIC
* COMPRESSED
1. InnoDB为了不同的⽬的⽽设计了不同类型的⻚,我们把⽤于存放记录的⻚叫做数据⻚。
2. ⼀个数据⻚可以被⼤致划分为7个部分,分别是
File Header,表示⻚的⼀些通⽤信息,占固定的38字节。
Page Header,表示数据⻚专有的⼀些信息,占固定的56个字节。
Infimum + Supremum,两个虚拟的伪记录,分别表示⻚中的最⼩和最⼤记录,占固定的26个字节。
User Records:真实存储我们插⼊的记录的部分,⼤⼩不固定。
Free Space:⻚中尚未使⽤的部分,⼤⼩不确定。
Page Directory:⻚中的某些记录相对位置,也就是各个槽在⻚⾯中的地址偏移量,⼤⼩不固定,插⼊的记录越多,这个部分占⽤的空间越多。
File Trailer:⽤于检验⻚是否完整的部分,占⽤固定的8个字节。
3. 每个记录的头信息中都有⼀个next_record属性,从⽽使⻚中的所有记录串联成⼀个单链表
4. InnoDB会为把⻚中的记录划分为若⼲个组,每个组的最后⼀个记录的地址偏移量作为⼀个槽,存放在Page Directory中,所以在⼀个⻚中根据主键查找记录是⾮常快的,分为两步:
通过⼆分法确定该记录所在的槽。
通过记录的next_record属性遍历该槽所在的组中的各个记录。
5. 每个数据⻚的File Header部分都有上⼀个和下⼀个⻚的编号,所以所有的数据⻚会组成⼀个双链表。
6. 为保证从内存中同步到磁盘的⻚的完整性,在⻚的⾸部和尾部都会存储⻚中数据的校验和和⻚⾯最后修改时对应的LSN值,如果⾸部和尾部的校验和和LSN值校验不成功的话,就说明同步过程出现了问题。
InnoDB存储引擎介绍- Innodb逻辑存储结构
如果创建表时没有显示的定义主键,mysql会按如下方式创建主键:
- 首先判断表中是否有非空的唯一索引,如果有,则该列为主键。
- 如果不符合上述条件,存储引擎会自动创建一个6字节大小的指针。
当表中有多个非空的唯一索引,会选择建表时第一个定义的非空唯一索引。注意根据的是定义索引的顺序,不是创建列的顺序。
- 表空间 tablespace(ibd文件)
- 段 segment(一个索引2个段)
- Extent(1MB)
- Page(16KB)
- Row
- Field
表空间
表空间可以看做是InnoDB存储引擎逻辑结构的最高层,所有的数据都存放在表空间中。在默认情况下InnoDB存储引擎有一个共享表空间ibdata1,即所有数据都存放在这个表空间内。如果用户启用了参数innodb_file_per_table,则每个表内的数据可以单独放到一个表空间内。
如果启动了innodb_file_per_table参数,需要注意的是每张表的表空间内存放的只是数据、索引和插入缓冲Bitmap页,其他类的数据,如回滚(undo)信息,插入缓冲索引页、系统事务信息,二次写缓冲(Double write buffer)等还是存放在原来的共享表空间内。这同时也说明了另一个问题:即使在启用了参数innodb_file_per_table之后,共享表空间还是会不断地增加其大小。可以来做一个实验,在实验之前已经将innodb_file_per_table设为ON了。现在看看初始共享表空间文件的大小:
mysql> show variables like \'innodb_file_per_table\'\\G *************************** 1. row *************************** Variable_name: innodb_file_per_table Value: ON 1 row in set (0.00 sec) mysql> system ls -tlhr /home/mysql/mysql/data/ibdata* -rw-rw----. 1 mysql mysql 204M Mar 21 05:54 /home/mysql/mysql/data/ibdata1
可以看到,共享表空间ibdata1的大小为204MB,接着模拟产生undo的操作,使用表orders,并把其存储引擎更改为InnoDB,执行如下操作:
mysql> set autocommit=0; mysql> update orders set device_number=0; Query OK, 3278492 rows affected (0.03 sec) Rows matched: 3278492 Changed: 3278492 Warnings: 0 mysql> system ls -tlhr /home/mysql/mysql/data/ibdata* -rw-rw----. 1 mysql mysql 652M Mar 21 07:38 /home/mysql/mysql/data/ibdata1
这里首先将自动提交设为0,即用户需要显式提交事务(注意,在上面操作结束时,并没有对该事务执行commit或rollback)。接着执行会产生大量的undo操作的语句update orders set device_number=0,完成后再观察共享表空间,会发现ibdata1已经增长到了652MB。这个例子虽然简单,但是足以说明共享表空间中还包含有undo信息。
有用户会问,如果对k这个事务执行rollback,ibdata1这个表空间会不会缩减至原来的大小(204MB)?这可以通过继续运行下面的语句得到验证:
mysql> rollback; Query OK, 0 rows affected (0.01 sec) mysql> system ls -tlhr /home/mysql/mysql/data/ibdata* -rw-rw----. 1 mysql mysql 652M Mar 21 07:49 /home/mysql/mysql/data/ibdata1
很“可惜”,共享表空间的大小还是204MB,即InnoDB存储引擎不会在执行rollback时去收缩这个表空间。虽然InnoDB不会回收这些空间,但是会自动判断这些undo信息是否还需要,如果不需要,则会将这些空间标记为可用空间,供下次undo使用。
master thread每10秒会执行一次的full purge操作,很有可能的一种情况是:用户再次执行上述的update语句后,会发现ibdata1不会再变大了,那就是这个原因了。
使用py_innodb_page_info小工具查看表空间中各页的类型和信息,用户可以在github.com进行查找。https://github.com/qingdengyue/david-mysql-tools/tree/master/py_innodb_page_type.
使用方法如下:
$ python ~/py_innodb_page_info.py ibdata1 Total number of page: 41728: Insert Buffer Free List: 1035 Insert Buffer Bitmap: 3 System Page: 131 Transaction system Page: 2 Freshly Allocated Page: 5074 Undo Log Page: 33238 File Segment inode: 5 B-tree Node: 2235 File Space Header: 2
可以看到共有41728个页,其中插入缓冲的空间有1035个页、5074个可用页、33238个undo页、2235个数据页等。用户可以通过添加-v参数来查看更详细的内容。
段
上图中显示了表空间是由各个段组成的,常见的段有数据段、索引段、回滚段等。InnoDB存储引擎表是索引组织的(index organized),因此数据即索引,索引即数据。那么数据段即为B+树的页节点(上图的leaf node segment),索引段即为B+树的非索引节点(上图的non-leaf node segment)。
与Oracle不同的是,InnoDB存储引擎对于段的管理是由引擎本身完成,这和Oracle的自动段空间管理(ASSM)类似,没有手动段空间管理(MSSM)的方式,这从一定程度上简化了DBA的管理。
需要注意的是,并不是每个对象都有段。因此更准确地说,表空间是由分散的页和段组成。
区
区是由64个连续的页组成的,每个页大小为16KB,即每个区的大小为1MB。对于大的数据段,InnoDB存储引擎最多每次可以申请4个区,以此来保证数据的顺序性能。
在我们启用了参数innodb_file_per_talbe后,创建的表默认大小是96KB。区是64个连续的页,那创建的表的大小至少是1MB才对啊?其实这是因为在每个段开始时,先有32个页大小的碎片页(fragment page)来存放数据,当这些页使用完之后才是64个连续页的申请。
通过一个实验来显示InnoDB存储引擎对于区的申请:
create table t1 (
col1 int not null auto_increment,
col2 varchar (7000),
primary key(col1)
)engine=InnoDB;
system ls -lh /usr/local/var/mysql/test/t1.ibd
创建了t1表,col2字段设为varchar(7000),这样能保证一个页中可以存放2条记录。可以看到,初始创建完t1后表空间默认大小为96KB.
页
同大多数数据库一样,InnoDB有页(page)的概念(也可以称为块),页是InnoDB磁盘管理的最小单位。与Oracle类似的是,Microsoft SQL Server数据库默认每页大小为8KB,不同于InnoDB页的大小(16KB),且不可以更改(也许通过更改源码可以)。
常见的页类型有:
- 数据页(B-tree Node)。
- Undo页(Undo Log Page)。
- 系统页(System Page)。
- 事务数据页(Transaction system Page)。
- 插入缓冲位图页(Insert Buffer Bitmap)。
- 插入缓冲空闲列表页(Insert Buffer Free List)。
- 未压缩的二进制大对象页(Uncompressed BLOB Page)。
- 压缩的二进制大对象页(Compressed BLOB Page)。
行
InnoDB存储引擎是面向行的(row-oriented),也就是说数据的存放按行进行存放。每个页存放的行记录也是有硬性定义的,最多允许存放16KB/2~200行的记录,即7992行记录。这里提到面向行(row-oriented)的数据库,那么也就是说,还存在有面向列(column-orientied)的数据库。MySQL infobright储存引擎就是按列来存放数据的,这对于数据仓库下的分析类SQL语句的执行以及数据压缩很有好处。类似的数据库还有Sybase IQ、Google Big Table。面向列的数据库是当前数据库发展的一个方向。
以上是关于InnoDB存储结构的主要内容,如果未能解决你的问题,请参考以下文章