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。

posted @ 2018-12-06 22:25  菜鸡de家  阅读(7429)  评论(1编辑  收藏  举报