先月、OLTPワークロードに関連する列側の暗黙的な変換の問題を抱えている多くのお客様と関わりました。 2つの場合、列側の暗黙的な変換の累積効果が、レビュー中のSQL Serverの全体的なパフォーマンスの問題の根本的な原因でした。残念ながら、状況を改善するために微調整できる魔法の設定または構成オプションはありません。これが事実であるとき。全体的なパフォーマンスに影響を与える可能性のある他のより低い成果を修正するための提案を提供できますが、列側の暗黙的な変換の効果は、修正するスキーマ設計の変更、または列を防ぐためのコード変更のいずれかを必要とするものです-現在のデータベーススキーマに対して完全に発生することからのサイドコンバージョン。
暗黙的な変換は、データベースエンジンが、クエリの実行中に異なるデータ型の値を比較した結果です。データベースエンジン内で発生する可能性のある暗黙的な変換のリストは、Books Onlineのトピック「データ型変換(データベースエンジン)」にあります。暗黙的な変換は、操作中に比較されるデータ型のデータ型の優先順位に基づいて常に発生します。データ型の優先順位は、BooksOnlineのトピック「データ型の優先順位(Transact-SQL)」に記載されています。最近、インデックススキャンの結果となる暗黙的な変換についてブログを書き、最も問題のある暗黙的な変換を特定するために使用できるグラフを提供しました。
テストの設定
インデックススキャンにつながる列側の暗黙的な変換に関連するパフォーマンスのオーバーヘッドを示すために、Sales.SalesOrderDetailテーブルを使用してAdventureWorks2012データベースに対して一連の異なるテストを実行し、テストテーブルとデータセットを作成しました。コンサルタントとして私が見る最も一般的な列側の暗黙的な変換は、列の型がcharまたはvarcharであり、アプリケーションコードがncharまたはnvarcharのパラメーターを渡し、charまたはvarchar列をフィルター処理する場合に発生します。このタイプのシナリオをシミュレートするために、SalesOrderDetailテーブル(SalesOrderDetail_ASCIIという名前)のコピーを作成し、CarrierTrackingNumber列をnvarcharからvarcharに変更しました。さらに、CarrierTrackingNumber列の非クラスター化インデックスを元のSalesOrderDetailテーブルと新しいSalesOrderDetail_ASCIIテーブルに追加しました。
USE [AdventureWorks2012] GO -- Add CarrierTrackingNumber index to original Sales.SalesOrderDetail table IF NOT EXISTS ( SELECT 1 FROM sys.indexes WHERE [object_id] = OBJECT_ID(N'Sales.SalesOrderDetail') AND name=N'IX_SalesOrderDetail_CarrierTrackingNumber' ) BEGIN CREATE INDEX IX_SalesOrderDetail_CarrierTrackingNumber ON Sales.SalesOrderDetail (CarrierTrackingNumber); END GO IF OBJECT_ID('Sales.SalesOrderDetail_ASCII') IS NOT NULL BEGIN DROP TABLE Sales.SalesOrderDetail_ASCII; END GO CREATE TABLE Sales.SalesOrderDetail_ASCII ( SalesOrderID int NOT NULL, SalesOrderDetailID int NOT NULL IDENTITY (1, 1), CarrierTrackingNumber varchar(25) NULL, OrderQty smallint NOT NULL, ProductID int NOT NULL, SpecialOfferID int NOT NULL, UnitPrice money NOT NULL, UnitPriceDiscount money NOT NULL, LineTotal AS (isnull(([UnitPrice]*((1.0)-[UnitPriceDiscount]))*[OrderQty],(0.0))), rowguid uniqueidentifier NOT NULL ROWGUIDCOL, ModifiedDate datetime NOT NULL ); GO SET IDENTITY_INSERT Sales.SalesOrderDetail_ASCII ON; GO INSERT INTO Sales.SalesOrderDetail_ASCII ( SalesOrderID, SalesOrderDetailID, CarrierTrackingNumber, OrderQty, ProductID, SpecialOfferID, UnitPrice, UnitPriceDiscount, rowguid, ModifiedDate ) SELECT SalesOrderID, SalesOrderDetailID, CONVERT(varchar(25), CarrierTrackingNumber), OrderQty, ProductID, SpecialOfferID, UnitPrice, UnitPriceDiscount, rowguid, ModifiedDate FROM Sales.SalesOrderDetail WITH (HOLDLOCK TABLOCKX); GO SET IDENTITY_INSERT Sales.SalesOrderDetail_ASCII OFF; GO ALTER TABLE Sales.SalesOrderDetail_ASCII ADD CONSTRAINT PK_SalesOrderDetail_ASCII_SalesOrderID_SalesOrderDetailID PRIMARY KEY CLUSTERED (SalesOrderID, SalesOrderDetailID); CREATE UNIQUE NONCLUSTERED INDEX AK_SalesOrderDetail_ASCII_rowguid ON Sales.SalesOrderDetail_ASCII (rowguid); CREATE NONCLUSTERED INDEX IX_SalesOrderDetail_ASCII_ProductID ON Sales.SalesOrderDetail_ASCII (ProductID); CREATE INDEX IX_SalesOrderDetail_ASCII_CarrierTrackingNumber ON Sales.SalesOrderDetail_ASCII (CarrierTrackingNumber); GO
新しいSalesOrderDetail_ASCIIテーブルには121,317行があり、サイズは17.5MBで、小さなテーブルのオーバーヘッドを評価するために使用されます。また、ブログのEnlarging the AdventureWorks Sample Databasesスクリプトの修正バージョンを使用して、10倍の大きさのテーブルを作成しました。このスクリプトには、1,334,487行が含まれ、サイズは190MBです。このためのテストサーバーは、以前の記事で使用した、Windows Server2008R2とSQLServer2012を実行する4GBRAMの4vCPUVMと同じであり、ServicePack1とCumulativeUpdate3を備えているため、テーブルは完全にメモリに収まります。 、実行中のテストに影響を与えることからディスクI/Oオーバーヘッドを排除します。
テストワークロードは、一連のPowerShellスクリプトを使用して生成されました。このスクリプトは、ArrayListを構築するSalesOrderDetailテーブルからCarrierTrackingNumbersのリストを選択し、次にArrayListからCarrierTrackingNumberをランダムに選択して、varcharパラメーターとnvarcharパラメーターを使用してSalesOrderDetail_ASCIIテーブルをクエリします。次に、nvarcharパラメーターを使用してSalesOrderDetailテーブルを照会し、列とパラメーターの両方がnvarcharである場所の比較を提供します。個々のテストはそれぞれステートメントを10,000回実行して、持続的なワークロードでのパフォーマンスのオーバーヘッドを測定できるようにします。
#No Implicit Conversions $loop = 10000; Write-Host "Small table no conversion start time:" [DateTime]::Now $query = @"SELECT * FROM Sales.SalesOrderDetail_ASCII " "WHERE CarrierTrackingNumber = @CTNumber;"; while($loop -gt 0) { $Value = Get-Random -InputObject $Results; $SqlCmd = $SqlConn.CreateCommand(); $SqlCmd.CommandText = $query; $SqlCmd.CommandType = [System.Data.CommandType]::Text; $SqlParameter = $SqlCmd.Parameters.AddWithValue("@CTNumber", $Value); $SqlParameter.SqlDbType = [System.Data.SqlDbType]::VarChar; $SqlParameter.Size = 30; $SqlCmd.ExecuteNonQuery() | Out-Null; $loop--; } Write-Host "Small table no conversion end time:" [DateTime]::Now Sleep -Seconds 10; #Small table implicit conversions $loop = 10000; Write-Host "Small table implicit conversions start time:" [DateTime]::Now $query = @"SELECT * FROM Sales.SalesOrderDetail_ASCII " "WHERE CarrierTrackingNumber = @CTNumber;"; while($loop -gt 0) { $Value = Get-Random -InputObject $Results; $SqlCmd = $SqlConn.CreateCommand(); $SqlCmd.CommandText = $query; $SqlCmd.CommandType = [System.Data.CommandType]::Text; $SqlParameter = $SqlCmd.Parameters.AddWithValue("@CTNumber", $Value); $SqlParameter.SqlDbType = [System.Data.SqlDbType]::NVarChar; $SqlParameter.Size = 30; $SqlCmd.ExecuteNonQuery() | Out-Null; $loop--; } Write-Host "Small table implicit conversions end time:" [DateTime]::Now Sleep -Seconds 10; #Small table unicode no implicit conversions $loop = 10000; Write-Host "Small table unicode no implicit conversion start time:" [DateTime]::Now $query = @"SELECT * FROM Sales.SalesOrderDetail " "WHERE CarrierTrackingNumber = @CTNumber;" while($loop -gt 0) { $Value = Get-Random -InputObject $Results; $SqlCmd = $SqlConn.CreateCommand(); $SqlCmd.CommandText = $query; $SqlCmd.CommandType = [System.Data.CommandType]::Text; $SqlParameter = $SqlCmd.Parameters.AddWithValue("@CTNumber", $Value); $SqlParameter.SqlDbType = [System.Data.SqlDbType]::NVarChar; $SqlParameter.Size = 30; $SqlCmd.ExecuteNonQuery() | Out-Null; $loop--; } Write-Host "Small table unicode no implicit conversion end time:" [DateTime]::Now
最初のテストセットと同じパラメーター化を使用して、SalesOrderDetailEnlarged_ASCIIテーブルとSalesOrderDetailEnlargedテーブルに対して2番目のテストセットを実行し、テーブルに格納されているデータのサイズが時間の経過とともに増加するときのオーバーヘッドの違いを示しました。最後の一連のテストは、ProductID列をint、bigint、smallintのパラメータータイプのフィルター列として使用してSalesOrderDetailテーブルに対しても実行され、インデックススキャンを行わない暗黙的な変換のオーバーヘッドの比較を提供しました。比較のために。
注:すべてのスクリプトはこの記事に添付されており、暗黙の変換テストを再現してさらに評価および比較できるようになっています。
テスト結果
各テストの実行中に、パフォーマンスモニターは、各テストのパフォーマンスオーバーヘッドを追跡するために、Processor \%ProcessorTimeおよびSQLServer:SQLStatisitics \ Batch Requests/secカウンターを含むデータコレクターセットを実行するように構成されました。さらに、拡張イベントは、rpc_completedイベントを追跡するように構成されており、各テストの平均期間、cpu_time、および論理読み取りを追跡できます。
小さなテーブルCarrierTrackingNumberの結果
図1-カウンターのパフォーマンスモニターチャート
TestID | 列のデータ型 | パラメータデータ型 | 平均%プロセッサ時間 | 平均バッチリクエスト/秒 | 期間h:mm:ss |
---|---|---|---|---|---|
1 | Varchar | Varchar | 2.5 | 192.3 | 0:00:51 |
2 | Varchar | Nvarchar | 19.4 | 46.7 | 0:03:33 |
3 | Nvarchar | Nvarchar | 2.6 | 192.3 | 0:00:51 |
表2–パフォーマンスモニターのデータ平均
結果から、varcharからnvarcharへの列側の暗黙的な変換と、結果のインデックススキャンが、ワークロードのパフォーマンスに大きな影響を与えることがわかります。カラム側の暗黙的な変換テスト(TestID =2)の平均%プロセッサ時間は、インデックススキャンをもたらすカラム側の暗黙的な変換が行われなかった他のテストのほぼ10倍です。さらに、列側の暗黙的な変換テストの平均バッチリクエスト/秒は、他のテストの25%をわずかに下回りました。データがnvarcharデータ型を使用してテスト番号3でnvarcharとして保存され、2倍のストレージスペースが必要であったにもかかわらず、暗黙的な変換が発生しなかったテストの期間は両方とも51秒かかりました。これは、テーブルがまだバッファプールよりも小さいために予想されます。
TestID | 平均cpu_time(µs) | 平均継続時間(µs) | 平均logical_reads |
---|---|---|---|
1 | 40.7 | 154.9 | 51.6 |
2 | 15,640.8 | 15,760.0 | 385.6 |
3 | 45.3 | 169.7 | 52.7 |
表3–拡張イベントの平均
拡張イベントのrpc_completedイベントによって収集されたデータは、列側の暗黙的な変換を実行しないクエリに関連付けられた平均cpu_time、duration、および論理読み取りがほぼ同等であることを示しています。ここで、列側の暗黙的な変換にはかなりのCPUが必要です。オーバーヘッド、および論理読み取りが大幅に多い平均期間が長くなります。
拡大テーブルCarrierTrackingNumberの結果
図4–カウンターのパフォーマンスモニターチャート
TestID | 列のデータ型 | パラメータデータ型 | 平均%プロセッサ時間 | 平均バッチリクエスト/秒 | 期間h:mm:ss |
---|---|---|---|---|---|
1 | Varchar | Varchar | 7.2 | 164.0 | 0:01:00 |
2 | Varchar | Nvarchar | 83.8 | 15.4 | 0:10:49 |
3 | Nvarchar | Nvarchar | 7.0 | 166.7 | 0:01:00 |
表5–パフォーマンスモニターのデータ平均
データのサイズが大きくなると、列側の暗黙的な変換のパフォーマンスオーバーヘッドも大きくなります。カラム側の暗黙的な変換テスト(TestID =2)の平均%プロセッサ時間は、インデックススキャンをもたらすカラム側の暗黙的な変換が発生しなかった他のテストのほぼ10倍です。さらに、列側の暗黙的な変換テストの平均バッチリクエスト/秒は、他のテストの10%をわずかに下回りました。暗黙的な変換が行われなかったテストの期間は両方とも1分かかりましたが、列側の暗黙的な変換テストの実行には11分近くかかりました。
TestID | 平均cpu_time(µs) | 平均継続時間(µs) | 平均logical_reads |
---|---|---|---|
1 | 728.5 | 1,036.5 | 569.6 |
2 | 214,174.6 | 59,519.1 | 4,358.2 |
3 | 821.5 | 1,032.4 | 553.5 |
表6–拡張イベントの平均
拡張イベントの結果は、実際には、ワークロードの列側の暗黙的な変換によって引き起こされるパフォーマンスのオーバーヘッドを示し始めています。実行あたりの平均cpu_timeは214msを超え、列側の暗黙的な変換がないステートメントのcpu_timeの200倍を超えます。期間も、列側の暗黙的な変換がないステートメントの60倍近くになります。
概要
データのサイズが増え続けると、ワークロードのインデックススキャンにつながる列側の暗黙的な変換に関連するオーバーヘッドも増え続けます。覚えておくべき重要なことは、ある時点でハードウェアの量がなくなることです。パフォーマンスのオーバーヘッドに対処できるようになります。暗黙の変換は、優れたデータベーススキーマ設計が存在する場合に簡単に防ぐことができ、開発者は優れたアプリケーションコーディング手法に従います。アプリケーションのコーディング手法により、nvarcharパラメーター化を利用するパラメーター化が行われる状況では、データベース設計でvarchar列を使用して列側の暗黙的な変換によるパフォーマンスのオーバーヘッドが発生するよりも、データベーススキーマ設計をクエリパラメーター化に一致させる方が適切です。
デモスクリプトをダウンロードします:Implicit_Conversion_Tests.zip(5 KB)