Campo de identificador único con un condicional

8

Tengo una base de datos que no está en producción, por lo que la tabla principal es CustodyDetails, esta tabla tiene una ID int IDENTITY(1,1) PRIMARY KEYcolumna y estoy buscando una forma de agregar otro identificador único al que no se haga referencia en ninguna otra tabla, creo que tomando esto en cuenta que el contenido de la columna no sería exactamente una clave de identidad.

Sin embargo, esta nueva columna de identidad tiene algunos detalles específicos, y aquí es donde comienza mi problema. El formato es el siguiente: XX/YYdonde XX es un valor auto incrementable que se reinicia / reinicia cada año nuevo e YY son los últimos 2 dígitos del año actual SELECT RIGHT(YEAR(GETDATE()), 2).

Entonces, por ejemplo, supongamos que se agrega un registro al día a partir del 28/12/2015 y finaliza el 01/03/2016 , la columna se vería así:

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

Pensé en usar la interfaz para analizar la ID compuesta (ID2 en el ejemplo), obtener los últimos 2 dígitos y compararlos con los últimos 2 dígitos del año actual y luego decidir si iniciar o no un nuevo correlativo. Por supuesto, sería grandioso poder hacerlo todo en el lado de la base de datos.

EDITAR 1: por cierto, también he visto personas que usan tablas separadas solo para almacenar claves de identidad paralelas, por lo que una clave de identidad de tabla se convierte en una segunda clave secundaria de tabla, esto suena un poco dudoso, pero tal vez este sea el caso de que dicha implementación se implemente.

EDITAR 2: Esta identificación adicional es una referencia de documento heredado que etiqueta cada archivo / registro. Supongo que uno podría pensarlo como un alias especial para la ID principal.

La cantidad de registros que maneja esta base de datos anualmente no ha estado fuera de los 100 en los últimos 20 años y es altamente (muy, muy altamente) improbable de lo que sería, por supuesto, si supera los 99, el campo podrá continúe con el dígito adicional y el frontend / procedimiento podrá superar los 99, por lo que no parece que cambie las cosas.

Por supuesto, algunos de estos detalles que no mencioné al principio porque solo reducirían las posibilidades de solución para satisfacer mi necesidad específica, trataron de mantener el rango del problema más amplio.

Nelz
fuente
¿De qué versión de SQL Server se trata?
Max Vernon
¿Por qué esto debe almacenarse en la tabla, si no se va a utilizar como referencia en ningún lado? ¿Por qué no puede ser una columna calculada (ya sea persistente o calculada en una consulta, cuando es necesario)? ¿Qué debería suceder si tiene más de 100 filas en un año?
ypercubeᵀᴹ
1
Con ID= 5, 6 y 7, el DATE_ADDED debería ser 2016-01-01 y así sucesivamente.
Kin Shah
@Kin lo parece. Corrija la muestra.
ypercubeᵀᴹ
Gracias por la corrección, sí, fueron 2016 recs, y su SQL Server 2005 lo estoy usando ahora. @ YperSillyCubeᵀᴹ Es principalmente una cuestión de encontrar una mejor solución, por lo que realmente cualquier sugerencia sería apreciada.
Nelz

Respuestas:

6

Puede usar una tabla de claves para almacenar la parte incremental de su segunda columna de ID. Esta solución no se basa en ningún código del lado del cliente, y es automáticamente consciente de varios años; cuando el @DateAddedparámetro pasa en un año no utilizado previamente, comenzará a usar automáticamente un nuevo conjunto de valores para la ID2columna, en función de ese año. Si, en consecuencia, el proceso se usa para insertar filas de años anteriores, esas filas se insertarán con valores "correctos" para el incremento. El GetNextID()proceso está diseñado para manejar los posibles puntos muertos con gracia, solo pasa un error a la persona que llama si ocurren 5 puntos muertos secuenciales al intentar actualizar la tblIDstabla.

Cree una tabla para almacenar una fila por año que contenga el valor de ID utilizado actualmente, junto con un procedimiento almacenado para devolver el nuevo valor a utilizar:

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

Su tabla, junto con un proceso para insertar filas en ella:

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

Inserte algunos datos de muestra:

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 ambas tablas:

SELECT *
FROM dbo.Cond;

SELECT *
FROM dbo.tblIDs;

Resultados:

ingrese la descripción de la imagen aquí

La tabla de claves y el proceso almacenado provienen de esta pregunta.

Max Vernon
fuente
Está configurando el nivel de aislamiento de transacción pero no está abriendo explícitamente una transacción. Además, si dos sesiones simultáneas intentaran insertar la misma (IDName, LastID)fila, ¿eso resultaría en un punto muerto o en una de las transacciones que viola la PK? Si es lo último, quizás tenga sentido darle a esa transacción otra oportunidad (para que finalmente obtenga la ID de 2).
Andriy M
Y otra cosa, probablemente establecería @NewIDexplícitamente a nulo al comienzo del ciclo: si la transacción que intenta insertar una fila se convierte en una víctima de punto muerto, no intentará insertar una fila en la próxima iteración, porque @NewIDya tendrá se estableció en 1 (que no es NULL, por lo que se omitirá la rama INSERT).
Andriy M
En realidad, no es necesario establecer el nivel de aislamiento de la transacción; Lo quitaré No veo cómo dos sesiones simultáneas podrían insertar el mismo valor en la tblIDstabla, ya que esa tabla se actualiza mediante una sola operación atómica; la actualización "peculiar".
Max Vernon
No es una mala idea configurarlo @NewID = NULLal comienzo del ciclo.
Max Vernon
Supongo que, en teoría, la primera acción para un nuevo año podría ser susceptible a un punto muerto.
Max Vernon