MySQL源码:Innobase字典管理及索引

前言:最近在重温innobase的B树及记录模块,发现对之前已经看过的字典管理及索引内容竟然忘却了,所以重新看了一遍并将它记录下来,防止哪天又给忘了。不过还是那句话,如果哪些有问题还请指正。谢谢。

 

    在innobase中,最基本的有四个系统表,用来存储用户定义的表、列、索引及索引列等信息,这些表分别为SYS_TABLESSYS_COLUMNSSYS_INDEXESSYS_FIELDS。每一个表的列分别如下:

1)、SYS_TABLES,用来存储所有的以innobase为存储引擎的表,每条记录对应已经定义的一个表。

NAME:表示一个表的表名。

ID:表示这个表的ID号(8个字节)。

N_COLS:表示这个表的列的个数,建表指定的列数(4个字节)。

TYPE:表示这个表的存储类型,包括记录的格式、压缩等信息(4个字节)。

MIX_ID:不清楚有什么用,好像没用。

MIX_LEN:不清楚有什么用,好像没用。

CLUSTER_NAME:不清楚有什么用,好像没用。

SPACE:表示这个表所在的表空间ID号(4字节)。

这个表对应的主键为(NAME),同时还有一个在ID号上的唯一索引。

2)、SYS_COLUMNS,用来存储innobase中定义的所有表的所有列的信息,每一个列对应这个表中的一条记录。

TABLE_ID:表示这个列所属的表的ID号(8字节)。

POS:表示这个列在表中是第几个列(4字节)。

NAME:表示这个列的列名。

MTYPE:表示这个列的主数据类型(4字节)。

PRTYPE:表示这个列的一些精确数据类型,它是一个组合值,包括NULL标志、是否是有符号数的标志、是否是二进制字符串的标志及表示这个列是真的VARCHAR(数据存储用两个字节)(4字节)。

LEN:表示这个列的数据长度,但不包括VARCHAR类型,因为这个类型是在记录里面存储了数据长度(4字节)。

PREC:表示这个列数据的精度,但目前好像没有使用(4字节)。

这个表的主键列为(TABLE_IDPOS)。

3)、SYS_INDEXES,用来存储innobase中所有表的索引信息,每条记录对应一个索引。

TABLE_ID:表示这个索引所属的表的ID号(8字节)。

ID:表示这个索引的索引ID号(8字节)。

NAME:表示这个索引的索引名。

N_FIELDS:表示这个索引的索引的个数(4字节)。

TYPE:表示这个索引的类型,包括聚簇索引、唯一索引、DICT_UNIVERSAL、DICT_IBUF(插入缓冲区B树)4字节)

SPACE:表示这个索引数据所在的表空间ID号(4字节)。

PAGE_NO:表示这个索引对应的B树的根页面(4字节)。

这个表的主键列为(TABLE_IDID)。

4)、SYS_FIELDS,用来存储所有索引中定义的索引列,每一条记录对应一个索引列。

INDEX_ID:这个列所在的索引(8字节)。

POS:这个列在某个索引中是第几个索引列(4字节)。

COL_NAME:这个索引列的列名。

这个表的主键列为(INDEX_IDPOS)。

       从上面的字典表就可以了解到,innobase是如何管理表、索引、列及关键字的,那么这几个表是如何加载的呢?首先从建库说起。

       innobase启动的时候,如果是要初始化库,则需要创建字典管理的B树等信息,则在innobase_start_or_create_for_mysql中调用dict_create函数来创建,因为innobase中的系统表的结构、个数等都是固定的,不会有人修改,所以在初始化库的时候只需要创建这几个表的存储B树(与上面所说的索引对应,一个索引一个B树)即可,同时将这几个B树的根页号存储在一个固定的位置,而不需要将这几个表的自身信息存储在系统表中了。在innobase中,专门有一个页面(0号表空间0号文件的7号页面)是用来管理字典信息的,这个页面用来存储上面提到的这四个表的五根页面号,以及下一个表ID值(全局变量,创建新表时的ID号从这里分配,每分配一个,这个ID号要加1)、下一个索引ID值(创建索引时的ID号从这里分配,每分配则加1)、下一个表空间ID值(与上同理)、ROWID(这个是表中的ROWID号,这个在下面另做介绍)这四个值。上面这些操作通过函数dict_hdr_create实现,这里面通过btr_create创建SYS_TABLES的两个索引;SYS_COLUMNS的一个索引;SYS_INDEXES的一个索引;SYS_FIELDS的一个索引。

       那么在innobase创建完B树及一些其它初始化操作之后,就通过函数dict_boot加载常驻内存的四个系统表及读取一些其它信息,上面已经提过,系统表的个数及结构都是不会被修改的,所以直接通过固定的硬编码构建这几个表即可,比如:

创建SYS_TABLES

    table = dict_mem_table_create("SYS_TABLES", DICT_HDR_SPACE, 8, 0);

    dict_mem_table_add_col(table, heap, "NAME", DATA_BINARY, 0, 0);

    dict_mem_table_add_col(table, heap, "ID", DATA_BINARY, 0, 0);

    dict_mem_table_add_col(table, heap, "N_COLS", DATA_INT, 0, 4);

    ……

创建SYS_TABLES表的索引:

    index = dict_mem_index_create("SYS_TABLES", "CLUST_IND",

                      DICT_HDR_SPACE,

                      DICT_UNIQUE | DICT_CLUSTERED, 1);

    dict_mem_index_add_field(index, "NAME", 0);

过程大致如上面所述。

初始化之后,因为它是常驻内存的,这四个表是挂在一个全局的字典结构中的:

struct dict_sys_struct{

    mutex_t         mutex;

    row_id_t            row_id;

    hash_table_t*   table_hash;

    hash_table_t*   table_id_hash;

    UT_LIST_BASE_NODE_T(dict_table_t)       table_LRU;  /*!< LRU list of tables */

    ulint           size;

    dict_table_t*   sys_tables; /*!< SYS_TABLES table */

    dict_table_t*   sys_columns;    /*!< SYS_COLUMNS table */

    dict_table_t*   sys_indexes;    /*!< SYS_INDEXES table */

    dict_table_t*   sys_fields; /*!< SYS_FIELDS table */

};

       很明显可以看到,结构体中最下面的四个就是用来存储对应的四个字典表的,其中第一个mutex自不用说,先说一下这个ROWID的管理,在innobase中,用户表中的记录不一定都会有一个ROWIDROWID只有在一个表没有定义主键的时,也就是ROWID自己作为索引列的时候这个表才会被分配ROWID,而这个ROWID也不是一个表独享一个ID空间,而是全局的,所有表都共享这个ID号,一般想来,或是像上面的TABLEIDINDEXID一样,每次分配一个就更新一次字典页面的值,但对于ROWID并不是这样,因为插入操作可比创建一个表或者索引频繁多了,每次都去修改未免对效率影响太大,所以innobase也做了相应的优化,就是每当分配一个ROWID,系统只是在内存中加1,不会修改页面,而只有当这个值为256的倍数时才会写入一次,那么自然会想到,如果插入200次,这个值还没有被写入,如果系统重启了,岂不是ID号会重复使用,这个当然不是问题,也就是在上面dict_boot中还做一个工作,就是将上次写入的ROWID值向上对齐256后再加上256,这样就不会有问题了,大不了可能会跳过很多ID号导致这个值增长太快而已。

       上面结构体中的下面三个链表及HASH表是用来存储innobase中所有表的缓存的,包括系统表、用户创建的表,这里有两个HASH,一个是按照名字缓存的,一个是按照ID来缓存的,各为其用罢。LRU链表用来管理表对象的缓存的,涉及到淘汰操作。

       上面介绍了字典对象存储及管理之后,下面介绍一下普通用户表的一个加载过程,当用户访问一个表时,如前面写的一篇《表对象的字典缓存》中介绍,系统首先会从表对象缓存池中找这个表的SHARE对象,如果找到则直接在实例化的并且空间链表中拿一个实例化的表对象出来使用即可,如果没有一个可用的实例化对象,则需要重新打开(实例化这个表),在实例化这个表的时候就需要找到这个表的字典信息,包括这个表本身、列信息及索引信息等,这个操作就通过我们下面介绍的主体dict_table_get来实现。

       这个函数里面的实现就是找到指定表的上面所述信息,找得过程是:首先从字典缓存中寻找,看这个表有没有被缓存在上面提到的HASH表中,如果找到则直接拿去使用即可,如果没有找到则通过dict_load_table从上面提到的四个系统表中加载,首先找到SYS_TABLES,过程与找普通表是一样的,也是首先找缓存,找不到则再从系统表中加载,但这似乎是废话,因为系统表在系统启动时就加载了,不可能找不到的,找到之后构造一个查询键值,因为是通过名字查询的,同时SYS_TABLES有一个按照名字排序的搜引,所以直接按照名字构造查询键值即可,然后从B树中查询对应记录,如果找不到则报错,找到之后,根据这个表的记录格式解析这个记录,取出IDN_COLSTYPESPACE这些基本的信息,然后根据这些信息创建一个表的内存对象,到这里这个表的自身对象加载完成,接着要加载其所有列信息。

       加载列操作与上面所述原理基本一致,找到系统表SYS_COLUMNS,因为这个表的聚簇索引是表IDPOS,所以在B树内,如果多个记录的表ID是相同的,则所以POS是从小到大排序的,所以构造查询键值的时候只需要构造表ID即可,将找到的所有列信息按照取出顺序依次加载每条记录的对应信息即可,将所有的具体相同表ID的记录加载完成后,这个表的所有列也相应加载完成。这里还有一点需要注意,在innobase中,一个表的列包括两部分,一部分是用户创建表时指定的列,另一部分则是系统列,包括ROWIDTRXIDROLLPTR三个列,ROWID表示记录的行号,上面已经提到过,TRXID表示这条记录最后一次被修改的事务号,主要用于事务的多版本(MVCC)管理,ROLLPTR也是用来实现多版本的,如果一个记录被某一个用户修改之后,另一个用户在这条记录不可见时,查询这条记录时要找到其原来的值,那么ROLLPTR指定的就是其原来值的位置,这个位置其实就是在修改时写下的回滚记录的位置。所以对于任何一个表,其中所包括的列除了用户定义的列之外,还包括这三个列。

       接下来就加载这个表的索引信息,加载索引是从SYS_INDEXES中查询的,原理与上面的SYS_COLUMNS是一样的,这个表的关键字是表ID及索引ID,所以具有相同表ID的所有索引记录都是按照索引ID排序的,那么对于每一条记录都对应一个索引,需要加载ID、名字、N_FIELDSTYPEPAGENOSPACE等基本信息,对于索引的加载,还需要加载它对应的所有关键字信息,这些信息存储在SYS_FIELDS系统表中,这个表的关键字是INDEXIDPOS,所以一个索引的所有关键字列都是按照POS排序的,这点很重要,因为如果有多个排序列的话,顺序不同排序结果是不同的,所以一定要按照POS的值从小到大加载(B树存储顺序),索引的关键字信息包括索引ID号、POS、列名,一个索引加载所有具有指定索引ID号的关键字列后一个索引的加载即完成,但是加载关键字还有一点需要注意,如果一个索引不是唯一索引,则需要将表中已经加载的ROWID列以这个索引的第一个关键字列的身份加载到这个索引中,如果是唯一索引,则不需要加载ROWID列,而是直接加载自身定义的列。在加载完所有的关键字列后(要么是ROWID列,要么是自字义列),还需要加载另外两个系统列,包括TRXIDROLLPTR两个列。而对于聚簇索引,因为这个索引存储了表中所有的列,所以后面还需要加载除关键字之外的所有列,这些列是按照建表时的顺序加载的,而对于二级索引,则这些列是不需要加载的。按照相同道理将一个表中所有的索引加载完成。

       到此一个表的加载就完成了,那么从上面就可以看出一个索引中加载的列的信息及顺序关系。总结如下:

       聚簇唯一索引:

       [有序的关键字列][TRXID][ROLLPTR][其它建表创建的非关键字列]

       比较列个数:有序的关键字列个数

       聚簇非唯一索引:

       [ROWID][TRXID][ROLLPTR][其它建表创建的非关键字列]

       比较列个数:只ROWID一列而已

       二级唯一索引或二级非唯一索引(聚簇索引为唯一索引):

       [有序的索引列][剩余聚簇索引顺序列][TRXID][ROLLPTR]

       比较列个数:有序的关键字列个数加剩余聚簇索引顺序列个数

       二级唯一索引或二级非唯一索引(聚簇索引为非唯一索引):

       [有序的索引列] [ROWID][TRXID][ROLLPTR]

       比较列个数:有序的关键字列个数加ROWID

       可以看出,对于innobase这样的记录格式定义,在比较两个记录时,只需要从第0个列开始,对比比较列个数的列信息即可,使用记录的比较非常容易。上面对于聚簇索引为唯一索引的二级索引,其有可能是在本身聚簇索引的某个或某些列上建立的,也有可能是在其它列上建立的,那么其索引列的比较列就是将建立索引时指定的索引列按照顺序放在最前面,根着就是聚簇索引中的剩余的其它所有列,顺序还是原来的顺序。因为二级索引的作用就是快速的在二级索引中找到相应的记录之后还可以快速的去聚簇索引中快速的找到原记录,那么二级索引中定义的比较列就是为了快速的找到聚簇索引的,因为这些列中包括了所有的聚簇索引的索引列,并且顺序与聚簇索引一致。那么如果二级索引的聚簇索引是非唯一索引,则二级索引中用于快速索引聚簇索引记录的索引键就是ROWID本身。

       上面已经叙述了所有innobase中的系统表的管理及索引管理的内容,但对于系统表,不能像我们一般想象的那样,用户是不能查询系统表的内容的,它只能是系统内部自己管理及维护,比如SYS_TABLES等这些表MYSQL是不承认的,这个问题也与MYSQL的特性有关,因为它是插件式的,它必须兼容所有的存储引擎,不是每个存储引擎都有这些系统表的,所以用户是不能通过MYSQL来直接查询或者访问innobase的系统表的。 

       总结:innobase的字典及索引记录等的设计还是很方便、很先进的。值得学习深思借鉴。

你可能感兴趣的:(mysql)