序文
MySQL は、2016 年もデータベースの人気が力強く成長し続けました。
MySQL データベース上でアプリケーションを構築したり、Oracle から MySQL に移行したりする顧客が増えています。ただし、一部の顧客は、MySQL データベースの使用時に応答時間の遅さや CPU のフル使用率などの問題に遭遇することもあります。
Alibaba Cloud の RDS エキスパート サービス チームは、クラウド顧客が多くの緊急問題を解決できるよう支援してきました。「ApsaraDB Expert Diagnostic Report」に表示される一般的な SQL の問題の一部を、参考までに以下にまとめます。
1. LIMIT ステートメント
ページング クエリは最も一般的に使用されるシナリオの 1 つですが、通常は問題が発生する可能性が最も高い場所でもあります。たとえば、次の単純なステートメントの場合、一般的な DBA のアイデアは、type、name、create_time フィールドに結合インデックスを追加することです。このように、条件付きソートはインデックスを有効に活用することができ、パフォーマンスを急速に向上させることができます。ソースコーダーのプログラミングに関する詳細な注意事項
SELECT *
FROM operation
WHERE type = 'SQLStats'
AND name = 'SlowLog'
ORDER BY create_time
LIMIT 1000, 10;
おそらく、90% 以上の DBA がこの問題を解決してそこで止まってしまうでしょう。しかし、LIMIT 句が「LIMIT 1000000,10」になっても、プログラマは依然として不満を抱くでしょう。「レコードを 10 件しかフェッチしないのに、なぜまだ遅いのですか?」
データベースでは 1,000,000 番目のレコードがどこから始まるのかがわからないため、インデックスがあっても最初から計算する必要があることを知っておく必要があります。この種のパフォーマンスの問題が発生する場合、ほとんどの場合、プログラマは怠惰です。フロントエンドのデータ閲覧やページめくり、ビッグデータの一括エクスポートなどのシナリオでは、前のページの最大値をクエリ条件のパラメータとして使用できます。SQL は次のように再設計されています。
SELECT *
FROM operation
WHERE type = 'SQLStats'
AND name = 'SlowLog'
AND create_time > '2017-03-16 14:00:00'
ORDER BY create_time limit 10;
新しい設計では、クエリ時間は基本的に固定されており、データ量が増加しても変化しません。
2. 暗黙的な変換
SQL ステートメント内のクエリ変数とフィールド定義の型が一致しないことも、よくあるエラーです。たとえば、次のステートメント:
mysql> explain extended SELECT *
> FROM my_balance b
> WHERE b.bpn = 14000000123
> AND b.isverified IS NULL ;
mysql> show warnings;
| Warning | 1739 | Cannot use ref access on index 'bpn' due to type or collation conversion on field 'bpn'
フィールド bpn は varchar(20) として定義されており、MySQL の戦略は文字列を数値に変換して比較することです。この関数はテーブルのフィールドに作用し、インデックスは無効になります。
上記の状況は、プログラマの本来の意図ではなく、アプリケーション フレームワークによって自動的にパラメータが入力された可能性があります。最近は非常に複雑なアプリケーションフレームワークが多く、使いやすい反面、穴を掘ってしまう可能性もあるので注意が必要です。
3. 関連付けの更新と削除
MySQL 5.6 ではマテリアライゼーション機能が導入されていますが、現時点ではクエリ ステートメントに対してのみ最適化されていることに注意することが重要です。更新または削除の場合は、手動で JOIN に書き換える必要があります。
たとえば、以下の UPDATE ステートメントでは、MySQL は実際にループ/ネストされたサブクエリ (DEPENDENT SUBQUERY) を実行しますが、その実行時間は想像できます。ソース公開アカウント: プログラマーのためのプログラミングに関する詳細な注意事項
UPDATE operation o
SET status = 'applying'
WHERE o.id IN (SELECT id
FROM (SELECT o.id,
o.status
FROM operation o
WHERE o.group = 123
AND o.status NOT IN ( 'done' )
ORDER BY o.parent,
o.id
LIMIT 1) t);
実行計画:
+----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+
| 1 | PRIMARY | o | index | | PRIMARY | 8 | | 24 | Using where; Using temporary |
| 2 | DEPENDENT SUBQUERY | | | | | | | | Impossible WHERE noticed after reading const tables |
| 3 | DERIVED | o | ref | idx_2,idx_5 | idx_5 | 8 | const | 1 | Using where; Using filesort |
+----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+
JOIN に書き換えると、サブクエリの選択モードが DEPENDENT SUBQUERY から DERIVED に変わり、実行速度が 7 秒から 2 ミリ秒と大幅に高速化されます。
UPDATE operation o
JOIN (SELECT o.id,
o.status
FROM operation o
WHERE o.group = 123
AND o.status NOT IN ( 'done' )
ORDER BY o.parent,
o.id
LIMIT 1) t
ON o.id = t.id
SET status = 'applying'
実行計画は次のように単純化されます。
+----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+
| 1 | PRIMARY | | | | | | | | Impossible WHERE noticed after reading const tables |
| 2 | DERIVED | o | ref | idx_2,idx_5 | idx_5 | 8 | const | 1 | Using where; Using filesort |
+----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+
4. ハイブリッドソート
MySQL は混合ソートにインデックスを使用できません。ただし、一部のシナリオでは、パフォーマンスを向上させるために特別な方法を使用する機会がまだあります。
SELECT *
FROM my_order o
INNER JOIN my_appraise a ON a.orderid = o.id
ORDER BY a.is_reply ASC,
a.appraise_time DESC
LIMIT 0, 20
実行計画にはテーブル全体のスキャンが表示されます。
+----+-------------+-------+--------+-------------+---------+---------+---------------+---------+-+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra
+----+-------------+-------+--------+-------------+---------+---------+---------------+---------+-+
| 1 | SIMPLE | a | ALL | idx_orderid | NULL | NULL | NULL | 1967647 | Using filesort |
| 1 | SIMPLE | o | eq_ref | PRIMARY | PRIMARY | 122 | a.orderid | 1 | NULL |
+----+-------------+-------+--------+---------+---------+---------+-----------------+---------+-+
is_reply は 0 と 1 の 2 つの状態しか持たないため、次の方法で書き換えると、実行時間が 1.58 秒から 2 ミリ秒に短縮されました。
SELECT *
FROM ((SELECT *
FROM my_order o
INNER JOIN my_appraise a
ON a.orderid = o.id
AND is_reply = 0
ORDER BY appraise_time DESC
LIMIT 0, 20)
UNION ALL
(SELECT *
FROM my_order o
INNER JOIN my_appraise a
ON a.orderid = o.id
AND is_reply = 1
ORDER BY appraise_time DESC
LIMIT 0, 20)) t
ORDER BY is_reply ASC,
appraisetime DESC
LIMIT 20;
5. EXISTS ステートメント
MySQL が EXISTS 句を処理するときも、ネストされたサブクエリの実行が使用されます。たとえば、次の SQL ステートメントです。
SELECT *
FROM my_neighbor n
LEFT JOIN my_neighbor_apply sra
ON n.id = sra.neighbor_id
AND sra.user_id = 'xxx'
WHERE n.topic_status < 4
AND EXISTS(SELECT 1
FROM message_info m
WHERE n.id = m.neighbor_id
AND m.inuser = 'xxx')
AND n.topic_type <> 5
実行計画は次のとおりです。
+----+--------------------+-------+------+-----+------------------------------------------+---------+-------+---------+ -----+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------------+-------+------+ -----+------------------------------------------+---------+-------+---------+ -----+
| 1 | PRIMARY | n | ALL | | NULL | NULL | NULL | 1086041 | Using where |
| 1 | PRIMARY | sra | ref | | idx_user_id | 123 | const | 1 | Using where |
| 2 | DEPENDENT SUBQUERY | m | ref | | idx_message_info | 122 | const | 1 | Using index condition; Using where |
+----+--------------------+-------+------+ -----+------------------------------------------+---------+-------+---------+ -----+
存在を削除して結合に変更すると、ネストされたサブクエリを回避し、実行時間を 1.93 秒から 1 ミリ秒に短縮できます。
SELECT *
FROM my_neighbor n
INNER JOIN message_info m
ON n.id = m.neighbor_id
AND m.inuser = 'xxx'
LEFT JOIN my_neighbor_apply sra
ON n.id = sra.neighbor_id
AND sra.user_id = 'xxx'
WHERE n.topic_status < 4
AND n.topic_type <> 5
新しい実行計画:
+----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+
| 1 | SIMPLE | m | ref | | idx_message_info | 122 | const | 1 | Using index condition |
| 1 | SIMPLE | n | eq_ref | | PRIMARY | 122 | ighbor_id | 1 | Using where |
| 1 | SIMPLE | sra | ref | | idx_user_id | 123 | const | 1 | Using where |
+----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+
6. 条件付きプッシュダウン
外部クエリ条件を複雑なビューまたはサブクエリにプッシュダウンできない状況には、次のようなものがあります。
集約サブクエリ。
LIMIT を含むサブクエリ。
UNION または UNION ALL サブクエリ。
出力フィールドのサブクエリ。
たとえば、次のステートメントでは、その条件が集計サブクエリの後に適用されることが実行プランからわかります。
SELECT *
FROM (SELECT target,
Count(*)
FROM operation
GROUP BY target) t
WHERE target = 'rm-xxxx'
+----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+
| 1 | PRIMARY | <derived2> | ref | <auto_key0> | <auto_key0> | 514 | const | 2 | Using where |
| 2 | DERIVED | operation | index | idx_4 | idx_4 | 519 | NULL | 20 | Using index |
+----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+
クエリ条件を意味的に直接プッシュダウンできることを確認した後、次のように書き換えます。
SELECT target,
Count(*)
FROM operation
WHERE target = 'rm-xxxx'
GROUP BY target
実行計画は次のようになります。
+----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+
| 1 | SIMPLE | operation | ref | idx_4 | idx_4 | 514 | const | 1 | Using where; Using index |
+----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+
プッシュダウンできない MySQL の外部条件の詳細については、以前の記事「MySQL · パフォーマンスの最適化 · マテリアライズド テーブルにプッシュダウンされる条件」を参照してください。
7. 事前に範囲を絞り込む
まず、最初の SQL ステートメントは次のとおりです。
SELECT *
FROM my_order o
LEFT JOIN my_userinfo u
ON o.uid = u.uid
LEFT JOIN my_productinfo p
ON o.pid = p.pid
WHERE ( o.display = 0 )
AND ( o.ostaus = 1 )
ORDER BY o.selltime DESC
LIMIT 0, 15
この SQL ステートメントの本来の意味は、まず一連の左結合を実行し、次にソートして最初の 15 レコードを取得することです。また、実行計画から、最後のステップでソートされたレコードの推定数は 900,000 で、所要時間は 12 秒であることがわかります。
+----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+
| 1 | SIMPLE | o | ALL | NULL | NULL | NULL | NULL | 909119 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | u | eq_ref | PRIMARY | PRIMARY | 4 | o.uid | 1 | NULL |
| 1 | SIMPLE | p | ALL | PRIMARY | NULL | NULL | NULL | 6 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+
最後の WHERE 条件と並べ替えはすべて左端のメイン テーブルに対するものであるため、事前に my_order を並べ替えてデータ量を減らしてから左結合を実行できます。SQLを以下のように書き換えたところ、実行時間は1ミリ秒程度に短縮されました。
SELECT *
FROM (
SELECT *
FROM my_order o
WHERE ( o.display = 0 )
AND ( o.ostaus = 1 )
ORDER BY o.selltime DESC
LIMIT 0, 15
) o
LEFT JOIN my_userinfo u
ON o.uid = u.uid
LEFT JOIN my_productinfo p
ON o.pid = p.pid
ORDER BY o.selltime DESC
limit 0, 15
実行計画を再度確認します。サブクエリが実体化された後 (select_type=DERIVED)、JOIN に参加します。推定行スキャンは依然として 900,000 ですが、インデックスと LIMIT 句を使用すると、実際の実行時間は非常に短くなります。
+----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 15 | Using temporary; Using filesort |
| 1 | PRIMARY | u | eq_ref | PRIMARY | PRIMARY | 4 | o.uid | 1 | NULL |
| 1 | PRIMARY | p | ALL | PRIMARY | NULL | NULL | NULL | 6 | Using where; Using join buffer (Block Nested Loop) |
| 2 | DERIVED | o | index | NULL | idx_1 | 5 | NULL | 909112 | Using where |
+----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+
8. 中間結果セットをプッシュダウンします。
最初に最適化された次の例を見てみましょう (左結合のメインテーブルがクエリ条件より優先されます)。
SELECT a.*,
c.allocated
FROM (
SELECT resourceid
FROM my_distribute d
WHERE isdelete = 0
AND cusmanagercode = '1234567'
ORDER BY salecode limit 20) a
LEFT JOIN
(
SELECT resourcesid, sum(ifnull(allocation, 0) * 12345) allocated
FROM my_resources
GROUP BY resourcesid) c
ON a.resourceid = c.resourcesid
それでは、この声明には他に問題があるのでしょうか? サブクエリ c が完全なテーブル集計クエリであることは、難しくありません。これにより、テーブルの数が特に多い場合、ステートメント全体のパフォーマンスが低下します。
実際、サブクエリ c の場合、左結合の最終結果セットは、メイン テーブルの resourceid と一致するデータのみを考慮します。したがって、ステートメントを次のように書き直すと、実行時間は元の 2 秒から 2 ミリ秒に短縮されます。
SELECT a.*,
c.allocated
FROM (
SELECT resourceid
FROM my_distribute d
WHERE isdelete = 0
AND cusmanagercode = '1234567'
ORDER BY salecode limit 20) a
LEFT JOIN
(
SELECT resourcesid, sum(ifnull(allocation, 0) * 12345) allocated
FROM my_resources r,
(
SELECT resourceid
FROM my_distribute d
WHERE isdelete = 0
AND cusmanagercode = '1234567'
ORDER BY salecode limit 20) a
WHERE r.resourcesid = a.resourcesid
GROUP BY resourcesid) c
ON a.resourceid = c.resourcesid
ただし、サブクエリ a は SQL ステートメント内に複数回出現します。この書き方では追加のオーバーヘッドが発生するだけでなく、ステートメント全体が複雑になります。WITH ステートメントを使用して再度書き直します。
WITH a AS
(
SELECT resourceid
FROM my_distribute d
WHERE isdelete = 0
AND cusmanagercode = '1234567'
ORDER BY salecode limit 20)
SELECT a.*,
c.allocated
FROM a
LEFT JOIN
(
SELECT resourcesid, sum(ifnull(allocation, 0) * 12345) allocated
FROM my_resources r,
a
WHERE r.resourcesid = a.resourcesid
GROUP BY resourcesid) c
ON a.resourceid = c.resourcesid
要約する
データベース コンパイラは、SQL が実際にどのように実行されるかを決定する実行プランを生成します。しかし、コンパイラはサービスを提供するために最善を尽くしているだけであり、すべてのデータベース コンパイラが完璧であるわけではありません。上記のシナリオのほとんどでは、他のデータベースでもパフォーマンスの問題が発生します。データベース コンパイラの特性を理解することによってのみ、データベース コンパイラの欠点を回避し、高パフォーマンスの SQL ステートメントを作成できます。
プログラマーは、データ モデルを設計したり SQL ステートメントを作成したりするときに、アルゴリズムのアイデアや認識を取り入れる必要があります。複雑な SQL ステートメントを作成する場合は、WITH ステートメントを使用する習慣を身に付ける必要があります。シンプルで明確な SQL ステートメントにより、データベースの負担も軽減されます。