'Id' com o formato: YYYYNNNNNN com a parte NNNNNN reiniciando a cada ano

11

Tenho um requisito comercial de que cada registro na tabela Fatura tenha um ID parecido com YYYYNNNNNN.

A parte NNNNNN precisa ser reiniciada no início de cada ano. Portanto, a primeira linha inserida em 2016 se pareceria com 2016000001 e a segunda como 2016000002 etc. Vamos dizer que o último registro para 2016 foi 2016123456, a próxima linha (de 2017) deve ser 2017000001

Não preciso que esse ID seja a chave primária e também armazeno a data de criação. A ideia é que esse 'ID de exibição' seja único (para que eu possa consultar) e capaz de agrupar pessoas por ano.

É improvável que quaisquer registros sejam excluídos; no entanto, eu estaria inclinado a codificar defensivamente contra algo assim.

Existe alguma maneira de criar esse ID sem precisar consultar o ID máximo este ano toda vez que inserir uma nova linha?

Ideias:

  • A CreateNewInvoiceSP, que obtém o MAXvalor para esse ano (nojento)
  • Algum recurso mágico incorporado para fazer exatamente isso (eu posso sonhar direito)
  • Ser capaz de especificar alguma UDF ou algo na declaração IDENTITYou DEFAULT(??)
  • Uma exibição que usa PARTITION OVER + ROW()(excluída seria problemática)
  • Um gatilho ativado INSERT(ainda seria necessário executar uma MAXconsulta :()
  • Um trabalho anual anterior, atualizava uma tabela com o MAX para cada ano inserido, que eu então ... Algo ?!

Todos os quais são um pouco não ideais. Todas as idéias ou variações são bem-vindas!

DarcyThomas
fonte
Você tem boas respostas, mas se você tiver um ano, identifique-se como PK e selecione max é muito rápido.
Paparazzo
o uso de uma consulta de seleção de ID máximo é uma prática comum. Use isso.
Uğur Gümüşhan 29/04

Respostas:

17

Existem 2 elementos no seu campo

  • Ano
  • Um número de incremento automático

Eles não precisam ser armazenados como um campo

Exemplo:

  • Uma coluna do ano que possui um padrão de YEAR(GETDATE())
  • Uma coluna numérica com base em uma sequência.

Em seguida, crie uma coluna computada concatenando-os (com formatação apropriada). A sequência pode ser redefinida na mudança de ano.

Código de exemplo no SQLfiddle : * (o SQLfiddle nem sempre 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
fonte
1
Talvez seja mais limpo ter uma sequência por ano. Dessa forma, não há necessidade de executar DDL como parte de operações regulares.
usr
@gbn Então, eu precisaria de um trabalho em segundo plano para reiniciar SEQUENCE no início de cada ano?
precisa saber é o seguinte
@usr Infelizmente você não pode usar NEXT VALUE FORem uma CASEdeclaração (eu tentei)
DarcyThomas
8

Você considerou criar um campo de identidade com seed = 2016000000?

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

Essa semente deve ser incrementada automaticamente a cada ano. Por exemplo, na noite de 2017/1/1, você precisa agendar

DBCC CHECKIDENT (Table1, RESEED, 2017000000)

Mas já vejo problemas com o design, por exemplo: e se você tiver milhões de registros?

Liya Tansky
fonte
2
Outro problema é se os registros não aparecerem cronologicamente. A identidade provavelmente não é o caminho a seguir, se for esse o caso.
Daniel Hutmacher
@LiyaTansky No meu caso, disseram-me que deveriam ser apenas 50 mil registros por ano. Mas eu tenho o que você quer dizer sobre ele ser frágil com 1kk linhas
DarcyThomas
1

O que eu fiz nesse cenário foi multiplicar o ano por 10 ^ 6 e adicionar o valor da sequência a isso. Isso tem a vantagem de não exigir um campo computado com sua sobrecarga contínua (pequena) e o campo pode ser usado como um PRIMARY KEY.

Existem duas dicas possíveis:

  • verifique se o seu multiplicador é suficientemente grande para nunca se esgotar e

  • você não tem uma sequência garantida sem lacunas devido ao armazenamento em cache da sequência.

Eu não sou especialista em SQL Server, mas você provavelmente pode definir um evento para disparar em 201x 00:00:00 para redefinir sua sequência para zero. Também foi o que fiz no Firebird (ou foi o Interbase?).

Vérace
fonte
1

Edit: Esta solução não funciona sob carga

Eu não sou fã de gatilhos, mas parece melhor que eu poderia malhar.

Prós:

  • Nenhum trabalho em segundo plano
  • Pode fazer consultas rápidas no DisplayId
  • O gatilho não precisa procurar a parte NNNNNN anterior
  • Reiniciará a parte NNNNN todos os anos
  • Funcionará se houver mais de 100000 linhas por ano
  • Não requer atualizações de esquema (por exemplo, redefinições de sequência) para continuar trabalhando no futuro

Editar: Contras:

  • Falhará sob carga (de volta à prancheta)

(Crédito para @gbn quando me inspirei na resposta deles) (Qualquer feedback e apontando os erros óbvios são bem-vindos :)

Adicionar alguns novos COLUMNs e umINDEX

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)')

Adicione o novo 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
fonte
Eu recomendo não fazer isso. É provável que entre em conflito e cause falhas de inserção depois de ficar com pouca carga. Você colocou uma cópia em um banco de dados fictício e o martelou com algumas dezenas de threads ao mesmo tempo fazendo inserções (e talvez também selecione / atualize / exclua) para ver o que acontece?
Cody Konior # 1942
@CodyKonior é fundamentalmente falho ou pode ser ressuscitado com um pouco de bloqueio criterioso? Se não, como você abordaria o problema?
precisa saber é o seguinte
Hummm. Ran com 10 threads. Não tenho certeza se são bloqueios, mas recebo algumas condições de corrida. Onde um gatilho é concluído, antes que o gatilho das linhas anteriores termine. Isso leva à inserção de vários NULLvalores. Voltar à prancheta de desenho ...
DarcyThomas
O desastre foi evitado :-) Meu segredo é que reconheci o padrão por algo que fiz cerca de cinco anos atrás. Só sei que a maneira como você escaneia a tabela dentro do gatilho, procurando a próxima sequência, atrapalha as coisas. Não me lembro de como resolvi, mas posso verificar mais tarde.
perfil completo de Cody Konior
@CodyKonior Eu não acho que ele está fazendo uma varredura ( ON Previous.Id = (I.Id -1) deve apenas procurar), mas sim ainda não funciona. Se eu pudesse bloquear a tabela (?) Durante a inserção e o gatilho, acho que funcionaria. Mas isso também parece um cheiro de código.
precisa saber é o seguinte