学習環境
Ruby on Rails 6.0.3
PostgreSQL 16.1
課題内容と要件
Ruby on RailsでActiveRecordを使ったテーブル結合の勉強をしている時のこと…
以下のような要件があったと想定した問題がありました。
【要件】注文されていないすべての料理を返すこと
※left_outer_joins
を使うこと
答えとして考えたこと
Railsガイドをパラパラと見ながら、
なるほど、”注文されていない”だから、注文されたデータ(order_foodテーブルのorder_id)を見て、where.not
を使うのかなと思ったのでした。
ただし、要件にはleft_outer_joins
を使うこととある。
答えとそれぞれの処理
left_outer_joins
を使った答え
@foods = Food.left_outer_joins(:order_foods).where(order_foods: {food_id: nil })
where.not
でもよくない?と思ったやつ
@foods = Food.where.not(id: OrderFood.all.pluck(:food_id))
left_outer_joins
を使った場合のRailsコンソールを使って確認した処理
[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コンソールを使って確認した処理
[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も2回動かしている点で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文はむしろ複雑になっており、処理の時間もかかっていたことがわかりました。