アプリケーションを開発するとき、指定されたフィールドの並べ替えに従って結果を表示する必要が生じることがよくあります。
例:select city、name、age from t where city = 'Hangzhou' order by name limit 1000; cityフィールドは通常のインデックスです
すべてのフィールドを並べ替える
実行結果を見てください:
Extraフィールドの「Usingfilesort」は、ソートが必要であることを示しています。MySQLは、sort_bufferと呼ばれるソート用の各スレッドにメモリを割り当てます。
通常の状況では、このステートメントの実行フローは次のとおりです。
- sort_bufferを初期化し、name、city、ageの3つのフィールドを必ず入力してください。
- インデックス都市からcity = 'Hangzhou'の条件を満たす最初の主キーIDを見つけます。
- 主キーIDインデックスに移動して行全体をフェッチし、名前、都市、年齢の3つのフィールドの値を取得して、sort_bufferに格納します。
- インデックス都市からレコードの主キーIDを取得します。
- cityの値がクエリ条件を満たさなくなるまで、手順3と4を繰り返します。
- フィールド名に従ってsort_bufferのデータをすばやくソートします。
- 並べ替えの結果によると、最初の1000行がクライアントに返されます。
とりあえず、このソートプロセスをフルフィールドソートと呼びましょう。実行プロセスの概略図は次のとおりです。
図の「名前でソート」のアクションは、メモリ内で実行される場合と、ソートに必要なメモリとパラメータsort_buffer_sizeに応じて、外部ソートを使用する必要がある場合があります。ただし、並べ替えデータの量が多すぎて内部に保存できない場合は、ディスクの一時ファイルを使用して並べ替えを支援する必要があります。
次の方法を使用して、並べ替えステートメントが一時ファイルを使用するかどうかを判別できます。
/* 打开optimizer_trace,只对本线程有效 */
SET optimizer_trace='enabled=on';
/* @a保存Innodb_rows_read的初始值 */
select VARIABLE_VALUE into @a from performance_schema.session_status where variable_name = 'Innodb_rows_read';
/* 执行语句 */
select city, name,age from t where city='杭州' order by name limit 1000;
/* 查看 OPTIMIZER_TRACE 输出 */
SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G
/* @b保存Innodb_rows_read的当前值 */
select VARIABLE_VALUE into @b from performance_schema.session_status where variable_name = 'Innodb_rows_read';
/* 计算Innodb_rows_read差值 */
select @b-@a;
この方法は、OPTIMIZER_TRACEの結果を確認することで確認できます。number_of_tmp_filesから、一時ファイルが使用されているかどうかを確認できます。
number_of_tmp_filesは、ソートプロセスで使用される一時ファイルの数を表します。内部ストレージが不十分な場合、外部ソーティングが必要であり、外部ソーティングは通常、マージソートアルゴリズムを使用します。MySQLは、ソートする必要のあるデータを12の部分に分割し、各部分を個別にソートしてこれらの一時ファイルに保存することは容易に理解できます。次に、これらの12個の順序付きファイルを1つの大きな順序付きファイルにマージします。
sort_buffer_sizeがソートされるデータのサイズを超える場合、number_of_tmp_filesは0です。これは、ソートがメモリー内で直接実行できることを意味します。sort_buffer_sizeが小さいほど、分割されるコピーの数が多くなり、number_of_tmp_filesの値が大きくなります。なぜなら、tmp_fileはsort_buffer_sizeサイズだからです。
さらに、examined_rows = 4000です。これは、並べ替えに関係する行の数が4000であることを意味します。sort_modeのpacked_additional_fieldsは、ソートプロセスで文字列が「圧縮」されることを意味します。名前フィールドの定義がvarchar(16)の場合でも、ソート処理中に実際の長さに応じてスペースを割り当てる必要があります。
最後のクエリステートメントselect @ b- @ aの戻り結果は4000です。これは、実行全体で4000行のみがスキャンされたことを意味します。ここで、結論への干渉を避けるために、internal_tmp_disk_storage_engineをMyISAMに設定したことに注意してください。それ以外の場合、select @ b- @ aの結果は4001として表示されます。
これは、OPTIMIZER_TRACEテーブルをクエリするときに一時テーブルが必要であり、internal_tmp_disk_storage_engineのデフォルト値がInnoDBであるためです。InnoDBエンジンを使用している場合、データが一時テーブルから取得されるときに、Innodb_rows_readの値が1増加します。
乱暴な並べ替え
全フィールドソートアルゴリズムの問題の1つは、クエリによって返されるフィールドが多い場合、sort_bufferに配置するフィールドの数が多すぎて、メモリに配置できる行の数が多すぎることです。同時に小さいので、多くの一時ファイルに分割する必要があり、並べ替えのパフォーマンスが低下します。したがって、1行が大きい場合、この方法は十分に効率的ではありません。
MySQLがソート用の単一行の長さが長すぎると判断した場合、MySQLは別のアルゴリズムのROWIDソートを使用します。パラメータを変更しましょう:
SET max_length_for_sort_data = 16;
max_length_for_sort_dataは、ソートに使用される行データの長さを具体的に制御するMySQLのパラメーターです。これは、単一行の長さがこの値を超える場合、MySQLは単一行が大きすぎると見なし、別のアルゴリズムに変更する必要があることを意味します。このようにして、都市、名前、年齢の3つのフィールドの定義の全長を再度テストできます。
新しいアルゴリズムは、sort_bufferフィールドに、ソートされる列(名前フィールド)と主キーIDのみを入力します。ただし、現時点では、都市フィールドと年齢フィールドの値が欠落しているため、並べ替えの結果を直接返すことはできず、実行フロー全体は次のようになります:
- sort_bufferを初期化し、nameとidの2つのフィールドを必ず入力してください。
- インデックス都市からcity = 'Hangzhou'の条件を満たす最初の主キーIDを見つけます。
- 主キーIDインデックスに移動して行全体をフェッチし、nameとidの2つのフィールドを取得して、sort_bufferに格納します。
- インデックス都市からレコードの主キーIDを取得します。
- city = 'Hangzhou'の条件が満たされないまで、手順3と4を繰り返します。
- フィールド名に従ってsort_bufferのデータをソートします。
- 並べ替えの結果をトラバースし、最初の1000行を取得し、idの値に従って元のテーブルに戻って、都市、名前、年齢の3つのフィールドを取り出し、クライアントに返します。
実行フローの概略図は次のとおりです。これをROWIDソートと呼びます。
フルフィールドソートと比較して、ROWIDソートは、テーブルtの主キーインデックスにもう一度アクセスします。これはステップ7です。
最終的な「結果セット」は論理的な概念であることに注意してください。実際、MySQLサーバーはソートされたsort_bufferからIDを順番に取得し、元の3つのフィールドcity、name、ageの結果を検索します。テーブル。結果を保存するには、サーバー上のメモリを消費する必要があります。結果は、クライアントに直接返されます。
図のexamined_rowsの値はまだ4000であり、並べ替えに使用されるデータが4000行であることを示しています。ただし、select @ b- @aステートメントの値は5000になります。このとき、並べ替え処理に加えて、並べ替えが完了した後、IDに従って元のテーブルを取得する必要があるためです。ステートメントは1000に制限されているため、さらに1000行が読み取られます。
OPTIMIZER_TRACEの結果から、他の2つのメッセージも変更されていることがわかります。
- sort_modeは<sort_key、rowid>になります。これは、nameとidの2つのフィールドのみがソートに関与することを意味します。
- number_of_tmp_filesは10になりました。これは、現時点では、並べ替えに関係する行数はまだ4000ですが、各行が少なくなっているため、並べ替えに必要なデータの合計量が少なくなり、必要な一時ファイルの数が少なくなっています。それに応じて減少します。
2つのアルゴリズムの実行フローからどのような結論を導き出すことができますか?
- MySQLがソートメモリが小さすぎてソート効率に影響することを本当に心配している場合は、ROWIDソートアルゴリズムを使用するため、ソートプロセス中に一度により多くの行をソートできますが、に戻る必要があります。データをフェッチするための元のテーブル。
- MySQLがメモリが十分に大きいと判断した場合、すべてのフィールドによる並べ替えが優先され、必要なすべてのフィールドがsort_bufferに配置されるため、並べ替え後にクエリ結果がメモリから直接返されます。元のテーブルに戻ってデータをフェッチします。
これは、MySQLの設計哲学も反映しています。十分なメモリがある場合は、ディスクアクセスを最小限に抑えるためにより多くのメモリを使用する必要があります。
ビジネスロジックからの注文のインデックス最適化
実際、すべてのorderbyステートメントでソート操作が必要なわけではありません。上記で分析した実行プロセスから、MySQLが一時テーブルを生成し、一時テーブルに対して並べ替え操作を実行する必要がある理由は、元のデータの順序が正しくないためであることがわかります。したがって、都市インデックスから取得された行が名前で昇順で自然に並べ替えられるようにする方法。
- 最適化1、都市と名前の共同インデックスを作成します
このインデックスでは、ツリー検索を使用して、city = 'Hangzhou'を満たす最初のレコードを見つけることができます。さらに、都市のIf値がHangzhouである限り、「次のレコード」を順番にフェッチするトラバーサルプロセスで確認できます。 、nameの値は順番になっている必要があります。このようにして、クエリプロセス全体のフローは次のようになります。
- インデックス(city、name)からcity = 'Hangzhou'の条件を満たす最初の主キーIDを見つけます。
- 主キーIDインデックスに移動して行全体をフェッチし、名前、都市、年齢の3つのフィールドの値を取得して、結果セットの一部として直接返します。
- インデックス(都市、名前)からレコードの主キーIDを取得します。
- 1000番目のレコードが見つかるまで、またはcity = 'Hangzhou'の条件が満たされないときにループが終了するまで、手順2と3を繰り返します。
このクエリプロセスでは、一時テーブルも並べ替えも必要ありません。次に、explainの結果を使用して確認します。
さらに、(city、name)ジョイントインデックス自体が順序付けられているため、このクエリは、条件を満たす最初の1000レコードが見つかる限り、4000行すべてを読み取る必要はありません。終了できます。つまり、この例では、1000回のスキャンのみが必要です。
- 最適化2、インデックスをカバーし、都市、名前、年齢の共同インデックスを作成します。
このようにして、クエリステートメント全体の実行フローは次のようになります。
- インデックス(city、name、age)からcity = 'Hangzhou'の条件を満たす最初のレコードを見つけ、city、name、ageの3つのフィールドの値を取り出し、それらをの一部として直接返します。結果セット;
- インデックス(都市、名前、年齢)からレコードを取得し、これら3つのフィールドの値も取得して、結果セットの一部として直接返します。
- 1000番目のレコードが見つかるまで、またはcity = 'Hangzhou'の条件が満たされないときにループが終了するまで、手順2を繰り返します。
ご覧のとおり、「インデックスの使用」が「追加」フィールドに追加されています。これは、カバーするインデックスが使用されることを意味し、パフォーマンスが大幅に向上します。
もちろん、これは、各クエリがカバーインデックスを使用できるようにするために、ステートメントに含まれるフィールドに共同インデックスを作成する必要があるということではありません。結局のところ、インデックスにはまだメンテナンスコストがかかります。これは、検討する必要のある決定です。
拡張ケース
select * from t where city in( "Hangzhou"、 "Suzhou")order by name limit 100;このSQLステートメントはソートする必要がありますか?並べ替えを回避するための解決策はありますか?
(city、name)ジョイントインデックスがありますが、単一の都市の場合、名前は増分されます。ただし、このSQLステートメントは「杭州」と「蘇州」の2つの都市を同時にチェックするため、条件を満たすすべての名前がインクリメントされるわけではありません。このSQLステートメントは並べ替える必要があります。
事業開発の観点から、データベース側でのソートを必要としないソリューションを実装します。さらに、ページングが必要な場合は、101ページを表示する必要があります。つまり、最後に文を「limit10000,100」に変更する必要があります。実装方法は何ですか。
(city、name)ジョイントインデックスの機能を使用し、このステートメントを2つのステートメントに分割する必要があります。実行フローは、次のとおりです。
- select * from t where city = "Hangzhou" order by name limit 100;を実行します。このステートメントでは、並べ替えは必要ありません。クライアントは、長さ100のメモリ配列Aを使用して結果を格納します。
- select * from t where city = "Suzhou" order by name limit 100を実行します。同様に、結果がメモリ配列Bに格納されていると仮定します。
- これで、AとBは2つの順序付けられた配列になり、マージと並べ替えのアイデアを使用して、最小の名前を持つ最初の100個の値を取得できます。これが必要な結果です。
このSQLステートメントの「limit100」を「limit10000,100」に変更した場合、処理方法は実際には似ています。つまり、上記の2つのステートメントを次のように変更します。select
* from t where city = "Hangzhou" order by名前制限10100;そして* from t where city = "Suzhou"を名前制限10100で注文します。
現時点では、データ量が多く、結果を2行で同時に読み取ることができます。マージソートアルゴリズムを使用して2つの結果セットを取得し、10001から10100までの名前の値を順番に取得します。 、これは望ましい結果です。
データの単一行が比較的大きい場合は、*をid、nameに変更してから、マージソート方法を使用してnameとidの値を名前の順に10001から10100まで取得し、これらを取得することを検討できます。データベースへの100個のIDすべてのレコードを検索します。
コンテンツソース:LinXiaobin「MySQLの実際の戦闘に関する45の講義」