Laravel N+1 problem

Jeongkuk Seo
sjk5766
Published in
4 min readJun 13, 2020

Lazy Loading vs Eager Loading

N+1 Problem에 대해 알아보기 전에 위 두 개념에 대해 이해해야 합니다.

DB 데이터를 조회 할 때, Lazy Loading연관된 데이터들을 실제로 접근하기 전에는 로딩하지 않습니다. 코드로 보면 아래와 같이 실제로 접근하는 시점에 데이터를 가져옵니다.

$books = App\Book::all(); // (1)

foreach ($books as $book) {
echo $book->author->name; // 연관된 author 정보의 name에 접근
}

반대로 Eager Loading은 위 코드에서 (1), Book 모델에 쿼리할 때 연관된 데이터를 가져오게 됩니다.

N+1 Problem

N+1 Problem은 ORM에서 Lazy Loading을 사용할 때 발생됩니다. (참고로 요즘 ORM은 default로 Lazy Loading을 사용합니다.)

$books = App\Book::all(); // SELECT * FROM book;

foreach ($books as $book) {
echo $book->author->name; // SELECT name FROM author
// WHERE id = $book["author_id"]
}

위 코드에서 Book 데이터가 100개라고 가정했을 때 총 101번의 쿼리가 발생합니다.

1번: 처음 Book 데이터 전체를 가져올 때

100번: Loop를 돌 때마다 author의 name 정보를 쿼리합니다.

만약 데이터가 N개라면 총 N+1개의 쿼리가 발생하기 때문에 N+1 Problem 이라 부릅니다.

해결방법

해결방법은 DB에 쿼리할 때 Eager Loading으로 데이터를 가져오는 겁니다. 대부분의 ORM은 N+1 문제를 막는 방법들을 제공합니다.

Laravel의 경우 Eager Loading을 제공하는 with 메소드를 사용하여 N+1 을 2개의 쿼리로 해결할 수 있습니다.

$books = App\Book::with('author')->get();

foreach ($books as $book) {
echo $book->author->name;
}

위 코드에서 with를 사용한 코드가 실행되면 아래와 같이 두 개의 쿼리가 실행됩니다.

select * from books
select * from authors where id in (1, 2, 3, 4, 5, ...)

만약 두 개 이상의 테이블Eager Loading 하고 싶다면 아래와 같이 사용합니다.

$books = App\Book::with(['author', 'publisher'])->get();

특정 컬럼에 대해 Eager Loading 하고 싶다면 아래와 같이 사용합니다. 아래 예제에선 author 테이블의 id, name 컬럼에 대해 Eager Loading 합니다.

$books = App\Book::with('author:id,name')->get();

쿼리 할 때 말고 Model에서 Eager Loading을 지정하고 싶다면 아래와 같이 Model에서 $with 를 사용합니다.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
protected $with = ['author']; // Model에서 지정

public function author()
{
return $this->belongsTo('App\Author');
}
}

그 외에 다양한 사용을 위해서 Laravel 공식 홈페이지를 참고하시면 됩니다.

Laravel GraphQL LightHouse N+1

Laravel에서 GraphQL을 지원하는 Lighthouse에서는 Schema에서 관계를 지정하는 디렉티브를 사용할 경우 Eager Loading을 지원하게 됩니다.

type Post {
title: String!
author: User! @belongsTo
}

type User {
name: String!
posts: [Post!]! @hasMany
}

위 예제에서 @belongsTo @hasMany 와 같은 디렉티브를 사용하면 내부에서 Eager Loading으로 데이터를 가져오게 됩니다. 혹은 with 디렉티브를 사용할 수도 있습니다.

--

--