MySQL 5.7で学ぶMySQLの最適化1 (環境設定とパラメータ理解)

いよいよ、昨年11月から、[Amazon Aurora 8.0](https://aws.amazon.com/jp/about-aws/whats-new/2021/11/amazon-aurora-mysql-8-0/)がリリースされ、8.0への移行の日が差し迫ってきました。

Spacelyでは、現在、Aurora MySQL5.7を利用していますが、8.0移行の前準備として、最近の最適化の仕組みを学びはじめました。
筆者が知っている時代よりも、最適化プロセス(オプティマイザの機能)がかなり高機能なものになっており、今まで思い込んでいた、勝手に作られるTemporary Tableが遅いとか、サブクエリが遅いとか、INよりEXISTSが早いとか・・・・・そういった固定観念を一気に拭うときがやってきたようです。インデックスが貼られていないと遅すぎてサーバーがマシンごとヒートアップする・・・というのも近い将来には見なくなる光景かもしれません・・・
今回は、Optimizer関連の変数を確認しながら最適化について学びます。
MySQL初心者や忘れてしまった方は以下のドキュメントや書籍を参考にするといいと思います。

- [MySQL用語辞典](https://dev.mysql.com/doc/refman/5.6/ja/glossary.html#glos_query_execution_plan)
- [詳解MySQL 5.7](https://www.amazon.co.jp/%E8%A9%B3%E8%A7%A3MySQL-5-7-%E6%AD%A2%E3%81%BE%E3%82%89%E3%81%AC%E9%80%B2%E5%8C%96%E3%81%AB%E4%B9%97%E3%82%8A%E9%81%85%E3%82%8C%E3%81%AA%E3%81%84%E3%81%9F%E3%82%81%E3%81%AE%E3%83%86%E3%82%AF%E3%83%8B%E3%82%AB%E3%83%AB%E3%82%AC%E3%82%A4%E3%83%89-NEXT-ONE/dp/4798147400)

# 1. オプティマイザ関連の変数について
※ 5.6(または8)の日本語ドキュメントを参照先として設定しています。

:::note info
optimizer_prune_level、optimizer_search_depth 、optimizer_switch、range_optimizer_max_mem_size などオプティマイザ自身の動作に関するパラメーター、optimizer_traceから始まるオプティマイザの挙動をトレースするにあたって必要なパラメーターと2種類あります
:::

| パラメータ  | 初期値 | 解説 |
|:-----------------|:------------------|:------------------|
| [optimizer_prune_level](https://dev.mysql.com/doc/refman/5.6/ja/server-system-variables.html#sysvar_optimizer_prune_level) | 1 | <b>実行計画の見積もりの粒度</b><br>テーブルごとにアクセスされる行数の見積もり(経験則)に基づいて、見込みのないプランを一部刈り込む<br>0を指定すると経験則を無効にできる |
| [optimizer_search_depth](https://dev.mysql.com/doc/refman/5.6/ja/server-system-variables.html#sysvar_optimizer_search_depth) | 62 | <b>実行計画の検索深度</b><br>クエリー内のリレーションの数より値が大きいと、適切なクエリー計画が得られるが、実行計画の生成に時間がかかり、値が小さいと、実行プランがすばやく返される<br>後者の場合、結果のプランが最適にならないことがある<br>0を指定することで自動判別を行ってくれる<br>大きすぎると指数関数的に検証するプランが増えてしまうので注意が必要|
| [optimizer_switch](https://dev.mysql.com/doc/refman/5.6/ja/server-system-variables.html#sysvar_optimizer_switch) | index_merge=on,<br>index_merge_union=on,<br>.......省略........ | (2. optimizer_switchの詳細を参照) |
| [optimizer_trace](https://dev.mysql.com/doc/refman/5.6/ja/server-system-variables.html#sysvar_optimizer_trace) | enabled=off,<br>one_line=off |  <b>実行計画のjsonトレース</b><br> enabled=onの時にトレースが有効|
|[optimizer_trace_features](https://dev.mysql.com/doc/refman/5.6/ja/server-system-variables.html#sysvar_optimizer_trace_features)| greedy_search=on,<br>range_optimizer=on,<br>dynamic_range=on,<br>repeated_subselect=on | <b>トレースする機能を指定する</b><br> |
| [optimizer_trace_limit](https://dev.mysql.com/doc/refman/5.6/ja/server-system-variables.html#sysvar_optimizer_trace_limit) | 1 | <b>表示されるオプティマイザトレースの最大値</b> |
| [optimizer_trace_max_mem_size](https://dev.mysql.com/doc/refman/5.6/ja/server-system-variables.html#sysvar_optimizer_trace_max_mem_size) | 16384 | <b>オプティマイザトレースの最大累積サイズ</b><br>オプティマイザがトレースログ出力するのに必要なメモリのサイズ<br>こちらのチューニングを事前に行っておくことも重要 |
| [optimizer_trace_offset](https://dev.mysql.com/doc/refman/5.6/ja/server-system-variables.html#sysvar_optimizer_trace_offset) | -1 | <b>表示されるオプティマイザトレースの最初のオフセット</b> |
| [range_optimizer_max_mem_size](https://dev.mysql.com/doc/refman/8.0/ja/range-optimization.html) | 8388608 | <b>[範囲オプティマイザ](https://dev.mysql.com/doc/refman/5.6/ja/range-optimization.html)で使用可能なメモリー</br>を制御</b><br>0を指定することで無限になる |

-  `range_optimizer_max_mem_size`が適切でない場合に、オプティマイザが[最適化候補の検索を諦めてフルスキャン](https://developers.freee.co.jp/entry/large-in-clouse-length-cause-full-scan)する問題が報告されているので、こちらも気をつけましょう
-  `innodb_buffer_pool_size`へのメモリ割り当ての追加で、rangeスキャンの性能が向上します
 - DB単体のサーバーであればメモリの80%を利用できるともいわれている
 - [Aurora](https://aws.amazon.com/jp/blogs/news/best-practices-for-amazon-aurora-mysql-database-configuration/)では
`{変数}インスタンスメモリの 3/4`という設定になっているので最適化されていると思われます

# 2. optimizer_switchの詳細
[切り替え可能な最適化制御](https://dev.mysql.com/doc/refman/5.6/ja/switchable-optimizations.html)

:::note info
optimizer_switchはスイッチON/OFFにより最適化をする/しないを設定できます。
:::


| optimizer_switch  | デフォルト値 | 意味 |
|:-----------------|:------------------|:------------------|
|[index_merge](https://dev.mysql.com/doc/refman/5.6/ja/index-merge-optimization.html) | on | <b>インデックスマージ</b><br>複数の rangeスキャンによって、行を取得しそれらの結果を 1 つにマージする機能<br>※ 単一のテーブルからのスキャン結果のみマージをする<br>※ rangeスキャンとは、INやOR句にIDを直接指定したり、不等号記号(<,>=....)やBETWEENなどの句で指定された場合の最適化のこと<br><br><i>e.g. WHERE t1.key1 IN (1,2) OR t1.key2 = 20 OR t1.key3 < 30 </i>|
|[index_merge_union](https://dev.mysql.com/doc/refman/5.6/ja/index-merge-optimization.html#index-merge-union) | on | <b>インデックスマージ</b><br>WHERE 句が【OR】で組み合わされたさまざまなキーに対する複数の範囲条件の和集合<br><br><i>e.g. WHERE key1 = 1 OR key2 = 2 OR key3 = 3</i>|
|[index_merge_sort_union](https://dev.mysql.com/doc/refman/5.6/ja/index-merge-optimization.html#index-merge-sort-union)|on| <b>インデックスマージ</b><br>WHERE 句が【OR】で組み合わされた複数の範囲条件の和集合<br>※ index_merge_unionを適用できない場合に採用<br>※ index_merge_unionとの違いは行を返却する前にsortすること<br><br><i>e.g. WHERE (key_col1 > 10 OR key_col2 = 20)</i>|
|[index_merge_intersection](https://dev.mysql.com/doc/refman/5.6/ja/index-merge-optimization.html#index-merge-intersection)|on| <b>インデックスマージ</b><br>WHERE 句が【AND】で結合されたさまざまなキーに対する複数の範囲条件の積集合<br><br><i>e.g. WHERE (key_col1 > 10 AND key_col2 = 20)</i>|
|[engine_condition_pushdown](https://dev.mysql.com/doc/refman/5.6/ja/condition-pushdown-optimization.html)|on| <b>コンディションプッシュダウン</b><br>インデックスが設定されていないカラムと定数(a=10 など)との直接比較の効率性を向上する<br>※ デフォルトで有効だが、NDBを利用していなければ関係ない<br><br><i>e.g. WHERE (no_key > 10 AND key_col2 = 20)</i>|
|[index_condition_pushdown](https://dev.mysql.com/doc/refman/8.0/ja/index-condition-pushdown-optimization.html)|on| <b>コンディションプッシュダウン</b><br>インデックスを使用してテーブルから行を取得する場合の最適化<br>※ デフォルトで有効だが、NDBを利用していなければ関係ない<br> |
|[mrr](https://dev.mysql.com/doc/refman/5.6/ja/mrr-optimization.html)|on| <b>Multi-Range Read</b><br>ランダムディスクアクセスの回数を軽減する最適化<br>※ MRR: ディスクの並び順に従ってデータファイルを読み取るアルゴリズムを利用<br>※ index読み取り => 行番号でのソート=> ソート順での行の取得を行う|
|[mrr_cost_based](https://dev.mysql.com/doc/refman/5.6/ja/switchable-optimizations.html) |on| <b>Multi-Range Read</b><br>mrr=on の場合にコストベースの MRR の使用を制御 |
|[block_nested_loop](https://dev.mysql.com/doc/refman/5.6/ja/bnl-bka-optimization.html)|on| <b>Block Nested Loop</b><br>オプティマイザがどのように BNLアルゴリズムと BKAJアルゴリズムを使用するかを制御<br>※ BNLJ: JOINバッファというメモリ領域を使ってスキャンの回数を減らすアルゴリズムを利用<br>[※ MySQLのネステッドループ JOINについて](https://dev.mysql.com/doc/refman/5.6/ja/nested-loop-joins.html)|
|[batched_key_access](https://dev.mysql.com/doc/refman/5.6/ja/switchable-optimizations.html)|off| <b>Bached Key Access</b><br>結合したテーブルと結合バッファーの両方でのインデックスアクセスを提供する、Batched Key Access (BKA) 結合アルゴリズムの使用を制御<br>※ BKAJ: MRRとBNLJの組み合わせのアルゴリズムを利用<br>※ JOINバッファを利用しつつMRRスキャンで行のID順にfetchをする<br>※ データ量が巨大でストレージからの読み取りバッファが何度も発生する状況で有効的<br>[※ Block Nested Loop Join／Batched Key Access Join](https://enterprisezine.jp/article/detail/3606)|
|[materialization](https://dev.mysql.com/doc/refman/5.6/ja/subquery-optimization.html#subquery-materialization)|on| <b>サブクエリー実体化</b><br>通常メモリー内に一時テーブルとしてサブクエリー結果を生成することによって、クエリー実行を高速化する<br>※ ハッシュインデックスを使用してテーブルをインデックス付けし、高速かつ低コストにルックアップできる場合がある |
|[subquery_materialization_cost_based](https://dev.mysql.com/doc/refman/5.6/ja/subquery-optimization.html)|on| <b>サブクエリー実体化</b><br>サブクエリ実体化やIN -> EXISTSサブクエリ変換の選択をコストベースで制御 |
|[semijoin](https://dev.mysql.com/doc/refman/5.6/ja/subquery-optimization.html#semi-joins)|on| <b>セミジョイン(準結合)</b><br>WHERE句やON句にINやEXISTSを使用したサブクエリに対して準結合を行うかを制御|
|[loosescan](https://dev.mysql.com/doc/refman/5.6/ja/subquery-optimization.html#semi-joins)|on| <b>セミジョイン(準結合)</b><br>サブクエリ内のテーブルの結合するカラムに対してインデックスが作成されている場合に動作、重複しないようにサブクエリテーブルをスキャン|
|[firstmatch](https://dev.mysql.com/doc/refman/5.6/ja/subquery-optimization.html#semi-joins)|on| <b>セミジョイン(準結合)</b><br>外部表の各行に対して、内部表に一致するものが見つかったら、即座二行を返す、見つかるまで調べて1行だけ返却する準結合 |
|duplicateweedout|on| <b>セミジョイン(準結合)</b>サブクエリに対して通常の結合を行い、一時テーブルを作成して、その一時テーブル内で重複レコードを除外 |
|[use_index_extensions](https://dev.mysql.com/doc/refman/5.6/ja/index-extensions.html)|on| <b>インデックス拡張</b><br>セカンダリインデックスの使用方法を判断する際に、オプティマイザが主キーカラムを考慮するかどうかを制御|
|[condition_fanout_filter](https://dev.mysql.com/doc/refman/8.0/ja/condition-filtering.html)|on| <b>条件フィルタ</b><br>2つ以上のテーブルの結合時に動作する<br>行の組合せの数が急速に増加しないように、接頭辞数が少ないテーブルから準に結合するように最適化 |
|[derived_merge](https://dev.mysql.com/doc/refman/8.0/ja/derived-table-optimization.html)|on| <b>派生テーブルマージ</b><br>[導出テーブル](https://dev.mysql.com/doc/refman/8.0/ja/derived-tables.html)、[ビュー参照](https://dev.mysql.com/doc/refman/8.0/ja/create-view.html)および[共通テーブル式](https://dev.mysql.com/doc/refman/8.0/ja/with.html)を外部クエリーブロックにマージしようとするかどうかを制御 |
|[prefer_ordering_index](https://dev.mysql.com/doc/refman/8.0/ja/limit-optimization.html)|on|<b>LIMITクエリの最適化</b><br>結果セット全体をフェッチして、余分なデータを破棄するのではなく、クエリーで LIMIT句で実行|
|[hash_join](https://dev.mysql.com/doc/refman/8.0/ja/hash-joins.html)|off| <b>ハッシュ結合</b> MySQL 8.0.2以降で、結合条件に適用できるインデックスがないクエリーに対してハッシュ結合が使用できる |
|hash_join_cost_based|on| No Information |

- `index_merge_*`で指定されたパラメータでwhere句のANDやORの最適化が行われます
- range(範囲)スキャンでは、[複数値比較の等価範囲の最適化](https://dev.mysql.com/doc/refman/5.6/ja/range-optimization.html)というのが行われます
    - こちらはINクエリなどでも利用され、対象のカラムがPrimaryKeyやUnique Indexの設定で、一意に特定できる場合は各範囲の行の見積もりは常に1です
    - そうでない場合には、インデックスのダイブまたはインデックス統計を利用します
-  `SET optimizer_switch='engine_condition_pushdown=off';` を利用することで、[NDB](https://dev.mysql.com/doc/refman/5.6/ja/mysql-cluster-ndb-innodb-engines.html)を利用している場合に、indexがついていない検索を5-10倍高速化できる可能性があります
 - くわしくは、[プッシュダウンの最適化](https://dev.mysql.com/doc/refman/5.6/ja/condition-pushdown-optimization.html)を参照してください
-  BNLJやBKAJのアルゴリズムを利用する際に、[join_buffer_size](https://dev.mysql.com/doc/refman/5.6/ja/server-system-variables.html#sysvar_join_buffer_size)の設定をすることで、JOIN Bufferのメモリ領域のチューニングが可能です
 - 大きなサイズのレポーティングを出力する前に`SET join_buffer_size=67108864`大きなサイズを指定することも可能
-  `condition_fanout_filter`の設定で結合が順序が最適化された結果思わぬ順番でテーブルがJOINされてしまうことがあります
 - 回避するには、[straight join構文](https://dev.mysql.com/doc/refman/5.6/ja/join.html)を使用する方法があります
 - [indexの付与が未完全でトラブった事例](https://vidaisuki.hatenablog.com/entry/mysqlcasualadventcalendar20171213)もあるので気をつけましょう
-  `prefer_ordering_index` MySQL 8.0.21 より前は、この最適化を無効にすることはできない
-   `hash_join`はMySQL 8で追加されたフラグになります
    -  [MySQL 8.0で追加されたフラグ] (https://gihyo.jp/dev/serial/01/mysql-road-construction-news/0135)
-  [Amazon Aurora Ver2(MySQL 5.6 or 5.7) または 3(MySQL8)](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.BestPractices.html)
の大規模結合のベストプラクティスとして詳解をされています
-  `hash_join_cost_based` については、マニュアルに記載がないのですが、[8.0での追加削除パラメータ](https://dev.mysql.com/doc/refman/8.0/ja/added-deprecated-removed.html)が一覧になっているので、今後の追加に期待をしましょう

# 3. 検証用のローカルMySQLの設定
SQLの検証ができるようにローカルのmysqlサーバーの設定をします

#### my.cnf
```
[mysqld]
# Only allow connections from localhost
# 多めに設定しておく
max_allowed_packet=1073741824
# オプティマイザトレースの有効化
optimizer_trace="enabled=on"
optimizer_trace_max_mem_size=1073741824
# unlimitに指定する
range-optimizer-max-mem-size = 0
#  3,4または0を指定する
optimizer_search_depth = 0
```

-  長いSQLがエラーにならないように、事前に`max_allowed_packet`を多めに設定しておきます
-  正しくtraceができない場合がありエラー判別しづらいので、オプティマイザトレースの有効化と`optimizer_trace_max_mem_size`も多めに設定しておきます
-  `optimizer_search_depth`も0に設定して、MySQLのオプティマイザの判断に委ねます
-  `query_cache_type`はdefaultでOFFですが、ローカルのデバック時には`show variables`でOFFになっていることを確認してください

:::note warn
また、検証のために一時的に設定を有効にしたい場合には、現在ログインしているセッションでのみ設定を有効化するために、下記の様なコマンドを利用できます
:::

```sql
mysql> SET optimizer_trace="enabled=on";
mysql> SET optimizer_trace_max_mem_size=1073741824;
```

MySQLを再起動して指定したパラメーターを有効化しましょう

# 4. Show warningで動いているOptimizerを確認する

```sql
mysql> explain 元のクエリを実行;
mysql> show warnings; 
+-----+------+------------------------------+
| Level | Code | Message.........
| Note  | 1003 | /* select#1 */ select count(0).........
```

 :mag: Messageの中のクエリを拡大してみる

![optimizer trace.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89997/24adda45-f2f0-42d3-5cb3-9f6d5354d67f.png)

-  Optimizerの動作を一目瞭然で確認できます
-  `<exists>`はサブクエリー述語は EXISTS 述語に変換されている
-  `select #2`N の id 値を持つ行に関連付けられます
-  `<in_optimizer >`ユーザーにとっては意味がない内部オプティマイザオブジェクト
-  `<cache>`式 (スカラーサブクエリーなど) が 1 回実行され、あとで使用するために、結果の値がメモリーに保存された状態
-  その他、サブクエリの実体化を示す、`<materialize>`や一時テーブルの自動的に生成されるキーの`<auto_key>`など様々なオプティマイザが自動的に動いています
-  `optimizer_switch`で指定できるものとそうでないものがあります
-  詳細[Explain出力の確認](https://dev.mysql.com/doc/refman/5.6/ja/explain-extended.html)についてを参照してください

# 5. OPTIMIZER_TRACEを確認する
```sql
mysql > SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;
```

Optimizer traceをonにした場合に、上記のコメントでexplainクエリを実行後に、直前のオプティマイザの動作が確認できるようになります。

今回は詳細は説明は割愛します。

![optimizertrace2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89997/9e8d7cde-4c44-dee3-1f77-546747e36e36.png)


# 6. MySQL Work BenchのVisual Explainを使う

[Downlod MySQL Work Bench](https://www.mysql.com/jp/products/workbench/)をダウンロードしてビジュアライザを利用できるようにします

![benh.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89997/f9eb3d62-1580-0441-aa5c-4c6ce7384c44.png)

-------
-------
本記事は、社内でのデーターベース勉強会のために作成した教材資料になります。
[Spacelyのブ・ロ・グサイト](https://tech.spacely.co.jp/)でもご覧になれます。
ご興味のある方は、他にも記事が掲載されているので、ご覧になってください。
ご指摘などございましたら、コメントいただけると幸いでございます。