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


14

現在、クエリの完了に平均2500msかかっています。私のテーブルは非常に狭いですが、4,400万行あります。パフォーマンスを改善するためにどのようなオプションが必要ですか、またはこれは得られるほど良いですか?

クエリ

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]元のクエリに追加すると、50ミリ秒の範囲になります。

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ヒントを使用する必要がありますか?


私はあなたには、いくつかのより程度のパフォーマンスを向上させるだろう「DateEntered」に1つの以上の非クラスタ化インデックスを追加することができると思います
Praveenさん

@Praveen基本的に既存のインデックスと同じでしょうか?同じフィールドに2つのインデックスがあるため、特別なことをする必要がありますか?
ネイト

@Nate、このテーブルはハートビートと呼ばれ、4400万のレコードが含まれているため、このテーブルに大量の挿入があると思いますか?インデックス付けでは、カバーリングインデックスのみを追加して、速度を上げることができます。しかし、あなたが言及したように、たまにこのクエリを使用するだけである場合、大量の挿入を行う場合はそのことを強くお勧めします。基本的に挿入負荷が2倍になります。エンタープライズ版で実行していますか?
エドワード・ドートランド

NCインデックスにdeviceIDがあることに気付きました。あなたのwhere句にそれを含めることは可能ですか?そして、それは結果セットをしきい値以下に下げますか?<35kレコード(上位1000句なし)。
エドワード・ドートランド

1
最後の質問、あなたは常にdateEnteredの順に挿入していますか?または、デバイスが相互に非同期を挿入する可能性があるため、これらが故障している可能性があります。クラスタ化インデックスをDateEntered列に変更しようとする場合があります。クラスター化インデックスの離脱ページは445ページになりました。intからdatetimeに移動すると、それは2倍になります。しかし、この場合、それは悪くないかもしれません。
エドワード・ドートランド

回答:


13

オプティマイザーが最初のインデックスに向かない理由:

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]列の選択性の問題です。

テーブルには4,400万行あると言っています。行サイズは次のとおりです。

IDには4バイト、デバイスIDには4バイト、日付には8バイト、4ビット列には1バイト。(タグ、Nullビットマップ、var colオフセット、colカウント)の合計は17バイト+ 7バイトのオーバーヘッドで、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%削減されます。


スキャンのしきい値は通常、それよりもはるかに低い(10%またはさらに低い)が、範囲は1年以上前の1日であるため、そのしきい値でさえ設定しないでください。また、カバーリングインデックスが追加されたため、クラスター化インデックススキャンは指定されていません。そのインデックスはWHERE句をSARG対応にするため、優先されるべきです。
–RBarryYoung

@RBarryYoung最初に[EnteredDate]、[DeviceID]の非クラスター化インデックスが使用されなかった理由を説明しようとしていました。しきい値については、私たち二人とも同意すると思いますが、私はページの観点からのみ話しているだけです。答えをより明確にするために変更します。
エドワード・ドートランド

答えを変更して、私が答えていたものをより明確にしました。@RBarryYoungが提案したカバリングインデックスが使用されない理由を説明することはできません。ここで100万行でテストし、最適化するためにカバーインデックスを使用しました。
エドワード・ドートランド

非常に包括的な応答をありがとう、非常に理にかなっています。ワークロードに関して、テーブルには5分間に150〜300回の挿入があり、レポート目的で1日に数回の読み取りがあります。
ネイト

カバリングインデックスのオーバーヘッドヘッドは、狭いテーブルであり、「カバリング」が行のほとんどをすでに含んでいる既存のインデックスへの単なる追加であるため、あまり重要ではありません。
-RBarryYoung

8

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

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

「SELECT *」クエリを実行しているため、エンジンはテーブルのすべての列を返す必要があります(誰かがその時点でアプリが必要としない新しい列を追加した場合でも)列が含まれていると、ほとんど役に立ちません。(インデックスのテーブルのすべての列を含める場合、何か間違ったことをしていることになります。)オプティマイザーは、おそらくこれらのNCインデックスを無視します。

じゃあ何をすればいいの?

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


行が昇順で挿入されると仮定すると、これが最も簡単な答えですが、非線形の順序で挿入すると断片化が発生します。
カークブロードハースト

bツリー構造にデータを追加すると、バランスが失われます。クラスター順で行を追加している場合でも、インデックスのバランスが失われます。テーブルのインデックスを再作成すると断片化が削除され、DBAは「十分な」データがテーブルに追加された後にテーブルのインデックスを再作成する必要があることを通知します。(「十分」の定義は議論されるかもしれませんし、「いつ」が議論になるかもしれません。)何らかの理由でインデックスの再作成ができないという質問には何も見当たりません。
ダリン海峡

4

そこに「*」がある限り、インデックス定義を次のように変更するだけで大​​きな違いが生じると想像できます。

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またはインデックスヒントで説得できます。


私はこれを試してみましたが、私はまだほぼ同じ場所にいます。サーバーの応答を2500ミリ秒待機し、クライアントの処理時間を10ミリ秒待機しています。
ネイト

クエリプランを投稿します。
–RBarryYoung

クラスタ化インデックスを使用しているようです。(SELECTコスト:0%<-トップコスト:20%<-クラスター化インデックススキャンPK_Heartbeatsコスト:80%)
ネイト

ええ、それは正しくありません、何かが統計/オプティマイザーをオフにします。新しいインデックスを使用するようにヒントを追加します。
–RBarryYoung

@Max Vernon:たぶん、しかしそれはクエリプランでフラグが付けられるべきでした。
–RBarryYoung

3

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

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

datetime列をダンプします-intに変更します。ルックアップテーブルを用意するか、日付の変換を行います。

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

最後に、SQL 2012を使用できる場合は、SEQUENCEを使用してみてください-挿入のidentity()よりも優れています。


興味深いソリューション。私の質問からは明らかではありませんが、DateTimeの時間部分は非常に重要です。通常、日付に基づいてクエリを実行し、その期間の特定の時間を確認します。それを考慮してこのソリューションをどのように調整しますか?
ネイト

その場合、datetime列を保持し、dateのint列を追加します(範囲は時間要素ではなく日付要素に基づいているため)。また、TIMEデータ型の使用を検討してから、日付と時刻を効果的に分割することもできます。この方法では、データのフットプリントが小さくなり、列のTime要素がまだあります。
ジェレミーローウェル

1
これを以前に見逃した理由はわかりませんが、クラスター化インデックスと非クラスター化インデックスにも行圧縮を使用します。テーブルで簡単なテストを行ったところ、次のようになりました。上記で定義したテーブルに一連のデータ(580万行)を作成しました。クラスター化インデックスと非クラスター化インデックスを圧縮(行)しました。正確なクエリに基づく論理読み取りは、2,074から1,433に減少しました。それは大幅な減少であり、私はそれだけであなたを助けると確信しています-それは非常に低いリスクです。
ジェレミーローウェル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.