データの取り出し方に喝!
こんにちは!ラブグラフでインターンをしているりょうさんです!
エンジニア初心者の私も、「結果的に出力が同じだから大丈夫だろう」と考えてしまいがちです。しかし、Railsを触れる中でそのような考えは危険であるということを改めて痛感しました。今回は、その経験から得た知見を備忘録的に残しておきたいと思います。
前提
例として、以下のmodelとDBのテーブルがあるとして話を進めたいと思います。
class User < ApplicationRecord
end
create :users do |t|
t.string :name
t.string :email
t.integer :user_id
end
まあそこまで深く関わってくる部分ではないのでここではあくまで例として捉えてください。
そしてこのusersテーブルには以下のようなデータが入っているとします。
| name | 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 | 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.lastかUser.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がないので、どんな順番で結果が返ってくるかわからない。
- 全ての
まとめ
結果的に同じデータが得られたとしても、その内部処理には大きな違いがあります。特に、大規模なデータを扱うアプリケーションでは、どのメソッドを使うかを慎重に選択することが重要だと感じました。「結果的に出力が同じだから大丈夫」という考えは、自分のような初心者がよくやりがちなミスです。これからは、常に内部処理とパフォーマンスを意識した開発をしていきたいと思います。
Discussion