SQL为什么SELECT COUNT(*),MIN(col),MAX(col)比SELECT MIN(col),MAX(col)快

时间:2011-09-20 07:53:50

标签: sql sql-server performance statistics correlation

我们看到这些查询之间存在巨大差异。

慢查询

SELECT MIN(col) AS Firstdate, MAX(col) AS Lastdate 
FROM table WHERE status = 'OK' AND fk = 4193

表'表'。扫描计数2,逻辑读取2458969,物理读取0,预读读取0,lob逻辑读取0,lob物理读取0,lob预读读取0。

SQL Server执行时间: CPU时间= 1966毫秒,已用时间= 1955毫秒。

快速查询

SELECT count(*), MIN(col) AS Firstdate, MAX(col) AS Lastdate 
FROM table WHERE status = 'OK' AND fk = 4193

表'表'。扫描计数1,逻辑读取5803,物理读取0,预读读取0,lob逻辑读取0,lob物理读取0,lob预读读取0。

SQL Server执行时间: CPU时间= 0 ms,已用时间= 9 ms。

问题

查询之间巨大的性能差异之间的原因是什么?

更新 根据评论提出的问题进行一点点更新:

执行顺序或重复执行不会改变性能。 没有使用额外的参数,并且(测试)数据库在执行期间没有执行任何其他操作。

慢查询

|--Nested Loops(Inner Join)
 |--Stream Aggregate(DEFINE:([Expr1003]=MIN([DBTest].[dbo].[table].[startdate])))
   |    |--Top(TOP EXPRESSION:((1)))
   |         |--Nested Loops(Inner Join, OUTER REFERENCES:([DBTest].[dbo].[table].[id], [Expr1008]) WITH ORDERED PREFETCH)
   |              |--Index Scan(OBJECT:([DBTest].[dbo].[table].[startdate]), ORDERED FORWARD)
   |              |--Clustered Index Seek(OBJECT:([DBTest].[dbo].[table].[PK_table]), SEEK:([DBTest].[dbo].[table].[id]=[DBTest].[dbo].[table].[id]),  WHERE:([DBTest].[dbo].[table].[FK]=(5806) AND [DBTest].[dbo].[table].[status]<>'A') LOOKUP ORDERED FORWARD)
   |--Stream Aggregate(DEFINE:([Expr1004]=MAX([DBTest].[dbo].[table].[startdate])))
        |--Top(TOP EXPRESSION:((1)))
             |--Nested Loops(Inner Join, OUTER REFERENCES:([DBTest].[dbo].[table].[id], [Expr1009]) WITH ORDERED PREFETCH)
                  |--Index Scan(OBJECT:([DBTest].[dbo].[table].[startdate]), ORDERED BACKWARD)
                  |--Clustered Index Seek(OBJECT:([DBTest].[dbo].[table].[PK_table]), SEEK:([DBTest].[dbo].[table].[id]=[DBTest].[dbo].[table].[id]),  WHERE:([DBTest].[dbo].[table].[FK]=(5806) AND [DBTest].[dbo].[table].[status]<>'A') LOOKUP ORDERED FORWARD)

快速查询

 |--Compute Scalar(DEFINE:([Expr1003]=CONVERT_IMPLICIT(int,[Expr1012],0)))
   |--Stream Aggregate(DEFINE:([Expr1012]=Count(*), [Expr1004]=MIN([DBTest].[dbo].[table].[startdate]), [Expr1005]=MAX([DBTest].[dbo].[table].[startdate])))
        |--Nested Loops(Inner Join, OUTER REFERENCES:([DBTest].[dbo].[table].[id], [Expr1011]) WITH UNORDERED PREFETCH)
             |--Index Seek(OBJECT:([DBTest].[dbo].[table].[FK]), SEEK:([DBTest].[dbo].[table].[FK]=(5806)) ORDERED FORWARD)
             |--Clustered Index Seek(OBJECT:([DBTest].[dbo].[table].[PK_table]), SEEK:([DBTest].[dbo].[table].[id]=[DBTest].[dbo].[table].[id]),  WHERE:([DBTest].[dbo].[table].[status]<'A' OR [DBTest].[dbo].[table].[status]>'A') LOOKUP ORDERED FORWARD)

The execution plan from SSMS

答案

Martin Smith给出的答案似乎解释了这个问题。超短版本是MS-SQL查询分析器错误地在慢查询中使用查询计划,这会导致完整的表扫描。

添加Count(*),查询提示(FORCESCAN)或startdate,FK和status列的组合索引可以解决性能问题。

1 个答案:

答案 0 :(得分:25)

SQL Server基数估算器可以进行各种建模假设,例如

  
      
  • 独立性:除非有相关信息,否则不同列上的数据分布是独立的。
  •   
  • 均匀性:在每个统计对象直方图步骤中,不同的值均匀分布,每个值具有相同的频率。
  •   
     

Source

表格中有810,064行。

您有查询

SELECT COUNT(*),
       MIN(startdate) AS Firstdate,
       MAX(startdate) AS Lastdate
FROM   table
WHERE  status <> 'A'
       AND fk = 4193 

1,893(0.23%)行符合fk = 4193谓词,其中两行未通过status <> 'A'部分,因此整体1,891匹配,需要汇总。

您还有两个索引,它们都不包含整个查询。

对于快速查询,它使用fk上的索引直接查找fk = 4193然后需要执行1,893 key lookups的行,以查找聚簇索引中的每一行以检查{{ 1}}谓词并检索status进行聚合。

startdate列表中删除COUNT(*)时,SQL Server不再具有来处理每个符合条件的行。因此,它考虑了另一种选择。

你有一个SELECT的索引,所以它可以从头开始扫描,将关键查找返回到基表,并在找到第一个匹配的行时找到{{1同样地,startdate可以通过从索引的另一端开始并向后工作的另一次扫描找到。

SQL Server估计这些扫描中的每一个都会在遇到与谓词匹配的行之前处理590行。总共1,180次查找与1,893次查找,因此它选择了此计划。

590数字仅为MIN(startdate)。即基数估计器假设匹配的行将在整个表中均匀分布。

不幸的是,符合谓词的1,891行相对于MAX 随机分布。事实上,它们都被压缩到索引末尾的单个8,205行段中,这意味着到达table_size / estimated_number_of_rows_that_match的扫描最终会在它停止之前执行801,859次键查找。

这可以在下面复制。

startdate

您可以考虑使用查询提示来强制计划使用MIN(startdate)而不是CREATE TABLE T ( id int identity(1,1) primary key, startdate datetime, fk int, [status] char(1), Filler char(2000) ) CREATE NONCLUSTERED INDEX ix ON T(startdate) INSERT INTO T SELECT TOP 810064 Getdate() - 1, 4192, 'B', '' FROM sys.all_columns c1, sys.all_columns c2 UPDATE T SET fk = 4193, startdate = GETDATE() WHERE id BETWEEN 801859 and 803748 or id = 810064 UPDATE T SET startdate = GETDATE() + 1 WHERE id > 810064 /*Both queries give the same plan. UPDATE STATISTICS T WITH FULLSCAN makes no difference*/ SELECT MIN(startdate) AS Firstdate, MAX(startdate) AS Lastdate FROM T WHERE status <> 'A' AND fk = 4192 SELECT MIN(startdate) AS Firstdate, MAX(startdate) AS Lastdate FROM T WHERE status <> 'A' AND fk = 4193 上的索引,或者在fk的执行计划中添加突出显示的建议缺失索引以避免这种情况问题。