English 中文(简体)
T-SQL 与 CTE 的不良表现
原标题:T-SQL bad performance with CTE

I have a performance question about Common table expressions in SQL Server. In our developer team we use a lot of chaining CTEs when building our queries. I am currently working on a query which had terrible performance. But I found out that if I in the middle of the chain inserted all the records up to that CTE in a temporary table instead and then continued but selecting from that temp table I improved performance significantly. Now I would like to get some help to understand if this type of change only applies to this specific query and why the two cases you will see below differ so much in performance. Or could we possibly overuse CTEs in our team and can we gain performance generally by learning from this case?

请解释一下这里到底发生了什么...

代码是完整的, 您可以在 SQL SQL 服务器上运行, 2008 年, 也可能是 2005 年 。 有一部分被评出, 我的想法是您可以通过评出一个或另一个来切换两个案例。 您可以看到可以将您的区块批注放在哪里, 我用 < code> - 区块批注在这里标记了这些地方 和 < code> - end 区块批注在这里

慢速的运行情况是未提醒的默认 。 您在这里 :

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

如果您想试着理解我在最后一步中在做什么, 我有一个问题要问:

不同的是,我把S的结果放在一个临时桌子里, 放在第2个临时桌子里, 以防万一我在下一个CTE中直接使用S。

问题回答

CTE 只是一个语法快捷键。 CTE 是在连接中运行( 并重新运行) 。 使用 # temp, 它会被评估一次, 然后结果会在连接中重新使用 。

这些文件令人产生误解。

< a href=" "http://msdn.microsoft.com/en-US/library/ms1907666%28v=sql. 105%29.aspx" rel="nofollow" >MSDN_CTE

共同表格表达式(CTE)可视为临时结果集。

本条解释得更好

"http://msdn.microsoft.com/en-us/magazine/cc163346.aspx" rel="无跟踪" >PapaCTEarticle

CTE是适合这种类型的情景的,因为它使 T-SQL 更易读(类似视图),但可以在同一批中紧随其后的询问中不止一次使用。当然,除了这一范围之外,它无法使用。此外, CTE是一个语言层次的构造,意味着 SQL 服务器不会在内部创建临时或虚拟表格。每次在紧接的查询中引用,CTE 基本查询都会被调用。

查看表值参数

"http://msdn.microsoft.com/en-us/library/bb510489.aspx" rel="nofollow" >TVP

它们的结构像# temp, 但没有那么大。 它们只是读取而已, 但您似乎只需要读取。 创建和丢弃 # temp 会有所不同, 但在低端到中端的服务器上, 它是0. 1 秒的点击, 而 TVP 基本上没有被击中 。

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 版本后, 查询优化器会丢失, 并产生非常复杂的执行计划 。 查询会永远运行。 Wthout 将它分秒运行 。

所以,cte可以是一个巨大的性能问题!





相关问题
Export tables from SQL Server to be imported to Oracle 10g

I m trying to export some tables from SQL Server 2005 and then create those tables and populate them in Oracle. I have about 10 tables, varying from 4 columns up to 25. I m not using any constraints/...

SQL server: Can NT accounts be mapped to SQL server accounts

In our database we have an SQL server account that has the correct roles to access some of the databases. We are now switching to windows authentication and I was wondering if we can create a NT user ...

SQL Server 2000, ADO 2.8, VB6

How to determine if a Transaction is active i.e. before issuing Begin Transaction I want to ensure that no previous transaction are open.. the platform is VB6, MS-SQL Server 2000 and ADO 2.8

热门标签