'Id' con el formato: YYYYNNNNNN con la parte NNNNNN reiniciando cada año

11

Tengo un requisito comercial de que cada registro en la tabla Factura tenga una identificación que se parezca a YYYYNNNNNN.

La parte NNNNNN necesita reiniciarse al comienzo de cada año. Entonces, la primera fila ingresada en 2016 se vería como 2016000001 y la segunda como 2016000002, etc. Digamos que el último registro para 2016 fue 2016123456, La siguiente fila (de 2017) debería verse como 2017000001

No necesito esta identificación para ser la clave principal y también almaceno la fecha de creación. La idea es que este 'ID de pantalla' es único (por lo que puedo consultar por él) y apto para grupos humanos, por año.

Es poco probable que se eliminen los registros; sin embargo, me inclinaría a codificar defensivamente contra algo así.

¿Hay alguna forma de que pueda crear esta identificación sin tener que consultar la identificación máxima este año cada vez que inserte una nueva fila?

Ideas:

  • A CreateNewInvoiceSP, que obtiene el MAXvalor para ese año (asqueroso)
  • Alguna característica mágica incorporada para hacer exactamente esto (puedo soñar bien)
  • Ser capaz de especificar algún UDF o algo en la declaración IDENTITYo DEFAULT(??)
  • Una vista que usa PARTITION OVER + ROW()(eliminada sería problemática)
  • Un activador activado INSERT(aún necesitaría ejecutar alguna MAXconsulta :()
  • Un trabajo de fondo anual, actualizó una tabla con el MAX para cada año insertado que luego ...

Todos los cuales son un poco no ideales. Sin embargo, cualquier idea o variación es bienvenida.

DarcyThomas
fuente
Tiene algunas buenas respuestas, pero si tiene año, id como PK, seleccione max es bastante rápido.
paparazzo el
El uso de una consulta select max id es una práctica común. usa eso.
Uğur Gümüşhan

Respuestas:

17

Hay 2 elementos en su campo.

  • Año
  • Un número de incremento automático

No necesitan ser almacenados como un solo campo

Ejemplo:

  • Una columna de año que tiene un valor predeterminado de YEAR(GETDATE())
  • Una columna numérica basada en una secuencia.

Luego cree una columna calculada que los concatene (con el formato adecuado). La secuencia se puede restablecer en el cambio de año.

Código de muestra en SQLfiddle : * (SQLfiddle no siempre funciona)

-- Create a sequence
CREATE SEQUENCE CountBy1
    START WITH 1
    INCREMENT BY 1 ;

-- Create a table
CREATE TABLE Orders
    (Yearly int NOT NULL DEFAULT (YEAR(GETDATE())),
    OrderID int NOT NULL DEFAULT (NEXT VALUE FOR CountBy1),
    Name varchar(20) NOT NULL,
    Qty int NOT NULL,
    -- computed column
    BusinessOrderID AS RIGHT('000' + CAST(Yearly AS VARCHAR(4)), 4)
                     + RIGHT('00000' + CAST(OrderID AS VARCHAR(6)), 6),
    PRIMARY KEY (Yearly, OrderID)
    ) ;


-- Insert two records for 2015
INSERT INTO Orders (Yearly, Name, Qty)
    VALUES
     (2015, 'Tire', 7),
     (2015, 'Seat', 8) ;


-- Restart the sequence (Add this also to an annual recurring 'Server Agent' Job)
ALTER SEQUENCE CountBy1
    RESTART WITH 1 ;

-- Insert three records, this year.
INSERT INTO Orders (Name, Qty)
    VALUES
     ('Tire', 2),
     ('Seat', 1),
     ('Brake', 1) ;
gbn
fuente
1
Quizás sea más limpio tener una secuencia por año. De esa manera no hay necesidad de ejecutar DDL como parte de las operaciones regulares.
usr
@gbn Entonces, ¿necesitaría un trabajo en segundo plano para reiniciar SEQUENCE al comienzo de cada año?
DarcyThomas
@ usr Lamentablemente no puedes usarlo NEXT VALUE FORen una CASEdeclaración (lo intenté)
DarcyThomas
8

¿Consideró crear un campo de identidad con seed = 2016000000?

 create table Table1 (
   id bigint identity(2016000000,1),
   field1 varchar(20)...
)

Esta semilla debe autoincrementarse cada año, por ejemplo, en la noche de 2017/1/1 debe programar

DBCC CHECKIDENT (Table1, RESEED, 2017000000)

Pero ya veo problemas con el diseño, por ejemplo: ¿qué pasa si tienes millones de registros?

Liya Tansky
fuente
2
Otro problema es si los registros no aparecen cronológicamente. La identidad probablemente no sea el camino a seguir si este es el caso.
Daniel Hutmacher
@LiyaTansky En mi caso, me dijeron que solo debería haber 50k registros por año. Pero entiendo lo que quieres decir con que sea frágil con filas de
1kk
1

Lo que hice en este escenario fue multiplicar el año por 10 ^ 6 y agregarle el valor de secuencia. Esto tiene la ventaja de no requerir un campo calculado con su sobrecarga continua (pequeña) y el campo se puede usar como un PRIMARY KEY.

Hay dos posibles trucos:

  • asegúrese de que su multiplicador sea lo suficientemente grande para que nunca se agote, y

  • no se garantiza una secuencia sin espacios debido al almacenamiento en caché de la secuencia.

No soy un experto en SQL Server, pero probablemente pueda configurar un evento para que se active a 201x 00:00:00 para restablecer su secuencia a cero. Eso también fue lo que hice en Firebird (¿o fue Interbase?).

Vérace
fuente
1

Editar: esta solución no funciona bajo carga

No soy un fanático de los desencadenantes, pero esto parece mejor que podría resolver.

Pros:

  • No hay trabajos en segundo plano
  • Puede hacer consultas rápidas en el DisplayId
  • El disparador no necesita buscar la parte NNNNNN anterior
  • Reiniciará la parte NNNNN cada año.
  • Funcionará si hay más de 100000 filas por año.
  • No requiere actualizaciones de esquema (por ejemplo, restablecimientos de secuencia) para seguir trabajando en el futuro

Editar: Contras:

  • Fallará bajo carga (de vuelta al tablero de dibujo)

(Gracias a @gbn porque me inspiré en su respuesta) (Cualquier comentario y señalar los errores obvios son bienvenidos :)

Agregue algunos nuevos COLUMNsy unINDEX

ALTER TABLE dbo.Invoices
ADD     [NNNNNNId]      INT  NULL 

ALTER TABLE dbo.Invoices
ADD [Year]              int NOT NULL DEFAULT (YEAR(GETDATE()))

ALTER TABLE dbo.Invoices
ADD [DisplayId]     AS  'INV' +
                        CAST([Year] AS VARCHAR(4))+
                        RIGHT('00000' + CAST([NNNNNNId] AS VARCHAR(4)),  IIF (5  >= LEN([NNNNNNId]), 5, LEN([NNNNNNId])) )                  

EXEC('CREATE NONCLUSTERED INDEX IX_Invoices_DisplayId
ON dbo.Invoices (DisplayId)')

Agrega el nuevo TRIGGER

CREATE TRIGGER Invoices_DisplayId
ON dbo.Invoices
  AFTER  INSERT
AS 
BEGIN

SET NOCOUNT ON;    

UPDATE dbo.Invoices
SET NNNNNNId = CalcDisplayId
FROM (SELECT I.ID, IIF (Previous.Year = I.Year , (ISNULL(Previous.NNNNNNId,0) + 1), 1) AS CalcDisplayId  FROM
        (SELECT 
            ID  
           ,NNNNNNId 
           ,[year]
        FROM  dbo.Invoices
        ) AS Previous
    JOIN inserted AS I 
    ON Previous.Id = (I.Id -1) 
    ) X
WHERE 
   X.Id = dbo.Invoices.ID       
END
GO
DarcyThomas
fuente
Recomiendo no hacer esto. Es probable que se bloquee y cause fallas en la inserción una vez que se encuentre bajo carga ligera. ¿Puso una copia en una base de datos ficticia y la martilló con unas pocas docenas de hilos a la vez haciendo inserciones (y tal vez selecciona / actualiza / elimina también) para ver qué sucede?
Cody Konior
@CodyKonior ¿Es fundamentalmente defectuoso o podría resucitar con un poco de bloqueo juicioso? Si no, ¿cómo abordarías el problema?
DarcyThomas
Hmmm Funcionó con 10 hilos. No estoy seguro de si son cerraduras muertas, pero tengo algunas condiciones de carrera. Donde se completa un activador, antes de que finalice el activador de filas anteriores. Esto lleva a que se ingresen varios NULLvalores. De vuelta al tablero de dibujo ...
DarcyThomas
Desastre evitado entonces :-) Mi secreto es que reconocí el patrón de algo que hice hace unos cinco años. Solo sé que la forma en que escanea la tabla dentro del gatillo buscando la siguiente secuencia hace que las cosas se descarguen bajo carga. No recuerdo cómo lo resolví, pero puedo verificarlo más tarde.
Cody Konior
@CodyKonior No creo que esté haciendo un escaneo ( ON Previous.Id = (I.Id -1) solo debería buscar), pero sí, todavía no funciona. Si pudiera bloquear la mesa (?) Durante la inserción y el disparo, creo que funcionaría. Pero eso también suena como un olor a código.
DarcyThomas