为啥当 WHERE 子句包含参数化值时 SQL Server 使用索引扫描而不是索引查找

Posted

技术标签:

【中文标题】为啥当 WHERE 子句包含参数化值时 SQL Server 使用索引扫描而不是索引查找【英文标题】:Why is SQL Server using index scan instead of index seek when WHERE clause contains parameterized values为什么当 WHERE 子句包含参数化值时 SQL Server 使用索引扫描而不是索引查找 【发布时间】:2014-12-19 11:12:16 【问题描述】:

如果where 子句包含参数化值而不是字符串文字,我们发现 SQL Server 正在使用索引扫描而不是索引查找。

以下是一个例子:

SQL Server 在以下情况下执行索引扫描(where 子句中的参数)

declare @val1 nvarchar(40), @val2 nvarchar(40);
set @val1 = 'val1';
set @val2 = 'val2';

select 
    min(id) 
from 
    scor_inv_binaries 
where 
    col1 in (@val1, @val2) 
group by 
    col1

另一方面,以下查询执行索引查找:

select 
    min(id) 
from 
    scor_inv_binaries 
where 
    col1 in ('val1', 'val2') 
group by 
    col1

有没有人观察到类似的行为,他们如何解决这个问题以确保查询执行索引查找而不是索引扫描?

我们无法使用forceseek 表提示,因为SQL Sserver 2005 支持forceseek。

我也更新了统计数据。 非常感谢您的帮助。

【问题讨论】:

如果您在 2005 年,请尝试使用 INDEX 提示 尝试将 OPTION (RECOMPILE) 添加到您的 SELECT 中 查询优化器可能没有考虑您的特定参数值,因此创建了最坏情况的查询执行计划。尝试使用 OPTIMIZE FOR @val1 = <value> 查询提示 - msdn.microsoft.com/en-us/library/ms181714.aspx。 【参考方案1】:

好吧,回答你的问题,为什么 SQL Server 会这样做,答案是查询不是按逻辑顺序编译的,每条语句都是根据自己的优点编译的, 因此,当您的 select 语句的查询计划正在生成时,优化器不知道 @val1 和 @Val2 将分别变为 'val1' 和 'val2'。

当 SQL Server 不知道该值时,它必须对该变量将在表中出现的次数做出最佳猜测,这有时会导致次优计划。我的主要观点是具有不同值的相同查询可以生成不同的计划。想象一下这个简单的例子:

IF OBJECT_ID(N'tempdb..#T', 'U') IS NOT NULL
    DROP TABLE #T;

CREATE TABLE #T (ID INT IDENTITY PRIMARY KEY, Val INT NOT NULL, Filler CHAR(1000) NULL);
INSERT #T (Val)
SELECT  TOP 991 1
FROM    sys.all_objects a
UNION ALL
SELECT  TOP 9 ROW_NUMBER() OVER(ORDER BY a.object_id) + 1
FROM    sys.all_objects a;

CREATE NONCLUSTERED INDEX IX_T__Val ON #T (Val);

我在这里所做的只是创建一个简单的表,并为列val 添加值 1-10 的 1000 行,但是其中 1 出现了 991 次,而其他 9 只出现了一次。前提是这个查询:

SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = 1;

只扫描整个表会比使用索引进行查找更有效,然后进行 991 次书签查找以获取 Filler 的值,但是以下查询只有 1 行:

SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = 2;

进行索引查找和单个书签查找以获取 Filler 的值会更有效(运行这两个查询将批准这一点)

我很确定搜索和书签查找的截止时间实际上会因情况而异,但它相当低。使用示例表,经过一些试验和错误,我发现在优化器对索引查找和书签查找进行全表扫描之前,我需要 Val 列有 38 行,值为 2:

IF OBJECT_ID(N'tempdb..#T', 'U') IS NOT NULL
    DROP TABLE #T;

DECLARE @I INT = 38;

CREATE TABLE #T (ID INT IDENTITY PRIMARY KEY, Val INT NOT NULL, Filler CHAR(1000) NULL);
INSERT #T (Val)
SELECT  TOP (991 - @i) 1
FROM    sys.all_objects a
UNION ALL
SELECT  TOP (@i) 2
FROM    sys.all_objects a
UNION ALL
SELECT  TOP 8 ROW_NUMBER() OVER(ORDER BY a.object_id) + 2
FROM    sys.all_objects a;

CREATE NONCLUSTERED INDEX IX_T__Val ON #T (Val);

SELECT  COUNT(Filler), COUNT(*)
FROM    #T
WHERE   Val = 2;

因此,对于此示例,限制为 3.7% 的匹配行。

由于查询不知道在使用变量时将匹配多少行,因此它必须猜测,最简单的方法是找出总行数,然后将其除以中不同值的总数列,因此在此示例中,WHERE val = @Val 的估计行数为 1000 / 10 = 100,实际算法比这更复杂,但例如,这样做可以。因此,当我们查看执行计划时:

DECLARE @i INT = 2;
SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = @i;

我们可以在此处(使用原始数据)看到估计的行数为 100,但实际行数为 1。从前面的步骤中我们知道,如果行数超过 38,优化器将选择聚集索引扫描通过索引查找,因此由于对行数的最佳猜测高于此,因此对未知变量的计划是聚集索引扫描。

为了进一步证明这个理论,如果我们创建一个包含 1000 行数字 1-27 均匀分布的表(因此估计的行数大约为 1000 / 27 = 37.037)

IF OBJECT_ID(N'tempdb..#T', 'U') IS NOT NULL
    DROP TABLE #T;

CREATE TABLE #T (ID INT IDENTITY PRIMARY KEY, Val INT NOT NULL, Filler CHAR(1000) NULL);
INSERT #T (Val)
SELECT  TOP 27 ROW_NUMBER() OVER(ORDER BY a.object_id)
FROM    sys.all_objects a;

INSERT #T (val)
SELECT  TOP 973 t1.Val
FROM    #T AS t1
        CROSS JOIN #T AS t2
        CROSS JOIN #T AS t3
ORDER BY t2.Val, t3.Val;

CREATE NONCLUSTERED INDEX IX_T__Val ON #T (Val);

然后再次运行查询,我们得到一个带有索引搜索的计划:

DECLARE @i INT = 2;
SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = @i;

因此,希望这非常全面地涵盖了您获得该计划的原因。现在我想下一个问题是如何强制执行不同的计划,答案是使用查询提示OPTION (RECOMPILE),在参数值已知的情况下强制查询在执行时进行编译。恢复到原始数据,其中Val = 2 的最佳计划是查找,但使用变量会产生带有索引扫描的计划,我们可以运行:

DECLARE @i INT = 2;
SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = @i;

GO

DECLARE @i INT = 2;
SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = @i
OPTION (RECOMPILE);

我们可以看到后者使用了索引查找和键查找,因为它在执行时检查了变量的值,并为该特定值选择了最合适的计划。 OPTION (RECOMPILE) 的问题在于,您无法利用缓存的查询计划,因此每次编译查询都会产生额外的成本。

【讨论】:

我有一些非常相似的东西,添加 OPTION (RECOMPILE) 并没有导致显示的估计执行计划发生变化(仍然是表扫描),但实际上,使用该 OPTION 运行它可以我在使用文字值进行比较时得到的近乎即时的结果。 +1【参考方案2】:

试试

declare @val1 nvarchar(40), @val2 nvarchar(40);
set @val1 = 'val1';
set @val2 = 'val2';

select 
    min(id) 
from 
    scor_inv_binaries 
where 
    col1 in (@val1, @val2) 
group by 
    col1
OPTION (RECOMPILE)

【讨论】:

【参考方案3】:

col1 是什么数据类型?

您的变量是 nvarchar 而您的文字是 varchar/char;如果 col1 是 varchar/char,它可能正在执行索引扫描以将 col1 中的每个值隐式转换为 nvarchar 以进行比较。

【讨论】:

【参考方案4】:

我猜第一个查询使用谓词,第二个查询使用搜索谓词。

Seek Predicate 是描述 Seek 的 b-tree 部分的操作。谓词是使用非键列描述附加过滤器的操作。根据描述,很明显 Seek Predicate 比 Predicate 更好,因为它搜索索引,而在 Predicate 中,搜索是在非键列上 - 这意味着搜索是在页面文件本身的数据上。

更多详情请访问:- https://social.msdn.microsoft.com/Forums/sqlserver/en-US/36a176c8-005e-4a7d-afc2-68071f33987a/predicate-and-seek-predicate

【讨论】:

【参考方案5】:

我遇到了这个确切的问题,并且查询选项解决方案似乎都没有任何效果。

原来我声明了一个 nvarchar(8) 作为参数,而表中有一个 varchar(8) 列。

更改参数类型后,查询会进行索引搜索并立即运行。一定是优化器被转换搞砸了。

在这种情况下,这可能不是答案,但值得检查。

【讨论】:

以上是关于为啥当 WHERE 子句包含参数化值时 SQL Server 使用索引扫描而不是索引查找的主要内容,如果未能解决你的问题,请参考以下文章

为啥聚集函数不能出现在where子句中

希望 Where 子句中的“存在”仅在传递的参数有值时使用

在 where 子句 SQL 中的 case 语句中使用参数

SQLite插入 - 如何使用参数化值避免语法错误时插入任何文本

在 SPARK SQL 中参数化 Where 子句

SQL中的WHERE子句中为啥不允许应用聚集函数呢?请通俗的解释一下或者谈谈自己的见解!