LoginSignup
60

More than 5 years have passed since last update.

使えるRailsのSQLとパフォーマンス改善

Last updated at Posted at 2017-07-11

パフォーマンス改善を触れる前にSQLのクエリを知っていた方がいいということで、ここでは基本的かつよく使うと思ったクエリーについてSQL編でまとめてみました。そのあとSQL実行計画のコストをRails Consoleで確認する方法をパフォーマンス編で紹介します。

この記事はruby2.3.1、Rails4.2.6の元に書いています。

SQL編

1. これのSQLを知りたいとき to_sql

Railsで User.where(id: [3, 5, 9])かけるのに、SQLがわからない人におすすめです。
まずは、Rails Consoleを立ち上げる。
例として、User.where.not(auth_name: "facebook")のSQLを知りたいときに
to_sqlメソッドを使います。

main>> User.where.not(auth_name: "facebook").to_sql
=> "SELECT \"users\".* FROM \"users\" WHERE (\"users\".\"auth_name\" != 'facebook')"

以下の項目でto_sqlしてみるとSQLの理解に繋がると思います。

2. "車"を持っている"ユーザー"を探す

内部結合で探すことができる。以下のようにモデル間にアソシエーション持っているとします。

class User < ActiveRecord::Base
 has_many :cars
end

class Car < ActiveRecord::Base
 belongs_to :user
end

joinsを用いることで、Carを持っているUserが全部出てきます。しかし、Userが2台の車を所持していると、2回表示されます。
したがって、distinctを使うことでUserをユニークに表示できます。

User.joins(:cars)
main>> User.joins(:cars).distinct.to_sql
=> "SELECT DISTINCT \"users\".* FROM \"users\" INNER JOIN \"cars\" ON \"cars\".\"user_id\" = \"users\".\"id\"

3. ローンが終わっていない車(人)を探す

User has_many Loan, Car has_one Loanのような関係を持っているとします。
ローンは

Loan(id: integer, car_id: integer, user_id: integer, started_at: datetime, ended_at: datetime, total_price: integer)

のようなテーブルとします。

User.joins(:loans).where("loans.ended_at <= ?", Date.today)
Car.joins(:loan).where("loans.ended_at <= ?", Date.today)

4. ローンの開始日と車の発売日が同じときのユーザーを探す

Car(released_at: datetime)を持つとします。

User.joins(:loans, :cars).where("loans.started_at = cars.released_at")

5. Scopeを使う

例えば上記の3番ローンが終わっていない車(人)を探すについてscopeをあてると、以下になります。

class User < ActiveRecord::Base
  scope :in_loan, -> { joins(:loans).where("loans.ended_at <= ?", Date.today) }
end

使うときはシンプルにUser.in_loanになります。引数を渡すこともできます。

class Car < ActiveRecord::Base
  scope :made_in_countries, ->(*countries) do
    where(country: countries)
  end
end

日本製とドイツ製の車はCar.made_in_countries(["Japan", "Germany"])
適切な名前のscopeを作ることでわかりやすいコードになります

パフォーマンス編

1. ベンチマークをとる

以下のようにRailsプロジェクトで様々な場所でベンチマークを取ることができます。特定のviewのパーシャルのレダリンク時間など、測定できるようになっています。以下はユーザーにメールと通知の送信をする際の例です。

benchmark("Send Mail and Notification to User in Loan") do
  User.includes(:preference).joins(:loans).where("loans.ended_at <= ?", Date.today).each do |user|
    InLoanMailer.notify_payment_mail(user).deliver
    InLoanNotification.send_notify_payment(user) if user.preference.recieve_notification
  end
end

Send Mail and Notification to User in Loan (97212ms)

2. SQLのコストを知る

SQL実行時間のコストはもちろん大きいほどよくないです。New RelicやDynatraceから遅いトランザクションの中のSQLを確認できます。
以下のクエリに時間が掛かったとします。(変数としておくと便利です)

slow_query = "SELECT \"users\".* FROM \"users\" INNER JOIN \"facebook_profiles\" ON \"facebook_profiles\".\"user_id\" = \"users\".\"id\" WHERE (facebook_profiles.gender = 'male')  ORDER BY \"users\".\"created_at\" ASC"

これのコストを見積もるにはexplainを使います。explainすることでクエリの実行計画が出てきます。Rails Consoleで

main>> ActiveRecord::Base.connection.explain(slow_query)
=> 
                                      QUERY PLAN
--------------------------------------------------------------------------------------
 Sort  (cost=8.68..8.69 rows=1 width=305)
   Sort Key: users.created_at
   ->  Hash Join  (cost=1.18..8.67 rows=1 width=305)
         Hash Cond: (users.id = facebook_profiles.user_id)
         ->  Seq Scan on users  (cost=0.00..7.08 rows=108 width=305)
         ->  Hash  (cost=1.16..1.16 rows=1 width=4)
               ->  Seq Scan on facebook_profiles  (cost=0.00..1.16 rows=1 width=4)
                     Filter: ((gender)::text = 'male'::text)
(8 rows)

(👆の例は特に遅い例ではありません)

このexplainは以下のように使えます。

main>> User.joins(:cars).distinct.explain

3. 実際に改善する

上記のexplainでコストのかかる箇所を直すためには、以下のことを確認するといいです。

  • N+1問題
  • 正しくindex張っているか
  • クエリを書き直せるか
  • クエリを利用しているロジックは良いのか
  • キャッシュを使えばいいのか
  • 非同期処理がいいのではないか
  • などなど

詳しいことは書いていないがN+1問題正しいindexの貼り方は検索するといっぱい出てきます。今後時間があれば追記します🙇

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
60