複合インデックスは順番が大事。WHEREとORDER BYから考えるインデックス設計

アイキャッチ画像

はじめに

SQLのパフォーマンスを改善しようとすると、

「検索に使っているカラムにインデックスを貼ればいい」

と考えがちです。

自分も最初は、インデックスがあるかどうかを中心に見ていました。

ただ、大量データを扱う中で感じたのは、

インデックスは「あるかどうか」だけではなく、「どの順番で作られているか」も重要

ということです。

特に、

WHERE
ORDER BY
LIMIT

が組み合わさるSQLでは、複合インデックスの設計によって実行方法が変わることがあります。

この記事では、次のようなSQLを例に、

SELECT *
FROM posts
WHERE user_id = 100
ORDER BY created_at DESC
LIMIT 20;

なぜ (user_id, created_at) のような複合インデックスを考えるのか

を整理します。


そもそも複合インデックスとは

複合インデックスとは、複数のカラムを組み合わせて作るインデックスです。

例えば、

CREATE INDEX index_posts_on_user_id_and_created_at
ON posts (user_id, created_at);

とすると、

user_id
+
created_at

を組み合わせたインデックスになります。

ここで重要なのが、

(user_id, created_at)

(created_at, user_id)

上記2つは同じではないということです。

カラムの順番にも意味があります。

MySQLの複合インデックスでは、基本的に左側のカラムから順番に利用されます。

例えば (user_id, created_at) なら、user_id 単体や user_id + created_at を条件とする検索では利用しやすい一方、created_at だけを条件にした検索では、同じようには利用できません。


まずSQLを見て考える

例えば、SNSの投稿一覧で、

特定ユーザーの最新投稿を20件取得する

という処理があるとします。

SQLは次のようになります。

SELECT *
FROM posts
WHERE user_id = 100
ORDER BY created_at DESC
LIMIT 20;

このSQLでは、

WHERE
user_id = 100

ORDER BY
created_at DESC

LIMIT
20

という3つの要素があります。

ここで考えたいのが、

データベースは、この20件をどうやって探すのか?

ということです。


user_id だけにインデックスがある場合

例えば、

CREATE INDEX index_posts_on_user_id
ON posts (user_id);

だけがあるとします。

これによって、

WHERE user_id = 100

に該当する投稿は探しやすくなります。

ただし、そのユーザーが大量に投稿している場合、

user_id = 100 の投稿を探す
↓
該当する投稿を取得
↓
created_at順に並べる
↓
上位20件を返す

という処理になる可能性があります。

つまり、

WHEREの絞り込みには使えても、ORDER BYまで効率よく処理できるとは限りません。


created_at だけならどうなるか

逆に、

CREATE INDEX index_posts_on_created_at
ON posts (created_at);

だけがある場合を考えます。

created_at の順番にデータをたどりやすくなります。

ただ、今回必要なのは、

WHERE user_id = 100

に一致する投稿だけです。

そのため、実行方法によっては、

新しい投稿から確認
↓
user_id = 100 か確認
↓
違ったら次へ
↓
20件見つかるまで続ける

という形になる可能性があります。

大量のユーザーの投稿が混ざっている場合、これも必ずしも効率がよいとは限りません。


(user_id, created_at) を考える

そこで、

CREATE INDEX index_posts_on_user_id_and_created_at
ON posts (user_id, created_at);

という複合インデックスを考えます。

このインデックスでは、イメージとして、

user_id
↓
その中で created_at 順

のようにデータが並びます。

今回のSQLは、

WHERE user_id = 100
ORDER BY created_at DESC
LIMIT 20;

なので、

user_id = 100 の範囲を探す
↓
created_at の順番を利用する
↓
必要な20件を取得する

という実行方法を取れる可能性があります。

MySQLでは、複合インデックスの先頭カラムが WHERE で固定されている場合、後続カラムの並びを ORDER BY に利用し、追加のソートを避けられることがあります。


順番を逆にすると同じではない

では、

(user_id, created_at)

ではなく、

(created_at, user_id)

でもよいのでしょうか。

今回のSQLでは、この2つを同じものとして扱うことはできません。

WHERE user_id = 100
ORDER BY created_at DESC

では、まず user_id = 100 の範囲に絞り、その中から created_at の順番を利用したいので、

(user_id, created_at)

の方が今回の検索パターンに合っています。

複合インデックスには、左端プレフィックスという考え方があります。

例えば、

(user_id, created_at, id)

というインデックスがあれば、基本的には、

user_id

user_id + created_at

user_id + created_at + id

のように、左側から連続したカラムを使う検索で利用しやすくなります。

一方、

created_at

created_at + id

のように先頭の user_id を使わない検索では、同じようには利用できません。

これが、

複合インデックスは順番が大事

と言われる理由の一つです。


「WHEREに使うカラムを先にすればいい」でもない

ここで注意したいのが、

WHEREに書いてあるカラムを全部先に置けばいい

という単純な話でもないことです。

例えば、

WHERE status = 1
  AND user_id = 100
ORDER BY created_at DESC

というSQLがあったとしても、

(status, user_id, created_at)

が常に最適とは限りません。

実際には、

  • それぞれのカラムの値の偏り
  • どのSQLがよく実行されるか
  • 何件取得するか
  • JOINの有無
  • 他のクエリでもそのインデックスを使うか

なども考える必要があります。

そのため、自分は、

SQLを見ただけでインデックスを決めず、必ず EXPLAIN で確認する

ようにしています。


EXPLAINで本当に使われているか確認する

例えば、

EXPLAIN
SELECT *
FROM posts
WHERE user_id = 100
ORDER BY created_at DESC
LIMIT 20;

を実行します。

見るポイントとしては、

possible_keys
key
rows
Extra

などがあります。

特に、

key

で、想定している複合インデックスが実際に使われているか確認します。

また、ORDER BY をインデックスの順序だけでは処理できない場合、Extra に Using filesort が表示されることがあります。

Using filesort が表示されたからといって、必ずしも問題があるわけではありません。

ただ、

なぜ追加のソートが必要になっているのか?

を確認する材料にはなります。

条件が合えば、MySQLはインデックスの順序を利用して ORDER BY を処理し、追加のソートを避けることができます。


Before / Afterで確認する

例えば、最初は、

INDEX(user_id)

だけだったとします。

Before

WHERE user_id = 100
↓
対象を取得
↓
created_atで並び替え
↓
20件返す

その後、

INDEX(user_id, created_at)

を追加します。

After

user_id = 100 の範囲を探す
↓
インデックス上の順序を利用
↓
必要な20件を取得

という実行方法になる可能性があります。

ただし、

複合インデックスを追加しただけで、改善したと判断してはいけません。

変更前にEXPLAIN
↓
インデックス変更
↓
変更後にEXPLAIN

という形で、実行計画を比較することが大切です。


インデックスは増やしすぎてもよくない

検索を速くしたいからといって、あらゆる組み合わせのインデックスを作ればよいわけではありません。

インデックスを追加すると、

INSERT
UPDATE
DELETE

の際に、そのインデックスも更新する必要があります。

さらに、インデックス自体がストレージ領域も使用します。

そのため、

遅い
↓
とりあえずインデックス追加

ではなく、

実際のSQLを見る
↓
WHEREを見る
↓
ORDER BYを見る
↓
既存インデックスを見る
↓
EXPLAINする
↓
必要なら複合インデックスを検討
↓
もう一度EXPLAINする

という流れで考えるようにしています。


まとめ

複合インデックスは、

検索に使うカラムをまとめてインデックスにすればいい

というものではありません。

今回の、

SELECT *
FROM posts
WHERE user_id = 100
ORDER BY created_at DESC
LIMIT 20;

のようなSQLなら、

WHERE
↓
user_id

ORDER BY
↓
created_at

という実際の検索方法を考え、

(user_id, created_at)

のような複合インデックスを検討できます。

大切なのは、

インデックス単体を見るのではなく、SQL全体を見ることです。

自分は大量データを扱うようになってから、

どのカラムにインデックスがある?

どんなWHERE条件?

どんなORDER BY?

どの順番でインデックスが作られている?

実際にそのインデックスは使われた?

まで確認するようになりました。

複合インデックスは「何を入れるか」だけではなく、「どの順番で入れるか」まで考える。

SQLのパフォーマンスを見る上で、特に意識したいポイントの一つです。