MySQL8.0新特性: Instant Add Column
MySQL8.0開始對一些DDL操作做了大量的優化,例如原子DDL, 快速DDL(只修改元資料),前者解決了長期以來mysql的一大詬病,後者則提升了dba同學的生活品質
官方文件列出了一些可以快速ddl的操作,大體包括:
- 修改索引型別
-
Add column (limited)
- 當一條alter語句中同時存在不支援instant的ddl時,則無法使用
- 只能順序加列
- 不支援壓縮表
- 不支援包含全文索引的表
- 不支援臨時表,臨時表只能使用copy的方式執行DDL
- 不支援那些在資料詞典表空間中建立的表
- 修改/刪除列的預設值
- 修改索引型別
-
修改ENUM/SET型別的定義
- 儲存的大小不變時
- 向後追加成員
- 增加或刪除型別為virtual的generated column
- RENAME TABLE操
本文主要介紹下在MySQL8.0.12引入的快速加列特性。雖然這個特性不能覆蓋所有加列場景,但已經能解決很大部分加列帶來的問題:
- 對超級大表的加列操作通常可能耗時幾個小時甚至數天的時間
- 在ddl的過程中產生的臨時表會佔用磁碟空間
- ddl帶來的複製延遲問題
具體的worklog為: WL#11250 - Support Instant Add Column
使用
ALTER語句增加了新的語法INSTANT,你可以顯式地指定,但MySQL自身也會自動選擇合適的演算法。所以這個特性通常對使用者是透明的。
增加了一些新的information_schema表來展示相關資訊:
I_S.innodb_tables.instant_cols
I_S.innodb_columns.has_default/default_value
舉個簡單的例子:
[email protected] 03:54:47>show create table t1\G
*************************** 1. row ***************************
Table: t1
Create Table: CREATE TABLE `t1` (
`a` int(11) NOT NULL,
`b` int(11) DEFAULT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT '11',
PRIMARY KEY (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)
c,d 是instant added...
[email protected] 03:55:40>select table_id, name, pos, len, has_default,default_value from information_schema.innodb_columns where has_default = 1\G
*************************** 1. row ***************************
table_id: 1143
name: c
pos: 2
len: 4
has_default: 1
default_value: NULL
*************************** 2. row ***************************
table_id: 1143
name: d
pos: 3
len: 4
has_default: 1
default_value: 8000000b
2 rows in set (0.00 sec)
[email protected] 03:56:35>select instant_cols from information_schema.innodb_tables where name = 'test/t1';
+--------------+
| instant_cols |
+--------------+
| 2 |
+--------------+
1 row in set (0.00 sec)
實現
記錄格式修改
快速加列特性,在增加列時,實際上只是修改了元資料,原來儲存在檔案中的行記錄並沒有被修改。當行格式為redundent型別時,記錄解析是不依賴元資料的,可以自解析,
但如果行格式是dynamic或者compact型別,由於行內不儲存元資料,尤其是列的個數資訊,其記錄的解析需要依賴元資料的輔助。因此為了支援動態加列功能,需要對行格式做一定的修改
其大體思路為:
- 如果表上從未發生過instant add column, 則行格式維持不變。
- 如果發生過instant ddl, 那麼所有新的記錄上都被特殊標記了一個flag, 同時在行記憶體儲了列的個數
- 由於只支援往後順序加列,通過列的個數就可以知道這個行記錄中包含了哪些列的資訊
我們先來看看典型compact行型別的記錄組織結構:
+--------------------------------+---------------------+---------------+
| Non-null variable-length array | SQL-null flags/bitmap | Extra 5 bytes |
+--------------------------------+----------------------+---------------+
其中extra 5 bytes包含如下資訊:
+-----------+---------------+----------+-------------+-----------------+
| Info bits | Records owned | Heap No. | Record type | Next record ptr |
+-----------+---------------+----------+-------------+-----------------+
extra info中包含的資訊如下:
a) Info bits: 4 bits
0x10: REC_INFO_MIN_REC_FLAG
0x20: REC_INFO_DELETED_FLAG
其中還有兩個bit是未使用的
b) Record owned: 4 bits
REC_NEW_N_OWNED
c) Heap No. : 13 bits
d) Record type: 3 bits
REC_STATUS_ORDINARY:葉子節點記錄
REC_STATUS_NODE_PTR:非葉子節點記錄
REC_STATUS_INFIMUM/REC_STATUS_SUPREMUM 系統記錄
e) Next record ptr: 2 bytes
為了支援instant add column, 使用了info bits中的一個bit位,如果被設定,表示這條記錄是第一次instant add column後插入的, flag為:
Ox80: REC_INFO_INSTANT_FLAG
當flag被設定時,在記錄中就會使用1或2個位元組來儲存列的個數
+--------------------------+----------------+---------------+---------------+
| Non-null variable-length | | | |
| array | SQL-null flags | fields number | Extra 5 bytes |
+--------------------------+----------------+---------------+---------------+
對於redundent型別,由於已經有了列個數資訊,無需進行修改
資料詞典資訊
對資料詞典進行了擴充套件並記錄:
- 在第一次instant add column之前的列個數
- 每次加的列的預設值
通過這些資訊加上記錄上的額外資訊,可以正確解析出記錄上的資料
資料詞典:
a) dd::Table::se_private_data::instant_col:在第一次instant ADD COLUMN之前表上面的列的個數
b) dd::Partition::se_private_data::instant_col, 和a類似,儲存分割槽表上instant col的個數,但有所不同的是,分割槽表上的分割槽之間
可能存在不同列的個數。因為我們單獨truncate一個分割槽,而truncate操作會清空instant標記,因此b)中儲存的instant_col不應該比a)中每個分割槽上的instant_col要小
c) dd::Column::se_private_data::default_null, 表示預設值為NULL
d) dd::Column::se_private_data::default, 當預設值不為null時,這裡儲存預設值
DD_instant_col_val_coder
--- column default value需要從innodb型別byte轉換成se_private_data中的text型別(char), 使用一個型別DD_instant_col_val_coder
來輔助轉換
example: 0XFF => 0x0F, 0x0F
在將表load到記憶體建立表物件dict_table_t和索引物件dict_index_t時,有幾個關鍵成員要載入進來,因為會用於輔助解析記錄
dict_table_t::n_instant_cols 第一次instant add column之前的非虛擬列個數,(包含系統列
dict_index_t::instant_cols flag用於標示是否存在Instant column
dict_index_t::n_instant_nullable: 第一次instant add column之前的可為null的列個數
dict_col_t::instant_default: 儲存預設值及其長度, 當解析資料時看到Instant column, 會直接引用到這裡的資料指標
載入邏輯:
ha_innobase::open
|-->dd_open_table
|--> dd_open_table_one
上述提到的幾個變數會被設定.
DDL
檢查表是否支援instant ddl
ha_innodb::check_if_supported_inplace_alter()
innobase_support_instant
innopart_support_instant
dict_table_t::support_instant_add()
condition:
- 不是壓縮表
- 不是data dictionary tablespace
- 不是全文索引表
- 不是臨時表
除此之外, 新增列還要確保不改變列的順序
當判定可以立刻加列時,僅僅需要修改資料詞典資訊即可
ha_innobase::commit_inplace_alter_table
|--> dd_commit_inplace_instant
|--> dd_commit_instant_part
|--> dd_commit_instant_table
1. dd::TABLE中記錄instant column的個數
2. 儲存新的列的預設值
Note:
- truncate操作會重置instant標記
ha_innobase::truncate_impl
dd_clear_instant_table
2.重建表的話,新不的表將不包含instant列
select
查詢的關鍵在於如何正確的解析出記錄中的每一行(對於不在其中的instant column,填預設值即可), 關鍵的函式是:
rec_init_offsets
|-->rec_init_offsets_comp_ordinary
|-->rec_init_null_and_len_comp
何時填寫instant add的default值:
default值儲存在dict_col_t::dict_col_default_t
將預設值填到返回的記錄中:
row_sel_store_mysql_field_func
rec_get_nth_field_instant
rec_get_nth_field_instant: 封裝了列值: 如果是記錄中的,則從記錄中讀取,否則返回其預設值
insert
記錄在插入之前從tuple轉換成physical record:
rec_convert_dtuple_to_rec
rec_convert_dtuple_to_rec_new
rec_convert_dtuple_to_rec_comp
當表上有instant column時
1. 會佔用1(如果列個數小於REC_N_FIELDS_ONE_BYTE_MAX)或者2個位元組來儲存列個數
2. 在記錄的Info bits欄位設定REC_INFO_INSTANT_FLAG,表示這個記錄是instant add column之後建立的
update
對於update,不會把default的值轉換成inline的,除非去更新包含default值的列(row_upd_changes_field_size_or_external
)
對於update的回滾做了特殊處理:
- 如果回滾的值從non-default到default值,那麼這個是不會儲存到列裡面去的。(dtuple_t::ignore_trailing_default())