昨年の夏、SQL Server 2014用のSP2がリリースされた後、クエリのパフォーマンスの問題を調査するだけでなく、DBCCCLONEDATABASEの使用について書きました。ある読者による最近の投稿へのコメントから、クローンデータベースをテストに使用する方法について私が考えていたことを拡張する必要があると思いました。ピーターは書いた:
「私は主にC#開発者であり、SQL Server(ほとんどすべてのDBAのもの、統計など)を超えることになると、常にT-SQLを作成して処理しますが、あまりよくわかりません。 。このようなクローンDBをパフォーマンスチューニングにどのように使用するのか、本当にわかりません。」ピーター、どうぞ。これがお役に立てば幸いです!
セットアップ
DBCCCLONEDATABASEはSQLServer2016 SP1で利用可能になりました。これは、現在のリリースであり、クエリストアを使用してデータをキャプチャできるため、テストに使用します。作業を楽にするために、Microsoftからサンプルを復元するのではなく、テスト用のデータベースを作成しています。
USE [master]; GO DROP DATABASE IF EXISTS [CustomerDB]、[CustomerDB_CLONE]; GO/*必要に応じてファイルの場所を変更*/CREATE DATABASE [CustomerDB] ON PRIMARY(NAME =N'CustomerDB'、FILENAME =N ' C:\ Databases \ CustomerDB.mdf'、SIZE =512MB、MAXSIZE =UNLIMITED、FILEGROWTH =65536KB)ログオン(NAME =N'CustomerDB_log'、FILENAME =N'C:\ Databases \ CustomerDB_log.ldf'、SIZE =512MB、 MAXSIZE =UNLIMITED、FILEGROWTH =65536KB); GO ALTER DATABASE [CustomerDB] SET RECOVERY SIMPLE;
次に、テーブルを作成してデータを追加します。
USE [CustomerDB]; GO CREATE TABLE [dbo]。[Customers]([CustomerID] [int] NOT NULL、[FirstName] [nvarchar](64)NOT NULL、[LastName] [nvarchar](64)NOT NULL、[EMail] [nvarchar](320)NOT NULL、[Active] [bit] NOT NULL DEFAULT 1、[Created] [datetime] NOT NULL DEFAULT SYSDATETIME()、[Updated] [datetime] NULL、CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED([CustomerID])); GO / *これにより、テーブルに1,000,000行が追加されます。自由に追加*/INSERTdbo.Customers WITH(TABLOCKX)(CustomerID、FirstName、LastName、EMail、[Active])SELECT rn =ROW_NUMBER()OVER(ORDER BY n)、fn、ln、em、FROM( SELECT TOP(1000000)fn、ln、em、a =MAX(a)、n =MAX(NEWID())FROM(SELECT fn、ln、em、a、r =ROW_NUMBER()OVER(PARTITION BY em ORDER BY em )FROM(SELECT TOP(20000000)fn =LEFT(o.name、64)、ln =LEFT(c.name、64)、em =LEFT(o.name、LEN(c.name)%5 + 1)+ '。'+LEFT(c.name、LEN(o.name)%5 + 2)+'@' + RIGHT(c.name、LEN(o.name + c.name)%12 + 1)+ LEFT( RTRIM(CHECKSUM(NEWID()))、3)+'.com'、a =CASE WHEN c.name LIKE'%y%' THEN 0 ELSE 1 END FROM sys.all_objects AS o CROSS JOIN sys.all_columns AS c ORDER BY NEWID())AS x)AS y WHERE r =1 GROUP BY fn、ln、em ORDER BY n)AS z ORDER BY rn; GO CREATE NONCLUSTERED INDEX [PhoneBook_Customers] ON [dbo]。[Customers]([LastName] 、[FirstName])INCLUDE([EMail]);
次に、クエリストアを有効にします:
USE [master]; GO ALTER DATABASE [CustomerDB] SET QUERY_STORE =ON; ALTER DATABASE [CustomerDB] SET QUERY_STORE(OPERATION_MODE =READ_WRITE、CLEANUP_POLICY =(STALE_QUERY_THRESHOLD_DAYS =30)、DATA_FLUSH_INTERVAL_SECONDS =60、INTERVAL_LENGTH_MINUTES =5、MAX_STORAGE_SIZE_MB =256、QUERY_CAPデータベースを作成してデータを入力し、クエリストアを構成したら、テスト用のストアドプロシージャを作成します。
USE [CustomerDB]; GO DROP PROCEDURE IFEXISTS[dbo]。[usp_GetCustomerInfo];GOCREATE OR ALTER PROCEDURE [dbo]。[usp_GetCustomerInfo](@LastName [nvarchar](64))AS SELECT [CustomerID]、[ FirstName]、[LastName]、[Email]、CASE WHEN [Active] =1 THEN'Active' ELSE'Inactive' END [Status]FROM[dbo]。[Customers]WHERE[LastName] =@LastName;注意:SP1で使用できるクールな新しいCREATE ORALTERPROCEDURE構文を使用しました。
ストアドプロシージャを数回実行して、クエリストアにデータを取得します。 WITH RECOMPILEを追加したのは、これら2つの入力値によって異なるプランが生成されることがわかっているため、両方を確実にキャプチャしたいからです。
EXEC[dbo]。[usp_GetCustomerInfo]'name'WITH RECOMPILE;GOEXEC[dbo]。[usp_GetCustomerInfo]'query_cost'WITH RECOMPILE;クエリストアを見ると、ストアドプロシージャからの1つのクエリと、2つの異なるプラン(それぞれが独自のplan_idを持つ)が表示されます。これが本番環境である場合、ランタイム統計(期間、IO、CPU情報)および実行の観点から、はるかに多くのデータがあります。デモのデータは少なくなりますが、理論は同じです。
SELECT [qsq]。[query_id]、[qsp]。[plan_id]、[qsq]。[object_id]、[rs]。[count_executions]、DATEADD(MINUTE、-(DATEDIFF(MINUTE、GETDATE()、 GETUTCDATE()))、[qsp]。[last_execution_time])AS [LocalLastExecutionTime]、[qst]。[query_sql_text]、ConvertedPlan =TRY_CONVERT(XML、[qsp]。[query_plan])FROM[sys]。[query_store_query][ qsq]JOIN[sys]。[query_store_query_text][qst]ON[qsq]。[query_text_id]=[qst]。[query_text_id]JOIN[sys]。[query_store_plan][qsp]ON[qsq]。[query_id]=[ qsp]。[query_id]JOIN[sys]。[query_store_runtime_stats][rs]ON[qsp]。[plan_id]=[rs]。[plan_id]WHERE[qsq]。[object_id]=OBJECT_ID(N'usp_GetCustomerInfo');クエリストアドプロシージャクエリからのデータの保存クエリストアドプロシージャの実行後のデータの保存(query_id =1)2つの異なるプラン(plan_id =1、plan_id =2)
plan_id =1(入力値='名前')のクエリプラン> plan_id =2のクエリプラン(入力値='query_cost')>クエリストアに必要な情報を取得したら、データベースのクローンを作成できます(クエリストアのデータはデフォルトでクローンに含まれます):
DBCC CLONEDATABASE(N'CustomerDB'、N'CustomerDB_CLONE');以前のCLONEDATABASEの投稿で述べたように、クローンデータベースは、クエリパフォーマンスの問題をテストするための製品サポートに使用されるように設計されています。そのため、クローンが作成された後は読み取り専用になります。 DBCC CLONEDATABASEが現在設計されている機能を超えて、Microsoftのドキュメントからこのメモを思い出させてください。
DBCC CLONEDATABASEから生成された新しく生成されたデータベースは、実動データベースとしての使用はサポートされておらず、主にトラブルシューティングと診断の目的で使用されます。テスト用に変更を加えるには、データベースを読み取り専用モードから解除する必要があります。これを本番環境で使用する予定はないので、問題ありません。このクローンデータベースが実稼働環境にある場合は、データベースをバックアップして開発サーバーまたはテストサーバーに復元し、そこでテストを行うことをお勧めします。本番環境でのテストはお勧めしません。また、に対してテストすることもお勧めしません。 本番インスタンス(データベースが異なっていても)
/ *読み取り/書き込みを行います(本番環境で作業していないように、バックアップして別の場所に復元します)* / ALTER DATABASE [CustomerDB_CLONE] SET READ_WRITE WITH NO_WAIT;読み取り/書き込み状態になったので、変更を加えたり、テストを行ったり、メトリックをキャプチャしたりできます。以前と同じ計画を取得していることを確認することから始めます(リマインダー、クローンデータベースにデータがないため、ここには出力が表示されません):
/*同じプランを取得することを確認します*/USE [CustomerDB_CLONE];GOEXEC[dbo]。[usp_GetCustomerInfo]'name';GOEXEC[dbo]。[usp_GetCustomerInfo]'query_cost'WITH RECOMPILE;クエリストアを確認すると、以前と同じplan_id値が表示されます。データがキャプチャされた時間間隔が異なるため、query_id / plan_idの組み合わせには複数の行があります(5に設定したINTERVAL_LENGTH_MINUTES設定によって決定されます)。
SELECT [qsq]。[query_id]、[qsp]。[plan_id]、[qsq]。[object_id]、[rs]。[count_executions]、DATEADD(MINUTE、-(DATEDIFF(MINUTE、GETDATE()、 GETUTCDATE()))、[qsp]。[last_execution_time])AS [LocalLastExecutionTime]、[rsi]。[runtime_stats_interval_id]、[rsi]。[start_time]、[rsi]。[end_time]、[qst]。[query_sql_text] 、ConvertedPlan =TRY_CONVERT(XML、[qsp]。[query_plan])FROM[sys]。[query_store_query][qsq]JOIN[sys]。[query_store_query_text][qst]ON[qsq]。[query_text_id]=[qst]。 [query_text_id]JOIN[sys]。[query_store_plan][qsp]ON[qsq]。[query_id]=[qsp]。[query_id]JOIN[sys]。[query_store_runtime_stats][rs]ON[qsp]。[plan_id]=[rs]。[plan_id]JOIN[sys]。[query_store_runtime_stats_interval][rsi]ON[rs]。[runtime_stats_interval_id]=[rsi]。[runtime_stats_interval_id]WHERE[qsq]。[object_id]=OBJECT_ID(N'usp_GetCustomer; GO
クローンデータベースに対してストアドプロシージャを実行した後にデータをクエリする コード変更のテスト
最初のテストでは、コードへの変更をテストする方法を見てみましょう。具体的には、ストアドプロシージャを変更して、SELECTリストから[Active]列を削除します。
/ * CREATE OR ALTERを使用してプロシージャを変更(クエリから[Active]を削除)* / CREATE OR ALTER PROCEDURE [dbo]。[usp_GetCustomerInfo](@LastName [nvarchar](64))AS SELECT [CustomerID]、[FirstName ]、[LastName]、[Email]FROM[dbo]。[Customers]WHERE[LastName] =@LastName;ストアドプロシージャを再実行します:
EXEC[dbo]。[usp_GetCustomerInfo]'name'WITH RECOMPILE;GOEXEC[dbo]。[usp_GetCustomerInfo]'query_cost'WITH RECOMPILE;実際の実行プランを表示した場合、クエリは最初に作成した非クラスター化インデックスでカバーされているため、両方のクエリが同じプランを使用していることに気付くでしょう。
ストアドプロシージャを変更して[アクティブ]を削除した後の実行プラン
クエリストアで確認できます。新しいプランのplan_idは41です:
SELECT [qsq]。[query_id]、[qsp]。[plan_id]、[qsq]。[object_id]、[rs]。[count_executions]、DATEADD(MINUTE、-(DATEDIFF(MINUTE、GETDATE()、 GETUTCDATE()))、[qsp]。[last_execution_time])AS [LocalLastExecutionTime]、[rsi]。[runtime_stats_interval_id]、[rsi]。[start_time]、[rsi]。[end_time]、[qst]。[query_sql_text] 、ConvertedPlan =TRY_CONVERT(XML、[qsp]。[query_plan])FROM[sys]。[query_store_query][qsq]JOIN[sys]。[query_store_query_text][qst]ON[qsq]。[query_text_id]=[qst]。 [query_text_id]JOIN[sys]。[query_store_plan][qsp]ON[qsq]。[query_id]=[qsp]。[query_id]JOIN[sys]。[query_store_runtime_stats][rs]ON[qsp]。[plan_id]=[rs]。[plan_id]JOIN[sys]。[query_store_runtime_stats_interval][rsi]ON[rs]。[runtime_stats_interval_id]=[rsi]。[runtime_stats_interval_id]WHERE[qsq]。[object_id]=OBJECT_ID(N'usp_GetCustomer;
ストアドプロシージャを変更した後のデータのクエリ ここで、新しいquery_id(40)があることにも気付くでしょう。クエリストアはテキストマッチングを実行し、クエリのテキストを変更したため、新しいquery_idが生成されます。また、useはCREATE OR ALTER構文を使用したため、object_idは同じままであることに注意してください。別の変更を加えましょう。ただし、DROPを使用してからCREATEORALTERを使用します。
/*DROPを使用してプロシージャを変更してからCREATEORALTER([FirstName]と[LastName]を連結)* / DROP PROCEDURE IFEXISTS[dbo]。[usp_GetCustomerInfo];GOCREATE OR ALTERPROCEDURE[dbo]。[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID]、RTRIM([FirstName])+'' + RTRIM([LastName])、[Email]FROM[dbo]。[Customers]WHERE[LastName] =@ LastName;ここで、手順を再実行します:
EXEC[dbo]。[usp_GetCustomerInfo]'name';GOEXEC[dbo]。[usp_GetCustomerInfo]'query_cost'WITH RECOMPILE;これで、クエリストアからの出力がより興味深いものになり、クエリストアの述語がWHERE[qsq]。[object_id]<>0に変更されたことに注意してください。
SELECT [qsq]。[query_id]、[qsp]。[plan_id]、[qsq]。[object_id]、[rs]。[count_executions]、DATEADD(MINUTE、-(DATEDIFF(MINUTE、GETDATE()、 GETUTCDATE()))、[qsp]。[last_execution_time])AS [LocalLastExecutionTime]、[rsi]。[runtime_stats_interval_id]、[rsi]。[start_time]、[rsi]。[end_time]、[qst]。[query_sql_text] 、ConvertedPlan =TRY_CONVERT(XML、[qsp]。[query_plan])FROM[sys]。[query_store_query][qsq]JOIN[sys]。[query_store_query_text][qst]ON[qsq]。[query_text_id]=[qst]。 [query_text_id]JOIN[sys]。[query_store_plan][qsp]ON[qsq]。[query_id]=[qsp]。[query_id]JOIN[sys]。[query_store_runtime_stats][rs]ON[qsp]。[plan_id]=[rs]。[plan_id]JOIN[sys]。[query_store_runtime_stats_interval][rsi]ON[rs]。[runtime_stats_interval_id]=[rsi]。[runtime_stats_interval_id]WHERE[qsq]。[object_id]<>0;DROPを使用してストアドプロシージャを変更した後、データをクエリし、次にCREATE OR ALTER
object_idが661577395に変更され、クエリテキストが変更されたため、新しいquery_id(42)と新しいplan_id(43)があります。このプランはまだ私の非クラスター化インデックスのインデックスシークですが、クエリストアではまだ別のプランです。クエリストアを使用しているときにオブジェクトを変更するための推奨される方法は、DROPおよびCREATEパターンではなくALTERを使用することであることを理解してください。これは本番環境でも当てはまります。このようなテストでは、object_idを同じに保ち、変更を見つけやすくする必要があるためです。
インデックス変更のテスト
テストのパートIIでは、クエリを変更するのではなく、インデックスを変更することでパフォーマンスを改善できるかどうかを確認したいと思います。したがって、ストアドプロシージャを元のクエリに戻し、インデックスを変更します。
CREATE OR ALTER PROCEDURE [dbo]。[usp_GetCustomerInfo](@LastName [nvarchar](64))AS SELECT [CustomerID]、[FirstName]、[LastName]、[Email]、CASE WHEN [Active] =1 THEN 'Active' ELSE'Inactive' END [Status]FROM[dbo]。[Customers]WHERE[LastName] =@LastName; GO / *既存のインデックスを変更して[Active]を追加し、クエリをカバーします* / CREATE NONCLUSTERED INDEX [PhoneBook_Customers] ON [dbo]。[Customers]([LastName]、[FirstName])INCLUDE([EMail]、[Active])WITH(DROP_EXISTING =ON);元のストアドプロシージャを削除したため、元のプランはキャッシュにありません。テストの一環として、最初にこのインデックスを変更した場合は、再コンパイルを強制しない限り、クエリは新しいインデックスを自動的に使用しないことに注意してください。オブジェクトでsp_recompileを使用するか、プロシージャでWITH RECOMPILEオプションを引き続き使用して、2つの異なる値で同じプランを取得したことを確認できます(最初は2つの異なるプランがあったことを思い出してください)。プランはキャッシュにないため、WITH RECOMPILEは必要ありませんが、一貫性を保つためにオンのままにしておきます。
EXEC[dbo]。[usp_GetCustomerInfo]'name'WITH RECOMPILE;GOEXEC[dbo]。[usp_GetCustomerInfo]'query_cost'WITH RECOMPILE;クエリストア内に、別の新しいquery_id(object_idが元のIDとは異なるため)と新しいplan_idが表示されます:
新しいインデックスを追加した後のデータのクエリ プランを確認すると、変更されたインデックスが使用されていることがわかります。
[アクティブ]がインデックスに追加された後のクエリプラン(plan_id =50)
そして、別のプランができたので、さらに一歩進んで本番ワークロードをシミュレートして、さまざまな入力パラメーターを使用して、このストアドプロシージャが同じプランを生成し、新しいインデックスを使用することを確認できます。ただし、ここには注意点があります。インデックスシーク演算子の警告に気付いたかもしれません。これは、[LastName]列に統計がないために発生します。 [Active]をインクルード列としてインデックスを作成すると、統計を更新するためにテーブルが読み取られました。テーブルにデータがないため、統計が不足しています。これは間違いなくインデックステストで覚えておくべきことです。統計が欠落している場合、オプティマイザーはヒューリスティックを使用します。ヒューリスティックは、オプティマイザーに期待するプランを使用するように説得する場合としない場合があります。
概要
私はDBCCCLONEDATABASEの大ファンです。私はクエリストアのさらに大きなファンです。これらの2つを組み合わせると、インデックスとコードの変更をすばやくテストするための優れた機能が得られます。この方法では、主に、改善を検証するための実行計画を検討します。クローンデータベースにはデータがないため、リソースの使用状況と実行時の統計情報を取得して、実行プランで認識されているメリットを証明または反証することはできません。データベースを復元し、データの完全なセットに対してテストする必要があります。クエリストアは、定量的なデータをキャプチャするのに非常に役立ちます。ただし、計画の検証で十分な場合、または現在テストを行っていない場合は、DBCCCLONEDATABASEが探していた簡単なボタンを提供します。クエリストアを使用すると、プロセスがさらに簡単になります。
注意事項:
ストアドプロシージャを呼び出すときにWITHRECOMPILEを使用することはお勧めしません(またはそのように宣言します。PaulWhiteの投稿を参照してください)。このデモでは、パラメーターに依存するストアドプロシージャを作成し、異なる値が異なるプランを生成し、キャッシュからのプランを使用しないようにしたかったため、このオプションを使用しました。
DBCCCLONEDATABASEを使用してSQLServer2014 SP2でこれらのテストを実行することは非常に可能ですが、クエリとメトリックをキャプチャし、パフォーマンスを確認するためのアプローチは明らかに異なります。クエリストアを使用せずに、これと同じテスト方法を確認したい場合は、コメントを残してお知らせください。