web-dev-qa-db-ja.com

グループごとにn行を取得する

多くの場合、結果セットの各グループからいくつかの行を選択する必要があります。

たとえば、顧客ごとの「n」個の最高または最低の最近の注文値をリストすることができます。

より複雑なケースでは、リストする行の数はグループごとに異なる場合があります(グループ化/親レコードの属性によって定義されます)。この部分は間違いなくオプション/追加のクレジットであり、人々が答えることを思いとどまらせることを意図していません。

SQL Server 2005以降でこれらの種類の問題を解決するための主なオプションは何ですか?各方法の主な長所と短所は何ですか?

AdventureWorksの例(わかりやすくするため、オプション)

  1. MからRまでの文字で始まる各製品について、TransactionHistoryテーブルから、最近の5つのトランザクション日付とIDをリストします。
  2. 再度同じですが、n履歴行が製品ごとにあります。ここで、nDaysToManufacture製品属性の5倍です。
  3. 同じように、製品ごとに1つの履歴行だけが必要な特別な場合(TransactionDateによる最新の単一のエントリ、TransactionIDのタイブレーク)。
92
Paul White 9

基本的なシナリオから始めましょう。

テーブルからいくつかの行を取得する場合、2つの主要なオプションがあります。またはTOP

最初に、特定のProductIDに対する_Production.TransactionHistory_のセット全体を考えてみましょう。

_SELECT h.TransactionID, h.ProductID, h.TransactionDate
FROM Production.TransactionHistory h
WHERE h.ProductID = 800;
_

これにより418行が返されます。プランは、テーブルのすべての行をチェックしてこれを探していることを示しています-無制限のクラスター化インデックススキャンと、フィルターを提供する述語。 797はここで読みますが、これは醜いです。

Expensive Scan with 'Residual' Predicate

それでは、公平を期して、より役立つインデックスを作成しましょう。私たちの条件は、ProductIDの等号一致を要求し、次にTransactionDateによる最新の検索を行います。 TransactionIDも返す必要があるので、CREATE INDEX ix_FindingMostRecent ON Production.TransactionHistory (ProductID, TransactionDate) INCLUDE (TransactionID);から始めましょう。

これを行うと、計画が大幅に変更され、読み取りがわずか3に減少します。したがって、すでに250倍以上改善されています...

Improved plan

競争の平準化が完了したので、上位のオプションであるランキング関数と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;
_

Two plans - basic TOP\RowNum

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回の読み取りが行われます。

APPLY plan

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;
_

ROW_NUMBER plan

ただし、重要なことに、このプランには高額なソート演算子があります。マージ結合はTransactionHistoryの行の順序を維持していないようです。行番号を見つけるには、データを再利用する必要があります。読み取りは少なくなりますが、このブロッキングソートは苦痛を感じる可能性があります。 APPLYを使用すると、ネストされたループは数回の読み取りの後に最初の行を非常に迅速に返しますが、並べ替えの場合、ROW_NUMBER()はほとんどの作業が完了した後にのみ行を返します。

興味深いことに、ROW_NUMBER()クエリが_INNER JOIN_ではなく_LEFT JOIN_を使用する場合、別の計画が考えられます。

ROW_NUMBER() with INNER 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読み取り未満です。

PARTITION BY on p.ProductID instead of h.ProductID

とにかく-手元の質問に戻ります...

質問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';
_

そして、どちらの計画も以前とほとんど同じです!

Variable rows

繰り返しになりますが、見積もられたコストは無視してください。ただし、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';
_

ここでの計画はより単純に見えます-それは妨害ではありませんが、隠れた危険があります。

Joining outside CTE

Productテーブルからデータを取得しているCompute Scalarに注目してください。これは_5 * p.DaysToManufacture_の値を計算しています。この値は、TransactionHistoryテーブルからデータをプルするブランチには渡されず、マージ結合で使用されます。残余として。

Sneaky Residual!

したがって、マージ結合はすべての行を消費しますが、最初に必要なものだけでなく、すべての行を消費してから残差チェックを実行します。トランザクション数が増えると危険です。私はこのシナリオのファンではありません-マージ結合の残りの述語はすぐにエスカレートします。私が_APPLY/TOP_シナリオを好むもう1つの理由。

questionの場合、それがちょうど1行である特別なケースでは、同じクエリを使用できますが、_1_ではなく_5_を使用します。ただし、通常の集計を使用するという追加オプションがあります。

_SELECT ProductID, MAX(TransactionDate)
FROM Production.TransactionHistory
GROUP BY ProductID;
_

このようなクエリは便利なスタートです。タイブレークの目的でTransactionIDを引き出すように簡単に変更できます(連結を使用して連結が解除される)が、インデックス全体を調べるか、私たちは製品ごとに飛び込んでいますが、このシナリオで以前に得られたものについては大きな改善はありません。

ただし、ここでは特定のシナリオを検討していることを指摘しておきます。実際のデータと、理想的でない可能性のあるインデックス付け戦略では、走行距離はかなり異なる場合があります。ここでAPPLYが強いことがわかりましたが、状況によっては遅くなる可能性があります。ただし、ネストされたループを使用する傾向があるため、ブロックされることはめったにありません。

私はここで並列処理を探求しようとしなかった、または質問3に一生懸命潜り込みました。これは、連結と分割の複雑さに基づいて人々がめったに望まない特別なケースと見なしています。ここで考慮すべき主なことは、これら2つのオプションはどちらも非常に強力であることです。

私はAPPLYを好みます。明らかですが、Top演算子を上手に使用しており、ブロッキングを引き起こすことはほとんどありません。

73
Rob Farley

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アプローチはより効率的な計画を作成します(以下の例)。ただし、これは普遍的な真実として解釈されるべきではありません。特に、インデックスが変更された場合、またはデータは大幅に歪んでいます。


AdventureWorksの例-変更なし

  1. 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つの比較:

enter image description here

CTE/OVER()計画:

enter image description here

_CROSS APPLY_プラン:

enter image description here

CTE計画はより複雑に見えますが、実際にははるかに効率的です。推定コスト%の数値にはほとんど注意を払う必要はありませんが、はるかに少ない読み取りやはるかに短い期間など、より重要なactual観測に焦点を当てます。私もこれらを並列処理なしで実行しましたが、これは違いではありませんでした。ランタイムメトリックとCTEプラン(_CROSS APPLY_プランは同じまま):

enter image description here

enter image description here

  1. 再度同じですが、n履歴行が製品ごとにあります。ここで、nDaysToManufacture製品属性の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';
_

実行時の結果:

enter image description here

並列CTE/OVER()計画:

enter image description here

シングルスレッドCTE/OVER() plan:

enter image description here

_CROSS APPLY_プラン:

enter image description here

  1. 同じように、製品ごとに1つの履歴行だけが必要な特別な場合(TransactionDateによる最新の単一のエントリ、TransactionIDのタイブレーク)。

ここでも、マイナーな変更があります。 CTEソリューションでは、TransactionIDOVER()句に追加し、外側のフィルターを_rn = 1_に変更します。 _CROSS APPLY_の場合、TOPTOP (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';
_

実行時の結果:

enter image description here

並列CTE/OVER()計画:

enter image description here

シングルスレッドCTE/OVER()プラン:

enter image description here

_CROSS APPLY_プラン:

enter image description here

ウィンドウ関数は常に最良の選択肢とは限りません(COUNT(*) OVER()を試してみてください)。これらは、グループごとのn行の問題を解決するための2つのアプローチだけではありませんが、この特定のケースでは、スキーマが与えられます。既存のインデックス、およびデータ分布-CTEは、すべての有意義なアカウントによって、より優れていました。


AdventureWorksの例-インデックスを追加する柔軟性

ただし、サポートインデックスを追加すると、 ポールがコメントで言及したもの に似ていますが、2列目と3列目がDESCの順序になります。

_CREATE UNIQUE NONCLUSTERED INDEX UQ3 ON Production.TransactionHistory 
  (ProductID, TransactionDate DESC, TransactionID DESC);
_

実際には、はるかに有利な計画が全体的に得られ、メトリックは、次の3つのケースすべてで_CROSS APPLY_アプローチを支持するように反転します。

enter image description here

これが私の実稼働環境である場合、おそらくこの場合の期間に満足し、さらに最適化する必要はありません。


これはAPPLYまたはOVER()句をサポートしていなかったSQL Server 2000ではすべて醜いものでした。

47
Aaron Bertrand

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...)で調整されました。

24
ypercubeᵀᴹ

私は少し異なるアプローチをとりました。主に、この手法が他の手法とどのように比較されるかを確認するためです。オプションがあるのは良いことですから?

テスト

さまざまな方法が互いにどのように重なり合っているかを見るだけから始めませんか。私は3セットのテストを行いました:

  1. 最初のセットはDB変更なしで実行されました
  2. 2番目のセットは、_Production.TransactionHistory_に対するTransactionDateベースのクエリをサポートするためにインデックスが作成された後に実行されました。
  3. 3番目のセットは少し異なる仮定をしました。 3つのテストすべてが同じ製品のリストに対して実行されたので、そのリストをキャッシュするとどうなるでしょうか。他のメソッドが同等の一時テーブルを使用している間、私のメソッドはメモリ内キャッシュを使用します。 2番目のテストセット用に作成されたサポートインデックスは、このテストセット用にまだ存在しています。

追加のテストの詳細:

  • テストは、SQL Server 2012、SP2(Developer Edition)の_AdventureWorks2012_に対して実行されました。
  • 各テストについて、私は誰の答えからクエリを取得し、どの特定のクエリであるかをラベル付けしました。
  • クエリオプションの[実行後に結果を破棄する]オプションを使用しました。結果。
  • 最初の2つのテストセットでは、RowCountsが私のメソッドに対して「オフ」になっているように見えることに注意してください。これは、_CROSS APPLY_が行っていることの手動実装である私のメソッドによるものです。_Production.Product_に対して最初のクエリを実行し、161行を取得して、_Production.TransactionHistory_に対するクエリに使用します。したがって、私のエントリのRowCount値は、他のエントリより常に161大きくなります。 3番目のテストセット(キャッシュあり)では、行数はすべてのメソッドで同じです。
  • SQL Serverプロファイラを使用して、実行プランに依存する代わりに統計をキャプチャしました。アーロンとミカエルはすでにクエリの計画を示す素晴らしい仕事をしており、その情報を再現する必要はありません。そして、私のメソッドの目的は、クエリをそれほど重要ではないような単純な形式に減らすことです。プロファイラーを使用する理由は他にもありますが、それについては後で説明します。
  • _Name >= N'M' AND Name < N'S'_構文を使用する代わりに、_Name LIKE N'[M-R]%'_を使用することを選択しました。SQLServerはそれらを同じように扱います。

結果

サポートインデックスがありません

これは、基本的に標準のAdventureWorks2012です。すべての場合において、私の方法は他の方法より明らかに優れていますが、上位1または2の方法ほど優れているわけではありません。

テスト1 Test 1 Results-with no index
アーロンのCTEは明らかにここで勝者です。

テスト2 Test 2 Results-with no index
アーロンのCTE(再び)とミカエルの2番目のapply row_number()メソッドは、近い秒です。

テスト Test 3 Results-with no index
アーロンの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 Test 1 Results-with supporting index
今回は、少なくとも論理読み取りに関して、私の方法が先に出てきます。以前はテスト1で最悪のパフォーマーであった_CROSS APPLY_メソッドは、Durationで勝利し、論理読み取りでCTEメソッドよりも優れています。

テスト2 Test 2 Results-with supporting index
今回は、Readsを見たときに勝者となったMikaelの最初のapply row_number()メソッドですが、以前は最悪のパフォーマーの1つでした。そして今、私のメソッドは、Readsを見ると非常に近い2番目の場所にあります。実際、CTE方式以外では、残りはすべて読み取りに関してかなり近いです。

テスト Test 3 Results-with supporting index
ここで、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 Test 1 Results-with supporting index AND caching
すべてのメソッドはキャッシングから同等に利益を得るようであり、私のメソッドはまだ先に出ています。

テスト2 Test 2 Results-with supporting index AND caching
ここで、メソッドが少し前に出て、Mikaelの最初のapply row_number()メソッドよりも2読み取りだけ優れているため、ラインナップに違いがあることがわかります。一方、キャッシュなしでは、私のメソッドは4読み取り遅れていました。

テスト Test 3 Results-with supporting index AND caching
下部(ラインの下)に向かって更新を参照してください。ここでもいくつかの違いがあります。私のメソッドの「パラメーター化された」フレーバーは、AaronのCROSS APPLYメソッドと比較して2リードでわずかに先行しています(キャッシングがないため、それらは同等でした)。しかし、本当に奇妙なのは、キャッシュによって悪影響を受けるメソッドを初めて目にしたことです。それは、アーロンのCTEメソッド(以前はテスト番号3に最適でした)です。しかし、私はそれが原因ではない場合に信用を取るつもりはありません。キャッシュがないと、AaronのCTEメソッドは、私のメソッドがここにあるキャッシュよりも高速であるため、この特定の状況に対する最良のアプローチは、AaronのCTEメソッドであると思われます。

結論下部(行の下)に向かって更新を参照してください
セカンダリクエリの結果を繰り返し使用する状況では、多くの場合(常にではありません)、それらの結果をキャッシュすることでメリットを得られます。しかし、キャッシングが利点である場合、そのキャッシングにメモリを使用すると、一時テーブルを使用するよりもいくつかの利点があります。

メソッド

一般的に

「ヘッダー」クエリ(つまり、ProductIDsを取得し、特定の文字で始まるDaysToManufactureに基づいてNameを取得する)を、「詳細」クエリ(つまり、TransactionIDsおよびTransactionDatesを取得する)から分離しました。コンセプトは非常に単純なクエリを実行することであり、オプティマイザがJOINするときに混乱しないようにしました。明らかにこれは常に有利ではありません。これは、オプティマイザが最適化できないようにするためです。しかし、結果からわかるように、クエリのタイプによっては、このメソッドにはメリットがあります。

このメソッドのさまざまなフレーバーの違いは次のとおりです。

  • 定数:パラメータではなく、インライン定数として置き換え可能な値を送信します。これは、3つのテストすべてでProductIDを参照し、テスト2で返される行数も参照します。これは、「DaysToManufacture Product属性の5倍」の関数であるためです。このサブメソッドは、各ProductIDが独自の実行プランを取得することを意味します。これは、ProductIDのデータ分布に幅広いバリエーションがある場合に役立ちます。しかし、データ分布にほとんど変化がない場合、追加の計画を生成するコストはおそらく価値がありません。

  • Parameterized:少なくともProductIDを_@ProductID_として送信し、実行プランのキャッシュと再利用を可能にします。追加のテストオプションがあり、テスト2で返される可変数の行もパラメータとして扱います。

  • 最適化不明:ProductIDを_@ProductID_として参照する場合、データ分布のバリエーションが大きい場合、他のProductID値に悪影響を与えるプランをキャッシュして、それが適切になるようにすることができますこのクエリヒントの使用が役立つかどうかを確認します。

  • キャッシュ製品:毎回_Production.Product_テーブルをクエリするのではなく、完全に同じリストを取得するためだけに、クエリを1回実行します(その間に、ProductIDsを除外します) TransactionHistoryテーブルでも、リソースを無駄にしないように)、そのリストをキャッシュします。リストにはDaysToManufactureフィールドを含める必要があります。このオプションを使用すると、最初の実行では論理読み取りの初期ヒットがわずかに高くなりますが、その後に照会されるのはTransactionHistoryテーブルのみです。

具体的には

わかりましたが、そうですね、ええと、CURSORを使用せずに各結果セットを一時テーブルまたはテーブル変数にダンプせずに、すべてのサブクエリを個別のクエリとして発行する方法はありますか。明らかに、CURSOR/Temp Tableメソッドを実行すると、読み取りと書き込みに非常に明らかに反映されます。まあ、SQLCLRを使用して:)。 SQLCLRストアドプロシージャを作成することで、結果セットを開き、基本的に各サブクエリの結果を連続した結果セット(複数の結果セットではない)としてストリーミングすることができました。製品情報(つまり、ProductIDName、および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を再実行しましたが、結果は実際に変わりました。

Test 3 Results-with supporting index (with TransactionId DESC)
上記の結果は、標準的な非キャッシングテストの結果です。今回は、(Aaronのテストが示したように)_CROSS APPLY_がCTEを上回っただけでなく、SQLCLRプロシージャが30回のリードでリードしました(うわー)。

Test 3 Results-with supporting index (with TransactionId DESC) AND caching
上記の結果は、キャッシュを有効にしたテストの結果です。今回は、CTEのパフォーマンスは低下していませんが、_CROSS APPLY_はそれを上回っています。ただし、今度はSQLCLRプロシージャが23回のリードでリードします(これもまたです)。

お持ち帰り

  1. 使用するさまざまなオプションがあります。それぞれに長所があるため、いくつか試してみるのが最善です。ここで行われたテストは、すべてのテスト(サポートインデックスを含む)全体で、最高のパフォーマンスと最悪のパフォーマンスの間の読み取りと期間の両方にかなり小さい差異を示しています。 Readsの変動は約350で、Durationは55 msです。 SQLCLRプロシージャは1つのテストを除いてすべて(読み取りに関して)勝ちましたが、通常、いくつかの読み取りを保存するだけでは、SQLCLRルートを維持するためのメンテナンスコストに値しません。しかし、AdventureWorks2012では、Productテーブルには504行しかなく、TransactionHistoryには113,443行しかありません。これらのメソッド間のパフォーマンスの違いは、行数が増えるにつれておそらくより顕著になります。

  2. この質問は特定の行のセットを取得することに関するものでしたが、パフォーマンスの最大の唯一の要因は特定のSQLではなく、インデックス作成であることをお見逃しなく。どの方法が本当に最適かを判断する前に、適切なインデックスを設定する必要があります。

  3. ここで見つかった最も重要なレッスンは、CROSS APPLY対CTE対SQLCLRではなく、テストに関するものです。想定しないでください。複数の人からアイデアを得て、できるだけ多くのシナリオをテストしてください。

21
Solomon Rutzky

_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;
_

enter image description here

_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';
_

enter image description here

これらの主な違いは、ネストされたループ結合の下の一番上の式で_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';
_

enter image description here

ご覧のとおり、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;
_

enter image description here

並べ替え演算子なしで上記の形状を取得するには、サポートインデックスをTransactionDateの降順で並べ替えるように変更する必要もあります。

_create index IX_TransactionHistoryX on 
  Production.TransactionHistory(ProductID, TransactionDate desc)
_

* 編集:追加の論理読み取りは、apply-topで使用される 入れ子ループのプリフェッチ が原因です。 undoc'd TF 8744(またはそれ以降のバージョンでは9115)でこれを無効にして、同じ数の論理読み取りを取得できます。プリフェッチは、適切な状況では、apply-topの代替の利点となります。 -ポールホワイト

18
Mikael Eriksson

私は通常、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
11