A lot of things have been said about UUID, and storing UUID in an optimized way. Now that we have generated columns, we can store the decomposed information inside the UUID and merge it again with generated columns. This blog post demonstrates this process.
First, I used a simple table with one char field that I called uuid_char to establish a base case. I used this table with and without a primary key:
|
1 |
CREATE TABLE uuid_char (<br>uuid char(36) CHARACTER SET utf8 NOT NULL,<br>) ENGINE=InnoDB DEFAULT CHARSET=latin1;<br><br>CREATE TABLE uuid_char_pk (<br>uuid char(36) CHARACTER SET utf8 NOT NULL,<br>PRIMARY KEY (uuid)<br>) ENGINE=InnoDB DEFAULT CHARSET=latin1;<br><br> |
I performed the tests on a local VM over MySQL 5.7.17 for 30 seconds, with only two threads, because I wanted to just compare the executions:
|
1 |
sysbench<br>--oltp-table-size=100000000<br>--test=/usr/share/doc/sysbench/tests/db/insert_uuid_generated_columns.uuid_char.lua<br>--oltp-tables-count=4<br>--num-threads=2<br>--mysql-user=root<br>--max-requests=5000000<br>--report-interval=5<br>--max-time=30<br>--mysql-db=generatedcolumn<br>run<br> |
One pair of executions is with the UUID generated by sysbench, which simulates the UUID that comes from the app:
|
1 |
rs = db_query("INSERT INTO uuid_char (uuid) VALUES " .. string.format("('%s')",c_val))<br> |
An alternative execution is for when the UUID is generated by the MySQL function uuid():
|
1 |
rs = db_query("INSERT INTO uuid_char (uuid) VALUES (uuid())") |
Below we can see the results:
The inserts are faster without a PK (but only by 5%), and using the uuid() function doesn’t impact performance.
Now, let’s see the alternative method, which is decomposing the UUID. It has four main information sets:
The structure of the table that we’ll use is:
|
1 |
CREATE TABLE `uuid_generated` (<br>`timestamp` decimal(18,7) unsigned NOT NULL,<br>`mac` bigint(20) unsigned NOT NULL,<br>`temp_uniq` binary(2) NOT NULL,<br>PRIMARY KEY (`timestamp`,`mac`,`temp_uniq`)<br>) ENGINE=InnoDB DEFAULT CHARSET=latin1; |
To understand how a UUID is unwrapped, I used this store procedure (which receives a UUID and inserts it into the table):
|
1 |
CREATE PROCEDURE ins_generated_uuid (uuid char(38))<br>begin<br>set @hex_timestamp = concat(substring(uuid, 16, 3), substring(uuid, 10, 4), substring(uuid, 1, 8));<br>set @timestamp = concat(conv(@hex_timestamp,16,10)div 10000000 - (141427 * 24 * 60 * 60),'.',right(conv(@hex_timestamp,16,10),7));<br>set @mac = conv(right(uuid,12),16,10);<br>set @temp_uniq = unhex(substring(uuid,20,4));<br>insert into uuid_generated (timestamp,mac,temp_uniq) values (@timestamp,@mac,@temp_uniq);<br>end ;; |
Explanation:
If I wanted to get the UUID again, I can use these two generated columns:
|
1 |
`hex_timestamp` char(40) GENERATED ALWAYS AS (conv(((`timestamp` * 10000000) + (((141427 * 24) * 60) * 600000000)),10,16)) VIRTUAL,<br>`uuid_generated` char(38) GENERATED ALWAYS AS (concat(right(`hex_timestamp`,8),'-',substr(`hex_timestamp`,4,4),'-1',left(`hex_timestamp`,3),'-',convert(hex(`temp_uniq`) using utf8),'-',lpad(conv(`mac`,10,16),12,'0'))) VIRTUAL, |
We performed tests over five scenarios:

The difference between the Base and the previous table structure with Primary Keys is very small. So, the new basic structure has no impact on performance.
We see that Base and +Char Field have the same performance. So leaving a char field has no performance impact (it just uses more disk space).
Using generated columns impact performance. This is expected, as the columns are generated to validate the type before the row is inserted.
Finally, the use of triggers and store procedure has the same impact in performance.
These are the three structures to the tables:
|
1 |
CREATE TABLE `uuid_generated` (<br>`timestamp` decimal(18,7) unsigned NOT NULL,<br>`mac` bigint(20) unsigned NOT NULL,<br>`temp_uniq` binary(2) NOT NULL,<br>PRIMARY KEY (`timestamp`,`mac`,`temp_uniq`)<br>) ENGINE=InnoDB DEFAULT CHARSET=latin1;<br><br>CREATE TABLE `uuid_generated_char` (<br>`timestamp` decimal(18,7) unsigned NOT NULL,<br>`mac` bigint(20) unsigned NOT NULL,<br>`temp_uniq` binary(2) NOT NULL,<br>`uuid` char(38) DEFAULT NULL,<br>PRIMARY KEY (`timestamp`,`mac`,`temp_uniq`)<br>) ENGINE=InnoDB DEFAULT CHARSET=latin1;<br><br>CREATE TABLE `uuid_generated_char_plus` (<br>`timestamp` decimal(18,7) unsigned NOT NULL,<br>`mac` bigint(20) unsigned NOT NULL,<br>`temp_uniq` binary(2) NOT NULL,<br>`uuid` char(38) DEFAULT NULL,<br>`hex_timestamp` char(40) GENERATED ALWAYS AS (conv(((`timestamp` * 10000000) + (((141427 * 24) * 60) * 600000000)),10,16)) VIRTUAL,<br>`uuid_generated` char(38) GENERATED ALWAYS AS (concat(right(`hex_timestamp`,8),'-',substr(`hex_timestamp`,4,4),'-1',left(`hex_timestamp`,3),'-',convert(hex(`temp_uniq`) using utf8),'-',lpad(conv(`mac`,10,16),12,'0'))) VIRTUAL,<br>PRIMARY KEY (`timestamp`,`mac`,`temp_uniq`)<br>) ENGINE=InnoDB DEFAULT CHARSET=latin1;<br> |
And this is the trigger:
|
1 |
DROP TRIGGER IF EXISTS ins_generated_uuid;<br>delimiter ;;<br>CREATE TRIGGER ins_uuid_generated BEFORE INSERT ON uuid_generated<br>FOR EACH ROW<br>begin<br>set @hex_timestamp = concat(substring(NEW.uuid, 16, 3), substring(NEW.uuid, 10, 4), substring(NEW.uuid, 1, 8));<br>set NEW.timestamp = concat(conv(@hex_timestamp,16,10)div 10000000 - (141427 * 24 * 60 * 60),'.',right(conv(@hex_timestamp,16,10),7));<br>set NEW.mac = conv(right(NEW.uuid,12),16,10);<br>set NEW.temp_uniq = unhex(substring(NEW.uuid,20,4));<br>end ;;<br>delimiter ; |
Decomposing the UUID is an alternative to storing them in order, but it won’t speed up inserts. It is simpler to execute queries over a range of dates, and look at the row for a particular device, as you will be able to use the MAC (it is recommended to add an index for it). Generated columns give you the possibility to build the UUID back in just one string.