「Id」の形式:YYYYNNNNNN、NNNNNNパートは毎年再開


11

Invoiceテーブルの各レコードがYYYYNNNNNNのようなIDを持つというビジネス要件があります。

NNNNNN部分は、毎年初めに再始動する必要があります。したがって、2016年に入力された最初の行は2016000001のようになり、2番目は2016000002のようになります。2016の最後のレコードが2016123456だったとします。(2017の)次の行は2017000001のようになります。

このIDを主キーにする必要はなく、作成日も保存します。アイデアは、この「表示ID」は一意であり(それでクエリできるため)、年ごとに人間でグループ化できるということです。

レコードが削除されることはほとんどありません。しかし、私はそのようなものに対して防御的にコーディングする傾向があります。

今年、新しい行を挿入するたびに最大IDを照会しなくても、このIDを作成できる方法はありますか?

アイデア:

  • その年CreateNewInvoiceSPMAX値を取得するA (yucky)
  • これを正確に行うためのいくつかの魔法の組み込み機能(私は夢を見ることができます)
  • IDENTITYor DEFAULT宣言でUDFまたは何かを指定できること(??)
  • を使用するビューPARTITION OVER + ROW()(削除すると問題が発生します)
  • トリガーINSERT(まだMAXクエリを実行する必要があります:()
  • 毎年のバックグラウンドジョブで、毎年挿入されるMAXを使用してテーブルを更新しました。

これらはすべて、理想的ではありません。どんなアイデアやバリエーションでも大歓迎です!


良い答えはいくつかありますが、年、PKとしてのidがある場合、select maxはかなり高速です。
パパラッツォ

select max idクエリの使用は一般的な方法です。それを使用してください。
UğurGümüşhan

回答:


17

あなたの分野には2つの要素があります

  • 自動インクリメント番号

1つのフィールドとして保存する必要はありません

例:

  • デフォルトの年列 YEAR(GETDATE())
  • シーケンスに基づく数値列。

次に、それらを連結する計算カラムを(適切なフォーマットで)作成します。シーケンスは年の変更時にリセットできます。

SQLfiddleのサンプルコード:*(SQLfiddleは常に機能するとは限りません)

-- 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
おそらく、1年に1つのシーケンスを持つ方がきれいです。そうすれば、通常の操作の一部としてDDLを実行する必要がなくなります。
usr

@gbnではSEQUENCE 、毎年の初めにを再起動するには、バックグラウンドジョブが必要でしょうか。
DarcyThomas

@usr残念ながらNEXT VALUE FORCASEステートメントでは使用できません(試してみました)
DarcyThomas

8

シード= 2016000000のIDフィールドを作成することを検討しましたか?

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

このシードは毎年自動インクリメントする必要があります。たとえば、スケジュールする必要がある2017/1/1の夜に

DBCC CHECKIDENT (Table1, RESEED, 2017000000)

しかし、たとえば、設計に問題があることはすでにわかります。たとえば、100万件のレコードがある場合はどうでしょうか。


2
別の問題は、レコードが年代順に表示されない場合です。これが事実である場合、アイデンティティはおそらく進むべき道ではありません。
Daniel Hutmacher

@LiyaTansky私の場合、年間5万件のレコードしかないと言われています。しかし、1 kkの行で壊れやすいという意味がわかります
DarcyThomas

1

このシナリオで私がしたことは、年に10 ^ 6を乗算し、それにシーケンス値を追加することでした。これには、(小さな)進行中のオーバーヘッドがある計算フィールドが不要で、フィールドをとして使用できるという利点がありPRIMARY KEYます。

次の2つの問題があります。

  • 乗算器が使い果たされないように十分に大きいことを確認してください。

  • シーケンスのキャッシングにより、ギャップのないシーケンスは保証されません。

私はSQL Serverの専門家ではありませんが、イベントを201x 00:00:00でトリガーするように設定して、シーケンスをゼロにリセットできます。それは私がFirebirdでやったことでもあります(またはInterbaseでしたか?)。


1

編集:このソリューションは負荷がかかると機能しません

私はトリガーのファンではありませんが、これは私が解決できる最善のようです。

長所:

  • バックグラウンドジョブなし
  • DisplayIdで高速クエリを実行できます
  • トリガーは前のNNNNNN部分をスキャンする必要はありません
  • NNNNNパートを毎年再開する
  • 年間100000行以上ある場合に機能します
  • スキーマの更新(例:シーケンスのリセット)が将来も機能し続ける必要はありません

編集:短所:

  • 負荷がかかると失敗する(製図板に戻る)

(私が彼らの答えからいくつかのインスピレーションを得たので@gbnにクレジットしてください)(どんなフィードバックでも歓迎し、明らかな間違いを歓迎します:)

新しいCOLUMNとを追加しますINDEX

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

新しいを追加 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

これをしないことを強くお勧めします。軽負荷になると、デッドロックが発生し、挿入エラーが発生する可能性があります。コピーをダミーのデータベースに入れ、一度に数十のスレッドで挿入して(そしておそらく選択/更新/削除も)、何が起こるかを確認しましたか?
Cody Konior

@CodyKoniorは根本的に欠陥があるのでしょうか、それとも少し慎重なロックで復活できるのでしょうか?そうでない場合、どのように問題に取り組みますか?
DarcyThomas

うーん。10スレッドで実行。デッドロックかどうかはわかりませんが、いくつかの競合状態が発生します。前の行のトリガーが終了する前に、1つのトリガーが完了する場所。これにより、多数のNULL値が入力されます。図面に戻る...
DarcyThomas

その後、災害は回避されました:-)私の秘密は、約5年前に私がしたことのパターンを認識したことです。トリガー内のテーブルをスキャンして次のシーケンスを探す方法では、負荷がかかっている状態でトリップすることがわかっています。どのように解決したか覚えていませんが、後で確認できます。
Cody Konior

@CodyKoniorスキャンを実行しているとは思いません(ON Previous.Id = (I.Id -1) シークするだけです)が、まだ機能しません。挿入およびトリガー中にテーブル(?)をロックできれば、うまくいくと思います。しかし、それはコードのにおいのようにも聞こえます。
DarcyThomas
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.