Railsのfind_by_sqlとは?安全な使い方と生SQLの注意点
CONTENTS
- 1 Railsのfind_by_sqlとは
- 2 find_by_sqlを使うべき場面
- 3 find_by_sqlの安全な書き方
- 4 JOIN・集計で使うfind_by_sqlの実務例
- 5 動的なSQLを組み立てるときの設計
- 6 find_by_sqlを使うときの注意点
- 7 N+1問題とプリロードの注意点
- 8 find_by_sqlとselect_all・execute・pluckの違い
- 9 find_by_sqlを保守しやすくする実務設計
- 10 パフォーマンス確認で見るべきポイント
- 11 find_by_sqlでよくある失敗
- 12 find_by_sqlを使う判断基準
- 13 参考にしたい公式情報・外部情報
- 14 まとめ:find_by_sqlは「便利な抜け道」ではなく「責任を持って使う設計手段」
Railsで複雑なSQLを書きたいとき、「ActiveRecordのwhereやjoinsだけでは表現しづらい」「集計やJOINをもっと直接書きたい」と感じる場面があります。
そのようなときに選択肢になるのが、find_by_sqlです。
ただし、find_by_sqlは便利な一方で、生SQLをそのまま扱うため、SQLインジェクション、DB依存、N+1、保守性低下といったリスクもあります。この記事では、find_by_sqlの基本的な使い方から、JOIN・集計の実務例、安全な書き方、select_allやexecuteとの使い分けまでを体系的に解説します。
Railsのfind_by_sqlとは
find_by_sqlは、RailsのActiveRecordで生SQLを直接実行し、その結果をモデルのインスタンス配列として受け取るためのメソッドです。
通常、Railsでは次のようにActiveRecordのクエリインターフェースを使ってデータを取得します。
User.where(status: "active").order(created_at: :desc)
しかし、複雑なJOIN、集計、サブクエリ、DB固有の関数、ウィンドウ関数などを使いたい場合、ActiveRecordのメソッドチェーンだけでは読みづらくなることがあります。
そのような場面で、SQLを直接書けるのがfind_by_sqlです。
Rails公式APIでも、find_by_sqlはカスタムSQLを実行し、呼び出したモデルのオブジェクト配列として結果を返すメソッドとして説明されています。また、SQLはそのままDBに渡され、DB非依存の変換は行われないため、DB固有のSQLを書く場合は注意が必要です。
find_by_sqlの基本形
基本的な書き方は次のとおりです。
users = User.find_by_sql("SELECT * FROM users WHERE status = 'active'")
この場合、戻り値はUserインスタンスの配列です。
users.each do |user|
puts user.name
end
User.find_by_sqlを呼び出しているため、結果はUserオブジェクトとして扱えます。
ただし、find_by_sqlはSELECT結果を取得するためのメソッドです。INSERT、UPDATE、DELETEのような更新系SQLを実行する目的では基本的に使いません。
更新系SQLを実行したい場合は、後述するconnection.executeなどを検討します。
find_by_sqlの戻り値はActiveRecordオブジェクトの配列
find_by_sqlの大きな特徴は、生SQLで取得した結果がActiveRecordモデルのインスタンスになることです。
例えば、次のように書いた場合です。
users = User.find_by_sql("SELECT id, name, email FROM users")
この戻り値は、Userインスタンスの配列です。
user = users.first
puts user.name
puts user.email
一方で、SELECTしていないカラムは基本的に使えません。
users = User.find_by_sql("SELECT id, name FROM users")
user = users.first
puts user.name # 使える
puts user.email # SELECTしていないため注意
特に注意したいのは、idをSELECTしないケースです。
ActiveRecordのモデルとして扱う場合、idはレコードの同一性や関連取得に関わります。Railsガイドでも、idがない状態で関連を扱うと注意が必要であることが説明されています。
そのため、通常のモデルとして扱う結果であれば、原則として主キーであるidを含めるのが安全です。
User.find_by_sql("SELECT users.* FROM users")
集計結果やレポート用データのように、モデルとして更新する予定がない場合は、find_by_sqlではなくselect_allを使った方が適切な場合もあります。
find_by_sqlを使うべき場面
find_by_sqlは、Railsアプリケーションで常に使うべきメソッドではありません。
まずはActiveRecordの標準的な書き方で表現できないかを検討し、それでも生SQLの方が明確・安全・高速になる場合に使うのが基本です。
ActiveRecordだけでは読みづらい複雑なJOIN
例えば、ユーザーごとの注文数を集計して一覧表示したい場合、ActiveRecordでも書けますが、条件が増えると可読性が下がることがあります。
sql = <<~SQL
SELECT
users.*,
COUNT(orders.id) AS orders_count
FROM users
LEFT JOIN orders ON orders.user_id = users.id
GROUP BY users.id
ORDER BY orders_count DESC
SQL
users = User.find_by_sql(sql)
このように書くと、SQLとして何を取得しているのかが明確になります。
特に、次のような条件が重なる場合は、生SQLの方が読みやすいことがあります。
- 複数テーブルをJOINする
- 集計列を追加する
- GROUP BYやHAVINGを使う
- サブクエリを使う
- DB固有の関数を使う
- 実行計画を意識してSQLを調整したい
DB側で集計してアプリ側の処理を減らしたい場合
大量データをRuby側で加工すると、メモリ使用量や処理時間が増えます。
例えば、全注文を取得してRubyでユーザーごとに集計するより、DB側でCOUNTやSUMを使って集計した方が効率的なケースは多いです。
sql = <<~SQL
SELECT
users.*,
COUNT(orders.id) AS orders_count,
COALESCE(SUM(orders.total_price), 0) AS total_sales
FROM users
LEFT JOIN orders ON orders.user_id = users.id
GROUP BY users.id
ORDER BY total_sales DESC
SQL
users = User.find_by_sql(sql)
このように、DBが得意な集計処理はDBに任せることで、アプリケーション側の処理を減らせます。
ただし、SQLの品質が低いと逆に遅くなるため、インデックス、JOIN条件、WHERE条件、GROUP BYの粒度を確認することが重要です。
ウィンドウ関数やDB固有機能を使いたい場合
PostgreSQLやMySQLには、ActiveRecordの標準メソッドだけでは表現しづらい機能があります。
例えば、ランキング表示でROW_NUMBER()を使いたい場合です。
sql = <<~SQL
SELECT
users.*,
ROW_NUMBER() OVER (ORDER BY users.created_at DESC) AS row_number
FROM users
SQL
users = User.find_by_sql(sql)
このようなSQLは、ActiveRecordで無理に表現するより、生SQLで書いた方が意図が明確になります。
find_by_sqlの安全な書き方
find_by_sqlで最も重要なのは、安全に値を渡すことです。
生SQLを扱う場合、ユーザー入力を文字列連結で埋め込むとSQLインジェクションの危険があります。
OWASPのSQL Injection Prevention Cheat Sheetでも、ユーザー入力を含む動的クエリを文字列連結で組み立てることは避け、パラメータ化されたクエリなどの防御策を使うことが重要だと説明されています。
NG例:文字列連結でSQLを組み立てる
次のような書き方は避けるべきです。
name = params[:name]
sql = "SELECT * FROM users WHERE name = '#{name}'"
users = User.find_by_sql(sql)
一見すると問題なさそうですが、params[:name]に悪意ある文字列が入った場合、SQLの構造そのものが変わる可能性があります。
生SQLでは、「値」と「SQL構文」を明確に分ける必要があります。
OK例:プレースホルダを使う
安全に値を渡すには、プレースホルダを使います。
sql = "SELECT * FROM users WHERE name = ?"
users = User.find_by_sql([sql, params[:name]])
複数の値を渡す場合は次のように書けます。
sql = <<~SQL
SELECT *
FROM users
WHERE status = ?
AND created_at >= ?
SQL
users = User.find_by_sql([sql, "active", 1.month.ago])
Rails公式APIでも、find_by_sqlではwhereと同じようにプレースホルダを使えることが示されています。
名前付きプレースホルダを使う
引数が増える場合は、名前付きプレースホルダを使うと読みやすくなります。
sql = <<~SQL
SELECT *
FROM users
WHERE status = :status
AND created_at >= :from
SQL
users = User.find_by_sql([
sql,
{
status: "active",
from: 1.month.ago
}
])
?プレースホルダよりも、どの値が何を意味しているのか分かりやすくなります。
実務では、SQLが長くなるほど名前付きプレースホルダの方が保守しやすくなります。
JOIN・集計で使うfind_by_sqlの実務例
ここからは、実務でよくあるパターンを見ていきます。
ユーザーごとの注文数を取得する
ユーザー一覧に注文数を表示したい場合、次のように書けます。
sql = <<~SQL
SELECT
users.*,
COUNT(orders.id) AS orders_count
FROM users
LEFT JOIN orders ON orders.user_id = users.id
GROUP BY users.id
ORDER BY orders_count DESC
SQL
users = User.find_by_sql(sql)
取得したorders_countは、Userインスタンスの属性のように参照できます。
users.each do |user|
puts "#{user.name}: #{user.orders_count}"
end
ただし、orders_countはDBやアダプタによって文字列として返る場合があります。
数値計算に使う場合は、明示的に変換しておくと安全です。
user.orders_count.to_i
HAVINGで集計結果を絞り込む
注文数が一定以上のユーザーだけを取得する場合は、HAVINGを使います。
sql = <<~SQL
SELECT
users.*,
COUNT(orders.id) AS orders_count
FROM users
LEFT JOIN orders ON orders.user_id = users.id
GROUP BY users.id
HAVING COUNT(orders.id) >= ?
ORDER BY orders_count DESC
SQL
users = User.find_by_sql([sql, 5])
ここで重要なのは、WHEREとHAVINGの違いです。
WHEREは集計前の行に対する条件、HAVINGは集計後の結果に対する条件です。
注文数のような集計結果で絞り込む場合は、HAVINGを使います。
LEFT JOINでNULLを考慮する
LEFT JOINを使うと、関連データが存在しないレコードも取得できます。
ただし、SUMなどの集計関数ではNULLが返ることがあります。
sql = <<~SQL
SELECT
users.*,
COALESCE(SUM(orders.total_price), 0) AS total_sales
FROM users
LEFT JOIN orders ON orders.user_id = users.id
GROUP BY users.id
SQL
users = User.find_by_sql(sql)
COALESCEを使うことで、NULLを0に変換できます。
アプリ側で毎回nilチェックを書くより、SQL側で期待する値に整えておく方が扱いやすくなります。
動的なSQLを組み立てるときの設計
検索画面や管理画面では、条件によってSQLを動的に変えたいことがあります。
例えば、ステータス、登録日、キーワード、並び順などをユーザーが指定できるケースです。
このとき、安易に文字列連結を増やすと危険です。
WHERE句は配列で組み立てる
条件が任意の場合は、WHERE句の断片とバインド値を分けて管理すると安全です。
where_clauses = []
binds = {}
if params[:status].present?
where_clauses << "users.status = :status"
binds[:status] = params[:status]
end
if params[:from].present?
where_clauses << "users.created_at >= :from"
binds[:from] = Time.zone.parse(params[:from])
end
where_sql =
if where_clauses.any?
"WHERE #{where_clauses.join(' AND ')}"
else
""
end
sql = <<~SQL
SELECT users.*
FROM users
#{where_sql}
ORDER BY users.created_at DESC
SQL
users = User.find_by_sql([sql, binds])
ポイントは、値を直接SQL文字列に埋め込まないことです。
SQLの構造はアプリケーション側で制御し、値はバインドで渡します。
ORDER BYは許可リストで制御する
ORDER BYは特に注意が必要です。
カラム名や並び順は、通常のプレースホルダで値として渡しにくいため、ユーザー入力をそのまま入れると危険です。
悪い例です。
sql = "SELECT * FROM users ORDER BY #{params[:sort]}"
安全にするには、許可リストを使います。
allowed_sorts = {
"created_at_desc" => "users.created_at DESC",
"created_at_asc" => "users.created_at ASC",
"name_asc" => "users.name ASC"
}
order_sql = allowed_sorts[params[:sort]] || "users.created_at DESC"
sql = <<~SQL
SELECT users.*
FROM users
ORDER BY #{order_sql}
SQL
users = User.find_by_sql(sql)
ユーザーが指定できる値を、アプリ側で定義した安全なSQL断片に変換するのがポイントです。
find_by_sqlを使うときの注意点
find_by_sqlは便利ですが、使い方を誤ると保守性やパフォーマンスに問題が出ます。
SQLインジェクション対策は必須
最も重要なのはSQLインジェクション対策です。
次のルールは必ず守るべきです。
- ユーザー入力を文字列連結しない
- 値はプレースホルダで渡す
- ORDER BYやカラム名は許可リストで制御する
- LIKE検索ではワイルドカードの扱いに注意する
- SQLの構造を外部入力で自由に変えない
特に管理画面や検索画面では、パラメータをSQLに使う場面が多くなります。
「管理画面だから安全」と考えるのではなく、外部入力はすべて危険な可能性があるものとして扱うべきです。
DB依存が強くなる
find_by_sqlで書いたSQLは、DBにそのまま渡されます。
そのため、PostgreSQLでは動くがMySQLでは動かない、またはその逆が起きる可能性があります。
例えば、次のようなものはDBによって書き方が変わることがあります。
- 日付関数
- 文字列結合
- 型変換
- NULLの扱い
- ウィンドウ関数
- JSON関数
- 正規表現
- LIMIT/OFFSETの細かな挙動
Rails公式APIでも、find_by_sqlはDB非依存の変換が行われないため、DB固有のSQLに依存する可能性があると説明されています。
将来的にDBを変更する可能性があるプロジェクトでは、DB依存のSQLをどこまで許容するかをチームで決めておく必要があります。
スキーマ変更の影響を受けやすい
生SQLでは、テーブル名やカラム名を文字列として直接書くことが多くなります。
そのため、カラム名を変更したときに、Rubyのメソッド呼び出しよりも修正漏れが起きやすくなります。
SELECT users.full_name FROM users
もしfull_nameがdisplay_nameに変更された場合、このSQLも修正しなければなりません。
対策としては、次のような運用が有効です。
- SQLをモデルやQueryオブジェクトに集約する
- コントローラにSQLを直書きしない
- 複雑なSQLにはテストを用意する
- スキーマ変更時に関連SQLを検索する
- SQLログや実行結果をレビューする
N+1問題とプリロードの注意点
find_by_sqlの戻り値はActiveRecordオブジェクトですが、ActiveRecord::Relationではありません。
そのため、通常のように後からincludesをつなげることはできません。
users = User.find_by_sql(sql)
users.includes(:orders) # このようには使えない
この性質を理解していないと、関連データを参照したときにN+1が発生する可能性があります。
N+1が起きやすい例
users = User.find_by_sql("SELECT * FROM users")
users.each do |user|
puts user.orders.count
end
この場合、ユーザーごとにordersを取得するSQLが発行される可能性があります。
ユーザー数が100件あれば、追加で100回近いクエリが発行されることもあります。
必要な情報を最初のSQLで取得する
一覧表示に必要な情報が集計値だけであれば、最初のSQLで取得してしまう方法があります。
sql = <<~SQL
SELECT
users.*,
COUNT(orders.id) AS orders_count
FROM users
LEFT JOIN orders ON orders.user_id = users.id
GROUP BY users.id
SQL
users = User.find_by_sql(sql)
これなら、一覧で注文数を表示するだけであれば、追加クエリを避けやすくなります。
ID一覧を取得して別クエリでまとめて取得する
関連データそのものが必要な場合は、ID一覧を使って別クエリでまとめて取得する方法もあります。
users = User.find_by_sql(sql)
user_ids = users.map(&:id)
orders = Order.where(user_id: user_ids).group_by(&:user_id)
このようにすれば、ユーザーごとにクエリを発行するのではなく、まとめて関連データを取得できます。
find_by_sqlを使うときは、SQLそのものだけでなく、その後に画面や処理でどの関連データを使うのかまで設計することが重要です。
find_by_sqlとselect_all・execute・pluckの違い
Railsには、生SQLやSQLに近い処理を扱う方法が複数あります。
目的に応じて使い分けることが重要です。
find_by_sql:モデルとして扱いたいSELECTに向いている
find_by_sqlは、SELECT結果をモデルインスタンスとして扱いたい場合に向いています。
users = User.find_by_sql("SELECT * FROM users")
向いているケースは次のとおりです。
- 取得結果をモデルとして扱いたい
- モデルのメソッドを使いたい
- 一覧表示でモデル属性として参照したい
- 複雑なSELECTを生SQLで書きたい
一方で、集計結果だけを取得したい場合や、モデルとして扱う必要がない場合には過剰になることがあります。
select_all:モデル化しない軽量な取得に向いている
select_allは、SQLの結果をActiveRecord::Resultとして返します。
Railsガイドでは、select_allはfind_by_sqlと似ていますが、オブジェクトをインスタンス化せず、ActiveRecord::Resultを返すと説明されています。
result = ActiveRecord::Base.connection.select_all(<<~SQL)
SELECT status, COUNT(*) AS count
FROM users
GROUP BY status
SQL
result.to_a
# => [{"status"=>"active", "count"=>10}, ...]
向いているケースは次のとおりです。
- 集計結果だけ欲しい
- レポート用のデータを取得したい
- モデルのメソッドを使わない
- インスタンス化コストを抑えたい
「モデルの配列として扱う必要があるか?」を基準に、find_by_sqlとselect_allを使い分けるとよいでしょう。
execute:更新系SQLやDDLに使う
executeは、任意のSQLを実行するための低レベルなメソッドです。
ActiveRecord::Base.connection.execute(<<~SQL)
UPDATE users
SET status = 'inactive'
WHERE last_login_at < '2024-01-01'
SQL
ただし、更新系SQLを生で実行する場合は、特に慎重に扱う必要があります。
- トランザクションを使う
- 影響範囲を事前に確認する
- WHERE条件の漏れを防ぐ
- バックアップやロールバック方針を確認する
- 本番実行前にSELECTで対象件数を確認する
SELECT目的ならfind_by_sqlやselect_all、更新目的ならexecuteというように、目的で使い分けるのが基本です。
pluck:特定カラムだけを軽量に取得する
特定のカラムだけを配列で取得したい場合は、pluckが便利です。
user_ids = User.where(status: "active").pluck(:id)
Railsガイドでは、pluckはActiveRecordオブジェクトを構築せず、DBの結果をRuby配列に変換するため、大量データや頻繁に実行されるクエリでパフォーマンス上有利になる場合があると説明されています。
単純にIDや名前だけが欲しい場合は、find_by_sqlではなくpluckを検討しましょう。
find_by_sqlを保守しやすくする実務設計
find_by_sqlをプロジェクト内で安全に使うには、書き方だけでなく配置場所も重要です。
コントローラにSQLを直書きしない
避けたいのは、コントローラに長いSQLを書くことです。
class UsersController < ApplicationController
def index
sql = "SELECT ..."
@users = User.find_by_sql(sql)
end
end
これでは、コントローラが肥大化し、SQLの再利用やテストが難しくなります。
代わりに、モデルのクラスメソッドやQueryオブジェクトに切り出します。
class User < ApplicationRecord
def self.popular(min_orders:)
sql = <<~SQL
SELECT
users.*,
COUNT(orders.id) AS orders_count
FROM users
LEFT JOIN orders ON orders.user_id = users.id
GROUP BY users.id
HAVING COUNT(orders.id) >= ?
ORDER BY orders_count DESC
SQL
find_by_sql([sql, min_orders])
end
end
呼び出し側は次のように書けます。
@users = User.popular(min_orders: 5)
SQLの詳細を呼び出し側から隠し、メソッド名で意図を表現できます。
Queryオブジェクトに分離する
SQLがさらに複雑な場合は、Queryオブジェクトに分けるのも有効です。
class PopularUsersQuery
def initialize(min_orders:)
@min_orders = min_orders
end
def call
User.find_by_sql([sql, @min_orders])
end
private
def sql
<<~SQL
SELECT
users.*,
COUNT(orders.id) AS orders_count
FROM users
LEFT JOIN orders ON orders.user_id = users.id
GROUP BY users.id
HAVING COUNT(orders.id) >= ?
ORDER BY orders_count DESC
SQL
end
end
呼び出し側です。
@users = PopularUsersQuery.new(min_orders: 5).call
この形にすると、検索条件が増えても責務を分けやすくなります。
SQLには意図を残す
複雑なSQLでは、「なぜその書き方にしているのか」が後から分からなくなりがちです。
必要に応じて、SQLコメントやRubyコメントで意図を残しましょう。
sql = <<~SQL
SELECT
users.*,
-- 注文がないユーザーも一覧に出すためLEFT JOINを使う
COUNT(orders.id) AS orders_count
FROM users
LEFT JOIN orders ON orders.user_id = users.id
GROUP BY users.id
SQL
特に、パフォーマンス対策としてJOIN順や条件を書いている場合は、意図をコメントに残すと保守しやすくなります。
パフォーマンス確認で見るべきポイント
find_by_sqlは、SQLを直接書けるからこそ、パフォーマンスの責任も実装者に寄ります。
発行SQLをログで確認する
まずは、Railsログで実際にどのSQLが発行されているかを確認します。
開発環境では、コンソールやログにSQLが表示されます。
確認すべき点は次のとおりです。
- 想定したSQLになっているか
- WHERE条件が正しく入っているか
- バインド値が意図どおりか
- 不要なカラムを取得していないか
- 追加クエリが大量に発行されていないか
EXPLAINで実行計画を見る
複雑なSQLや重いSQLでは、DBのEXPLAINを使って実行計画を確認します。
見るべきポイントは次のとおりです。
- インデックスが使われているか
- フルスキャンになっていないか
- JOINの順序が妥当か
- 想定以上の行数を読み込んでいないか
- GROUP BYやORDER BYで重い処理が発生していないか
Railsガイドでも、ActiveRecordのRelationではexplainを使って実行計画を確認できることが説明されています。find_by_sqlでは直接Relationとして扱えないため、DBコンソールやSQLログを使って同等の確認を行うとよいでしょう。
必要なカラムだけを取得する
SELECT *は便利ですが、常に最適とは限りません。
一覧表示で必要なカラムが限られているなら、必要なカラムだけを取得することで、DBからアプリケーションへの転送量を減らせます。
sql = <<~SQL
SELECT
users.id,
users.name,
users.email,
users.created_at
FROM users
SQL
ただし、モデルとして扱う場合は、必要な属性が不足しないように注意します。
find_by_sqlでよくある失敗
失敗1:とりあえず生SQLにしてしまう
ActiveRecordで簡単に書ける処理までfind_by_sqlにしてしまうと、保守性が下がります。
例えば、次のような処理ならActiveRecordで十分です。
User.where(status: "active").order(created_at: :desc)
これをわざわざ次のように書く必要はありません。
User.find_by_sql("SELECT * FROM users WHERE status = 'active' ORDER BY created_at DESC")
単純な検索はActiveRecord、複雑な取得や集計はfind_by_sqlというように使い分けることが大切です。
失敗2:戻り値をRelationだと思ってしまう
find_by_sqlの戻り値は配列です。
そのため、次のようなチェーンはできません。
User.find_by_sql(sql).where(status: "active")
後から条件を追加したい場合は、SQLを組み立てる段階で条件を入れる必要があります。
失敗3:集計結果を通常モデルと同じように更新してしまう
集計列を含むfind_by_sqlの結果は、通常のモデルレコードのように見えます。
user.orders_count
しかし、orders_countはDBテーブルの実カラムではなく、SQLで作った派生列です。
このような結果をそのまま更新処理に使うと、意図しない挙動になる可能性があります。
集計結果は読み取り専用として扱うのが安全です。
失敗4:N+1を見落とす
find_by_sqlで取得した結果に対して関連をループ内で参照すると、N+1が起こりやすくなります。
一覧画面で関連データを表示する場合は、最初のSQLで取得するのか、別クエリでまとめて取得するのかを事前に決めておきましょう。
失敗5:SQLの置き場所がバラバラになる
コントローラ、モデル、サービス、ビューなどにSQLが散らばると、修正漏れが起きます。
find_by_sqlを使う場合は、SQLを置く場所をチームで決めることが重要です。
おすすめは次のいずれかです。
- モデルのクラスメソッド
- Queryオブジェクト
- Repository的なクラス
- レポート専用クラス
find_by_sqlを使う判断基準
最後に、実務で判断しやすいように、find_by_sqlを使うべきかどうかの基準を整理します。
find_by_sqlを使ってよいケース
次のような場合は、find_by_sqlを検討してよいでしょう。
- ActiveRecordで書くと極端に読みづらい
- 複雑なJOINや集計が必要
- DB固有の関数を使いたい
- 実行計画を意識してSQLを調整したい
- モデルインスタンスとして結果を扱いたい
- 読み取り専用の複雑な一覧を作りたい
find_by_sqlを避けた方がよいケース
次のような場合は、別の方法を検討しましょう。
- 単純なwhereやorderで書ける
- IDだけ取得したい
- 集計結果だけ欲しい
- 更新系SQLを実行したい
- DB非依存性を強く保ちたい
- チーム内にSQLレビュー体制がない
目的に応じた使い分けは次のイメージです。
| 目的 | 適した方法 |
|---|---|
| 通常の検索 | ActiveRecordのwhere/joins/order |
| モデルとして複雑なSELECT結果を扱う | find_by_sql |
| 集計結果やレポート用データを軽量に取得 | select_all |
| 特定カラムだけ取得 | pluck |
| 件数だけ取得 | count / count_by_sql |
| 更新系SQLを実行 | execute |
参考にしたい公式情報・外部情報
find_by_sqlやSQLインジェクション対策を正しく理解するには、公式情報や信頼できるセキュリティ資料を確認することが重要です。
Rails公式API:ActiveRecord::Querying
find_by_sqlの引数、戻り値、プレースホルダ、DB依存に関する注意点を確認できます。実務で使う前に一度確認しておきたい一次情報です。
Rails Guides:Active Record Query Interface
ActiveRecord全体の検索、集計、find_by_sql、select_all、pluckの使い分けを確認できます。Railsらしい書き方と、生SQLを使うべき場面の判断に役立ちます。
OWASP SQL Injection Prevention Cheat Sheet
SQLインジェクション対策の基本を確認できるセキュリティ資料です。生SQLを扱うエンジニアは、Railsに限らず理解しておくべき内容です。
まとめ:find_by_sqlは「便利な抜け道」ではなく「責任を持って使う設計手段」
find_by_sqlは、Railsで生SQLを直接実行し、結果をActiveRecordモデルの配列として受け取れる便利なメソッドです。
複雑なJOIN、集計、DB固有機能、パフォーマンスチューニングが必要な場面では、ActiveRecordのメソッドチェーンよりも意図が明確になることがあります。
一方で、find_by_sqlには次のような注意点があります。
- SQLインジェクション対策が必須
- DB依存が強くなる
- スキーマ変更の影響を受けやすい
- ActiveRecord::Relationではないため後からincludesできない
- N+1を生みやすい
- SQLの置き場所を誤ると保守性が下がる
実務では、まずActiveRecordで書けるかを検討し、複雑な取得や集計で生SQLの方が明確な場合にfind_by_sqlを使うのが安全です。
そして、使う場合は必ず次のポイントを押さえましょう。
- 値はプレースホルダで渡す
- ORDER BYなどは許可リストで制御する
- SQLをコントローラに直書きしない
- Queryオブジェクトやモデルメソッドに集約する
- N+1を含めて取得設計を考える
- SQLログと実行計画を確認する
- モデル化が不要なら
select_allやpluckも検討する
RailsはActiveRecordによって多くのSQLを抽象化してくれます。しかし、実務の現場では、抽象化の仕組みを理解したうえで、必要に応じてSQLを正しく書ける力も求められます。
find_by_sqlを安全に扱えるようになると、複雑なデータ取得やパフォーマンス改善に対応できる幅が広がります。
こうしたRails・DB・SQLの知識を深めながら、実務で価値ある設計や改善に取り組みたい方にとって、技術力を伸ばせる環境はとても重要です。私たちも、Webアプリケーション開発やデータ設計に関心を持ち、より良いサービスづくりに一緒に向き合える仲間を探しています。
学んだSQLを、実務で使えるスキルにしたい方へ
本記事ではSQLの基本的な考え方や使い方を解説しましたが、
「実務で使えるレベルまで身につけたい」と感じた方も多いのではないでしょうか。
SQLは、文法を理解するだけでなく、
- どんなデータ構造で使われるのか
- どんなクエリが現場で求められるのか
を意識して学ぶことで、実践力が大きく変わります。
そうした「実務を見据えたSQL学習」を進めたい方には、
完全無料で学べるプログラミングスクール ZeroCode PLUS という選択肢もあります。
- SQLを含むWeb・データベース系スキルを体系的に学べる
- 未経験者でも実務を意識したカリキュラム
- 受講料・教材費がかからない完全無料の学習環境
- 完全オンラインでスキマ学習
※学習内容や進め方を確認するだけでもOKです