Campo identificador exclusivo com um condicional

8

Eu tenho um banco de dados que não está em produção, portanto, a tabela principal é CustodyDetails, esta tabela possui uma ID int IDENTITY(1,1) PRIMARY KEYcoluna e estou procurando uma maneira de adicionar outro identificador exclusivo que não seja referenciado em nenhuma outra tabela. conta, o conteúdo da coluna não seria exatamente uma chave de identidade.

Essa nova coluna de identidade possui alguns detalhes específicos e é aqui que o meu problema começa. O formato é o seguinte: XX/YYonde XX é um valor incremental automático que redefine / reinicia a cada novo ano e YY são os últimos 2 dígitos do ano atual SELECT RIGHT(YEAR(GETDATE()), 2).

Assim, por exemplo, vamos fingir que um registro é adicionado por dia a partir de 28/12/2015, terminando em 01/01/2016 , a coluna teria a seguinte aparência:

ID    ID2     DATE_ADDED
1     1/15    2015-12-28
2     2/15    2015-12-29
3     3/15    2015-12-30
4     4/15    2015-12-31
5     1/16    2016-01-01
6     2/16    2016-01-02
7     3/16    2016-01-03

Pensei em usar o front-end para analisar o ID composto (ID2 no exemplo), obter os dois últimos dígitos e comparar com os últimos dois dígitos do ano atual e depois decidir se devo ou não iniciar um novo correlativo. É claro que seria ótimo poder fazer tudo isso no lado do banco de dados.

EDIT 1: btw, eu também vi pessoas usando tabelas separadas apenas para armazenar chaves de identidade paralelas, então uma tabela Chave de identidade se torna uma segunda chave secundária da tabela, isso soa um pouco desonesto, mas talvez seja esse o caso dessa implementação?

EDIT 2: Esse ID extra é uma referência de documento legado que rotula todos os arquivos / registros. Eu acho que alguém poderia pensar nisso como um alias especial para o ID principal.

O número de registros que este banco de dados manipula anualmente não foi dos 100 nos últimos 20 anos e é altamente (realmente, extremamente altamente) improvável que seria, é claro, se ultrapassar os 99, o campo será capaz de continue com o dígito extra e o frontend / procedimento poderá ultrapassar 99, por isso não é como se isso mudasse as coisas.

É claro que alguns desses detalhes que eu não mencionei no início, porque apenas restringiriam as possibilidades de solução para atender às minhas necessidades específicas, tentaram manter o alcance do problema mais amplo.

Nelz
fonte
Qual é a versão do SQL Server?
Max Vernon
Por que isso precisa ser armazenado na tabela, se não deve ser usado como referência em nenhum lugar? Por que não pode ser uma coluna computada (persistida ou calculada em uma consulta, quando necessário)? O que deve acontecer se você tiver mais de 100 linhas em um ano?
ypercubeᵀᴹ
11
Com ID= 5, 6 e 7, o DATE_ADDED deve ser 2016-01-01 e assim por diante?
Kin Shah
@Kin parece com isso. Corrigi a amostra.
ypercubeᵀᴹ
Obrigado pela correção, sim, eram recs de 2016 e seu SQL Server 2005 estou usando agora. @ YperSillyCubeᵀᴹ É principalmente uma questão de encontrar uma solução melhor ; portanto, qualquer sugestão seria apreciada.
Nelz

Respostas:

6

Você pode usar uma tabela de chaves para armazenar a parte de incremento da sua segunda coluna de ID. Esta solução não depende de nenhum código do lado do cliente e tem reconhecimento automático de vários anos; quando o @DateAddedparâmetro passa em um ano não utilizado anteriormente, ele começará automaticamente a usar um novo conjunto de valores para a ID2coluna, com base nesse ano. Se o proc for usado consequentemente para inserir linhas de anos anteriores, essas linhas serão inseridas com valores "corretos" para o incremento. O GetNextID()proc é voltado para lidar com possíveis bloqueios graciosamente, transmitindo apenas um erro ao chamador se ocorrerem 5 bloqueios sequenciais ao tentar atualizar a tblIDstabela.

Crie uma tabela para armazenar uma linha por ano contendo o valor de ID usado atualmente, juntamente com um procedimento armazenado para retornar o novo valor a ser usado:

CREATE TABLE [dbo].[tblIDs]
(
    IDName nvarchar(255) NOT NULL,
    LastID int NULL,
    CONSTRAINT [PK_tblIDs] PRIMARY KEY CLUSTERED 
    (
        [IDName] ASC
    ) WITH 
    (
        PAD_INDEX = OFF
        , STATISTICS_NORECOMPUTE = OFF
        , IGNORE_DUP_KEY = OFF
        , ALLOW_ROW_LOCKS = ON
        , ALLOW_PAGE_LOCKS = ON
        , FILLFACTOR = 100
    ) 
);
GO

CREATE PROCEDURE [dbo].[GetNextID](
    @IDName nvarchar(255)
)
AS
BEGIN
    /*
        Description:    Increments and returns the LastID value from
                        tblIDs for a given IDName
        Author:         Max Vernon / Mike Defehr
        Date:           2012-07-19

    */
    SET NOCOUNT ON;

    DECLARE @Retry int;
    DECLARE @EN int, @ES int, @ET int;
    SET @Retry = 5;
    DECLARE @NewID int;
    WHILE @Retry > 0
    BEGIN
        SET @NewID = NULL;
        BEGIN TRY
            UPDATE dbo.tblIDs 
            SET @NewID = LastID = LastID + 1 
            WHERE IDName = @IDName;

            IF @NewID IS NULL
            BEGIN
                SET @NewID = 1;
                INSERT INTO tblIDs (IDName, LastID) 
                VALUES (@IDName, @NewID);
            END
            SET @Retry = -2; /* no need to retry since the 
                                  operation completed */
        END TRY
        BEGIN CATCH
            IF (ERROR_NUMBER() = 1205) /* DEADLOCK */
                SET @Retry = @Retry - 1;
            ELSE
                BEGIN
                SET @Retry = -1;
                SET @EN = ERROR_NUMBER();
                SET @ES = ERROR_SEVERITY();
                SET @ET = ERROR_STATE()
                RAISERROR (@EN,@ES,@ET);
                END
        END CATCH
    END
    IF @Retry = 0 /* must have deadlock'd 5 times. */
    BEGIN
        SET @EN = 1205;
        SET @ES = 13;
        SET @ET = 1
        RAISERROR (@EN,@ES,@ET);
    END
    ELSE
        SELECT @NewID AS NewID;
END
GO

Sua tabela, juntamente com um proc para inserir linhas nela:

CREATE TABLE dbo.Cond
(
    CondID INT NOT NULL
        CONSTRAINT PK_Cond
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , CondID2 VARCHAR(30) NOT NULL
    , Date_Added DATE NOT NULL
);

GO
CREATE PROCEDURE dbo.InsertCond
(
    @DateAdded DATE
)
AS
BEGIN
    DECLARE @NextID INT;
    DECLARE @Year INT;
    DECLARE @IDName NVARCHAR(255);
    SET @Year = DATEPART(YEAR, @DateAdded);
    DECLARE @Res TABLE
    (
        NextID INT NOT NULL
    );
    SET @IDName = 'Cond_' + CONVERT(VARCHAR(30), @Year, 0);
    INSERT INTO @Res (NextID)
    EXEC dbo.GetNextID @IDName;

    INSERT INTO dbo.Cond (CondID2, Date_Added)
    SELECT CONVERT(VARCHAR(30), NextID) + '/' + 
        SUBSTRING(CONVERT(VARCHAR(30), @Year), 3, 2), @DateAdded
    FROM @Res;
END
GO

Insira alguns dados de amostra:

EXEC dbo.InsertCond @DateAdded = '2015-12-30';
EXEC dbo.InsertCond @DateAdded = '2015-12-31';
EXEC dbo.InsertCond @DateAdded = '2016-01-01';
EXEC dbo.InsertCond @DateAdded = '2016-01-02';

Mostrar as duas tabelas:

SELECT *
FROM dbo.Cond;

SELECT *
FROM dbo.tblIDs;

Resultados:

insira a descrição da imagem aqui

A tabela de chaves e o procedimento armazenado vêm dessa pergunta.

Max Vernon
fonte
Você está definindo o nível de isolamento da transação, mas não está abrindo explicitamente uma transação. Além disso, se duas sessões simultâneas tentassem inserir a mesma (IDName, LastID)linha, isso resultaria em um impasse ou em uma das transações violando o PK? Nesse último caso, talvez faça sentido dar outra chance a essa transação (para que, eventualmente, obtenha o ID 2).
Andriy M
E outra coisa, eu provavelmente definiria @NewIDexplicitamente como nulo no início do loop: se a transação que tenta inserir uma linha se tornar uma vítima de impasse, ela não tentará inserir uma linha na próxima iteração, porque @NewIDjá terá foi definido como 1 (que não é NULL e, portanto, a ramificação INSERT será omitida).
Andriy M
Na verdade, o nível de isolamento da transação não precisa ser definido; Vou removê-lo. Não vejo como duas sessões simultâneas poderiam inserir o mesmo valor na tblIDstabela, pois essa tabela é atualizada por uma única operação atômica; a atualização "peculiar".
Max Vernon
Não é uma má idéia definir @NewID = NULLno início do loop.
Max Vernon
Suponho que teoricamente a primeira ação para um novo ano possa ser suscetível a um impasse.
Max Vernon