環境
この質問は、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ブロック)
- ソートされたブロックをバイナリ検索してキーを見つけます
- 値からデータファイルオフセットを取得します
- オフセットを使用してデータファイル内のレコードを検索する
- データを呼び出し元に返す
質問のセットアップ...
さて、ここで質問がまとめられます...
ステップ#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の提案に対する潜在的なマイナス面)
この対話を自分自身で継続するために、これには次の欠点があることに気付きました...
すべての「ブロック」にオフセットデータが付いている場合、ヘッダーで正しく開始されていないデータやブロックで読み込まれる可能性があるため、後の設定でブロックサイズを調整することはできません。複数のヘッダーが含まれていました。
巨大なキー値にインデックスを付けている場合(誰かがchar(8192)またはblob(8192)の列にインデックスを付けようとしている場合)、キーが1つのブロックに収まらず、2つのブロックにまたがってオーバーフローする必要がある可能性があります。つまり、最初のブロックにはオフセットヘッダーがあり、2番目のブロックはキーデータですぐに始まります。
このすべての解決策は、データベースブロックサイズを固定しないことです。調整周りにヘッダーブロックデータ構造を開発することです。たとえば、すべてのブロックサイズを4KB(通常はとにかく最適)に修正し、非常に小さい先頭に「ブロックタイプ」を含むブロックヘッダー。通常のブロックの場合は、ブロックヘッダーの直後にオフセットヘッダーを指定する必要があります。「オーバーフロー」タイプの場合、ブロックヘッダーの直後は生のキーデータです。
更新(潜在的な素晴らしいアップサイド)
ブロックが一連のバイトとして読み込まれ、オフセットがデコードされた後。技術的には、検索するキーを未加工バイトにエンコードし、バイトストリームで直接比較することができます。
探しているキーが見つかると、ポインターをデコードして追跡できます。
グレッグのアイデアのもう一つの素晴らしい副作用!ここでのCPU時間の最適化の可能性は十分に大きいため、固定ブロックサイズを設定するだけで、このすべてを獲得する価値があります。