😡

データの取り出し方に喝!

2024/09/24に公開

こんにちは、現在学生でラブグラフにて開発インターンをしている浅井です。

エンジニア初心者の私も、「結果的に出力が同じだから大丈夫だろう」と考えてしまいがちです。しかし、Railsを触れる中でそのような考えは危険であるということを改めて痛感しました。今回は、その経験から得た知見を備忘録的に残しておきたいと思います。

前提

例として、以下のmodelとDBのテーブルがあるとして話を進めたいと思います。

class User < ApplicationRecord
end
create :users do |t|
    t.string :name
    t.string :email
    t.integer :user_id
end

まあそこまで深く関わってくる部分ではないのでここではあくまで例として捉えてください。
そしてこのusersテーブルには以下のようなデータが入っているとします。

name email user_id
Suzuki aaa@a.com 100
Tanaka bbb@b.com 101
Yamamoto ccc@c.com 102
Nakamura ddd@d.com 103
Yoshikawa eee@e.com 104

そして、メール送信メソッドがあるとします。

class UserMailer < ApplicationMailer
    def test_email(user_id)
        @user = User.find(user_id)
        mail(to: @user.email, subject: "Hello, World")
    end
end
補足

ご存知の方も多いと思いますが、ActiveRecordには以下の特性があります。(Railsガイドより引用)

デフォルトでは id という名前のintegerカラムがテーブルの主キーに使われます(PostgreSQLやMySQLではbigint、SQLiteではinteger)。Active Recordマイグレーションでテーブルを作成すると、これらのカラムが自動的に作成されます。[1]

つまりは、定義したDBにおいて、idカラムも存在しており、実際の中身としては以下のようになっています。また、タイムスタンプカラムも自動追加されるので、結果的にusersテーブルは以下のようになります。今回、タイムスタンプは一旦無視します。

id name email user_id created_at updated_at
1 Suzuki aaa@a.com 100
2 Tanaka bbb@b.com 101
3 Yamamoto ccc@c.com 102
4 Nakamura ddd@d.com 103
5 Yoshikawa eee@e.com 104

本題

ここから本題です。私は、最新のnameを取得してメール送信させる動作確認をしたかったので、Rails console上で、以下のコマンドを実行しました。

UserMailer.test_email(User.ids.last).deliver_later

しかし、実は、同様の処理を行う似たようなコードがあります。

UserMailer.test_email(User.last.id).deliver_later

違う部分は引数が、User.ids.lastUser.last.idということのみです。
どちらのコードも同じ結果、つまり、上記の最新のユーザーであるYoshikawaさんにメールが送信されることになります。一見「どちらを使っても問題ないだろう」と思われがちですが、実際には内部処理が大きく異なります。

User.lastの内部処理

まず初めに、便宜上、後者のUser.lastについてから解説します。
User.lastは、RailsのActiveRecordのメソッドであり、DBに対して以下のSQLクエリを発行します。

SELECT * FROM users ORDER BY id DESC LIMIT 1;

これにより、usersテーブルの中でも最もidが大きい最新のレコードがDBから1件だけ取得されます。つまり、User.last.idは、効率よく最新のidを取得できます。

User.ids.lastの内部処理

一方、RailsではUser.idsを実行すると、DBに対して以下のSQLクエリを発行します。

SELECT id FROM users;

このクエリにより、DBから全てのレコードのidが取得され、それらのIDが配列としてメモリ上に保存されます。
つまり、内部的には

[1, 2, 3, 4, 5]

を取得したことになっており、lastで最後の要素である5を取得しているという処理になります。

結論

  • User.last.id
    • DBから1件だけを取得しているため、非常に効率的であり、データ量が多くても処理速度の影響を受けにくい通常ならこちらを選択するべき
  • User.ids.last
    • 全てのidを配列として取得するため、レコード数が少ない場合には問題がないが、レコード数が多い場合にはメモリ消費が大きくなるケースがあり、パフォーマンスが低下するので通常なら避けるべき。また、ORDER BYがないので、どんな順番で結果が返ってくるかわからない

まとめ

結果的に同じデータが得られたとしても、その内部処理には大きな違いがあります。特に、大規模なデータを扱うアプリケーションでは、どのメソッドを使うかを慎重に選択することが重要だと感じました。「結果的に出力が同じだから大丈夫」という考えは、自分のような初心者がよくやりがちなミスです。これからは、常に内部処理とパフォーマンスを意識した開発をしていきたいと思います。

脚注
  1. Railsガイド v7.2 ↩︎

ラブグラフのエンジニアブログ

Discussion