MySQL源碼分析:Innobase字典管理及索引

        最近在看innobase加載B樹索引的時候發現了innodb的字典管理和索引加載機制。

  在innobase中,最基本的有四個系統表,用來存儲用戶定義的表、列、索引及索引列等信息,這些表分別爲SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_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_ID、POS)。

  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_ID、ID)。

  4)、SYS_FIELDS,用來存儲所有索引中定義的索引列,每一條記錄對應一個索引列。

  INDEX_ID:這個列所在的索引(8字節)。

  POS:這個列在某個索引中是第幾個索引列(4字節)。

  COL_NAME:這個索引列的列名。

  這個表的主鍵列爲(INDEX_ID、POS)。

  從上面的字典表就可以瞭解到,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中,用戶表中的記錄不一定都會有一個ROWID,ROWID只有在一個表沒有定義主鍵的時,也就是ROWID自己作爲索引列的時候這個表纔會被分配ROWID,而這個ROWID也不是一個表獨享一個ID空間,而是全局的,所有表都共享這個ID號,一般想來,或是像上面的TABLEID、INDEXID一樣,每次分配一個就更新一次字典頁面的值,但對於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樹中查詢對應記錄,如果找不到則報錯,找到之後,根據這個表的記錄格式解析這個記錄,取出ID、N_COLS、TYPE、SPACE這些基本的信息,然後根據這些信息創建一個表的內存對象,到這裏這個表的自身對象加載完成,接着要加載其所有列信息。

  加載列操作與上面所述原理基本一致,找到系統表SYS_COLUMNS,因爲這個表的聚簇索引是表ID及POS,所以在B樹內,如果多個記錄的表ID是相同的,則所以POS是從小到大排序的,所以構造查詢鍵值的時候只需要構造表ID即可,將找到的所有列信息按照取出順序依次加載每條記錄的對應信息即可,將所有的具體相同表ID的記錄加載完成後,這個表的所有列也相應加載完成。這裏還有一點需要注意,在innobase中,一個表的列包括兩部分,一部分是用戶創建表時指定的列,另一部分則是系統列,包括ROWID、TRXID及ROLLPTR三個列,ROWID表示記錄的行號,上面已經提到過,TRXID表示這條記錄最後一次被修改的事務號,主要用於事務的多版本(MVCC)管理,ROLLPTR也是用來實現多版本的,如果一個記錄被某一個用戶修改之後,另一個用戶在這條記錄不可見時,查詢這條記錄時要找到其原來的值,那麼ROLLPTR指定的就是其原來值的位置,這個位置其實就是在修改時寫下的回滾記錄的位置。所以對於任何一個表,其中所包括的列除了用戶定義的列之外,還包括這三個列。

  接下來就加載這個表的索引信息,加載索引是從SYS_INDEXES中查詢的,原理與上面的SYS_COLUMNS是一樣的,這個表的關鍵字是表ID及索引ID,所以具有相同表ID的所有索引記錄都是按照索引ID排序的,那麼對於每一條記錄都對應一個索引,需要加載ID、名字、N_FIELDS、TYPE、PAGENO、SPACE等基本信息,對於索引的加載,還需要加載它對應的所有關鍵字信息,這些信息存儲在SYS_FIELDS系統表中,這個表的關鍵字是INDEXID、POS,所以一個索引的所有關鍵字列都是按照POS排序的,這點很重要,因爲如果有多個排序列的話,順序不同排序結果是不同的,所以一定要按照POS的值從小到大加載(B樹存儲順序),索引的關鍵字信息包括索引ID號、POS、列名,一個索引加載所有具有指定索引ID號的關鍵字列後一個索引的加載即完成,但是加載關鍵字還有一點需要注意,如果一個索引不是唯一索引,則需要將表中已經加載的ROWID列以這個索引的第一個關鍵字列的身份加載到這個索引中,如果是唯一索引,則不需要加載ROWID列,而是直接加載自身定義的列。在加載完所有的關鍵字列後(要麼是ROWID列,要麼是自字義列),還需要加載另外兩個系統列,包括TRXID及ROLLPTR兩個列。而對於聚簇索引,因爲這個索引存儲了表中所有的列,所以後面還需要加載除關鍵字之外的所有列,這些列是按照建表時的順序加載的,而對於二級索引,則這些列是不需要加載的。按照相同道理將一個表中所有的索引加載完成。

  到此一個表的加載就完成了,那麼從上面就可以看出一個索引中加載的列的信息及順序關係。總結如下:

  聚簇唯一索引:

  [有序的關鍵字列][TRXID][ROLLPTR][其它建表創建的非關鍵字列]

  比較列個數:有序的關鍵字列個數

  聚簇非唯一索引:

  [ROWID][TRXID][ROLLPTR][其它建表創建的非關鍵字列]

  比較列個數:只ROWID一列而已

  二級唯一索引或二級非唯一索引(聚簇索引爲唯一索引):

  [有序的索引列][剩餘聚簇索引順序列][TRXID][ROLLPTR]

  比較列個數:有序的關鍵字列個數加剩餘聚簇索引順序列個數

  二級唯一索引或二級非唯一索引(聚簇索引爲非唯一索引):

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

  比較列個數:有序的關鍵字列個數加ROWID

  可以看出,對於innobase這樣的記錄格式定義,在比較兩個記錄時,只需要從第0個列開始,對比比較列個數的列信息即可,使用記錄的比較非常容易。上面對於聚簇索引爲唯一索引的二級索引,其有可能是在本身聚簇索引的某個或某些列上建立的,也有可能是在其它列上建立的,那麼其索引列的比較列就是將建立索引時指定的索引列按照順序放在最前面,根着就是聚簇索引中的剩餘的其它所有列,順序還是原來的順序。因爲二級索引的作用就是快速的在二級索引中找到相應的記錄之後還可以快速的去聚簇索引中快速的找到原記錄,那麼二級索引中定義的比較列就是爲了快速的找到聚簇索引的,因爲這些列中包括了所有的聚簇索引的索引列,並且順序與聚簇索引一致。那麼如果二級索引的聚簇索引是非唯一索引,則二級索引中用於快速索引聚簇索引記錄的索引鍵就是ROWID本身。

  上面已經敘述了所有innobase中的系統表的管理及索引管理的內容,但對於系統表,不能像我們一般想象的那樣,用戶是不能查詢系統表的內容的,它只能是系統內部自己管理及維護,比如SYS_TABLES等這些表MYSQL是不承認的,這個問題也與MYSQL的特性有關,因爲它是插件式的,它必須兼容所有的存儲引擎,不是每個存儲引擎都有這些系統表的,所以用戶是不能通過MYSQL來直接查詢或者訪問innobase的系統表的。

  總結:innobase的字典及索引記錄等的設計還是很方便、很先進的。值得學習深思借鑑。

 

        參考:http://tech.ddvip.com/2012-06/1338865679176562.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章