MySQL / データの読み取りとクエリ
MySQLでの低速クエリのプロファイリングと最適化
はじめに
データベースシステムのパフォーマンスを維持することは、データベースをバックエンドとするアプリケーションの応答性を最適化する上で不可欠な部分です。これには、サーバーのハードウェアリソースとソフトウェア設定の管理が含まれますが、実行を要求するクエリにも関係しています。
以前のガイドでは、MySQLで低パフォーマンスの低速クエリを特定する方法を検討しました。このガイドでは、低速で実行されるクエリを評価し、最適化する方法について議論することで、次のステップに進みます。これにより、以前の記事での議論を基に、パフォーマンスの低いクエリに関連する問題を特定し、修正するための完全な戦略が提供されます。
クエリプロファイリングを有効にして低速クエリの段階を理解する
低速クエリを最適化しようとするときに最初に行いたいことの1つは、クエリが正確にどこで最も時間を費やしているかを発見するために、クエリのプロファイリングを行うことです。
まず、クエリプロファイリングが有効になっているかを確認するには、次のように入力します。
SELECT @@PROFILING;
+-------------+| @@profiling |+-------------+| 0 |+-------------+1 row in set, 1 warning (0.00 sec)
0はプロファイリングが現在無効になっていることを示します。プロファイリングを有効にするには、次のように入力します。
SET PROFILING = 1;
プロファイリングが有効になったら、次のように入力して利用可能なプロファイルをリスト表示できます。
SHOW PROFILES;
+----------+------------+--------------------------+| Query_ID | Duration | Query |+----------+------------+--------------------------+| 1 | 3.00368075 | select sleep(3) || 2 | 0.00740700 | select * from mysql.user || 3 | 0.00075875 | select @@profiling |+----------+------------+--------------------------+3 rows in set, 1 warning (0.00 sec)
出力には、プロファイリングが有効になってから現在のセッションで実行された最新のステートメントが表示されます。各クエリには合計実行時間が表示され、さらに深く掘り下げるために使用できるクエリ番号があります。
最新のクエリのプロファイリング情報を表示するには、次のように入力します。
SHOW PROFILE;
+----------------------+----------+| Status | Duration |+----------------------+----------+| starting | 0.000144 || checking permissions | 0.000020 || Opening tables | 0.000024 || init | 0.000013 || optimizing | 0.000047 || executing | 0.000032 || end | 0.000010 || query end | 0.000016 || closing tables | 0.000012 || freeing items | 0.000367 || cleaning up | 0.000074 |+----------------------+----------+11 rows in set, 1 warning (0.00 sec)
デフォルトでは、出力にはステートメントがクエリ処理の各段階でどれくらいの時間かかったかが正確に表示されます。これにより、クエリのどの部分のパフォーマンスが低いかを正確に特定するのに役立ちます。
異なるクエリに関連付けられた結果を表示するには、SHOW PROFILES
出力によって提供されるクエリIDとともに、FOR QUERY <N>
句を追加できます。
たとえば、クエリ1のデフォルト表示を取得するには、次のように入力します。
SHOW PROFILE FOR QUERY 1;
+----------------------+----------+| Status | Duration |+----------------------+----------+| starting | 0.001419 || checking permissions | 0.000028 || Opening tables | 0.000030 || init | 0.000020 || optimizing | 0.000020 || executing | 0.000025 || User sleep | 3.000165 || end | 0.000043 || query end | 0.000019 || closing tables | 0.000012 || freeing items | 0.000540 || logging slow query | 0.001320 || cleaning up | 0.000041 |+----------------------+----------+13 rows in set, 1 warning (0.00 sec)
ここでは、出力がUser sleep
ステータスに長い時間が費やされていることを明確に示しており、これは実行された関数を直接管理しています。
異なる種類の情報を表示するように出力を変更することもできます。これは、デフォルトの出力を確認した後、より具体的な情報をチェックするのに役立つことが多いです。
たとえば、最初のクエリのブロック入出力に関する情報は、次のように入力することで確認できます。
SHOW PROFILE BLOCK IO FOR QUERY 1;
+----------------------+----------+--------------+---------------+| Status | Duration | Block_ops_in | Block_ops_out |+----------------------+----------+--------------+---------------+| starting | 0.001419 | 184 | 0 || checking permissions | 0.000028 | 0 | 0 || Opening tables | 0.000030 | 0 | 0 || init | 0.000020 | 0 | 0 || optimizing | 0.000020 | 0 | 0 || executing | 0.000025 | 0 | 0 || User sleep | 3.000165 | 0 | 0 || end | 0.000043 | 0 | 0 || query end | 0.000019 | 0 | 0 || closing tables | 0.000012 | 0 | 0 || freeing items | 0.000540 | 0 | 0 || logging slow query | 0.001320 | 64 | 8 || cleaning up | 0.000041 | 0 | 0 |+----------------------+----------+--------------+---------------+13 rows in set, 1 warning (0.00 sec)
以前の情報に加えて、これはMySQLがファイルシステム上のブロックデータとやり取りする必要があった時間を表示します。
指定できる利用可能な出力バリエーションには以下が含まれます。
ALL
:利用可能なすべての情報を表示しますBLOCK IO
:クエリが生成した入出力ブロックの数を表示しますCONTEXT SWITCHES
:クエリ実行中に発生した、自発的および非自発的なコンテキストスイッチの数を表示しますCPU
:ユーザー空間およびシステムカテゴリでのCPU時間を表示しますIPC
:送受信されたプロセス間メッセージの数を表示しますPAGE FAULTS
:メジャーおよびマイナーフォルトに分類されたページフォルトの数を表示しますSOURCE
:MySQL内で実行されている実際の関数と、そのソースファイルおよび行番号を表示し、実行マッピングを可能にしますSWAPS
:クエリが必要としたスワップの数を表示します
クエリに関連する可能なすべての情報を表示するには、次のように入力します。
SHOW PROFILE ALL FOR QUERY 1\G
表示される追加出力が多いため、結果をより読みやすくするために、\G
ステートメント終端シーケンスを使用して代替出力フォーマットをトリガーします。
プロファイリング情報を使用して、クエリのどの部分が遅いかについて良いアイデアが得られたら、リソースを節約するためにプロファイリングをオフに戻すことができます。
SET PROFILING = 0;
クエリパフォーマンスを理解するためのEXPLAIN
の使用
クエリプロファイルは、クエリ実行のどの段階が最も長くかかっているか、およびクエリに関連するリソースを理解するのに役立ちます。その後、MySQLのEXPLAIN
ステートメントを使用して、クエリオプティマイザによるクエリの評価を理解できます。
EXPLAIN
ステートメントは、SELECT
、DELETE
、UPDATE
、INSERT
、またはREPLACE
ステートメントを受け取り、クエリオプティマイザが指定されたクエリをどのように評価し実行するかを表示します。出力は、MySQLがテーブルを結合し、フィールドを選択し、結果をソートおよびフィルタリングする方法などを示します。また、評価される行数や、プロセスを高速化するために参照されるインデックスなども表示されます。
このステートメントがどのように機能するかを理解するために、MySQLにクエリSELECT * FROM INFORMATION_SCHEMA.VIEWS;
をどのように実行するかを説明してもらいます。
EXPLAIN SELECT * FROM information_schema.views\G
*************************** 1. row ***************************id: 1select_type: SIMPLEtable: catpartitions: NULLtype: indexpossible_keys: PRIMARYkey: namekey_len: 194ref: NULLrows: 1filtered: 100.00Extra: Using index*************************** 2. row ***************************id: 1select_type: SIMPLEtable: vwpartitions: NULLtype: refpossible_keys: schema_id,type,view_client_collation_id,view_connection_collation_id,type_2key: typekey_len: 1ref: constrows: 100filtered: 100.00Extra: Using where*************************** 3. row ***************************id: 1select_type: SIMPLEtable: schpartitions: NULLtype: eq_refpossible_keys: PRIMARY,catalog_idkey: PRIMARYkey_len: 8ref: mysql.vw.schema_idrows: 1filtered: 100.00Extra: Using where*************************** 4. row ***************************id: 1select_type: SIMPLEtable: conn_collpartitions: NULLtype: eq_refpossible_keys: PRIMARYkey: PRIMARYkey_len: 8ref: mysql.vw.view_connection_collation_idrows: 1filtered: 100.00Extra: NULL*************************** 5. row ***************************id: 1select_type: SIMPLEtable: client_collpartitions: NULLtype: eq_refpossible_keys: PRIMARY,character_set_idkey: PRIMARYkey_len: 8ref: mysql.vw.view_client_collation_idrows: 1filtered: 100.00Extra: NULL*************************** 6. row ***************************id: 1select_type: SIMPLEtable: cspartitions: NULLtype: eq_refpossible_keys: PRIMARYkey: PRIMARYkey_len: 8ref: mysql.client_coll.character_set_idrows: 1filtered: 100.00Extra: NULL6 rows in set, 1 warning (0.01 sec)
上記の出力は、そのクエリの情報を収集するために6つの個別のクエリが必要であることを示しています。テーブル定義を見ると、その理由がわかります。
SHOW CREATE TABLE information_schema.views\G
整形のために変更された、結果のテーブル作成コマンドは次のようになります。
CREATE algorithm=undefined definer=`mysql.infoschema`@`localhost` SQL security definer view `views`ASSELECT `cat`.`name` AS `table_catalog`,`sch`.`name` AS `table_schema`,`vw`.`name` AS `table_name`,IF((can_access_view(`sch`.`name`,`vw`.`name`,`vw`.`view_definer`,`vw`.`options`) = TRUE),`vw`.`view_definition_utf8`,'') AS `view_definition`,`vw`.`view_check_option` AS `check_option`,`vw`.`view_is_updatable` AS `is_updatable`,`vw`.`view_definer` AS `definer`,IF((`vw`.`view_security_type` = 'DEFAULT'),'DEFINER',`vw`.`view_security_type`) AS `security_type`,`cs`.`name` AS `character_set_client`,`conn_coll`.`name` AS `collation_connection`FROM (((((`mysql`.`tables` `vw`JOIN `mysql`.`schemata` `sch`ON ((`vw`.`schema_id` = `sch`.`id`)))JOIN `mysql`.`catalogs` `cat`ON ((`cat`.`id` = `sch`.`catalog_id`)))JOIN `mysql`.`collations` `conn_coll`ON ((`conn_coll`.`id` = `vw`.`view_connection_collation_id`)))JOIN `mysql`.`collations` `client_coll`ON ((`client_coll`.`id` = `vw`.`view_client_collation_id`)))JOIN `mysql`.`character_sets` `cs`ON ((`cs`.`id` = `client_coll`.`character_set_id`)))WHERE ((0 <> can_access_table(`sch`.`name`,`vw`.`name`))AND (`vw`.`type` = 'VIEW'))
5つのJOINステートメントがあり、結果を構築するために6つの個別のテーブルをクエリする必要があることを意味します。これは、実行したEXPLAIN
ステートメントで返されたエントリ数(テーブルごとに1つ)と一致します。
EXPLAIN
ステートメントの結果を解釈するには、各列が何を表しているかを理解する必要があります。特に重要なのは、type
、key
、rows
、filtered
の各列です。
type
列の解釈
type
列は、MySQLがクエリ内で結合操作をどのように実装するかを示します。MySQLドキュメントには、利用可能な異なる結合タイプと、それらがパフォーマンスにどのように影響するかについての詳細な説明があります。
type: const
たとえば、型がconst
の場合、MySQLが一意の列を定数値と比較しており、最大で1行しか返さないことを意味するため、非常に高速になります。
type: eq_ref
eq_ref
タイプも同様に、比較のために完全な一意のNULLなしインデックス値を使用することを示すため、2つのテーブルを結合するための最速の結合操作です。
type: index
index
タイプは、フルテーブルスキャンが必要な場合に使用されますが、実際のテーブルをスキャンする代わりに、クエリはインデックスツリーをスキャンできます。これにより、クエリの範囲内では比較的に低パフォーマンスですが、実際のテーブルではなくより小さなインデックスをスキャンできるため、ある程度最適化されています。
type: ref
ref
結合タイプは、MySQLがインデックスの先頭列を使用して比較できたことを意味します(インデックス全体に比較に使用されない複数の列が含まれている場合)。これは、MySQLが完全な比較に一致しない場合でも、部分的なインデックスを効率的に使用できることを意味します。
key
列の解釈
key
列および関連する列(possible_keys
、key_length
、およびref
)は、どのインデックスが利用可能で、どのインデックスが使用され、インデックスの使用がどれほど効率的だったかを理解するのに役立ちます。
. . .possible_keys: PRIMARYkey: PRIMARYkey_len: 8ref: mysql.client_coll.character_set_id. . .
possible_keys
列は、結合されるテーブルで利用可能なインデックスを示します。これは、クエリを満たすのに適切なインデックスを見つけようとするときにMySQLが何から始めたかを示すため役立ちます。非主キーでテーブルを結合している場合に、プロセスを高速化するためのインデックスが利用可能であることを確認するために調べる場所かもしれません。
key
列自体は、MySQLがクエリを実行するために使用することを決定したインデックスを示します。場合によっては、possible_keys
列にリストされていなかった結果も含まれることがあります。たとえば、インデックスがクエリが要求するすべての列を含んでいる場合、インデックスが実際の結合には適切でなかった(また使用されなかった)としても、関連するテーブルよりも効率的にスキャンできます。
key_length
列は、実際にインデックスのどれくらいが使用されたかを示します。複数列インデックスの場合、これはこの特定の比較のために格納されたインデックスのどれくらいが使用されたかを理解するのに役立ちます。ref
列も、特定のインデックスと比較されている具体的な内容を示すことで、ここで役割を果たします。これにより、より最適化されたインデックスを作成することが良いアイデアであるかどうかを判断するのに役立ちます。
評価された行の解釈
rows
およびfiltered
列は、検査されているテーブルの範囲を示すため役立ちます。
rows: 1filtered: 100.00
rows
列は、MySQLがクエリの各部分を実行するために評価する必要があると考える行数を示します。ここでの数値は、これまで議論したすべての列に影響されます。これは、提供されたステートメントと利用可能なインデックスに基づいて、MySQLがテーブル全体をどれだけ効率的に絞り込むことができるかを示すものです。
filtered
列は、クエリ内の行フィルタリング条件が適用された後に返される、評価された行の割合を推定します。値が100の場合、行フィルタリングは発生しないことを示します。100未満の値は、結合条件やその他のフィルタリングのため、評価された行の一部が返されないことを示します。
低速クエリを高速に実行できるように書き換える
MySQLが個々のステートメントのパフォーマンスを評価できるようにする方法のいくつかについて議論したので、そのパフォーマンスを改善する方法について話すことができます。
検討すべき正確な手順は、クエリプロファイリングとEXPLAIN
調査で見つかった内容に大きく依存します。以下で、クエリパフォーマンスを最適化する最も一般的な方法のいくつかについて説明します。
追加のインデックスを作成する
クエリの実行速度を向上させる最も簡単な方法の1つは、テーブルに適切なインデックスが利用可能であることを確認することです。インデックスは、データをクエリする最も一般的な方法を反映している必要があり、使用状況の進化や追加のクエリの実装に伴い、時間とともに変更される場合があります。
新しいインデックスを作成するには、CREATE INDEX
ステートメントを使用できます。
CREATE INDEX <index_name> ON <table> (<columns_to_index>)
インデックス化する列のデータ型、値の最大長、使用したいインデックス構造によって、作成できるインデックスの種類は多岐にわたります。クエリがすでにインデックスを使用しており、そのインデックスのパフォーマンスが低い場合は、これらのオプションを検討する価値があるかもしれません。しかし、まずは、ほとんどデフォルト値を使用することで、比較的シンプルに始めることができます。
最初に行うべき考慮事項は、どの列にインデックスを作成するかです。特定の列で定期的に結合を行う場合は、それらがインデックスに含まれていることを確認することが重要です。インデックスは主に列のルックアップに使用されるため、異なるテーブルの様々な行が一致する結合操作は、インデックスがないと非常にコストがかかります。
通常テーブルをクエリする方法を完全にカプセル化している場合は、単一列でインデックスを作成できます。インデックス作成をより有用にするために、複数列でインデックスを作成することもできます。複数列でインデックスを作成する場合、順序が非常に重要です。最初の列は一致を検索する主要な方法であるべきであり、追加の列はそれらの一致を絞り込むのに役立つべきです。
たとえば、person
テーブルのfirst_name
とlast_name
にインデックスを作成する場合、ほぼ確実に次のような形でインデックスを定義することになります。
CREATE INDEX last_and_first_names ON person (last_name, first_name);
ここで、last_name
列はインデックスの最初または左端の列であり、これはAlan Smith
またはすべてのSmith
エントリを高速に検索できることを意味します。ただし、first_name
としてAlan
のすべてのインスタンスを検索するのには効果的ではありませんが、おそらくファーストネームのみで頻繁にクエリを実行することを想定しない限り、問題ありません。
インデックスを作成することでデータセットの読み取りパフォーマンスを向上させることはできますが、テーブルにインデックスを過剰に作成しないことが重要です。新しい値がテーブルに記録されるたびに、それに関連するすべてのインデックスも更新する必要があるため、インデックスは書き込み速度のコストを伴います。テーブルは、プライマリキーが使用されるクエリパターンにうまく適合している場合に最も効果的であり、これにより、異なるアクセスパターンに対応するために多数の補足インデックスを必要とせずに、強力なインデックスパフォーマンスの恩恵を受けることができます。
候補行の制限
クエリを高速化するもう1つの方法は、可能な限りステートメント内の候補行と返される行の数を制限することです。これは非常にコンテキストに依存する推奨事項であるため、多くの場合には適切ではありませんが、MySQLが評価しなければならない行数を減らすことができれば、パフォーマンスを大幅に向上させることができます。
これはLIMIT
句を使用して行うことができます。たとえば、5つの結果のみを返すには、次のような形式を使用します。
SELECT * FROM <table> LIMIT 5
多くの場合(クエリに含まれる他の句にもよりますが)、返される行の数を制限することで、MySQLがクエリプロセスをショートカットし、通常よりも早く停止するのに役立ちます。これにより、データセット全体を実行してから後で結果を切り捨てるのではなく、早い段階で時間を節約できます。
これは複数のテーブルを結合する際に特に役立ちます。可能な限り、結合が発生する前にクエリの結果を制限することが最善です。これにより、MySQLは最初のテーブル全体ではなく、少量のデータを残りの結合に対して評価することができます。
関連するフィールドのみを選択する
クエリのパフォーマンスを向上させるもう1つの方法は、実際に返している列に注意を払うことです。これにより、いくつかの異なる方法で結果を高速化できます。
選択する列を制限することがパフォーマンスに影響を与える主な方法は、ネットワークペイロードを最小限に抑えることです。不要な列を返すことで、より大きなクエリはクエリによって生成されるネットワークトラフィックを大幅に増加させる可能性があります。クエリの論理的制約を満たすために必要な列のみを選択することで、ネットワーク経由で転送されるデータを削減し、この問題を防止するのに役立ちます。
返される列を厳選することがパフォーマンスに影響を与えるもう1つの方法は、インデックスに関連しています。MySQLが返されるすべての列をインデックスと関連付けることができる場合、完全なテーブルではなく、より小さく整理されたデータセット上で操作できる可能性があります。クエリする列を制限することで、インデックスがクエリパターンとよく一致している場合にこれが可能になります。
先頭のワイルドカードを含む比較を削除する
クエリのパフォーマンスに悪影響を与える可能性のあることの1つは、ワイルドカードへの過度の依存です。SQLでは、%
文字は比較において、任意の値が置換可能であることを示すワイルドカードとして使用されます。これはレコード内のデータを見つける非常に強力な方法ですが、クエリパフォーマンスに非常に大きな影響を与える可能性があります。
これは特に先頭のワイルドカードまたはプレフィックスワイルドカードに当てはまります。先頭のワイルドカードは、列の値がワイルドカードで始まるパターンに一致するかどうかをチェックする比較です。たとえば、列が%Main St.
に一致するかどうかをチェックする場合です。これは、MySQLがインデックスを使用して一致する結果を発見できないため、特にパフォーマンスが悪いです。その列にインデックスがある場合でも、一致するものをテーブル全体で検索しなければなりません。
この特定の例では、ある通りのすべての住所を発見するために、定期的に通り名でクエリを実行する予定がある場合、通り名と番地を2つの異なる列に分割する方が理にかなっているかもしれません。これにより、「Main St.」の住所をインデックスを使用して迅速にクエリし、特定の住所を探している場合に必要に応じてそれらの結果をフィルタリングできるようになります。通り名を最初に、番地を2番目にした複合インデックスを作成すると、これがさらに高速になります。
まとめ
このガイドでは、個々のクエリを評価し、何がそれらを遅くしているのかについてさらに情報を得るための基本的な方法をいくつか説明しました。クエリのプロファイリングとMySQLのEXPLAIN
ステートメントを使用して、クエリプランナーがクエリをどのように評価するかについて詳細な情報を得る方法について議論しました。その後、データベースがデータを取得する方法によりよく合うようにクエリを修正することで、これらの遅延を回避できるいくつかの方法について検討しました。
データベースは、多くの異なる形式で存在する可能性のある大量のデータを操作する責任を持つ複雑なソフトウェアです。必要なデータを単に取得するだけでなく、データベースが迅速に結果を返すことができるという期待があります。MySQLのクエリプランナーとオプティマイザのメカニズムには、クエリに応答するのにかかる時間を最小限に抑えるために、数えきれないほどの最適化が組み込まれています。
しかし、データベースが自動的に最適化できるのは限られた範囲です。データベースのユーザーとして、アクセスされているデータ、クエリの要件、そしてどのクエリが最適化にとって最も重要であるかを理解できる必要があります。このため、クエリのパフォーマンスが期待されるレベルにない場合に、基準を評価し、調整を行う方法を理解することが重要です。
MySQLデータベースでPrismaを使用している場合、ドキュメントのクエリ最適化セクションでクエリを最適化する方法について読むことができます。これにより、Prismaを使用する際に、さまざまなクエリ構造がデータベースパフォーマンスにどのように影響するかを理解するのに役立ちます。