日付の比較を行うサブクエリのパフォーマンスが悪い


15

サブクエリを使用して、一致するフィールドを持つすべての以前のレコードの合計数を検索する場合、5万件のレコードがあるテーブルでパフォーマンスはひどいです。サブクエリがなければ、クエリは数ミリ秒で実行されます。サブクエリを使用すると、実行時間は1分以上になります。

このクエリの場合、結果は次のようになります。

  • 特定の日付範囲内のレコードのみを含めます。
  • 日付範囲に関係なく、現在のレコードを含まない、以前のすべてのレコードのカウントを含めます。

基本的なテーブルスキーマ

Activity
======================
Id int Identifier
Address varchar(25)
ActionDate datetime2
Process varchar(50)
-- 7 other columns

サンプルデータ

Id  Address     ActionDate (Time part excluded for simplicity)
===========================
99  000         2017-05-30
98  111         2017-05-30
97  000         2017-05-29
96  000         2017-05-28
95  111         2017-05-19
94  222         2017-05-30

推測される結果

日付範囲のため2017-05-292017-05-30

Id  Address     ActionDate    PriorCount
=========================================
99  000         2017-05-30    2  (3 total, 2 prior to ActionDate)
98  111         2017-05-30    1  (2 total, 1 prior to ActionDate)
94  222         2017-05-30    0  (1 total, 0 prior to ActionDate)
97  000         2017-05-29    1  (3 total, 1 prior to ActionDate)

レコード96および95は結果から除外されますが、PriorCountサブクエリに含まれます

現在のクエリ

select 
    *.a
    , ( select count(*) 
        from Activity
        where 
            Activity.Address = a.Address
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc

現在のインデックス

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON [dbo].[Activity]
(
    [ActionDate] ASC
)
INCLUDE ([Address]) WITH (
    PAD_INDEX = OFF, 
    STATISTICS_NORECOMPUTE = OFF, 
    SORT_IN_TEMPDB = OFF, 
    DROP_EXISTING = OFF, 
    ONLINE = OFF, 
    ALLOW_ROW_LOCKS = ON, 
    ALLOW_PAGE_LOCKS = ON
)

質問

  • このクエリのパフォーマンスを向上させるためにどのような戦略を使用できますか?

編集1
DBで何を変更できるかという質問に対する回答として、テーブル構造だけでなく、インデックスを変更できます。

編集2列に
基本的なインデックスを追加しましたAddressが、あまり改善されていないようです。私は現在、一時テーブルを作成し、値なしで値を挿入PriorCountし、特定のカウントで各行を更新することで、はるかに優れたパフォーマンスを見つけています。

編集3
インデックススプールジョーオブビッシュ(受け入れられた答え)が見つかったことが問題でした。新しいを追加nonclustered index [xyz] on [Activity] (Address) include (ActionDate)すると、一時テーブルを使用せずにクエリ時間が1分以上から1秒未満に短縮されました(編集2を参照)。

回答:


17

インデックス定義を使用するとIDX_my_nme、SQL ServerはActionDate列を使用してシークできますが、列をシークできませんAddress。インデックスには、サブクエリをカバーするために必要なすべての列が含まれていますが、そのサブクエリに対してはあまり選択的ではない可能性があります。テーブル内のほとんどすべてのデータのActionDate値がよりも早いと仮定し'2017-05-30'ます。のシークはActionDate < '2017-05-30'、インデックスからほとんどすべての行を返します。これらの行は、インデックスから行がフェッチされた後にさらにフィルタリングされます。クエリが200行を返す場合、ほぼ200回の完全なインデックススキャンIDX_my_nmeを実行することになります。つまり、インデックスから約50000 * 200 = 1000万行を読み取ることになります。

Addressクエリに関する完全な統計情報を提供していないので、サブクエリのシークがはるかに選択的になる可能性が高いので、それは私の側の仮定です。ただし、ちょうどにインデックスを作成し、Addressテーブルにの一意の値が1万個あるとしますAddress。新しいインデックスを使用すると、SQL Serverはサブクエリの実行ごとにインデックスから5行をシークするだけで済むため、インデックスから約200 * 5 = 1000行を読み取ることができます。

SQL Server 2016でテストしているので、若干の構文の違いがあるかもしれません。以下は、データ配信に関して上記と同様の仮定を行ったサンプルデータです。

CREATE TABLE #Activity (
    Id int NOT NULL,
    [Address] varchar(25) NULL,
    ActionDate datetime2 NULL,
    FILLER varchar(100),
    PRIMARY KEY (Id)
);

INSERT INTO #Activity WITH (TABLOCK)
SELECT TOP (50000) -- 50k total rows
x.RN
, x.RN % 10000 -- 10k unique addresses
, DATEADD(DAY, x.RN / 100, '20160201') -- 100 rows per day
, REPLICATE('Z', 100)
FROM
(
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) x;

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([ActionDate] ASC) INCLUDE ([Address]);

質問の説明に従ってインデックスを作成しました。私は問題のクエリと同じデータを返すこのクエリに対してテストしています:

select 
    a.*
    , ( select count(*) 
        from #Activity Activity
        where 
            Activity.[Address] = a.[Address]
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from #Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc;

インデックススプールを取得します。基本レベルで意味することは、テーブルに対する既存のインデックスがどれも適切ではなかったため、クエリオプティマイザがその場で一時インデックスを作成することです。

インデックススプール

クエリはまだ私のためにすぐに終了します。おそらく、システムでインデックススプールの最適化を取得していないか、テーブル定義またはクエリに関して何か異なることがあります。教育目的のために、文書化されていない機能OPTION (QUERYRULEOFF BuildSpool)を使用して、インデックススプールを無効にすることができます。計画は次のようになります。

悪いインデックスシーク

単純なインデックスシークの外観にだまされないでください。SQL Serverは、インデックスから約1,000万行を読み取ります。

インデックスから1,000万行

クエリを複数回実行する場合、クエリオプティマイザが実行するたびにインデックスを作成することはおそらく意味がありません。このクエリに対してより選択的なインデックスを事前に作成できます。

CREATE NONCLUSTERED INDEX [IDX_my_nme_2] ON #Activity
([Address] ASC) INCLUDE (ActionDate);

計画は以前と同様です:

インデックスシーク

ただし、新しいインデックスでは、SQL Serverはインデックスから1000行のみを読み取ります。800行が返されてカウントされます。インデックスはより選択的に定義することもできますが、データの分布によってはこれで十分な場合があります。

良いシーク

テーブルに追加のインデックスを定義できない場合は、ウィンドウ関数の使用を検討します。次のように動作するようです:

SELECT t.*
FROM
(
    select 
        a.*
        , -1 + ROW_NUMBER() OVER (PARTITION BY [Address] ORDER BY ActionDate) PriorCount
    from #Activity a
) t
where t.ActionDate between '2017-05-29' and '2017-05-30'
order by t.ActionDate desc;

このクエリはデータの1回のスキャンを実行しますが、高価な並べ替えを実行ROW_NUMBER()し、テーブル内のすべての行の関数を計算するため、ここで追加の作業が行われたように感じます。

悪いソート

ただし、そのコードパターンが本当に好きな場合は、インデックスを定義してより効率的にすることができます。

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([Address], [ActionDate]) INCLUDE (FILLER);

これにより、ソートが最後に向かって移動し、はるかに安価になります。

いいね

これが役に立たない場合は、質問にさらに情報を追加する必要があります。できれば、実際の実行計画を含めてください。


1
見つけたインデックススプールが問題でした。新しいを追加するnonclustered index [xyz] on [Activity] (Address) include (ActionDate)と、クエリ時間は1分以上から1秒未満に短縮されました。可能であれば+10。ありがとう!
メトロスマーフ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.