web-dev-qa-db-ja.com

ウィンドウ関数を使用したサブクエリの最適化

私のパフォーマンスチューニングスキルは十分とは思えないので、一部のクエリに対してmore最適化を実行できるかどうか常に疑問に思っています。この質問が関係する状況は、サブクエリ内にネストされたウィンドウ化MAX関数です。

私が調べているデータは、より大きなセットのさまざまなグループでの一連のトランザクションです。重要なフィールドは4つあります。トランザクションの一意のID、トランザクションのバッチのグループID、およびそれぞれの一意のトランザクションまたはトランザクションのグループに関連付けられた日付です。ほとんどの場合、グループの日付はバッチの最大一意のトランザクションの日付と一致しますが、システムで手動調整が行われ、グループのトランザクションの日付がキャプチャされた後に一意の日付の操作が発生する場合があります。この手動編集では、グループの日付は意図的に調整されません。

このクエリで特定するのは、一意の日付がグループの日付の後にあるレコードです。次のサンプルクエリは、私のシナリオにほぼ相当するものを構築し、SELECTステートメントは探しているレコードを返しますが、このソリューションに最も効率的な方法でアプローチしていますか?これは、ファクトテーブルの読み込み中にレコードが上位9桁の数を数えるため、実行に時間がかかりますが、ほとんどの場合、サブクエリを無視することで、ここにもっと良いアプローチがあるかどうか疑問に思います。インデックスは既に用意されていると確信しているので、インデックスについては心配していません。私が探しているのは、同じことを実現するが、さらに効率的な代替のクエリアプローチです。どんなフィードバックでも大歓迎です。

CREATE TABLE #Example
(
    UniqueID INT IDENTITY(1,1)
  , GroupID INT
  , GroupDate DATETIME
  , UniqueDate DATETIME
)

CREATE CLUSTERED INDEX [CX_1] ON [#Example]
(
    [UniqueID] ASC
)


SET NOCOUNT ON

--Populate some test data
DECLARE @i INT = 0, @j INT = 5, @UniqueDate DATETIME, @GroupDate DATETIME

WHILE @i < 10000
BEGIN

    IF((@i + @j)%173 = 0)
    BEGIN
        SET @UniqueDate = GETDATE()+@i+5
    END
    ELSE
    BEGIN
        SET @UniqueDate = GETDATE()+@i
    END

    SET @GroupDate = GETDATE()+(@j-1)

    INSERT INTO #Example (GroupID, GroupDate, UniqueDate)
    VALUES (@j, @GroupDate, @UniqueDate)

    SET @i = @i + 1

    IF (@i % 5 = 0)
    BEGIN
        SET @j = @j+5
    END
END
SET NOCOUNT OFF

CREATE NONCLUSTERED INDEX [IX_2_4_3] ON [#Example]
(
    [GroupID] ASC,
    [UniqueDate] ASC,
    [GroupDate] ASC
)
INCLUDE ([UniqueID])

-- Identify any UniqueDates that are greater than the GroupDate within their GroupID
SELECT UniqueID
     , GroupID
     , GroupDate
     , UniqueDate
FROM (
    SELECT UniqueID
         , GroupID
         , GroupDate
         , UniqueDate
         , MAX(UniqueDate) OVER (PARTITION BY GroupID) AS maxUniqueDate
    FROM #Example
    ) calc_maxUD
WHERE maxUniqueDate > GroupDate
    AND maxUniqueDate = UniqueDate

DROP TABLE #Example

dbfiddle ここ

8
John Eisbrener

インデックスを提供していないので、インデックスはないと仮定しています。

すぐに次のインデックスを使用すると、プラン内の並べ替え演算子が削除されます。そうしないと、大量のメモリを消費する可能性があります。

CREATE INDEX IX ON #Example (GroupID, UniqueDate) INCLUDE (UniqueID, GroupDate);

この場合、サブクエリはパフォーマンスの問題ではありません。どちらかと言えば、ネスト関数とテーブルスプール構造を回避するために、ウィンドウ関数(MAX ... OVER)を排除する方法を検討します。

同じインデックスを使用すると、次のクエリは一見効率が悪いように見える可能性があります。ベーステーブルでのスキャンは2〜3回行われますが、スプールオペレーターが不足しているため、内部で大量の読み取りが行われなくなります。特に十分なCPUコアがあり、サーバーにIOパフォーマンスがある場合は、特にパフォーマンスが向上すると思います。

SELECT e.UniqueID
     , e.GroupID
     , e.GroupDate
     , e.UniqueDate
FROM (
    SELECT GroupID, MAX(UniqueDate) AS maxUniqueDate
    FROM #Example
    GROUP BY GroupID) AS agg
INNER JOIN #Example AS e ON agg.GroupID=e.GroupID
WHERE agg.maxUniqueDate > e.GroupDate
    AND agg.maxUniqueDate = e.UniqueDate
OPTION (MERGE JOIN);

(注:MERGE JOINクエリのヒントですが、統計が整えば、おそらく自動的に行われるはずです。ベストプラクティスは、可能であれば、このようなヒントを省くことです。)

9

SQL Server 2012からSQL Server 2016にアップグレードできる場合は、新しいバッチモードのWindow Aggregateオペレーターによって提供される大幅に向上したパフォーマンス(特にフレームレスウィンドウアグリゲート)を利用できる可能性があります。

ほとんどすべての大規模なデータ処理シナリオは、行ストアよりも列ストアストレージの方がうまく機能します。ベーステーブルの列ストアに変更しなくても、いずれかのベーステーブルに空の非クラスター化列ストアフィルターインデックスを作成するか、列ストア構成への外部結合を冗長化することにより、新しい2016演算子とバッチモード実行の利点を引き続き利用できますテーブル。

2番目のオプションを使用すると、クエリは次のようになります。

-- Just to get batch mode processing and the window aggregate operator
CREATE TABLE #Dummy (a integer NOT NULL, INDEX DummyCC CLUSTERED COLUMNSTORE);

-- Identify any UniqueDates that are greater than the GroupDate within their GroupID
SELECT
    calc_maxUD.UniqueID,
    calc_maxUD.GroupID,
    calc_maxUD.GroupDate,
    calc_maxUD.UniqueDate
FROM 
(
    SELECT
        E.UniqueID,
        E.GroupID,
        E.GroupDate,
        E.UniqueDate,
        maxUniqueDate = MAX(UniqueDate) OVER (
            PARTITION BY GroupID)
    FROM #Example AS E
    LEFT JOIN #Dummy AS D -- The only change to the original query
        ON 1 = 0
) AS calc_maxUD
WHERE 
    calc_maxUD.maxUniqueDate > calc_maxUD.GroupDate
    AND calc_maxUD.maxUniqueDate = calc_maxUD.UniqueDate;

db <> fiddle

元のクエリに対する唯一の変更は、空の一時テーブルを作成し、左結合を追加することです。実行計画は次のとおりです。

batch mode window aggregate plan

(58 row(s) affected)
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0
Table '#Example'. Scan count 1, logical reads 40, physical reads 0, read-ahead reads 0

詳細とオプションについては、Itzik Ben-Ganの優れたシリーズ SQL Server 2016のバッチモードウィンドウ集計演算子について知っておくべきこと (3つの部分)を参照してください。

11
Paul White 9

私はそこにオールクロス申請を投げるつもりです:

SELECT e.*
    FROM #Example AS e
    CROSS APPLY ( SELECT TOP 1 e2.UniqueDate AS maxUniqueDate
                    FROM #Example AS e2
                    WHERE e2.GroupID = e.GroupID 
                    ORDER BY e2.UniqueDate DESC
                    ) AS ca
    WHERE ca.maxUniqueDate > e.GroupDate
        AND ca.maxUniqueDate = e.UniqueDate;

なんらかのインデックスがあれば、かなりうまくいきます。

CREATE CLUSTERED INDEX cx_whatever ON #Example (GroupID)

CREATE UNIQUE NONCLUSTERED INDEX ix_whatever ON #Example (GroupID, UniqueDate DESC, GroupDate)

Stats timeとioは次のようになります(クエリが最初の結果です)

Table 'Worktable'. Scan count 3, logical reads 28004, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#Example'. Scan count 1, logical reads 51, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 15 ms,  elapsed time = 20 ms.

Table '#Example'. Scan count 10001, logical reads 21336, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 16 ms,  elapsed time = 11 ms.

クエリプランがここにあります(ここでも、あなたのものが最初です)。

https://www.brentozar.com/pastetheplan/?id=BJYJvqAal

なぜこのバージョンを好むのですか?糸巻きは避けます。それらがディスクにこぼれ始めたら、醜くなります。

しかし、これも試してみたいかもしれません。

SELECT e.*
    FROM #Example AS e
    CROSS APPLY ( SELECT e2.UniqueDate AS maxUniqueDate
                    FROM #Example AS e2
                    WHERE e2.GroupID = e.GroupID 
                    ) AS ca
    WHERE ca.maxUniqueDate > e.GroupDate
        AND ca.maxUniqueDate = e.UniqueDate;

これが大きなDWである場合、TOP 1クエリの最後でフィルター演算子として使用するのではなく、ハッシュ結合と結合での行フィルタリングを使用することをお勧めします。

計画はこちら: https://www.brentozar.com/pastetheplan/?id=BkUF55ATx

ここで統計時間とio:

Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#Example'. Scan count 2, logical reads 84, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 16 ms,  elapsed time = 5 ms.

お役に立てれば!

@ypercubeのアイデアに基づく1つの編集と新しいインデックス。

CREATE NONCLUSTERED INDEX ix_meh ON #Example (UniqueDate,GroupDate) INCLUDE (UniqueID,GroupID);

WITH t1 AS 
(
    SELECT DISTINCT
    e.GroupID ,
    MAX(UniqueDate) AS MaxUniqueDate
    FROM #Example AS e
    GROUP BY e.GroupID
)
SELECT *
FROM #Example AS e
CROSS APPLY (
SELECT *
FROM t1
    WHERE t1.MaxUniqueDate > e.GroupDate
        AND t1.MaxUniqueDate = e.UniqueDate
        AND t1.GroupID = e.GroupID
) ca

統計の時間とioは次のとおりです。

Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#Example'. Scan count 2, logical reads 91, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 4 ms.

ここに計画があります:

https://www.brentozar.com/pastetheplan/?id=SJv8foR6g

7
Erik Darling

top with ties

GroupDateGroupIdごとに同じ場合:

select top 1 with ties 
   UniqueID
 , GroupID
 , GroupDate
 , UniqueDate
from #Example
where UniqueDate > GroupDate
order by row_number() over (partition by GroupId order by UniqueDate desc)

その他: top with ties共通テーブル式

with cte as (
  select top 1 with ties 
      UniqueID
    , GroupID
    , GroupDate
    , UniqueDate
  from #Example
  order by row_number() over (partition by GroupId order by UniqueDate desc)
)
select *
from cte
where UniqueDate > GroupDate

dbfiddle: http://dbfiddle.uk/?rdbms=sqlserver_2016&fiddle=c058994c2f5f3d99b212f06e1dae9fd

元のクエリ

Table 'Worktable'. Scan count 3, logical reads 28001, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#Example____________________________________________________________________________________________________________0000000000CB'. Scan count 1, logical reads 43, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 31 ms,  elapsed time = 31 ms.

vs top with ties共通テーブル式

Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#Example____________________________________________________________________________________________________________0000000000CB'. Scan count 1, logical reads 43, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 16 ms,  elapsed time = 15 ms.
4
SqlZim

そのため、これまでに投稿されたさまざまなアプローチについて分析しましたが、私の環境では、ダニエルのアプローチが実行時間で一貫して勝っているようです。驚いたことに(私にとって)sp_BlitzErikの3番目のCROSS APPLYアプローチはそれほど遅れていませんでした。興味がある場合の出力を次に示しますが、すべての代替アプローチについてTONに感謝します。この質問の答えを掘り下げることで、私はかなり前より多くのことを学びました!

Windowed Function - baseline metric

(10406 row(s) affected)
Table 'DateDim'. Scan count 9, logical reads 791, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 9, logical reads 140181, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 89815, logical reads 42553550, physical reads 0, read-ahead reads 84586, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 9, logical reads 7688, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 9, logical reads 7819, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 87753 ms,  elapsed time = 13031 ms.
Warning: Null value is eliminated by an aggregate or other SET operation.


Basic Aggregated Subquery - Daniel Hutmacher

(10406 row(s) affected)
Table 'DateDim'. Scan count 18, logical reads 1194, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 18, logical reads 280362, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 48, logical reads 82408, physical reads 9629, read-ahead reads 72779, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 89791, logical reads 6861425, physical reads 0, read-ahead reads 14565, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 9, logical reads 7688, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 18, logical reads 15726, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 40527 ms,  elapsed time = 6182 ms.
Warning: Null value is eliminated by an aggregate or other SET operation.


CROSS APPLY Operation A - sp_BlitzErik

(10406 row(s) affected)
Table 'DateDim'. Scan count 9, logical reads 6199331, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 3099273, logical reads 12844012, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 3109676, logical reads 9350502, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 3109676, logical reads 9482456, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 132632 ms,  elapsed time = 20955 ms.


CROSS APPLY Operation C - sp_BlitzErik

(10406 row(s) affected)
Table 'DateDim'. Scan count 18, logical reads 1194, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 18, logical reads 280362, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 56, logical reads 92800, physical reads 10872, read-ahead reads 81928, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 89791, logical reads 6861425, physical reads 0, read-ahead reads 14563, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 18, logical reads 15376, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 18, logical reads 15726, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 46082 ms,  elapsed time = 6804 ms.
Warning: Null value is eliminated by an aggregate or other SET operation.


TOP 1 WITH TIES - B - SqlZim

(10406 row(s) affected)
Table 'DateDim'. Scan count 9, logical reads 791, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 9, logical reads 140181, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 89791, logical reads 6866304, physical reads 0, read-ahead reads 93468, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 9, logical reads 7688, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 9, logical reads 7835, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 79406 ms,  elapsed time = 15852 ms.
4
John Eisbrener