为什么不要用uuid做主键

之前文章中介绍Snowflake的时候其实已经说过了,自增id做主键对于InnoDB更友好,如果用uuid做主键数据插入时会伴随大量的数据迁移,导致插入性能降低,今天从InnoDB数据结构聊聊数据是如何插入的。

表空间

数据库中所有数据都需要记录在磁盘、磁带、光盘等存储介质上实现长期保存。这些介质被划分成文件,他们是存储数据的物理空间。

当将数据写到文件上时,首先需要指定文件路径。而我们主要操作的是表,很难确定表对应的文件路径是哪个,为解决文件与表之间的耦合,增加一层表空间,研发人员只需要操作表空间中的表,而具体的存储交由InnoDB存储引擎自动维护。

表空间包括以下几种类型:

  • 系统表空间:存储change buffer,double write buffer及和InnoDB相关的所有对象的元数据。如:表空间和数据库信息,表结构与字段信息等;

  • 独立表空间:每张表都对应一个独立的表空间。当表被创建后,会自动为表创建一个对应表名的空间,并在数据库目录下生成一个“表名.ibd”的表空间文件;

  • 普通表空间:是主动通过“create tablespace 表空间名”手动创建的表空间;

  • 临时表空间:存储临时表以及临时表编号对应的回滚段;

总结:表空间是数据库中的逻辑结构,解耦了表、索引与文件的关联。

Page页

一个磁盘的文件容量是很大的,为便于管理,InnoDB会将文件划分成一个个大小相等的存储块,也就是我们说的“页”。

类比于我们读书,如果页是连续装订在一起的,那么阅读起来就比较方便,如果是零散的放在各处,读起来就非常不方便。

根据CPU局部性原理,下次读取大概率会使用逻辑上相邻的数据。因此为提高数据读操作的性能,InnoDB会把逻辑上相邻的数据尽可能在物理上存储在相邻的页中,为实现这个目标InnoDB引入了“簇”的概念。

一个“簇”是物理上连续分配的一段空间,一个“页”默认大小是16k,一个“簇”默认由64个连续的“页”组成。也可以通过innodb_page_size参数制定页大小,对应的簇大小也会随着变化。

实际上,InnoDB是先把文件划分成连续的“簇”,在“簇”内划分出连续的“页”,我们知道了一个页的大小和页的编号(也就是页码),就可以计算出一个页在磁盘上的具体位置,同理根据页码可以计算出一个簇的大小及页所在的簇码。

在InnoDB中还有一个概念是“段”,InnoDB将逻辑上有关联的“簇”归属为一个段(segment)。段的目的是为了记录与管理簇使用情况及为数据分配空间时,提供空间存储状态。

总结:页是IO操作的最小单元,页是有编号的,通过编号可以与物理空间建立联系。

B+树

InnoDB引擎中数据是以B+树方式组织的,叶子节点存储索引数据和行数据,非叶子节点存储索引数据和页码。由于索引数据和行数据有不同的数据结构,因此他们会分开存储。非叶子节点的索引数据存储在一个段中,叶子节点的行数据存储在一个段中,他们分别存储在不同结构的簇和页中。

数据存储逻辑如下:(非叶子节点的索引段和叶子节点的数据段)

物理存储逻辑如下:

“段”主要用于存储具有相同意义的数据,常见的段有数据段、索引段、回滚段。每创建一个索引会创建两个段:一个是数据段(B+树对应的叶子节点),一个是是索引段(B+树非叶子节点)

  • 对于聚集索引(一般是主键索引),数据段存储的是索引关键字和所有业务字段。

  • 对于非聚集索引,数据段存储的是索引关键字和主键字段。

如果通过非聚集索引查询,需要先通过B+树找到主键,再通过主键从聚集索引中查询具体行数据,这个过程叫做回表。

上图中,左边是非聚集索引,右边是聚集索引。会有一次回表查询。

非聚集索引????:(索引段)

聚集索引????:(数据段)

表数据是通过聚集索引组织存储的,也就是表按照主键索引创建的B+树进行数据存储,所以创建表的时候需要制定一个主键,如果没有制定主键,表会默认创建一个隐藏的自增字段row_id作为聚集索引

由于一张表对应一个聚集索引,同时聚集索引元数据中指定了页码,因此InnoDB引擎可以根据页码和页大小计算出索引B+树页码的准确位置。

InnoDB是基于行存储的,一行存储结构如下:

  • 聚集索引叶子节点:记录存储的是表中的业务行数据,除了行数据还包括事务id,回滚段指针,以及没有指定主键和唯一索引时的隐藏字段row_id;

  • 非叶子节点主要是用于在B+树搜索的,主要记录的是子节点的最小值记录和页码;

Page是物理上的存储空间,是数据存储的载体;B+树节点是数据的逻辑结构。在Page中可以存储一颗完整的B+树,也可以多个page一起存储完整的B+树。InnoDB为了实现简单,B+树节点和Page页是一一对应的。

比如最开始一个聚集索引的B+树是这个样子:(假设每个页只能存储三行数据)

当向B+树插入数据(绿色部分)后如下:

插入16、17、18三行数据,创建新的page8并插入三行数据,之后向父节点插入索引关键字行,也就是子节点的最小值16。

继续向B+树插入一行数据19(蓝色部分)后如下:

由于没有可以直接使用的page,所以新分配出page9插入数据19,再向父节点插入索引关键字行,由于父级索引page已满,所以分配出新page10,并插入索引19,最后向根节点插入索引行,由于根节点页没满,则不需要分配新页。

如果聚集索引中主键是自增主键,那么数据是以追加的方式存储在页中的,如果页满,则重新分配一个页空间继续追加数据即可。

但如果聚集索引主键使用的是无序的uuid则复杂很多。B+树是一个逻辑上有序的集合,向B+树中插入数据可能插入到原来已经满了的page上,这样会导致原来的页分裂,为新的数据腾出空间,同时伴随着数据的移动,所以我们建议使用有序的列作为聚集索引。

数据是如何插入的呢?

说了这么多,那一条数据究竟是怎么插入的呢?

我们创建一张表,如果指定了主键或唯一索引,则使用指定的列创建聚集索引。否则采用隐藏的row_id创建聚集索引。

接下来我们向表中插入一条数据,逻辑如下:

  1. 在表空间中根据库名/表名,查到表id(table_id);

  2. 再根据表id查询到其主键索引对应的页码(page_no);

  3. 根据page_no计算出page的物理地址,插入索引数据,再插入行数据,并关联两个两种类型的页;

  4. 如果一页空间不足,会计算出当前页所在的簇申请空间;

  5. 当B+树对应的物理页不断变化时,为保证树的平衡,会产生新的root节点,为保持root页不变,InnoDB通过交换的方式,把新的root节点数据复制交换到原来的root page页,保证root page永远不变,保证表和物理空间关联不断开;

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值