MySQL中UUID的性能表现?

109
我们正在考虑使用UUID值作为MySQL数据库的主键。 插入的数据由数十个,数百个甚至数千台远程计算机生成,并以每秒100-40,000次的速度插入,我们永远不会进行任何更新。
数据库本身通常会在开始清除数据之前达到约50M条记录,因此既不是庞大的数据库,也不是微小的数据库。 我们还计划在InnoDB上运行,尽管如果有更好的引擎适合我们所做的事情,我们也可以改变这一点。
我们准备使用Java的Type 4 UUID,但在测试中发现了一些奇怪的行为。 首先,我们存储为varchar(36),我现在意识到我们最好使用binary(16)-虽然我不确定会好多少。
更重要的问题是:当我们有5000万条记录时,这些随机数据会严重损坏索引吗? 如果我们使用例如type-1 UUID,其中左侧位戳记,则是否更好? 或者也许我们应该放弃UUID,考虑自动增量主键?
我正在寻找关于将不同类型的UUID存储为MySQL的索引/主键时性能的普遍想法/提示。 谢谢!

2
一个重要的细节缺失了:主键由日志服务器生成还是由客户端机器自己生成? - user3850
1
希望它们是由插入数据的10-1000个客户端生成的。 - Patrick Lightbody
你的场景中需要通用唯一性的地方在哪里?我的建议是坚持使用auto_increment,并使用一个单独的字段来描述发送数据的远程计算机。这里没有必要重新发明轮子。 - Theodore Zographos
UUIDs中性能陷阱的更多讨论。 - Rick James
https://dev59.com/7F0a5IYBdhLWcg3wVHTU#30462400 - Eugen Konkov
11个回答

93
在我的工作中,我们使用UUID作为主键。根据我的经验,我可以告诉你,不要将它们用作主键(顺便提一下,这是针对SQL Server的)。
当记录少于1000条时,这样做还可以接受,但是当记录达到百万级别时,这就是你能做的最糟糕的事情了。为什么呢?因为UUID不是按顺序生成的,所以每次插入新记录时,MSSQL都需要查找正确的页面来插入记录,然后再插入记录。这个问题的一个非常丑陋的后果是,页面最终会变得大小不一,并且会出现碎片化,因此我们必须定期进行碎片整理。
当你使用自增主键时,MSSQL总是会去最后一页,这样你就会得到大小相等的页面(理论上),所以选择这些记录的性能要好得多(也因为插入操作不会长时间阻塞表/页面)。
然而,使用UUID作为主键的一个重要优势是,在我们有数据库集群时,合并时不会发生冲突。
我建议采用以下模型:
1. PK INT Identity 2. 自动生成的UUID作为附加列。
这样,合并过程就变得可能了(UUID将成为您的真实键,而PK只是一个临时的东西,可以提供良好的性能)。
注意:最好的解决方案是使用NEWSEQUENTIALID(就像我在评论中说的那样),但对于遗留应用程序来说,没有太多时间进行重构(甚至更糟糕的是,无法控制所有插入操作),这是不可能的。 但确实从2017年开始,我会说在这里最好的解决方案是使用NEWSEQUENTIALID或者在NHibernate中使用Guid.Comb。

3
我认为有一件事情需要考虑,那就是这种方法在父子关系中可能效果不佳。在这种情况下,我认为你需要在子表中添加:parent-pk,parent-guid。否则,你可能会失去数据库之间的引用关系。尽管我还没有仔细思考或做过任何示例,但这可能是必要的。 - Kat Lim Ruiz
4
在 SQL Server 中,您可以使用 NEWSEQUENTIALID() 函数(http://technet.microsoft.com/en-us/library/ms189786.aspx) 来避免性能问题。 - giammin
确实,但NEWSEQUENTIALID只能作为默认值使用。因此,您需要围绕此设计整个数据访问层(DAL),这对于新项目来说是可以的,但对于大型遗留项目来说并不容易。 - Kat Lim Ruiz
@KatLimRuiz 天才。这是一个很好的妥协。 - jmgunn87
注意:NEWSEQUENTIALID 是特定于 SQL Server 的。问题明确要求关于 MySQL。 - Miles Elam
显示剩余5条评论

40

UUID是通用唯一标识符,重点是其中的“通用”属性。

你是否真的需要这些ID具备全球唯一性?如果是的话,那么UUID可能是您唯一的选择。

我强烈建议如果您使用UUID,则将它们存储为数字而非字符串。如果您有5000万以上的记录,则存储空间的节省将提高您的性能(尽管我无法确定提高多少)。

如果您的ID不需要具备全球唯一性,那么我认为您最好只是使用自动增量,因为它可以保证在表内ID是唯一的(由于值会每次递增)。


2
有趣的观点;这将并行生成密钥。我相信这将提高密钥生成的性能。 然而,如果您使用VARCHAR存储UUID,则选择INSERT性能而不是SELECT性能。您绝对应该选择VARBINARY进行存储以确保SELECT性能。额外的步骤可能会影响INSERT性能,但您将通过SELECT性能的改进得到回报。 - Dancrumb
12
我们最终对真实数据进行了基准测试,没有键的GUID非常快,有键的GUID非常慢(即使存储为二进制),而带有AUTO_COMPLETE的整数最快。我认为在我们的情况下,我们确实忽视了大局,因为与GUID的随机性导致BTREE非常糟糕相比,序列生成似乎微不足道,而存储更多数据的成本也很高。 - Patrick Lightbody
1
将数字存储为数字意味着以二进制格式存储吗?但是二进制格式对人类来说是不可读的。如果使用大字节的uuid主键,它会变慢吗?如果是这样,那么我可以使用另一列存储自动递增的uuid。然后,性能就不会受到影响了。我是正确的吗? - Chamnap
4
严格来说,UUID是全球唯一的,意味着它不会在世界上任何其他地方出现。仅当您公开共享数据时才需要它。至于将UUID存储为数字,我指的不是二进制格式,而是作为128位数字,而不是288位字符串。例如,ASCII中的单词'hello'是68 65 6C 6C 6F,即数字448,378,203,247。存储字符串'68656C6C6F'需要10个字节。数字448,378,203,247仅需要5个字节。总之,除非您真正需要UUID中的第一个U,否则您无法比auto_increment更好。 - Dancrumb
1
@Chamnap:建议你在Stack Overflow上提问:o) - Dancrumb
显示剩余7条评论

28

需要考虑的一点是,自增长是一个接一个地生成的,不能使用并行解决方案来解决。使用UUID的争论最终归结为您想要实现的目标与您可能会牺牲的目标之间的平衡。

关于性能,简要

像上面那样的UUID长度为36个字符,包括破折号。如果你将其存储为VARCHAR(36),你将大幅降低比较性能。这是你的主键,你不希望它很慢。

在位级别上,UUID是128位,这意味着它将适合16字节,注意这不是非常易读,但它将保持存储低,并且仅比32位int大4倍或64位int大2倍。我将使用VARBINARY(16)。理论上,这可以在没有太多开销的情况下工作。

我建议阅读以下两篇文章:

我认为,在这两者之间,它们回答了您的问题。


2
实际上,在发布这个问题之前,我阅读了这两篇文章,但仍然没有得到一个好的答案。例如,它们都没有讨论类型1与类型4 UUIDS :( - Patrick Lightbody
公平起见,我稍微更新了我的答案。但是我认为这并没有提供太多额外的见解。 - Kyle Rosendo
@Patrick:你的问题涉及太多不同的主题了。 - user3850
1
9年过去了,但为了后人而言,需要注意的是,与整数ID不同,应用程序可以安全地生成UUID,从而完全消除了数据库中的生成。对UUID进行性能优化的操作(基于时间戳但修改后可以被原生排序)在任何语言中都比SQL更容易实现。幸运的是,今天几乎所有的数据库(包括MySQL)都比以前更好地处理UUID主键。 - Miles Elam

4

我倾向于避免使用UUID,因为它存储和使用起来很麻烦,但有一些优点。最主要的是它们是唯一的。

通常我通过使用双键字段解决问题并避免使用UUID。

COLLECTOR = 分配给机器的唯一编号

ID = 被COLLECTOR收集的记录(自增字段)

这为我提供了两个好处:自增字段的速度和数据被收集和组合后存储在中央位置的唯一性。同时,在浏览数据时,我也知道在哪里收集了数据,这对我的需求经常非常重要。

我曾经处理客户的其他数据集时看到许多情况,他们决定使用UUID,但仍然有一个字段用于记录数据的收集位置,这实际上是一种浪费。简单地使用两个(或更多,如果需要)字段作为您的主键真的很有帮助。

我已经看到太多使用UUID导致性能下降的情况了。它们感觉像一种欺骗……


1
这实际上是一个相当不错的想法,一旦提到就有点显而易见了。问题是,在大型连接或其他情况下使用双键会有多么昂贵。 - Shayne

3
不要为每个插入生成唯一的密钥,而是为各个服务器分配密钥块。当它们用完密钥后,可以请求一个新的密钥块。这样你就解决了每个插入都需要连接的问题。
密钥服务器维护下一个可用ID。
- 服务器1请求ID块。 - 密钥服务器返回(1,1000)
服务器1可以插入1000条记录,直到需要请求新的块 - 服务器2请求索引块。 - 密钥服务器返回(1001,2000) - 等等...
你也可以设计一个更复杂的版本,其中服务器可以请求所需密钥的数量,或将未使用的块返回给密钥服务器,后者当然需要维护已使用/未使用块的映射。

理论上是个有趣的建议。但在实践中管理起来可能会很复杂。更实际的解决方案可能就是schworak提出的答案。 - Simon East

3
我知道这个问题比较老了,但在我的研究中遇到了它。自那时以来发生了许多事情(SSD非常普及,InnoDB得到了更新等)。
在我的研究中,我发现这篇关于性能的帖子相当有趣:
声称由于GUID / UUID索引树的随机性可能会变得相当不平衡。在MariaDB KB中,我找到了另一篇文章提出的解决方案。但是,由于新的UUID_TO_BIN已经解决了这个问题。此函数仅在MySQL(测试版本8.0.18)中可用,而不在MariaDB(版本10.4.10)中可用。
TL;DR:将UUID存储为转换/优化的BINARY(16)值。

2
简短的回答是,由于索引方法与UUID故意熵在高位比特中的冲突,许多数据库存在性能问题(尤其是高INSERT量),有几种常见的解决办法:
  • 选择不介意它的不同索引类型(例如,在MSSQL上非集群)
  • 改变数据以将熵移动到较低位(例如,在MySQL上重新排序V1 UUID的字节)
  • 将UUID作为自增int主键的次要键
但是这些都是hack,而且可能很脆弱。
最好的答案,但不幸的是最慢的答案,是要求您的供应商改进其产品,以便像处理任何其他类型的主键一样处理UUIDs。他们不应该强迫您自己制定不成熟的hack来弥补他们未能解决已成为常见用例且只会继续增长的问题。

2
我会按照事务方式为每个服务器分配一个数字ID。然后,每个插入的记录将自动递增其自己的计数器。ServerID和RecordID的组合将是唯一的。ServerID字段可以索引,并且基于ServerID的未来选择性能(如果需要)可能会更好。

1

那么手工制作的UID怎么样?给成千上万台服务器分配一个ID,并将主键设置为自增、机器ID的组合键?


我已经考虑过这个问题,可能需要运行一些基准测试。即使在每台1000台机器上都有一个临时本地序列,再加上时间戳,也足够了。例如:machine_id + temp_seq + timestamp。 - Patrick Lightbody
能否拥有一个在每个时间戳时刻重置的临时序列呢?我不确定。 - MindStalker

1

由于主键是分散生成的,因此您无法使用auto_increment选项。

如果你不必隐藏远程机器的身份,可以使用Type 1 UUIDs代替UUIDs。它们更容易生成,至少不会影响数据库性能。

对于varchar(char)vs. binary也一样:它只能有所帮助。这真的很重要,改善了多少性能?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接