web-dev-qa-db-ja.com

数百万行の狭いテーブルでクエリのパフォーマンスを向上させることは可能ですか?

現在、完了までに平均2500ミリ秒かかるクエリがあります。テーブルは非常に狭いですが、4400万行あります。パフォーマンスを向上させるにはどのようなオプションが必要ですか、それともこれで十分ですか?

クエリ

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31'; 

テーブル

CREATE TABLE [dbo].[Heartbeats](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [DeviceID] [int] NOT NULL,
    [IsPUp] [bit] NOT NULL,
    [IsWebUp] [bit] NOT NULL,
    [IsPingUp] [bit] NOT NULL,
    [DateEntered] [datetime] NOT NULL,
 CONSTRAINT [PK_Heartbeats] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

インデックス

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

追加のインデックスを追加すると役立ちますか?もしそうなら、彼らはどのように見えますか?クエリはたまにしか実行されないため、現在のパフォーマンスは許容範囲内ですが、学習課題として考えているのですが、これを速くするために何かできることはありますか?

[〜#〜]更新[〜#〜]

強制インデックスヒントを使用するようにクエリを変更すると、クエリは50ミリ秒で実行されます。

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats] WITH(INDEX(CommonQueryIndex))
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 

正しく選択的なDeviceID句を追加すると、50ミリ秒の範囲にヒットします。

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' AND DeviceID = 4;

ORDER BY [DateEntered], [DeviceID]を元のクエリに追加すると、50msの範囲になります。

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 
ORDER BY [DateEntered], [DeviceID];

これらはすべて私が期待していたインデックス(CommonQueryIndex)を使用しているので、私の質問は、このインデックスをこのようなクエリで強制的に使用する方法はあるのでしょうか?または、テーブルのサイズがオプティマイザを使いすぎているため、ORDER BYまたはヒントを使用する必要がありますか?

14
Nate

オプティマイザが最初のインデックスを作成しない理由:

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

[DateEntered]列の選択性の問題です。

テーブルには4400万行あるとのことですが、行サイズは次のとおりです。

ID用に4バイト、デバイスID用に4バイト、日付用に8バイト、4ビット列用に1バイト。これは、17バイト+ 7バイトのオーバーヘッド(タグ、Nullビットマップ、変数列オフセット、列数)の合計で、1行あたり24バイトです。

それは大まかに140kページに翻訳されます。これらの4,400万行を格納します。

これで、オプティマイザは2つのことを実行できます。

  1. テーブルをスキャンできます(クラスター化インデックススキャン)
  2. または、インデックスを使用することもできます。インデックスのすべての行について、クラスター化インデックスでブックマークルックアップを実行する必要があります。

ここで、特定の時点で、非クラスタ化インデックスで見つかったすべてのインデックスエントリに対して、クラスタ化インデックスでこれらすべての単一のルックアップを実行すると、コストが高くなります。そのためのしきい値は、通常、ルックアップの合計数がテーブルページの合計数の25%から33%を超える必要があります。

したがって、この場合:140k/25%= 35000行140k/33%= 46666行。

(@ RBarryYoung、35kは合計行の0.08%で、46666は0.10%なので、それが混乱の原因だったと思います)

したがって、where句の結果が35000〜46666行になる場合(これは一番上の句の下にあります!)非クラスター化は使用されず、クラスター化インデックススキャンが使用される可能性が非常に高くなります。

これを変更する方法は2つだけです。

  1. Where句をより選択的にします。 (可能なら)
  2. カバリングインデックスを使用できるように、*をドロップして数列のみを選択します。

select *を使用する場合でも、カバーするインデックスを作成できることを確認してください。ただし、挿入、更新、削除のオーバーヘッドが大きくなるだけです。それが最善の解決策であるかどうかを確認するには、作業負荷(読み取りと書き込み)の詳細を知る必要があります。

Datetimeからsmalldatetimeに変更すると、クラスター化インデックスのサイズが16%削減され、非クラスター化インデックスのサイズが24%削減されます。

13
Edward Dortland

PKがクラスター化されている特別な理由はありますか?多くの人々は、デフォルトでそのようにするか、PKをクラスター化する必要があると考えているため、これを行います。違います。通常、クラスター化インデックスは、範囲クエリ(このクエリなど)または子テーブルの外部キーに最適です。

クラスタリングインデックスの効果は、データがクラスタbツリーのリーフノードに格納されるため、すべてのデータをまとめて束ねることです。したがって、範囲の「広すぎる」ことを要求していないと仮定すると、オプティマイザはbツリーのどの部分にデータが含まれているかを正確に認識し、行識別子を見つけてデータのある場所に移動する必要がなくなります。です(NCインデックスを処理する場合と同様)。範囲の「広すぎる」とは何ですか?ばかげた例としては、1年分のレコードしかないテーブルから11か月分のデータを要求する場合があります。統計が最新であると仮定すると、1日分のデータをプルしても問題にはなりません。 (ただし、昨日のデータを探していて、3日間統計を更新しなかった場合、オプティマイザは問題を起こす可能性があります。)

"SELECT *"クエリを実行しているので、エンジンはテーブル内のすべての列を返す必要があります(その時点でアプリが必要としない新しい列を誰かが追加した場合でも)。そのため、カバリングインデックスまたはインデックス含まれている列を使用しても、ほとんど役に立たないでしょう。 (テーブルのすべての列をインデックスに含めると、何か問題が発生します。)オプティマイザは、これらのNCインデックスをおそらく無視します。

それで、何をすべきか?

私の提案は、NCインデックスを削除し、クラスター化PKを非クラスター化に変更して、[DateEntered]にクラスター化インデックスを作成することです。他の方法で証明されるまでは、シンプルな方が良いです。

8
darin strait

そこに "*"がある限り、大きな違いをもたらすと私が想像できる唯一のことは、次のようにインデックス定義を変更することです。

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)INCLUDE (ID, IsWebUp, IsPingUp, IsPUp)
 WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

コメントで述べたように、そのインデックスを使用する必要がありますが、使用しない場合は、ORDER BYまたはインデックスヒントのいずれかでそれを説得できます。

4
RBarryYoung

これは少し違った見方をします。

  • はい、古いスレッドであることはわかっていますが、興味をそそられます。

私は日時列をダンプします-それを整数に変更します。ルックアップテーブルを用意するか、日付の変換を行います。

クラスタ化インデックスをダンプします-ヒープのままにして、日付を表す新しいINT列に非クラスタ化インデックスを作成します。つまり、今日は20121015になります。この順序は重要です。テーブルをロードする頻度に応じて、DESC順にインデックスを作成する方法を見てください。メンテナンスコストが高くなり、Fill Factorまたはパーティション化を導入する必要があります。パーティション化は、実行時間の短縮にも役立ちます。

最後に、SQL 2012を使用できる場合は、SEQUENCEを使用してみてください。挿入の場合、identity()よりもパフォーマンスが高くなります。

3
Jeremy Lowell