web-dev-qa-db-ja.com

SQL Server-インデックス付きビューを使用するときに共有範囲ロックを「無限」に防止する方法はありますか?

基本的なヘッダー/詳細テーブルがあります(注文と注文の詳細を考えてください)。ヘッダーテーブルにはクラスター化キーとしてID列があり、詳細にはクラスター化キーとしてヘッダーIDと行番号列があります。ヘッダーIDは常に増加するID値であり、行番号も増加する値です。

詳細の上にインデックス付きビューを追加してデータを集計しようとしたので、既存のシステムに独自の同時実行性の問題があるコードまたはトリガーを使用してこれを実行する必要はありませんでした。

負荷テストを開始するまで、すべてが正常に動作します。テーブルに1500詳細/秒(90,000 /分)が追加されることが予想されます。

行が詳細テーブルに挿入されると、インデックス付きビューも更新されます。挿入中に、インデックス付きビューで共有範囲ロック(RangeS-U)が取得されているように見えます。取得される範囲は、シリアル化可能な分離レベルでロックが取得される方法と同様に、現在のキーから次のキーまでです。接続は、読み取りコミットの下でセットアップされます。 「next」キーがテーブルに存在しない場合、ボトルネックが発生するようです。この状況では、共有ロックは「infinity(ffffffff)」キーに対して行われます。

これは基本的に私が見ている動作を説明していますが、回避策は提供していません。 https://www.brentozar.com/archive/2018/09/locks-taken-during-indexed-view-modifications/

上記の負荷がかかると、サーバーは挿入に対応できなくなり、かなり高速にバックアップを開始します。同時に600の接続のうち500がブロックされます。増え続けるキーのインデックス付き集計ビューでは、同時実行性の要件に対応できないようです。

SQL Server 2012 Standard Editionを使用しており、間もなく2019年にアップグレードします。

インデックス付きビューでこのロック動作を変更する方法はありますか、またはこれは私の部分で無駄な努力ですか? 2019が私の場合と同じ動作を示さない場合、作業が完了する前にデータベースがアップグレードされます。

含まれているスクリプトは関連するテーブルを表していますが、明らかに実際のテーブルではありません。動作はそれらを使用して再現可能です。

セットアップ

if object_id(N'dbo.LockTest') is null
begin
  create table dbo.LockTest
  ( LockTestID    int not null primary key
  , LockTestValue int     null
  );
  insert into dbo.locktest values(1, 1), (2, 2), (3, 3), (7, 7), (8, 8);
end;

if object_id(N'dbo.LockTestDetails') is null
begin
  create table dbo.LockTestDetails
  ( LockTestID  int not null
  , LineNumber  int not null
  , Val         int not null
  , PRIMARY KEY(LockTestID, LineNumber)
  , foreign key(LockTestID) references dbo.LockTest(LockTestID)
  );
  insert into dbo.LockTestDetails values(2, 1, 5), (2, 2, 4);
end

if object_id(N'dbo.LockTestTotals') is null
begin
  exec sp_executesql N'
    CREATE VIEW dbo.LockTestTotals with schemabinding as
    SELECT d.LockTestID, Lines = COUNT_BIG(*), Val = SUM(Val)
    FROM dbo.LockTestDetails d
    GROUP BY d.LockTestID';
  exec sp_executesql N'
    create unique clustered index PK_LockTestTotals on dbo.LockTestTotals(LockTestID)';
end

例1

-- run in session 1.  
-- range lock taken from 1 to the next key, 2.
begin transaction
insert into dbo.LockTestDetails values(1, 1, 1);
waitfor delay '00:00:20';
rollback

-- run in session 2
-- record is inserted.  not blocked by session 1 range lock.
-- range lock taken from 7 to next key, 'infinity(ffffffff)'
-- no other details can be added with an id higher than 7.
begin transaction
insert into dbo.LockTestDetails values(7, 1, 1);
waitfor delay '00:00:20';
rollback

例2

-- run in session 1.  
-- range lock taken from 7 to next key, 'infinity(ffffffff)'
-- no other details can be added with an id higher than 7.
begin transaction
insert into dbo.LockTestDetails values(7, 1, 1);
waitfor delay '00:00:20';
rollback

-- run in session 2
-- record is blocked by session 1 range lock.
begin transaction
insert into dbo.LockTestDetails values(8, 1, 1);
waitfor delay '00:00:20';
rollback

ロックを表示するスクリプト

declare @session int = null;

select
  l.request_session_id
, l.resource_type
, resource_description = rtrim(l.resource_description)
, [object_name] = CASE
    WHEN resource_type = 'OBJECT'
    THEN OBJECT_SCHEMA_NAME(l.resource_associated_entity_id) + '.' + OBJECT_NAME(l.resource_associated_entity_id)
    ELSE OBJECT_SCHEMA_NAME(p.[OBJECT_ID]) + '.' + OBJECT_NAME(p.[object_id])
    END
, index_name = i.[name]
, l.request_mode
, l.request_status
, l.resource_subtype
, l.resource_associated_entity_id
from sys.dm_tran_locks l
  left join sys.partitions p 
    ON p.hobt_id = l.resource_associated_entity_id
  LEFT JOIN sys.indexes i
    ON i.[OBJECT_ID] = p.[OBJECT_ID] 
    AND i.index_id = p.index_id
where resource_database_id = db_id()
and request_session_id between  isnull(@session, 0) and isnull(@session, 5000)
and request_session_id <> @@spid
order by 
  [object_name]
, CASE 
    WHEN i.[name] is null then 0
    WHEN LEFT(i.[name], 2) = 'PK' THEN 1
    WHEN LEFT(i.[name], 2) = 'UK' THEN 2
    ELSE 3 END
, index_name
, case resource_type
    when 'DATABASE' then 0
    when 'OBJECT' then 1
    when 'PAGE' then 2
    when 'KEY' then 3
    when 'RID' then 4
    else 99 end
, resource_description
, request_session_id;
7
ScorpionJL

これについてあなたができることは何もありません。 SQL Serverは、インデックス付きビューalwaysがベーステーブルとの同期を維持するように、自動的に手順を実行します。

インデックス付きビューを読み取って、変更されたキーに関連付けられたデータが存在するかどうかを確認する場合、SQL Serverは、ビューのメンテナンスが完了するまでデータが変更されないようにする必要があります。これには、キーが存在しない場合も含まれます。挿入されるまで存在しないようにするには、継続である必要があります。エンジンは、serializable分離でインデックス付きビューにアクセスすることにより、この要件を満たします。このローカル分離エスカレーションは、セッションの現在の分離レベルに関係なく発生します。

興味深いことに、インデックス付きビューの読み取りに追加されるヒントは次のとおりです。

UPDLOCK SERIALIZABLE DETECT-SNAPSHOT-CONFLICT

DETECT-SNAPSHOT-CONFLICTヒントは、スナップショット分離の下で書き込みの競合をチェックするようにSQL Serverに指示します。

あなたの例では、エンジンはまた、外部キー関係を検証するために親テーブルの読み取りにヒントを追加します:

READ-COMMITTEDLOCK FORCEDINDEX DETECT-SNAPSHOT-CONFLICT

READ-COMMITTEDLOCKヒントは、読み取りコミットスナップショットアイソレーションで実行しているときに共有ロックが確実に取得されるようにします。

これらのヒントは正確性に必要であり、無効にすることはできません。

回避策

ascendingの代わりにクラスタ化インデックスdescendingを作成することを考えるかもしれませんが、これは追加の問題(昇順の挿入の場合)を引き起こし、競合のポイントを一方の端からのみ移動します他への構造の。

トリガー、またはデータベースの外部のコードを使用して同じロジックを記述しようとすると、エッジケースが欠落するか(不正確な要約につながる)、SQL Serverと同じヒントを使用することになります。この種のロジックは、最初に正しく理解することが難しいことで有名であり、検証するには、高い同時実行性の下での広範なテストが必要です。一方、大まかな合計で十分な場合もあります。

ある程度のレイテンシを許容できる場合は、挿入をバッチ処理して、単一のセッション/スレッドで一括してインデックス付きビューに適用できます。たとえば、ステージング領域に挿入された行を保持してから、1つのinsertステートメントでベーステーブルを随時更新します。ここでの「バルク」の意味は、予想されるピークのワークロードに快適に対応するのに十分なだけで、それほど大きくなくてもかまいません。これはエラー報告を複雑にします。

基本的に、インデックス付きビューは、一般に非常に迅速なベーステーブルの更新、特に範囲の終わりの挿入にはあまり適していません。

8
Paul White 9