SELECTをブロックする大規模なINSERT


14

SELECT操作をブロックしている大量のINSERTに問題があります。

スキーマ

このようなテーブルがあります:

CREATE TABLE [InverterData](
    [InverterID] [bigint] NOT NULL,
    [TimeStamp] [datetime] NOT NULL,    
    [ValueA] [decimal](18, 2) NULL,
    [ValueB] [decimal](18, 2) NULL
    CONSTRAINT [PrimaryKey_e149e28f-5754-4229-be01-65fafeebce16] PRIMARY KEY CLUSTERED 
    (
        [TimeStamp] DESC,
        [InverterID] ASC
    ) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF
    , IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON
    , ALLOW_PAGE_LOCKS = ON)
)

また、MERGEコマンドを使用して挿入または更新(競合の更新)を行うことができるこの小さなヘルパープロシージャもあります。

CREATE PROCEDURE [InsertOrUpdateInverterData]
    @InverterID bigint, @TimeStamp datetime
    , @ValueA decimal(18,2), @ValueB decimal(18,2)
AS
BEGIN
    MERGE [InverterData] AS TARGET
        USING (VALUES (@InverterID, @TimeStamp, @ValueA, @ValueB))
        AS SOURCE ([InverterID], [TimeStamp], [ValueA], [ValueB])
        ON TARGET.[InverterID] = @InverterID AND TARGET.[TimeStamp] = @TimeStamp
    WHEN MATCHED THEN
        UPDATE
        SET [ValueA] = SOURCE.[ValueA], [ValueB] = SOURCE.[ValueB]              
    WHEN NOT MATCHED THEN
        INSERT ([InverterID], [TimeStamp], [ValueA], [ValueB]) 
        VALUES (SOURCE.[InverterID], SOURCE.[TimeStamp], SOURCE.[ValueA], SOURCE.[ValueB]);
END

使用法

私は今、複数のサーバー上でサービスインスタンスを実行しました。 [InsertOrUpdateInverterData]プロシージャをすばやくしました。

SELECTクエリを行うWebサイトもあります [InverterData]テーブルでます。

問題

SELECTクエリを [InverterData]テーブルでと、サービスインスタンスのINSERTの使用状況に応じて、異なるタイムスパンで処理されます。すべてのサービスインスタンスを一時停止すると、SELECTは非常に高速になり、インスタンスが高速挿入を実行すると、SELECTが非常に遅くなるか、タイムアウトがキャンセルされます。

試み

[sys.dm_tran_locks]このようなロックプロセスを見つけるために、テーブルでいくつかのSELECTを実行しました

SELECT
tl.request_session_id,
wt.blocking_session_id,
OBJECT_NAME(p.OBJECT_ID) BlockedObjectName,
h1.TEXT AS RequestingText,
h2.TEXT AS BlockingText,
tl.request_mode

FROM sys.dm_tran_locks AS tl

INNER JOIN sys.dm_os_waiting_tasks AS wt ON tl.lock_owner_address = wt.resource_address
INNER JOIN sys.partitions AS p ON p.hobt_id = tl.resource_associated_entity_id
INNER JOIN sys.dm_exec_connections ec1 ON ec1.session_id = tl.request_session_id
INNER JOIN sys.dm_exec_connections ec2 ON ec2.session_id = wt.blocking_session_id
CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2

これが結果です:

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

S =共有。保留セッションには、リソースへの共有アクセスが許可されます。

質問

SELECTがブロックされるのはなぜですか [InsertOrUpdateInverterData]MERGEコマンドのみを使用プロシージャですか?

内部で定義された分離モードで何らかのトランザクションを使用する必要がありますか [InsertOrUpdateInverterData]ますか?

更新1(@Paulからの質問に関連)

[InsertOrUpdateInverterData]次の統計に関するMS-SQLサーバーの内部レポートに基づきます。

  • 平均CPU時間:0.12ミリ秒
  • 平均読み取りプロセス:5.76 per / s
  • 平均書き込みプロセス:0.4 per / s

これに基づいて、MERGEコマンドは、テーブルをロックする読み取り操作でほとんど忙しいようです!(?)

更新2(@Paulからの質問に関連)

[InverterData]次の表には、次のストレージ統計があります。

  • データ容量:26,901.86 MB
  • 行数:131,827,749
  • パーティション分割:true
  • パーティション数:62

(ほとんど)完全なsp_WhoIsActive結果セットは次のとおりです。

SELECT コマンド

  • dd hh:mm:ss.mss:00 00:01:01.930
  • session_id:73
  • wait_info:(12629ms)LCK_M_S
  • CPU:198
  • blocking_session_id:146
  • 読み取り:99,368
  • 書き込み:0
  • ステータス:一時停止
  • open_tran_count:0

ブロック[InsertOrUpdateInverterData]コマンド

  • dd hh:mm:ss.mss:00 00:00:00.330
  • session_id:146
  • wait_info:NULL
  • CPU:3,972
  • blocking_session_id:NULL
  • 読み取り:376,95
  • 書き込み:126
  • ステータス:睡眠中
  • open_tran_count:1

([TimeStamp] DESC, [InverterID] ASC)クラスタ化インデックスのための奇妙な選択のように見えます。私はDESC一部を意味します。
ypercubeᵀᴹ

私はあなたのポイントを理解しています:データを挿入するクラスター化インデックスDESCは、最後に追加するのではなく、テーブルの再構築を強制します...パフォーマンスドッグ; 再構築中にテーブルをロックします...はい。ジョーブによって、あなたはそれを持っています。構造はロック以上のロックの原因です。
アロサイト

回答:


12

まず、主要な質問とは少し関係ありませんが、あなたのMERGEステートメントは潜在的に競合状態によるエラーのリスクがあります。問題は、簡単に言えば、複数の同時スレッドがターゲット行が存在しないと結論付けて、挿入しようとする試みが衝突する可能性があることです。根本的な原因は、存在しない行で共有ロックまたは更新ロックを取得できないことです。解決策は、ヒントを追加することです。

MERGE [dbo].[InverterData] WITH (SERIALIZABLE) AS [TARGET]

シリアライズ分離レベルヒントは行く行がロックされているキー範囲を保証します。範囲ロックをサポートする一意のインデックスがあるため、このヒントがロックに悪影響を与えることはありません。この潜在的な競合状態に対する保護を得るだけです。

主な質問

コマンドSELECTsのみを使用している[InsertOrUpdateInverterData]プロシージャによってブロックされているのはなぜMERGEですか?

デフォルトのロック読み取りコミット分離レベルでは、データの読み取り時に共有(S)ロックが取得され、通常(常にではありませんが)読み取りの完了後すぐに解放されます。一部の共有ロックは、ステートメントの最後まで保持されます。

A MERGEただ実際の変更を行う前に、排他的(X)ロックに変換される変化にデータを配置するとき、それはSまたは更新(U)ロックを取得するようステートメント修正データ。UとXの両方のロックは、トランザクションの最後まで保持する必要があります。

これは、「楽観的」スナップショット分離(SI)を除くすべての分離レベルで当てはまります-読み取りコミットのバージョン管理と混同しないでください。読み取りコミットスナップショット分離とも呼ばれます。(RCSI)。

Uロックを保持しているセッションによってブロックされているSロックを待機しているセッションを示す質問はありません。これらのロックは互換性あります。ブロッキングは、ほぼ確実に、保持されたXロックのブロッキングによって引き起こされます。これは、短期間に大量の短期ロックが取得、変換、および解放されているときにキャプチャするのが少し難しい場合があります。

open_tran_count: 1InsertOrUpdateInverterDataコマンドでは、調査の価値があります。コマンドはそれほど長く実行されていませんでしたが、不必要に長いコンテナトランザクション(アプリケーションまたは高レベルストアドプロシージャ内)がないことを確認する必要があります。ベストプラクティスは、トランザクションをできるだけ短くすることです。これは何でもないかもしれませんが、必ず確認する必要があります。

潜在的な解決策

Kinがコメントで示唆したように、このデータベースで行バージョン管理分離レベル(RCSIまたはSI)を有効にすることを検討できます。RCSIは、通常、多くのアプリケーション変更を必要としないため、最も頻繁に使用されます。有効にすると、デフォルトの読み取りコミット分離レベルでは、読み取り用にSロックを取得する代わりに行バージョンが使用されるため、SXブロッキングが削減または排除されます。一部の操作(外部キーチェックなど)は、RCSIの下でSロックを取得します。

ただし、行バージョンはtempdbスペースを消費することに注意してください。大まかに言えば、変更アクティビティのレートとトランザクションの長さに比例します。あなたの場合のRCSI(またはSI)の影響を理解し、計画するために、負荷の下で実装を徹底的にテストする必要があります。

ワークロード全体でバージョニングを有効にするのではなく、バージョニングの使用をローカライズする場合は、SIが依然としてより良い選択である可能性があります。読み取りトランザクションにSIを使用することにより、リーダーとライター間の競合を回避できます。ただし、同時変更が開始される前にリーダーが行のバージョンを見るというコストがかかります(より正確には、SIでの読み取り操作は常にコミットされた状態を確認しますSIトランザクションが開始された時点の行)。書き込みロックが引き続き行われ、書き込みの競合を処理する必要があるため、書き込みトランザクションにSIを使用してもほとんどまたはまったく利点がありません。それがあなたが望むものでない限り:)

注: RCSI(一度有効にすると読み取りコミットで実行されるすべてのトランザクションに適用されます)とは異なり、SIはを使用して明示的に要求する必要がありSET TRANSACTION ISOLATION SNAPSHOT;ます。

ライターがライターをブロックすることに依存する微妙な動作(トリガーコードを含む!)により、テストが不可欠になります。詳細については、リンクされた記事シリーズとBooks Onlineをご覧ください。RCSIを決定する場合は、特に「コミットされたスナップショットの分離の読み取り」でデータの変更を確認してください

最後に、インスタンスがSQL Server 2008 Service Pack 4にパッチされていることを確認する必要があります。


0

謙虚に、私はマージを使用しません。IF Exists(UPDATE)ELSE(INSERT)を使用します。行を識別するために使用する2つの列を持つクラスター化キーがあるので、簡単にテストできます。

あなたはMASSIVE挿入に言及し、まだ1 つずつ実行しています...ステージングテーブルのデータをバッチ処理し、POWER OVERWHELMING SQLデータセットパワーを使用して一度に複数の更新/挿入を行うことを考えていますか?ステージングテーブルのコンテンツを定期的にテストし、一度に1つずつではなく、上位10000を一度に取得します...

私は私のアップデートでこのようなことをします

DECLARE @Set TABLE (StagingKey, ID,DATE)
INSERT INTO @Set
UPDATE Staging 
SET InProgress = 1
OUTPUT StagingKey, Staging.ID, Staging.Date
WHERE InProgress = 0
AND StagingID IN (SELECT TOP (100000) StagingKey FROM Staging WHERE inProgress = 0 ORDER BY StagingKey ASC ) --FIFO

DECLARE @Temp 
INSERT INTO @TEMP 
UPDATE [DEST] SET Value = Staging.Value [whatever]
OUTPUT INSERTED.ID, DATE [row identifiers]
FROM [DEST] 
JOIN [STAGING]
JOIN [@SET]; 
INSERT INTO @TEMP 
INSERT [DEST] 
SELECT
OUTPUT INSERT.ID, DATE [row identifiers] 
FROM [STAGING] 
JOIN [@SET] 
LEFT JOIN [DEST]

UPDATE Staging
SET inProgress = NULL
FROM Staging 
JOIN @set
ON @Set.Key = Staging.Key
JOIN @temp
ON @temp.id = @set.ID
AND @temp.date = @set.Date

おそらく、更新バッチをポップする複数のジョブを実行できますが、トリクル削除を実行する別のジョブが必要になります

while exists (inProgress is null) 
delete top (100) from staging where inProgress is null 

ステージングテーブルをクリーンアップします。

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