'Id' au format: YYYYNNNNNN avec la partie NNNNNN recommençant chaque année


11

J'ai une exigence commerciale selon laquelle chaque enregistrement dans la table des factures a un identifiant qui ressemble à YYYYNNNNNN.

La partie NNNNNN doit redémarrer au début de chaque année. Ainsi, la première ligne entrée en 2016 ressemblerait à 2016000001 et la seconde à 2016000002 etc. Disons que le dernier record pour 2016 était 2016123456, la ligne suivante (de 2017) devrait ressembler à 2017000001

Je n'ai pas besoin que cet identifiant soit la clé primaire et je stocke également la date de création. L'idée est que cet «ID d'affichage» est unique (donc je peux l'interroger) et qu'il peut être groupé humain, par année.

Il est peu probable que des enregistrements soient supprimés; cependant, je serais enclin à coder défensivement contre quelque chose comme ça.

Existe-t-il un moyen de créer cet identifiant sans avoir à demander l'id max cette année à chaque fois que vous insérez une nouvelle ligne?

Idées:

  • A CreateNewInvoiceSP, qui obtient la MAXvaleur de cette année (ouais)
  • Quelques fonctionnalités magiques intégrées pour faire exactement cela (je peux rêver correctement)
  • Pouvoir spécifier un UDF ou quelque chose dans la déclaration IDENTITYor DEFAULT(??)
  • Une vue qui utilise PARTITION OVER + ROW()(supprimée serait problématique)
  • Un déclencheur activé INSERT(aurait encore besoin d'exécuter une MAXrequête :()
  • Un travail de fond annuel, mis à jour un tableau avec le MAX pour chaque année inséré que j'ai ensuite ... Quelque chose?!

Tout cela est un peu non idéal. Toutes les idées ou variations sont les bienvenues!


Vous avez de bonnes réponses, mais si vous avez l'année, id en tant que PK, puis sélectionnez max est assez rapide.
paparazzo

l'utilisation d'une requête select max id est une pratique courante. Utiliser ça.
Uğur Gümüşhan

Réponses:


17

Il y a 2 éléments dans votre domaine

  • Année
  • Un numéro d'incrémentation automatique

Ils n'ont pas besoin d'être stockés dans un seul champ

Exemple:

  • Une colonne d'année dont la valeur par défaut est YEAR(GETDATE())
  • Une colonne numérique basée sur une séquence.

Créez ensuite une colonne calculée les concaténant (avec un formatage approprié). La séquence peut être réinitialisée en cas de changement d'année.

Exemple de code dans SQLfiddle : * (SQLfiddle ne fonctionne pas toujours)

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

1
C'est peut-être plus propre d'avoir une séquence par an. De cette façon, il n'est pas nécessaire d'exécuter DDL dans le cadre d'opérations régulières.
usr

@gbn Donc, aurais-je besoin d'un travail d'arrière-plan pour redémarrer le SEQUENCE au début de chaque année?
DarcyThomas

@usr Malheureusement, vous ne pouvez pas utiliser NEXT VALUE FORdans une CASEdéclaration (j'ai essayé)
DarcyThomas

8

Avez-vous envisagé de créer un champ d'identité avec seed = 2016000000?

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

Cette graine doit être auto-incrémentée chaque année, par exemple la nuit du 2017/1/1, vous devez planifier

DBCC CHECKIDENT (Table1, RESEED, 2017000000)

Mais je vois déjà des problèmes avec la conception, par exemple: que faire si vous avez des millions d'enregistrements?


2
Un autre problème est que les enregistrements n'apparaissent pas chronologiquement. L'identité n'est probablement pas la voie à suivre si tel est le cas.
Daniel Hutmacher

@LiyaTansky Dans mon cas, on m'a dit qu'il ne devrait y avoir que 50 000 enregistrements par an. Mais je comprends ce que vous entendez par être fragile avec 1kk de lignes
DarcyThomas

1

Ce que j'ai fait dans ce scénario était de multiplier l'année par 10 ^ 6 et d'y ajouter la valeur de séquence. Cela a l'avantage de ne pas nécessiter de champ calculé avec son (petit) surcoût continu et le champ peut être utilisé comme a PRIMARY KEY.

Il y a deux pièges possibles:

  • assurez-vous que votre multiplicateur est suffisamment grand pour ne jamais être épuisé, et

  • vous n'êtes pas assuré d'une séquence sans lacunes en raison de la mise en cache de la séquence.

Je ne suis pas un expert de SQL Server, mais vous pouvez probablement définir un événement à déclencher à 201x 00:00:00 pour réinitialiser votre séquence à zéro. C'est aussi ce que j'ai fait sur Firebird (ou était-ce Interbase?).


1

Edit: Cette solution ne fonctionne pas sous charge

Je ne suis pas un fan des déclencheurs, mais cela semble mieux que je puisse travailler.

Avantages:

  • Aucun emploi d'arrière-plan
  • Peut faire des requêtes rapides sur le DisplayId
  • Le déclencheur n'a pas besoin de rechercher la partie NNNNNN précédente
  • Redémarrera la partie NNNNN chaque année
  • Fonctionnera s'il y a plus de 100 000 lignes par an
  • Ne nécessite pas de mises à jour de schéma (par exemple, des réinitialisations de séquence) pour continuer à fonctionner à l'avenir

Modifier: Inconvénients:

  • Échouera sous charge (retour à la planche à dessin)

(Nous remercions @gbn car je me suis inspirée de leur réponse) (Tout commentaire et soulignant les erreurs évidentes sont les bienvenus :)

Ajoutez de nouveaux COLUMNs et 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)')

Ajoutez le nouveau 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

Je recommande fortement de ne pas le faire. Il est susceptible de se bloquer et de provoquer des pannes d'insert une fois que vous êtes sous une charge légère. Avez-vous mis une copie dans une base de données fictive et l'avez-vous martelée avec quelques dizaines de threads à la fois en faisant des insertions (et peut-être aussi des sélections / mises à jour / suppressions) pour voir ce qui se passe?
Cody Konior

@CodyKonior est-il fondamentalement défectueux ou pourrait-il être ressuscité avec un peu de verrouillage judicieux? Sinon, comment aborderiez-vous le problème?
DarcyThomas

Hmmm. A couru avec 10 fils. Je ne sais pas s'il s'agit de verrous morts, mais j'obtiens des conditions de course. Où un déclencheur se termine, avant la fin du déclencheur des lignes précédentes. Cela conduit à un tas de NULLvaleurs entrées. Retour à la planche à dessin ...
DarcyThomas

Désastre évité alors :-) Mon secret est que j'ai reconnu le schéma de quelque chose que j'ai fait il y a environ cinq ans. Je sais seulement que la façon dont vous scannez la table à l'intérieur du déclencheur à la recherche de la prochaine séquence déclenche les choses sous charge. Je ne me souviens pas comment je l'ai résolu mais je peux vérifier plus tard.
Cody Konior

@CodyKonior Je ne pense pas qu'il effectue une analyse ( ON Previous.Id = (I.Id -1) devrait simplement chercher), mais oui ne fonctionne toujours pas. Si je pouvais verrouiller la table (?) Pendant l'insertion et le déclenchement, je pense que cela fonctionnerait. Mais cela ressemble aussi à une odeur de code.
DarcyThomas
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.