パフォーマンス改善を触れる前に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の貼り方
は検索するといっぱい出てきます。今後時間があれば追記します🙇