web-dev-qa-db-ja.com

インデックスヒントなしでクロスアプライを高速化

次のステートメントで作成できる12行の非常に小さなテーブルがあります。

CREATE TABLE dbo.SmallTable(ScoreMonth tinyint NOT NULL PRIMARY KEY,
                            ScoreGoal float NOT NULL
                           );

次のステートメントで作成できる、1億行以上の別のテーブルがあります。

CREATE TABLE dbo.SlowCrossApply(RecordKey nvarchar(12) NOT NULL,
                                Score1 decimal(3, 2) NOT NULL,
                                Score2 decimal(3, 2) NOT NULL,
                                Score3 decimal(3, 2) NOT NULL,
                                Score4 decimal(3, 2) NOT NULL,
                                Score5 decimal(3, 2) NOT NULL,
                                Score6 decimal(3, 2) NOT NULL,
                                FromToday bit NOT NULL
                               );

ALTER TABLE dbo.SlowCrossApply ADD CONSTRAINT i01PK PRIMARY KEY CLUSTERED(RecordKey ASC)
    WITH(FILLFACTOR = 90, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON,
         DATA_COMPRESSION = PAGE
        );

CREATE NONCLUSTERED INDEX i02TodayRecords ON dbo.SlowCrossApply(FromToday)
    INCLUDE (Score1, Score2, Score3, Score4, Score5, Score6)
    WHERE FromToday = 1
    WITH(FILLFACTOR = 100, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON,
         DATA_COMPRESSION = PAGE
        );

i02TodayRecordsには≈1M行あります。次のクエリを実行すると、見栄えがよくなり、水平スクロールバーが表示されないようにフォーマットするのに苦労しました。完了するまでに5分以上かかります。

SELECT b.RecordKey,
       COALESCE(NULLIF(ROUND(((0.95 * (ROW_NUMBER() OVER(PARTITION BY a.Prefix
                                                         ORDER BY b.Score6 ASC
                                                        ) - 1
                                      )
                              )
                              / COALESCE(NULLIF(COUNT(*) OVER(PARTITION BY a.Prefix) - 1, 0
                                               ), 1
                                        )
                             ) + 0.005, 2
                            ), 0.96
                      ), 0.95
               ) AS NewScore
FROM (SELECT LEFT(s.RecordKey, 2) AS Prefix,
             CAST(ROUND(sm.ScoreGoal * COUNT(*), 0) AS int) AS Quant
      FROM dbo.SlowCrossApply AS s
      CROSS JOIN dbo.SmallTable AS sm
      WHERE s.FromToday = 1 AND sm.ScoreMonth = MONTH(GETDATE())
      GROUP BY LEFT(s.RecordKey, 2), sm.ScoreGoal
     ) AS a
CROSS APPLY (SELECT TOP(a.Quant) s2.RecordKey, s2.Score6
             FROM dbo.SlowCrossApply AS s2
             WHERE s2.FromToday = 1 AND s2.Score6 > 0 AND LEFT(s2.RecordKey, 2) = a.Prefix
             ORDER BY s2.Score6 DESC
            ) AS b;

外側のサブクエリは10行のみを返します。 i02TodayRecordsを使用するためのヒントを提供したり、外部サブクエリの結果をテーブル変数に入れたりすると、1秒もかかりません。最終結果は8000行をわずかに超えています。

実行計画は、コストの64%がCross Apply部分のクラスター化インデックスの熱心なインデックススプールによるものであることを示しています。

インデックスヒントが機能することはわかっていますが(少なくとも今のところ)、使用しないようにしています。理想的には、テーブル変数のルートにも行かないでしょう。クエリオプティマイザーにi02TodayRecordsを利用することを「知って」もらうためにできることはありますか? たくさんおそらくもっと重要な情報があることに気づきました。要求があれば、その情報を提供できるよう最善を尽くします。

いくつかの潜在的に有用な情報:インデックスの断片化は1%未満です。両方のインデックスの統計はFULLSCANを介して更新され、データベースは単純なパラメーター化とパラメータースニッフィングを使用するように設定されています。残念ながら、これらの設定を変更することはできません。後者に関しては、クエリオプティマイザーはnotを実行して、特定のフィルターされたインデックスを利用するためにヒントを使用することを余儀なくされた他の単純なクエリとは異なり、値をパラメーターに置き換えました。

3
basketballfan22

あなたが直面している可能性のある問題は、 SARGability の周りにあります。つまり、LEFT句でWHERE関数を使用しています。

LEFT(s2.RecordKey, 2) = a.Prefix

それがそこにあると、すべての行に対して関数を実行し、それを比較するのに行き詰まっています。そのため、そのままではインデックスを作成できません。変換をCTE、ビュー、または派生テーブルに配置しても、操作を実行するための関数を作成するのに役立ちません。

これを回避する1つの方法は、計算列を作成してインデックスを作成することです。

ALTER TABLE dbo.SlowCrossApply ADD LeftTwoPrefix AS LEFT(RecordKey, 2);

インデックスを付けることができます。インデックスの定義も少し変更しています:

CREATE NONCLUSTERED INDEX TodayRecords_Filtered ON 
dbo.SlowCrossApply(LeftTwoPrefix, FromToday, Score6)
INCLUDE (Score1, Score2, Score3, Score4, Score5)
WHERE FromToday = 1

もう1つの方法は、CROSS JOINの結果を一時テーブルにダンプすることです。

SELECT     LEFT(s.RecordKey, 2) AS Prefix, CAST(ROUND(sm.ScoreGoal * COUNT(*), 0) AS INT) AS Quant
INTO       #yourmom
FROM       dbo.SlowCrossApply AS s
CROSS JOIN dbo.SmallTable AS sm
WHERE      s.FromToday = 1
AND        sm.ScoreMonth = MONTH(GETDATE())
GROUP BY   LEFT(s.RecordKey, 2), sm.ScoreGoal;

フォローアップ:

...ここで使用したクエリの詳細を無視して、FromToday [列]がRecordKeyではなくi02TodayRecordsのインデックスキーであることに同意しますか?

この場合、それはあまり重要ではありません。非一意の非クラスター化インデックスは、クラスター化インデックスキー列を非クラスター化インデックスのすべてのレベルに格納します。ここの私の投稿を参照してください: Where Clustered Index Keys Dare

そして:

さらに、RecordKeyは「ランダム」であるため、常に1であるFromTodayとは対照的に、それがインデックスキーである場合、大量の断片化が発生する可能性があります。これらのポイントは有効に聞こえますか?

私は通常、インデックスの断片化について心配していません。クエリに役立つインデックスを作成することを目指しています。断片化されたインデックスは、存在しないインデックスよりもはるかに役立ちます。多くの状況では、 断片化に気付かない になります。

お役に立てれば!

4
Erik Darling