1. 程式人生 > >MySQL8.0新特性: Instant Add Column

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:

  1. 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())