1. N+1 問題(アプリケーション層)
N+1 問題は SQL 編でも触れたが、ORM 利用時に最も頻発する問題として再度確認する。1 回の「全件取得」クエリの後、ループ内でレコード数 N 回のクエリが発生するパターン。
❌ NG# クエリ 1 回で全 Order を取得
orders = Order.objects.all()
for order in orders:
# ← ループのたびに User を取得するクエリが発行される(N 回)
print(order.user.name)
# JOIN で一括取得。ループ内で追加クエリは発生しない
orders = Order.objects.select_related('user').all()
for order in orders:
print(order.user.name) # キャッシュ済みデータを参照
| ORM | 一対多 (FK) | 多対多 (M2M) |
|---|---|---|
| Django | select_related() | prefetch_related() |
| SQLAlchemy | joinedload() | subqueryload() |
| ActiveRecord | includes() / eager_load() | |
| Prisma | include: { user: true } | |
2. ループ内の重複処理
ループのたびに同じ計算や関数呼び出しをすると、無駄な処理がN回実行される。ループ外で計算できるものは事前に変数に持つ。
❌ NGitems = get_items()
for i in range(len(items)): # len() が毎回呼ばれる
config = load_config() # 毎ループで設定をDBから読み込む
process(items[i], config)
items = get_items()
config = load_config() # ループ前に 1 回だけ読み込む
count = len(items) # ループ前に計算
for item in items: # Python では直接イテレートするのがベスト
process(item, config)
3. ループ内での文字列結合
多くの言語で文字列はイミュータブル(不変)だ。+= で結合するたびに新しい文字列オブジェクトが生成され、大量のループでは O(N²) のメモリコストになる。
result = ""
for word in words:
result += word + ", " # 毎回新しい文字列オブジェクトを生成
result = ", ".join(words) # O(N) で効率的
# HTML / SQL 生成なら同様のパターン
rows = "\n".join(f"<tr><td>{row.name}</td></tr>" for row in data)
4. 早すぎる最適化
「プログラムにおける悪の根源は、早すぎる最適化にある」(Donald Knuth)。プロファイリングでボトルネックを特定する前に可読性を犠牲にした最適化を行うと、メンテしにくいコードだけが残る。
⚠️ 最適化の正しい順序
① まず正しく動くコードを書く → ② プロファイリングでボトルネックを計測する → ③ ボトルネックだけを最適化する。計測せずに直感で最適化すると、速くなっていない場所だけ読みにくくなる。
import cProfile
import pstats
profiler = cProfile.Profile()
profiler.enable()
my_function() # プロファイリングしたい処理
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')
stats.print_stats(20) # 上位 20 件を表示
5. キャッシュを使わない
同じ計算・クエリ結果を毎回取得するのは無駄だ。特に変化しない設定値・マスタデータ・計算コストの高い結果はキャッシュが有効。ただし「キャッシュの invalidation」を考慮しないまま使うと古いデータが残るバグを生む。
✅ OKfrom functools import lru_cache
@lru_cache(maxsize=128)
def get_exchange_rate(from_currency: str, to_currency: str) -> float:
"""為替レートを外部APIから取得(結果をキャッシュ)"""
return external_api.fetch_rate(from_currency, to_currency)
# 同じ通貨ペアへの 2 回目以降の呼び出しはキャッシュを返す
シリーズ全体まとめ
SQL アンチパターン(PART 01〜03)
| カテゴリ | 主なアンチパターン | 対策の核心 |
|---|---|---|
| インデックス無効化 | 関数適用・型変換・前方ワイルドカード・NOT IN・OR多用 | インデックスが使える形でカラムを参照する |
| 不要な読み込み | SELECT *・大 OFFSET・DISTINCT 乱用 | 必要な列・行だけ取得する |
| 結合・サブクエリ | N+1・相関サブクエリ・デカルト積 | JOIN で一括取得・結合条件を明示する |
| ロック競合 | 長大 TX・デッドロック・過剰 FOR UPDATE | TX を短く・ロック順序を統一する |
| 集計・ソート | HAVING 誤用・COUNT の使い分け | WHERE で先に絞る・COUNT の違いを理解する |
| その他 | 大 IN リスト・EXPLAIN 未確認 | 一時テーブル活用・実行計画を毎回確認する |
コーディングアンチパターン(PART 04〜06)
| カテゴリ | 主なアンチパターン | 対策の核心 |
|---|---|---|
| 設計 | ゴッドクラス・散弾銃手術・特性の横恋慕 | 単一責任・データとロジックを同居させる |
| 命名 | マジックナンバー・意味不明な変数名・深い継承 | 定数化・意図を示す名前・継承よりコンポジション |
| 可読性 | 深いネスト・長大関数・DRY 違反 | ガード節・関数分割・共通化 |
| エラー処理 | 空 catch・例外乱用・広すぎるキャッチ | ログを残す・制御フローに例外を使わない |
| パフォーマンス | N+1・ループ内重複処理・文字列結合・早すぎる最適化 | 計測してからボトルネックだけを直す |
✅ アンチパターンを覚えることの価値
アンチパターンは「こう書いてはいけない」という知識ではなく、「なぜそれが問題を引き起こすか」を理解するための道具だ。原因を理解することで、応用の利いた設計判断ができるようになる。コードレビューでも「〇〇パターンになっているので〜」と言語化できるようになると、チーム全体のコード品質が上がる。