避免使用较大ID值的原因


17

我们正在开发一个尚不为用户所用的Web应用程序。我的老板注意到,即使表中只有不到100条记录,新创建的记录的ID也超过10000。她认为Web界面出于某种原因会创建比实际记录多100倍的临时记录(并删除它们),并且这可能导致我们在发布后的几个月内超出范围。

我不认为她对身份证通货膨胀的原因是正确的(可以回答这个问题的同事正在休假,所以我们不确定),但是我们假设她是。她说,她不想使用bigint列,并且希望我们停止自动递增ID列,并编写选择第一个“未使用”整数并将其用作ID的服务器端代码。

我是计算机科学专业的研究生,几乎没有实践经验,担任初级开发人员。她在管理我们组织的所有数据库以及设计大多数数据库方面具有多年的经验。我认为她在这种情况下是不正确的,bigint ID不用担心,而且模仿DBMS功能的味道像是反模式。但是我不相信我的判断。

支持和反对每个立场的理由是什么?如果我们使用bigint会发生什么不好的事情,以及重新发明轮子自动递增功能的危险是什么?有没有比任何一个都更好的第三种解决方案?她为什么要避免身份证面值膨胀的原因是什么?我也有兴趣了解实用的原因-也许bigint ID在理论上起作用,但在实践中引起头痛?

该应用程序不应处理大量数据。我怀疑在未来几年内是否会达到10,000条实际记录。

如果有什么不同,我们正在使用Microsoft SQL Server。该应用程序用C#编写,并使用Linq进行SQL。

更新资料

谢谢,我发现现有的答案和评论很有趣。但恐怕您误解了我的问题,因此它们包含了我想知道的内容。

我并不真正担心ID高的真正原因。如果我们自己找不到它,我可以问一个不同的问题。我感兴趣的是了解这种情况下的决策过程。为此,请假定应用程序每天将写入1000条记录,然后删除其中的9999条。我几乎可以肯定不是这种情况,但这就是我老板在提出要求时所坚信的。因此,在这些假设的情况下,使用bigint或编写自己的将分配ID的代码(以重新使用已删除记录的ID以确保没有间隙的方式)的利弊是什么?

出于实际原因,我强烈怀疑这是因为我们曾经编写了从另一个数据库导入数据的代码,作为可以在以后进行一定程度迁移的概念证明。我认为我的同事实际上在导入过程中创建了数千条记录,后来又删除了它们。我必须确认是否确实如此,但如果确实如此,则甚至无需采取任何措施。


参见SM Ahasan Habib的文章,位于 codeproject.com/Tips/668042/…–
RLF

你能澄清一下吗?新ID是否仅获得值> 1​​0000?还是新ID的间隔为10000?估计未来的应用程序寿命中需要多少个ID?
user2338816 2014年

1
关于查找第一个未使用的ID,在Bill Karwin的书“ SQL Antipatterns”中有一章正好与此有关。因此,可以肯定,它可以看作是反模式!
Thomas Padron-McCarthy

Answers:


24

如果不看代码,很难确切地说出正在发生什么。尽管很可能会IDENTITY缓存该值,但在重新启动SQL Server之后会导致该值出现空白。请参阅/programming/17587094/identity-column-value-suddenly-jumps-to-1001-in-sql-server,以获取一些好的答案和信息。

一个简单的INT字段最多可以容纳2,147,483,647。实际上,您可以将身份值起始于-2,147,483,648,以提供完整的32位值。40亿个不同的价值。我非常怀疑您会耗尽要使用的值。假设你的应用程序耗时增加每个实际行1000倍的值,你需要要创建每天每天近12000行ID的6个月内假设你开始冒了出来IDENTITY,在0值,并使用INT。如果您使用的是BIGINT,则每天要写入12,000行,每行消耗1,000个“值”,则必须等待2,100万个世纪才能用完值。

说了这么多,如果您想BIGINT用作身份字段数据类型,那肯定没有错。这将为您提供所有意图和目的,供您无限使用的价值提供。INT和BIGINT之间的性能差异实际上在现代64位硬件上是不存在的,并且比用于NEWID()生成GUID 的实例更好。

如果您想管理自己的ID列值,则可以创建一个密钥表,并使用此问题的答案中所示的一种方法,提供一种防弹的漂亮方法: 处理并发访问密钥表而无需SQL Server中的死锁

假设您使用的是SQL Server 2012+,则另一个选择是使用SEQUENCE对象获取列的ID值。但是,您需要将序列配置为不缓存值。例如:

CREATE SEQUENCE dbo.MySequence AS INT START WITH -2147483648 INCREMENT BY 1 NO CACHE;

为了回答您老板对“高”数字的负面看法,我想说这有什么不同?假设你使用的INT领域,有IDENTITY,你其实可以启动IDENTITY,在2147483647与“增量”的通过值-1。由于32位数字为4字节,所以无论它是0还是,这都不会对所使用的内存消耗,性能或磁盘空间造成任何影响21474836470以二进制形式00000000000000000000000000000000存储在32位带符号INT字段中时。 214748364701111111111111111111111111111111-这两个数字在内存和磁盘上都占用完全相同的空间量,并且都需要完全相同的CPU操作量来处理。正确设计应用程序代码比迷恋存储在关键字段中的实际数字更为重要。

您询问了(a)使用容量较大的ID列(例如BIGINTa)或(b)滚动自己的解决方案以防止ID间隙的优缺点。要解决这些问题:

  1. BIGINT而不是INT作为相关列的数据类型。使用a时BIGINT,列本身的磁盘和内存存储量都需要加倍。如果该列是所涉及表的主键索引,则连接到该表的每个非聚集索引也BIGINT将以两倍于大小的值存储该值,该值也是INT内存和磁盘上的两倍。SQL Server将数据存储在8KB页的磁盘上,其中每“页”的“行”数取决于每一行的“宽度”。因此,例如,如果您有一个包含10列的表格,每列一个INT,则大约每页可以存储160行。如果那些列在哪里BIGINT列,则每页只能存储80行。对于具有大量行的表,这显然意味着在此示例中,对于任何给定的行数,读写该表所需的I / O将是两倍。当然,这是一个非常极端的示例-如果您有一行包含一行INT或一BIGINT列和一NCHAR(4000)列,则(简单地)每页将获得一行,无论您使用INT还是BIGINT。在这种情况下,不会有太大的不同。

  2. 滚动自己的方案以防止ID列出现空白。您需要以这样一种方式编写代码,即确定要使用的“下一个” ID值与表中发生的其他操作不冲突。SELECT TOP(1) [ID] FROM [schema].[table]想到一些天真的想法。如果有多个参与者同时尝试向表中写入新行怎么办?两个参与者可以轻松获得相同的值,从而导致写冲突。要解决此问题,需要序列化对表的访问,从而降低性能。关于此问题的文章很多。我将它留给读者以对该主题进行搜索。

这里的结论是:您需要了解您的需求,并正确估计行数,行宽度以及应用程序的并发需求。与往常一样,它取决于™。


4
+1,但我不会放弃BIGINT的空间要求。磁盘上的空间不是很多,而是内存中的I / O和空间浪费了。您可以使用数据压缩来抵消很多此类费用,因此,在您超过20亿之后,您才真正感受到BIGINT类型的冲击。理想情况下,他们只是解决问题(我很犹豫地将其本身称为错误)-尽管人们不必担心漏洞,而且人们不应该每天重启服务器15次,但我们都遇到了这两种情况相当普遍,并且经常串联在一起。
亚伦·伯特兰

3
和往常一样,亚伦非常有效。无论如何,我都会倾向于使用INT,因为BIGINT几乎完全是多余的,除非他们期望大量的行。
Max Vernon 2014年

除非您同时在内存中有成千上万的数据,否则ID列的BIGINT数据类型不会对内存产生太大影响。即使这样,它也可能只占总行大小的一小部分。
user2338816 2014年

2
关键是@ user2338816-如果表变大,内存中将有很多。而且由于标识列通常是集群键,所以对于每个索引中的每一行来说,这也是一个额外的4个字节。在每种情况下都重要吗?否。应该忽略它吗?绝对不。直到现在为时已晚,似乎没有人对可伸缩性有所了解。
亚伦·伯特兰

3
尽管如果确实有合理的期望,您可能bigint会感谢您自己预先决定,而不是需要将其添加到具有数十亿行的表中。
马丁·史密斯

6

主要任务是找到导致当前值如此之高的根本原因。

对于SQL2012之前的SQL Server版本,最合理的解释(假设您在谈论测试数据库)将是先进行负载测试,然后进行清理。

从SQL2012开始,最可能的原因是由于几次重新启动SQL Engine(如提供的第一个链接Max中所述)。

如果差距是由测试场景引起的,那么从我的观点来看,没有理由担心。但是为了安全起见,我会在正常使用应用程序期间以及引擎重新启动之前和之后检查身份值。

MS指出这两种选择(跟踪标志272或新的SEQUENCE对象)可能会影响性能,这是“滑稽的”。

为了安全起见,涵盖MS的下一个“改进”,使用BIGINT而不是INT可能是最好的解决方案...


我可能以错误的方式表达了我的问题,但我对寻找原因并不那么感兴趣。很可能是某些东西不会再次出现(测试运行的结果),或者是应用程序中的错误设计决策,可以在数据库外部解决。关键是要理解为什么有经验的DBA会认为高ID不好,或者比滚动我们自己的ID管理差。
rumtscho 2014年

2

Rumtscho,如果您每天仅创建1000行,则几乎没有什么决定-将INT数据类型与Identity字段一起使用并完成此操作。简单的数学说,如果给您的应用一个30年的生命周期(不太可能),则您每天可能有200,000行,并且仍在INT数据类型的正数范围内。

在您的情况下,使用BigInt是过大的,如果通过ODBC访问您的应用程序或数据(例如带入Excel或MS Access等),它也会引起问题,Bigint不能很好地将大多数ODBC驱动程序转换为桌面应用程序。

至于GUIDS,除了额外的磁盘空间和额外的I / O之外,还有一个巨大的问题,即它们在设计上不是顺序的,因此,如果它们是排序索引的一部分,那么您可以猜测每个插入都将要求使用索引。-吉姆


关于GUID的要点是,除非您使用NEWSEQUENTIALID()-我仍然同意,在这个问题上没有明显的理由使用它们。
Max Vernon 2014年

1

使用的值之间有差距吗?还是起始值为10.000,从那时起,所有值都加1?有时,如果将要提供给客户的号码,则初始号码大于零(例如,假设为1500),因此客户不会意识到系统是“新的”。

使用bigint而不是smallint的缺点是,因为bigint使用“更多磁盘空间”,因此在读取磁盘时,每个磁盘读取的磁盘块较少。如果您的行空间很小,那么这可能是一个缺点,如果不是,则没有太大关系。同样,如果您不一次查询大量资源,也没有适当的索引,也没关系。

就像在其他答复中所说的那样,如果您担心索引用完了,那么不必担心,除非您拥有百万富翁业务,否则smallint可以处理。发明一种“恢复ID”的机制非常昂贵,并且会增加软件的故障点和复杂性。

问候


2
OP看到服务重启方面的差距。这是因为这个问题。我也不认为smallint在短期内是对以后修复它的工作的好折衷。
亚伦·贝特朗

@AaronBertrand实际上,恐怕其他人在暗示这种可能性时误解了这一点。我非常确定,这不是导致数量众多的原因,但是即使是这样,我也没有试图找到原因,而是了解有什么理由支持和反对提议的解决方案。有关详细信息,请参见我的更新。
rumtscho 2014年

@rumtscho实际上,即使没有直接解决您的问题,这个答案也突出了一个优点:“发明一种'恢复ID'的机制非常昂贵,并且会增加故障点和软件的复杂性。”
Doktor J 2014年

@DoktorJ我同意你的看法。我是支持答案的人:)只是想消除误解,这就是为什么我留下我的第一条评论。
rumtscho 2014年

1

如果我是你的老板,我最感兴趣的原因出乎意料的ID值...我看到它的方式,为每个列出的两种情况:

  1. 如果先前的测试提高了身份值,那么您对预期记录数的其他评论也会促使我建议使用较小的密钥类型。坦率地说,如果测试对于表的当前预期用途而言字符不足,那么我还考虑是否可以重置序列并重新编号现有记录(大多数人会认为这种过大杀伤力-“取决于”)。

  2. 如果写入表的大多数记录在我倾向于考虑使用两个表后不久就被删除,一个临时表,其中记录不会长期保存,而另一个表中,只有我们将永久创建的记录会保留。同样,您对长期记录的数量的期望向我建议您在键列中使用较小的类型,每天几条记录几乎不会导致性能问题,从而将记录从一个表“移动”到另一个表一。我怀疑这不是您的情况,但可以想象购物网站可能更喜欢维护Basket / BasketItem,并在实际下订单时将数据移至Order / OrderItem集。

概括地说 在我看来,不必担心BIGINT,但是坦率地说,在许多情况下,它们都是不必要的大。如果表永远不会变大,您将永远不会意识到选择类型会过大……但是,当您拥有数百万行的表以及许多BIGINT的FK列时,它们可能会变小,那么您可能希望类型的选择较为保守(不仅要考虑键列,还要考虑所有的foreign键列以及保留的所有备份,等等!)。磁盘空间并不总是便宜的(考虑在托管位置使用SAN磁盘-即磁盘空间已租用)。

实质上,我主张始终而不是有时仔细地考虑对数据类型的选择。您将不会总是正确地预测使用模式,但是我认为您将始终假设“越大越好”将是一个更好的决定。通常,我选择可以包含所需且合理的值范围的最小类型,如果我认为该值在可预见的将来很可能适合该类型,我会很乐意考虑INT,SMALLINT甚至TINYINT。较小的类型不太可能与IDENTITY列一起使用,但可能会与在其中手动设置键值的查找表一起使用。

最后,人们使用的技术会大大影响他们的期望和答案。一些工具更可能引起范围差距,例如通过预先预订每个过程的身份范围。相反,@ DocSalvager提出了一个完整的可审核序列,该序列似乎反映了您老板的观点。我个人从来没有要求过这么高的权限-尽管身份是连续的并且通常没有空隙的一般规则对于我在支持情况和问题分析中通常是非常有用的。


1

使用bigint或编写自己的代码来分配ID(以重新使用已删除记录的ID,以确保没有间隙)的利弊是什么?

使用bigint作为身份和与间隙生活:

  • 全部都是内置功能
  • 您可以确定它可以开箱即用
  • 这会浪费空间,因为int仍然会为您提供约200万天的数据;需要阅读和编写更多页面;索引可能会更深。(但是,在这些数量上,这并不是一个重要的问题)。
  • 代理键列是没有意义的,因此可以使用空格。如果将其显示给用户并且差距被解释为很重要,那么您做错了。

自己动手:

  • 您的开发团队将永远从事所有开发和错误修复工作。
  • 您是否也想填补尾部或中间的空隙?设计决策争论不休。
  • 每次写操作都必须发出强锁,以防止并发进程获取相同的新ID或解决事后的冲突。
  • 最坏的情况是,如果删除rowid = 1,则必须更新表中的每一行以缩小差距。这将提高并发性和性能,以及所有级联的外键更新等。
  • 懒惰还是渴望填补空白?并发发生时会发生什么?
  • 您必须先读取新ID,然后再进行写入=额外负载。
  • id列上将需要一个索引,以有效地找到空位。

0

如果您确实担心要为PK达到INT的上限,请考虑使用GUID。是的,我知道它是16字节vs 4字节,但是磁盘很便宜。

这是一个很好的利弊文章。


4
+1是因为这是一种解决方案,但是请参阅Aaron对Max答案的评论,这是“磁盘便宜”的原因,而不是在不仔细权衡所有选项的情况下使用GUID的原因。
杰克·道格拉斯

1
这是来自SQL Server索引和体系结构专家而不是开发人员的更好的文章
Aaron Bertrand

哦,当然要当心NEWID()的页面拆分
Max Vernon

1
我的老板似乎仅以高价值观为由反对高价值观。我希望这个问题会向我显示更多可能的反对意见,但是如果这是她的主要论点之一,她可能会对GUID的反应甚至更加消极。
rumtscho 2014年

1
@rumtscho告诉您的老板,替代数字只是一个没有意义的数字(数字的“大小”无关紧要),序列中的间隔是自然的,并且在很大程度上是不可避免的。
亚伦·伯特兰

0

RDBMS主键(通常称为“ ID”
列)在RDBMS自动递增列(字段)中无法避免。它们主要用于创建独特的PK。为了提高性能,主要产品会分批分配这些产品,因此针对各种正常操作故障的自动恢复机制可能导致未使用的数量。这是正常的。

不间断的序列
当您需要不间断的序列号(如用户经常期望的)时,它应该是以编程方式分配的单独列,而不应该是PK。因此,这1000条记录在该列中都可以具有相同的编号。

用户为什么要不间断的序列?
缺少序列号是在任何类型的审核中发现的错误的最基本标志。这种“簿记101”原则无处不在。但是,对于手工维护的少量记录而言,当将其应用于数据库中的大量记录时,就会遇到严重的问题...

重复使用不相关记录的键值会使数据库无效。
使用“第一个未使用的整数”会在将来的某个时候引入一个数字,该数字将被重新用于与原始记录无关的记录。这使得数据库无法可靠地表示事实。这是自动递增机制经过专门设计以永远不会重复使用值的原则原因。

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.