COALESCEを使用して主キーをIDENTITYから永続化された計算列に変更する


10

モノリシックデータベースからアプリケーションを分離するために、さまざまなテーブルのINT IDENTITY列を、COALESCEを使用するPERSISTED計算列に変更しようとしました。基本的に、分離アプリケーションには、多くのアプリケーションで共有される共通データのデータベースを更新しながら、コードやプロシージャを変更することなく、既存のアプリケーションがこれらのテーブルにデータを作成できる機能が必要です。

したがって、基本的に、列の定義から移動しました。

PkId INT IDENTITY(1,1) PRIMARY KEY

に;

PkId AS AS COALESCE(old_id, external_id, new_id) PERSISTED NOT NULL,
old_id INT NULL, -- Values here are from existing records of PkId before table change
external_id INT NULL,
new_id INT IDENTITY(2000000,1) NOT NULL

すべての場合でPkIdは主キーでもあり、1つを除いてすべてクラスタ化されています。すべてのテーブルには、以前と同じ外部キーとインデックスがあります。本質的に、新しい形式では、PkIdを分離されたアプリケーションから(external_idとして)提供できますが、PkIdをIDENTITY列の値にすることもできるため、SCOPE_IDENTITYおよび@@ IDENTITYを使用してIDENTITY列に依存する既存のコードを許可できます。以前と同じように動作します。

私たちが抱えていた問題は、許容範囲内で実行されていたクエリが2つ出てきて、完全に機能しなくなったことです。これらのクエリで使用される生成されたクエリプランは、以前のクエリプランとは異なります。

新しい列がPRIMARY KEY、以前と同じデータ型、およびPERSISTEDであることを考えると、クエリとクエリプランは以前と同じように動作するはずです。COMPUTED PERSISTED INT PkIdは、SQL Serverが実行プランを生成する方法に関して、本質的に明示的なINT定義と同じように動作する必要がありますか?あなたが見ることができるこのアプローチの他の可能性のある問題はありますか?

この変更の目的は、既存のプロシージャやコードを変更せずにテーブル定義を変更できるようにすることでした。これらの問題を考えると、私はこのアプローチで進むことができるとは思いません。


コメントは詳細な議論のためのものではありません。この会話はチャットに移動しました
ポールホワイト9

回答:


4

最初

あなたはおそらく、すべての3つの列を必要としません:old_idexternal_idnew_id。であるnew_id列は、にIDENTITY挿入した場合でも、行ごとに新しい値が生成されexternal_idます。しかし、間old_idexternal_id、それらはかなり相互に排他的です:すでにそこにあるいずれかのold_idまたは現在の構想では、その列は、ちょうどになりますNULL使用している場合external_idnew_id。すでに存在する行(つまり、old_id値を持つもの)に新しい「外部」IDを追加せず、に新しい値が入力されないためold_id、使用される列が1つある可能性があります。両方の目的で。

したがって、external_idold_idを削除し、名前を変更して、何かのような名前にしますold_or_external_id。これにより、何かを実際に変更する必要はなくなりますが、複雑さの一部は軽減されます。external_idアプリコードがに挿入するようにすでに記述されている場合、「古い」値が含まれている場合でも、最大で列を呼び出す必要がある場合がありますexternal_id

これにより、新しい構造が次のようになります。

PkId AS AS COALESCE(old_or_external_id, new_id, -1) PERSISTED NOT NULL,
old_or_external_id INT NULL, -- values from existing record OR passed in from app
new_id INT IDENTITY(2000000, 1) NOT NULL

これで、行ごとに12バイトではなく8バイトのみが追加されました(SPARSEオプションまたはデータ圧縮を使用していない場合)。また、T-SQLやAppコードを変更する必要はありませんでした。

セカンド

この単純化の道を続けて、私たちが残したものを見てみましょう:

  • old_or_external_idカラムは、いずれかの既に値を持つ、またはアプリから新しい値を与えられるか、またはとして残されますNULL
  • new_id常に生成された新たな価値を持つことになりますが、場合にのみ値が使用されますold_or_external_id列ですNULL

との両方old_or_external_idで値が必要になるときはありませんnew_id。両方の列が原因に値がある場合はい、時間があるだろうnew_idことをIDENTITY、しかし、これらのnew_id値は無視されます。繰り返しますが、これら2つのフィールドは相互に排他的です。ならどうしよう?

これexternal_idで、最初にが必要だった理由を調べることができます。をIDENTITY使用して列に挿入することが可能であることを考えるとSET IDENTITY_INSERT {table_name} ON;、スキーマをまったく変更せずINSERTに済みSET IDENTITY_INSERT {table_name} ON;SET IDENTITY_INSERT {table_name} OFF;ステートメントと操作をラップするようにアプリコードを変更するだけです。IDENTITY高い値を挿入すると次の自動生成値が発生するため、アプリのコードが挿入する値をはるかに超える必要があるため、(新しく生成された値の)列をリセットする開始範囲を決定する必要があります。現在のMAX値より大きい。ただし、IDENT_CURRENT値よりも小さい値はいつでも挿入できます。

old_or_external_idnew_id列を組み合わせても、自動生成値とアプリ生成値の間で値が重複する状況に遭遇する可能性は高くなりません。これは、2つまたは3つの列を使用する意図がそれらを主キー値に結合することであるためです。そしてそれらは常にユニークな値です。

このアプローチでは、次のことが必要です。

  • テーブルはそのままにしておきます。

    PkId INT IDENTITY(1,1) PRIMARY KEY

    これにより、各行に8バイトまたは12バイトではなく0バイトが追加されます。

  • アプリ生成値の開始範囲を決定します。これらは、各テーブルの現在のMAX値よりも大きくなりますが、自動生成値の最小値になる値よりは小さくなります。
  • 自動生成範囲の開始値を決定します。現在のMAX値成長する余地の間に十分な余地が必要です。上限は21億4000万を少し超えていることを知っています。次に、DBCC CHECKIDENTを使用して、この新しい最小シード値を設定できます。
  • SET IDENTITY_INSERT {table_name} ON;and SET IDENTITY_INSERT {table_name} OFF;ステートメントでアプリコードのINSERTをラップします。

SECOND、パートB

上記のアプローチのバリエーションは、アプリのコードに-1で始まり、そこから下に向かっ値を挿入することです。これにより、IDENTITY値が上がる唯一の値になります。ここでの利点は、スキーマを複雑にするだけでなく、IDが重複することを心配する必要がないことです(アプリで生成された値が新しい自動生成範囲に達する場合)。これは、まだ負のID値を使用していない場合のみのオプションです(自動生成された列で負の値を使用することは非常にまれであるため、ほとんどの状況で可能性があります)。

このアプローチでは、次のことが必要です。

  • テーブルはそのままにしておきます。

    PkId INT IDENTITY(1,1) PRIMARY KEY

    これにより、各行に8バイトまたは12バイトではなく0バイトが追加されます。

  • アプリが生成する値の開始範囲はになります-1
  • SET IDENTITY_INSERT {table_name} ON;and SET IDENTITY_INSERT {table_name} OFF;ステートメントでアプリコードのINSERTをラップします。

ここでもまだを実行する必要がありますがIDENTITY_INSERT、新しい列を追加したり、列を「再シード」しIDENTITYたり、将来的に重複したりするリスクはありません。

SECOND、パート3

このアプローチの最後のバリエーションは、IDENTITY列をスワップアウトして、代わりにSequencesを使用することです。このアプローチを採用する理由は、アプリコードに、自動生成された範囲(下ではなく)を上回り、を必要としない正の値を挿入できるようにするためSET IDENTITY_INSERT ON / OFFです。

このアプローチでは、次のことが必要です。

  • CREATE SEQUENCEを使用してシーケンスを作成する
  • プロパティIDENTITYを持たないが、NEXT VALUE FOR関数を使用IDENTITYしてDEFAULT制約がある新しい列に列をコピーします。

    PkId INT PRIMARY KEY CONSTRAINT [DF_TableName_NextID] DEFAULT (NEXT VALUE FOR...)

    これにより、各行に8バイトまたは12バイトではなく0バイトが追加されます。

  • アプリ生成値の開始範囲は、自動生成値が近づくと思われる範囲をはるかに上回ります。
  • SET IDENTITY_INSERT {table_name} ON;and SET IDENTITY_INSERT {table_name} OFF;ステートメントでアプリコードのINSERTをラップします。

もつとも、どちらかと要件そのコードに起因するSCOPE_IDENTITY()@@IDENTITY、シーケンスのためにそれらの機能に対応するものはありませんように見えるよう、適切に機能し、まだ、シーケンスへの切り替えは、現在オプションではありません:-(。悲しいです!


回答ありがとうございます。ここで内部的に議論されたいくつかのポイントを上げます。残念ながら、これらのいくつかはいくつかの理由で私たちにとってうまくいきません。私たちのデータベースはかなり古く、ややもろく、2005互換モードで実行されるため、SEQUENCESは公開されていません。アプリのデータプッシュは、Service Brokerキューから新しいレコードを取得して複数のスレッドを介してプッシュするデータロードツールを介して行われます。IDENTITY_INSERTはセッションごとに1つのテーブルにのみ使用できます。現在の考え方では、私たちのアーキテクチャは大幅な変更なしにはそれを提供できません。私はあなたのこぶしの提案を今テストしています。
ムース氏2016年

@MrMooseええ、私は最後にシーケンスに関する詳細情報を含めるために私の答えを更新しました。とにかくあなたの状況ではうまくいきません。との潜在的な同時実行の問題について疑問に思っIDENTITY_INSERTていましたが、テストしていません。オプション#1が全体的な問題を解決するかどうかはわかりませんが、これは不必要な複雑さを軽減するための観察にすぎません。それでも、新しい「外部」IDを挿入する複数のスレッドがある場合、それらが一意であることをどのように保証しますか?
ソロモンRutzky 2016年

@MrMoose実際、「IDENTITY_INSERTはセッションごとに1つのテーブルにのみ使用できます」に関して、ここでの問題は正確に何ですか?1)一度に1つのテーブルにしか挿入できないため、TableAに対してオフにしてから、TableBに挿入します。2)テストしたところ、思っていたのとは反対に、同時実行の問題はありません-できました。有するIDENTITY_INSERT ON二つのセッションで同じテーブルのと何の問題の両方に挿入しました。
ソロモンRutzky 2016年

1
ご提案のとおり、変更1はほとんど違いがありませんでした。使用するIDは、現在のデータベースの外部に割り当てられ、レコードを関連付けるために使用されます。セッションに関する私の理解が正しくないため、IDENTITY_INSERTが機能する可能性があります。調査には少し時間がかかるので、しばらくレポートを返すことはできません。入力いただきありがとうございます。大変感謝しております。
ムース氏2016年

1
IDENTITY_INSERT(既存のアプリのシード値が高い)の使用に関する提案はうまく機能すると思います。Aaron Bertrandがここで回答を提供し、並行性でテストする良い例を示しました。ID値を指定する必要があるテーブルを処理できるようにデータロードツールを変更しました。今後数週間でさらにテストを行います。
ムース氏2016年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.