Mysql ネストループ結合アルゴリズムと MRR

MySQL8 は、以前は 1 つの結合アルゴリズム (ネストされたループ) のみをサポートしていましたが、MySQL8 では、ネストされたループよりも効率的な新しいアルゴリズムのハッシュ結合が導入されました。(この結合アルゴリズムについては後で紹介する機会があります)

1. MySQL ドライバー テーブルとドリブン テーブルおよび結合の最適化

まず、結合接続中にどのテーブルが駆動テーブル (外部テーブルとも呼ばれる) であり、どのテーブルが駆動テーブル (内部テーブルとも呼ばれる) であるかを理解します。

  1. 左結合を使用する場合、左側のテーブルが駆動テーブル、右側のテーブルが駆動テーブルになります。
  2. 右結合を使用する場合、右側のテーブルが駆動テーブル、左側のテーブルが駆動テーブルになります。
  3. 結合を使用する場合、mysql オプティマイザーは比較的少量のデータを含むテーブルを駆動テーブルとして選択し、大きなテーブルを駆動テーブルとして選択します。

1.1) 結合クエリの駆動テーブルと駆動テーブルの選択方法

SQL の最適化では、大きなテーブルは常に小さなテーブルによって駆動されます。例: A は小さなテーブル、B は大きなテーブル 左結合を使用する場合は、次のように記述する必要があります。

select * from A a left join B b on a.code=b.code

このように、テーブル A が駆動テーブル、テーブル B が被駆動テーブルとなります。

1) ドライブテーブルの意味:

ネストループジョインやハッシュジョインでは、まずデータを取得し、このテーブルのデータを元に他のテーブルのデータを徐々に取得し、最終的に条件を満たすデータを全て見つけた最初のテーブルをドライブテーブルと呼びます。 。

駆動テーブルは必ずしもテーブルである必要はなく、データ セットである場合もあります。つまり、サブコレクションは特定のテーブルの条件を満たすデータ行で構成され、このサブコレクションはデータ ソースとして使用されます。他のテーブルを接続します。このサブコレクションが実際の駆動テーブルであり、便宜上、条件に従って最初にサブコレクションを取得するテーブルを直接駆動テーブルと呼ぶこともあります。

3 つ以上のテーブルがある場合は、結合アルゴリズムを使用して 1 番目と 2 番目のテーブルの結果セットが取得され、その結果セットが外部データとして使用され、結果セットが走査されて内部のデータがクエリされます。 3番目のテーブル。

2) 運転テーブルとしての小さなテーブル:

駆動テーブルは小さいテーブルでなければならないとよく言われますが、これは、エンティティ テーブル自体が小さい必要があるということではなく、条件に従って取得されるサブセットが小さい必要があることを意味します。小さいテーブル、大きいテーブルはドライブ テーブルとも呼ばれます。なぜなら:

小表驱动大表:需要通过140多次的扫描
for(140条){
  for(20万条){

  }
}
大表驱动小表:要通过20万次的扫描
for(20万条){
  for(140条){

  }
}

したがって、テーブル A とテーブル B のデータ量がほぼ同じであれば、どちらを駆動テーブルとして選択しても問題ないと結論付けることもできます。

例を見てみましょう: テーブル A には 140 個を超えるデータがあり、テーブル B には約 200,000 個のデータがあります。

select * from A a left join B b on a.code=b.code
执行时间:7.5s

select * from B b left join A a on a.code=b.code
执行时间:19s

3) 説明を通じて、ドライバー テーブルが誰であるかを表示します。

EXPLAIN 分析を使用すると、SQL の駆動テーブルを特定できます。EXPLAIN ステートメントによって分析される最初の行のテーブルが駆動テーブルです。

1.2) 駆動テーブルと駆動テーブルのインデックスが使用されます。

結合クエリにはインデックス条件があります。

  • ドライバーテーブルにはインデックスがあり、インデックスは使用されません
  • 駆動テーブルはインデックスを使用してインデックスを作成します

大きなテーブルを小さなテーブルで駆動する場合、大きなテーブルにインデックスを付けると実行速度が大幅に向上します。

テスト 1: テーブル A とテーブル B のインデックスを作成する

分析:EXPLAIN select * from A a left join B b on a.code=b.code

テーブル B コードのみがインデックスを使用します

テスト 2: テーブル A のコードのみにインデックスが付けられている場合はどうなりますか?

この場合、A テーブルのインデックスは無効になります。

結論は:

  • 大きなテーブルを小さなテーブルで駆動する
  • ドリブンテーブルにインデックスを作成する

2、ネストループ結合

MySQL 8.0 より前では、JOIN アルゴリズムは 1 つだけサポートされていました。Nested-Loop Join (ネストされたループ リンク) でした。Nested-Loop Join にはさまざまなバリエーションがあり、MySQL が JOIN 操作をより効率的に実行するのに役立ちます。

2.1) Simple Nested-Loop Join (単純なネストループ接続)

ネストループジョインのアルゴリズムは、簡単に言うと二重層のforループで、外表の行データをループして内表の全行データと1つずつ比較して結果を求めることで、擬似的にコードは次のとおりです。

select * from user tb1 left join level tb2 on tb1.id=tb2.user_id

for (user_table_row ur : user_table) {
    for (level_table_row lr : level_table) {
        if (ur.id == lr.user_id)) {
            //返回匹配成功的数据
        }
    }
}

特徴:

Nested-Loop Join はシンプルで大まかでわかりやすいです 2 層のループでデータを比較して結果を求めるものですが、明らかにアルゴリズムが荒すぎます 各テーブルのデータが 10,000 個ある場合、データの数は比較 = 10,000 * 1 10,000 = 1 億回、明らかにこの種のクエリ効率は非常に遅くなります。

もちろん、mysql は決して乱暴にテーブルを結合することはありませんので、Nested-Loop Join には 2 つの最適化アルゴリズムがあります。結合クエリを実行するとき、mysql は状況に応じて 2 つの最適な結合最適化アルゴリズムのいずれかを選択します。実行されました。

2.2) インデックスネストループ結合 (インデックスネストループ接続)

インデックス ネストループ結合の最適化のアイデアは、主に内部テーブルのデータの一致時間を短縮することです。簡単に言うと、インデックス ネストループ結合は、内部テーブルとの一致を避けるために、外部テーブルの一致条件を通じて内部テーブルのインデックスを直接一致させることです。テーブル データ。これにより、内部テーブルの一致数が、元の一致数 = 外部テーブルの行数 * 内部テーブルの行数 * から、外部テーブルの行数 * に大幅に減少します。内部テーブルのインデックスの高さにより、結合のパフォーマンスが大幅に向上します。

select * from user tb1 left join level tb2 on tb1.id=tb2.user_id
# 伪代码
for (user_table_row ur : user_table) {
    lookup level_user_id_index {
        if (ur.id == lr.user_id)) {
            //返回匹配成功的数据
        }
    }
}

注: インデックス ネストループ結合アルゴリズムを使用する前提は、一致するフィールドに駆動テーブル (内部テーブル) でインデックスが確立されている必要があるということです。

2.3) バッチ化されたキー アクセス結合 (BKA と呼ばれる)

前述の INLJ アルゴリズムには問題があり、駆動テーブルに関連付けられたインデックスが補助インデックスであり、クエリ フィールドをインデックスでカバーできない場合、データを組み立てるときにテーブルを返す操作が必要になります。ただし、一致するすべてのレコードをテーブルに返す場合、効率は決して高くありません。テーブルの戻りでは主キー インデックスを使用できますが、ここでの ID は必ずしも順序通りではないため、ランダムな分散読み取りにもなりますこの状況に対して、MySQL は最適化手段を提供し、Batched Key Access join と呼ばれるアルゴリズム、つまりバッチ主キー アクセス結合アルゴリズムを提供します。

1)BKA算法:

BKA アルゴリズムの原理は、まず条件に従って駆動テーブル内の修飾レコードをクエリし、結合バッファーに格納します。次に、インデックスに従って駆動テーブルのインデックス レコードを取得し、read_rnd_buffer に格納します。結合バッファまたは read_rnd_buffer のいずれかがいっぱいの場合は、最初にバッファ内のデータを処理します。主キーに従って、read_rnd_buffer 内の駆動テーブル インデックス レコードを昇順で並べ替えてから、この順序付けされたレコードに基づいてテーブルに戻ります。クエリでは、主キーのインデックスによってレコードが主キーに従って昇順に並べ替えられるため、テーブルに戻る効率が向上します。BKA アルゴリズムを有効にするには、batched_key_access を有効にする必要があります。

説明: まず MRR を理解してから BKA を見ると、非常に明確になります (下記)。BKA はデフォルトでは無効になっており、有効にするには実行する必要があります。

mysql> SET optimizer_switch='mrr=on,mrr_cost_based=on,batched_key_access=on';

ここでテストしてみてください:

set optimizer_switch='batched_key_access=off';
explain select a.*,b.* from b join a on b.key = a.key

ここでは、Batched_key_access が有効になっていません。a がドライバー テーブルであり、b.key フィールドにインデックスがあることがわかります。クエリ フィールドは a.*、b.* を使用します。必要なデータをテーブルに返す必要があります。ただし、INLJ アルゴリズムは引き続き使用されます。ここで、batched_key_access をオンにして、次のことを試してください。

set optimizer_switch='batched_key_access=on';
explain select a.*,b.* from b join a on b.key = a.key

 今回は BKA アルゴリズムが使用されており、結合バッファーが使用されていることがわかります。クエリフィールドに b.* を選択しない場合は、b.id フィールドのみが返されるため、カバーインデックスが使用できます。 :

set optimizer_switch='batched_key_access=on';
explain select a.*,b.id from b join a on b.key = a.key 

 BKA アルゴリズムはもう使用されていないことがわかりました。

2.4) ブロックネストループ結合 (キャッシュブロックネストループ接続、BNL と呼ばれる)

駆動テーブルの関連フィールドに使用可能なインデックスがない場合は、BNL アルゴリズムを使用する必要があります。このアルゴリズムでは、BKA アルゴリズムと同様に結合バッファーを使用する必要がありますが、read_rnd_buffer は使用されません。BNL と BKA のどちらを選択するかは、駆動テーブルに使用可能なインデックスがあるかどうかが重要です。ない場合は BNL を直接使用し、インデックスはあるがテーブルに戻る必要がある場合は BKA を使用します。

1) BNL アルゴリズム:

まず、条件に従ってドライバー テーブル内の修飾レコードを検索し、結合バッファーに格納します。結合バッファーに100 レコードしか格納できないが、ドライバー テーブルが条件を満たす場合、結果セットが 100 レコードを超える場合、取得できるレコードは 100 件のみです。これはバッチと呼ばれ、駆動テーブルが完全にスキャンされ、駆動テーブルの各行が結合バッファ内のレコードと照合され、一致したレコードが最終結果セットに入れられます。 。駆動テーブルがスキャンされた後、結合バッファーがクリアされ、残りのレコードが繰り返し駆動テーブルから取得されて結合バッファーに格納されます。その後、駆動テーブルが完全にスキャンされ、データが結合バッファー内で照合されます。データが一致するまでこのサイクルが繰り返されます。

説明: MySQL 8.0.18 より前では、結合中に駆動テーブルのインデックスを使用できない場合、BNL が結合に使用されます。MySQL 8.0.18 以降では、この場合にハッシュ結合最適化アルゴリズムが使用されます。MySQL 8.0.20 以降、MySQL は BNL アルゴリズムを使用しなくなり、以前に BNL が使用されていたすべてのケースでハッシュ結合アルゴリズムを使用します。

# SQL
select * from R join S on R.r=S.s
# 伪代码
for each tuple r in R do                             # 扫描外表R
    store used columns as p from R in join buffer    # 将部分或者全部R记录保存到join buffer中,记为p
    for each tuple s in S do                         # 扫描内表S
        if p and s satisfy the join buffer           # p与s满足join条件
           then output the tuple <p,s>               # 返回结果集

処理の流れ:

  1. フィルター条件を満たすドライバーテーブル内のすべてのレコード (SQL クエリのすべてのフィールド) を走査し、それらを結合バッファーに入れます。
  2. 駆動テーブル内のすべてのレコードが条件を満たす場合、それらを結合バッファーに入れ、駆動テーブル内のすべてのレコードを走査し、結合条件を満たすレコードの結果セットを取得します。
  3. 結合バッファーにすべてのドライバー テーブル レコードを一度に保存できない場合は、バッチでレコードを結合バッファーに読み取り、2 番目の手順を繰り返すことができます。

注: バージョン 5.6 以降では、オプティマイザー管理パラメーター optimizer_switch の block_nested_loop パラメーターによって、BNL がオプティマイザーに使用されるかどうかが制御されます。これはデフォルトで有効になっており、オフに設定されている場合、オプティマイザは結合方法を選択するときに NLJ アルゴリズムを選択します。

mysql> show variables like 'optimizer_switch'\G;
block_nested_loop=on # BNL优化,默认打开

mysql> set optimizer_switch='block_nested_loop=on'; # 开启BNL

2) BNL の紹介:

BNL は主に、駆動テーブルの関連フィールドにインデックスがない場合の最適化を目的としています (駆動テーブルにインデックスがない場合、またはインデックスが失敗した場合、INLJ は使用できず、mysql は BNL を通じて最適化します)。 Extra 値に使用が含まれている場合、結合バッファー (ブロック ネスト ループ) で、タイプ値が ALL、インデックス、または範囲の場合、BNL が使用されていることを意味します。また、ドリブン テーブルのテーブル関連フィールドにインデックスがないか、インデックスが不足していることも意味します。は無効であるため、インデックスを効果的に使用できません。

BNL アルゴリズムは SNLJ アルゴリズムを最適化したもので、最適化のためにアルゴリズム BNL を INLJ に昇格させることができます。SQL スキャン データの場合、ドライバー テーブルのスキャン数は 1、駆動テーブルのスキャン数はドライブ テーブルのレコード サイズ/join_buffer_size、SQL のスキャン レコードの場合、SQL によってスキャンされた行数です。 = ドライブ テーブルのレコード数 + (ドライバー テーブルのレコード/join_buffer_size) * ドリブン テーブルのレコード数。

join_buffer_size のサイズを増やすと、BNL アルゴリズムを使用した SQL の実行効率がある程度向上します。

# 手动调整 join_buffer_size 的大小
mysql> show variables like '%join_buffer_size%';
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| join_buffer_size | 262144 |
+------------------+--------+
1 row in set (0.04 sec)

mysql> set join_buffer_size=1024;
Query OK, 0 rows affected (0.04 sec)

mysql> show variables like '%join_buffer_size%';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| join_buffer_size | 1024  |
+------------------+-------+
1 row in set (0.03 sec)

3) BNL の特徴:

  1. join_buffer_size 変数はバッファ サイズを決定します。
  2. 結合バッファは、結合タイプが all、index、または range の場合にのみ使用できます。
  3. バッファリングできる各結合にはバッファが割り当てられます。これは、クエリが最終的に複数の結合バッファを使用する可能性があることを意味します。
  4. 最初の非定数テーブルは、スキャン タイプが all またはインデックスであっても、結合バッファを割り当てません。
  5. 結合バッファは結合前に割り当てられ、クエリの実行後に解放されます。
  6. データ行全体ではなく、結合に参加している列のみが結合バッファーに保存されます。

 3、MRR

MRR (正式名は「Multi-Range Read Optimization」) は MySQL 5.6 の新機能です。簡単に言えば、MRR は「ランダム ディスク読み取り」を「シーケンシャル ディスク読み取り」に変換し、それによってインデックス クエリのパフォーマンスを向上させます。

3.1) mysql はどのようにしてディスクからデータを読み取りますか?

範囲クエリを実行します。

mysql > explain select * from stu where age between 10 and 20;
+----+-------------+-------+-------+------+---------+------+------+-----------------------+
| id | select_type | table | type  | key  | key_len | ref  | rows | Extra                 |
+----+-------------+-------+-------+----------------+------+------+-----------------------+
|  1 | SIMPLE      |  stu  | range | age  | 5       | NULL |  960 | Using index condition |
+----+-------------+-------+-------+----------------+------+------+-----------------------+

この SQL が実行されると、次の図に示すように、MySQL はディスクに移動してデータを読み取ります (データがデータ バッファー プールにないと仮定して)。 

 

図中の赤線がクエリ処理全体、青線がディスクの移動経路です。

この図は Myisam のインデックス構造に従って描かれていますが、Innodb にも適用できます。Myisam の場合、左側はフィールド age の 2 次インデックスで、右側は完全な行データが格納される場所です。Innodb も同様で、Innodb はクラスター インデックス (クラスター インデックス) であるため、右側をリーフ ノード上の完全なデータを持つ B+ ツリーに置き換えるだけで済みます。

以下の Myisam エンジン分析:

  1. まず、左側のセカンダリ インデックスに移動して、条件を満たす最初のレコードを見つけます (実際、各ノードはページであり、ページには多数のレコードを含めることができます。ここでは、各ページにレコードが 1 つだけあると仮定します)。右側にあるこのデータの完全なレコードを取得します。
  2. 読み込んだら左に戻って条件を満たす次のレコードを探し続け、見つかったら右に読み込むと、このデータは前のデータと続いていることがわかります。物理的な保管場所という点では、遠いです。
  3. どうすればよいかというと、方法はありません。ディスクと磁気ヘッドを一緒に機械的に動かしてこのデータを読み取るしかありません。3番目と4番目も同様で、データを読み取るたびにディスクと磁気ヘッドが長距離を移動する必要があります。

説明: MySQL は実際には「ページ」単位でデータを読み取りますが、ここではこれらのデータがたまたま異なるページに配置されていると仮定します。さらに、「ページ」という概念は、実際にはオペレーティング システムの不連続なメモリ管理メカニズムに由来しており、同様の「セグメント」が存在します。

注: 10,000 RPM (回転数/分) の機械式ハードディスクは、1 秒あたり約 167 回のディスク読み取りを実行できるため、極端な場合、MySQL は 1 秒あたり 167 個のデータしか返せません。これは、CPU キュー時間をカウントするのに十分ではありません。

3.2) シーケンシャルリード

この時点で、ランダム ディスク アクセスがいかに贅沢であるかがわかるので、ランダム アクセスをシーケンシャル アクセスに変換することは明らかです。

mysql > set optimizer_switch='mrr=on';
Query OK, 0 rows affected (0.06 sec)

mysql > explain select * from stu where age between 10 and 20;
+----+-------------+-------+-------+------+---------+------+------+----------------+
| id | select_type | table | type  | key  | key_len | ref  | rows | Extra          |
+----+-------------+-------+-------+------+---------+------+------+----------------+
|  1 | SIMPLE      | tbl   | range | age  |    5    | NULL |  960 | ...; Using MRR |
+----+-------------+-------+-------+------+---------+------+------+----------------+

MRR を有効にして SQL ステートメントを再実行したところ、Extra に追加の「MRR の使用」があることがわかりました。

MySQL クエリのプロセスは次のようになります。

  • Myisam の場合、ディスクにアクセスして完全なデータを取得する前に、ROWID に従ってデータがソートされ、ディスクが順番に読み取られます。
  • Innodb の場合、クラスター化インデックスのキー値に従って並べ替えられ、クラスター化インデックスが順番に読み取られます。

連続読み取りにはいくつかの利点があります。

  1. ディスクと磁気ヘッドは機械的に前後に移動する必要がなくなりました。
  2. ディスクの事前読み取りを最大限に活用できます。たとえば、クライアントが 1 ページのデータを要求した場合、次のページのデータもまとめて返してデータ バッファー プールに入れることができます。次回必要になる場合は、再度ディスクに読み取る必要はありません。この理論的根拠は、コンピュータ サイエンスにおけるよく知られた局所性の原則です。つまり、データの一部が使用されると、通常は近くのデータがすぐに使用されます。
  3. クエリでは、各ページのデータはディスクから 1 回だけ読み取られます: MySQL がディスクからページのデータを読み取った後、データはデータ バッファ プールに配置されます。ディスクから読み取り、メモリから直接読み取ります。しかし、ソートされていない場合は、おそらく 1 ページ目のデータを読んだ後、2 ページ目、3 ページ目、4 ページ目のデータを読み、次に 1 ページ目のデータを読むことになります。この時点で、そのページが見つかります。 1 ページのデータはキャッシュから削除されているため、最初のページのデータをディスクから再度読み取る必要があります。シーケンシャル読み取りに変換した後も、引き続きページ 1 のデータを使用します。このとき、MySQL のキャッシュ削除メカニズムに従って、このページのデータを使い切るまでこのページのキャッシュは無効になりません。これはシーケンシャル読み取りです。このクエリの残りのプロセスでは、このページのデータは再度使用されないことが確実なので、このページのデータに別れを告げることができます。

シーケンシャル読み取りは、これら 3 つの側面を通じてインデックス読み取りを最大限に最適化します。インデックス自体はディスク IO を削減してクエリを高速化するためのものであり、MRR はディスク IO の削減におけるインデックスの役割をさらに強化するものであることを忘れないでください。

3.3) MRRの構成について

MRR に関連する構成は2 つあります。

  • ミスター:オン/オフ
  • mrr_cost_based: オン/オフ

1)mrr=オン/オフ

MRR をオンにするために使用されるスイッチ。オンにしないと、MRR は使用されません。

mysql > set optimizer_switch='mrr=on';

2)mrr_cost_based=オン/オフ

これは、MRR を使用するコストに基づいて MRR を使用するかどうかをオプティマイザーに指示し、MRR を使用する価値があるかどうかを検討し (コストに基づく選択)、特定の SQL ステートメントで MRR を使用するかどうかを決定するために使用されます。明らかに、1 行のデータのみを返すクエリには MRR は必要ありません。また、mrr_cost_based を off に設定すると、オプティマイザは完全に MRR を使用しますが、場合によってはこれは非常に愚かであるため、この構成を引き続き に設定することをお勧めします。結局のところ、オプティマイザはほとんどの場合に正しいのです。

3)read_rnd_buffer_size

read_rnd_buffer_size構成もあり  、これはROWIDのソートに使用されるメモリーのサイズを設定するために使用されます。明らかに、MRR は本質的に空間と時間を交換するアルゴリズムです。MySQL ではソート用に無制限のメモリを提供することは不可能です。read_rnd_buffer がいっぱいの場合、まず完全な rowid をソートしてディスクから読み取り、次にクリアして、read_rnd_buffer が上限に達するまで rowid を格納し続けます。 read_rnd_buffer の設定を再度変更し、上限などを設定します。

さらに、MySQL のブランチの 1 つである Mariadb は、MySQL の MRR に多くの最適化を行っています。興味のある学生は、記事の最後にある推奨事項を読むことができます。

3.4) エピローグ

MRR がインデックスと大きく関係していることもわかります。

インデックス作成は、MySQL によるクエリの最適化であり、元々バラバラだったデータを整然とした構造に編成するため、テーブル全体のスキャンがルールベースのクエリになります。私たちが話している MRR は、インデックスベースのクエリに対する MySQL の最適化であり、最適化の中の最適化であると言えます。

MySQL クエリを最適化するには、まず MySQL クエリ プロセスを理解する必要があり、インデックス クエリを最適化するには、MySQL インデックスの原則を知る必要があります。「 MySQL の学習方法」で述べたように、テクノロジーを最適化し、そのチューニング方法を学ぶには、まずその原理を理解する必要があります。この 2 つは異なるレベルです。

推奨読書:

おすすめ

転載: blog.csdn.net/liuxiao723846/article/details/129315753