Index Seek 根据参数值扫描整个表

SEa*_*986 3 performance sql-server execution-plan type-conversion query-performance

我有一个疑问:

SELECT  Id, 
        ColumnA,
        ColumnB
FROM    MyTable
WHERE   ColumnA = @varA OR
        ColumnB = @varB  
Run Code Online (Sandbox Code Playgroud)

该表定义为

CREATE TABLE MyTable
(
    Id INT IDENTITY(-2147483648,1) PRIMARY KEY,
    ColumnA VARCHAR(22)
    ColumnB VARCAHR(22)
)
Run Code Online (Sandbox Code Playgroud)

并且表上有一个非聚集索引

CREATE INDEX IX_MyIndex ON MyTable
(
    ColumnA
)
Run Code Online (Sandbox Code Playgroud)

当我使用以下参数运行查询时:

DECLARE @varA nvarchar(4000) = ''
DECLARE @varB  nvarchar(8) = '10140730'
Run Code Online (Sandbox Code Playgroud)

执行计划显示索引搜索IX_MyIndex,但它显示读取的行数为 1700 万行,但实际行数为 0(MyTable.ColumnA 中有 0 行,值为 '')如果我转动,SET STATISTICS IO ON我可以看到完整的表正在阅读

这是有道理的:“这是一个“糟糕的”索引搜索部分中的这篇文章

但是,当我使用参数运行相同的查询时:

DECLARE @varA nvarchar(8) = 'a'
DECLARE @varB  nvarchar(8) = '10140730'
Run Code Online (Sandbox Code Playgroud)

搜索运算符没有“读取的行数”属性(MyTable.ColumnA 中有 0 行,值为 'a')并SET STATISTICS IO报告单图逻辑读取

顺便说一句,该计划有一个隐式转换警告,当我像这样更改查询时,问题就消失了:

SELECT  Id, 
        ColumnA,
        ColumnB
FROM    MyTable
WHERE   ColumnA = CONVERT(VARCHAR(22),@varA) OR
        ColumnB = CONVERT(VARCHAR(22),@varB)
Run Code Online (Sandbox Code Playgroud)

或将基础列更改为 NVARCHAR

但是,我很好奇为什么索引查找的行为与两个不同的值@varA不同,即使它们都返回表中相同数量的记录 (0)

Pau*_*ite 8

当列的数据类型与变量的数据类型不匹配时,SQL Server 无法直接使用 B 树索引的查找能力来定位正确的值范围。

数据类型优先规则意味着必须将列数据转换为变量的数据类型时,这意味着扫描整个表或索引,转换每个值并将其作为剩余谓词针对变量进行测试。

这显然不是理想的,但很常见(不幸的是)以至于 SQL Server 有一个内置的方法来在这些情况下实现索引查找。它获取提供的值并计算它映射到的值的范围,考虑类型转换和排序规则。

此功能称为动态搜索,计算映射范围的内部方法称为GetRangeThroughConvert

例如,当nvarchar变量包含“a”时,数据类型值的映射范围varchar可能是“a”到“B”(确切范围取决于排序规则)。这意味着 SQL Server 可以查找varchar'a' 和 'B' 之间的索引,只测试与 'a' (as nvarchar) 作为剩余谓词的匹配是否相等。

当提供的值为空字符串时,计算范围是无限的,因此整个索引被有效地扫描。

例如:

DROP TABLE IF EXISTS dbo.MyTable;
GO
CREATE TABLE dbo.MyTable
(
    ColumnA varchar(22) COLLATE Latin1_General_CI_AS NOT NULL
);
GO
INSERT dbo.MyTable 
    WITH (TABLOCKX)
    (ColumnA)
SELECT TOP (1000)
    REPLICATE(CHAR(65 + ROW_NUMBER() OVER (ORDER BY @@SPID) % 26), 22)
FROM master.sys.all_columns AS AC1
CROSS JOIN master.sys.all_columns AS AC2;
GO
CREATE INDEX IX_MyIndex_A ON dbo.MyTable(ColumnA);
Run Code Online (Sandbox Code Playgroud)

以下查询使用动态搜索,范围为“a”到“B”,剩余谓词为CONVERT_IMPLICIT(nvarchar(22),[dbo].[MyTable].[ColumnA],0)=[@varA]

DECLARE @varA nvarchar(22) = N'a';

SELECT MT.ColumnA 
FROM dbo.MyTable AS MT 
WHERE MT.ColumnA = @varA;
Run Code Online (Sandbox Code Playgroud)

执行计划示出了动态寻求与由寻求合格38行的形状,但所有最终由残余拒绝:

动态搜索计划

38 行是查询计数的行:

SELECT COUNT_BIG(*)
FROM dbo.MyTable AS MT 
WHERE MT.ColumnA > 'a' 
AND MT.ColumnA < 'B';
Run Code Online (Sandbox Code Playgroud)

当变量包含空字符串时,计算的范围是无界的,因此搜索有效地扫描整个索引:

DECLARE @varA nvarchar(22) = N''; -- empty string

SELECT MT.ColumnA 
FROM dbo.MyTable AS MT 
WHERE MT.ColumnA = @varA;
Run Code Online (Sandbox Code Playgroud)

执行计划显示了所有1000行从索引被读取(但同样,由残留丢弃):

求整桌

空字符串是GetRangeThroughConvert无法产生有用范围的特殊情况。单个空格字符确实会产生狭窄的搜索范围(plan)。

无论如何,消息是要仔细注意数据类型。

数据库<>小提琴