← 아티클 목록

N+1 쿼리 문제 — 발견하는 법과 해결하는 법

2026-09-14#database#ORM#성능

목록 1번 조회에 연관 데이터가 행마다 추가 조회되면, 쿼리가 1 + N번 나갑니다. 게시글 100개를 가져오면서 각 글의 작성자를 따로 조회하면 총 101번의 쿼리가 실행됩니다. ORM이 알아서 lazy loading을 해주는 탓에 코드만 봐서는 안 보입니다.

먼저 쿼리 로그로 본다

추측하지 말고 실제로 몇 번 나가는지 봅니다.

SQL
SET GLOBAL general_log = 'ON';
SET GLOBAL general_log_file = '/var/log/mysql/general.log';
OUTPUT
SELECT * FROM posts LIMIT 100;
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE id = 2;
SELECT * FROM users WHERE id = 3;
...   -- users 조회가 100번 반복

같은 테이블에 WHERE id = ?만 값을 바꿔가며 반복된다면 N+1입니다. ORM 로깅(spring.jpa.show-sql=true, Django django.db 로거, Sequelize logging: true)으로도 똑같이 확인할 수 있습니다.

원인별 해결

상황해결
목록 + 연관 1:N 조회JOIN 한 번으로 합쳐서 가져오기
ORM lazy loading 반복eager/fetch join으로 전환
연관 데이터가 많아 JOIN 비쌈부모 ID 모아 IN 절로 2번에 조회

① JOIN으로 한 방에

SQL
SELECT p.*, u.name
FROM posts p
JOIN users u ON u.id = p.user_id
LIMIT 100;

② IN 절로 2번에 (배치 로딩)

SQL
SELECT * FROM posts LIMIT 100;
SELECT * FROM users WHERE id IN (1, 2, 3, ...);  -- 100번이 1번으로

③ ORM별 eager loading

  • JPA: @EntityGraph 또는 JOIN FETCH
  • Django: select_related(1:1, N:1) / prefetch_related(1:N, N:M)
  • Sequelize/TypeORM: include / relations 옵션

체크리스트

로컬 터미널
# 1. 쿼리 횟수 측정 (로그 켜고 요청 1회 날린 뒤)
grep -c "SELECT" /var/log/mysql/general.log

# 2. 같은 패턴 반복 확인
grep "WHERE id =" /var/log/mysql/general.log | sort | uniq -c

목록 응답 한 번에 수십~수백 쿼리가 찍히면 JOIN이나 배치 로딩으로 바꿉니다.


ORM 쿼리 로그를 직접 켜고 N+1을 재현·해결하는 실습은 데이터베이스 트랙에서 회원가입 없이 무료로 할 수 있습니다.