【完全比較】Rails find vs find_by vs where の違い|戻り値・SQL・パフォーマンス・使い分けを徹底解説

  • 作成日 2026.06.28
  • rails
【完全比較】Rails find vs find_by vs where の違い|戻り値・SQL・パフォーマンス・使い分けを徹底解説

Rails で ActiveRecord からレコードを取得する3大メソッド:

User.find(1)                              # find
User.find_by(email: "alice@example.com")  # find_by
User.where(active: true)                  # where

どれもデータベースから情報を取得しますが、戻り値・例外発生・SQL・パフォーマンスが大きく異なります

しかし、

  • どう使い分ければいい?
  • find_bywhere(...).first の違いは?
  • find で 999 を渡したら例外、find_by だと nil…なぜ?
  • where(...).first には隠れた ORDER BY がある?
  • パフォーマンスにどう影響する?
  • find_by!takesole は何が違う?

など、明確に答えられない開発者も多くいます。

本記事では、find / find_by / whereすべての違いを、戻り値、SQL、パフォーマンス、使い分けの観点から徹底比較します。実際の SQL を見ながら、where().first の隠れ罠、関連メソッド、アソシエーション越しの利用、ありがちな間違いまで完全網羅。この1本で ActiveRecord の検索メソッドを自信を持って選べるようになります。


目次

結論:3つの違いを一発で理解

時間がない方向けに、最重要ポイントを先に示します。

早見表

観点findfind_bywhere
検索条件主キー(id)任意の条件任意の条件
取得件数1件 or 配列1件複数件(コレクション)
戻り値(見つかる)レコードレコードActiveRecord::Relation
戻り値(見つからない)例外nil空Relation
発生例外RecordNotFoundなしなし
メソッドチェーン不可(既にオブジェクト)不可可能(さらに絞り込み)
遅延評価(lazy)即時即時遅延(呼ばれるまで実行されない)

一言で言うと

  • find: id で1件確実に取得(無ければ404扱い)
  • find_by: 条件で1件取得(無ければ nil)
  • where: 条件で複数件取得(チェーン可能)

詳細は以下で解説します。


まず押さえる:それぞれの基本

find の使い方

# 主キーで取得
user = User.find(1)
# => #<User id: 1, email: "alice@example.com", ...>

# 複数主キーで取得
users = User.find([1, 2, 3])
# => [#<User id: 1>, #<User id: 2>, #<User id: 3>]

# 見つからない場合
User.find(999)
# => ActiveRecord::RecordNotFound (Couldn't find User with 'id'=999)

find_by の使い方

# 任意の条件で1件
user = User.find_by(email: "alice@example.com")
# => #<User id: 1, ...>

# 複数条件
user = User.find_by(email: "alice@example.com", active: true)
# => #<User id: 1, ...>

# 見つからない場合
user = User.find_by(email: "nobody@example.com")
# => nil

where の使い方

# 条件指定(複数件)
users = User.where(active: true)
# => #<ActiveRecord::Relation [#<User id: 1>, #<User id: 2>, ...]>

# 件数
User.where(active: true).count

# チェーン
User.where(active: true).where("created_at > ?", 1.week.ago).order(:name).limit(10)

# 見つからない場合
User.where(email: "nobody@example.com")
# => #<ActiveRecord::Relation []> (空のRelation、nilではない!)

戻り値の決定的な違い

これが最も重要な違い。

find の戻り値

状況戻り値
見つかった(1件)レコード(モデルインスタンス)
見つかった(複数指定)Array
見つからないActiveRecord::RecordNotFound例外

find_by の戻り値

状況戻り値
見つかったレコード(モデルインスタンス)
見つからないnil

where の戻り値

状況戻り値
見つかったActiveRecord::Relation(コレクション)
見つからない空のActiveRecord::Relation(nilではない)

コード例で確認

# find
result = User.find(1)
result.class
# => User

# find_by
result = User.find_by(email: "test@example.com")
result.class
# => User or NilClass

# where
result = User.where(active: true)
result.class
# => ActiveRecord::Relation

Relation の罠

where の戻り値は配列ではなく Relation:

users = User.where(active: true)
# 配列のように使えるが…

users.first        # => 最初のUser
users.each { |u| ... }  # eachで回せる
users.count        # 件数

# でも実体は Relation
users.is_a?(Array)
# => false

users.is_a?(ActiveRecord::Relation)
# => true

生成される SQL を比較

実際に発行される SQL を見ると違いがより明確に。

find のSQL

User.find(1)
# SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]

User.find([1, 2, 3])
# SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3)  [["id", 1], ["id", 2], ["id", 3]]

find_by のSQL

User.find_by(email: "alice@example.com")
# SELECT "users".* FROM "users" WHERE "users"."email" = $1 LIMIT $2  [["email", "alice@example.com"], ["LIMIT", 1]]

where のSQL

# Relation のままなら SQL は実行されない(遅延評価)
relation = User.where(active: true)
# まだ SQL 発行なし

# .to_sql で見るだけ
relation.to_sql
# => "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"active\" = TRUE"

# 実際にデータを使う時に SQL 発行
relation.each { |u| ... }
# SELECT "users".* FROM "users" WHERE "users"."active" = TRUE

where(…).first のSQL(重要な罠)

User.where(email: "alice@example.com").first
# SELECT "users".* FROM "users"
#   WHERE "users"."email" = $1
#   ORDER BY "users"."id" ASC  ← 隠れORDER BY!
#   LIMIT $2

⚠️ 暗黙的に ORDER BY id ASC が付与されるfind_by と挙動が違う:

User.find_by(email: "alice@example.com")
# SELECT "users".* FROM "users"
#   WHERE "users"."email" = $1
#   LIMIT $2
# ← ORDER BY なし

詳しくは後述の「パフォーマンス比較」で。


find vs find_by 詳細比較

検索条件

# find: 主キー(id)のみ
User.find(1)

# find_by: 任意のカラム
User.find_by(id: 1)
User.find_by(email: "...")
User.find_by(email: "...", active: true)

見つからない時の動作

# find: 例外
User.find(999)
# => ActiveRecord::RecordNotFound

# find_by: nil
User.find_by(id: 999)
# => nil

URLパラメータからの取得

class UsersController < ApplicationController
  def show
    # 通常はこちら(404に自動マッピング)
    @user = User.find(params[:id])
  end
end

find の例外は Rails が自動的に404に変換します。詳細はActiveRecord::RecordNotFound の記事

任意条件での検索

# 標準
user = User.find_by(email: params[:email])

# 例外を出したい
user = User.find_by!(email: params[:email])
# => ActiveRecord::RecordNotFound(見つからなければ)

# nil チェック
user = User.find_by(email: params[:email])
return redirect_to root_path unless user

詳細はundefined method nil:NilClass の記事

使い分け表

状況推奨
URLパラメータからのID取得(不在=404)find
任意条件で1件、不在は正常find_by
任意条件で1件、不在=異常find_by!
ログイン処理(メール検索)find_by
ユーザー詳細ページfind

find vs where 詳細比較

戻り値

User.find(1)
# => User オブジェクト

User.where(id: 1)
# => ActiveRecord::Relation(複数返る可能性)

操作

# find:そのまま使える
user = User.find(1)
user.name
user.posts

# where:first / each などが必要
users = User.where(active: true)
users.first.name
users.each { |u| puts u.name }

複数取得

# find:配列で受け取る
users = User.find([1, 2, 3])
users.class  # => Array

# where:Relation
users = User.where(id: [1, 2, 3])
users.class  # => ActiveRecord::Relation

find の方が高速か?

主キー検索(id)でPostgreSQL/MySQL の主キーインデックスを使う場合、性能差は無視できるレベル

ただし find([1,2,3]) で1件でも見つからないと全件取得していても例外になる:

User.find([1, 2, 999])
# => ActiveRecord::RecordNotFound (Couldn't find all Users with 'id': [1, 2, 999])

許容したいなら:

User.where(id: [1, 2, 999])
# => 見つかった2件だけ返す(例外なし)

find_by vs where 詳細比較

ここが最も悩むポイント。

戻り値の違い

# find_by: 1件 or nil
User.find_by(email: "...")
# => User or nil

# where: Relation
User.where(email: "...")
# => ActiveRecord::Relation(0件以上)

where().first との違い(隠れた罠)

# 同じ結果に見えるが…
user_a = User.find_by(email: "...")
user_b = User.where(email: "...").first

実行 SQL を比較:

# find_by
# SELECT "users".* FROM "users" WHERE "users"."email" = $1 LIMIT 1

# where().first
# SELECT "users".* FROM "users" WHERE "users"."email" = $1
#   ORDER BY "users"."id" ASC LIMIT 1

where().first には暗黙の ORDER BY id ASC が付く。 これにより:

  1. インデックスがid以外なら non-indexed scanになる可能性
  2. ソート処理が必要
  3. 巨大テーブルで顕著なパフォーマンス劣化

実例:パフォーマンス問題

# email に unique index あり
User.where(email: "alice@example.com").first
# 1. email_index でアクセス
# 2. ORDER BY id ASC でソート(不要なソート!)
# 3. LIMIT 1

User.find_by(email: "alice@example.com")
# 1. email_index でアクセス
# 2. LIMIT 1
# ↑ ソートなし

巨大テーブル + 高頻度実行で差が顕在化

結論:1件取得は find_by 推奨

# ❌ 不要な ORDER BY
User.where(email: email).first

# ✅ シンプルで高速
User.find_by(email: email)

複数条件 + チェーン

# where はチェーン可能
User.where(active: true)
    .where("created_at > ?", 1.month.ago)
    .order(:name)
    .limit(10)

# find_by はチェーン不可(既にオブジェクト or nil)
User.find_by(active: true).where(...)
# => NoMethodError(user オブジェクトに where はない)

使い分け表(find_by vs where)

状況推奨
1件取得(unique制約あり)find_by
1件取得(複数あり得る、最初の1件)where(...).first(ORDER BY 意図的)
複数件取得where
条件で絞り込み後にチェーンwhere
件数だけ知りたいwhere(...).count または exists?

関連メソッド全紹介

ActiveRecord の検索メソッドは他にも多数あります。

find_by!

User.find_by!(email: "...")
# 見つかれば返す、無ければ ActiveRecord::RecordNotFound

find_by + 例外発生のセット。

first / last

User.first
# SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1

User.last
# SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT 1

# nil 返却(見つからない場合)
EmptyModel.first
# => nil

first! / last!

EmptyModel.first!
# => ActiveRecord::RecordNotFound

例外版。

take / take!

User.take
# 任意の1件(ORDER BY 無し、LIMIT 1)

User.take!
# 同上、見つからない場合は例外

take は ORDER BY なしで任意の1件。最も高速な「1件取得」

-- take
SELECT "users".* FROM "users" LIMIT 1

-- first
SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1

順序を気にしない場合は take が最速。

take(n)

User.take(5)
# => Array of 5 users

複数件もOK。

sole / find_sole_by(Rails 7+)

User.where(email: "alice@example.com").sole
# 厳密に1件のみ期待
# 0件: ActiveRecord::RecordNotFound
# 2件以上: ActiveRecord::SoleRecordExceeded
User.find_sole_by(email: "...")
# 同上の便利版

exists?

User.exists?(1)               # idで存在チェック
User.exists?(email: "...")    # 条件で存在チェック
User.where(active: true).exists?
# SELECT 1 AS one FROM "users" WHERE ... LIMIT 1

存在するかだけ知りたい時はこれが最速

pluck

User.where(active: true).pluck(:name)
# => ["Alice", "Bob", ...]

# 複数カラム
User.pluck(:id, :email)
# => [[1, "alice@example.com"], [2, "bob@example.com"]]

SELECT 指定で軽量。

ids

User.where(active: true).ids
# => [1, 2, 3, ...]

# = User.where(active: true).pluck(:id)

主キーだけ取得。

全メソッド早見表

メソッド戻り値見つからない用途
find(id)レコードRecordNotFoundURLパラメータ等
find(ids)ArrayRecordNotFound複数主キー
find_by(...)レコードnil任意条件
find_by!(...)レコードRecordNotFound任意条件、必須
where(...)Relation空Relation複数件、チェーン
where(...).firstレコードnil1件(ORDER BY付)
where(...).first!レコードRecordNotFound同上、必須
first / lastレコードnil順序付き1件
first! / last!レコードRecordNotFound同上、必須
takeレコードnil任意1件(最速)
take!レコードRecordNotFound同上、必須
take(n)Array空Array任意n件
soleレコードRecordNotFound / SoleRecordExceeded厳密に1件
find_sole_by(...)レコード同上厳密に1件、条件付き
exists?(...)true/falsefalse存在確認
pluck(:col)Array空Arrayカラム値のみ
idsArray空Arrayid一覧

アソシエーション越しの利用

find は権限制御に有用

class UsersController < ApplicationController
  def show
    # 自分の投稿のみ
    @post = current_user.posts.find(params[:id])
    # 他人の投稿IDなら RecordNotFound = 404
  end
end

current_user.posts.find は「現在のユーザーの投稿の中から探す」ため、セキュリティ的に有用。

find_by での権限制御

@post = current_user.posts.find_by(slug: params[:slug])
return redirect_to root_path unless @post

where でのフィルタリング

@articles = current_user.articles.where(published: true).order(created_at: :desc)

スコープでの絞り込み

# app/models/article.rb
class Article < ApplicationRecord
  scope :published, -> { where(published: true) }
end

# 利用
Article.published.find(1)
# 公開記事の中から探す

SQL の最適化

find_by に index を活かす

User.find_by(email: "...")
# email に index があれば高速
class AddIndexToUsersEmail < ActiveRecord::Migration[8.0]
  def change
    add_index :users, :email, unique: true
  end
end

詳細はrails db:migrate 使い方の記事

where のチェーン最適化

# ❌ 非効率
User.where(active: true).each do |user|
  next unless user.recent?   # ループ内でRubyフィルタ
  process(user)
end

# ✅ DB側でフィルタ
User.where(active: true).where("created_at > ?", 1.week.ago).each do |user|
  process(user)
end

N+1 防止:includes

# ❌ N+1問題
posts = Post.where(published: true)
posts.each { |p| p.author.name }
# 各 post で author 取得(N+1クエリ)

# ✅ includes
posts = Post.where(published: true).includes(:author)
posts.each { |p| p.author.name }
# 2クエリで完結

詳細は別途「includes vs preload vs eager_load 違い」記事で。

find_each(大量データ)

# ❌ メモリ大量消費
User.all.each { |u| u.process }

# ✅ バッチ処理
User.find_each(batch_size: 1000) { |u| u.process }

ありがちな間違い

1. find_by で nil チェック忘れ

# ❌ nil の可能性
user = User.find_by(email: email)
user.notify   # => NoMethodError if nil
# ✅ チェック追加
user = User.find_by(email: email)
return unless user
user.notify

# または find_by! で例外化
user = User.find_by!(email: email)
user.notify

詳細はundefined method nil:NilClass の記事

2. where(…).first の不要なORDER BY

# ❌ 隠れORDER BY
User.where(email: email).first

# ✅ find_by 推奨
User.find_by(email: email)

3. find([1,2,999]) の例外

# ❌ 1件でも欠けると例外
User.find([1, 2, 999])
# => ActiveRecord::RecordNotFound

# ✅ 許容
User.where(id: [1, 2, 999])
# 見つかった分だけ返す

4. where の戻り値を nil チェック

# ❌
result = User.where(active: true)
return if result.nil?   # 絶対 nil にならない!
# ✅
result = User.where(active: true)
return if result.empty?
# または
return if result.none?

where絶対 nil を返さない。空のRelation を返す。

5. count vs size vs length

# count: 常にSQL発行(COUNT(*))
User.where(active: true).count

# size: ロード済みなら配列の長さ、未ロードならSQL
users = User.where(active: true)
users.size   # ロードしてればRubyのsize、未ロードならSQL

# length: 常にRubyの配列length(全件ロード)
users.length   # 全レコードをメモリにロード後カウント
メソッド動作推奨
countSQL COUNT件数だけ知りたい時
size状況による通常はこれが安全
length全件ロード既に配列化してる時のみ

6. find_by の引数間違い

# ❌ Hash でない
User.find_by("email = ?", "...")
# 古い書き方(非推奨)

# ✅ Hash
User.find_by(email: "...")

# Hash で書けない複雑な条件は where
User.where("email LIKE ?", "%@example.com").first

7. find と find_by の混同

# ❌ 主キー以外を find に渡す
User.find(email: "...")
# 動かない

# ✅ find は id だけ
User.find(1)

# ✅ 任意条件は find_by
User.find_by(email: "...")

8. 大文字小文字の問題

# DB によって挙動が違う
User.find_by(email: "ALICE@example.com")
# PostgreSQL: 大文字小文字区別する
# MySQL: collation 次第

# 確実に case-insensitive
User.find_by("LOWER(email) = ?", email.downcase)

ベストプラクティス

1. 用途別の使い分けを徹底

# URLパラメータからの取得
@user = User.find(params[:id])

# 任意条件・存在不問
@user = User.find_by(email: params[:email])

# 必須レコード
@user = User.find_by!(email: params[:email])

# 複数件・フィルタ
@users = User.where(active: true).order(:name)

# 存在チェックのみ
User.exists?(email: params[:email])

2. where().first を避ける

# ❌
User.where(email: "...").first

# ✅
User.find_by(email: "...")

3. アソシエーション活用でセキュリティ

# ❌ 他人のリソースも検索
@post = Post.find(params[:id])

# ✅ 自分のものだけ
@post = current_user.posts.find(params[:id])

4. インデックスを意識

# 検索カラムには index
add_index :users, :email, unique: true
add_index :posts, [:user_id, :status]

5. N+1 を防ぐ

# includes で事前ロード
Post.includes(:author, :tags).where(published: true)

6. 大量データは find_each

User.where(active: true).find_each(batch_size: 1000) do |user|
  user.process
end

7. console で SQL を確認

# bin/rails c
User.where(active: true).to_sql
# 実行前に SQL を確認可能

詳細はrails console 使い方の記事


よくある質問(FAQ)

Q1. find と find_by(id: x) は同じ?

似ていますが微妙に違います:

User.find(1)
# => User or RecordNotFound

User.find_by(id: 1)
# => User or nil

例外の有無が違う。

Q2. find_by と find_by! の使い分け

# 不在=正常(nil で処理)
User.find_by(email: email)

# 不在=異常(即例外)
User.find_by!(email: email)

Q3. where().first と find_by は同じ?

SQL が違う:

User.where(email: "...").first
# ORDER BY id ASC LIMIT 1

User.find_by(email: "...")
# LIMIT 1(ORDER BYなし)

性能上 find_by 推奨。

Q4. 件数を知るには?

# DB COUNT クエリ
User.where(active: true).count

# 存在チェックのみ(より高速)
User.where(active: true).exists?

Q5. find_each と each の違い

# each: 全件メモリにロード
User.where(active: true).each { |u| u.process }

# find_each: バッチ処理(デフォルト1000件ずつ)
User.where(active: true).find_each { |u| u.process }

大量データは find_each

Q6. take と first の違い

# take: ORDER BY なし、任意の1件
User.take
# LIMIT 1

# first: ORDER BY id ASC
User.first
# ORDER BY id ASC LIMIT 1

順序を気にしないなら take が最速。

Q7. sole が便利な場面

# unique 制約を活かしたい
User.where(email: email).sole
# 厳密に1件 → 多重なら気付ける

データ整合性チェックに有効。

Q8. pluck はいつ使う?

# ❌ 全カラム取得 → name だけ使う
User.all.map(&:name)
# メモリ無駄

# ✅ name のみ取得
User.pluck(:name)
# SELECT name FROM users

メモリ・通信量節約。

Q9. where と and / or

# AND(複数 where)
User.where(active: true).where("age > ?", 18)

# OR(Rails 5+)
User.where(active: true).or(User.where("age > ?", 65))

Q10. like 検索

User.where("email LIKE ?", "%@example.com")
User.where("name ILIKE ?", "alice%")   # PostgreSQL、大文字小文字無視

# 動的な値は必ず ? でバインド(SQL injection 防止)

Q11. find_or_create_by

user = User.find_or_create_by(email: "alice@example.com") do |u|
  u.name = "Alice"
end
# 存在: 返す
# なし: 作成して返す

⚠️ 競合状態に注意。unique index で DB レベル制約推奨。

Q12. find_by での null 検索

# nil カラム検索
User.find_by(deleted_at: nil)
# WHERE deleted_at IS NULL

# 非 null
User.where.not(deleted_at: nil)
# WHERE deleted_at IS NOT NULL

参考リンク・関連資料

Rails 公式

関連記事(本サイト)


まとめ

find / find_by / where の違いを理解することは、Rails 開発の基礎中の基礎。要点を再整理します。

  • find: 主キーで1件、不在は RecordNotFound例外、URL パラメータに最適
  • find_by: 任意条件で1件、不在は nil、任意検索の基本
  • where: 任意条件で複数件(Relation)、空も nil ではなく空Relation

SQL の違い(重要)

  • find_by(...): WHERE ... LIMIT 1
  • where(...).first: WHERE ... ORDER BY id ASC LIMIT 1隠れORDER BY

→ 1件取得は find_by 推奨

関連メソッド

  • find_by! / first! / take! / sole: 例外バージョン
  • first / last / take: 順序付き / 順序なし
  • exists?: 存在チェックのみ(最速)
  • pluck: 特定カラムのみ
  • find_each: 大量データのバッチ処理

使い分け

状況推奨
URLパラメータからIDfind
任意条件で1件、不在=正常find_by
任意条件で1件、不在=異常find_by!
複数件取得where
件数だけcount / exists?
大量データ処理find_each

これらの知識は、Rails アプリの設計・パフォーマンス改善・コードレビューなど、あらゆる場面で活用できます。本記事をブックマークしておけば、ActiveRecord クエリの選択に迷うことが大幅に減ります。


本記事は2026年6月時点の情報をもとに、Ruby on Rails 7.x / 8.x での動作確認・公式ドキュメントに基づき作成しています。Rails のバージョンによって挙動が異なる場合があるため、最新の情報はRails公式ガイドもあわせてご確認ください。