【问题标题】:T-SQL bad performance with CTECTE 的 T-SQL 性能不佳
【发布时间】:2012-05-31 21:16:10
【问题描述】:

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

请尝试向我解释这里到底发生了什么......

代码已完成,您将能够在 SQL Server 2008 和可能的 2005 上运行它。一部分被注释掉了,我的想法是你可以通过注释掉一个或另一个来切换这两种情况。你可以看看你的block cmets放在哪里,我已经用--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 问题here

对我来说,案例 1 大约需要 34 秒,案例 2 大约需要 2 秒。不同之处在于我将 S 的结果存储在案例 2 中的临时表中,在案例 1 中我直接在下一个 CTE 中使用 S。

【问题讨论】:

  • +1 用于可运行代码。我建议同时运行它们并将执行计划 XML 粘贴到 SQL Sentry Plan Explorer 中。差异的原因将非常明显。例如,它最终在计划的一部分扫描#Preparation 20,000 次,在另一部分扫描 10,000 次。
  • 谢谢。我安装了 SQL Sentry Plan Explorer。我仍在学习 SQL Server,我无法阅读执行计划,也无法阅读哨兵计划。但答案是,如果不调查执行计划,就不能说在某些情况下 CTE 或临时表是否最好?你在哪里看到 20,000 和 10,000 次?我找不到它。有没有可能通过参考代码来说明为什么一个零件要扫描#Preparation 20,000 次?
  • 你见过这个吗? “SQL 2005 CTE 与 TEMP 表在用于连接其他表时的性能”stackoverflow.com/questions/1531835/…

标签: sql-server performance tsql common-table-expression


【解决方案1】:

CTE 本质上只是一个一次性视图。它几乎永远不会比将CTE 代码作为表表达式放入FROM 子句中更快地进行查询。

在您的示例中,我认为真正的问题是日期函数。

您的第一个(慢速)案例需要为每一行运行日期函数。

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

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

有关 CTE 的更深入阅读,请参阅 this question on DBA.SE

【讨论】:

    【解决方案2】:

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

    文档具有误导性。

    MSDN_CTE

    可以将公用表表达式 (CTE) 视为临时结果集。

    这篇文章解释得更好

    PapaCTEarticle

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

    看看表值参数

    TVP

    它们的结构类似于#temp,但没有那么多开销。它们是只读的,但您似乎只需要只读。创建和删除 #temp 会有所不同,但在中低端服务器上是 0.1 秒命中,而使用 TVP 基本上不会命中。

    【讨论】:

    • TVP 不是一个很好的建议。它们可能存在很多问题,尤其是它们存在于事务范围之外,因此无法回滚!
    • @JNK 帮帮我。 TVP 是只读的,所以我为什么要关心它是否不能回滚。此示例仅选择不插入、更新或删除。风险在哪里?你不止一次地帮助了我,而且一直都是正确的。
    • 除了输出或测试之外,我对 TVP 普遍不信任。它们仍然写入日志文件并具有磁盘 IO 开销,但实际上不是原子的,无法编制索引,没有统计信息,并且 exec 计划总是为它们假设一行。
    【解决方案3】:

    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,它会在瞬间运行。

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

    【讨论】:

      猜你喜欢
      • 2011-03-21
      • 2021-02-04
      • 1970-01-01
      • 1970-01-01
      • 2023-04-09
      • 1970-01-01
      • 2017-06-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多