主にこのテクニックが他のテクニックとどのように比較されるかを見るために、私は少し異なるアプローチを取りました。オプションがあるのは良いことですよね?
テスト
さまざまな方法が互いにどのように積み重なっているのかを見て始めてみませんか。3セットのテストを行いました。
- 最初のセットはDBの変更なしで実行されました
- 2番目のセットは、インデックスが作成された後に
TransactionDate
実行され、ベースのクエリをサポートしProduction.TransactionHistory
ます。
- 3番目のセットは、少し異なる仮定をしました。3つのテストすべてが同じ製品リストに対して実行されたため、そのリストをキャッシュした場合はどうなりますか?私の方法はメモリ内キャッシュを使用し、他の方法は同等の一時テーブルを使用しました。2番目のテストセット用に作成されたサポートインデックスは、このテストセット用にまだ存在しています。
追加のテストの詳細:
- テストは
AdventureWorks2012
、SQL Server 2012 SP2(Developer Edition)で実行されました。
- 各テストに対して、クエリの取得元と回答を特定したラベルを付けました。
- [クエリオプション]の[実行後に結果を破棄]オプションを使用しました。結果。
- 最初の2セットのテストでは
RowCounts
、私のメソッドでは「オフ」に見えることに注意してください。これは、私のメソッドが何CROSS APPLY
をしているのかを手動で実装しているためです。最初のクエリを実行しProduction.Product
て161行を取得し、それをに対してクエリに使用しProduction.TransactionHistory
ます。したがって、RowCount
私のエントリの値は常に他のエントリよりも161増えます。テストの3番目のセット(キャッシュあり)では、行カウントはすべてのメソッドで同じです。
- 実行計画に依存する代わりに、SQL Server Profilerを使用して統計をキャプチャしました。アーロンとミカエルはすでに彼らの質問の計画を示す素晴らしい仕事をしており、その情報を再現する必要はありません。そして、私のメソッドの目的は、クエリをそれほど重要ではないような単純な形式に減らすことです。プロファイラーを使用する別の理由がありますが、それについては後で説明します。
Name >= N'M' AND Name < N'S'
構造を使用するのではなく、を使用することを選択しName LIKE N'[M-R]%'
、SQL Serverはそれらを同じように扱います。
結果
サポートインデックスなし
これは基本的にすぐに使えるAdventureWorks2012です。すべての場合において、私の方法は他の方法より明らかに優れていますが、トップ1または2の方法ほど優れたものではありません。
テスト1
アーロンのCTEがここで明らかに勝者です。
テスト2
アーロンのCTE(もう一度)とミカエルの2番目のapply row_number()
方法は、2番目に近いものです。
テスト3
アーロンのCTE(再び)が勝者です。
結論に
サポートするインデックスがない場合TransactionDate
、私の方法は標準を実行するよりも優れていますCROSS APPLY
が、それでも、CTEメソッドを使用することは明らかに進むべき方法です。
サポートインデックスあり(キャッシュなし)
この一連のテストではTransactionHistory.TransactionDate
、すべてのクエリがそのフィールドでソートされるため、明確なインデックスを追加しました。他のほとんどの答えもこの点に同意するので、私は「明白」と言います。そして、クエリはすべて最新の日付をTransactionDate
必要としているDESC
ため、フィールドを順序付けする必要があります。そのためCREATE INDEX
、Mikaelの答えの下部にあるステートメントを取得し、明示的に追加しましたFILLFACTOR
。
CREATE INDEX [IX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
WITH (FILLFACTOR = 100);
このインデックスが設定されると、結果はかなり変化します。
テスト1
今回は、少なくとも論理読み取りに関しては、私の方法が先に出てきます。CROSS APPLY
法、実験1について以前に最悪のパフォーマーは、期間に勝っても、論理読み取りにCTE方法を打ちます。
テスト2
今回apply row_number()
は、読み取りを見るとミカエルの最初のメソッドが勝者となりますが、以前は最悪のパフォーマンスの1つでした。そして今、私のメソッドは、Readsを見るときに非常に近い2番目の場所に入ります。実際、CTEメソッド以外では、残りはすべて読み取りに関してかなり近いです。
テスト3
ここでは、CTEが依然として勝者ですが、他の方法の違いは、インデックスを作成する前に存在していた劇的な違いと比較してほとんど目立ちません。
結論
私の方法の適用可能性はより明確になりましたが、適切なインデックスが適切に設定されていない場合の回復力は低下します。
サポートインデックスとキャッシングを使用
この一連のテストでは、キャッシュを使用しました。私の方法では、他の方法ではアクセスできないメモリ内キャッシュを使用できます。公平を期すために、次の一時テーブルを作成しました。この一時テーブルは、Product.Product
3つのテストすべてで、他のメソッドのすべての参照の代わりに使用されました。このDaysToManufacture
フィールドはテスト番号2でのみ使用されますが、同じテーブルを使用するためにSQLスクリプト間で一貫性を保つことが容易であり、そこにあることを害することはありませんでした。
CREATE TABLE #Products
(
ProductID INT NOT NULL PRIMARY KEY,
Name NVARCHAR(50) NOT NULL,
DaysToManufacture INT NOT NULL
);
INSERT INTO #Products (ProductID, Name, DaysToManufacture)
SELECT p.ProductID, p.Name, p.DaysToManufacture
FROM Production.Product p
WHERE p.Name >= N'M' AND p.Name < N'S'
AND EXISTS (
SELECT *
FROM Production.TransactionHistory th
WHERE th.ProductID = p.ProductID
);
ALTER TABLE #Products REBUILD WITH (FILLFACTOR = 100);
テスト1
すべてのメソッドはキャッシングの恩恵を平等に受けているようで、私のメソッドはまだ先に出ています。
テスト2
ここでは、ミカエルの最初のapply row_number()
メソッドよりも2リードだけ優れているのに対し、私のメソッドは4リード遅れているのに対して、私のメソッドはわずかに先行しているため、ラインアップに違いが見られます。
テスト3
下部(行の下)に向かって更新をご覧ください。ここでも、いくつかの違いが見られます。私のメソッドの「パラメータ化された」フレーバーは、AaronのCROSS APPLYメソッドと比較して2回の読み取りでほとんどリードしていません(キャッシュなしで同等)。しかし、本当に奇妙なことは、キャッシングによって悪影響を受けるメソッドを初めて目にすることです。アーロンのCTEメソッド(以前はテスト番号3に最適でした)。しかし、私はそれが当然ではないところで信用を取るつもりはありません。キャッシングなしではアーロンのCTEメソッドは私のメソッドがキャッシングを使用するよりもまだ速いので、この特定の状況に最適なアプローチはアーロンのCTEメソッドのようです。
結論 最下部(行の下)の更新を参照してください。
セカンダリクエリの結果を繰り返し使用する状況では、多くの場合(常にではないが)結果をキャッシュすることでメリットが得られます。ただし、キャッシングが利点である場合、一時的なテーブルを使用するよりも、キャッシングにメモリを使用することにはいくつかの利点があります。
方法
一般的に
「ヘッダー」クエリ(つまり、ProductID
sを取得し、特定の文字で始まるDaysToManufacture
に基づいてName
)を「詳細」クエリ(つまり、TransactionID
sおよびTransactionDate
sを取得)から分離しました。コンセプトは、非常に単純なクエリを実行し、オプティマイザがそれらを結合するときに混乱しないようにすることでした。明らかに、これはオプティマイザが最適化を許可しないため、常に有利であるとは限りません。しかし、結果で見たように、クエリのタイプに応じて、このメソッドにはメリットがあります。
このメソッドのさまざまなフレーバーの違いは次のとおりです。
定数:パラメータではなく、インライン定数として置き換え可能な値を送信します。これはProductID
、3つのテストすべてと、「DaysToManufacture
製品属性の5倍」の関数であるため、テスト2で返される行数を指します。このサブメソッドは、それぞれProductID
が独自の実行計画を取得することを意味します。これは、のデータ分布に大きなばらつきがある場合に役立ちProductID
ます。ただし、データの分布にわずかなばらつきがある場合、追加のプランを生成するコストはおそらく価値がありません。
パラメータ化:少なくともProductID
として送信し@ProductID
、実行計画のキャッシュと再利用を可能にします。テスト2で返す可変行数をパラメーターとして扱う追加のテストオプションがあります。
不明な最適化:ProductID
として参照する@ProductID
場合、データ分布にさまざまなバリエーションがある場合、他のProductID
値に悪影響を与えるプランをキャッシュすることができるため、このクエリヒントの使用が役に立つかどうかを知ることができます。
製品をキャッシュする:Production.Product
毎回テーブルをクエリするのではなく、まったく同じリストを取得するために、クエリを1回実行します(そして、その間ProductID
に、TransactionHistory
テーブルにないsをすべて除外して無駄を省きます)リソースがあります)、そのリストをキャッシュします。リストにはDaysToManufacture
フィールドが含まれている必要があります。このオプションを使用すると、最初の実行では論理読み取りでわずかに高い初期ヒットがありますが、その後TransactionHistory
はクエリされるのはテーブルのみです。
具体的には
わかりましたが、そうですね、CURSORを使用せずに各サブクエリを個別のクエリとして発行し、各結果セットを一時テーブルまたはテーブル変数にダンプする方法はありますか?CURSOR / Temp Tableメソッドを実行すると、読み取りと書き込みに明らかに反映されます。さて、SQLCLRを使用して:)。SQLCLRストアドプロシージャを作成することにより、結果セットを開き、基本的に各サブクエリの結果を連続した結果セット(複数の結果セットではない)としてストリーミングできました。製品情報の外側(すなわちProductID
、Name
とDaysToManufacture
)、サブクエリの結果はどこにも保存する必要がなく(メモリまたはディスク)、SQLCLRストアドプロシージャのメイン結果セットとしてそのまま渡されます。これにより、単純なクエリを実行して製品情報を取得し、それを循環して、に対して非常に単純なクエリを発行できましたTransactionHistory
。
そして、これが統計をキャプチャするためにSQL Server Profilerを使用しなければならなかった理由です。SQLCLRストアドプロシージャは、 "Include Actual Execution Plan"クエリオプションを設定するか、を発行することにより、実行プランを返しませんでしたSET STATISTICS XML ON;
。
製品情報のキャッシュには、readonly static
ジェネリックリストを使用しました(_GlobalProducts
以下のコード)。コレクションに追加することは違反していないようだreadonly
アセンブリがある場合、したがって、このコードは動作しますが、オプションをPERMISSON_SET
するSAFE
ことが直感に反している場合でも、:)を。
生成されたクエリ
このSQLCLRストアドプロシージャによって生成されるクエリは次のとおりです。
製品情報
テスト番号1および3(キャッシュなし)
SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM Production.Product prod1
WHERE prod1.Name LIKE N'[M-R]%';
テスト番号2(キャッシュなし)
;WITH cte AS
(
SELECT prod1.ProductID
FROM Production.Product prod1 WITH (INDEX(AK_Product_Name))
WHERE prod1.Name LIKE N'[M-R]%'
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM Production.Product prod2
INNER JOIN cte
ON cte.ProductID = prod2.ProductID;
テスト番号1、2、および3(キャッシュ)
;WITH cte AS
(
SELECT prod1.ProductID
FROM Production.Product prod1 WITH (INDEX(AK_Product_Name))
WHERE prod1.Name LIKE N'[M-R]%'
AND EXISTS (
SELECT *
FROM Production.TransactionHistory th
WHERE th.ProductID = prod1.ProductID
)
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM Production.Product prod2
INNER JOIN cte
ON cte.ProductID = prod2.ProductID;
取引情報
テスト番号1および2(定数)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC;
テスト番号1および2(パラメーター化)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
テスト番号1および2(パラメーター化+最適化不明)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));
テスト番号2(パラメーター化された両方)
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
テスト番号2(パラメーター化された両方+ OPTIMIZE UNKNOWN)
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));
テスト番号3(定数)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC, th.TransactionID DESC;
テスト番号3(パラメーター化)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
;
テスト番号3(パラメーター化+ OPTIMIZE UNKNOWN)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));
コード
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
public class ObligatoryClassName
{
private class ProductInfo
{
public int ProductID;
public string Name;
public int DaysToManufacture;
public ProductInfo(int ProductID, string Name, int DaysToManufacture)
{
this.ProductID = ProductID;
this.Name = Name;
this.DaysToManufacture = DaysToManufacture;
return;
}
}
private static readonly List<ProductInfo> _GlobalProducts = new List<ProductInfo>();
private static void PopulateGlobalProducts(SqlBoolean PrintQuery)
{
if (_GlobalProducts.Count > 0)
{
if (PrintQuery.IsTrue)
{
SqlContext.Pipe.Send(String.Concat("I already haz ", _GlobalProducts.Count,
" entries :)"));
}
return;
}
SqlConnection _Connection = new SqlConnection("Context Connection = true;");
SqlCommand _Command = new SqlCommand();
_Command.CommandType = CommandType.Text;
_Command.Connection = _Connection;
_Command.CommandText = @"
;WITH cte AS
(
SELECT prod1.ProductID
FROM Production.Product prod1 WITH (INDEX(AK_Product_Name))
WHERE prod1.Name LIKE N'[M-R]%'
AND EXISTS (
SELECT *
FROM Production.TransactionHistory th
WHERE th.ProductID = prod1.ProductID
)
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM Production.Product prod2
INNER JOIN cte
ON cte.ProductID = prod2.ProductID;
";
SqlDataReader _Reader = null;
try
{
_Connection.Open();
_Reader = _Command.ExecuteReader();
while (_Reader.Read())
{
_GlobalProducts.Add(new ProductInfo(_Reader.GetInt32(0), _Reader.GetString(1),
_Reader.GetInt32(2)));
}
}
catch
{
throw;
}
finally
{
if (_Reader != null && !_Reader.IsClosed)
{
_Reader.Close();
}
if (_Connection != null && _Connection.State != ConnectionState.Closed)
{
_Connection.Close();
}
if (PrintQuery.IsTrue)
{
SqlContext.Pipe.Send(_Command.CommandText);
}
}
return;
}
[Microsoft.SqlServer.Server.SqlProcedure]
public static void GetTopRowsPerGroup(SqlByte TestNumber,
SqlByte ParameterizeProductID, SqlBoolean OptimizeForUnknown,
SqlBoolean UseSequentialAccess, SqlBoolean CacheProducts, SqlBoolean PrintQueries)
{
SqlConnection _Connection = new SqlConnection("Context Connection = true;");
SqlCommand _Command = new SqlCommand();
_Command.CommandType = CommandType.Text;
_Command.Connection = _Connection;
List<ProductInfo> _Products = null;
SqlDataReader _Reader = null;
int _RowsToGet = 5; // default value is for Test Number 1
string _OrderByTransactionID = "";
string _OptimizeForUnknown = "";
CommandBehavior _CmdBehavior = CommandBehavior.Default;
if (OptimizeForUnknown.IsTrue)
{
_OptimizeForUnknown = "OPTION (OPTIMIZE FOR (@ProductID UNKNOWN))";
}
if (UseSequentialAccess.IsTrue)
{
_CmdBehavior = CommandBehavior.SequentialAccess;
}
if (CacheProducts.IsTrue)
{
PopulateGlobalProducts(PrintQueries);
}
else
{
_Products = new List<ProductInfo>();
}
if (TestNumber.Value == 2)
{
_Command.CommandText = @"
;WITH cte AS
(
SELECT prod1.ProductID
FROM Production.Product prod1 WITH (INDEX(AK_Product_Name))
WHERE prod1.Name LIKE N'[M-R]%'
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM Production.Product prod2
INNER JOIN cte
ON cte.ProductID = prod2.ProductID;
";
}
else
{
_Command.CommandText = @"
SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM Production.Product prod1
WHERE prod1.Name LIKE N'[M-R]%';
";
if (TestNumber.Value == 3)
{
_RowsToGet = 1;
_OrderByTransactionID = ", th.TransactionID DESC";
}
}
try
{
_Connection.Open();
// Populate Product list for this run if not using the Product Cache
if (!CacheProducts.IsTrue)
{
_Reader = _Command.ExecuteReader(_CmdBehavior);
while (_Reader.Read())
{
_Products.Add(new ProductInfo(_Reader.GetInt32(0), _Reader.GetString(1),
_Reader.GetInt32(2)));
}
_Reader.Close();
if (PrintQueries.IsTrue)
{
SqlContext.Pipe.Send(_Command.CommandText);
}
}
else
{
_Products = _GlobalProducts;
}
SqlDataRecord _ResultRow = new SqlDataRecord(
new SqlMetaData[]{
new SqlMetaData("ProductID", SqlDbType.Int),
new SqlMetaData("Name", SqlDbType.NVarChar, 50),
new SqlMetaData("TransactionID", SqlDbType.Int),
new SqlMetaData("TransactionDate", SqlDbType.DateTime)
});
SqlParameter _ProductID = new SqlParameter("@ProductID", SqlDbType.Int);
_Command.Parameters.Add(_ProductID);
SqlParameter _RowsToReturn = new SqlParameter("@RowsToReturn", SqlDbType.Int);
_Command.Parameters.Add(_RowsToReturn);
SqlContext.Pipe.SendResultsStart(_ResultRow);
for (int _Row = 0; _Row < _Products.Count; _Row++)
{
// Tests 1 and 3 use previously set static values for _RowsToGet
if (TestNumber.Value == 2)
{
if (_Products[_Row].DaysToManufacture == 0)
{
continue; // no use in issuing SELECT TOP (0) query
}
_RowsToGet = (5 * _Products[_Row].DaysToManufacture);
}
_ResultRow.SetInt32(0, _Products[_Row].ProductID);
_ResultRow.SetString(1, _Products[_Row].Name);
switch (ParameterizeProductID.Value)
{
case 0x01:
_Command.CommandText = String.Format(@"
SELECT TOP ({0}) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC{2}
{1};
", _RowsToGet, _OptimizeForUnknown, _OrderByTransactionID);
_ProductID.Value = _Products[_Row].ProductID;
break;
case 0x02:
_Command.CommandText = String.Format(@"
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
{0};
", _OptimizeForUnknown);
_ProductID.Value = _Products[_Row].ProductID;
_RowsToReturn.Value = _RowsToGet;
break;
default:
_Command.CommandText = String.Format(@"
SELECT TOP ({0}) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = {1}
ORDER BY th.TransactionDate DESC{2};
", _RowsToGet, _Products[_Row].ProductID, _OrderByTransactionID);
break;
}
_Reader = _Command.ExecuteReader(_CmdBehavior);
while (_Reader.Read())
{
_ResultRow.SetInt32(2, _Reader.GetInt32(0));
_ResultRow.SetDateTime(3, _Reader.GetDateTime(1));
SqlContext.Pipe.SendResultsRow(_ResultRow);
}
_Reader.Close();
}
}
catch
{
throw;
}
finally
{
if (SqlContext.Pipe.IsSendingResults)
{
SqlContext.Pipe.SendResultsEnd();
}
if (_Reader != null && !_Reader.IsClosed)
{
_Reader.Close();
}
if (_Connection != null && _Connection.State != ConnectionState.Closed)
{
_Connection.Close();
}
if (PrintQueries.IsTrue)
{
SqlContext.Pipe.Send(_Command.CommandText);
}
}
}
}
テストクエリ
ここにテストを投稿するのに十分なスペースがないので、別の場所を見つけます。
結論
特定のシナリオでは、SQLCLRを使用して、T-SQLで実行できないクエリの特定の側面を操作できます。また、一時テーブルの代わりにキャッシュにメモリを使用する機能がありますが、メモリはシステムに自動的に解放されないため、慎重に行う必要があります。また、このメソッドはアドホッククエリを支援するものではありませんが、実行されるクエリのより多くの側面を調整するためにパラメータを追加するだけで、ここで示したよりも柔軟にすることができます。
更新
追加テスト
サポートインデックスを含む私の元のテストではTransactionHistory
、次の定義を使用しました。
ProductID ASC, TransactionDate DESC
私はTransactionId DESC
、最後にテスト番号3を助けるかもしれないと考えながら、最後を控えることを決めていました(これは、最新のタイブレークを指定します- TransactionId
まあ、明確に述べられていないので、「最新」が仮定されますが、誰もがこの仮定に同意するために)、違いを生むのに十分な関係はないでしょう。
しかし、その後、アーロンは、3つのテストすべてでメソッドが勝者TransactionId DESC
であるCROSS APPLY
ことが判明したサポートインデックスで再テストしました。これは、テスト番号3にCTE方式が最適であることを示す私のテストとは異なりました(キャッシュが使用されていない場合、アーロンのテストを反映しています)。テストする必要がある追加のバリエーションがあることは明らかでした。
現在のサポートインデックスを削除し、で新しいインデックスを作成TransactionId
し、プランキャッシュをクリアしました(念のため)。
DROP INDEX [IX_TransactionHistoryX] ON Production.TransactionHistory;
CREATE UNIQUE INDEX [UIX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC, TransactionID DESC)
WITH (FILLFACTOR = 100);
DBCC FREEPROCCACHE WITH NO_INFOMSGS;
テスト番号1を再実行しましたが、結果は予想どおり同じでした。次に、テスト番号3を再実行しましたが、結果は実際に変わりました。
上記の結果は、標準の非キャッシングテストのものです。今回は、CROSS APPLY
アーロンのテストが示したように、CTEに勝つだけでなく、SQLCLRプロシージャが30リード(woo hoo)でリードしました。
上記の結果は、キャッシュを有効にしたテスト用です。今回は、CTEのパフォーマンスは低下しませんが、CTEのパフォーマンスは低下しませんCROSS APPLY
。ただし、現在はSQLCLRプロシージャが23リードでリードしています(再び、フー)。
奪う
使用するさまざまなオプションがあります。それぞれに長所があるため、いくつか試してみるのが最善です。ここで行われたテストでは、すべてのテスト(サポートインデックスを使用)で、最高のパフォーマンスと最低のパフォーマンスの間で、読み取りと継続時間の両方でかなり小さな差異が示されています。読み取りの変動は約350で、継続時間は55ミリ秒です。SQLCLR procは1回のテスト(Readの観点から)を除くすべてで勝ちましたが、通常、いくつかのReadを保存するだけでSQLCLRルートを維持するためのメンテナンスコストの価値はありません。ただし、AdventureWorks2012では、Product
テーブルには504行TransactionHistory
しかなく、113,443行しかありません。これらのメソッド間のパフォーマンスの違いは、行数が増加するにつれておそらくより顕著になります。
この質問は特定の行セットの取得に固有のものでしたが、パフォーマンスの最大の要因は特定のSQLではなくインデックス作成であったことも見逃してはなりません。どのメソッドが本当に最適であるかを判断する前に、適切なインデックスを設定する必要があります。
ここで見られる最も重要な教訓は、CROSS APPLY対CTE対SQLCLRに関するものではなく、テストに関するものです。想定しないでください。複数の人からアイデアを取得し、できるだけ多くのシナリオをテストします。