現場で使えるSQLチューニングテクニック 〜構造を変えずにヒント句で高速化〜

## はじめに

「SQLが遅いな…」と感じたこと、ありませんか？
僕も入社して最初の案件で、SQLの実行が遅くてレビューで何度も指摘されました。

SQLチューニングというと「インデックスを貼ろう」「正規化しよう」といった話になりがちですが、現場では**テーブル構造を変えられない制約**も多いです。

そこで今回は、**SQLの構造を変えずにできるチューニング方法**を紹介します。
特に「**ヒント句**」という、ちょっと便利な裏技的テクニックに注目していきます。

---

## 1. チューニングの基本ステップ

まずは、「SQLが遅い！」となったときに僕が学んだ基本ステップを共有します。

1. **遅いSQLを特定する**（APMやスロークエリログなど）
2. **実行計画を確認する**
3. **ボトルネックを見つける**
4. **対応策を検討する**（構造変更が無理ならヒント句を検討）

この記事では特に「SQLやテーブルを変更できない」ケースに役立つ対策を中心に紹介します。

---

## 2. 実行計画を読む（最低限でOK）

実行計画と聞くと難しそうですが、最低限の知識でもチューニングには十分役立ちます。

例：PostgreSQL の `EXPLAIN`

```sql
EXPLAIN SELECT * FROM users WHERE age = 30;
```

```
Seq Scan on users  (cost=0.00..431.00 rows=5000 width=64)
  Filter: (age = 30)
```

この出力から「インデックス効いてないな」といったことがわかります。

### 実行計画に出てくる各値の意味

PostgreSQLの実行計画には、以下のような数値情報も含まれています：

| 項目    | 意味                                                                          |
| ----- | --------------------------------------------------------------------------- |
| cost  | クエリプランナーが予測したコスト（開始コスト..終了コスト）で、相対的な重さを表します。単位はPostgreSQL内部独自で、実行時間とは異なります。 |
| rows  | プランナーが予測する出力行数。実際の実行時とは異なることがあります。                                          |
| width | 出力される1行あたりのバイト数の推定値。                                                        |

これらを参考にして、SQLが「どこで重くなっているのか」を見極めることができます。

### よく出る用語と意味（ノード種別）

| 用語          | 意味                |
| ----------- | ----------------- |
| Seq Scan    | テーブル全件をスキャン（遅い）   |
| Index Scan  | インデックスを利用した検索（速い） |
| Nested Loop | 小さなデータには強いループ結合方式 |
| Hash Join   | 大量データの結合に強い方式     |

---

## 3. 遅いSQLのよくあるパターンと対策（＋ヒント句例）

ここでは、実行計画に現れやすい典型的な「遅いSQL」のパターンと、その原因、ヒント句による具体的な対処方法をまとめます。

> ✅ ポイント：問題の「傾向」と「それに対して強制すべき実行方法（ヒント句）」を対応させるのが効果的です。

| アンチパターン          | 実行計画の兆候                       | 問題の本質             | ヒント句で強制する内容                        |
| ---------------- | ----------------------------- | ----------------- | ---------------------------------- |
| 全件スキャン           | `Seq Scan`, `type: ALL`       | インデックスが使われていない    | **スキャン方式（IndexScan）**              |
| 重いNested Loop    | `Nested Loop` + 大量データ         | 結合順・結合方式が非効率      | **結合方式（HashJoinやMergeJoin）**       |
| 後段のFilter        | `Filter:` が遅延評価される            | 条件が早期に使われていない     | **アクセスパスと結合順（IndexScan, Leading）** |
| SortやGroup Byが遅い | `Sort`, `HashAggregate` が高コスト | 並び替えや集計がインデックス非活用 | **スキャン順序（IndexScan with ORDERED）** |

以下で、それぞれの例とヒント句の使い方を詳しく紹介します。

### パターン1：全件スキャン

* 原因：インデックスが使われていない
* 実行計画に `Seq Scan` や `type: ALL` が出てくる

**ヒント句で改善：**

```sql
-- Oracle
SELECT /*+ INDEX(users users_age_idx) */ * FROM users WHERE age = 30;
-- MySQL
SELECT * FROM users USE INDEX (users_age_idx) WHERE age = 30;
-- PostgreSQL（pg_hint_plan）
SELECT /*+ IndexScan(users) */ * FROM users WHERE age = 30;
```

---

### パターン2：大量データ × Nested Loop

* 原因：結合対象が大きく、インデックスも使われていない

**ヒント句で改善：**

```sql
-- Oracle
SELECT /*+ USE_NL(a b) INDEX(b b_fk_idx) */ * FROM a JOIN b ON a.id = b.a_id;
-- MySQL
SELECT STRAIGHT_JOIN * FROM a JOIN b USE INDEX (b_fk_idx) ON a.id = b.a_id;
-- PostgreSQL（pg_hint_plan）
SELECT /*+ NestLoop(a b) Leading(a b) */ * FROM a JOIN b ON a.id = b.a_id;
```

---

### パターン3：Filterで後から絞る

* 原因：WHEREやJOINの条件がインデックスに効いていない

**ヒント句で改善：**

```sql
-- Oracle
SELECT /*+ INDEX(users users_age_status_idx) */ * FROM users WHERE age = 30 AND status = 'active';
-- MySQL
SELECT * FROM users USE INDEX (users_age_status_idx) WHERE age = 30 AND status = 'active';
-- PostgreSQL（pg_hint_plan）
SELECT /*+ IndexScan(users) */ * FROM users WHERE age = 30 AND status = 'active';
```

---

### パターン4：Sort や GROUP BY に時間がかかる

* 原因：該当列にインデックスがない、または非効率な集計

**ヒント句で改善：**

```sql
-- Oracle
SELECT /*+ INDEX_ASC(users users_created_at_idx) */ * FROM users ORDER BY created_at;
-- MySQL
SELECT * FROM users USE INDEX (users_created_at_idx) ORDER BY created_at;
-- PostgreSQL（pg_hint_plan）
SELECT /*+ IndexScan(users) Leading(users) */ * FROM users ORDER BY created_at;
```

---

## 4. 各DBでのヒント句の書き方

### Oracle

```sql
SELECT /*+ INDEX(table index_name) */ ...
```

### MySQL

```sql
SELECT * FROM table USE INDEX (index_name) ...
```

### PostgreSQL

PostgreSQLは標準でヒント句がありませんが、セッション変数である程度制御できます。

```sql
SET enable_seqscan TO off;
```

さらに、拡張機能 `pg_hint_plan` を導入すればヒント句が使えます：

```sql
SELECT /*+ IndexScan(table) */ ...
```

※ 詳しくは [pg\_hint\_plan GitHub](https://github.com/ossc-db/pg_hint_plan) を参照

---

## 5. ヒント句の注意点

* DBごとに文法が異なる
* 将来のバージョンで非推奨になる可能性もある
* 不適切な指定で逆に遅くなるケースもある

→ **ヒント句は「最終手段」として慎重に使うのがベスト**です。

---

## 6. 実際にどう変わった？（事例）

以下、実際の改善事例（PostgreSQL）です：

```sql
-- ヒント句なし
EXPLAIN ANALYZE SELECT * FROM users WHERE age = 30;
-- 実行計画: Seq Scan on users (cost=0.00..431.00 rows=5000)
```

```sql
-- ヒント句あり（pg_hint_plan）
EXPLAIN ANALYZE SELECT /*+ IndexScan(users) */ * FROM users WHERE age = 30;
-- 実行計画: Index Scan using users_age_idx on users
```

結果：実行時間が大幅に改善（例：800ms → 60ms など）

---

## まとめ

* SQLが遅いときはまず実行計画を確認
* パターンを把握して原因を探る
* 構造変更できないならヒント句で対策
* ヒント句は使い方次第。過信せず慎重に！

僕自身、何度もヒント句に救われてきました。
「構造はいじれないけど、なんとかしたい…」という場面では、きっと役立つはずです。

---

## 参考リンク

* [SQLパフォーマンス詳説（Zenn/ikeponさん）](https://zenn.dev/ikepon/articles/sql-performance-explained)
* [Use the Index, Luke!（日本語訳）](https://use-the-index-luke.com/ja/sql/table-of-contents)
* [pg\_hint\_plan GitHub](https://github.com/ossc-db/pg_hint_plan)
* [PostgreSQL enable\_\* 設定一覧](https://www.postgresql.org/docs/current/runtime-config-query.html)
* [MySQL USE INDEX ドキュメント](https://dev.mysql.com/doc/refman/8.0/en/index-hints.html)
* [Oracle SQLヒント一覧](https://docs.oracle.com/cd/B19306_01/server.102/b14200/sql_elements006.htm)

---

## おわりに

最後まで読んでいただきありがとうございました！
この記事は、僕が新人時代に困った経験をもとにまとめました。

「こういうケースも知りたい」「もっと実例が見たい」といったリクエストがあれば、ぜひコメントください！