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


11

次のシナリオでインデックス付きビューを設定して、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

回答:


12

私がつけたインデックスは無視しているようです

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

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

もう1つの興味深い点は、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;

実行計画:

最終クエリ計画

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.