MySQL 8.0 數(shù)據(jù)字典表
統(tǒng)一保存到 mysql 庫的數(shù)據(jù)字典表中了。mysql 庫中,除了 general_log、slow_log 2 個日志表,其它所有表的存儲引擎都是 InnoDB,伴隨而來的是 DDL 終于能夠支持原子操作了。
以 DROP TABLE t1, t2 為例,不會出現(xiàn) t1 表刪除成功,t2 表刪除失敗的情況,而是要么都刪除成功,要么都刪除失敗。
本文我們就來聊聊 MySQL 8.0 中的數(shù)據(jù)字典表。
本文內(nèi)容基于 MySQL 8.0.29 源碼。
目錄
1. 概述
2. 數(shù)據(jù)字典表有哪些?
3. 數(shù)據(jù)字典表元數(shù)據(jù)在哪里?
4. 創(chuàng)建數(shù)據(jù)字典表
5. 打開數(shù)據(jù)字典表
6. 總結(jié)
正文
1. 概述
MySQL 8.0 重構(gòu)數(shù)據(jù)字典之后,廢除了 MySQL 5.7 中用于保存元數(shù)據(jù)的磁盤文件:.frm、.par、.TRN、.TRG、.isl、db.opt、ddl_log.log。
如果想要了解上面這些磁盤文件都保存了什么元數(shù)據(jù),可以參照 MySQL 官方文檔:
https://dev.mysql.com/doc/refman/8.0/en/data-dictionary-file-removal.html
這些文件被廢除之后,原本保存到這些文件中的元數(shù)據(jù),都保存到數(shù)據(jù)字典表中了。
數(shù)據(jù)字典表本身也大變樣了:
數(shù)據(jù)字典表不再位于 InnoDB 系統(tǒng)表空間,而是遷移到 mysql 庫中,mysql 庫位于 mysql 表空間,磁盤文件為 mysql.ibd。SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_FIELDS 這 4 個數(shù)據(jù)字典表也不再完全依賴硬編碼在源碼中的元數(shù)據(jù)了,而是和其它表一樣,使用保存在 mysql 庫的數(shù)據(jù)字典表中的元數(shù)據(jù)。
上面 4 個數(shù)據(jù)字典表的名字也發(fā)生了變化,后面會介紹。
2. 數(shù)據(jù)字典表有哪些?
按照官方文檔的定義,MySQL 8.0 一共有 31 張數(shù)據(jù)字典表:
dd_properties
innodb_ddl_log
catalogs
character_sets
check_constraints
collations
column_statistics
column_type_elements
columns
events
foreign_key_column_usage
foreign_keys
index_column_usage
index_partitions
index_stats
indexes
parameter_type_elements
parameters
resource_groups
routines
schemata
st_spatial_reference_systems
table_partition_values
table_partitions
table_stats
tables
tablespace_files
tablespaces
triggers
view_routine_usage
view_table_usage
上面只是簡單列出了數(shù)據(jù)字典表的表名,如果想了解每個表存放了什么內(nèi)容,可以參照官方文檔:https://dev.mysql.com/doc/refman/8.0/en/system-schema.html
默認情況下,我們是看不到數(shù)據(jù)字典表的,需要滿足以下條件才能看到:
源碼編譯 Debug 版本 MySQL,以使用 cmake 編譯為例,需要帶上 -DCMAKE_BUILD_TYPE=Debug編譯選項。連接 MySQL 之后,先執(zhí)行下面的 SQL 告訴 MySQL 跳過數(shù)據(jù)字典表的權(quán)限檢查:
SET SESSION debug = '+d,skip_dd_table_access_check'
滿足以上 2 個條件之后,執(zhí)行下面這條 SQL 就可以看到所有數(shù)據(jù)字典表了:
SELECT a.name AS db_name, b.*
FROM mysql.schemata AS a
INNER JOIN mysql.tables AS b ON a.id = b.schema_id
WHERE b.schema_id = 1 AND b.hidden = 'System'
ORDER BY b.id
執(zhí)行上面的 SQL 列出來的表有 32 個,其中
innodb_dynamic_metadata表不屬于數(shù)據(jù)字典表。
上面列出的數(shù)據(jù)字典表中,有 4 個需要重點介紹,因為不管是數(shù)據(jù)字典表本身,還是用戶表,都離不開這 4 個表:
tables:存儲表的元數(shù)據(jù),包括表空間 ID、數(shù)據(jù)庫 ID、表 ID、表名、表注釋、行格式等信息,對應 MySQL 5.7 中的數(shù)據(jù)字典表SYS_TABLES。columns:存儲表中字段的元數(shù)據(jù),包括表 ID、字段 ID、字段名、字段注釋、字段類型、是否自增等信息,對應 MySQL 5.7 中的數(shù)據(jù)字典表SYS_COLUMNS。indexes:存儲表的索引元數(shù)據(jù),包括表空間 ID、表 ID、索引 ID、索引名、索引注釋、是否是隱藏索引等信息,對應 MySQL 5.7 中的數(shù)據(jù)字典表SYS_INDEXES。index_column_usage:存儲索引中字段的元數(shù)據(jù),包括索引 ID、字段 ID、字段在索引中的編號(從 1 開始)、索引字段長度(如果是前綴索引字段,則是前綴的長度)、索引字段排序、是否隱藏,共6個字段,對應 MySQL 5.7 中的數(shù)據(jù)字典表SYS_FIELDS。
這個表中沒有包含更詳細的字段信息,如果需要,可以通過字段 ID 到columns表獲取。
index_column_usage 和 SYS_FIELDS 表不完全一樣,有 2 點需要說明:
index_column_usage 包含 6 個字段,比 SYS_FIELDS 多 3 個字段: order:表示索引字段的排序。length:hidden =0時,表示索引字段長度,或前綴索引字段的前綴長度;hidden =1時,字段值為NULL。hidden:0表示索引中該字段由用戶定義;1表示索引中該字段是 MySQL 給加上的。以下是一個測試表,圖中
name是從 columns 表中連表查詢得到的,其它都是 index_column_usage 表的字段。
CREATE TABLE `t5` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`str1` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8_general_ci NOT NULL DEFAULT '',
`i1` int NOT NULL DEFAULT '0',
`str2` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8_general_ci NOT NULL DEFAULT '',
`i2` int NOT NULL DEFAULT '0',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `idx_i1` (`i1`) USING BTREE,
KEY `idx_str1` (`str1`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

index_id = 310 是主鍵索引,hidden = 0 的記錄是主鍵字段;hidden = 1 的記錄是主鍵索引中的其它字段,也就是表中的字段。index_id = 312 是二級索引,其中 str1 是前綴索引字段,前綴長度為 255 * 3(utf8 一個字符最多占用的字節(jié)數(shù)) = 765,hidden = 0 表示 str1 是用戶定義的二級索引字段;hidden = 1 的記錄是 MySQL 自己增加到二級索引中的主鍵字段。
index_column_usage 表中的 ordinal_position表示編號,從 1 開始;SYS_FIELDS 中的POS表示序號,從 0 開始。
除了在 Debug 版本的 MySQL 中設置跳過數(shù)據(jù)字典表的權(quán)限檢查之外,還可以通過 information_schema 數(shù)據(jù)庫中的表或視圖查看其對應的數(shù)據(jù)字典表:
| 數(shù)據(jù)字典表 | information_schema 表或視圖 |
|---|---|
| tables | INNODB_TABLES |
| columns | INNODB_COLUMNS |
| indexes | INNODB_INDEXES |
| index_column_usage | INNODB_FIELDS |
| …… | …… |
3. 數(shù)據(jù)字典表元數(shù)據(jù)在哪里?
數(shù)據(jù)字典表用于存儲用戶表的元數(shù)據(jù),這個比較好理解,因為創(chuàng)建用戶表的時候,所有數(shù)據(jù)字典表都已經(jīng)存在了,把用戶表的各種元數(shù)據(jù)插入到相應的數(shù)據(jù)字典表就可以了。
數(shù)據(jù)字典表本身的元數(shù)據(jù)也會保存到數(shù)據(jù)字典表里,但是某個數(shù)據(jù)字典表創(chuàng)建的時候,有一些數(shù)據(jù)字典表還沒有創(chuàng)建,這就有問題了。
我們以 columns、indexes 這 2 個數(shù)據(jù)字典表為例來說明:columns 表先于 indexes 表創(chuàng)建,columns 表創(chuàng)建成功之后,需要把索引元數(shù)據(jù)保存到 indexes 表中,而此時 indexes 表還沒有創(chuàng)建,columns 表的索引元數(shù)據(jù)自然也就沒辦法保存到 indexes 表中了。
MySQL 解決這個問題的方案是引入一個中間層,用于臨時存放所有數(shù)據(jù)字典表的各種元數(shù)據(jù),等到所有數(shù)據(jù)字典表都創(chuàng)建完成之后,再把臨時存放在中間層的所有數(shù)據(jù)字典表的元數(shù)據(jù)保存到相應的數(shù)據(jù)字典表中。
這里所謂的中間層實際上是一個存儲適配器,源碼中對應的類名為 Storage_adapter,這是一個實現(xiàn)了單例模式的類。
MySQL 在初始化數(shù)據(jù)目錄的過程中,Storage_adapter 類的實例屬性 m_core_registry 就是所有數(shù)據(jù)字典表元數(shù)據(jù)的臨時存放場所。
4. 創(chuàng)建數(shù)據(jù)字典表
我們安裝 MySQL 完成之后,想讓 MySQL 運行起來,要做的第一件事就是初始化 MySQL,實際上就是初始化 MySQL 數(shù)據(jù)目錄。
初始化過程會創(chuàng)建 MySQL 運行時需要的各種表空間、數(shù)據(jù)庫、表,其中就包含數(shù)據(jù)字典表。
創(chuàng)建數(shù)據(jù)字典表的過程分為 3 個步驟進行:
第 1 步,把代表每個數(shù)據(jù)字典表的 Object_table 對象注冊到 System_tables 類的實例屬性 m_registry 中。
除了數(shù)據(jù)字典表,m_registry 中還包含了
mysql 庫中的其它 MySQL 系統(tǒng)表。
第 2 步,循環(huán) m_registry 中的所有表,通過 Object_table 得到數(shù)據(jù)字典表的 DDL,然后調(diào)用 dd::execute_query() 執(zhí)行 DDL 語句創(chuàng)建數(shù)據(jù)字典表。
dd::execute_query() 創(chuàng)建數(shù)據(jù)字典表的過程中,會把表的元數(shù)據(jù)臨時存放到 Storage_adapter 類的實例屬性 m_core_registry 中,而不會保存到各種元數(shù)據(jù)對應的數(shù)據(jù)字典表中,這么做的原因在上一小節(jié)中介紹數(shù)據(jù)字典表的元數(shù)據(jù)在哪里時,已經(jīng)介紹過了,這里不再贅述。
dd::execute_query() 執(zhí)行完一個數(shù)據(jù)字典表的 DDL 語句之后,這個數(shù)據(jù)字典表在表空間中就已經(jīng)存在了,m_registry 中的所有表都處理完成之后,所有數(shù)據(jù)字典表就都存在了。

第 3 步,循環(huán) m_registry 中的所有表,把每個表本身的元數(shù)據(jù)(數(shù)據(jù)庫 ID、表 ID、表名、注釋、字段數(shù)量等)保存到 mysql.tables 數(shù)據(jù)字典表中,然后把表的字段、索引等元數(shù)據(jù)保存到對應的數(shù)據(jù)字典表中。
所有數(shù)據(jù)字典表的元數(shù)據(jù)都從
Storage_adapter類的實例屬性m_core_registry中讀取。

經(jīng)過 3 個步驟的通力協(xié)作,所有數(shù)據(jù)字典表的元數(shù)據(jù)就都保存到數(shù)據(jù)字典表中了,這個雞生蛋、蛋生雞的問題,就這樣通過引入外力(m_core_registry)解決了。
5. 打開數(shù)據(jù)字典表
數(shù)據(jù)字典表保存著 MySQL 運行過程中需要的一系列關(guān)鍵數(shù)據(jù),使用頻次很高,MySQL 啟動過程中就會把數(shù)據(jù)字典表的元數(shù)據(jù)都加載到內(nèi)存中,這就是打開表的過程。
也就是說,打開數(shù)據(jù)字典表是在 MySQL 啟動過程中完成的。
前面我們介紹過,數(shù)據(jù)字典表的元數(shù)據(jù)也是保存在數(shù)據(jù)字典表中的。
MySQL 啟動過程中,要先打開數(shù)據(jù)字典表才能拿到數(shù)據(jù)字典表的元數(shù)據(jù),而要拿到數(shù)據(jù)字典表的元數(shù)據(jù),又必須先打開數(shù)據(jù)字典表。
這個過程很繞,不是很好理解,我們來打個比方:數(shù)據(jù)字典表是一個房間,數(shù)據(jù)字典表的元數(shù)據(jù)是打開房間門的鑰匙。
現(xiàn)在問題來了,因為 MySQL 把數(shù)據(jù)字典表的元數(shù)據(jù)保存在數(shù)據(jù)字典表中,這就相當于把打開房間門的鑰匙落在房間里了。
要想打開房間,必須先拿到鑰匙,而要想拿到鑰匙又必須先打開房間,這樣一轉(zhuǎn)換,問題是不是更好理解點了?
我們先來想想怎么解決房間和鑰匙問題,如果把打開房間的鑰匙落在房間里了,有哪些辦法可以解決?
我能想到的有以下 3 種解決方案:
暴力破解,把鎖撬開。 找專業(yè)的開鎖師傅把鎖打開。 用備用鑰匙開門。這個方法最好,但是有個 前提條件:已經(jīng)提前準備好了備用鑰匙。
MySQL 里沒有前 2 種方案,而是留了一把備用鑰匙,也就是第 3 種方案,接下來我們看看 MySQL 打開數(shù)據(jù)字典表的過程:
第 1 步,和創(chuàng)建數(shù)據(jù)字典表一樣,把代表每個數(shù)據(jù)字典表的 Object_table 對象注冊到 System_tables 類的實例屬性 m_registry 中。
每個數(shù)據(jù)字典表的 Object_table 對象中,都定義了這個表的表名、字段、索引、外鍵等信息。
Object_table 對象中保存的并不是 DDL 語句,卻類似于我們建表時的 DDL 語句。
下面這個例子是源碼中表空間數(shù)據(jù)字典表 mysql.tablespaces Object_table 對象中定義的該表的信息:
Tablespaces::Tablespaces() {
// 表名
m_target_def.set_table_name("tablespaces");
// 字段
m_target_def.add_field(FIELD_ID, "FIELD_ID", "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT");
m_target_def.add_field(FIELD_NAME, "FIELD_NAME", "name VARCHAR(268) NOT NULL COLLATE " + String_type(Object_table_definition_impl::name_collation()->m_coll_name));
m_target_def.add_field(FIELD_OPTIONS, "FIELD_OPTIONS", "options MEDIUMTEXT");
m_target_def.add_field(FIELD_SE_PRIVATE_DATA, "FIELD_SE_PRIVATE_DATA", "se_private_data MEDIUMTEXT");
m_target_def.add_field(FIELD_COMMENT, "FIELD_COMMENT", "comment VARCHAR(2048) NOT NULL");
m_target_def.add_field(FIELD_ENGINE, "FIELD_ENGINE", "engine VARCHAR(64) NOT NULL COLLATE utf8_general_ci");
m_target_def.add_field(FIELD_ENGINE_ATTRIBUTE, "FIELD_ENGINE_ATTRIBUTE", "engine_attribute JSON");
// 索引
m_target_def.add_index(INDEX_PK_ID, "INDEX_PK_ID", "PRIMARY KEY(id)");
m_target_def.add_index(INDEX_UK_NAME, "INDEX_UK_NAME", "UNIQUE KEY(name)");
// 如果有外鍵等其它信息,也會加在這里
}
第 2 步,循環(huán) m_registry 中的所有表,通過 Object_table 得到數(shù)據(jù)字典表的 DDL,然后調(diào)用 dd::execute_query() 執(zhí)行 DDL 語句創(chuàng)建數(shù)據(jù)字典表。
和創(chuàng)建數(shù)據(jù)字典表中的第 2 步不一樣,dd::execute_query() 執(zhí)行 DDL,并不會真正的創(chuàng)建表,只是為了生成數(shù)據(jù)字典表元數(shù)據(jù),并把元數(shù)據(jù)保存到 Storage_adapter 類的實例屬性 m_core_registry 中。
保存到 m_core_registry 中的數(shù)據(jù)字典表元數(shù)據(jù),就是我們前面說的備用鑰匙,有了這把備用鑰匙,就能打開數(shù)據(jù)字典表了。

第 3 步,循環(huán) m_registry 中的所有表,通過第 2 步生成的數(shù)據(jù)字典表元數(shù)據(jù),去 mysql 表空間中(表空間文件:mysql.ibd)讀取各個數(shù)據(jù)字典表的元數(shù)據(jù)。
這一步執(zhí)行完成之后,所有數(shù)據(jù)字典表的元數(shù)據(jù)都被加載到內(nèi)存中了,數(shù)據(jù)字典表都被打開了。
第 4 步,循環(huán) m_registry 中的所有表,把數(shù)據(jù)字典表的元數(shù)據(jù)從 m_core_registry 刪除。
第 5 步,循環(huán) m_registry 中所有的表,把從表空間中讀取出來的數(shù)據(jù)字典表的元數(shù)據(jù)存入 m_core_registry 中。
不過,這一步存入 m_core_registry 的并不是所有數(shù)據(jù)字典表的元數(shù)據(jù),而是 22 個核心(CORE)數(shù)據(jù)字典表的元數(shù)據(jù):
catalogs
character_sets
check_constraints
collations
column_statistics
column_type_elements
columns
foreign_key_column_usage
foreign_keys
index_column_usage
index_partitions
indexes
resource_groups
schemata
table_partition_values
table_partitions
tables
tablespace_files
tablespaces
triggers
view_routine_usage
view_table_usage
1 ~ 5 步執(zhí)行完成之后,m_core_registry 中就只包含上面 22 個核心數(shù)據(jù)字典表的元數(shù)據(jù)了,有了這些表的元數(shù)據(jù),就可以打開其它所有表了。
第 6 步,調(diào)用 dd::execute_query() 執(zhí)行 FLUSH TABLES 關(guān)閉已經(jīng)打開的所有數(shù)據(jù)字典表、非數(shù)據(jù)字典表,后續(xù)就可以用從數(shù)據(jù)字典表中讀取出來的元數(shù)據(jù)來打開數(shù)據(jù)字典表和其它所有需要的表了。
到這里,打開數(shù)據(jù)字典表的大體流程就已經(jīng)介紹完了,也許大家會有疑問:
第 2 步調(diào)用 dd::execute_query() 執(zhí)行 DDL,已經(jīng)拿到了數(shù)據(jù)字典表的元數(shù)據(jù)。
為了區(qū)分,把這里拿到的元數(shù)據(jù)叫作
備用元數(shù)據(jù)。
第 3 步根據(jù)備用元數(shù)據(jù)打開數(shù)據(jù)字典表,從表空間中讀取到數(shù)據(jù)字典表的元數(shù)據(jù)。
同樣為了區(qū)分,把這里拿到的元數(shù)據(jù)叫作
原配元數(shù)據(jù)。
第 4 步從 m_core_registry 中刪除備用元數(shù)據(jù)。第 5 步把原配元數(shù)據(jù)存入 m_core_registry。
數(shù)據(jù)字典表的備用元數(shù)據(jù)和原配元數(shù)據(jù)不是一樣的嗎?為什么還要用原配元數(shù)據(jù)替換備用元數(shù)據(jù),這是不是多此一舉?
我沒有逐個對比備用元數(shù)據(jù)和原配元數(shù)據(jù)是否完全一樣,這是個不小的工程。不過,既然源碼中這么實現(xiàn),那應該是有它的原因,只是我還沒有發(fā)現(xiàn)。如果后面發(fā)現(xiàn)其中的原因,我會再補充到我的博客中。
6. 總結(jié)
要理解 MySQL 8.0 中的數(shù)據(jù)字典表,核心是理解以下 2 點:
初始化數(shù)據(jù)目錄時,數(shù)據(jù)字典表的元數(shù)據(jù)是怎么存放到數(shù)據(jù)字典表中的?
這主要是借助了 Storage_adapter 類實例的 m_core_registry 屬性。
在創(chuàng)建數(shù)據(jù)字典表的過程中,先創(chuàng)建每個數(shù)據(jù)字典表,并把元數(shù)據(jù)臨時存放到m_core_registry中,所有數(shù)據(jù)字典表都創(chuàng)建成功之后,最后再一次性把所有數(shù)據(jù)字典表的元數(shù)據(jù)保存到對應的數(shù)據(jù)字典表中。MySQL 啟動時,怎么用數(shù)據(jù)字典表的元數(shù)據(jù)打開數(shù)據(jù)字典表?
這同時借助了硬編碼在源碼中的數(shù)據(jù)字典表定義,以及 Storage_adapter 類實例的m_core_registry屬性。
MySQL 啟動過程中,先通過 Object_table 得到創(chuàng)建數(shù)據(jù)字典表的 DDL,調(diào)用dd::execute_query()執(zhí)行 DDL,拿到元數(shù)據(jù)(備用原數(shù)據(jù)),把備用元數(shù)據(jù)臨時存放到 m_core_registry 屬性中,再通過備用元數(shù)據(jù)打開數(shù)據(jù)字典表。
以上就是本文的全部內(nèi)容了,如果本文對你有所幫助,還請幫忙 轉(zhuǎn)發(fā)朋友圈、點贊、在看,謝謝 ^_^
