なぜSQLが遅くなるのか

アプリケーションのパフォーマンス問題の多くはデータベースのクエリに起因します。適切なインデックス設計と実行計画の理解が、SQL最適化の鍵です。

インデックスの基本

インデックスは本の索引と同じで、特定の列の値を高速に検索するためのデータ構造(B-Tree)です。

-- インデックス作成
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);

-- 複合インデックスは左端から使われる
-- idx_orders_user_date は以下に有効:
-- WHERE user_id = 5
-- WHERE user_id = 5 AND created_at > '2024-01-01'
-- 以下には無効:
-- WHERE created_at > '2024-01-01'  ← user_idなし

EXPLAINで実行計画を読む

EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id, u.name;

見るべきポイント

キーワード 意味
Seq Scan テーブル全スキャン(遅い)
Index Scan インデックス使用(速い)
Hash Join ハッシュ結合
Nested Loop ネストループ(小テーブル向き)
rows= 推定行数
cost= 相対コスト

よくあるアンチパターンと対策

-- NG: WHERE句で関数使用(インデックス無効化)
SELECT * FROM users WHERE YEAR(created_at) = 2024;

-- OK: 範囲で書く
SELECT * FROM users
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';

-- NG: SELECT *(不要な列を取得)
SELECT * FROM users;

-- OK: 必要な列のみ
SELECT id, name, email FROM users;

-- NG: N+1問題
for user in users:
    orders = "SELECT * FROM orders WHERE user_id = ?"  # ユーザー数分クエリ発行

-- OK: JOINでまとめて取得
SELECT u.id, u.name, o.id, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

クエリ最適化チェックリスト

  1. EXPLAIN ANALYZEで実行計画確認
  2. Seq Scanを探してインデックス追加検討
  3. SELECT *を必要列のみに絞る
  4. サブクエリをJOINに変換検討
  5. 大量データはLIMITでページネーション
  6. 定期的なVACUUM ANALYZE(PostgreSQL)

適切なSQLを書く力はどんなシステムでも求められる普遍的なスキルです。