在 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'

【讨论】:

MySQL UUID() 函数不生成 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

在核心数据中存储 UUID

vue中使用唯一标识uuid——uuid.v1()-时间戳uuid.v4()-随机数

生成 v4 UUID 的 PHP 函数

在 Cloud Spanner 中存储 UUID

使用中间件将 UUID 存储在会话中