Railsでパフォーマンスを低下させないために気をつけること

## 概要

Railsを開発する上でパフォーマンスを低下させないためのTipsをメモ

## Tips

### 実行時間の大きい処理はJobで非同期実行する

リクエスト内で複数な処理を実行してしまうと、レスポンスが遅くなりユーザビリティの低下につながります
リクエスト内で実行する必要がないものは、ActiveJob等で非同期に実効するか、バッチ処理で定期的に処理するように
することでユーザビリティの向上が図れます

### 条件式判定よりもSQLで絞る

取得してきたデータを1件ずつ条件反映して処理をするよりは、
取得するデータそのものをSQLで絞る方がはるかにパフォーマンスが良くなります
ループの中で毎回条件式で反映してしまうと、ループの中で毎回SQLが走ってしまうこともあるので、
パフォーマンスが要求される場合はSQLで絞るか、SQLで絞れる設計にすることをおすすめします

### `Array` よりも `ActiveRecord::Relation`

`Array` オブジェクトはデータを全てメモリ上にもった上で処理を実効するのに対し、
`ActiveRecord::Relation` オブジェクトはSQLの構造だけをメモリ上に持ち、
処理する直前に遅延評価を実行し、値をメモリ上に展開します
`ActiveRecord::Relation` を使える場合は、そちらを使う方がパフォーマンスが良くなります

### `in_batches`

`ActiveRecord::Relation` オブジェクトを分割して取得したい場合、 Rails5からは `in_batches` メソッドが使えます(※Rails5以前では `find_in_batches` が使えます)
`in_batches` はオプションで `of` を受けることができ、分割で取得するレコード数を指定できます(デフォルトは1000件です)
`Array` で渡してしまうとパフォーマンスが低下してしまう場合は `in_batches` を使って処理することをおすすめします

```ruby
Log.in_batches do |logs|
  # ActiveRecord:: Relationオブジェクトをblockで受け取れる
end
```

[Source Code on Github](https://github.com/rails/rails/blob/406d3b190044e21b4efc45c80d2e171595615cae/activerecord/lib/active_record/relation/batches.rb#L194)

### `each` よりも `find_each` 

大量のデータを処理する際、DBから取得してきたデータを `each` のループで回してしまうと、
ループを回す直前にデータを全てメモリ上に展開してしまいます
取得するデータ量が多い場合は、 `find_each` を使ってDBからデータを分割して取得するようにし、
メモリ上に展開されるデータ数を絞る方がパフォーマンスが良い場合があります
ちなみに、 `find_each`は `in_batches` で取得したデータをeachで返しています

※ 追記
find_each では、取得結果がprimary_keyで一度whereもしくはpluckされるため、order等でソートしていた場合は
ソートの順序が無視されてしまうため注意が必要です
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/batches.rb#L223

× データ数が多いと一度に全てをメモリ上に展開してしまうためパフォーマンスが悪い

```ruby
Log.all.each do |log|
  # 処理を実行
end
```

◯ DBから分割して取得してくれるので、必要とするメモリの量を抑えながら実行できる

```ruby
Log.find_each do |log|
  # 処理を実行
end
```

[Source Code on Github](https://github.com/rails/rails/blob/2a7cf24cb7aab28f483a6772b608e2868a9030ba/activerecord/lib/active_record/relation/batches.rb#L48)

### CSVの取込は分割して取得する

大量のデータをCSVから一度に取得してしまうと、メモリに大量のデータが展開されてしまい、
パフォーマンスが低下してしまいます
この場合、 `foreach` メソッドを使って分割して取得することでパフォーマンスの低下を防ぐことができます

```ruby
CSV.foreach(filename, headers: true, skip_blanks: true).with_index do |row, index|
  # CSVを1行ずつ処理する
end
```

下記例では、`each_slice` と組み合わせて1000件ずつCSVデータを取得し処理を実行します

```ruby
CSV.foreach(filename, headers: true, skip_blanks: true).each_slice(1000).with_index do |rows, index|
  # CSVを1000件ずつまとめて処理する
end
```

### バッチ処理では不要になった変数に明示的に `nil` を設定する

HTTPのリクエストで完結する処理に関しては、リクエスト終了後に展開された変数は全てメモリから開放されますが、
バッチ処理等で展開した変数を何度も使いまわした場合、変数がGCされる対象にならずメモリに残り続けてしまう場合があります
この場合、ループ処理の最後で不要になった変数に明示的に `nil` を設定することで
使用するメモリ量を抑えることができ、パフォーマンスの改善が図れる場合があります

### 大量にデータを挿入する場合はBULK INSERTをする

[activerecord-import](https://github.com/zdennis/activerecord-import)を使うことで、DBへのINSERTを1回のSQLで実行できます
大量のデータの取込処理がある場合は導入を検討してみることをおすすめします
下記例では、CSVから1000件ずつデータを取得し、取得した1000件のデータを1回のSQLでINSERTしています

```ruby
fields = [:title, :body, :created_at, :updated_at]
CSV.foreach('blog.csv', headers: true, skip_blanks: true).each_slice(1000).with_index do |rows, index|
  values = []
  rows.each do |row|
    values << [row['title'], row['body'], row['created_at'], row['updated_at']]
  end
  Blog.import fields, values, validate: false, timestamps: false, on_duplicate_key_update: fields
  # @note 使用済みの変数に明示的にnilを設定してメモリから開放する
  values = nil
  rows = nil
end
```

### `destroy_all` vs `delete_all`

`destroy_all` で削除を実行した場合、レコードを1件ずつ `each` でまわし、 1件ずつ `destroy` を実行するため、
レコードごとにバリデーションとコールバックが実行されます
レコード数が多い場合はDBが詰まってしまう可能性があるため、
バリデーションもコールバックも不要な場合は、 `delete_all` を使い、SQLから一括削除する方が良い場合があります
※ 外部参照制約に要注意

`destroy_all` と `delete_all` の違いについては下記リンクでもまとめたので、参照ください

[destroy_allとdelete_all](https://qiita.com/nikadon/items/eb24664ec0d9bf6a3749)

### Cardinalityが大きいカラムに対する絞込やソートにはINDEXを貼ろう

Railsアプリケーションのパフォーマンスが悪い場合の、よくある例がINDEXの貼り忘れです
SlowQueryを検出してくれるツールを導入し、該当するテーブルにINDEXを貼ることでパフォーマンスが劇的に改善します
ツールの例として、ヘルスチェックも合わせて実行してくれる[NewRelic](https://newrelic.com/)を紹介しておきます

また、複数のカラムを組み合わせて絞込やソートを実行している場合は複合INDEXを貼ることでパフォーマンスが変わることがあります
SQLで `explain` を実行し、SQLの実行計画を調査してみると良いです
`ActiveRecord::Relation` オブジェクトにも `explain` メソッドがあるので、 rails console から確認することも可能です

[Source Code on Github](https://github.com/rails/rails/blob/8b69e32412cc2867b5fdd9a33cf4e4e759057e95/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L178)

## 参考

- [activerecord-import](https://github.com/zdennis/activerecord-import)
- [Ruby 製バッチ処理を省メモリ化した](http://blog.inouetakuya.info/entry/2015/07/26/204320)
- [NewRelic](https://newrelic.com/)
- [destroy_allとdelete_all](https://qiita.com/nikadon/items/eb24664ec0d9bf6a3749)
- [そのクエリ、ちゃんとチューニングされてる？Railsで確認する方法](https://qiita.com/yn-misaki/items/109bc81a225d04abec76)