为什么查询电话= N'1234'慢于电话='1234'?

问题描述 投票:45回答:3

我有一个varchar字段(20)

执行此查询时,它很快(使用索引查找):

SELECT * FROM [dbo].[phone] WHERE phone = '5554474477'

但这个很慢(使用索引扫描)。

SELECT * FROM [dbo].[phone] WHERE phone = N'5554474477'

我猜测如果我将字段更改为nvarchar,那么它将使用Index Seek。

sql sql-server query-performance
3个回答
57
投票

因为nvarchardatatype precedence具有更高的varchar所以它需要对nvarchar执行列的隐式转换,这可以防止索引搜索。

在某些排序规则下,它仍然可以使用搜索,只需将cast推入与搜索匹配的行相对应的残差谓词(而不是需要通过扫描对整个表中的每一行执行此操作),但可能您不是使用这样的整理。

整理对此的影响如下所示。使用SQL排序规则时,您会进行扫描,对于Windows排序规则,它会调用内部函数GetRangeThroughConvert并将其转换为搜索。

CREATE TABLE [dbo].[phone]
  (
     phone1 VARCHAR(500) COLLATE sql_latin1_general_cp1_ci_as CONSTRAINT uq1 UNIQUE,
     phone2 VARCHAR(500) COLLATE latin1_general_ci_as CONSTRAINT uq2 UNIQUE,
  );

SELECT phone1 FROM [dbo].[phone] WHERE phone1 = N'5554474477';
SELECT phone2 FROM [dbo].[phone] WHERE phone2 = N'5554474477';

enter image description here

SHOWPLAN_TEXT在下面

查询1

  |--Index Scan(OBJECT:([tempdb].[dbo].[phone].[uq1]),  WHERE:(CONVERT_IMPLICIT(nvarchar(500),[tempdb].[dbo].[phone].[phone1],0)=CONVERT_IMPLICIT(nvarchar(4000),[@1],0)))

查询2

  |--Nested Loops(Inner Join, OUTER REFERENCES:([Expr1005], [Expr1006], [Expr1004]))
       |--Compute Scalar(DEFINE:(([Expr1005],[Expr1006],[Expr1004])=GetRangeThroughConvert([@1],[@1],(62))))
       |    |--Constant Scan
       |--Index Seek(OBJECT:([tempdb].[dbo].[phone].[uq2]), SEEK:([tempdb].[dbo].[phone].[phone2] > [Expr1005] AND [tempdb].[dbo].[phone].[phone2] < [Expr1006]),  WHERE:(CONVERT_IMPLICIT(nvarchar(500),[tempdb].[dbo].[phone].[phone2],0)=[@1]) ORDERED FORWARD)

在第二种情况下,计算标量emits the following values

Expr1004 = 62
Expr1005 = '5554474477'
Expr1006 = '5554474478'

计划中显示的搜索谓词是在phone2 > Expr1005 and phone2 < Expr1006所以在它的表面将排除'5554474477',但标志62意味着这匹配。


37
投票

其他答案已经解释了会发生什么我们已经看到NVARCHARVARCHAR具有更高的类型优先级。我想解释为什么数据库必须将列的每一行都转换为NVARCHAR,而不是将单个提供的值转换为VARCHAR,即使第二个选项显然要快得多,直观和经验。

NVARCHARVARCHAR的铸造是一个缩小的转换。也就是说,NVARCHAR可能比类似的VARCHAR值更多的信息。用NVARCHAR输出表示每个VARCHAR输入是不可能的,因此从前者到后者的输入可能会丢失一些信息。但相反的演员阵容是一个扩大的转换。从VARCHAR值转换为NVARCHAR值永远不会丢失信息;它是安全的。

这个想法是在提供两种不匹配的类型进行比较时,Sql Server应该总是选择安全转换。这是旧的“正确性胜过表现”的口头禅。或者,用Benjamin Franklin的话来说,“那些为了一点点的表现而交易本质正确性的人,既不能正确也不能表现。”因此,类型优先规则旨在确保选择安全转换。

现在你和我都知道你的缩小转换对于这个特定数据也是安全的,但是Sql Server查询优化器并不关心这一点。无论好坏,它在构建执行计划时遵循数据类型信息并遵循类型优先规则。

这是真正的踢球者:现在我们正在进行这个演员,我们必须为表格中的每一行做这个。即使对于不会与比较滤波器匹配的行也是如此。更重要的是,列中的强制转换值不再与存储在索引中的值相同,因此列上的任何索引现在对于此查询都毫无价值。

我认为你很幸运能够获得这个查询的索引扫描,而不是全表扫描,这可能是因为有一个覆盖索引可以满足查询的需要(优化器可以选择转换所有记录在索引中就像表中的所有记录一样容易)。

您可以通过以更有利的方式明确解决类型不匹配来修复此查询的内容。当然,实现这一目标的最佳方法是首先提供简单的VARCHAR,并且完全不需要进行铸造/转换:

SELECT * FROM [dbo].[phone] WHERE phone = '5554474477'

但我怀疑我们看到的是应用程序提供的值,您不一定控制文字的那一部分。如果是这样,你仍然可以这样做:

SELECT * FROM [dbo].[phone] WHERE phone = cast(N'5554474477' as varchar(20))

这两个示例都有利地解决了原始代码的类型不匹配问题。即使在后一种情况下,您可能对文字的控制力比您所知。例如,如果此查询是从.Net程序创建的,则问题可能与AddWithValue()函数有关。 I've written about this issue in the past以及如何正确处理它。

这些修复也有助于证明为什么会这样。

在将来的某个时候,Sql Server开发人员可能会增强查询优化器,以查看类型优先规则导致每行转换导致表或索引扫描的情况,但相反的转换涉及常量数据并且可能只是一个索引搜索,在这种情况下,首先查看数据,看它是否也是安全的。

但是,我发现他们不可能做到这一点。在我看来,相对于完成单个查询评估的额外性能成本以及理解优化器正在做什么的复杂性,现有系统中查询的更正太容易了(“为什么服务器不遵循记录的优先级规则在这里?“)为它辩护。


7
投票
 SELECT * FROM [dbo].[phone] WHERE phone = N'5554474477'

被解释为

 SELECT * from [dbo].[phone] WHERE CAST(phone as NVARCHAR) = N'5554474477'

这会阻止索引使用

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