テーブル結合したほうが良い理由(Active Record)

## 学習環境
Ruby on Rails 6.0.3
PostgreSQL 16.1

## 課題内容と要件

Ruby on RailsでActiveRecordを使ったテーブル結合の勉強をしている時のこと…
以下のような要件があったと想定した問題がありました。

【要件】注文されていないすべての料理を返すこと
　※`left_outer_joins`を使うこと

ER図は以下のようなイメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190651/d1758d7d-2e19-297e-191d-345282aae7f6.png)

## 答えとして考えたこと
Railsガイドをパラパラと見ながら、
なるほど、”注文されていない”だから、注文されたデータ(order_foodテーブルのorder_id)を見て、`where.not`を使うのかなと思ったのでした。
ただし、要件には`left_outer_joins`を使うこととある。
## 答えとそれぞれの処理
`left_outer_joins`を使った答え

```ruby
@foods = Food.left_outer_joins(:order_foods).where(order_foods: {food_id: nil })
```

`where.not`でもよくない？と思ったやつ

```ruby
@foods = Food.where.not(id: OrderFood.all.pluck(:food_id))
```

`left_outer_joins`を使った場合のRailsコンソールを使って確認した処理

```bash
[12] pry(main)> @foods = Food.left_outer_joins(:order_foods).where(order_foods: {food_id: nil })
Food Load (1.4ms)  SELECT "foods".* FROM "foods" LEFT OUTER JOIN "order_foods" ON "order_foods"."food_id" = "foods"."id" WHERE "order_foods"."food_id" IS NULL
=> [#<ChineseFood:0x000055c0fcdfad78 id: 497, shop_id: 87, name: "アメリカビーバーの洋菓子", created_at: Fri, 09 Feb 2024 03:27:24 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:24 UTC +00:00, price: 1090, type: "ChineseFood">,
#<JapaneseFood:0x000055c0fcdfac10 id: 209, shop_id: 40, name: "ホンドフクロウのカフェ", created_at: Fri, 09 Feb 2024 03:27:18 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:18 UTC +00:00, price: 1590, type: "JapaneseFood">,
#<ItalianFood:0x000055c0fcdfaad0 id: 437, shop_id: 74, name: "マンドリルのふぐ", created_at: Fri, 09 Feb 2024 03:27:23 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:23 UTC +00:00, price: 1690, type: "ItalianFood">,
#<ChineseFood:0x000055c0fcdfa968 id: 518, shop_id: 90, name: "シロサイのおでん", created_at: Fri, 09 Feb 2024 03:27:25 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:25 UTC +00:00, price: 490, type: "ChineseFood">,
#<ChineseFood:0x000055c0fcdfa850 id: 549, shop_id: 94, name: "オウサマペンギンのカフェ", created_at: Fri, 09 Feb 2024 03:27:25 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:25 UTC +00:00, price: 590, type: "ChineseFood">,
#<ItalianFood:0x000055c0fcdfa6e8 id: 268, shop_id: 49, name: "ショウジョウトキのうどん", created_at: Fri, 09 Feb 2024 03:27:19 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:19 UTC +00:00, price: 1490, type: "ItalianFood">,
#<ChineseFood:0x000055c0fcdfa5a8 id: 397, shop_id: 67, name: "フンボルトペンギンのファミレス", created_at: Fri, 09 Feb 2024 03:27:22 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:22 UTC +00:00, price: 890, type: "ChineseFood">,
#<JapaneseFood:0x000055c0fcdfa468 id: 359, shop_id: 63, name: "アカカンガルーのハンバーグ", created_at: Fri, 09 Feb 2024 03:27:21 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:21 UTC +00:00, price: 1690, type: "JapaneseFood">,
#<ItalianFood:0x000055c0fcdfa300 id: 292, shop_id: 53, name: "モモイロペリカンのステーキ", created_at: Fri, 09 Feb 2024 03:27:20 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:20 UTC +00:00, price: 1190, type: "ItalianFood">,
#<ItalianFood:0x000055c0fcdfa1c0 id: 335, shop_id: 58, name: "マンドリルのそば", created_at: Fri, 09 Feb 2024 03:27:20 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:20 UTC +00:00, price: 990, type: "ItalianFood">,
#<ItalianFood:0x000055c0fcdfa0a8 id: 555, shop_id: 95, name: "アフリカタテガミヤマアラシのステーキ", created_at: Fri, 09 Feb 2024 03:27:25 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:25 UTC +00:00, price: 1590, type: "ItalianFood">,
#<ItalianFood:0x000055c0fcdf9f40 id: 541, shop_id: 93, name: "コモンリスザルのたこ焼き", created_at: Fri, 09 Feb 2024 03:27:25 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:25 UTC +00:00, price: 590, type: "ItalianFood">,
//省略
[13] pry(main)> @foods.size
=> 126
```

`where.not`を使った場合のRailsコンソールを使って確認した処理

```bash
[9] pry(main)> @foods = Food.where.not(id: OrderFood.all.pluck(:food_id))
(0.7ms)  SELECT "order_foods"."food_id" FROM "order_foods"
Food Load (4.0ms)  SELECT "foods".* FROM "foods" WHERE "foods"."id" NOT IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, //省略)
[["id", 1], ["id", 1], ["id", 1], ["id", 1], ["id", 1], ["id", 1] //省略]
=> [#<JapaneseFood:0x000055c0fc2e11d0 id: 209, shop_id: 40, name: "ホンドフクロウのカフェ", created_at: Fri, 09 Feb 2024 03:27:18 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:18 UTC +00:00, price: 1590, type: "JapaneseFood">,
#<JapaneseFood:0x000055c0fcbcbc50 id: 216, shop_id: 41, name: "アルダブラゾウガメのしゃぶしゃぶ", created_at: Fri, 09 Feb 2024 03:27:18 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:18 UTC +00:00, price: 1790, type: "JapaneseFood">,
#<ChineseFood:0x000055c0fcbaa398 id: 220, shop_id: 41, name: "マントヒヒのカレー", created_at: Fri, 09 Feb 2024 03:27:18 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:18 UTC +00:00, price: 1990, type: "ChineseFood">,
#<ChineseFood:0x000055c0fcdca9c0 id: 240, shop_id: 47, name: "アルパカの串揚げ", created_at: Fri, 09 Feb 2024 03:27:19 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:19 UTC +00:00, price: 590, type: "ChineseFood">,
#<ItalianFood:0x000055c0fcdbbc90 id: 244, shop_id: 47, name: "ミーアキャットのうどん", created_at: Fri, 09 Feb 2024 03:27:19 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:19 UTC +00:00, price: 1790, type: "ItalianFood">,
#<ItalianFood:0x00007fcdf80276c8 id: 247, shop_id: 47, name: "ジェフロイクモザルのかに", created_at: Fri, 09 Feb 2024 03:27:19 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:19 UTC +00:00, price: 1290, type: "ItalianFood">,
#<ItalianFood:0x00007fcdf8027498 id: 249, shop_id: 47, name: "ライオンの和菓子", created_at: Fri, 09 Feb 2024 03:27:19 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:19 UTC +00:00, price: 1290, type: "ItalianFood">,
#<ItalianFood:0x00007fcdf8026b60 id: 258, shop_id: 48, name: "シシオザルのすき焼き", created_at: Fri, 09 Feb 2024 03:27:19 UTC +00:00, updated_at: Fri, 09 Feb 2024 03:27:19 UTC +00:00, price: 490, type: "ItalianFood">,
//省略
[8] pry(main)> @foods.size
=> 126
```

結果として”注文されていないすべての料理を返すこと”についてはどちらも正解だが、
処理の時間、SQLも２回動かしている点で`left_outer_joins`を使うのが正解なのだとわかりました。

## それぞれ何をしているかを文章にして考えてみる

`left_outer_joins`を使った場合、

1.foodsテーブルとorider_foodsテーブルを左外部結合をする。

2.order_foodsテーブルにあるfood_idカラムの値が`nil`だった場合は返す。

`where.not`を使った場合、

1.order_foodsテーブルを全ての中から、food_idカラムのレコードの配列を取得する。

2.foodsテーブルのidカラムと比べて一致しないものを返す。

一見同じに見えるが、SQL文としては前者は1つで済むが後者は2つになってしまう。
また、一致しないものを返す処理というのは全ての情報を取得する必要もあるため、DBの処理としては重たくなってしまうため、テーブル結合をした方が処理も軽い。

## まとめ
コードだけ見ると`where.not`のほうが短いので良のではと思ってしまいましたが、SQL文はむしろ複雑になっており、処理の時間もかかっていたことがわかりました。