MySQL最適化のコツ:5つのステップ + 10の事例

MySQL最適化のコツ:5つのステップ + 10の事例

皆さんのお役に立てれば幸いです

最終更新 2022/05/12 8:05
IT技术分享社区
読了目安 5 分
カテゴリ
MySQL
タグ
MySQL

SQL最適化の一般的な手順

1 スロークエリログなどから、実行効率の低いSQL文を特定する

2 explainでSQLの実行計画を分析する

重点的に確認すべき項目は、type、rows、filtered、extraです。

typeは上から下に向かって効率が高くなります。

  • ALL フルテーブルスキャン
  • index インデックスフルスキャン
  • range インデックス範囲スキャン。<, <=, >=, between, inなどで使用される。
  • ref 非ユニークインデックススキャンまたはユニークインデックスのプレフィックススキャン。単一のレコードを返す。関連クエリでよく見られる。
  • eq_ref refに似ているが、ユニークインデックスを使用する点が異なる。主キーを使用した関連クエリで使用される。
  • const/system 単一レコード。システムはマッチした行の他のカラムを定数として扱う。主キーやユニークインデックスによるクエリなど。
  • null MySQLはテーブルやインデックスにアクセスせず、直接結果を返す。

上から下に向かって効率が高くなりますが、コストモデルに基づくと、インデックスidx1(a, b, c)idx2(a, c)がある場合、SQLがselect * from t where a = 1 and b in (1, 2) order by c;だと、idx1を使うとtypeはrange、idx2を使うとtypeはrefになります。スキャン行数が、idx2を使う場合にidx1の約5倍以上になる場合はidx1が使われ、そうでなければidx2が使われます。

Extra

  • Using filesort:MySQLは行をソート順に取得する方法を見つけるために、追加のパスを必要とする。結合タイプですべての行をブラウズし、WHERE句にマッチするすべての行のソートキーと行ポインタを保存することでソートを完了する。その後キーがソートされ、ソート順に行が取得される。
  • Using temporary:中間結果を保存するために一時テーブルを使用。パフォーマンスが非常に悪いため、重点的に最適化が必要。
  • Using index:対応するSELECT操作でカバリングインデックス(Covering Index)が使用されていることを示す。テーブルのデータ行へのアクセスを回避しており、効率が良い!もしusing whereも同時に表示される場合、インデックス検索だけでは条件に合うデータを直接クエリできないことを意味する。
  • Using index condition:MySQL 5.6以降で追加されたICP(Index Condition Pushdown)を使用。using index conditionはICP(インデックス条件プッシュダウン)を使用していることを示す。ストレージエンジンレベルでデータフィルタリングを行い、サービスレイヤーでフィルタリングするのではなく、インデックスの既存データを利用してテーブルへのアクセス(回表)を減らす。

3 show profileによる分析

SQL実行のスレッド状態と消費時間を把握する。

デフォルトでは無効。有効化にはset profiling = 1;を実行。

SHOW PROFILES ;
SHOW PROFILE FOR QUERY  #{id};

4 trace

traceはオプティマイザがどのように実行計画を選択するかを分析する。traceファイルを通じて、なぜオプティマイザがAの実行計画を選び、Bを選ばなかったのかをさらに理解できる。

set optimizer_trace="enabled=on";
set optimizer_trace_max_mem_size=1000000;
select * from information_schema.optimizer_trace;

5 問題を特定し、適切な対策を実施する

  • インデックスの最適化
  • SQL文の最適化:SQLの修正、INクエリの分割、時間範囲クエリの分割、前回のデータに基づくフィルタリング
  • 別の実装方法への変更:ES、データウェアハウスなど
  • データフラグメントの処理

マイクロサービスの思想に基づき、B2C ECサイトのシナリオで構築されたプロジェクト実践。コア技術スタックはSpring Boot + Dubbo。将来的にはSpring Cloud Alibabaにリファクタリング予定。

プロジェクトアドレス:https://github.com/YunaiV/onemall

シナリオ分析

ケース1:最左マッチング

インデックス

KEY `idx_shopid_orderno` (`shop_id`,`order_no`)

SQL文

select * from _t where orderno=''

クエリマッチングは左から右へ行われる。order_noでインデックスを使用するには、クエリ条件にshop_idを含めるか、インデックス(shop_id,order_no)の順序を入れ替える必要がある。

ケース2:暗黙的な型変換

インデックス

KEY `idx_mobile` (`mobile`)

SQL文

select * from _user where mobile=12345678901

暗黙的な型変換はインデックス上で演算を行うことになり、インデックスを無効にする。mobileは文字型だが数値が使用されている。文字列でマッチさせるべきで、そうしないとMySQLが暗黙的な変換を行い、インデックスが無効になる。

ケース3:大きなページネーション

インデックス

KEY `idx_a_b_c` (`a`, `b`, `c`)

SQL文

select * from _t where a = 1 and b = 2 order by c desc limit 10000, 10;

大きなページネーションのシナリオでは、まずプロダクトに要件の最適化を依頼することを優先する。最適化できない場合は、以下の二つの方法がある。

  • ひとつは、前回の最後のデータ(上記のc)を受け渡し、"c < xxx"のように処理する方法。ただし、一般的にインターフェースプロトコルの変更が必要であり、必ずしも実行可能ではない。
  • もうひとつは、遅延結合(延迟关联)を用いた処理方法。SQLのテーブルアクセス(回表)を減らすが、インデックスが完全にカバーしている必要がある。SQLの変更例は以下の通り。
SELECT
 t1.*
FROM
 _t t1,
 ( SELECT id FROM _t WHERE a = 1 AND b = 2 ORDER BY c DESC LIMIT 10000, 10 ) t2
WHERE
 t1.id = t2.id;

ケース4:in + order by

インデックス

KEY `idx_shopid_status_created` (`shop_id`, `order_status`, `created_at`)

SQL文

SELECT
 *
FROM
 _order
WHERE
 shop_id = 1
 AND order_status IN ( 1, 2, 3 )
ORDER BY
 created_at DESC
 LIMIT 10

inクエリはMySQL内部でn*mの方法で検索される。unionに似ているが、効率はunionより高い。

inクエリのコスト計算(コスト = タプル数 * IO平均)では、inに含まれる値を一つずつクエリしてタプル数を取得するため、計算処理が遅くなる。そのためMySQLはしきい値(eq_range_index_dive_limit)を設定しており、5.6以降ではこのしきい値を超えると、そのカラムのコストは計算に含まれなくなる。

このため、実行計画が不正確に選択される可能性がある。デフォルトは200で、in条件が200データを超えるとinのコスト計算に問題が生じ、MySQLが不正確なインデックスを選択する可能性がある。

対処方法としては、(order_status, created_at)の順序を入れ替え、SQLを遅延結合に調整すること。

ケース5:範囲クエリによる後続フィールドのインデックス使用不可

インデックス

KEY `idx_shopid_created_status` (`shop_id`, `created_at`, `order_status`)

SQL文

SELECT
 *
FROM
 _order
WHERE
 shop_id = 1
 AND created_at > '2021-01-01 00:00:00'
 AND order_status = 10

範囲クエリには"IN"や"between"も含まれる。

ケース6:不等号、NOTを含む条件はインデックスの高速検索に使用できない(ICPは使用可能)

select * from _order where shop_id=1 and order_status not in (1,2)

select * from _order where shop_id=1 and order_status != 1

インデックス上では、NOT、!=、<>、!<、!>、NOT EXISTS、NOT IN、NOT LIKEなどを避けること。

ケース7:オプティマイザがインデックスを使用しないケース

要求されるデータ量が非常に少ない場合、オプティマイザは補助インデックスを選択する。しかし、アクセスするデータがテーブル全体のデータのかなりの部分(一般的には約20%)を占める場合、オプティマイザはクラスタ化インデックスを使用してデータを検索する。

select * from _order where  order_status = 1

未払いの注文を全て取得するクエリ。通常このような注文は少ない。たとえインデックスを作成しても、インデックスを使用できない。

ケース8:複雑なクエリ

select sum(amt) from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01';

select * from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01' limit 10;

統計データの集計などであれば、データウェアハウスに切り替えることを検討する。

ビジネス上どうしても複雑なクエリが必要な場合、SQLにこだわらず、ESなどの別の方法で解決することを推奨する。

ケース9:ascとdescの混在

select * from _t where a=1 order by b desc, c asc
descとascを混在させるとインデックスが無効になる

ケース10:大規模データ

プッシュ業務のデータ保存では、データ量が非常に大きくなる可能性がある。もしMySQLに保存し、7日間などの有効期限を設定する場合、頻繁なデータ削除によってデータフラグメントが発生する可能性があるため、DBAに連絡してデータフラグメント処理を依頼する必要がある。

さらに探索

関連読書

その他の記事
同じカテゴリ / 同じタグ 2022/04/15

MySQLの曖昧検索にもうlike+%は不要

InnoDBで曖昧検索する際に '%xx' を使うとインデックスが効かなくなることは周知の事実ですが、時にはそうした要件もあるのです。同様の要件は他にもたくさんあります。

続きを読む
同じカテゴリ / 同じタグ 2021/12/22

DockerでMySQLを動かしてる?おめでとう、解雇だ!

現在、コンテナとDockerは依然としてテクノロジー分野で最もホットなキーワードです。ステートレスサービスのコンテナ化はすでに大きな流れとなっており、同時に「データベース MySQL はコンテナ化すべきか?」というホットな話題が議論されています。

続きを読む