<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>SQLAlchemy on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/sqlalchemy/</link>
    <description>Recent content in SQLAlchemy on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Sat, 07 Mar 2026 11:10:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/sqlalchemy/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>FastAPI &#43; SQLAlchemy性能改善プレイブック: 遅いAPIを計測ベースで高速化する</title>
      <link>https://www.ai2core.com/posts/2026-03-07-fastapi-sqlalchemy-performance-tuning-playbook/</link>
      <pubDate>Sat, 07 Mar 2026 11:10:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-07-fastapi-sqlalchemy-performance-tuning-playbook/</guid>
      <description>FastAPIとSQLAlchemyのAPI性能を、N&#43;1解消・クエリ最適化・接続プール設定・負荷検証まで含めて具体的に改善する実践手順を解説。</description>
      <content:encoded><![CDATA[<h1 id="fastapi--sqlalchemy性能改善プレイブック-遅いapiを計測ベースで高速化する">FastAPI + SQLAlchemy性能改善プレイブック: 遅いAPIを計測ベースで高速化する</h1>
<p>FastAPIの初期実装は非常に快適です。しかし運用フェーズに入ると、次のような症状が出てきます。</p>
<ul>
<li>一覧APIのレスポンスが急に遅くなる</li>
<li>同時接続が増えるとp95が跳ねる</li>
<li>CPUは余っているのにタイムアウトが増える</li>
<li>DB接続数が上限に張り付く</li>
</ul>
<p>こうした問題の多くは「Pythonが遅い」のではなく、<strong>SQLAlchemyの使い方とDBアクセス設計</strong> に起因します。</p>
<p>本記事では、FastAPI + SQLAlchemy + PostgreSQL構成を前提に、実際の改善手順を計測ベースで整理します。</p>
<h2 id="1-最初に測るべき指標">1. 最初に測るべき指標</h2>
<p>最適化は、体感ではなく数値で進めます。最低限、以下を可視化します。</p>
<ul>
<li>APIのp50/p95/p99レイテンシ</li>
<li>エンドポイント別SQL発行回数</li>
<li>1リクエストあたりのDB滞在時間</li>
<li>connection pool待ち時間</li>
<li>slow query件数（200ms以上など）</li>
</ul>
<p>OpenTelemetryやNew Relicを使っているなら、アプリspanとDB spanを必ず紐付けてください。これだけでボトルネック特定速度が上がります。</p>
<h2 id="2-n1問題を最優先で潰す">2. N+1問題を最優先で潰す</h2>
<p>最も頻出するのがN+1です。例えばユーザー一覧でプロフィールを参照すると、ユーザー数分の追加クエリが発行されます。</p>
<h3 id="21-悪い例">2.1 悪い例</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>users <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>query(User)<span style="color:#f92672">.</span>limit(<span style="color:#ae81ff">100</span>)<span style="color:#f92672">.</span>all()
</span></span><span style="display:flex;"><span>result <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> u <span style="color:#f92672">in</span> users:
</span></span><span style="display:flex;"><span>    result<span style="color:#f92672">.</span>append({
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;id&#34;</span>: u<span style="color:#f92672">.</span>id,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;name&#34;</span>: u<span style="color:#f92672">.</span>name,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;profile&#34;</span>: u<span style="color:#f92672">.</span>profile<span style="color:#f92672">.</span>bio,
</span></span><span style="display:flex;"><span>    })
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="22-改善例joinedloadselectinload">2.2 改善例（joinedload/selectinload）</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.orm <span style="color:#f92672">import</span> selectinload
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>users <span style="color:#f92672">=</span> (
</span></span><span style="display:flex;"><span>    session<span style="color:#f92672">.</span>query(User)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>options(selectinload(User<span style="color:#f92672">.</span>profile))
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>limit(<span style="color:#ae81ff">100</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>all()
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>joinedload</code> と <code>selectinload</code> はデータ量で使い分けます。</p>
<ul>
<li>1対1/少量: <code>joinedload</code></li>
<li>1対多/件数多め: <code>selectinload</code></li>
</ul>
<p>闇雲に <code>joinedload</code> を増やすと行爆発が起きるため、EXPLAINで確認しながら適用します。</p>
<h2 id="3-sqlalchemy-2xスタイルへ揃える">3. SQLAlchemy 2.xスタイルへ揃える</h2>
<p>旧query APIと新APIが混在すると可読性と最適化精度が落ちます。2.xスタイルへ統一しましょう。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> select
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>stmt <span style="color:#f92672">=</span> (
</span></span><span style="display:flex;"><span>    select(Order)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>where(Order<span style="color:#f92672">.</span>status <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;paid&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>order_by(Order<span style="color:#f92672">.</span>created_at<span style="color:#f92672">.</span>desc())
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>limit(<span style="color:#ae81ff">50</span>)
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>orders <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>execute(stmt)<span style="color:#f92672">.</span>scalars()<span style="color:#f92672">.</span>all()
</span></span></code></pre></td></tr></table>
</div>
</div><p>この形式は、<code>EXPLAIN</code> の追跡や再利用がしやすく、レビュー品質も上がります。</p>
<h2 id="4-必要な列だけ取る過剰フェッチの削減">4. 必要な列だけ取る（過剰フェッチの削減）</h2>
<p>ORMは便利ですが、何も考えずモデル全体を取ると不要データまで転送されます。特にJSONカラムやTEXTが重い場合、ここが効きます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>stmt <span style="color:#f92672">=</span> select(User<span style="color:#f92672">.</span>id, User<span style="color:#f92672">.</span>name, User<span style="color:#f92672">.</span>email)<span style="color:#f92672">.</span>where(User<span style="color:#f92672">.</span>active<span style="color:#f92672">.</span>is_(<span style="color:#66d9ef">True</span>))
</span></span><span style="display:flex;"><span>rows <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>execute(stmt)<span style="color:#f92672">.</span>all()
</span></span></code></pre></td></tr></table>
</div>
</div><p>一覧APIはDTO用の軽量SELECTを使い、詳細APIでのみ重いカラムを取得する設計が安定します。</p>
<h2 id="5-接続プール設定を環境に合わせる">5. 接続プール設定を環境に合わせる</h2>
<p><code>pool_size</code> を適当に増やすだけでは逆効果です。PostgreSQL側上限とアプリ台数を合わせて設計します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>engine <span style="color:#f92672">=</span> create_engine(
</span></span><span style="display:flex;"><span>    DB_URL,
</span></span><span style="display:flex;"><span>    pool_size<span style="color:#f92672">=</span><span style="color:#ae81ff">20</span>,
</span></span><span style="display:flex;"><span>    max_overflow<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>,
</span></span><span style="display:flex;"><span>    pool_timeout<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>,
</span></span><span style="display:flex;"><span>    pool_recycle<span style="color:#f92672">=</span><span style="color:#ae81ff">1800</span>,
</span></span><span style="display:flex;"><span>    pool_pre_ping<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></td></tr></table>
</div>
</div><p>設計の目安:</p>
<ul>
<li>DB max_connections = 300</li>
<li>API Pod = 6</li>
<li>1 Podあたりpool_size 20</li>
</ul>
<p>この時点で理論最大120接続。バッチや管理接続も見込み、余白を残すのが安全です。</p>
<h2 id="6-トランザクション境界を短くする">6. トランザクション境界を短くする</h2>
<p>長いトランザクションはロック競合とスループット低下を招きます。</p>
<p>悪い例:</p>
<ol>
<li>DB更新</li>
<li>外部API呼び出し</li>
<li>メール送信</li>
<li>commit</li>
</ol>
<p>この順は危険です。外部I/Oをトランザクション外へ逃がします。</p>
<p>改善例:</p>
<ol>
<li>DB更新 + commit</li>
<li>外部通知は非同期ジョブで実行</li>
</ol>
<p>これだけで同時処理性能が目に見えて改善します。</p>
<h2 id="7-インデックス設計をapi単位で見直す">7. インデックス設計をAPI単位で見直す</h2>
<p>「インデックスはある」だけでは不足です。実際のWHERE + ORDER BYに合っているかが重要です。</p>
<p>例: 注文履歴API</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> id, total_amount, created_at
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> orders
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> user_id <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">1</span> <span style="color:#66d9ef">AND</span> status <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;paid&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> created_at <span style="color:#66d9ef">DESC</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LIMIT</span> <span style="color:#ae81ff">50</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>この場合、次の複合indexが有効です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> CONCURRENTLY idx_orders_user_status_created_at_desc
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> orders (user_id, status, created_at <span style="color:#66d9ef">DESC</span>);
</span></span></code></pre></td></tr></table>
</div>
</div><p>単独indexを乱立させるより、アクセスパターンに合わせた複合indexを厳選した方が効きます。</p>
<h2 id="8-キャッシュ導入は遅い理由の解決後に行う">8. キャッシュ導入は「遅い理由の解決後」に行う</h2>
<p>キャッシュは万能ではありません。N+1やスロークエリを放置したまま載せると、整合性事故の温床になります。</p>
<p>導入順序:</p>
<ol>
<li>SQL最適化</li>
<li>接続プール調整</li>
<li>必要なエンドポイントに限定してRedis cache</li>
</ol>
<p>キャッシュキーは <code>resource:id:version</code> 形式にし、更新時の無効化戦略を先に定義してください。</p>
<h2 id="9-負荷試験シナリオk6例">9. 負荷試験シナリオ（k6例）</h2>
<p>最適化の成果は負荷試験で確認します。最低3シナリオが必要です。</p>
<ul>
<li>steady: 通常トラフィック</li>
<li>burst: 短時間ピーク</li>
<li>soak: 長時間連続実行（リーク検知）</li>
</ul>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">http</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;k6/http&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">check</span>, <span style="color:#a6e22e">sleep</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;k6&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">options</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">stages</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">duration</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;2m&#39;</span>, <span style="color:#a6e22e">target</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">50</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">duration</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;5m&#39;</span>, <span style="color:#a6e22e">target</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">50</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">duration</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;1m&#39;</span>, <span style="color:#a6e22e">target</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">200</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">duration</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;2m&#39;</span>, <span style="color:#a6e22e">target</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">0</span> },
</span></span><span style="display:flex;"><span>  ],
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> () {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">res</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#39;https://api.example.com/orders?limit=50&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">check</span>(<span style="color:#a6e22e">res</span>, { <span style="color:#e6db74">&#39;status 200&#39;</span><span style="color:#f92672">:</span> (<span style="color:#a6e22e">r</span>) =&gt; <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">status</span> <span style="color:#f92672">===</span> <span style="color:#ae81ff">200</span> });
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">sleep</span>(<span style="color:#ae81ff">1</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>改善前後で <code>p95</code>, SQL回数, DB CPU を比較し、定量で判断します。</p>
<h2 id="10-本番での改善手順テンプレート">10. 本番での改善手順テンプレート</h2>
<ol>
<li>ボトルネックendpointの特定</li>
<li>SQLログとEXPLAIN ANALYZE取得</li>
<li>N+1解消</li>
<li>必要列取得へ変更</li>
<li>インデックス追加（CONCURRENTLY）</li>
<li>pool設定調整</li>
<li>負荷試験再実施</li>
<li>段階リリース（10%→50%→100%）</li>
</ol>
<p>この手順を運用チーム全体でテンプレ化すると、パフォーマンス問題への対応速度が上がります。</p>
<h2 id="まとめ">まとめ</h2>
<p>FastAPI + SQLAlchemyの性能改善は、派手なテクニックより <strong>計測→原因分離→小さく改善</strong> の積み重ねが効きます。</p>
<ul>
<li>N+1解消</li>
<li>過剰フェッチ削減</li>
<li>接続プール最適化</li>
<li>インデックスの再設計</li>
<li>負荷試験で再検証</li>
</ul>
<p>この5点を回せば、遅いAPIは高確率で改善できます。まずは「1リクエストあたりのSQL発行数」を可視化するところから始めるのが最短です。</p>
<h2 id="付録-改善施策の優先順位最短で効く順">付録: 改善施策の優先順位（最短で効く順）</h2>
<p>時間が限られる現場では、まず「SQL発行回数削減 → インデックス最適化 → connection pool調整」の順で着手すると効果が出やすいです。特に、N+1解消だけでp95が半減するケースは珍しくありません。改善後は必ず同一負荷条件で再計測し、数字で効果を残しておくと、次の改善投資判断が通りやすくなります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>FastAPI</category>
      <category>SQLAlchemy</category>
      <category>PostgreSQL</category>
      <category>Python</category>
      <category>Performance</category>
    </item>
  </channel>
</rss>
