Rails6 のちょい足しな新機能を試す86（optimizer_hints 編）

# はじめに

Rails 6 に追加された新機能を試す第86段。 今回は、 `optimizer_hints` 編です。
Rails 6 では、ActiveRecord で `optimizer_hints` により、 OPTIMIZER HINTS が使えるようになりました。

Ruby 2.6.4, Rails 6.0.0 で確認しました。
また、データベースは MySQL 8.0.17 を使っています。

```shell
$ rails --version
Rails 6.0.0
```

今回は、 `name` の属性を持つUserモデルを作り、scope を使い、 `rails console` から試してみます。

# プロジェクトを作る

```shell
rails new rails_sandbox
cd rails_sandbox
```

# User モデルを作る

`name` の属性を持つ User モデルを作ります。

```shell
bin/rails g model User name
```

# `name` にインデックスを追加する

`name` にユニークインデックスをつけます。

`optimizer_hints` の機能を確認するためです。

```ruby:db/migrate/20190915055510_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    ...
    add_index :users, :name
  end
end
```

# seed データを作る

あらかじめデータを登録しておくため、seed データを作ります。

```ruby:db/seeds.rb
now = Time.now

users = 10_000.times.each.map do |i|
  { name: "name#{i}", created_at: now, updated_at: now }
end

User.insert_all(users)
```

# User モデルに scope を定義する

かなり手抜きですが、 `optimizer_hints` の動作を確認する目的ですので、そこは割り切ります。
また、ここで定義している `no_range_optimizer` は、 MySQL 専用で、汎用性がありません。

```ruby:app/models/user.rb
class User < ApplicationRecord
  scope :name1, -> { where("name like 'name1%'") }

  # for MySQL only
  scope :no_optimizer, -> { optimizer_hints("NO_RANGE_OPTIMIZATION(#{table_name})") }
end
```

# マイグレーションを行い seed データを登録する

```shell
$ bin/rails db:create db:migrate db:seed
```

# rails console で確認する

まずは、 `optimizer_hints` を使わない場合です。 `explain` メソッドを使って実行計画を確認します。

```irb
irb(main):001:0> User.name1.explain
  ...
  User Load (2.4ms)  SELECT `users`.* FROM `users` WHERE (name like 'name1%')
=> EXPLAIN for: SELECT `users`.* FROM `users` WHERE (name like 'name1%')
+----+-------------+-------+------------+-------+---------------------+---------------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type  | possible_keys       | key                 | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-------+------------+-------+---------------------+---------------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | users | NULL       | range | index_users_on_name | index_users_on_name | 1023    | NULL | 1111 |    100.0 | Using index condition |
+----+-------------+-------+------------+-------+---------------------+---------------------+---------+------+------+----------+-----------------------+
1 row in set (0.00 sec)
```

key に `index_users_on_name` を用いた範囲検索 (type=range) になることがわかります。

では、今度は、 `optimizer_hints` を使ってみます。

```irb
irb(main):002:0> User.name1.no_optimizer.explain
  User Load (8.4ms)  SELECT /*+ NO_RANGE_OPTIMIZATION(users) */ `users`.* FROM `users` WHERE (name like 'name1%')
=> EXPLAIN for: SELECT /*+ NO_RANGE_OPTIMIZATION(users) */ `users`.* FROM `users` WHERE (name like 'name1%')
+----+-------------+-------+------------+------+---------------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys       | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | users | NULL       | ALL  | index_users_on_name | NULL | NULL    | NULL | 9884 |    11.11 | Using where |
+----+-------------+-------+------------+------+---------------------+------+---------+------+------+----------+-------------+
1 row in set (0.00 sec)
```

key を使わない (key=NULL) フルテーブルスキャン (type=ALL) になっていることがわかります。
(通常、検索処理が遅くなる。)

# 本来は...

本来は、SQLを高速に実行できるように、 OPTIMIZER HINTS を使うので、今回の使い方は本末転倒な気もしますが、適切な例を思いつけませんでした。一応、実際に OPTIMIZER HINTS が使えることが確認できたので、これで良しとします。

# 試したソース

試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try086_optimizer_hint

# 参考情報

* [What is new in Rails 6.0](https://bogdanvlviv.com/posts/ruby/rails/what-is-new-in-rails-6_0.html)
* [Support Optimizer Hints](https://github.com/rails/rails/pull/35615)
* [MySQL EXPLAINのそれぞれの項目についての覚書](https://qiita.com/kzbandai/items/ea02727f4bb539fcedb5)