T-CREATOR

MySQL オプティマイザ概説:実行計画が決まるまでの舞台裏

MySQL オプティマイザ概説:実行計画が決まるまでの舞台裏

データベースの世界において、SQL クエリが実行される際、その舞台裏では「オプティマイザ」という重要な仕組みが働いています。オプティマイザは、私たちが書いた SQL 文を最も効率的に実行するための方法を決定する、まさにデータベースの頭脳とも言える存在です。

今回は、MySQL のオプティマイザが実行計画を決定するまでのプロセスを詳しく解説し、パフォーマンス向上のための理解を深めていきましょう。

背景

SQL 文から実行計画まで:オプティマイザの役割

MySQL において、私たちが記述した SQL 文が実際に実行されるまでには、複数の段階を経る必要があります。その中でも特に重要な役割を担うのがオプティマイザです。

オプティマイザの基本的な役割を図で確認してみましょう。

mermaidflowchart LR
    sql[SQL文] -->|構文解析| parser[パーサー]
    parser -->|解析結果| optimizer[オプティマイザ]
    optimizer -->|最適化| plan[実行計画]
    plan -->|実行| executor[実行エンジン]
    executor -->|結果| result[クエリ結果]

オプティマイザは、パーサーで解析された SQL 文を受け取り、データを取得するための最適な方法を決定します。この過程で、インデックスの使用可否、テーブルの結合順序、結合方法などを総合的に判断し、最も効率的な実行計画を作成するのです。

データベースパフォーマンスの要となるオプティマイザ

データベースのパフォーマンスは、オプティマイザの判断に大きく左右されます。同じデータを取得する SQL 文でも、オプティマイザが選択する実行計画によって、実行時間が数十倍、時には数百倍も変わることがあります。

typescript// 同じ結果を返すクエリでも実行時間が大きく異なる例
// パターン1:フルテーブルスキャン(遅い)
SELECT * FROM users WHERE age > 25;

// パターン2:インデックス利用(速い)
SELECT * FROM users WHERE user_id = 12345;

オプティマイザが適切な判断を行うことで、アプリケーション全体のレスポンス性能が向上し、ユーザー体験の質が大幅に改善されます。

開発者が知っておくべきオプティマイザの重要性

現代の Web アプリケーション開発において、データベースのパフォーマンスは避けて通れない課題です。オプティマイザの動作を理解することで、以下のようなメリットが得られます。

項目メリット
1クエリの性能問題を早期に発見できる
2適切なインデックス設計が可能になる
3スロークエリの原因を特定しやすくなる
4データベース設計時の判断精度が向上する
5運用時のパフォーマンスチューニングが効率的になる

オプティマイザの仕組みを理解することは、データベースを使った開発において必須のスキルと言えるでしょう。

課題

複雑なクエリで期待通りの性能が出ない理由

開発現場でよく遭遇する問題として、複雑なクエリが期待通りの性能を発揮しないケースがあります。これには複数の要因が関係しています。

sql-- 複雑なクエリの例:複数テーブル結合とサブクエリ
SELECT
    u.user_name,
    o.order_date,
    (SELECT COUNT(*) FROM order_items oi WHERE oi.order_id = o.order_id) as item_count
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
WHERE u.created_at > '2023-01-01'
  AND o.status = 'completed'
ORDER BY o.order_date DESC;

このようなクエリでは、オプティマイザが以下の判断を行う必要があります。

mermaidflowchart TD
    start[クエリ受信] --> analyze[テーブル統計分析]
    analyze --> join_order[結合順序決定]
    join_order --> index_select[インデックス選択]
    index_select --> subquery[サブクエリ最適化]
    subquery --> final_plan[最終実行計画]

複雑になるほど、オプティマイザの判断が困難になり、最適でない実行計画が選択される可能性が高まります。

インデックスがあるのに使われない現象

インデックスを作成したにも関わらず、それが使用されないという現象もよく発生します。この現象には以下のような理由があります。

sql-- インデックスが使われないケースの例

-- ケース1:関数を使用したWHERE句
CREATE INDEX idx_created_at ON users(created_at);
SELECT * FROM users WHERE YEAR(created_at) = 2023; -- インデックス未使用

-- ケース2:データ型の不一致
CREATE INDEX idx_user_id ON orders(user_id);
SELECT * FROM orders WHERE user_id = '12345'; -- 数値型に文字列で比較

-- ケース3:統計情報の問題
-- インデックスがあっても、統計情報が古い場合は使用されない可能性

これらの問題を理解するためには、オプティマイザがインデックス使用を判断する基準を知る必要があります。

オプティマイザの判断基準がブラックボックスな問題

多くの開発者にとって、オプティマイザの判断基準は見えにくく、なぜその実行計画が選択されたのかが分からないという問題があります。

sql-- なぜこの実行計画が選択されたのか分からない例
EXPLAIN SELECT * FROM large_table
WHERE column_a = 'value1' AND column_b > 100;

-- 実行計画の結果
-- +----+-------+-------+------+----------+
-- | id | type  | key   | rows | filtered |
-- +----+-------+-------+------+----------+
-- |  1 | ALL   | NULL  | 5000 |     5.00 |
-- +----+-------+-------+------+----------+

このような状況では、パフォーマンス問題の解決が困難になり、適切な対策を講じることができません。

解決策

オプティマイザの基本動作原理

MySQL のオプティマイザは「コストベース最適化」という手法を採用しています。これは、各実行方法にコスト(時間やリソースの使用量)を計算し、最もコストの低い方法を選択するアプローチです。

mermaidflowchart LR
    query[クエリ] --> cost_calc[コスト計算]
    cost_calc --> plan1[実行計画1<br/>コスト: 100]
    cost_calc --> plan2[実行計画2<br/>コスト: 50]
    cost_calc --> plan3[実行計画3<br/>コスト: 75]
    plan2 --> selected[選択された計画]

コスト計算では以下の要素が考慮されます。

javascript// コスト計算の主要要素
const costFactors = {
  ioCost: 'ディスクI/Oのコスト',
  cpuCost: 'CPU処理のコスト',
  memoryCost: 'メモリ使用のコスト',
  networkCost: 'ネットワーク通信のコスト',
};

統計情報とコストベース最適化

オプティマイザが適切な判断を行うためには、正確な統計情報が不可欠です。統計情報には以下の情報が含まれます。

sql-- 統計情報の確認
SHOW TABLE STATUS LIKE 'users';
SHOW INDEX FROM users;

-- 統計情報の更新
ANALYZE TABLE users;

統計情報の主要項目:

項目説明影響
行数テーブルの総行数フルスキャンのコスト計算
カーディナリティインデックスの一意性インデックス選択の判断
データサイズテーブルのデータサイズI/O コストの計算
分布情報データの分布状況結合順序の決定

実行計画の決定プロセス

オプティマイザが実行計画を決定するプロセスを詳しく見てみましょう。

mermaidsequenceDiagram
    participant Parser as パーサー
    participant Optimizer as オプティマイザ
    participant Statistics as 統計情報
    participant CostModel as コストモデル

    Parser->>Optimizer: 解析済みクエリ
    Optimizer->>Statistics: 統計情報要求
    Statistics->>Optimizer: テーブル統計
    Optimizer->>CostModel: コスト計算要求
    CostModel->>Optimizer: 各プランのコスト
    Optimizer->>Optimizer: 最適プラン選択
    Optimizer->>Parser: 実行計画

実行計画決定の各段階:

typescript// 実行計画決定の段階的処理
class QueryOptimizer {
  // 段階1:アクセスパス生成
  generateAccessPaths(table: Table): AccessPath[] {
    const paths = [];
    paths.push(new TableScanPath(table));

    table.indexes.forEach((index) => {
      paths.push(new IndexScanPath(index));
    });

    return paths;
  }

  // 段階2:結合順序決定
  determineJoinOrder(tables: Table[]): JoinOrder {
    // 動的プログラミングによる最適結合順序の計算
    return this.dynamicProgramming(tables);
  }

  // 段階3:最終コスト計算
  calculateFinalCost(plan: ExecutionPlan): number {
    return plan.ioOps * IO_COST + plan.cpuOps * CPU_COST;
  }
}

オプティマイザヒントの活用方法

オプティマイザの判断が最適でない場合、ヒントを使用して動作を制御できます。

sql-- インデックスヒントの例
SELECT /*+ USE_INDEX(users, idx_created_at) */
  * FROM users
  WHERE created_at > '2023-01-01';

-- 結合順序のヒント
SELECT /*+ STRAIGHT_JOIN */
  u.user_name, o.order_date
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id;

-- オプティマイザスイッチの制御
SET optimizer_switch = 'index_merge=off';

主要なヒントの種類:

ヒント用途使用例
USE_INDEX特定インデックスの使用を強制統計情報が不正確な場合
IGNORE_INDEX特定インデックスの使用を禁止インデックスが逆効果の場合
STRAIGHT_JOIN結合順序を固定最適でない結合順序の場合

具体例

EXPLAIN 文による実行計画の確認

実際のクエリで EXPLAIN 文を使用して、オプティマイザの判断を確認してみましょう。

sql-- 基本的なEXPLAIN文の使用
EXPLAIN SELECT * FROM users WHERE user_id = 12345;

EXPLAIN 文の出力解読:

sql-- EXPLAIN結果の詳細分析
EXPLAIN FORMAT=JSON
SELECT u.user_name, COUNT(o.order_id) as order_count
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE u.created_at > '2023-01-01'
GROUP BY u.user_id;
json{
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "2841.25"
    },
    "grouping_operation": {
      "using_temporary_table": true,
      "nested_loop": [
        {
          "table": {
            "table_name": "u",
            "access_type": "range",
            "possible_keys": ["idx_created_at"],
            "key": "idx_created_at",
            "rows_examined_per_scan": 1500,
            "filtered": "100.00"
          }
        }
      ]
    }
  }
}

図で理解できる要点:

  • クエリの総コストが 2841.25 と計算されている
  • users テーブルで range 型のアクセスが選択されている
  • idx_created_at インデックスが使用されている

インデックス選択の実例分析

複数のインデックスが存在する場合のオプティマイザの選択を分析してみます。

sql-- テーブル作成とインデックス設定
CREATE TABLE user_activity (
  id INT PRIMARY KEY,
  user_id INT,
  activity_date DATE,
  activity_type VARCHAR(20),
  INDEX idx_user_id (user_id),
  INDEX idx_activity_date (activity_date),
  INDEX idx_composite (user_id, activity_date)
);
sql-- 異なる条件でのインデックス選択比較
-- ケース1:単一カラム条件
EXPLAIN SELECT * FROM user_activity WHERE user_id = 12345;

-- ケース2:複合条件
EXPLAIN SELECT * FROM user_activity
WHERE user_id = 12345 AND activity_date > '2023-01-01';

-- ケース3:範囲検索
EXPLAIN SELECT * FROM user_activity
WHERE activity_date BETWEEN '2023-01-01' AND '2023-12-31';

各ケースでの最適化結果:

mermaidflowchart TD
    case1[ケース1: user_id = 12345] --> idx1[idx_user_id使用]
    case2[ケース2: user_id + date範囲] --> idx2[idx_composite使用]
    case3[ケース3: date範囲のみ] --> idx3[idx_activity_date使用]

    idx1 --> result1[高速検索: 1行]
    idx2 --> result2[最適化: 複合インデックス]
    idx3 --> result3[範囲検索: 効率的]

結合順序の最適化事例

複数テーブルの結合における最適化を実例で確認します。

sql-- 3つのテーブルを結合するクエリ
SELECT
  u.user_name,
  p.product_name,
  o.order_date
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
WHERE u.status = 'active'
  AND o.order_date > '2023-01-01';

オプティマイザが考慮する結合順序のパターン:

typescript// 結合順序の候補(4テーブルの場合)
const joinOrderCandidates = [
  'users → orders → order_items → products',
  'orders → users → order_items → products',
  'products → order_items → orders → users',
  // その他の組み合わせ...
];

// 各順序のコスト計算例
const costCalculation = {
  pattern1: {
    step1: 'users(active filter) → 1000行',
    step2: 'orders(date filter) → 500行',
    step3: 'order_items → 2000行',
    step4: 'products → 100行',
    totalCost: 3600,
  },
  pattern2: {
    // 他パターンのコスト計算
    totalCost: 5200,
  },
};

パフォーマンス改善の実践例

実際のスロークエリを最適化する実践例を見てみましょう。

sql-- 問題のあるクエリ(実行時間:5.2秒)
SELECT
  u.user_name,
  COUNT(o.order_id) as order_count,
  SUM(o.total_amount) as total_spent
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE YEAR(u.created_at) = 2023
GROUP BY u.user_id
HAVING COUNT(o.order_id) > 5;

問題点の分析:

mermaidflowchart LR
    problem1[YEAR関数使用] --> issue1[インデックス未使用]
    problem2[LEFT JOIN] --> issue2[不要な外部結合]
    problem3[HAVING句] --> issue3[全行処理後のフィルタ]

最適化後のクエリ:

sql-- 改善後のクエリ(実行時間:0.3秒)
SELECT
  u.user_name,
  COUNT(o.order_id) as order_count,
  SUM(o.total_amount) as total_spent
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
WHERE u.created_at >= '2023-01-01'
  AND u.created_at < '2024-01-01'
GROUP BY u.user_id, u.user_name
HAVING COUNT(o.order_id) > 5;

-- 対応するインデックス作成
CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX idx_orders_user_id ON orders(user_id);

改善ポイント:

改善項目変更前変更後効果
WHERE 句YEAR(created_at) = 2023created_at >= '2023-01-01'インデックス使用可能
結合方法LEFT JOININNER JOIN不要な行を除外
フィルタリングHAVING 句のみWHERE 句 + HAVING 句早期絞り込み

まとめ

オプティマイザ理解のメリット

MySQL オプティマイザの仕組みを理解することで、以下のような具体的なメリットが得られます。

まず、クエリ性能の予測可能性が向上します。オプティマイザがどのような判断基準で実行計画を選択するかを理解することで、書いたクエリがどの程度の性能を発揮するかを事前に予測できるようになります。

次に、効果的なデバッグ能力が身につきます。スロークエリが発生した際に、EXPLAIN 文の結果を適切に解釈し、問題の根本原因を特定できるようになります。これにより、パフォーマンス問題の解決時間が大幅に短縮されるでしょう。

さらに、最適なデータベース設計が可能になります。テーブル構造やインデックス設計の段階で、オプティマイザの動作を考慮した設計ができるため、運用開始後のパフォーマンス問題を未然に防ぐことができます。

mermaidflowchart LR
    understanding[オプティマイザ理解] --> prediction[性能予測]
    understanding --> debugging[効率的デバッグ]
    understanding --> design[最適設計]

    prediction --> benefit1[開発効率向上]
    debugging --> benefit2[運用負荷軽減]
    design --> benefit3[安定したサービス]

継続的なパフォーマンス監視の重要性

オプティマイザの理解は一度身につけて終わりではありません。データベースの状況は時間とともに変化し、それに応じて最適な実行計画も変わっていきます。

継続的な監視において重要なポイントは以下のとおりです。

typescript// パフォーマンス監視の継続ポイント
const monitoringPoints = {
  statisticsUpdate: '定期的な統計情報の更新',
  slowQueryAnalysis: 'スロークエリログの継続分析',
  indexUsageReview: 'インデックス使用状況の定期確認',
  planStabilityCheck: '実行計画の安定性確認',
};

特に以下の状況では、実行計画の見直しが必要になります:

  • データ量の大幅な増加
  • アクセスパターンの変化
  • MySQL バージョンのアップデート
  • ハードウェア構成の変更

これらの変化に対応するため、Performance Schema やスロークエリログを活用した継続的な監視体制を構築することが重要です。

MySQL オプティマイザの理解は、データベースを扱う開発者にとって必須のスキルです。今回解説した基本的な仕組みを理解し、実際のプロジェクトで活用することで、より高性能で安定したアプリケーションの開発が可能になるでしょう。

関連リンク