32
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RailsとDBインデックス 〜なぜ index があると速いのか〜

32
Posted at

お疲れ様です!
シリーズで計算量やレースコンディションを書いてきましたが、今回は DBインデックス です :pencil:
「indexを貼ると速くなる」は知っていても、なぜ速いのか/どこに貼るべきかまで説明できると強いので、そこをまとめてみました。
興味ある方は読んでいただけると嬉しいです :thumbsup:

indexがないとどうなる?(フルスキャン = O(n))

indexが無いと、DBは目的の行がどこにあるか分からないので、全行を先頭から1つずつ確認します。これがフルテーブルスキャンです。

SELECT * FROM users WHERE email = 'a@example.com'
-- index なし → 100万行を1行ずつ「これ?違う…」と全部見る = O(n) 💥

件数が増えるほど線形に遅くなります。前回までの計算量でいう O(n) です。

indexは「探し方の地図」(O(log n))

indexを貼ると、DBはその列専用の探しやすい索引を別に持ちます。多くのDB(MySQL/PostgreSQL)では、この索引が B-tree(B+tree) という常にソートされた木です。

# マイグレーション
add_index :users, :email
SELECT * FROM users WHERE email = 'a@example.com'
-- index あり → B-treeをたどって一気に絞る = O(log n) 🙆

O(log n) =「毎回、半分に絞る」

O(log n) のイメージは 「1回調べるごとに、候補が半分になる」 です。
100万件から目的の1件を探す様子を比べてみます。

【index なし】O(n) … 1件ずつ全部確認
 1 → 2 → 3 → 4 → ... → 1,000,000     最悪 100万回 💥

【index あり】O(log n) … 毎回「半分」に絞る
 1,000,000 件
    ↓ 半分
   500,000
    ↓ 半分
   250,000
    ↓ 半分
   125,000
    ↓ 半分の半分の半分…
      … 
      1 件   ← たった約20回で到達!🙆

「半分 → その半分 → さらに半分…」を繰り返すと、100万件でもたった約20回log₂ 1,000,000 ≈ 20)で1件まで絞れます。これが O(log n) の正体です。

件数を増やすと、差はさらに劇的になります。

件数 index なし(O(n)) index あり(O(log n))
1,000 最悪 1,000 回 10
1,000,000 最悪 100万 回 20
1,000,000,000 最悪 10億 回 30

件数が 1000倍→100万倍→10億倍 と爆発しても、O(log n)は 10→20→30 としか増えません。
「データが増えても、ほぼ増えない」——これがindexの威力です。

B-treeは各階層で「大きい?小さい?」で枝を選び、関係ない枝を丸ごと捨てて降りていきます。
1段降りるごとに探す範囲が半分(実際はもっと)に減るので、上のように少ないステップで到達します。
「全部見ない=枝を捨てる」からこそ O(log n)。まさに 二分探索のDB版 です。

なぜハッシュじゃなくB-tree?

「一発で引くならハッシュ(O(1))の方が速いのでは?」と思いますよね。実際 Redis はハッシュで O(1) です。
でもDBが B-treeを選ぶのは、= 以外の検索も当たり前に使うからです。

WHERE age > 20               -- 範囲(>, <, BETWEEN)
ORDER BY created_at          -- 並び替え
WHERE name LIKE 'tan%'       -- 前方一致

ハッシュは値を散らばして置くので 「順番」が無く、=(完全一致)専用。上のような検索が全部できません
B-treeはソート済みなので、範囲も並び替えも前方一致も得意。多少遅くても(O(log n))、何でもできる方を選んでいるわけです。

ハッシュ B-tree
完全一致 = O(1) 最速 O(log n)
範囲・並び替え・前方一致 不可 得意

B-treeは「型」ではなく「順序(大小比較)」で並べます。整数=数値順、日付=時系列、文字列=辞書順(1文字ずつ照合)。
文字列の並びは collation(照合順序) 次第で、大文字小文字の扱いなどが変わります。

index、貼るべき? 貼らないべき?

ここが本題。「とりあえず全部に貼る」は間違いです。indexにはコストもあるので、効く場所に絞ります。

貼るべき(効く)

  • WHEREで頻繁に検索する列(検索条件になる列)
  • 外部キーuser_id など)… JOINや関連の絞り込みで多用。Railsは t.references :user(= belongs_to)でデフォルトでindexが付きます
  • ORDER BY / GROUP BY で使う列
  • 一意にしたい列add_index ..., unique: true)… 重複防止+検索高速化。前回のレースコンディション対策そのもの
  • カーディナリティが高い列(=値の種類が多い列)… email, user_id など。絞り込めるほどindexは効く

貼らないべき / 効きにくい

  • カーディナリティが低い列(値の種類が少ない)… boolean, status(数種), 性別など。絞り込めないので、DBが「全走査した方が速い」と判断しindexを使わないことも
  • 更新が非常に多い列… indexは書き込み(INSERT/UPDATE/DELETE)のたびに更新が必要。貼りすぎると書き込みが遅くなる
  • 小さいテーブル… 数百行ならフルスキャンでも一瞬。indexの旨味が薄い
  • 中間一致 LIKE '%xxx%'… B-treeは前方一致('xxx%')には効くが、先頭が%の中間・後方一致には効かない

indexはタダではありません。 貼るほど ①書き込みが遅くなる ②ストレージが増える ③オプティマイザが迷う、というコストがあります。
よく検索する × 絞り込める列」に絞って貼るのが鉄則です。

複合index(複数列)は「電話帳」

複数列をまとめて貼るときは、順序が命です。

add_index :users, [:last_name, :first_name]

これは電話帳と同じで「まず姓で並べ、姓が同じなら名で並べる」。
なので last_name 単体の検索には効きますが、first_name 単体には効きません(=左端一致の原則)。よく使う条件を左に置くのがコツです。

EXPLAIN で「効いているか」を確認する

貼ったつもりでも、実際に使われているかは EXPLAIN で確認しましょう!

EXPLAIN SELECT * FROM users WHERE email = 'a@example.com';
  • PostgreSQLSeq Scan(全走査)が出たら要注意。Index Scan ならindexが効いている
  • MySQLtype: ALL(全走査)は危険信号。ref / const ならindexが効いている

JOINの Nested Looploops にも注意

JOINをすると Nested Loop外側の行1件ごとに、内側を繰り返し引く方式)がよく出ます。ここが index の効き目で天と地に分かれます。

  • 内側の結合列にindexがある → 外側の行ごとに O(log n) で引ける ✅
  • 内側にindexが無い → 外側の行ごとに内側を全走査 → 「外側の行数 × 内側の全件」= O(n×m) で激重 💥

EXPLAIN ANALYZE を使うと、各ノードに loops=N(実際に何回実行されたか) が出ます。

Nested Loop
  -> Seq Scan on users        (rows=1000)
  -> Seq Scan on orders  (loops=1000)   ← 内側が1000回フルスキャン!💥

loops が大きいノードが Seq Scan になっていたら、そこに index が足りていないサインです。

チェックの流れ:遅いクエリを見つけたら、まず EXPLAIN(できれば EXPLAIN ANALYZE
① フルスキャン(Seq Scan/type: ALL)していないか、② Nested Loop の内側で loops が多い割にindexが効いていないか、を見ることが多いです!

最後に

  • indexが無いとフルスキャン(O(n))、あるとB-treeで一気に絞る(O(log n))
  • ハッシュじゃなくB-treeなのは、範囲・並び・前方一致もこなす万能型だから
  • 「よく検索する × 絞り込める列」に貼る。カーディナリティが低い列・更新が多い列・小さいテーブルは慎重に
  • 効いているかは EXPLAIN で確認

そしてこれからは、AIが出したクエリやマイグレーションに"効かないindex"や"フルスキャン"が潜んでいないか見抜けるのもエンジニアの役目 です。AIは動くSQLは書いてくれますが、「その検索、100万件でフルスキャンになりますよ」までは言ってくれないことがあります。ぜひ EXPLAIN を覗く習慣を持ってみてください!

32
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?