#04 データベースの高速化

インデックスが使われないケース

「インデックスを貼ったのに EXPLAIN を見たら type: ALL のまま……」そんな経験はありませんか? インデックスは存在するだけでは意味がなく、クエリがそれを使える形になっている必要があります。このエピソードでは、インデックスが無効化されてしまうパターンを徹底的に洗い出します。

インデックスが無効化されるパターン ① カラムへの関数適用 YEAR(created_at) = 2026 → 範囲条件に書き直す ② 型変換 WHERE code = 12345 (VARCHAR列) → 文字列 '12345' で比較する ③ LIKE 前方一致以外 LIKE '%keyword%' → 前方一致 'key%' のみ有効 ④ OR 条件の落とし穴 col_a = 1 OR col_b = 2 → UNION ALL で分割する ⑤ カーディナリティが低い WHERE gender = 'M' (2種類のみ) → オプティマイザがFSを選ぶ場合も ⑥ 複合インデックス 先頭列省略 IDX(last,first) → first だけでNG → 最左プレフィックスルール 共通の確認方法 1. EXPLAIN で type を確認 → ALL または key: NULL ならインデックス未使用 2. カラムを加工せず、条件の右辺側を変えるのが基本 3. 型変換が起きていないか、カラム定義と比較値の型を照合する 4. 複合インデックスは最左列から順に使われることを意識する
図1: インデックスが無効化されるパターン

パターン1:カラムに関数や演算を適用する

インデックスは「カラムの値そのもの」に対して作成されています。カラムを関数で変換してしまうと、インデックスが指す値とは別の値になるため、MySQL はインデックスを使えません。

関数適用でインデックスが使えない理由 NG: 関数をカラムに適用 WHERE YEAR(created_at) = 2026 ↑ カラムを変換してしまっている インデックスの中身 '2026-01-15 09:23:00' → row '2026-03-01 14:00:00' → row '2025-12-31 23:59:59' → row (DATETIME型のままインデックス化) YEAR()を適用すると... 全行に YEAR() を計算してから比較 → インデックスとは別の値になる フルテーブルスキャン発生! EXPLAIN → type: ALL, key: NULL OK: 範囲条件に書き直す WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01' ↑ カラムはそのまま、定数側を変える インデックスの中身(変換なし) '2025-12-31 23:59:59' → row '2026-01-15 09:23:00' → row ✓ '2026-03-01 14:00:00' → row ✓ (ソート済みで範囲をそのまま検索) 範囲条件にすると... B-treeの開始値を特定してそこから リーフリンクを辿るだけ インデックスの range スキャン! EXPLAIN → type: range, key: idx_created_at
図2: 関数適用でインデックスが使えない仕組み

Before(インデックスが使えない)

-- YEAR() 関数を適用しているのでインデックス無効
SELECT * FROM orders WHERE YEAR(created_at) = 2026;

-- 文字列結合で加工してしまっている
SELECT * FROM users WHERE CONCAT(first_name, ' ', last_name) = 'Alice Smith';

After(インデックスが使える)

-- 範囲条件に書き直す
SELECT * FROM orders
WHERE created_at >= '2026-01-01'
  AND created_at < '2027-01-01';

-- カラムを加工せず条件側を変える
SELECT * FROM users
WHERE first_name = 'Alice' AND last_name = 'Smith';

演算も同様です。

-- Before: カラムを計算している
SELECT * FROM products WHERE price * 1.1 > 1000;

-- After: 定数側を計算する
SELECT * FROM products WHERE price > 909.09;

パターン2:型変換が発生する

カラムの型と比較する値の型が異なると、MySQL が暗黙的に型変換を行い、インデックスが使えなくなります。

-- user_code は VARCHAR(20) 型
-- Before: 数値で比較するとカラム側が型変換される
SELECT * FROM users WHERE user_code = 12345;

-- After: 文字列で比較する
SELECT * FROM users WHERE user_code = '12345';

EXPLAINtype: indextype: ALL になっていたら型変換を疑いましょう。

パターン3:LIKE の前方一致以外

LIKE 検索でインデックスが使えるのは前方一致(前側が固定の場合)のみです。

-- Before: 前後ワイルドカードはフルスキャン
SELECT * FROM articles WHERE title LIKE '%データベース%';

-- Before: 後方ワイルドカードもNG
SELECT * FROM articles WHERE title LIKE '%チューニング';

-- After: 前方一致はインデックスが使える
SELECT * FROM articles WHERE title LIKE 'データベース%';

全文検索が必要な場合は FULLTEXT INDEX や Elasticsearch などの全文検索エンジンの利用を検討してください。

パターン4:OR 条件の落とし穴

OR で結んだ条件では、すべての条件にインデックスがないと フルテーブルスキャン? インデックスを使わずにテーブルの全行を順番に読み込む操作。少量データなら問題ないが、大きなテーブルでは深刻なパフォーマンス問題になる。EXPLAINのtypeが`ALL`の場合がこれ。 になることがあります。

-- Before: status にしかインデックスがない場合、OR 全体がフルスキャンになる
SELECT * FROM orders WHERE status = 'pending' OR total_amount > 10000;

-- After: UNION ALL で分割する
SELECT * FROM orders WHERE status = 'pending'
UNION ALL
SELECT * FROM orders WHERE total_amount > 10000 AND status != 'pending';

ただし同一カラムの ORIN に書き直すとインデックスが使われます。

-- Before
SELECT * FROM users WHERE status = 'active' OR status = 'trial';

-- After: IN に書き直すとインデックスが有効
SELECT * FROM users WHERE status IN ('active', 'trial');

パターン5:NULL を含む比較

IS NULLIS NOT NULL はインデックスが使われる場合と使われない場合があります。カーディナリティが低い場合(NULLが大半を占める場合など)はフルスキャンが選ばれることがあります。

-- MySQL は IS NULL でもインデックスを使うことがある(バージョン・状況による)
SELECT * FROM tasks WHERE deleted_at IS NULL;

NULL が多いカラムでの検索が多い場合は、NULL の代わりに番兵値(0'active' など)を使う設計を検討します。

パターン6:カーディナリティが低い列

カーディナリティとは値の種類数のことです。例えば gender カラムに 'M''F' の2種類しかない場合、インデックスを使っても半数の行を読むことになります。MySQL のオプティマイザはこういった場合にフルスキャンのほうが速いと判断し、インデックスを使わないことがあります。

目安として、全行の5〜10%以下に絞り込めるカラムにインデックスを貼るのが効果的です。

パターン7:複合インデックスで先頭列を省略する

複合インデックス? 複数のカラムを組み合わせて作成するインデックス。`(last_name, first_name)` のように定義すると、先頭列から順に検索する場合に効果的。列の順序が重要。 は「先頭列から順に」使われます。先頭列を省略した条件では、インデックスが使われません。

-- (last_name, first_name) の複合インデックスを作成
ALTER TABLE users ADD INDEX idx_name (last_name, first_name);

-- OK: 先頭列 last_name を使っている
SELECT * FROM users WHERE last_name = 'Tanaka';

-- OK: 両方使っている
SELECT * FROM users WHERE last_name = 'Tanaka' AND first_name = 'Taro';

-- NG: first_name だけでは先頭列がないのでインデックス不使用
SELECT * FROM users WHERE first_name = 'Taro';

これを「最左プレフィックスルール」と呼びます。複合インデックスを設計するときは、最も絞り込み効果が高いカラムを先頭に置き、単体でも検索するカラムを先頭に持ってくるようにしましょう。


インデックスが使われないパターンを知っていれば、クエリを書く段階で「これは効かないな」と気付けます。 EXPLAIN? MySQLがSQLをどのように実行するかの「実行計画」を表示するコマンド。インデックスが使われているか、何行スキャンしているかなどを確認でき、チューニングの出発点になる。 で確認しながら、正しい形のクエリを書く習慣を身につけましょう。次のエピソードは、クエリの本数そのものが多くなる N+1 問題を扱います。