web-dev-qa-db-ja.com

MS SQL MERGE INTOは更新のためにテーブル全体をロックします

私は統計値のテーブルを持っています、それは次のように定義されている何百万ものレコードを保持しています:

CREATE TABLE [dbo].[Statistic]
(
    [Id] [INT] IDENTITY(1, 1) NOT NULL
  , [EntityId] [INT] NULL
  , [EntityTypeId] [UNIQUEIDENTIFIER] NOT NULL
  , [ValueTypeId] [UNIQUEIDENTIFIER] NOT NULL
  , [Value] [DECIMAL](19, 5) NOT NULL
  , [Date] [DATETIME2](7) NULL
  , [AggregateTypeId] [INT] NOT NULL
  , [JsonData] [NVARCHAR](MAX) NULL
  , [WeekDay] AS (DATEDIFF(DAY, CONVERT([DATETIME], '19000101', (112)), [Date]) % (7) + (1)) PERSISTED
  , CONSTRAINT [PK_Statistic]
        PRIMARY KEY NONCLUSTERED ([Id] ASC)
);

CREATE UNIQUE CLUSTERED INDEX [IX_Statistic_EntityId_EntityTypeId_ValueTypeId_AggregateTypeId_Date]
ON [dbo].[Statistic] (
                         [EntityId] ASC
                       , [EntityTypeId] ASC
                       , [ValueTypeId] ASC
                       , [AggregateTypeId] ASC
                       , [Date] ASC
                     );

CREATE NONCLUSTERED INDEX [IX_Date] ON [dbo].[Statistic] ([Date] ASC);

CREATE NONCLUSTERED INDEX [IX_EntityId]
ON [dbo].[Statistic] ([EntityId] ASC)
INCLUDE ([Id]);

CREATE NONCLUSTERED INDEX [IX_EntityType_Agg_Date]
ON [dbo].[Statistic] ([EntityTypeId] ASC, [AggregateTypeId] ASC, [Date] ASC)
INCLUDE ([Id], [EntityId], [ValueTypeId]);

CREATE NONCLUSTERED INDEX [IX_Statistic_ValueTypeId]
ON [dbo].[Statistic] ([ValueTypeId] ASC)
INCLUDE ([Id]);

CREATE NONCLUSTERED INDEX [IX_WeekDay]
ON [dbo].[Statistic] ([AggregateTypeId] ASC, [WeekDay] ASC, [Date] ASC)
INCLUDE ([Id]);

ALTER TABLE [dbo].[Statistic]
ADD CONSTRAINT [PK_Statistic]
    PRIMARY KEY NONCLUSTERED ([Id] ASC);

マージによる更新中、SQLサーバーはページ/行ではなくテーブル全体をロックします。@inTblはパラメーターとして渡されるキー/値のデータテーブルです

MERGE INTO Statistic AS stat
USING
    (SELECT inTbl.EntityId, inTbl.Value FROM @p0 AS inTbl) AS src
ON src.EntityId = stat.EntityId
   AND stat.EntityTypeId = @p1
   AND stat.ValueTypeId = @p2
   AND stat.Date IS NULL
   AND stat.AggregateTypeId = @p3
WHEN MATCHED THEN
    UPDATE SET stat.Value = src.value
WHEN NOT MATCHED BY TARGET THEN
    INSERT (EntityTypeId, ValueTypeId, Date, AggregateTypeId, EntityId, Value)
    VALUES
    (@p4, @p5, @p6, @p7, src.entityId, src.value);

したがって、私には2つの問題があります。1)マージが完了するまでに時間がかかることがあります。

2)このような更新はマージが完了するのを待ちます:

UPDATE [dbo].[Statistic]
SET [Value] = @p0, [JsonData] = @p1
WHERE [EntityTypeId] = @p2
      AND [ValueTypeId] = @p3
      AND [Date] = @p4
      AND [EntityId] = @p5
      AND [AggregateTypeId] = @p6;

クエリ用のplan/locksファイルがありますが、かなり大きいので、ここにあります

インデックスの再構築前: https://www.brentozar.com/pastetheplan/?id=S19EgxYIB

インデックスの再構築後: https://www.brentozar.com/pastetheplan/?id=SyjexxtLH

何が問題なのでしょうか?これはときどき発生し、クラスター化インデックスの再構築後に消えることがあります。

クラスター化インデックスは、1日程度で90%以上に断片化されます。この断片化を防ぐにはどうすればよいですか?

4
xumix

統計値のテーブルがあり、数百万レコードが保持されています

...

クラスタ化インデックスは、1日程度で90%以上に断片化されます。

clustered indexを見てください。_key48 bytesの長さです。テーブルが十分に大きく、nonclustered indexesも5つあるため、これは適切な選択ではありません。 すべて48 bytesごとにindex levelがあるため、すべての非クラスター化インデックスは、必要な領域の少なくとも2倍を占有します。

IMHO、最初にすべきことは、可能であればclustered index keyを変更することです。clustered indexidentityで定義できます。これは一意で、常に増加し、狭く、これによりyor clustered index fragmentationを減らし、JsonDataフィールドが更新されない場合はclustered index fragmentationは0になります。

これにより、insertの時間も短縮されます。page slitsへの挿入が原因で、clustered indexのログ記録に時間がかかりすぎます。

2番目の問題:lock escalation。あなたが言ったように、すべてのバッチはソーステーブルに2000行を含みますが、それらは(estimated planに従って)3402行を挿入させ、これはclustered indexにのみ当てはまります。 5つのnonclustered indexesがあるため、1つのstatementに少なくとも6 * 2000 = 12000行、または推定が正しい場合はすべての20412行を挿入します。

Lock escalationlocksごとに5000 statementでトリガーされます:

SQL Serverは、インスタンス全体のしきい値を超えたときにロックをエスカレートするだけでなく、個々のセッションが1つのステートメントで5,000を超えるロックを取得したときにもロックをエスカレートします。この場合、ロックをエスカレートするセッションを選択する際にランダム性はありません。ロックを取得したセッションです。

そして、あなたの場合、それらはおそらくrow locksです。これは、クラスタ化インデックスキーランダムが原因です。常に増加するキーに挿入する場合、page locksがかかる可能性がありますが、clustered keyはランダムです。いずれの場合も、非クラスター化インデックスへの挿入もランダムであるため、サーバーがrow locksを選択するのは通常のことです。

したがって、テーブルでlock escalationを無効にするか、バッチをバッチあたり1000行以下に分割できます。これをテストする必要があります。


これは、このコメントに対する応答の小さな再現です。

挿入はロックを取得できません(存在しないリソースをロックできません)

if object_id('dbo.t') is not null drop table dbo.t;
create table dbo.t(id int identity primary key, col1 varchar(10), col2 varchar(10));
create index ix_col1 on dbo.t(col1);
create index ix_col2 on dbo.t(col2);

begin tran
insert into dbo.t (col1, col2)
select top 1000 'aaa', 'bbb'
from sys.columns c1 cross join sys.columns c2;

select *
from sys.dm_tran_locks
where resource_type <> 'DATABASE'
      and request_session_id = @@spid
order by resource_associated_entity_id,
         resource_type;

rollback tran;
3
sepupic

質問には2つの部分があります。 1つ目は、MERGE操作のパフォーマンスをより確実にする方法です。マージステートメントを下にあるものに近いものに変更すると、テーブルロックを最小限に抑えるのに役立つと思います。

CTEを使用すると、マージに適用するデータをより簡単に選択できるようになるだけでなく、エンジンが何をロックするかをさらに促進するROWLOCK、UPDLOCK、およびHOLDLOCKヒントを指定できるようになりますニーズ。必要な場合は、テーブルロックに昇格することができますが、これは役に立ちます。

;WITH CTE_Statistic AS
    (
    SELECT S.ID
        , S.EntityID
        , S.EntityTypeID
        , S.ValueTypeID
        , S.Value
        , S.[Date]
        , S.AggregateTypeID
        , S.JsonData
        , S.WeekDay
    FROM dbo.Statistic AS S WITH (UPDLOCK, ROWLOCK, HOLDLOCK)
    WHERE EXISTS (SELECT TOP (1) 1 FROM @p0 AS P WHERE P.EntityID = S.EntityID)
        AND S.EntityTypeId = @p1
        AND S.ValueTypeId = @p2
        AND S.Date IS NULL
        AND S.AggregateTypeId = @p3 
    )
MERGE INTO CTE_Statistic AS stat
USING
    (SELECT inTbl.EntityId, inTbl.Value FROM @p0 AS inTbl) AS src
ON src.EntityId = stat.EntityId
   AND stat.EntityTypeId = @p1
   AND stat.ValueTypeId = @p2
   AND stat.Date IS NULL
   AND stat.AggregateTypeId = @p3
WHEN MATCHED AND stat.value <> src.value THEN
    UPDATE SET stat.Value = src.value
WHEN NOT MATCHED BY TARGET THEN
    INSERT (EntityTypeId, ValueTypeId, Date, AggregateTypeId, EntityId, Value)
    VALUES
    (@p4, @p5, @p6, @p7, src.entityId, src.value);

2つ目は、例を示したように、これらのインデックスがすぐに断片化し、パフォーマンスが低下することです。これは、インデックス構造(特にクラスター化インデックス)により、インデックス挿入の中間であると想定しているためです。これは、ほとんどの場合と同様に、読み取りよりも書き込みを重視する場合に最適ですが、それを考慮する必要があります。

まず、インデックスのFILL FACTORを指定して、インデックスが再構築されるときに、順不同の挿入用にスペースを確保できるようにします。クラスタインデックスをフィルファクター70に設定し、残りを90から始めます。

たとえば、(この例では、ONLINE = OFFに設定されていない場合はエンタープライズが存在することを前提としており、代わりにreorganizeを使用しています。

ALTER INDEX [IX_Statistic_EntityId_EntityTypeId_ValueTypeId_AggregateTypeId_Date] ON dbo.Statistic REBUILD WITH (ONLINE=ON, FILL_FACTOR=70);  

これはディスク上のより多くのスペースをとりますが、ディスクは通常安価です。

1
Jonathan Fite