FastAPI + SQLAlchemy性能改善プレイブック: 遅いAPIを計測ベースで高速化する

FastAPIの初期実装は非常に快適です。しかし運用フェーズに入ると、次のような症状が出てきます。

  • 一覧APIのレスポンスが急に遅くなる
  • 同時接続が増えるとp95が跳ねる
  • CPUは余っているのにタイムアウトが増える
  • DB接続数が上限に張り付く

こうした問題の多くは「Pythonが遅い」のではなく、SQLAlchemyの使い方とDBアクセス設計 に起因します。

本記事では、FastAPI + SQLAlchemy + PostgreSQL構成を前提に、実際の改善手順を計測ベースで整理します。

1. 最初に測るべき指標

最適化は、体感ではなく数値で進めます。最低限、以下を可視化します。

  • APIのp50/p95/p99レイテンシ
  • エンドポイント別SQL発行回数
  • 1リクエストあたりのDB滞在時間
  • connection pool待ち時間
  • slow query件数(200ms以上など)

OpenTelemetryやNew Relicを使っているなら、アプリspanとDB spanを必ず紐付けてください。これだけでボトルネック特定速度が上がります。

2. N+1問題を最優先で潰す

最も頻出するのがN+1です。例えばユーザー一覧でプロフィールを参照すると、ユーザー数分の追加クエリが発行されます。

2.1 悪い例

1
2
3
4
5
6
7
8
users = session.query(User).limit(100).all()
result = []
for u in users:
    result.append({
        "id": u.id,
        "name": u.name,
        "profile": u.profile.bio,
    })

2.2 改善例(joinedload/selectinload)

1
2
3
4
5
6
7
8
from sqlalchemy.orm import selectinload

users = (
    session.query(User)
    .options(selectinload(User.profile))
    .limit(100)
    .all()
)

joinedloadselectinload はデータ量で使い分けます。

  • 1対1/少量: joinedload
  • 1対多/件数多め: selectinload

闇雲に joinedload を増やすと行爆発が起きるため、EXPLAINで確認しながら適用します。

3. SQLAlchemy 2.xスタイルへ揃える

旧query APIと新APIが混在すると可読性と最適化精度が落ちます。2.xスタイルへ統一しましょう。

1
2
3
4
5
6
7
8
9
from sqlalchemy import select

stmt = (
    select(Order)
    .where(Order.status == "paid")
    .order_by(Order.created_at.desc())
    .limit(50)
)
orders = session.execute(stmt).scalars().all()

この形式は、EXPLAIN の追跡や再利用がしやすく、レビュー品質も上がります。

4. 必要な列だけ取る(過剰フェッチの削減)

ORMは便利ですが、何も考えずモデル全体を取ると不要データまで転送されます。特にJSONカラムやTEXTが重い場合、ここが効きます。

1
2
stmt = select(User.id, User.name, User.email).where(User.active.is_(True))
rows = session.execute(stmt).all()

一覧APIはDTO用の軽量SELECTを使い、詳細APIでのみ重いカラムを取得する設計が安定します。

5. 接続プール設定を環境に合わせる

pool_size を適当に増やすだけでは逆効果です。PostgreSQL側上限とアプリ台数を合わせて設計します。

1
2
3
4
5
6
7
8
engine = create_engine(
    DB_URL,
    pool_size=20,
    max_overflow=10,
    pool_timeout=10,
    pool_recycle=1800,
    pool_pre_ping=True,
)

設計の目安:

  • DB max_connections = 300
  • API Pod = 6
  • 1 Podあたりpool_size 20

この時点で理論最大120接続。バッチや管理接続も見込み、余白を残すのが安全です。

6. トランザクション境界を短くする

長いトランザクションはロック競合とスループット低下を招きます。

悪い例:

  1. DB更新
  2. 外部API呼び出し
  3. メール送信
  4. commit

この順は危険です。外部I/Oをトランザクション外へ逃がします。

改善例:

  1. DB更新 + commit
  2. 外部通知は非同期ジョブで実行

これだけで同時処理性能が目に見えて改善します。

7. インデックス設計をAPI単位で見直す

「インデックスはある」だけでは不足です。実際のWHERE + ORDER BYに合っているかが重要です。

例: 注文履歴API

1
2
3
4
5
SELECT id, total_amount, created_at
FROM orders
WHERE user_id = $1 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 50;

この場合、次の複合indexが有効です。

1
2
CREATE INDEX CONCURRENTLY idx_orders_user_status_created_at_desc
ON orders (user_id, status, created_at DESC);

単独indexを乱立させるより、アクセスパターンに合わせた複合indexを厳選した方が効きます。

8. キャッシュ導入は「遅い理由の解決後」に行う

キャッシュは万能ではありません。N+1やスロークエリを放置したまま載せると、整合性事故の温床になります。

導入順序:

  1. SQL最適化
  2. 接続プール調整
  3. 必要なエンドポイントに限定してRedis cache

キャッシュキーは resource:id:version 形式にし、更新時の無効化戦略を先に定義してください。

9. 負荷試験シナリオ(k6例)

最適化の成果は負荷試験で確認します。最低3シナリオが必要です。

  • steady: 通常トラフィック
  • burst: 短時間ピーク
  • soak: 長時間連続実行(リーク検知)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 50 },
    { duration: '5m', target: 50 },
    { duration: '1m', target: 200 },
    { duration: '2m', target: 0 },
  ],
};

export default function () {
  const res = http.get('https://api.example.com/orders?limit=50');
  check(res, { 'status 200': (r) => r.status === 200 });
  sleep(1);
}

改善前後で p95, SQL回数, DB CPU を比較し、定量で判断します。

10. 本番での改善手順テンプレート

  1. ボトルネックendpointの特定
  2. SQLログとEXPLAIN ANALYZE取得
  3. N+1解消
  4. 必要列取得へ変更
  5. インデックス追加(CONCURRENTLY)
  6. pool設定調整
  7. 負荷試験再実施
  8. 段階リリース(10%→50%→100%)

この手順を運用チーム全体でテンプレ化すると、パフォーマンス問題への対応速度が上がります。

まとめ

FastAPI + SQLAlchemyの性能改善は、派手なテクニックより 計測→原因分離→小さく改善 の積み重ねが効きます。

  • N+1解消
  • 過剰フェッチ削減
  • 接続プール最適化
  • インデックスの再設計
  • 負荷試験で再検証

この5点を回せば、遅いAPIは高確率で改善できます。まずは「1リクエストあたりのSQL発行数」を可視化するところから始めるのが最短です。

付録: 改善施策の優先順位(最短で効く順)

時間が限られる現場では、まず「SQL発行回数削減 → インデックス最適化 → connection pool調整」の順で着手すると効果が出やすいです。特に、N+1解消だけでp95が半減するケースは珍しくありません。改善後は必ず同一負荷条件で再計測し、数字で効果を残しておくと、次の改善投資判断が通りやすくなります。