web-dev-qa-db-ja.com

重複なしでテーブルから返された重複レコード

私のシステムで作業を分散するために使用されるビジーキューテーブルをクエリするストアドプロシージャがあります。問題のテーブルにはWorkIDの主キーがあり、重複はありません。

クエリの簡略版は次のとおりです。

INSERT INTO #TempWorkIDs (WorkID)
SELECT
        W.WorkID

    FROM
        dbo.WorkTable W

    WHERE
        (@bool_param = 0 AND
        ((W.InProgress = 0
         AND ISNULL(W.UserID, -1) != @userid_param
         AND (@bool_filtered = 0
              OR W.TypeID IN (SELECT TypeID FROM #Types AS t)))
         OR 
         (@bool_param = 1
          AND W.InProgress = 1
          AND W.UserID != @userid_param)
        OR
        (@Auto_Param = 0
         AND W.UserID = @userid_param)))
         OR
         (@bool_param = 1 AND W.UserID = @userid_param)
    OPTION
        (RECOMPILE)

#Typesテーブルは、手順の前半で入力されています。

先ほど述べたように、WorkTableはビジーで、このクエリの実行中に時々私[〜#〜] suspect [〜#〜]レコードの1つがWHERE内の1つのフィルターセットから別のセットに移動しています。具体的には、誰かがアイテムで作業を開始し、W.InProgressが0から1に変更されたときにこれが発生します。これが発生すると、一時テーブルに主キーを追加しようとすると、重複するキー違反が発生します。このクエリは挿入。

エラーが発生したときに生成されるクエリプランで、並列処理がなく、分離レベルがREAD COMMITTEDであり、ソーステーブルに重複レコードがないことを確認しました。ここには、デカルト積を取得するJOINsまたは他の方法がないことも確認できます。

これは匿名化されたクエリプランです。

enter image description here

問題は、重複の原因と停止方法を教えてください

READ COMMITTEDはここで機能するはずです。ロックが必要です。クエリ中にレコードのInProgressビットが変更されると、重複が発生することはほぼ確実です。テーブルがその変更の時間を格納し、クエリを実行してエラーが発生してから数ミリ秒以内であるため、これはわかっています。

8
JNK

READ COMMITTED分離レベル の下でも、同じ行がインデックスから2回読み取られる可能性があるいくつかのトリッキーなシナリオがあります。

クエリは 割り当て順序スキャン、 の対象ではないため、ストレージエンジンはテーブルからクラスター化されたキーの順序でデータを読み取ります。

テーブルの場合、クラスター化キーの最初の列としてInProgressがあります。テーブルをスキャンするときに、行またはページがロックされる可能性があります。スキャンの開始近くで行を読み取った場合は、そのロックを解除し、その行を更新してInProgressを0から1に変更してから、その行を別のページで再度読み取ります。クエリの重複するWorkID値を確認してください。

回避策はたくさんあります。ヒープに挿入して、重複する値を削除するだけです。 DISTINCTをクエリに追加できます。トランザクションの開始時( スナップショット分離 )または開始時のいずれかで、データベースのコミットされた状態の安定したビューを提供するために、行バージョンの分離レベルを有効にすることもできます。ステートメント( 読み取りコミットスナップショット分離 )。

おそらく、ロックのヒントを追加したり、テーブルの構造を変更したりするのが適切です。かなり楽しいソリューション(おそらく本番環境には適さない)の場合は、インデックスを逆に読み取ってみてください。これは、ORDER BYとともに余分なTOPを使用して行うことができます。以下は、ポイントを説明するための非常に単純なデモです。

CREATE TABLE #WorkTable (
    InProgress TINYINT NOT NULL,
    WorkID INT NOT NULL
    , PRIMARY KEY (InProgress, WorkID)
);

INSERT INTO #WorkTable WITH (TABLOCK)
SELECT (RN - 1) / 5000, RN
FROM
(
    SELECT TOP (10000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) t
OPTION (MAXDOP 1);

次のクエリにはOrdered:falseプロパティがありますが、クラスター化されたキーの順序でデータを読み取ります。

SELECT WorkId
FROM #WorkTable;

ただし、次のクエリはデータを逆のクラスター化された順序で読み取ります。

SELECT TOP (9223372036854775807) WorkId
FROM #WorkTable
ORDER BY InProgress DESC, WorkId DESC;

これは、スキャンのプロパティを見るとわかります。

backwards scan

テーブルの場合、これは、InProgressが0から1に変更されるように行が更新されると、2回表示される可能性がはるかに低くなることを意味します。まったく表示されない場合があり、別の問題である可能性があります。

9
Joe Obbish