35
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-06-01

はじめに

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

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

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

N+1問題とは

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

具体例

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

  • orders テーブル:各注文に関する情報を保持
  • items テーブル:販売商品の情報を保持
  • order_items テーブル:各注文がどの商品を含んでいるかを記録(ordersとitemsの中間テーブル)
仮ECサイトのテーブル図
  +-----------------+     +-------------+     +-------------+
  |      Orders     |     | Order_items |     |   Items     |
  |-----------------|     |-------------|     |-------------|
  |       id        |-----|  order_id   |     |     id      |
  |                 |     |  item_id    |-----|    name      |
  |                 |     |                 |     |             |
  +-----------------+     +-------------+     +-------------+

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

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つの注文とその注文に含まれる全てのアイテムを取得するために、注文の数に応じた多数のクエリが実行されます。

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メソッドを使用し解消していきます。

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問題が解消されます

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問題の原因となることもあります。

参考記事

35
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?