Laravel N+1 problem
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 디렉티브를 사용할 수도 있습니다.