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

Cod*_*eld 15 sql sql-server statistics performance correlation

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

慢查询

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

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

SQL Server执行时间:CPU时间= 1966 ms,已用时间= 1955 ms.

快速查询

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

表'表'.扫描计数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)
Run Code Online (Sandbox Code Playgroud)

快速查询

 |--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)
Run Code Online (Sandbox Code Playgroud)

SSMS的执行计划

回答

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

添加Count(*),查询提示(FORCESCAN)或startdate,FK和status列上的组合索引可修复性能问题.

Mar*_*ith 25

SQL Server基数估计器做出各种建模假设,如

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

资源

表中有810,064行.

你有查询

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

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

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

对于快速查询,它使用索引fk来直接查找行,fk = 4193然后需要执行1,893次键查找以查找聚簇索引中的每一行以检查status谓词并检索startdatefor聚合.

当您删除COUNT(*)SELECT列表SQL Server不再具有处理每一个符合条件的行.因此,它考虑另一种选择.

你有一个索引,startdate所以它可以从头开始扫描,执行键查找返回到基表,一旦找到第一个匹配的行停止,就像找到它一样MIN(startdate),同样MAX可以找到另一个扫描开始索引的另一端并向后工作.

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

590的数字是正确的table_size / estimated_number_of_rows_that_match.即,基数估计器假设匹配的行将在整个表中均匀分布.

不幸的是,符合谓词的1,891行不是随机分布的startdate.事实上,它们都被压缩到索引末尾的单个8,205行段中,这意味着扫描到达MIN(startdate)最终需要进行801,859次键查找才能停止.

这可以在下面复制.

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
Run Code Online (Sandbox Code Playgroud)

您可以考虑使用查询提示强制计划使用索引fk而不是startdate或添加执行计划中突出显示的建议缺失索引(fk,status) INCLUDE (startdate)以避免此问题.

  • @CodingBarfield - 是的,问题与统计质量无关.即使用`FULLSCAN'更新了我的答案中的复制品.问题是SQL Server目前没有逻辑来检测`startdate`和`fk`之间的相关性 (3认同)