web-dev-qa-db-ja.com

外部キーの特定の値に対して一意の増分IDを生成します

免責事項:この質問はSOで最初に尋ねられましたが、そこにはあまり牽引力がなかったため、DBAにとってより興味深いものになることを願ってここで試してみました...


私たちは、さまざまな売り手からの注文を集約するeコマースシステムに取り組んでいます。簡単に想像できるように、すべてのセラーの注文のデータを保持するOrderテーブルがあります。各販売者には、AccountIDテーブルの外部キーである一意のOrderがあります。

システムに入力される注文ごとに注文番号を生成して、特定のセラー(およびAccountID)の注文番号がシーケンスを作成するようにします(最初の注文は1、2、3の順になります)。等)。

これに対していくつかの解決策を試しましたが、それらには避けたい欠点があります。それらはすべてトリガー内にあります。

_ALTER TRIGGER [dbo].[Trigger_Order_UpdateAccountOrderNumber] 
   ON [dbo].[Order]
   AFTER INSERT
BEGIN
     ...
END
_

私たちのソリューション1は:

_UPDATE
    [Order]
SET
    AccountOrderNumber = o.AccountOrderNumber
FROM
(
    SELECT
        OrderID,
        AccountOrderNumber =
            ISNULL((SELECT TOP 1 AccountOrderNumber FROM [Order] WHERE AccountID = i.AccountID ORDER BY AccountOrderNumber DESC), 1) +
            (ROW_NUMBER() OVER (PARTITION BY i.AccountID ORDER BY i.OrderID))
    FROM
        inserted AS i
) AS o
WHERE [Order].OrderID = o.OrderID
_

_READ_COMMITTED_SNAPSHOT ON_があることに注意してください。しばらくはうまく機能しているように見えましたが、最近、AccountOrderNumber列の値が重複していることがわかりました。コードを分析した後、操作がアトミックではないため重複が表示される可能性があることは論理的であるように思われます。そのため、2つの注文が同時に追加されると、Orderテーブルから同じ_TOP 1_値を読み取ります。

重複に気付いた後、Solution 2を見つけました。ここで、各AccountOrderNumberの次のAccountを追跡する別のテーブルがあります。

_DECLARE @NewOrderNumbers TABLE
    (
        AccountID int, 
        OrderID int,
        AccountOrderNumber int
    }
_

その場合、トリガー本体は次のようになります。

_INSERT INTO @NewOrderNumbers (AccountID, OrderID, AccountOrderNumber)
    SELECT
        I.AccountID,
        I.OrderID,
        ASN.Number + (ROW_NUMBER() OVER (PARTITION BY I.AccountID ORDER BY I.OrderID))
    FROM
        inserted AS I
        INNER JOIN AccountSequenceNumber ASN WITH (UPDLOCK) ON I.AccountID = ASN.AccountID AND ASN.AccountSequenceNumberTypeID = @AccountOrderNumberTypeID


    UPDATE [Order] ...
_

このソリューションでは重複は作成されませんでしたが、WITH (UPDLOCK)が原因で、新しく作成された_@NewOrderNumbers_テーブルでデッドロックが発生しました。残念ながら、ロックは重複を避けるために必要でした。

私たちの最新の試み(ソリューション)はシーケンスを使用することです。そのためには、システムのAccountごとにシーケンスを作成し、新しい注文が挿入されるときにそれを使用する必要があります。以下は、_AccountID = 1_のシーケンスを作成するコードです。

_CREATE SEQUENCE Seq_Order_AccountOrderNumber_1 AS INT START WITH 1 INCREMENT BY 1 CACHE 100
_

そして_AccountID = 1_のトリガー本体:

_    DECLARE @NumbersRangeToAllocate INT = (SELECT COUNT(1) FROM inserted);

    DECLARE @range_first_value_output SQL_VARIANT; 
    EXEC sp_sequence_get_range N'Seq_Order_AccountOrderNumber_1', @range_size = @NumbersRangeToAllocate, @range_first_value = @range_first_value_output OUTPUT; 

    DECLARE @Number INT = CAST(@range_first_value_output AS INT) - 1;
    UPDATE 
        o
    SET 
        @Number = o.AccountOrderNumber = @Number + 1
    FROM 
        dbo.[Order] AS b JOIN inserted AS i on o.OrderID = i.OrderID
_

シーケンスを使用したアプローチでは、システムに100K以上のアカウントがすぐに存在すると予想され、これらのアカウントのそれぞれについて、6つの異なるテーブルでそのようなインクリメントされたIDが現在必要なので、心配になります。つまり、何十万ものシーケンスが発生し、DB全体のパフォーマンスに悪影響を及ぼす可能性があります。影響があるかどうかはわかりませんが、SQL Serverでこれだけ多くのシーケンスを使用したことのある人からWebで証言を見つけることはほぼ不可能です。

最後に、問題は次のとおりです。問題のより良い解決策を考えることができますか?これは、外部キーのすべての値に対して個別に増分されるIDが必要になるかなり一般的な使用例のようです。ここに明らかなものが足りないのでしょうか?

上記の3つのソリューションについてのコメントも歓迎します。そのうちの1つは許容範囲に近く、ちょっとした微調整が必​​要なだけでしょうか?

6
silvo

言及されていませんが、重複を防ぐために、AccountIDと注文番号にUNIQUE制約/インデックスを設定します(重複が既に存在し、クリーンアップできない場合は不可能です)。

私が使用したシステムでは、現在の最高値を取得して1を追加することによって、INSERT操作中にIDが生成されるか、シーケンステーブル(前述のとおり)が使用されます。私は個人的にシーケンス方式が好きではありませんでした。それは、完全に無関係なテーブルに依存して、基本的にインクリメントされた数だけを生成することを意味し、それを行う他の方法があります。

コードで注文番号を設定するには、指定されたAccountIDの注文番号の現在の最高値を取得し、それに追加する必要があります。 AccountIDがパラメーターとして渡されると仮定します。

INSERT INTO dbo.Orders
(
      AccountID
    , AccountOrderNo
)
SELECT
    AccoutID = @AccountID
    , AccountOrderNo = chk + 1
FROM
    (
        SELECT
            currAccountOrderNo = MAX(AccountOrderNo)
        FROM
            dbo.Orders
        WHERE
            (AccountID = @AccountID)
    ) AS chk
;

トリガーから外すと、重複の問題が減るはずです。それでも重複が発生する場合は、UNIQUE制約によって停止されます。 UNIQUE違反エラーを使用してコードの自動再試行を駆動し、ステータス値(正常または失敗)と再試行(5回の再試行後に停止)のいずれかを確認するWHILEループを使用する可能性のあるオプションを確認できました。これがどれほど必要かわからない。

挿入後に値を設定しようとするトリガーが問題だと思います。そこから離れることができれば、より良い結果が得られるでしょう。

1
Tim Sceurman