为啥 SQL Server 不使用计算列上的索引?

Posted

技术标签:

【中文标题】为啥 SQL Server 不使用计算列上的索引?【英文标题】:Why doesn't SQL Server use the index on the computed column?为什么 SQL Server 不使用计算列上的索引? 【发布时间】:2017-08-07 04:52:52 【问题描述】:

鉴于 SQL Server 2014 数据库中的以下内容:

create table t 
(
    c1 int primary key,
    c2 datetime2(7),
    c3 nvarchar(20),
    c4 as cast(dbo.toTimeZone(c2, c3, 'UTC') as date) persisted
);

create index i on t (c4);

declare @i int = 0;

while @i < 10000 
begin
    insert into t (c1, c2, c3) values
        (@i, dateadd(day, @i, '1970-01-02 03:04:05:6'), 'Asia/Manila');
    set @i = @i + 1;
end;

toTimeZone 是一个 CLR UDF,它将一个时区的 datetime2 转换为另一个时区的 datetime2

当我运行以下查询时:

select c1 
from t 
where c4 >= '1970-01-02'
    and c4 <= '1970-03-04';

SQL Server 后面的执行计划表明i 没有被使用。

取而代之的是扫描 PK 上的隐式索引,然后进行几个标量计算,然后最后使用查询的谓词进行过滤。我期待的执行计划是扫描i

使用this ZIP file 中的 SSDT 项目尝试复制问题。它包括 CLR UDF 的模拟定义。还包括我得到的执行计划。

【问题讨论】:

以下是索引计算列的最终要求列表:msdn.microsoft.com/en-us/library/ms189292.aspx 检查您的情况;很可能,您需要将计算列声明为persisted 只需在我的实例(2014,x64 开发版)上运行,我就会看到索引搜索。因此,您需要进一步完善重现此问题的确切方法。 奇怪。我再次运行查询,这次在i 上有一个索引搜索。 但现在的问题是我们没有to_time_zone,所以其他人能够在他们自己的系统上重现它的机会现在为零。 @AndrewO'Brien - 提供的项目至少我的 2014 版本根本无法匹配它,即使有提示 i.stack.imgur.com/U1Dyr.png 【参考方案1】:

我能够使用您附加的项目重现该问题(这可能与带有连接项 here 的 here 相同)

计算列首先扩展为基础表达式,然后可能会或可能不匹配回计算列。

您的计划中的过滤器显示它已扩展到

CONVERT(date,[computed-column-index-problem].[dbo].[toTimeZone](CONVERT_IMPLICIT(datetime,[computed-column-index-problem].[dbo].[t].[c2],0),CONVERT_IMPLICIT(nvarchar(max),[computed-column-index-problem].[dbo].[t].[c3],0),CONVERT_IMPLICIT(nvarchar(max),'UTC',0)),0)>=CONVERT_IMPLICIT(date,[@1],0) 
AND 
CONVERT(date,[computed-column-index-problem].[dbo].[toTimeZone](CONVERT_IMPLICIT(datetime,[computed-column-index-problem].[dbo].[t].[c2],0),CONVERT_IMPLICIT(nvarchar(max),[computed-column-index-problem].[dbo].[t].[c3],0),CONVERT_IMPLICIT(nvarchar(max),'UTC',0)),0)<=CONVERT_IMPLICIT(date,[@2],0)

这些对nvarchar(max) 的隐式转换似乎正在造成损害。一个不需要 CLR 的简单重现是

DROP TABLE IF EXISTS t 
DROP FUNCTION IF EXISTS [dbo].[toTimeZone]

GO

CREATE FUNCTION [dbo].[toTimeZone] (@newTimeZone [NVARCHAR](max))
RETURNS DATE
WITH schemabinding
AS
  BEGIN
      RETURN DATEFROMPARTS(1970, 01, 02)
  END

GO

CREATE TABLE t
  (
     c1 INT IDENTITY PRIMARY KEY,
     c4 AS dbo.toTimeZone(N'UTC') persisted
  );

CREATE INDEX i
  ON t (c4);

INSERT INTO t
DEFAULT VALUES

SELECT c1
FROM   t WITH (forceseek)
WHERE  c4 >= '1970-01-02'
       AND c4 <= '1970-03-04'; 

消息 8622,级别 16,状态 1,行 27 查询处理器无法生成 由于此查询中定义的提示而导致的查询计划。重新提交 不指定任何提示且不使用 SET FORCEPLAN 的查询。

如果我将函数定义更改为

public static DateTime toTimeZone(DateTime dateTime,
    [SqlFacet(IsFixedLength=false, IsNullable=true, MaxSize=50)]
    string originalTimeZone,
    [SqlFacet(IsFixedLength=false, IsNullable=true, MaxSize=50)]
    string newTimeZone)

    return dateTime.AddHours(-8);

所以字符串参数变成nvarchar(50)。然后它能够​​匹配并给出一个搜索

具体来说,第二个参数需要传递文字UTC。如果注释仅应用于第一个参数,那么即使有with (forceseek) 提示,该计划也不会产生搜索。如果仅将注释应用于第二个参数,则它可以产生搜索 - 尽管计划显示警告。

【讨论】:

哇。您不仅设法重现了问题,而且深入了解了问题并找到了解决方案。 @Vladimir Baranov 对此有什么解决方案或修复或解决方法。 我创建了基于标识列的 PK 计算的持久列为 ('P'+ RIGHT('000000000'+CONVERT([VARCHAR](8),[ID],(0)),( 7)))坚持。我以同样的方式创建了两个表,一个持久列。当我尝试在内部连接上加入两个持久列时,它没有选择索引并显示警告“类型转换表达式 convert(varchar(8),id,0) 可能会影响 Microsoft sql server 2016 -sp1cu4 版本中的基数估计"

以上是关于为啥 SQL Server 不使用计算列上的索引?的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server 2008 在具有空间索引的可空地理列上的性能

常用的SQL优化技巧

sqlserver 在数据查询时是按时间顺序排列的 在时间字段上还有必要加聚集索引吗 为啥

SQL Server 中索引的排序规则

SP 执行时 SQL Server 2008 中的 QUOTED IDENTIFIER 错误

在 SQL Server 2005 中恢复 SQL Server 2014 数据库