データベース設計 / 約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 にする。
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とは');スキルセルフチェック
このレッスンの内容をどの程度理解できましたか?