将 UUID v4 存储在 MySQL
Store UUID v4 in MySQL
我根据找到的函数 here
使用 PHP 生成 UUID
现在我想将其存储在 MySQL 数据库中。什么是 best/most 有效的 MySQL 存储 UUID v4 的字段格式?
我目前有 varchar(256),但我很确定它比需要的大得多。我找到了很多差不多的答案,但它们通常对所指的 UUID 形式含糊不清,因此我要求提供具体格式。
如果您想要完全匹配,请将其存储为 VARCHAR(36)
,或者存储成本相同的 VARCHAR(255)
。没有理由在这里对字节大惊小怪。
记住 VARCHAR
字段是 可变长度 ,因此存储成本与其中实际的数据量成正比,而不是与其中可能包含的数据量成正比。
将其存储为 BINARY
非常烦人,这些值不可打印,并且在 运行 查询时可能显示为垃圾。很少有理由使用文字二进制表示。可以复制粘贴人类可读的值,并轻松使用。
一些其他平台,如 Postgres,有一个适当的 UUID 列,它以更紧凑的格式在内部存储它,但将其显示为人类可读的,因此您可以充分利用这两种方法。
如果每一行总是有一个 UUID,则可以将其存储为 CHAR(36)
并在 VARCHAR(36)
上每行保存 1 个字节。
uuid CHAR(36) CHARACTER SET ascii
In contrast to CHAR, VARCHAR values are stored as a 1-byte or 2-byte
length prefix plus data. The length prefix indicates the number of
bytes in the value. A column uses one length byte if values require no
more than 255 bytes, two length bytes if values may require more than
255 bytes.
https://dev.mysql.com/doc/refman/5.7/en/char.html
尽管使用 CHAR
时要小心,即使该字段为空,它也会始终占用定义的完整长度。另外,确保使用 ASCII 作为字符集,因为 CHAR
会为最坏的情况做计划(即 utf8
中每个字符 3 个字节,utf8mb4
中每个字符 4 个字节)
[...] MySQL must reserve four bytes for each character in a CHAR
CHARACTER SET utf8mb4 column because that is the maximum possible
length. For example, MySQL must reserve 40 bytes for a CHAR(10)
CHARACTER SET utf8mb4 column.
https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
问题是关于在 MySQL 中存储一个 UUID。
从 mySQL 的 8.0 版开始,您可以使用 binary(16)
通过 UUID_TO_BIN/BIN_TO_UUID
函数进行自动转换:
https://mysqlserverteam.com/mysql-8-0-uuid-support/
请注意 mySQL 也有一种快速生成 UUID 作为主键的方法:
INSERT INTO t VALUES(UUID_TO_BIN(UUID(), true))
效率最高的肯定是 BINARY(16)
,存储人类可读的字符使用两倍以上的存储空间 space,这意味着更大的索引和更慢的查找。如果您的数据足够小以至于将它们存储为文本不会影响性能,那么您可能不需要 UUID 而不是无聊的整数键。存储原始文件并不像其他人建议的那样痛苦,因为任何体面的数据库管理工具都会 display/dump 八位字节为十六进制,而不是“文本”的字面字节。您不需要在数据库中手动查找 UUID;如果必须,HEX()
和 x'deadbeef01'
文字是您的朋友。在你的应用程序中编写一个函数——就像你引用的那个——来为你处理这个是微不足道的。您甚至可以在数据库中以虚拟列和存储过程的形式执行此操作,这样应用程序就不会为原始数据操心。
我会将 UUID 生成逻辑与显示逻辑分开,以确保现有数据永远不会更改并且可以检测到错误:
function guidv4($prettify = false)
{
static $native = function_exists('random_bytes');
$data = $native ? random_bytes(16) : openssl_random_pseudo_bytes(16);
$data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
$data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
if ($prettify) {
return guid_pretty($data);
}
return $data;
}
function guid_pretty($data)
{
return strlen($data) == 16 ?
vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)) :
false;
}
function guid_ugly($data)
{
$data = preg_replace('/[^[:xdigit:]]+/', '', $data);
return strlen($data) == 32 ? hex2bin($data) : false;
}
编辑:如果读数据库时只需要漂亮的列,像下面这样的语句就足够了:
ALTER TABLE test ADD uuid_pretty CHAR(36) GENERATED ALWAYS AS (CONCAT_WS('-', LEFT(HEX(uuid_ugly), 8), SUBSTR(HEX(uuid_ugly), 9, 4), SUBSTR(HEX(uuid_ugly), 13, 4), SUBSTR(HEX(uuid_ugly), 17, 4), RIGHT(HEX(uuid_ugly), 12))) VIRTUAL;
最 space 有效的是 BINARY(16)
或两个 BIGINT UNSIGNED
。
前者可能会让您头疼,因为手动查询不会(以直接的方式)为您提供 readable/copyable 值。
后者可能会让您头疼,因为必须在一个值和两列之间进行映射。
如果这是主键,我绝对不会在上面浪费任何 space,因为它也会成为每个二级索引的一部分。换句话说,我会选择其中一种类型。
对于性能,随机 UUID(即 UUID v4,它是随机的)的随机性会严重损害。当 UUID 是您的主键或者您对其进行大量范围查询时,这适用。您对主索引的插入将无处不在,而不是全部(或接近)末尾。您的数据失去了时间局部性,这在各种情况下都很有用 属性。
我的主要改进是使用类似于 UUID v1 的东西,它使用时间戳作为其数据的一部分,并确保时间戳位于最高位。例如,UUID 可能是这样组成的:
Timestamp | Machine Identifier | Counter
这样,我们就得到了一个类似于自增值的局部性。
我刚刚发现一篇关于这些主题的更深入的好文章:https://www.xaprb.com/blog/2009/02/12/5-ways-to-make-hexadecimal-identifiers-perform-better-on-mysql/
它涵盖了值的存储,在本页的不同答案中已经表达了相同的选项:
- 一:注意字符集
- 二:使用定长、不可空值
- 三:二进制化
但也增加了一些关于索引的有趣见解:
- 四:使用前缀索引
In many but not all cases, you don’t need to index the full length of
the value. I usually find that the first 8 to 10 characters are
unique. If it’s a secondary index, this is generally good enough. The
beauty of this approach is that you can apply it to existing
applications without any need to modify the column to BINARY or
anything else—it’s an indexing-only change and doesn’t require the
application or the queries to change.
请注意,本文并未告诉您如何创建这样的 "prefix" 索引。查看 Column Indexes 的 MySQL 文档,我们发现:
[...] you can create an index that uses only the first N characters of the
column. Indexing only a prefix of column values in this way can make
the index file much smaller. When you index a BLOB or TEXT column, you
must specify a prefix length for the index. For example:
CREATE TABLE test (blob_col BLOB, INDEX(blob_col(10)));
[...] the prefix length in
CREATE TABLE, ALTER TABLE, and CREATE INDEX statements is interpreted
as number of characters for nonbinary string types (CHAR, VARCHAR,
TEXT) and number of bytes for binary string types (BINARY, VARBINARY,
BLOB).
- 五:建立哈希索引
What you can do is generate a checksum of the values and index that.
That’s right, a hash-of-a-hash. For most cases, CRC32() works pretty
well (if not, you can use a 64-bit hash function). Create another
column. [...] The CRC column isn’t guaranteed to be unique, so you
need both criteria in the WHERE clause or this technique won’t work.
Hash collisions happen quickly; you will probably get a collision with
about 100k values, which is much sooner than you might think—don’t
assume that a 32-bit hash means you can put 4 billion rows in your
table before you get a collision.
如果您使用 binary(16) 数据类型,这可能会有用:
INSERT INTO table (UUID) VALUES
(UNHEX(REPLACE(UUID(), "-","")))
在 MySQL 8.0.26
中,这对我来说就像一个魅力
create table t (
uuid BINARY(16) default (UUID_TO_BIN(UUID())),
)
查询时可以使用
select BIN_TO_UUID(uuid) uuid from t;
结果是:
# uuid
'8c45583a-0e1f-11ec-804d-005056219395'
这是一个相当古老的 post 但仍然相关并且经常出现在搜索结果中,所以我将把我的答案添加到组合中。由于您已经必须在查询中使用触发器或自己调用 UUID(),这里有一对函数,我用它们将 UUID 作为文本保存在数据库中以便于在数据库中查看,但减少了 36 的占用空间到 24 个字符。 (节省 33%)
delimiter //
DROP FUNCTION IF EXISTS `base64_uuid`//
DROP FUNCTION IF EXISTS `uuid_from_base64`//
CREATE definer='root'@'localhost' FUNCTION base64_uuid() RETURNS varchar(24)
DETERMINISTIC
BEGIN
/* converting INTO base 64 is easy, just turn the uuid into binary and base64 encode */
return to_base64(unhex(replace(uuid(),'-','')));
END//
CREATE definer='root'@'localhost' FUNCTION uuid_from_base64(base64_uuid varchar(24)) RETURNS varchar(36)
DETERMINISTIC
BEGIN
/* Getting the uuid back from the base 64 version requires a little more work as we need to put the dashes back */
set @hex = hex(from_base64(base64_uuid));
return lower(concat(substring(@hex,1,8),'-',substring(@hex,9,4),'-',substring(@hex,13,4),'-',substring(@hex,17,4),'-',substring(@hex,-12)));
END//
我根据找到的函数 here
使用 PHP 生成 UUID现在我想将其存储在 MySQL 数据库中。什么是 best/most 有效的 MySQL 存储 UUID v4 的字段格式?
我目前有 varchar(256),但我很确定它比需要的大得多。我找到了很多差不多的答案,但它们通常对所指的 UUID 形式含糊不清,因此我要求提供具体格式。
如果您想要完全匹配,请将其存储为 VARCHAR(36)
,或者存储成本相同的 VARCHAR(255)
。没有理由在这里对字节大惊小怪。
记住 VARCHAR
字段是 可变长度 ,因此存储成本与其中实际的数据量成正比,而不是与其中可能包含的数据量成正比。
将其存储为 BINARY
非常烦人,这些值不可打印,并且在 运行 查询时可能显示为垃圾。很少有理由使用文字二进制表示。可以复制粘贴人类可读的值,并轻松使用。
一些其他平台,如 Postgres,有一个适当的 UUID 列,它以更紧凑的格式在内部存储它,但将其显示为人类可读的,因此您可以充分利用这两种方法。
如果每一行总是有一个 UUID,则可以将其存储为 CHAR(36)
并在 VARCHAR(36)
上每行保存 1 个字节。
uuid CHAR(36) CHARACTER SET ascii
In contrast to CHAR, VARCHAR values are stored as a 1-byte or 2-byte length prefix plus data. The length prefix indicates the number of bytes in the value. A column uses one length byte if values require no more than 255 bytes, two length bytes if values may require more than 255 bytes. https://dev.mysql.com/doc/refman/5.7/en/char.html
尽管使用 CHAR
时要小心,即使该字段为空,它也会始终占用定义的完整长度。另外,确保使用 ASCII 作为字符集,因为 CHAR
会为最坏的情况做计划(即 utf8
中每个字符 3 个字节,utf8mb4
中每个字符 4 个字节)
[...] MySQL must reserve four bytes for each character in a CHAR CHARACTER SET utf8mb4 column because that is the maximum possible length. For example, MySQL must reserve 40 bytes for a CHAR(10) CHARACTER SET utf8mb4 column. https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
问题是关于在 MySQL 中存储一个 UUID。
从 mySQL 的 8.0 版开始,您可以使用 binary(16)
通过 UUID_TO_BIN/BIN_TO_UUID
函数进行自动转换:
https://mysqlserverteam.com/mysql-8-0-uuid-support/
请注意 mySQL 也有一种快速生成 UUID 作为主键的方法:
INSERT INTO t VALUES(UUID_TO_BIN(UUID(), true))
效率最高的肯定是 BINARY(16)
,存储人类可读的字符使用两倍以上的存储空间 space,这意味着更大的索引和更慢的查找。如果您的数据足够小以至于将它们存储为文本不会影响性能,那么您可能不需要 UUID 而不是无聊的整数键。存储原始文件并不像其他人建议的那样痛苦,因为任何体面的数据库管理工具都会 display/dump 八位字节为十六进制,而不是“文本”的字面字节。您不需要在数据库中手动查找 UUID;如果必须,HEX()
和 x'deadbeef01'
文字是您的朋友。在你的应用程序中编写一个函数——就像你引用的那个——来为你处理这个是微不足道的。您甚至可以在数据库中以虚拟列和存储过程的形式执行此操作,这样应用程序就不会为原始数据操心。
我会将 UUID 生成逻辑与显示逻辑分开,以确保现有数据永远不会更改并且可以检测到错误:
function guidv4($prettify = false)
{
static $native = function_exists('random_bytes');
$data = $native ? random_bytes(16) : openssl_random_pseudo_bytes(16);
$data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
$data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
if ($prettify) {
return guid_pretty($data);
}
return $data;
}
function guid_pretty($data)
{
return strlen($data) == 16 ?
vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)) :
false;
}
function guid_ugly($data)
{
$data = preg_replace('/[^[:xdigit:]]+/', '', $data);
return strlen($data) == 32 ? hex2bin($data) : false;
}
编辑:如果读数据库时只需要漂亮的列,像下面这样的语句就足够了:
ALTER TABLE test ADD uuid_pretty CHAR(36) GENERATED ALWAYS AS (CONCAT_WS('-', LEFT(HEX(uuid_ugly), 8), SUBSTR(HEX(uuid_ugly), 9, 4), SUBSTR(HEX(uuid_ugly), 13, 4), SUBSTR(HEX(uuid_ugly), 17, 4), RIGHT(HEX(uuid_ugly), 12))) VIRTUAL;
最 space 有效的是 BINARY(16)
或两个 BIGINT UNSIGNED
。
前者可能会让您头疼,因为手动查询不会(以直接的方式)为您提供 readable/copyable 值。 后者可能会让您头疼,因为必须在一个值和两列之间进行映射。
如果这是主键,我绝对不会在上面浪费任何 space,因为它也会成为每个二级索引的一部分。换句话说,我会选择其中一种类型。
对于性能,随机 UUID(即 UUID v4,它是随机的)的随机性会严重损害。当 UUID 是您的主键或者您对其进行大量范围查询时,这适用。您对主索引的插入将无处不在,而不是全部(或接近)末尾。您的数据失去了时间局部性,这在各种情况下都很有用 属性。
我的主要改进是使用类似于 UUID v1 的东西,它使用时间戳作为其数据的一部分,并确保时间戳位于最高位。例如,UUID 可能是这样组成的:
Timestamp | Machine Identifier | Counter
这样,我们就得到了一个类似于自增值的局部性。
我刚刚发现一篇关于这些主题的更深入的好文章:https://www.xaprb.com/blog/2009/02/12/5-ways-to-make-hexadecimal-identifiers-perform-better-on-mysql/
它涵盖了值的存储,在本页的不同答案中已经表达了相同的选项:
- 一:注意字符集
- 二:使用定长、不可空值
- 三:二进制化
但也增加了一些关于索引的有趣见解:
- 四:使用前缀索引
In many but not all cases, you don’t need to index the full length of the value. I usually find that the first 8 to 10 characters are unique. If it’s a secondary index, this is generally good enough. The beauty of this approach is that you can apply it to existing applications without any need to modify the column to BINARY or anything else—it’s an indexing-only change and doesn’t require the application or the queries to change.
请注意,本文并未告诉您如何创建这样的 "prefix" 索引。查看 Column Indexes 的 MySQL 文档,我们发现:
[...] you can create an index that uses only the first N characters of the column. Indexing only a prefix of column values in this way can make the index file much smaller. When you index a BLOB or TEXT column, you must specify a prefix length for the index. For example:
CREATE TABLE test (blob_col BLOB, INDEX(blob_col(10)));
[...] the prefix length in CREATE TABLE, ALTER TABLE, and CREATE INDEX statements is interpreted as number of characters for nonbinary string types (CHAR, VARCHAR, TEXT) and number of bytes for binary string types (BINARY, VARBINARY, BLOB).
- 五:建立哈希索引
What you can do is generate a checksum of the values and index that. That’s right, a hash-of-a-hash. For most cases, CRC32() works pretty well (if not, you can use a 64-bit hash function). Create another column. [...] The CRC column isn’t guaranteed to be unique, so you need both criteria in the WHERE clause or this technique won’t work. Hash collisions happen quickly; you will probably get a collision with about 100k values, which is much sooner than you might think—don’t assume that a 32-bit hash means you can put 4 billion rows in your table before you get a collision.
如果您使用 binary(16) 数据类型,这可能会有用:
INSERT INTO table (UUID) VALUES
(UNHEX(REPLACE(UUID(), "-","")))
在 MySQL 8.0.26
中,这对我来说就像一个魅力create table t (
uuid BINARY(16) default (UUID_TO_BIN(UUID())),
)
查询时可以使用
select BIN_TO_UUID(uuid) uuid from t;
结果是:
# uuid
'8c45583a-0e1f-11ec-804d-005056219395'
这是一个相当古老的 post 但仍然相关并且经常出现在搜索结果中,所以我将把我的答案添加到组合中。由于您已经必须在查询中使用触发器或自己调用 UUID(),这里有一对函数,我用它们将 UUID 作为文本保存在数据库中以便于在数据库中查看,但减少了 36 的占用空间到 24 个字符。 (节省 33%)
delimiter //
DROP FUNCTION IF EXISTS `base64_uuid`//
DROP FUNCTION IF EXISTS `uuid_from_base64`//
CREATE definer='root'@'localhost' FUNCTION base64_uuid() RETURNS varchar(24)
DETERMINISTIC
BEGIN
/* converting INTO base 64 is easy, just turn the uuid into binary and base64 encode */
return to_base64(unhex(replace(uuid(),'-','')));
END//
CREATE definer='root'@'localhost' FUNCTION uuid_from_base64(base64_uuid varchar(24)) RETURNS varchar(36)
DETERMINISTIC
BEGIN
/* Getting the uuid back from the base 64 version requires a little more work as we need to put the dashes back */
set @hex = hex(from_base64(base64_uuid));
return lower(concat(substring(@hex,1,8),'-',substring(@hex,9,4),'-',substring(@hex,13,4),'-',substring(@hex,17,4),'-',substring(@hex,-12)));
END//