Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rails: ActiveRecordのスコープで`present?`を使うとパフォーマンスが落ちることがある(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

Rails: ActiveRecordのスコープでpresent?を使うとパフォーマンスが落ちることがある(翻訳)

RailsではあらゆるRubyオブジェクトで#present?メソッドが利用できるので、このメソッドは多用される傾向があります。たとえば、ビューで配列をループで回す前に配列に要素があるかどうかをチェックしてからデータを表示するなどです。

しかし、#present?をスコープで使うと、思わぬパフォーマンス低下が生じることがあります。

次のように書くのではなく

Active Relation(Active Recordクラスのスコープ)で#present?を使う。

books = Book.recently_released
if books.present?
  books.paperbacks.each { |book| puts book.title }
end

次のように書く

#any?#exists?を使う。

books = Book.recently_released
if books.any?
  books.paperbacks.each { |book| puts book.title }
end

そうする理由

Active Relationで#present?を呼び出すと、そのスコープのActive Recordオブジェクトがすべてメモリ上の配列に読み込まれ、それから(おそらく非常に巨大な)配列が空かどうかがチェックされます。

多くの場合これで問題はありませんが、極端なケース(モデルが複雑な場合やデータセットが巨大な場合)ではメモリ使用量が大きく跳ね上がってパフォーマンスが低下するかもしれません。この現象は、それらのオブジェクトが使われるかどうかにかかわらず発生します。

#any?を使えば、配列を一切ビルドしない別のもっとシンプルなSQLコマンドが実行されます。一般に、SQL呼び出しを追加するときのパフォーマンス低下は、全オブジェクトをメモリに読み込む場合と比べればたかが知れています。

そのリレーションが既に利用されている(つまりオブジェクトがメモリに読み込まれている)のであれば、#present?を実行してもペナルティは受けません。この場合#present?は新しい配列を読み込むのではなく、既存の配列を使います。

アプリでのデータベースの使い方が変わるとなれば、きっと皆さんも自分たちのアプリで現実のパフォーマンスにどう影響するかを監視したいと思うでしょう。その場合、#any?で発行される余分なSQLリクエストによるパフォーマンス低下が、データに#present?を使ったときのパフォーマンス低下を下回らないことを確認する必要があります。

Railsでattendance gemを用いれば、Active Relationsの#present?で常に#any?を使うようデフォルトの振る舞いを変更するモンキーパッチを当てることも可能です。attendanceは少々そっけないツールですが、このgemでまとめて解決した場合の自分たちのアプリのパフォーマンスをじっくり監視したくなることでしょう。

そうしない理由があるとすれば

#any?を用いる場合、SQLクエリを余分に発行することになるので、パフォーマンスが必ずしも改善するとは限りません。レコードを事前に読み込む方が効率よくできるのであれば、そうしましょう。

Railsで、attendance gemと同様の修正(Active Relationの#present?の動作を変更する)である#10539がいったんマージされましたが、その後2b76313で取り消され、既存の振る舞いが採用されました。このため、この振る舞いについては各自が注意して#any?を明示的に使わなければなりません。

ここから、このパフォーマンスチューニングは非常に微妙であることがわかります。皆さんのアプリで必ずしも効果を上げるとは限らないでしょう。

関連記事

Rails: present?より便利なActiveSupportのpresenceメソッド(翻訳)

Rails: pluckでメモリを大幅に節約する(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。