MySQL表中儲存UUID值作為主鍵,使用UNHEX()提升效能
假設我們有一個使用者表,每個使用者都有一個UUID。MySQL有一個UUID()函式,它使MySQL生成一個UUID值,並以VARCHAR(36)型別的可讀形式返回。讓我們試試MySQL 5.7.8:
mysql> select uuid();
+--------------------------------------+
| uuid() |
+--------------------------------------+
| aab5d5fd-70c1-11e5-a4fb-b026b977eb28 |
+--------------------------------------+
mysql> select uuid();
+--------------------------------------+
| uuid() |
+--------------------------------------+
| aab5d5fd-70c1-11e5-a4fb-b026b977eb28 |
+--------------------------------------+
所以第一個想法是簡單地做到這一點:
create table users(id varchar(36), name varchar(200));
insert into users values(uuid(), 'Andromeda');
create table users(id varchar(36), name varchar(200));
insert into users values(uuid(), 'Andromeda');
但是這種UUID的可讀形式並不緊湊。讓我們觀察一下:
四個破折號是多餘的
每對字元實際上是一個在00-FF範圍內的十六進位制數; 總共有16個數字(以上為:0xAA,0xB5等),每個數字可以儲存在一個位元組中。
所以我們可以使用REPLACE()去掉破折號,UNHEX()把每個
雙字元對轉換成一個位元組:
create table users(id_bin binary(16), name varchar(200));
insert into users values(unhex(replace(uuid(),'-','')), 'Andromeda');
create table users(id_bin binary(16), name varchar(200));
insert into users values(unhex(replace(uuid(),'-','')), 'Andromeda');
這個二進位制形式使用16個位元組,比人類可讀形式(我現在稱之為“文字”形式)使用的VARCHAR(36)小得多。如果UUID必須是主鍵,則增益更大,如InnoDB中的主鍵值被複制到所有二級索引值中。
二進位制(16)是...好吧...只是二進位制!沒有字符集,沒有排序,只有十六個位元組。適合我們的需求。
也許在某些應用程式中,文字形式仍然是必需的,所以讓我們把它作為表格中的一個附加列; 但為了儘量減少磁碟佔用,讓我們將文字形成一個虛擬的生成 列(這是MySQL 5.7的一個新特性,在CREATE TABLE的文件中有描述)。該列將通過二進位制格式列的公式計算:我們將二進位制格式轉換回十六進位制數字並插入破折號。
create table users(
id_bin binary(16),
id_text varchar(36) generated always as
(insert(
insert(
insert(
insert(hex(id_bin),9,0,'-'),
14,0,'-'),
19,0,'-'),
24,0,'-')
) virtual,
name varchar(200));
insert into users (id_bin,name)
values(unhex(replace(uuid(),'-','')), 'Andromeda');
select id_text, name from users;
+--------------------------------------+-----------+
| id_text | name |
+--------------------------------------+-----------+
| C2770D2E-70E6-11E5-A4FB-B026B977EB28 | Andromeda |
+--------------------------------------+-----------+
create table users(
id_bin binary(16),
id_text varchar(36) generated always as
(insert(
insert(
insert(
insert(hex(id_bin),9,0,'-'),
14,0,'-'),
19,0,'-'),
24,0,'-')
) virtual,
name varchar(200));
insert into users (id_bin,name)
values(unhex(replace(uuid(),'-','')), 'Andromeda');
select id_text, name from users;
+--------------------------------------+-----------+
| id_text | name |
+--------------------------------------+-----------+
| C2770D2E-70E6-11E5-A4FB-B026B977EB28 | Andromeda |
+--------------------------------------+-----------+
我沒有在SELECT中包含id_bin,因為它會以隱藏字元(ASCII碼0xC2,0x77等:通常不在人可讀的字元範圍內)出現。我們沒有理由需要看id_bin的內容; 但是,如果這樣做,則可以使用HEX(id_bin)來顯示其十六進位制程式碼。
請注意,id_text被宣告為VIRTUAL,因此在磁碟上的表中不佔用空間。
使id_text成為生成列的另一個好處是消除了兩列之間的任何不一致的風險。事實上,如果id_text是一個簡單的列,可以做
update users set id_bin = <something>;
而無意更新id_text。但是作為一個生成的列,id_text不能直接更新,而是在更新id_bin時自動更新。換句話說,資訊只在一個地方(id_bin),資料庫保證了一致性。
那麼,那麼查詢呢?例如,我們可能想通過UUID找到一個使用者:
select * from users where <it has UUID XYZ>;
WHERE子句應該指定二進位制還是文字形式?這取決於:
如果我們建立一個二進位制形式的索引:
alter table users add unique(id_bin);
那麼,為了使用這個索引,WHERE應該指定二進位制形式:
WHERE id_bin = binary_form_of_XYZ
相反,如果我們在文字表單上建立一個索引:
alter table users add unique(id_text);
那麼,WHERE應該指定文字形式:
WHERE id_text = text_form_of_XYZ
即使id_text是一個虛擬列,也可以像上面那樣在其上新增索引(在這種情況下,索引佔用磁碟空間)。這是MySQL 5.7.8中引入的一個新功能。
但是,如果我們有一個選擇,由於二進位制形式更短,它索引它看起來更合乎邏輯,而不是文字形式 - 索引將更小,從而更快地遍歷,更快的備份...
最後,還有如何巧妙地重新排列二進位制形式的位元組的問題。
要了解這一點,我們需要了解更多關於UUID的資訊。它們存在幾個版本,不同的來源可以生成不同的版本。MySQL的UUID()使用版本1,這意味著,正如在RFC 4.1.2中所解釋的那樣,三個最左邊的以破折號分隔的組是8位元組的時間戳:最左邊的組是時間戳的低四個位元組; 第二組是中間兩個位元組,第三個組是高(最重要)兩個位元組的時間戳。因此,最左邊的組變化最快(每微秒10次)。我們可以驗證:
mysql> select uuid(); do sleep(2); select uuid();
+--------------------------------------+
| uuid() |
+--------------------------------------+
| 3b96402f-70c5-11e5-a4fb-b026b977eb28 |
+--------------------------------------+
+--------------------------------------+
| uuid() |
+--------------------------------------+
| 3cc7f7dc-70c5-11e5-a4fb-b026b977eb28 |
+--------------------------------------+
mysql> select uuid(); do sleep(2); select uuid();
+--------------------------------------+
| uuid() |
+--------------------------------------+
| 3b96402f-70c5-11e5-a4fb-b026b977eb28 |
+--------------------------------------+
+--------------------------------------+
| uuid() |
+--------------------------------------+
| 3cc7f7dc-70c5-11e5-a4fb-b026b977eb28 |
+--------------------------------------+
你可以看到最左邊的8個字元是如何變化的,而其他的則沒有。
因此,在由單個機器連續產生的UUID序列中,所有UUID具有不同的第一位元組。將該序列插入索引列(二進位制或文字形式)將因此每次修改不同的索引頁面,從而防止記憶體中的快取。因此,在我們儲存到id_bin之前,重新安排UUID,使得快速變化的部分走到最後,是有意義的。再一次請注意,這個想法只適用於版本1的UUID。
這個想法不是我的 ; 我在第一次看到它這個部落格(http://mysql.rjweb.org/doc.php/uuid)和那一個(https://www.percona.com/blog/2014/12/19/store-uuid-optimized-way/)。
以下,通過改變時間低/時間中/時間高到時間高/時間中/時間低來重新排列二進位制形式。
create table users(id_bin binary(16), name varchar(200));
set @u = unhex(replace(uuid(),'-',''));
insert into users (id_bin,name)
values
(
concat(substr(@u, 7, 2), substr(@u, 5, 2),
substr(@u, 1, 4), substr(@u, 9, 8)),
'Andromeda'
);
create table users(id_bin binary(16), name varchar(200));
set @u = unhex(replace(uuid(),'-',''));
insert into users (id_bin,name)
values
(
concat(substr(@u, 7, 2), substr(@u, 5, 2),
substr(@u, 1, 4), substr(@u, 9, 8)),
'Andromeda'
);
我在(@u)之上使用了一個使用者變數,因為每個SUBSTR()呼叫需要引用UUID值,但是我不能寫UUID()四次:每次都會生成一個新的UUID!所以我呼叫一次UUID(),刪除破折號,將其轉換為二進位制檔案,將其儲存在一個變數,並做它的四個SUBSTR。
但是,我仍然希望文字格式處於“未重新排列”的順序,因為...或許這個文字格式將用於一些錯誤日誌記錄,除錯?如果人類要閱讀它,我不想通過重新排列的順序來混淆它們。
新增id_text可以在CREATE TABLE中完成,或者作為後續的ALTER TABLE:
alter table users add
id_text varchar(36) generated always as
(
insert(
insert(
insert(
insert(
hex(
concat(substr(id_bin,5,4),substr(id_bin,3,2),
substr(id_bin,1,2),substr(id_bin,9,8))
),
9,0,'-'),
14,0,'-'),
19,0,'-'),
24,0,'-')
) virtual;
alter table users add
id_text varchar(36) generated always as
(
insert(
insert(
insert(
insert(
hex(
concat(substr(id_bin,5,4),substr(id_bin,3,2),
substr(id_bin,1,2),substr(id_bin,9,8))
),
9,0,'-'),
14,0,'-'),
19,0,'-'),
24,0,'-')
) virtual;
它將二進位制形式的部分(帶有SUBSTR)放在“正常”位置(CONCAT),將位元組轉換為十六進位制數字(HEX)並插入破折號。對,這是一個複雜的表示式,但是在建立生成的列時只能輸入一次。
現在看看資料:
select id_bin, hex(id_bin), name, id_text from users;
+------------------+----------------------------------+-----------+--------------------------------------+
| id_bin | hex(id_bin) | name | id_text |
+------------------+----------------------------------+-----------+--------------------------------------+
| �p�:�ˤ� &�w� | 11E570EA3A059CCBA4FBB026B977EB28 | Andromeda | 3A059CCB-70EA-11E5-A4FB-B026B977EB28 |
+------------------+----------------------------------+-----------+--------------------------------------+
select id_bin, hex(id_bin), name, id_text from users;
+------------------+----------------------------------+-----------+--------------------------------------+
| id_bin | hex(id_bin) | name | id_text |
+------------------+----------------------------------+-----------+--------------------------------------+
| �p�:�ˤ� &�w� | 11E570EA3A059CCBA4FBB026B977EB28 | Andromeda | 3A059CCB-70EA-11E5-A4FB-B026B977EB28 |
+------------------+----------------------------------+-----------+--------------------------------------+
專欄是:
1.隱含字元(重新排列的二進位制UUID)
2.第一列的相應的十六進位制程式碼
3.名字
4.未重新排列的文字UUID。看看這個文字形式最左邊的3A059CCB是如何快速變化的部分,處於二進位制形式(第2列)的中間,所以二進位制形式將會提供更有效的索引。
java中使用UUID.randomUUID().toString().replace("-", "").toLowerCase()來生成UUID。