使用COALESCE将主键从IDENTITY更改为持久计算列


10

为了使应用程序与整体数据库脱钩,我们尝试将各种表的INT IDENTITY列更改为使用COALESCE的PERSISTED计算列。基本上,我们需要分离的应用程序能够为许多应用程序之间共享的通用数据更新数据库,同时仍允许现有应用程序在这些表中创建数据而无需修改代码或过程。

因此,从本质上讲,我们已经脱离了的列定义;

PkId INT IDENTITY(1,1) PRIMARY KEY

至;

PkId AS AS COALESCE(old_id, external_id, new_id) PERSISTED NOT NULL,
old_id INT NULL, -- Values here are from existing records of PkId before table change
external_id INT NULL,
new_id INT IDENTITY(2000000,1) NOT NULL

在所有情况下,PkId也是主键,在除一种情况以外的所有情况下,它都是聚集的。所有表都具有与以前相同的外键和索引。本质上,新格式允许由解耦的应用程序提供PkId(作为external_id),但也允许PkId作为IDENTITY列值,因此允许通过使用SCOPE_IDENTITY和@@ IDENTITY依赖于IDENTITY列的现有代码照常工作。

我们遇到的问题是,我们遇到了一些查询,这些查询过​​去可以在可接受的时间内运行,现在已经完全崩溃了。这些查询使用的生成查询计划与以前不同。

鉴于新列是PRIMARY KEY,数据类型与以前相同,并且为PERSISTED,因此我希望查询和查询计划的行为与之前相同。就SQL Server如何生成执行计划而言,COMPUTED PERSISTED INT PkId是否应在本质上与显式INT定义具有相同的行为?您还可以看到这种方法还有其他可能的问题吗?

更改的目的是使我们能够更改表定义,而无需修改现有过程和代码。考虑到这些问题,我觉得我们不能采用这种方法。


评论不作进一步讨论;此对话已转移至聊天
保罗怀特9

Answers:


4

第一

你可能并不需要所有三列:old_idexternal_idnew_id。即使将插入,该new_idIDENTITY也将为每一行生成一个新值external_id。但是,在old_id和之间external_id,它们之间几乎是互斥的:或者已经存在一个old_id值,或者在当前概念中该列仅在NULL使用external_id或时才存在new_id。由于您不会在已存在的行(即具有old_id值的行)中添加新的“外部” ID ,并且不会有任何新值输入old_id,因此可以使用一列出于两个目的。

因此,摆脱external_id列,并重命名old_id为类似old_or_external_id或类似的东西。这不需要对任何内容进行任何实际更改,但可以减少一些复杂性。external_id即使已经编写了要插入的应用程序代码,即使您包含“旧”值,您最多也可能需要调用该列external_id

这样可以使新结构变为:

PkId AS AS COALESCE(old_or_external_id, new_id, -1) PERSISTED NOT NULL,
old_or_external_id INT NULL, -- values from existing record OR passed in from app
new_id INT IDENTITY(2000000, 1) NOT NULL

现在,您仅增加了每行8个字节,而不是12个字节(假设您没有使用该SPARSE选项或数据压缩)。而且您不需要更改任何代码,T-SQL或App代码。

第二

继续简化的过程,让我们看一下剩下的内容:

  • old_or_external_id列要么已经具有值,要么将从应用程序中获得新值,或者保留为NULL
  • new_id总会有产生新的价值,但如果将只使用该值old_or_external_idNULL

从来没有机会需要同时在old_or_external_id和中使用值new_id。是的,有时两列都具有new_id作为的值IDENTITY,但是这些new_id值会被忽略。同样,这两个字段是互斥的。所以现在怎么办?

现在,我们可以看看为什么我们首先需要external_id。考虑到可以使用插入到IDENTITY列中SET IDENTITY_INSERT {table_name} ON;,因此您可以完全不进行任何模式更改,而只需修改应用程序代码以将INSERT语句/操作包装在SET IDENTITY_INSERT {table_name} ON;and SET IDENTITY_INSERT {table_name} OFF;语句中即可。然后,您需要确定将IDENTITY列重置为哪个起始范围(对于新生成的值),因为它必须远高于应用代码将插入的值,因为插入较高的值将导致下一个自动生成的值大于当前的MAX值。但是,您始终可以插入一个低于IDENT_CURRENT值的值。

old_or_external_idnew_id列合并也不会增加自动生成的值与应用生成的值之间出现重叠值情况的机会,因为使用2列甚至3列的目的是将它们合并为主键值,这些始终是唯一的值。

使用这种方法,您只需要:

  • 将表保留为:

    PkId INT IDENTITY(1,1) PRIMARY KEY

    这会为每行增加0个字节,而不是8个甚至12个字节。

  • 确定应用程序生成的值的起始范围。这些值将大于每个表中的当前MAX值,但小于将自动生成的值的最小值。
  • 确定自动生成的范围应从哪个值开始。当前的MAX值增长的空间之间应该有足够的空间,知道上限为21.4亿美元。然后,您可以通过DBCC CHECKIDENT设置这个新的最小种子值。
  • SET IDENTITY_INSERT {table_name} ON;SET IDENTITY_INSERT {table_name} OFF;语句中包装应用代码INSERT 。

第二部分,B部分

上面直接指出的方法的一种变化是使App代码插入从-1开始并从该位置向下的值。这使这些IDENTITY值成为唯一上升的值。这样做的好处是,您不仅不使架构复杂化,而且还不必担心会遇到重叠的ID(如果应用程序生成的值进入了新的自动生成的范围)。仅当您尚未使用负ID值时(这是一个选择),而且人们很少在自动生成的列上使用负值,因此在大多数情况下这应该是可能的)。

使用这种方法,您只需要:

  • 将表保留为:

    PkId INT IDENTITY(1,1) PRIMARY KEY

    这会为每行增加0个字节,而不是8个甚至12个字节。

  • 应用程序生成的值的起始范围为-1
  • SET IDENTITY_INSERT {table_name} ON;SET IDENTITY_INSERT {table_name} OFF;语句中包装应用代码INSERT 。

在这里,您仍然需要执行IDENTITY_INSERT,但是:您无需添加任何新列,无需“播种”任何IDENTITY列,并且将来没有重叠的风险。

第二部分,第3部分

这种方法的最后一种变体是可能换出IDENTITY列,而使用Sequences。采用这种方法的原因是能够使应用代码插入以下值:正数,高于自动生成的范围(不低于),并且不需要SET IDENTITY_INSERT ON / OFF

使用这种方法,您只需要:

  • 使用CREATE SEQUENCE创建序列
  • 使用NEXT VALUE FOR函数将IDENTITY列复制到不具有该IDENTITY属性但具有DEFAULT约束的新列:

    PkId INT PRIMARY KEY CONSTRAINT [DF_TableName_NextID] DEFAULT (NEXT VALUE FOR...)

    这会为每行增加0个字节,而不是8个甚至12个字节。

  • 应用程序生成的值的起始范围将远远超出您认为自动生成的值所能达到的范围。
  • SET IDENTITY_INSERT {table_name} ON;SET IDENTITY_INSERT {table_name} OFF;语句中包装应用代码INSERT 。

但是,由于要求带有一个SCOPE_IDENTITY()或两个@@IDENTITY仍能正常运行的代码的要求,因此当前无法选择切换到Sequences,因为看来对于Sequences :-(。


非常感谢您的回答。您提出了一些内部讨论的要点。不幸的是,由于某些原因,其中一些对我们不起作用。我们的数据库很旧,有些脆弱,并且在2005兼容模式下运行,因此SEQUENCES消失了。我们的应用程序数据推送是通过数据加载工具进行的,该工具从Service Broker队列中获取新记录并通过多个线程来推送它们。IDENTITY_INSERT每个会话只能用于一个表,当前的想法是,如果不进行重大更改,我们的体系结构将无法满足这一要求。我正在测试你的拳头建议。
穆斯先生

@MrMoose是的,我更新了答案,以在末尾包含有关序列的更多信息。无论如何,这将不起作用。我想知道有关的潜在并发问题IDENTITY_INSERT,但还没有测试。不确定选项1是否会解决您的整体问题,这只是为了减少不必要的复杂性。但是,如果您有多个线程在插入新的“外部” ID,那么如何保证它们是唯一的?
所罗门·鲁兹基

@MrMoose实际上,关于“ IDENTITY_INSERT每个会话只能用于一个表 ”,这到底是什么问题?1)一次只能插入一个表,因此在插入TableB之前先将其关闭,然后再插入TableB; 2)我刚刚进行了测试,与我的想法相反,没有并发问题-我能够IDENTITY_INSERT ON在两个会话中具有相同的表,并且可以毫无问题地插入两个表中。
所罗门·鲁兹基

1
如您所建议,变更1的作用不大。我们将使用的ID将在当前数据库之外分配,并用于关联记录。我对会话的理解可能不太正确,因此IDENTITY_INSERT可能有效。不过,这将需要我花费一些时间进行调查,因此我将无法在一段时间内进行举报。再次感谢您的输入。非常感谢。
穆斯先生

1
我认为您建议使用IDENTITY_INSERT(对于现有应用而言具有很高的种子价值)会很好。Aaron Bertrand 在此处提供了一个答案并提供了一个很好的有关并发测试的小示例。我们已经修改了数据加载工具,使其能够处理需要指定标识值的表,并且在接下来的几周中将进行进一步的测试。
穆斯先生
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.