1. 背景先容
数据库中每一行数据都被持久化存储在磁盘中。当我们对表进行ADD/DROP COLUMN操作时,磁盘中的数据也会相应地被修正,所需韶光与对应表的大小成正比。因此,对大表进行ADD/DROP COLUMN操作时,花费的韶光可能长达数小时或数天,这给用户的业务带来了诸多不便。
MySQL 5.5版本之前,只支持DDL的COPY算法。MySQL 5.6版本后,新增了支持INPLACE算法。两者差异在于是否能进行ONLINE DDL操作。对付ADD/DROP COLUMN操作,无论是COPY算法还是INPLACE算法,都须要修正用户原有数据,且没有办理耗时问题。
MySQL 8.0.12版本引入了INSTANT(即时)算法,对部分ADD COLUMN操作,不再修正用户原有数据,只需对表元信息进行修正。因此,操作韶光与表大小脱钩,并且操作韶光可以到秒级,但这个版本的 INSTANT 算法有以下几点问题:

MySQL 8.0.29版本中推出了INSTANT算法新的实现办法,办理了上述的2个问题。本文将剖析最新INSTANT算法的实现办法。
2. INSTANT语法ALTER TABLE tbl_name ADD [COLUMN] col_name column_definition [FIRST | AFTER col_name] , ALGORITHM=INSTANT; ALTER TABLE tbl_name DROP [COLUMN] col_name , ALGORITHM=INSTANT;
把稳 :
未指定ALGORITHM情形下,所有的ADD/DROP COLUMNS会优先考试测验INSTANT算法;
在单条ALTER TABLE语句中可以ADD/DROP COLUMNS多列。
3. 事理剖析对付ADD/DROP COLUMN操作,之条件到的COPY/INPLACE算法都须要重修表,将磁盘中原有record进行修正。而INSTANT算法只须要修正表元信息,就可以快速完成ADD/DROP COLUMN操作。
INSTANT算法技能上须要办理什么问题?
我们通过一个例子来剖析:
CREATE TABLE t1 (C1 char(10), C2 char(10), C3 char(10), C4 char(10)); INSERT INTO t1 values ("r1c1", "r1c2", "r1c3", "r1c4"); -- 数据一 ALTER TABLE t1 ADD COLUMN C5 CHAR(10) DEFAULT "c5_def", ALGORITHM=INSTANT; INSERT INTO t1 values ("r2c1", "r2c2", "r2c3", "r2c4", "r2c5"); -- 数据二 ALTER TABLE t1 DROP COLUMN C3, ALGORITHM=INSTANT; INSERT INTO t1 values ("r3c1", "r3c2", "r3c4", "r3c5");-- 数据三 SELECT FROM t1; +------+------+------+--------+ | C1 | C2 | C4 | C5 | +------+------+------+--------+ | r1c1 | r1c2 | r1c4 | c5_def| | r2c1 | r2c2 | r2c4 | r2c5 | | r3c1 | r3c2 | r3c4 | r3c5 | +------+------+------+--------+
由于INSTANT ADD/DROP操作不会修正原有数据,以是可以推出数据在磁盘等分布如下:
图1 数据磁盘分布
可以看到,只有数据三的存储内容和当前表的元信息(表的列)匹配,数据三能正常解析读取。数据一多了C3列的值,但短缺了C5列的值,数据二则多了C3列的值。
于是问题涌现了:如何精确解析返回老数据。若将解析和返回分开,则可以分为以下两个问题:
第一,解析。如何精确解析老数据?
第二,返回。老数据中短缺列如何添补?
注:这里老数据指的是存储内容和当前列不匹配的数据,上述例子中便是数据一、二。
INSTANT如何办理上述问题
如何解析
以COMPACT行格式为例,我们知道在record中存储数据是须要结合表的元数据(列类型、是否为空、是否变长等信息)进行解析的。
上述用例进行了多次DDL,每次都会修正表的元信息。以是在解析数据一、二、三时,所需的元数据版本肯定是不同的。
我们很自然会想到对元数据信息的历史版本进行掩护。在解析数据一的时候,找到与数据一对应的元数据版本;在解析数据二时,找到与数据二对应的元数据版本,以此类推。
MySQL 8.0.29中INSTANT DDL功能的实现,在于引入了元数据version的观点。
如何返回
可以在INSTANT ADD COLUMN操作时,将新列的默认值存储在元数据中。对付短缺该新列的老数据,在返回客户端时,可直策应用默认值进行添补。
此办法在MySQL 8.0.12版本的INSTANT DDL就已经实现,并在MySQL 8.0.29版本中进行了沿用。
本次INSTANT算法的实现重难点在于元数据version实现,下文也将着重阐明。
4、元数据version
元数据version实现涉及表元数据、列元数据、行格式三部分的修正。
表元数据在INFORMATION_SCHEMA.INNODB_TABLES中新增了TOTAL_ROW_VERSIONS字段:记录当前表VERSION值,新建的表初始值为0,每次INSTANT ADD/DROP后值递增1,重修表后清零。
-- 新建表,TOTAL_ROW_VERSIONS为0CREATE TABLE t1 (C1 char(10), C2 char(10), C3 char(10), C4 char(10));INSERT INTO t1 values ("r1c1", "r1c2", "r1c3", "r1c4"); -- 数据一SELECT NAME, TOTAL_ROW_VERSIONS FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME LIKE '%t1%';+---------+--------------------+| NAME | TOTAL_ROW_VERSIONS |+---------+--------------------+| test/t1 | 0 |+---------+--------------------+-- INSTANT ADD COLUMN,TOTAL_ROW_VERSIONS递增1ALTER TABLE t1 ADD COLUMN C5 CHAR(10) DEFAULT "c5_def", ALGORITHM=INSTANT;INSERT INTO t1 values ("r2c1", "r2c2", "r2c3", "r2c4", "r2c5"); -- 数据二SELECT NAME, TOTAL_ROW_VERSIONS FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME LIKE '%t1%';+---------+--------------------+| NAME | TOTAL_ROW_VERSIONS |+---------+--------------------+| test/t1 | 1 |+---------+--------------------+-- INSTANT DROP COLUMN,TOTAL_ROW_VERSIONS递增1ALTER TABLE t1 DROP COLUMN C3, ALGORITHM=INSTANT;INSERT INTO t1 values ("r3c1", "r3c2", "r3c4", "r3c5"); -- 数据三SELECT NAME, TOTAL_ROW_VERSIONS FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME LIKE '%t1%';+---------+--------------------+| NAME | TOTAL_ROW_VERSIONS |+---------+--------------------+| test/t1 | 2 |+---------+--------------------+-- 重修表,TOTAL_ROW_VERSIONS重置为0ALTER TABLE t1 force;SELECT NAME, TOTAL_ROW_VERSIONS FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME LIKE '%t1%';+---------+--------------------+| NAME | TOTAL_ROW_VERSIONS |+---------+--------------------+| test/t1 | 0 |+---------+--------------------+
把稳:在实行一次INSTANT ADD/DROP语句后,表VERSION的值会自增1,而不是针对每次添加或删除的列进行自增。如果一条语句增加了n个列,减少了m个列,表VERSION仍旧只会自增1。
列元数据在进行ADD/DROP COLUMN操作后,列元数据将进行修正。可以在mysql.columns表中表示。
CREATE TABLE t1 (C1 char(10), C2 char(10), C3 char(10), C4 char(10));INSERT INTO t1 values ("r1c1", "r1c2", "r1c3", "r1c4"); -- 数据一SET SESSION DEBUG = '+d,skip_dd_table_access_check';SELECT NAME, HIDDEN, SE_PRIVATE_DATA FROM mysql.columns WHERE table_id = (SELECT ID FROM mysql.tables WHERE NAME = 't1');
图2 列元数据查询结果
图3 磁盘数据分布
新建表列元数据如上图所示,HIDDEN列表示可见性。三个别系列的可见性为SE,代表INNODB可见,SERVER层不可见。SE_PRIVATE_DATA中也只记录着table_id。
下面将进行INSTANT ADD COLUMN操作。
ALTER TABLE t1 ADD COLUMN C5 CHAR(10) DEFAULT "c5_def" FIRST, ALGORITHM=INSTANT;INSERT INTO t1 values ("r2c1", "r2c2", "r2c3", "r2c4", "r2c5");SELECT NAME, HIDDEN, SE_PRIVATE_DATA FROM mysql.columns WHERE table_id = (SELECT ID FROM mysql.tables WHERE NAME = 't1');
图4 列元数据查询结果
图5 磁盘数据分布
原有列变革(DB_ROLL_PTR、DB_ROW_ID、DB_TRX_ID、C1、C2、C3、C4)
SE_PRIVATE_DATA中新增了physical_pos,表示列在record中的相对物理位置。为何是相对物理位置,后续解释。
ADD列变革(C5)
SE_PRIVATE_DATA中default存储列的默认值,对付短缺该列的数据(如数据一),解析返回时将利用默认值。
SE_PRIVATE_DATA中physical_pos新增列的physical_pos为原表physical_pos最大值加1,解释该列的物理位置在行尾,此特性与新增列是否指定位置无关。
SE_PRIVATE_DATA中version_added值为本次INSTANT操作后表的VERSION,对付本例,添加后的表的VERSION值从0到1,故version_added记录为1。
下面将进行INSTANT DROP COLUMN操作
图6 列元数据查询结果
图7 磁盘上数据分布
DROP列变革(C3/ !hidden!_dropped_v2_p5_C3)
name
列元数据中C3列的名字已经修正为了 !hidden!_dropped_v2_p5_C3。v2代表在表version为2的时候删除的,p5表示此列的physical_pos为5。
可能有人会问:C3列不是被删除了么,为啥在元数据中还要保留C3列?其余,为什么要改名为 !hidden!_dropped_v2_p5_C3,而不直接沿用C3的名字?
那是由于部分record(数据一、数据二)中还有此列数据,解析record时须要所有列元数据,以是C3列的元数据须要保留。而关于改名成 !hidden!_dropped_v2_p5_C3,是由于C3列在逻辑上被删除了,后续若添加同名列(重新添加C3列),那么该当要添加成功。于是就换成由删除版本信息以及当前物理位置信息共同构成的一个名字。
hidden
将被删除列的可见性设置为SE,对存储引擎可见,但是对server层不可见,和DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID一样。因此,在Innodb层解析出该列数据后,这些数据不会返回到server层。
SE_PRIVATE_DATA中version_dropped
本次INSTANT操作后,表的VERSION更新为新的值。对付本例,删除列后的表的VERSION值从1变成2,故version_dropped记录为2。
可以看到数据三C4列的physical_pos值为6,但是由于数据三中没有C3列的数据,以是,C4列数据实际在位置5。这解释physical_pos记录的是相对物理位置,要结合其他元信息才能确定该record中列的详细物理分布情形。
列元数据修正总结:
1. 经由instant操作后,所有列都将新增一个physical_pos字段记录的相对物理位置。
2. 对付instant add的列,新增version_added字段来记录添加后的表version值,并且列的physical_pos值为当前表最大的physical_pos值加1,与该列实际添加的位置无关。
3. 对付instant drop的列,元数据将被保留,但列名会进行修正名字,并将hidden属性设置为SE。
行格式INSTANT思路是,对付磁盘中的每一行record,利用不同的元数据去解析。元数据修正如上文所述。但如何确定record对应元数据哪一个版本呢?以是须要修正行格式,确保在每条新产生的record中记录当前表VERSION信息。
图8 Instant修正后的行格式
本次修正包括:
利用了行格式中原有INFO BITS字段的第二个比特位(称为`VERSION BIT`);行格式新增了ROW VERSION字段。VERSION BIT
INFO BITS是行格式原有字段,之前VERSION BIT所在的第二个比特位未被授予含义。故已有的record此处值都为0。
本次实现规定:
表VERSION为0时,此时插入的record的version bit为0;表VERSION不为0时,此时插入的record的version bit为1;ROW VERSION
本次实现规定:
表VERSION为0时,此时插入的record没有ROW VERSION字段;表VERSION不为0时,此时插入的record有ROW VERSION字段;注:如果表的VERSION不为0,那么表示该表曾经进行过INSTANT ADD/DROP操作,且之后未被重修。
下面两图(图9和图10)表示插入和查询时如何利用version bit和row version。
图9 插入数据流程
图10 查询数据流程
INSTANT任意位置实现
在列元数据中提到:
1. 进行INSTANT操作后,列元数据中会掩护一个physical_pos字段,用于记录该列在行中的相对物理位置。
2. 对付INSTANT ADD的列,physical_pos值都为原表最大值加1,解释新列的物理位置在行尾。
CREATE TABLE t1 (b int);ALTER TABLE t1 ADD COLUMN a INT FIRST, ALGORITHM = INSTANT;INSERT INTO t1 VALUES(1, 2); -- 数据一SET SESSION DEBUG = '+d,skip_dd_table_access_check';SELECT ID INTO @ID FROM mysql.tables WHERE NAME = 't1';SELECT NAME,HIDDEN,SE_PRIVATE_DATA FROM mysql.columns WHERE TABLE_ID = @ID;
图11列元数据
从physical_pos可知,数据一的实际存储如下,b列数据在a列前面。
图12 磁盘数据分布
而我们期待的返回,a列该当在b列前面(a列添加时有FIRST关键字)。
图13 表查询结果
故须要掩护一个逻辑位置和物理位置的对应关系。在 dict_index_t 构造体中新增名字为 fields_array 的成员。t1表的fields_array如下:
DB_ROW_ID: fields_array[0] = 0 物理和逻辑位置都是0DB_TRX_ID: fields_array[1] = 1 物理和逻辑位置都是1DB_ROLL_PTR: fields_array[2] = 2 物理和逻辑位置都是2a: fields_array[4] = 3 物理位置是4,逻辑位置是3b: fields_array[3] = 4 物理位置是3,逻辑位置是4
有了这个对应关系,从record中解析出的数据,可以以精确的逻辑顺序返回。
5. 解析/返回record列元数据中引入了VERSION_ADDED、VERSION_DROPPED。解析/返回一个record,可以依照下面的规则:
1. 解析数据时,忽略VERSION_DROPPED > 0的列(表示已经被删除);
2. 返回结果时,对付VERSION_ADDED > ROW_VERSION的情形(record中无此列数据),利用该列元数据中默认值。
CREATE TABLE t1 (C1 CHAR(10), C2 CHAR(10), C3 CHAR(10), C4 CHAR(10));INSERT INTO t1 VALUES ("r1c1", "r1c2", "r1c3", "r1c4");ALTER TABLE t1 DROP COLUMN C3, ALGORITHM=INSTANT;ALTER TABLE t1 ADD COLUMN C5 CHAR(10) DEFAULT "C5d", ALGORITHM=INSTANT;SELECT from t1;
图示:
+----------------------------------+| Columns' Metadata in DD |+---------+------------------------+| Version | C1 | C2 | C3 | C4 | C5 |+---------+------------------------+| 0 | E | E | E | E | - | +---------------------------++---------+------------------------+ | Default value of C5 in DD || 1 | E | E | ID | E | - | +---------------------------++---------+------------------------+ | C5d || 2 | E | E | - | E | IA | +---------------------------+ +---------+------------------------+ | E => Existing Column | ID => INSTANT Dropped Column | IA => INSTANT Added Column | |+----------------+-----+-----+-----+-----+ || V0 row on disk | r1c1| r1c2| r1c3| r1c4| |+----------------+-----+-----+-----+-----+ | | | x | +-----------+ | | x | | V V V V +-----------------+------+------+------+------+| Row fetched | r1c1 | r1c2 | r1c4 | C5d | +-----------------+------+------+------+------+
6. 用例总图解
新建表,插入数据一
CREATE TABLE t1 (C1 char(10), C2 char(10), C3 char(10), C4 char(10));INSERT INTO t1 values("r1c1", "r1c2", "r1c3", "r1c4");
图14 数据和列元数据
ADD COLUMN,插入数据二
ALTER TABLE t1 ADD COLUMN C5 CHAR(10) DEFAULT "c5_def", ALGORITHM=INSTANT;INSERT INTO t1 values ("r2c1", "r2c2", "r2c3", "r2c4", "r2c5");
图15 数据和列元数据
DROP COLUMN,插入数据三
ALTER TABLE t1 DROP COLUMN C3, ALGORITHM=INSTANT;INSERT INTO t1 values ("r3c1", "r3c2", "r3c4", "r3c5");
图16 数据和列元数据
7. 总结
Instant 算法的事理大致如上。实现特点总结:
1. 修正元数据,不修正原有的数据,让ADD/DROP COLUMN操作的韶光缩短到秒级;
2. 掩护元数据版本,并修正行格式,让record找到对应的元数据版本进行解析;
3. 掩护列的物理位置和逻辑位置的对应关系,以确保能够按照精确顺序将数据返回客户端。
INSTANT ADD/DROP COLUMN功能同样存在一些小问题,MySQL 8.0.29版本推出Instant算法最新实现后,一开始碰着了不少bug,导致社区也认为该版本是不稳定的,到8.0.33版本之后才逐渐稳定。我们在MySQL 8.0.37版本进行测试时,也创造2个bug,并已向社区反馈。
[1] https://bugs.mysql.com/bug.php?id=116035
[2] https://bugs.mysql.com/bug.php?id=115890
社区的改进极大优化了ADD/DROP COLUMN的效率,希望通过本文的先容,读者对最新的实现有进一步的理解,欢迎互换。
参考:https://blogs.oracle.com/mysql/post/mysql-80-instant-add-drop-columns
点击关注,第一韶光理解华为云新鲜技能~
华为云博客_大数据博客_AI博客_云打算博客_开拓者中央-华为云