SQL Server 查询:使用文字查询速度快,但使用变量查询速度慢

问题描述 投票:0回答:9

我有一个使用 CTE 从表中返回 2 个整数的视图。如果我像这样查询视图,它会在不到一秒的时间内运行

SELECT * FROM view1 WHERE ID = 1

但是,如果我像这样查询视图,则需要 4 秒。

DECLARE @id INT = 1
SELECT * FROM View1 WHERE ID = @id

我已经检查了 2 个查询计划,第一个查询正在主表上执行聚集索引查找,返回 1 条记录,然后将视图查询的其余部分应用于该结果集,而第二个查询正在执行索引扫描,其中返回大约 3000 条记录,而不仅仅是我感兴趣的记录,然后过滤结果集。

是否有任何明显的事情我错过了尝试让第二个查询使用索引查找而不是索引扫描。我正在使用 SQL 2008,但我所做的任何事情都需要在 SQL 2005 上运行。起初我以为这是某种参数嗅探问题,但即使我清除缓存,我也会得到相同的结果。

sql-server performance sql-server-2008
9个回答
71
投票

可能是因为在参数情况下,优化器无法知道该值不为空,因此需要创建一个即使为空也能返回正确结果的计划。如果您有 SQL Server 2008 SP1,您可以尝试将

OPTION(RECOMPILE)
添加到查询中。


9
投票

您可以在查询中添加 OPTIMIZE FOR hint,例如

DECLARE @id INT = 1
SELECT * FROM View1 WHERE ID = @id OPTION (OPTIMIZE FOR (@ID = 1))

5
投票

在我的例子中,数据库表列类型被定义为 VarChar,而参数化查询参数类型被定义为 NVarChar,这在实际执行计划中引入了

CONVERT_IMPLICIT
,以在比较之前匹配数据类型,这是母猪性能的罪魁祸首,2 秒与 11 秒相比只需更正参数类型即可使参数化查询与非参数化版本一样快。

一种可能的方法是

CAST
参数,如下所示:

SELECT ...
FROM ...
WHERE name = CAST(:name AS varchar)

希望这可以帮助有类似问题的人。


2
投票

我自己也遇到了这个问题,一个视图运行了 < 10ms with a direct assignment (WHERE UtilAcctId=12345),但使用变量赋值却花费了 100 倍的时间(WHERE UtilAcctId = @UtilAcctId)。
后者的执行计划与我在整个表上运行视图没有什么不同。

我的解决方案不需要大量索引、优化器提示或长时间的统计更新。

相反,我将视图转换为 User-Table-Function,其中参数是 WHERE 子句所需的值。事实上,这个 WHERE 子句嵌套了 3 个查询,它仍然有效,并且回到了 < 10ms speed.

最终我将参数更改为 TYPE,即 UtilAcctIds (int) 表。然后我可以将 WHERE 子句限制为表中的列表。 WHERE UtilAcctId = [参数列表].UtilAcctId。 这效果更好。我认为用户表函数是预编译的。


1
投票

当 SQL 开始使用变量优化查询的查询计划时,它将根据列匹配可用索引。在本例中,有一个索引,因此 SQL 认为它只会扫描索引来查找值。当 SQL 使用列和文字值制定查询计划时,它可以查看统计信息和值来决定是否应该扫描索引或查找是否正确。

使用优化提示和一个值告诉 SQL“这是大多数时间将使用的值,因此针对该值进行优化”,并且存储计划就像使用该文字值一样。使用优化提示和 UNKNOWN 的子提示告诉 SQL 您不知道该值是什么,因此 SQL 查看列的统计信息并决定查找或扫描最佳,并相应地制定计划。


1
投票

我知道这个问题早已得到解答,但我遇到了同样的问题,并且有一个相当简单的解决方案,不需要提示、统计更新、附加索引、强制计划等。

基于上面的评论“优化器无法知道该值不为空”,我决定将值从变量移到表中:

原始代码:

declare @StartTime datetime2(0) = '10/23/2020 00:00:00'
declare @EndTime datetime2(0) = '10/23/2020 01:00:00'
    
SELECT * FROM ...
WHERE 
C.CreateDtTm >= @StartTime
AND  C.CreateDtTm < @EndTime

新代码:

declare @StartTime datetime2(0) = '10/23/2020 00:00:00'
declare @EndTime datetime2(0) = '10/23/2020 01:00:00'

CREATE TABLE #Times (StartTime datetime2(0) NOT NULL, EndTime datetime2(0) NOT NULL)
INSERT INTO #Times(StartTime, EndTime) VALUES(@StartTime, @EndTime)

SELECT * FROM ...
WHERE 
C.CreateDtTm >= (SELECT MAX(StartTime) FROM #Times)
AND  C.CreateDtTm < (SELECT MAX(EndTime) FROM #Times)

这会立即执行,而不是原始代码的几分钟(显然您的结果可能会有所不同)。

我假设如果我将主表中的数据类型更改为 NOT NULL,它也会工作,但由于系统限制,我此时无法测试这一点。


0
投票

我也遇到了类似的问题,但需要加入。这对我有用。

在不使用case的情况下,使用@id可以为null的选项进行计数。这样,我就强制它永远不会为空。

declare @id bigint 
table.nameid = (SELECT CASE  WHEN @id IS not NULL THEN @id ELSE 0 END)

-1
投票

我自己也遇到了同样的问题,结果是缺少索引,涉及子查询结果的(左)连接。

select *
from foo A
left outer join (
  select x, count(*)
  from bar
  group by x
) B on A.x = B.x

为 bar.x 添加了名为 bar_x 的索引


-1
投票
DECLARE @id INT = 1

SELECT * FROM View1 WHERE ID = @id

这样做

DECLARE @sql varchar(max)

SET @sql = 'SELECT * FROM View1 WHERE ID =' + CAST(@id as varchar)

EXEC (@sql)

解决你的问题

© www.soinside.com 2019 - 2024. All rights reserved.