使用GUID作为主键


32

我通常在数据库中使用自动增量ID作为主键。我正在尝试学习使用GUID的好处。我已经阅读了这篇文章:https : //betterexplained.com/articles/the-quick-guide-to-guids/

我意识到这些GUID用于在应用程序级别识别对象。它们是否也作为主键存储在数据库级别。例如,假设我有以下课程:

public class Person
{
public GUID ID;
public string Name;
..

//Person Methods follow
}

假设我想在内存中创建一个新人员,然后将其插入数据库。我可以这样做吗?

Person p1 = new Person();
p1.ID=GUID.NewGUID();
PersonRepository.Insert(p1);

假设我有一个包含数百万行的数据库,其中GUID作为主键。这将永远是独一无二的吗?我什至正确理解了GUID吗?

我之前读过这篇文章:http : //enterprisecraftsmanship.com/2014/11/15/cqs-with-database-generation-ids/。它似乎使我感到困惑,因为它似乎建议在GUID和整数之间作为主键使用快乐的介质。

编辑11/06/18

我已经相信Guid比int更适合我的要求。这些天来,我使用CQRS的次数更多,而GUID则更适合。

我确实注意到一些开发人员在域模型中将GUID建模为字符串,例如:https : //github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/AggregatesModel/BuyerAggregate/ Buyer.cs-在这种情况下:IdentityGuid是一个建模为字符串的GUID。除了此处说明的内容外,是否还有其他理由:在分布式系统中使用自定义值对象或Guid作为实体标识符?。将GUID建模为字符串是“正常的”还是应该在模型和数据库中将其建模为GUID?



7
尽管不太可能会碰到碰撞,但不能保证是唯一的。stackoverflow.com/questions/1155008/how-unique-is-uuid/…–
icirellik

2
另请参阅:UUID碰撞

2
另请参见dba.stackexchange.com/questions/54690/…以及许多其他问题-这个主题经常被问到,回答和争论。
Greenstone Walker

1
目前正在使用的系统使用UUID。一个不错的属性是,ID唯一标识一条记录,而不是顺序ID标识该表中的记录。
贾斯汀

Answers:


41

根据定义,GUID是“全局唯一标识符”。Java中有一个类似但略有不同的概念,称为UUIDs“ Universally Unique IDentifiers”。这些名称可互换用于所有实际用途。

GUID对于Microsoft设想的数据库群集的工作方式至关重要,如果您需要合并有时连接的源中的数据,则GUID确实有助于防止数据冲突。

Pro-GUID的一些事实:

  • GUID防止按键碰撞
  • GUID有助于在网络,机器等之间合并数据。
  • SQL Server支持半序列GUIDS,以帮助最大程度地减少索引碎片(参考,一些警告)

GUID的一些缺点

  • 它们很大,每个16字节
  • 它们是乱序的,因此您无法对ID进行排序,并希望像自动增量ID一样获得插入顺序
  • 它们使用起来比较麻烦,特别是在小型数据集(例如查找表)上
  • 新的GUID实现在SQL Server上比在C#库中更强大(您可以从SQL Server获得顺序GUIDS,在C#中是随机的)

GUID将使您的索引更大,因此索引列的磁盘空间成本会更高。随机GUID将使您的索引碎片化。

如果您知道不打算同步来自不同网络的数据,则GUID可能会承载比其价值更多的开销。

如果您需要从有时连接的客户端中提取数据,则与依赖于为那些客户端设置序列范围相比,它们在防止键冲突方面要强大得多。


18
我的理解是GUID是UUID的同义词。UUID是标准名称。GUID是Microsoft在RFC 4122之前创造的。
JimmyJames

13
“它们乱序,所以您不能对ID进行排序,并希望像自动增量ID一样获得插入顺序。”坦白说,我也不愿意依靠常规ID来插入。虽然在极端情况下可能会将一个较低的ID稍后提交到磁盘,但我还是希望依靠有用的排序数据,例如插入时间戳。应该将Id视为内存地址-一切都有一个,但值本身毫无意义。最多可将它们用于决胜局。特别是因为如果您有大量负载,则不能保证插入顺序。
Clockwork-Muse

8
@CortAmmon根据WikipediaRFC 4122,它们是同义词。微软的P. Leach是RFC的创建者之一。我认为自RFC创建以来,两者是相同的。在RFC中:“ UUID(通用唯一IDentifier),也称为GUID(全局唯一IDentifier)”。我认为注意到GUID不是由MS创建的也很有用。他们刚刚为从其他地方采用的技术创建了一个新名称。
JimmyJames

6
“ SQL Server对GUID进行了优化,因此不会对查询性能产生太大影响。” -1不够优化。我正在与一个所有PK都是向导的DB一起工作,这是导致性能下降的主要原因之一。
安迪

7
“ SQL Server对GUID进行了优化,因此它不会对查询性能产生太大影响。 ”事实并非如此。该语句假定其他数据类型未优化。例如,数据库服务器还具有用于处理简单int值的优化。GUID / UUID比使用4字节int值要慢得多。16字节永远不会快于4字节-特别是在本机最多可处理4或8字节的计算机上。
安德鲁·亨勒

28

这将永远是独一无二的吗?

总是?不,并非总是如此;这是一个有限的位序列。

假设我有一个包含数百万行的数据库,其中GUID作为主键。

数百万,您可能很安全。一亿,碰撞可能性变得很大。不过,有个好消息:到发生这种情况时,您已经用完了磁盘空间。

我可以这样做吗?

您可以; 这不是一个好主意。您的域模型通常不应生成随机数;它们应该是模型的输入。

除此之外,当您处理的网络不可靠时,您可能会收到重复的消息,确定性生成的UUID将保护您避免重复的实体。但是,如果为每个随机数分配一个新的随机数,则您需要做更多的工作来识别重复项。

请参阅RFC 4122中基于名称的uuid的描述

将GUID建模为字符串是“正常的”还是应该在模型和数据库中将其建模为GUID?

我认为这不是很重要。对于您的大多数域模型,它是一个标识符;您询问的唯一查询是它是否与某些其他标识符相同。您的域模型通常不会查看标识符的内存中表示形式。

如果GUID在您的域不可知设置中可用作“原始类型”,则应使用它;否则,将使用它。它允许支持上下文选择可能可用的适当优化。

但是,您应该认识到,标识符在内存和存储中的表示方式都是您在实现过程中做出的决定,因此,您应该采取步骤以确保与此相关的代码足迹决定很小-参见Parnas 1972


20
+1表示“到发生时您已经用完了磁盘空间”。
w0051977

2
我认为“ 确定性生成的UUID ” 的概念至关重要(请参阅Data Vault 2)
alk

确实,能够根据其他数据重新计算UUID / GUID是一个巨大的帮助,尤其是检测重复项时。我曾经构建了一个消息处理系统,该系统存储消息并通过处理管道推送它们。我创建了消息的哈希,并将其用作整个系统的主键。只是,就其本身而言,它解决了我很多问题,以便我们在必须扩展时可以识别消息。
Newtopian '17

一亿= 2 ^ 40。这造成2 ^ 79对可能的碰撞。GUID具有2 ^ 128位,因此机会为2 ^ 49。您可能有一个错误,该错误将同一GUID重复用于两个记录,或者错误地认为没有碰撞就会发生碰撞。
gnasher729

我将回顾我的历史性问题。在我接受之前 你能看看我的编辑吗?
w0051977

11

由于GUID或UUID的生成方式,它们很可能是唯一的,并且它们提供了一种安全的方式来确保唯一性,而不必与中央机构进行通信。

GUID作为主键的好处:

  • 您可以在集群的不同分片之间复制数据,而不必担心PK冲突。
  • 它允许您在插入任何记录之前就知道主键。
  • 简化了插入子记录的事务逻辑。
  • 不能轻易猜到。

在示例中,您提供了:

Person p1 = new Person();
p1.ID = GUID.NewGUID();
PersonRepository.Insert(p1);

在插入时间之前指定GUID可以在插入连续的子记录时保存到数据库的往返行程,并允许您在同一事务中提交它们。

Person p2 = new Person();
p2.ParentID = p1.ID
PersonRepository.Insert(p2);

不利于GUID的主键:

  • 它们大16个字节,这意味着在添加索引和外键时它们将占用更多空间。
  • 它们本质上是随机数,因此排序不佳。
  • 索引使用非常非常非常糟糕。
  • 很多叶子在移动。
  • 他们很难记住。
  • 他们很难说话。
  • 它们会使URL难以阅读。

如果您的应用程序不需要分片或群集,则最好坚持使用较小,更简单的数据类型,例如int或bigint。

很多数据库都试图减轻由GUID的和SQL Server甚至有一个功能的存储问题,他们自己内部实现NEWSEQUENTIALID帮助的UUID的允许指标的更好的使用,他们普遍有更好的表现特征排序。

此外,从测试人员,用户或开发人员使用该应用程序的角度来看,在GUID上使用ID将大大改善通信。想象一下必须通过电话读取GUID。

最后,除非需要大规模的群集或混淆URL,否则坚持使用自动递增的ID更为实用。


1
要考虑的一件事是,根据UUID的类型,它们包含可能用于标识生成它们的机器的信息。没有足够的熵,纯随机变量可能会发生碰撞。在URI中使用之前应考虑这一点。
JimmyJames

同意,尽管永远不要在URL中公开其主键。应该使用一些更合适的方法来确保没有安全的数据泄漏到外部系统
。s

1
还有一个用例:重插入OLTP数据库,其中序列锁定是一个瓶颈。据我的Oracle DBA朋友说,这并不像听起来那样罕见,您甚至不需要大规模或集群。•最后,权衡利弊(不要像某些张贴者那样混淆UUID的利弊与非UUID的利弊)并进行衡量
mirabilos

1
如果您使用newsequentialid,那么您必须去db获取ID(例如,带有标识int的标识符),不是吗?这有什么好处。
w0051977

1
@mirabilos需要明确的是,当我说可怕时,我们最终得到的插入内容每行要花几分钟。它从OK开始,但是在有成千上万的行之后,它很快就横盘整理了。如果不是很明显,则成千上万的行是一个很小的表。
JimmyJames

4

我会说不,不要将GUID用作主键。我现在实际上正在处理这样的数据库,它们是导致性能问题的主要原因之一。

多余的12个字节很快就累加了;请记住,大多数PK将是其他表中的FK,而在一个表中只有三个FK,您现在每行多了48个字节。这加在表和索引中。它还在磁盘I / O中加起来。这些额外的12个字节需要读取和写入。

而且,如果您不使用顺序向导,并且PK是集群的(默认情况下会发生这种情况),SQL将不时地不得不移动整个数据页,以将更多的数据挤入正确的“位置”。对于具有大量插入,更新和删除操作的高事务性数据库,情况会很快陷入困境。

如果您需要某种唯一的标识符来进行同步或其他操作,请添加一个guid列。只是不要使其成为PK。


4
Person p1 = new Person();
p1.ID=GUID.NewGUID();
PersonRepository.Insert(p1);

到目前为止,这是使用GUID的最重要原因。

您可以在代码不了解持久层或不与持久层通信的情况下创建唯一ID的事实,这是一个巨大的好处。

您可以确保刚在服务器,个人计算机电话,笔记本电脑,脱机设备或全球所有服务器上唯一但生成的Person对象上生成了该对象。

您可以将其粘贴在任何类型的数据库rdb或no-sql,文件中,将其发送到任何Web服务或立即将其丢弃

不,您永远不会碰撞。

是的,插入可能会稍微慢一些,因为可能需要处理索引。

是的,它比int大。

  • 编辑。在完成之前必须先射击。

我知道很多人对auto inc ints都有强烈的看法,这是DBA引起争议的话题

但是我真的不能足够强力地陈述优越的指导。默认情况下,您应在任何应用程序中使用guid 。

汽车公司有很多缺陷

  • 您使用No-Sql分布式数据库。您根本无法与所有其他实例进行对话以找出下一个数字是什么。

  • 您使用消息队列系统。事物在进入数据库之前需要ID

  • 您正在创建多个项目并在保存之前对其进行编辑。每个人在进入数据库之前都需要一个ID

  • 您要删除并重新插入行。确保您不计算自己的汽车公司名并用光!

  • 您不希望向每个用户公开您今年已收到的订单数量

  • 您想要将匿名数据从生产移动到测试并保持关系完整。但不能删除所有现有的测试数据。

  • 您想将单个租户产品合并到一个多租户数据库中,但是每个人都有一个订单56。

  • 您创建持久但短暂的对象。(订单不完整),请不要将所有int用完不再存在的东西。

列表是无止境的,它们都是真正的问题,一直在人们身上发生。不像磁盘空间不足,因为FK col稍大

最终,int的巨大问题是您用完了它们!!!好吧,理论上你没有,有很多负担。但实际上,您这样做是因为人们不会像对待没有意义的随机数一样对待它们。他们做类似的事情

  • 哦,我不希望客户认为我们是新的。始于10,000

  • 我必须导入大量数据,所以我只是将种子增加到1m,所以我们知道导入了什么

  • 我们需要数据的类别。每个周期都从下一个百万开始,因此我们可以将前几个数字用作魔术数字

  • 我删除并重新导入了具有新ID的所有数据。是的,甚至审核日志。

  • 使用此数字(它是复合键)作为另一件事的ID


1
这个答案实际上并没有错,但是我(抵制进一步的否决)也许会明确指出一个警告,即即使现实中的应用程序不会遇到冲突,从理论上讲也是有可能的。(或者可能有45个以上的Exabyte数据库比我想象的要流行...)。尽管我确实认为语言“最重要的原因”有点强,但这是我发现最有用的。
BurnsBA '17

2
它比起Guid更可能与汽车碰撞
Ewan

4
-1表示“默认情况下,您应在任何应用程序中使用向导”。它取决于™。而且,正如其他人所表明的那样,绝对不能保证GUID / UUID是唯一的。
Max Vernon

3
“取决于”答案毫无用处,请确保在某些int更好的应用程序中会有奇怪的应用。但是您的应用程序不是其中之一。GUID是您可以获得的最独特的东西
Ewan

2
我认为会有一些更好的指导。唯一性不是最重要的考虑因素。您的整数“缺陷”被过度夸大了,您不会考虑任何弊端。
安迪

2

我意识到这些GUID用于在应用程序级别识别对象。它们是否也作为主键存储在数据库级别。

那是您应该停下来的地方,然后重新思考。

您的数据库主键从不具有商业意义。根据定义,它应该毫无意义。

因此,将GUID添加为您的业务密钥,并将普通主键(通常为long int)添加为数据库主键。您始终可以在GUID上放置唯一索引以确保唯一性。

这当然是在谈论数据库理论,但它也是一种好习惯。我已经处理了主键具有业务意义的数据库(例如,一位客户曾考虑过通过将它们用作员工编号,客户编号等来节省一些数据库资源),但它总是会带来麻烦。


1
这与使用整数主键从应用程序层查询有什么不同?在这一点上,它也被用来识别应用程序层的对象。您需要一种从应用程序层识别数据库中对象的方法。
icirellik

@icirellik主键是供数据库内部使用的,用于链接父子记录等。它并不意味着由应用程序逻辑使用,您为此使用了业务ID,例如产品编号或名称。
jwenting

2

始终使用数据库生成的自动递增主键(PK)。

为什么要使用自动递增而不是GUID / UUID?

  • GUID(UUID)并不是唯一的键冲突,因为它们是由多种来源生成的,因此无法使它们唯一。
  • GUID不利于合并,因为它们极大地增加了本已很耗时的合并过程,并且需要非常长的非整数PK和FK列,这需要花费大量时间来处理。请记住,对于大多数PK,将至少有一个其他表,且至少有2个大小相同的键:它是自己的PK,并且FK返回第一个表。所有这些都必须合并解决。

但是,如何处理分片,群集等呢?

  • 创建由单独的列组成的多列PK,以标识每个分片/集群/数据库/管理其自己的自动增量密钥的任何对象。例如...

群集表的三列PK可能是...

 DB | SH | KEY     |
----|----|---------|
 01 | 01 | 1234567 |

但是关于...?

  • 多次访问数据库-大多数应用程序无需唯一地标识正在创建的记录,直到将其插入数据库为止,因为该线程/会话/任何内容一次只能处理一个。如果应用程序确实需要此功能,请使用应用程序生成的临时PK ,该临时PK 不会发送到数据库。然后让数据库在插入时将其自己的自动增量PK放在行上。插入将使用临时PK,而更新和删除将使用数据库分配的永久PK。

  • 性能-由于GUID(37)与整数(10)中每个元素的可能值都很大,因此计算机可以处理简单的整数,其处理速度远远快于任何其他整数。还要记住,GUID中的每个字符必须首先转换为要由CPU操纵的数字。

主键的常见误用 PK只有一个目的...绝对唯一地标识表中的一行。其他任何事情都是很常见的误用。

检测丢失的记录

  • 通过查看PK不能检测到丢失的记录。保佑质量检查人员至少可以尝试确保数据质量。但是,他们和程序员对现代数据库系统中键的分配方法缺乏了解,常常使他们误以为自动递增PK中的数字丢失意味着数据丢失。它因为...
  • 为了提高性能,数据库系统按“序列”(批,范围)分配数字块,以最大程度地减少对存储中实际数据库的访问。这些数字序列的大小通常在DBA的控制之下,但可能无法在每个表的基础上进行调整。
  • 关键要点是...这些序列中未使用的数字永远不会返回数据库,因此PK编号中总是存在空白。
  • 您为什么会有未使用的号码?因为各种数据库维护操作可能导致放弃序列。这些是重新启动,批量重新加载表,从备份还原的某些类型以及其他一些操作。

排序

  • 按PK排序很容易出错,因为大多数人会认为它按创建顺序列出了与时钟时间相对应的行。通常,但不是必须的。
  • 数据库引擎已针对最大性能进行了优化,这可能意味着延迟插入长时间运行的复杂事务的结果,以插入简短的简单事务(可以说是“转码”)。

您对表架构有什么想法,使得唯一的唯一列是数据库创建的自动增量主键?特别是对于没有外键但其主键是多个相关表的外键的表?
RibaldEddie '17

我已经按照这些思路在答案中添加了更多内容。由于我要挂起的Android SE应用,原始答案不完整。我认为该应用程序正在进行重大改写。
DocSalvager '17

因此,在您看来,一个表可以包含任意数量的相同行(除了它们的自动递增主键之外)是可以的吗?
RibaldEddie '17

@RibaldEddie-就数据库的设计而言...绝对是。删除很容易。当您的情况发生时,我认为这是要在软件中修复的错误,然后删除其中任一行。但是,更常见的情况是同一事物的两条记录的数据略有不同,因此必须将它们合并。如果一个记录中的一列为空,而另一记录中有一个值,则选择是显而易见的,并且可以自动进行。通常,datetimestamp可用于仲裁自动合并。有些重复项需要人员根据业务规则完成并验证合并。
DocSalvager '17

1

像其他任何东西一样,这样做有其优点和缺点:

好:

  1. 您的密钥长度始终相同(非常大的数据库可以具有非常大的密钥)

  2. 几乎可以保证唯一性-即使是从单独的系统生成它们,并且/或者还没有从数据库中读取最后一个ID时

坏处:

  1. 如上文所述-较大的索引和数据存储。

  2. 您无法通过ID进行订购,而必须通过其他方式进行订购。索引更多,效率可能更低。

  3. 它们不太容易让人理解。人们通常更容易解析,记住和输入整数。在多个连接的表中的WHERE子句中将GUID用作ID,可能会让您大吃一惊。

像所有东西一样,请在适当的地方使用它们,而不必教条-在许多情况下,自动递增的整数会更好,而GUID有时会很棒。


0

是的,您可以将GUID用作主键。不利的一面是索引的大小和快速碎片化。

除非您需要跨数据库(例如集群)的唯一性,否则整数是首选。


GUID生成器可能多次生成同一GUID,这是一个缺陷。它们是否将取决于其粒度,主要取决于时钟滴答之间的间隔。例如,基于时钟的生成器可能仅每100毫秒进行一次滴答,导致该计算机上在该100毫秒内请求的2个GUID相同。有很多方法可以避免这种情况,但是许多GUID生成器完全依靠IP地址和/或MAC地址以及时间戳工作。
jwenting

0

这是我对这个问题的看法-解决方案是GUID和int值之间的中途选择,同时兼顾了两者的优点。

该类生成伪随机(但随时间增加)Id值,该值类似于Comb GUID

关键优势在于,它允许在客户端上生成Id值,而不是使用服务器上生成的自动增量值(这需要往返),而重复值的风险几乎为零。

生成的值仅对GUID使用8个字节而不是16个字节,并且不依赖于一个特定的数据库排序顺序(例如,用于GUID的Sql Server)。可以将值扩展为使用整个无符号长范围,但是这将导致任何仅具有带符号整数类型的数据库或其他数据存储库出现问题。

public static class LongIdGenerator
{
    // set the start date to an appropriate value for your implementation 
    // DO NOT change this once any application that uses this functionality is live, otherwise existing Id values will lose their implied date
    private static readonly DateTime PeriodStartDate = new DateTime(2017, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    private static readonly DateTime PeriodEndDate = PeriodStartDate.AddYears(100);
    private static readonly long PeriodStartTicks = PeriodStartDate.Ticks;
    private static readonly long PeriodEndTicks = PeriodEndDate.Ticks;
    private static readonly long TotalPeriodTicks = PeriodEndTicks - PeriodStartTicks;

    // ensures that generated Ids are always positve
    private const long SEQUENCE_PART_PERMUTATIONS = 0x7FFFFFFFFFFF; 

    private static readonly Random Random = new Random();

    private static readonly object Lock = new object();
    private static long _lastSequencePart;

    public static long GetNewId()
    {
        var sequencePart = GetSequenceValueForDateTime(DateTime.UtcNow);

        // extra check, just in case we manage to call GetNewId() twice before enough ticks have passed to increment the sequence 
        lock (Lock)
        {
            if (sequencePart <= _lastSequencePart)
                sequencePart = _lastSequencePart + 1;

            _lastSequencePart = sequencePart;
        }

        // shift so that the sequence part fills the most significant 6 bytes of the result value
        sequencePart = (sequencePart << 16);

        // randomize the lowest 2 bytes of the result, just in case two different client PCs call GetNewId() at exactly the same time
        var randomPart = Random.Next() & 0xFFFF;

        return sequencePart + randomPart;
    }

    // used if you want to generate an Id value for a historic time point (within the start and end dates)
    // there are no checks, compared to calls to GetNewId(), but the chances of colliding values are still almost zero
    public static long GetIdForDateTime(DateTime dt)
    {
        if (dt < PeriodStartDate || dt > PeriodStartDate)
            throw new ArgumentException($"value must be in the range {PeriodStartDate:dd MMM yyyy} - {PeriodEndDate:dd MMM yyyy}");

        var sequencePart = GetSequenceValueForDateTime(dt.ToUniversalTime());
        var randomPart = Random.Next() & 0xFFFF;
        return ( sequencePart << 16 ) + randomPart;
    }

    // Get a 6 byte sequence value from the specified date time - startDate => 0 --> endDate => 0x7FFFFFFFFFFF
    // For a 100 year time period, 1 unit of the sequence corresponds to about 0.022 ms
    private static long GetSequenceValueForDateTime(DateTime dt)
    {
        var ticksFromStart = dt.ToUniversalTime().Ticks - PeriodStartTicks;
        var proportionOfPeriod = (decimal)ticksFromStart / TotalPeriodTicks;
        var result = proportionOfPeriod * SEQUENCE_PART_PERMUTATIONS;
        return (long)result;
    }

    public static DateTime GetDateTimeForId(long value)
    {
        // strip off the random part - the two lowest bytes
        var timePart = value >> 16;
        var proportionOfTotalPeriod = (decimal) timePart / SEQUENCE_PART_PERMUTATIONS;
        var ticks = (long)(proportionOfTotalPeriod * TotalPeriodTicks);
        var result = PeriodStartDate.AddTicks(ticks);
        return result;
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.