让我们有以下数据
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
我要计算行数,去年的行数和最近十年的行数。可以使用条件聚合查询或以下子查询来实现
-- 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
如果执行查询并查看查询计划,则会看到类似
显然,第一种解决方案的查询计划,成本估算要好得多,甚至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.
因此,第二种解决方案的性能比使用条件聚合的解决方案更好(或相同)。如果我们在
datesent
属性上创建索引,则差异变得更加明显。CREATE INDEX ix_logtable_datesent ON dbo.LogTable(DateSent)
然后,第二种解决方案开始使用
Index Seek
而不是Table Scan
,并且其查询CPU时间性能在我的计算机上降至16ms。我的问题有两个:(1)为什么条件聚合解决方案至少在没有索引的情况下不优于子查询解决方案,(2)是否可以为条件聚合解决方案创建“索引”(或重写条件聚合查询)以避免扫描,或者如果我们关注性能,通常不适合使用条件聚合吗?
旁注:我可以说,这种情况对于条件聚合是非常乐观的,因为我们选择了所有行的数目,这总是导致使用扫描的解决方案。如果不需要所有行的数量,则带子查询的索引解决方案将不会进行扫描,而带条件聚合的解决方案无论如何都必须执行扫描。
编辑
弗拉基米尔·巴拉诺夫(Vladimir Baranov)基本上回答了第一个问题(非常感谢您)。但是,第二个问题仍然存在。我可以在StackOverflow上看到使用条件聚合解决方案的答案,而且它们引起了极大的关注,它们被认为是最优雅,最清晰的解决方案(有时被提议为最有效的解决方案)。因此,我将稍微概括一下这个问题:
您能否举个例子,其中条件聚集的性能明显优于子查询解决方案?
为简单起见,让我们假定不存在物理访问(数据位于缓冲区高速缓存中),因为当今的数据库服务器无论如何仍将其大部分数据保留在内存中。
最佳答案
简短的摘要
子查询方法可以比条件聚合更快或更慢,这取决于数据分布。
自然,如果表具有合适的索引,则子查询可能会从中受益,因为索引将只允许扫描表的相关部分,而不是完全扫描。拥有合适的索引不太可能显着受益于条件聚合方法,因为无论如何它将扫描整个索引。唯一的好处是,如果索引比表窄,并且引擎必须将更少的页面读入内存。
知道这一点,您可以决定选择哪种方法。
第一次测试
我做了一个更大的测试表,有500万行。桌上没有索引。
我使用SQL Sentry Plan Explorer测量了IO和CPU统计信息。我将SQL Server 2014 SP1-CU7(12.0.4459.0)Express 64位用于这些测试。
确实,您的原始查询的行为与您描述的相同,即子查询的读取速度提高了3倍。
在没有索引的表上尝试了几次之后,我重新编写了条件聚集并添加了变量来保存
DATEADD
表达式的值。总的时间变得明显更快。
然后我将
SUM
替换为COUNT
,它又变得更快了一点。毕竟,条件聚合变得与子查询一样快。
热缓存(CPU = 375)
SELECT -- warm cache
COUNT(*) AS all_cnt
FROM LogTable
OPTION (RECOMPILE);
子查询(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);
原始条件聚合(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);
使用变量(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);
使用变量和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);
基于这些结果,我的猜测是
CASE
为每一行调用了DATEADD
,而WHERE
足够聪明,可以计算一次。再加上COUNT
比SUM
效率高一点。最后,条件聚合仅比子查询慢一点(1062 vs 1031),可能是因为
WHERE
本身比CASE
效率更高,而且WHERE
过滤掉了很多行,因此COUNT
必须处理更少的行。在实践中,我将使用条件聚合,因为我认为读取次数更为重要。如果您的表很小,无法容纳并保留在缓冲池中,那么对于最终用户而言,任何查询都会很快。但是,如果表大于可用内存,那么我预计从磁盘读取将大大减慢子查询的速度。
第二次测试
另一方面,尽早过滤掉行也很重要。
这是测试的微小变化,证明了这一点。在这里,我将阈值设置为GETDATE()+ 100年,以确保没有行满足过滤条件。
热缓存(CPU = 344)
SELECT -- warm cache
COUNT(*) AS all_cnt
FROM LogTable
OPTION (RECOMPILE);
子查询(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);
原始条件聚合(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);
使用变量(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);
使用变量和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);
以下是带有子查询的计划。您可以看到第二行子查询中有0行进入了Stream Aggregate,所有这些行都在“表扫描”步骤中被过滤掉了。
结果,子查询再次变得更快。
第三次测试
在这里,我更改了先前测试的过滤条件:所有
>
都替换为<
。结果,条件COUNT
计数了所有行,而不是没有行。惊喜,惊喜!条件聚合查询花费了相同的750毫秒,而子查询变为813,而不是500。这是子查询的计划:
Could you give me an example, where conditional aggregation notably outperforms the subquery solution?
这里是。子查询的性能方法取决于数据分布。条件聚合的性能不取决于数据分布。
子查询方法可以比条件聚合更快或更慢,这取决于数据分布。
知道这一点,您可以决定选择哪种方法。
奖金详情
如果将鼠标悬停在
Table Scan
运算符上,您会看到不同变体的Actual Data Size
。COUNT(*)
:测试2中的
测试3中的
现在很明显,性能差异可能是由流经计划的数据量差异引起的。
如果是简单的
COUNT(*)
,则无需Output list
(不需要列值),并且数据大小最小(43MB)。在有条件聚合的情况下,此数量在测试2和3之间不会更改,始终为72MB。
Output list
具有一列datesent
。如果是子查询,此数量会根据数据分布更改。
关于sql - 有条件的聚合性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45795898/