【Spring Boot】N+1問題とは?やさしく解説

Java入門・実践
スポンサーリンク
スポンサーリンク

はじめに

Spring Data JPA を使っていると、
「なんか遅い…?」 という場面に出会うことがあります。

  • SQL を書いていないのに大量のクエリが発行される
  • 画面表示が遅い
  • 本番だけパフォーマンスが落ちる
  • ローカルでは気づかない

その原因のひとつが N+1問題 です。

この記事では、N+1問題を
やさしく・実務寄り で整理していきます。


N+1問題とは

一言でいうと、

「1回のつもりが、実は N+1 回クエリが発行されてしまう問題」

です。

例えるなら

  • 「1回で全部取ってきてね」と頼んだのに
  • 「1回取ってきて、残りは1件ずつ取りに行きますね」
    と勝手に分割されるイメージ。

具体例で理解する(最も分かりやすいパターン)

User と Post の関係

  • User(1)
  • Post(多)

コード

Java
List<User> users = userRepository.findAll();

for (User user : users) {
    System.out.println(user.getPosts().size());
}

発行される SQL

  1. User を全部取得するクエリ(1回)
  2. 各 User の posts を取得するクエリ(N回)

合計 N+1回 のクエリが発行されます。


なぜ N+1 が起きるのか

原因は Lazy ロード(遅延ロード) です。

Lazy の動き

  • User を取得したとき → Post はまだ取得しない
  • user.getPosts() を呼んだ瞬間に SQL が発行される

Hibernate は
「必要になったら取りに行くね」
という賢い動きをしますが、
ループの中で呼ぶと大量の SQL が発行されます。


N+1 が起きやすいパターン

@OneToMany(多側の取得)

最も多い。

@ManyToOne(親を何度も取りに行く)

意外と起きる。

画面表示でループして関連データを参照

一覧画面でよく発生。

Service 層で複数の関連を参照

複雑なロジックで発生。


N+1問題の何が悪いのか

  • クエリが大量に発行される
  • DB への負荷が増える
  • ネットワークの往復が増える
  • 本番環境で顕著に遅くなる
  • ローカルでは気づきにくい

静かに性能を奪う“隠れバグ” のような存在です。


N+1問題の解決方法(実務で使う順)

Fetch Join を使う(最も一般的)

Java
@Query("SELECT u FROM User u JOIN FETCH u.posts")
List<User> findAllWithPosts();

User と Post を 1回のクエリでまとめて取得 できます。

EntityGraph を使う(アノテーションで解決)

Java
@EntityGraph(attributePaths = "posts")
List<User> findAll();

Repository に書くだけで Lazy を上書きできます。

DTO で必要なデータだけ取得する

Java
@Query("SELECT new com.example.UserDto(u.id, u.name, p.title) ...")
List<UserDto> findUserWithPost();

画面用のデータは DTO に寄せると効率的。

Lazy のままバッチサイズを設定する(Hibernate の機能)

YAML
spring.jpa.properties.hibernate.default_batch_fetch_size: 100

Hibernate がまとめて取得してくれるため、
N+1 が 1+1 に近づきます。


実務での判断基準

Fetch Join を使うべき場面

  • 一覧画面
  • 関連データを必ず使う場面
  • N+1 が確実に起きる場面

EntityGraph を使うべき場面

  • Repository のメソッド単位で制御したい
  • コードをシンプルにしたい

DTO を使うべき場面

  • 画面表示用のデータが複雑
  • JOIN が多い
  • パフォーマンスが重要

バッチサイズを使うべき場面

  • 既存コードを大きく変えたくない
  • Lazy を維持したい

N+1 を見つける方法

SQL ログを出す

YAML
spring.jpa.show-sql: true

Hibernate の SQL フォーマットを有効にする

YAML
spring.jpa.properties.hibernate.format_sql: true

ログを DEBUG にする

YAML
logging.level.org.hibernate.SQL: DEBUG

SQL が大量に出ていたら N+1 の可能性が高いです。


よくあるつまずきポイント

FetchType.EAGER にすれば解決する?

しません。 EAGER は別の問題(予期せぬ JOIN)を生むため非推奨。

@OneToMany に Fetch Join を使うと重複が出る

→ DISTINCT を付けると解決。

DTO にすると Entity のメリットが消える?

→ 画面用は DTO、内部は Entity が基本。


まとめ

N+1問題とは、

「1回のつもりが、実は N+1 回クエリが発行される問題」

です。

  • Lazy ロードが原因
  • 一覧画面で起きやすい
  • 本番で性能劣化を引き起こす
  • Fetch Join / EntityGraph / DTO / バッチサイズで解決

難しく聞こえますが、
「関連データをループで参照すると N+1 が起きる」
と理解できれば十分です。


decopon
decopon

N+1問題は、最初は気づきにくいですが、
“仕組み” を理解すると落ち着いて対処できるようになります。
パフォーマンス改善の第一歩として、
とても価値のある知識です。
あなたの開発が、今日より少しだけ楽になりますように。

moco
moco

1回でいいのに、何回も取りに行っちゃうなんて…
かわいいけど困っちゃうね。

コメント

タイトルとURLをコピーしました