データベースは、可変長フィールドのインデックスキー値(ディスク上)をどのように格納しますか?


16

環境

この質問は、SQLデータベースシステムとNoSQLデータベースシステムの両方でのインデックスの低レベルの実装の詳細に関するものです。質問はこれらの実装の単一ノード内に保存されたキーに特に関係するため、インデックスの実際の構造(B +ツリー、ハッシュ、SSTableなど)は無関係です。

バックグラウンド

SQL(MySQLなど)およびNoSQL(CouchDB、MongoDBなど)データベースでは、データの列またはJSONドキュメントフィールドにインデックスを作成するときに、実際にデータベースに実行させるのは、本質的にすべてのソート済みリストを作成することですこれらの値と、その値に関連するレコードが存在するメインデータファイルへのファイルオフセット。

(簡単にするために、特定の実装のその他の難解な詳細を手で振り払うかもしれません)

シンプルなクラシックSQLの例

インデックスを作成する単純な32ビットint主キーを持つ標準SQLテーブルを考えます。データファイルへの64ビットオフセットに関連付けられ、関連付けられた整数キーのディスク上のインデックスが作成されます。レコードは存続します。例:

id   | offset
--------------
1    | 1375
2    | 1413
3    | 1786

インデックス内のキーのディスク上の表現は、次のようになります。

[4-bytes][8-bytes] --> 12 bytes for each indexed value

ファイルシステムとデータベースシステムでのディスクI / Oの最適化に関する標準的な経験則に固執して、ディスク上の4KBブロックにキーを保存するとします。

4096 bytes / 12 bytes per key = 341 keys per block

インデックスの全体構造(B +ツリー、ハッシュ、ソート済みリストなど)を無視して、341キーのブロックを一度に読み書きし、必要に応じてディスクに戻します。

クエリの例

前のセクションの情報を使用して、「id = 2」のクエリが来たとしましょう。従来のDBインデックスルックアップは次のようになります。

  1. インデックスのルートを読み取ります(この場合、1ブロック)
  2. ソートされたブロックをバイナリ検索してキーを見つけます
  3. 値からデータファイルオフセットを取得します
  4. オフセットを使用してデータファイル内のレコードを検索する
  5. データを呼び出し元に返す

質問のセットアップ...

さて、ここで質問がまとめられます...

ステップ#2は、これらのクエリは、O(LOGN)の時間で実行することを可能にする最も重要な部分です...情報をソートする必要がある、しかし、あなたは、クイックソートの方法でリストを横断することができるように持って...もっと具体的には、明確に定義されたオフセットに自由にジャンプして、その位置のインデックスキー値を読み込む必要があります。

ブロックを読み取った後、すぐに170番目の位置にジャンプし、キー値を読み取って、探しているのがその位置のGTまたはLTかどうかを確認する必要があります(など)。

このようにブロック内のデータをジャンプできる唯一の方法は、上記の例のようにキー値のサイズがすべて明確に定義されている場合です(キーごとに4バイト、次に8バイト)。

質問

さて、ここで効率的なインデックスデザインにこだわっています。SQLデータベースのvarchar列、より具体的には、CouchDBやNoSQLのようなドキュメントデータベースの完全に自由形式のフィールドで、インデックスを作成するフィールドはどれでもかまいませんlength インデックスを構築するインデックス構造のブロック内にあるキー値をどのように実装しますか?

たとえば、CouchDBのIDにシーケンシャルカウンターを使用し、ツイートのインデックスを作成しているとしましょう。数か月後に値が「1」から「100,000,000,000」になります。

データベースに4つのツイートしかない場合、1日目にデータベースにインデックスを作成するとします。CouchDBは、インデックスブロック内のキー値に次の構成を使用したくなるかもしれません。

[1-byte][8-bytes] <-- 9 bytes
4096 / 9 = 455 keys per block

ある時点でこれが壊れ、キー値をインデックスに保存するために可変バイト数が必要になります。

「tweet_message」などのような本当に可変長のフィールドにインデックスを付ける場合、ポイントはさらに明白です。

キー自体が完全に可変長であり、データベースがインデックスの作成および更新時に何らかの「最大キーサイズ」をインテリジェントに推測する方法がないため、これらのキー、これらのデータベースのインデックスのセグメントを表すブロック内に実際にどのよう格納されますか??

キーのサイズが可変であり、キーのブロックを読み取る場合、実際にいくつのキーが実際にあるかわからないだけではないに、リストの中央にジャンプしてバイナリを実行する方法もわかりません。それらを検索します。

これは私がすべてつまずいたところです。

古典的なSQLデータベースの静的型フィールド(bool、int、charなど)を使用すると、インデックスがキーの長さを事前に定義し、それに固執することができることを理解しています...しかし、この世界のドキュメントデータストアでは、 O(logn)時間でスキャンできるように、ディスク上のこのデータを効率的にモデル化する方法を当惑させ、ここで説明をいただければ幸いです。

説明が必要な場合はお知らせください!

更新(グレッグの回答)

グレッグの回答に添付された私のコメントをご覧ください。一週間以上の研究の後、私は彼が本当に気付いていない重要な値のデシリアライズを回避することで大きなパフォーマンスの勝利を提供しながら、インプラクティスは実装と使用が非常に簡単であるという驚くほどシンプルでパフォーマンスの良い提案に本当につまずいたと思います。

3つの個別のDBMS実装(CouchDB、kivaloo、およびInnoDB)を調べましたが、それらはすべて、実行環境(erlang / C)内の値を検索する前に、ブロック全体を内部データ構造に逆シリアル化することでこの問題を処理します。

これは、グレッグの提案についてとても素晴らしいと思います。通常の2048のブロックサイズには通常50以下のオフセットがあり、結果として非常に小さな数のブロックを読み込む必要があります。

更新(Gregの提案に対する潜在的なマイナス面)

この対話を自分自身で継続するために、これには次の欠点があることに気付きました...

  1. すべての「ブロック」にオフセットデータが付いている場合、ヘッダーで正しく開始されていないデータやブロックで読み込まれる可能性があるため、後の設定でブロックサイズを調整することはできません。複数のヘッダーが含まれていました。

  2. 巨大なキー値にインデックスを付けている場合(誰かがchar(8192)またはblob(8192)の列にインデックスを付けようとしている場合)、キーが1つのブロックに収まらず、2つのブロックにまたがってオーバーフローする必要がある可能性があります。つまり、最初のブロックにはオフセットヘッダーがあり、2番目のブロックはキーデータですぐに始まります。

このすべての解決策は、データベースブロックサイズを固定しないことです。調整周りにヘッダーブロックデータ構造を開発することです。たとえば、すべてのブロックサイズを4KB(通常はとにかく最適)に修正し、非常に小さい先頭に「ブロックタイプ」を含むブロックヘッダー。通常のブロックの場合は、ブロックヘッダーの直後にオフセットヘッダーを指定する必要があります。「オーバーフロー」タイプの場合、ブロックヘッダーの直後は生のキーデータです。

更新(潜在的な素晴らしいアップサイド)

ブロックが一連のバイトとして読み込まれ、オフセットがデコードされた後。技術的には、検索するキーを未加工バイトにエンコードし、バイトストリームで直接比較することができます。

探しているキーが見つかると、ポインターをデコードして追跡できます。

グレッグのアイデアのもう一つの素晴らしい副作用!ここでのCPU時間の最適化の可能性は十分に大きいため、固定ブロックサイズを設定するだけで、このすべてを獲得する価値があります。


このトピックに興味がある他の人にとって、Redisの主任開発者は、Redisの無効な「ディスクストア」コンポーネントを実装しようとして、まさにこの問題にぶつかりました。彼はもともと32バイトの「十分に大きい」静的キーサイズを選択していましたが、問題の可能性を認識し、代わりにキーのハッシュ(sha1またはmd5)を保存してサイズを統一することを選択しました。これにより、さまざまなクエリを実行できなくなりますが、ツリーとFWIWのバランスがうまく取れます。ここでは詳細はredis.hackyhack.net/2011-01-12.html
リヤードカラ

私が見つけたいくつかの詳細情報。SQLiteは、キーが取得できる大きさに上限があるように見えます。または、実際にキー値の上限を切り捨てて、残りをディスク上の「オーバーフローページ」に入れます。これにより、ランダムなI / Oが倍増するため、巨大なキーのクエリが恐ろしくなります。ここでは「Bツリーページ」セクションまでスクロールsqlite.org/fileformat2.html
リヤードカラに

回答:


7

インデックスを、固定サイズのオフセットのリストとして、キーデータを含むブロックに保存できます。例えば:

+--------------+
| 3            | number of entries
+--------------+
| 16           | offset of first key data
+--------------+
| 24           | offset of second key data
+--------------+
| 39           | offset of third key data
+--------------+
| key one |
+----------------+
| key number two |
+-----------------------+
| this is the third key |
+-----------------------+

(まあ、キーデータは実際の例でソートされますが、あなたはアイデアを得る)。

これは、インデックスブロックが実際にどのデータベースで構築されるかを必ずしも反映するものではないことに注意してください。これは、キーデータが可変長であるインデックスデータのブロックを整理する方法の一例にすぎませ


グレッグ、私はあなたの答えを事実上の答えとしてまだ選びませんでした。なぜなら、私は他のDBMSのさらなる調査と同様にフィードバックを望んでいるからです(元のQにコメントを追加しています)。これまでのところ、最も一般的なアプローチは上限キャップであるようで、オーバーフローキーは完全なキーが必要な場合にのみチェックされます。それほどエレガントではありません。あなたのソリューションには私が好きなエレガンスがありますが、キーがページサイズを大きくするエッジケースでは、オーバーフローテーブルが必要か、それを許可しないだけです。
リヤドカラ

スペースが足りなくなった...要するに、dbデザイナーがキーサイズに厳しい制限を課すことができるなら、あなたのアプローチは最も効率的で柔軟だと思います。スペースとCPU効率の素晴らしい組み合わせ。オーバーフローテーブルはより柔軟性がありますが、常にオーバーフローするキーの検索にランダムなI / Oを追加することは恐ろしいことです。これについての入力をありがとう!
リヤドカラ

グレッグ、私はこれについてますます考え、代替ソリューションを検討してきましたが、オフセットヘッダーのアイデアでそれを釘付けにしたと思います。ブロックを小さくしておけば、8ビット(1バイト)のオフセットで逃げることができます。より大きなブロックでは、最大128 KBまたは256 KBのブロックであっても妥当です(4または8バイトのキーを想定します)。大きな勝利は、オフセットデータをどれだけ安くて速く読み込むことができるか、そして結果としてどれだけのデシリアライゼーションを節約できるかです。素晴らしい提案、ありがとうございます。
リヤドカラ

また、これはUpscaleDBで使用されるアプローチです:upscaledb.com/about.html#varlength
マチューRodic
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.