カーソル、トリガー、動的SQLなど、私たちの多くが敬遠する機能があります。それぞれにユースケースがあることは間違いありませんが、動的SQL内にカーソルがあるトリガーを見ると、それは私たちをうんざりさせる可能性があります(トリプルワーミー)。
プランガイドとsp_prepareは似たようなボートに乗っています。私がそれらのいずれかを使用しているのを見たら、眉を上げます。私がそれらを一緒に使用しているのを見たら、おそらく私の体温をチェックするでしょう。ただし、カーソル、トリガー、動的SQLと同様に、それらにはユースケースがあります。そして最近、それらを一緒に使用することが有益であるというシナリオに出くわしました。
背景
たくさんのデータがあります。そして、そのデータに対して実行されている多くのアプリケーション。これらのアプリケーションの一部、特にサードパーティの既製のアプリケーションは、変更が困難または不可能です。したがって、コンパイルされたアプリケーションがアドホッククエリをSQL Serverに送信する場合、特にプリペアドステートメントとして、インデックスを追加または変更する自由がない場合、いくつかの調整の機会はすぐにテーブルから外れます。
この場合、数百万行のテーブルがありました。簡略化およびサニタイズされたバージョン:
CREATE TABLE dbo.TheThings ( ThingID bigint NOT NULL, TypeID uniqueidentifier NOT NULL, dt1 datetime NOT NULL DEFAULT sysutcdatetime(), dt2 datetime NOT NULL DEFAULT sysutcdatetime(), dt3 datetime NOT NULL DEFAULT sysutcdatetime(), CONSTRAINT PK_TheThings PRIMARY KEY (ThingID) ); CREATE INDEX ix_type ON dbo.TheThings(TypeID); SET NOCOUNT ON; GO DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4', @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; INSERT dbo.TheThings(ThingID, TypeID) SELECT TOP (1000) 1000 + ROW_NUMBER() OVER (ORDER BY name), @guid1 FROM sys.all_columns; INSERT dbo.TheThings(ThingID, TypeID) SELECT TOP (1) 2500, @guid2 FROM sys.all_columns; INSERT dbo.TheThings(ThingID, TypeID) SELECT TOP (1000) 3000 + ROW_NUMBER() OVER (ORDER BY name), @guid1 FROM sys.all_columns;
アプリケーションから準備されたステートメントは次のようになりました(プランキャッシュに表示されます):
(@P0 varchar(8000))SELECT * FROM dbo.TheThings WHERE TypeID = @P0
問題は、TypeID
の一部の値についてです 、何千もの行があります。他の値の場合、10未満になります。1つのパラメータータイプに基づいて間違ったプランが選択(および再利用)された場合、これは他のパラメーターにとって問題になる可能性があります。少数の行を取得するクエリの場合、ルックアップを使用したインデックスシークでカバーされていない追加の列を取得する必要がありますが、700K行を返すクエリの場合は、クラスター化インデックススキャンが必要です。 (理想的には、インデックスがカバーしますが、このオプションは今回のカードには含まれていませんでした。)
実際には、アプリケーションは常にスキャンのバリエーションを取得していましたが、それが約1%の時間必要だったものでした。クエリの99%は、シーク+4または5回のルックアップを使用できたときに200万行のスキャンを使用していました。
次のクエリを実行することで、ManagementStudioでこれを簡単に再現できます。
DBCC FREEPROCCACHE; DECLARE @P0 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4'; SELECT * FROM dbo.TheThings WHERE TypeID = @P0; GO DBCC FREEPROCCACHE; DECLARE @P0 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; SELECT * FROM dbo.TheThings WHERE TypeID = @P0; GO
計画は次のように戻ってきました:
どちらの場合も推定値は1,000行でした。右側の警告は、I/Oの残留によるものです。
パラメータに応じてクエリが正しい選択をしたことをどのように確認できますか?クエリにヒントを追加したり、トレースフラグをオンにしたり、データベース設定を変更したりせずに、再コンパイルする必要があります。
OPTION (RECOMPILE)
を使用してクエリを個別に実行した場合 、必要に応じてシークを取得します:
DBCC FREEPROCCACHE; DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4', @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; SELECT * FROM dbo.TheThings WHERE TypeID = @guid1 OPTION (RECOMPILE); SELECT * FROM dbo.TheThings WHERE TypeID = @guid2 OPTION (RECOMPILE);
RECOMPILEを使用すると、より正確な見積もりを取得し、必要なときにシークします。
ただし、ここでも、クエリにヒントを直接追加することはできませんでした。
プランガイドを試してみましょう
多くの人がプランガイドに対して警告しますが、私たちはここの隅にいるようなものでした。可能であれば、クエリまたはインデックスを変更することを強くお勧めします。しかし、これは次善の策かもしれません。
EXEC sys.sp_create_plan_guide @name = N'TheThingGuide', @stmt = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0', @type = N'SQL', @params = N'@P0 varchar(8000)', @hints = N'OPTION (RECOMPILE)';
簡単そうです。それをテストすることが問題です。 Management Studioでプリペアドステートメントをシミュレートするにはどうすればよいですか?アプリケーションがガイド付きプランを取得していること、およびそれがプランガイドのために明示的に行われていることをどのように確認できますか?
SSMSでこのクエリをシミュレートしようとすると、これは準備されたステートメントではなくアドホックステートメントとして扱われ、プランガイドを取得するためにこれを取得できませんでした:
DECLARE @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- also tried uniqueidentifier SELECT * FROM dbo.TheThings WHERE TypeID = @P0も試しました
動的SQLも機能しませんでした(これもアドホックステートメントとして扱われました):
DECLARE @sql nvarchar(max) = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0', @params nvarchar(max) = N'@P0 varchar(8000)', -- also tried uniqueidentifier @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; EXEC sys.sp_executesql @sql, @params, @P0;
また、プランガイドも取得できないため、これを行うことができませんでした(パラメーター化がここで引き継がれ、準備されたステートメントのように扱われる場合でも、データベース設定を変更する自由がありませんでした) :
SELECT * FROM TheThings WHERE TypeID = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
キャッシュされたプランはプランガイドの使用法について何も示さないため、アプリから実行されているクエリのプランキャッシュを確認できません(SSMSは、実際のプランを生成するときにその情報をXMLに挿入します)。そして、クエリがプランガイドに渡すRECOMPILEヒントを本当に観察している場合、とにかくプランキャッシュ内の証拠をどのように見ることができますか?
sp_prepareを試してみましょう
私は自分のキャリアでsp_prepareをプランガイドよりも使用していません。アプリケーションコードに使用することはお勧めしません。 (Erik Darlingが指摘しているように、推定値は、パラメーターをスニッフィングすることからではなく、密度ベクトルから引き出すことができます。)
私の場合、パフォーマンス上の理由から使用したくありません。アプリからのプリペアドステートメントをシミュレートするために(sp_executeと一緒に)使用したいと思います。
DECLARE @o int; EXEC sys.sp_prepare @o OUTPUT, N'@P0 varchar(8000)', N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0'; EXEC sys.sp_execute @o, 'EE81197A-B2EA-41F4-882E-4A5979ACACE4'; -- PK scan EXEC sys.sp_execute @o, 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- IX seek + lookup
SSMSは、両方の場合にプランガイドが使用されたことを示しています。
再コンパイルのため、これらの結果についてプランキャッシュを確認することはできません。しかし、私のようなシナリオでは、監視、拡張イベントによる明示的なチェック、または最初にこのクエリを調査した症状の緩和を観察することで効果を確認できるはずです(平均実行時間、クエリに注意してください)統計などは、追加のコンパイルによって影響を受ける可能性があります。
結論
これは、プランガイドが有益であり、sp_prepareがアプリケーションで機能することを検証するのに役立つ1つのケースでした。これらはあまり役に立たず、一緒に使用することもあまりありませんが、私にとっては興味深い組み合わせでした。プランガイドがなくても、SSMSを使用してプリペアドステートメントを送信するアプリをシミュレートする場合は、sp_prepareが最適です。 (sp_prepexecも参照してください。これは、同じクエリに対して2つの異なるプランを検証しようとしない場合のショートカットになります。)
この演習は、必ずしも常にパフォーマンスを向上させるためではなく、パフォーマンスの変動を平坦化するためのものであることに注意してください。再コンパイルは明らかに無料ではありませんが、クエリの99%が絶対にひどい計画に固執するのではなく、クエリの99%が250ミリ秒で実行され、1%が5秒で実行されるように少しのペナルティを支払いますまたはクエリの1%。