web-dev-qa-db-ja.com

異なるテーブルからORDER BYを使用してTOP 1を選択するときにインデックス付きビューを設定する方法

次のシナリオでインデックス付きビューを設定して、2つのクラスター化インデックススキャンなしで次のクエリが実行されるようにしています。このクエリのインデックスビューを作成して使用するときはいつでも、それに付けたインデックスはすべて無視されるようです。

    -- +++ 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

テーブルの設定は次のとおりです。

  • 2つのテーブル
  • 上記のクエリによる内部結合で結合されている
  • 上記のクエリにより、最初の列の列、次に2番目のテーブルの列の順に並べられます。 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
11
ManOnAMission

それが私がそれに置いたインデックスを無視しているようです

SQL Server Enterprise Edition(または同等のTrial and Developer)を使用している場合を除き、ビュー参照でWITH (NOEXPAND)を使用して使用する必要があります。実際、Enterpriseを使用している場合でも、 そのヒントを使用する十分な理由 があります。

ヒントがないと、(Enterprise Editionの)クエリオプティマイザーは、マテリアライズドビューを使用するか、ベーステーブルにアクセスするかをコストベースで選択できます。ビューがベーステーブルと同じ大きさの場合、この計算ではベーステーブルが優先されます。

もう1つの興味深い点は、NOEXPANDヒントがないと、ビュー参照は常に最適化が始まる前に基本クエリに展開されるということです。最適化の進行に伴い、オプティマイザーは、以前の最適化アクティビティに応じて、拡張された定義をマテリアライズドビューに戻すことができる場合とできない場合があります。これはほとんどの場合、単純なクエリには当てはまりませんが、完全を期すために言及します。

したがって、NOEXPANDテーブルヒントを使用することが主なオプションですが、ビューでの順序付けに必要なベーステーブルキーと列を具体化することも考えられます。結合されたキー列に一意のクラスター化インデックスを作成してから、順序付け列に個別の非クラスター化インデックスを作成します。

これにより、マテリアライズドビューのサイズが小さくなり、ビューとベーステーブルの同期を維持するために必要な自動更新の数が制限されます。次に、クエリを作成して、ビューから上位1つのキーを必要な順序で(理想的には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;

実行計画:

Brute force index

非クラスター化インデックスの使用

-- 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);

実行計画:

Nonclustered index for ordering

このプランにはルックアップがありますが、これは単一の行をフェッチするためにのみ使用されます。

最小限のインデックス付きビュー

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;

実行計画:

Final query plan

これは、テーブルキーが取得され(ビューのクラスター化インデックスから順に単一行がフェッチされる)、続いてベーステーブルで単一行を2回検索して残りの列をフェッチすることを示しています。

12
Paul White 9