N+1 query in go

N+1, golang

N+1 Query 문제는 데이터베이스 쿼리를 비효율적으로 수행하는 패턴을 말한다.

주로 ORM(Object-Relational Mapping) 프레임워크를 사용할 때 발생하는데, 이는 N개의 레코드에 대해 각각 추가적인 쿼리를 실행하여 데이터를 가져오는 상황을 뜻한다.

이 문제는 데이터 크기에 따라 유의미한 성능 저하를 초래할 수 있다.

N+1 Query 문제를 이해하기 위해 예시를 들어보겠다.

가령, 블로그 포스트와 각각의 포스트에 속한 댓글을 가져오는 상황을 생각해보자.

  1. 먼저, 모든 블로그 포스트를 가져오는 쿼리를 실행한다:

    SELECT * FROM posts;
  2. 각 포스트에 대해 해당하는 댓글을 가져오는 추가 쿼리를 N번 실행한다:

    SELECT * FROM comments WHERE post_id = 1;
    SELECT * FROM comments WHERE post_id = 2;
    ...
    SELECT * FROM comments WHERE post_id = N;

이 경우, 총 N+1개의 쿼리가 실행되는데, 이는 데이터베이스 성능에 큰 부담을 줄 수 있다.

Go에서 N+1 Query 문제 방지 방법

Go 언어에서 N+1 Query 문제를 방지하기 위해 주로 ORM이나 SQL 빌더를 사용하는데, 대표적인 라이브러리로는 gorm, sqlx, ent 등이 있다. 여기서는 gorm을 사용한 해결 방법을 설명한다.

Eager Loading 사용

Eager Loading을 사용하면 연관된 데이터를 한 번의 쿼리로 함께 가져올 수 있다. gorm에서는 Preload 메서드를 사용하여 이를 구현할 수 있다.

type Post struct {
    ID       uint
    Title    string
    Comments []Comment
}

type Comment struct {
    ID     uint
    PostID uint
    Body   string
}

// Preload 사용하여 한 번의 쿼리로 모든 데이터를 가져온다
var posts []Post
db.Preload("Comments").Find(&posts)

이 코드에서는 Preload("Comments")를 사용하여 모든 포스트와 해당하는 댓글들을 한 번의 쿼리로 가져온다. 이렇게 하면 N+1 Query 문제를 피할 수 있다.

다만 Eager Loading 은 복잡한 연관 관계가 많은 경우 쿼리가 복잡해지고 성능에 안좋은 영향을 줄 수 있다.

Join을 사용

SQL Join을 직접 사용하여 데이터를 가져오는 방법도 있다. 이는 SQL 쿼리를 최적화할 수 있는 방법이다.

var results []struct {
    PostID    uint
    PostTitle string
    CommentID uint
    CommentBody string
}

db.Table("posts").
    Select("posts.id as post_id, posts.title as post_title, comments.id as comment_id, comments.body as comment_body").
    Joins("left join comments on comments.post_id = posts.id").
    Scan(&results)

이 코드에서는 left join을 사용하여 포스트와 댓글을 함께 가져온다.

이를 통해 필요한 데이터를 한 번의 쿼리로 가져올 수 있어 N+1 Query 문제를 방지할 수 있다.

대신 위와 같이 Raw 쿼리로 작성하게 되는 경우는 유지보수성이 낮아질 수 있으니 쿼리 빌더를 사용하는 것이 좋겠다.


N+1 Query 문제는 성능을 크게 저하시킬 수 있는 패턴으로, Eager Loading, Join, Batch Query 등의 기법을 통해 이를 방지할 수 있다.

이러한 기법들을 적절히 활용하면 데이터베이스 접근의 효율성을 높이고 애플리케이션의 성능을 최적화할 수 있다.

Last updated