MySQL 8.0.23新特性 - 不可見(jiàn)列
在新的MySQL 8.0.23中,引入了新的有趣功能:不可見(jiàn)列。
這是第一篇關(guān)于這個(gè)新功能的文章,我希望寫一個(gè)3篇的系列。這是前言。
在MySQL 8.0.23之前,表中所有的列都是可見(jiàn)的(如果您有權(quán)限的話)?,F(xiàn)在可以指定一個(gè)不可見(jiàn)的列,它將對(duì)查詢隱藏。如果顯式引用,它可以被查到。
讓我們看看它是怎樣的:
- create table table1 (
- id int auto_increment primary key,
- name varchar(20),
- age int invisible);
在表結(jié)構(gòu)中我們?cè)贓xtra列可以看到INVISIBLE 關(guān)鍵字:
- desc table1;
- +-------+-------------+------+-----+---------+----------------+
- | Field | Type | Null | Key | Default | Extra |
- +-------+-------------+------+-----+---------+----------------+
- | id | int | NO | PRI | NULL | auto_increment |
- | name | varchar(20) | YES | | NULL | |
- | age | int | YES | | NULL | INVISIBLE |
- +-------+-------------+------+-----+---------+----------------+
查看show create table語(yǔ)句,注意到有一個(gè)不同,當(dāng)我創(chuàng)建表時(shí),我希望看到INVISIBLE 關(guān)鍵字,但事實(shí)并非如此:
- show create table table1\\G
- ************************* 1. row *************************
- Table: table1
- Create Table: CREATE TABLE `table1` (
- id int NOT NULL AUTO_INCREMENT,
- name varchar(20) DEFAULT NULL,
- age int DEFAULT NULL /*!80023 INVISIBLE */,
- PRIMARY KEY (id)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
但是我確認(rèn)這個(gè)語(yǔ)句在創(chuàng)建表時(shí)會(huì)將age 列設(shè)置為不可見(jiàn)。所以我們有2個(gè)不同的語(yǔ)法來(lái)創(chuàng)建不可見(jiàn)列。
INFORMATION_SCHEMA 中也可以看到相關(guān)信息:
- SELECT TABLE_NAME, COLUMN_NAME, EXTRA
- FROM INFORMATION_SCHEMA.COLUMNS
- WHERE TABLE_SCHEMA = 'test' AND TABLE_NAME = 'table1';
- +------------+-------------+----------------+
- | TABLE_NAME | COLUMN_NAME | EXTRA |
- +------------+-------------+----------------+
- | table1 | id | auto_increment |
- | table1 | name | |
- | table1 | age | INVISIBLE |
- +------------+-------------+----------------+
插入一些數(shù)據(jù),繼續(xù)觀察:
- insert into table1 values (0,'mysql', 25),
- (0,'kenny', 35),
- (0, 'lefred','44');
- ERROR: 1136: Column count doesn't match value count at row 1
如預(yù)期,插入語(yǔ)句中如果我們不引用它,會(huì)報(bào)錯(cuò)。引用這些列:
- insert into table1 (id, name, age)
- values (0,'mysql', 25),
- (0,'kenny', 35),
- (0, 'lefred','44');
- Query OK, 3 rows affected (0.1573 sec
查詢表中數(shù)據(jù):
- select * from table1;
- +----+--------+
- | id | name |
- +----+--------+
- | 1 | mysql |
- | 2 | kenny |
- | 3 | lefred |
- +----+--------+
再一次,如預(yù)期,我們看到不可見(jiàn)列沒(méi)有顯示。
如果我們指定它:
- select name, age from table1;
- +--------+-----+
- | name | age |
- +--------+-----+
- | mysql | 25 |
- | kenny | 35 |
- | lefred | 44 |
- +--------+-----+
當(dāng)然我們可以將列從可見(jiàn)轉(zhuǎn)為不可見(jiàn)或者將不可見(jiàn)轉(zhuǎn)為可見(jiàn):
- alter table table1 modify name varchar(20) invisible,
- modify age integer visible;
- Query OK, 0 rows affected (0.1934 sec)
- select * from table1;
- +----+-----+
- | id | age |
- +----+-----+
- | 1 | 25 |
- | 2 | 35 |
- | 3 | 44 |
- +----+-----+
我對(duì)這個(gè)新功能感到非常高興,在下一篇文章中我們將會(huì)看到為什么這對(duì)InnoDB來(lái)說(shuō)是一個(gè)重要的功能。
本文是與MySQL不可見(jiàn)列相關(guān)的系列文章的第二部分。
這篇文章介紹了為什么不可見(jiàn)列對(duì)InnoDB存儲(chǔ)引擎很重要。
首先,讓我簡(jiǎn)單解釋一下InnoDB是如何處理主鍵的,以及為什么一個(gè)好的主鍵很重要。最后,為什么主鍵也很重要。
InnoDB如何存儲(chǔ)數(shù)據(jù)?
InnoDB在表空間存儲(chǔ)數(shù)據(jù)。這些記錄存儲(chǔ)并用聚簇索引排序(主鍵):它們被稱為索引組織表。
所有的二級(jí)索引也將主鍵作為索引中的最右邊的列(即使沒(méi)有公開(kāi))。這意味著當(dāng)使用二級(jí)索引檢索一條記錄時(shí),將使用兩個(gè)索引:二級(jí)索引指向用于最終檢索該記錄的主鍵。
主鍵會(huì)影響隨機(jī)I/O和順序I/O之間的比率以及二級(jí)索引的大小。
隨機(jī)主鍵還是順序主鍵?
如上所述,數(shù)據(jù)存儲(chǔ)在聚簇索引中的表空間中。這意味著如果您不使用順序索引,當(dāng)執(zhí)行插入時(shí),InnoDB不得不重平衡表空間的所有頁(yè)。
如果我們用InnoDB Ruby來(lái)說(shuō)明這個(gè)過(guò)程,下面的圖片顯示了當(dāng)使用隨機(jī)字符串作為主鍵插入記錄時(shí)表空間是如何更新的:
每次有一個(gè)插入,幾乎所有的頁(yè)都會(huì)被觸及。
當(dāng)使用自增整型作為主鍵時(shí),同樣的插入:
自增主鍵的情況下,只有第一個(gè)頁(yè)和最后一個(gè)頁(yè)才會(huì)被觸及。
讓我們用一個(gè)高層次的例子來(lái)解釋這一點(diǎn):
假設(shè)一個(gè)InnoDB頁(yè)可以存儲(chǔ)4條記錄(免責(zé)聲明:這只是一個(gè)虛構(gòu)的例子),我們使用隨機(jī)主鍵插入了一些記錄:
插入新記錄,主鍵為AA!
修改所有頁(yè)以"重新平衡"聚簇索引,在連續(xù)主鍵的情況下,只有最后一個(gè)頁(yè)面會(huì)被修改。想象一下成千上萬(wàn)的插入發(fā)生時(shí)所要做的額外工作。
這意味著選擇好的主鍵是重要的。需要注意兩點(diǎn):
- 主鍵必須連續(xù)。
- 主鍵必須短。
UUID怎么樣?
我通常建議使用自增整型(或bigint)作為主鍵,但是不要忘記監(jiān)控它們!
但我也明白越來(lái)越多的開(kāi)發(fā)人員喜歡使用uuid。
如果您打算使用UUID,您應(yīng)該閱讀MySQL8.0中UUID的支持,這篇文章推薦您用binary(16) 存儲(chǔ)UUID。
如:
- CREATE TABLE t (id binary(16) PRIMARY KEY);
- INSERT INTO t VALUES(UUID_TO_BIN(UUID()));
然而,我并不完全同意這個(gè)觀點(diǎn),為什么?
因?yàn)槭褂胾uid_to_bin() 可能會(huì)改變MySQL的UUID實(shí)現(xiàn)的順序行為(有關(guān)更多信息,請(qǐng)參閱額外部分)。
但是如果您需要UUID,你需要在大索引上花費(fèi)一定代價(jià),索引不要浪費(fèi)存儲(chǔ)和內(nèi)存在不需要的二級(jí)索引上:
- select * from sys.schema_unused_indexes where object_schema not in ('performance_schema', 'mysql');
沒(méi)有任何主鍵?
對(duì)InnoDB表來(lái)說(shuō),當(dāng)沒(méi)有定義主鍵,會(huì)使用第一個(gè)唯一非空列。如果沒(méi)有可用的列,InnoDB會(huì)創(chuàng)建一個(gè)隱藏主鍵(6位)。
這類主鍵的問(wèn)題在于您無(wú)法控制它,更糟糕的是,這個(gè)值對(duì)所有沒(méi)有主鍵的表是全局的,如果您同時(shí)對(duì)這些表執(zhí)行多次寫操作,可能會(huì)產(chǎn)生爭(zhēng)用問(wèn)題(dict_sys->mutex)。
不可見(jiàn)列的用處
有了新的不可見(jiàn)列,如果應(yīng)用不允許添加新列,我們現(xiàn)在就可以向沒(méi)有主鍵的表添加合適的主鍵。
首先先找到這些表:
- SELECT tables.table_schema , tables.table_name , tables.engine
- FROM information_schema.tables LEFT JOIN (
- SELECT table_schema , table_name
- FROM information_schema.statistics
- GROUP BY table_schema, table_name, index_name
- HAVING SUM(
- case when non_unique = 0 and nullable != 'YES' then 1 else 0 end ) = count(*) ) puks
- ON tables.table_schema = puks.table_schema
- AND tables.table_name = puks.table_name
- WHERE puks.table_name IS null
- AND tables.table_type = 'BASE TABLE'
- AND Engine="InnoDB";
- +--------------+--------------+--------+
- | TABLE_SCHEMA | TABLE_NAME | ENGINE |
- +--------------+--------------+--------+
- | test | table2 | InnoDB |
- +--------------+--------------+--------+
您也可以使用MySQL Shell中的校驗(yàn)插件:https://github.com/lefred/mysqlshell-plugins/wiki/check#getinnodbtableswithnopk
讓我們查看表定義:
- show create table table2\\G
- *************** 1. row ***************
- Table: table2
- Create Table: CREATE TABLE table2 (
- name varchar(20) DEFAULT NULL,
- age int DEFAULT NULL
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
其中的數(shù)據(jù):
- select * from table2;
- +--------+-----+
- | name | age |
- +--------+-----+
- | mysql | 25 |
- | kenny | 35 |
- | lefred | 44 |
- +--------+-----+
現(xiàn)在添加指定不可見(jiàn)主鍵:
- alter table table2
- add column id int unsigned auto_increment
- primary key invisible first;
插入一條新記錄:
- insert into table2 (name, age) values ('PHP', 25);
- select * from table2;
- +--------+-----+
- | name | age |
- +--------+-----+
- | mysql | 25 |
- | kenny | 35 |
- | lefred | 44 |
- | PHP | 25 |
- +--------+-----+
如果我們想要查看主鍵:
- select id, table2.* from table2;
- +----+--------+-----+
- | id | name | age |
- +----+--------+-----+
- | 1 | mysql | 25 |
- | 2 | kenny | 35 |
- | 3 | lefred | 44 |
- | 4 | PHP | 25 |
- +----+--------+-----+
總結(jié)
現(xiàn)在您知道InnoDB中為什么主鍵很重要,為什么一個(gè)好的主鍵更重要。
從MySQL8.0.23開(kāi)始,您可以用不可見(jiàn)列解決沒(méi)有主鍵的表。
額外
僅為娛樂(lè),并說(shuō)明我對(duì)使用UUID_TO_BIN(UUID()) 作為主鍵的看法,讓我們重新使用UUID作為不可見(jiàn)列重復(fù)這個(gè)例子。
- alter table table2 add column id binary(16) invisible first;
- alter table table2 modify column id binary(16)
- default (UUID_TO_BIN(UUID())) invisible;
- update table2 set id=uuid_to_bin(uuid());
- alter table table2 add primary key(id);
到目前還沒(méi)什么特別的,只是創(chuàng)建不可見(jiàn)主鍵需要一些技巧。
查詢:
- select * from table2;
- +--------+-----+
- | name | age |
- +--------+-----+
- | mysql | 25 |
- | kenny | 35 |
- | lefred | 44 |
- +--------+-----+
現(xiàn)在,我們?cè)傧蜻@個(gè)表插入一條新數(shù)據(jù):
- insert into table2 (name, age) values ('PHP', 25);
- select * from table2;
- +--------+-----+
- | name | age |
- +--------+-----+
- | PHP | 25 |
- | mysql | 25 |
- | kenny | 35 |
- | lefred | 44 |
- +--------+-----+
Mmmm...為什么PHP現(xiàn)在是第一行?
因?yàn)閡uid() 并不連續(xù)...
- select bin_to_uuid(id), table2.* from table2;
- +--------------------------------------+--------+-----+
- | bin_to_uuid(id) | name | age |
- +--------------------------------------+--------+-----+
- | 05aedcbd-5b36-11eb-94c0-c8e0eb374015 | PHP | 25 |
- | af2002e8-5b35-11eb-94c0-c8e0eb374015 | mysql | 25 |
- | af20117a-5b35-11eb-94c0-c8e0eb374015 | kenny | 35 |
- | af201296-5b35-11eb-94c0-c8e0eb374015 | lefred | 44 |
- +--------------------------------------+--------+-----+
我們還有別的選擇嗎?
是的,如果我們參考官檔,我們可以使用uuid_to_bin() 函數(shù)。
- alter table table2 add column id binary(16) invisible first;
- alter table table2 modify column id binary(16)
- default (UUID_TO_BIN(UUID(),1)) invisible;
- update table2 set id=uuid_to_bin(uuid(),1);
現(xiàn)在我們每次插入一條新記錄,插入如期望一樣是順序的:
- select bin_to_uuid(id,1), table2.* from table2;
- +--------------------------------------+--------+-----+
- | bin_to_uuid(id,1) | name | age |
- +--------------------------------------+--------+-----+
- | 5b3711eb-023c-e634-94c0-c8e0eb374015 | mysql | 25 |
- | 5b3711eb-0439-e634-94c0-c8e0eb374015 | kenny | 35 |
- | 5b3711eb-0471-e634-94c0-c8e0eb374015 | lefred | 44 |
- | f9f075f4-5b37-11eb-94c0-c8e0eb374015 | PHP | 25 |
- | 60ccffda-5b38-11eb-94c0-c8e0eb374015 | PHP8 | 1 |
- | 9385cc6a-5b38-11eb-94c0-c8e0eb374015 | Python | 20 |
- +--------------------------------------+--------+-----+
我們之前看了從MySQL8.0.23后,新的不可見(jiàn)列的功能。如果主鍵沒(méi)有定義,我們?nèi)绾问褂盟鼮镮nnoDB表添加主鍵。
如之前所述,好的主鍵對(duì)InnoDB很重要(存儲(chǔ),IOPS,二級(jí)索引,內(nèi)存等)但是MySQL中主鍵還有一個(gè)重要的作用:復(fù)制!
異步復(fù)制
當(dāng)使用"傳統(tǒng)復(fù)制"時(shí),如果您修改了一行記錄(更新和刪除),那么要在副本上修改的記錄將使用索引來(lái)標(biāo)識(shí),當(dāng)然如果有主鍵的話,還會(huì)使用主鍵。InnoDB自動(dòng)生成的隱藏全局6字節(jié)主鍵永遠(yuǎn)不會(huì)被使用,因?yàn)樗侨值?,所以不能保證源和副本之間是相同的。你根本不應(yīng)該考慮它。
如果算法不能找到合適的索引,或者只能找到一個(gè)非唯一索引或者包含null值,則需要使用哈希表來(lái)識(shí)別表記錄。該算法創(chuàng)建一個(gè)哈希表,其中包含更新或者刪除操作的記錄,并用鍵作為該行之前完整的映像。然后,該算法遍歷目標(biāo)表中的所有記錄,如果找到了所選索引,則使用該索引,否則執(zhí)行全表掃描(參見(jiàn)官檔)。
因此,如果應(yīng)用程序不支持使用額外的鍵作為主鍵,則使用隱藏列作為主鍵是加快復(fù)制的一個(gè)方法。
- mysql> create table t1 (name varchar(20), age int);
- mysql> insert into t1 values ('mysql',25),('kenny', 35),('lefred', 44);
現(xiàn)在添加一個(gè)自增列作為主鍵:
- mysql> alter table t1 add id int auto_increment primary key first;
然后按照應(yīng)用程序中指定的INSERT語(yǔ)句添加一條記錄:
- mysql > insert into t1 values ('python',20);
- ERROR: 1136: Column count doesn't match value count at row 1
最好的方法是修改應(yīng)用的INSERT 語(yǔ)句,但是可能嗎?
多少應(yīng)用程序仍然是使用SELECT * ,并且引用列時(shí)如col[2]?
如果是這樣,您有兩種方法:
- 分析所有的查詢,使用重寫查詢插件
- 使用不可見(jiàn)列
在這種情況下,選擇是容易的(至少對(duì)像我這樣的懶人說(shuō))。
- mysql > alter table t1 modify id int auto_increment invisible;
- mysql > insert into t1 values ('python',20);
- Query OK, 1 row affected (0.0887 sec)
很簡(jiǎn)單,不是嗎?
組復(fù)制
MySQL InnoDB Cluster使用另一種復(fù)制:Group Replication。
使用組復(fù)制的要求之一是要有一個(gè)主鍵(這就是為什么可以使用sql_require_primary_key)。
我們使用上例中重構(gòu)表,不加主鍵,檢查該實(shí)例能否作為InnoDB Cluster:
https://lefred.be/wp-content/uploads/2021/01/Selection_9991017-1024x561.png
提示很清楚,該表上的修改不會(huì)復(fù)制到其他節(jié)點(diǎn)。
添加不可見(jiàn)主鍵,重新檢查:
https://lefred.be/wp-content/uploads/2021/01/Selection_9991018-1024x89.png
https://lefred.be/wp-content/uploads/2021/01/Selection_9991019-1024x384.png
這意味著,如果應(yīng)用程序使用的表沒(méi)有主鍵,不允許遷移到MySQL InnoDB Cluster等高可用架構(gòu)中,現(xiàn)在多虧了不可見(jiàn)列,這可以做到了。
這也解決了Hadoop Hive對(duì)MySQL InnoDB Cluster的支持(參見(jiàn)Hive-17306)。