在 MySQL 中存储 UUID v4
Posted
技术标签:
【中文标题】在 MySQL 中存储 UUID v4【英文标题】:Store UUID v4 in MySQL 【发布时间】:2017-08-20 17:45:30 【问题描述】:我正在使用 php 生成 UUID,根据找到的函数 here
现在我想将它存储在 mysql 数据库中。存储 UUID v4 的最佳/最有效的 MySQL 字段格式是什么?
我目前有 varchar(256),但我很确定这比必要的要大得多。我找到了很多差不多的答案,但他们通常对他们所指的 UUID 的形式模棱两可,所以我要求提供具体的格式。
【问题讨论】:
如果您只需要某种随机标记,则根本不需要 uuid。 varchar256 不需要 256 个字节,所以“太大”可能没什么大不了的。 你不需要 uuid-anything。只是随机字节。您可以将它们存储为十六进制或其他格式。 php.net/manual/en/function.random-bytes.php 获取 16 个随机字节。存储为十六进制。而已。登录完成后不要忘记从数据库中折腾。哦,不要像答案所暗示的那样作为获取请求传递,因为那太疯狂了。 MySQL 有函数UUID()
所以你不需要 PHP 来生成它。您可以删除破折号并将十六进制数字保存为binary(16)
。如果您通过触发器进行操作,则为SELECT UNHEX(REPLACE(UUID(), '-', ''));
,如果您需要索引,请设置为unique
,获利。
@N.B.那是一个UUID1。 random_bytes
来自 OS CSPRNG,这就是您在这种情况下想要的。
【参考方案1】:
如果您想要精确匹配,请将其存储为 VARCHAR(36)
,或者将其存储为 VARCHAR(255)
,无论如何都将使用相同的存储成本。没有理由在这里大惊小怪。
请记住,VARCHAR
字段是可变长度,因此存储成本与其中实际包含多少数据成正比,而不是与其中可能包含多少数据成正比。
将其存储为BINARY
非常烦人,这些值无法打印,并且在运行查询时可能显示为垃圾。很少有理由使用文字二进制表示。人类可读的值可以复制粘贴并轻松使用。
其他一些平台,如 Postgres,有一个适当的 UUID 列,它以更紧凑的格式在内部存储它,但将其显示为人类可读的,因此您可以充分利用这两种方法。
【讨论】:
考虑使用 binary(16) 来节省存储空间...... MySQL 提供了一些函数来使存储 UUID(作为二进制文件)变得非常简单,因此如果您需要做的就是更新查询,那就没有任何借口了。 . UUID_TO_BIN BIN_TO_UUID IS_UUID 例如mysqltutorial.org/mysql-uuid 这让我很困惑,这怎么可能是公认的答案。 MySQL 仅为 VARCHAR 列的前 4 个字节创建索引,UUID 中的 ID 表明它将用于标识(因此也用于搜索)。这个答案是导致大规模性能灾难的秘诀。存储它的正确方法是 BINARY(16) 甚至更好 - 使用支持 UUID 的足够现代的数据库。 使用 BINARY(16) 与 Varchar 相比,搜索会更快吗? @vstoyanov 性能/存储成本在几乎所有情况下都可以忽略,如果它只是一个未索引的数据字段。如果它是一个成为更多问题的索引字段并且如果它被用作主键,哦,男孩,这将是一个显着的差异,尤其是对于写入,请参阅percona.com/blog/2019/11/22/… 所以,这取决于并且 OP 没有指定哪个据我所知 @tadman 喜欢mysqlserverteam.com/mysql-8-0-uuid-support 这里的答案,这表明他们低估了对此的需求......“创建新的数据类型需要我们做大量工作,我们希望看到反馈我们继续我们刚刚介绍的功能,如果仍然强烈需要 UUID 数据类型,我们会在未来的版本中考虑它。”.. 但也许有一天他们会考虑它^^【参考方案2】:如果每行总是有一个 UUID,则可以将其存储为 CHAR(36)
,并在 VARCHAR(36)
上每行节省 1 个字节。
uuid CHAR(36) CHARACTER SET ascii
与 CHAR 相比,VARCHAR 值存储为 1 字节或 2 字节 长度前缀加数据。长度前缀表示的数量 值中的字节。如果值不需要,则列使用一个长度字节 超过 255 个字节,如果值可能需要超过 2 个长度字节 255 字节。 https://dev.mysql.com/doc/refman/5.7/en/char.html
尽管使用CHAR
时要小心,但即使该字段为空,它也会始终使用定义的完整长度。另外,请确保使用 ASCII 作为字符集,否则CHAR
会为最坏的情况做计划(即utf8
中每个字符 3 个字节,utf8mb4
中每个字符 4 个字节)
[...] MySQL 必须为 CHAR 中的每个字符保留四个字节 CHARACTER SET utf8mb4 列,因为这是可能的最大值 长度。例如,MySQL 必须为 CHAR(10) 保留 40 个字节 字符集 utf8mb4 列。 https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
【讨论】:
【参考方案3】:问题是关于在 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))
【讨论】:
内置的 MySQL UUID 函数不是创建 UUID v1,而不是 v4 吗?UUID_TO_BIN/BIN_TO_UUID
适用于 v4 UUID(完全不依赖于版本)。 UUID()
生成“小于”v1 UUID:dev.mysql.com/doc/refman/8.0/en/…
我会注意到使用 uuid v4 作为主键是一个非常糟糕的主意!这个函数所做的转换实际上变得毫无意义。 UUID v4 的随机性会影响数据库的性能。
@BradenRockwellNapier 本文声称它可以比整数键更快 - qcode.in/ready-to-use-uuid-in-your-next-laravel-app
@digout - 我只在那里看了几秒钟,但看起来它的 uuidv1 在那里使用会很棒。虽然可能是错的。对于 Sequelize 用户,我还提供了这个,它可以正确格式化并允许使用 Sequelize github.com/odo-network/sequelize-binary-uuid【参考方案4】:
最高效的肯定是BINARY(16)
,存储人类可读的字符会占用两倍以上的存储空间,这意味着更大的索引和更慢的查找。如果您的数据足够小以至于将它们存储为文本不会影响性能,那么您可能不需要 UUID 来代替无聊的整数键。存储 raw 并不像其他人建议的那样痛苦,因为任何像样的数据库管理工具都会将八位字节显示/转储为十六进制,而不是“文本”的字面字节。您不需要在数据库中手动查找 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;
【讨论】:
【参考方案5】:最节省空间的是BINARY(16)
或两个BIGINT UNSIGNED
。
前者可能会让您头疼,因为手动查询不会(以直接的方式)为您提供可读/可复制的值。 后者可能会让您头疼,因为必须在一个值和两列之间进行映射。
如果这是一个主键,我绝对不会在它上面浪费任何空间,因为它也成为每个二级索引的一部分。换句话说,我会选择其中一种。
为了性能,随机 UUID 的随机性(即 UUID v4,它是随机的)会严重损害。这适用于 UUID 是您的主键或者您对其进行大量范围查询的情况。您对主索引的插入将无处不在,而不是全部(或接近)末尾。您的数据会丢失时间局部性,这在各种情况下都是有用的属性。
我的主要改进是使用类似于 UUID v1 的东西,它使用时间戳作为其数据的一部分,并确保时间戳位于最高位。例如,UUID 可能是这样组成的:
Timestamp | Machine Identifier | Counter
这样,我们得到一个类似于自增值的局部性。
【讨论】:
在 C# 中,对于具有增量属性的 UUID 替代方案(但大多数 UUID 的属性仍然完好无损),您可以使用 DistributedId。这些作为数据库键非常有效。事实上,它们甚至可以存储为DECIMAL(28, 0)
或 CHAR(16)
(确保使用带有二进制排序规则的 ASCII 字符集)。【参考方案6】:
如果您使用 binary(16) 数据类型,这可能会很有用:
INSERT INTO table (UUID) VALUES
(UNHEX(REPLACE(UUID(), "-","")))
【讨论】:
【参考方案7】:这在 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'
【讨论】:
MySQLUUID()
函数不生成 UUID v4,因此默认情况下不起作用。虽然存储看起来不错【参考方案8】:
我刚刚发现一篇关于这些主题更深入的好文章:https://www.xaprb.com/blog/2009/02/12/5-ways-to-make-hexadecimal-identifiers-perform-better-on-mysql/
它涵盖了值的存储,在此页面上的不同答案中已经表达了相同的选项:
一:注意字符集 二:使用固定长度、不可为空的值 三:将其设为二进制但也增加了一些关于索引的有趣见解:
四:使用前缀索引在许多但不是所有情况下,您不需要索引 价值。我通常发现前 8 到 10 个字符是 独特。如果是二级索引,这通常就足够了。这 这种方法的美妙之处在于您可以将其应用于现有的 应用程序无需将列修改为 BINARY 或 其他任何东西——这是一个仅索引的更改,不需要 应用程序或要更改的查询。
请注意,文章并没有告诉您如何创建这样的“前缀”索引。查看Column Indexes 的 MySQL 文档,我们发现:
五:构建哈希索引[...] 您可以创建一个仅使用前 N 个字符的索引 柱子。以这种方式仅索引列值的前缀可以使 索引文件小得多。当您索引 BLOB 或 TEXT 列时,您 必须为索引指定前缀长度。例如:
CREATE TABLE test (blob_col BLOB, INDEX(blob_col(10)));
[...] 中的前缀长度 CREATE TABLE、ALTER TABLE 和 CREATE INDEX 语句被解释 作为非二进制字符串类型的字符数(CHAR、VARCHAR、 TEXT)和二进制字符串类型(BINARY、VARBINARY、 BLOB)。
您可以做的是生成值的校验和并为其编制索引。 没错,一个散列的散列。在大多数情况下,CRC32() 工作得很好 好吧(如果没有,您可以使用 64 位哈希函数)。创建另一个 柱子。 [...] CRC 列不能保证是唯一的,所以你 在 WHERE 子句中需要这两个条件,否则这种技术将不起作用。 哈希冲突发生得很快;您可能会与 大约 100k 个值,这比你想象的要快得多——不要 假设一个 32 位散列意味着你可以在你的 发生碰撞之前的表。
【讨论】:
【参考方案9】:这是一篇相当老的帖子,但仍然相关,并且经常出现在搜索结果中,所以我将添加我的答案。由于您已经必须在查询中使用触发器或您自己对 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//
【讨论】:
以上是关于在 MySQL 中存储 UUID v4的主要内容,如果未能解决你的问题,请参考以下文章
将 Java UUID 存储在二进制数据库列中,Hibernate/JPA 与原始 JDBC