0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DB設計のindexについて学んだので振り返る

Posted at

データベースパフォーマンスの向上において、Indexの適切な設計は非常に重要です。本記事では、RailsでのIndexと複合Indexの設計について、実用的な例とともに詳しく解説します。

Indexとは

Indexは、データベースの検索速度を向上させるための仕組みです。本の索引のように、データを効率的に見つけるための「道しるべ」の役割を果たします。

Indexがない場合、データベースはテーブル全体をスキャンする必要がありますが、Indexがあれば目的のデータに直接アクセスできるため、大幅な高速化が可能です。

Railsでの基本的なIndex作成

単一カラムのIndex

class AddIndexToUsers < ActiveRecord::Migration[7.0]
  def change
    add_index :users, :email
  end
end

これにより、usersテーブルのemailカラムにIndexが作成され、以下のような検索が高速化されます:

User.find_by(email: "user@example.com")
User.where(email: "user@example.com")

ユニークIndex

重複を許さないカラムには、ユニークIndexを設定します:

add_index :users, :email, unique: true

複合Index(Composite Index)の威力

複合Indexは、複数のカラムを組み合わせたIndexです。複数の条件で同時に検索する場合に威力を発揮します。

複合Indexの基本例

class AddCompositeIndexToOrders < ActiveRecord::Migration[7.0]
  def change
    add_index :orders, [:user_id, :status, :created_at]
  end
end

この複合Indexは以下のようなクエリを高速化します:

# 効率的に実行される
Order.where(user_id: 123, status: 'pending')
Order.where(user_id: 123, status: 'pending').order(:created_at)
Order.where(user_id: 123)

複合Indexの重要なポイント

カラムの順序が決定的に重要

複合Indexでは、カラムの順序が検索効率に大きく影響します。

[:user_id, :status, :created_at]のIndexの場合:

検索パターン 効果 理由
user_idのみ ✅ 効果的 先頭カラムを使用
user_id, status ✅ 効果的 先頭から連続するカラムを使用
user_id, status, created_at ✅ 効果的 全カラムを使用
statusのみ ❌ 効果が薄い 先頭カラムをスキップ
created_atのみ ❌ 効果が薄い 先頭カラムをスキップ

カラム順序の決定方法

  1. 選択性の高いカラムを先頭に: より絞り込み効果の高いカラム
  2. よく単独で検索されるカラムを先頭に: 単一カラム検索でも効果を発揮
  3. 範囲検索するカラムは最後に: BETWEEN>などの範囲検索

実用的なIndex設計例

ECサイトの注文管理

class CreateOrders < ActiveRecord::Migration[7.0]
  def change
    create_table :orders do |t|
      t.references :user, null: false
      t.string :status
      t.decimal :total_amount
      t.datetime :shipped_at
      t.timestamps
    end

    # よく使われる検索パターンに基づいたIndex
    add_index :orders, [:user_id, :status]           # ユーザーの特定ステータス注文
    add_index :orders, [:status, :created_at]        # ステータス別の時系列検索
    add_index :orders, [:shipped_at]                 # 発送日での検索
    add_index :orders, [:user_id, :created_at]       # ユーザーの注文履歴
  end
end

対応するクエリ例:

# ユーザーの特定ステータスの注文
Order.where(user_id: current_user.id, status: 'shipped')

# 特定期間の特定ステータス注文
Order.where(status: 'pending')
     .where(created_at: 1.week.ago..Time.current)

# ユーザーの最新注文履歴
Order.where(user_id: current_user.id)
     .order(created_at: :desc)
     .limit(10)

ブログシステムの記事管理

class CreatePosts < ActiveRecord::Migration[7.0]
  def change
    create_table :posts do |t|
      t.references :user, null: false
      t.string :status, default: 'draft'
      t.string :slug
      t.datetime :published_at
      t.integer :views_count, default: 0
      t.timestamps
    end

    # Index設計
    add_index :posts, [:status, :published_at]       # 公開記事の時系列検索
    add_index :posts, [:user_id, :status]            # ユーザー別ステータス検索
    add_index :posts, [:slug], unique: true          # URL用スラッグ
    add_index :posts, [:published_at]                # 公開日検索
    add_index :posts, [:views_count]                 # 人気記事検索
  end
end

Indexのベストプラクティス

1. 外部キーには必ずIndex

class CreateComments < ActiveRecord::Migration[7.0]
  def change
    create_table :comments do |t|
      t.references :post, null: false, foreign_key: true, index: true
      t.references :user, null: false, foreign_key: true, index: true
      t.text :content
      t.timestamps
    end
  end
end

2. WHERE句でよく使うカラムを優先

# アクセス頻度の高い検索条件
add_index :users, :email                    # ログイン時の検索
add_index :posts, :published_at             # 記事一覧の表示
add_index :orders, :status                  # 注文管理

3. 部分Index(条件付きIndex)の活用

PostgreSQLでは条件付きIndexが有効です:

# アクティブユーザーのみにIndex
add_index :users, :last_login_at, where: "deleted_at IS NULL"

# 公開記事のみにIndex
add_index :posts, :published_at, where: "status = 'published'"

4. カバリングIndexの検討

検索結果に必要なカラムをIndexに含めることで、テーブルアクセスを削減:

# SELECT id, name, email WHERE active = true のためのIndex
add_index :users, [:active, :id, :name, :email]

パフォーマンス確認方法

ログでクエリ実行時間を確認

# 開発環境でのログ確認
User.where(email: "test@example.com").to_a
# User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."email" = ?

EXPLAIN文で実行プランを確認

# 実行プランの確認
puts User.where(email: "test@example.com").explain

# PostgreSQLの場合、より詳細な分析
puts User.where(email: "test@example.com").explain(:analyze, :buffers)

本番環境でのクエリ分析

# config/environments/production.rb
config.active_record.logger = Logger.new(STDOUT)

# または、より詳細な分析のためのGem使用
# gem 'pg_query'
# gem 'marginalia'

Index作成時の注意点

1. 大きなテーブルでの並行Index作成

class AddIndexConcurrently < ActiveRecord::Migration[7.0]
  disable_ddl_transaction!

  def change
    add_index :large_table, :column, algorithm: :concurrently
  end
end

2. Indexの命名規則

# 明確な命名
add_index :orders, [:user_id, :status], name: 'idx_orders_user_status'
add_index :posts, [:published_at], name: 'idx_posts_published_date'

3. 不要なIndexの削除

class RemoveUnusedIndex < ActiveRecord::Migration[7.0]
  def change
    remove_index :table_name, :column_name
  end
end

Indexの監視とメンテナンス

使用状況の確認(PostgreSQL)

-- Index使用統計
SELECT 
  schemaname,
  tablename,
  indexname,
  idx_scan,
  idx_tup_read,
  idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC;

重複Indexの検出

-- 重複または類似Indexの検出
SELECT 
  a.indrelid::regclass,
  a.indexrelid::regclass,
  b.indexrelid::regclass
FROM pg_index a
JOIN pg_index b ON a.indrelid = b.indrelid
WHERE a.indexrelid > b.indexrelid
  AND a.indkey::text = b.indkey::text;

まとめ

適切なIndex設計により、Railsアプリケーションのパフォーマンスは劇的に改善されます。重要なポイントは:

  1. アプリケーションの実際の検索パターンを分析
  2. 複合Indexのカラム順序を慎重に決定
  3. 定期的な監視とメンテナンス
  4. 過度なIndexは避け、必要なものに絞る

Index設計は、アプリケーションの成長とともに継続的に見直しが必要な領域です。定期的にクエリパフォーマンスを確認し、最適化を行っていきましょう。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?