从不同表中使用ORDER BY选择TOP 1时如何设置索引视图


11

我正在努力在以下情况下设置索引视图,以便在执行以下查询时不会进行两次聚集索引扫描。每当我为该查询创建索引视图然后使用它时,它似乎都会忽略我在其上放置的任何索引:

    -- +++ THE QUERY THAT I WANT TO IMPROVE PERFORMANCE-WISE +++

    SELECT TOP 1 *
    FROM    dbo.TB_test1 t1
            INNER JOIN dbo.TB_test2 t2 ON t1.PK_ID1 = t2.FK_ID1
    ORDER BY t1.somethingelse1
           ,t2.somethingelse2;


    GO

表格设置如下:

  • 两张桌子
  • 它们由上面的查询通过内部联接联接
  • 并按上面的查询从第一个表开始的列排序,然后从第二个表开始的列排序;仅选择TOP 1
  • (在下面的脚本中,还有一些行可以生成测试数据,以防万一有助于重现问题)

    -- +++ TABLE SETUP +++
    
    CREATE TABLE [dbo].[TB_test1]
        (
         [PK_ID1] [INT] IDENTITY(1, 1)  NOT NULL
        ,[something1] VARCHAR(40) NOT NULL
        ,[somethingelse1] BIGINT NOT NULL
            CONSTRAINT [PK_TB_test1] PRIMARY KEY CLUSTERED ( [PK_ID1] ASC )
        );
    
    GO
    
    create TABLE [dbo].[TB_test2]
        (
         [PK_ID2] [INT] IDENTITY(1, 1)  NOT NULL
        ,[FK_ID1] [INT] NOT NULL
        ,[something2] VARCHAR(40) NOT NULL
        ,[somethingelse2] BIGINT NOT NULL
            CONSTRAINT [PK_TB_test2] PRIMARY KEY CLUSTERED ( [PK_ID2] ASC )
        );
    
    GO
    
    ALTER TABLE [dbo].[TB_test2]  WITH CHECK ADD  CONSTRAINT [FK_TB_Test1] FOREIGN KEY([FK_ID1])
    REFERENCES [dbo].[TB_test1] ([PK_ID1])
    GO
    
    ALTER TABLE [dbo].[TB_test2] CHECK CONSTRAINT [FK_TB_Test1]
    
    GO
    
    
    -- +++ TABLE DATA GENERATION +++
    
    -- this might not be the quickest way, but it's only to set up test data
    
    INSERT INTO dbo.TB_test1
            ( something1, somethingelse1 )
    VALUES  ( CONVERT(VARCHAR(40), NEWID())  -- something1 - varchar(40)
              ,ISNULL(ABS(CHECKSUM(NewId())) % 92233720368547758078, 1)   -- somethingelse1 - bigint
              )
    
    GO 100000
    
    RAISERROR( 'Finished setting up dbo.TB_test1', 0, 1) WITH NOWAIT    
    
    GO    
    
    INSERT INTO dbo.TB_test2
            ( FK_ID1, something2, somethingelse2 )
    VALUES  ( ISNULL(ABS(CHECKSUM(NewId())) % ((SELECT MAX(PK_ID1) FROM dbo.TB_test1) - 1), 0) + 1 -- FK_ID1 - int
              ,CONVERT(VARCHAR(40), NEWID())  -- something2 - varchar(40)
              ,ISNULL(ABS(CHECKSUM(NewId())) % 92233720368547758078, 1)   -- somethingelse2 - bigint
              )
    
    GO 100000
    
    RAISERROR( 'Finished setting up dbo.TB_test2', 0, 1) WITH NOWAIT          
    
    GO

索引视图可能应定义如下,并且生成的TOP 1查询如下。但是,我需要什么索引才能使此查询比没有索引视图时更好地执行?

    CREATE VIEW VI_test
    WITH SCHEMABINDING
    AS
        SELECT  t1.PK_ID1
               ,t1.something1
               ,t1.somethingelse1
               ,t2.PK_ID2
               ,t2.FK_ID1
               ,t2.something2
               ,t2.somethingelse2
        FROM    dbo.TB_test1 t1
                INNER JOIN dbo.TB_test2 t2 ON t1.PK_ID1 = t2.FK_ID1


    GO


    SELECT TOP 1 * FROM dbo.VI_test ORDER BY somethingelse1,somethingelse2


    GO

Answers:


12

似乎忽略了我添加的任何索引

除非您使用的是SQL Server Enterprise Edition(或等效的Trial和Developer),否则需要WITH (NOEXPAND)在视图引用上使用才能使用它。实际上,即使您使用的是Enterprise,也有充分的理由使用该提示

在没有提示的情况下,查询优化器(在Enterprise Edition中)可能会在使用实例化视图或访问基表之间做出基于成本的选择。如果视图与基本表一样大,则此计算可能会有利于基本表。

另一个有趣的问题是,在没有NOEXPAND提示的情况下,视图引用总是在优化开始之前扩展到基本查询。随着优化的进行,取决于先前的优化活动,优化器可能会也可能无法将扩展的定义与物化视图匹配。对于您的简单查询,几乎可以肯定不是这种情况,但是出于完整性考虑,我提到了这一点。

因此,使用NOEXPAND表提示是您的主要选择,但您也可能会考虑只实现基本表键和在视图中进行排序所需的列。在组合键列上创建一个唯一的聚集索引,然后在顺序列上创建一个单独的非聚集索引。

这将减小实例化视图的大小,并限制为使视图与基本表保持同步而必须进行的自动更新的次数。然后,您可以编写查询来以所需的顺序从视图中获取前1个键(理想情况下为NOEXPAND),然后再联接回基表以使用视图中的键获取其余的列。

另一个变体是将视图聚集在排序列和表键上,然后编写查询以使用键从基本表中手动获取非视图列。您的最佳选择取决于更广泛的上下文。一种确定的好方法是使用实​​际数据和工作负载进行测试。

基本解决方案

CREATE VIEW VI_test
WITH SCHEMABINDING
AS
    SELECT
        t1.PK_ID1,
        t1.something1,
        t1.somethingelse1,
        t2.PK_ID2,
        t2.FK_ID1,
        t2.something2,
        t2.somethingelse2
    FROM dbo.TB_test1 t1
    INNER JOIN dbo.TB_test2 t2 
        ON t1.PK_ID1 = t2.FK_ID1;
GO
-- Brute force unique clustered index
CREATE UNIQUE CLUSTERED INDEX cuq 
ON dbo.VI_test 
    (somethingelse1, somethingelse2, PK_ID1, PK_ID2);
GO
SELECT TOP (1) * 
FROM dbo.VI_test WITH (NOEXPAND)
ORDER BY somethingelse1,somethingelse2;

执行计划:

蛮力指数

使用非聚集索引

-- Minimal unique clustered index
CREATE UNIQUE CLUSTERED INDEX cuq 
ON dbo.VI_test 
    (PK_ID1, PK_ID2)
WITH (DROP_EXISTING = ON);
GO
-- Nonclustered index for ordering
CREATE NONCLUSTERED INDEX ix 
ON dbo.VI_test (somethingelse1, somethingelse2);

执行计划:

非聚集索引

此计划中有一个查找,但仅用于获取单行。

最小索引视图

ALTER VIEW VI_test
WITH SCHEMABINDING
AS
    SELECT
        t1.PK_ID1,
        t2.PK_ID2,
        t1.somethingelse1,
        t2.somethingelse2
    FROM dbo.TB_test1 t1
    INNER JOIN dbo.TB_test2 t2 
        ON t1.PK_ID1 = t2.FK_ID1;
GO
-- Unique clustered index
CREATE UNIQUE CLUSTERED INDEX cuq 
ON dbo.VI_test 
    (somethingelse1, somethingelse2, PK_ID1, PK_ID2);

查询:

SELECT TOP (1)
    V.PK_ID1,
    TT1.something1,
    V.somethingelse1,
    V.PK_ID2,
    TT2.FK_ID1,
    TT2.something2,
    V.somethingelse2
FROM dbo.VI_test AS V WITH (NOEXPAND)
JOIN dbo.TB_test1 AS TT1 ON TT1.PK_ID1 = V.PK_ID1
JOIN dbo.TB_test2 AS TT2 ON TT2.PK_ID2 = V.PK_ID2
ORDER BY somethingelse1,somethingelse2;

执行计划:

最终查询计划

这显示正在检索的表键(按顺序从视图聚簇索引中获取单行),然后在基表上进行两次单行查找以获取其余列。

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.