未経験からエンジニア 奮闘記

未経験からエンジニアで自由に生きる途中

MENU

N+1問題とは?基礎から対策まで解説

この記事を読むと得られること

  • N+1とは何か をしっかり理解できる
  • N+1の対策 を自信を持って打てるようになる
  • データ取得時に意識すべきパフォーマンス観点 を身につけられる

データベースとアプリケーションの間でやりとりされるSQLクエリ。
その中でも開発初期によくある落とし穴が「N+1問題」です。

この問題を放置すると、リリース後にアプリが遅くなるなど重大な影響を及ぼします。
この記事では、N+1がどういう場面で発生しやすいのか、そしてどのように対策を打つべきかを丁寧に解説していきます。


N+1問題とは何か?どんな時に発生するのか?

N+1問題とは、データ取得の際に 不要に多くのSQLが実行されてしまう状態 を指します。

たとえば、以下のような処理を見てみましょう。

// ユーザー一覧を取得
users := db.Find(&[]User{})

for _, user := range users {
    // 各ユーザーのTodoを取得
    db.Where("user_id = ?", user.ID).Find(&[]Todo{})
}

この場合、ユーザー一覧を取得する1回のクエリに加え、ユーザーごとに関連するTodoを取得しており、ユーザーの数だけクエリが追加で実行されます。
つまり、N人のユーザーに対してN+1回のクエリが発行されることになります。


データ取得で気をつけるべき2つのポイント

N+1を理解するためには、まずパフォーマンスに影響する基本的な観点を押さえることが大切です。

1. SQLのリクエスト回数

クエリ数が増えるほど、アプリケーションとデータベースの間の通信が増え、処理速度に悪影響を及ぼします。

2. テーブル結合(JOIN)のコスト

複数のテーブルを結合するとDB内での処理が重くなります。大量データをJOINする場合は特に注意が必要です。


N+1の何が問題なのか?

要点は「SQLのリクエスト回数が増えてしまう」という点に尽きます。

1件ずつ関連データを取得することで、ユーザー数に比例してクエリ数が増加。
その結果、処理が遅くなったり、DBへの負荷が急増したりと、アプリケーションのパフォーマンスが著しく悪化します。


N+1問題の対策方法

N+1問題を回避するためには、以下のような方法があります。

Preload(GORMなどのORマッパーで使用)

関連データをあらかじめ一括で取得しておく方法です。

db.Preload("Todos").Find(&users)

これにより、ユーザー一覧と各ユーザーのTodoを 2クエリで一気に取得 できます。

JOIN を使う(用途に応じて)

場合によっては JOIN を使って関連データをまとめて取得する方法も有効です。ただし、JOINを多用するとデータ量が膨らみ、パフォーマンスに影響する可能性もあるため、慎重に使う必要があります。

💡 PreloadとJOIN、どちらを選ぶべきか?
この判断は場面ごとに異なり、データ構造や用途に応じた知識が必要になります。
このテーマは中級者以上向けの内容になるため、別記事で詳しく解説します。


まとめ:中級者への第一歩は「パフォーマンス意識」

今回は「N+1問題」について基本から対策までを紹介しました。

重要なのは、単にコードを書くのではなく、クエリの数や処理の重さに対する意識を常に持つことです。

  • クエリが何回実行されているか?
  • 1回のクエリでどれだけのデータを持ってくるか?
  • PreloadやJOINは適切に使われているか?

こうした視点を身につければ、日々の開発で「なんとなく書いている処理」が一段レベルアップし、パフォーマンスを意識できる中級者へのステップアップになります。


📘 次回予告:PreloadとJOINの違いと選び方を徹底解説!
さらに深く知りたい方のために、別記事で実際の使い分けパターンやベンチマーク結果も紹介する予定です。


このままブログに貼ってもOKな形に整えましたが、必要なら図やコード例を増やすカスタマイズも対応できます。どうしますか?