web-dev-qa-db-ja.com

この非常に大きなテーブルにインデックスを付ける最良の方法

私は次の表を持っています

CREATE TABLE DiaryEntries
(
 [userId] [uniqueidentifier] NOT NULL,
 [setOn] [datetime] NOT NULL, -- always set to GETDATE().
 [entry] [nvarchar](255) NULL
)

各ユーザーは、1日に約3つのエントリを挿入します。約1'000'000人のユーザーがいます。これは、このテーブルに毎日3'000'000の新しいレコードがあることを意味します。レコードが1か月以上経過すると、削除します。

ほとんどのクエリには、次のWHERE句があります。

WHERE userId = @userId AND setOn > @setOn

今月中に挿入されたすべての行(最大90行)を返すクエリを除いて、ほとんどのクエリは3行以下を返します。

レコードが挿入されると、日付とuserIdは変更できません。

今私の質問は-このテーブルのインデックスを最適に配置する方法は?私は2つの選択肢で立ち往生しています:

  1. (userId、setOn)のクラスター化インデックス-これにより検索が高速になりますが、多くの中間値(userIdは同じですが日付が異なる)を挿入するため、過度のページ分割が心配です。
  2. (userId)および(setOn)の非クラスター化インデックス-これにより、(userId)インデックスのページ分割も発生します(ただし、最初のオプションと同じくらい費用がかかりますか?)。 NCインデックスを使用しているため、検索速度が低下します。
  3. 追加の列(id)のクラスター化インデックスと(userId、setOn)の非クラスター化インデックス-これにより、データテーブルのページ分割が排除されますが、NCインデックスに一部が発生します。 NCインデックスを使用して検索するため、このオプションも検索には最適ではありません。

あなたの提案は何ですか?他のオプションはありますか?

PS-お時間をいただきありがとうございます。


2日間熟考した後、私はこの問題に対する別の解決策を思いつきました。

CREATE TABLE MonthlyDiaries
(
 [userId] uniqueidentifier NOT NULL,
 [setOn] datetime NOT NULL, -- always set to GETDATE().

 [entry1_1] bigint NULL, -- FK to the 1st entry of the 1st day of the month.
 [entry1_2] bigint NULL, -- FK to the 2nd entry of the 1st day of the month.
 [entry1_3] bigint NULL,
 [entry2_1] bigint NULL,
 [entry2_2] bigint NULL,
 [entry2_3] bigint NULL,
 ...
 [entry31_1] bigint NULL,
 [entry31_2] bigint NULL,
 [entry31_3] bigint NULL,
 PRIMARY KEY (userId, setOn)
)
CREATE TABLE DiaryEntries
(
 [id] bigint IDENTITY(1,1) PRIMARY KEY CLUSTERED,
 [entry] nvarchar(255) NOT NULL
)

基本的に、私は31日を1つの行にグループ化しました。これは、ユーザーごとに月に1回だけ新しいレコードを挿入することを意味します。これにより、ページ分割がユーザーごとに1日3回から、ユーザーごとに月に1回に削減されます。明らかに欠点があります、ここにそれらのいくつかがあります

  • 行サイズは非常に大きいですが、99.999%の確率で、MonthlyDiariesから1行だけをクエリします。
  • エントリがない日もあるため、必要以上のスペースを使用している可能性があります。大したことではありません。
  • 特定の日のエントリを見つけるには、DiaryEntriesで追加のインデックスシークが必要になります。私は90行以下を取得しており、80%の場合、1行しか取得していないため、それほど大きなコストになることはないと思います。

全体として、これは良いトレードオフだと思います。3ページ分割/日/ユーザーから1ページ分割/月/ユーザーに削減しますが、代わりに検索を少し遅くすることで少額の料金を支払います。どう思いますか?

4
niaher

GuidをIDとして使用する正当な理由があると思います。

断片化は主にスキャンの問題であり、シークの問題ではありません。断片化は先読みに大きな影響を与え、シークは先読みを使用したり、必要としたりしません。列の選択が不十分なフラグメント化されていないインデックスは、適切で使用可能な列を持つ99%のフラグメントインデックスよりも常にパフォーマンスが低下します。テーブルをスキャンするDWレポートスタイルのクエリについて説明した場合は、断片化の排除に焦点を当てることをお勧めしますが、説明する負荷については、効率的な(カバーする)シークと(小さな)範囲スキャンに焦点を当てる方が理にかなっています。

アクセスパターンは常に@userIdによって駆動されるため、これはクラスター化インデックスの左端の列である必要があります。また、ほとんどのクエリで限界値を追加するため、クラスター化インデックスの2番目の列としてsetOnを追加します(@userIdは非常に選択的であり、最悪の場合、90 milから90レコードであるため、余分なフィルタリングが追加されます。 @setOnは重要ではありません)。あなたが説明するクエリから、クラスター化されていないインデックスを追加する必要はありません。

唯一の問題は、古いレコードの削除(30日間の保持)です。これを満たすために、セカンダリNCインデックスを使用しないことをお勧めします。スライディングウィンドウを使用して毎週のパーティション分割スキームを展開したいと思います。 SQL Server 2005のパーティションテーブルに自動スライディングウィンドウを実装する方法 を参照してください。このソリューションでは、パーティションスイッチによって古いレコードが削除されます。これは、可能な限り最も効率的な方法です。毎日のパーティション分割スキームは、30日間の保持要件をより正確に満たし、おそらく試してテストする価値があります。各パーティションで特定の@userIdレコードを検索する可能性のあるクエリについて説明しているため、30パーティションを直接お勧めすることを躊躇します。また、31パーティションは、高負荷でパフォーマンスの問題を引き起こす可能性があります。両方をより適切にテストおよび測定します。

5
Remus Rusanu

まず、テーブルにデフォルトの制約を追加します。次に、パーティションスキームを追加します。 3番目に最も一般的なクエリを書き直します。

クラスター化インデックスは、setOn、ユーザーIDに設定する必要があります。これにより、インデックスが断片化する可能性がなくなります。テーブルのパーティション分割を使用してテーブルを分割し、毎月個別のファイルに保存する必要があります。これにより、メンテナンスが削減されます。毎月実行して翌月の新しいテーブルを作成し、最も古い月を削除し、パーティションスキームを調整できるパーティションスライディングウィンドウスクリプトをオンラインで探すことができます。ストレージに関係がない場合は、本当に古い月をアーカイブテーブルに移動することもできます。

クエリのwhere句は次の形式にする必要があります。

WHERE setOn > @setOn AND userId = @userId

または、1か月間返品する場合:

WHERE setOn BETWEEN @setOnBegin AND @setOnEnd AND userId = @userId

パーティショニングなしの新しいスキーマデザインは次のようになります。

-- Stub table for foreign key
CREATE TABLE Users
(
 [userId] [uniqueidentifier] NOT NULL
  CONSTRAINT PK_Users PRIMARY KEY NONCLUSTERED
  CONSTRAINT DF_Users_userId DEFAULT NEWID(),
 [userName] VARCHAR(50) NOT NULL
)
GO

CREATE TABLE DiaryEntries
(
 [userId] [uniqueidentifier] NOT NULL
  CONSTRAINT FK_DiaryEntries_Users FOREIGN KEY REFERENCES Users,
 [setOn] [datetime] NOT NULL
  CONSTRAINT DF_DiaryEntries_setOn DEFAULT GETDATE(),
 [entry] [nvarchar](255) NULL,
 CONSTRAINT PK_DiaryEntries PRIMARY KEY CLUSTERED (setOn, userId)
)
GO

それが機能するようになったら、パーティショニングを追加する必要があります。そのためには、 このブログ投稿 から始めてください。次に、読み始めます このMSDNホワイトペーパー 。ホワイトペーパーは2005年に作成され、2008年には私が調査していないパーティションの改善があったため、2008年のソリューションはより単純になる可能性があります。

6
Justin Dearing

私は問題について十分に知らないので、あなたの解決策を批評するためにここにいるわけではなく、私もそうする立場にありません。これが私のフィードバックです:

  • 行サイズが原因でディスク領域を使いすぎることが不満な場合は、チェックアウトしてください 疎列 そうすれば、すべてのnullがそれほど多くの領域を占有することはありません。
  • 外部キーを使用すると、挿入が大幅に遅くなりますが、これをテストしましたか?
2
Nick Kavadias

私はあなたの新しいソリューションのファンではありません。これは、新しい問題を引き起こすだけです。最大の問題は、UPDATESが(通常)INSERTSよりも遅く、更新が行われているときにブロックされるリスクが大きくなることです。

ページ分割が心配な場合は、クラスター化インデックスの "FillFactor"を調整するだけです。 FillFactorは、変更または挿入を可能にするために、各ページのどれだけを空白のままにするか(デフォルト)を定義します。

妥当なFillFactorを設定するということは、挿入ページ分割を引き起こしてはならないことを意味し、古いレコードのパージは、より多くのスペースを解放する必要があることを意味しますそれらのページは、ページごとに(ある程度)一貫した空き領域を維持します。

残念ながら、SQLのデフォルトは通常0(100と同じ意味)です。これは、すべてのページが完全にいっぱいになっていることを意味し、多くのページ分割が発生します。多くの人が90の値を推奨しています(各データページに10%の空き容量)。テーブルに最適なものはわかりませんが、ページ分割について非常に偏執的な場合は、余分なディスク領域を節約できるのであれば、75以下を試してください。ページ分割を監視するために監視できるperfmonカウンターがいくつかあります。または、クエリを実行して、各データページの空き容量の割合を確認することもできます。

テーブル(元のバージョン)のインデックスの詳細については、Remusが言及した理由から、([userId]、[setOn])のクラスター化インデックスをお勧めします。

また、([setOn])に非クラスター化インデックスが必要です。これにより、「古いレコードの削除」クエリで、すべての古いレコードを見つけるために全表スキャンを実行する必要がなくなります。

また、ほとんどの場合、単純な識別子のGUIDも好きではありませんが、変更するのはおそらく少し遅いと思います。

編集:このテーブルの推定フィルファクターに関するいくつかの予備計算。

ユーザーごとに、1日あたり3つの新しいエントリが、30日間保持されるため、合計で最大90のエントリが保持されます。 30日より古いすべてのレコードのdailyパージを実行すると仮定すると(30日ごとにパージするだけではなく)、追加/削除するのは少なくなります毎日の記録の5%以上。

したがって、90のフィルファクター(各ページに10%の空き領域)で十分です。

毎月パージするだけの場合、最も古い30を削除する前に、60日近く積み重ねることになります。つまり、何かが必要になるということです。 50%のフィルファクターのように。

毎日のパージを強くお勧めします。

編集2:さらに検討した結果、[setOn]の非クラスター化インデックスは、パージクエリで使用するのに十分な選択性がない可能性があります(1日は行の1/30または3.3%であり、 「便利」の端にあります)。インデックスが存在する場合でも、とにかくクラスター化インデックススキャンを実行する可能性があります。おそらく、この追加のインデックスがある場合とない場合の両方でテストする価値があります。

1
BradC

毎日挿入される行が非常に多いため、ジャーナルファイルのように、テーブルの物理ファイルの最後に新しい行を挿入する必要があります。

したがって、行は時系列で並べ替える必要があります

したがって、setOnは主キーの最初の部分である必要があります。 -または、理想的には、「postId」列を追加します。これは、それ自体を自動インクリメントする単なる整数です。

PostId列が必要ない場合、主キーは(setOn、userId)になります。それ以外の場合は、単にpostIdにすることができます。

したがって、速い挿入時間が得られました。ここで、userIdごとに選択するときに、取得時間を短縮する必要があります。

このために、useId上にあるはずのセカンダリインデックスをテーブルに追加する必要があります。ユーザーあたり90レコードしかないため、rdbmsがそのユーザーのすべての行をすばやく取得し(一度に1か月の行しかないため、90行すべて)、それらの90行をテーブルスキャンするのに十分です。これは目がくらむほど速くなります。

インデックスは、データベースに付属しているものなら何でも、標準のbツリー、赤黒木、インデックスにすることができます。

インデックスへの挿入により、挿入はわずかに遅くなりますが、それほど大きくはありません。ツリー構造は、ランダムな挿入の処理に非常に優れています。

UserIdインデックスは、安定したセットであるUserIdのセットに基づいているため、ツリーはかなり安定していて、あまりリバランスする必要はありません。ジャーナルエントリが追加およびパージされると、最後のリーフノードだけが変更されます。木の形をあまり変えないでください。

1
Hugh Perkins

これを解決する1つの方法は、毎日のテーブルを用意することです。

3Mレコードのテーブルでは、useridとsetonにクラスター化インデックスがあることは問題ではありません。挿入時間ははるかに短くなります。

1日の終わりにその日のテーブルでメンテナンスを実行して、テーブルが断片化されず、応答時間が問題ないようにすることができます。

1か月分のデータを取得するために、テーブル全体にビューを作成することもできます。

0
Shiraz Bhaiji

私は提案します:

  1. ユーザーIDのクラスター化インデックス
  2. Seton&entryの非クラスター化カバーインデックス、またはsetonの非クラスター化インデックスのみ
0
OMG Ponies