CTE、子查询、临时表或表变量之间是不是存在性能差异?

Posted

技术标签:

【中文标题】CTE、子查询、临时表或表变量之间是不是存在性能差异?【英文标题】:Is there a performance difference between CTE , Sub-Query, Temporary Table or Table Variable?CTE、子查询、临时表或表变量之间是否存在性能差异? 【发布时间】:2012-06-25 12:58:33 【问题描述】:

在这个出色的SO question 中,讨论了CTEsub-queries 之间的区别。

我想特别问一下:

以下哪种情况下更高效/更快?

CTE 子查询 临时表 表变量

传统上,我在开发 stored procedures 时使用了很多 temp tables - 因为它们似乎比许多相互交织的子查询更具可读性。

Non-recursive CTEs 很好地封装了一组数据,并且可读性很好,但是在某些特定情况下可以说它们总是会表现得更好吗?还是必须总是摆弄不同的选项才能找到最有效的解决方案?


编辑

最近有人告诉我,就效率而言,临时表是一个不错的首选,因为它们具有关联的直方图,即统计信息。

【问题讨论】:

一般性回答:视情况而定。 这取决于许多因素,任何一般性陈述都可能是错误的 - 在某些情况下。基本上:您需要测试和测量 - 看看哪个最适合您! @marc_s - 好的;也许这个问题应该因为主观而被关闭?请注意,很多关于 SO 的 SQL 问题都可能被认为是主观的。 它可能会因为过于宽泛而被关闭 - 我同意你的观点 - SQL 中的许多事情和主题确实会得到取决于的答案。有时可以列出两个或三个标准来做出决定,但是对于您的问题,几乎不可能给出合理的建议-这取决于很多-您的表结构,这些表中的数据,您正在使用的查询,您的索引策略等等...... @marc_s 尝试并保留会很好 - 关于可能对 OP 进行编辑以使其更具体和更窄的任何建议? 请注意这个问题是特定于 SQL Server 的。对于 postgres 等其他数据库,CTE 通常比等效子查询慢得多(请参阅http://blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences/) 【参考方案1】:

SQL 是一种声明性语言,而不是过程性语言。也就是说,您构造一个 SQL 语句来描述您想要的结果。您没有告诉 SQL 引擎如何完成这项工作。

作为一般规则,让 SQL 引擎和 SQL 优化器找到最佳查询计划是个好主意。开发 SQL 引擎需要很多人-年的努力,所以让工程师做他们知道该怎么做的事情。

当然,也有查询计划不是最优的情况。然后你想使用查询提示、重组查询、更新统计信息、使用临时表、添加索引等等以获得更好的性能。

至于你的问题。 CTE 和子查询的性能理论上应该是相同的,因为它们都向查询优化器提供相同的信息。一个不同之处在于,使用不止一次的 CTE 可以很容易地识别和计算一次。然后可以多次存储和读取结果。不幸的是,SQL Server 似乎没有利用这种基本的优化方法(您可以将这种常见的子查询消除称为消除)。

临时表是另一回事,因为您提供了有关如何运行查询的更多指导。一个主要区别是优化器可以使用临时表中的统计信息来建立其查询计划。这可以带来性能提升。此外,如果您有一个多次使用的复杂 CTE(子查询),则将其存储在临时表中通常会提高性能。查询只执行一次。

您的问题的答案是,您需要尝试获得预期的性能,特别是对于定期运行的复杂查询。在理想情况下,查询优化器会找到完美的执行路径。虽然它经常这样做,但您或许能够找到一种方法来获得更好的性能。

【讨论】:

微软关于该领域未来可能改进的一些研究发表在“Efficient Exploitation of Similar Subexpressions for Query Processing”Available from here 鉴于该论文是在 2007 年提出的,是否知道他们是否已将其合并到 SQL Server 2012 中? 一个很好的答案!只是要强调一下:SQL 是一种声明性语言,我们不控制如何提取数据。因此,性能/速度因查询而异。 @RGS 。 . .临时表上的索引肯定会改进可以利用这些索引的查询——就像永久表上的索引一样。但是,如果将子查询具体化为临时表,则可能会失去原始表上的索引的优势。 @RGS 。 . .当数据库引擎在执行复杂查询的过程中具体化子查询/CTE 时,它不会在具体化上添加索引。您可以使用临时表手动执行此操作。【参考方案2】:

没有规则。我发现 CTE 更具可读性,并使用它们除非它们表现出一些性能问题,在这种情况下,我会调查实际问题而不是猜测 CTE 是问题并尝试使用不同的方法重写它方法。这个问题通常比我选择以声明方式通过查询声明我的意图的方式更多。

在某些情况下,您可以解开 CTE 或删除子查询并将其替换为 #temp 表并缩短持续时间。这可能是由于各种原因造成的,例如过时的统计信息、甚至无法获得准确的统计信息(例如加入表值函数)、并行性,甚至由于查询的复杂性而无法生成最佳计划(在这种情况下,分解它可能会给优化器一个战斗的机会)。但在某些情况下,创建 #temp 表所涉及的 I/O 可能会超过其他性能方面,这可能会降低使用 CTE 的特定计划形状的吸引力。

老实说,有太多变量无法为您的问题提供“正确”答案。没有可预测的方法可以知道查询何时可能倾向于一种或另一种方法 - 只需知道理论上,CTE 或单个子查询的相同语义应该执行完全相同。我认为,如果您提出一些不正确的情况,您的问题会更有价值-可能是您发现了优化器中的限制(或发现了已知限制),或者您的查询在语义上不等效或者包含阻碍优化的元素。

因此,我建议以对您来说最自然的方式编写查询,并且只有在您发现优化器存在的实际性能问题时才会偏离。我个人对它们进行 CTE 排名,然后是子查询,#temp 表是最后的手段。

【讨论】:

+1 原来是一个相当主观的问题;我希望它不会因为太模糊而被关闭,因为到目前为止的答案是有用的。我意识到 :-) 当问题发生变化时你不喜欢它,但你对缩小 OP 中的问题有什么建议吗? 我认为这个问题很好,你会注意到还没有一个投票结束,但如果答案开始疯狂地四处乱跳,它可能会被关闭。正如我在回答中建议的那样,如果您有一个 特殊 案例,您发现 CTE 和子查询之间存在很大差异,请使用实际查询和执行计划开始一个新问题(它可能是更适合dba.se)。只需意识到对于 that 查询的帮助答案可能与具有相同场景的不同查询的答案不同。 在您的问题下方有链接link / edit / close / flag - 如果有任何投票结束问题,您将看到close (n) 其中n 代表投票结束的用户数量你的问题。如果您单击该链接,您将看到这些用户选择的原因。 @whytheq 另见this recent blog post by Bob Beauchemin。它没有专门处理 CTE 与子查询,但同样的概念适用:如果您出于性能原因选择不直观的模式,请记录其中的废话并重新访问以确保您发现的怪癖仍然是真实的。我什至可能建议将更自然的查询版本注释掉,除非您有一个可靠的源代码控制系统来保存以前的版本。 上面的固定链接:sqlskills.com/blogs/bobb/…【参考方案3】:

#temp 是物质化的,而 CTE 不是。

CTE 只是语法,所以理论上它只是一个子查询。它被执行。 #temp 已实现。因此,执行多次的连接中昂贵的 CTE 在#temp 中可能会更好。另一方面,如果它是一个简单的评估,但没有执行几次,那么不值得#temp 的开销。

SO 上有一些人不喜欢表变量,但我喜欢它们,因为它们比#temp 实现且创建速度更快。与表变量相比,查询优化器有时使用#temp 做得更好。

在 #temp 或表变量上创建 PK 的能力为查询优化器提供了比 CTE 更多的信息(因为您不能在 CTE 上声明 PK)。

【讨论】:

什么是首字母缩略词“TVP”...类似于 #temp 的东西? TVP 正在成为一个常用术语,因为它听起来令人印象深刻(对某些人来说)。简而言之,TVP 是作为参数传递的表。任何使用过表变量的人都可以轻松使用它们。 警告 - TVP 没有执行计划!不要将 TVP 用于任何其他最简单的短查找列表。如果您对它们进行任何复杂的连接、插入或更新,您可能会遇到大量优化问题。相信我,我已经被这个烧死了。【参考方案4】:

我认为使用#Temp Table 而不是CTE 总是更可取的两件事是:

    您不能在 CTE 上放置主键,因此 CTE 访问的数据必须遍历 CTE 表中的每个索引,而不是仅访问临时表上的 PK 或索引。

    因为您不能向 CTE 添加约束、索引和主键,所以它们更容易出现错误和错误数据。


-昨天的一天

这是一个示例,其中#table 约束可以防止坏数据,而 CTE 的情况并非如此

DECLARE @BadData TABLE ( 
                       ThisID int
                     , ThatID int );
INSERT INTO @BadData
       ( ThisID
       , ThatID
       ) 
VALUES
       ( 1, 1 ),
       ( 1, 2 ),
       ( 2, 2 ),
       ( 1, 1 );

IF OBJECT_ID('tempdb..#This') IS NOT NULL
    DROP TABLE #This;
CREATE TABLE #This ( 
             ThisID int NOT NULL
           , ThatID int NOT NULL
                        UNIQUE(ThisID, ThatID) );
INSERT INTO #This
SELECT * FROM @BadData;
WITH This_CTE
     AS (SELECT *
           FROM @BadData)
     SELECT *
       FROM This_CTE;

【讨论】:

ALWAYS 有点过分,但感谢您的回答。就可读性而言,使用 CTE 可能是一件好事。 我完全不明白你的第二点。在我看来,定义 CTE 的查询类似于您对临时表施加的约束,请注意前者可以包含任意复杂的谓词,而后者则受到更多限制(例如,CHECK 约束指的是多行/tables 是不允许的)。您能否发布一个示例,其中 CTE 显示了临时表等效项没有的错误? 我知道这是一篇旧帖子;但只是想指出您的示例不是将 CTE 与 #temp 表进行比较;您正在将 @table 变量与 #temp 表进行比较。

以上是关于CTE、子查询、临时表或表变量之间是不是存在性能差异?的主要内容,如果未能解决你的问题,请参考以下文章

mysql性能优化2

sql语句优化

sql优化总结

sql语句优化

SQL语句调优

为啥 CTE 比游标/派生表/子查询/临时表等更好?