在 MongoDB 中使用 UUID 而不是 ObjectID

Using UUIDs instead of ObjectIDs in MongoDB

提问人:Christina 提问时间:3/6/2015 最后编辑:anothernodeChristina 更新时间:10/28/2022 访问量:100563

问:

出于性能原因,我们正在将数据库从 MySQL 迁移到 MongoDB,并考虑将什么用于 MongoDB 文档的 ID。我们正在争论使用ObjectIDs(MongoDB的默认值)还是使用UUID(这是我们迄今为止在MySQL中一直在使用的UUID)。到目前为止,我们必须支持这些选项中的任何一个的论据如下:

对象 ID:ObjectID 是 MongoDB 的默认值,我假设(尽管我不确定)这是有原因的,这意味着我希望 MongoDB 可以比 UUID 更有效地处理它们,或者有其他原因更喜欢它们。我还发现了这个 stackoverflow 答案,其中提到使用 ObjectID 可以提高索引效率,但是最好有一些指标来说明这种“效率”有多大。

UUID:我们支持使用 UUID 的基本论点(这是一个非常重要的论点)是,几乎任何数据库都以某种方式支持它们。这意味着,如果出于某种原因,我们决定从MongoDB切换到其他东西,并且我们已经有一个API,可以根据其ID从数据库中检索文档,那么对于该API的客户端来说,没有任何变化,因为ID可以继续完全相同。如果我们要使用 ObjectID,我真的不确定我们将如何将它们迁移到另一个数据库。

有没有人对这些选项中的一个是否可能比另一个更好以及为什么有任何见解?您是否曾经在 MongoDB 中使用过 UUID 而不是 ObjectID,如果是,您遇到的优点/问题是什么?

蒙戈德

评论


答:

54赞 Philipp 3/6/2015 #1

MongoDB 的字段可以具有您想要的任何值,只要您能保证它对于集合是唯一的。当您的数据已经具有自然键时,没有理由不使用它来代替自动生成的 ObjectID。_id

ObjectID 是一种合理的默认解决方案,可以安全地生成自己的唯一键(并阻止初学者尝试复制 SQL,这在分布式数据库中是一个坏主意)。AUTO INCREMENT

如果不使用 ObjectID,您还会错过另一个便利功能:ObjectID 在生成时还包括一个 unix 时间戳,并且许多驱动程序提供了提取它并将其转换为日期的功能。这有时会使单独的字段变得多余。create-date

但是,当两者都不是您关心的问题时,您可以自由地使用您的 UUID 作为字段。_id

评论

1赞 Christina 3/6/2015
谢谢,事实是我并不真正关心持有创建日期信息的 ID(我已经将其作为单独的列)。您对两者之间的性能差异有什么见解吗?
10赞 Roman Blachman 9/19/2015
嗨,克里斯蒂娜,实际上MongoDB Java Driver中有一张有趣的照片,它显示了ObjectId和UUID值 jira.mongodb.org/browse/JAVA-403 之间的插入时间。很想听听你最后采取的方法。
1赞 Molomby 8/9/2018
UUIDv1 还包括一个时间戳,精度提高了 ~6 个数量级。UUIDv1 编码的时间为 60 位(纳秒),而 ObjectID 编码的时间为 32 位(秒)。
0赞 Peter Colclough 8/11/2021
那么为什么不直接添加一个时间戳呢?使用 ObjectId ,它靠近 GUID,更安全。从中解开时间戳,以获得插入时间,我认为是次优的吗?
0赞 Philipp 8/11/2021
@PeterColclough 当然,您可以添加单独的时间戳字段。但正如我所写的,如果对象的创建和 ObjectId 的创建是相同的,那么该时间戳字段有时”可能是多余的。什么是“次优”取决于你正在优化的内容。如果要优化存储空间,则冗余数据是次优的。那么你优化的目的是什么呢?如果答案是查询速度,那么究竟是哪个查询操作的速度呢?
10赞 sws 11/2/2017 #2

考虑在每种情况下要存储的数据量。

MongoDB ObjectID 的大小为 12 字节,打包用于存储,其部分根据性能进行组织(即首先存储时间戳,这是一个逻辑排序标准)。

相反,标准 UUID 为 36 个字节,包含破折号,通常存储为字符串。此外,即使您剥离非数字字符并打算以数字方式存储,您仍然必须满足于其“索引”部分(UUID v1 中基于时间戳的部分)位于 UUID 的中间,并且不适合排序。有一些研究允许高性能的UUID存储,我甚至编写了一个Node.js库来协助其管理。

如果您打算使用 UUID,请考虑重新组织它以获得最佳索引和排序;否则,您可能会遇到性能障碍。

评论

0赞 Robin F. 4/20/2018
可能会补充一点,应该仔细考虑,因为并非在所有情况下您都不希望有可排序/可预测的东西。例如,在生成会话 ID 时,应采用 UUID v4 版本(随机)。
0赞 mjaggard 4/27/2018
分片怎么样,您可以使用非哈希 UUID 进行分片,还是会遇到与 ObjectID 相同的问题,其中新写入将全部集中在一个分片中?
3赞 Dmitry Gusarov 6/30/2018
没有理由将 UUID 存储为字符串...标准 UUID 正好是 16 个字节,即使在 mongo 中也通常存储为原始字节。没有人使用 v1 UUID,只有 v4(随机)和 v5(sha1)。
8赞 Molomby 8/9/2018
如@Dmitry所述,UUID 为 16 个字节(128 位),通常存储为字符串。MongoDB具有本机支持,并将它们存储为Binary子类型。不过,你对不幸的时间戳分块是正确的,这真的很痛苦。我希望有一个官方的 UUID 版本,它的行为更像 SQUUID。0x04
0赞 Elias 1/7/2018 #3

前段时间,当我遇到同样的问题时,我发现了这些基准。 它们基本上表明,使用 Guid 而不是 ObjectId 会导致索引性能下降。

无论如何,我建议您自定义基准测试以模仿您的特定现实生活场景,并查看数字的样子,不能 100% 依赖通用基准测试。

124赞 Molomby 8/10/2018 #4

在 Mongo 中使用 UUID 当然是可能的,并且得到了合理的支持。例如,Mongo 文档将 UUID 列为 _id 字段的常用选项之一。

考虑

  • 性能 – 正如其他答案所提到的,基准测试显示 UUID 会导致插入的性能下降。在最坏的情况下(从集合中的 10M 到 20M 文档),它们的速度大约慢了 ~2-3 倍——这是每秒插入 2,000 个 (UUID) 和 7,500 个 (ObjectID) 文档之间的差异。这是一个很大的差异,但其重要性完全取决于您的用例。你会一次批量插入数百万个文档吗?对于我构建的大多数应用程序,常见的情况是插入单个文档。同样的基准表明,对于这种使用模式,差异要小得多(6,250 -vs- 7,500;~20%)。不容小觑。但也不是惊天动地。
  • 可移植性 – 许多其他数据库平台都有很好的 UUID 支持,因此可移植性将得到提高。或者,由于 UUID 较大(更多位),因此可以将 ObjectID 重新打包为 UUID 的“形状”。这种方法不如直接可移植性好,但它确实为您提供了一种在现有 ObjectID 和 UUID 之间“映射”的方法。
  • 去中心化 – UUID 的一大卖点是它们具有普遍的独特性。这使得以分散的方式在任何地方生成它们变得可行(例如,与自动递增值相反,自动递增值需要集中的事实来源来确定“下一个”值)。当然,Mongo 对象 ID 也承认了这种好处。不同之处在于,UUID 基于 15+ 年的标准,并且(几乎?)所有平台、语言等都受支持。如果您需要在不与数据库交互的情况下,它们将非常有用。您可以创建一个包含 ID 和外键的数据集,然后在将来的某个时间点将整个图形写入数据库,而不会发生冲突。尽管 Mongo ObjectID 也可以做到这一点,但找到生成它们/使用该格式的代码通常会更加困难。

修正

与其他一些答案相反:

  • UUID 确实具有原生 Mongo 支持——您可以在 Mongo Shell 中使用 UUID() 函数,就像使用方式完全相同;将 UUID 字符串转换为等效的 BSON 对象ObjectID()
  • UUID 不是特别大 – 当使用二进制子类型编码时,它们是 128 位,而 ObjectID 为 96 位。(如果编码为字符串,它们将非常浪费,大约需要 288 位。0x04
  • UUID 可以包含时间戳 – 具体来说,UUIDv1 以 60 位的精度对时间戳进行编码,而 ObjectID 中为 32 位。在十进制中,这比精确度高出 6 个数量级以上——所以秒而不是秒。它实际上是一种不错的存储创建时间戳的方式,其准确性高于 Mongo/JS Date 对象支持,但是......
    • 内置函数仅生成 v4(随机)UUID,因此,要利用这一点,您可以依靠您的应用程序或 Mongo 驱动程序来创建 ID。UUID()
    • 与 ObjectID 不同,由于 UUID 的分块方式,时间戳不会给出自然顺序。这可能是好事,也可能是坏事,具体取决于您的用例。(新标准可能会改变这一点;请参阅下面的 2021 年更新。
    • 在您的 ID 中包含时间戳有时是个坏主意。您最终会在暴露 ID 的任何地方泄露文档的创建时间。(当然,ObjectID 也会对时间戳进行编码,因此对它们来说也部分正确。
    • 如果使用(符合规范的)v1 UUID 执行此操作,则还会对服务器 MAC 地址的一部分进行编码,该地址可能用于标识计算机。对于大多数系统来说可能不是问题,但也不理想。(新标准可能会改变这一点;请参阅下面的 2021 年更新。

结论

如果孤立地考虑 Mongo DB,则 ObjectID 是显而易见的选择。它们开箱即用,是一个完全有能力的默认值。相反,使用 UUID 确实会增加一些摩擦,无论是在处理值(需要转换为二进制类型等)时,还是在性能方面。这种轻微的不便是否值得拥有标准化的 ID 格式,实际上取决于您对可移植性和架构选择的重视程度。

您是否会在不同的数据库平台之间同步数据?将来会将数据迁移到其他平台吗?您是否需要在数据库之外、其他系统或浏览器中生成 ID?如果不是现在,在未来的某个时候?UUID 可能值得麻烦。

2021年8月更新

IEFT最近发布了UUID规范的更新草案,该草案将引入该格式的一些新版本。

具体来说,UUIDv6 和 UUIDv7 基于 UUIDv1,但翻转时间戳块,以便位从最重要到最不重要排列。这为结果值提供了一个自然顺序,该顺序(或多或少)反映了它们的创建顺序。新版本还排除了从服务器MAC地址派生的数据,解决了长期以来对v1 UUID的批评。

这些更改需要时间才能流向实现,但(恕我直言)它们显着现代化和改进了格式。

评论

6赞 e-info128 10/4/2021
uuid v4更安全,MongoDB ID 是可预测的,并且容易受到记录枚举的影响。
2赞 Buzz Moschetti 1/3/2020 #5

我们必须小心区分MongoDB插入事物的成本与首先生成事物的成本以及相对于有效负载大小的成本。下面是一个小矩阵,它显示了根据可选的额外字节有效负载的大小生成交叉的方法。测试仅使用 javascript,在 MacBook Pro localhost 上进行 100,000 次插入,使用 100 个批次,没有事务,以尝试删除网络、聊天和其他因素。还进行了两次 batch = 1 的运行,只是为了突出戏剧性的差异。_idinsertMany


Method                                                                                         
A  :  Simple int:          _id:0, _id:1, ...                                                   
B  :  ObjectId             _id:ObjectId("5e0e6a804888946fa61a1976"), ...                       
C  :  Simple string:       _id:"A0", _id:"A1", ...                                             

D  :  UUID length string   _id:"9575edcc-cb70-4d63-97ed-ee5d624de87b0", ...                    
      (but not actually                                                                        
      generated by UUID()                                                                      

E  :  Real generated UUID  _id: UUID("35992974-21ea-4f61-b715-2dfaed663b73"), ...              
      (stored UUID() object)                                                                   

F  :  Real generated UUID  _id: "6b16f733-ff24-4172-83f9-e4f96ace6775"                         
      (stored as string, e.g.                                                                  
      UUID().toString().substr(6,36)                                                           

Time in milliseconds to perform 100,000 inserts on fresh (empty) collection.

Extra                M E T H O D   (Batch = 100)                                                               
Payload   A     B     C     D     E     F       % drop A to F                                  
--------  ----  ----  ----  ----  ----  ----    ------------                                   
None      2379  2386  2418  2492  3472  4267    80%                                            
512       2934  2928  3048  3128  4151  4870    66%                                            
1024      3249  3309  3375  3390  4847  5237    61%                                            
2048      3953  3832  3987  4342  5448  5888    49% 
4096      6299  6343  6199  6449  7634  8640    37%                                            
8192      9716  9292  9397 10816 11212 11321    16% 

Extra              M E T H O D   (Batch = 1)                                          
Payload   A      B      C      D      E      F       % drop A to F              
--------  -----  -----  -----  -----  -----  -----                              
None      48006  48419  49136  48757  50649  51280   6.8%                       
1024      50986  50894  49383  49373  51200  51821   1.2%                       


这是一个快速的测试,但似乎很明显,基本字符串和 ints 的速度大致相同,但实际生成 UUID 会增加时间——特别是如果你采用对象的字符串版本,例如 还值得注意的是,构建一个似乎同样快。_idUUID()UUID().toString().substr(6,36)ObjectId

1赞 John 2/21/2021 #6

在过去的几周里,我一直在思考这个问题。我只是发现 ObjectId 和 UUID 都是唯一的。事实上,在集合级别,您不能重复_id您使用的任何类型。一些答案谈到了插入性能。重要的是,这与插入性能无关,它需要的是索引性能。这需要根据您将用于索引_ids的内存量来计算。我们知道 ObjectId 是 12 个字节,而 UUID 是 36 个字节。它说,对于相同数量的索引,如果您使用 UUID 而不是 ObjectId,您将需要 2 倍的 ram 空间。

因此,从这个角度来看,最好在 mongodb 中使用 ObjectId 而不是 UUID。

1赞 Khanna111 6/28/2022 #7

UUID是位(16 字节),并且是全局唯一的。请参阅 RFC 4122128

Object Ids是特定于 MongoDB 的构造,并且是位(12 字节)。尽管在全球范围内提供唯一性就足够了,但存在一些边缘条件。MongoDB有这个官方文档来比较两者。96

我们宁愿不被MongoDB特定的ID生成所束缚,而是更喜欢在客户端进行。我们还使用多种数据库。最重要的是,选择是人们可以根据其特定用例做出的决定。UUIDObjectId

-1赞 Souvik Banerjee 7/20/2022 #8

试试这个

    const uuid = require('uuid')
const mongoose = require('mongoose')
const YourSchema = new Schema({
  _id:{
    type: String,
        default: () => uuid.v4().replace(/\-/g, ""), 
   }


})