【问题标题】:Why is query with phone = N'1234' slower than phone = '1234'?为什么使用 phone = N'1234' 的查询比 phone = '1234' 慢?
【发布时间】:2019-06-29 15:45:41
【问题描述】:

我有一个字段是 varchar(20)

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

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

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

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

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

【问题讨论】:

  • 因为它需要进行隐式数据转换
  • 因为Phone 是varchar 而N'2164474477' 是nvarchar。

标签: sql sql-server query-performance


【解决方案1】:

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

NVARCHAR 转换为VARCHAR 是一种缩小转换。也就是说,NVARCHAR 可能比类似的VARCHAR 值具有更多信息。不可能用VARCHAR 输出来表示每个NVARCHAR 输入,因此从前者转换为后者可能丢失一些信息。但相反的演员是扩大转换。从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 开发人员可能会增强查询优化器,以查看类型优先规则导致每行转换导致表或索引扫描的情况,但相反的转换涉及常量数据并且可能只是索引搜索,在这种情况下,首先查看数据以查看它是否也安全。但是,我发现他们不太可能这样做。在我看来,相对于完成单个查询的评估的额外性能成本以及理解优化器正在做什么的复杂性(“为什么服务器不遵循记录在案的优先规则在这里?”)来证明这一点。

【讨论】:

    【解决方案2】:
     SELECT * FROM [dbo].[phone] WHERE phone = N'5554474477'
    

    被解释为

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

    防止索引使用

    【讨论】:

      【解决方案3】:

      因为nvarchardatatype precedencevarchar 高,所以它需要将列隐式转换为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';
      

      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 &gt; Expr1005 and phone2 &lt; Expr1006 上,因此从表面上看,它会排除'5554474477',但标志62 表示这确实匹配。

      【讨论】:

        猜你喜欢
        • 2017-09-28
        • 2012-08-22
        • 1970-01-01
        • 1970-01-01
        • 2012-07-26
        • 2010-12-28
        • 1970-01-01
        • 2015-01-30
        相关资源
        最近更新 更多