「プレビューモード」のデータベースストアドプロシージャ


15

私が使用しているデータベースアプリケーションのかなり一般的なパターンは、「プレビューモード」を持つレポートまたはユーティリティのストアドプロシージャを作成する必要があることです。そのようなプロシージャが更新を行う場合、このパラメータはアクションの結果が返されるべきであることを示しますが、プロシージャは実際にデータベースの更新を実行するべきではありません。

これを実現する1つの方法ifは、パラメーターのステートメントを記述するだけで、2つの完全なコードブロックを作成することです。1つは更新を実行してデータを返し、もう1つはデータを返すだけです。しかし、これは、コードの重複と、プレビューデータが実際に更新で発生することを正確に反映しているという比較的低い信頼度のため、望ましくありません。

次の例では、トランザクションセーブポイントと変数(一時テーブルとは対照的に、トランザクションの影響を受けない)を活用して、ライブ更新モードとしてプレビューモードのコードの単一ブロックのみを使用しようとします。

注:トランザクションのロールバックは、このプロシージャコール自体がトランザクションにネストされている可能性があるため、オプションではありません。これはSQL Server 2012でテストされています。

CREATE TABLE dbo.user_table (a int);
GO

CREATE PROCEDURE [dbo].[PREVIEW_EXAMPLE] (
  @preview char(1) = 'Y'
) AS

CREATE TABLE #dataset_to_return (a int);

BEGIN TRANSACTION; -- preview mode required infrastructure
  DECLARE @output_to_return TABLE (a int);
  SAVE TRANSACTION savepoint;

  -- do stuff here
  INSERT INTO dbo.user_table (a)
    OUTPUT inserted.a INTO @output_to_return (a)
    VALUES (42);

  -- catch preview mode
  IF @preview = 'Y'
    ROLLBACK TRANSACTION savepoint;

  -- save output to temp table if used for return data
  INSERT INTO #dataset_to_return (a)
  SELECT a FROM @output_to_return;
COMMIT TRANSACTION;

SELECT a AS proc_return_data FROM #dataset_to_return;
RETURN 0;
GO

-- Examples
EXEC dbo.PREVIEW_EXAMPLE @preview = 'Y';
SELECT a AS user_table_after_preview_mode FROM user_table;

EXEC dbo.PREVIEW_EXAMPLE @preview = 'N';
SELECT a AS user_table_after_live_mode FROM user_table;

-- Cleanup
DROP TABLE dbo.user_table;
DROP PROCEDURE dbo.PREVIEW_EXAMPLE;
GO

このコードとデザインパターンに関するフィードバック、および/または同じ問題に対する別の解決策が異なる形式で存在するかどうかを探しています。

回答:


12

このアプローチにはいくつかの欠陥があります。

  1. 「プレビュー」という用語は、操作対象のデータの性質(および操​​作ごとに変化する)に応じて、ほとんどの場合非常に誤解を招く可能性があります。「プレビュー」データが収集されてから15分後にユーザーが戻ってくるまで、操作中の現在のデータが同じ状態にあることを確認するために、コーヒーを手に取り、煙のために外に出て、歩いた後ブロックの周りに戻って、eBayで何かをチェックします。そして、実際に操作を実行するために「OK」ボタンをクリックしなかったので、最後にボタンをクリックしましたか?

    プレビューが生成された後、操作を進めるのに時間制限がありますか?または、変更時のデータが初期状態と同じ状態にあることを判断する方法はありSELECTますか?

  2. サンプルコードは急いで行われた可能性があり、実際のユースケースを表していないため、これは小さなポイントですが、なぜINSERT操作の「プレビュー」があるのでしょうか。のようなものを介して複数の行を挿入する場合、これは理にかなってINSERT...SELECTおり、可変数の行が挿入される可能性がありますが、これはシングルトン操作にはあまり意味がありません。

  3. これは、プレビューデータが実際に更新で発生することを正確に反映しているという比較的低い信頼度のために望ましくありません。

    この「低い信頼度」はどこから来たのでしょうか?SELECT複数のテーブルが結合され、結果セットに行が重複している場合に表示される行数とは異なる行数を更新することは可能ですが、ここでは問題になりません。によって影響を受けるはずの行UPDATEは、それ自体で選択可能です。不一致がある場合、クエリを誤って実行しています。

    また、更新されるテーブルの複数の行と一致するJOINedテーブルが原因で重複している状況は、「プレビュー」が生成される状況ではありません。そして、このような場合は、レポート内で繰り返されるレポートのサブセットが更新されることをユーザーに説明する必要があります。これにより、誰かが影響を受ける行の数を調べます。

  4. 完全を期すために(他の回答でこれについて言及されていたとしても)、TRY...CATCHコンストラクトを使用していないため、これらの呼び出しをネストするときに問題が発生する可能性があります(保存ポイントを使用しない場合でも、トランザクションを使用しない場合でも)。ネストされたストアドプロシージャコール全体でトランザクションを処理するテンプレートについては、DBA.SEの次の質問に対する私の回答を参照してください。

    C#コードおよびストアドプロシージャでトランザクションを処理する必要がありますか

  5. EVEN IF問題は上述占めた、重大な欠陥が依然として存在する:操作は(前にすなわち行われている時間の短い期間のためにROLLBACK)、任意のダーティ・リードクエリ(使用してクエリWITH (NOLOCK)またはSET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED)は、データを取得することができます少し後でありません。ダーティリードクエリを使用している人はすでにこれを認識し、その可能性を受け入れているはずですが、このような操作は、デバッグが非常に困難なデータ異常を導入する可能性を大幅に高めます(つまり、どのくらいの時間を試してみたいですか?明らかな直接的な原因がない問題を見つけますか?)。

  6. このようなパターンは、より多くのロックを取得することでブロックを増やし、より多くのトランザクションログアクティビティを生成することにより、システムのパフォーマンスを低下させます。(@MartinSmithは質問のコメントでこれら2つの問題についても言及していることがわかりました。)

    さらに、変更中のテーブルにトリガーがある場合、それは不要なかなりの追加処理(CPUおよび物理/論理読み取り)になる可能性があります。また、トリガーは、ダーティリードに起因するデータ異常の可能性をさらに高めます。

  7. 上記のポイントに関連する-ロックの増加-トランザクションを使用すると、特にトリガーが関係している場合、デッドロックに陥る可能性が高くなります。

  8. あまり起こりそうにないINSERT操作のシナリオにのみ関連する、それほど深刻ではない問題:「プレビュー」データは、DEFAULT制約(Sequences/ NEWID()/ NEWSEQUENTIALID())およびによって決定される列値に関して挿入されるものと同じではない場合がありIDENTITYます。

  9. テーブル変数の内容を一時テーブルに書き込むための追加のオーバーヘッドは必要ありません。ROLLBACKそれは単により多くの意味になるだろうので、(あなたが最初の場所でテーブル変数を使用していたと述べた理由である)テーブル変数のデータに影響を与えないSELECT FROM @output_to_return;終わり、その後も、一時的に作成する気にしないでくださいテーブル。

  10. セーブポイントのこのニュアンスが不明な場合(単一のストアドプロシージャのみを示しているため、サンプルコードではわかりにくい):操作が期待どおりに動作するように、一意のセーブポイント名を使用する必要がありますROLLBACK {save_point_name}。名前を再利用すると、ROLLBACKはその名前の最新のセーブポイントをロールバックします。これROLLBACKは、呼び出し元と同じネストレベルにない場合があります。この動作の動作を確認するには、次の回答の最初のサンプルコードブ​​ロックを参照してください。ストアドプロシージャのトランザクション

結果は次のとおりです。

  • 「プレビュー」を行うことは、ユーザー向けの操作にはあまり意味がありません。メンテナンス操作のためにこれを頻繁に行うので、操作を続行した場合に何が削除されるか、ガベージコレクトされるかを確認できます。と呼ばれるオプションのパラメーターを追加し、他のときにそれ@TestModeを行うIFステートメントを実行します。アプリケーションによって呼び出されるストアドプロシージャにパラメーターを追加して、データの状態に影響を与えずに簡単なテストを行えるようにすることもありますが、このパラメーターはアプリケーションによって使用されることはありません。SELECT@TestMode = 1DELETE@TestMode

  • 「問題」の上部のセクションからこれが明確でない場合に備えて:

    「プレビュー」/「テスト」モードが必要/必要な場合、DMLステートメントが実行された場合に何が影響を受けるかを確認するには、トランザクションを使用しないでください。(つまりBEGIN TRAN...ROLLBACKパターン)を使用してこれを実行しないでください。これは、せいぜいシングルユーザーシステムでしか機能しないパターンであり、そのような状況ではあまり良い考えではありません。

  • IFステートメントの2つのブランチ間でクエリの大部分を繰り返すと、変更するたびに両方のブランチを更新する必要があるという潜在的な問題が発生します。ただし、2つのクエリの違いは通常、コードレビューでキャッチするのに十分簡単で、修正も簡単です。一方、状態の違いやダーティリードなどの問題を見つけて修正するのははるかに困難です。また、システムパフォーマンスの低下の問題を修正することは不可能です。SQLはオブジェクト指向言語ではなく、カプセル化/重複コードの削減は、他の多くの言語のようにSQLの設計目標ではないことを認識して受け入れる必要があります。

    クエリが十分に長い/複雑な場合は、インラインテーブル値関数にカプセル化できます。次にSELECT * FROM dbo.MyTVF(params);、「プレビュー」モードの簡単な操作を行い、「実行」モードのキー値に結合します。例えば:

    UPDATE tab
    SET    tab.Col2 = tvf.ColB
           ...
    FROM   dbo.Table tab
    INNER JOIN dbo.MyTVF(params) tvf
            ON tvf.ColA = tab.Col1;
  • これがレポートシナリオである可能性がある場合、最初のレポートの実行は「プレビュー」です。レポートに表示されるもの(ステータスなど)を変更したい場合、現在表示されているデータを変更することが期待されるため、追加のプレビューは不要です。

    操作がおそらく特定の%またはビジネスルールによって入札額を変更することである場合、プレゼンテーションレイヤーで処理できます(JavaScript?)。

  • エンドユーザー向けの操作本当に「プレビュー」を行う必要がある場合は、最初にデータの状態をキャプチャする必要があります(UPDATE操作の結果セット内のすべてのフィールドのハッシュ、またはDELETE操作)、次に、操作を実行する前に、キャプチャされた状態情報を現在の情報と比較します-テーブルでロックを実行しているトランザクション内で、HOLDこの比較を行った後に何も変わらないようにします-そして、何らかの違いがあれば、またはをROLLBACK続行するのではなく、エラーを実行します。UPDATEDELETE

    UPDATE操作の違いを検出するために、関連するフィールドでハッシュを計算する代わりに、ROWVERSION型の列を追加することもできます。ROWVERSIONデータ型の値は、その行が変更されるたびに自動的に変更されます。そのような列がある場合はSELECT、他の「プレビュー」データと一緒に、キー値と値とともに「確認、先に進んで更新を行う」ステップに渡します。変更する。次にROWVERSION、「プレビュー」から渡された値を現在の値(各キーごと)と比較し、一致した値のみを続行します。ここでの利点は、たとえ可能性が低いとしても、偽陰性の可能性があるハッシュを計算する必要がないことです。UPDATE場合ALLを実行するたびにことですSELECT。一方、ROWVERSION値は変更された場合にのみ自動的にインクリメントされるため、心配する必要はありません。ただし、ROWVERSIONタイプは8バイトであり、多くのテーブルおよび/または多くの行を処理する場合は合計することができます。

    UPDATE操作に関連する一貫性のない状態を検出するためのこれら2つの方法にはそれぞれ長所と短所があります。そのため、システムの「con」よりも「pro」の方が多いメソッドを判断する必要があります。ただし、どちらの場合でも、プレビューを生成してから操作を実行してエンドユーザーの予想外の動作を引き起こすまでの遅延を回避できます。

  • エンドユーザー向けの「プレビュー」モードを実行している場合は、選択時にレコードの状態キャプチャし、通過し、変更時にチェックすることに加えて、DATETIMEfor SelectTimeやpopvia via GETDATE()などを含めます。それをストアドプロシージャ(ほとんどの場合単一の入力パラメータとして)に戻して、ストアドプロシージャでチェックできるように、アプリレイヤーに渡します。次に、操作が「プレビュー」モードではない場合、現在の値のX分前までに@SelectTime値を設定する必要があると判断できますGETDATE()。たぶん2分?5分?ほとんどの場合、10分以内です。DATEDIFFin MINUTESがそのしきい値を超えている場合、エラーをスローします。


4

多くの場合、最も単純なアプローチが最善であり、SQL、特に同じモジュールではコードの重複に関する問題はそれほど多くありません。2つのクエリがすべて異なることを実行した後。したがって、「ルート1」または「キープイットシンプル」を選択して、ストアドプロシージャに2つのセクションを配置しないでください。

CREATE TABLE dbo.user_table ( rowId INT IDENTITY PRIMARY KEY, a INT NOT NULL, someGuid UNIQUEIDENTIFIER DEFAULT NEWID() );
GO
CREATE PROCEDURE [dbo].[PREVIEW_EXAMPLE2]

    @preview CHAR(1) = 'Y'

AS

    SET NOCOUNT ON

    --!!TODO add error handling

    IF @preview = 'Y'

        -- Simulate INSERT; could be more complex
        SELECT 
            ISNULL( ( SELECT MAX(rowId) FROM dbo.user_table ), 0 ) + 1 AS rowId,
            42 AS a,
            NEWID() AS someGuid

    ELSE

        -- Actually do the INSERT, return inserted values
        INSERT INTO dbo.user_table ( a )
        OUTPUT inserted.rowId, inserted.a, inserted.someGuid
        VALUES ( 42 )

    RETURN

GO

これには、自己文書化(つまり、IF ... ELSEわかりやすい)、複雑さが低い(テーブル変数アプローチIMOを使用した保存ポイントと比較して)ため、バグが発生しにくい(@Codyからのスポット)という利点があります。

低信頼性についてのあなたの主張に関して、私は理解しているかどうかわかりません。論理的に同じ基準を持つ2つのクエリは、同じことを行う必要があります。an UPDATEとaの間でカーディナリティが一致しない可能性がありますが、これはSELECT結合と基準の機能になります。さらに説明してもらえますか?

余談ですが、NULL/ NOT NULLプロパティとテーブルおよびテーブル変数を設定する必要があります。主キーの設定を検討してください。

あなたのオリジナルのアプローチは思わビットとして、おそらくデッドロックが発生しやすくなる可能性が過剰に複雑INSERT/ UPDATE/ DELETE操作は無地よりも高いロックレベルを必要としますSELECTs

実際のprocはもっと複雑だと思うので、上記のアプローチがうまくいかないと思われる場合は、いくつかの例を投稿してください。


3

私の懸念は次のとおりです。

  • トランザクション処理は、Begin Try / Begin Catchブロックにネストされた標準パターンに従っていません。これがテンプレートの場合、さらにいくつかのステップを含むストアドプロシージャで、データを変更したままプレビューモードでこのトランザクションを終了できます。

  • フォーマットに従うと、開発者の作業が増えます。内部列を変更する場合は、テーブル変数定義も変更し、一時テーブル定義を変更してから、最後に挿入列を変更する必要があります。人気が出るわけではありません。

  • 一部のストアドプロシージャは、毎回同じ形式のデータを返しません。一般的な例としてsp_WhoIsActiveを考えてください。

より良い方法は提供していませんが、あなたが持っているものが良いパターンだとは思いません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.