数据存放在页中,页中的数据通过单向链表连接,每页的数据会进行分组,每个分组都对应一个槽,每页的page Directory存放了该页所有的槽,每个槽都存放了分组内值最大的数据在页面中的地址偏移量。
页与页之间通过双向链表连接起来。每个页都记录了自己的key(该页最小的值)和page_no(页号)。
B+树有三级,叶子节点的数据页存储的是普通的用户记录。非叶子节点的数据页存放的是普通目录项记录。
例如查找ID=100的数据,是先从根节点找ID=100的数据所在的页,在根据槽找到在页中的分组,再遍历分组中的所有元素,最终找到ID=100的记录。
InnoDB页
Inner DB将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位。页的大小一般是16 KB。当我们想要从表中获取某些记录时,Inner DB存储引擎一次最少从磁盘中读取16 KB的内容到内存中。刷盘的时候,一次至少把内存中的16 KB内容刷新到磁盘。
InnoDB行格式

记录真实的数据,除了表的字段信息之外,还有三个隐藏列:row_id,trx_id,roll_pointer。
溢出列
在COMPACT和REDUNDANT行格式中,对于占用存储空间非常多的列,在记录的真实数据处只会存储该列的一部分数据,而把剩余的数据分散存储在几个其他的页中,然后在记录的真实数据处用20字节存储指向这些页的地址。

存放正常记录的页面和溢出液是两种不同类型的页面。
不同类型的页
InnoDB为了不同的目的而设计了多种不同类型的页。存放表中记录的页叫索引页。


我们的存储记录存放在User Records
我们自己存储的记录会按照指定的行格式存储到user records部分。但是在一开始生成页的时候,其实并没有user records部分。每当插入一条记录时,都会从free space部分申请一个记录大小的空间,并将这个空间划分到user records部分。当free space部分的空间全部被user record部分替代之后,那就意味着这个页使用完了。此时如果还有新的记录插入,就需要去申请新的页了。
User Records存储记录的管理


Infimum、Supremum。数据与数据之间单向链表。delete_flag(值为1,说明该条数据被删除)。next_record(值为0表示没有下条数据)。

PageDirectory

每页的数据会进行分组。分组的规则:Infimum记录所在的分组只能有1条数据;Supremum记录所在的分组记录数在1到8之间;其余分组的记录数在4到8之间。
几个分组就对应有几个槽。槽中存放每个分组中最大的那条记录在页面中的地址偏移量。
之后每插入一条记录,都会从页目录中找到对应记录的主键值比待插入记录的主键值大,并且差值最小的槽,然后把该槽对应的记录的n_owned值加1。表示本组内又添加了一条记录,直到该分组中的记录数等于8个。
当一个组中的记录数等于8后,再插入一条记录,会将组中的记录拆分成两个组,其中一个组中4条记录,另一个5条记录。这个拆分过程中会在页记录中新增一个槽,记录这个新增分组中最大的那条记录的偏移量。
综上所述,在一个数据页中查找指定主键值的记录时,过程分为两步:
1、通过2分法确定该记录所在分组对应的槽,然后找到该槽所在分组中主键值最小的那条记录。
2、通过记录的next_record属性遍历该槽所在的组中的各个记录。
InnoDB是以页为单位存放数据的,有时在存放某种类型的数据时,占用的空间非常大,InnoDB可能无法一次性为这么多数据分配一个非常大的存储空间,而如果分散到多个不连续的页中进行存储,则需要把这些页关联起来。FIL_PAGE_PREV和FIL_PAGE_NEXT就分别代表本数据页的上一个页和下一个页的页号。这样通过建立一个双向链表,就把许许多多的页串联起来了,而无需这些液在物理上真正连着。
总结:
各个数据页可以组成一个双向链表,而每个数据页中的记录会按照主键值从小到大的顺序,组成一个单向链表。每个数据页都会为存储在它里面的记录生成一个页目录,再通过主键查找某条记录的时候,可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录,即可快速找到指定的记录。
B+树索引
当前一个数据页存满之后,再插入数据,将不得不再分配一个新页。新分配的数据页编号可能并不是连续的。也就是说,这些页在磁盘上可能并不挨着。
假如一个数据页最多能存放三条记录。假如这个数据页已经存满了三条记录,ID分别是1、2、5,此时如果插入一条ID=4的。将分配一个新的数据页,ID=5的会被移到新分配的数据页。ID=4的这条数据会插入到已存在的数据页。(双向链表的数据页中的数据根据id列排序)
页分裂:
在对页中记录进行增删改操作的过程中。我们必须通过一些诸如记录移动的操作来始终保持这个状态一直成立:下一数据页中用户记录的主键值,必须大于上一个页中用户记录的主键值。
页目录:

++页目录就是索引。++
key:页的用户记录中最小的主键值。
page_no:页号。
record_type的四种类型
0:普通的用户记录
1:目录项记录
2:Infimum记录
3:Supremum记录

查询id=20的数据怎么查找?

存放目录项的页也会有多个,目录项页之间通过双向链表连接。
// 省略了很多,看书。 todo
索引不是越多越好
1、空间代价。
一个索引就要建立一个B+树,一个数据页默认就会占用16kb的空间,如果是一颗很大的B+树,就是很大的一片存储空间。
2、时间代价。
每次对表进行增删改操作时,都需要维护B+树。还有就是,如果建立了太多的索引,在执行查询语句前,会导致成本分析的时间太长,从而影响查询性能。
如何更好地创建和使用索引
1、只为用于搜索、排序或分组的列创建索引。
2、为某个列创建索引时,需要考虑该列的值不重复的比例,如果重复的值比例较高,那么通过二级索引+回表的方式执行查询,回表次数会过多。
3、索引列的的类型尽量小。例如,能用int就不要用bigint。
4、为列前缀建立索引。例如一个字段的类型是字符串,可以为前10个字符建立索引。
5、覆盖索引。只查询需要的字段,不要使用select *。
6、让索引列以列名的形式在搜索条件中单独出现。
7、新插入记录时主键最好递增。如果主键忽大忽小,就需要频繁地维护B+树。
8、不要有不必要的冗余索引。例如已经有联合索引(a,b,c)了,就无需再新建索引index(a)了。
访问方法
1、const(意思是常数级别的,代价是可以忽略不计的)
通过主键或唯一二级索引列,与常数的等值比较来定位一条记录。
例如:select * from t_user where id = 2;
2、ref
通过普通的二级索引列与常数进行等值比较。
例如:select * from t_user where key1 = 'abc'; // 对key1字段创建了普通二级索引
由于不是唯一二级索引,因此符合条件的数据可能是多条。该查询的代价取决于该扫描区间中的记录条数。
利用二级索引来执行查询时,其实每获取到一条二级索引记录,就会立刻对其执行回表操作,而不是将所有二级索引记录的主键值都收集起来后再进行统一的回表操作。
3、ref_or_null
例如:select * from single_table where key1 = 'abc' or key1 is null;
和ref相比较多扫描了一些值为null的二级索引记录。
4、range
使用索引执行查询时,对应的扫描区间为若干个单点扫描区间或者范围扫描区间的访问方法称为range。
例如:select * from single_table where key2 in (1438, 6328) or (key2 >= 38 and key2 <= 79);
5、index
扫描全部二级索引记录的访问方法称为index访问方法。
例如:联合索引包含三列(key_part1, key_part2, key_part3)。
select key_part1, key_part2, key_part3 from single_table where key_part2 = 'abc';
该查询:直接遍历联合索引的所有二级索引记录,针对获取到的每一条二级索引记录,都判断key_part2='abc'条件是否成立。如果成立,就读取key_part1, key_part2, key_part3三个列的值,并发送给客户端。
6、all
全表扫描。
注意事项:如果有多个索引,优化器会通过一定算法来计算分别使用这两种索引的成本,进而选择成本最小的索引执行查询。
索引合并
1、InterSection索引合并
2、Union索引合并
3、Sort-Union索引合并