TTT*_*TTT 6 sql-server identity sql-server-2017
为了防止XY 问题,这是我们要解决的实际问题:
问题:
不幸的是,我们有一堆查找表,它们是在主键上使用标识列创建的,这是一个int. 我们希望可以简单地删除身份,但是,我们有一些带有指向身份列的外键的大型表,我的理解是在这种情况下删除身份很困难。我们对身份感到遗憾的原因是因为这些表需要跨多个环境同步,而开发人员通过编写脚本将数据插入到这些表中,而我们在多个环境上运行这些脚本但不一定总是按照相同的顺序,所以我们问开发人员始终:
如果每个人都这样做,数据要么保持同步,要么脚本失败,我们可以立即采取纠正措施来解决冲突。当然,有时开发者忘记遵守规则,直接无标识地插入,不同环境下以不同顺序运行的不同脚本的自动增量导致它们不同步,从而出现问题。
一个想法:
我们可以强制开发人员始终指定标识列吗?我认为没有办法简单地禁用这些表上的身份。如果我们将身份重新播种到较低的数字会怎样?当种子值已经存在时,任何未指定所有列的插入都会失败,并且会继续失败,直到插入尝试的次数超过现有(连续)行的数量。但是,在一次正确的插入之后,就会重新为表设定种子,下一次不正确的插入将再次使用自动增量。因此,这个想法的推断是在每次插入后(也许使用触发器,这感觉很奇怪,但可能有效?),或者按计划,或者可能每次我们运行开发人员的脚本时,将表重新设置为一个较低的现有数字。
这是一个合理的想法,和/或有更好的解决方案吗?
旁注:我们确实有一些其他想法,我认为这些想法超出了这个问题的范围,例如:
尽管从长远来看,这些其他想法可能会更好,但似乎最容易实现的目标只是重新播种这些表,因此不正确的插入将会失败。
与其“将表重新播种到现有的较低数字”,然后必须在每次插入后重置种子,“正确”地执行此操作,更好的想法(假设通常是正增量值)可能是将其设置为最大值数据类型支持的值。
这应该会导致IDENTITY值的自动生成被破坏,除非有人再次重新播种。
示例(两个 catch 块中返回的错误均为“将 IDENTITY 转换为 int 数据类型时出现算术溢出错误。”)
DROP TABLE IF EXISTS dbo.MyLookupTable
CREATE TABLE dbo.MyLookupTable
(
IdentityColumnOfWoe INT IDENTITY PRIMARY KEY,
SomeValue VARCHAR(30)
)
SET IDENTITY_INSERT dbo.MyLookupTable ON
INSERT dbo.MyLookupTable(IdentityColumnOfWoe, SomeValue)
VALUES (1, 'Explicit value 1'),
(2, 'Explicit value 2');
SET IDENTITY_INSERT dbo.MyLookupTable OFF
DBCC CHECKIDENT ('dbo.MyLookupTable', RESEED, 2147483647);
BEGIN TRY
INSERT dbo.MyLookupTable VALUES ('Bad Insert')
END TRY
BEGIN CATCH
SELECT ERROR_MESSAGE()
END CATCH
SET IDENTITY_INSERT dbo.MyLookupTable ON
INSERT dbo.MyLookupTable(IdentityColumnOfWoe, SomeValue)
VALUES (3, 'Explicit value 3');
SET IDENTITY_INSERT dbo.MyLookupTable OFF
BEGIN TRY
INSERT dbo.MyLookupTable VALUES ('Still broken?')
END TRY
BEGIN CATCH
SELECT ERROR_MESSAGE()
END CATCH
Run Code Online (Sandbox Code Playgroud)
对此的一个变体(取决于数据类型提供了多少备用容量)可能是为错误插入保留空间并使用检查约束阻止它们。这可以提供更多信息的错误消息。
ALTER TABLE dbo.MyLookupTable ADD CONSTRAINT CK_MyLookupTable_AlwaysUseSetIdentityInsert CHECK (IdentityColumnOfWoe <= 2000000000)
DBCC CHECKIDENT ('dbo.MyLookupTable', RESEED, 2000000000);
Run Code Online (Sandbox Code Playgroud)
消息 547,级别 16,状态 0,第 21 行 INSERT 语句与 CHECK 约束“CK_MyLookupTable_AlwaysUseSetIdentityInsert”冲突。冲突发生在数据库“Test”、表“dbo.MyLookupTable”、列“IdentityColumnOfWoe”中。
另一种选择是使用而不是触发器:
CREATE TABLE dbo.Regret
(
i integer IDENTITY(1, 1) NOT NULL
CONSTRAINT [PK dbo.Regret i]
PRIMARY KEY CLUSTERED,
v integer NOT NULL
);
CREATE TABLE dbo.Consequence
(
ri integer NOT NULL
CONSTRAINT [FK dbo.Regret i]
FOREIGN KEY (ri)
REFERENCES dbo.Regret (i)
ON DELETE CASCADE
ON UPDATE CASCADE,
c integer NOT NULL
);
Run Code Online (Sandbox Code Playgroud)
CREATE OR ALTER TRIGGER MustUseIdentityInsert
ON dbo.Regret
INSTEAD OF INSERT
AS
IF @@ROWCOUNT = 0 RETURN;
SET NOCOUNT, XACT_ABORT ON;
SET ROWCOUNT 0;
INSERT dbo.Regret
(i, v)
SELECT
-- The identity column arrives containing zero if
-- an explicit value wasn't provided by the original statement
NULLIF(I.i, 0), I.v
FROM Inserted AS I;
GO
Run Code Online (Sandbox Code Playgroud)
测试:
--Msg 544, Level 16, State 1, Procedure MustUseIdentityInsert
--Cannot insert explicit value for identity column in table 'Regret'
--when IDENTITY_INSERT is set to OFF.
INSERT dbo.Regret (v)
VALUES (1);
--Msg 544, Level 16, State 1
--Cannot insert explicit value for identity column in table 'Regret'
--when IDENTITY_INSERT is set to OFF.
INSERT dbo.Regret (i, v)
VALUES (1, 1);
SET IDENTITY_INSERT dbo.Regret ON;
--Msg 515, Level 16, State 2, Procedure MustUseIdentityInsert
--Cannot insert the value NULL into column 'i', table 'Sandpit.dbo.Regret';
--column does not allow nulls. INSERT fails.
INSERT dbo.Regret (v)
VALUES (1);
Run Code Online (Sandbox Code Playgroud)
-- Success (with IDENTITY_INSERT still on)
INSERT dbo.Regret
(i, v)
VALUES
(1, 1),
(2, 2);
-- Also succeeds
MERGE dbo.Regret AS R
USING
(
SELECT SV.number
FROM master.dbo.spt_values AS SV
WHERE SV.[type] = N'P'
AND SV.number > 0
) AS U (n)
ON U.n = R.i
WHEN NOT MATCHED THEN
INSERT (i, v)
VALUES (U.n, CHECKSUM(NEWID()));
SET IDENTITY_INSERT dbo.Regret OFF;
Run Code Online (Sandbox Code Playgroud)
这一想法的一个缺点是您不能使用显式的身份值零。
我可能更喜欢其中一种重新播种方法,但这是另一种选择。
这是一种痛苦,需要直接的语言支持。
在某些涉及大型桌子的情况下,使用众所周知的程序可以减少创伤SWITCH。这并不真正适用于您的情况,因为查找表很小而引用表很大。重要的是,重新创建外键将需要一段时间并保持Sch-M锁定,从而阻止两个表上的所有其他活动。
尽管如此:
IF COLUMNPROPERTY(OBJECT_ID(N'dbo.Regret', 'U'), N'i', 'IsIdentity') = 1
BEGIN TRY
BEGIN TRANSACTION;
DROP TABLE IF EXISTS dbo.NoRegret;
-- Switch target with a compatible schema but no IDENTITY
CREATE TABLE dbo.NoRegret
(
i integer NOT NULL
CONSTRAINT [PK dbo.NoRegret i]
PRIMARY KEY,
v integer NOT NULL
);
-- Drop the FK
ALTER TABLE dbo.Consequence
DROP CONSTRAINT [FK dbo.Regret i];
-- Metadata-only switch
ALTER TABLE dbo.Regret
SWITCH TO dbo.NoRegret;
-- Don't need the original now
DROP TABLE dbo.Regret;
-- Fix up the names
EXECUTE sys.sp_rename
@objname = N'dbo.NoRegret',
@newname = N'Regret',
@objtype = 'OBJECT';
EXECUTE sys.sp_rename
@objname = N'[PK dbo.NoRegret i]',
@newname = N'[PK dbo.Regret i]',
@objtype = 'OBJECT';
-- Add the FK back
ALTER TABLE dbo.Consequence
WITH CHECK ADD
CONSTRAINT [FK dbo.Regret i]
FOREIGN KEY (ri)
REFERENCES dbo.Regret;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
THROW;
END CATCH;
Run Code Online (Sandbox Code Playgroud)
最后一点是,当涉及到一些重定向时,更改事情会更容易。在您的情况下,部署机制与数据库物理设计(身份属性)紧密耦合。
如果没有对表的直接数据访问,并且所有数据更改都通过存储过程进行,那么进行此更改会更容易。对存储过程代码添加检查或更改通常很简单。
在理想的情况下,部署脚本通常应该能够针对任何当前状态运行(和重新运行),使代码和数据库达到相同的最终状态。毫无疑问你知道这一切。
| 归档时间: |
|
| 查看次数: |
1420 次 |
| 最近记录: |