子查询与MAX聚合函数的性能

时间:2016-08-10 13:05:57

标签: sql performance subquery aggregate-functions sql-server-2014

MAX聚合与子查询:

这似乎是我最近编写的查询中一个反复出现的问题,我想发现哪种查询样式:

  • 效率最高(时间和资源)
  • 更可靠,更易于维护
  • 最有意义的使用

更多信息:

我写的查询总是从一个基表拉出来,也会加入到其他几个表中;但是,连接的表通常具有垂直方向,其中外键被多次引用,具有唯一的"描述符"和#34;回应。" (参见:表#MovieDescriptions的示例。)

请使用以下SQL查询作为测试方案:

 -- Drop temp tables if exist

IF OBJECT_ID('TempDB..#Movies','U') IS NOT NULL
     DROP TABLE #Movies

IF OBJECT_ID('TempDB..#MovieDescriptions','U') IS NOT NULL
     DROP TABLE #MovieDescriptions

-- Creating temp tables

CREATE TABLE #Movies
(
     MovieID int IDENTITY(1,1),
     MovieName varchar (100),
     ReleaseYear datetime,
     Director varchar (100)
)

CREATE TABLE #MovieDescriptions
(
     MovieDescID int IDENTITY(1,1),
     FK_MovieID varchar(100),
     DescriptionType varchar(100),
     DescriptionResponse varchar(100)
)

-- Inserting test data

INSERT INTO #Movies (MovieName, ReleaseYear, Director) VALUES ('Gone With the Wind', CONVERT(datetime,'12/15/1939'), 'Victor Fleming')
INSERT INTO #Movies (MovieName, ReleaseYear, Director) VALUES ('2001: A Space Odyssey', CONVERT(datetime,'01/01/1968'), 'Stanley Kubrick')


INSERT INTO #MovieDescriptions (FK_MovieID, DescriptionType, DescriptionResponse) VALUES ('1', 'Written By', 'Sideny Howard')
INSERT INTO #MovieDescriptions (FK_MovieID, DescriptionType, DescriptionResponse) VALUES ('1', 'Genre', 'Drama')
INSERT INTO #MovieDescriptions (FK_MovieID, DescriptionType, DescriptionResponse) VALUES ('1', 'Rating', 'G')

INSERT INTO #MovieDescriptions (FK_MovieID, DescriptionType, DescriptionResponse) VALUES ('2', 'Written By', 'Standley Kubrick')
INSERT INTO #MovieDescriptions (FK_MovieID, DescriptionType, DescriptionResponse) VALUES ('2', 'Genre', 'Sci-Fi')
INSERT INTO #MovieDescriptions (FK_MovieID, DescriptionType, DescriptionResponse) VALUES ('2', 'Rating', 'G')

-- Using subqueries

SELECT
     MovieName,
     ReleaseYear,
     (SELECT DescriptionResponse
      FROM #MovieDescriptions
      WHERE FK_MovieID = #Movies.MovieID AND DescriptionType = 'Genre'
      ) AS Genre,
     (SELECT DescriptionResponse
      FROM #MovieDescriptions
      WHERE FK_MovieID = #Movies.MovieID AND DescriptionType = 'Rating'
      ) AS Rating
FROM #Movies

-- Using aggregate functions

SELECT
     MovieName,
     ReleaseYear,
     MAX(CASE WHEN md.DescriptionType = 'Genre' THEN DescriptionResponse END) AS Genre,
     MAX(CASE WHEN md.DescriptionType = 'Rating' THEN DescriptionResponse END) AS Rating
FROM #Movies m
     INNER JOIN #MovieDescriptions md
     ON m.MovieID = md.FK_MovieID
GROUP BY MovieName, ReleaseYear

此外,如果有更好的方法可以选择此数据,那么这也会有所帮助。

3 个答案:

答案 0 :(得分:1)

假设您的表被正确编入索引的更正常的设置,以及外键关系列具有匹配的数据类型(提示提示:它们当前不匹配,intvarchar) ,那么你应该总是发现你的第二个查询(连接+聚合)优于第一个(select子句中的子查询)。少量数据可能不会引起注意。但是你的基表有越多的数据(#Movies),差异就越明显。

原因很简单。在您的第一个查询中:

SELECT
     MovieName,
     ReleaseYear,
     (SELECT DescriptionResponse
      FROM #MovieDescriptions
      WHERE FK_MovieID = #Movies.MovieID AND DescriptionType = 'Genre'
      ) AS Genre,
     (SELECT DescriptionResponse
      FROM #MovieDescriptions
      WHERE FK_MovieID = #Movies.MovieID AND DescriptionType = 'Rating'
      ) AS Rating
FROM #Movies

如果#Movies包含1000行,那么SQL Server别无选择,只能在#Movies上执行一次全表扫描,并且对于1000行中的每一行,它都需要执行2个额外的查询在#MovieDescriptions。实际上,您正在执行总共2001次查询。因为您的子查询位于SELECT子句中,所以SQL Server别无选择,只能以这种方式执行查询。

另一方面,您的第二个查询:

SELECT
     MovieName,
     ReleaseYear,
     MAX(CASE WHEN md.DescriptionType = 'Genre' THEN DescriptionResponse END) AS Genre,
     MAX(CASE WHEN md.DescriptionType = 'Rating' THEN DescriptionResponse END) AS Rating
FROM #Movies m
     INNER JOIN #MovieDescriptions md
     ON m.MovieID = md.FK_MovieID
GROUP BY MovieName, ReleaseYear

由于您在此处使用联接,这为SQL Server提供了灵活性,以便找出加入#Movies#MovieDescriptions数据的最有效方式。根据您的索引,过滤器,行数等,它可能决定进行散列连接,也许它将使用嵌套循环等。重点是SQL Server有更多选项,现在可以找出最佳方法减少2个表(和索引)中数据块读取的数量。

编辑:我还要补充一点,上面假设您从查询中返回 每个 行。如果查询返回数千行,但您只获取前10行,那么在某些情况下,第一个查询实际上可能优于第二个查询。这是因为子查询只会在行选择获取时在行上执行。如果您从未获取某些行,则可能永远不会产生在这些未获取的行上执行子查询的成本。需要考虑的事情。

答案 1 :(得分:1)

我更喜欢这个版本的查询..

SELECT
     MovieName,
     ReleaseYear,
     MAX(CASE WHEN md.DescriptionType = 'Genre' THEN DescriptionResponse END) AS Genre,
     MAX(CASE WHEN md.DescriptionType = 'Rating' THEN DescriptionResponse END) AS Rating
 FROM #Movies m
     INNER JOIN #MovieDescriptions md
     ON m.MovieID = md.FK_MovieID
GROUP BY MovieName, ReleaseYear

但是这涉及电影表格的排序,这种分类费用是总费用的63%,而且你只能获得所有列。

enter image description here

我使用Apply重写了这个版本,它没有排序,你不受限制地获取所有列。

select 
* from
#movies m
cross apply
(
select 
max(case when descriptiontype='genre' then descriptionresponse  end) as genre,
max(case when descriptiontype='rating' then descriptionresponse  end) as rating
from
#MovieDescriptions md
where  md.fk_movieid=m.movieid)b

上述版本的执行计划。
enter image description here

现在两个查询都没有索引..

第一个查询工作和扩展所需的索引是什么。

MovieName上的附加索引,ReleaseYear以避免排序并告诉SQLServer这些是唯一的...因为你已经拥有(假设)Movieid上的主键,还有一个索引可以避免排序,有些事我不知道&#39 ; t更喜欢,如果您需要更多来自第一个查询的列,则必须再次将其添加到索引..

对于第二个要更好地扩展的查询,
你需要一个关于FK_movieid的索引,我假设你已经拥有它...

PS:

我可能已经错过了许多东西,但我会继续我的询问..

答案 2 :(得分:0)

有很多方法可以从多个表中获取结果,例如嵌套/子查询,连接,聚合函数等。

但是当你需要来自n个表的结果时,联接比子查询更有效。

几张桌子可能无法观察到。但是,当您同时使用5个或6个表时,您将观察联接的执行方式。