我有一个奇怪的问题,希望有人能帮助我解决。
我的SQL Server版本是2008 R2。它在一台24核的服务器上运行。
我有两个查询字符串:
String SQL_1 = "select
t.testconfig_id, t.minuteSequence, t.location_id,
sum(t.vuPerNode) as totalVu,
sum(t.backOffPctSum) / sum(t.recordNum) as avgBackOffPct
from
(select
p.testconfig_id, p.minuteSequence, r.location_Id,
SUM(p.activeCount) * 1.0 / COUNT(1) as vuPerNode,
SUM(p.backOffPct) as backOffPctSum, COUNT(1) as recordNum
from
loadtest_progress_in_minute p (nolock)
join
loadtestRunrecord r (nolock) on p.test_id = r.test_id and p.nodeId = r.nodeId
where
p.test_id = ?
group by
p.testconfig_id, p.minuteSequence, p.nodeId, r.location_id) t
group by
t.testconfig_id, t.minuteSequence, t.location_id
order by
t.testconfig_id, t.minuteSequence, t.location_id option (maxdop 23)"
String SQL-2 = "select
t.testconfig_id, t.minuteSequence, t.location_id,
sum(t.vuPerNode) as totalVu,
sum(t.backOffPctSum) / sum(t.recordNum) as avgBackOffPct
from
(select
p.testconfig_id, p.minuteSequence, r.location_Id,
SUM(p.activeCount) * 1.0 / COUNT(1) as vuPerNode,
SUM(p.backOffPct) as backOffPctSum, COUNT(1) as recordNum
from
loadtest_progress_in_minute p (nolock)
join
loadtestRunrecord r (nolock) on p.test_id = r.test_id and p.nodeId = r.nodeId
where
p.test_id = ?
group by
p.testconfig_id, p.minuteSequence, p.nodeId, r.location_id) t
group by
t.testconfig_id, t.minuteSequence, t.location_id
order by
t.testconfig_id, t.minuteSequence, t.location_id option (maxdop 23) "
两个查询之间的唯一区别在于SQL-2末尾多了一个制表符。我在同一环境中使用以下代码运行这两个查询:
PreparedStatemen ps = conn.prepareStatement(SQL_1);
//PreparedStatemen ps = conn.prepareStatement(SQL_2);
ps.setLong(1234);
ps.execute();
我发现在代码片段中,这两个查询的性能有时会出现很大的差异。
在此代码片段中,SQL_1 的
ps.execute()
只需要约10秒钟。当 SQL_1 运行时,我看到 CPU 使用率很高。服务器的24个CPU全部被使用。但是,相同代码片段中的 SQL_2 的
ps.execute()
大约需要150秒。当 SQL_2 运行时,CPU 使用率很低。只有24个 CPU 中的 2 个被使用。SQL_1 和 SQL_2 同时运行。
但是以上观察结果并不总是如此。有时 SQL_1 和 SQL_2 的
ps.execute()
性能相同。有时 SQL_1 和 SQL_2 的 ps.execute()
性能如上所述。这就是我发现的情况。这让人很困惑。SQL 的最后一个空格会影响 SQL Server 的性能吗?
我认为这不是由于自动参数化缓存引起的,就像在Space in SQL Server 2008 R2 slows down performance中描述的那样。
因为我通过无限循环调用代码片段来得到上述观察结果。
从 Wireshark 中,我发现 Microsoft JDBC 会在 SQL 字符串和参数之间添加2个额外的 tab(1 个 tab 字符 = 4 个空格字符)字符,如下所示:
[20] [00] [20] [00] [20] [00] [20] [00] [20] [00] [20] [00][20] [00][20] [00]
我不知道它是否与我的问题有关。
谢谢。
更新2012-8-20:
我在sql-server管理工具中执行了以下3个sql。 它们具有相同的执行计划。 但我发现了奇怪的事情:
declare @sql varchar(max);
set @sql='Declare @testId bigint;set @testId = 1234;select p.testconfig_id, p.minuteSequence,r.location_Id, SUM(p.activeCount) * 1.0 / COUNT(1) as vuPerNode, SUM(p.backOffPct) as backOffPctSum, COUNT(1) as recordNum from loadtest_progress_in_minute p with( nolock,index(idx_loadtest_progress_in_minute_1) ) join loadtestRunrecord r ( nolock ) on p.test_id = r.test_id and p.nodeId = r.nodeId where p.test_id = @testId group by p.testconfig_id, p.minuteSequence, p.nodeId, r.location_id option (maxdop 23)';
execute (@sql);
这个SQL的性能非常差,需要大约90秒才能完成。只使用了2个CPU。
Declare @sSQL nvarchar(2000);
Declare @paramDefine nvarchar(2000);
Declare @testId bigint;
set @testId = 1234;
set @sSQL = N' select t.testconfig_id, t.minuteSequence, t.location_id, sum(t.vuPerNode) as totalVu, sum(t.backOffPctSum) / sum(t.recordNum) as avgBackOffPct from ( select p.testconfig_id, p.minuteSequence, r.location_Id, SUM(p.activeCount) * 1.0 / COUNT(1) as vuPerNode, SUM(p.backOffPct) as backOffPctSum, COUNT(1) as recordNum from loadtest_progress_in_minute p ( nolock ) join loadtestRunrecord r ( nolock ) on p.test_id = r.test_id and p.nodeId = r.nodeId where p.test_id = @P0 group by p.testconfig_id, p.minuteSequence, p.nodeId, r.location_id ) t group by t.testconfig_id, t.minuteSequence, t.location_id order by t.testconfig_id, t.minuteSequence, t.location_id option (maxdop 23) ';
set @paramDefine = N'@P0 bigint';
execute sp_executesql @sSQL, @paramDefine, @P0 = @testId;
这个SQL在管理工具中只需约10秒,使用了所有CPU。
declare @p1 int
--set @p1=1
exec sp_prepexec @p1 output,N'@P0 bigint',N' select t.testconfig_id, t.minuteSequence, t.location_id, sum(t.vuPerNode) as totalVu, sum(t.backOffPctSum) / sum(t.recordNum) as avgBackOffPct from ( select p.testconfig_id, p.minuteSequence, r.location_Id, SUM(p.activeCount) * 1.0 / COUNT(1) as vuPerNode, SUM(p.backOffPct) as backOffPctSum, COUNT(1) as recordNum from loadtest_progress_in_minute p ( nolock ) join loadtestRunrecord r ( nolock ) on p.test_id = r.test_id and p.nodeId = r.nodeId where p.test_id = @P0 group by p.testconfig_id, p.minuteSequence, p.nodeId, r.location_id ) t group by t.testconfig_id, t.minuteSequence, t.location_id order by t.testconfig_id, t.minuteSequence, t.location_id option (maxdop 23) ',@P0=1234
select @p1
这个SQL的性能很快,只需要大约10秒钟就能完成。所有的CPU都被利用。
更新2012-8-21
到目前为止,我还没有得出关于我发现的内容的最终结论。因为Windows世界不是开放的,也许我们永远无法获得SQL Server内部的详细信息。在这里,我只是解释了我所发现的内容。有些解释只是我的猜测。我希望这对其他人有所帮助。
1)为什么我在JDBC中测试两个类似的SQL时(除了最后一个制表符是否存在之外),有时会得到不同的性能表现?
我们的测试不是在一个隔离的环境中进行的。当我用JDBC测试这两个SQL时,其他进程也同时执行具有最后一个制表符的SQL。因此,我们的测试结果受其他进程的影响。
不同的性能根本原因在于它们选择了不同的执行计划。其中一个选择了具有良好并行性的执行计划。另一个选择了具有较差并行性的执行计划。因为所有其他进程都在执行带有制表符的SQL,所以没有制表符的SQL被视为新SQL。因此,当执行没有制表符的SQL时,它会根据记录统计数据生成一个基于典型参数值的新执行计划。也许最初的典型值在性能方面不是很好。但实际访问的参数值直方图可以刷新执行计划缓存。没有制表符的SQL仅在我的测试中使用。我的测试只使用参数值(1234)。SQL服务器认为(1234)经常访问该SQL,并刷新具有实际最常访问的参数值(1234)的执行计划。因此,性能变得很好。
当我切换回带有制表符的SQL时,SQL Server将采用较旧的执行计划缓存。这个缓存可能是由其他正在运行的进程引入并受到其他进程的影响。这个执行计划也是基于实际上最常访问的参数值生成的。但是这个值受其他进程的影响。因此,它可能不是(1234),而基于该值的执行计划在性能上对(1234)不利。这就是为什么带有制表符的SQL性能有时很差。
因为有时我的带有制表符的SQL测试程序也可以刷新执行计划缓存,如果我的测试运行频率足够高以改变实际访问的参数值。带有制表符的SQL的性能有时也会变得很好。
2)为什么在SSMS中以下SQL总是很慢
declare @sql varchar(max)
set @sql='Declare @testId bigint;set @testId = 36887;select p.testconfig_id, p.minuteSequence,r.location_Id, SUM(p.activeCount) * 1.0 / COUNT(1) as vuPerNode, SUM(p.backOffPct) as backOffPctSum, COUNT(1) as recordNum from loadtest_progress_in_minute p with( nolock,index(idx_loadtest_progress_in_minute_1) ) join loadtestRunrecord r ( nolock ) on p.test_id = r.test_id and p.nodeId = r.nodeId where p.test_id = @testId group by p.testconfig_id, p.minuteSequence, p.nodeId, r.location_id option (maxdop 23)'
execute (@sql)
由于SSMS还基于记录统计的典型参数值生成执行计划。参数值(1234)是非典型值,因此上述SQL在开始时速度较慢。我猜测SSMS中的“execute”命令很特殊,其缓存不会被实际最常访问的参数值清除。因此它总是慢的。根据我的实验结果,我认为“sp_prepexec”和“sp_executesql”与“execute”不同,它们的计划缓存可以通过实际最常访问的参数值进行清除,并且与JDBC具有类似的行为。
3) 为什么添加recompile提示会加速上述SQL的性能 在回答这个问题之前,请看一下MSDN在线帮助文档中的以下文字。
“指示SQL Server数据库引擎在查询执行后放弃为查询生成的计划,强制查询优化器在下次执行相同查询时重新编译查询计划。如果没有指定RECOMPILE,则数据库引擎将缓存查询计划并重用它们。在编译查询计划时,RECOMPILE查询提示使用查询中任何本地变量的当前值,如果查询在存储过程中,则使用传递给任何参数的当前值。
RECOMPILE是创建使用WITH RECOMPILE子句的存储过程的有用替代方法,当只需要重新编译存储过程中的一部分查询而不是整个存储过程时。有关详细信息,请参见重新编译存储过程。当创建计划指南时,RECOMPILE也非常有用。”
请注意以下句子: 在编译查询计划时,RECOMPILE查询提示使用查询中任何本地变量的当前值,如果查询在存储过程中,则使用传递给任何参数的当前值。
这意味着RECOMPILE查询提示改变了SSMS执行计划生成的行为。SSMS在没有RECOMPILE查询提示的情况下基于典型参数值生成执行计划,而在有RECOMPILE查询提示的情况下基于当前参数值生成执行计划。因此,重新编译提示使执行计划对于当前参数值(1234)是完美的。
总之,执行计划是由复杂因素选择的。我们必须仔细考虑。