新卒研修でN＋1問題について学んだこと

# はじめに
今回は私が新卒研修で学んだN+1問題について学んだことをアウトプットしていきます。
この記事の対象者としては

- N+1問題について知らない方
- Laravel初心者の方

を想定しております。修正点やアドバイスなどございましたら、ご意見ください！

# N+1問題とは
SQLのクエリを発行して情報を集めていく必要がありますが、その際、余分にたくさんのクエリを発行してしまい、少なからず表示速度等に悪影響を出してしまうことを指します。親レコードを1つ取得し、その後、各子レコードを個別に取得するため、合計でN+1回のクエリが実行されます。ここでNは子レコードの数を指します。

# 具体例
具体的な例を挙げてみましょう。仮に、あるECサイトの注文履歴を表示するというケースを考えます。この場合、以下の3つのテーブルが関与します

- orders テーブル：各注文に関する情報を保持
- items テーブル：販売商品の情報を保持
- order_items テーブル：各注文がどの商品を含んでいるかを記録（ordersとitemsの中間テーブル）

```text:仮ECサイトのテーブル図
  +-----------------+     +-------------+     +-------------+
  |      Orders     |     | Order_items |     |   Items     |
  |-----------------|     |-------------|     |-------------|
  |       id        |-----|  order_id   |     |     id      |
  |                 |     |  item_id    |-----|    name　　　　  |
  |                 |     |   　　　　　　　　      |     |             |
  +-----------------+     +-------------+     +-------------+

```
まずは、N＋1問題を含んだコードを書いていきます。

```php:test1_N+1問題を含む
public static function test1()
{
    $orders = Order::all();
    Log::info('====test1====');
    foreach ($orders as $order) {
        $items = $order->orderItems()->select('item_id')->get();
        foreach ($items as $item) {
            $item = Item::select('name')->where('id', $item->item_id)->first();
            Log::info($item->name);
        }
    }
}
```
このコードは、全ての注文とその注文に関連する全てのアイテムを取得し、各アイテムの名前をログに出力するものです。1つの注文とその注文に含まれる全てのアイテムを取得するために、注文の数に応じた多数のクエリが実行されます。

```sql:test1_発行されたクエリー
===test1===
総クエリ時間: 8.81秒 + 1.99秒 + 114.13秒 + 23.81秒 + 14.34秒 = 163.08秒
クエリ数: 26 (1のクエリ数 + 9のクエリ数 + 9のクエリ数 + 6のクエリ数 + 1のクエリ数 = 26)

===test1===
[2023-05-31 16:50:43] local.INFO: Query Time:14.34s] select * from `items` where 0 = 1  
[2023-05-31 16:50:54] local.INFO: Query Time:8.81s] select * from `users` where `id` = ? limit 1  
[2023-05-31 16:50:54] local.INFO: Query Time:1.99s] select * from `orders`  
[2023-05-31 16:50:54] local.INFO: Query Time:3.91s] select `item_id` from `order_items` where `order_items`.`order_id` = ? and `order_items`.`order_id` is not null  
[2023-05-31 16:50:54] local.INFO: Query Time:2.02s] select `name` from `items` where `id` = ? limit 1  
[2023-05-31 16:50:54] local.INFO: Query Time:1.8s] select `item_id` from `order_items` where `order_items`.`order_id` = ? and `order_items`.`order_id` is not null  
[2023-05-31 16:50:54] local.INFO: Query Time:1.49s] select `name` from `items` where `id` = ? limit 1  
[2023-05-31 16:50:54] local.INFO: Query Time:1.2s] select `item_id` from `order_items` where `order_items`.`order_id` = ? and `order_items`.`order_id` is not null  
[2023-05-31 16:50:54] local.INFO: Query Time:1.18s] select `name` from `items` where `id` = ? limit 1  
 
(省略) 
```

次にN＋1問題をイーガーロードという手法でwithメソッドを使用し解消していきます。
```php:test2 _イーガーロードを使用した場合
public static function test2()
{
    $orders = Order::with(['orderItems' => function ($query) {
        $query->select('order_id', 'item_id');
    }, 'orderItems.item' => function ($query) {
        $query->select('id', 'name');
    }])->get();

    Log::info('====test2====');
    foreach ($orders as $order) {
        foreach ($order->orderItems as $item) {
            Log::info($item->item->name);
        }
    }
}
```
上記のコードでは必要な全てのOrderItemとそれぞれのItemが一度のクエリで取得され、N+1問題が解消されます

```sql:test2_発行されたクエリ
===test2===
総クエリ時間: 11.23秒 + 3.03秒 + 2.84秒 + 1.36秒 = 18.46秒
クエリ数: 4

[2023-05-31 16:50:04] local.INFO: Query Time:11.23s] select * from `users` where `id` = ? limit 1  
[2023-05-31 16:50:04] local.INFO: Query Time:3.03s] select * from `orders`  
[2023-05-31 16:50:04] local.INFO: Query Time:2.84s] select `order_id`, `item_id` from `order_items` where `order_items`.`order_id` in (271, 272, 273, 276, 277, 278, 279, 280, 281)  
[2023-05-31 16:50:04] local.INFO: Query Time:1.36s] select `id`, `name` from `item` where `items`.`id` in (4, 6, 7, 8, 9, 10, 33, 49, 97)  
```
これによりクエリの数が大幅に削減され、パフォーマンスが向上します。

# まとめ
今回はN＋1問題について研修の中で学んだことをまとめました。
N+1問題はORMを使用する際に避けられない問題ですが、理解と適切な対策を行うことでパフォーマンスの低下を防ぐことが可能ですLaravelのEloquent ORMは開発を大いに容易にしますが、これがN+1問題の原因となることもあります。

# 参考記事
https://readouble.com/laravel/10.x/ja/eloquent-relationships.html

https://haruguchi-yuma.hatenablog.com/entry/2022/12/23/121820

https://se-tomo.com/2018/10/08/laravel%E3%81%AEeager%E3%83%AD%E3%83%BC%E3%83%89%E3%81%A8%E3%81%AF/