はじめに
RUNTEQ Advent Calendar 2025 の11日目(シリーズ1)を担当します、Masa と申します。
今年のテーマは 『みんなにおすすめしたい〇〇tips』 です。
今回のタイトルは 「質問できるアホであれ!! -N+1から読み解く効率化-」 です!
初学者エンジニアが業務を進める中で、しばしば 「質問タイミング問題」 にぶつかるそうです。自分も間違いなく今後 ぶち当たるので、 先に技術的に整理してみることにします。
ちなみに本日、RUNTEQ Advent Calendar 2025シーズン2にも記事を書かせて頂きましたので、ご興味ある方はぜひ下記からお願い致します。
https://note.com/masa_007_02/n/n0738de4cd9e7
さて、先ずはこちらのショート動画をご覧ください。
先輩の背中をチラ見しながら、初学者の脳内は…
・今ロジック考えてる…?
・設計してる?
・エラー対応中?
・今は聞かないほうがいい?
・明日に回したほうがいいのかな?
・もうなんかちょっと怒ってない…!!??
こういった「深読み」によって質問が遅れ、
その日やるべきTODOが終わらない→不安が増えるという悪循環に陥ります。
そして、本日のテーマを整理するために 「N+1問題」 を使います。
そもそもN+1とは?
まずは普通の技術としての N+1 から見ていきます。
#投稿一覧を1回取得
posts = Post.all
#しかしループで毎回 user を取得(N回)
posts.each do |post|
puts post.user.name
end
#→ 合計 N+1 回クエリが発生(非効率)
本来 includes(:user) でまとめれば1回で済むのに、
なぜか 毎回1件ずつデータを全て取りに行ってしまう構造のことを指します。
これによって
・不必要なクエリが大量発生する
・処理時間が増える
・CPU負荷・DB負荷が上がるといった問題につながります。
それに対して下記のように書いた場合は、
#投稿とユーザーをまとめて取得(1回だけ)
posts = Post.includes(:user)
#ループ内では追加クエリが発生しない
posts.each do |post|
puts post.user.name
end
#→ クエリ発生は1回のみ(効率的)
includes(:user) を使用することで、関連データを事前にまとめて取得するため、
ループ内で post.user を呼んでも追加クエリが発生しません。
つまり 「1回でまとめて取りに行く」か「N回チマチマ取りに行くか」 の違いが処理効率に大きな差を生むのです。
このようにN+1問題はコード自体が間違っているわけではありませんが、パフォーマンスを著しく悪化させる要因になります。
そしてこの本当は1回で済むのに、余計に何度も処理してしまう構造は、初学者エンジニアが質問する前に深読みしすぎるときの思考パターンにも似ているのではないでしょうか。
N+1問題を、「質問前の思考パターン」に置き換えて見ていきます。
※以下のコードは“思考プロセスを技術的に可視化するために
書いた疑似コードです。実際に動かすことを目的としたコードではありません。
それでは2人のエンジニアを使って比較してみます。
①Aさん(気遣い型・高学歴・超丁寧)
# Aさん(N+1が発生する思考)= Post.all
posts = Post.all
posts.each do |post|
puts post.user.name # ← ここで user を N 回取りに行く(N+1)
end
# Aさんの思考処理(人間版 N+1)
# 心理的な心配(心理N)
mental_fears = [
:忙しいかも,
:迷惑かも,
:質問の粒度大丈夫?,
:もっと調べるべき?,
:また質問?と思われない?
]
# 技術的な心配(技術N)
technical_suspects = [
:nilチェックが足りない?,
:関連付けミス?,
:コールバックが悪さしてる?,
:API仕様変更?,
:自作コードが遅い?
]
# Step1:心理的心配を1件ずつ“都度ロード”(心理N)
mental_fears.each do |fear|
log_internal_state(fear) # 深読み
hesitate(fear) # 躊躇
end
# Step2:心理的心配の影響で技術的心配も1件ずつ“都度ロード”(技術N)
technical_suspects.each do |suspect|
investigate(suspect)
check_logs(suspect)
google_error(suspect)
end
# 技術的には「nilチェック漏れ」が濃厚に
possible_issue = :nilチェック漏れっぽい
# Step3:技術的に解決できそうでも心理Nが“2周目”としてまた動き出す
mental_fears.each do |fear|
doubt(possible_issue)
worry_about_timing(fear)
end
# N+1に苦しんだ末やっと質問
answer = ask_senior("ここまで調べて、nilチェック漏れだと思うのですが…")
# 返答をもとに修正
final_answer = apply_fix(answer)
Aさんは正しい答えには辿り着きます。
ただしとても時間がかかってしまいました。
次は②Bさん(図太い・素直・あまり深く考えない)
# 投稿とユーザーをまとめて取得(1回だけで済む)
posts = Post.includes(:user)
posts.each do |post|
puts post.user.name # ← ここで追加クエリが発生しない
end
# Bさんの思考処理
# Step0:心理・技術の不安を「最初にまとめてロード」する(=includes と同じ構造)
loaded_context = {
mental_fears: :とりあえず聞けばなんとかなる, # 心理を一括ロード
technical_suspects: [
:nilチェック漏れかなぁ?,
:関連付け書き忘れかなぁ?,
:ビュー参照ミスかなぁ?
] # 技術的心配も最初に全部ロード
}
# Step1:心理的処理
acknowledge(loaded_context[:mental_fears])
# Step2:技術的心配をまとめてロード済みなので追加問い合わせしない
loaded_context[:technical_suspects].each do |suspect|
quick_check(suspect) # includes のおかげで全部まとめて処理できるイメージ
end
# Step2.5:1つの課題に絞る
possible_issue = loaded_context[:technical_suspects].first
# 念のため再確認も 1件のみ
reconfirm(possible_issue)
# Step3:迷ったら即質問(途中で追加ロードしない=N+1が発生しない)
answer = ask_senior("nilチェック漏れだと思うんですが、どうですかぁ?")
# Step4:先輩の返事を反映する
final_answer = apply_fix(answer)
Bさんも同じ正しい答えに辿り着きます。しかし結果的にAさんよりも早く答えに辿り着くことができました。
まとめ
Aさんは心理的な不安 → 技術的な不安 → さらに心理的な不安と、正にN+1のように 「余計な2周目」 が発生したことで、答えに辿り着くまでに大きな時間を要しました。
一方でBさんは技術的な心配は3つだけに絞り、心理的な不安も深掘りしなかったため、N+1が発生せず、短時間で結論に辿り着けました。
キャリアを重ねるほど質問はしにくくなるものですが、
初学者のうちは深読みしても間違えていることの方が多いはずです。
大事なのは 「何も考えず即質問」 でも 「考えすぎて遅れる」 でもなく、
必要最低限だけ考えたら、早めに相談するという適切なタイミングです。
N+1のように余計な追加処理が積み重なるほど作業効率は落ちていきます。
だからこそ私は、N+1思考を発生させない質問の仕方 = 「早めに質問を投げられるアホ」
を目標にしたいと思います!!
おまけ
ちなみに下記の動画も大変参考になります。

