【SQL】WHERE句でインデックスが使われなくなる書き方まとめ

### はじめに

SQLパフォーマンスを左右する大きな要素のひとつに、「インデックスをうまく活用できているか」があります。
しかし、ちょっとした書き方や演算の仕方によっては、せっかく作ったインデックスが利用されず、テーブルフルスキャンや遅い実行計画につながってしまうことがあります。

本記事では、「WHERE句でインデックスが使われなくなる」 代表的なパターンと回避策をまとめます。

### 1. 関数や式をカラムに直接適用してしまう

#### 1.1 例: 関数呼び出しでインデックスが無効化

```sql
-- NG例（カラムに関数をかけてしまう）
SELECT * 
FROM users
WHERE UPPER(username) = 'TARO'; 
```

**なぜインデックスが効かなくなるか:**
一般的に、多くのRDBMSはカラムそのものに対する比較（username = 'XXX'など）であればインデックスを利用できますが、UPPER()など関数を使うと、カラム値をすべて関数変換した結果を比較しないといけません。これにより**SARGable（Search ARGument able）**ではなくなり、インデックスが使われないことが多いです。

**回避策:**
1. 機能付きインデックス(機能インデックス, Function Index)がサポートされているDB（OracleのFunction-Based Indexなど）なら、同様の関数を含むインデックスを作成する。
2. アプリケーション側や別の仕組みで大文字・小文字の正規化を行い、テーブルに正規化済みの値を格納する（別カラムを設けて格納するなど）。

#### 1.2 例: 計算式の左辺にカラムを含む

```sql
-- NG例（カラムに対して計算をしてしまう）
SELECT * 
FROM orders
WHERE order_date + 1 = DATE '2025-01-01';
```

**なぜインデックスが効かなくなるか:**
order_date + 1 という演算結果と '2025-01-01' を比較するため、内部的にはカラム値すべてを計算した結果で判定する必要があります。結果としてインデックスが機能せず、テーブルフルスキャンになる可能性が高いです。

**回避策:**

```sql
-- OK例
SELECT *
FROM orders
WHERE order_date = DATE '2025-01-01' - 1;
```

こうすることで order_date カラムそのものに対する比較になるため、インデックス利用が期待できます。

### 2. 先頭にワイルドカードを付けたLIKE検索

#### 2.1 例: %で始まるパターン

```sql
-- NG例
SELECT *
FROM products
WHERE product_name LIKE '%ABC';
```

**なぜインデックスが効かなくなるか:**
文字列検索において、先頭に % や _（ワイルドカード）がついてしまうと、先頭文字が何なのか分からない状態で検索をすることになり、通常のB-Treeインデックスが効果を発揮できません。

**回避策:**
1. 先頭が確定する検索にする（product_name LIKE 'ABC%'ならインデックスが使えるケース多し）。
2. もし先頭も含めてあいまい検索が必要なら、全文検索エンジンや逆順インデックス、Ngramインデックスなど特別な手段を検討する。
3. データベースによってはFULLTEXTインデックスやGIN/GiSTインデックスを活用できる場合がある。

### 3. OR条件をむやみに使う

#### 3.1 例: 複数カラムに対してORで結合

```sql
-- NG例
SELECT *
FROM users
WHERE username = 'Taro'
   OR email = 'taro@example.com';
```

**なぜインデックスが効かなくなるか:**
通常のインデックスは複数条件をORで結合したとき、それぞれのカラムに別々のインデックスが存在しても、インデックスを効率的に同時利用しづらいケースがあります（DBによっては条件分解してインデックスを使用できる最適化を持つものもありますが、一般的には苦手）。

**回避策:**
- 条件を分けて個別に検索し、UNIONなどでまとめる方法もある（最適化されることも多い）。
- もしくはアプリケーション側で結合結果を統合するなどの設計を検討。
- データベースやバージョンによってはbitmap index やINDEX MERGEによってOR条件でもインデックスをうまく使うことがあるので、実行計画を確認。

### 4. != や <> など「≠」を使った条件

```sql
-- NG例
SELECT *
FROM employees
WHERE department_id <> 10;
```

**なぜインデックスが効かなくなるか:**
「ある値ではない」を示す条件は、単純に「それ以外全部」という検索となり、範囲特定が曖昧になるため、インデックスの効率が著しく悪いです。結局大部分のデータを走査することになり、フルスキャンを選択するケースが多いです。

**回避策:**
- どうしても「≠」条件が必要な場合は、ほとんどの場合フルスキャンになることを受け入れる。
- アプリケーションの要件上、絞り込みの仕方を再検討できないか確認（テーブル設計や検索方法を変更するなど）。

### 5. IS NULL / IS NOT NULL を使った条件

```sql
-- 例
SELECT *
FROM tasks
WHERE completed_at IS NULL;
```

**なぜインデックスが効かなくなるか:**
RDBMSによって挙動は異なりますが、NULLはインデックスに含まれない実装が多いです（もしくはデフォルト挙動ではNULLは索引対象外）。そのため、IS NULLやIS NOT NULLを検索条件に使うと、該当データがインデックスにない（または特殊処理が必要）場合があり、フルスキャンになりやすいです。

**回避策:**
- データベースやインデックス種別によってはNULL値を含める設定が可能な場合もある。
- 設計上「NULLを使わない」ようにして、代わりに特別なフラグで管理するなど、要件と相談する。

### 6. データ型が一致しない比較

#### 6.1 例: 数値カラムに文字列リテラルを渡す

```sql
-- NG例 (priceは数値型カラム)
SELECT *
FROM products
WHERE price = '1000';
```

**なぜインデックスが効かなくなるか:**
DBによってはこの時点で暗黙の型変換が走り、CAST(price AS VARCHAR) などと同様の扱いになってしまうケースがあります。これによりインデックスが利用されず、フルスキャンになることがあります。（実際の挙動はRDBMSによって異なる。エラーになる場合もあれば、暗黙変換する場合も。）

**回避策:**
- SQLで型を正しく合わせる
- 数値カラムには数値リテラルを使う
- 文字列カラムには文字列を使う


### 7. 結果セットをさらに関数で絞り込む (本質的に同じ問題)

**なぜインデックスが効かなくなるか:**
HAVING句は集計後の結果に対して条件をかけるため、そもそもインデックスを直接使う場面がない。また、集計前のテーブルをフルスキャンしてから計算する必要が出るため、大量データを一度に集計することになる。

**回避策:**
- 可能であればWHERE句で事前に絞り込む（WHERE created_at >= '2025-01-01' AND created_at < '2025-01-02' など）。
- 集計対象をできるだけ減らしてからGROUP BYすることでパフォーマンスを向上できる。

### 8. その他の落とし穴

#### 8.1 BETWEENを大雑把に使う

```sql
SELECT *
FROM orders
WHERE order_date BETWEEN '2025-01-01' AND '2030-12-31';
```

- 巨大範囲を指定すると、結果的に大半のデータを読むことになるため、インデックスが選択されない場合がある。
- ただしBETWEEN自体はインデックスを使用可能な演算子ではあるので、一概にNGとは言い切れません。
- 問題は範囲が広すぎると、DBのオプティマイザが「どうせ大半の行を読むならテーブルフルスキャンのほうが早い」と判断する点です。

#### 8.2 サブクエリやNOT EXISTSなどの複雑条件

```sql
SELECT *
FROM employees e
WHERE NOT EXISTS (
  SELECT 1
  FROM black_list b
  WHERE b.emp_id = e.id
);
```

- これ自体が悪いというより、NOT系を含むサブクエリはインデックス利用が難しい場合があり、フルスキャンに近い動きをすることもある。
- 実行計画次第でインデックスをうまく使うケースもあるが、状況により大きく変動。

#### 8.3 DBの最適化設定やヒント句
- 同じSQLでも、オプティマイザの設定やヒント句、実行計画キャッシュの有無によってインデックス利用が変わる場合がある。
- 「書き方」の問題とは少しずれますが、インデックス使用可否にはヒント句やパラメータ設定も大きく影響する。

### まとめ
- カラム側に関数・演算をかけない (SARGableクエリを書く)
- LIKEの先頭にワイルドカードは避ける (全文検索やNgram等を検討)
- ORは要注意（複数インデックスを使いづらい; UNIONなど別アプローチを検討）
- ≠ (<>, !=) と NULL判定 は範囲特定が難しい
- 型変換が入らないように気を付ける
- 集計結果に対するHAVINGではなく、WHERE句で極力絞り込む
- 範囲が広すぎる条件ではオプティマイザがフルスキャンを選ぶことも

インデックスを有効活用するためには、SARGableな書き方（カラムに直接比較を当てる形）を意識し、さらに先頭ワイルドカードやOR条件などがある場合は実行計画の確認が必須です。

#### チェックリスト
1. 実行計画(EXPLAIN) を確認しよう
2. 関数・演算は極力カラムの右側(リテラルやバインド変数の側)で行う
3. インデックスを作る前に検索方法の要件を再検討
4. DB固有の機能インデックス(Function Indexなど)も活用可能か調査
5. ワイルドカード検索の要件を再検証（先頭一致だけで良いか？全文検索エンジン利用できないか？）

### おわりに

「せっかくインデックスを貼っているのに、なぜかパフォーマンスが出ない…」というときは、本記事で挙げたような非SARGableな書き方が隠れていないか一度チェックしてみましょう。

インデックスを利用するためには、
- カラムに対して素直に比較する
- 不必要なORやワイルドカード、関数適用を避ける
- 適切な型と演算子を使う

などの基本を押さえておくことが重要です。ぜひ参考にしてみてください。質問や補足があればコメントでお寄せください！

以上が、WHERE句でインデックスが使われなくなる典型的な書き方一覧です。皆さんのSQLチューニングに少しでもお役立ていただければ幸いです。