目次
クエリチューニング
- クエリ(query):データの検索や処理を求める命令文
- クエリチューニング:クエリのパフォーマンスを改善してDBのパフォーマンスを上げること
クエリチューニングを行うケース
例えば以下のような場面では、クエリチューニングが有効な可能性がある:
- Webアプリの応答が遅い。特に検索の操作などが遅い
- データ処理で遅くなっていることは判明しているが、原因が分からない
- サービス開始直後は大丈夫だったが、サービス運用期間が長くなるほど、処理が遅くなってくる
スロークエリ
- スロークエリ:実行時間が一定の基準を超える遅いクエリ
- 一定の基準はシステムによって異なるが、1秒を指定することが多い
- 参考:スロークエリ(slow query)とは - IT用語辞典 e-Words
スロークエリの確認方法
- スロークエリはログとして出力することが可能
- MySQLの場合、以下の設定でスロークエリログを有効化
-- スロークエリログを有効化
SET GLOBAL slow_query_log = 'ON';
-- 閾値を1秒に設定(これ以上かかるクエリがログに記録される)
SET GLOBAL long_query_time = 1;
-- ログの出力先を確認
SHOW VARIABLES LIKE 'slow_query_log_file'; スロークエリの原因
- クエリに何かしらの問題
- 不必要なデータを参照している など……
- 対策:期待された結果から変わらないようにクエリを書換え
- 巨大なテーブルデータ全体を読み取っている(フルスキャン)
- 対策:カラムにインデックスを張って、フルスキャンを回避
インデックス
- インデックス(index、索引):データベースの検索性能を向上させる方法の一種
- 本の索引のようなもの
インデックスの仕組み(Bツリー)
- 多くのDBではインデックスにBツリーと呼ばれる、バランス木の一種であるデータ構造を採用
- MySQLも、ほとんどのインデックスはBツリーを使用
- データをソートした状態でツリー構造に格納し、目的の値を二分探索のように探索
- フルスキャン:n個のデータを順番にチェック → 検索コストは
- Bツリー:n個のデータに対して、ツリー構造で目的の値に到達 → 検索コストは
いつもの関係図
インデックスのコスト
- 空間コスト
- インデックスが別の領域に追加で書き込まれるため、ディスク容量を追加で消費
- 時間コスト
- INSERT/UPDATE/DELETE の操作でインデックスも更新されるため、書き込み性能は低下
- 「読み取りを速くする代わりに、書き込みコストを払う」トレードオフ
- INSERT/UPDATE/DELETE の操作でインデックスも更新されるため、書き込み性能は低下
インデックスは万能か?
📌
インデックスを作成した方がいいケースとそうでないケースがある
作成した方が良いケース
- 大規模テーブルの1〜15%程度の行を頻繁にアクセスするケース
- WHERE/ORDER BY/GROUP BY で頻繁に使用される列
- カーディナリティが高い列(メールアドレス、社員番号など)
- カーディナリティ:あるカラムに含まれる値の種類の数
- 外部キー制約で使用されている列
作成しない方が良いケース
- データ件数が少ないテーブル(フルスキャンの方が速い場合がある)
- NULL値が大半で、NULL以外の値を検索しない列
EXPLAIN
- クエリの実行計画を確認するためのSQLコマンド。「このクエリをDBがどう実行しようとしているか」を実行前に確認できる。
- 使い方:確認したいSELECT文の先頭に
EXPLAINをつけるだけ
インデックスの有無による違いをEXPLAINで確認する例
employees テーブルから、 first_name がAで始まるレコードを取得するクエリについて考える。
EXPLAIN SELECT * FROM employees WHERE first_name like 'A%'; +----+-------------+-----------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-----------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | employees | ALL | NULL | NULL | NULL | NULL | 14 | 11.11 | Using where |
+----+-------------+-----------+------+---------------+------+---------+------+------+----------+-------------+ ⚠️
改善が必要!
type=ALL(フルスキャン)、key=NULL(インデックス未使用)になっている。
データが14件だから問題にならないが、これが数万件になると処理速度に影響がある。
CREATE INDEX idx_first_name ON employees(first_name);
EXPLAIN SELECT * FROM employees WHERE first_name like 'A%'; +----+-------------+-----------+-------+----------------+----------------+---------+------+------+----------+-----------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-----------+-------+----------------+----------------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | employees | range | idx_first_name | idx_first_name | 44 | NULL | 2 | 100.00 | Using index condition |
+----+-------------+-----------+-------+----------------+----------------+---------+------+------+----------+-----------------------+ ✅
改善例
type=range(必要な範囲だけスキャン)、key=idx_first_name(インデックス使用)を実施。
rowsが14→2に減少。
出力される項目と種類
| 項目名 | 説明 |
|---|---|
| id | クエリにおけるテーブルのID |
| select_type | このテーブルがどのような役割を果たすか |
| table | MySQLがレコードを読み出すテーブル名 |
| partitions | 使用したパーティションテーブル |
| type | レコードへのアクセス種別。良い順に const > eq_ref > ref > range > index > ALL ALLはフルスキャンなので避けたほうがよい。 |
| possible_keys | 選択可能なインデックス |
| key | 実際に選択されたインデックス |
| key_len | 選択されたkeyの長さ |
| ref | 検索条件でkeyと比較されている値やカラム |
| rows | 調査される行の見積もり |
| filtered | テーブル条件によってフィルタ処理される行の推定の割合 |
| Extra | 追加情報。Using index(インデックスだけで完結)は良好、Using filesortやUsing temporaryは改善余地あり |
LIMIT
- SQLクエリの結果から特定の数の行だけを返すことを指定する句
- クライアントへのデータ転送量を削減
- 適切なインデックスがあれば、DBサーバ側のスキャン量自体も削減可能
どんな時に使うか
例:入社年度が古い順(=勤続が長い順)に上位5人を出力する
-- join_yearにインデックスがある場合
SELECT name, join_year FROM employees ORDER BY join_year ASC LIMIT 5; 注意点
-
ORDER BY+LIMITでも、ソート対象のカラムにインデックスがない場合- 全件ソートしてから上位N件を返すため、DB側の処理負荷は減らない
参考:N件のレコードがあるテーブルから、n件のレコードを抽出する際のコスト
| インデックスなし | インデックスあり | |
| LIMIT Nのみ | ||
| ORDER BY+LIMIT |
SELECT * を避ける
- チューニングの基本として、必要なカラムだけを指定することが重要
-
SELECT *は全カラムを取得するため、不要なデータ転送が発生
-- 悪い例:全カラム取得
SELECT * FROM employees WHERE department = '営業';
-- 良い例:必要なカラムだけ
SELECT employee_id, name, email FROM employees WHERE department = '営業'; DBチューニング
データベースのパフォーマンスを上げる方法は以下の2つに分けられる。
クエリチューニング(本章で紹介)
- 個々のクエリの実行時間(レスポンスタイム)を短縮
- テーブルの構成やクエリの最適化
- 秒〜分レベル
DBチューニング
- 単位時間あたりの処理量(スループット)を向上
- DBサーバーのメモリ割当、コネクションプール、キャッシュ設定などを調整
- 1リクエストあたりの改善はミリ秒単位だが、大量リクエストが同時に効果を発揮