我遇到了一个奇怪的问题。也许这里有人可以帮助我。
我的 SQL Server 版本是 2008 R2。它在具有 24 个内核的服务器上运行。
我有 2 个查询字符串:
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();
我发现代码片段中 2 个查询的性能有时非常不同。
ps.excute()
此代码段中的 SQL_1 仅花费大约 10 秒。我看到 SQL_1 运行时 CPU 使用率很高。服务器的24个CPU全部使用。
但是ps.excute()
同一代码段中的 SQL_2 大约需要 150 秒。SQL_2 运行时 CPU 使用率很慢。仅使用了 24 个 CPU 中的 2 个。
SQL_1 和 SQL_2 同时运行。
但上述观察并非总是如此。有时ps.execute()
SQL_1 和 SQL_2 的性能是相同的。有时ps.execute()
SQL_1 和 SQL_2 的性能和我上面描述的一样。
这就是我发现的。这很令人困惑。SQL 的最后一个空格会损害 SQL Server 的性能吗?
我认为这不是由 SQL Server 2008 R2 中的空间降低性能中所述的自动参数化缓存引起的
因为我通过长时间无限循环调用代码片段得到了上述观察结果。
从wireshark我发现Microsoft JDBC会在SQL字符串和参数之间附加2个额外的制表符(1个制表符= 4个空格字符)字符,如下所示:
[20] [00] [20] [00] [20] [00] [20] [00] [20] [00] [20] [00][20] [00][20] [00]
不知道是不是和我的问题有关。
谢谢。
2012 年 8 月 20 日更新:
我在 sql-server management studio 中执行了以下 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 在 management studio 中只需要大约 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 world 没有打开,我们可能永远无法得到SQL Server 内部的细节。在这里,我只是解释一下我的发现。一些解释只是我的猜测。我希望它对其他人有帮助。
1)为什么我有时会在 JDBC 中获得两个类似 SQL 的不同性能(这些 SQL 是相同的,除了最后一个制表符是否存在)
我们的测试不是在孤立的环境中进行的。当我用 JDBC 测试这两个 SQL 时,其他进程也会同时执行最后一个制表符的 SQL。所以我们的测试结果会受到其他过程的影响。
性能不同的根本原因是他们选择了不同的执行计划。一个选择了具有良好parellesim的执行计划。另一个选择了parellesim不好的执行计划。因为所有其他进程都在执行带制表符的 SQL,所以不带制表符的 SQL 被视为新 SQL。因此在执行不带制表符的 SQL 时,它会根据记录的统计信息,根据典型参数值生成一个新的执行计划。也许典型值在第一次表现不佳。但是实际访问的参数值直方图可以刷新执行计划缓存。没有制表符的 SQL 仅在我的测试中使用。我的测试只使用参数值(1234)。SQL server 认为 SQL 经常访问 (1234),并用实际最常访问的参数值 (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 中的“执行”命令很特殊,它的缓存不会被实际最常见的访问参数值刷新。所以总是很慢。根据我的实验结果,我猜 'sp_prepexec' 和 'sp_executesql' 与 'execute' 不同,它们的计划缓存可以通过实际最常见的访问参数值刷新,并且与 JDBC 具有类似的行为。
3)为什么添加重新编译提示会加快上述SQL的性能在回答这个问题之前,让我们看一下MSDN在线帮助文档中的以下文字。
“指示 SQL Server 数据库引擎在执行后丢弃为查询生成的计划,强制查询优化器在下次执行相同查询时重新编译查询计划。如果不指定 RECOMPILE,数据库引擎会缓存查询计划并重用它们. 编译查询计划时,RECOMPILE 查询提示使用查询中任何局部变量的当前值,如果查询在存储过程中,则将当前值传递给任何参数。
RECOMPILE 是创建使用 WITH RECOMPILE 子句的存储过程的有用替代方法,当必须重新编译存储过程中的查询子集而不是整个存储过程时。有关详细信息,请参阅重新编译存储过程。当您创建计划指南时,重新编译也很有用。”
请注意以下句子:编译查询计划时,RECOMPILE 查询提示使用查询中任何局部变量的当前值,如果查询在存储过程中,则将当前值传递给任何参数。
这意味着 RECOMPILE 查询提示改变了 SSMS 执行计划生成行为。SSMS 基于典型参数值生成执行计划,而没有 RECOMPILE 查询提示,而 SSMS 则基于当前参数值生成执行计划,并使用 RECOMPILE 查询提示。所以重新编译提示使执行计划非常适合当前参数值(1234)
一言以蔽之,执行计划是由复杂的因素选择的。我们必须仔细考虑。