Stack Overflowには、クラスター化された列ストアインデックスを使用するテーブルがいくつかあり、これらはワークロードの大部分でうまく機能します。しかし、最近、「パーフェクトストーム」(複数のプロセスがすべて同じCCIから削除しようとしている)がCPUを圧倒し、それらがすべて広く並列になり、操作を完了するために戦ったという状況に遭遇しました。 SolarWindsSQLSentryでの外観は次のとおりです。
そして、これらのクエリに関連する興味深い待機は次のとおりです。
競合するクエリはすべて次の形式でした:
DELETE dbo.LargeColumnstoreTable WHERE col1 = @p1 AND col2 = @p2;
計画は次のようになりました:
そして、スキャンの警告は、かなり極端な残留I / Oを知らせてくれました:
テーブルには19億行ありますが、32GBしかありません(ありがとう、列型ストレージです!)。それでも、これらの単一行の削除にはそれぞれ10〜15秒かかり、この時間のほとんどはSOS_SCHEDULER_YIELD
に費やされます。 。
ありがたいことに、このシナリオでは削除操作が非同期である可能性があるため、2つの変更で問題を解決できました(ただし、ここでは大幅に単純化しすぎています):
-
MAXDOP
を制限しました データベースレベルであるため、これらの削除はそれほど並行して行うことはできません - アプリケーションからのプロセスのシリアル化を改善しました(基本的に、単一のディスパッチャーを介して削除をキューに入れました)
DBAとして、MAXDOP
を簡単に制御できます 、クエリレベルでオーバーライドされない限り(別の日の別のウサギの穴)。特に配布されているかどうかにかかわらず、アプリケーションをこの程度まで制御できるとは限りません。この場合、アプリケーションロジックを大幅に変更せずに、書き込みをシリアル化するにはどうすればよいですか?
モックセットアップ
20億行のテーブルをローカルで作成するつもりはありませんが、正確なテーブルは気にしないでください。ただし、より小さなスケールで何かを概算して、同じ問題を再現することはできます。
これがSuggestedEdits
であるとしましょう。 テーブル(実際にはそうではありません)。ただし、Stack Exchange Data Explorerからスキーマをプルできるため、これは簡単な例です。これをベースとして使用して、同等のテーブルを作成し(入力を容易にするためにいくつかの小さな変更を加えて)、クラスター化された列ストアインデックスをそのテーブルにスローできます:
CREATE TABLE dbo.FakeSuggestedEdits ( Id int IDENTITY(1,1), PostId int NOT NULL DEFAULT CONVERT(int, ABS(CHECKSUM(NEWID()))) % 200, CreationDate datetime2 NOT NULL DEFAULT sysdatetime(), ApprovalDate datetime2 NOT NULL DEFAULT sysdatetime(), RejectionDate datetime2 NULL, OwnerUserId int NOT NULL DEFAULT 7, Comment nvarchar (800) NOT NULL DEFAULT NEWID(), Text nvarchar (max) NOT NULL DEFAULT NEWID(), Title nvarchar (250) NOT NULL DEFAULT NEWID(), Tags nvarchar (250) NOT NULL DEFAULT NEWID(), RevisionGUID uniqueidentifier NOT NULL DEFAULT NEWSEQUENTIALID(), INDEX CCI_FSE CLUSTERED COLUMNSTORE );
1億行を入力するには、sys.all_objects
を相互結合します。 およびsys.all_columns
5回(私のシステムでは、毎回268万行が生成されますが、YMMV):
-- 2680350 * 5 ~ 3 minutes INSERT dbo.FakeSuggestedEdits(CreationDate) SELECT TOP (10) /*(2000000) */ modify_date FROM sys.all_objects AS o CROSS JOIN sys.columns AS c; GO 5>
次に、スペースを確認できます:
EXEC sys.sp_spaceused @objname = N'dbo.FakeSuggestedEdits';
わずか1.3GBですが、これで十分です:
クラスター化された列ストアの削除を模倣する
これは、アプリケーションがテーブルに対して行っていたこととほぼ一致する簡単なクエリです。
DECLARE @p1 int = ABS(CHECKSUM(NEWID())) % 10000000, @p2 int = 7; DELETE dbo.FakeSuggestedEdits WHERE Id = @p1 AND OwnerUserId = @p2;
ただし、この計画は完全には一致していません。
それを並行させて、私の貧弱なラップトップで同様の競合を引き起こすために、私はこのヒントでオプティマイザーを少し強制する必要がありました:
OPTION (QUERYTRACEON 8649);
今、それは正しく見えます:
問題の再現
次に、SqlStressCmdを使用して同時削除アクティビティの急増を作成し、16スレッドと32スレッドを使用して1,000のランダムな行を削除できます。
sqlstresscmd -s docs/ColumnStore.json -t 16 sqlstresscmd -s docs/ColumnStore.json -t 32
これがCPUに与える負担を観察できます:
CPUの負担は、それぞれ約64秒と130秒のバッチ全体で持続します。
注:SQLQueryStressからの出力は、反復で少しずれることがありますが、要求された作業が正確に行われることを確認しました。
潜在的な回避策:削除キュー
最初に、データベースにキューテーブルを導入することを考えました。これを使用して、削除アクティビティをオフロードできます。
CREATE TABLE dbo.SuggestedEditDeleteQueue ( QueueID int IDENTITY(1,1) PRIMARY KEY, EnqueuedDate datetime2 NOT NULL DEFAULT sysdatetime(), ProcessedDate datetime2 NULL, Id int NOT NULL, OwnerUserId int NOT NULL );
必要なのは、アプリケーションからのこれらの不正な削除をインターセプトし、バックグラウンド処理のためにキューに配置するためのINSTEADOFトリガーです。残念ながら、クラスター化された列ストアインデックスを持つテーブルにトリガーを作成することはできません:
メッセージ35358、レベル16、状態1クラスター化された列ストアインデックスを使用してテーブルにトリガーを作成できないため、テーブル'dbo.FakeSuggestedEdits'でトリガーを作成できませんでした。他の方法でトリガーのロジックを適用することを検討してください。トリガーを使用する必要がある場合は、代わりにヒープまたはBツリーインデックスを使用してください。
削除を処理するためにストアドプロシージャを呼び出すように、アプリケーションコードに最小限の変更を加える必要があります。
CREATE PROCEDURE dbo.DeleteSuggestedEdit @Id int, @OwnerUserId int AS BEGIN SET NOCOUNT ON; DELETE dbo.FakeSuggestedEdits WHERE Id = @Id AND OwnerUserId = @OwnerUserId; END
これは永続的な状態ではありません。これは、アプリ内の1つだけを変更しながら、動作を同じに保つためです。アプリが変更され、アドホックな削除クエリを送信する代わりにこのストアドプロシージャを正常に呼び出すと、ストアドプロシージャが変更される可能性があります。
CREATE PROCEDURE dbo.DeleteSuggestedEdit @Id int, @OwnerUserId int AS BEGIN SET NOCOUNT ON; INSERT dbo.SuggestedEditDeleteQueue(Id, OwnerUserId) SELECT @Id, @OwnerUserId; END
キューの影響のテスト
ここで、代わりにストアドプロシージャを呼び出すようにSqlQueryStressを変更すると、次のようになります。
DECLARE @p1 int = ABS(CHECKSUM(NEWID())) % 10000000, @p2 int = 7; EXEC dbo.DeleteSuggestedEdit @Id = @p1, @OwnerUserId = @p2;
同様のバッチを送信します(キューに16Kまたは32K行を配置します):
DECLARE @p1 int = ABS(CHECKSUM(NEWID())) % 10000000, @p2 int = 7; EXEC dbo.@Id = @p1 AND OwnerUserId = @p2;
CPUへの影響はわずかに高くなります:
ただし、ワークロードははるかに速く終了します—それぞれ16秒と23秒:
これにより、同時実行性の高い期間に入るときにアプリケーションが感じる苦痛が大幅に軽減されます。
削除を実行する必要がありますが
これらの削除をバックグラウンドで処理する必要がありますが、バッチ処理を導入して、操作間に注入するレートと遅延を完全に制御できるようになりました。キューを処理するためのストアドプロシージャの非常に基本的な構造は次のとおりです(完全に確定したトランザクション制御、エラー処理、またはキューテーブルのクリーンアップはありません):
CREATE PROCEDURE dbo.ProcessSuggestedEditQueue @JobSize int = 10000, @BatchSize int = 100, @DelayInSeconds int = 2 -- must be between 1 and 59 AS BEGIN SET NOCOUNT ON; DECLARE @d TABLE(Id int, OwnerUserId int); DECLARE @rc int = 1, @jc int = 0, @wf nvarchar(100) = N'WAITFOR DELAY ' + CHAR(39) + '00:00:' + RIGHT('0' + CONVERT(varchar(2), @DelayInSeconds), 2) + CHAR(39); WHILE @rc > 0 AND @jc < @JobSize BEGIN DELETE @d; UPDATE TOP (@BatchSize) q SET ProcessedDate = sysdatetime() OUTPUT inserted.Id, inserted.OwnerUserId INTO @d FROM dbo.SuggestedEditDeleteQueue AS q WITH (UPDLOCK, READPAST) WHERE ProcessedDate IS NULL; SET @rc = @@ROWCOUNT; IF @rc = 0 BREAK; DELETE fse FROM dbo.FakeSuggestedEdits AS fse INNER JOIN @d AS d ON fse.Id = d.Id AND fse.OwnerUserId = d.OwnerUserId; SET @jc += @rc; IF @jc > @JobSize BREAK; EXEC sys.sp_executesql @wf; END RAISERROR('Deleted %d rows.', 0, 1, @jc) WITH NOWAIT; END
現在、行の削除には時間がかかります。10,000行の平均は223秒で、そのうち100行までは意図的な遅延です。しかし、待っているユーザーはいないので、誰が気にしますか? CPUプロファイルはほぼゼロであり、アプリは、バックグラウンドジョブとの競合がほぼゼロで、キューにアイテムを必要なだけ同時に追加し続けることができます。 10,000行を処理しているときに、キューにさらに16K行を追加すると、以前と同じCPUが使用され、ジョブが実行されていないときよりも1秒長くかかりました。
そして、計画は次のようになり、推定/実際の行がはるかに良くなりました:
このキューテーブルアプローチは、高いDML同時実行性に対処する効果的な方法であることがわかりますが、DMLを送信するアプリケーションには少なくとも少しの柔軟性が必要です。これが、アプリケーションにストアドプロシージャを呼び出させることが本当に好きな理由の1つです。データをより詳細に制御できます。
その他のオプション
アプリケーションからの削除クエリを変更する機能がない場合、または削除をバックグラウンドプロセスに延期できない場合は、削除の影響を減らすための他のオプションを検討できます。
- ポイントルックアップをサポートするための述語列の非クラスター化インデックス(アプリケーションを変更せずにこれを個別に実行できます)
- ソフト削除のみを使用する(アプリケーションを変更する必要があります)
これらのオプションが同様のメリットを提供するかどうかを確認するのは興味深いことですが、将来の投稿のために保存しておきます。