【DB2026 #6】チューニング

目次

クエリチューニング

  • クエリ(query):データの検索や処理を求める命令文
  • クエリチューニング:クエリのパフォーマンスを改善してDBのパフォーマンスを上げること
クエリチューニングを行うケース

例えば以下のような場面では、クエリチューニングが有効な可能性がある:

  • Webアプリの応答が遅い。特に検索の操作などが遅い
  • データ処理で遅くなっていることは判明しているが、原因が分からない
  • サービス開始直後は大丈夫だったが、サービス運用期間が長くなるほど、処理が遅くなってくる

スロークエリ

スロークエリの確認方法
  • スロークエリはログとして出力することが可能
    • 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個のデータを順番にチェック → 検索コストは O(n)O(n)
    • Bツリー:n個のデータに対して、ツリー構造で目的の値に到達 → 検索コストは O(logn)O( \log n)
    いつもの関係図
    Image in a image block
インデックスのコスト
  • 空間コスト
    • インデックスが別の領域に追加で書き込まれるため、ディスク容量を追加で消費
  • 時間コスト
    • 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 filesortUsing 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のみ O(N)O(N) O(n)O(n)
ORDER BY+LIMIT O(NlogN)O(N \log N) O(n)O(n)

SELECT * を避ける

  • チューニングの基本として、必要なカラムだけを指定することが重要
  • SELECT * は全カラムを取得するため、不要なデータ転送が発生
-- 悪い例:全カラム取得
SELECT * FROM employees WHERE department = '営業';

-- 良い例:必要なカラムだけ
SELECT employee_id, name, email FROM employees WHERE department = '営業';

DBチューニング

データベースのパフォーマンスを上げる方法は以下の2つに分けられる。

クエリチューニング(本章で紹介)
  • 個々のクエリの実行時間(レスポンスタイム)を短縮
  • テーブルの構成やクエリの最適化
  • 秒〜分レベル
DBチューニング
  • 単位時間あたりの処理量(スループット)を向上
  • DBサーバーのメモリ割当、コネクションプール、キャッシュ設定などを調整
  • 1リクエストあたりの改善はミリ秒単位だが、大量リクエストが同時に効果を発揮