条件聚合性能

Rad*_*ača 21 sql sql-server query-performance conditional-aggregation

我们有以下数据

 IF OBJECT_ID('dbo.LogTable', 'U') IS NOT NULL  DROP TABLE dbo.LogTable

 SELECT TOP 100000 DATEADD(day, ( ABS(CHECKSUM(NEWID())) % 65530 ), 0) datesent 
 INTO [LogTable]
 FROM    sys.sysobjects
 CROSS JOIN sys.all_columns
Run Code Online (Sandbox Code Playgroud)

我想计算行数,去年行数和最近十年行数.这可以使用条件聚合查询或使用子查询来实现,如下所示

-- conditional aggregation query
SELECT
    COUNT(*) AS all_cnt,
    SUM(CASE WHEN datesent > DATEADD(year,-1,GETDATE())
             THEN 1 ELSE 0 END) AS last_year_cnt,
    SUM(CASE WHEN datesent > DATEADD(year,-10,GETDATE())
             THEN 1 ELSE 0 END) AS last_ten_year_cnt
FROM LogTable


-- subqueries
SELECT
(
    SELECT count(*) FROM LogTable 
) all_cnt, 
(
    SELECT count(*) FROM LogTable WHERE datesent > DATEADD(year,-1,GETDATE())
) last_year_cnt,
(
    SELECT count(*) FROM LogTable WHERE datesent > DATEADD(year,-10,GETDATE())
) last_ten_year_cnt
Run Code Online (Sandbox Code Playgroud)

如果您执行查询并查看查询计划,那么您会看到类似的内容

在此输入图像描述

显然,第一个解决方案有更好的查询计划,成本估算甚至SQL命令看起来更简洁和花哨.但是,如果使用SET STATISTICS TIME ON我测量查询的CPU时间,我会得到以下结果(我已经测量了几次大致相同的结果)

(1 row(s) affected)

 SQL Server Execution Times:
   CPU time = 47 ms,  elapsed time = 41 ms.

(1 row(s) affected)

(1 row(s) affected)

 SQL Server Execution Times:
   CPU time = 31 ms,  elapsed time = 26 ms.
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.
Run Code Online (Sandbox Code Playgroud)

因此,第二种解决方案比使用条件聚合的解决方案具有稍好(或相同)的性能.如果我们在datesent属性上创建索引,差异就会变得更明显.

CREATE INDEX ix_logtable_datesent ON dbo.LogTable(DateSent)
Run Code Online (Sandbox Code Playgroud)

然后第二个解决方案开始使用Index Seek而不是Table Scan它的查询CPU时间性能下降到我的计算机上16毫秒.

我的问题是两个:(1)为什么条件聚合解决方案至少在没有索引的情况下不优于子查询解决方案,(2)是否有可能为条件聚合解决方案创建'索引'(或重写条件聚合查询)为了避免扫描,或者如果我们关注性能,条件聚合通常是不合适的?

旁注:我可以说,这种情况对于条件聚合非常乐观,因为我们选择所有行的数量总是导致使用扫描的解决方案.如果不需要所有行的数量,则具有子查询的索引解决方案没有扫描,而具有条件聚合的解决方案无论如何都必须执行扫描.

编辑

弗拉基米尔巴拉诺夫基本上回答了第一个问题(非常感谢你).但是,第二个问题仍然存在.我可以在StackOverflow上看到使用条件聚合解决方案的答案,它们吸引了很多关注,被认为是最优雅和最清晰的解决方案(有时被认为是最有效的解决方案).因此,我将稍微概括一下这个问题:

你能举个例子,条件聚合明显优于子查询解决方案吗?

为简单起见,我们假设物理访问不存在(数据在缓冲区缓存中),因为今天的数据库服务器仍然将大部分数据保留在内存中.

Vla*_*nov 19

简短的摘要

  • 子查询方法的性能取决于数据分布.
  • 条件聚合的性能不依赖于数据分布.

子查询方法可以比条件聚合更快或更慢,它取决于数据分布.

当然,如果表具有合适的索引,则子查询可能会从中受益,因为索引将仅允许扫描表的相关部分而不是完整扫描.拥有合适的索引不太可能显着有利于条件聚合方法,因为它无论如何都会扫描整个索引.唯一的好处是,如果索引比表更窄,并且引擎必须将更少的页面读入内存.

了解这一点,您可以决定选择哪种方法.


第一次测试

我做了一个更大的测试表,有5M行.桌子上没有索引.我使用SQL Sentry Plan Explorer测量了IO和CPU统计信息.我使用SQL Server 2014 SP1-CU7(12.0.4459.0)Express 64位进行这些测试.

实际上,您的原始查询的行为与您所描述的相同,即即使读取次数高3倍,子查询也会更快.

几次尝试没有索引的表后,我重写了条件聚合并添加了变量来保存DATEADD表达式的值.

整体时间明显加快.

然后我换SUMCOUNT,它再次变得快一点.

毕竟,条件聚合变得和子查询一样快.

加热缓存(CPU = 375)

SELECT -- warm cache
    COUNT(*) AS all_cnt
FROM LogTable
OPTION (RECOMPILE);
Run Code Online (Sandbox Code Playgroud)

子查询(CPU = 1031)

SELECT -- subqueries
(
    SELECT count(*) FROM LogTable 
) all_cnt, 
(
    SELECT count(*) FROM LogTable WHERE datesent > DATEADD(year,-1,GETDATE())
) last_year_cnt,
(
    SELECT count(*) FROM LogTable WHERE datesent > DATEADD(year,-10,GETDATE())
) last_ten_year_cnt
OPTION (RECOMPILE);
Run Code Online (Sandbox Code Playgroud)

原始条件聚合(CPU = 1641)

SELECT -- conditional original
    COUNT(*) AS all_cnt,
    SUM(CASE WHEN datesent > DATEADD(year,-1,GETDATE())
             THEN 1 ELSE 0 END) AS last_year_cnt,
    SUM(CASE WHEN datesent > DATEADD(year,-10,GETDATE())
             THEN 1 ELSE 0 END) AS last_ten_year_cnt
FROM LogTable
OPTION (RECOMPILE);
Run Code Online (Sandbox Code Playgroud)

带变量的条件聚合(CPU = 1078)

DECLARE @VarYear1 datetime = DATEADD(year,-1,GETDATE());
DECLARE @VarYear10 datetime = DATEADD(year,-10,GETDATE());

SELECT -- conditional variables
    COUNT(*) AS all_cnt,
    SUM(CASE WHEN datesent > @VarYear1
             THEN 1 ELSE 0 END) AS last_year_cnt,
    SUM(CASE WHEN datesent > @VarYear10
             THEN 1 ELSE 0 END) AS last_ten_year_cnt
FROM LogTable
OPTION (RECOMPILE);
Run Code Online (Sandbox Code Playgroud)

使用变量和COUNT而不是SUM进行条件聚合(CPU = 1062)

SELECT -- conditional variable, count, not sum
    COUNT(*) AS all_cnt,
    COUNT(CASE WHEN datesent > @VarYear1
             THEN 1 ELSE NULL END) AS last_year_cnt,
    COUNT(CASE WHEN datesent > @VarYear10
             THEN 1 ELSE NULL END) AS last_ten_year_cnt
FROM LogTable
OPTION (RECOMPILE);
Run Code Online (Sandbox Code Playgroud)

时

基于这些结果,我猜测是为每一行CASE调用DATEADD的,而且WHERE足够聪明,可以计算一次.加上COUNT比一点点效率更高SUM.

最后,条件聚合只比子查询稍慢(1062对1031),可能因为WHERE它比CASE它本身更有效,而且,WHERE过滤掉了很多行,因此COUNT必须处理较少的行.


在实践中,我会使用条件聚合,因为我认为读取次数更重要.如果您的表很小以适应并保留在缓冲池中,那么对于最终用户来说任何查询都会很快.但是,如果表大于可用内存,那么我预计从磁盘读取会显着减慢子查询.


第二次测试

另一方面,尽早过滤行也很重要.

这是测试的一个细微变化,它证明了这一点.在这里,我将阈值设置为GETDATE()+ 100年,以确保没有行满足过滤条件.

加热缓存(CPU = 344)

SELECT -- warm cache
    COUNT(*) AS all_cnt
FROM LogTable
OPTION (RECOMPILE);
Run Code Online (Sandbox Code Playgroud)

子查询(CPU = 500)

SELECT -- subqueries
(
    SELECT count(*) FROM LogTable 
) all_cnt, 
(
    SELECT count(*) FROM LogTable WHERE datesent > DATEADD(year,100,GETDATE())
) last_year_cnt
OPTION (RECOMPILE);
Run Code Online (Sandbox Code Playgroud)

原始条件聚合(CPU = 937)

SELECT -- conditional original
    COUNT(*) AS all_cnt,
    SUM(CASE WHEN datesent > DATEADD(year,100,GETDATE())
             THEN 1 ELSE 0 END) AS last_ten_year_cnt
FROM LogTable
OPTION (RECOMPILE);
Run Code Online (Sandbox Code Playgroud)

带变量的条件聚合(CPU = 750)

DECLARE @VarYear100 datetime = DATEADD(year,100,GETDATE());

SELECT -- conditional variables
    COUNT(*) AS all_cnt,
    SUM(CASE WHEN datesent > @VarYear100
             THEN 1 ELSE 0 END) AS last_ten_year_cnt
FROM LogTable
OPTION (RECOMPILE);
Run Code Online (Sandbox Code Playgroud)

使用变量和COUNT而不是SUM进行条件聚合(CPU = 750)

SELECT -- conditional variable, count, not sum
    COUNT(*) AS all_cnt,
    COUNT(CASE WHEN datesent > @VarYear100
             THEN 1 ELSE NULL END) AS last_ten_year_cnt
FROM LogTable
OPTION (RECOMPILE);
Run Code Online (Sandbox Code Playgroud)

times2

下面是一个包含子查询的计划.您可以看到在第二个子查询中有0行进入Stream Aggregate,所有这些行都在Table Scan步骤中被过滤掉了.

plan_subqueries

结果,子查询再次更快.

第三次测试

在这里,我更改了之前测试的过滤条件:所有内容>都替换为<.因此,条件COUNT计算所有行而不是无.惊喜,惊喜!条件聚合查询花费相同的750毫秒,而子查询变为813而不是500.

times3

这是子查询的计划:

plan_subqueries3

你能举个例子,条件聚合明显优于子查询解决方案吗?

这里是.子查询方法的性能取决于数据分布.条件聚合的性能不依赖于数据分布.

子查询方法可以比条件聚合更快或更慢,它取决于数据分布.

了解这一点,您可以决定选择哪种方法.


奖金详情

如果将鼠标悬停在Table Scan操作员上,则可以看到Actual Data Size不同的变体.

  1. 简单COUNT(*):

数据大小计数

  1. 条件聚合:

数据大小有条件

  1. 测试2中的子查询:

数据大小子查询test2

  1. 测试3中的子查询:

数据大小子查询test3

现在很明显,性能差异可能是由于流经计划的数据量的差异造成的.

在简单的情况下COUNT(*)没有Output list(不需要列值)并且数据大小最小(43MB).

在条件聚合的情况下,这个数量在测试2和3之间不会改变,它总是72MB.Output list有一栏datesent.

对于子查询,此数量确实会根据数据分布而更改.

  • 真是出色的答案.我只想略微改变结论."子查询方法的性能取决于数据分布和索引的存在." 类似地,在条件聚合的情况下可以提及索引.我认为索引使用对结论很重要,因为条件聚合不能使用任何. (2认同)