MEDIA

メディア

  1. TOP
  2. メディア
  3. プログラミング
  4. Railsのfind_by_sqlとは?安全な使い方と生SQLの注意点

Railsのfind_by_sqlとは?安全な使い方と生SQLの注意点

CONTENTS

Railsで複雑なSQLを書きたいとき、「ActiveRecordのwhereやjoinsだけでは表現しづらい」「集計やJOINをもっと直接書きたい」と感じる場面があります。

そのようなときに選択肢になるのが、find_by_sqlです。

ただし、find_by_sqlは便利な一方で、生SQLをそのまま扱うため、SQLインジェクション、DB依存、N+1、保守性低下といったリスクもあります。この記事では、find_by_sqlの基本的な使い方から、JOIN・集計の実務例、安全な書き方、select_allexecuteとの使い分けまでを体系的に解説します。

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側でCOUNTSUMを使って集計した方が効率的なケースは多いです。

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])

ここで重要なのは、WHEREHAVINGの違いです。

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_namedisplay_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_allfind_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_sqlselect_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_sqlselect_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_sqlselect_allpluckの使い分けを確認できます。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_allpluckも検討する

RailsはActiveRecordによって多くのSQLを抽象化してくれます。しかし、実務の現場では、抽象化の仕組みを理解したうえで、必要に応じてSQLを正しく書ける力も求められます。

find_by_sqlを安全に扱えるようになると、複雑なデータ取得やパフォーマンス改善に対応できる幅が広がります。

こうしたRails・DB・SQLの知識を深めながら、実務で価値ある設計や改善に取り組みたい方にとって、技術力を伸ばせる環境はとても重要です。私たちも、Webアプリケーション開発やデータ設計に関心を持ち、より良いサービスづくりに一緒に向き合える仲間を探しています。

学んだSQLを、実務で使えるスキルにしたい方へ

本記事ではSQLの基本的な考え方や使い方を解説しましたが、
「実務で使えるレベルまで身につけたい」と感じた方も多いのではないでしょうか。

SQLは、文法を理解するだけでなく、

  • どんなデータ構造で使われるのか
  • どんなクエリが現場で求められるのか

を意識して学ぶことで、実践力が大きく変わります。

そうした「実務を見据えたSQL学習」を進めたい方には、
完全無料で学べるプログラミングスクール ZeroCode PLUS という選択肢もあります。

  • SQLを含むWeb・データベース系スキルを体系的に学べる
  • 未経験者でも実務を意識したカリキュラム
  • 受講料・教材費がかからない完全無料の学習環境
  • 完全オンラインでスキマ学習


ZeroCode PLUSについて詳しく見る

※学習内容や進め方を確認するだけでもOKです

Join us! 未経験からエンジニアに挑戦できる環境で自分の可能性を信じてみよう 採用ページを見る→

記事監修

ドライブライン編集部

[ この記事をシェアする ]

記事一覧へ戻る