web-dev-qa-db-ja.com

SELECTをブロックする大量のINSERT

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]プロシージャをすばやく呼び出して大規模な更新を実行する複数のサーバーでサービスインスタンスを実行できました。

[InverterData]テーブルに対してSELECTクエリを実行するWebサイトもあります。

問題

[InverterData]テーブルでSELECTクエリを実行すると、サービスインスタンスの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

これが結果です:

enter image description here

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

質問

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

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

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

MS-SQLサーバーの内部レポートに基づく、統計情報[InsertOrUpdateInverterData]に関する統計:

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

これに基づいて、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
14
Steffen Mangold

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

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

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

主な質問

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

デフォルトの locking read commit 分離レベルでは、共有(S)ロックはデータの読み取り時に取得され、通常(常にではないが)読み取りが完了した直後に解放されます。一部の共有ロックは、ステートメントの最後まで保持されます。

MERGEステートメントはデータを変更するため、変更するデータを見つけるとSまたは更新(U)ロックを取得し、実際の変更を実行する直前に排他(X)ロックに変換されます。 UロックとXロックの両方をトランザクションの最後まで保持する必要があります。

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

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

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

可能な解決策

Kinがコメントで提案したように、このデータベースで 行バージョン管理分離レベル (RCSIまたはSI)を有効にすることを検討できます。 RCSIは、通常、それほど多くのアプリケーション変更を必要としないため、最もよく使用されます。有効にすると、デフォルトの読み取りコミット分離レベルは、読み取りにSロックを取得する代わりに行バージョンを使用するため、S-Xブロックが削減または排除されます。一部の操作(外部キーチェックなど)は、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にパッチされていることを確認する必要があります。

12
Paul White 9

何卒、マージは使用しません。 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 

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

0
Alocyte