'Id' mit dem Format: YYYYNNNNNN, wobei der NNNNNN-Teil jedes Jahr neu gestartet wird

11

Ich habe eine Geschäftsanforderung, dass jeder Datensatz in der Rechnungstabelle eine ID hat, die wie JJJJNNNNNN aussieht.

Der NNNNNN-Teil muss zu Beginn eines jeden Jahres neu gestartet werden. Die erste Zeile, die 2016 eingegeben wurde, würde also wie 2016000001 und die zweite wie 2016000002 usw. aussehen. Nehmen wir an, der letzte Rekord für 2016 war 2016123456, die nächste Zeile (von 2017) sollte wie 2017000001 aussehen

Ich brauche diese ID nicht als Primärschlüssel und speichere auch das Erstellungsdatum. Die Idee ist, dass diese 'Anzeige-ID' nach Jahr eindeutig (damit ich sie abfragen kann) und gruppenfähig ist.

Es ist unwahrscheinlich, dass Datensätze gelöscht werden. Ich wäre jedoch geneigt, defensiv gegen so etwas zu programmieren.

Gibt es eine Möglichkeit, diese ID zu erstellen, ohne jedes Jahr bei jedem Einfügen einer neuen Zeile nach der maximalen ID fragen zu müssen?

Ideen:

  • A CreateNewInvoiceSP, das den MAXWert für dieses Jahr erhält (yucky)
  • Einige magische integrierte Funktionen, um genau dies zu tun (ich kann richtig träumen)
  • In der Lage sein, eine UDF oder etwas in der IDENTITYoder DEFAULTDeklaration (??) anzugeben
  • Eine Ansicht, die verwendet PARTITION OVER + ROW()(gelöscht wäre problematisch)
  • Ein Trigger ein INSERT(müsste noch eine MAXAbfrage ausführen :()
  • Ein jährlicher Hintergrundjob, aktualisiert eine Tabelle mit dem MAX für jedes eingefügte Jahr, die ich dann ... Etwas?!

All das ist ein bisschen nicht ideal. Irgendwelche Ideen oder Variationen sind jedoch willkommen!

DarcyThomas
quelle
Sie haben einige gute Antworten, aber wenn Sie ein Jahr haben, id als PK, dann wählen Sie max ist ziemlich schnell.
Paparazzo
Die Verwendung einer Select Max ID-Abfrage ist eine gängige Praxis. verwende das.
Uğur Gümüşhan

Antworten:

17

Ihr Feld besteht aus 2 Elementen

  • Jahr
  • Eine automatisch inkrementierende Nummer

Sie müssen nicht als ein Feld gespeichert werden

Beispiel:

  • Eine Jahresspalte mit einem Standardwert von YEAR(GETDATE())
  • Eine Zahlenspalte basierend auf einer Sequenz.

Erstellen Sie dann eine berechnete Spalte, die sie verkettet (mit entsprechender Formatierung). Die Sequenz kann bei Jahreswechsel zurückgesetzt werden.

Beispielcode in SQLfiddle : * (SQLfiddle funktioniert nicht immer)

-- 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
quelle
1
Vielleicht ist es sauberer, eine Sequenz pro Jahr zu haben. Auf diese Weise muss DDL nicht im Rahmen des regulären Betriebs ausgeführt werden.
usr
@gbn Also würde ich einen Hintergrundjob brauchen, um den SEQUENCE zu Beginn eines jeden Jahres neu zu starten ?
DarcyThomas
@usr Leider können Sie nicht NEXT VALUE FORin einer CASEAussage verwenden (ich habe es versucht)
DarcyThomas
8

Haben Sie darüber nachgedacht, ein Identitätsfeld mit seed = 2016000000 zu erstellen?

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

Dieser Samen sollte jedes Jahr automatisch erhöht werden, zum Beispiel in der Nacht vom 1.1.2017, die Sie einplanen müssen

DBCC CHECKIDENT (Table1, RESEED, 2017000000)

Aber ich sehe bereits Probleme mit dem Design, zum Beispiel: Was ist, wenn Sie Millionen Datensätze haben?

Liya Tansky
quelle
2
Ein weiteres Problem ist, wenn die Datensätze nicht chronologisch angezeigt werden. Identität ist wahrscheinlich nicht der richtige Weg, wenn dies der Fall ist.
Daniel Hutmacher
@LiyaTansky In meinem Fall wurde mir gesagt, dass es nur 50.000 Datensätze pro Jahr sein sollten. Aber ich verstehe, was du damit meinst, dass es mit 1kk Reihen spröde ist
DarcyThomas
1

In diesem Szenario habe ich das Jahr mit 10 ^ 6 multipliziert und den Sequenzwert hinzugefügt. Dies hat den Vorteil, dass kein berechnetes Feld mit seinem (kleinen) laufenden Overhead erforderlich ist und das Feld als verwendet werden kann PRIMARY KEY.

Es gibt zwei mögliche Fallstricke:

  • Stellen Sie sicher, dass Ihr Multiplikator groß genug ist, um niemals erschöpft zu sein

  • Durch das Zwischenspeichern der Sequenz wird Ihnen keine lückenlose Sequenz garantiert.

Ich bin kein Experte für SQL Server, aber Sie können wahrscheinlich ein Ereignis festlegen, das um 201x 00:00:00 ausgelöst wird, um Ihre Sequenz auf Null zurückzusetzen. Das habe ich auch auf Firebird gemacht (oder war es Interbase?).

Vérace
quelle
1

Bearbeiten: Diese Lösung funktioniert nicht unter Last

Ich bin kein Fan von Triggern, aber das scheint das Beste zu sein, was ich herausfinden kann.

Vorteile:

  • Keine Hintergrundjobs
  • Kann schnelle Abfragen auf der DisplayId machen
  • Der Auslöser muss nicht scannen für den vorherigen NNNNNN Teil
  • Startet den NNNNN-Teil jedes Jahr neu
  • Funktioniert, wenn mehr als 100000 Zeilen pro Jahr vorhanden sind
  • Erfordert keine Schemaaktualisierungen (z. B. Zurücksetzen der Sequenz), um in Zukunft weiterarbeiten zu können

Bearbeiten: Nachteile:

  • Wird unter Last versagen (zurück zum Zeichenbrett)

(Dank an @gbn, da ich mich von ihrer Antwort inspirieren ließ) (Feedback und Hinweis auf die offensichtlichen Fehler sind willkommen :)

Fügen Sie einige neue COLUMNs und einINDEX

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

Fügen Sie das neue hinzu 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
quelle
Ich empfehle dringend, dies nicht zu tun. Es ist wahrscheinlich, dass es blockiert und Einfügungsfehler verursacht, wenn Sie unter leichter Last stehen. Haben Sie eine Kopie in eine Dummy-Datenbank eingefügt und sie mit ein paar Dutzend Threads gleichzeitig gehämmert, um Einfügungen (und möglicherweise auch Auswählen / Aktualisieren / Löschen) durchzuführen, um zu sehen, was passiert?
Cody Konior
@CodyKonior ist es grundlegend fehlerhaft oder könnte es mit ein wenig vernünftigem Sperren wiederbelebt werden? Wenn nicht, wie würden Sie das Problem angehen?
DarcyThomas
Hmmm. Lief mit 10 Fäden. Ich bin mir nicht sicher, ob es tote Schlösser sind, aber ich bekomme einige Rennbedingungen. Wo ein Trigger abgeschlossen ist, bevor der Trigger der vorherigen Zeilen beendet ist. Dies führt dazu, dass eine Reihe von NULLWerten eingegeben werden. Zurück zum Zeichenbrett ...
DarcyThomas
Die Katastrophe wurde dann abgewendet :-) Mein Geheimnis ist, dass ich das Muster für etwas erkannt habe, das ich vor ungefähr fünf Jahren getan habe. Ich weiß nur, dass die Art und Weise, wie Sie die Tabelle im Trigger nach der nächsten Sequenz durchsuchen, die Dinge unter Last auslöst. Ich erinnere mich nicht, wie ich es gelöst habe, aber ich kann es später überprüfen.
Cody Konior
@CodyKonior Ich glaube nicht, dass es einen Scan macht ( ON Previous.Id = (I.Id -1) sollte nur suchen), aber ja, funktioniert immer noch nicht. Wenn ich die Tabelle (?) Während des Einfügens und Auslösens sperren könnte , würde es funktionieren. Aber das klingt auch nach einem Codegeruch.
DarcyThomas