📌
DBのインデックスを完全理解しよう!📚
DBのインデックスを完全理解しよう!📚
データベースのインデックスって聞いたことはあるけど、実際どういうものかよくわからない...そんな方も多いのではないでしょうか?
この記事では、本の索引という身近な例から始めて、データベースのインデックスを完全に理解できるよう解説します。
📖 本の索引で理解するインデックスの仕組み
索引なしの本で特定の言葉を探すと...
想像してみてください。500ページの分厚い技術書で「データベース」という言葉が出てくるページを全て見つけたいとします。
索引がない場合:
1ページ目: 「データベース」はあるかな? → なし
2ページ目: 「データベース」はあるかな? → なし
3ページ目: 「データベース」はあるかな? → なし
...
500ページ目まで全てチェック!
これは**全表スキャン(Full Table Scan)**と呼ばれる状況です。最悪の場合、500ページ全てを確認する必要があります。
索引がある本なら...
索引を開く
「た」の項目 → 「データベース」を発見!
→ 「15ページ、89ページ、234ページ、456ページ」
たった数秒で完了!
🏪 実店舗での例え
コンビニの商品陳列(インデックスあり)
- ドリンクコーナー: 冷たい飲み物はここ
- パンコーナー: パンはここ
- お菓子コーナー: お菓子はここ
欲しい商品をすぐに見つけられます!
倉庫のランダム配置(インデックスなし)
商品がランダムに置かれている倉庫では、コーラを見つけるために全ての棚を端から順番に探す必要があります...
💾 データベースでのインデックス
ユーザーテーブルの例
users テーブル(100万レコード)
id | name | email
1 | 田中太郎 | tanaka@example.com
2 | 佐藤花子 | sato@example.com
3 | 鈴木一郎 | suzuki@example.com
...
999999 | 山田次郎 | yamada@example.com
インデックスなしでの検索
SELECT * FROM users WHERE email = 'yamada@example.com';
データベースの動作:
レコード1: tanaka@example.com == yamada@example.com? → NO
レコード2: sato@example.com == yamada@example.com? → NO
レコード3: suzuki@example.com == yamada@example.com? → NO
...
レコード999999: yamada@example.com == yamada@example.com? → YES!
結果: 100万レコード全てをチェック!⏰
インデックスありでの検索
emailカラムにインデックスを作成すると、データベース内部で索引表が作られます。
email索引表(アルファベット順にソート済み)
sato@example.com → レコード2
suzuki@example.com → レコード3
tanaka@example.com → レコード1
yamada@example.com → レコード999999
検索時の動作:
1. 索引表をバイナリサーチ(二分探索)で検索
2. yamada@example.com を発見
3. 対応するレコード999999を直接取得
結果: 数回の比較で完了!⚡
🚀 Railsでのインデックス実装
1. マイグレーション生成
$ rails generate migration add_index_to_users_email
2. マイグレーションファイル編集
# db/migrate/[timestamp]_add_index_to_users_email.rb
class AddIndexToUsersEmail < ActiveRecord::Migration[7.0]
def change
add_index :users, :email, unique: true
end
end
3. マイグレーション実行
$ rails db:migrate
⚡ パフォーマンスの違い
実際の数値例
| レコード数 | インデックスなし | インデックスあり | 速度向上 |
|---|---|---|---|
| 1,000 | 500回の比較 | 約10回の比較 | 50倍 |
| 100,000 | 50,000回の比較 | 約17回の比較 | 2,941倍 |
| 1,000,000 | 500,000回の比較 | 約20回の比較 | 25,000倍 |
🔒 一意性制約の重要性
問題のシナリオ
# 同時に2つのリクエストが処理される場合
# リクエスト1のタイミング
user1 = User.new(email: "alice@example.com")
user1.valid? # => true (まだDBには存在しない)
# リクエスト2のタイミング(ほぼ同時)
user2 = User.new(email: "alice@example.com")
user2.valid? # => true (まだDBには存在しない)
# 保存処理
user1.save # 成功
user2.save # Active Recordレベルでは成功してしまう!
解決策: データベースレベルの一意性制約
# マイグレーション
add_index :users, :email, unique: true
これにより、データベースレベルで重複が防がれます:
user1.save # 成功
user2.save # ActiveRecord::RecordNotUnique エラーが発生
📊 インデックスの種類
1. 通常のインデックス
add_index :users, :name
検索速度は向上するが、重複は許可
2. 一意インデックス
add_index :users, :email, unique: true
検索速度向上 + 重複防止
3. 複合インデックス
add_index :users, [:last_name, :first_name]
複数カラムの組み合わせで検索する場合に有効
⚠️ インデックスの注意点
デメリットもあります
- 追加のストレージ容量: 索引表の分だけ容量が増加
- 書き込み速度の低下: データ挿入・更新時に索引表も更新が必要
- メンテナンス: インデックスの管理が必要
いつインデックスを作るべき?
✅ 作るべき場合:
- 頻繁に検索されるカラム
- WHERE句でよく使われるカラム
- 一意性を保証したいカラム
- 外部キー
❌ 不要な場合:
- ほとんど検索されないカラム
- 頻繁に更新されるテーブルの多数のカラム
🛠️ 実践的なマイグレーション例
class AddIndexesToUsers < ActiveRecord::Migration[7.0]
def change
# 一意性を保証(ユーザー登録用)
add_index :users, :email, unique: true
# 検索パフォーマンス向上(ログイン用)
add_index :users, :username
# 複合インデックス(名前での検索用)
add_index :users, [:last_name, :first_name]
# 外部キー用
add_index :posts, :user_id
end
end
🔍 インデックスの確認方法
Rails console
# インデックス一覧を表示
ActiveRecord::Base.connection.indexes('users')
データベース直接確認(PostgreSQL)
\d users
データベース直接確認(MySQL)
SHOW INDEX FROM users;
📈 実際の効果測定
ベンチマークの例
# インデックス追加前
Benchmark.measure do
1000.times { User.find_by(email: "test#{rand(100000)}@example.com") }
end
# => 2.5秒
# インデックス追加後
Benchmark.measure do
1000.times { User.find_by(email: "test#{rand(100000)}@example.com") }
end
# => 0.05秒(50倍高速化!)
🎯 まとめ
データベースのインデックスは:
- 本の索引と同じ仕組みで動作
- 検索速度を劇的に向上させる
- 一意性制約でデータ整合性を保証
- 適切に使えばアプリケーションのパフォーマンスが大幅改善
特にRailsアプリケーションでは、ユーザー認証機能でemailカラムにuniqueインデックスを追加することは必須と言えるでしょう。
まずは自分のアプリケーションで頻繁に検索するカラムから、インデックスを追加してみてください!
参考資料
- Rails Guide: Active Record Migrations
- データベース設計のベストプラクティス
Discussion