„Id” w formacie: YYYYNNNNNN z częścią NNNNNN uruchamianą co roku

11

Mam wymaganie biznesowe, aby każdy rekord w tabeli faktur miał identyfikator, który wygląda jak YYYYNNNNNN.

Część NNNNNN musi zostać uruchomiona ponownie na początku każdego roku. Tak więc pierwszy wiersz wprowadzony w 2016 roku będzie wyglądał jak 2016000001, a drugi jak 2016000002 itd. Powiedzmy, że ostatni rekord dla 2016 roku to 2016123456, Następny wiersz (z 2017 roku) powinien wyglądać jak 2017000001

Nie potrzebuję tego identyfikatora jako klucza podstawowego i przechowuję również datę utworzenia. Chodzi o to, że ten „identyfikator wyświetlania” jest unikalny (więc mogę za jego pomocą zapytać) i może być grupowany przez ludzi z roku na rok.

Jest mało prawdopodobne, aby jakiekolwiek zapisy zostały usunięte; Byłbym jednak skłonny kodować obronnie przed czymś takim.

Czy jest jakiś sposób, żebym mógł utworzyć ten identyfikator bez pytania o maksymalny identyfikator w tym roku za każdym razem, gdy wstawiam nowy wiersz?

Pomysły:

  • A CreateNewInvoiceSP, która otrzymuje MAXwartość za ten rok (szczęście)
  • Jakaś magiczna wbudowana funkcja do robienia dokładnie tego (mogę marzyć, dobrze)
  • Będąc w stanie określić UDF lub coś w deklaracji IDENTITYlub DEFAULT(??)
  • Widok, który używa PARTITION OVER + ROW()(usunięty byłby problematyczny)
  • Wyzwalacz włączony INSERT(nadal musiałby uruchomić jakieś MAXzapytanie :()
  • Coroczne zadanie w tle, aktualizowałem tabelę z dodanym MAX dla każdego roku, który następnie ... Coś ?!

Z których wszystkie są nieco nie idealne. Wszelkie pomysły i odmiany są mile widziane!

DarcyThomas
źródło
Masz dobre odpowiedzi, ale jeśli masz rok, id jako PK, a następnie wybierz maks., Jest dość szybki.
paparazzo
Używanie zapytania select max id jest powszechną praktyką. użyć tego.
Uğur Gümüşhan

Odpowiedzi:

17

W twoim polu znajdują się 2 elementy

  • Rok
  • Automatycznie zwiększająca się liczba

Nie muszą być przechowywane jako jedno pole

Przykład:

  • Kolumna roku z domyślną wartością YEAR(GETDATE())
  • Kolumna liczbowa oparta na sekwencji.

Następnie utwórz kolumnę obliczeniową łączącą je (z odpowiednim formatowaniem). Sekwencję można zresetować po zmianie roku.

Przykładowy kod w SQLfiddle : * (SQLfiddle nie zawsze działa)

-- 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
źródło
1
Może lepiej jest mieć jedną sekwencję rocznie. W ten sposób nie ma potrzeby wykonywania DDL w ramach regularnych operacji.
usr
@ gbn Czy potrzebowałbym zadania w tle, aby uruchomić ponownie SEQUENCE na początku każdego roku?
DarcyThomas 12.04.16
@usr Niestety nie można użyć NEXT VALUE FORw CASEoświadczeniu (próbowałem)
DarcyThomas 12.04.16
8

Czy rozważałeś utworzenie pola tożsamości o ziarnie = 2016000000?

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

To ziarno powinno być automatycznie zwiększane co roku, na przykład w nocy 2017/1/1 należy zaplanować

DBCC CHECKIDENT (Table1, RESEED, 2017000000)

Ale już widzę problemy z projektem, na przykład: co jeśli masz milion płyt?

Liya Tansky
źródło
2
Innym problemem jest to, że rekordy nie pojawiają się chronologicznie. W takim przypadku tożsamość prawdopodobnie nie jest właściwą drogą.
Daniel Hutmacher
@LiyaTansky W moim przypadku powiedziano mi, że powinno to być tylko 50 000 rekordów rocznie. Ale rozumiem, co masz na myśli mówiąc, że jest kruchy z rzędami 1kk
DarcyThomas 12.04.16
1

To, co zrobiłem w tym scenariuszu, to pomnożyć rok przez 10 ^ 6 i dodać do tego wartość sekwencji. Ma to tę zaletę, że nie wymaga obliczonego pola z jego (małym) bieżącym narzutem, a pole może być używane jako PRIMARY KEY.

Istnieją dwa możliwe problemy:

  • upewnij się, że twój mnożnik jest wystarczająco duży, aby nigdy się nie wyczerpać, i

  • nie ma zagwarantowanej sekwencji bez przerw z powodu buforowania sekwencji.

Nie jestem ekspertem od SQL Server, ale prawdopodobnie możesz ustawić zdarzenie wyzwalające o godzinie 201x 00:00:00, aby zresetować sekwencję do zera. Tak też zrobiłem na Firebird (czy to był Interbase?).

Vérace
źródło
1

Edycja: To rozwiązanie nie działa pod obciążeniem

Nie jestem fanem wyzwalaczy, ale wydaje mi się, że najlepiej to wypracować.

Plusy:

  • Brak zadań w tle
  • Może tworzyć szybkie zapytania na DisplayId
  • Wyzwalacz nie musi skanować w poszukiwaniu poprzedniej części NNNNNN
  • Zrestartuje część NNNNN co roku
  • Działa, jeśli jest więcej niż 100 000 wierszy rocznie
  • Nie wymaga aktualizacji schematu (np. Resetowania sekwencji), aby kontynuować pracę w przyszłości

Edycja: Wady:

  • Nie powiedzie się pod obciążeniem (powrót do deski kreślarskiej)

(Podziękowania dla @gbn, ponieważ czerpałem inspirację z ich odpowiedzi) (Wszelkie opinie zwrotne i wskazanie oczywistych błędów są mile widziane :)

Dodaj kilka nowych COLUMNiINDEX

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

Dodaj nowy 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
źródło
Zdecydowanie nie polecam tego robić. Prawdopodobnie zakleszczy się i spowoduje awarie wkładki, gdy znajdzie się pod niewielkim obciążeniem. Czy umieściłeś kopię w obojętnej bazie danych i wrzuciłeś ją kilkadziesiąt wątków naraz, robiąc wstawki (a może także wybiera / aktualizuje / usuwa), aby zobaczyć, co się stanie?
Cody Konior,
@CodyKonior jest zasadniczo wadliwy, czy można go wskrzesić z odrobiną rozsądnego zablokowania? Jeśli nie, jak podejdziesz do problemu?
DarcyThomas 19.04.16
Hmmm. Ran z 10 wątkami. Nie jestem pewien, czy to martwe zamki, ale dostaję pewne warunki wyścigowe. Tam, gdzie kończy się jeden wyzwalacz, zanim zakończą się poprzednie wyzwalacze. Prowadzi to do wprowadzenia szeregu NULLwartości. Powrót do deski kreślarskiej ...
DarcyThomas,
Katastrofa uniknęła wtedy :-) Moim sekretem jest to, że rozpoznałem wzór czegoś, co zrobiłem około pięć lat temu. Wiem tylko, że sposób, w jaki skanujesz tabelę wewnątrz wyzwalacza, szukając następnej sekwencji, wyzwala rzeczy pod obciążeniem. Nie pamiętam, jak to rozwiązałem, ale mogę to sprawdzić później.
Cody Konior,
@CodyKonior Nie sądzę, że wykonuje skanowanie ( ON Previous.Id = (I.Id -1) powinien po prostu szukać), ale tak, nadal nie działa. Gdybym mógł zablokować tabelę (?) Podczas wstawiania i wyzwalania, myślę, że to zadziałałoby. Ale to też brzmi jak zapach kodu.
DarcyThomas 19.04.16