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


8

私のシステムで作業を分散するために使用されるビジーキューテーブルをクエリするストアドプロシージャがあります。問題のテーブルには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ビジー状態で、このクエリの実行中に、レコードの1 つが内の 1つのフィルターセットWHEREから別のフィルターセットに移動しているようです。具体的には、誰かがアイテムで作業を開始すると、これが発生しW.InProgress、0から1に変更されます。このクエリが挿入される一時テーブルに主キーを追加しようとすると、重複キー違反が発生します。

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

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

ここに画像の説明を入力してください

問題は、何が重複を引き起こしているのか、そしてどのようにしてそれを停止させるのかということです。

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

回答:


9

下、インデックスから2回読み出される同一の行をもたらすことができるいくつかのトリッキーなシナリオがある分離レベルはREAD COMMITTED

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

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

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

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

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;

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

逆スキャン

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

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