我们看到这些查询之间存在巨大差异。
慢查询
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)
答案
Martin Smith给出的答案似乎解释了这个问题。超短版本是MS-SQL查询分析器错误地在慢查询中使用查询计划,这会导致完整的表扫描。
添加Count(*),查询提示(FORCESCAN)或startdate,FK和status列的组合索引可以解决性能问题。
答案 0 :(得分:25)
SQL Server基数估算器可以进行各种建模假设,例如
- 独立性:除非有相关信息,否则不同列上的数据分布是独立的。
- 均匀性:在每个统计对象直方图步骤中,不同的值均匀分布,每个值具有相同的频率。
表格中有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
的执行计划中添加突出显示的建议缺失索引以避免这种情况问题。