持久计算列上的索引需要键查找才能获取计算表达式中的列


24

我在表上有一个持久的计算列,该表只是由串联的列组成,例如

CREATE TABLE dbo.T 
(   
    ID INT IDENTITY(1, 1) NOT NULL CONSTRAINT PK_T_ID PRIMARY KEY,
    A VARCHAR(20) NOT NULL,
    B VARCHAR(20) NOT NULL,
    C VARCHAR(20) NOT NULL,
    D DATE NULL,
    E VARCHAR(20) NULL,
    Comp AS A + '-' + B + '-' + C PERSISTED NOT NULL 
);

在这Comp不是唯一的,并且D是的每个组合的有效起始日期A, B, C,因此我使用以下查询来获取每个的结束日期A, B, C(基本上是Comp的相同值的下一个开始日期):

SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 t2.D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY t2.D
            )
FROM    dbo.T t1
WHERE   t1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY t1.Comp;

然后,我在计算列中添加了一个索引来协助此查询(以及其他查询):

CREATE NONCLUSTERED INDEX IX_T_Comp_D ON dbo.T (Comp, D) WHERE D IS NOT NULL;

查询计划使我感到惊讶。我本以为,因为我有一个where子句指出了这一点,D IS NOT NULL并且我正在按进行排序Comp,并且没有引用索引之外的任何列,所以可以使用计算列上的索引来扫描t1和t2,但是我看到了聚集索引扫描。

在此处输入图片说明

因此,我强制使用此索引来查看它是否产生了更好的计划:

SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 t2.D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY t2.D
            )
FROM    dbo.T t1 WITH (INDEX (IX_T_Comp_D))
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;

哪个给了这个计划

在此处输入图片说明

这表明正在使用密钥查找,其详细信息是:

在此处输入图片说明

现在,根据SQL-Server文档:

如果该列在CREATE TABLE或ALTER TABLE语句中标记为PERSISTED,则可以在使用确定性表达式但不精确的表达式定义的计算列上创建索引。这意味着数据库引擎将计算出的值存储在表中,并在更新计算出的列所依赖的任何其他列时更新它们。当数据库引擎在列上创建索引以及在查询中引用该索引时,数据库引擎将使用这些持久值。当数据库引擎无法准确证明返回计算列表达式的函数(特别是在.NET Framework中创建的CLR函数)是否既确定又精确时,使用此选项可以在计算列上创建索引。

因此,如果像文档所说的那样“数据库引擎将计算出的值存储在表中”,并且该值也存储在我的索引中,为什么在不引用A,B和C时需要进行键查找来获取A,B和C根本没有查询?我认为它们被用来计算Comp,但是为什么呢?另外,为什么查询可以在上使用索引t2,但不能在上使用索引t1

SQL Fiddle上的查询和DDL

注意:我已经标记了SQL Server 2008,因为这是我的主要问题所在的版本,但我在2012年也得到了相同的行为。

Answers:


20

当根本没有在查询中引用A,B和C时,为什么需要进行键查找?我认为它们被用来计算Comp,但是为什么呢?

列在查询计划中A, B, and C 引用-seek on使用它们T2

另外,为什么查询可以在t2上使用索引,而不在t1上使用索引?

优化程序决定,扫描聚簇索引比扫描过滤后的非聚簇索引然后执行查找以检索列A,B和C的值便宜。

说明

真正的问题是,为什么优化器觉得根本需要检索A,B和C才能进行索引查找。我们希望它Comp使用非聚集索引扫描读取该列,然后在同一索引(别名T2)上执行查找以找到前1个记录。

查询优化器在优化开始之前扩展计算的列引用,从而使它有机会评估各种查询计划的成本。对于某些查询,扩展计算列的定义可使优化器找到更有效的计划。

当优化器遇到相关的子查询时,它会尝试将其“展开”为更易于推理的形式。如果找不到更有效的简化,则将其重写为一个应用(相关联)的相关子查询:

应用重写

碰巧的是,此应用程序展开将逻辑查询树置于一种无法与项目规范化完美配合的形式中(在后期,该表达式看起来将通用表达式与计算列匹配)。

在您的情况下,查询的编写方式会与优化程序的内部详细信息交互,从而使扩展表达式定义不与计算列匹配,最终导致查找引用列A, B, and C而不是计算列Comp。这是根本原因。

解决方法

解决此副作用的一种方法是手动将查询编写为应用:

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
CROSS APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

不幸的是,此查询将不会使用我们希望的过滤索引。D对应用程序内部列的不等式测试将拒绝NULLs,因此显然多余的谓词WHERE T1.D IS NOT NULL被优化掉了。

没有该显式谓词,过滤索引匹配逻辑将决定它不能使用过滤索引。有很多方法可以解决第二个副作用,但是最简单的方法可能是将交叉应用更改为外部应用(反映先前在相关子查询上执行的优化程序的重写逻辑):

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
OUTER APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

现在,优化器本身不需要使用apply重写(因此,计算出的列匹配按预期工作),并且谓词也没有被优化,因此过滤后的索引可用于两个数据访问操作,而seek使用该Comp列双方:

外申请计划

通常,这比INCLUDEd在过滤索引中添加A,B和C作为列更为可取,因为它解决了问题的根本原因,并且不需要不必要地扩展索引。

持久计算列

附带说明一下PERSISTED,如果您不介意在CHECK约束中重复其定义,则无需将计算列标记为。

CREATE TABLE dbo.T 
(   
    ID integer IDENTITY(1, 1) NOT NULL,
    A varchar(20) NOT NULL,
    B varchar(20) NOT NULL,
    C varchar(20) NOT NULL,
    D date NULL,
    E varchar(20) NULL,
    Comp AS A + '-' + B + '-' + C,

    CONSTRAINT CK_T_Comp_NotNull
        CHECK (A + '-' + B + '-' + C IS NOT NULL),

    CONSTRAINT PK_T_ID 
        PRIMARY KEY (ID)
);

CREATE NONCLUSTERED INDEX IX_T_Comp_D
ON dbo.T (Comp, D) 
WHERE D IS NOT NULL;

PERSISTED在这种情况下,仅当您要使用NOT NULL约束或在约束中Comp直接引用该列(而不是重复其定义)时,才需要使用计算列CHECK


2
+1顺便说一句我在寻找可能(或可能没有)感兴趣的同时遇到了另一种多余的查找情况。SQL小提琴
马丁·史密斯

@MartinSmith是的,这很有趣。另一个通用规则重写(FOJNtoLSJNandLASJN)导致事情无法正常运行,并留下了垃圾(BaseRow / Checksums),在某些类型的计划(例如游标)中有用,但此处不需要。
保罗·怀特说GoFundMonica

Chk是校验和!谢谢,我不确定。最初,我认为这可能与检查约束有关。
马丁·史密斯

6

尽管由于测试数据的人为性质,这可能有点巧合,但是正如您提到的SQL 2012一样,我尝试重写:

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;

这样就可以使用您的索引生成一个不错的低成本计划,并且读取次数明显少于其他选项(并且测试数据的结果相同)。

Plan Explorer的四个选项的费用:原始; 带提示的原件; 外用和铅

我怀疑您的实际数据会更复杂,因此在某些情况下此查询的行为在语义上可能会与您的不同,但是它确实表明有时新功能可以带来真正的改变。

我做了一些不同数据的实验,发现一些场景可以匹配,而另一些则不匹配:

--Example 1: results matched
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn + b.rn, '1 Jan 2013')
FROM cte a
    CROSS JOIN cte b
WHERE a.rn % 3 = 0
 AND b.rn % 5 = 0
ORDER BY 1, 2, 3
GO


-- Original query
SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY D
            )
INTO #tmp1
FROM    dbo.T t1 
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;
GO

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
INTO #tmp2
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;
GO


-- Checks ...
SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1


Example 2: results did not match
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn, '1 Jan 2013')
FROM cte a

-- Add some more data
INSERT dbo.T (A, B, C, D)
SELECT A, B, C, D 
FROM dbo.T
WHERE DAY(D) In ( 3, 7, 9 )


INSERT dbo.T (A, B, C, D)
SELECT A, B, C, DATEADD( day, 1, D )
FROM dbo.T
WHERE DAY(D) In ( 12, 13, 17 )


SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1

SELECT * FROM #tmp2
INTERSECT
SELECT * FROM #tmp1


select * from #tmp1
where comp = 'A2-B2-C2'

select * from #tmp2
where comp = 'A2-B2-C2'

1
好吧,它只使用索引,但只限于一点。如果comp不是计算列,则看不到排序。
马丁·史密斯,

谢谢。我的实际情况并没有复杂得多,并且该LEAD功能完全按照我在2012 Express的本地实例上的预期运行。不幸的是,这给我带来的不便还不是升级生产服务器的充分理由……
GarethD 2013年

-1

当我尝试执行相同的操作时,得到了另一个结果。首先,我没有索引的表的执行计划如下:在此处输入图片说明

正如我们从聚簇索引扫描(t2)中所看到的,谓词用于确定要返回的所需行(由于条件):

在此处输入图片说明

添加索引时,无论是由WITH运算符定义还是不定义索引,执行计划如下:

在此处输入图片说明

如我们所见,群集索引扫描被索引扫描取代。如上所述,SQL Server使用计算列的源列来执行嵌套查询的匹配。在聚簇索引扫描期间,可以同时获取所有这些值(无需其他操作)。添加索引后,正在根据索引对表中的必要行进行过滤(在主选择中),但是comp仍需要获取计算列的源列的值(最后一个操作嵌套循环) 。

在此处输入图片说明

因此,使用了“关键字查找”操作-获取所计算的源列的数据。

PS看起来像SQL Server中的错误。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.