序文
特定のSQLを実行して速度が遅くなると、無意識のうちにインデックスが追加されたかどうかに反応するため、インデックスを追加してデータ検索を高速化する理由や、インデックスの基になるレイヤーを格納するために使用する構造について考えたことはありますか?はい、タイトルを読んだ後、誰もが答えを持っていると思います、はい!B +ツリー!では、一般的なリンクリストやハッシュなどとはどのように違うのでしょうか。なぜ、ほとんどのストレージエンジンはそれを使用するのでしょうか?今日はB +ツリーのデバンキングを行います。この記事を読んだ後、B +ツリーはもう不思議ではないと思います。次の高頻度のインタビューの質問を理解することは非常に役立ちます。
-
インデックスがB +ツリーを基本的なデータ構造として一般的に使用する理由
-
B +ツリーインデックスに加えて、どのインデックスを知っていますか
-
自己増分IDを主キーとして推奨するのに、主キーを自己構築できないのはなぜですか
-
ページ分割、ページ結合とは
-
インデックスに基づいて行レコードを見つける方法
この記事では、次の点からB +ツリーについて説明します
-
定義の問題
-
いくつかの一般的なデータ構造の比較
-
ページ分割とページ結合
定義の問題
インデックスの最下層がB +ツリーを使用する理由を知るには、それがどの問題を解決するかによって異なります。
次のユーザーテーブルがあるとします。
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL COMMENT '姓名',
`idcard` varchar(20) DEFAULT NULL COMMENT '身份证号码',
`age` tinyint(10) DEFAULT NULL COMMENT '年龄',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户信息';
通常、次の要件があります。
1.ユーザーIDに基づいてユーザー情報を確認する
select * from user where id = 123;
2.間隔の値に基づいてユーザー情報を見つける
select * from user where id > 123 and id < 234;
3. IDで逆順に配置し、ページ内のユーザー情報を取得します
select * from user where id < 1234 order by id desc limit 10;
上記の一般的なSQLから、インデックスで使用されるデータ構造は次の3つの条件を満たす必要があることがわかります。
-
特定の値に基づく正確で高速な検索
-
間隔値の上限と下限に基づいて、この間隔のデータをすばやく見つけます
-
インデックス値はソートする必要があり、高速な順序検索と逆順検索をサポートします
次に、主キーインデックス(idインデックス)を例として取り上げ、対応するデータ構造でそれを構築する方法を確認します。
いくつかの一般的なデータ構造の比較
次に、上記の条件を満たすデータ構造について考えます。
1.ハッシュテーブル
ハッシュテーブル(ハッシュテーブルとも呼ばれます)は、キー値(キー値)に基づいて直接アクセスされるデータ構造です。これにより、ハッシュ関数の変換を通じてコード値をハッシュテーブルの対応する位置にマップでき、検索効率が非常に高くなります。ハッシュインデックスは、ハッシュテーブルに基づいて実装されます。名前にハッシュインデックスが設定されていると仮定すると、次の図に検索プロセスを示します。
データの各行について、ストレージエンジンはすべてのインデックス列(上図の名前列)のハッシュコード(上図のハッシュテーブルの位置)を計算し、ハッシュテーブルの各要素はデータ行のポインターを指します。対応するハッシュ値自体を格納するだけなので、インデックス構造は非常にコンパクトなので、ハッシュインデックスの検索速度が非常に速くなります。ただし、ハッシュインデックスには次のような欠点もあります。
-
ハッシュインデックスの場合、インデックスのすべての列に完全に一致するクエリのみが有効です。たとえば、列(A、B)にハッシュインデックスを設定しました。データ列Aのみがクエリされる場合、インデックスは使用できません。
-
ハッシュインデックスは、インデックス値に従って順番に格納されないため、ソートに使用できません。つまり、間隔に従って迅速に検索できません。
-
ハッシュインデックスにはハッシュ値と行ポインタのみが含まれ、フィールド値は格納されないため、行の読み取りを回避するためにインデックスの値を使用することはできません。ただし、ほとんどの場合、これはハッシュインデックスのほとんどがメモリ内で実行されるためです。問題ない
-
ハッシュインデックスは、=、IN()を含む同等の比較クエリのみをサポートし、年齢> 17などの検索範囲をサポートしません
要約すると、ハッシュインデックスは特定の状況にのみ適しています。正しく使用すると、実際にパフォーマンスを大幅に向上させることができます。たとえば、InnoDBエンジンには、「アダプティブハッシュインデックス」と呼ばれる特別な関数があります。 InnoDBは、特定のインデックス列の値が頻繁に使用されることに気づくと、メモリ内のB +ツリーインデックスに基づいてハッシュインデックスを作成します。これにより、B +ツリーには、高速ハッシュなどのハッシュインデックスの利点もあります。見つける。
2.リンクされたリスト
二重リンクリストは、以下に示すように、順次検索と逆順検索をサポートしています。
ただし、特定の値または間隔ですばやく検索するという私たちの言ったことがサポートされていないことは明らかです。さらに、テーブル内のデータは常に増加しており、インデックスは時間内に挿入および更新される必要があります。リンクされたリストは、データの迅速な挿入をサポートしていないため、リンクされたリストに基づいて変換するかどうかにかかわらず、高速な検索、更新、削除をサポートします。私たちのニーズにぴったり合う構造があり、ここではジャンプテーブルの概念を紹介します。
ジャンプテーブルとは何ですか?簡単に言うと、ジャンプテーブルは、リンクリストの上に複数のインデックスレイヤーを追加することによって形成されます。以下に示すように
今度は7-13の間隔でレコードを検索したいとします。最初から検索する必要がなくなり、上の図のセカンダリインデックスで検索を開始する限り、3回トラバースした後にリンクリストの間隔位置を見つけることができ、時間の複雑さはO( logn)、非常に高速で、このようにして、ジャンプテーブルは私たちのニーズを満たすことができます。実際、その構造はB +ツリーに非常に近いですが、B +ツリーはバランスのとれた二分探索ツリーから進化しています。次に、バランスのとれた二分探索木をB +木に変換する方法を段階的に見ていきます。
まず、平衡型二分探索木とは何かを見てください。平衡型二分探索木には、次の特性があります。
-
左側のサブツリーが空でない場合、左側のサブツリー上のすべてのノードの値は、そのルートノードの値よりも小さくなります。
-
右側のサブツリーが空でない場合、右側のサブツリーのすべてのノードの値は、ルートノードの値以上になります。
-
各非リーフノードの左サブツリーと右サブツリーの間の高さの差の絶対値(バランス係数)は最大で1です。
次の図は、平衡型二分探索木です
その特性から、バランスのとれた二分探索木でノードを見つける時間の複雑さがO(log2n)であることがわかります。
それをB +ツリーに変換します
主な違いは、すべてのノード値が二重リンクリストで最後のリーフノードに接続されていることです。これをジャンプテーブルと慎重に比較してください。非常によく似ていますか?この間隔で数値を検索する場合15〜27 15ノード(時間の複雑さlogn = 3回)を見つけてから、正面から27ノードに移動すると、この間隔でノードを見つけることができるため、前述の3つのニーズを完全にサポートします。値をすばやく見つけます。インターバル、逆順検索。
1億のノードがあり、各ノードが何回クエリを実行する必要があると仮定します。明らかに最大はlog21億= 27倍です。これらの1億のノードがメモリにある場合、27倍は明らかに問題ではなく、非常に高速であると言えます。しかし、新しい問題が発生します。メモリ内のこれらの1億のノードのサイズはどれくらいですか?簡単に計算してみましょう。ノードあたり16バイトを想定すると、1億のノードはおそらく1.5Gのメモリを占有します。メモリなどの貴重なリソースの場合、それはひどいスペース消費です。これは単なるインデックスです。通常、テーブルまたはライブラリの複数のテーブルに複数のインデックスを定義します。この場合、メモリはすぐにいっぱいになります!したがって、メモリにB +ツリーインデックスを完全にロードすること、それを解決する方法は明らかに問題です。
ディスクに入れることはできません。ディスク領域はメモリよりも多くなりますが、新たな問題が再び発生します。メモリとディスクの読み取り速度が大きすぎることがわかっています。通常、メモリはナノ秒のオーダーです。ディスクはミリ秒であり、同じサイズのデータを読み取ると、2つの間の差は数万回になる可能性があるため、前のステップで計算した27のクエリは、ディスクに配置されている場合は非常にひどくなります(ノードの検索は1回と見なすことができます)ディスクIO、つまり27のディスクIOがあります!)、27のクエリを最適化できますか?
クエリの数がツリーの高さに関係していること、およびツリーの高さが関係していることを明確に見ることができます。これは、各ノードの子ノードの数、つまりNフォークツリーのNに明らかに関係しています。ここで、16の数があると仮定すると、バイナリツリーと5ツリーツリーを使用して構築し、ツリーの高さを確認します
バイナリツリーを使用する場合は、5つのノードをトラバースする必要があることがわかります。5つのツリーを使用する場合は、3回トラバースするだけで、ディスクIOが2倍に減少します。上記の1億のノードを振り返ると、100ツリー構築するために必要なIOの数
5回までトラバースすることがわかります(実際、ルートノードは通常メモリに格納されるため、4回と見なすことができます)。ディスクIOが27から5に減少しました!パフォーマンスは大幅に改善されたと言えます。5倍はまだ多すぎると言う人もいます。100フォークツリーを1000または10000フォークツリーに変更して、IOの数なしでIOの数をさらに減らすことはできますか。
ここでは、(ページサイズは通常4キロバイトである)ページサイズを読み込むことであり、ディスクは各読みます、メモリやディスクのいずれか、オペレーティングシステムのページ(ページ)の概念を理解する必要があり、コンピュータで前を読み取り、連続データを事前にメモリに読み取り、複数のIOを回避するために、これはコンピューターで有名な局所性原理です。つまり、私はデータの一部を使用します。このデータの近くのデータも同様に使用し、一緒にロードするだけで、複数のIOが節約されて速度が低下します。この連続データの大きさは、オペレーティングシステムのページサイズの整数倍でなければなりません。この連続データはMySQLページであり、デフォルト値は16 KBです。つまり、 B +ツリーのノードは、ページサイズ(16 KB)に設定するのが好ましいため、B +ツリーのノードには1つのIO読み取りしかありません。
このページサイズは大きいのでしょうか?設定が大きいほど、ノードが保持できるデータが多くなります。ツリーの高さが小さいほど、IOは小さくなります。ここでは、ページサイズに注意する必要があります。 InnoDBは、メモリのプールバッファーを介してディスクから読み取られたページデータを管理します。ページが大きすぎると、バッファプールがすぐにいっぱいになり、メモリとディスクの間でページが頻繁にスワップインおよびスワップアウトされ、パフォーマンスに影響を与える可能性があります。
上記の分析から、各ノードのサイズがページのサイズ(16kb)と等しくなるように各ノードのサイズが選択されている限り、NフォークツリーでNを設定する方法を推測することは難しくないと思います。
ページ分割とページ結合
では、最初の質問を見てみましょう。主キーとして自己増分IDを推奨するのはなぜですか?主キーを作成することは不可能ですか?ユーザーのIDは一意であり、主キーとして使用できると言う人もいます。IDが主キーとして使用されていると仮定すると、何が問題でしょうか?
インデックスの順序を維持するために、B +ツリーは、レコードが挿入または更新されるたびにインデックスを更新します。IDカードに基づく元のB +ツリーが次のとおりであると想定します(バイナリツリーを想定して、IDカードの最初の4桁のみが図にリストされています)。
これで、3604で始まるIDカードに対応するレコードがdbに挿入されました。この時点で、インデックスを更新する必要があります。ソートによって更新する場合は、明らかに、この3604のID番号を左側のノード3504の後に挿入する必要があります(下図に示すように、バイナリツリーを想定しています)。
ID番号3604が3504の後に挿入された場合、このノードの要素数は3になり、明らかにバイナリツリーの条件を満たしていないため、ページ分割が発生します。このノードを調整して、バイナリツリーに準拠させる必要があります状態
図に示すように、調整後、バイナリツリーの条件が満たされます。
ページ分割によるこの調整は、必然的にパフォーマンスの低下につながります。特に、IDカードが主キーとして使用されている場合は、IDカードのランダム性により、必然的に多数のランダムなノード挿入が発生し、その結果、多数のページ分割が発生し、その結果、ページ分割が発生します。パフォーマンスが大幅に低下します。自己増加IDが主キーとして使用されている場合、新しく挿入されたテーブルで生成されたIDはインデックス内のすべての値よりも大きいため、既存のノードと組み合わせる必要があります(要素の数がいっぱいではありません) 、または新しく作成したノードに配置して(下図を参照)、自己増分IDが主キーとして使用されている場合、ページ分割の問題はありません。これをお勧めします!
ページ分割がある場合、ページマージが必要です。ページマージはいつ発生しますか?テーブルレコードが削除されると、インデックスも削除されます。このとき、図に示すように、ページマージが発生する可能性があります。
id 7、9に対応する行を削除すると、上の図のインデックスが更新され、7、9が削除されます。このとき、8、10は1つのノードに結合されます。それ以外の場合、8、10は2つのノードに分散されます。上記では、2つのIO読み取りが発生する可能性があり、必然的に検索効率に影響します!次に、ページマージが発生するときに、ノード数がN / 2未満の場合、たとえば、Nフォークツリーに対してしきい値を設定できます。近くのノードとマージする必要がありますが、マージされたノードの要素のサイズがNを超えてページ分割が発生する可能性があることに注意してください。親ノードはNフォークツリーを満たすように調整する必要があります。
インデックスに基づいて行レコードを見つける方法
上記のB +ツリーインデックスの概要を読んだ後は、誰もが疑問を持つ必要があると思います。対応するインデックス値に従って行レコードを見つける方法。実際、対応する行レコードは最後のリーフノードに配置され、インデックス値が見つかり、それが見つかります。ラインレコード。示されているように
非リーフノードはインデックス値のみを格納し、最後の行にのみ行レコードを格納します。これにより、インデックスサイズが大幅に削減され、インデックス値が見つかる限り、行レコードが見つかるので、効率も向上します。
リーフノードでレコードの行全体を格納するこの種のインデックスはクラスター化インデックスと呼ばれ、その他は非クラスター化インデックスと呼ばれます。
B +ツリーのまとめ
要約すると、B +ツリーには次の特性があります。
-
各ノードの子ノードの数はNを超えることはできず、N / 2未満にすることもできません(そうしないと、ページ分割またはページマージが発生します)。
-
ルートノードの子ノードの数はm / 2を超えることはできません。これは例外です
-
mフォークツリーはインデックスのみを格納し、実際にはデータを格納せず、最後の行のリーフノードのみが行データを格納します。
-
リーフノードはリンクされたリストを介して直列に接続されているので、間隔で検索すると便利です
まとめ
この記事では、日常の一般的なSQLのB +ツリーの特徴をまとめています。誰もがB +ツリーインデックスをより明確に理解している必要があると思います。なぜ、B +ツリーを学習した後、元のオリジナルを習得する必要があるのでしょうか。私が提起したいくつかの質問は実際には同じです。最下層を深く掘り下げると、常に変化することができます。