404 Not Found

404 Not Found


nginx

MySQLのパフォーマンス最適化:低速クエリの分析とインデックスのチューニングに関する実践ガイド

年末セール期間中、アリスのECプラットフォームでは、データベースの応答時間が50ミリ秒から5秒へと急上昇し、ユーザーからは「ページが表示されない」「注文送信がタイムアウトする」といった苦情が相次ぎました。運用チームが緊急調査を行ったところ、最適化されていない低速クエリが複数存在し、システム全体の動作を著しく遅らせていたことが判明しました。低速クエリのロギングを有効にして問題のあるSQL文を特定し、EXPLAINを用いて実行プランを分析し、不足していたインデックスを追加し、非効率なクエリを書き換え、InnoDBの設定パラメータを調整した結果、最終的に応答時間を100ミリ秒未満まで短縮することに成功しました。

1. 学習内容


2. エンドツーエンドのパフォーマンス最適化プロセス

パフォーマンスの最適化は、一度きりの作業ではなく、「発見→分析→最適化→検証」という循環的なプロセスです。

100%
flowchart TD
    A[Identifying Slow Queries] --> B[EXPLAIN Analyze the Execution Plan]
    B --> C{Issue Type?}
    C -->|Missing Index| D[Index Optimization]
    C -->|Inefficient query syntax| E[Query Rewrite]
    C -->|Unreasonable configuration| F[Configuration Tuning]
    D --> G[Verifying Performance Improvements]
    E --> G
    F --> G
    G -->|Still does not meet the standards| A
    G -->|Meet the requirements| H[Deployment Monitoring]

3. スロークエリログ

スロークエリログは、パフォーマンスの問題を特定するための第一の防衛線であり、実行時間が一定の閾値を超えたSQL文を自動的に記録します。

(1) はじめにおよび設定

SQL
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = ON;
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

(2) ログ解析ツール

mysqldumpslow は、MySQL に組み込まれているスロークエリログ集計ツールであり、クエリを実行時間や頻度順に並べ替えます。

BASH
mysqldumpslow -s t -t 10 /var/lib/mysql/slow.log

(3) パフォーマンス・スキーマの代替手段

MySQL 5.6 以降のバージョンでは、Performance Schema を使用して遅いクエリを収集する機能がサポートされており、設定ファイルを変更することなく動的に有効化することができます。

SQL
UPDATE performance_schema.setup_consumers
SET ENABLED = 'YES' WHERE NAME = 'events_statements_history_long';

▶ 例:スロークエリログを有効にし、上位5つのスロークエリを特定する

SQL
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.5;
SET GLOBAL min_examined_row_limit = 100;
SHOW VARIABLES LIKE 'slow_query_log_file';
▶ 試してみよう
BASH
mysqldumpslow -s t -t 5 /var/lib/mysql/slow.log

▶ 例:sysビューを使用して、処理に時間がかかるクエリをすばやく確認する

SQL
SELECT query_id, LEFT(query, 80) AS query_text,
       exec_count, avg_timer_ms, rows_examined
FROM sys.statements_with_runtimes_in_95th_percentile
ORDER BY avg_timer_ms DESC LIMIT 10;
▶ 試してみよう

4. EXPLAIN 実行計画の詳細な分析

EXPLAIN は、SQL チューニングにおいて最も重要な診断ツールであり、MySQL がクエリをどのように実行するかを示します。

(1) EXPLAIN 出力フィールドの概要

項目 意味 要点
id クエリ番号 サブクエリの実行順序
select_type クエリの種類 DERIVED、UNCACHEABLE を避ける
テーブル 参照されたテーブル 関連するテーブルの数
タイプ アクセスタイプ 「system」から「ALL」まで。左側にあるほど良い
possible_keys 作成可能なインデックス キーとの比較に基づくインデックスの選択
キー 実際に使用されたインデックス NULL はインデックスが使用されなかったことを示す
key_len インデックスの長さ 複合インデックスで使用されるフィールドの数を決定します
行数 スキャン対象となる行数の推定値 数値が低いほど良い
補足 追加情報 filesort の使用/一時ファイルの使用—最適化が必要

(2) 「type」フィールドの詳細な説明

typeフィールドは、EXPLAINの中で最も重要なフィールドであり、クエリの効率を直接反映しています。

種類 意味 スキャン方法 性能評価
システム テーブルに1行のみ 直接読み取り ★★★★★
const 主キー/一意インデックスによる等値クエリ 最大1行に一致 ★★★★★
eq_ref 結合における主キー/一意インデックス 各行に対して1行を結合 ★★★★☆
ref 一意でないインデックスに対する等値クエリ 複数の行に一致 ★★★☆☆
範囲 インデックス範囲スキャン BETWEEN/IN/>/< ★★★☆☆
インデックス インデックスの完全スキャン インデックスツリー全体を走査 ★★☆☆☆
すべて テーブル全体のスキャン テーブル全体を順に処理 ★☆☆☆☆

(3) 追加フィールドのキー値

▶ 例:EXPLAIN を使用した単一テーブルクエリの分析

SQL
EXPLAIN SELECT order_id, user_id, total_amount
FROM orders
WHERE user_id = 42 AND status = 'PAID';
▶ 試してみよう
TEXT
+----+-------------+--------+------+---------------+------+---------+------+------+-----------------------+
| id | select_type | table  | type | possible_keys | key  | key_len | rows | Extra                     |
+----+-------------+--------+------+---------------+------+---------+------+------+-----------------------+
|  1 | SIMPLE      | orders | ref  | idx_user      | idx_user | 4    |  120 | Using where               |
+----+-------------+--------+------+---------------+------+---------+------+------+-----------------------+

▶ 例:EXPLAIN を使用した結合クエリの分析

SQL
EXPLAIN SELECT o.order_id, u.username
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.create_time > '2025-01-01';
▶ 試してみよう

5. インデックスの最適化戦略

インデックスはクエリを高速化する強力なツールですが、不適切なインデックスを使用することは、インデックスを全く使用しないことよりも危険です。

(1) カバレッジ指数

カバリングインデックスとは、クエリで使用されるすべてのフィールドがインデックスに含まれているため、行データを読み取るためにテーブルにアクセスする必要がないインデックスのことです。

SQL
ALTER TABLE orders ADD INDEX idx_cover (user_id, status, total_amount);

SELECT user_id, status, total_amount FROM orders WHERE user_id = 42 に対するクエリでは、データはすべてインデックスから取得されます。

(2) 複合インデックスと最左前置子

複合インデックスは「最左接頭辞の原則」に従います。つまり、クエリ条件はインデックスの最左の列から一致し始めなければなりません。

SQL
ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, create_time);
クエリ条件 インデックスがヒットするか? 理由
WHERE user_id = 1 最左列と一致
WHERE user_id = 1 AND status = 'PAID' 最初の2つの列に一致
WHERE user_id = 1 AND create_time > '2025-01-01' ⚠️ user_id のみを対象とし、status はスキップ
WHERE status = 'PAID' 最左列の user_id が欠落しています

(3) インデックス障害のシナリオに関するクイックリファレンス

失敗例 誤った構文 正しい構文
インデックス付きカラムでの関数の使用 WHERE YEAR(create_time) = 2025 WHERE create_time >= '2025-01-01' AND create_time < '2026-01-01'
暗黙の型変換 WHERE varchar_col = 123 WHERE varchar_col = '123'
左側ファジー検索 WHERE name LIKE '%alice' WHERE name LIKE 'alice%'
OR 演算子でインデックスのない列を結合している WHERE indexed_col = 1 OR unindexed = 2 UNION に分割するか、インデックスのない列にインデックスを追加する
異なる WHERE status != 'PAID' WHERE status IN ('UNPAID', 'CANCELLED')
計算に使用されるインデックス列 WHERE id + 1 = 100 WHERE id = 99

▶ 例:複合インデックスの最左端のプレフィックスの検証

SQL
ALTER TABLE products ADD INDEX idx_cat_brand_price (category_id, brand_id, price);

EXPLAIN SELECT * FROM products WHERE category_id = 5 AND brand_id = 10;
EXPLAIN SELECT * FROM products WHERE brand_id = 10;
▶ 試してみよう

2番目のクエリでは、左端の列である category_id をスキップするため、typeALL となります。

▶ 例:カバリングインデックスを使用してテーブルの参照を排除する

SQL
ALTER TABLE orders ADD INDEX idx_user_status_amount (user_id, status, total_amount);

EXPLAIN SELECT user_id, status, total_amount
FROM orders WHERE user_id = 42;
▶ 試してみよう

「Extra」欄に Using index と表示されている場合、テーブル参照は不要であることを意味します。


6. クエリ最適化の手法

インデックスがあっても、クエリの記述が不適切であれば、その恩恵を受けることはできません。

(1) SELECT * の使用は避ける

SELECT * を使用すると、すべての列が読み込まれ、I/O が増加し、カバリングインデックスが機能しなくなる可能性があります。

SQL
SELECT id, username, email FROM users WHERE id = 100;

(2) サブクエリを JOIN に書き換える

関連するサブクエリは各行ごとに1回実行されますが、これをJOINに書き換えることで、スキャン回数を大幅に削減できます。

SQL
SELECT o.order_id, o.total_amount
FROM orders o
WHERE o.user_id IN (SELECT id FROM users WHERE vip_level >= 3);

最適化対象:

SQL
SELECT o.order_id, o.total_amount
FROM orders o
JOIN users u ON o.user_id = u.id AND u.vip_level >= 3;

(3) 詳細なページネーションの最適化

オフセットが大きい場合、従来の LIMIT offset, n の性能は著しく低下します。

SQL
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;

最適化計画—カーソルによるページネーション:

SQL
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 10;

(4) クエリ最適化手法のクイックリファレンス

最適化のヒント アンチパターン 推奨される手法 適用可能なシナリオ
SELECT * を避ける SELECT * 必要な列のみをクエリする すべてのクエリ
サブクエリをJOINに変換する WHERE IN (SELECT ...) JOIN ... ON ... 結合クエリ
カーソルによるページネーション LIMIT 100000, 10 WHERE id > last_id LIMIT 10 ディープページネーション
一括挿入 個々のレコードをループ処理してINSERT INSERT INTO ... VALUES (...),(...),(...) データインポート
大規模なトランザクションを避ける 長期にわたるロックの保持 トランザクションを分割する 高同時実行性の書き込み

▶ 例:ディープページネーションの最適化比較

SQL
SELECT * FROM orders ORDER BY create_time LIMIT 500000, 20;
▶ 試してみよう

遅延関連付けに最適化されています:

SQL
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY create_time LIMIT 500000, 20) t
ON o.id = t.id;

このサブクエリは主キーのみを取得し、カバリングインデックスを使用してIDを素早く特定した後、テーブルから完全なデータを取得します。

▶ 例:一括挿入の最適化

SQL
INSERT INTO order_items (order_id, product_id, quantity)
VALUES (1001, 201, 2), (1001, 202, 1), (1001, 203, 5);
▶ 試してみよう

ループ内で単一のINSERT文を3回実行する場合と比較して、これによりネットワークの往復回数とトランザクションのコミットにかかるオーバーヘッドが2回分削減されます。


7. テーブル構造の最適化

適切に設計されたテーブル構造こそがパフォーマンスの基盤であり、SQLを書き直しても、既存の構造を最適化することしかできません。

(1) フィールドタイプの選択

データ型 推奨される選択肢 理由
主キー BIGINT UNSIGNED 自動インクリメントされる整数。B-木による高い挿入効率
ステータス/列挙型 TINYINT 1バイトの格納サイズ、CHECK制約と併用
金額 DECIMAL(10,2) 浮動小数点誤差を避けるため、正確に計算してください
短いテキスト VARCHAR(N) 実際の長さに基づいて領域を割り当てる
長文 テキスト オーバーフローがメインレコードに影響を与えないよう、別途保存される
時間 DATETIME / TIMESTAMP TIMESTAMP は 4 バイトを占有するが、範囲に制限がある
Boolean TINYINT(1) MySQL にはネイティブの BOOLEAN 型がありません

(2) アンチパラダイム・デザイン

クエリ負荷が高い状況では、適度な冗長性を確保することで、JOIN操作の回数を減らすことができます。

SQL
CREATE TABLE order_summary (
    order_id BIGINT PRIMARY KEY,
    user_id BIGINT,
    username VARCHAR(64),
    total_amount DECIMAL(10,2),
    INDEX idx_user (user_id)
);

orders テーブルの username 列を複製し、クエリごとに users テーブルと JOIN を行う必要をなくします。

▶ 例:フィールドタイプの最適化の比較

SQL
ALTER TABLE products MODIFY COLUMN weight DECIMAL(8,2);
ALTER TABLE products MODIFY COLUMN description TEXT;
▶ 試してみよう

プライマリレコードの長さを短縮するために、weight 列を VARCHAR から DECIMAL に変更し、description 列を VARCHAR(5000) から TEXT に変更してください。


8. 設定パラメータの調整

MySQLの設定パラメータは、ストレージエンジンの動作やリソースの割り当てに直接影響を与えます。

(1) 主な設定パラメータと推奨値

パラメータ 説明 推奨値 最適化の根拠
innodb_buffer_pool_size InnoDB バッファプールのサイズ 物理メモリの 60%~80% データページやインデックスページをキャッシュし、ディスク I/O を削減する
innodb_log_file_size 1つのリドゥログファイルのサイズ 256M–1G 小さすぎるとチェックポイントが頻繁に行われる
max_connections 最大同時接続数 200–500 値が高すぎるとメモリを浪費し、低すぎると接続が拒否される
innodb_flush_method フラッシュ方法 O_DIRECT OSのキャッシュをバイパスし、二重キャッシュを回避する
sync_binlog バイナリログの同期頻度 1 (セキュリティ) / 100 (パフォーマンス) 1 はコミットごとに同期を行うことを意味します。これが最も安全なオプションです
innodb_io_capacity InnoDB I/O 容量 SSD: 2000 / HDD: 200 バックグラウンドでのダーティページのフラッシュ速度に影響する
query_cache_type クエリキャッシュの有効/無効 OFF MySQL 8.0 で削除されました。5.7 では無効にすることを推奨します

(2) パラメータの動的な調整

一部のパラメータは、インスタンスを再起動することなくオンラインで変更できます。

SQL
SET GLOBAL innodb_buffer_pool_size = 8589934592;
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';

▶ 例:現在の InnoDB バッファプールのヒット率を確認する

SQL
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
▶ 試してみよう
TEXT
+---------------------------------------+-----------+
| Variable_name                         | Value     |
+---------------------------------------+-----------+
| Innodb_buffer_pool_read_requests      | 1024000   |
| Innodb_buffer_pool_reads              | 512       |
+---------------------------------------+-----------+

ヒット率 = 1 - (512 / 1024000) ≈ 99.95%;バッファプールのサイズは適切です。


9. パフォーマンス・スキーマの概要

パフォーマンス・スキーマは、MySQLに組み込まれたパフォーマンス監視エンジンであり、スロークエリログよりも詳細な診断データを提供します。

(1) パフォーマンス・スキーマを有効にする

SQL
SHOW VARIABLES LIKE 'performance_schema';
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME LIKE '%statement/%';

(2) 一般的な監視ビュー

システムビュー 目的
95パーセンタイルの実行時間を伴うステートメント 95パーセンタイルの遅いクエリ
sys.schema_index_statistics インデックスの使用状況統計
sys.memory_by_host_by_current_bytes 接続ごとのメモリ使用量
sys.io_by_thread_by_latency I/O レイテンシの分布

▶ 例:使用されていないインデックスの確認

SQL
SELECT object_schema, object_name, index_name
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL
  AND count_star = 0
  AND object_schema = 'ecommerce'
ORDER BY object_name;
▶ 試してみよう

使用されていないインデックスはストレージ容量を無駄にし、INSERT や UPDATE 操作の際に余分なメンテナンスを必要とするため、速やかに削除すべきです。


10. 包括的な実践演習:パフォーマンス診断プロセスの全容

アリスのeコマースプラットフォームを例に、パフォーマンスの低いクエリの特定から、パフォーマンス改善の効果確認に至るまでの全プロセスについて解説します。

SQL
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = ON;
BASH
mysqldumpslow -s t -t 5 /var/lib/mysql/slow.log
TEXT
Count: 328  Time=4.52s  Rows=1.0  Rows_examined=890000
SELECT * FROM orders WHERE YEAR(create_time)=2025 AND status='PAID';
SQL
EXPLAIN SELECT * FROM orders
WHERE YEAR(create_time) = 2025 AND status = 'PAID';
TEXT
type: ALL | key: NULL | rows: 890000 | Extra: Using where

インデックスが失敗した理由:create_time で YEAR() 関数が使用されました。解決策:

SQL
ALTER TABLE orders ADD INDEX idx_status_time (status, create_time);

SELECT order_id, user_id, total_amount
FROM orders
WHERE create_time >= '2025-01-01' AND create_time < '2026-01-01'
  AND status = 'PAID';
SQL
EXPLAIN SELECT order_id, user_id, total_amount
FROM orders
WHERE create_time >= '2025-01-01' AND create_time < '2026-01-01'
  AND status = 'PAID';
TEXT
type: range | key: idx_status_time | rows: 3200 | Extra: Using index condition

スキャンされた行数は 890,000 行から 3,200 行に減少し、クエリ実行時間は 4.5 秒から 0.08 秒に短縮されました。最後に、バッファプールを調整します:

SQL
SET GLOBAL innodb_buffer_pool_size = 8589934592;

❓ よくある質問

Q スロークエリログは本番環境のパフォーマンスに影響を与えますか?
A 影響はごくわずかです。long_query_time が 1 秒に設定されている場合、タイムアウトしたクエリのみがログに記録され、ログへの書き込みによるオーバーヘッドは無視できる程度です。I/O 負荷が気になる場合は、ログをテーブルではなくファイルに出力するか、代わりに Performance Schema を使用することができます。
Q rows フィールドの値は正確ですか?
A rows は統計データに基づく推定値であり、正確な値ではありません。データの分布が偏っている列の場合、推定値が大幅にずれる可能性があります。ANALYZE TABLE を使用することで、統計データを更新し、精度を向上させることができます。
Q インデックスを多く作成すると、クエリの実行速度は向上しますか?
A いいえ。インデックスはディスク容量を消費します。また、INSERT、UPDATE、DELETE 操作を行うたびに、すべてのインデックスの更新が必要になります。1つのテーブルに作成するインデックスは5~6個以内に抑えることを推奨します。インデックスの総数を減らすため、複合インデックスの使用を優先してください。
Q データベースやテーブルのシャーディングは、どのような場合に検討すべきですか?
A 単一のテーブルに行数が5,000万行を超え、SQLやインデックスの最適化が限界に達した場合にのみ検討してください。データベースやテーブルのシャーディングはシステムの複雑さを増大させる(データベース間のJOIN、分散トランザクションなど)ため、第一の選択肢としては避けるべきです。
Q innodb_buffer_pool_size の適切な値はどれくらいですか?
A 専用データベースサーバーの場合、この値を物理メモリの 60%~80% に設定することをお勧めします。共有サーバーの場合は、OS やその他のプロセス用に十分なメモリを確保してください。これは、innodb_buffer_pool_readsのヒット率を確認することで評価できます。ヒット率が99%を下回る場合は、バッファプールが小さすぎることを示しています。
Q クエリに「Using filesort」というメッセージが表示された場合、必ず最適化が必要ですか?
A 必ずしもそうとは限りません。結果セットが小さい場合(数十行程度)、ファイルソートはメモリ内で実行されるため、オーバーヘッドは無視できる程度です。最適化が必要になるのは、ソート対象の行数が非常に多く、一時ファイルがディスクに書き込まれる場合のみです。これは、sort_buffer_sizeの値を大きくすることで軽減できます。
Q インデックスが使用されているかどうかをどのように確認できますか?
A sys.schema_unused_indexes ビューを使用して、一度も使用されたことのないインデックスを照会するか、Performance Schema ビューで、table_io_waits_summary_by_index_usage テーブルの count_star 列の値が 0 であるレコードを確認してください。

📖 まとめ


📝 練習問題

  1. 基本問題(難易度:⭐):スロークエリログを有効にし、しきい値を0.5秒に設定して、mysqldumpslow を使用して、実行回数が最も多いスロークエリ上位5件を特定してください。

  2. 上級演習(難易度 ⭐⭐)typeALL であるクエリについて、EXPLAIN を使用して分析し、適切なインデックスを追加して、typeref または range レベルにアップグレードされることを確認してください。

  3. 課題問題(難易度:⭐⭐⭐):100万行の注文テーブルに対して、クエリ SELECT user_id, status, total_amount FROM orders WHERE user_id = ? AND status = ? を最適化するためにカバリングインデックスを用いたインデックス構成を設計し、最適化前後の出力 EXPLAIN における rows および Extra フィールドの違いを比較してください。

  4. 実践演習(難易度:⭐⭐⭐):JOIN を使用したサブクエリを含む処理の遅い SQL クエリを書き直し、インデックスを活用してクエリ実行時間を数秒から 100 ミリ秒未満に短縮するように最適化してください。最適化の全プロセスを文書化してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%