初心者のためのSQLパフォーマンス改善入門

## はじめに

Webアプリケーションの速度に影響を与える要素の一つに、データベースクエリの実行効率があります。特に、データ量が増えてくると、SQLの書き方やインデックスの有無が処理速度に大きく関わってきます。

本記事では、MySQL 8.0から追加されたEXPLAIN ANALYZE機能を使って、SQL初学者でも理解しやすい範囲で実行計画の確認方法や、基本的なパフォーマンス改善のヒントを紹介します。

## EXPLAIN ANALYZEとは？

### MySQL 8.0の新機能

`EXPLAIN ANALYZE`は、**MySQL 8.0から新たに追加された機能**で、SQLクエリがどのように実行されているかを詳細に分析できるコマンドです。従来の`EXPLAIN`コマンドとは異なり、実際にクエリを実行しながら、内部的にどのような処理が行われているかを可視化します。

:::note info
PostgreSQLにも同名の機能がありますが、出力形式や表示内容が異なります。本記事ではMySQL固有の内容を扱います。
:::

### EXPLAINとの違い

| 項目 | EXPLAIN | EXPLAIN ANALYZE |
|------|---------|----------------|
| クエリ実行 | 実行しない | 実際に実行する |
| 表示内容 | 推定値のみ | 推定値 + 実際の値 |
| 安全性 | データ変更なし | 実際に実行されるため注意が必要 |
| 情報の精度 | 推定値のため参考程度 | 実際の実行結果で正確 |

## 基本的な使い方

### 実行方法

```sql
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'a@example.com';
```

### 出力例の見方

```sql
-> Index lookup on users using idx_users_email (email='a@example.com')
   (cost=0.35 rows=1) (actual time=0.025..0.026 rows=1 loops=1)
```

この出力から読み取れる情報：

- **Index lookup**: インデックスを使った効率的な検索が行われている
- **cost=0.35**: データベースが推定した処理コスト
- **rows=1**: 推定される結果行数
- **actual time=0.025..0.026**: 実際の処理時間（ミリ秒）
- **rows=1**: 実際に返された行数
- **loops=1**: この処理が実行された回数

## 実行計画でよく見る処理方式

### Table Scan（テーブルスキャン）

```sql
-> Table scan on customers (cost=50435 rows=496903) (actual time=0.173..167 rows=500000 loops=1)
```

テーブル全体を1行ずつ読み取る方式です。データ量が多い場合はパフォーマンスに大きな影響を与えます。

### Index Scan（インデックススキャン）

```sql
-> Index scan on users using idx_users_name (cost=0.29..8.31 rows=5) (actual time=0.025..0.045 rows=3 loops=1)
```

インデックスを利用した効率的な検索方式です。一般的に高速で、望ましい状態です。

### Nested Loop（ネステッドループ）

```sql
-> Nested loop inner join (cost=25.35 rows=25) (actual time=0.123..0.456 rows=15 loops=1)
    -> Table scan on orders (cost=2.50 rows=5) (actual time=0.050..0.080 rows=5 loops=1)
    -> Index lookup on customers using PRIMARY (id=orders.customer_id)
       (cost=4.57 rows=5) (actual time=0.015..0.075 rows=3 loops=5)
```

JOINで使われる処理方式の一つです。外側のテーブル（orders）の各行に対して、内側のテーブル（customers）を繰り返し検索します。この例では、`loops=5`により内側の検索が5回実行されていることがわかります。

## パフォーマンス問題のよくあるパターン

### 問題のあるクエリ例

#### 1. 不要なカラムの取得
```sql
-- 悪い例：全カラム取得
SELECT * FROM users WHERE id = 1;

-- 良い例：必要なカラムのみ
SELECT id, name, email FROM users WHERE id = 1;
```

#### 2. WHERE句での関数使用
```sql
-- 悪い例：インデックスが効かない
SELECT * FROM orders WHERE DATE(created_at) = '2024-06-01';

-- 良い例：範囲条件でインデックスを活用
SELECT * FROM orders 
WHERE created_at >= '2024-06-01' 
  AND created_at < '2024-06-02';
```

#### 3. 部分一致検索の乱用
```sql
-- 悪い例：前方にワイルドカード
SELECT * FROM users WHERE name LIKE '%田中%';

-- 良い例：前方一致でインデックス活用
SELECT * FROM users WHERE name LIKE '田中%';
```

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

50万行のcustomersテーブルを使って、改善前後を比較してみましょう。

### 改善前：全カラム取得
```sql
EXPLAIN ANALYZE SELECT * FROM customers;
```

```bash
-> Table scan on customers (cost=50435 rows=496903) (actual time=0.173..167 rows=500000 loops=1)
```

### 改善後：必要なカラムのみ取得
```sql
EXPLAIN ANALYZE SELECT age FROM customers;
```

```bash
-> Table scan on customers (cost=50435 rows=496903) (actual time=0.113..67.3 rows=500000 loops=1)
```

**結果**：actual timeが167ms → 67.3msに改善（約60%短縮）

同じTable scanでも、取得するデータ量を減らすことで大幅にパフォーマンスが向上しました。

## EXPLAIN ANALYZEを使う際の注意点

1. 実際にクエリが実行される
   - INSERT、UPDATE、DELETEの場合はデータが変更される
   - 本番環境での実行には十分注意する

2. **大量データでの実行時間**
   - 重いクエリの場合、完了まで時間がかかる
   - 必要に応じてLIMITを付けてテストする

3. **推定値と実際値の差**
   - 統計情報が古い場合、大きな差が生じることがある
   - 定期的な統計情報の更新が重要

## まとめ

MySQL 8.0のEXPLAIN ANALYZEは、SQLパフォーマンス改善の強力な武器です。実際の実行結果を見ることで、推測ではなく事実に基づいた最適化が可能になります。

**改善のポイント**
- 必要なカラムのみを取得する
- インデックスが効くようにWHERE条件を工夫する
- EXPLAIN ANALYZEで改善効果を数値で確認する

特別な知識がなくても、日々のSQLを書く中で「本当に効率が良いのか？」という視点を持ち、EXPLAIN ANALYZEで確認する習慣をつけることが、パフォーマンス改善への第一歩です。