定義テーブル:
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`city` varchar(16) NOT NULL,
`name` varchar(16) NOT NULL,
`age` int(11) NOT NULL,
`addr` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `city` (`city`)
) ENGINE=InnoDB;
SQL ステートメント:
select city,name,age from t where city='杭州' order by name limit 1000 ;
都市が「杭州」であるすべての人の名前をクエリし、名前順に並べ替えた最初の 1000 人の都市名年齢を返します。
インデックスを作成します。
CREATE INDEX index_city ON `t` (city);
その実行フローを見てみましょう。
すべてのフィールドを並べ替える
まず、インデックス構造を見てみましょう
索引は都市のフィールドによってソートされています
まず、MYSQL はソートのために各スレッドにスペース sort_buffer を割り当てます。
この場合、SQL ステートメントのこの行の実行シーケンスは次のようになります。
sort_buffer を初期化します。都市、名前、年齢の 3 つのフィールドを入力します。
インデックスからフィルター条件を満たす 最初の主キー ID 、 つまり ID.x を見つけます。
主キー ID を使用して行全体を取り出し、次に都市、名前、年齢の 3 つのフィールドを取り出してsort_buffer に入れます。
インデックスから次のレコードの主キー ID を取得します。
上記の 2 つの手順を繰り返して、フィルター条件が満たされていないことを確認します。
sort_buffer 内のデータを名前ですばやく並べ替えます
ソート結果に従って最初の 1000 行を取得し、クライアントに返します。
このソート方法をフルフィールドソートと呼びます。
「名前による並べ替え」のアクションは、並べ替えに必要なメモリとパラメータ sort_buffer_size に応じて、メモリ内で実行される場合もあれば、外部並べ替えを使用する必要がある場合もあります。
sort_buffer_size は、ソートのために MySQL によって開かれたメモリ (sort_buffer) のサイズです。ソートされるデータの量が sort_buffer_size 未満の場合、ソートはメモリ内で行われます。ただし、並べ替えられたデータの量が大きすぎてメモリに格納できない場合は、並べ替えを支援するために一時ディスク ファイルを使用する必要があります。つまり、外部ソートでは、通常はマージ ソート アルゴリズムが使用されます。MySQL ではソート対象のデータが 12 個の部分に分割され、各部分が個別にソートされてこれらの一時ファイルに保存されることが非常に簡単に理解できます。次に、これらの 12 個の順序付けされたファイルを 1 つの順序付けられた大きなファイルにマージします。(必ず12分割する必要はありません)
sort_buffer_size がソート対象のデータのサイズを超える場合、number_of_tmp_files は 0 になり、メモリ内で直接ソートを実行できることを示します。
それ以外の場合は、一時ファイルに並べ替える必要があります。sort_buffer_size が小さいほど、より多くのコピーに分割する必要があり、number_of_tmp_files の値が大きくなります。
ROWIDソート
上記のアルゴリズム プロセスでは、元のテーブルのデータのみが 1 回読み取られ、残りの操作は sort_buffer と一時ファイルで実行されます。しかし、このアルゴリズムには問題があります。つまり、クエリによって返されるフィールドが多すぎる場合、sort_buffer に配置できるフィールドが多すぎるため、メモリに格納できる行の数が制限されてしまいます。同時に非常に小さいので、多くの一時ファイルに分割する必要があります。
したがって、単一の行が大きい場合、この方法は十分に効率的ではありません。
では、ソートされた単一行の長さが大きすぎると考えた場合、MySQL はどうするのでしょうか?
次に、パラメータを変更して、MySQL が別のアルゴリズムを使用できるようにします。
SET max_length_for_sort_data = 16;
max_length_for_sort_data は、ソートに使用される行データの長さを特に制御する MySQL のパラメータです。これは、単一行の長さがこの値を超える場合、MySQL は単一行が大きすぎるため、アルゴリズムを変更する必要があるとみなします。
city、name、age の 3 つのフィールド定義の合計の長さは 36 です。max_length_for_sort_data を 16 に設定しました。計算プロセスにどのような変更があるかを見てみましょう。
新しいアルゴリズムでは、sort_buffer のフィールドに、ソート対象の列 (name フィールド) と主キー ID のみを配置します。
ただしこの時点では、都市と年齢のフィールドの値が欠落しているため、ソート結果を直接返すことができず、全体の実行プロセスは次のようになります。
- sort_buffer を初期化します。必ず 2 つのフィールド、つまり name と id を入力してください。
- インデックス都市から city='Hangzhou' の条件を満たす最初の主キー ID (図の ID_X) を見つけます。
- 主キー ID インデックスに移動して行全体を取得し、name と ID の 2 つのフィールドを取得して、sort_buffer に格納します。
- インデックス都市からレコードの主キー ID を取得します。
- city='Hangzhou' 条件が満たされなくなるまで、つまり図の ID_Y になるまで、手順 3 と 4 を繰り返します。
- sort_buffer 内のデータをフィールド名に従って並べ替えます。
- ソート結果を走査し、最初の 1000 行を取得し、id の値に従って元のテーブルに戻り、都市、名前、年齢の 3 つのフィールドを取り出してクライアントに返します。
このアルゴリズムはスペースを節約しているように見えますが、7 番目のステップでもう一度テーブルを走査する必要があります。
フルフィールドソート VS ROWID ソート
これら 2 つの実行プロセスからどのような結論が導き出せるかを分析してみましょう。
MySQL がソート メモリが小さすぎてソート効率に影響を与えることを本当に懸念している場合は、ROWID ソート アルゴリズムを使用して、ソート プロセス中に一度により多くの行をソートできるようにしますが、元の状態に戻る必要があります。データをフェッチする元のテーブル。
MySQL がメモリが十分に大きいと判断した場合、すべてのフィールドによるソートを優先し、すべての必要なフィールドを sort_buffer に入れます。これにより、クエリ結果はソート後に元に戻ることなくメモリから直接返されます。データを取得するためのテーブル。
これは、MySQL の設計思想も反映しています。十分なメモリがある場合は、より多くのメモリを使用し、ディスク アクセスを最小限に抑える必要があります。
InnoDB テーブルの場合、ROWID ソートではテーブルに戻る必要があり、より多くのディスク読み取りが発生するため、推奨されません。
これを見ると、MySQL のソートは比較的高価な操作であることがわかります。それでは、すべての注文ごとに並べ替え操作が必要なのでしょうか?と疑問に思うかもしれません。ソートを行わずに正しい結果が得られる場合、システムの消費量は大幅に少なくなり、ステートメントの実行時間も短くなります。
実際、すべての order by ステートメントで並べ替え操作が必要なわけではありません。上記で分析した実行プロセスから、MySQL が一時テーブルを生成し、その一時テーブルに対してソート操作を実行する必要がある理由は、元のデータの順序が狂っているためであることがわかります。
想像できると思いますが、都市インデックスからフェッチされた行が名前の昇順で自然に並べ替えられることが保証できれば、再度並べ替える必要がなくなるでしょうか?
まさにその通りです。
したがって、Citizen テーブルに city と name の結合インデックスを作成できます。対応する SQL ステートメントは次のとおりです。
alter table t add index city_user(city, name);
都市指数との比較として、この指数の模式図を見てみましょう。
このインデックスでは、引き続きツリー検索メソッドを使用して city='Hangzhou' を満たす最初のレコードを見つけることができ、さらに、都市の条件が満たされている限り、「次のレコード」を順番にフェッチするトラバース プロセス中に確実に取得できます。値は杭州、名前の値は順序どおりである必要があります。(ソートはインデックス作成時に行われます)
このようにして、クエリ プロセス全体の流れは次のようになります。
- インデックス (city,name) から city='Hangzhou' の条件を満たす最初の主キー ID を見つけます。
- 主キー ID インデックスに移動して行全体を取得し、名前、都市、年齢の 3 つのフィールドの値を取得し、結果セットの一部として直接返します。
- インデックス (city,name) から次のレコードの主キー ID を取得します。
- 1000 番目のレコードが見つかるまで手順 2 と 3 を繰り返すか、city='Hangzhou' の条件が満たされない場合はループが終了します。
カバリング インデックスとは、インデックス上の情報がクエリ リクエストを満たすのに十分であり、データをフェッチするために主キー インデックスに戻る必要がないことを意味します。
カバリングインデックスの概念に従って、このクエリステートメントの実行プロセスをさらに最適化できます。
このクエリでは、都市、名前、年齢の結合インデックスを作成できます。対応する SQL ステートメントは次のとおりです。
alter table t add index city_user_age(city, name, age);
このとき、city フィールドの値が同じ行については、name フィールドの値に応じて昇順にソートされるため、このときのクエリ文はソートする必要はありません。このように、クエリ ステートメント全体の実行フローは次のようになります。
- インデックス (city,name,age) から city='Hangzhou' の条件を満たす最初のレコードを検索し、city、name、age の 3 つのフィールドの値を取り出し、結果の一部として直接返します。設定;
- インデックスから次のレコード (都市、名前、年齢) を取得し、これら 3 つのフィールドの値も取得して、結果セットの一部として直接返します。
- 1000 番目のレコードが見つかるまで手順 2 を繰り返すか、city='Hangzhou' の条件が満たされない場合はループが終了します。
(レコード ID の動作の削減)