DBパフォーマンスを意識したRailsの書き方まとめ

# DBパフォーマンスを意識したRailsの書き方まとめ

Railsではデータセットが巨大化したときのデフォルトのパフォーマンスがかなり低下するケースがあります。

そんなときに気をつけた方が良いポイントを記載します。

## 1. N+1
N+1は簡単に言うと必要以上に SQL が走るせいでパフォーマンスが低下する問題です。

### 悪い例

``` ruby: tweets_controller.rb
class UsersController < ApplicationController
  def index
    @tweets = Tweets.all
  end
end
```

``` ruby: index.html.erb
<% @tweets.each do |tweet| %>
  <%= tweet.user.name %> # ここでN+1が起きている
<% end %> 
```
この書き方だと、tweetsの数だけSQLが発行されるのでパフォーマンスが落ちます。

### 良い例

``` ruby: tweets_controller.rb
class TweetsController < ApplicationController
  def index
    @tweets = Tweets.includes(:user)
  end
end
```
`includes`を利用して事前にクエリキャッシュをすることで、N+1解決できます。

参考：
https://qiita.com/hirotakasasaki/items/e0be0b3fd7b0eb350327
https://qiita.com/massaaaaan/items/4eb770f20e636f7a1361

## 2. countではなくてsizeを使用する
Active Recordのメソッドで、データの件数を取得するためには`count`,`length`, `size`の３つのメソッドがあります。

この挙動の違いを理解せずに利用するとパフォーマンスに影響が生まれます。


### 悪い例

以下のコードはRailsビューで非常によく見かけます。

``` ruby
irb(main):001:0> users = User.all #usersテーブルのレコードを全件取得

irb(main):002:0> users.count #取得したレコードの件数を取得
   (0.6ms)  SELECT COUNT(*) FROM `users`
=> 3

irb(main):003:0> users.count #取得したレコードの件数を再度取得
   (1.9ms)  SELECT COUNT(*) FROM `users`
=> 3
```
`count`を実行すると毎回データベースに対してクエリを投げていられます。

テーブルの全行数が多ければ多いほど、パフォーマンス影響が大きくなります。

### 良い例

``` ruby
irb(main):001:0> users = User.all

irb(main):002:0> users.length #取得したレコードの件数を取得
  User Load (0.3ms)  SELECT `users`.* FROM `users`
=> 3

irb(main):003:0> users.length #取得したレコードの件数を再度取得
=> 3
```

`length`を利用すると、初回のlength実行時にデータをメモリに保存しているので、２回目からクエリが発行されません・

参考：https://himakuro.com/rails-count-length-size-guide

## 3.安易にallで取得した結果をループ処理しない

`all`では取得全件をメモリに展開するという特徴があります。

取得件数が少なければそこまで問題になりませんが、取得件数が大きい場合はメモリ圧迫されてしまいメモリエラーになるケースがあるので注意が必要です。

### 悪い例

``` ruby
User.all.each do |user|
  # 何かuserのオブジェクトを使用して処理をするコード
end
```
仮にUserデータが10,000件取得された場合、10,000件が全てメモリ解放されてしまいメモリ消費が激しくなります。

### 良い例

``` ruby
User.find_in_batches(batch_size: 1000) do |users|
  # 何かuserのオブジェクトを使用して処理をするコード
end
```
もし仮にUserデータが10,000件取得されたとしても、find_in_batchesを利用すれば1000ずつ処理されます。

つまり、10,000 / 1000 = 10回の処理に分けるイメージです。

ちなみに、大量にデータを挿入する場合はBULK INSERTをすると


参考：https://qiita.com/Marusoccer/items/af75b387cdb17eef91c6

## 4.不必要なActive Recordオブジェクトの生成
Active Recordオブジェクトは膨大な数のモジュールやメソッドをラップしているので、無駄な作成は避けるべきです。

### 悪い例

``` ruby
user_names = User.all.map(&:name)
```
Userのオブジェクトを生成して、そこから`ActiveRecord::AttributeMethods`関連のメソッドを呼び出しています。

### 良い例

``` ruby
user_names = User.pluck(:name)
```

`pluckメソッド`を使うとオブジェクトの生成を行わずにテーブルの値を取得することができます。

`pluckメソッド`はActiveRecordインスタンスの配列ではなく指定されたカラムの取得値配列を返すメソッドです。

## 5. メモ化する
メモ化とはメソッドが最初に呼び出されたときに戻り値がキャッシュされ、それ以降、同じスコープ内でメソッドが呼び出されるたびにキャッシュされた値が返されることを意味します。

### 悪い例

``` ruby
 def user
    @user = User.find(params[:id])
 end
```
毎度、UserテーブルにアクセスしているのでDB負荷が高い

### 良い例

``` ruby
 def user
    @user ||= User.find(params[:id])
 end
```
`@user`がnillの場合 User.find(params[:id])が`@user`に代入されます。
しかし、二回目同じメソッドを呼び出した時にUser.find(params[:id])この部分の処理が走らないので余計なDBアクセスを省略することができます。（一回目処理が走った時に、`@user`にキャッシュされているので）

## ６. 一括更新するときは一括更新にすべき
バルクインサートでは1回のトランザクションでインサートすることができるため、トランザクションを発行して何回もインサート処理をループさせるよりもコンテキストスイッチの回数が減るため実行速度が速いです。

一括更新するときはできる限りバルクインサート処理に寄せる形で実装する方がベターです。

### 悪い例
``` ruby
class Entry < ApplicationRecord
end

now = Time.current

#　この書き方だと毎回insert処理クエリが走るのでパフォーマンスがあまり良くない。
Entry.create(title: 'foo1', body: 'bar1', created_at: now, updated_at: now)
Entry.create(title: 'foo2', body: 'bar3', created_at: now, updated_at: now)

```


### 良い例
DBアクセスが少なく済むようにこのように一つのINSERT文にまとめらる方がベターです。

``` ruby
class Entry < ApplicationRecord
end

now = Time.current
Entry.insert_all([
  { title: 'foo1', body: 'bar1', created_at: now, updated_at: now },
  { title: 'foo2', body: 'bar3', created_at: now, updated_at: now },
])

#　内部的にはBulkInsert処理になる
#=> Entry Bulk Insert (7.3ms)  INSERT INTO "entries"("title","body","created_at","updated_at") VALUES ('foo1','bar1','2020-01-25 17:22:50.533182', '2020-01-25 17:22:50.533182'),('foo2','bar2','2020-01-25 17:22:50.533182', '2020-01-25 17:22:50.533182')
```


## 7. 重い処理は非同期にする
コードの書き方ではないですが、重い処理は同期処理ではなく非同期処理にすることを検討しましょう。

高負荷処理がかかる実装はバックグランド処理に移動させることで、リクエストタイムアウトエラーやメモリエラーを事前に防ぐことができます。

## 8. 大量レコードを扱う時はメモリ圧迫とオブジェクト作成を抑えるよう意識する

大量レコードをメモリに乗せすぎない、オブジェクト作成ではなくHashをできる限り利用するように意識するの大事。

## 9. 大きいテーブルをJoinしない
レコード数が多いテーブルをJoinは処理が重くなるのでできる限り避けましょう。


## 10.大量データのCSVダウンロード処理はタイムアウトなども考慮する

大量データのCSVダウンロード処理はタイムアウトも考慮しましょう。
考慮しないとタイムアウトエラーになるケースがあります。

# パフォーマンスが悪くなっている場所を見つけよう
パフォーマンスを意識して書いたとしても、人間なので抜け漏れはあります。

ですので、パフォーマンス検知ライブラリを導入して、パフォーマンスが落ちている場所に気づけるような仕組みを作っておきましょう。

## rack-mini-profilerを導入

[rack-mini-profiler](https://github.com/MiniProfiler/rack-mini-profiler)は手軽にRuby on Railsのパフォーマンス計測ができるgemです。

HTMLレンダリングした時のパフォーマンスを確認したいときに利用するのがオススメ

## bulletを導入

[bullet](https://github.com/flyerhzm/bullet)はN+1問題をアラート表示してくれるgemです。N+1は気をつけていても発生してしまうケースがあるので事前に導入しておくことをオススメします。

## New Relicを導入

アクション実行時にどの処理にどれだけ時間が掛かったかをメトリクス収集してくれる
https://newrelic.com/jp/ruby/rails

## Benchmarkモジュールを利用したベンチマークの実行

Ruby標準のBenchmarkモジュールを利用したベンチマークの実行をしてみてパフォーマンスチェックするのも良い方法です。

実際の利用方法は[こちら](https://nishinatoshiharu.com/usage-benchmark/)が参考になります。


# まとめ
他にも気をつけた方が良いポイントなどがあれば、ご指摘いただければ嬉しいです！