文章目录
- 页的概念
- 页分裂与页合并
页的概念
在计算机里,无论是内存还是磁盘,操作系统都是按页的大小进行读取的(页大小通常为 4 kb),磁盘每次读取都会预读,会提前将连续的数据读入内存中,这样就避免了多次 IO,这就是计算机中有名的局部性原理,即我用到一块数据,很大可能这块数据附近的数据也会被用到,干脆一起加载,省得多次 IO 拖慢速度, 这个连续数据有多大呢,必须是操作系统页大小的整数倍。
所以MySQL 的页,默认值为 16 KB,也就是说对于 B+ 树的节点,最好设置成页的大小(16 KB),这样一个 B+ 树上的节点就只会有一次 IO 读。
那有人就会问了,这个页大小是不是越大越好呢,设置大一点,节点可容纳的数据就越多,树高越小,IO 不就越小了吗,这里要注意,页大小并不是越大越好,InnoDB 是通过内存中的缓存池(pool buffer)来管理从磁盘中读取的页数据的。页太大的话,很快就把这个缓存池撑满了,可能会造成页在内存与磁盘间频繁换入换出,影响性能。
通过以上分析,相信我们不难猜测出 N 叉树中的 N 该怎么设置了,只要选的时候尽量保证每个节点的大小等于一个页(16kb)的大小即可。
现在我们来看看开头的问题, 为啥推荐自增 id 作为主键,第一个原因就是自增主键一般都会设置为int,占用4个字节,所有可以这样说,InnoDB的B+树最多为4叉。
那第二原因就是下面举例说到的。
有人可能会说用户的身份证是唯一的,可以用它来做主键,假设以身份证作主键,会有什么问题呢。
页分裂与页合并
B+ 树为了维护索引的有序性,每插入或更新一条记录的时候,会对索引进行更新。假设原来基于身份证作索引的 B+ 树如下(假设为二叉树 ,图中只列出了身份证的前四位)
现在有一个开头是 3604 的身份证对应的记录插入 db ,此时要更新索引,按排序来更新的话,显然这个 3604 的身份证号应该插到左边节点 3504 后面(如下图示,假设为二叉树)
如果把 3604 这个身份证号插入到 3504 后面的话,这个节点的元素个数就有 3 个了,显然不符合二叉树的条件,此时就会造成页分裂,就需要调整这个节点以让它符合二叉树的条件
这种由于页分裂造成的调整必然导致性能的下降,尤其是以身份证作为主键的话,由于身份证的随机性,必然造成大量的随机结点中的插入,进而造成大量的页分裂,进而造成性能的急剧下降,那如果是以自增 id 作为主键呢,由于新插入的表中生成的 id 比索引中所有的值都大,所以它要么合到已存在的节点(元素个数未满)中,要么放入新建的节点中(如下图示)所以如果是以自增 id 作为主键,就不存在页分裂的问题了,推荐!
有页分裂就必然有页合并,什么时候会发生页合并呢,当删除表记录的时候,索引也要删除,此时就有可能发生页合并,如图示
当我们删除 id 为 7,9 对应行的时候,上图中的索引就要更新,把 7,9 删掉,此时 8,10 就应该合到一个节点,不然 8,10 分散在两个节点上,可能造成两次 IO 读,势必会影响查找效率!
那什么时候会发生页合并呢,我们可以定个阈值,比如对于 N 叉树来说,当节点的个数小于 N/2 的时候就应该和附近的节点合并,不过需要注意的是合并后节点里的元素大小可能会超过 N,造成页分裂,需要再对父节点等进行调整以让它满足 N 叉树的条件。
实际中Innodb的主键索引,非叶子节点只存了索引值,只在最后一行才存放了行记录,这样极大地减小了索引了大小,而且只要找到索引值就找到了行记录,也提升了效率,
这种在叶节点存放一整行记录的索引被称为聚簇索引,其他的就称为非聚簇索引。