一人前エンジニア道場

データベース設計 / 約20分

N+1問題を読み解く

なぜこれを学ぶか

「開発環境では速かったのに本番で遅い」の代表格がN+1問題です。ORMを使うと意識せず踏むため、コードを読んで検出できるようになりましょう。

N+1問題とは: 一覧取得で1回クエリ(N件)を発行した後、各レコードごとに追加クエリ を発行してしまい、合計N+1回のクエリが走る問題です。100件なら101回。

ORMのコードで for ループ内に await 付きのDBアクセス(や関連オブジェクトへのアクセス)があったら要注意です。

解決策は Eager Loading(JOINやINで一括取得)です。SQLなら WHERE user_id IN (1,2,3,...)、ORMなら include / preload 等の機能を使います。

検出方法: 開発時に クエリログを有効化 して、1画面で何本のクエリが走っているか確認する習慣をつけましょう。

重要ポイント

  • ループ内のDBアクセスはN+1の兆候
  • Eager Loading(一括取得)で解決
  • 開発時にクエリログを見る習慣

コードレビュー演習

チームメンバーが書いたコードです。「投稿一覧に著者名を表示する」機能です。

// 投稿を全件取得
const posts = await db.query('SELECT * FROM posts LIMIT 50')

// 各投稿の著者情報を取得
for (const post of posts) {
  post.author = await db.query(
    'SELECT * FROM users WHERE id = ?', [post.user_id]
  )
}

return c.json(posts)

このコードの問題と適切な修正は?

ヒントを見る

クエリが「何回」実行されるか数えてみましょう。最初の1回(投稿50件)に加えて、ループ内で何回走りますか? 「1本あたりの速さ」と「本数」のどちらが問題かがポイントです。

コーディング演習実際に書く

N+1を回避する結合処理を実装してください。posts 配列の各要素に、users 配列から対応する著者オブジェクトを author プロパティとして付けた「新しい配列」を返します。条件: ①ユーザー検索はO(1)にすること(Mapの使用推奨。find をループ内で使うとO(n×m)で本末転倒)②元の posts/users を変更しない ③対応するユーザーがいない場合は author を null にする。

function attachAuthors(posts, users)

SQL演習ブラウザ内SQLite

N+1問題をSQLで解決しましょう。要件: 「すべての投稿のタイトルと著者名を、1回のクエリ(JOIN)で取得する」SELECT文を書いてください。投稿IDの昇順で並べてください。

テーブル定義と初期データを見る
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE posts (id INTEGER PRIMARY KEY, user_id INTEGER, title TEXT);
INSERT INTO users VALUES (1, '田中'), (2, '佐藤');
INSERT INTO posts VALUES (1, 1, 'はじめての投稿'), (2, 2, 'SQL入門'), (3, 1, 'N+1とは');

スキルセルフチェック

このレッスンの内容をどの程度理解できましたか?