为什么 phone = N'1234' 的查询比 phone = '1234' 慢?

Why is query with phone = N'1234' slower than phone = '1234'?

我有一个字段是 varchar(20)

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

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

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

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

我猜测如果我将字段更改为 nvarchar,那么它将使用索引查找。

因为 nvarchardatatype precedencevarchar 高,所以它需要将列隐式转换为 nvarchar,这会阻止索引查找。

在某些归类下,它仍然可以使用搜索,只需将 cast 推入针对搜索匹配的行的剩余谓词(而不需要对整个 table 通过扫描)但大概你没有使用这样的排序规则。

整理对此的影响如下图所示。当使用 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';

下面是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 意味着它确实匹配。

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

被解释为

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

防止使用索引

其他答案已经解释了发生了什么;我们已经看到 NVARCHAR 的类型优先级高于 VARCHAR。我想解释 为什么 数据库必须将列的每一行都转换为 NVARCHAR,而不是将单个提供的值转换为 VARCHAR,即使第二个选项显然要快得多,无论是直观上还是经验上。另外,我想解释一下为什么性能影响会如此剧烈。

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

原则是 Sql 服务器在出现两个不匹配的类型时始终选择安全转换。这是同样古老的“正确性胜过性能”的口头禅。或者,套用 Benjamin Franklin,“愿意用基本的正确性换取一点性能的人既不配得正确性也不配得性能。”那么,类型优先规则旨在确保选择安全的转换。

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

这才是真正的关键:现在我们要进行此转换,我们必须对 table 中的每一行进行 转换。即使对于不匹配比较筛选器的行也是如此。此外,来自列的转换值不再与存储在索引中的值相同,因此 列上的任何索引现在对于此查询毫无价值 .

我认为您非常幸运对此查询进行了索引扫描,而不是完整的table扫描,这可能是因为有一个覆盖满足查询需要的索引(优化器可以选择将索引中的所有记录与table中的所有记录一样轻松地进行转换)。


您可以通过以更有利的方式显式解决类型不匹配来解决此查询的问题。当然,最好的方法是首先提供一个简单的 VARCHAR 并完全避免对 casting/conversion 的任何需要:

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 服务器开发人员可能会增强查询优化器,以查看类型优先规则导致每行转换的情况,从而导致 table 或索引扫描,但相反的转换涉及常量数据,可能只是索引查找,在这种情况下,首先查看数据以查看它是否也安全。但是,我发现他们不太可能会这样做。在我看来,相对于完成单个查询评估的额外性能成本以及理解优化器正在做什么的复杂性(“为什么服务器不遵循记录的优先规则在这里?”)来证明这一点。