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


222

在这个出色的SO问题中,讨论了CTE和之间的sub-queries差异。

我想特别问一下:

在以下哪种情况下,以下各项中的每一种都更有效/更快?

  • CTE
  • 子查询
  • 临时表
  • 表变量

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

Non-recursive CTE可以很好地封装数据集,并且可读性很强,但是在特定情况下有人可以说它们将始终表现更好吗?还是总是必须摆弄各种选项以找到最有效的解决方案?


编辑

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


4
一般回答:这取决于。而且这取决于许多因素,在某些情况下,任何一般性陈述都可能是错误的。基本上:您需要测试和测量-看看哪个最适合您!
marc_s 2012年

@marc_s-好的;也许应该以主观的观点来关闭这个问题?请注意,关于SO的许多SQL问题都可以判断为主观的。
whytheq 2012年

1
由于范围太广,它可能会关闭-我同意您的观点 -SQL中的许多事情和主题确实会得到答案,具体取决于它。有时,您可以列出两三个条件来做出决定,但是在这里提出您的问题时,几乎不可能给出合理的建议-这在很大程度上取决于-您的表结构,这些表中的数据,您正在使用的查询,您的索引策略,以及更多...
marc_s

@marc_s最好尝试保留-关于OP可能进行的编辑的任何建议,以尝试使其更加具体和狭窄?
whytheq 2012年

请注意,此问题特定于SQL Server。对于其他的DB像postgres的,一个CTE通常比等效的子查询慢得多(见http://blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences/

Answers:


243

SQL是一种声明性语言,而不是过程性语言。即,您构造一个SQL语句来描述所需的结果。您没有在告诉SQL引擎如何进行工作。

通常,让SQL引擎和SQL优化器找到最佳查询计划是一个好主意。开发SQL引擎需要花费很多人年的精力,所以让工程师去做他们知道如何做的事情。

当然,在某些情况下查询计划不是最佳的。然后,您想使用查询提示,重组查询,更新统计信息,使用临时表,添加索引等等,以获得更好的性能。

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

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

您问题的答案是,您需要努力获得期望的性能,尤其是对于定期运行的复杂查询。在理想情况下,查询优化器将找到理想的执行路径。尽管它经常发生,但您可能能够找到一种获得更好性能的方法。


11
有关此领域将来可能进行的改进的一些Microsoft研究在出版物“对查询处理的类似子表达式的有效利用”中,可从此处获得
Martin Smith

3
鉴于该论文是在2007年提出的,是否知道他们是否已将其合并到SQL Server 2012中?
Gordon Linoff 2012年

3
一个很好的答案!仅强调一下:SQL是一种声明性语言,我们无法控制如何提取数据。因此,性能/速度因查询而异。
Simcha Khabinsky 2014年

2
@RGS。。。临时表上的索引肯定会改善可以利用这些索引的查询-就像永久表上的索引一样。但是,如果将子查询实现为临时表,则可能会失去原始表上索引的优势。
Gordon Linoff 2016年

2
@RGS。。当数据库引擎在执行复杂查询的过程中实现子查询/ CTE时,它不会在实现上添加索引。您可以使用临时表手动执行此操作。
Gordon Linoff '16

77

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

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

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

因此,我建议您以最自然的方式编写查询,并且仅在发现优化器存在实际性能问题时才偏离。我个人将它们排名为CTE,然后是子查询,并且#temp表是不得已的方法。


4
+1成为一个相当主观的问题;我希望它不会因过于模糊而封闭,因为到目前为止的答案是有益的。我意识到:-)当问题改变时,您不喜欢它,但是您对在OP中缩小问题有任何建议吗?
whytheq 2012年

2
我认为这个问题很好,您会注意到还没有一个投票要关闭,但是,如果答案开始四处徘徊,它可能会被关闭。正如我在回答中所建议的那样,如果您遇到的特殊情况是CTE和子查询之间存在很大差异,请使用实际的查询和执行计划开始一个新问题(这可能更适合dba.se) 。只需意识到,针对查询的帮助答案可能与针对具有相同场景的不同查询的答案不同。
亚伦·伯特兰

在您的问题下方有链接link / edit / close / flag-如果有任何投票关闭该问题,您将看到close (n)在哪里n代表已投票关闭该问题的用户数量。如果单击链接,将看到这些用户选择的原因。
亚伦·伯特兰

@whytheq也可以看到Bob Beauchemin最近的博客文章。它不会专门处理CTE与子查询,但是会应用相同的概念:如果出于性能原因选择了不直观的模式,请记录下来的废话,然后重新访问以确保发现的怪癖仍然存在。我什至建议您删除更自然的查询版本,除非您拥有一个可靠的源代码管理系统来保存先前的版本。
亚伦·伯特兰

1
上面的固定链接:sqlskills.com/blogs/bobb/…–
ADJenks,

19

#temp已完成,CTE未完成。

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

SO上有些人不喜欢表变量,但是我喜欢他们,因为它们比#temp更物化并且创建速度更快。有时查询优化器使用#temp的性能要优于表变量。

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


首字母缩写词“ TVP”是什么……类似于#temp?
whytheq 2012年

TVP正成为一个通用术语,因为它(对某些人)听起来令人印象深刻。简而言之,TVP是作为参数传递的表。使用过Table变量的任何人都将与他们在一起。
WonderWorker

1
警告-TVP没有执行计划!除了最简单的简短查找列表之外,请勿将TVP用作其他任何内容。如果对它们进行任何复杂的联接,插入或更新,则可能会遇到大量的优化问题。相信我,我已经为此感到沮丧。
Heliac

12

我认为让#临时表而不是CTE总是更可取的只有两件事:

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

  2. 因为您不能向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;

3
ALWAYS有点太远了,但感谢您的回答。就可读性而言,使用CTE可能是一件好事。
whytheq

3
我完全不明白你的第二点。我的看法是,定义CTE的查询类似于您要放在temp表上的约束,请注意,前者可以包含任意复杂的谓词,而后者则要受限制得多(例如,CHECK引用多行/表的约束是不允许)。您能发表一个示例,其中CTE表现出与临时表等效的错误吗?
一天,2016年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.