多くの場合、結果セットの各グループからいくつかの行を選択する必要があります。
たとえば、顧客ごとの「n」個の最高または最低の最近の注文値をリストすることができます。
より複雑なケースでは、リストする行の数はグループごとに異なる場合があります(グループ化/親レコードの属性によって定義されます)。この部分は間違いなくオプション/追加のクレジットであり、人々が答えることを思いとどまらせることを意図していません。
SQL Server 2005以降でこれらの種類の問題を解決するための主なオプションは何ですか?各方法の主な長所と短所は何ですか?
AdventureWorksの例(わかりやすくするため、オプション)
TransactionHistory
テーブルから、最近の5つのトランザクション日付とIDをリストします。n
履歴行が製品ごとにあります。ここで、n
はDaysToManufacture
製品属性の5倍です。TransactionDate
による最新の単一のエントリ、TransactionID
のタイブレーク)。基本的なシナリオから始めましょう。
テーブルからいくつかの行を取得する場合、2つの主要なオプションがあります。またはTOP
。
最初に、特定のProductID
に対する_Production.TransactionHistory
_のセット全体を考えてみましょう。
_SELECT h.TransactionID, h.ProductID, h.TransactionDate
FROM Production.TransactionHistory h
WHERE h.ProductID = 800;
_
これにより418行が返されます。プランは、テーブルのすべての行をチェックしてこれを探していることを示しています-無制限のクラスター化インデックススキャンと、フィルターを提供する述語。 797はここで読みますが、これは醜いです。
それでは、公平を期して、より役立つインデックスを作成しましょう。私たちの条件は、ProductID
の等号一致を要求し、次にTransactionDate
による最新の検索を行います。 TransactionID
も返す必要があるので、CREATE INDEX ix_FindingMostRecent ON Production.TransactionHistory (ProductID, TransactionDate) INCLUDE (TransactionID);
から始めましょう。
これを行うと、計画が大幅に変更され、読み取りがわずか3に減少します。したがって、すでに250倍以上改善されています...
競争の平準化が完了したので、上位のオプションであるランキング関数とTOP
を見てみましょう。
_WITH Numbered AS
(
SELECT h.TransactionID, h.ProductID, h.TransactionDate, ROW_NUMBER() OVER (ORDER BY TransactionDate DESC) AS RowNum
FROM Production.TransactionHistory h
WHERE h.ProductID = 800
)
SELECT TransactionID, ProductID, TransactionDate
FROM Numbered
WHERE RowNum <= 5;
SELECT TOP (5) h.TransactionID, h.ProductID, h.TransactionDate
FROM Production.TransactionHistory h
WHERE h.ProductID = 800
ORDER BY TransactionDate DESC;
_
2番目の(TOP
)クエリは、クエリと計画の両方で、最初のクエリよりもはるかに単純であることがわかります。ただし、非常に重要なことに、どちらもTOP
を使用して、実際にインデックスから取り出される行の数を制限しています。コストは推定値であり、無視するだけの価値がありますが、2つのプランには多くの類似点が見られます。ROW_NUMBER()
バージョンは、数値を割り当て、それに応じてフィルタリングするためにわずかな追加作業を行い、両方のクエリが終了します。彼らの仕事をするためにたった2回の読み取りを行うまでクエリオプティマイザーは、ROW_NUMBER()
フィールドでのフィルター処理のアイデアを確実に認識し、Top演算子を使用して不要な行を無視できることを認識しています。これらのクエリはどちらも十分に優れています-TOP
はコードを変更する価値があるほど優れているわけではありませんが、初心者にとってはシンプルでわかりやすいでしょう。
したがって、これは単一の製品で機能します。ただし、複数の製品でこれを行う必要がある場合はどうなるかを考慮する必要があります。
反復プログラマーは、目的の製品をループして、このクエリを複数回呼び出すという考えを検討します。実際には、カーソルを使用せずにAPPLY
を使用して、この形式でクエリを作成することができます。 。私は_OUTER APPLY
_を使用しています。トランザクションがない場合は、NULLでProductを返したいと考えているためです。
_SELECT p.Name, p.ProductID, t.TransactionID, t.TransactionDate
FROM
Production.Product p
OUTER APPLY (
SELECT TOP (5) h.TransactionID, h.ProductID, h.TransactionDate
FROM Production.TransactionHistory h
WHERE h.ProductID = p.ProductID
ORDER BY TransactionDate DESC
) t
WHERE p.Name >= 'M' AND p.Name < 'S';
_
この計画は、反復的なプログラマーの方法である、ネストされたループで、各製品に対してトップ操作を実行し、シーク(以前に行った2つの読み取り)を行います。これにより、Productに対して4回、TransactionHistoryに対して360回の読み取りが行われます。
ROW_NUMBER()
を使用する方法は、OVER
句で_PARTITION BY
_を使用して、各製品の番号付けを再開することです。これは、以前と同じようにフィルタリングできます。その計画は結局かなり違うものになってしまいます。 TransactionHistoryでの論理読み取りは約15%低くなり、完全なインデックススキャンが行を取得します。
_WITH Numbered AS
(
SELECT p.Name, p.ProductID, h.TransactionID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY h.ProductID ORDER BY h.TransactionDate DESC) AS RowNum
FROM Production.Product p
LEFT JOIN Production.TransactionHistory h ON h.ProductID = p.ProductID
WHERE p.Name >= 'M' AND p.Name < 'S'
)
SELECT Name, ProductID, TransactionID, TransactionDate
FROM Numbered n
WHERE RowNum <= 5;
_
ただし、重要なことに、このプランには高額なソート演算子があります。マージ結合はTransactionHistoryの行の順序を維持していないようです。行番号を見つけるには、データを再利用する必要があります。読み取りは少なくなりますが、このブロッキングソートは苦痛を感じる可能性があります。 APPLY
を使用すると、ネストされたループは数回の読み取りの後に最初の行を非常に迅速に返しますが、並べ替えの場合、ROW_NUMBER()
はほとんどの作業が完了した後にのみ行を返します。
興味深いことに、ROW_NUMBER()
クエリが_INNER JOIN
_ではなく_LEFT JOIN
_を使用する場合、別の計画が考えられます。
このプランは、APPLY
と同様に、ネストされたループを使用します。しかし、Top演算子がないため、各製品のすべてのトランザクションをプルし、以前よりも多くの読み取りを使用します-TransactionHistoryに対して492の読み取り。ここで[マージ結合]オプションを選択しない理由はありません。そのため、このプランは「十分に良い」と見なされていたと思います。それでも、ブロックしません。つまり、ニースです。APPLY
ほどニースではありません。
Productテーブルに結合する前にRowNum値を生成するオプションをQOに提供したかったため、ROW_NUMBER()
に使用した_PARTITION BY
_列はどちらの場合も_h.ProductID
_でした。 _p.ProductID
_を使用すると、_INNER JOIN
_バリエーションと同じ形状プランが表示されます。
_WITH Numbered AS
(
SELECT p.Name, p.ProductID, h.TransactionID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY p.ProductID ORDER BY h.TransactionDate DESC) AS RowNum
FROM Production.Product p
LEFT JOIN Production.TransactionHistory h ON h.ProductID = p.ProductID
WHERE p.Name >= 'M' AND p.Name < 'S'
)
SELECT Name, ProductID, TransactionID, TransactionDate
FROM Numbered n
WHERE RowNum <= 5;
_
ただし、結合演算子は、「内部結合」ではなく「左外部結合」と言います。読み取りの数は、TransactionHistoryテーブルに対して500読み取り未満です。
とにかく-手元の質問に戻ります...
質問1と回答しました。選択して選択できる2つのオプションがあります。個人的には、APPLY
オプションが好きです。
これを拡張して変数番号(質問2)を使用するには、_5
_をそれに応じて変更するだけです。ああ、私は別のインデックスを追加したので、DaysToManufacture
列を含む_Production.Product.Name
_のインデックスがありました。
_WITH Numbered AS
(
SELECT p.Name, p.ProductID, p.DaysToManufacture, h.TransactionID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY h.ProductID ORDER BY h.TransactionDate DESC) AS RowNum
FROM Production.Product p
LEFT JOIN Production.TransactionHistory h ON h.ProductID = p.ProductID
WHERE p.Name >= 'M' AND p.Name < 'S'
)
SELECT Name, ProductID, TransactionID, TransactionDate
FROM Numbered n
WHERE RowNum <= 5 * DaysToManufacture;
SELECT p.Name, p.ProductID, t.TransactionID, t.TransactionDate
FROM
Production.Product p
OUTER APPLY (
SELECT TOP (5 * p.DaysToManufacture) h.TransactionID, h.ProductID, h.TransactionDate
FROM Production.TransactionHistory h
WHERE h.ProductID = p.ProductID
ORDER BY TransactionDate DESC
) t
WHERE p.Name >= 'M' AND p.Name < 'S';
_
そして、どちらの計画も以前とほとんど同じです!
繰り返しになりますが、見積もられたコストは無視してください。ただし、TOPシナリオの方がはるかに単純で、計画にはブロッキングオペレーターがないため、依然としてTOPシナリオが好きです。 DaysToManufacture
のゼロの数が多いため、TransactionHistoryの読み取りは少なくなりますが、実際には、その列を選択することはないと思います。 ;)
ブロックを回避する1つの方法は、結合の右側(プラン内)でROW_NUMBER()
ビットを処理するプランを考え出すことです。 CTEの外で参加することで、これを説得できます。
_WITH Numbered AS
(
SELECT h.TransactionID, h.ProductID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY ProductID ORDER BY TransactionDate DESC) AS RowNum
FROM Production.TransactionHistory h
)
SELECT p.Name, p.ProductID, t.TransactionID, t.TransactionDate
FROM Production.Product p
LEFT JOIN Numbered t ON t.ProductID = p.ProductID
AND t.RowNum <= 5 * p.DaysToManufacture
WHERE p.Name >= 'M' AND p.Name < 'S';
_
ここでの計画はより単純に見えます-それは妨害ではありませんが、隠れた危険があります。
Productテーブルからデータを取得しているCompute Scalarに注目してください。これは_5 * p.DaysToManufacture
_の値を計算しています。この値は、TransactionHistoryテーブルからデータをプルするブランチには渡されず、マージ結合で使用されます。残余として。
したがって、マージ結合はすべての行を消費しますが、最初に必要なものだけでなく、すべての行を消費してから残差チェックを実行します。トランザクション数が増えると危険です。私はこのシナリオのファンではありません-マージ結合の残りの述語はすぐにエスカレートします。私が_APPLY/TOP
_シナリオを好むもう1つの理由。
questionの場合、それがちょうど1行である特別なケースでは、同じクエリを使用できますが、_1
_ではなく_5
_を使用します。ただし、通常の集計を使用するという追加オプションがあります。
_SELECT ProductID, MAX(TransactionDate)
FROM Production.TransactionHistory
GROUP BY ProductID;
_
このようなクエリは便利なスタートです。タイブレークの目的でTransactionIDを引き出すように簡単に変更できます(連結を使用して連結が解除される)が、インデックス全体を調べるか、私たちは製品ごとに飛び込んでいますが、このシナリオで以前に得られたものについては大きな改善はありません。
ただし、ここでは特定のシナリオを検討していることを指摘しておきます。実際のデータと、理想的でない可能性のあるインデックス付け戦略では、走行距離はかなり異なる場合があります。ここでAPPLY
が強いことがわかりましたが、状況によっては遅くなる可能性があります。ただし、ネストされたループを使用する傾向があるため、ブロックされることはめったにありません。
私はここで並列処理を探求しようとしなかった、または質問3に一生懸命潜り込みました。これは、連結と分割の複雑さに基づいて人々がめったに望まない特別なケースと見なしています。ここで考慮すべき主なことは、これら2つのオプションはどちらも非常に強力であることです。
私はAPPLY
を好みます。明らかですが、Top演算子を上手に使用しており、ブロッキングを引き起こすことはほとんどありません。
SQL Server 2005以降でこれを行う一般的な方法は、CTEとウィンドウ関数を使用することです。グループごとの上位nの場合、ROW_NUMBER()
をPARTITION
句とともに使用し、外部クエリでそれに対してフィルタリングできます。したがって、たとえば、顧客ごとの最新の上位5件の注文を次のように表示できます。
_DECLARE @top INT;
SET @top = 5;
;WITH grp AS
(
SELECT CustomerID, OrderID, OrderDate,
rn = ROW_NUMBER() OVER
(PARTITION BY CustomerID ORDER BY OrderDate DESC)
FROM dbo.Orders
)
SELECT CustomerID, OrderID, OrderDate
FROM grp
WHERE rn <= @top
ORDER BY CustomerID, OrderDate DESC;
_
_CROSS APPLY
_でもこれを行うことができます。
_DECLARE @top INT;
SET @top = 5;
SELECT c.CustomerID, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
CROSS APPLY
(
SELECT TOP (@top) OrderID, OrderDate
FROM dbo.Orders AS o
WHERE CustomerID = c.CustomerID
ORDER BY OrderDate DESC
) AS o
ORDER BY c.CustomerID, o.OrderDate DESC;
_
Paulが指定した追加オプションを使用して、Customersテーブルに、顧客ごとに含める行数を示す列があるとします。
_;WITH grp AS
(
SELECT CustomerID, OrderID, OrderDate,
rn = ROW_NUMBER() OVER
(PARTITION BY CustomerID ORDER BY OrderDate DESC)
FROM dbo.Orders
)
SELECT c.CustomerID, grp.OrderID, grp.OrderDate
FROM grp
INNER JOIN dbo.Customers AS c
ON grp.CustomerID = c.CustomerID
AND grp.rn <= c.Number_of_Recent_Orders_to_Show
ORDER BY c.CustomerID, grp.OrderDate DESC;
_
また、_CROSS APPLY
_を使用して、顧客の行数を顧客テーブルのいくつかの列で指定するオプションを追加します。
_SELECT c.CustomerID, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
CROSS APPLY
(
SELECT TOP (c.Number_of_Recent_Orders_to_Show) OrderID, OrderDate
FROM dbo.Orders AS o
WHERE CustomerID = c.CustomerID
ORDER BY OrderDate DESC
) AS o
ORDER BY c.CustomerID, o.OrderDate DESC;
_
これらは、データの分布とサポートするインデックスの可用性に応じて異なる動作をするため、パフォーマンスの最適化と最良の計画の取得は、実際にはローカル要因に依存することに注意してください。
個人的には、_CROSS APPLY
_/TOP
よりもCTEとウィンドウソリューションの方がロジックをより適切に分離し、(私にとって)より直感的であるため、私は好みます。一般的に(この場合と私の一般的な経験の両方で)、CTEアプローチはより効率的な計画を作成します(以下の例)。ただし、これは普遍的な真実として解釈されるべきではありません。特に、インデックスが変更された場合、またはデータは大幅に歪んでいます。
- MからRまでの文字で始まる各製品について、
TransactionHistory
テーブルから、最近の5つのトランザクション日付とIDをリストします。
_-- CTE / OVER()
;WITH History AS
(
SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate,
rn = ROW_NUMBER() OVER
(PARTITION BY t.ProductID ORDER BY t.TransactionDate DESC)
FROM Production.Product AS p
INNER JOIN Production.TransactionHistory AS t
ON p.ProductID = t.ProductID
WHERE p.Name >= N'M' AND p.Name < N'S'
)
SELECT ProductID, Name, TransactionID, TransactionDate
FROM History
WHERE rn <= 5;
-- CROSS APPLY
SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate
FROM Production.Product AS p
CROSS APPLY
(
SELECT TOP (5) TransactionID, TransactionDate
FROM Production.TransactionHistory
WHERE ProductID = p.ProductID
ORDER BY TransactionDate DESC
) AS t
WHERE p.Name >= N'M' AND p.Name < N'S';
_
ランタイムメトリックにおけるこれら2つの比較:
CTE/OVER()
計画:
_CROSS APPLY
_プラン:
CTE計画はより複雑に見えますが、実際にははるかに効率的です。推定コスト%の数値にはほとんど注意を払う必要はありませんが、はるかに少ない読み取りやはるかに短い期間など、より重要なactual観測に焦点を当てます。私もこれらを並列処理なしで実行しましたが、これは違いではありませんでした。ランタイムメトリックとCTEプラン(_CROSS APPLY
_プランは同じまま):
- 再度同じですが、
n
履歴行が製品ごとにあります。ここで、n
はDaysToManufacture
製品属性の5倍です。
ここでは非常に小さな変更が必要です。 CTEの場合、内部クエリに列を追加し、外部クエリでフィルタリングできます。 _CROSS APPLY
_の場合、相関TOP
内で計算を実行できます。これは_CROSS APPLY
_ソリューションにいくらかの効率をもたらすと思いますが、この場合は起こりません。クエリ:
_-- CTE / OVER()
;WITH History AS
(
SELECT p.ProductID, p.Name, p.DaysToManufacture, t.TransactionID, t.TransactionDate,
rn = ROW_NUMBER() OVER
(PARTITION BY t.ProductID ORDER BY t.TransactionDate DESC)
FROM Production.Product AS p
INNER JOIN Production.TransactionHistory AS t
ON p.ProductID = t.ProductID
WHERE p.Name >= N'M' AND p.Name < N'S'
)
SELECT ProductID, Name, TransactionID, TransactionDate
FROM History
WHERE rn <= (5 * DaysToManufacture);
-- CROSS APPLY
SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate
FROM Production.Product AS p
CROSS APPLY
(
SELECT TOP (5 * p.DaysToManufacture) TransactionID, TransactionDate
FROM Production.TransactionHistory
WHERE ProductID = p.ProductID
ORDER BY TransactionDate DESC
) AS t
WHERE p.Name >= N'M' AND p.Name < N'S';
_
実行時の結果:
並列CTE/OVER()
計画:
シングルスレッドCTE/OVER()
plan:
_CROSS APPLY
_プラン:
- 同じように、製品ごとに1つの履歴行だけが必要な特別な場合(
TransactionDate
による最新の単一のエントリ、TransactionID
のタイブレーク)。
ここでも、マイナーな変更があります。 CTEソリューションでは、TransactionID
をOVER()
句に追加し、外側のフィルターを_rn = 1
_に変更します。 _CROSS APPLY
_の場合、TOP
をTOP (1)
に変更し、TransactionID
を内部_ORDER BY
_に追加します。
_-- CTE / OVER()
;WITH History AS
(
SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate,
rn = ROW_NUMBER() OVER
(PARTITION BY t.ProductID ORDER BY t.TransactionDate DESC, TransactionID DESC)
FROM Production.Product AS p
INNER JOIN Production.TransactionHistory AS t
ON p.ProductID = t.ProductID
WHERE p.Name >= N'M' AND p.Name < N'S'
)
SELECT ProductID, Name, TransactionID, TransactionDate
FROM History
WHERE rn = 1;
-- CROSS APPLY
SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate
FROM Production.Product AS p
CROSS APPLY
(
SELECT TOP (1) TransactionID, TransactionDate
FROM Production.TransactionHistory
WHERE ProductID = p.ProductID
ORDER BY TransactionDate DESC, TransactionID DESC
) AS t
WHERE p.Name >= N'M' AND p.Name < N'S';
_
実行時の結果:
並列CTE/OVER()
計画:
シングルスレッドCTE/OVER()プラン:
_CROSS APPLY
_プラン:
ウィンドウ関数は常に最良の選択肢とは限りません(COUNT(*) OVER()
を試してみてください)。これらは、グループごとのn行の問題を解決するための2つのアプローチだけではありませんが、この特定のケースでは、スキーマが与えられます。既存のインデックス、およびデータ分布-CTEは、すべての有意義なアカウントによって、より優れていました。
ただし、サポートインデックスを追加すると、 ポールがコメントで言及したもの に似ていますが、2列目と3列目がDESC
の順序になります。
_CREATE UNIQUE NONCLUSTERED INDEX UQ3 ON Production.TransactionHistory
(ProductID, TransactionDate DESC, TransactionID DESC);
_
実際には、はるかに有利な計画が全体的に得られ、メトリックは、次の3つのケースすべてで_CROSS APPLY
_アプローチを支持するように反転します。
これが私の実稼働環境である場合、おそらくこの場合の期間に満足し、さらに最適化する必要はありません。
これはAPPLY
またはOVER()
句をサポートしていなかったSQL Server 2000ではすべて醜いものでした。
MySQLのように、ウィンドウ関数または_CROSS APPLY
_を持たないDBMSでは、これを行う方法は標準SQL(89)を使用することです。遅い方法は、集約を使用した三角形のクロス結合です。より高速な方法(ただし、クロスアプライまたはrow_number関数を使用するよりも効率的ではない可能性があります)は "貧しい人の_CROSS APPLY
_"。このクエリを他のクエリと比較すると興味深いでしょう。
仮定:Orders (CustomerID, OrderDate)
にはUNIQUE
制約があります:
_DECLARE @top INT;
SET @top = 5;
SELECT o.CustomerID, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
JOIN dbo.Orders AS o
ON o.CustomerID = c.CustomerID
AND o.OrderID IN
( SELECT TOP (@top) oi.OrderID
FROM dbo.Orders AS oi
WHERE oi.CustomerID = c.CustomerID
ORDER BY oi.OrderDate DESC
)
ORDER BY CustomerID, OrderDate DESC ;
_
グループごとにカスタマイズされた上部行の追加の問題について:
_SELECT o.CustomerID, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
JOIN dbo.Orders AS o
ON o.CustomerID = c.CustomerID
AND o.OrderID IN
( SELECT TOP (c.Number_of_Recent_Orders_to_Show) oi.OrderID
FROM dbo.Orders AS oi
WHERE oi.CustomerID = c.CustomerID
ORDER BY oi.OrderDate DESC
)
ORDER BY CustomerID, OrderDate DESC ;
_
注:MySQLでは、AND o.OrderID IN (SELECT TOP(@top) oi.OrderID ...)
の代わりにAND o.OrderDate >= (SELECT oi.OrderDate ... LIMIT 1 OFFSET (@top - 1))
を使用します。 SQL-Serverは、2012バージョンで_FETCH / OFFSET
_構文を追加しました。ここでのクエリは、以前のバージョンで機能するようにIN (TOP...)
で調整されました。
私は少し異なるアプローチをとりました。主に、この手法が他の手法とどのように比較されるかを確認するためです。オプションがあるのは良いことですから?
さまざまな方法が互いにどのように重なり合っているかを見るだけから始めませんか。私は3セットのテストを行いました:
Production.TransactionHistory
_に対するTransactionDate
ベースのクエリをサポートするためにインデックスが作成された後に実行されました。追加のテストの詳細:
AdventureWorks2012
_に対して実行されました。RowCounts
が私のメソッドに対して「オフ」になっているように見えることに注意してください。これは、_CROSS APPLY
_が行っていることの手動実装である私のメソッドによるものです。_Production.Product
_に対して最初のクエリを実行し、161行を取得して、_Production.TransactionHistory
_に対するクエリに使用します。したがって、私のエントリのRowCount
値は、他のエントリより常に161大きくなります。 3番目のテストセット(キャッシュあり)では、行数はすべてのメソッドで同じです。Name >= N'M' AND Name < N'S'
_構文を使用する代わりに、_Name LIKE N'[M-R]%'
_を使用することを選択しました。SQLServerはそれらを同じように扱います。これは、基本的に標準のAdventureWorks2012です。すべての場合において、私の方法は他の方法より明らかに優れていますが、上位1または2の方法ほど優れているわけではありません。
テスト1
アーロンのCTEは明らかにここで勝者です。
テスト2
アーロンのCTE(再び)とミカエルの2番目のapply row_number()
メソッドは、近い秒です。
テスト
アーロンのCTE(再度)が勝者です。
結論TransactionDate
にサポートするインデックスがない場合、私の方法は標準の_CROSS APPLY
_を実行するよりも優れていますが、それでもCTEメソッドを使用する方法が明らかに適しています。
この一連のテストでは、すべてのクエリがそのフィールドでソートするため、_TransactionHistory.TransactionDate
_に明白なインデックスを追加しました。他のほとんどの回答もこの点に同意しているので、「明白」と言います。そして、クエリはすべて最新の日付を必要としているので、TransactionDate
フィールドはDESC
の順序で並べる必要があるため、Mikaelの回答の下部にある_CREATE INDEX
_ステートメントを取得し、明示的にFILLFACTOR
を追加しました。
_CREATE INDEX [IX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
WITH (FILLFACTOR = 100);
_
このインデックスが設定されると、結果はかなり変化します。
テスト1
今回は、少なくとも論理読み取りに関して、私の方法が先に出てきます。以前はテスト1で最悪のパフォーマーであった_CROSS APPLY
_メソッドは、Durationで勝利し、論理読み取りでCTEメソッドよりも優れています。
テスト2
今回は、Readsを見たときに勝者となったMikaelの最初のapply row_number()
メソッドですが、以前は最悪のパフォーマーの1つでした。そして今、私のメソッドは、Readsを見ると非常に近い2番目の場所にあります。実際、CTE方式以外では、残りはすべて読み取りに関してかなり近いです。
テスト
ここで、CTEは依然として勝者ですが、現在、他の方法の違いは、インデックスを作成する前に存在していた劇的な違いと比較してほとんど目立ちません。
結論
適切なインデックスが設定されていない場合の回復力は低くなりますが、私の方法の適用性はより明白になりました。
この一連のテストでは、キャッシュを利用しました。私のメソッドでは、他のメソッドがアクセスできないメモリ内キャッシュを使用できます。公平を期すために、私は次の一時テーブルを作成しました。これは、3つのテストすべてにわたって、他のメソッドのすべての参照に対して_Product.Product
_の代わりに使用されました。 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
ここで、メソッドが少し前に出て、Mikaelの最初のapply row_number()
メソッドよりも2読み取りだけ優れているため、ラインナップに違いがあることがわかります。一方、キャッシュなしでは、私のメソッドは4読み取り遅れていました。
テスト
下部(ラインの下)に向かって更新を参照してください。ここでもいくつかの違いがあります。私のメソッドの「パラメーター化された」フレーバーは、AaronのCROSS APPLYメソッドと比較して2リードでわずかに先行しています(キャッシングがないため、それらは同等でした)。しかし、本当に奇妙なのは、キャッシュによって悪影響を受けるメソッドを初めて目にしたことです。それは、アーロンのCTEメソッド(以前はテスト番号3に最適でした)です。しかし、私はそれが原因ではない場合に信用を取るつもりはありません。キャッシュがないと、AaronのCTEメソッドは、私のメソッドがここにあるキャッシュよりも高速であるため、この特定の状況に対する最良のアプローチは、AaronのCTEメソッドであると思われます。
結論下部(行の下)に向かって更新を参照してください
セカンダリクエリの結果を繰り返し使用する状況では、多くの場合(常にではありません)、それらの結果をキャッシュすることでメリットを得られます。しかし、キャッシングが利点である場合、そのキャッシングにメモリを使用すると、一時テーブルを使用するよりもいくつかの利点があります。
「ヘッダー」クエリ(つまり、ProductID
sを取得し、特定の文字で始まるDaysToManufacture
に基づいてName
を取得する)を、「詳細」クエリ(つまり、TransactionID
sおよびTransactionDate
sを取得する)から分離しました。コンセプトは非常に単純なクエリを実行することであり、オプティマイザがJOINするときに混乱しないようにしました。明らかにこれは常に有利ではありません。これは、オプティマイザが最適化できないようにするためです。しかし、結果からわかるように、クエリのタイプによっては、このメソッドにはメリットがあります。
このメソッドのさまざまなフレーバーの違いは次のとおりです。
定数:パラメータではなく、インライン定数として置き換え可能な値を送信します。これは、3つのテストすべてでProductID
を参照し、テスト2で返される行数も参照します。これは、「DaysToManufacture
Product属性の5倍」の関数であるためです。このサブメソッドは、各ProductID
が独自の実行プランを取得することを意味します。これは、ProductID
のデータ分布に幅広いバリエーションがある場合に役立ちます。しかし、データ分布にほとんど変化がない場合、追加の計画を生成するコストはおそらく価値がありません。
Parameterized:少なくともProductID
を_@ProductID
_として送信し、実行プランのキャッシュと再利用を可能にします。追加のテストオプションがあり、テスト2で返される可変数の行もパラメータとして扱います。
最適化不明:ProductID
を_@ProductID
_として参照する場合、データ分布のバリエーションが大きい場合、他のProductID
値に悪影響を与えるプランをキャッシュして、それが適切になるようにすることができますこのクエリヒントの使用が役立つかどうかを確認します。
キャッシュ製品:毎回_Production.Product
_テーブルをクエリするのではなく、完全に同じリストを取得するためだけに、クエリを1回実行します(その間に、ProductID
sを除外します) TransactionHistory
テーブルでも、リソースを無駄にしないように)、そのリストをキャッシュします。リストにはDaysToManufacture
フィールドを含める必要があります。このオプションを使用すると、最初の実行では論理読み取りの初期ヒットがわずかに高くなりますが、その後に照会されるのはTransactionHistory
テーブルのみです。
わかりましたが、そうですね、ええと、CURSORを使用せずに各結果セットを一時テーブルまたはテーブル変数にダンプせずに、すべてのサブクエリを個別のクエリとして発行する方法はありますか。明らかに、CURSOR/Temp Tableメソッドを実行すると、読み取りと書き込みに非常に明らかに反映されます。まあ、SQLCLRを使用して:)。 SQLCLRストアドプロシージャを作成することで、結果セットを開き、基本的に各サブクエリの結果を連続した結果セット(複数の結果セットではない)としてストリーミングすることができました。製品情報(つまり、ProductID
、Name
、およびDaysToManufacture
)以外では、サブクエリの結果をどこにも保存する必要がなく(メモリまたはディスク)、SQLCLRストアドプロシージャのメイン結果セットとして渡されただけです。これにより、単純なクエリを実行して製品情報を取得し、それを循環して、TransactionHistory
に対して非常に単純なクエリを発行することができました。
そして、これがSQL Server Profilerを使用して統計を取得する必要があった理由です。 SQLCLRストアドプロシージャは、 "実際の実行プランを含める"クエリオプションを設定するか、_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(パラメータ化+最適化不明)
_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
-- wellでタイブレークを指定するのに役立つかもしれないが、「最新」が想定されていないため、明確に述べられていますが、誰もがこの仮定に同意しているようです)、違いを生むのに十分な関係はおそらくないでしょう。
しかし、その後、アーロンは_TransactionId DESC
_を含むサポートインデックスを使用して再テストし、_CROSS APPLY
_メソッドが3つのテストすべての勝者であることを発見しました。これは、CTE方式がテスト番号3(Aaronのテストを反映するキャッシュが使用されなかった場合)に最適であることを示した私のテストとは異なりました。テストが必要な追加のバリエーションがあることは明らかでした。
現在サポートしているインデックスを削除し、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を再実行しましたが、結果は実際に変わりました。
上記の結果は、標準的な非キャッシングテストの結果です。今回は、(Aaronのテストが示したように)_CROSS APPLY
_がCTEを上回っただけでなく、SQLCLRプロシージャが30回のリードでリードしました(うわー)。
上記の結果は、キャッシュを有効にしたテストの結果です。今回は、CTEのパフォーマンスは低下していませんが、_CROSS APPLY
_はそれを上回っています。ただし、今度はSQLCLRプロシージャが23回のリードでリードします(これもまたです)。
お持ち帰り
使用するさまざまなオプションがあります。それぞれに長所があるため、いくつか試してみるのが最善です。ここで行われたテストは、すべてのテスト(サポートインデックスを含む)全体で、最高のパフォーマンスと最悪のパフォーマンスの間の読み取りと期間の両方にかなり小さい差異を示しています。 Readsの変動は約350で、Durationは55 msです。 SQLCLRプロシージャは1つのテストを除いてすべて(読み取りに関して)勝ちましたが、通常、いくつかの読み取りを保存するだけでは、SQLCLRルートを維持するためのメンテナンスコストに値しません。しかし、AdventureWorks2012では、Product
テーブルには504行しかなく、TransactionHistory
には113,443行しかありません。これらのメソッド間のパフォーマンスの違いは、行数が増えるにつれておそらくより顕著になります。
この質問は特定の行のセットを取得することに関するものでしたが、パフォーマンスの最大の唯一の要因は特定のSQLではなく、インデックス作成であることをお見逃しなく。どの方法が本当に最適かを判断する前に、適切なインデックスを設定する必要があります。
ここで見つかった最も重要なレッスンは、CROSS APPLY対CTE対SQLCLRではなく、テストに関するものです。想定しないでください。複数の人からアイデアを得て、できるだけ多くのシナリオをテストしてください。
_APPLY TOP
_またはROW_NUMBER()
?この件に関して、他に何が言えるでしょうか?
違いの簡単な要約であり、本当に短く保つために、オプション2の計画のみを示し、_Production.TransactionHistory
_にインデックスを追加しました。
_create index IX_TransactionHistoryX on
Production.TransactionHistory(ProductID, TransactionDate)
_
row_number()
クエリ:。
_with C as
(
select T.TransactionID,
T.TransactionDate,
P.DaysToManufacture,
row_number() over(partition by P.ProductID order by T.TransactionDate desc) as rn
from Production.Product as P
inner join Production.TransactionHistory as T
on P.ProductID = T.ProductID
where P.Name >= N'M' and
P.Name < N'S'
)
select C.TransactionID,
C.TransactionDate
from C
where C.rn <= 5 * C.DaysToManufacture;
_
_apply top
_バージョン:
_select T.TransactionID,
T.TransactionDate
from Production.Product as P
cross apply (
select top(cast(5 * P.DaysToManufacture as bigint))
T.TransactionID,
T.TransactionDate
from Production.TransactionHistory as T
where P.ProductID = T.ProductID
order by T.TransactionDate desc
) as T
where P.Name >= N'M' and
P.Name < N'S';
_
これらの主な違いは、ネストされたループ結合の下の一番上の式で_apply top
_フィルターが結合するのに対し、_row_number
_バージョンは結合後にフィルターすることです。つまり、実際に必要な数よりも_Production.TransactionHistory
_からの読み取りが多いということです。
結合の前に行を列挙する責任がある演算子を下のブランチにプッシュする方法が存在する場合は、_row_number
_バージョンの方が適している可能性があります。
apply row_number()
versionと入力します。
_select T.TransactionID,
T.TransactionDate
from Production.Product as P
cross apply (
select T.TransactionID,
T.TransactionDate
from (
select T.TransactionID,
T.TransactionDate,
row_number() over(order by T.TransactionDate desc) as rn
from Production.TransactionHistory as T
where P.ProductID = T.ProductID
) as T
where T.rn <= cast(5 * P.DaysToManufacture as bigint)
) as T
where P.Name >= N'M' and
P.Name < N'S';
_
ご覧のとおり、apply row_number()
は_apply top
_とほとんど同じですが、少しだけ複雑です。実行時間もほぼ同じか少し遅くなります。
それで、なぜ私はすでに私たちが持っているものよりも優れていない答えを思いついたのですか?まあ、あなたは現実の世界で試してみることがもう一つあり、実際には読み取りに違いがあります。説明のないもの*。
_APPLY - ROW_NUMBER
(961 row(s) affected)
Table 'TransactionHistory'. Scan count 115, logical reads 230, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Product'. Scan count 1, logical reads 15, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
APPLY - TOP
(961 row(s) affected)
Table 'TransactionHistory'. Scan count 115, logical reads 268, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Product'. Scan count 1, logical reads 15, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
_
私がそれにいる間、私は2番目のrow_number()
バージョンを投入するかもしれません。これらの特定のケースは、_Production.TransactionHistory
_と列挙された_Production.Product
_の間のマージ結合を取得するため、実際に_Production.TransactionHistory
_の行のほとんどが必要になると予想される場合です。
_with C as
(
select T.TransactionID,
T.TransactionDate,
T.ProductID,
row_number() over(partition by T.ProductID order by T.TransactionDate desc) as rn
from Production.TransactionHistory as T
)
select C.TransactionID,
C.TransactionDate
from C
inner join Production.Product as P
on P.ProductID = C.ProductID
where P.Name >= N'M' and
P.Name < N'S' and
C.rn <= 5 * P.DaysToManufacture;
_
並べ替え演算子なしで上記の形状を取得するには、サポートインデックスをTransactionDate
の降順で並べ替えるように変更する必要もあります。
_create index IX_TransactionHistoryX on
Production.TransactionHistory(ProductID, TransactionDate desc)
_
* 編集:追加の論理読み取りは、apply-topで使用される 入れ子ループのプリフェッチ が原因です。 undoc'd TF 8744(またはそれ以降のバージョンでは9115)でこれを無効にして、同じ数の論理読み取りを取得できます。プリフェッチは、適切な状況では、apply-topの代替の利点となります。 -ポールホワイト
私は通常、CTEとウィンドウ関数を組み合わせて使用します。次のようなものを使用してこの答えを得ることができます:
;WITH GiveMeCounts
AS (
SELECT CustomerID
,OrderDate
,TotalAmt
,ROW_NUMBER() OVER (
PARTITION BY CustomerID ORDER BY
--You can change the following field or sort order to whatever you'd like to order by.
TotalAmt desc
) AS MySeqNum
)
SELECT CustomerID, OrderDate, TotalAmt
FROM GiveMeCounts
--Set n per group here
where MySeqNum <= 10
追加のクレジット部分については、異なるグループが異なる行数を返す場合があるため、別のテーブルを使用できます。州などの地理的基準を使用するとします。
+-------+-----------+
| State | MaxSeqnum |
+-------+-----------+
| AK | 10 |
| NY | 5 |
| NC | 23 |
+-------+-----------+
値が異なる可能性がある場合にこれを実現するには、次のようにCTEを状態テーブルに結合する必要があります。
SELECT [CustomerID]
,[OrderDate]
,[TotalAmt]
,[State]
FROM GiveMeCounts gmc
INNER JOIN StateTable st ON gmc.[State] = st.[State]
AND gmc.MySeqNum <= st.MaxSeqNum