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

Man*_*ion 11 index sql-server-2008 sql-server materialized-view index-tuning

我正在努力在以下场景中设置索引视图,以便在没有两个聚集索引扫描的情况下执行以下查询。每当我为此查询创建索引视图然后使用它时,它似乎忽略了我放在它上面的任何索引:

    -- +++ 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
Run Code Online (Sandbox Code Playgroud)

表设置如下:

  • 两张桌子
  • 它们通过上面的查询通过内部连接连接起来
  • 并通过上面的查询从第一个表中的一列和第二个表中的一列排序;仅选择 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
    
    Run Code Online (Sandbox Code Playgroud)

索引视图可能应该定义如下,结果 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
Run Code Online (Sandbox Code Playgroud)

Pau*_*ite 12

它似乎忽略了我放在它上面的任何索引

除非您使用的是 SQL Server 企业版(或等效的试用版和开发版),否则您需要WITH (NOEXPAND)在视图引用上使用才能使用它。事实上,即使您使用的是 Enterprise,也有充分的理由使用该提示

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

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

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

这将减小物化视图的大小,并限制为保持视图与基表同步而必须进行的自动更新的数量。然后可以编写您的查询以从视图(理想情况下使用NOEXPAND)获取所需顺序的前 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
-- 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;
Run Code Online (Sandbox Code Playgroud)

执行计划:

蛮力指数

使用非聚集索引

-- 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);
Run Code Online (Sandbox Code Playgroud)

执行计划:

用于排序的非聚集索引

这个计划中有一个查找,但它只用于获取单行。

最小索引视图

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);
Run Code Online (Sandbox Code Playgroud)

询问:

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;
Run Code Online (Sandbox Code Playgroud)

执行计划:

最终查询计划

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