<?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>Index on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/index/</link>
    <description>Recent content in Index on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Fri, 27 Feb 2026 13:00:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/index/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>PostgreSQLインデックス最適化の現場手順：遅いクエリを再現・診断・改善する実践プレイブック</title>
      <link>https://www.ai2core.com/posts/2026-02-27-postgresql-indexing-production-playbook/</link>
      <pubDate>Fri, 27 Feb 2026 13:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-27-postgresql-indexing-production-playbook/</guid>
      <description>EXPLAIN ANALYZEの読み方から複合/部分/式インデックスの使い分け、リリース手順までを実例ベースで解説。</description>
      <content:encoded><![CDATA[<h1 id="postgresqlインデックス最適化の現場手順遅いクエリを再現診断改善する実践プレイブック">PostgreSQLインデックス最適化の現場手順：遅いクエリを再現・診断・改善する実践プレイブック</h1>
<p>「CPUは余っているのに画面が遅い」「特定時間帯だけ API が詰まる」。この手の問題の多くは、アプリではなく SQL の実行計画に原因があります。特に PostgreSQL では、インデックス設計と統計情報の状態が性能をほぼ決めます。</p>
<p>本記事では、実務で使う手順に沿って、遅延クエリの改善を再現可能な形で解説します。単なる理論紹介ではなく、<strong>調査順序、判断基準、リリース時の注意点</strong>まで含めてまとめます。</p>
<h2 id="まず守るべき3原則">まず守るべき3原則</h2>
<ol>
<li><strong>推測でインデックスを作らない</strong>
体感で追加すると write 性能とストレージが悪化します。必ず実行計画を見てから判断します。</li>
<li><strong>改善前後を数値で比較する</strong>
P95、rows、shared read blocks を記録し、効果を証明します。</li>
<li><strong>本番反映は CONCURRENTLY を基本にする</strong>
テーブルロックで事故らないため、<code>CREATE INDEX CONCURRENTLY</code> を優先します。</li>
</ol>
<h2 id="ケース設定注文一覧apiが遅い">ケース設定：注文一覧APIが遅い</h2>
<p>次のクエリが遅いとします。</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></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, user_id, status, 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> tenant_id <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> status <span style="color:#66d9ef">IN</span> (<span style="color:#e6db74">&#39;paid&#39;</span>, <span style="color:#e6db74">&#39;shipped&#39;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> created_at <span style="color:#f92672">&gt;=</span> NOW() <span style="color:#f92672">-</span> INTERVAL <span style="color:#e6db74">&#39;30 days&#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>データ量は <code>orders</code> 1.2億件、1テナントあたり数百万件。現象は「特定テナントだけ 3〜6 秒」です。</p>
<h2 id="手順1pg_stat_statementsで優先度をつける">手順1：pg_stat_statementsで優先度をつける</h2>
<p>まずは遅い順ではなく、**影響度順（総時間）**で見るのが現場では正解です。</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></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> queryid, calls, total_exec_time, mean_exec_time, <span style="color:#66d9ef">rows</span>, query
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> pg_stat_statements
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> total_exec_time <span style="color:#66d9ef">DESC</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LIMIT</span> <span style="color:#ae81ff">20</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>ここで対象クエリの <code>calls</code> が多く、<code>mean_exec_time</code> が高いことを確認。改善効果が大きいと判断できます。</p>
<h2 id="手順2explain-analyze-buffersでボトルネックを特定">手順2：EXPLAIN ANALYZE BUFFERSでボトルネックを特定</h2>
<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">EXPLAIN</span> (<span style="color:#66d9ef">ANALYZE</span>, BUFFERS, <span style="color:#66d9ef">VERBOSE</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> ...;
</span></span></code></pre></td></tr></table>
</div>
</div><p>典型的な悪い例は次の通りです。</p>
<ul>
<li><code>Seq Scan on orders</code></li>
<li><code>Rows Removed by Filter</code> が極端に多い</li>
<li><code>Sort Method: external merge Disk</code>（メモリ不足でディスクソート）</li>
</ul>
<p>この状態では、絞り込み条件に合うインデックスが不足しています。</p>
<h2 id="手順3最小コストで効くインデックス設計">手順3：最小コストで効くインデックス設計</h2>
<p>今回の条件は <code>tenant_id</code>, <code>status</code>, <code>created_at</code> です。ORDER BY も <code>created_at DESC</code>。したがって候補は次です。</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_tenant_status_created_at_desc
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> orders (tenant_id, status, created_at <span style="color:#66d9ef">DESC</span>);
</span></span></code></pre></td></tr></table>
</div>
</div><p>ここで順序が重要です。先頭列は等価条件（tenant_id）、次に低カーディナリティ条件（status）、最後に範囲・並び替え列（created_at）を置きます。</p>
<h3 id="部分インデックスの検討">部分インデックスの検討</h3>
<p><code>status</code> が多数あるが実際に使うのが paid/shipped だけなら、部分インデックスでさらに削減できます。</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></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_recent_paid_shipped
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> orders (tenant_id, created_at <span style="color:#66d9ef">DESC</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> status <span style="color:#66d9ef">IN</span> (<span style="color:#e6db74">&#39;paid&#39;</span>, <span style="color:#e6db74">&#39;shipped&#39;</span>);
</span></span></code></pre></td></tr></table>
</div>
</div><p>この方式はサイズが小さく、キャッシュ効率が高いのが利点です。</p>
<h2 id="手順4改善効果を検証">手順4：改善効果を検証</h2>
<p>同一条件で再度 <code>EXPLAIN ANALYZE</code> を実施します。</p>
<p>確認ポイント:</p>
<ul>
<li><code>Index Scan</code> か <code>Bitmap Heap Scan</code> に変わっているか</li>
<li>実行時間が目標値（例: 200ms 未満）に入ったか</li>
<li>shared read blocks が大幅に減ったか</li>
<li><code>rows=50</code> を早期に取り出せているか</li>
</ul>
<p>改善後に 4.2 秒 → 120ms 程度まで落ちるケースは珍しくありません。</p>
<h2 id="それでも遅い場合の追加施策">それでも遅い場合の追加施策</h2>
<h3 id="1-カバリングインデックスinclude">1) カバリングインデックス（INCLUDE）</h3>
<p>取得列が多いとテーブルアクセスが残ります。PostgreSQL では <code>INCLUDE</code> が使えます。</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></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_covering
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> orders (tenant_id, status, created_at <span style="color:#66d9ef">DESC</span>)
</span></span><span style="display:flex;"><span>INCLUDE (total_amount, user_id);
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="2-統計情報の更新">2) 統計情報の更新</h3>
<p>データ偏りが強いと planner が誤判定します。</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></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">ANALYZE</span> orders;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> orders <span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">COLUMN</span> status <span style="color:#66d9ef">SET</span> <span style="color:#66d9ef">STATISTICS</span> <span style="color:#ae81ff">1000</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ANALYZE</span> orders;
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="3-パーティショニング">3) パーティショニング</h3>
<p>30日検索が多いなら、月次パーティションで読み取り範囲を削るのも有効です。既存移行はコストが高いので、まずは新規データから段階導入します。</p>
<h2 id="リリース時の安全手順">リリース時の安全手順</h2>
<p>本番では速度改善より安全性が優先です。次の順番を守ると事故が減ります。</p>
<ol>
<li>負荷が低い時間帯を選ぶ</li>
<li><code>CREATE INDEX CONCURRENTLY</code> を実行</li>
<li>進捗確認: <code>pg_stat_progress_create_index</code></li>
<li>完了後に代表クエリで実行計画を確認</li>
<li>監視（CPU、I/O、lock wait、replication lag）を 30 分観察</li>
<li>不要化した旧インデックスは別日に削除</li>
</ol>
<p>いきなり削除しない理由は、想定外クエリで回帰する可能性があるためです。1〜2日観測してから <code>DROP INDEX CONCURRENTLY</code> するのが安定運用です。</p>
<h2 id="アンチパターン集">アンチパターン集</h2>
<ul>
<li><code>LIKE '%keyword%'</code> に B-tree インデックスを貼る
<ul>
<li>→ pg_trgm + GIN を使う</li>
</ul>
</li>
<li>すべての列に単体インデックスを作る
<ul>
<li>→ planner が迷う、write コスト増</li>
</ul>
</li>
<li>UUID主キーだけ見て満足する
<ul>
<li>→ 実際の検索条件列を優先</li>
</ul>
</li>
<li>autovacuum 設定を放置
<ul>
<li>→ bloat 増で index scan が遅くなる</li>
</ul>
</li>
</ul>
<h2 id="計測テンプレート運用向け">計測テンプレート（運用向け）</h2>
<p>改善作業を属人化しないために、次のテンプレートで記録すると再利用できます。</p>
<ul>
<li>対象クエリID（pg_stat_statements）</li>
<li>改善前 mean/P95</li>
<li>改善前実行計画（テキスト保存）</li>
<li>追加したインデックスDDL</li>
<li>改善後 mean/P95</li>
<li>副作用（write増、ストレージ増、vacuum時間）</li>
<li>ロールバック手順</li>
</ul>
<p>このフォーマットをWiki化しておくと、次回の性能障害対応が非常に速くなります。</p>
<h2 id="まとめ">まとめ</h2>
<p>PostgreSQL の性能改善は、魔法のパラメータよりも「再現・診断・検証」の手順で決まります。特にインデックスは効果が大きい反面、副作用もあるため、実行計画と計測値で判断することが重要です。</p>
<p>遅延問題に直面したら、まず <code>pg_stat_statements</code> で対象を絞り、<code>EXPLAIN ANALYZE BUFFERS</code> で事実を取り、<code>CONCURRENTLY</code> で安全に改善する。この流れをチーム標準にすれば、DB運用の安定性は確実に上がります。</p>
<h2 id="現場で使うトラブルシュート手順夜間障害対応向け">現場で使うトラブルシュート手順（夜間障害対応向け）</h2>
<p>実際の障害対応では、理想的な調査順序を守れないことがあります。そこで夜間当番でも使える短縮手順を用意しておくと有効です。</p>
<ol>
<li>まず <code>pg_stat_activity</code> で待機イベントを確認（lock か I/O か）</li>
<li>次に <code>pg_locks</code> で競合トランザクションを特定</li>
<li>対象クエリの <code>EXPLAIN (ANALYZE, BUFFERS)</code> を取得</li>
<li>直近デプロイ差分（SQL/マイグレーション）を確認</li>
<li>即効性のある一時回避（statement timeout、read replica 振り分け）を実施</li>
</ol>
<p>短期回避後に恒久対策を行う、という二段運用が安定します。</p>
<h3 id="ロック競合の例">ロック競合の例</h3>
<p><code>ALTER TABLE</code> と長時間 SELECT が競合すると、アプリの体感遅延が一気に悪化します。マイグレーションは <code>LOCK TIMEOUT</code> を短く設定し、失敗時に即リトライしない設計にしましょう。</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">SET</span> lock_timeout <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;2s&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SET</span> statement_timeout <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;30s&#39;</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="クエリヒントが使えない前提での工夫">クエリヒントが使えない前提での工夫</h3>
<p>PostgreSQL は MySQL のようなヒント句が限定的なため、実行計画の誘導は以下で行います。</p>
<ul>
<li>統計情報を正しく更新</li>
<li>不要な関数適用を避ける（索引利用阻害）</li>
<li>OR 条件を UNION ALL 分割で単純化</li>
</ul>
<p>例えば <code>WHERE date(created_at) = CURRENT_DATE</code> は 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">WHERE</span> created_at <span style="color:#f92672">&gt;=</span> <span style="color:#66d9ef">CURRENT_DATE</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> created_at <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">CURRENT_DATE</span> <span style="color:#f92672">+</span> INTERVAL <span style="color:#e6db74">&#39;1 day&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>この1点だけで scan 範囲が激減することがあります。</p>
<h2 id="チーム運用に落とし込むためのルール">チーム運用に落とし込むためのルール</h2>
<p>最後に、性能改善を個人依存にしないための運用ルールを提案します。</p>
<ul>
<li>新規API追加時は必ず「想定SQL」と「必要インデックス案」を設計レビューに含める</li>
<li>週次で slow query 上位10件を確認し、改善オーナーを割り当てる</li>
<li>重要テーブルの index hit ratio と bloat 率を定期監視する</li>
</ul>
<p>この運用が回ると、障害対応だけでなく機能開発の速度も上がります。DB 性能は裏方ではなく、プロダクト体験の中心です。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>PostgreSQL</category>
      <category>Performance</category>
      <category>Index</category>
      <category>Database</category>
    </item>
  </channel>
</rss>
