【Rails入門】パフォーマンスが悪いならincludes/orderを検討しよう

みなさんは、2つの関連付けられたテーブルを扱っているときに、どのように値を取り出していますか?

パフォーマンスを気にせず、alljoinを使って、N+1問題を発生させていませんか?

それともパフォーマンスを気にして、includespreloadeager_loadを使っていますか?

この記事では、パフォーマンスを上げるための1つの解法であるincludesを取り上げ、以下の内容を説明しています。

・N+1問題とは
・2つのテーブルからデータを取得する方法
・ネストされた複数のテーブルからincludesでデータを一括取得する方法

さらに、テーブルからデータを並べ替えた状態で取得するためのorderを取り上げ、以下の内容を説明します。

・データを並べ替える方法
・複数のカラムを使って並べ替える方法
・reorderの使いどころ
・order_as_specified gemを使って順番を1つ1つ指定してデータを取得する方法

データが少ないうちはパフォーマンスに問題を感じなくても、データが多くなったときのことを考えて、今のうちにN+1問題を解決しておきましょう!

また、データを取得してからRailsで並べ替えるよりも、データを並べ替えた状態で取得したほうがパフォーマンス的にも有利です。

この記事で、orderの使い方もあわせてマスターしてしまいましょう。

では、順番に説明していきますね。

includesとは、N+1問題とは

includesは、関連付けられた2つのテーブルのデータを参照するメソッドです。

主にN+1問題を解決するために利用されます。

N+1問題については、具体例を見ると解りやすいと思いますので、早速見ていきましょう。

以下のような2つのテーブルを作成します。

テーブルカラム
Userテーブルname
posts(has_many)
Postテーブルuser_id(belongs_to)
title
month

そして、Userテーブルから、Postテーブルのtitleを参照する方法を考えます。

一番手っ取り早いのは以下のようなコードですね。

しかし、これでは、データ数が多くなったときにパフォーマンスに問題が発生してしまいます。

実行結果:

実行結果の見かたを説明しておきます。

データベースのUserテーブルを読み込んだタイミングで「User Load (0.1ms)…」と表示されます。

同様にPostテーブルを読み込んだときに「Post Load (0.1ms)…」と表示されるのですが、これが6回表示されていますね。

つまり、6名分のpost.titleを取得するために、データベースに6回アクセスしているワケですが、includesを使うとこれを1回に削減できます。

1回のアクセスで解決できるものを、6回もアクセスしているので問題とされていて、一般的にN+1問題が発生していると言います。

具体的には、以下のコードでPostテーブルにアクセスする回数を1回に減らせます。

実行結果:

あとで試してもらいますので、ここでは「Post Load」がすごく減っているという雰囲気だけをつかんでください。

includesの動作を理解するためのWebアプリを作成する

Railsの動作を理解するために、scaffoldを使ってWebアプリを作っておきましょう。

scaffoldの使い方は、以下の記事で解説していますので、ぜひご覧ください。

【Rails入門】初めてのWebアプリ開発ならscaffoldが最短!
更新日 : 2018年8月21日

この記事では、app/samurai/includes-demoディレクトリを作成し、以下のコマンドでWebアプリを作成した場合を例に、説明を続けます。

データの準備

Railsコンソールを起動してテーブルにデータを入力します。

scaffoldで作成していますので、http://localhost:3000/users/newや、http://localhost:3000/posts/newで1つずつデータを入力しても構いません。

(1)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。

(2)以下のコードを入力します。

UserテーブルとPostテーブルにデータが入力され、Railsコンソールが終了します。

ブラウザでデータを確認しておきましょう。

(3)ブラウザで「http://localhost:3000/users」にアクセスします。

rails-include01

(4)ブラウザで「http://localhost:3000/posts」にアクセスします。

rails-include02

うまく登録されていますね!

次にUserテーブルとPostテーブルを関連付けます。

(5)app/models/user.rbを以下のように編集します。

変更前:

変更後:

(6)app/models/post.rbを以下のように編集します。

変更前:

変更後:

これで、準備ができました。

hirb gemとhirb-unicode gemをインストールする

この後、includesの動作を確認する際、Railsコンソールを使用します。

Railsコンソールで出力したデータの見栄えを良くにするために、以下の2つのgemをインストールしましょう。

gem説明
hirb gem出力結果を表形式で出力する
hirb-unicode gemマルチバイト文字の表示を補正する

(1)Gemfileの最終行に以下の内容を追記します。

(2)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。

これで、hirb gemhirb-unicode gemがインストールされました。

関連付けられた2つのテーブルのデータを参照する方法

関連付けられた2つのテーブル(上で紹介したUserテーブルとPostテーブル)で、すべてのpostの投稿者を取得する方法を考えてみましょう。

ここからは、Railsコンソールでコードを入力して、実行結果を確認してみましょう。

(1)以下のコマンドを1行ずつ順番に入力します。

(2)すべてのpostのデータを確認するために、以下のコードを入力します。

実行結果:

(3)すべてのuserのデータを確認するために、以下のコードを入力します。

手順(2)で表示されたuser_idを使って、手順(3)で表示されたUserテーブルから該当ユーザーを特定して、nameを取得すればokですね。

では、それを実現する3つの方法を紹介しましょう。

Post.all

Post.allですべてのpostを取得できますので、以下のコードを実行すればよさそうです。

実行結果:

実行結果で★が表示されている行をご覧ください。

1番のpostの投稿者が細川修二であることが取得できています。

ただ、データベースには全部で9回アクセスしていますので、N+1問題が発生しています。

joins

次はjoinsを使う方法です。

試してみると、Post.allとほとんど同じですが、joinsは内部結合(INNER JOIN)を行っています。

実行結果:

この方法でも、やはりN+1問題が発生していますね。

includes

3つ目は、includesを使う方法です。

includesは、条件によって2つのテーブルをまとめる方法が変わるという特徴があります。

詳細は省きますが、以下の2つのパターンがあります。

  • 2つのテーブルを左外部結合(LEFT OUTER JOIN)して、そのテーブルをメモリに読み込む
  • 2つのテーブルをそのままメモリに読み込む

どちらのパターンで動作したとしても、関連付けられた2つのテーブルのデータを参照できますので、いったん試してみるのが良いでしょう。

コード実行時に発行されたSQLに、「LEFT OUTER JOIN」と表示されていれば左外部結合をしており、表示されていなければ2つのテーブルをそのままメモリに読み込んでいます。

実行結果:

前述の2つとはだいぶ違うことに気がついたでしょうか。

1番のpostの投稿者が細川修二さんであることが取得できています。

が、データベースには全部で2回しかアクセスしておらず、見事にN+1問題を回避していますね!

左外部結合(LEFT OUTER JOIN)とは

左外部結合(LEFT OUTER JOIN)は、1つ目のテーブルのカラムを優先して2つ目のテーブルを結合する、その際、2つのテーブルのどちらかにしか存在しないデータも含めるという意味です。

左外部結合(LEFT OUTER JOIN)以外に、以下のような結合方法があります。

  • 右外部結合(RIGHT OUTER JOIN)
  • 完全外部結合(FULL OUTER JOIN)
  • 内部結合(INNER JOIN)

ちなみに、joinsは内部結合(INNER JOIN)を行うようです。

結合方法について詳しく説明すると一つの記事になってしまいますので、以下の記事を参考にしてください。

参考:Quiita SQL素人でも分かるテーブル結合(inner joinとouter join)

その他の方法

関連付けられた2つのテーブルのデータを参照する方法は、includesを含めると以下の4つが有名です。

  • joins
  • includes
  • preload
  • eager_load

このうちjoinsはN+1問題が発生しますが、それ以外はN+1問題が発生しません。

includesは、preloadとeager_loadを状況に応じて自動的に使い分けるメソッドになっています。

トラブルを避けるためには、preloadとeager_loadの動作を理解して、しっかり使い分ける方が良いでしょう。

includesに条件をつける(where)

includeswhereを同時に使用すると、SQLの発行回数を少なく抑えつつ、条件をつけられます。

Postテーブルからデータを取得するときに、month=9のpostだけを対象に、ユーザー名を取得しましょう。

(1)Railsコンソールで次のコードを入力してください。

先ほどのincludesを使ったコードと比較すると、途中に「.where(posts: {month: 9})」が追加されています。

実行結果は以下のとおりです。

このコードの場合は、以下のように動作します。

①Postテーブルから(すべてのpostを取得するのではなく)month=9のpostだけを取得し、取得したpostのuser_id(今回は4, 3)を保持(キャッシュ)する

②Userテーブルから、id = 4, 3のデータだけを取得する。

③取得したuserデータを参照して、post.user.nameを表示する。

whereを追加して条件を付けても、SQLを発行するのは①と②の2回だけです。

(2)続けて、次のコードを入力してください。

name=”立川裕美”のusersだけを対象に、ユーザー名を取得します。

実行結果は以下のとおりです。

先ほどのSQL文とはまったく異なりますが、期待するデータが取得できていますね。

このコードの場合は、以下のように動作しています。

①UserテーブルのidとPostテーブルのuser_idを対応させる方法で、PostテーブルとUserテーブルを左外部結合(LEFT OUTER JOIN)する。

②左外部結合したテーブルから、users.name = “立川裕美”のデータだけを取得する。

③最後に、取得したデータを参照して、post.user.nameを表示する。

①と②をまとめて、1回のSQLで済ませていますね。

ネストされた複数のテーブルからincludesでデータを一括取得する方法

最後に、ネストされた複数のテーブルを扱ったときにN+1問題が発生しない方法を紹介しましょう。

(1)混乱を避けるために、すでに起動しているRailsサーバーを終了します。

次に、scaffoldで作成したデータをすべて削除しましょう。

(2)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。

(3)以下のコマンドを1行ずつ順番に入力します。

(4)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。

(5)以下のコードを入力します。

UserテーブルとPostテーブルにデータが入力され、Railsコンソールが終了します。

次にTeamテーブルとUserテーブル、Postテーブルを関連付けます。

(4)app/models/team.rbを以下のように編集します。

変更前:

変更後:

(5)app/models/user.rbを以下のように編集します。

変更前:

変更後:

(6)app/models/post.rbを以下のように編集します。

変更前:

変更後:

これで、データの準備ができました。

(7)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。

(8)以下のコードを入力します。

実行結果:

見事な多段N+1問題とでも言えるほど、SQLが発行されていますね。

(9)次は以下のコードを入力します。

実行結果:

このコードなら多段N+1問題を回避できますね!

データを並べ替える(order)

ここまでで、効率良くデータを取り出す方法を説明しました。

この記事の最後では、ここまで説明してきたincludesを使って効率良くデータを取り出し、これから説明するorderを使ってデータを並べ替えるという合わせ技を紹介します。

その準備段階として、orderの使い方を説明しておきましょう。

まずは、基本的な方法から始めます。

(1)Railsコンソールで以下のコードを入力します。

実行結果:

monthが昇順(asc:ascending order)に並んでいますね。

次は降順(desc:descending order)に並べてみましょう。

(2)Railsコンソールで以下のコードを入力します。

実行結果:

ちなみに、Post.order(month: :desc)は、以下のどの書きかたでも同じ結果が得られます。

複数のカラムを使って並べ替える

次は複数のカラムを条件にする方法です。

先ほどのPostテーブルを、user_id順に、user_idが同じ場合はmonth順に並べ替えてみましょう。

まずは、user_id順に並べ替えるコードからですが、これは上で説明したコードのmonthをuser_idに変えるだけです。

(1)Railsコンソールで以下のコードを入力します。

実行結果:

user_idが3のデータのmonthに注目すると、12, 8, 9の順に並んでいますね。

これをmonth順に並べてみましょう。

(2)Railsコンソールで以下のコードを入力します。

実行結果:

monthを降順にしてみましょう。

(3)Railsコンソールで以下のコードを入力します。

実行結果:

ちなみに、Post.order(:user_id, month: :desc)は、以下のどの書きかたでも同じ結果が得られます。

設定済みのorderを上書きする(reorder)

orderの書きかたを探しているとreorderの説明も見つかると思います。

ただ、ほとんどのサンプルコードでorderの直後にreorderしていて、使いどころがあるの!?という気がします。

でも、ちゃんと使いどころがあるんです。

ちなみに、reorderはdeprecated(廃止予定、非推奨)になった時期もありましたが、現在はdeprecatedではありませんので、堂々と使いましょう。

(1)Railsコンソールを終了します。

(2)app/models/user.rbを以下のように編集します。

変更前:

変更後:

この変更で、User.find(3).postsのようにアクセスしたときに、postsをmonth順に取得できるようになります。

(3)以下のコマンドを1行ずつ順番に入力します。

実行結果:

手順(2)で指定したとおり、month順に取得できていますね。

では、monthを逆順に並べ替えてみましょう。

まずは、ここまでと変わらずorderを使ってみます。

(4)Railsコンソールで以下のコードを入力します。

実行結果:

SQLも変わっていますが、残念ながら逆順になりませんでした。

ここでreorderを使ってみましょう。

(5)Railsコンソールで以下のコードを入力します。

実行結果:

SQL文も変わり、意図したとおり逆順になっていますね!

このように、あらかじめ順番が指定されているデータを、別の方法で並べ替えるときはreorderを使う必要があるのです。

順番を1つ1つ指定してデータを取得する(order_as_specified gem)

order_as_specified gemを使って、順番を1つ1つ指定してデータを取得する方法を紹介しましょう。

参考:https://github.com/panorama-ed/order_as_specified

order_as_specified gemをインストールする

順番を1つ1つ指定してデータを取得するために、order_as_specified gemをインストールしましょう。

(1)Gemfileの最終行に以下の内容を追記します。

(2)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。

これで、order_as_specified gemがインストールされました。

順番を1つ1つ指定してデータを取得する

では、順番を指定してデータを取得してみましょう。

ここでは、Postテーブルから、user_idが「1、3、5、2、4」の順番になるようにデータを取得してみます。

(1)Railsコンソールを終了します。

(2)app/models/post.rbを以下のように編集します。

変更前:

変更後:

(3)以下のコマンドを1行ずつ順番に入力します。

実行結果:

user_idに注目すると、「1、3、5、2、4」の順番でデータを取得できていることがわかります。

これをさらに、month順に並べてみましょう。

(5)Railsコンソールで以下のコードを入力します。

実行結果:

素晴らしいですね!

includesの結果を並べ替える(includes、order)

ここまでで、includesとorderの使い方を別々に確認しましたので、最後に、includesとorderを同時に使うケースを紹介します。

Postテーブルのmonthの値を使って、結果を並び替えてみましょう。

(1)Railsコンソールで以下のコードを入力します。

先ほど試したname=”立川裕美”のusersだけを対象にしたコードに「.order(“posts.month asc”)」を追加しました。

orderには、並び替えに使うカラム(今回の例では、posts.month)と、並び替える順番(asc:昇順、desc:降順)を指定します。

ascをdescに変更すれば、逆順になります。

まとめ

この記事では、パフォーマンスに影響が出るN+1問題を、includesを使って解決する方法を紹介しました。

今回はincludesを説明しましたが、同様のメソッド(joinspreloadeager_load)もありますので、しっかり理解して、適材適所でデータを取り出しましょう。

これを機にそれぞれの違いを勉強してみてはいかがでしょうか。

さらに、データを並べ替えた状態で取得するorder/reorderの使い方、order_as_specified gemの使い方も説明しました。

もしincludesやorderの使い方を忘れてしまったら、この記事でもう一度確認してくださいね!

LINEで送る
Pocket

最短でエンジニアを目指すなら侍エンジニア塾

cta_under_bnr

侍エンジニア塾は業界で初めてマンツーマンレッスンを始めたプログラミングスクールです。これまでの指導実績は16,000名を超え、未経験から数多くのエンジニアを輩出しています。

あなたの目的に合わせてカリキュラムを作成し、現役エンジニア講師が専属であなたの学習をサポートするため効率よく学習を進めることができますよ。

無理な勧誘などは一切ありません。まずは無料体験レッスンを受講ください。

無料体験レッスンの詳細はこちら

書いた人

侍テック編集部

侍テック編集部

おすすめコンテンツ

あなたにぴったりなプログラミング学習プランを無料で診断!

プログラミング学習の効率を劇的に上げる学習メソッドを解説