インデックスを貼ったのに、思ったほど速くならない。そんなときに効くのがカバリングインデックス(covering index)です。クエリが使う列をすべてインデックスに入れておくと、MySQLはテーブル本体を読みに行かずにインデックスだけで答えを返せます。この記事では、EXPLAINの Extra 列に出る Using index の意味、カバリングにするための複合インデックスの作り方、そして SELECT * で効果が消える落とし穴までを、実際の実行結果で確かめていきます。
以下の結果は MySQL 8.0 で実際に実行したものです。
このシナリオで考える
ネットショップの注文テーブルを例にします。マイページで「自分の直近5件の注文日時と金額」を表示する画面を想定してください。ユーザー300人が10件ずつ、合計3000件の注文がある状態です。最初は user_id だけにインデックスがあります。
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
status VARCHAR(10) NOT NULL,
total INT NOT NULL,
ordered_at DATETIME NOT NULL,
memo VARCHAR(200) NULL,
INDEX idx_user (user_id)
);
-- 再帰CTEで3000行作るため、再帰の上限(既定1000)を上げる
SET SESSION cte_max_recursion_depth = 5000;
INSERT INTO orders (user_id, status, total, ordered_at, memo)
WITH RECURSIVE seq (n) AS (
SELECT 1 UNION ALL SELECT n + 1 FROM seq WHERE n < 3000
)
SELECT
MOD(n, 300) + 1,
ELT(MOD(n, 4) + 1, 'paid', 'shipped', 'canceled', 'returned'),
(MOD(n * 53, 89) + 1) * 100,
'2026-04-01' + INTERVAL n * 2 HOUR,
CONCAT('memo-', n)
FROM seq;
ANALYZE TABLE orders;
マイページで使うクエリはこちらです。
SELECT ordered_at, total
FROM orders
WHERE user_id = 42
ORDER BY ordered_at DESC
LIMIT 5;
+---------------------+-------+
| ordered_at | total |
+---------------------+-------+
| 2026-11-15 10:00:00 | 2600 |
| 2026-10-21 10:00:00 | 5700 |
| 2026-09-26 10:00:00 | 8800 |
| 2026-09-01 10:00:00 | 3000 |
| 2026-08-07 10:00:00 | 6100 |
+---------------------+-------+
インデックスだけで答える仕組み
インデックスで行を見つけたあと、ほかの列を取りにテーブル本体へ戻る処理が遅さの原因になります。InnoDBのテーブル本体は、主キー順に並んだ「クラスタインデックス」という形で保存されています。user_id などに貼るインデックスはセカンダリインデックスと呼ばれ、中身は「インデックスの列+主キーの値」だけです。
そのため、idx_user で user_id = 42 の行を見つけても、ordered_at や total は入っていません。MySQLは主キーを手がかりに、1行ずつ本体を読みに戻ります。件数が増えるほど、この往復が効いてきます。
今のクエリのEXPLAINを見てみましょう。
EXPLAIN SELECT ordered_at, total
FROM orders
WHERE user_id = 42
ORDER BY ordered_at DESC
LIMIT 5;
+------+----------+------+----------------+
| type | key | rows | Extra |
+------+----------+------+----------------+
| ref | idx_user | 10 | Using filesort |
+------+----------+------+----------------+
(見やすさのため、主な列だけを抜き出しています)
idx_user は使われていますが、Extra に Using index はありません。本体を読みに戻ったうえで、並べ替え(Using filesort)もしています。
一方、インデックスの中にある列だけを取るクエリならどうなるでしょうか。
EXPLAIN SELECT id, user_id
FROM orders
WHERE user_id = 42;
+------+----------+------+-------------+
| type | key | rows | Extra |
+------+----------+------+-------------+
| ref | idx_user | 10 | Using index |
+------+----------+------+-------------+
Using index が出ました。これが「インデックスだけで答えた」印です。id はインデックスの定義に書いていません。それでもカバーされるのは、先ほどの「セカンダリインデックスには主キーも入っている」仕組みのおかげです。
複合インデックスでカバリングにする
カバリングにするには、WHEREで絞る列、並べ替える列、SELECTで取る列を、この順番で1つの複合インデックスに入れます。今回なら user_id → ordered_at → total の順です。
ALTER TABLE orders ADD INDEX idx_user_date_total (user_id, ordered_at, total);
ANALYZE TABLE orders;
EXPLAIN SELECT ordered_at, total
FROM orders
WHERE user_id = 42
ORDER BY ordered_at DESC
LIMIT 5;
+------+---------------------+------+----------------------------------+
| type | key | rows | Extra |
+------+---------------------+------+----------------------------------+
| ref | idx_user_date_total | 10 | Backward index scan; Using index |
+------+---------------------+------+----------------------------------+
変化は2つあります。
Using indexが付き、本体を読みに戻らなくなったUsing filesortが消えた。インデックスがordered_at順に並んでいるため、後ろから読む(Backward index scan)だけで新しい順になる
EXPLAIN FORMAT=TREE でも確かめられます。こちらはより直接的な言葉で表示されます。
EXPLAIN FORMAT=TREE SELECT ordered_at, total
FROM orders
WHERE user_id = 42
ORDER BY ordered_at DESC
LIMIT 5;
-> Limit: 5 row(s) (cost=1.87 rows=5)
-> Covering index lookup on orders using idx_user_date_total (user_id=42) (reverse) (cost=1.87 rows=10)
Covering index lookup と出ていれば、カバリングインデックスが使われています。
列の順番は性能を大きく左右します。考え方を表にまとめます。
| 位置 | 入れる列 | 理由 |
|---|---|---|
| 先頭 | WHEREの等号(=)で絞る列 |
インデックスの範囲を一気に狭められる |
| 次 | ORDER BY・範囲条件の列 | 並んだ順に読むだけで済み、並べ替えが不要になる |
| 最後 | SELECTで取るだけの列 | 検索には使わないが、本体に戻らずに済む |
集計クエリもカバリングで軽くなる
COUNTやGROUP BYだけのクエリも、カバリングの恩恵を受けやすい場面です。ステータスごとの件数を数える例を見てみましょう。
ALTER TABLE orders ADD INDEX idx_status (status);
ANALYZE TABLE orders;
SELECT status, COUNT(*) AS cnt
FROM orders
GROUP BY status
ORDER BY status;
+----------+-----+
| status | cnt |
+----------+-----+
| canceled | 750 |
| paid | 750 |
| returned | 750 |
| shipped | 750 |
+----------+-----+
EXPLAIN SELECT status, COUNT(*) AS cnt
FROM orders
GROUP BY status;
+-------+------------+------+-------------+
| type | key | rows | Extra |
+-------+------------+------+-------------+
| index | idx_status | 3000 | Using index |
+-------+------------+------+-------------+
type が index なので、3000行すべてを読んでいます。それでも読む対象は memo などを含む本体ではありません。status と主キーだけの小さなインデックスです。全件を数える集計ほど、この差が効いてきます。
よくある落とし穴
SELECT * や列の追加で効果が消える
カバリングは「使う列がすべてインデックスにある」ときだけ成り立ちます。1列でも足りないと、本体に戻る処理が復活します。
-- status はインデックスにない
EXPLAIN SELECT ordered_at, total, status
FROM orders
WHERE user_id = 42
ORDER BY ordered_at DESC
LIMIT 5;
-- 全列を取る
EXPLAIN SELECT *
FROM orders
WHERE user_id = 42
ORDER BY ordered_at DESC
LIMIT 5;
-- どちらも同じ結果
+------+---------------------+------+---------------------+
| type | key | rows | Extra |
+------+---------------------+------+---------------------+
| ref | idx_user_date_total | 10 | Backward index scan |
+------+---------------------+------+---------------------+
同じインデックスを使っていても、Using index が消えています。画面に必要な列だけをSELECTに書く習慣が、カバリングを活かす第一歩です。
「Using index」と「Using index condition」は別物
名前は似ていますが、意味は違います。取り違えやすいので表で整理します。
| Extraの表示 | 意味 | 本体を読むか |
|---|---|---|
Using index |
インデックスだけで答えた(カバリング) | 読まない |
Using index condition |
インデックスの段階で条件を先に確かめた(ICP) | 条件に合った行だけ読む |
Using where; Using index |
インデックスだけを読み、WHEREで絞り込んだ | 読まない |
Using indexでも速いとは限らない
先頭の列を使わない条件では、インデックス全体をなめる動きになります。
EXPLAIN SELECT user_id, ordered_at
FROM orders
WHERE ordered_at >= '2026-10-01';
+-------+---------------------+------+----------+--------------------------+
| type | key | rows | filtered | Extra |
+-------+---------------------+------+----------+--------------------------+
| index | idx_user_date_total | 3000 | 33.33 | Using where; Using index |
+-------+---------------------+------+----------+--------------------------+
Using index は付いていますが、type は index、rows は3000です。本体より小さいので全表スキャンよりはましですが、絞り込みには効いていません。この検索が多いなら、ordered_at を先頭にしたインデックスを別に考えましょう。
列を詰め込みすぎない
インデックスの列を増やすほど、INSERTやUPDATEのたびに更新する量が増えます。ディスクとメモリも余分に使います。よく呼ばれて遅いクエリに絞って、カバリングを狙うのが現実的です。
まとめ
カバリングインデックスは、クエリが使う列をすべてインデックスに入れ、テーブル本体を読まずに答える工夫です。EXPLAINの Extra に Using index、FORMAT=TREEで Covering index lookup と出れば成功と覚えてください。列は「等号で絞る列 → 並べ替え・範囲の列 → 取るだけの列」の順に並べます。InnoDBでは主キーが自動で含まれる点も押さえておくと、設計の幅が広がります。SELECT * を避けて、必要な列だけを取る書き方とセットで使いましょう。

