🎃

パフォーマンスを低下させる!?初心者ありがち?pluckメソッドについて調べてみた。

2023/10/05に公開

はじめに

はじめまして! 私は現在プログラミングスクールでRailsを学習しています。
この度は、スクール内で開催されたアドベントカレンダーの企画に参加し、今回始めての技術記事に挑戦することに致しました。
このような貴重な機会を頂き、主催者さまには感謝御礼申し上げます。

概要

RUNTEQエンジニア転職チャンネルを見ていて気になったメソッドがあったので深掘りしてみようと思います!
動画はこちら
https://www.youtube.com/watch?v=NrBvUDe5i-g
動画で紹介されているサンプルコードはこちら
https://gist.github.com/hisaju/8785ae3812d96c380ea77dd9fa14d8a8
X
https://x.com/hisaju01/status/1697437719973261557?s=20

ここで紹介されているpluckメソッドですが、おや?とても既視感が…
気になって過去の自分のコードを調べてみると、やっぱり使っていました!

    @post = current_user.posts.find(params[:id])
    @tags = @post.tags.pluck(:name).join(',')

果たしてこのコードはアウトなの!?セーフなの!?改善の余地はある???
ということで、pluckに付いて調べてみることにしました!

目次

そもそもpluckってなに?

pluckはRailsのメソッドです。引数で指定したカラムのみをデータベースから直接取得します。特定のカラムのみデータベースから取り出すためメモリを節約できるメリットがあります。
一方で、メソッド実行時に毎回SQLが発行されるというデメリットがあります。

動画でも、パフォーマンスが悪くなる…と言われていますね。

実際に動画で紹介されているサンプルコードを使い、発行されるSQLを比べてみました。

books = Book.where(user_id: current_user.id, display_status: :displayed)

このコードが発行するSQLは

SELECT * FROM books WHERE user_id = [current_user.id] AND display_status = 'displayed';

そして問題の、

pages = Page.where(book_id: books.pluck(:id))

コチラのコードは、まずbooks.pluck(:id)によって以下のSQLを発行します。

SELECT id FROM books WHERE user_id = [current_user.id] AND display_status = 'displayed';

その結果を基に、次のSQLが発行されます。

SELECT * FROM pages WHERE book_id IN ([book_ids...]);

最初のコードは、まずbookモデルからuser_id フィールドが現在のユーザーのIDと一致する書籍を選択します。そしてその中から更に、状態がdisplayの状態のものを取り出しています。
二行目のコードは、book_id: books.pluck(:id)でbooks_idがbooks変数に格納されているIDと一致するページを取り出しています。

この方法だと、1行目のBook.whereでSQLが発行され、更に2行目のPage.whereで、更にpluckの部分で、合計3回のSQLを発行する書き方になってしまいます。

改善後のコードはどうでしょうか。

books = current_user.books.displayed
pages = Page.joins(:book).merge(books)
SELECT * FROM books WHERE user_id = [current_user.id] AND display_status = 'displayed';
SELECT pages.* FROM pages INNER JOIN books ON pages.book_id = books.id WHERE books.user_id = [current_user.id] AND books.display_status = 'displayed';

2行目のコードが変更され、発行するSQLは2つになっています。

よく見るとこのコードだとbooks変数はインスタンス変数になっていません。もしインスタンス変数になっていれば、@booksでログイン中のuserの保持するbooksでdisplay(公開中?)のものを一覧表示する。などで使用することが出来ます。
今回はインスタンス変数になっていないのでviewで使用する必要がなく、その場合は下記のように1行でSQLを発行する文に書き換えることで更にパフォーマンスの改善につなげることが出来ます。

pages = Page.joins(:book).where(user_id: current_user.id, status: :displayed)

pluckに変わる手段

pluckについて調べていると、mapメソッドで同じようにSQLを発行する方法が紹介されています。
mapはRubyのメソッドです。ブロック内の処理を実行したレシーバを配列として返します。mapはレシーバをメモリ に読み込むためメモリを浪費するデメリットがあります。

どちらにもメリットとデメリットが有るため、使い分けの仕方としては

  • 特定のカラムのみ利用する場合はメモリ節約の観点からpluckを利用したほうがよい。
  • インスタンス化されたActive Recordモデルから特定のカラムを取り出す場合はmapを利用したほうがよい。

ということになります。

自分の書いたコードはどうだろう?

以上を踏まえた上で、問題の私のコードを再度確認します。

    @post = current_user.posts.find(params[:id])
    @tags = @post.tags.pluck(:name).join(',')

上記のコードで発行されるSQLはコチラです。

SELECT * FROM posts WHERE posts.user_id = [current_user.id] AND posts.id = [params[:id]] LIMIT 1;
SELECT name FROM tags WHERE tags.post_id = [@post.id];

これを、mapを使用すると次のように変更できます。

@post = current_user.posts.includes(:tags).find(params[:id])
@tags = @post.tags.map(&:name).join(',')

このコードから発行されるSQLはコチラです。

SELECT * FROM posts WHERE posts.user_id = [current_user.id] AND posts.id = [params[:id]] LIMIT 1;
SELECT tags.* FROM tags WHERE tags.post_id IN ([@post.id]);

私の書いたコードは、1行目でログイン中のユーザーの投稿したPostの中から特定のIDに対応する投稿を取得し、リレーションされているtags情報を合わせて@postに格納しています。
2行目では、@postに関連付けられた全てのタグにアクセスし、それをpluck(:name)で名前の配列にしています。

どちらのコードもSQLを発行していますが、@postも一覧を表示する為にviewで使用しているためまとめることは出来ません。
仮にmapメソッドに書き換えた場合、配列に変更するための処理が必要になるのと、メモリを消費してしまうのとでかえってパフォーマンスを低下させてしまう事がわかりました。

つまり、今回の私の書いたコードはセーフ!改善は必要ありませんでした。

まとめ

pluckを使うことで全てのパターンでパフォーマンスを低下させるわけではありません。むしろpluckを使うことでのメリットもたくさんあります。どんな状況で使用するかの判断が重要になります。

Rubyにはたくさんのメソッドがあり、似たような動きをするものもたくさんあります。その中でも自分がどうしてこのコードを使って書いたのか?を明確にすることはとても重要なことだと思います。
まだGPTに頼りコードを書いていますが、今後も少しずつメソッドの意味や使い方を深掘りし、ちゃんと理解してコードを書けるようになりたいです。

まだまだ知識が浅くておかしい部分もたくさんあるかと思います。有識者の方は是非おかしなところを見つけ、ご指摘をお願いいたします。

最後までお読み頂きありがとうございました。

Discussion