web-dev-qa-db-ja.com

CTEでのT-SQLのパフォーマンスの低下

SQL Serverの共通テーブル式に関するパフォーマンスの質問があります。開発者チームでは、クエリを構築する際に多くの連鎖CTEを使用します。私は現在、ひどいパフォーマンスがあったクエリに取り組んでいます。しかし、チェーンの途中で、代わりにそのCTEまでのすべてのレコードを一時テーブルに挿入して続行した場合、その一時テーブルから選択すると、パフォーマンスが大幅に向上することがわかりました。ここで、この種の変更がこの特定のクエリにのみ適用されるかどうか、および以下に示す2つのケースのパフォーマンスが大きく異なる理由を理解するための助けを求めたいと思います。または、チームでCTEを使いすぎて、このケースから学ぶことで一般的にパフォーマンスを上げることができるでしょうか?

ここで何が起こっているのか正確に説明してください...

コードは完成しており、SQL Server 2008とおそらく2005でも実行できます。一部はコメント化されており、私の考えでは、どちらか一方をコメント化することで2つのケースを切り替えることができます。あなたはあなたのブロックコメントを置く場所を見ることができます、私はこれらの場所に--block comment hereおよび--end block comment here

コメント化されていないデフォルトはパフォーマンスの遅いケースです。はい、どうぞ:

--Declare tables to use in example.
CREATE TABLE #Preparation 
(
    Date DATETIME NOT NULL
    ,Hour INT NOT NULL
    ,Sales NUMERIC(9,2)
    ,Items INT
);

CREATE TABLE #Calendar
(
    Date DATETIME NOT NULL
)

CREATE TABLE #OpenHours
(
    Day INT NOT NULL,
    OpenFrom TIME NOT NULL,
    OpenTo TIME NOT NULL
);

--Fill tables with sample data.
INSERT INTO #OpenHours (Day, OpenFrom, OpenTo)
VALUES
    (1, '10:00', '20:00'),
    (2, '10:00', '20:00'),
    (3, '10:00', '20:00'),
    (4, '10:00', '20:00'),
    (5, '10:00', '20:00'),
    (6, '10:00', '20:00'),
    (7, '10:00', '20:00')

DECLARE @CounterDay INT = 0, @CounterHour INT = 0, @Sales NUMERIC(9, 2), @Items INT;

WHILE @CounterDay < 365
BEGIN
    SET @CounterHour = 0;
    WHILE @CounterHour < 5
    BEGIN
        SET @Items = CAST(Rand() * 100 AS INT);
        SET @Sales = CAST(Rand() * 1000 AS NUMERIC(9, 2));
        IF @Items % 2 = 0
        BEGIN
            SET @Items = NULL;
            SET @Sales = NULL;
        END

        INSERT INTO #Preparation (Date, Hour, Items, Sales)
        VALUES (DATEADD(DAY, @CounterDay, '2011-01-01'), @CounterHour + 13, @Items, @Sales);

        SET @CounterHour += 1;
    END
    INSERT INTO #Calendar (Date) VALUES (DATEADD(DAY, @CounterDay, '2011-01-01'));
    SET @CounterDay += 1;
END

--Here the query starts.
;WITH P AS (
    SELECT DATEADD(HOUR, Hour, Date) AS Hour
        ,Sales
        ,Items
    FROM #Preparation
),
O AS (
        SELECT DISTINCT DATEADD(HOUR, SV.number, C.Date) AS Hour
        FROM #OpenHours AS O
            JOIN #Calendar AS C ON O.Day = DATEPART(WEEKDAY, C.Date)
            JOIN master.dbo.spt_values AS SV ON SV.number BETWEEN DATEPART(HOUR, O.OpenFrom) AND DATEPART(HOUR, O.OpenTo)
),
S AS (
    SELECT O.Hour, P.Sales, P.Items
    FROM O
        LEFT JOIN P ON P.Hour = O.Hour
)

--block comment here case 1 (slow performing)
--With this technique it takes about 34 seconds.
,N AS (
        SELECT  
            A.Hour
            ,A.Sales AS SalesOrg
            ,CASE WHEN COALESCE(B.Sales, C.Sales, 1) < 0
                THEN 0 ELSE COALESCE(B.Sales, C.Sales, 1) END AS Sales
            ,A.Items AS ItemsOrg
            ,COALESCE(B.Items, C.Items, 1) AS Items
        FROM S AS A
        OUTER APPLY (SELECT TOP 1 *
                     FROM S
                     WHERE Hour <= A.Hour
                        AND Sales IS NOT NULL
                        AND DATEDIFF(DAY, Hour, A.Hour) = 0                      
                     ORDER BY Hour DESC) B
        OUTER APPLY (SELECT TOP 1 *
                     FROM S
                     WHERE Sales IS NOT NULL
                        AND DATEDIFF(DAY, Hour, A.Hour) = 0
                     ORDER BY Hour) C
    )
--end block comment here case 1 (slow performing)

/*--block comment here case 2 (fast performing)
--With this technique it takes about 2 seconds.
SELECT * INTO #tmpS FROM S;

WITH
N AS (
        SELECT  
            A.Hour
            ,A.Sales AS SalesOrg
            ,CASE WHEN COALESCE(B.Sales, C.Sales, 1) < 0
                THEN 0 ELSE COALESCE(B.Sales, C.Sales, 1) END AS Sales
            ,A.Items AS ItemsOrg
            ,COALESCE(B.Items, C.Items, 1) AS Items
        FROM #tmpS AS A
        OUTER APPLY (SELECT TOP 1 *
                     FROM #tmpS
                     WHERE Hour <= A.Hour
                        AND Sales IS NOT NULL
                        AND DATEDIFF(DAY, Hour, A.Hour) = 0                      
                     ORDER BY Hour DESC) B
        OUTER APPLY (SELECT TOP 1 *
                     FROM #tmpS
                     WHERE Sales IS NOT NULL
                        AND DATEDIFF(DAY, Hour, A.Hour) = 0
                     ORDER BY Hour) C
    )
--end block comment here case 2 (fast performing)*/
SELECT * FROM N ORDER BY Hour


IF OBJECT_ID('tempdb..#tmpS') IS NOT NULL DROP TABLE #tmpS;

DROP TABLE #Preparation;
DROP TABLE #Calendar;
DROP TABLE #OpenHours;

最後のステップで私がやっていることを試して理解したいのであれば、SOそれについて質問 ここ があります。

私の場合、ケース1には約34秒、ケース2には約2秒かかります。違いは、Sの結果をケース2の一時テーブルに格納することです。ケース1では、次のCTEでSを直接使用します。

17
John

CTEは、本質的には使い捨てのビューです。 CTEコードをFROM句にテーブル式として挿入するよりも、クエリが速くなることはほとんどありません。

あなたの例では、本当の問題は私が信じている日付関数です。

最初の(遅い)ケースでは、すべての行に対して日付関数を実行する必要があります。

2番目の(より速い)場合は、一度実行されてテーブルに格納されます。

これは、関数から派生したフィールドでなんらかのロジックを実行しない限り、通常それほど目立ちません。あなたの場合、あなたはORDER BY on Hour、これは非常にコストがかかります。 2番目の例では、フィールドでの単純なソートですが、最初の例では、行ごとにその関数を実行してから、ソートします。

CTEの詳細については、 DBA.SEに関するこの質問 を参照してください。

13
JNK

CTEは単なる構文のショートカットです。そのCTEは、結合で実行(および再実行)されます。 #tempを使用すると、1回評価され、その結果が結合で再利用されます。

ドキュメントは誤解を招くものです。

MSDN_CTE

共通テーブル式(CTE)は、一時的な結果セットと考えることができます。

この記事はそれをよりよく説明しています

PapaCTEarticle

CTEは、T-SQLをはるかに読みやすく(ビューのように)するため、このタイプのシナリオに適していますが、同じバッチの直後に続くクエリで複数回使用できます。もちろん、その範囲を超えて利用することはできません。さらに、CTEは言語レベルの構成要素です。つまり、SQL Serverは内部的に一時テーブルまたは仮想テーブルを作成しません。 CTEの基になるクエリは、直後のクエリで参照されるたびに呼び出されます。

テーブル値パラメーターを見てください

[〜#〜] tvp [〜#〜]

#tempのような構造ですが、オーバーヘッドはそれほどではありません。それらは読み取り専用ですが、読み取り専用で十分です。 #tempの作成と削除はさまざまですが、低から中のサーバーでは0.1秒のヒットであり、TVPでは本質的にヒットしません。

6
paparazzo

CTEは非常に優れた構文糖であり、クエリをはるかに読みやすくしています。ただし、大規模なデータセットの場合、私の経験ではパフォーマンスは壊滅的であり、必要に応じてすべてを一時テーブルに特定のインデックスで置き換える必要がありました。

例えば:

SELECT IdBL, LgnBL, chemin, IdBE, IdLot, SUM(CuTrait) AS CuTraitBE
INTO #temp_arbo_of_8_cte
FROM #CoutTraitParBE
GROUP BY IdBL, LgnBL, chemin, IdBE, IdLot;

CREATE NONCLUSTERED INDEX #temp_arbo_of_8_cte_index_1 ON #temp_arbo_of_8_cte(chemin, IdBE, IdLot);

SELECT a.*, CuTraitBE, ROUND(CuTraitBE * QteSortieBE, 3) AS CoutTraitParBE, QteFactParBE*PxVte AS CaParBE
INTO #temp_arbo_of_8
FROM #temp_arbo_of_7 a
LEFT JOIN #temp_arbo_of_8_cte b ON a.chemin=b.chemin AND a.IdBE=b.IdBE AND a.IdLot=b.IdLot;

/*
WITH cte AS (
    SELECT IdBL, LgnBL, chemin, IdBE, IdLot, SUM(CuTrait) AS CuTraitBE 
    FROM #CoutTraitParBE
    GROUP BY IdBL, LgnBL, chemin, IdBE, IdLot
)
SELECT a.*, CuTraitBE, ROUND(CuTraitBE * QteSortieBE, 3) AS CoutTraitParBE, QteFactParBE*PxVte AS CaParBE
INTO #temp_arbo_of_8
FROM #temp_arbo_of_7 a
LEFT JOIN cte b ON a.chemin=b.chemin AND a.IdBE=b.IdBE AND a.IdLot=b.IdLot;
*/

Cteバージョンを使用すると、クエリオプティマイザーが失われ、非常に複雑な実行プランが生成されます。クエリは永久に実行されます。それは一瞬で実行されます。

だからはい、cteは大きなパフォーマンスの問題になる可能性があります!

0
Ludovic Aubert