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

パフォーマンス改善を触れる前に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`メソッドを使います。

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

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

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

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

```ruby
class User < ActiveRecord::Base
 has_many :cars
end

class Car < ActiveRecord::Base
 belongs_to :user
end
```

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

```ruby
User.joins(:cars)
```

```ruby
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)
```

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


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

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

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

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

## 5. Scopeを使う

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

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

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

```ruby
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のパーシャルのレダリンク時間など、測定できるようになっています。以下はユーザーにメールと通知の送信をする際の例です。

```ruby
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を確認できます。
以下のクエリに時間が掛かったとします。（変数としておくと便利です）

```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で

```ruby
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`は以下のように使えます。

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

## 3. 実際に改善する

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

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

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