【问题标题】:SQL why is SELECT COUNT(*) , MIN(col), MAX(col) faster then SELECT MIN(col), MAX(col)SQL 为什么 SELECT COUNT(*) , MIN(col), MAX(col) 比 SELECT MIN(col), MAX(col) 快
【发布时间】:2011-11-20 20:41:20
【问题描述】:

我们发现这些查询之间存在巨大差异。

慢查询

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 毫秒,运行时间 = 9 毫秒。

问题

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

更新 基于作为 cmets 给出的问题的一点更新:

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

慢查询

|--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)

回答

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

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

【问题讨论】:

  • 如果在第二个查询之后再次运行第一个查询会怎样?
  • 可能是因为当您使用 count(*) 时,您不会检查每条记录的 fk=4193?
  • 您是在一个接一个地运行这些吗?如果是这样:如果您将DBCC DROPCLEANBUFFERSDBCC FREEPROCCACHE 放在两个查询之前会发生什么?如果您更改顺序 - 先运行快速查询,然后运行慢速查询,会发生什么?
  • @Martin Smith:我在想 1. 统计数据更新 2. 可重复性
  • 您的计划与查询不匹配。 -1 误导我们。

标签: sql sql-server performance statistics correlation


【解决方案1】:

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 &lt;&gt; 'A' 部分,因此总共有 1,891 行匹配,需要汇总。

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

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

当您从SELECT 列表中删除COUNT(*) 时,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

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

【讨论】:

  • 从查询中删除状态列会使两个查询减慢几个百分点。
  • 自动创建统计/自动更新统计为真
  • 执行计划 XML pastebin.com/mBcgHYkN 我们现在正在检查它是否可能是一个未运行的维护计划。
  • 重新运行维护计划并没有提高性能。添加带有 (FORCESEEK) 的查询提示修复了性能问题。我会接受这是一个很好的答案,查询分析器显然猜错了如何执行此查询。
  • @CodingBarfield - 是的,问题与统计质量无关。即使使用FULLSCAN 更新它们,我的回答中的重现也会发生。问题是 SQL Server 目前没有逻辑来检测startdatefk 之间的相关性
猜你喜欢
  • 2021-05-06
  • 2018-02-25
  • 2014-08-15
  • 1970-01-01
  • 2023-03-21
  • 2011-06-24
  • 1970-01-01
  • 1970-01-01
  • 2019-09-07
相关资源
最近更新 更多