TSQL:从自定义标识创建自定义标识?(管理数据库修订)

6 sql t-sql identity sql-server-2000

我想基于自定义标识创建自定义标识.或者类似于身份的东西,其功能类似于自动递增键.

例如,如果我有一个绘图的主键,我希望它的修订版基于图纸编号.

DRAWING
ID    | REV   | INFO
------+-------+------
1     | 0     | "Draw1"
2     | 0     | "Draw2"
2     | 1     | "Draw2Edit"
2     | 2     | "Draw2MoreEdit"
3     | 0     | "Draw3"
4     | 0     | "Draw4"

如果我要在我的表中插入更多记录,例如:

INSERT INTO DRAWING (INFO) VALUES ("Draw5")
INSERT INTO DRAWING (ID,INFO) VALUES (3,"Draw3Edit")
Run Code Online (Sandbox Code Playgroud)

我的表想:

DRAWING
ID    | REV   | INFO
------+-------+------
1     | 0     | "Draw1"
2     | 0     | "Draw2"
2     | 1     | "Draw2Edit"
2     | 2     | "Draw2MoreEdit"
3     | 0     | "Draw3"
3     | 1     | "Draw3Edit"      --NEW ROW
4     | 0     | "Draw4"
5     | 0     | "Draw5"          --NEW ROW

T-SQL

CREATE TABLE DRAWING
(
    ID INT,
    REV INT,  
    INFO VARCHAR(50),
    PRIMARY KEY (ID,REV)
);

CREATE TABLE CURRENT_DRAWING
(
    ID INT IDENTITY (1,1),
    DRAWING_ID INT,
    DRAWING_REV INT,
    PRIMARY KEY (ID),
    FOREIGN KEY (DRAWING_ID,DRAWING_REV) REFERENCES DRAWING (ID,REV)
        ON UPDATE CASCADE
        ON DELETE CASCADE
);
Run Code Online (Sandbox Code Playgroud)

我正在使用SQL Server Management Studio 2005并在SQL Server 2000数据库上工作.

我也会接受可能的替代方案.主要目标是ID为新图纸自动递增.ID将保持不变,REV将在新的图纸修订中递增.

更新:

我想我接近我想要的东西:

DROP TABLE DRAW

GO

CREATE TABLE DRAW
(
    ID INT DEFAULT(0), 
    REV INT DEFAULT(-1), 
    INFO VARCHAR(10), 
    PRIMARY KEY(ID, REV)
)

GO

CREATE TRIGGER TRIG_DRAW ON DRAW
FOR INSERT
AS
BEGIN
    DECLARE @newId INT,
            @newRev INT,
            @insId INT,
            @insRev INT

    SET TRANSACTION ISOLATION LEVEL READ COMMITTED
    BEGIN TRANSACTION

    SELECT @insId = ID FROM inserted
    SELECT @insRev = REV FROM inserted

    PRINT 'BEGIN TRIG'
    PRINT @insId
    PRINT @insRev
    PRINT @newId
    PRINT @newRev


    --IF ID=0 THEN IT IS A NEW ID
    IF @insId <=0
    BEGIN
        --NEW DRAWING ID=MAX+1 AND REV=0
        SELECT @newId = COALESCE(MAX(ID), 0) + 1 FROM DRAW
        SELECT @newRev = 0
    END
    ELSE
    --ELSE IT IS A NEW REV
    BEGIN
        --CHECK TO ENSURE ID EXISTS
        IF EXISTS(SELECT * FROM DRAW WHERE ID=@insId AND REV=0)
        BEGIN
            PRINT 'EXISTS'
            SELECT @newId = @insId
            SELECT @newRev = MAX(REV) + 1 FROM DRAW WHERE ID=@insID
        END
        ELSE
        --ID DOES NOT EXIST THEREFORE NO REVISION
        BEGIN
            RAISERROR 50000 'ID DOES NOT EXIST.'
            ROLLBACK TRANSACTION
            GOTO END_TRIG
        END
    END

    PRINT 'END TRIG'
    PRINT @insId
    PRINT @insRev
    PRINT @newId
    PRINT @newRev

    SELECT * FROM DRAW

    UPDATE DRAW SET ID=@newId, REV=@newRev WHERE ID=@insId



    COMMIT TRANSACTION
    END_TRIG:
END

GO


INSERT INTO DRAW (INFO) VALUES ('DRAW1')
INSERT INTO DRAW (INFO) VALUES ('DRAW2')
INSERT INTO DRAW (ID,INFO) VALUES (2,'DRAW2EDIT1') --PROBLEM HERE
INSERT INTO DRAW (ID,INFO) VALUES (2,'DRAW2EDIT2')
INSERT INTO DRAW (INFO) VALUES ('DRAW3')
INSERT INTO DRAW (INFO) VALUES ('DRAW4')

GO

--SHOULD THROW
INSERT INTO DRAW (ID,INFO) VALUES (9,'DRAW9')

GO

SELECT * FROM DRAW

GO
Run Code Online (Sandbox Code Playgroud)

但是,我一直在努力Violation of PRIMARY KEY constraint.

我已经放了调试语句,我似乎不太可能违反了我的主键:

BEGIN TRIG
0
-1


END TRIG
0
-1
1
0

(1 row(s) affected)

(1 row(s) affected)

(1 row(s) affected)
BEGIN TRIG
0
-1


END TRIG
0
-1
2
0

(2 row(s) affected)

(1 row(s) affected)

(1 row(s) affected)
BEGIN TRIG
2
-1


EXISTS
END TRIG
2
-1
2
1

(3 row(s) affected)
Msg 2627, Level 14, State 1, Procedure TRIG_DRAW, Line 58
Violation of PRIMARY KEY constraint 'PK__DRAW__56D3D912'. Cannot insert duplicate key in object 'DRAW'.
The statement has been terminated.

它打印

ID  | REV    | INFO
----+--------+------------
1   |   0    |  DRAW1
2   |  -1    |  DRAW2EDIT1  --This row is being updated to 2 1 
2   |   0    |  DRAW2

就在它失败之前,第2行正在更新为2 1.它不应该违反我的主键.

WCW*_*din 3

我实际上会推荐一种替代数据设计。这种键和序列模式很难在关系数据库中正确实现,而且弊大于利。

\n\n

您有很多选择,但最简单的选择是从将表分成两部分开始:

\n\n
CREATE TABLE DRAWING\n(\n    ID INT IDENTITY(1, 1),\n    PRIMARY KEY (ID)\n);\n\nCREATE TABLE DRAWING_REVISION\n(    \n    ID INT IDENTITY(1, 1),\n    DRAWING_ID INT,\n    INFO VARCHAR(50),\n    PRIMARY KEY (ID),\n    CONSTRAINT FK_DRAWING_REVISION_DRAWING FOREIGN KEY (DRAWING_ID) REFERENCES DRAWING(ID)\n);\n
Run Code Online (Sandbox Code Playgroud)\n\n

这样做的好处是可以准确地表示数据并正确工作,而无需您付出额外的努力。DRAWING_REVISION当您想要向工程图添加新修订时,只需向表中添加一行即可。因为主键使用IDENTITY规范,所以您不必执行查找下一个ID.

\n\n

显而易见的解决方案及其缺点

\n\n

但是,如果您需要一个人类可读的修订号,而不是仅用于您的服务器的眼睛ID,则可以通过两种方式完成。它们都首先添加 的REV INT数据定义DRAWING_REVISION以及CONSTRAINT UK_DRAWING_REVISION_DRAWING_ID_REV UNIQUE (DRAWING_ID, REV). 当然,诀窍是找出给定绘图的下一个修订号。

\n\n

如果您希望每个用户只有很少数量的并发用户,您可以简单地SELECT MAX(REV) + 1 FROM DRAWING_REVISION WHERE DRAWING_ID = @DRAWING_ID在应用程序代码中或在INSTEAD OF INSERT触发器中。然而,如果并发性较高或运气不好,用户最终可能会互相阻塞,因为他们可能会尝试将相同的DRAWING_ID和组合REV插入DRAWING_REVISION.

\n\n

一些背景

\n\n

这个问题实际上只有一种解决方案,尽管解释为什么只有一种解决方案需要一点背景信息。考虑以下代码:

\n\n
BEGIN TRAN\n\nINSERT DRAWING DEFAULT VALUES;\nINSERT DRAWING DEFAULT VALUES;\nSELECT ID FROM DRAWING; -- Output: 1, 2\n\nROLLBACK TRAN\n\nBEGIN TRAN\n\nINSERT DRAWING DEFAULT VALUES;\nSELECT ID FROM DRAWING; -- Output: 3\n\nROLLBACK TRAN\n
Run Code Online (Sandbox Code Playgroud)\n\n

当然,后续执行的输出会有所不同。在幕后,SQL Server 正在分发IDENTITY值并递增计数器。如果您从未实际提交该值,则服务器不会尝试“回填”序列 \xe2\x80\x93 中的漏洞,这些值是仅向前提供的。

\n\n

这是一个功能,而不是一个错误。IDENTITY色谱柱的设计是有序且独特的,但不必紧密堆积。保证紧密打包的唯一方法是序列化所有传入请求,确保每个请求在下一个请求开始之前完成或终止;否则,服务器可能会尝试回填IDENTITY半小时前发出的值,结果却导致长时间运行的事务(即该IDENTITY值的初始接收者)提交具有重复主键的行。

\n\n

(值得指出的是,当我说“事务”时,不需要引用 TSQL TRANSACTION,尽管我建议使用它们。它绝对可以是应用程序或 SQL 服务器端上的任何过程,可能会花费任意时间,即使该时间只是下SELECT一个修订号以及紧随其后的INSERT新版本号所需的时间DRAWING_REVISION。)

\n\n

这种回填值的尝试只是变相的序列化,因为在有两个同时INSERT请求的情况下,它会惩罚第二个提交的请求。这迫使最后到达的人再次尝试(可能多次,直到碰巧不存在冲突)。一次有一个成功的提交:序列化,尽管没有队列的好处。

\n\n

SELECT MAX(REV) + 1方法也有同样的缺点。当然,MAX方法不会尝试回填值,但它确实会强制每个并发请求争夺相同的修订号,并获得相同的结果。

\n\n

为什么这样不好?数据库系统是为并行性和通用性而设计的:这种能力是托管数据库相对于平面文件格式的主要优势之一。

\n\n

假装正确

\n\n

那么,经过这么多冗长的阐述,你能做些什么来解决这个问题呢?您可以祈祷自己永远不会看到太多并发用户,但为什么您不希望自己的应用程序得到广泛使用呢?毕竟,你不希望成功成为你的失败。

\n\n

解决方案是执行 SQL Server 所做的操作IDENTITY:将它们分配出去,然后将它们扔到一边。您可以使用类似以下 SQL 代码的内容,或使用等效的应用程序代码:

\n\n
ALTER TABLE DRAWING ADD REV INT NOT NULL DEFAULT(0);\n\nGO\n\nCREATE PROCEDURE GET_REVISION_NUMBER (@DRAWING_ID INT) AS\nBEGIN\n    DECLARE @ATTEMPTS INT;\n    SET @ATTEMPTS = 0;\n    DECLARE @ATTEMPT_LIMIT INT;\n    SET @ATTEMPT_LIMIT = 5;\n    DECLARE @CURRENT_REV INT;\n    LOOP:\n        SET @CURRENT_REV = (SELECT REV FROM DRAWING WHERE DRAWING.ID = @DRAWING_ID);\n        UPDATE DRAWING SET REV = @CURRENT_REV + 1 WHERE DRAWING.ID = @DRAWING_ID AND REV = @CURRENT_REV;\n        SET @ATTEMPTS = @ATTEMPTS + 1;\n        IF (@@ROWCOUNT = 0)\n        BEGIN\n            IF (@ATTEMPTS >= @ATTEMPT_LIMIT) RETURN NULL;\n            GOTO LOOP;\n        END\n    RETURN @CURRENT_REV + 1;\nEND\n
Run Code Online (Sandbox Code Playgroud)\n\n

@@ROWCOUNT 检查非常重要\xe2\x80\x93 这个过程需要是非事务性的,因为你不想隐藏并发请求中的冲突;你想解决它们。确保更新确实完成的唯一方法是检查是否有任何行已更新。

\n\n

当然,您可能已经猜到这种方法并非万无一失。“解决”冲突的唯一方法是在放弃之前尝试几次。没有一种自制解决方案能够与硬编码到数据库服务器软件中的解决方案一样好。但它可以非常接近!

\n\n

存储过程并不能消除冲突,但它确实大大缩短了冲突发生的时间跨度。您不是为待处理的INSERT事务“保留”修订号,而是收到最新的修订号并尽快更新静态计数器,为下一次调用GET_REVISION_NUMBER. (当然,这是序列化的,但仅适用于需要以串行方式执行的过程的极小部分;与许多其他方法不同,算法的其余部分可以自由并行执行。)

\n\n

我的团队使用了与上面概述的解决方案类似的解决方案,我们发现阻塞冲突的发生率下降了几个数量级。我们能够从本地网络上的六台机器连续提交数千个请求,然后其中一台机器最终卡住。

\n\n

卡住的机器陷入了循环,从 SQL 服务器请求新的数字,但总是得到空结果。可以这么说,它无法从边缘插入任何单词。这与本案中的冲突行为类似SELECT MAX,但要少得多。您可以通过保证方法(以及任何相关方法)的连续编号SELECT MAX来换取千倍的可扩展性。这种权衡或多或少是基本的:据我所知,不存在保证连续的、非序列化的解决方案。

\n\n

外卖

\n\n

当然,所有这些混乱都是基于对本地化的半连续数字的需要。如果您可以忍受不太用户友好的版本号,您可以简单地公开DRAWING_REVISION.ID. (不过,如果你问我的话,暴露代理键本身就是令人讨厌的。)

\n\n

这里真正的要点是,自定义标识列比乍一看更难实现,并且任何有一天可能需要可扩展性的应用程序都必须非常小心如何获取新的自定义标识值。

\n