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


88

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

たとえば、顧客ごとに最近の注文値の最高または最低を「n」個リストしたい場合があります。

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

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

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

  1. TransactionHistoryMからRまでの文字で始まる製品ごとに、テーブルから最新の5つのトランザクション日付とIDをリストします。
  2. 繰り返しますが、n製品ごとに履歴行がnあり、DaysToManufacture製品属性の5倍です。
  3. 同様に、製品ごとに正確に1行の履歴行が必要な特別な場合(TransactionDate、タイブレークでの最新の単一エントリ)TransactionID

回答:


70

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

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

まず、Production.TransactionHistory特定のセット全体を考えてみましょうProductID

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 \ RowNum

TOPクエリと計画の両方で、2番目()のクエリが最初のクエリよりもはるかに単純であることに気付くでしょう。しかし非常に重要なことは、どちらもTOP実際にインデックスから取り出される行の数を制限するために使用します。コストは推定値であり、無視する価値がありますが、2つのプランに多くの類似点があります。ROW_NUMBER()バージョンでは、それに応じて番号を割り当ててフィルター処理するためのわずかな追加作業が行われます。彼らの仕事。クエリオプティマイザーは、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()する方法はPARTITION BYOVER句で使用することであるため、各製品の番号付けを再開します。これは、以前と同様にフィルタリングできます。計画はまったく異なるものになります。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プラン

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

興味深いことに、ROW_NUMBER()クエリがのINNER JOIN代わりにを使用する場合、LEFT JOIN別のプランが表示されます。

ROW_NUMBER()とINNER JOIN

このプランでは、と同様にネストループを使用しAPPLYます。ただし、Top演算子はないため、各製品のすべてのトランザクションを取得し、以前よりも多くの読み取りを使用します-TransactionHistoryに対して492の読み取り。ここで「結合」オプションを選択しない理由はないので、計画は「十分」と見なされたと思います。それでも-それはブロックしません、それは素晴らしいです-だけではありませんAPPLY

PARTITION BY私はのために用いたカラムはROW_NUMBER()だったh.ProductID私はQOにProductテーブルに参加する前にROWNUM値を生成するオプションを与えたいと思ったので、両方のケースで。を使用する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;

ただし、Join演算子は、「Inner Join」ではなく「Left Outer Join」と表示します。読み取りの数は、まだTransactionHistoryテーブルに対する500未満の読み取りです。

h.ProductIDの代わりにp.ProductIDのPARTITION BY

とにかく-手元の質問に戻って...

質問1に回答しました。2つのオプションを選択して選択できます。個人的には、APPLYオプションが好きです。

これを拡張して変数を使用するには(質問2)、5適宜変更する必要があります。ああ、別のインデックスを追加したので、Production.Product.NameそのDaysToManufacture列に含まれるインデックスがありました。

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シナリオは非常に単純であり、計画にはブロッキングオペレーターがありません。のゼロの数が多いため、TransactionHistoryの読み取りは少なくなりますDaysToManufactureが、実際には、この列を選択することはできません。;)

ブロックを回避する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';

ここでの計画は単純に見えます-ブロックされていませんが、隠れた危険があります。

CTE外への参加

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

卑劣な残留!

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

質問3の 1行のみである特殊なケースでは、明らかに同じクエリを使用できますが、の1代わりに使用し5ます。ただし、追加のオプションがあります。これは、通常の集計を使用することです。

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

このようなクエリは便利な開始点であり、簡単に変更して、タイブレーク用にTransactionIDを引き出すこともできます(その後、分割される連結を使用して)が、インデックス全体を調べるか、製品ごとに詳しく説明しますが、このシナリオでこれまでに得られたものを実際に大きく改善することはできません。

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

ここでは並列性を探求しようとはしていませんし、質問3に非常に深く入り込んだわけでもありません。ここで考慮すべき主なことは、これら2つのオプションが両方とも非常に強力であることです。

私が好むAPPLY。それは明らかで、Top演算子をうまく使用し、ブロッキングを引き起こすことはめったにありません。


44

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/ よりもCTEとウィンドウ化ソリューションの方が好きです。TOPなぜなら、ロジックがより良く分離され、より直感的だからです(私にとって)。一般に(この場合と私の一般的な経験の両方で)、CTEアプローチはより効率的な計画を作成します(下の例を参照)データが大幅に歪んでいます。


AdventureWorksの例-変更なし

  1. TransactionHistoryMからRまでの文字で始まる製品ごとに、テーブルから最新の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計画はより複雑に見えますが、実際にははるかに効率的です。推定コスト%数にはほとんど注意を払いませんが、読み取りの数を減らし、継続時間を大幅に短縮するなど、より重要な実際の観測に注目してください。私もこれらを並列処理なしで実行しましたが、これは違いではありませんでした。ランタイムメトリックとCTEプラン(CROSS APPLYプランは同じまま):

ここに画像の説明を入力してください

ここに画像の説明を入力してください

  1. 繰り返しますが、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()プラン:

ここに画像の説明を入力してください

CROSS APPLY 予定:

ここに画像の説明を入力してください

  1. 同様に、製品ごとに正確に1行の履歴行が必要な特別な場合(TransactionDate、タイブレークでの最新の単一エントリ)TransactionID

繰り返しますが、ここで小さな変更があります。CTEソリューションではTransactionIDOVER()句に追加し、外部フィルターをに変更しrn = 1ます。の場合、to CROSS APPLYを変更し、innerに追加します。TOPTOP (1)TransactionIDORDER 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は、すべての意味のあるアカウントによって好調でした。


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

ただし、コメントで言及されているPaulに似ていますが、2番目と3番目の列が順序付けられているサポートインデックスを追加する場合DESC

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

実際には、より有利な計画が至る所で得られ、CROSS APPLY3つのケースすべてでアプローチが有利になるようにメトリックが反転します。

ここに画像の説明を入力してください

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


これは、すべてのサポートされていなかった2000年のSQL Server、中くらいの醜いだったAPPLYOVER()句。


24

MySQLなどのウィンドウ関数またはを持たないDBMSでは、CROSS APPLYこれを行う方法は標準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 ...)1つの代わりにを使用しますAND o.OrderDate >= (SELECT oi.OrderDate ... LIMIT 1 OFFSET (@top - 1))。SQL-Server FETCH / OFFSETは2012バージョンで構文を追加しました。ここでのクエリはIN (TOP...)、以前のバージョンで動作するように調整されました。


21

主にこのテクニックが他のテクニックとどのように比較されるかを見るために、私は少し異なるアプローチを取りました。オプションがあるのは良いことですよね?

テスト

さまざまな方法が互いにどのように積み重なっているのかを見て始めてみませんか。3セットのテストを行いました。

  1. 最初のセットはDBの変更なしで実行されました
  2. 2番目のセットは、インデックスが作成された後にTransactionDate実行され、ベースのクエリをサポートしProduction.TransactionHistoryます。
  3. 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 テスト1の結果-インデックスなし
アーロンのCTEがここで明らかに勝者です。

テスト2 テスト2の結果-インデックスなし
アーロンのCTE(もう一度)とミカエルの2番目のapply row_number()方法は、2番目に近いものです。

テスト3 テスト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 テスト1の結果-サポートインデックス付き
今回は、少なくとも論理読み取りに関しては、私の方法が先に出てきます。CROSS APPLY法、実験1について以前に最悪のパフォーマーは、期間に勝っても、論理読み取りにCTE方法を打ちます。

テスト2 テスト2の結果-サポートインデックス付き
今回apply row_number()は、読み取りを見るとミカエルの最初のメソッドが勝者となりますが、以前は最悪のパフォーマンスの1つでした。そして今、私のメソッドは、Readsを見るときに非常に近い2番目の場所に入ります。実際、CTEメソッド以外では、残りはすべて読み取りに関してかなり近いです。

テスト3 テスト3の結果-サポートインデックス付き
ここでは、CTEが依然として勝者ですが、他の方法の違いは、インデックスを作成する前に存在していた劇的な違いと比較してほとんど目立ちません。

結論
私の方法の適用可能性はより明確になりましたが、適切なインデックスが適切に設定されていない場合の回復力は低下します。

サポートインデックスとキャッシングを使用

この一連のテストでは、キャッシュを使用しました。私の方法では、他の方法ではアクセスできないメモリ内キャッシュを使用できます。公平を期すために、次の一時テーブルを作成しました。この一時テーブルは、Product.Product3つのテストすべてで、他のメソッドのすべての参照の代わりに使用されました。この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 テスト1の結果-サポートするインデックスとキャッシュ
すべてのメソッドはキャッシングの恩恵を平等に受けているようで、私のメソッドはまだ先に出ています。

テスト2 テスト2の結果-インデックスとキャッシュをサポート
ここでは、ミカエルの最初のapply row_number()メソッドよりも2リードだけ優れているのに対し、私のメソッドは4リード遅れているのに対して、私のメソッドはわずかに先行しているため、ラインアップに違いが見られます。

テスト3 テスト3の結果-インデックスとキャッシュをサポート
下部(行の下)に向かって更新をご覧ください。ここでも、いくつかの違いが見られます。私のメソッドの「パラメータ化された」フレーバーは、AaronのCROSS APPLYメソッドと比較して2回の読み取りでほとんどリードしていません(キャッシュなしで同等)。しかし、本当に奇妙なことは、キャッシングによって悪影響を受けるメソッドを初めて目にすることです。アーロンのCTEメソッド(以前はテスト番号3に最適でした)。しかし、私はそれが当然ではないところで信用を取るつもりはありません。キャッシングなしではアーロンのCTEメソッドは私のメソッドがキャッシングを使用するよりもまだ速いので、この特定の状況に最適なアプローチはアーロンのCTEメソッドのようです。

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

方法

一般的に

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

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

  • 定数:パラメータではなく、インライン定数として置き換え可能な値を送信します。これは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ストアドプロシージャを作成することにより、結果セットを開き、基本的に各サブクエリの結果を連続した結果セット(複数の結果セットではない)としてストリーミングできました。製品情報の外側(すなわちProductIDNameDaysToManufacture)、サブクエリの結果はどこにも保存する必要がなく(メモリまたはディスク)、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を再実行しましたが、結果は実際に変わりました。

テスト3の結果-サポートインデックスあり(TransactionId DESCあり)
上記の結果は、標準の非キャッシングテストのものです。今回は、CROSS APPLYアーロンのテストが示したように、CTEに勝つだけでなく、SQLCLRプロシージャが30リード(woo hoo)でリードしました。

テスト3の結果-サポートインデックス(TransactionId DESCを使用)およびキャッシング
上記の結果は、キャッシュを有効にしたテスト用です。今回は、CTEのパフォーマンスは低下しませんが、CTEのパフォーマンスは低下しませんCROSS APPLY。ただし、現在はSQLCLRプロシージャが23リードでリードしています(再び、フー)。

奪う

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

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

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


2
applyに関連する余分な論理読み取りの理由については、Mikaelの回答の編集を参照してください。
ポールホワイト

18

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()バージョンを入力してください。

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ほんの少し複雑なものとほとんど同じです。実行時間もほぼ同じか少し遅くなります。

それで、なぜ私たちがすでに持っているものよりも良くない答えを思いついたのですか?さて、実世界でもう1つ試してみることがありますが、実際には読み取りに違いがあります。私が説明していないもの*。

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.

私がそれに取り組んでいる間にrow_number()、特定のケースでは行くべきかもしれない第2のバージョンを投入するかもしれません。これらの特定の場合は、実際にほとんどの行が必要になると予想される場合です。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されたTF 8744(および/またはそれ以降のバージョンでは9115)でこれを無効にして、同じ数の論理読み取りを取得できます。プリフェッチは、適切な状況でapply-topの代替手段の利点になる可能性があります。-ポール・ホワイト


11

私は通常、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をStateテーブルに参加させる必要があります。

SELECT [CustomerID]
    ,[OrderDate]
    ,[TotalAmt]
    ,[State]
FROM GiveMeCounts gmc
INNER JOIN StateTable st ON gmc.[State] = st.[State]
    AND gmc.MySeqNum <= st.MaxSeqNum
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.