私は統計値のテーブルを持っています、それは次のように定義されている何百万ものレコードを保持しています:
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%以上に断片化されます。この断片化を防ぐにはどうすればよいですか?
統計値のテーブルがあり、数百万レコードが保持されています
...
クラスタ化インデックスは、1日程度で90%以上に断片化されます。
clustered index
を見てください。_key
は48 bytes
の長さです。テーブルが十分に大きく、nonclustered indexes
も5つあるため、これは適切な選択ではありません。 すべて48 bytes
ごとにindex level
があるため、すべての非クラスター化インデックスは、必要な領域の少なくとも2倍を占有します。
IMHO、最初にすべきことは、可能であればclustered index key
を変更することです。clustered index
はidentity
で定義できます。これは一意で、常に増加し、狭く、これにより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 escalation
はlocks
ごとに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;
質問には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);
これはディスク上のより多くのスペースをとりますが、ディスクは通常安価です。