T-SQL与CTE的性能不佳

Joh*_*ohn 17 t-sql sql-server performance common-table-expression

我有一个关于SQL Server中的公用表表达式的性能问题.在我们的开发团队中,我们在构建查询时使用了很多链式CTE.我目前正在处理一个性能很糟糕的查询.但我发现,如果我在链的中间将所有记录插入临时表中的CTE,然后继续,但从该临时表中选择,我显着提高了性能.现在,我希望得到一些帮助,以了解这种类型的更改是否仅适用于此特定查询以及为什么您将在下面看到的两种情况在性能上有很大差异.或者我们是否可能过度使用我们团队中的CTE,我们是否可以通过学习这个案例来获得性能?

请尝试向我解释这里发生的事情......

代码已经完成,您可以在SQL Server 2008上运行它,也可能在2005年运行它.一部分被注释掉了,我的想法是你可以通过注释掉一个或另一个来切换这两个案例.你可以看到你的块评论的位置,我用--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;
Run Code Online (Sandbox Code Playgroud)

如果你想尝试理解我在最后一步中做了什么,我在这里有一个关于它的问题.

对我来说,情况1需要大约34秒,情况2需要大约2秒.区别在于我将S中的结果存储在案例2的临时表中,如果是1,我直接在下一个CTE中使用S.

JNK*_*JNK 12

A CTE基本上只是一次性视图.除了将CTE代码FROM作为表表达式放入子句之外,它几乎不会更快地进行查询.

在您的示例中,真正的问题是我相信的日期函数.

您的第一个(慢)情况需要为每一行运行日期函数.

对于您的第二个(更快)情况,它们运行一次并存储到表中.

除非您在函数派生字段上执行某种逻辑,否则通常不会这么明显.在你的情况,你正在做一个ORDER BYHour,这是非常昂贵的.在你的第二个例子中,它是对字段的简单排序,但是在第一个例子中,你为每一行运行该函数,然后排序.

有关CTE的更深入阅读,请参阅DBA.SE上的这个问题.


pap*_*zzo 6

CTE只是语法快捷方式.CTE在连接中运行(并重新运行).使用#temp进行一次评估,然后在连接中重复使用结果.

该文件具有误导性.

MSDN_CTE

公共表表达式(CTE)可以被认为是临时结果集.

本文更好地解释了它

PapaCTEarticle

CTE非常适合这种类型的场景,因为它使T-SQL更具可读性(如视图),但它可以在同一批次中紧跟的查询中多次使用.当然,除了该范围之外,它不可用.此外,CTE是一种语言级构造 - 这意味着SQL Server不会在内部创建临时或虚拟表.每次在紧接的后续查询中引用CTE时,都会调用CTE的基础查询.

看一下表值参数

TVP

它们的结构类似于#temp但没有那么多的开销.它们是只读的,但看起来你只需要只读.创建和删除#temp会有所不同,但在低到中等服务器上它的命中时间为0.1秒,而TVP基本上没有命中.

  • [这是比较临时表和tvps的绝佳参考.](http://dba.stackexchange.com/questions/16385/whats-the-difference-between-a-temp-table-and-table-variable-in- SQL服务器) (2认同)