<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:webfeeds="http://webfeeds.org/rss/1.0">
  <title>DB カテゴリ | フューチャー技術ブログ</title>
  <subtitle>DB カテゴリの記事一覧</subtitle>
  <icon>https://future-architect.github.io/feed_icon.png</icon>
  <logo>https://future-architect.github.io/apple-touch-icon.png</logo>
  <webfeeds:icon>https://future-architect.github.io/apple-touch-icon.png</webfeeds:icon>
  <webfeeds:accentColor>258fb8</webfeeds:accentColor>
  <link href="https://future-architect.github.io/categories/DB/atom.xml" rel="self"/>
  <link href="https://future-architect.github.io/categories/DB/"/>
  <updated>2026-08-16T15:00:00.000Z</updated>
  <id>https://future-architect.github.io/categories/DB/</id>
  <generator uri="https://hexo.io/">Hexo</generator>
  <entry>
    <title>SQLフォーマッター uroborosql-fmt がNeovim/Eclipseなど各種エディタに対応 ― 言語サーバーとβ版リンターを公開しました🎉</title>
    <link href="https://future-architect.github.io/articles/20260817a/"/>
    <id>https://future-architect.github.io/articles/20260817a/</id>
    <published>2026-08-16T15:00:00.000Z</published>
    <updated>2026-08-16T15:00:00.000Z</updated>
    <author><name>仲泰志</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><img fetchpriority="high" src="/images/2026/20260817a/top.png" alt="" width="630" height="229">

<p>コアテクノロジーグループでアルバイトをしている仲です。</p>
<p>先日、SQL フォーマッターである uroborosql-fmt の新たなアップデートとして、言語サーバーおよびβ版のリンター機能をリリースしました 🎉</p>
<p>uroborosql-fmt は、当社が公開している PostgreSQL 向けの SQL コーディング規約に基づいて SQL 文をフォーマットするツールです。これまでの歩みについては以下の過去記事で詳しく解説しています。</p>
<ul>
<li>Pure Rustで生まれ変わったPostgreSQL公式構文準拠SQLフォーマッター「uroborosql-fmt」をリリース🎉 | フューチャー技術ブログ</li>
</ul>
<p>本記事では、言語サーバー提供の背景や利用方法、β 版リンターについてご紹介します！</p>
<h2 id="言語サーバー開発の背景">言語サーバー開発の背景</h2><p>uroborosql-fmt では、フォーマッターに加えて SQL 向けリンターの開発を進めています。</p>
<p>SQL における不具合や性能劣化は、性能試験や本番運用の段階で発覚して事後的な対応となることがしばしばあります。原因のひとつとしては、本番環境のように量や種類の豊富なデータを使ったテストの実施が開発時には難しいという構造的な問題があるでしょう。</p>
<p>一方で、インデックスが効かない書き方や NULL の扱いに起因する不具合など、SQL 文がもつ問題の一部は実行前にアンチパターンとして発見できることがあります。当社の SQL コーディング規約にはこのようなアンチパターンの回避を目的としたルールが多数含まれています。</p>
<p>現在開発を進めているリンターは、対象の SQL 文がこうしたルールに沿っているかを機械的にチェックするツールです。レビューや実行を待たずに開発中の SQL の問題を検出することで、不具合や性能劣化の未然防止につなげることを目的としています。</p>
<p>リンターの提供にあたっては、CI や AI エージェントから使いやすい CLI に加えて、エディタとの連携も実現したいと考えました。コード解析の結果をエディタへ通知する一般的な方法として LSP（Language Server Protocol）があります。今回はこの LSP に準拠した言語サーバーを開発・リリースしました。</p>
<h2 id="言語サーバーの機能について">言語サーバーの機能について</h2><p>2026年7月現在、この言語サーバーは以下の機能を提供しています。</p>
<ul>
<li>Document Formatting: ファイル全体のフォーマット</li>
<li>Document Range Formatting: 選択範囲のフォーマット</li>
<li>Diagnostics (Linting): 構文エラーや規約違反のエディタ上警告</li>
<li>Code Actions (QuickFix): リンター警告の表示制御</li>
</ul>
<h2 id="エディタ別セットアップ例">エディタ別セットアップ例</h2><p>主要なエディタでの利用方法についてご紹介します。</p>
<p>なお、リンター機能を有効にするには設定ファイル <code>.uroborosqllintrc.json</code> が必要です（後述の「設定と実行」を参照）。フォーマット機能は設定ファイルなしでも利用できます。</p>
<h3 id="VS-Code">VS Code</h3><p>VS Code ではこれまでと変わらず 拡張機能（v2.1.0 以降）から利用できます。<br>手動で言語サーバーをインストールする必要はありません。</p>
<p>また、VS Code 拡張限定の機能として、新たに <code>Format Selection as SQL</code> コマンドを提供しています。<br>SQL 以外のファイルに埋め込まれた SQL（たとえば TypeScript のテンプレートリテラル内の SQL）を選択してこのコマンドを実行すると、選択範囲を SQL としてフォーマットし、その範囲だけを置き換えます。<br>標準の LSP 機能ではなくカスタムメソッドを利用して実現しているため、VS Code 拡張でのみ利用できます。</p>
<h3 id="その他のエディタ">その他のエディタ</h3><p>言語サーバーの提供により、uroborosql-fmt のフォーマット機能とリンターは LSP クライアントを持つ任意のエディタで利用できるようになりました。VS Code 以外のエディタで利用する場合は、まず言語サーバー本体をインストールします：</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-qpkvyu-1" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-qpkvyu-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">cargo install --git https://github.com/future-architect/uroborosql-fmt uroborosql-language-server</span><br></pre></td></tr></table></figure></div>

<p>Rust の環境を用意したくない場合は、GitHub Releases から Linux &#x2F; Windows &#x2F; macOS 向けのビルド済みバイナリを直接ダウンロードしてご利用ください。</p>
<p>インストールしたバイナリは標準入出力で LSP 通信をするため、各エディタの LSP クライアントからコマンド名（<code>uroborosql-language-server</code>）を指定するだけで利用できます。</p>
<h4 id="Neovim">Neovim</h4><p>Neovim 組み込み LSP クライアントの設定例です。</p>
<figure class="highlight lua"><table><tr><td class="code"><pre><span class="line">vim.api.nvim_create_autocmd(<span class="string">&quot;FileType&quot;</span>, &#123;</span><br><span class="line">  pattern = <span class="string">&quot;sql&quot;</span>,</span><br><span class="line">  callback = <span class="function"><span class="keyword">function</span><span class="params">(args)</span></span></span><br><span class="line">    <span class="keyword">local</span> root = vim.fs.root(args.buf, &#123;</span><br><span class="line">      <span class="string">&quot;.uroborosqllintrc.json&quot;</span>,</span><br><span class="line">      <span class="string">&quot;.uroborosqlfmtrc.json&quot;</span>,</span><br><span class="line">    &#125;) <span class="keyword">or</span> vim.uv.cwd()</span><br><span class="line"></span><br><span class="line">    vim.lsp.start(&#123;</span><br><span class="line">      name = <span class="string">&quot;uroborosql-language-server&quot;</span>,</span><br><span class="line">      cmd = &#123; <span class="string">&quot;uroborosql-language-server&quot;</span> &#125;,</span><br><span class="line">      root_dir = root,</span><br><span class="line">    &#125;)</span><br><span class="line">  <span class="keyword">end</span>,</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<h4 id="Emacs">Emacs</h4><p>Eglot を使用する場合の設定例です。</p>
<div class="code-block"><figure class="highlight plaintext"><input type="checkbox" id="code-wrap-qpkvyu-2" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-qpkvyu-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">(require &#x27;eglot)</span><br><span class="line"></span><br><span class="line">(add-to-list &#x27;eglot-server-programs</span><br><span class="line">             &#x27;(sql-mode . (&quot;uroborosql-language-server&quot;)))</span><br><span class="line"></span><br><span class="line">(add-hook &#x27;sql-mode-hook #&#x27;eglot-ensure)</span><br><span class="line"></span><br><span class="line">(setq eglot-autoshutdown t)</span><br></pre></td></tr></table></figure></div>

<h4 id="Eclipse">Eclipse</h4><p>Eclipse では、Eclipse 公式の LSP クライアントである LSP4E を導入することで利用できます。LSP4E には設定を GUI から行う仕組みがあるため、プラグインを自作することなく言語サーバーを登録できます。</p>
<p>まず LSP4E をインストールします。<code>Help &gt; Install New Software...</code> を開き、<code>Work with</code> に以下の更新サイトを入力します。</p>
<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-qpkvyu-3" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-qpkvyu-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">https://download.eclipse.org/lsp4e/releases/latest/</span><br></pre></td></tr></table></figure></div>

<p>一覧に表示される <code>Eclipse LSP4E</code> を選択してインストールし、Eclipse を再起動します。お使いの Eclipse パッケージにすでに LSP4E が含まれている場合、この手順は不要です。</p>
<p>続いて、以下の手順で言語サーバーを登録します。</p>
<ol>
<li>SQL 用の content-type を用意する<ul>
<li><code>Preferences &gt; General &gt; Content Types</code> で、<code>*.sql</code> に関連付けられた content-type があるかを確認します。無い場合は <code>Text</code> の下に子の content-type を追加し、File associations に <code>*.sql</code> を登録します。content-type は <code>Text</code> の子孫である必要があります。</li>
</ul>
</li>
<li>言語サーバーの起動設定を作成する<ul>
<li><code>Run &gt; External Tools &gt; External Tools Configurations...</code> で <code>Program</code> の構成を新規作成し、Location にインストールした <code>uroborosql-language-server</code> の実行ファイルパスを指定します。引数は不要です。</li>
</ul>
</li>
<li>content-type と起動設定を関連付ける<ul>
<li><code>Preferences &gt; Language Servers</code> で <code>Add...</code> を選び、左側で手順 1 の content-type を、右側で手順 2 の起動設定を選択します。</li>
</ul>
</li>
<li>SQL ファイルを Generic Editor で開く<ul>
<li>対象ファイルを右クリックし、<code>Open With &gt; Generic Editor</code> を選択します。すでに別のエディタで開いている場合は、一度閉じてから開き直してください。</li>
</ul>
</li>
</ol>
<p>以上で、Problems ビューおよびエディタ上へのリンター警告の表示と、フォーマットが利用できるようになります。なお、リンターを有効にするにはプロジェクトのルートに <code>.uroborosqllintrc.json</code> を配置してください。</p>
<h4 id="JetBrains-系-IDE">JetBrains 系 IDE</h4><p>JetBrains 系 IDE では、Red Hat が提供している LSP4IJ プラグインを導入することで利用できます。</p>
<h2 id="SQL-リンター（β版）のご紹介">SQL リンター（β版）のご紹介</h2><p>今回のリリースに含まれる SQL リンター（<code>uroborosql-lint</code>）についても紹介します。<br>このリンターは、SQL コーディング規約に含まれるアンチパターン回避のルールに沿っているか等をチェックすることで、不具合や性能劣化の未然防止をめざすツールです。</p>
<figure><img src="/images/2026/20260817a/lint-language-server-demo.gif" alt="" width="640" height="360" loading="lazy"><figcaption>エディタ上でリンター警告が表示される様子</figcaption></figure>
<p>現在は β 版のため、バグや仕様変更の可能性があります。</p>
<h3 id="現時点のルール">現時点のルール</h3><p>現在は以下 7 つのルールを実装しています。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>ルール名</th>
<th>内容</th>
</tr>
</thead>
<tbody><tr>
<td><code>no-distinct</code></td>
<td><code>DISTINCT</code> の使用を警告</td>
</tr>
<tr>
<td><code>no-wildcard-projection</code></td>
<td><code>SELECT *</code> などのワイルドカード利用を警告</td>
</tr>
<tr>
<td><code>no-not-in</code></td>
<td><code>NOT IN</code> の使用を警告</td>
</tr>
<tr>
<td><code>no-union-distinct</code></td>
<td><code>UNION DISTINCT</code>（暗黙の <code>UNION</code> を含む）を警告</td>
</tr>
<tr>
<td><code>no-function-on-column-in-join-or-where</code></td>
<td>JOIN &#x2F; WHERE 条件でのカラムへの関数適用を警告</td>
</tr>
<tr>
<td><code>too-large-in-list</code></td>
<td>要素数が多すぎる <code>IN</code> リストを警告</td>
</tr>
<tr>
<td><code>missing-two-way-sample</code></td>
<td>2WaySQL のバインドパラメータのサンプル値抜けを警告</td>
</tr>
</tbody></table></div>
<p>ここではいくつかのルールについて紹介します。</p>
<h4 id="no-function-on-column-in-join-or-where"><code>no-function-on-column-in-join-or-where</code></h4><p><code>no-function-on-column-in-join-or-where</code> ルールは、JOIN や WHERE の条件でカラムに関数を適用している箇所を検出します。インデックスカラムへ関数を適用しているケースの検出を念頭に置いたルールです。（コーディング規約）</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-qpkvyu-4" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-qpkvyu-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- NG: カラムに関数を適用しており、インデックスが効かない</span></span><br><span class="line"><span class="keyword">SELECT</span> id <span class="keyword">FROM</span> orders <span class="keyword">WHERE</span> to_char(created_at, <span class="string">&#x27;YYYYMMDD&#x27;</span>) <span class="operator">=</span> <span class="string">&#x27;20260101&#x27;</span>;</span><br><span class="line"><span class="comment">--                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></span><br><span class="line"><span class="comment">--                          Functions in JOIN or WHERE conditions can prevent index usage; rewrite without wrapping the column. created_at (no-function-on-column-in-join-or-where)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- OK: 定数のみに関数を適用する</span></span><br><span class="line"><span class="keyword">SELECT</span> id <span class="keyword">FROM</span> orders <span class="keyword">WHERE</span> created_at <span class="operator">&gt;=</span> to_date(<span class="string">&#x27;20260101&#x27;</span>, <span class="string">&#x27;YYYYMMDD&#x27;</span>);</span><br></pre></td></tr></table></figure></div>

<h4 id="no-not-in"><code>no-not-in</code></h4><p><code>no-not-in</code> ルールは、<code>NOT IN</code> の使用を検出します。<code>NOT IN</code> はサブクエリの結果に <code>NULL</code> が 1 つでも含まれると結果が 1 行も返らなくなるという落とし穴が知られています。代わりに <code>NOT EXISTS</code> の利用を促すルールです。（コーディング規約）</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-qpkvyu-5" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-qpkvyu-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- NG: orders.user_id に NULL が 1 件でもあると、このクエリは 1 行も返さない</span></span><br><span class="line"><span class="keyword">SELECT</span> id <span class="keyword">FROM</span> users <span class="keyword">WHERE</span> id <span class="keyword">NOT</span> <span class="keyword">IN</span> (<span class="keyword">SELECT</span> user_id <span class="keyword">FROM</span> orders);</span><br><span class="line"><span class="comment">--                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></span><br><span class="line"><span class="comment">--                            Avoid using NOT IN; prefer NOT EXISTS or other alternatives to handle NULL correctly. (no-not-in)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- OK: NOT EXISTS なら NULL があっても意図どおりに動く</span></span><br><span class="line"><span class="keyword">SELECT</span> id <span class="keyword">FROM</span> users</span><br><span class="line"><span class="keyword">WHERE</span> <span class="keyword">NOT</span> <span class="keyword">EXISTS</span> (<span class="keyword">SELECT</span> <span class="number">1</span> <span class="keyword">FROM</span> orders <span class="keyword">WHERE</span> orders.user_id <span class="operator">=</span> users.id);</span><br></pre></td></tr></table></figure></div>

<h4 id="missing-two-way-sample"><code>missing-two-way-sample</code></h4><p><code>missing-two-way-sample</code> ルールは、2WaySQL をターゲットとしている uroboroSQL ならではのルールです。</p>
<p>2WaySQL とは、SQL クライアントツールでそのまま実行でき、プログラムからも実行できる形式の SQL 文のことです。</p>
<p>SQL 実行ライブラリ uroboroSQL では、<code>/*user_id*/10</code> のようにコメント形式でバインドパラメータ（<code>/*user_id*/</code>）を指定し、直後に対応するサンプル値（<code>10</code>）を記述します。</p>
<p>プログラムから実行する際にはサンプル値が除去されてパラメータのバインドが行われる一方で、SQL クライアントツールから直接実行する際にはサンプル値がそのまま使われるため、どちらの方法でも実行できるという仕組みです。</p>
<p>このサンプル値が抜けていると SQL としての直接実行ができなくなってしまうため、本ルールはサンプル値が書かれていないバインドパラメータを検出し警告します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-qpkvyu-6" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-qpkvyu-6" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- NG: バインドパラメータ（/*user_id*/）に対応するサンプル値が欠けている</span></span><br><span class="line"><span class="keyword">SELECT</span> id <span class="keyword">FROM</span> users <span class="keyword">WHERE</span> id <span class="operator">=</span> <span class="comment">/*user_id*/</span>;</span><br><span class="line"><span class="comment">--                                        ^^</span></span><br><span class="line"><span class="comment">--                                        Sample value for bind parameter is missing. (missing-two-way-sample)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- OK: バインドパラメータに対するサンプル値がある</span></span><br><span class="line"><span class="keyword">SELECT</span> id <span class="keyword">FROM</span> users <span class="keyword">WHERE</span> id <span class="operator">=</span> <span class="comment">/*user_id*/</span><span class="number">10</span>;</span><br></pre></td></tr></table></figure></div>

<h3 id="利用方法">利用方法</h3><p>リンターは CLI または言語サーバー経由（VS Code 拡張を含む）で利用できます。言語サーバー経由での利用は前述の「エディタ別セットアップ例」をご覧ください。</p>
<p>CLI は、CI や AI エージェントの hook に組み込んでチェックを自動化する用途におすすめです。以下では CLI での利用方法を紹介します。</p>
<h4 id="インストール">インストール</h4><p>Cargo でインストールできます：</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-qpkvyu-7" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-qpkvyu-7" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">cargo install --git https://github.com/future-architect/uroborosql-fmt uroborosql-lint-cli</span><br></pre></td></tr></table></figure></div>

<p>Rust の環境を用意したくない場合は、GitHub Releases から Linux &#x2F; Windows &#x2F; macOS 向けのビルド済みバイナリを直接ダウンロードしてご利用ください。</p>
<h4 id="設定と実行">設定と実行</h4><p>リンターの実行には設定ファイル <code>.uroborosqllintrc.json</code> が必要です（言語サーバー経由の場合も、このファイルがあるときのみリンターが有効になります）。<code>--init</code> でひな形を生成できます：</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-qpkvyu-8" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-qpkvyu-8" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">uroborosql-lint --init      <span class="comment"># .uroborosqllintrc.json を生成</span></span><br><span class="line">uroborosql-lint query.sql   <span class="comment"># lint 実行</span></span><br></pre></td></tr></table></figure></div>

<p>設定ファイルの書き方やディレクティブコメントによる警告の抑制、CLI のオプションなどの詳細はドキュメントをご覧ください。</p>
<h2 id="今後の展望">今後の展望</h2><p>今後は安定版のリリースに向けてリンターの開発を進めていく予定です。</p>
<p>現在は SQL のテキストのみを利用するルールしか提供していませんが、今後はデータベースのカタログ情報を参照するルールを追加していく予定です。実際のインデックス定義やテーブル定義を踏まえた検出ができるようになれば、性能劣化の未然防止という目的にさらに近づけると考えています。</p>
<p>また、現状では限定的にしかサポートできていない 2WaySQL 対応の拡充を予定しています。</p>
<h2 id="さいごに">さいごに</h2><p>本記事では、uroborosql-fmt の言語サーバー提供と SQL リンター（β 版）についてご紹介しました。VS Code 以外のエディタをお使いの方も、ぜひ uroborosql-fmt を試してみてください。</p>
<p>不具合の報告や機能の要望など、GitHub リポジトリの Issue にてお気軽にお寄せください。</p>
]]></content>
    <summary type="html">SQLフォーマッター uroborosql-fmt の言語サーバーとβ版リンターをリリースしました。Neovim / Emacs / Eclipse など各種エディタでのセットアップ方法と、SQLコーディング規約に基づくリンターのルールを紹介します。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="2WaySQL" scheme="https://future-architect.github.io/tags/2WaySQL/"/>
    <category term="LSP" scheme="https://future-architect.github.io/tags/LSP/"/>
    <category term="Linter" scheme="https://future-architect.github.io/tags/Linter/"/>
    <category term="uroboroSQL" scheme="https://future-architect.github.io/tags/uroboroSQL/"/>
    <category term="フォーマッター" scheme="https://future-architect.github.io/tags/%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%82%BF%E3%83%BC/"/>
  </entry>
  <entry>
    <title>PostgreSQLユーザーがSQL Server開発で知っておきたかった実践Tips10選</title>
    <link href="https://future-architect.github.io/articles/20260512a/"/>
    <id>https://future-architect.github.io/articles/20260512a/</id>
    <published>2026-05-11T15:00:00.000Z</published>
    <updated>2026-05-11T15:00:00.000Z</updated>
    <author><name>辻大志郎</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2026/20260512a/top.jpg" alt="" width="800" height="422">

<p>春の入門祭り2026の9日目の記事です。</p>
<h2 id="はじめに">はじめに</h2><p>製造エネルギー事業部の辻です。ここ数年は PostgreSQL を中心に開発していましたが、最近 SQL Server（Azure SQL Database）を利用する機会がありました。</p>
<p>細かいところで、いくつか PostgreSQL と SQL Server で挙動が異なる点がありました。本記事では実運用を見据えた開発で役に立ちそうな少しディープなTipsをまとめました。</p>
<h2 id="1-照合順序は後から直せない">1. 照合順序は後から直せない</h2><p>SQL Server では、並び順・比較ルール・非 Unicode のコードページが照合順序に含まれます<sup id="fnref:1">1</sup>。たとえば <code>Japanese_CI_AS_KS_WS</code> のように、名前そのものが比較の挙動を表します。</p>
<ul>
<li><code>CI/CS</code>: 大文字小文字を区別しない&#x2F;する</li>
<li><code>AI/AS</code>: 濁点・半濁点を区別しない&#x2F;する</li>
<li><code>KS</code>: ひらがな&#x2F;カタカナを区別する</li>
<li><code>WS</code>: 全角&#x2F;半角を区別する</li>
</ul>
<p>SQL Serverにおける照合順序は、一度運用を始めてしまうと実質的に後から変更できず、変更時はデータベースの再作成になると考えておくべきです。これは、PostgreSQL の <code>lc_collate</code> や <code>lc_ctype</code> の変更に多大なコストがかかるのと同じです。どちらのDBであっても、照合順序は最初の設計段階で完全に確定させるべき項目です。</p>
<h2 id="2-varchar-nvarchar-の違いを意識し、適切に使い分ける">2. <code>varchar</code>&#x2F;<code>nvarchar</code> の違いを意識し、適切に使い分ける</h2><p>PostgreSQL では文字列型を UTF-8 前提で扱えるため、文字コードを意識する場面は多くありません。SQL Server では <code>varchar</code>（非 Unicode 系）と <code>nvarchar</code>（Unicode, UTF-16）の2種類があり、これらを区別して扱うよう意識する必要があります。</p>
<p>特に、アプリから接続する際のドライバ設定に気をつける必要があります。Java から Microsoft JDBC Driver for SQL Server を使う場合、<code>sendStringParametersAsUnicode=true</code>（デフォルト）<sup id="fnref:2">2</sup>により、パラメータが <code>nvarchar</code> として送られます。この状態でテーブル列が <code>varchar</code> だと、クエリごとに型変換が発生し、インデックスが効きにくくなります。プロジェクト初期に文字列型を統一すること（日本語を扱うなら原則 <code>nvarchar</code> など）、ドライバ設定と DB 定義をセットで設計レビューすることを考慮しておくことが重要です。</p>
<p>なお、<code>nvarchar</code> は UTF-16 でデータを保持するため、基本的には1文字あたり2バイトを消費します。<code>varchar</code> と比べてデータ容量やインデックスのサイズが大きくなるというトレードオフがある点には留意が必要です。</p>
<h2 id="3-varchar-n-の-n-は文字数ではなくバイト数である">3. <code>varchar(n)</code> の <code>n</code> は文字数ではなくバイト数である</h2><p>PostgreSQL における <code>varchar(n)</code> の <code>n</code> は「文字数」を意味し、全角・半角を問わず <code>n</code> 文字格納できます。</p>
<p>一方、SQL Server の <code>varchar(n)</code> における <code>n</code> はバイト数です<sup id="fnref:10">3</sup>。そのため SQL Server の <code>varchar(10)</code> には 10 バイト分しか格納できず、Shift_JIS 環境であれば全角 5 文字、SQL Server 2019 以降で利用できる UTF-8 照合順序環境であれば全角 3 文字で上限に達します。なお、同じ SQL Server でも <code>nvarchar(n)</code> の <code>n</code> は文字数（文字列長）ベースとなります。<code>nvarchar(10)</code> と定義した場合は、全角・半角を問わず最大 10 文字まで格納できます。</p>
<p>PostgreSQL の感覚で <code>varchar</code> を用いてテーブル設計すると、予期せぬ文字数超過によるエラーを招きやすくなります。日本語を扱う場合は、前述したとおり文字数ベースで直感的に扱える <code>nvarchar</code> を原則として利用するのが望ましいです。</p>
<h2 id="4-search-path-がない">4. <code>search_path</code> がない</h2><p>PostgreSQL の <code>search_path</code> は複数のスキーマを順番に探索する仕組みです。たとえば <code>myapp, public</code> のように設定しておけば、スキーマ名を省略したときにその順番でスキーマが解決されます。</p>
<p>一方、SQL Server にはこれに相当する複数スキーマ探索の仕組みはありません。代わりにあるのは、ユーザーごとに 1 つだけ設定されるデフォルトスキーマです。スキーマ名を省略した場合は、まずそのデフォルトスキーマが参照され、見つからなければSQL Server のデフォルトスキーマである <code>dbo</code> が参照されます。</p>
<p>SQL Server の <code>dbo</code> は PostgreSQL の <code>public</code> に相当します。原則として <code>public</code> と同様に、ユーザーが業務用オブジェクトを <code>dbo</code> に直接作成することは避け、アプリケーション用のスキーマを別途作成してそこに配置するのが望ましいです。</p>
<p>複数のスキーマを扱う場合は、スキーマをまたいだ参照が増えるほど管理が煩雑になるため、スキーマ間の依存関係を最小化する設計を意識することが重要です。スキーマをまたいだ参照が必要な場合は、シノニム（<code>CREATE SYNONYM</code>）を使ってアプリ側の SQL からスキーマを意識させない構成にする方法はよくあるナレッジです。</p>
<h2 id="5-パーティションは子テーブルではない">5. パーティションは子テーブルではない</h2><p>PostgreSQL のパーティションは子テーブルとして独立しており、子テーブル名を直接指定してクエリ実行もできます。一方、SQL Server のパーティションはあくまでテーブル内部の分割であり、外からは 1 つのテーブルとしか見えません。パーティションを個別に参照したい場合は <code>$PARTITION</code> 関数<sup id="fnref:3">4</sup>を使う必要があり、子テーブルを直接クエリする感覚では扱えません。</p>
<h2 id="6-文字列比較で値の末尾空白が無視される">6. 文字列比較で値の末尾空白が無視される</h2><p>SQL Server（T-SQL）の <code>=</code> 比較では末尾空白が無視されるため、<code>&#39;abc&#39; = &#39;abc &#39;</code> が真になります<sup id="fnref:4">5</sup>。一方 PostgreSQL では別文字列として扱われます。SQL Serverの仕様によるものですが、初見では驚きました。また、ややこしいのは、<code>=</code> では末尾空白が無視される一方、<code>LIKE</code> では末尾空白が区別される点です。</p>
<p>外部取り込みデータやユーザー入力データの末尾に空白が含まれる場合の挙動に注意が必要です。</p>
<h2 id="7-ロック挙動が-PostgreSQL-と異なる">7. ロック挙動が PostgreSQL と異なる</h2><p>SQL Server の悲観ロックは PostgreSQL よりもロックされる範囲が広くなりやすいため注意が必要です。</p>
<p>PostgreSQL の <code>SELECT ... FOR UPDATE</code> は、最終的に条件に一致した行だけをロックします。対して SQL Server で <code>WITH (UPDLOCK)</code> を用いた場合、条件に合う行をスキャンする途中で読み込んだ行にもロックが及ぶことがあります<sup id="fnref:5">6</sup>。そのため、PostgreSQL と同じ感覚で使うと、更新対象ではない行までブロックしてしまい、システム全体の並行性を下げるリスクがあります。</p>
<div class="note-container note-info"><span class="note-icon"></span><div>

<p>Azure SQL Database のデフォルト分離レベル (RCSI)</p>
<p>Azure SQL Database では、デフォルトで READ_COMMITTED_SNAPSHOT (RCSI) が有効になっています<sup id="fnref:6">7</sup>。これは PostgreSQL の MVCC と同様に読込が書込をブロックしない挙動です。Azure SQL Database ではこの設定により並行性が確保されています。ただし、明示的に <code>WITH (UPDLOCK)</code> を使用して悲観的ロックを行う場合は、上述のスキャン範囲によるロックの特性に留意する必要があります。</p>
</div></div>



<h2 id="8-ロックエスカレーションで突然テーブル全体が詰まる">8. ロックエスカレーションで突然テーブル全体が詰まる</h2><p>SQL Server には PostgreSQL にない概念として「ロックエスカレーション<sup id="fnref:7">8</sup>」があります。SQL Server では、行単位のロックを管理するためにメモリを消費します。1 つのトランザクションで大量の行ロックを取得すると、メモリ効率を保つためにロックを行単位からテーブル単位に自動的に昇格させる仕組みがあり、これがロックエスカレーションです。デフォルトの閾値は約 5,000 行で、これを超えるとテーブル全体がロックされます。</p>
<p>テーブルロックが取得されると、そのテーブルへのすべての読み書きがブロックされるため、バッチ処理中に他のクエリが一切通らなくなる、という事象が起きます。そのため、大量の更新処理を行う場合は、エスカレーションの閾値を超えないよう小さなバッチに分割するなどの工夫が必要です。またロックエスカレーションそのものを無効化したい場合は、テーブル単位で <code>LOCK_ESCALATION</code> オプションを <code>DISABLE</code> に設定する方法があります。</p>
<h2 id="9-WITH-句はマテリアライズされず、複数回参照すると都度計算される">9. <code>WITH</code> 句はマテリアライズされず、複数回参照すると都度計算される</h2><p>PostgreSQL では、<code>WITH</code> 句を定義してメインクエリ内で同じ CTE を複数回参照した場合、デフォルトではマテリアライズされるため 1 回しか計算されません<sup id="fnref:11">9</sup>。そのため、重い処理を <code>WITH</code> 句に切り出して再利用するテクニックが有効な場面があります。</p>
<p>一方 SQL Server における <code>WITH</code> 句はインラインビューとして扱われます。結果セットは物理的に保持されないため、メインクエリ内で複数回参照すると、その回数分だけ元テーブルへのスキャンや再計算が行われます<sup id="fnref:12">10</sup>。</p>
<p>PostgreSQL の感覚で重い処理を 1 回で済ませる目的で <code>WITH</code> 句を多用すると、SQL Server ではかえって遅くなることがあります。複数回参照する重い処理がある場合は、<code>WITH</code> 句ではなく一時テーブル（<code>#temp</code> テーブル）に結果を格納するなどの代替アプローチを検討するのが安全です。</p>
<h2 id="10-スロークエリの実行計画をクエリストアで確認できる">10. スロークエリの実行計画をクエリストアで確認できる</h2><p>PostgreSQL では <code>EXPLAIN ANALYZE</code> で任意のクエリの実行計画を確認できますが、スロークエリが発生したクエリの実行計画を確認したいことがよくあります。スロークエリの実行計画をログに残す場合は <code>auto_explain.log_min_duration</code> を設定し、閾値を超えたクエリの実行計画を PostgreSQL のログに出力する運用が一般的です。</p>
<p>Azure SQL Database では、クエリストア（Query Store）に実行計画が自動的に蓄積されます<sup id="fnref:8">11</sup>。Azure SQL Database でも診断設定（Azure Monitor &#x2F; Log Analytics）や拡張イベントを使えばログ出力は可能ですが、日常的な調査ではクエリストア起点のほうが扱いやすいです。Azure SQL Database ではクエリストアがデフォルトで有効となっています。クエリストアは非常に便利です<sup id="fnref:9">12</sup>。</p>
<p>たとえば、以下のようにして平均 CPU 時間の高いクエリをクエリストアから取得できます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1rpbp2l-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1rpbp2l-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> TOP <span class="number">10</span></span><br><span class="line">    q.query_id,</span><br><span class="line">    qt.query_sql_text,</span><br><span class="line">    rs.avg_cpu_time,</span><br><span class="line">    rs.avg_duration,</span><br><span class="line">    rs.count_executions,</span><br><span class="line">    p.plan_id,</span><br><span class="line">    TRY_CAST(p.query_plan <span class="keyword">AS</span> XML) <span class="keyword">AS</span> query_plan</span><br><span class="line"><span class="keyword">FROM</span> sys.query_store_runtime_stats rs</span><br><span class="line"><span class="keyword">JOIN</span> sys.query_store_plan p <span class="keyword">ON</span> rs.plan_id <span class="operator">=</span> p.plan_id</span><br><span class="line"><span class="keyword">JOIN</span> sys.query_store_query q <span class="keyword">ON</span> p.query_id <span class="operator">=</span> q.query_id</span><br><span class="line"><span class="keyword">JOIN</span> sys.query_store_query_text qt <span class="keyword">ON</span> q.query_text_id <span class="operator">=</span> qt.query_text_id</span><br><span class="line"><span class="keyword">ORDER</span> <span class="keyword">BY</span> rs.avg_cpu_time <span class="keyword">DESC</span>;</span><br></pre></td></tr></table></figure></div>

<p>補足ですが、各ビューの役割は以下の通りです。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>ビュー</th>
<th>内容</th>
</tr>
</thead>
<tbody><tr>
<td><code>sys.query_store_runtime_stats</code></td>
<td>クエリごとの実行統計（CPU時間・実行回数・経過時間など）</td>
</tr>
<tr>
<td><code>sys.query_store_plan</code></td>
<td>クエリに紐づく実行計画（XML形式）</td>
</tr>
<tr>
<td><code>sys.query_store_query</code></td>
<td>クエリの識別情報（クエリIDとクエリテキストIDの対応）</td>
</tr>
<tr>
<td><code>sys.query_store_query_text</code></td>
<td>クエリの SQL テキスト本文</td>
</tr>
</tbody></table></div>
<p>また SQL Server Management Studio (SSMS) のGUIツールを使用すると、グラフで直感的に遅いクエリや実行計画の推移を確認できるため、システムカタログへのクエリとあわせて活用するのがおすすめです。</p>
<img src="/images/2026/20260512a/query-store-waits-detail.png" alt="query-store-waits-detail.png" width="1180" height="593" loading="lazy">

<p>出典：クエリ ストアを使用したパフォーマンスの監視 より引用</p>
<h2 id="まとめ">まとめ</h2><p>PostgreSQL ユーザーが SQL Server へ入門した際に、事前に知っておくと嬉しい設計開発 Tips を紹介しました。基本的な考え方は共通する部分もありますが、ロック挙動の思想の違いや、後から変更が難しい照合順序などの初期設計には注意が必要だと感じています。クエリストアなどの強力な機能も活かしつつ、これから SQL Server（Azure SQL Database） 環境での開発に臨む方の参考になれば嬉しいです。</p>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">https://learn.microsoft.com/ja-jp/sql/relational-databases/collations/collation-and-unicode-support</span> ↩</li><li id="fn:2"><span style="vertical-align: top; padding-right: 10px;">2.</span><span style="vertical-align: top;">https://learn.microsoft.com/ja-jp/sql/connect/jdbc/reference/setsendstringparametersasunicode-method-sqlserverdatasource</span> ↩</li><li id="fn:10"><span style="vertical-align: top; padding-right: 10px;">3.</span><span style="vertical-align: top;">https://learn.microsoft.com/ja-jp/sql/t-sql/data-types/char-and-varchar-transact-sql?view=sql-server-ver17</span> ↩</li><li id="fn:3"><span style="vertical-align: top; padding-right: 10px;">4.</span><span style="vertical-align: top;">https://learn.microsoft.com/ja-jp/sql/t-sql/functions/partition-transact-sql</span> ↩</li><li id="fn:4"><span style="vertical-align: top; padding-right: 10px;">5.</span><span style="vertical-align: top;">https://learn.microsoft.com/ja-jp/sql/t-sql/language-elements/string-comparison-assignment</span> ↩</li><li id="fn:5"><span style="vertical-align: top; padding-right: 10px;">6.</span><span style="vertical-align: top;">https://learn.microsoft.com/ja-jp/sql/relational-databases/sql-server-transaction-locking-and-row-versioning-guide?view=sql-server-ver17</span> ↩</li><li id="fn:6"><span style="vertical-align: top; padding-right: 10px;">7.</span><span style="vertical-align: top;">https://learn.microsoft.com/ja-jp/sql/t-sql/statements/set-transaction-isolation-level-transact-sql?view=sql-server-ver17</span> ↩</li><li id="fn:7"><span style="vertical-align: top; padding-right: 10px;">8.</span><span style="vertical-align: top;">https://learn.microsoft.com/ja-jp/troubleshoot/sql/database-engine/performance/resolve-blocking-problems-caused-lock-escalation</span> ↩</li><li id="fn:11"><span style="vertical-align: top; padding-right: 10px;">9.</span><span style="vertical-align: top;">https://www.postgresql.org/docs/current/queries-with.html</span> ↩</li><li id="fn:12"><span style="vertical-align: top; padding-right: 10px;">10.</span><span style="vertical-align: top;">https://learn.microsoft.com/ja-jp/sql/t-sql/queries/with-common-table-expression-transact-sql?view=sql-server-ver17</span> ↩</li><li id="fn:8"><span style="vertical-align: top; padding-right: 10px;">11.</span><span style="vertical-align: top;">https://learn.microsoft.com/ja-jp/sql/relational-databases/performance/monitoring-performance-by-using-the-query-store?view=sql-server-ver17</span> ↩</li><li id="fn:9"><span style="vertical-align: top; padding-right: 10px;">12.</span><span style="vertical-align: top;">なお、PostgreSQL でもサードパーティ製の拡張機能（pg_store_plans など）を利用することで、システムカタログから実行計画を確認できます。</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">ここ数年は PostgreSQL を中心に開発していましたが、最近 SQL Server（Azure SQL Database）を利用する機会がありました。細かいところで、いくつか PostgreSQL と SQL Server で挙動が異なる点がありました。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="PostgreSQL" scheme="https://future-architect.github.io/tags/PostgreSQL/"/>
  </entry>
  <entry>
    <title>Spring Boot マルチDataSource構成で secondary DB だけに Flyway を適用する</title>
    <link href="https://future-architect.github.io/articles/20260410a/"/>
    <id>https://future-architect.github.io/articles/20260410a/</id>
    <published>2026-04-09T15:00:00.000Z</published>
    <updated>2026-04-09T15:00:00.000Z</updated>
    <author><name>二村暢之</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><h3 id="Flyway-とは">Flyway とは</h3><p>Flyway はデータベースのスキーマ変更をバージョン管理するマイグレーションツールです。<code>V1__create_table.sql</code>、<code>V2__add_column.sql</code> のようにバージョン番号付きの SQL ファイルを用意しておくと、アプリケーション起動時に未適用のマイグレーションを自動的に検出・実行してくれます。Spring Boot では <code>spring-boot-starter</code> に Flyway が統合されており、依存関係を追加するだけで自動構成が有効になります。</p>
<h3 id="本記事の背景">本記事の背景</h3><p>私たちのプロジェクトでは長らく Flyway なしで運用してきました。テーブル追加やカラム変更は手動で DDL を流していたのですが、環境が増えてきて正直面倒になってきたので、重い腰を上げて導入することにしました。</p>
<p>ところが、いざ導入してみるとすんなりとはいきませんでした。というのも、このプロジェクトはテナント用と管理用の2つの DataSource を持つ構成で、Flyway を適用したいのは管理 DB だけ。Spring Boot の Flyway 自動構成は primary DataSource にしか紐づかないため、手動で Flyway Bean を構成する必要がありました。さらに、既存のテーブルがある状態への後付け導入でいくつかハマりポイントがあったので、本記事ではその辺りの知見を共有します。</p>
<ul>
<li>secondary DataSource に手動で Flyway を構成する方法</li>
<li><code>baselineOnMigrate(true)</code> の正しい理解と V1 に書くべき内容</li>
<li><code>@ConditionalOnProperty</code> を使った環境別の有効&#x2F;無効切り替え</li>
</ul>
<h3 id="なぜ-Flyway-を選んだか-—-Liquibase-との比較">なぜ Flyway を選んだか — Liquibase との比較</h3><p>DB マイグレーションツールとしては Liquibase も候補に挙がりました。参考までに両者の比較を載せておきます。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">比較軸</th>
<th align="left">Flyway</th>
<th align="left">Liquibase</th>
</tr>
</thead>
<tbody><tr>
<td align="left">変更定義の形式</td>
<td align="left">SQL のみ</td>
<td align="left">XML &#x2F; YAML &#x2F; JSON &#x2F; SQL</td>
</tr>
<tr>
<td align="left">DB 製品差異の吸収</td>
<td align="left">自前で location 分割が必要</td>
<td align="left">宣言的な定義なら Liquibase が吸収</td>
</tr>
<tr>
<td align="left">ロールバック</td>
<td align="left">非サポート（手動で逆 SQL を書く）</td>
<td align="left">DSL 定義なら約8割を自動生成、SQL 定義は手動</td>
</tr>
<tr>
<td align="left">学習コスト</td>
<td align="left">SQL が書ければすぐ使える</td>
<td align="left">DSL（XML&#x2F;YAML）の習得が必要</td>
</tr>
<tr>
<td align="left">Spring Boot 統合</td>
<td align="left"><code>flyway-core</code> を追加するだけ</td>
<td align="left"><code>liquibase-core</code> を追加するだけ</td>
</tr>
<tr>
<td align="left">適したシステム</td>
<td align="left">DB 固有の SQL を多用する &#x2F; 複数 RDB 製品をサポートする</td>
<td align="left">単一 RDB で完結する &#x2F; ロールバックを自動化したい</td>
</tr>
</tbody></table></div>
<p>Liquibase は宣言的な定義で RDB の差異を吸収してくれる点が魅力です。単一の RDB 製品で完結するシステムや、ロールバックの自動化が必要なケースでは有力な選択肢になるでしょう。</p>
<p>一方、私たちのプロジェクトでは Oracle と PostgreSQL の両方をサポートしており、DB 固有の構文（<code>CLOB</code> &#x2F; <code>TEXT</code> の違いや PL&#x2F;SQL 等）を多用していました。宣言的な定義だけでは対応しきれない場面が多いと判断し、素の SQL をそのまま書ける Flyway を採用しました。Flyway はロールバックをサポートしていないため、マイグレーションに失敗した場合は手動で逆の DDL を流す運用になりますが、管理 DB のスキーマ変更頻度はそこまで高くないのでこれで十分と割り切りました。</p>
<h2 id="システム構成">システム構成</h2><p>今回のシステムは以下の2つの DataSource を持ちます。<br>今考えると、primaryとsecondaryは逆にすべきでした・・・反省。</p>
<pre class="mermaid" data-mermaid="b3c3666b467eeeb49db38cb45c60e14f1df045239a944550a9cc49ba63b5c460">graph LR
    App[Spring Boot アプリケーション]
    App -->|primary| TenantDB[(テナントDB<br/>データ)]
    App -->|secondary| ManagerDB[(管理DB<br/>設定・マスタ管理)]

    style TenantDB fill:#2e7d32,color:#fff
    style ManagerDB fill:#1565c0,color:#fff</pre>

<div class="scroll"><table>
<thead>
<tr>
<th align="left">DataSource</th>
<th align="left">役割</th>
<th align="left">Flyway</th>
</tr>
</thead>
<tbody><tr>
<td align="left">primary（テナントDB）</td>
<td align="left">テナントごとのデータを格納。環境ごとに接続先が変わる</td>
<td align="left"><strong>不要</strong></td>
</tr>
<tr>
<td align="left">secondary（管理DB）</td>
<td align="left">アプリの設定やマスタ情報を管理。全環境で共通のスキーマ</td>
<td align="left"><strong>必要</strong></td>
</tr>
</tbody></table></div>
<p>Spring Boot では <code>spring.datasource.*</code> プロパティで定義された DataSource が primary DataSource として扱われます。Flyway の自動構成（<code>FlywayAutoConfiguration</code>）はこの primary DataSource に紐づいて動作するため、<code>spring.datasource.*</code> 以外で定義した secondary DataSource にはマイグレーションが適用されません。</p>
<p>やりたいことを整理すると以下の通りです。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">環境</th>
<th align="left">primary（テナントDB）</th>
<th align="left">secondary（管理DB）</th>
</tr>
</thead>
<tbody><tr>
<td align="left">dev</td>
<td align="left">Flyway 無効</td>
<td align="left">Flyway <strong>無効</strong>（開発環境なのでマイグレーション用のDDLを手動確認・試行錯誤）</td>
</tr>
<tr>
<td align="left">staging &#x2F; prod</td>
<td align="left">Flyway 無効</td>
<td align="left">Flyway <strong>有効</strong>（自動マイグレーション）</td>
</tr>
</tbody></table></div>
<h2 id="実装-secondary-DataSource-に手動で-Flyway-Bean-を構成する">実装: secondary DataSource に手動で Flyway Bean を構成する</h2><h3 id="yml-設定">yml 設定</h3><p>まず <code>application.yml</code>（共通設定）で Spring Boot の自動構成 Flyway を無効化します。</p>
<div class="code-block"><figure class="highlight yaml"><input type="checkbox" id="code-wrap-1pwa59s-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1pwa59s-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="comment"># Spring Boot自動構成の Flyway（= primary DataSource に適用）の有効/無効。</span></span><br><span class="line">  <span class="comment"># primary DataSource はテナント用でマイグレーション不要のため無効化。</span></span><br><span class="line">  <span class="comment"># 管理DB（secondary）へのマイグレーションはこの設定とは独立して</span></span><br><span class="line">  <span class="comment"># Java 側で手動構成・実行される。</span></span><br><span class="line">  <span class="attr">flyway:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">false</span></span><br><span class="line">    <span class="attr">locations:</span> <span class="string">classpath:db/migration/common,classpath:db/migration/oracle</span></span><br></pre></td></tr></table></figure></div>

<p>ポイントは2つです。</p>
<ol>
<li><code>enabled: false</code> で自動構成 Flyway を無効化（primary DataSource にはマイグレーション不要）</li>
<li><code>locations</code> は設定しておく（後述の手動構成 Bean で <code>@Value</code> 経由で再利用）</li>
</ol>
<h3 id="Java-実装">Java 実装</h3><p>secondary DataSource の設定クラスに Flyway Bean を手動で定義します。</p>
<div class="code-block"><figure class="highlight java"><input type="checkbox" id="code-wrap-1pwa59s-2" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1pwa59s-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@ConfigurationProperties(prefix = &quot;app.manager.datasource&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ManagerSqlConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Value(&quot;$&#123;spring.flyway.locations:classpath:db/migration/common&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String[] flywayLocations;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean(name = &quot;managerDataSource&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> DataSource <span class="title function_">managerDataSource</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// secondary DataSource の構成（省略）</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 管理DB用の Flyway マイグレーションを実行する.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * Spring Boot の Flyway 自動構成は primary DataSource に紐づくため、</span></span><br><span class="line"><span class="comment">     * secondary DataSource には手動で Flyway を構成する必要がある.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Bean(name = &quot;managerFlyway&quot;, initMethod = &quot;migrate&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Flyway <span class="title function_">managerFlyway</span><span class="params">(</span></span><br><span class="line"><span class="params">            <span class="meta">@Qualifier(&quot;managerDataSource&quot;)</span> DataSource managerDataSource)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> Flyway.configure()</span><br><span class="line">                .dataSource(managerDataSource)</span><br><span class="line">                .locations(flywayLocations)</span><br><span class="line">                .baselineOnMigrate(<span class="literal">true</span>)</span><br><span class="line">                .load();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p><code>@Bean(initMethod = &quot;migrate&quot;)</code> により、Bean 生成時に自動的に <code>Flyway#migrate()</code> が呼ばれます。</p>
<p><code>spring.flyway.locations</code> を <code>@Value</code> で参照することで、yml 側でマイグレーションファイルのパスを管理でき、Oracle &#x2F; PostgreSQL などの Dialect 切り替えにも対応できます。</p>
<h2 id="落とし穴-baselineOnMigrate-と-V1-マイグレーションの関係">落とし穴: baselineOnMigrate と V1 マイグレーションの関係</h2><h3 id="問題">問題</h3><p>上記の実装でデプロイしたところ、<strong>V1 マイグレーションが実行されない</strong>という問題が発生しました。</p>
<p>マイグレーションファイル（<code>V1__add_column.sql</code>）に差分DDL（ALTER TABLE）を書いていたにもかかわらず、管理DB にカラムが追加されていませんでした。<code>flyway_schema_history</code> テーブルを確認すると、V1 に対して <code>TYPE=BASELINE</code> のレコードが記録されており、実際のSQLは実行されていませんでした。</p>
<h3 id="baselineOnMigrate-とは">baselineOnMigrate とは</h3><p><code>baselineOnMigrate(true)</code> は <strong>既にテーブルが存在するスキーマに対して、途中から Flyway を導入する</strong>ための設定です。</p>
<p>Flyway は通常、空のスキーマに対して V1 から順にマイグレーションを適用することを前提としています。しかし既にテーブルが存在するスキーマに Flyway を導入する場合、Flyway は「このスキーマは管理下にない」としてエラーを出します。<code>baselineOnMigrate(true)</code> を指定すると、Flyway は既存スキーマに対して「ここまでは適用済み」というベースラインを自動的に作成し、それ以降のバージョンからマイグレーションを開始します。</p>
<h3 id="baselineVersion-のデフォルトは-“1”">baselineVersion のデフォルトは “1”</h3><p>ベースラインを「どのバージョンで作成するか」を決めるのが <code>baselineVersion</code> です。<strong>デフォルト値は <code>&quot;1&quot;</code> です。</strong></p>
<p>つまり <code>baselineOnMigrate(true)</code> を指定すると:</p>
<ul>
<li><code>flyway_schema_history</code> テーブルが自動作成される</li>
<li><strong>V1 &#x3D; ベースライン（＝適用済み扱い）</strong> として記録される</li>
<li>実際に実行されるのは <strong>V2 以降のみ</strong></li>
</ul>
<p>私たちのような既存スキーマに途中からflywayを導入する場合はこんな流れになります。</p>
<ol>
<li>既存スキーマ(初期状態)<ul>
<li>flyway_schema_historyテーブルなし</li>
</ul>
</li>
<li>flywayがbaselineOnMigrate&#x3D;true で初回実行<ul>
<li>flyway_schema_historyテーブル作成</li>
<li>V1をベースラインとして記録<ul>
<li><code>TYPE=BASELINE</code> のレコードが作成されるのみ</li>
<li>この段階ではマイグレーションSQLは実行されない</li>
</ul>
</li>
</ul>
</li>
<li>flywayがV2以降のマイグレーションを実行</li>
</ol>
<p>実際に <code>flyway_schema_history</code> を見ると、V1 が <code>TYPE=BASELINE</code> として記録されていることが確認できます。<code>execution_time</code> は 0、つまり SQL は実行されていません。</p>
<img fetchpriority="high" src="/images/2026/20260410a/flyway_schema_history_の_BASELINE_レコード.png" alt="flyway_schema_history の BASELINE レコード" width="1200" height="321">

<h3 id="V1-に何を書くべきか">V1 に何を書くべきか</h3><p>Flyway の <code>baselineOnMigrate</code> は <strong>「V1 の内容は既にスキーマに反映済みである」</strong> という前提で動作します。したがって、V1 には<strong>現在のスキーマ状態を再現する全DDL</strong>（CREATE TABLE 等）を記述するのが正しい設計です。</p>
<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1pwa59s-3" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1pwa59s-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">db/migration/common/</span><br><span class="line">├── V1__initial_schema.sql       ← 既存スキーマの全DDL（既存環境ではスキップされる）</span><br><span class="line">├── V2__add_is_default.sql       ← ここから実際の差分マイグレーション</span><br><span class="line">├── V3__add_index.sql</span><br><span class="line">└── ...</span><br></pre></td></tr></table></figure></div>

<p>V1 に全DDLを書いておくことで、<strong>環境によって2つの動作</strong>が実現できます。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">環境</th>
<th align="left">baselineOnMigrate</th>
<th align="left">V1 の扱い</th>
<th align="left">V2 以降</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>既存環境</strong>（テーブルあり）</td>
<td align="left"><code>true</code></td>
<td align="left">ベースラインとしてスキップ</td>
<td align="left">差分を自動適用</td>
</tr>
<tr>
<td align="left"><strong>新規環境</strong>（空スキーマ）</td>
<td align="left"><code>false</code>（デフォルト）</td>
<td align="left">全DDLとして実行</td>
<td align="left">差分を自動適用</td>
</tr>
</tbody></table></div>
<p>新規環境では <code>baselineOnMigrate</code> はデフォルトの <code>false</code> のままで、V1 の全DDLから順に実行されます。既存環境では <code>baselineOnMigrate(true)</code> により V1 がスキップされ、V2 から差分が適用されます。</p>
<h3 id="V1-に差分DDLを書いてしまった">V1 に差分DDLを書いてしまった</h3><p>上記のことが理解できてなかったため、V1 に差分DDL（ALTER TABLE）を書いてしまいました・・・</p>
<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1pwa59s-4" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1pwa59s-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">V1__add_column.sql  ← ALTER TABLE（差分DDL）を記述 → ベースラインでスキップされた</span><br></pre></td></tr></table></figure></div>

<p>既存環境に途中から Flyway を導入する場合、V1 は「既にスキーマに存在する状態の記録」であり、差分マイグレーションは <strong>V2 から開始する</strong>のが正解です。</p>
<h3 id="修正後のマイグレーション構成">修正後のマイグレーション構成</h3><div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1pwa59s-5" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1pwa59s-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">db/migration/common/</span><br><span class="line">├── V1__initial_schema.sql       ← 全DDL（新規環境構築用に配置）</span><br><span class="line">└── V2__add_is_default.sql       ← 差分マイグレーション（V1 からリネーム）</span><br></pre></td></tr></table></figure></div>

<p>修正後にアプリを再起動すると、V1 は <code>TYPE=BASELINE</code> のまま、V2 が <code>TYPE=SQL</code> として実行されます。</p>
<img src="/images/2026/20260410a/V2_適用後の_flyway_schema_history.png" alt="V2 適用後の flyway_schema_history" width="1198" height="111" loading="lazy">

<h3 id="もし-V1-に差分を書いてデプロイしてしまったら">もし V1 に差分を書いてデプロイしてしまったら</h3><p>既に <code>baselineOnMigrate(true)</code> で V1 が BASELINE として記録されてしまった場合でも、<code>flyway_schema_history</code> をリセットすることでやり直しが可能です。<br>私は誤解釈をしていたためやってしまいました・・・みなさんは気をつけてください。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1pwa59s-6" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1pwa59s-6" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- V1 で適用された差分DDLを手動で巻き戻す（例: カラム追加の場合は削除）</span></span><br><span class="line"><span class="keyword">ALTER TABLE</span> テーブル物理名 <span class="keyword">DROP</span> <span class="keyword">COLUMN</span> カラム物理名;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- flyway_schema_history を初期化</span></span><br><span class="line"><span class="keyword">TRUNCATE</span> <span class="keyword">TABLE</span> &quot;flyway_schema_history&quot;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">COMMIT</span>;</span><br></pre></td></tr></table></figure></div>

<p>リセット後にアプリを再起動すれば、Flyway が BASELINE(V1) を再作成し、V2 から差分マイグレーションを実行します。</p>
<p>なお、<code>flyway_schema_history</code> は Flyway が小文字でテーブルを作成するため、もしOracleを利用してる場合は <strong>ダブルクォートで囲まないと <code>ORA-00942: table or view does not exist</code> になります</strong>。Flyway 内部では JdbcTemplate 経由で小文字固定のテーブル名が使われており、Oracle のデフォルトの大文字変換とは異なる名前で格納されています。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1pwa59s-7" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1pwa59s-7" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- NG: Oracle が大文字の FLYWAY_SCHEMA_HISTORY を探しに行く</span></span><br><span class="line"><span class="keyword">TRUNCATE</span> <span class="keyword">TABLE</span> flyway_schema_history;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- OK: 小文字のテーブル名を明示指定</span></span><br><span class="line"><span class="keyword">TRUNCATE</span> <span class="keyword">TABLE</span> &quot;flyway_schema_history&quot;;</span><br></pre></td></tr></table></figure></div>

<p>ただし、この手順は Flyway の管理情報を直接操作するため <strong>自己責任</strong> でお願いします。本番環境では十分に検証してから実施してください。</p>
<p>Java コードは <code>baselineVersion</code> の明示指定なし（デフォルト “1”）のままで正しく動作します。</p>
<div class="code-block"><figure class="highlight java"><input type="checkbox" id="code-wrap-1pwa59s-8" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1pwa59s-8" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="meta">@Bean(name = &quot;managerFlyway&quot;, initMethod = &quot;migrate&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Flyway <span class="title function_">managerFlyway</span><span class="params">(</span></span><br><span class="line"><span class="params">        <span class="meta">@Qualifier(&quot;managerDataSource&quot;)</span> DataSource managerDataSource)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> Flyway.configure()</span><br><span class="line">            .dataSource(managerDataSource)</span><br><span class="line">            .locations(flywayLocations)</span><br><span class="line">            .baselineOnMigrate(<span class="literal">true</span>)  <span class="comment">// 既存環境向け: V1 をベースラインとしてスキップ</span></span><br><span class="line">            .load();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<h2 id="複数の-RDB-製品をサポートする場合の-tips">複数の RDB 製品をサポートする場合の tips</h2><p>Oracle と PostgreSQL の両方をサポートしている場合、マイグレーション SQL の書き方に少し工夫が必要です。</p>
<h3 id="共通-SQL-で書ける範囲は意外と広い">共通 SQL で書ける範囲は意外と広い</h3><p>Oracle は <code>VARCHAR</code> で宣言すると内部的に <code>VARCHAR2</code> として扱い、<code>NUMERIC</code> は <code>NUMBER</code> に対応します。そのため、カラム定義を <code>VARCHAR</code> &#x2F; <code>NUMERIC</code> で統一しておけば、Oracle でも PostgreSQL でも同じ SQL が通ります。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1pwa59s-9" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1pwa59s-9" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- Oracle/PostgreSQL 共通で動く</span></span><br><span class="line"><span class="keyword">ALTER TABLE</span> LLM <span class="keyword">ADD</span> IS_DEFAULT <span class="type">VARCHAR</span>(<span class="number">1</span>) <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> <span class="keyword">NOT NULL</span>;</span><br></pre></td></tr></table></figure></div>

<h3 id="共通化できない場合は-location-を分ける">共通化できない場合は location を分ける</h3><p><code>CLOB</code>（Oracle）と <code>TEXT</code>（PostgreSQL）のように自動変換されない型や、DB 固有の構文（<code>CREATE SEQUENCE</code> のオプション、<code>COMMENT ON</code> の書き方の違い等）がある場合は、同じ SQL ファイルでは対応できません。</p>
<p>Flyway の <code>locations</code> 設定で共通と DB 固有のディレクトリを分けて管理します。</p>
<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1pwa59s-10" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1pwa59s-10" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">db/migration/</span><br><span class="line">├── common/                         ... Oracle/PostgreSQL 共通</span><br><span class="line">│   ├── V2__add_is_default.sql</span><br><span class="line">│   └── V3__insert_default_llm.sql</span><br><span class="line">├── oracle/                         ... Oracle 固有</span><br><span class="line">│   └── V4__add_clob_column.sql</span><br><span class="line">└── postgresql/                     ... PostgreSQL 固有</span><br><span class="line">    └── V4__add_text_column.sql</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight yaml"><input type="checkbox" id="code-wrap-1pwa59s-11" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1pwa59s-11" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment"># application.yml</span></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">flyway:</span></span><br><span class="line">    <span class="attr">locations:</span> <span class="string">classpath:db/migration/common,classpath:db/migration/oracle</span></span><br></pre></td></tr></table></figure></div>

<p>ポイントは、<strong>共通と固有の両方に同じバージョン番号を置かないこと</strong>です。DB 固有の構文が必要な場合は <code>common/</code> には置かず、<code>oracle/</code> と <code>postgresql/</code> の両方に同じバージョンの SQL をそれぞれの方言で配置します。共通の <code>V4</code> と Oracleもしくはpostgresql の <code>V4</code> の両方あると、Flyway が重複バージョンとしてエラーにします。</p>
<p>環境ごとの <code>locations</code> 切り替えは yml のプロファイル分割で対応します。</p>
<div class="code-block"><figure class="highlight yaml"><input type="checkbox" id="code-wrap-1pwa59s-12" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1pwa59s-12" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment"># application-oracle.yml</span></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">flyway:</span></span><br><span class="line">    <span class="attr">locations:</span> <span class="string">classpath:db/migration/common,classpath:db/migration/oracle</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># application-postgresql.yml</span></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">flyway:</span></span><br><span class="line">    <span class="attr">locations:</span> <span class="string">classpath:db/migration/common,classpath:db/migration/postgresql</span></span><br></pre></td></tr></table></figure></div>

<h2 id="dev-環境だけ-Flyway-を無効にする">dev 環境だけ Flyway を無効にする</h2><p>開発中はテーブル定義が確定するまで DDL を試行錯誤することが多く、Flyway が自動で走ると不便です。<code>@ConditionalOnProperty</code> を使って、<strong>Bean 登録自体を条件付き</strong>にします。</p>
<h3 id="ConditionalOnProperty-とは">@ConditionalOnProperty とは</h3><p>Spring Boot のアノテーションで、<strong>プロパティの値に応じて Bean の登録自体をスキップ</strong>できます。</p>
<div class="code-block"><figure class="highlight java"><input type="checkbox" id="code-wrap-1pwa59s-13" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1pwa59s-13" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="meta">@Bean(name = &quot;managerFlyway&quot;, initMethod = &quot;migrate&quot;)</span></span><br><span class="line"><span class="meta">@ConditionalOnProperty(</span></span><br><span class="line"><span class="meta">    name = &quot;app.manager.flyway.enabled&quot;,</span></span><br><span class="line"><span class="meta">    havingValue = &quot;true&quot;,</span></span><br><span class="line"><span class="meta">    matchIfMissing = true)</span></span><br><span class="line"><span class="keyword">public</span> Flyway <span class="title function_">managerFlyway</span><span class="params">(...)</span> &#123; ... &#125;</span><br></pre></td></tr></table></figure></div>

<p>3つのパラメータの意味は以下の通りです。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">パラメータ</th>
<th align="left">値</th>
<th align="left">意味</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>name</code></td>
<td align="left"><code>&quot;app.manager.flyway.enabled&quot;</code></td>
<td align="left">チェック対象のプロパティキー</td>
</tr>
<tr>
<td align="left"><code>havingValue</code></td>
<td align="left"><code>&quot;true&quot;</code></td>
<td align="left">プロパティがこの値のときだけ Bean を登録する</td>
</tr>
<tr>
<td align="left"><code>matchIfMissing</code></td>
<td align="left"><code>true</code></td>
<td align="left">プロパティが<strong>未定義</strong>の場合も Bean を登録する（&#x3D; デフォルト有効）</td>
</tr>
</tbody></table></div>
<p>この3つの組み合わせにより、以下の挙動になります。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">プロパティの状態</th>
<th align="left">Bean 登録</th>
<th align="left">理由</th>
</tr>
</thead>
<tbody><tr>
<td align="left">未定義</td>
<td align="left"><strong>登録される</strong></td>
<td align="left"><code>matchIfMissing = true</code> により未定義 &#x3D; 条件一致</td>
</tr>
<tr>
<td align="left"><code>true</code></td>
<td align="left"><strong>登録される</strong></td>
<td align="left"><code>havingValue = &quot;true&quot;</code> に一致</td>
</tr>
<tr>
<td align="left"><code>false</code></td>
<td align="left"><strong>登録されない</strong></td>
<td align="left"><code>havingValue = &quot;true&quot;</code> に不一致</td>
</tr>
</tbody></table></div>
<p><code>matchIfMissing = true</code> がポイントです。これにより、staging &#x2F; prod の yml にわざわざ <code>enabled: true</code> を書く必要がなく、<strong>dev だけ <code>false</code> を明示指定すればよい</strong>設計になります。</p>
<h3 id="Bean-単位で効く">Bean 単位で効く</h3><p><code>@ConditionalOnProperty</code> はメソッドレベルのアノテーションです。クラス全体ではなく、<strong>そのメソッドで定義される Bean だけ</strong>が条件の対象になります。</p>
<div class="code-block"><figure class="highlight java"><input type="checkbox" id="code-wrap-1pwa59s-14" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1pwa59s-14" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">// 例；管理DB用のConfiguration</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ManagerSqlConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> DataSource <span class="title function_">managerDataSource</span><span class="params">()</span> &#123; ... &#125;      <span class="comment">// ← 常に登録</span></span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="meta">@ConditionalOnProperty(...)</span>                         <span class="comment">// ★ ここだけ条件付き</span></span><br><span class="line">    <span class="keyword">public</span> Flyway <span class="title function_">managerFlyway</span><span class="params">(...)</span> &#123; ... &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>これにより、Flyway を無効にしても管理DB への接続や SQL 実行は通常通り動作します。</p>
<h3 id="yml-設定-1">yml 設定</h3><p>dev 環境用の yml にだけプロパティを追加します。</p>
<div class="code-block"><figure class="highlight yaml"><input type="checkbox" id="code-wrap-1pwa59s-15" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1pwa59s-15" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment"># application-dev.yml</span></span><br><span class="line"><span class="attr">app:</span></span><br><span class="line">  <span class="attr">manager:</span></span><br><span class="line">    <span class="attr">flyway:</span></span><br><span class="line">      <span class="attr">enabled:</span> <span class="literal">false</span>  <span class="comment"># マイグレーション DDL を手動確認してから適用する</span></span><br></pre></td></tr></table></figure></div>

<p>staging &#x2F; prod では未定義のまま（&#x3D; <code>matchIfMissing = true</code> によりデフォルト有効）にします。</p>
<h2 id="設定の全体像まとめ">設定の全体像まとめ</h2><p>最終的な設定の対応関係を整理します。</p>
<h3 id="プロパティの役割分担">プロパティの役割分担</h3><div class="scroll"><table>
<thead>
<tr>
<th align="left">プロパティ</th>
<th align="left">制御対象</th>
<th align="left">デフォルト</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>spring.flyway.enabled</code></td>
<td align="left">Spring Boot 自動構成の Flyway（primary DataSource）</td>
<td align="left"><code>true</code></td>
</tr>
<tr>
<td align="left"><code>app.manager.flyway.enabled</code></td>
<td align="left">手動構成の Flyway Bean（secondary DataSource）</td>
<td align="left"><code>true</code>（<code>matchIfMissing</code>）</td>
</tr>
</tbody></table></div>
<h3 id="環境別設定">環境別設定</h3><div class="scroll"><table>
<thead>
<tr>
<th align="left">環境</th>
<th align="left"><code>spring.flyway.enabled</code></th>
<th align="left"><code>app.manager.flyway.enabled</code></th>
<th align="left">結果</th>
</tr>
</thead>
<tbody><tr>
<td align="left">共通</td>
<td align="left"><code>false</code></td>
<td align="left">（未指定）</td>
<td align="left">primary: 無効 &#x2F; secondary: 有効</td>
</tr>
<tr>
<td align="left">dev</td>
<td align="left">（共通を継承）</td>
<td align="left"><code>false</code></td>
<td align="left">primary: 無効 &#x2F; secondary: <strong>無効</strong></td>
</tr>
<tr>
<td align="left">staging &#x2F; prod</td>
<td align="left">（共通を継承）</td>
<td align="left">（未指定 &#x3D; 有効）</td>
<td align="left">primary: 無効 &#x2F; secondary: <strong>有効</strong></td>
</tr>
</tbody></table></div>
<h3 id="Java-コード最終形">Java コード最終形</h3><div class="code-block"><figure class="highlight java"><input type="checkbox" id="code-wrap-1pwa59s-16" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1pwa59s-16" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@ConfigurationProperties(prefix = &quot;app.manager.datasource&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ManagerSqlConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Value(&quot;$&#123;spring.flyway.locations:classpath:db/migration/common&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String[] flywayLocations;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean(name = &quot;managerDataSource&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> DataSource <span class="title function_">managerDataSource</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// DataSource 構成（省略）</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 管理DB用の Flyway マイグレーション.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * &#123;<span class="doctag">@code</span> app.manager.flyway.enabled=false&#125; で無効化できる（dev 環境向け）.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Bean(name = &quot;managerFlyway&quot;, initMethod = &quot;migrate&quot;)</span></span><br><span class="line">    <span class="meta">@ConditionalOnProperty(</span></span><br><span class="line"><span class="meta">        name = &quot;app.manager.flyway.enabled&quot;,</span></span><br><span class="line"><span class="meta">        havingValue = &quot;true&quot;,</span></span><br><span class="line"><span class="meta">        matchIfMissing = true)</span></span><br><span class="line">    <span class="keyword">public</span> Flyway <span class="title function_">managerFlyway</span><span class="params">(</span></span><br><span class="line"><span class="params">            <span class="meta">@Qualifier(&quot;managerDataSource&quot;)</span> DataSource managerDataSource)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> Flyway.configure()</span><br><span class="line">                .dataSource(managerDataSource)</span><br><span class="line">                .locations(flywayLocations)</span><br><span class="line">                .baselineOnMigrate(<span class="literal">true</span>)</span><br><span class="line">                .load();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<h2 id="おわりに">おわりに</h2><p>マルチ DataSource 構成で Flyway を使う際のポイントをまとめます。</p>
<ol>
<li><strong>Spring Boot の Flyway 自動構成は primary DataSource 専用</strong>。secondary DataSource には <code>@Bean(initMethod = &quot;migrate&quot;)</code> で手動構成が必要</li>
<li><strong><code>baselineOnMigrate(true)</code> は既存スキーマへの Flyway 後付け導入用</strong>。V1 は「現在のスキーマ状態」を記録する全DDLを配置し、実際の差分マイグレーションは V2 から開始する</li>
<li><strong><code>@ConditionalOnProperty</code> で Bean 単位の有効&#x2F;無効切り替え</strong>が可能。<code>matchIfMissing = true</code> により、無効にしたい環境だけ <code>false</code> を指定すればよい</li>
</ol>
<p>特に 2 は Flyway のドキュメントを読んでいても見落としがちなポイントだと感じました。<code>baselineOnMigrate</code> の意味を正しく理解せずに V1 に差分DDLを書いてしまうと、既存環境ではスキップされ、新規環境でしか実行されないという事態になります。</p>
<h2 id="Appendix-Flyway-公式ドキュメント">Appendix: Flyway 公式ドキュメント</h2><p>本記事で扱った Flyway の設定項目に関する公式ドキュメントへのリンクです。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">設定項目</th>
<th align="left">説明</th>
<th align="left">ドキュメント</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>baselineOnMigrate</code></td>
<td align="left">既存スキーマへの後付け導入時にベースラインを自動作成する</td>
<td align="left">Flyway Baseline On Migrate Setting</td>
</tr>
<tr>
<td align="left"><code>baselineVersion</code></td>
<td align="left">ベースラインのバージョン番号（デフォルト: “1”）</td>
<td align="left">Flyway Baseline Version Setting</td>
</tr>
<tr>
<td align="left"><code>locations</code></td>
<td align="left">マイグレーション SQL の配置先ディレクトリ</td>
<td align="left">Flyway Locations Setting</td>
</tr>
<tr>
<td align="left">Baselines（概念説明）</td>
<td align="left">ベースラインの仕組みと使い方の概要</td>
<td align="left">Baselines</td>
</tr>
<tr>
<td align="left">Configuration（全体）</td>
<td align="left">Flyway の全設定項目一覧</td>
<td align="left">Configuration</td>
</tr>
</tbody></table></div>
]]></content>
    <summary type="html">このプロジェクトはテナント用と管理用の2つの DataSource を持つ構成で、Flyway を適用したいのは管理 DB だけ。Spring Boot の Flyway 自動構成は primary DataSource にしか紐づかないため、手動で Flyway Bean を構成する必要がありました。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="Java" scheme="https://future-architect.github.io/tags/Java/"/>
    <category term="SpringBoot" scheme="https://future-architect.github.io/tags/SpringBoot/"/>
  </entry>
  <entry>
    <title>DynamoDB設計ガイドラインを公開しました</title>
    <link href="https://future-architect.github.io/articles/20260227a/"/>
    <id>https://future-architect.github.io/articles/20260227a/</id>
    <published>2026-02-26T15:00:00.000Z</published>
    <updated>2026-02-26T15:00:00.000Z</updated>
    <author><name>後藤玲雄</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2026/20260227a/top.jpg" alt="" width="900" height="399">

<h2 id="はじめに">はじめに</h2><p>こんにちは。製造エネルギーサービス事業部の後藤です。</p>
<p>フューチャーでは、社内の有志メンバーが集まり、システム開発におけるベストプラクティスをまとめた「アーキテクチャ設計ガイドライン」の整備・公開しています。</p>
<p>これまでフロントエンドからバックエンド、クラウドインフラ、Git戦略など幅広い分野のガイドラインを公開してきましたが、この度新たに「DynamoDB設計ガイドライン」を公開しました！</p>
<p>📄 DynamoDB設計ガイドラインはこちら</p>
<p>本記事では、このDynamoDB設計ガイドラインの概要と、特に読んでいただきたい見どころ、ガイドライン作成活動の感想をお届けします。</p>
<h2 id="フューチャーのアーキテクチャ設計ガイドラインとは？">フューチャーのアーキテクチャ設計ガイドラインとは？</h2><p>フューチャーの設計ガイドラインは「社内の有志が作成する、良いアーキテクチャを実現するための設計ガイドライン」です。<br>エンタープライズ領域では、高度なセキュリティや保守運用性などの非機能要件が重視されます。社内で培ったエンタープライズシステム開発向けのノウハウを集約することで、以下のような目的を達成することを目指しています。</p>
<ul>
<li>車輪の再発明を防ぐ: 設計者が悩むポイントを軽減し、本当に必要な設計に集中する</li>
<li>設計品質の標準化: プロジェクト間での品質のばらつきや属人性をなくす</li>
<li>リスクの低減: 非機能要件や法令遵守などの考慮漏れを防ぐ</li>
<li>知見の共有: チーム・組織内での知見の共有を促進する</li>
</ul>
<p>「答えを提供するのではなく、考えるための土台を提供する」というスタンスです。プロジェクト固有の要件に合わせて、評価・上書きして利用することを想定しています。</p>
<h2 id="なぜDynamoDBのガイドラインを作ったのか">なぜDynamoDBのガイドラインを作ったのか</h2><p>AWSが提供するフルマネージドなNoSQLデータベースである「DynamoDB」。<br>インフラのメンテナンスがほとんど不要で、アクセスパターンが決まっている場合の超高速処理やスケーリング性能は非常に魅力的です。</p>
<p>しかし、RDBMS（リレーショナルデータベース）と同じ感覚で設計・利用しようとすると、痛い目を見ます。<br>「SQLが使えない」「複雑な集計ができない」といった特性を正しく理解し、技術選定の段階から慎重に判断する必要があるため、今回その勘所をガイドラインとしてまとめることにしました。</p>
<h2 id="DynamoDB設計ガイドラインの目次">DynamoDB設計ガイドラインの目次</h2><p>本ガイドラインは、技術選定から運用・セキュリティまで、エンタープライズ開発で考慮すべき事項を網羅しています。</p>
<ul>
<li>はじめに</li>
<li>DynamoDB選定</li>
<li>命名規則</li>
<li>データモデリング</li>
<li>インデックス設計</li>
<li>API利用</li>
<li>DynamoDB Streams</li>
<li>他のデータストアとの組み合わせ</li>
<li>SDK</li>
<li>テスト</li>
<li>性能</li>
<li>コスト</li>
<li>可用性</li>
<li>監視</li>
<li>セキュリティ</li>
<li>移行</li>
</ul>
<h2 id="ガイドラインの見どころ">ガイドラインの見どころ</h2><h3 id="1-迷ったらRDBMSを選定する！〜DynamoDB採用のノックアウト要件〜">1. 迷ったらRDBMSを選定する！〜DynamoDB採用のノックアウト要件〜</h3><p>DynamoDBは万能ではありません。「予測可能なアクセスパターンに対して高トラフィックを低遅延で処理できる」という強みは、クエリの柔軟性との引き換えで成り立っています。</p>
<p>そのため、ガイドラインでは「以下の要件に該当する場合はDynamoDB採用のノックアウト要件とし、RDBMSを検討すべき」と推奨しています。</p>
<ul>
<li>将来アクセスパターンが変化する場合（キーを基本的に変更できないため）</li>
<li>リアルタイムかつ柔軟な分析・複雑な検索条件が必要な場合</li>
<li>複数のデータ集約をまたがる、厳格な一貫性が必要な場合（会計システムや在庫引当など）</li>
</ul>
<p>「高負荷になりそうだからとりあえずDynamoDB」ではなく、Aurora（PostgreSQL）などのRDBMSで適切なチューニングを行えば、秒間700トランザクション（1トランザクションあたり3SQL程度）を処理できた実績もあるため、「まずはRDBMSで解決できない課題がある時にのみDynamoDBの採用を検討する」という考え方を推奨しています。</p>
<h3 id="2-データモデリングは「1にも2にもアクセスパターン」">2. データモデリングは「1にも2にもアクセスパターン」</h3><p>RDBMSとDynamoDBでは、設計アプローチ（プロセス）が全く異なります。ここを理解せずにテーブル設計をすると失敗します。</p>
<ul>
<li>RDBMSの設計: データの構造と正規化に重点を置いてデータモデルを設計し、その後でクエリを作成する（データ中心）</li>
<li>DynamoDBの設計: アプリケーションがデータをどのように利用するか、「アクセスパターン」の分析とデータモデルの設計を同時に行う（アプリケーション中心）</li>
</ul>
<p>DynamoDBは基本的にテーブルの結合（JOIN）ができず、キーを基準にデータにアクセスします。そのため、ビジネス要件から「従業員情報をIDで検索する」などのユースケースを洗い出し、それを満たせるテーブルとインデックスのスキーマを設計していく具体的なプロセスをガイドライン内で解説しています。</p>
<h2 id="ガイドライン作成の裏側（会の流れと雰囲気）">ガイドライン作成の裏側（会の流れと雰囲気）</h2><p>今回のガイドライン作成は、社内の有志メンバーが集まり、約2ヶ月間（30分×全8回）のタスクフォース形式で実施しました。</p>
<ol>
<li>募集・キックオフ: Slackで参加者を募集し、目次案と担当を決定。</li>
<li>非同期執筆: Google Docsの提案モードを使って各自が原稿を執筆。</li>
<li>定例レビュー: 週1回のミーティングでレビューと議論を実施。</li>
</ol>
<h2 id="ガイドライン作成に参加しての感想">ガイドライン作成に参加しての感想</h2><p>今回、社内の有志活動としてこのガイドラインの執筆に参加しましたが、非常に得られるものが多かったです。</p>
<p>シニアで技術に強いメンバーが多く参加しており、手厚いレビューを受けられたことや、普段関わることの少ないスペシャリストたちと繋がりを持てたことは貴重な経験でした。また、異なるプロジェクトの要件や運用方法を知ることで、視野が大きく広がりました。</p>
<p>技術ブログの執筆やガイドライン作成といった「言語化・体系化」のアウトプット作業を通じて、確実に構成力や文章力が上がったと感じています。この力は普段の業務でも大いに活きています。</p>
<h2 id="おわりに">おわりに</h2><p>今回公開したDynamoDB設計ガイドラインが、皆様のシステム開発における技術選定や設計の一助になれば幸いです。</p>
<p>ぜひ、実際のドキュメントをご覧ください。フィードバックやGitHubへのPull Requestもお待ちしております！</p>
]]></content>
    <summary type="html">フューチャーでは、社内の有志メンバーが集まり、システム開発におけるベストプラクティスをまとめた「アーキテクチャ設計ガイドライン」の整備・公開を行っています。これまでフロントエンドからバックエンド、クラウドインフラ、Git戦略など幅広い分野のガイドラインを公開してきましたが、この度新たに「DynamoDB設計ガイドライン」を公開しました！</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="DynamoDB" scheme="https://future-architect.github.io/tags/DynamoDB/"/>
    <category term="ガイドライン" scheme="https://future-architect.github.io/tags/%E3%82%AC%E3%82%A4%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3/"/>
  </entry>
  <entry>
    <title>データベースと向き合う決意をしてから3年たった</title>
    <link href="https://future-architect.github.io/articles/20251106a/"/>
    <id>https://future-architect.github.io/articles/20251106a/</id>
    <published>2025-11-05T15:00:00.000Z</published>
    <updated>2025-11-05T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<p>秋のブログ週間の4本目です。</p>
<p>3年前の秋のブログ週間でデータベースと向き合う決意というエントリーを書きました。3年経ちましたが、多少ベクトルは変わりましたが今も基本的な気持ちは変わっていません。むしろ、「生成AIによって自然言語で気軽に作れるようになった」ことで、SQLの敷居は大きく下がりました。</p>
<h2 id="DFDのガイドライン作りを始めた">DFDのガイドライン作りを始めた</h2><p>今年はDFDの本が出ました。5月にブログも書きました。</p>
<ul>
<li>データフローダイアグラム本の献本をいただきました</li>
</ul>
<blockquote>
<p>本書を読んだことで、フューチャー社内のDFDが何を工夫してどのように活用してきたのか、というのを相対的に見れるようになりました。フューチャースタイルも有志で公開しているガイドライン集にいつか入れられたらいいな、ということを思いました。そして、いつか他社のDFD図の発展がどのようになっているのかとかも世の中に出てきて、50年分の図の進化が行われると良いな、という気持ちを新たにしました。</p>
</blockquote>
<p>言ったからにはきちんとやろうということでざっとGoogle Docsで20ページぐらい書いて社内の有識者からコメントもらったりしているところです。年内にはある程度目処をつけたいところです。DFD本の表紙にも「いにしえの技術」と書かれているぐらいなので、オリジナルのものをそのまま使っている人はレアかと思います。だいたい各社の中で独自の進化を遂げたDFDを使っているのではないかと思っています。今のガイドラインも社内のDFDの知見のガイドライン化ですし、他社の人が触れる場合には「どこがオリジナルか」というのを明示するのは大事かな、と思い、<br>DFDの原典とされる本も買って読み始めました。</p>
<img fetchpriority="high" src="/images/2025/20251106a/IMG_7227.JPG" alt="IMG_7227.JPG" width="1200" height="900">

<p>構造化分析の本は初めて読みましたが、自分がこの手の勉強をした、UMLやらオブジェクト指向分析よりも実は筋がいいのでは、と読みながら思ったり。2000年ごろのアジャイルブームはUMLの複雑なモデリングへのアンチテーゼだったと思いますが、あのときにUML（を売りたいベンダー）が否定してきた過去のシンプルな手法に目をもっと向けられていたらなぁ、というのはちょっと思いました。</p>
<p>あと、この本で強調されていることは「DFDではコントロールの流れは書かない」ということです。これはDFDのガイドラインでも触れていましたが、この特性は2つの点で大事だな、と思いました。</p>
<p>1つは以前書いたReactのDFDがなぜうまくいったのかという点です。コントロールフロー≒手続きは、いわゆる条件式とかループです。これらはDFDの1つ1つのプロセスの中に閉じ込められているものです。一方、今時のウェブフロントエンドは手続きではなく、宣言的に書くという思想で作られています。どちらもコントロールフローを見せない仕組みなので相性が良いのは自然なことだったんだな、と思いました。</p>
<p>もう1つはコントロールをかかない、そのままソフトウェアに落ちたりしないという点ですね。2000年初頭はCASEツールとかその手のツールで設計することでソフトウェアの自動生成などを行って効率アップ、というのがやたら喧伝されていました。その手のツールからすると、書いてもそこから生成できなければ描くだけ無駄ということで、DFDが2000年代に冷遇された理由はこれだったのかもな、というということです。</p>
<h2 id="2Way-SQLを作り始めた">2Way SQLを作り始めた</h2><img src="/images/2025/20251106a/スクリーンショット_2025-11-05_20.09.00.png" alt="スクリーンショット_2025-11-05_20.09.00.png" width="1200" height="912" loading="lazy">

<p>今年ちょくちょく生成AI使ってみた外部発表をしていますが、その題材として作っているのが2Way SQLのライブラリです。Goで作っていますが、ランタイムはGoに引き続き、Pythonとかも実装しようとしています。これも、3年前のエントリーでも作っていると書いていましたが、まあ何度目かのチャレンジ。だいたいこの手のやつは3回ぐらい作って初めて納得いくものができるというのが実感ですが、実際、3回目ぐらいですかね。これまでで一番コード行数は多いかも。5ヶ月ぐらいで11万行ぐらい？</p>
<ul>
<li>github.com&#x2F;shibukawa&#x2F;snapsql</li>
</ul>
<p>生のSQLに制御コメントを入れることでそのままDBにも投げられるし、アプリケーションコードの中でSQLのテンプレートとしても使える、というのが2Way SQLです。2Way SQL自体はたくさん実装が世の中にはあるのですが、だいたい式言語が組み込まれていて、それがJava依存のライブラリで、Java以外ではマイナーな存在となってしまっています。そこも、GoogleのCELを採用することでいろんな言語で使えるようにしたいなと思っているところです。</p>
<ul>
<li>ソースは、SQLもしくは、文芸的プログラミングスタイルのSQLが書かれたMarkdown</li>
<li>SQLはユーザーが作るが、レスポンスの型と呼び出しコードは自動生成<ul>
<li>BlogPost -&gt; Commentのように、親子関係がある要素群を子スライス（配列）にまとめて返す</li>
</ul>
</li>
<li>テスト機能内蔵。テストのレスポンスを使ったモック機能も内蔵</li>
<li>UPDATE&#x2F;DELETEでWHERE句がない（条件なしで前適用）場合にオプトアウトでエラーチェックを追加</li>
</ul>
<p>だいたい、どのライブラリも「クエリービルダー」として生SQLを元にWHERE句の条件を足したり減らしたりしたSQLを組み立てる感じです。だいたいの2Way SQLのライブラリは呼び出すところぐらいですが、Goのsqlcのように、型がしっかりついた関数を生成します。クエリー情報を元にサブクエリとかCTEの中も追いかけて型推論しています。WHERE句の条件でレスポンスが単数になるか複数になるかとかも静的解析しています。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-1truduf-1" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1truduf-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">// 結果のオブジェクト</span></span><br><span class="line"><span class="keyword">type</span> BoardListResult <span class="keyword">struct</span> &#123;</span><br><span class="line">	ID         <span class="type">int</span>        <span class="string">`json:&quot;id&quot;`</span></span><br><span class="line">	Name       <span class="type">string</span>     <span class="string">`json:&quot;name&quot;`</span></span><br><span class="line">	Status     <span class="type">string</span>     <span class="string">`json:&quot;status&quot;`</span></span><br><span class="line">	ArchivedAt *time.Time <span class="string">`json:&quot;archived_at&quot;`</span></span><br><span class="line">	CreatedAt  time.Time  <span class="string">`json:&quot;created_at&quot;`</span></span><br><span class="line">	UpdatedAt  time.Time  <span class="string">`json:&quot;updated_at&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 生成される関数。クエリーの解析結果でレスポンスは変わる</span></span><br><span class="line"><span class="comment">// * WHERE句が主キー指定で1要素しか返らないことが明確なら構造体そのまま</span></span><br><span class="line"><span class="comment">// * レスポンスが複数レコードになるならrange over func</span></span><br><span class="line"><span class="comment">// * returningがないupdate/insert/deleteならsql.Result</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BoardList</span><span class="params">(ctx context.Context, executor snapsqlgo.DBExecutor, opts ...snapsqlgo.FuncOpt)</span></span> iter.Seq2[*BoardListResult, <span class="type">error</span>] &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>トランザクションは隠さないようにしており、2つ目の引数は<code>sql.Conn</code>か<code>sql.DB</code>, <code>sql.Tx</code>何でも受けられるようにしています。3つめ以降はテンプレートにパラメータがあればそれが入り、型安全な関数となっています。</p>
<p>他にもPlaywright Lightnings #1で発表した、E2EテストのFixtureやリクエスト後のDBの状態をテストするツールであるgithub.com&#x2F;shibukawa&#x2F;dbtestifyの機能も取り込み、アプリケーションコードを実装しなくてもSQLだけでユニットテストできる機能なども入っています。このテストの期待されるレスポンスをモックとして返せるようにもしています。「モックを使うと実装とずれても気づけない」という問題があったりしますが、その問題を解消できるかな、と。</p>
<p>このツールでやっているのは、SQLをトークン分割して構文解析をしてASTを組み立てて、エラーチェックなどをしつつシンプルな命令列に変換してから、最適化を行なって最終的にGoなどのコードを生成する、という動きをします。生成時に文字列結合（<code>CONCAT</code>, <code>||</code>）、キャスト（<code>CAST( AS type)</code>, <code>::type</code>）や日付関数などは生成先に合わせて吸収するとかもしてます。まあコンパイラですね。パーサコンビネータから自作してます。ツール自体は結構なロジックの量になっていますが、静的なコードを生成するようにしており、アプリケーションが太らないようにランタイムの依存はほとんどないという思想でやっています。最後のコード生成部分はなるべく薄くしているので、いろんな言語に対応させたいです。</p>
<p>サンプルコードを作って動かしてみたりしていますが、Goに関しては大体できてきたかなというところです。ドキュメントも昨日、今日でだいぶブラッシュアップしました。生成AIにお願いすると無い機能まであると説明を書き出すのでなかなか大変ですね。</p>
<p>実際にプロジェクトで使えるものを、という意識で作っていますが作ること自体が勉強になりますね。DBMSごとに書けるSQLの違いとかを意識することになりますし。生成AIにきくとぱっとまとめを作ってくれるけど間違っていることがあるのでまた調べますし。昔は使えなかったけど今は使えるSQLiteの<code>RETURNING</code>とかそういう感じで。</p>
<h2 id="次のステップ">次のステップ</h2><p>だいぶデータベースへの苦手意識も減りデータベースのモデルの議論も積極的に行えるようになってきました。</p>
<p>2Way SQLも「しっかりしたのを作りたい」というのをずっと思っていたところ、生成AIが重い腰を上げてくれてパッと走り出すことができました。この手の盆栽的な目標があると、生成AIの無料枠が降ってきたときに「お、機能追加するぞ！」というネタになっていいですね。ぼちぼち、Python、Java、TypeScriptあたりのコード生成は追加していこうと思っています。Pythonはasyncioにきっちり対応（むしろそれ以外サポートしない）勢いでやろうかと思っています。</p>
<p>開発環境の変化、生成AI活用という点で、GitHubやGitLabでプレビューしやすくて生成AIフレンドリーなツール群、というのが今後は求められていく流れは変わらないと思います。生成AIは同じ入力に対して同じ結果を確実に返すのは苦手ですし、そこは何かしらのツールが必要です。論物変換をきちんとこなせるデータベース系のツールはまだまだ少ないなと思うし、その手のツールは今後も作っていきたいなと思っています。</p>
<p>物理モデルが作られると概念モデルがメンテされなくて放置される問題とかそういうのをいつか改善できないかな、と思ったりしています。</p>
]]></content>
    <summary type="html">3年前の秋のブログ週間でデータベースと向き合う決意というエントリーを書きました。3年経ちましたが、多少ベクトルは変わりましたが今も基本的な気持ちは変わっていません。むしろ、「生成AIによって自然言語で気軽に作れるようになった」ことで、SQLの敷居は大きく下がりました。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="2WaySQL" scheme="https://future-architect.github.io/tags/2WaySQL/"/>
    <category term="DFD" scheme="https://future-architect.github.io/tags/DFD/"/>
    <category term="エッセー" scheme="https://future-architect.github.io/tags/%E3%82%A8%E3%83%83%E3%82%BB%E3%83%BC/"/>
    <category term="生成AI" scheme="https://future-architect.github.io/tags/%E7%94%9F%E6%88%90AI/"/>
  </entry>
  <entry>
    <title>PostgreSQL 18の新機能、仮想生成列の使い方や制約、格納生成列との使い分けについて</title>
    <link href="https://future-architect.github.io/articles/20251030a/"/>
    <id>https://future-architect.github.io/articles/20251030a/</id>
    <published>2025-10-29T15:00:00.000Z</published>
    <updated>2025-10-29T15:00:00.000Z</updated>
    <author><name>真野隼記</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20251030a/top.jpg" alt="top.jpg" width="800" height="664">

<p>PostgreSQL 18連載の6本目です。</p>
<p>PostgreSQL 18がリリースされ、仮想生成列についてまとめます。PostgreSQLで従来から利用できた格納生成列や、生成列自体と合わせて紹介します。</p>
<h2 id="生成列">生成列</h2><p>生成列は他の列から計算される列のことで、テーブルに対するビューをつくるように、ある列に対してビューのような列を作ることができます。ビューにも、MViewと通常のViewがあるように、生成列も「格納生成列」と「仮想生成列」の2種類があります。格納生成列は、登録&#x2F;更新時に計算されて物理的にストレージが割り当てられます（MVIEWに似ています）。仮想列は列が読み取られる時に動的に計算されます（Viewに似ています）。</p>
<p>PostgreSQL 12で「格納」生成列が利用可能となり、今回18から「仮想」生成列が利用可能となりました。ここまで説明した内容をざっと、表でまとめました。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>特徴</th>
<th>格納生成列</th>
<th>仮想生成列</th>
</tr>
</thead>
<tbody><tr>
<td>サポート</td>
<td>PostgreSQL 12以降</td>
<td>PostgreSQL 18以降</td>
</tr>
<tr>
<td>説明</td>
<td>書き込み時に計算し、ストレージに保存される生成列</td>
<td>読み取り時に計算され、ストレージに保存されない生成列</td>
</tr>
<tr>
<td>ストレージ容量</td>
<td>⚠️消費する</td>
<td>✅️消費なし</td>
</tr>
<tr>
<td>書き込み性能</td>
<td>⚠️やや遅くなる</td>
<td>✅️影響なし</td>
</tr>
<tr>
<td>読み取り性能</td>
<td>✅️計算済みのため</td>
<td>⚠️都度計算するため</td>
</tr>
</tbody></table></div>
<p>生成列の全般に共通する使い方としては、「導出列」があります。導出列とは、他のカラムから算出できる列です。まさに生成列の用途にドンピシャ被りですね。一般論としては、導出列をもたせることは冗長性であるため、SSoT（信頼できる唯一の情報源）原則を守るため、作成しない方針を取るチームが多いでしょう。ただ、導出列にインデックスを貼りたいといった性能要件や、その他、設計の見える化や運用要件などで作成することがあります。</p>
<p>例をいくつか上げます。（凡例: 元の列 -&gt; 導出列）</p>
<ul>
<li>例1: 単価×数量×税係数×割引係数 -&gt; 請求金額</li>
<li>例2: 姓 + 名 -&gt; フルネーム</li>
<li>例3: 日付カラム -&gt; 曜日</li>
<li>例4: 生年月日 -&gt; 年齢</li>
<li>例5: メールアドレス -&gt; 検索用メールアドレス（全て小文字にするなど加工し、検索用に正規化する）</li>
</ul>
<p>どれもアプリケーション側で計算して明示的にインサートしても良いものですが、生成列を使用することでその列が読み取り専用であることを開発者に伝えることができ、また整合性を伴わない更新事故を無くすことができます。また、テーブル定義上で宣言的に意図を伝えられる点で、きっと生成AIとも親和性が良いと思います（これは未検証、想像で書いています）。</p>
<p>ただ、PostgreSQL設計ガイドライン では、ライフサイクルがアプリケーションに近く、格納生成列の定義変更はAccessExclusiveLock（SELECTもブロックされるロック）を取って全行更新の処理が必要となるため、この用途での格納生成列の使用は非推奨としていました。同様の部分の懸念はそれなりの規模感のシステムでは共通的であるため、あまり利用頻度は高くないと思います（※2025年10月29日時点では、仮想生成列についての記述はありません）。ただし、仮想生成列であれば、おそらく定義変更してもAccessExclusiveLockを取らないと思うので、障壁は下がるかもしれません。このあたりを検証していきます。</p>
<h2 id="使用方法の基礎">使用方法の基礎</h2><p>検証環境情報やセットアップは村田さんのB-treeインデックスのスキップスキャン記事の手順に従います。Dockerで <code>postgres:18</code> のイメージを利用します。</p>
<p>生成列の使い方ですが、ドキュメント にあるように、 <code>GENERATED ALWAYS AS (ロジック) STORED</code> で格納生成列、<code>VIRTUAL</code> を付けると仮想生成列になります。デフォルトは <code>VIRTUAL</code> です。身長[cm]をインチ版と尺版を生成列で作ってみます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-1" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> people (</span><br><span class="line">    person_id <span class="type">BIGINT</span> GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    height_cm <span class="type">numeric</span>,</span><br><span class="line">    height_in <span class="type">numeric</span> GENERATED ALWAYS <span class="keyword">AS</span> (height_cm <span class="operator">/</span> <span class="number">2.54</span>) STORED,     <span class="comment">-- 格納生成列</span></span><br><span class="line">    height_syaku <span class="type">numeric</span> GENERATED ALWAYS <span class="keyword">AS</span> (height_cm <span class="operator">*</span> <span class="number">0.033</span>) VIRTUAL <span class="comment">-- 仮想生成列</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure></div>

<p>実際にデータを登録 &amp; 検索します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-2" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">INSERT INTO</span> people (height_cm) <span class="keyword">VALUES</span> (<span class="number">170.5</span>), (<span class="number">158.2</span>), (<span class="number">181.0</span>);</span><br><span class="line"><span class="keyword">INSERT</span> <span class="number">0</span> <span class="number">3</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> people;</span><br><span class="line"> person_id <span class="operator">|</span> height_cm <span class="operator">|</span>      height_in      <span class="operator">|</span> height_syaku</span><br><span class="line"><span class="comment">-----------+-----------+---------------------+--------------</span></span><br><span class="line">         <span class="number">1</span> <span class="operator">|</span>     <span class="number">170.5</span> <span class="operator">|</span> <span class="number">67.1259842519685039</span> <span class="operator">|</span>       <span class="number">5.6265</span></span><br><span class="line">         <span class="number">2</span> <span class="operator">|</span>     <span class="number">158.2</span> <span class="operator">|</span> <span class="number">62.2834645669291339</span> <span class="operator">|</span>       <span class="number">5.2206</span></span><br><span class="line">         <span class="number">3</span> <span class="operator">|</span>     <span class="number">181.0</span> <span class="operator">|</span> <span class="number">71.2598425196850394</span> <span class="operator">|</span>       <span class="number">5.9730</span></span><br><span class="line">(<span class="number">3</span> <span class="keyword">rows</span>)</span><br></pre></td></tr></table></figure></div>

<p>読み取りすると、自動変換された値が取得できました。インチ・尺のどちらも実生活で意識して使ったことが無いでので合っているかよく分からないですが、自動計算されるのは便利です。また、INSERT文がシンプルになるという心理的な嬉しさを思ったより感じました。</p>
<h2 id="検証サマリ">検証サマリ</h2><p>格納生成列、仮想生成列それぞれで以下の観点を比較します。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">検証項目</th>
<th align="left">結果</th>
</tr>
</thead>
<tbody><tr>
<td align="left">1. 生成列で自分自身の列を参照できるか</td>
<td align="left">不可（生成列は生成列を参照できないた）</td>
</tr>
<tr>
<td align="left">2. IDENTITY列を参照できるか</td>
<td align="left">可能（格納・仮想の両方で可能）</td>
</tr>
<tr>
<td align="left">3. 他のテーブルの列を参照できるか</td>
<td align="left">不可（サブクエリの利用は不可）</td>
</tr>
<tr>
<td align="left">4. ネストした生成列定義は可能か</td>
<td align="left">不可（生成列は生成列を参照できないため）</td>
</tr>
<tr>
<td align="left">5. 計算途中でnull値が混入したらどうなるか</td>
<td align="left">普通のクエリと同じように　<code>null</code>　になる</td>
</tr>
<tr>
<td align="left">6. NOT NULL制約を付けることができるか</td>
<td align="left">可能。仮想生成列も登録時チェックになる</td>
</tr>
<tr>
<td align="left">7. 生成列は一意制約を付けることができるか</td>
<td align="left">格納生成列は可能。仮想生成列は不可。式インデックスで代用</td>
</tr>
<tr>
<td align="left">8. 生成列はインデックスに使えるか</td>
<td align="left">格納生成列は可能。仮想生成列は式インデックスで代用</td>
</tr>
<tr>
<td align="left">9. 生成列はPKにできるか</td>
<td align="left">格納生成列は可能。仮想生成列はインデックスを持てないので不可</td>
</tr>
<tr>
<td align="left">10. 生成列はパーティションキーに使えるか</td>
<td align="left">不可（<code>STORED</code> &#x2F; <code>VIRTUAL</code> 共に不可）</td>
</tr>
<tr>
<td align="left">11. テーブル作成後に後から生成列を追加できるか</td>
<td align="left">可能。AccessExclusiveLock を取る</td>
</tr>
<tr>
<td align="left">12. NOT NULL制約を付けた生成列を後から追加すると処理時間はどうなるか</td>
<td align="left"><code>NOT NULL</code>検証のためのテーブルフルスキャンが発生し、変更時間が長くなる</td>
</tr>
<tr>
<td align="left">13. 生成列の定義変更はできるか</td>
<td align="left">不可（<code>DROP COLUMN</code> &amp; <code>ADD COLUMN</code> で対応する必要がある）</td>
</tr>
<tr>
<td align="left">14. 利用しているカラムをRENAMEしたら？</td>
<td align="left">PostgreSQLが自動で定義式を追随・更新してくれる</td>
</tr>
<tr>
<td align="left">15. 利用しているカラムをDROPしたら？</td>
<td align="left">エラーになる。 <code>CASCADE</code> を付けると依存した列ごと削除可能）</td>
</tr>
</tbody></table></div>
<h3 id="1-生成列で自分自身の列を参照できるか">1. 生成列で自分自身の列を参照できるか</h3><p>試しに、生成列で自分自身（<code>self_value</code>）を参照した定義を実行してみます。内容に意味は無いですが、 <code>self_value = self_value + 1</code> となるような計算式を設定しています。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-3" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> self_reference (</span><br><span class="line">    id <span class="type">BIGINT</span> GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 自分自身（value）を参照して値を生成しようとする仮想列</span></span><br><span class="line">    &quot;self_value&quot; <span class="type">int</span> GENERATED ALWAYS <span class="keyword">AS</span> (self_value <span class="operator">+</span> <span class="number">1</span>) VIRTUAL</span><br><span class="line">);</span><br><span class="line">ERROR:  cannot use generated <span class="keyword">column</span> &quot;self_value&quot; <span class="keyword">in</span> <span class="keyword">column</span> generation expression</span><br><span class="line">LINE <span class="number">5</span>:     &quot;self_value&quot; <span class="type">int</span> GENERATED ALWAYS <span class="keyword">AS</span> (self_value <span class="operator">+</span> <span class="number">1</span>) VI...</span><br><span class="line">                                                  <span class="operator">^</span></span><br><span class="line">DETAIL:  A generated <span class="keyword">column</span> cannot reference another generated column.</span><br></pre></td></tr></table></figure></div>

<p>結果はNGです。「生成列は他の生成列を参照できない」とありますね。実現したいことも意味不明なので、失敗して当然なので想定通りかなと。この結果は、<code>VIRTUAL</code> を <code>STORED</code> に変えても同じです。</p>
<h3 id="2-IDENTITY列を参照できるか">2. IDENTITY列を参照できるか</h3><p>IDENTITY列（シリアル）を参照できるか確認します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-4" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">CREATE TABLE</span> m_product (</span><br><span class="line">    item_id <span class="type">BIGINT</span> GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    product_name TEXT,</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 格納生成列</span></span><br><span class="line">    item_code_stored TEXT GENERATED ALWAYS <span class="keyword">AS</span> (item_id<span class="operator">*</span><span class="number">10</span>) STORED,</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 仮想生成列</span></span><br><span class="line">    item_code_virtual TEXT GENERATED ALWAYS <span class="keyword">AS</span> (item_id<span class="operator">*</span><span class="number">100</span>) VIRTUAL</span><br><span class="line">);</span><br><span class="line"><span class="keyword">CREATE TABLE</span></span><br><span class="line"></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">INSERT INTO</span> m_product (product_name) <span class="keyword">VALUES</span> (<span class="string">&#x27;Apple&#x27;</span>);</span><br><span class="line"><span class="keyword">INSERT</span> <span class="number">0</span> <span class="number">1</span></span><br><span class="line"></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> m_product;</span><br><span class="line"> item_id <span class="operator">|</span> product_name <span class="operator">|</span> item_code_stored <span class="operator">|</span> item_code_virtual</span><br><span class="line"><span class="comment">---------+--------------+------------------+-------------------</span></span><br><span class="line">       <span class="number">1</span> <span class="operator">|</span> Apple        <span class="operator">|</span> <span class="number">10</span>               <span class="operator">|</span> <span class="number">100</span></span><br><span class="line">(<span class="number">1</span> <span class="type">row</span>)</span><br></pre></td></tr></table></figure></div>

<p>格納生成列・仮想生成列のどちらも問題なく、IDENTITY列を参照できました。</p>
<h3 id="3-他のテーブルの列を参照できるか">3. 他のテーブルの列を参照できるか</h3><p>まず、税率を保持するテーブルを作成します。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> m_tax (</span><br><span class="line">    region_code <span class="type">CHAR</span>(<span class="number">2</span>) <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    tax_rate <span class="type">NUMERIC</span>(<span class="number">4</span>, <span class="number">2</span>)</span><br><span class="line">);</span><br></pre></td></tr></table></figure>

<p>続いて、さきほど作った税率テーブルを参照する、生成列を作ってみます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-5" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> t_order (</span><br><span class="line">    order_id <span class="type">BIGINT</span> GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    base_price <span class="type">NUMERIC</span>,</span><br><span class="line">    region <span class="type">CHAR</span>(<span class="number">2</span>),</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- taxテーブルのtax_rate列を参照する計算式</span></span><br><span class="line">    total_price <span class="type">NUMERIC</span> GENERATED ALWAYS <span class="keyword">AS</span> (</span><br><span class="line">        base_price <span class="operator">*</span> (<span class="number">1.0</span> <span class="operator">+</span> (<span class="keyword">SELECT</span> tax_rate <span class="keyword">FROM</span> m_tax <span class="keyword">WHERE</span> region_code <span class="operator">=</span> region))</span><br><span class="line">    ) VIRTUAL</span><br><span class="line">);</span><br><span class="line">ERROR:  cannot use subquery <span class="keyword">in</span> <span class="keyword">column</span> generation expression</span><br><span class="line">LINE <span class="number">8</span>:         base_price <span class="operator">*</span> (<span class="number">1.0</span> <span class="operator">+</span> (<span class="keyword">SELECT</span> tax_rate <span class="keyword">FROM</span> m_tax WHER...</span><br></pre></td></tr></table></figure></div>

<p>これはエラーになりました。サブクエリはNG（つまり、別テーブルの参照は不可）のようです。</p>
<p>ドキュメントにも <code>References to other tables are not allowed.</code> （他のテーブルは参照できない）と書いていますので、その通りの結果です。格納生成列、仮想生成列ともに結果は変わりません。</p>
<h3 id="4-ネストした生成列定義は可能か">4. ネストした生成列定義は可能か</h3><p>「割引額」という生成列を参照する、「料金」という生成列の定義を試みます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-6" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-6" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> m_price (</span><br><span class="line">    item_id <span class="type">BIGINT</span> GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    base_price <span class="type">NUMERIC</span>,</span><br><span class="line">    discount_rate <span class="type">NUMERIC</span>(<span class="number">3</span>, <span class="number">2</span>) <span class="keyword">DEFAULT</span> <span class="number">0.0</span>,</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 割引額という生成列を定義</span></span><br><span class="line">    discount_amount <span class="type">NUMERIC</span> GENERATED ALWAYS <span class="keyword">AS</span> ( base_price <span class="operator">*</span> discount_rate ) VIRTUAL,</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 他の生成列「割引率」を「料金」という生成列を定義</span></span><br><span class="line">    price <span class="type">NUMERIC</span> GENERATED ALWAYS <span class="keyword">AS</span> ( base_price <span class="operator">-</span> discount_amount ) VIRTUAL</span><br><span class="line">);</span><br><span class="line">ERROR:  cannot use generated <span class="keyword">column</span> &quot;discount_amount&quot; <span class="keyword">in</span> <span class="keyword">column</span> generation expression</span><br><span class="line">LINE <span class="number">10</span>: ... price <span class="type">NUMERIC</span> GENERATED ALWAYS <span class="keyword">AS</span> ( base_price <span class="operator">-</span> discount_a...</span><br><span class="line">                                                              <span class="operator">^</span></span><br><span class="line">DETAIL:  A generated <span class="keyword">column</span> cannot reference another generated column.</span><br></pre></td></tr></table></figure></div>

<p>こちらもエラーになります。「他の生成列を参照できません」という内容です。デジャブ感があるのは、「生成列で自分自身の列を参照できるか」節でもこのメッセージを見たためです。格納生成列、仮想生成列のどちらも同じ結果になります。</p>
<p>ドキュメントにも、 <code>The generation expression can refer to other columns in the table, but not other generated columns.</code>（生成式はテーブル内の他の列を参照できますが、他の生成列を参照できません。）とあるので、記載通りの挙動です。</p>
<h3 id="5-計算途中でnull値が混入したらどうなるか">5. 計算途中でnull値が混入したらどうなるか</h3><p>例えば、総額&#x3D;単価×数量 という生成列を定義します。この時、単価がNULLの場合にはどのように挙動するか確かめます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-7" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-7" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> t_order (</span><br><span class="line">    item_id <span class="type">BIGINT</span> GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    unit_price <span class="type">NUMERIC</span>,</span><br><span class="line">    quantity <span class="type">INT</span>,</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 生成列（単価x数量）で構成</span></span><br><span class="line">    total_price <span class="type">NUMERIC</span> GENERATED ALWAYS <span class="keyword">AS</span> (unit_price <span class="operator">*</span> quantity) VIRTUAL</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- データ登録（unit_priceにNULLを入れる）</span></span><br><span class="line"><span class="keyword">INSERT INTO</span> t_order (unit_price, quantity) <span class="keyword">VALUES</span> (<span class="keyword">NULL</span>, <span class="number">5</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 検索</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> t_order;</span><br><span class="line"> item_id <span class="operator">|</span> unit_price <span class="operator">|</span> quantity <span class="operator">|</span> total_price</span><br><span class="line"><span class="comment">---------+------------+----------+-------------</span></span><br><span class="line">       <span class="number">1</span> <span class="operator">|</span>            <span class="operator">|</span>        <span class="number">5</span> <span class="operator">|</span></span><br><span class="line">(<span class="number">1</span> <span class="type">row</span>)</span><br></pre></td></tr></table></figure></div>

<p>unit_priceがNULLの場合は、total_priceもNULLという結果です。SQL的に自然な挙動ですね。回避するには、unit_priceやquantityにNOT NULL制約を付けたり、COALESCEでNULLを実値に置き換える必要があります。</p>
<h3 id="6-NOT-NULL制約を付けることができるか">6. NOT NULL制約を付けることができるか</h3><p>ちょっとテクニカルなテーブル定義に書き換えます。生成列のtotal_priceのみNOT NULL制約を付けて、ソースのunit_price, quantity はNULL許容にします。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-8" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-8" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">CREATE TABLE</span> t_order (</span><br><span class="line">    item_id <span class="type">BIGINT</span> GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    unit_price <span class="type">NUMERIC</span>,</span><br><span class="line">    quantity <span class="type">INT</span>,</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 生成列（単価x数量）で構成。★さらに、NOT NULL制約を追加</span></span><br><span class="line">    total_price <span class="type">NUMERIC</span> GENERATED ALWAYS <span class="keyword">AS</span> (unit_price <span class="operator">*</span> quantity) VIRTUAL <span class="keyword">NOT NULL</span></span><br><span class="line">);</span><br><span class="line"><span class="keyword">CREATE TABLE</span></span><br></pre></td></tr></table></figure></div>

<p>これは成功します。生成列でもNOT NULL制約を付与は可能です。</p>
<p>続いて、先ほどと同様 <code>unit_price</code> に NULL 値を含んだINSERT文を実行します。格納生成列の場合はインサート時に計算しますが、仮想生成列の場合は読み取り時に計算されるため、検知できない気がしますが…？</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-9" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-9" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">INSERT INTO</span> t_order (unit_price, quantity) <span class="keyword">VALUES</span> (<span class="keyword">NULL</span>, <span class="number">5</span>);</span><br><span class="line">ERROR:  <span class="keyword">null</span> <span class="keyword">value</span> <span class="keyword">in</span> <span class="keyword">column</span> &quot;total_price&quot; <span class="keyword">of</span> relation &quot;t_order&quot; violates <span class="keyword">not</span><span class="operator">-</span><span class="keyword">null</span> <span class="keyword">constraint</span></span><br><span class="line">DETAIL:  Failing <span class="type">row</span> <span class="keyword">contains</span> (<span class="number">1</span>, <span class="keyword">null</span>, <span class="number">5</span>, virtual).</span><br></pre></td></tr></table></figure></div>

<p>なんと、仮想生成列でも、インサート時に登録が失敗しました。仮想生成列も登録時に計算しているようです。仮想生成列は格納生成列に比べて、登録時に計算しないから高速というのは、必ずしも成立する話ではないように感じます。</p>
<p>ちなみに、格納生成列でもこの挙動は変わりません（こちらは直感的な動作かなと思います）。</p>
<p>この検証は「11」節ではさらに詳しく調べています。</p>
<h3 id="7-生成列は一意制約を付けることができるか">7. 生成列は一意制約を付けることができるか</h3><p>格納生成列の場合は成功します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-10" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-10" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">CREATE TABLE</span> t_order_detail (</span><br><span class="line">    order_id <span class="type">BIGINT</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    item_id <span class="type">BIGINT</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    quantity <span class="type">INT</span>,</span><br><span class="line">    order_item_key TEXT GENERATED ALWAYS <span class="keyword">AS</span> (order_id::TEXT <span class="operator">||</span> <span class="string">&#x27;-&#x27;</span> <span class="operator">||</span> item_id::TEXT) STORED <span class="keyword">UNIQUE</span></span><br><span class="line">);</span><br><span class="line"><span class="keyword">CREATE TABLE</span></span><br></pre></td></tr></table></figure></div>

<p>仮想生成の場合は、失敗します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-11" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-11" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">CREATE TABLE</span> t_order_detail (</span><br><span class="line">    order_id <span class="type">BIGINT</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    item_id <span class="type">BIGINT</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    quantity <span class="type">INT</span>,</span><br><span class="line">    order_item_key TEXT GENERATED ALWAYS <span class="keyword">AS</span> (order_id::TEXT <span class="operator">||</span> <span class="string">&#x27;-&#x27;</span> <span class="operator">||</span> item_id::TEXT) VIRTUAL <span class="keyword">UNIQUE</span></span><br><span class="line">);</span><br><span class="line">ERROR:  <span class="keyword">unique</span> constraints <span class="keyword">on</span> virtual generated columns <span class="keyword">are</span> <span class="keyword">not</span> supported</span><br></pre></td></tr></table></figure></div>

<p>これは後述するインデックスのサポート有無の挙動の差でしょう。なお、これまた後述する式インデックスに一意制約をつけることで、実質的に、仮想生成列に一意制約をつけることはできます。</p>
<h3 id="8-生成列はインデックスに使えるか">8. 生成列はインデックスに使えるか</h3><p>格納生成列、仮想生成列それぞれにインデックスを追加してみます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-12" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-12" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> m_user (</span><br><span class="line">    user_id <span class="type">BIGINT</span> GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    first_name TEXT,</span><br><span class="line">    last_name TEXT,</span><br><span class="line">    email TEXT,</span><br><span class="line">    <span class="comment">-- (1) 格納生成列: 大文字小文字を区別せずにメールアドレスを検索するため</span></span><br><span class="line">    email_lower TEXT GENERATED ALWAYS <span class="keyword">AS</span> (<span class="built_in">LOWER</span>(email)) STORED,</span><br><span class="line">    <span class="comment">-- (2) 仮想生成列: 用途: 姓と名を連結して表示・検索するため</span></span><br><span class="line">    full_name TEXT GENERATED ALWAYS <span class="keyword">AS</span> (last_name <span class="operator">||</span> <span class="string">&#x27; &#x27;</span> <span class="operator">||</span> first_name) VIRTUAL</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 格納生成列へのインデックス追加は成功</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">CREATE</span> INDEX idx_01_m_user <span class="keyword">ON</span> m_user (email_lower);</span><br><span class="line"><span class="keyword">CREATE</span> INDEX</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 仮想生成列へのインデックス追加は失敗</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">CREATE</span> INDEX idx_02_m_user <span class="keyword">ON</span> m_user (full_name);</span><br><span class="line">ERROR:  indexes <span class="keyword">on</span> virtual generated columns <span class="keyword">are</span> <span class="keyword">not</span> supported</span><br></pre></td></tr></table></figure></div>

<p>仮想生成列へのインデックス追加はサポートされていないようです（まぁ実体がないのでそれはそう）。直接的な回避方法としては、式インデックスを使うことになるでしょう。つまり、仮想生成列の式定義と同じ式を、式インデックスに指定します。</p>
<p>例えば、以下のように <code>idx_02_m_user</code> の定義を変更します。テーブル作成と式が重複するのですが仕方なしです。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-13" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-13" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">CREATE</span> INDEX idx_02_m_user <span class="keyword">ON</span> m_user ((last_name <span class="operator">||</span> <span class="string">&#x27; &#x27;</span> <span class="operator">||</span> first_name));</span><br><span class="line"><span class="keyword">CREATE</span> INDEX</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ダミーデータ登録</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">INSERT INTO</span> m_user (first_name, last_name, email)</span><br><span class="line"><span class="keyword">VALUES</span> (<span class="string">&#x27;Taro&#x27;</span>, <span class="string">&#x27;Yamada&#x27;</span>, <span class="string">&#x27;Taro.Yamada@example.com&#x27;</span>),</span><br><span class="line">       (<span class="string">&#x27;Hanako&#x27;</span>, <span class="string">&#x27;Suzuki&#x27;</span>, <span class="string">&#x27;hanako.suzuki@example.jp&#x27;</span>);</span><br><span class="line"><span class="keyword">INSERT</span> <span class="number">0</span> <span class="number">2</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 件数が少ないとbitmap scanが選択されがちなのでOFF</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">SET</span> enable_bitmapscan <span class="operator">=</span> OFF;</span><br><span class="line"><span class="keyword">SET</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- インデックスが使われていることを確認（実質、式インデックスの確認）</span></span><br><span class="line">postgres<span class="operator">=</span># EXPLAIN <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> m_user</span><br><span class="line"><span class="keyword">WHERE</span> full_name <span class="operator">=</span> <span class="string">&#x27;Yamada Taro&#x27;</span>;</span><br><span class="line">                                   QUERY PLAN</span><br><span class="line"><span class="comment">--------------------------------------------------------------------------------</span></span><br><span class="line"> Index Scan <span class="keyword">using</span> idx_02_m_user <span class="keyword">on</span> m_user  (cost<span class="operator">=</span><span class="number">0.15</span>.<span class="number">.12</span><span class="number">.19</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">2</span> width<span class="operator">=</span><span class="number">168</span>)</span><br><span class="line">   Index Cond: (((last_name <span class="operator">||</span> <span class="string">&#x27; &#x27;</span>::text) <span class="operator">||</span> first_name) <span class="operator">=</span> <span class="string">&#x27;Yamada Taro&#x27;</span>::text)</span><br><span class="line">(<span class="number">2</span> <span class="keyword">rows</span>)</span><br></pre></td></tr></table></figure></div>

<p>実行計画レベルで、式インデックスが使われていることを確認できました。多少の回避方法が必要ですが、仮想列も事実上、インデックスを貼れると思ってよいでしょう。</p>
<h3 id="9-生成列はPKにできるか">9. 生成列はPKにできるか</h3><p>例えば、受注明細トランで、受注番号+商品IDを組み合わせてPKにするケースを考えます（普通は、サロゲートにして欲しい案件ですが、あくまで動作確認上の”例”です）。</p>
<p>まず格納生成列で試します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-14" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-14" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> t_order_detail (</span><br><span class="line">    order_id <span class="type">BIGINT</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    item_id <span class="type">BIGINT</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    quantity <span class="type">INT</span>,</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 受注番号+商品IDの組み合わせでPKを作成してみる</span></span><br><span class="line">    order_item_key TEXT GENERATED ALWAYS <span class="keyword">AS</span> (order_id::TEXT <span class="operator">||</span> <span class="string">&#x27;-&#x27;</span> <span class="operator">||</span> item_id::TEXT) STORED,</span><br><span class="line"></span><br><span class="line">    <span class="keyword">PRIMARY KEY</span> (order_item_key)</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- データ登録</span></span><br><span class="line"><span class="keyword">INSERT INTO</span> t_order_detail (order_id, item_id, quantity) <span class="keyword">VALUES</span></span><br><span class="line">(<span class="number">1001</span>, <span class="number">201</span>, <span class="number">2</span>), (<span class="number">1001</span>, <span class="number">205</span>, <span class="number">1</span>), (<span class="number">1002</span>, <span class="number">201</span>, <span class="number">5</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 検索</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> t_order_detail;</span><br><span class="line"> order_id <span class="operator">|</span> item_id <span class="operator">|</span> quantity <span class="operator">|</span> order_item_key</span><br><span class="line"><span class="comment">----------+---------+----------+----------------</span></span><br><span class="line">     <span class="number">1001</span> <span class="operator">|</span>     <span class="number">201</span> <span class="operator">|</span>        <span class="number">2</span> <span class="operator">|</span> <span class="number">1001</span><span class="number">-201</span></span><br><span class="line">     <span class="number">1001</span> <span class="operator">|</span>     <span class="number">205</span> <span class="operator">|</span>        <span class="number">1</span> <span class="operator">|</span> <span class="number">1001</span><span class="number">-205</span></span><br><span class="line">     <span class="number">1002</span> <span class="operator">|</span>     <span class="number">201</span> <span class="operator">|</span>        <span class="number">5</span> <span class="operator">|</span> <span class="number">1002</span><span class="number">-201</span></span><br><span class="line">(<span class="number">3</span> <span class="keyword">rows</span>)</span><br></pre></td></tr></table></figure></div>

<p>普通にPKとして扱えました。</p>
<p>続いて、仮想生成列で試します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-15" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-15" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> t_order_detail (</span><br><span class="line">    order_id <span class="type">BIGINT</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    item_id <span class="type">BIGINT</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    quantity <span class="type">INT</span>,</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 受注番号+商品IDの組み合わせでPKを作成してみる</span></span><br><span class="line">    order_item_key TEXT GENERATED ALWAYS <span class="keyword">AS</span> (order_id::TEXT <span class="operator">||</span> <span class="string">&#x27;-&#x27;</span> <span class="operator">||</span> item_id::TEXT) VIRTUAL,</span><br><span class="line"></span><br><span class="line">    <span class="keyword">PRIMARY KEY</span> (order_item_key)</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line">ERROR:  <span class="keyword">primary</span> keys <span class="keyword">on</span> virtual generated columns <span class="keyword">are</span> <span class="keyword">not</span> supported</span><br></pre></td></tr></table></figure></div>

<p>これはエラーになりました。仮想生成列のPKはサポートされていないようです。仮想生成列はインデックスを使えないため、PKにできないのでしょう。これについては先程の式インデックスで代替できません。ただし、式インデックスは、一意制約とNOT NULL制約を付与できるので、類似の機能を持たせることはできます。</p>
<p>注意として式インデックスでは、 <code>:::txt</code> による型変換が使えず <code>CAST()</code> で変換するなど微妙にクセがあることです。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-16" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-16" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">CREATE</span> <span class="keyword">UNIQUE</span> INDEX idx_01_t_order_detail</span><br><span class="line"><span class="keyword">ON</span> t_order_detail ( (<span class="built_in">CAST</span>(order_id <span class="keyword">AS</span> TEXT) <span class="operator">||</span> <span class="string">&#x27;-&#x27;</span> <span class="operator">||</span> <span class="built_in">CAST</span>(item_id <span class="keyword">AS</span> TEXT)) );</span><br><span class="line"><span class="keyword">CREATE</span> INDEX</span><br></pre></td></tr></table></figure></div>

<p>一意性のチェックです。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-17" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-17" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">INSERT INTO</span> t_order_detail (order_id, item_id, quantity) <span class="keyword">VALUES</span> (<span class="number">1003</span>, <span class="number">202</span>, <span class="number">3</span>);</span><br><span class="line"><span class="keyword">INSERT</span> <span class="number">0</span> <span class="number">1</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">INSERT INTO</span> t_order_detail (order_id, item_id, quantity) <span class="keyword">VALUES</span> (<span class="number">1003</span>, <span class="number">202</span>, <span class="number">3</span>);</span><br><span class="line">ERROR:  duplicate key <span class="keyword">value</span> violates <span class="keyword">unique</span> <span class="keyword">constraint</span> &quot;idx_01_t_order_detail&quot;</span><br><span class="line">DETAIL:  Key (((order_id::text <span class="operator">||</span> <span class="string">&#x27;-&#x27;</span>::text) <span class="operator">||</span> item_id::text))<span class="operator">=</span>(<span class="number">1003</span><span class="number">-202</span>) already exists.</span><br></pre></td></tr></table></figure></div>

<p>無事動作しました。ただし、あくまで仮想列自体に一意制約＋NOT NULL制約をつけたわけではなく、仮想列と同等の定義を持った式インデックスに、一意制約＋NOT NULL制約をつけたことになります。そのため、外部キー制約の参照の対象にはできないでしょう。</p>
<h3 id="10-生成列はパーティションキーに使えるか">10. 生成列はパーティションキーに使えるか</h3><p>受注日時から受注日付（yyyy-MM-dd）を生成列で作成し、それをパーティションキーとするようなケースで試します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-18" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-18" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">CREATE TABLE</span> t_order (</span><br><span class="line">    order_id <span class="type">BIGINT</span> GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span>,</span><br><span class="line">    item_name TEXT,</span><br><span class="line">    order_at TIMESTAMPTZ <span class="keyword">NOT NULL</span>,</span><br><span class="line">    order_date <span class="type">DATE</span> GENERATED ALWAYS <span class="keyword">AS</span> ((timezone(<span class="string">&#x27;JST&#x27;</span>, order_at)::<span class="type">date</span>)) STORED,</span><br><span class="line">    <span class="keyword">PRIMARY KEY</span> (order_id, order_date)</span><br><span class="line">) <span class="keyword">PARTITION</span> <span class="keyword">BY</span> <span class="keyword">RANGE</span> (order_date);</span><br><span class="line">ERROR:  cannot use generated <span class="keyword">column</span> <span class="keyword">in</span> <span class="keyword">partition</span> key</span><br><span class="line">LINE <span class="number">7</span>: ) <span class="keyword">PARTITION</span> <span class="keyword">BY</span> <span class="keyword">RANGE</span> (order_date);</span><br><span class="line">                              <span class="operator">^</span></span><br><span class="line">DETAIL:  <span class="keyword">Column</span> &quot;order_date&quot; <span class="keyword">is</span> a generated column.</span><br></pre></td></tr></table></figure></div>

<p>生成列はパーティションキーに使えないようです。この用途ですと、生成列を実カラムに戻し、アプリケーション側で明示的に設定する方が良いように思えます（アプリケーションの代わりにトリガーを使用しても良いですが、さすがにテクニカル過ぎるでしょう）。仮想生成列でも同じ結果になります。</p>
<p>もちろんドキュメントにも、<code>A generated column cannot be part of a partition key.</code>（生成列はパーティションキーには利用できません。）と書かれています。</p>
<h3 id="11-テーブル作成後に後から生成列を追加できるか">11. テーブル作成後に後から生成列を追加できるか</h3><p><code>m_user</code> に格納生成列、仮想生成列の順番で足してみます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-19" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-19" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">CREATE TABLE</span> m_user (</span><br><span class="line">    user_id <span class="type">BIGINT</span> GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    first_name TEXT,</span><br><span class="line">    last_name TEXT,</span><br><span class="line">    email TEXT</span><br><span class="line">);</span><br><span class="line"><span class="keyword">CREATE TABLE</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 格納生成列の追加</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">ALTER TABLE</span> m_user</span><br><span class="line"><span class="keyword">ADD</span> <span class="keyword">COLUMN</span> email_lower TEXT GENERATED ALWAYS <span class="keyword">AS</span> (<span class="built_in">LOWER</span>(email)) STORED;</span><br><span class="line"><span class="keyword">ALTER TABLE</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 仮想生成列の追加</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">ALTER TABLE</span> m_user</span><br><span class="line"><span class="keyword">ADD</span> <span class="keyword">COLUMN</span> full_name TEXT GENERATED ALWAYS <span class="keyword">AS</span> (last_name <span class="operator">||</span> <span class="string">&#x27; &#x27;</span> <span class="operator">||</span> first_name) VIRTUAL;</span><br><span class="line"><span class="keyword">ALTER TABLE</span></span><br></pre></td></tr></table></figure></div>

<p>結果は成功でした。ちなみに、ALTER文実行前にはBEGINEを実行し、別プロセスで<code>pg_locks</code> を確認したところ、どちらも <code>AccessExclusiveLock</code> を取っていました。格納生成列は既存行が多ければ長時間、参照もできないので注意が必要です。仮想生成列はメタデータの書き換えのみで済むため、<code>AccessExclusiveLock</code> を取りますが一瞬で終わります。</p>
<h3 id="12-NOT-NULL制約を付けた生成列を後から追加すると処理時間はどうなるか">12. NOT NULL制約を付けた生成列を後から追加すると処理時間はどうなるか</h3><p>以下の <code>t_order</code> に1万件のダミーデータを登録し、[格納|生成] x[NOT NULL有無]の4パターンで処理時間を計測しました。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-20" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-20" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- 初期テーブル</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> t_order (</span><br><span class="line">    item_id <span class="type">BIGINT</span> GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    unit_price <span class="type">NUMERIC</span>,</span><br><span class="line">    quantity <span class="type">INT</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ダミーデータ登録</span></span><br><span class="line"><span class="keyword">INSERT INTO</span> t_order (unit_price, quantity)</span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">    (random() <span class="operator">*</span> <span class="number">9999</span> <span class="operator">+</span> <span class="number">1</span>)::<span class="type">numeric</span>(<span class="number">10</span>, <span class="number">2</span>),  <span class="comment">-- 1.00 ～ 10000.99 のランダムな単価</span></span><br><span class="line">    (random() <span class="operator">*</span> <span class="number">99</span> <span class="operator">+</span> <span class="number">1</span>)::<span class="type">int</span>                 <span class="comment">-- 1 ～ 100 のランダムな数量</span></span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">    generate_series(<span class="number">1</span>, <span class="number">10000000</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 統計情報の最新化</span></span><br><span class="line">ANALYZE t_order;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 確認</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="built_in">count</span>(<span class="operator">*</span>) <span class="keyword">FROM</span> t_order;</span><br><span class="line">  count</span><br><span class="line"><span class="comment">----------</span></span><br><span class="line"> <span class="number">10000000</span></span><br><span class="line">(<span class="number">1</span> <span class="type">row</span>)</span><br></pre></td></tr></table></figure></div>

<p>検証は以下のフローです。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-21" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-21" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">SET</span> autovacuum <span class="operator">=</span> OFF;</span><br><span class="line">\timing <span class="keyword">on</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 格納生成列（NULL許容）</span></span><br><span class="line"><span class="keyword">ALTER TABLE</span> t_order <span class="keyword">ADD</span> <span class="keyword">COLUMN</span> total_price <span class="type">NUMERIC</span> GENERATED ALWAYS <span class="keyword">AS</span> (unit_price <span class="operator">*</span> quantity) STORED;</span><br><span class="line"><span class="keyword">ALTER TABLE</span> t_order <span class="keyword">DROP</span> <span class="keyword">COLUMN</span> total_price;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 格納生成列（NOT NULL）</span></span><br><span class="line"><span class="keyword">ALTER TABLE</span> t_order <span class="keyword">ADD</span> <span class="keyword">COLUMN</span> total_price <span class="type">NUMERIC</span> GENERATED ALWAYS <span class="keyword">AS</span> (unit_price <span class="operator">*</span> quantity) STORED <span class="keyword">NOT NULL</span>;</span><br><span class="line"><span class="keyword">ALTER TABLE</span> t_order <span class="keyword">DROP</span> <span class="keyword">COLUMN</span> total_price;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 仮想生成列(NULL許容)</span></span><br><span class="line"><span class="keyword">ALTER TABLE</span> t_order <span class="keyword">ADD</span> <span class="keyword">COLUMN</span> total_price <span class="type">NUMERIC</span> GENERATED ALWAYS <span class="keyword">AS</span> (unit_price <span class="operator">*</span> quantity) VIRTUAL;</span><br><span class="line"><span class="keyword">ALTER TABLE</span> t_order <span class="keyword">DROP</span> <span class="keyword">COLUMN</span> total_price;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 仮想生成列（NOT NULL）</span></span><br><span class="line"><span class="keyword">ALTER TABLE</span> t_order <span class="keyword">ADD</span> <span class="keyword">COLUMN</span> total_price <span class="type">NUMERIC</span> GENERATED ALWAYS <span class="keyword">AS</span> (unit_price <span class="operator">*</span> quantity) VIRTUAL <span class="keyword">NOT NULL</span>;</span><br><span class="line"><span class="keyword">ALTER TABLE</span> t_order <span class="keyword">DROP</span> <span class="keyword">COLUMN</span> total_price;</span><br></pre></td></tr></table></figure></div>

<div class="scroll"><table>
<thead>
<tr>
<th align="left">検証パターン</th>
<th align="left">処理結果</th>
</tr>
</thead>
<tbody><tr>
<td align="left">格納生成列（NULL許容）</td>
<td align="left">14.4秒</td>
</tr>
<tr>
<td align="left">格納生成列（NOT NULL）</td>
<td align="left">30.3秒</td>
</tr>
<tr>
<td align="left">仮想生成列 (NULL許容)</td>
<td align="left">0.006秒</td>
</tr>
<tr>
<td align="left">仮想生成列（NOT NULL）</td>
<td align="left">10.2秒</td>
</tr>
</tbody></table></div>
<p>格納生成列もNOT NULL化すると少し処理時間が増します。理由を深く調査はしていませんが、NOT NULL計算分が上乗せになるからでしょう。そして、仮想生成列ですが、NOT NULL制約を追加すると大幅に時間がかかります。これはおそらくテーブルフルスキャンでNOT NULLにならないかチェックするからでしょう。</p>
<p><strong>（2025.11.7追記）</strong></p>
<p>ちなみに、元テーブルに生成列の計算元列にNOT NULL制約を付けると、フルスキャンが論理的にはスキップできるのでは？という声をもらいましたので、検証しました。</p>
<p>テーブル定義だけ以下で、残りは同じです。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-22" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-22" title="コードの折り返しを切り替える"></label><figcaption><span>テーブル定義</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> t_order (</span><br><span class="line">    item_id <span class="type">BIGINT</span> GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    unit_price <span class="type">NUMERIC</span> <span class="keyword">NOT NULL</span>, <span class="comment">-- NOT NULL制約を追加</span></span><br><span class="line">    quantity <span class="type">INT</span> <span class="keyword">NOT NULL</span>        <span class="comment">-- NOT NULL制約を追加</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure></div>

<div class="scroll"><table>
<thead>
<tr>
<th align="left">検証パターン</th>
<th align="left">処理結果</th>
</tr>
</thead>
<tbody><tr>
<td align="left">1.格納生成列（NULL許容）</td>
<td align="left">9.0秒</td>
</tr>
<tr>
<td align="left">2.格納生成列（NOT NULL）</td>
<td align="left">25.5秒</td>
</tr>
<tr>
<td align="left">3.仮想生成列 (NULL許容)</td>
<td align="left">0.014秒</td>
</tr>
<tr>
<td align="left">4.仮想生成列（NOT NULL）</td>
<td align="left">4.9秒</td>
</tr>
</tbody></table></div>
<p>元列のNOT NULL制約無し版に比べ、少し早くなっていますが、実行の度に処理時間は変動するため気にしないでください。</p>
<p>重要なのは、NOT NULL制約をつけても、4の結果は4.9秒かかっている（≒フルスキャンが発生していると推測できる）ことです。現状のPostgreSQLでは、元列にNOT NULL制約がついていていたとしても、生成列の「式」を確認して、この結果だとNOT NULLになりえないから、チェックは不要であると言った判定は行っていないと言えます。</p>
<h3 id="13-生成列の定義変更はできるか">13. 生成列の定義変更はできるか</h3><p>ドキュメントを読む限り、生成列の定義を直接変更できないように思えます（文法の読み取りが間違っていたらご指摘ください）。</p>
<p>そのため、一度そのカラムを削除してから作り直すことになると思われます。例えば、先程の <code>email_lower</code> をいう検索専用の生成列を、さらに前後の空白をトリムする処理を追加します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-23" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-23" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- (1) 既存の格納列を削除</span></span><br><span class="line"><span class="keyword">ALTER TABLE</span> m_user <span class="keyword">DROP</span> <span class="keyword">COLUMN</span> email_lower;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- (2) 新しい定義で格納列を再度追加</span></span><br><span class="line"><span class="keyword">ALTER TABLE</span> m_user</span><br><span class="line"><span class="keyword">ADD</span> <span class="keyword">COLUMN</span> email_lower TEXT GENERATED ALWAYS <span class="keyword">AS</span> (<span class="built_in">LOWER</span>(<span class="built_in">TRIM</span>(email))) STORED;</span><br></pre></td></tr></table></figure></div>

<p>流れ自体は仮想生成列でも同様です。格納生成列の場合は、(2)の処理でテーブルサイズによってはかなり時間がかかると思うので、注意が必要そうです（格納生成列のまま、瞬時に切り替える手順は今のところ、テーブル単位で新旧Verを作ってリネームする方法しか思いつきませんでした。また、格納生成列をDROP &amp; ADDするということは、統計情報も消えるということなので、インデックス項目の場合はANALYZEもしたほうが良いでしょう）。</p>
<h3 id="14-利用しているカラムをRENAME-COLUMNしたらどうなるか">14. 利用しているカラムをRENAME COLUMNしたらどうなるか</h3><p>生成列で利用しているカラムをリネームはできるのでしょうか？試してみます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-24" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-24" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> m_user (</span><br><span class="line">    user_id <span class="type">BIGINT</span> GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    first_name TEXT,</span><br><span class="line">    last_name TEXT,</span><br><span class="line">    email TEXT,</span><br><span class="line">    email_lower TEXT GENERATED ALWAYS <span class="keyword">AS</span> (<span class="built_in">LOWER</span>(email)) STORED,</span><br><span class="line">    full_name TEXT GENERATED ALWAYS <span class="keyword">AS</span> (last_name <span class="operator">||</span> <span class="string">&#x27; &#x27;</span> <span class="operator">||</span> first_name) VIRTUAL</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 生成列で利用されているカラムをリネームする</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">ALTER TABLE</span> m_user RENAME <span class="keyword">COLUMN</span> first_name <span class="keyword">TO</span> given_name;</span><br><span class="line"><span class="keyword">ALTER TABLE</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">ALTER TABLE</span> m_user RENAME <span class="keyword">COLUMN</span> email <span class="keyword">TO</span> email_address;</span><br><span class="line"><span class="keyword">ALTER TABLE</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 検索</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> m_user;</span><br><span class="line"> user_id <span class="operator">|</span> given_name <span class="operator">|</span> last_name <span class="operator">|</span>    email_address     <span class="operator">|</span>     email_lower      <span class="operator">|</span>   full_name</span><br><span class="line"><span class="comment">---------+------------+-----------+----------------------+----------------------+---------------</span></span><br><span class="line">       <span class="number">1</span> <span class="operator">|</span> Hanako     <span class="operator">|</span> Suzuki    <span class="operator">|</span> h.suzuki<span class="variable">@example</span>.com <span class="operator">|</span> h.suzuki<span class="variable">@example</span>.com <span class="operator">|</span> Suzuki Hanako</span><br><span class="line">(<span class="number">1</span> <span class="type">row</span>)</span><br></pre></td></tr></table></figure></div>

<p>格納・仮想のどちらの生成列で利用しているカラム名を変更が成功し、挙動も問題なかったです。</p>
<p><code>\d m_user</code> でテーブル定義を確認すると、生成列定義の列名も書き換わっていました。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-25" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-25" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># \d m_user</span><br><span class="line">                                            <span class="keyword">Table</span> &quot;public.m_user&quot;</span><br><span class="line">    <span class="keyword">Column</span>     <span class="operator">|</span>  Type  <span class="operator">|</span> <span class="keyword">Collation</span> <span class="operator">|</span> Nullable <span class="operator">|</span>                           <span class="keyword">Default</span></span><br><span class="line"><span class="comment">---------------+--------+-----------+----------+--------------------------------------------------------------</span></span><br><span class="line"> user_id       <span class="operator">|</span> <span class="type">bigint</span> <span class="operator">|</span>           <span class="operator">|</span> <span class="keyword">not null</span> <span class="operator">|</span> generated <span class="keyword">by</span> <span class="keyword">default</span> <span class="keyword">as</span> <span class="keyword">identity</span></span><br><span class="line"> given_name    <span class="operator">|</span> text   <span class="operator">|</span>           <span class="operator">|</span>          <span class="operator">|</span></span><br><span class="line"> last_name     <span class="operator">|</span> text   <span class="operator">|</span>           <span class="operator">|</span>          <span class="operator">|</span></span><br><span class="line"> email_address <span class="operator">|</span> text   <span class="operator">|</span>           <span class="operator">|</span>          <span class="operator">|</span></span><br><span class="line"> email_lower   <span class="operator">|</span> text   <span class="operator">|</span>           <span class="operator">|</span>          <span class="operator">|</span> generated always <span class="keyword">as</span> (<span class="built_in">lower</span>(email_address)) stored</span><br><span class="line"> full_name     <span class="operator">|</span> text   <span class="operator">|</span>           <span class="operator">|</span>          <span class="operator">|</span> generated always <span class="keyword">as</span> ((last_name <span class="operator">||</span> <span class="string">&#x27; &#x27;</span>::text) <span class="operator">||</span> given_name)</span><br><span class="line">Indexes:</span><br><span class="line">    &quot;m_user_pkey&quot; <span class="keyword">PRIMARY KEY</span>, btree (user_id)</span><br></pre></td></tr></table></figure></div>

<p>リネームにも追随してくれるの、気が効いていますね。賢い。</p>
<h3 id="15-利用しているカラムをDROP-COLUMNしたときどうなるか">15. 利用しているカラムをDROP COLUMNしたときどうなるか</h3><p>格納生成列、仮想生成列それぞれで利用しているカラムを、DROPできるか試しました。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-26" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-26" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> m_user (</span><br><span class="line">    user_id <span class="type">BIGINT</span> GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    first_name TEXT,</span><br><span class="line">    last_name TEXT,</span><br><span class="line">    email TEXT,</span><br><span class="line">    email_lower TEXT GENERATED ALWAYS <span class="keyword">AS</span> (<span class="built_in">LOWER</span>(email)) STORED,</span><br><span class="line">    full_name TEXT GENERATED ALWAYS <span class="keyword">AS</span> (last_name <span class="operator">||</span> <span class="string">&#x27; &#x27;</span> <span class="operator">||</span> first_name) VIRTUAL</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 格納生成列で利用されているカラムを削除</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">ALTER TABLE</span> m_user <span class="keyword">DROP</span> <span class="keyword">COLUMN</span> email;</span><br><span class="line">ERROR:  cannot <span class="keyword">drop</span> <span class="keyword">column</span> email <span class="keyword">of</span> <span class="keyword">table</span> m_user because other objects depend <span class="keyword">on</span> it</span><br><span class="line">DETAIL:  <span class="keyword">column</span> email_lower <span class="keyword">of</span> <span class="keyword">table</span> m_user depends <span class="keyword">on</span> <span class="keyword">column</span> email <span class="keyword">of</span> <span class="keyword">table</span> m_user</span><br><span class="line">HINT:  Use <span class="keyword">DROP</span> ... CASCADE <span class="keyword">to</span> <span class="keyword">drop</span> the dependent objects too.</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 仮想生成列で利用されているカラムを削除</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">ALTER TABLE</span> m_user <span class="keyword">DROP</span> <span class="keyword">COLUMN</span> first_name;</span><br><span class="line">ERROR:  cannot <span class="keyword">drop</span> <span class="keyword">column</span> first_name <span class="keyword">of</span> <span class="keyword">table</span> m_user because other objects depend <span class="keyword">on</span> it</span><br><span class="line">DETAIL:  <span class="keyword">column</span> full_name <span class="keyword">of</span> <span class="keyword">table</span> m_user depends <span class="keyword">on</span> <span class="keyword">column</span> first_name <span class="keyword">of</span> <span class="keyword">table</span> m_user</span><br><span class="line">HINT:  Use <span class="keyword">DROP</span> ... CASCADE <span class="keyword">to</span> <span class="keyword">drop</span> the dependent objects too.</span><br></pre></td></tr></table></figure></div>

<p>どちらもエラーかつ、ヒントで依存しているオブジェクト（列）もCASCADEオプションで消せるとありますね。CASCADEつけて実行してみます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1m85fi0-27" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1m85fi0-27" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- 格納生成列で利用されているカラムを削除（CASCADE追加）</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">ALTER TABLE</span> m_user <span class="keyword">DROP</span> <span class="keyword">COLUMN</span> email CASCADE;</span><br><span class="line">NOTICE:  <span class="keyword">drop</span> cascades <span class="keyword">to</span> <span class="keyword">column</span> email_lower <span class="keyword">of</span> <span class="keyword">table</span> m_user</span><br><span class="line"><span class="keyword">ALTER TABLE</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 仮想生成列で利用されているカラムを削除（CASCADE追加）</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">ALTER TABLE</span> m_user <span class="keyword">DROP</span> <span class="keyword">COLUMN</span> first_name CASCADE;</span><br><span class="line">NOTICE:  <span class="keyword">drop</span> cascades <span class="keyword">to</span> <span class="keyword">column</span> full_name <span class="keyword">of</span> <span class="keyword">table</span> m_user</span><br><span class="line"><span class="keyword">ALTER TABLE</span></span><br><span class="line"></span><br><span class="line">postgres<span class="operator">=</span># \d m_user;</span><br><span class="line">                            <span class="keyword">Table</span> &quot;public.m_user&quot;</span><br><span class="line">  <span class="keyword">Column</span>   <span class="operator">|</span>  Type  <span class="operator">|</span> <span class="keyword">Collation</span> <span class="operator">|</span> Nullable <span class="operator">|</span>             <span class="keyword">Default</span></span><br><span class="line"><span class="comment">-----------+--------+-----------+----------+----------------------------------</span></span><br><span class="line"> user_id   <span class="operator">|</span> <span class="type">bigint</span> <span class="operator">|</span>           <span class="operator">|</span> <span class="keyword">not null</span> <span class="operator">|</span> generated <span class="keyword">by</span> <span class="keyword">default</span> <span class="keyword">as</span> <span class="keyword">identity</span></span><br><span class="line"> last_name <span class="operator">|</span> text   <span class="operator">|</span>           <span class="operator">|</span>          <span class="operator">|</span></span><br><span class="line">Indexes:</span><br><span class="line">    &quot;m_user_pkey&quot; <span class="keyword">PRIMARY KEY</span>, btree (user_id)</span><br></pre></td></tr></table></figure></div>

<p><code>CASCADE</code> を利用したら、利用していた元のカラムも同時に削除されました。強力ですね…。事故不可避なので存在自体を忘れたほうが良いでしょう。</p>
<h2 id="格納生成列と仮想生成列の使い分け">格納生成列と仮想生成列の使い分け</h2><p>格納生成列ですが、最初に紹介したPostgreSQL設計ガイドラインにある通り、業務要件の変更でロジックを変更したい場合の、テーブルマイグレーション（デプロイ作業）が大変過ぎるため避けるべきは変わりませんでした。</p>
<p>仮想生成列は、その手の苦労は今回動かした範囲内ではあまり感じませんでした（実データの変更は伴わず、メタデータの変更のみだからです）。一方で、NOT NULL制約を付けたときの挙動には注意で、定義変更時は既存の全行をフルスキャンするような動きになっていると思われます。システムメンテナンスタイムを確保できるシステムであっても、それなりのデータ量になりえる場合は、選択しにくいでしょう（パーティションテーブルごとに定義変更できるなどの手順が確立できればまだ考えようがありますが..）。</p>
<p>総合すると、NOT NULLを絶対に付けないかつ、アプリケーション側でロジックが散らばるのであれば、いっそDB定義側で仮想生成列を用いて、統制を図るのも一手ではないかと感じました。一方で、将来的にNOT NULL制約をつける変更もありえなくない場合は、防御的な考えから異現時点では採用すべきでない気がします。式インデックスなどハマりどころもあるので、大規模だと利用は現時点では禁止にしたい。</p>
<p>みなさんの意見もいただけると嬉しいです。</p>
<h2 id="まとめ">まとめ</h2><p>格納生成列、仮想生成列の両方について触ってみました。私の見解としては以下の意見です。</p>
<ul>
<li>格納生成列は使わない</li>
<li>仮想生成列は、NOT NULL制約を付けたときの挙動は気になるけど、NOT NULL制約を絶対に付けないのであれば害は少ないので、統制が取れるなら利用してもよいのでは。大規模ではハマりどころも多いのでテックリード的な視点では、現時点では禁止にしておきたい</li>
<li>どちらも、パーティションキーに使えないなど制約があるので、ドキュメントを一読することを推奨します</li>
</ul>
]]></content>
    <summary type="html">仮想生成列についてまとめます。PostgreSQLで従来から利用できた格納生成列や、生成列自体の説明から合わせて紹介します。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="PostgreSQL" scheme="https://future-architect.github.io/tags/PostgreSQL/"/>
    <category term="PostgreSQL18" scheme="https://future-architect.github.io/tags/PostgreSQL18/"/>
  </entry>
  <entry>
    <title>PostgreSQL 18の新機能「B-treeインデックスのスキップスキャン」</title>
    <link href="https://future-architect.github.io/articles/20251014a/"/>
    <id>https://future-architect.github.io/articles/20251014a/</id>
    <published>2025-10-13T15:00:00.000Z</published>
    <updated>2025-10-13T15:00:00.000Z</updated>
    <author><name>村田靖拓</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20251014a/top.jpg" alt="" width="800" height="664">

<p>PostgreSQL18連載の5本目の記事です。</p>
<p>PostgreSQL 18がリリースされました。リリースされた機能のうち私は「B-treeインデックスのスキップスキャン」機能が気になったので、機能の特徴を深堀りしつつ、実際の挙動を確認してみます。</p>
<h2 id="B-treeインデックスのスキップスキャンとは">B-treeインデックスのスキップスキャンとは</h2><p>複合インデックス（複数の列で構成されるインデックス）の利用効率を劇的に向上させる新しいスキャン方法です。</p>
<h3 id="従来の課題">従来の課題</h3><p>PostgreSQLでは、例えば<code>(列A, 列B)</code>という順番で複合インデックスを作成した場合、これまではWHERE句に先頭の「列A」の条件がないと、インデックスを効率的に使えませんでした。</p>
<p>例えば、<code>WHERE 列B = &#39;hoge&#39;</code>というクエリでは、せっかくの <code>(列A, 列B)</code> インデックスをうまく使えず、結果としてテーブル全体をスキャン（シーケンシャルスキャン）してしまう、あるいは、インデックスを使えたとしても「列A」の条件を指定していない分だけパフォーマンスが低下する原因となっていました。</p>
<p>これにより「列B」だけのインデックスを別途作成するケースもあり、ストレージの無駄やデータ更新時のコスト増につながっていました。</p>
<h3 id="新機能による効果">新機能による効果</h3><p>スキップスキャン機能では、インデックスの先頭列（列A）がWHERE句になくてもPostgreSQLがインデックスの内部を「スキップ」しながら、2番目以降の列（列B）の条件に合うデータをより効率的なアルゴリズムで探し出してくれます。</p>
<h3 id="機能の仕組み">機能の仕組み</h3><ol>
<li>まず、インデックスの先頭列（列A）にどのような値の種類があるかを把握</li>
<li>次に、列Aの各値の「先頭」にジャンプ</li>
<li>そこから、2番目の列（列B）が条件に合致するかどうかをチェック</li>
</ol>
<p>これを列Aの値の種類ぶんだけ繰り返すことで、インデックス全体を舐めるよりもはるかに効率的にデータを見つけ出すことができます。</p>
<p>公式ドキュメントでは、上記2,3における挙動が詳細に説明されています。</p>
<blockquote>
<p>スキップスキャンは、インデックス列のすべての可能な値に一致する動的な等価制約を内部的に生成することによって機能します</p>
</blockquote>
<p>つまり上記の例であれば、列Bに対してのみ条件が指定されている場合でも列Aに対する条件を内部的に生成して動作することを意味します。</p>
<blockquote>
<p>例えば、(x, y)に対するインデックスがあり、クエリ条件がWHERE y &#x3D; 7700である場合、B-treeインデックススキャンはスキップスキャン最適化を適用できる可能性があります。これは一般的に、クエリプランナが、テーブルで利用可能なインデックスを考慮した上で、Nのすべての可能な値（またはインデックスに実際に格納されているすべてのxの値）に対してWHERE x &#x3D; N AND y &#x3D; 7700という検索を繰り返すことが最も高速なアプローチであると予測する場合に発生します。</p>
</blockquote>
<p>この仕組みは、インデックスの先頭列の値の種類が少ない（カーディナリティが低い）場合に特に高い効果を発揮します。 例えば、「性別」「注文ステータス」「都道府県」のように、値のバリエーションが限られている列が先頭にある複合インデックスで非常に有効だと言えます。</p>
<p>https://www.postgresql.org/docs/current/indexes-multicolumn.html</p>
<h2 id="実際に検証してみる">実際に検証してみる</h2><p>さて、PostgreSQL 17と18を比較してどのようにクエリ応答性能が変化しているか比べてみます。</p>
<h3 id="検証環境">検証環境</h3><ul>
<li>Windows11 Home</li>
<li>WSL2(ubuntu)</li>
</ul>
<h3 id="PostgreSQL-18-の場合">PostgreSQL 18 の場合</h3><h4 id="下準備">下準備</h4><h5 id="データベースの準備">データベースの準備</h5><p>まずはPostgreSQL18が動く環境を準備します。（今回はDockerを利用）</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1f3kxly-1" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1f3kxly-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">docker run --name pg18-handson -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgres:18</span><br></pre></td></tr></table></figure></div>

<p>コンテナの起動を確認。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1f3kxly-2" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1f3kxly-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker ps</span></span><br><span class="line">CONTAINER ID   IMAGE                        COMMAND                  CREATED         STATUS                     PORTS                                       NAMES</span><br><span class="line">0806a36cb87c   postgres:18                  &quot;docker-entrypoint.s…&quot;   8 seconds ago   Up 7 seconds               0.0.0.0:5432-&gt;5432/tcp, :::5432-&gt;5432/tcp   pg18-handson</span><br></pre></td></tr></table></figure></div>

<p><code>psql</code>コマンドで入ってみると、、しっかりと動いてることが確認できたので次に進みます。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker <span class="built_in">exec</span> -it pg18-handson psql -U postgres</span></span><br><span class="line">psql (18.0 (Debian 18.0-1.pgdg13+3))</span><br><span class="line">Type &quot;help&quot; for help.</span><br><span class="line"></span><br><span class="line">postgres=#</span><br></pre></td></tr></table></figure>

<h5 id="テーブルの作成と検証用データの投入">テーブルの作成と検証用データの投入</h5><p><code>orders</code>テーブルを作成し、100万件のデータを投入します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1f3kxly-3" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1f3kxly-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> orders (</span><br><span class="line">  order_id       SERIAL <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">  order_status   TEXT <span class="keyword">NOT NULL</span>, <span class="comment">-- &#x27;pending&#x27;, &#x27;processing&#x27;, &#x27;shipped&#x27;, &#x27;delivered&#x27;, &#x27;cancelled&#x27; の5種類</span></span><br><span class="line">  customer_id    <span class="type">INTEGER</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">  order_date     TIMESTAMPTZ <span class="keyword">NOT NULL</span>,</span><br><span class="line">  order_details  TEXT</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- サンプルデータを100万件投入</span></span><br><span class="line"><span class="keyword">INSERT INTO</span> orders (order_status, customer_id, order_date, order_details)</span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">  <span class="comment">-- 5種類のステータスをランダムに割り当て</span></span><br><span class="line">  (<span class="keyword">ARRAY</span>[<span class="string">&#x27;pending&#x27;</span>, <span class="string">&#x27;processing&#x27;</span>, <span class="string">&#x27;shipped&#x27;</span>, <span class="string">&#x27;delivered&#x27;</span>, <span class="string">&#x27;cancelled&#x27;</span>])[<span class="built_in">floor</span>(random() <span class="operator">*</span> <span class="number">5</span>) <span class="operator">+</span> <span class="number">1</span>],</span><br><span class="line">  <span class="comment">-- 1万人の顧客IDをランダムに割り当て</span></span><br><span class="line">  <span class="built_in">floor</span>(random() <span class="operator">*</span> <span class="number">10000</span>) <span class="operator">+</span> <span class="number">1</span>,</span><br><span class="line">  <span class="comment">-- 過去1年間のランダムな日時</span></span><br><span class="line">  NOW() <span class="operator">-</span> (random() <span class="operator">*</span> <span class="number">365</span>) <span class="operator">*</span> <span class="string">&#x27;1 day&#x27;</span>::<span class="type">interval</span>,</span><br><span class="line">  <span class="string">&#x27;details...&#x27;</span></span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">  generate_series(<span class="number">1</span>, <span class="number">1000000</span>);</span><br></pre></td></tr></table></figure></div>

<h5 id="複合インデックスの作成">複合インデックスの作成</h5><p>スキップスキャンの効果を検証するため、カーディナリティの低い <code>order_status</code> を先頭にした複合インデックスを作成します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1f3kxly-4" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1f3kxly-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> INDEX idx_orders_status_customer <span class="keyword">ON</span> orders (order_status, customer_id);</span><br></pre></td></tr></table></figure></div>

<h5 id="統計情報の更新">統計情報の更新</h5><p>クエリオプティマイザが正しい判断を下せるように、テーブルの統計情報を最新の状態にします。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">ANALYZE orders;</span><br></pre></td></tr></table></figure>

<h4 id="スキップスキャン機能の検証">スキップスキャン機能の検証</h4><p>では、ここから実際に機能を検証してみます。</p>
<p>まずは複合インデックスの2番目の列である<code>customer_id</code>のみをwhere句に指定して検索してみます。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1f3kxly-5" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1f3kxly-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres=*# EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123;</span><br><span class="line">                                                                 QUERY PLAN</span><br><span class="line">--------------------------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Index Scan using idx_orders_status_customer on orders  (cost=0.42..418.60 rows=100 width=36) (actual <span class="keyword">time</span>=0.052..0.250 rows=93.00 loops=1)</span><br><span class="line">   Index Cond: (customer_id = 123)</span><br><span class="line">   Index Searches: 11</span><br><span class="line">   Buffers: shared hit=126</span><br><span class="line"> Planning Time: 0.062 ms</span><br><span class="line"> Execution Time: 0.276 ms</span><br></pre></td></tr></table></figure></div>

<p>Index Scanが行われており、 <code>0.276ms</code>で応答しました。高速ですね。ただし、実行計画にはスキップスキャンを示す表記が登場しないため、厳密にはスキップスキャンを行ったか否かを判断できないのが悩ましいところです。今回のクエリは複合インデックスの2列目に対してのみ等価条件を指定しており、1列目データ群はカーディナリティが低いため、”おそらく”スキップスキャンが行われているだろうと考えられます。</p>
<p>次に、Seq Scanが採択された場合にどのような結果となるかも試してみます。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1f3kxly-6" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1f3kxly-6" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres=*# SET LOCAL enable_indexscan = off;</span><br><span class="line">SET</span><br><span class="line">postgres=*# SET LOCAL enable_bitmapscan = off;</span><br><span class="line">SET</span><br><span class="line">postgres=*# EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123;</span><br><span class="line">                                                        QUERY PLAN</span><br><span class="line">--------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Gather  (cost=1000.00..15164.33 rows=100 width=36) (actual <span class="keyword">time</span>=0.363..18.606 rows=93.00 loops=1)</span><br><span class="line">   Workers Planned: 2</span><br><span class="line">   Workers Launched: 2</span><br><span class="line">   Buffers: shared hit=8946</span><br><span class="line">   -&gt;  Parallel Seq Scan on orders  (cost=0.00..14154.33 rows=42 width=36) (actual <span class="keyword">time</span>=0.141..14.359 rows=31.00 loops=3)</span><br><span class="line">         Filter: (customer_id = 123)</span><br><span class="line">         Rows Removed by Filter: 333302</span><br><span class="line">         Buffers: shared hit=8946</span><br><span class="line"> Planning Time: 0.062 ms</span><br><span class="line"> Execution Time: 18.625 ms</span><br></pre></td></tr></table></figure></div>

<p><code>18.625ms</code>で応答しており、Index Scanよりも大幅に遅い結果になりました。</p>
<h3 id="PostgreSQL-17-の場合">PostgreSQL 17 の場合</h3><p>次は同様の検証をPostgreSQL 17にて実施してみます。</p>
<h4 id="下準備-1">下準備</h4><p>以下コマンドでコンテナを立ち上げた後は18の時と同じ手順でデータを投入していきます。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1f3kxly-7" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1f3kxly-7" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">docker run --name pg17-handson -e POSTGRES_PASSWORD=mysecretpassword -p 5433:5432 -d postgres:17</span><br></pre></td></tr></table></figure></div>

<p>データが揃ったところで実際に検索してみます。</p>
<blockquote>
<p>最も大きな変更点として、PostgreSQL 18からEXPLAIN ANALYZEを実行すると、バッファ使用量が自動的に表示されるようになりました。これまではBUFFERSオプションを明示的に指定する必要がありましたが、18からは標準で出力されます。</p>
</blockquote>
<p>先日の山本さんの記事にて触れられてましたが、PostgreSQL 17時点では<code>EXPLAIN ANALYZE</code>のみではバッファ使用量が出力されないので、Buffersオプションを付けて実行します。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1f3kxly-8" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1f3kxly-8" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres=#  EXPLAIN (ANALYZE,BUFFERS) SELECT * FROM orders WHERE customer_id = 123;</span><br><span class="line">                                                                QUERY PLAN</span><br><span class="line">-------------------------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Index Scan using idx_orders_status_customer on orders  (cost=0.42..12020.71 rows=100 width=36) (actual <span class="keyword">time</span>=0.015..2.206 rows=95 loops=1)</span><br><span class="line">   Index Cond: (customer_id = 123)</span><br><span class="line">   Buffers: shared hit=1120</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=5</span><br><span class="line"> Planning Time: 0.077 ms</span><br><span class="line"> Execution Time: 2.220 ms</span><br></pre></td></tr></table></figure></div>

<p><code>2.220ms</code>で応答しました。</p>
<p>次に、Seq Scan時の応答性能を確認しておきます。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1f3kxly-9" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1f3kxly-9" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres=*# SET LOCAL enable_indexscan = off;</span><br><span class="line">SET</span><br><span class="line">postgres=*# SET LOCAL enable_bitmapscan = off;</span><br><span class="line">SET</span><br><span class="line">postgres=*# EXPLAIN (ANALYZE,BUFFERS) SELECT * FROM orders WHERE customer_id = 123;</span><br><span class="line">                                                      QUERY PLAN</span><br><span class="line">-----------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Gather  (cost=1000.00..15162.33 rows=100 width=36) (actual <span class="keyword">time</span>=0.269..18.116 rows=95 loops=1)</span><br><span class="line">   Workers Planned: 2</span><br><span class="line">   Workers Launched: 2</span><br><span class="line">   Buffers: shared hit=8944</span><br><span class="line">   -&gt;  Parallel Seq Scan on orders  (cost=0.00..14152.33 rows=42 width=36) (actual <span class="keyword">time</span>=0.281..14.290 rows=32 loops=3)</span><br><span class="line">         Filter: (customer_id = 123)</span><br><span class="line">         Rows Removed by Filter: 333302</span><br><span class="line">         Buffers: shared hit=8944</span><br><span class="line"> Planning Time: 0.066 ms</span><br><span class="line"> Execution Time: 18.132 ms</span><br></pre></td></tr></table></figure></div>

<p>結果は<code>18.132ms</code>でした。試行回数は少ないですが、バージョン18と大きな乖離があるわけではないと考えられます。</p>
<h3 id="結果の考察">結果の考察</h3><p>改めてバージョン18および17で実施した検証結果をまとめると以下の通りです。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">Type</th>
<th align="right">ver.18</th>
<th align="right">ver.17</th>
</tr>
</thead>
<tbody><tr>
<td align="left">Index Scan</td>
<td align="right">0.276 ms</td>
<td align="right">2.220 ms</td>
</tr>
<tr>
<td align="left">Seq Scan</td>
<td align="right">18.625 ms</td>
<td align="right">18.132 ms</td>
</tr>
</tbody></table></div>
<p>Index Scan性能は約8倍の差がありますね。18単体の結果を見た時点ではスキップスキャンによって高速化してるのかが正直分かりづらかったですが、こうして17と比較するとスキップスキャンが機能していることが確認できます。</p>
<h2 id="まとめ">まとめ</h2><p>PostgreSQL 18で追加されたスキップスキャン機構により、Index Scan時のクエリ応答性能が向上しました。また、複合インデックスを有効活用できるシーンが増えたことにより、不要なインデックスを削除でき、ディスク容量を節約するとともに登録・更新時の性能の向上も期待できます。</p>
<p>この新機能の特徴をしっかりとおさえた上で、インデックス設計および性能検証をしていきましょう。</p>
]]></content>
    <summary type="html">「B-treeインデックスのスキップスキャン」機能が気になったので、機能の特徴を深堀りしつつ、実際の挙動を確認してみます。複合インデックス（複数の列で構成されるインデックス）の利用効率を劇的に向上させる新しいスキャン方法です。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="PostgreSQL" scheme="https://future-architect.github.io/tags/PostgreSQL/"/>
    <category term="PostgreSQL18" scheme="https://future-architect.github.io/tags/PostgreSQL18/"/>
    <category term="実行計画" scheme="https://future-architect.github.io/tags/%E5%AE%9F%E8%A1%8C%E8%A8%88%E7%94%BB/"/>
  </entry>
  <entry>
    <title>PostgreSQL: 4億件のテーブルでSeq Scanが選ばれる問題を、統計情報(n_distinct)の改善で解決するまでのプロセス</title>
    <link href="https://future-architect.github.io/articles/20251010a/"/>
    <id>https://future-architect.github.io/articles/20251010a/</id>
    <published>2025-10-09T15:00:00.000Z</published>
    <updated>2025-10-09T15:00:00.000Z</updated>
    <author><name>市川裕也</name></author>
    <content type="html"><![CDATA[<p>PostgreSQL18連載の4本目の記事です。</p>
<h2 id="はじめに">はじめに</h2><p>こんにちは、CSIG (Cyber Security Innovation Group) の市川です。</p>
<p>本記事では、私が現場で行った PostgreSQL のパフォーマンスチューニングについて、原因調査から解決までのプロセスを共有します。この記事が、「なぜか適切な実行計画が選ばれない、インデックスが使われない」といった同様の問題に直面している方の助けになれば幸いです。</p>
<p>また、今回の鍵となった <code>n_distinct</code> という統計情報の計算方法について、Appendix に考察を記載しているので、合わせてご一読ください。 (不適切な記載がある場合は、ご指摘いただけますと幸いです)</p>
<p>まず本記事の要点として、発生した問題と解決策の要約を紹介します。</p>
<h3 id="発生した問題と解決策の要約">発生した問題と解決策の要約</h3><ul>
<li><strong>問題:</strong> 4 億件のレコードを持つ <code>childs</code> テーブルをスキャンする際、インデックスがあるにも関わらず <code>Seq Scan</code> が実行され、特定のクエリがタイムアウトしていた</li>
<li><strong>原因:</strong> <code>ANALYZE</code> で収集される統計情報である <code>n_distinct</code> （カラム内のユニークな値の数）が、実態と大きく乖離していた。この乖離により、PostgreSQL が「1 つの <code>parent</code> に大量の <code>child</code> が紐付いている」と誤認し、その誤認に基づいてクエリ実行のコストを計算してしまったため。</li>
<li><strong>解決策:</strong><ol>
<li>採用した案:<ul>
<li><code>random_page_cost</code> を下げて、相対的に <code>Index Scan</code> が選ばれやすいようにコストを調整する。</li>
<li><code>n_distnct</code> を手動変更し、統計情報を直接実態に近づけることで、根本原因を解消する</li>
</ul>
</li>
<li>不採用とした案:<ul>
<li>hint 句を用いて、Index Scan を強制する</li>
<li>統計情報を計算する際にサンプリングされる行数を増やす (&#x3D; stats target を増やす)</li>
</ul>
</li>
</ol>
</li>
</ul>
<h2 id="前提の説明">前提の説明</h2><h3 id="前提1-本記事の問題が発生した環境">前提1 : 本記事の問題が発生した環境</h3><ul>
<li>バージョン : PostgreSQL 15.5</li>
<li>環境 : Amazon Aurora</li>
</ul>
<h3 id="前提2-本記事に登場するテーブル">前提2 : 本記事に登場するテーブル</h3><p>今回の話では、 <code>parents</code>　テーブルと <code>childs</code> テーブルが登場します。<br>大体のレコード数とカラム名だけ頭の片隅に置いておいていただけると、この後の話が入ってきやすいかと思います。</p>
<img fetchpriority="high" src="/images/2025/20251010a/テーブル.png" alt="テーブル.png" width="1200" height="789">

<h2 id="本題-発生していた問題の原因と、解決するためのアプローチの比較検討">本題: 発生していた問題の原因と、解決するためのアプローチの比較検討</h2><p>本節では、</p>
<ul>
<li>① 発生していた問題</li>
<li>② 問題が発生していた原因</li>
<li>③ 解決策の選択肢</li>
<li>④ 各案の比較検討と、最終的な選択</li>
</ul>
<p>を順に説明します。</p>
<h3 id="①-発生していた問題">① 発生していた問題</h3><p>問題となっていたのは、複数の <code>parent_id</code> に紐づく <code>child</code> を一括で取得する、以下のような単純なクエリでした。 (ARRAY 内には、 <code>parent_id</code> の可変長配列が入ります)</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-g4xdz5-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-g4xdz5-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> childs <span class="keyword">WHERE</span> parent_id <span class="operator">=</span> <span class="keyword">ANY</span> (<span class="keyword">ARRAY</span>[...]);</span><br></pre></td></tr></table></figure></div>

<p><code>parent_id</code> カラムにはインデックスが設定されていましたが、なぜか Index Scan や Bitmap Heap Scan ではなく、 Seq Scan が走ってしまっていました。<br>結果、クエリの実行に約 68 秒もかかってしまっていました。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-g4xdz5-2" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-g4xdz5-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">hoge<span class="operator">=</span><span class="operator">&gt;</span> explain (analyze, buffers) <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> childs <span class="keyword">where</span> parent_id <span class="operator">=</span> <span class="keyword">ANY</span> (<span class="keyword">ARRAY</span>[:parent_ids_array]::<span class="type">bigint</span>[]);</span><br><span class="line">                                                 QUERY PLAN</span><br><span class="line"><span class="comment">--------------------------------------------------------------------------------------------</span></span><br><span class="line"><span class="comment">-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------</span></span><br><span class="line"> Seq Scan <span class="keyword">on</span> childs  (cost<span class="operator">=</span><span class="number">17.28</span>.<span class="number">.4083986</span><span class="number">.65</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">2402424</span> width<span class="operator">=</span><span class="number">228</span>) (actual <span class="type">time</span><span class="operator">=</span><span class="number">18.351</span>.<span class="number">.68034</span><span class="number">.715</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">487741</span> loops<span class="operator">=</span><span class="number">1</span>)</span><br><span class="line">   <span class="keyword">Filter</span>: (parent_id <span class="operator">=</span> <span class="keyword">ANY</span> (<span class="string">&#x27;&#123;1,2,3,4,..&#125;&#x27;</span>::<span class="type">bigint</span>[]))</span><br><span class="line">   <span class="keyword">Rows</span> Removed <span class="keyword">by</span> <span class="keyword">Filter</span>: <span class="number">26638329</span></span><br><span class="line">   Buffers: shared hit<span class="operator">=</span><span class="number">13147</span> read<span class="operator">=</span><span class="number">3671454</span></span><br><span class="line">   I<span class="operator">/</span>O Timings: shared read<span class="operator">=</span><span class="number">62633.011</span></span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit<span class="operator">=</span><span class="number">246</span></span><br><span class="line"> Planning <span class="type">Time</span>: <span class="number">14.745</span> ms</span><br><span class="line"> Execution <span class="type">Time</span>: <span class="number">68066.801</span> ms</span><br><span class="line">(<span class="number">9</span> <span class="keyword">rows</span>)</span><br></pre></td></tr></table></figure></div>

<p>ちなみに、Seq Scan を off にすると、 1 秒程度でクエリ実行が完了しました。<br>このことから、Seq Scan は不適切な実行計画だったことが分かります。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-g4xdz5-3" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-g4xdz5-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">hoge<span class="operator">=</span><span class="operator">&gt;</span> <span class="keyword">set</span> enable_seqscan <span class="operator">=</span> <span class="string">&#x27;off&#x27;</span>;</span><br><span class="line"><span class="keyword">SET</span></span><br><span class="line">hoge<span class="operator">=</span><span class="operator">&gt;</span> explain (analyze, buffers) <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> childs <span class="keyword">where</span> parent_id <span class="operator">=</span> <span class="keyword">ANY</span> (<span class="keyword">ARRAY</span>[:parent_ids_array]::<span class="type">bigint</span>[]);</span><br><span class="line">                 Query Plan</span><br><span class="line"><span class="comment">----------------------------------------------------------------------------------------------------------------------------</span></span><br><span class="line"><span class="comment">----------------------------------------------------------------------------------------------------------------------------</span></span><br><span class="line">Bitmap Heap Scan <span class="keyword">on</span> childs  (cost<span class="operator">=</span><span class="number">1270253.43</span>.<span class="number">.17566326</span><span class="number">.72</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">55852774</span> width<span class="operator">=</span><span class="number">132</span>) (actual <span class="type">time</span><span class="operator">=</span><span class="number">1094.272</span>.<span class="number">.1168</span><span class="number">.348</span> r</span><br><span class="line">ows<span class="operator">=</span><span class="number">76774</span> loops<span class="operator">=</span><span class="number">1</span>)</span><br><span class="line">   Recheck Cond: (parent_id <span class="operator">=</span> <span class="keyword">ANY</span> (<span class="string">&#x27;&#123;1,2,3,...)&#x27;</span>))</span><br><span class="line">		 Buffers: shared hit<span class="operator">=</span><span class="number">33381</span> read<span class="operator">=</span><span class="number">1055</span></span><br><span class="line">         I<span class="operator">/</span>O Timings: shared<span class="operator">/</span><span class="keyword">local</span> read<span class="operator">=</span><span class="number">1054.971</span></span><br><span class="line"> Planning <span class="type">Time</span>: <span class="number">18.576</span> ms</span><br><span class="line"> Execution <span class="type">Time</span>: <span class="number">1177.448</span> ms</span><br><span class="line">(<span class="number">11</span> <span class="keyword">rows</span>)</span><br></pre></td></tr></table></figure></div>

<p>上記の結果は、オプティマイザが「インデックスを使うよりもテーブルを全件スキャンする方がコストが低い」と誤って判断してしまったことを意味しています。<br>なぜオプティマイザが誤った判断をしてしまい、適切な実行計画が選択されなかったかを突き止める必要がありました。</p>
<h3 id="②-問題が発生していた原因">② 問題が発生していた原因</h3><p>調査の結果、Seq Scan が過度に選ばれてしまった原因は、 <code>n_distinct</code> と呼ばれる統計情報が実際の値と乖離していることであると分かりました。</p>
<div class="note-container note-info note-has-title"><div class="note-title"><span class="note-icon"></span><code>n_distinct</code> とは</div><div class="note-body">

<p>テーブル内でのユニークな値の個数を表す統計情報です。<br>詳細は、 pg_stats のドキュメント をご覧ください。</p>
<p><code>n_distinct</code> が正の値の場合、その値がそのまま列内のユニークな値の推定数を示します。<br><code>n_distinct</code> が負の値(-1.0 ~ 0) の場合、(実際のユニークな値の数 &#x2F; 全行数) * -1 の値、つまりテーブル全体に対する割合として扱われます。例えば、 <code>n_distinct</code>&#x3D;-0.1 の場合、ユニークな値の数は全体のレコード数の 1&#x2F;10、と推測されます。</p>
</div></div>

<p><code>childs.parent_id</code> カラムの <code>n_distinct</code> は以下の通りでした。</p>
  <div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-g4xdz5-4" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-g4xdz5-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">hoge<span class="operator">=</span><span class="operator">&gt;</span> <span class="keyword">SELECT</span> n_distinct <span class="keyword">FROM</span> pg_stats <span class="keyword">WHERE</span> tablename <span class="operator">=</span> <span class="string">&#x27;childs&#x27;</span> <span class="keyword">AND</span> attname <span class="operator">=</span> <span class="string">&#x27;parent_id&#x27;</span>;</span><br><span class="line">n_distinct</span><br><span class="line"><span class="comment">------------</span></span><br><span class="line">      <span class="number">67122</span></span><br><span class="line">(<span class="number">1</span> <span class="type">row</span>)</span><br></pre></td></tr></table></figure></div>

<p>PostgreSQL のオプティマイザは、「この <code>parent_id</code> カラムには約 6.7 万個しかユニークな値が存在しない」と認識していました。つまり、<strong>各 parent に 4 億&#x2F;6.7 万 ≒ 6000 個の child が紐づいている</strong>、と認識されていた訳です。<br>実際に、1 個の parent に紐づく child を取得する SQL の実行計画を見てみると、 <code>rows=5951</code> と予想されています。これは、「1 つの <code>parent</code> に紐づく child を取得する場合、5951 個の child が取得される」とオプティマイザが予測していることを意味しています。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-g4xdz5-5" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-g4xdz5-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">hoge<span class="operator">=</span><span class="operator">&gt;</span> EXPLAIN <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> childs <span class="keyword">WHERE</span> parent_id <span class="operator">=</span> <span class="number">8887212</span>;</span><br><span class="line">											   QUERY PLAN</span><br><span class="line"><span class="comment">--------------------------------------------------------------------------------------------------------</span></span><br><span class="line"> Index Scan <span class="keyword">using</span> childs_parent_id_idx <span class="keyword">on</span> childs  (cost<span class="operator">=</span><span class="number">0.57</span>.<span class="number">.18009</span><span class="number">.19</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">5951</span> width<span class="operator">=</span><span class="number">130</span>)</span><br><span class="line">   Index Cond: (parent_id <span class="operator">=</span> <span class="number">8887212</span>)</span><br><span class="line">(<span class="number">2</span> <span class="keyword">rows</span>)</span><br></pre></td></tr></table></figure></div>

<p>一方で、実際の <code>parent_id</code> のユニーク数は約 1400 万 (&#x3D; <strong>1  parent あたりの child 数は約 25 個</strong>) であり、統計情報の値と大きく乖離していました。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-g4xdz5-6" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-g4xdz5-6" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">hoge<span class="operator">=</span><span class="operator">&gt;</span> <span class="keyword">select</span> <span class="built_in">count</span>(<span class="keyword">distinct</span> parent_id) <span class="keyword">from</span> childs;</span><br><span class="line">count</span><br><span class="line"><span class="comment">----------</span></span><br><span class="line"><span class="number">15480429</span> (<span class="number">1</span> <span class="type">row</span>)</span><br></pre></td></tr></table></figure></div>

<p>この不正確な <code>n_distinct</code> の値により、オプティマイザによって seq scan が過度に選ばれやすい状況になってしまっていました。</p>
<h3 id="③-解決策の選択肢">③ 解決策の選択肢</h3><p>根本原因が統計情報の不正確さにあると分かったところで、いくつかの解決策を考えました。</p>
<h4 id="A-案-random-page-cost-の調整">A 案: <code>random_page_cost</code> の調整</h4><p>この案は、 Index Scan が選ばれやすくなるように、 <code>random_page_cost</code> と呼ばれる値を下げる方向に調整するアプローチです。</p>
<p>PostgreSQL のコスト計算には、ディスクアクセスのコストを以下のパラメータで制御しています。</p>
<ul>
<li><code>seq_page_cost</code>: シーケンシャルアクセスのコスト（デフォルト: 1.0）</li>
<li><code>random_page_cost</code>: ランダムアクセスのコスト（デフォルト: 4.0）</li>
</ul>
<p>ランダムアクセスのコストがシーケンシャルアクセスのコストよりもかなり大きめに設定されているのは、デフォルト値が HDD を想定しているためです。 (HDD は、ランダムアクセスのコストが大きい)<br>しかし、データベースのディスクが SSD の場合、実際のところランダムアクセスのコストとシーケンシャルアクセスのコストの差はそこまで大きくありません。<br>そのため、SSD の場合は <code>random_page_cost</code> を 下げる（例: 1.1）ことによって、より実際のディスクアクセスコストに即した実行計画が選ばれるようになります。</p>
<p><code>random_page_cost</code> を下げると、ランダムアクセスのコストが低く見積もられるようになるため、 Index Scan が選ばれやすくなります。そのため、データベースのディスクが SSD の場合は、変更を検討する価値があるパラメータです。</p>
<p><code>seq_page_cost</code> と <code>random_page_cost</code> について、詳しくは PostgreSQL 公式ドキュメント: 問い合わせ計画 を参照してください。</p>
<h4 id="B-案-stats-target-の引き上げ">B 案: <code>stats target</code> の引き上げ</h4><div class="note-container note-info note-has-title"><div class="note-title"><span class="note-icon"></span><code>stats target</code> とは</div><div class="note-body">

<p>stats target とは、「統計情報」を取得する際にサンプルする行数を示す値です。<br>0~10000 まで設定でき、デフォルトは 100 です。 <code>ANALYZE</code> の際にサンプリングされる行数は <code>stats target * 300</code> で決定されます。</p>
</div></div>

<p>この案は、<code>ANALYZE</code> 時のサンプル数を増やすことで、 <code>n_distinct</code> の精度を上げ、実際のユニーク数に近づけるアプローチです。<br>以下のような SQL を実行することにより、変更した stats distinct に基づいて統計情報が計算されるようになります。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-g4xdz5-7" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-g4xdz5-7" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">ALTER TABLE</span> childs <span class="keyword">ALTER</span> <span class="keyword">COLUMN</span> parent_id <span class="keyword">SET</span> STATISTICS <span class="number">1000</span>;</span><br><span class="line">ANALYZE childs;</span><br><span class="line"><span class="comment">-- ANALYZE の際にサンプリングされる行数が増加する</span></span><br></pre></td></tr></table></figure></div>

<h4 id="C-案-n-distinct-の直接設定">C 案: <code>n_distinct</code> の直接設定</h4><p>この案は、 <code>n_distinct</code> を実際のユニーク値に近い値に直接書き換えてしまおう、というアプローチです。</p>
<p>PostgreSQL では、<code>n_distinct</code> の値を直接上書きもできます。<br>詳しくは ALTER TABLE のドキュメント &gt; <code>SET (attribute_option)</code> の項 を参照してください。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-g4xdz5-8" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-g4xdz5-8" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- n_distinctを正の値にすると、その値が採用される</span></span><br><span class="line"><span class="comment">-- n_distinctを負の値(-1.0 ~ 0)にすると、(実際のユニーク数 / 全行数) * -1 の値として扱われる</span></span><br><span class="line"><span class="keyword">ALTER TABLE</span> childs <span class="keyword">ALTER</span> <span class="keyword">COLUMN</span> parent_id <span class="keyword">SET</span> (n_distinct <span class="operator">=</span> <span class="number">-0.1</span>);</span><br><span class="line">ANALYZE childs;</span><br></pre></td></tr></table></figure></div>

<p>② 問題が発生していた原因 節の再掲にはなりますが、<code>n_distinct</code> を負の値 (-1.0 ~ 0) に手動変更することで、テーブル全体に対するユニークな <code>parent_id</code> 数の割合を示すこともできます。例えば、 <code>n_distinct</code>&#x3D;-0.1 に設定することで、「ユニークな <code>parent_id</code> の数は全体のレコード数の 1&#x2F;10 である」、とオプティマイザに推測させることができます。</p>
<h4 id="D-案-pg-hint-plan-を用いて、スキャン方法を強制する">D 案: <code>pg_hint_plan</code> を用いて、スキャン方法を強制する</h4><p>この案は、「ヒント句」を用いて特定のクエリに対してスキャン方法を強制するアプローチです。</p>
<p><code>pg_hint_plan</code> は、ユーザー側で実行計画を制御するためのツールです。<br><code>pg_hint_plan</code> というエクステンションを導入した上で、SQLコメント内にヒント句 (&#x2F;*+ IndexScan(childs) *&#x2F; など) を書くことで、スキャン方法や JOIN の方法を強制できます。<br>詳細は <code>pg_hint_plan</code> のドキュメント &gt; スキャン方法 などを参照してください。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-g4xdz5-9" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-g4xdz5-9" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- 以下のようなコメントを付すことで、 childs のスキャン時に Index Scan が使用されるよう固定される</span></span><br><span class="line"><span class="comment">/*+ IndexScan(childs) */</span></span><br><span class="line">explain (analyze, buffers) <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> childs <span class="keyword">where</span> parent_id <span class="operator">=</span> <span class="keyword">ANY</span> (<span class="keyword">ARRAY</span>[:parent_ids_array]::<span class="type">bigint</span>[]);</span><br></pre></td></tr></table></figure></div>

<h3 id="④-各案の比較検討と、最終的な選択">④ 各案の比較検討と、最終的な選択</h3><h4 id="不採用-D-案-ヒント句-について">[不採用]  D 案 (ヒント句) について</h4><p>D 案は、オプティマイザの挙動に拠らずスキャン方法を指定できるため、想定している実行計画をオプティマイザに選ばせやすく便利ですが、以下の理由から不採用としました。</p>
<ul>
<li>現在使用している ORM に組みこむのが難しかった。現状のアプリケーションコードに組み込むには、 ORM を最大限利用した現在の書き方から生の SELECT 文に書き換える必要があり、アプリケーション側の変更が大きくなってしまいそうだった。</li>
<li>統計情報を改善しないと、別の箇所で <code>childs</code> をスキャンする際にも同様の問題が発生する可能性がある。そのため、統計情報の改善によって根本的に解決できるのであれば、統計情報の改善の方が望まれる。</li>
</ul>
<h4 id="採用-A-案-random-page-cost-の調整-について">[採用] A 案 (<code>random_page_cost</code> の調整) について</h4><p>現在稼働しているシステムでは SSD ボリュームの DB を使用しています。A 案は SSD ボリュームで実施する分にはデメリットが特になさそうだったため、開発環境でしばらく運用して問題が発生しないことを確認した上で、採用することにしました。</p>
<p>ただし、A 案を採用しても Index Scan が選ばれやすくなるだけで、統計情報自体が改善される訳ではありません。そのため、指定される parent 数が多いと結局 Seq Scan が選ばれてしまう、という状況でした。<br>よって、統計情報を改善する施策である B 案と C 案についても検討し、いずれかの方法も合わせて採用する必要がありました。</p>
<h4 id="B-案-stats-target-の引き上げ-の現実性について検討">B 案 (<code>stats target</code> の引き上げ) の現実性について検討</h4><p>B 案 による解決が可能かを判断するため、 <code>childs</code> テーブルで、 stats target にさまざまな値を指定して、実際の <code>n_distinct</code> と ANALYZE の時間を計測しました。以下がその結果です。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">stats target</th>
<th align="left">n_distinct</th>
<th align="left">ANALYZE に<br/>かかる時間</th>
</tr>
</thead>
<tbody><tr>
<td align="left">100 (デフォルト)</td>
<td align="left">6.7 万</td>
<td align="left">25 秒</td>
</tr>
<tr>
<td align="left">200</td>
<td align="left">9 万</td>
<td align="left">45 秒</td>
</tr>
<tr>
<td align="left">500</td>
<td align="left">15 万</td>
<td align="left">62 秒</td>
</tr>
<tr>
<td align="left">1000</td>
<td align="left">22 万</td>
<td align="left">114 秒</td>
</tr>
<tr>
<td align="left">2000</td>
<td align="left">24 万</td>
<td align="left">362 秒</td>
</tr>
<tr>
<td align="left">&#x3D;&#x3D;&#x3D;</td>
<td align="left">&#x3D;&#x3D;&#x3D;</td>
<td align="left">&#x3D;&#x3D;&#x3D;</td>
</tr>
<tr>
<td align="left"><strong>実際のユニーク数</strong></td>
<td align="left"><strong>1400 万</strong></td>
<td align="left">-</td>
</tr>
</tbody></table></div>
<p>stats target を大きくしていくことで <code>n_distinct</code> が実際のユニークな数に近づいてはいますが、依然として大きな乖離があることが分かります。<br>ただ、 stats target を 2500 まで引き上げると、現状実行されうるすべてのクエリに対して Index Scan が選ばれるようになることはわかりました。 (stats target を 2500 にしたの際のデータは掘り起こせなかったため載せていません。申し訳ありません。)</p>
<p>なお、この「可能か」というポイントに加えて、stats target を大きくすると ANALYZE の時間が伸びてしまう、というデメリットも検討に入れる必要のあるポイントです。</p>
<h4 id="C-案-n-distinct-手動変更-による保守性の低下についての検討">C 案 (<code>n_distinct</code> 手動変更) による保守性の低下についての検討</h4><p>この案は、統計情報を手動で無理やり変更する方法なので、テーブルの肥大化により実際のデータと乖離が生じる可能性がある、というデメリットを抱えていました。<br>ただ、今回のケースは少し特殊で、 「将来データが増加した場合でも、<code>parents</code> と <code>childs</code> のレコード数の比率は大きく変動しない」という特徴を持ち合わせていました。このデータ特性は、 <code>n_distinct</code> を割合として設定できれば、ある程度の保守性を担保できることを意味しています。</p>
<p>この考察から、 <code>n_distinct</code> に負の数 (&#x3D;割合) を設定すれば、データが増えてもある程度の保守性を担保できると判断しました。</p>
<h4 id="B-案-stats-target-と-C-案-n-distinct-手動変更-の比較">B 案 (stats target) と C 案 (<code>n_distinct</code> 手動変更) の比較</h4><p>上記の B 案と C 案の検討を元に、統計情報自体を改善する方法として、 B 案 と C 案の採用についてそれぞれ検討しました。<br>以下のようにメリデメを整理しました。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>方針</th>
<th align="center">解決可能か</th>
<th>保守性</th>
<th>analyze の時間</th>
</tr>
</thead>
<tbody><tr>
<td>B 案: stats target を大きくする (&lt; 2000)</td>
<td align="center">△ <br/>(parent_id が多いと<br/> Seq Scan が<br/>選ばれてしまう)</td>
<td>△<br/>(自動更新だが、将来のデータ増で再調整の可能性あり)</td>
<td>増加する</td>
</tr>
<tr>
<td>B 案: stats target を大きくする (&gt; 2500)</td>
<td align="center">○</td>
<td>△<br/>(自動更新だが、将来のデータ増で再調整の可能性あり)</td>
<td>大幅に増加する</td>
</tr>
<tr>
<td>C 案: <code>n_distinct</code> の手動変更 (正の数)</td>
<td align="center">○</td>
<td>×<br/>(手動設定。データ増で実態と乖離する)</td>
<td>変化なし</td>
</tr>
<tr>
<td>C 案: <code>n_distinct</code> の手動変更 (負の数)</td>
<td align="center">○</td>
<td>△<br/>(手動設定だが、割合指定なのでデータ増に強い)</td>
<td>変化なし</td>
</tr>
</tbody></table></div>
<h4 id="最終的な選択">最終的な選択</h4><p>上記のメリデメの表と <code>n_distinct</code> に関するデータモデルの考察より、最終的に「A 案: <code>random_page_cost</code> の調整」に加えて、 「C 案: <code>n_distinct</code> の手動変更 (負の数)」を採用することにしました。</p>
<h3 id="実施内容と効果">実施内容と効果</h3><p>上記の案を実施したことで、 <code>parent_id</code> が多い場合でも Index Scan が選ばれるようになりました。</p>
<h2 id="まとめ">まとめ</h2><p>本記事では、4 億件超のレコードを持つテーブルで発生したパフォーマンス問題について、調査から解決までのプロセスを共有しました。</p>
<p>今回の問題の核心は、PostgreSQL の統計情報（特に <code>n_distinct</code>）が実態と乖離していたことでした。<br>この問題を解決するための策を複数列挙し、比較検討を行いました。</p>
<p>今回は 「C 案: <code>n_distinct</code> の手動変更」を最終的な解決策としましたが、アプリケーションやテーブルの規模によって、「B 案: stats target を大きくする」や「D 案: <code>pg_hint_plan</code> を活用する」も最善の改善策になりうると思います。<br>改善策を複数考え、その場の状況に最も即した改善策を取ることが重要だと思います。</p>
<p>本記事が、同様の問題に直面している方の一助となれば幸いです。</p>
<hr>
<h2 id="Appendix-n-distinct-の計算方法についての考察">Appendix: <code>n_distinct</code> の計算方法についての考察</h2><p>(以下の考察は統計学に基づいたものではないため、不正確な記述を含んでいる可能性があります。間違いや不適切な記述がある場合は、ご指摘いただけると幸いです)</p>
<p><code>n_distinct</code> は、 <code>ANALYZE</code> を実行した際に以下の式で計算されます。(コード内の <code>stadistinct</code> が <code>n_distinct</code> を表しています)<br><code>n_distinct</code> の計算方法について、詳細は <code>postgres/~/analyze.c</code> のコード を確認してください。</p>
<div class="code-block"><figure class="highlight c"><input type="checkbox" id="code-wrap-g4xdz5-10" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-g4xdz5-10" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">/* Count the number of values we found multiple times */</span></span><br><span class="line">summultiple = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">for</span> (nmultiple = <span class="number">0</span>; nmultiple &lt; track_cnt; nmultiple++)</span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">if</span> (track[nmultiple].count == <span class="number">1</span>)</span><br><span class="line">    <span class="keyword">break</span>;</span><br><span class="line">  summultiple += track[nmultiple].count;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">* where f1 is the number of distinct values that occurred</span></span><br><span class="line"><span class="comment">* exactly once in our sample of n rows (from a total of N),</span></span><br><span class="line"><span class="comment">* and d is the total number of distinct values in the sample.</span></span><br><span class="line"><span class="comment">...</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="type">int</span>			f1 = nonnull_cnt - summultiple;</span><br><span class="line"><span class="type">int</span>			d = f1 + nmultiple;</span><br><span class="line"><span class="type">double</span>		n = samplerows - null_cnt;</span><br><span class="line"><span class="type">double</span>		N = totalrows * (<span class="number">1.0</span> - stats-&gt;stanullfrac);</span><br><span class="line"><span class="type">double</span>		stadistinct;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* N == 0 shouldn&#x27;t hhogeen, but just in case ... */</span></span><br><span class="line"><span class="keyword">if</span> (N &gt; <span class="number">0</span>)</span><br><span class="line">  stadistinct = (n * d) / ((n - f1) + f1 * n / N);</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">  stadistinct = <span class="number">0</span>;</span><br></pre></td></tr></table></figure></div>

<p>この計算式では、以下のようなデメリットがあるように感じました。</p>
<ul>
<li><code>N</code> が分母に含まれないため、 <code>N</code> の大きさが <code>n_distinct</code> に反映されにくい</li>
<li><code>f1</code> が相当大きくないと、上限が <code>n</code> の整数倍で抑えられてしまう<ul>
<li>例えば、 <code>f1=(9/10)*n</code> だった場合、 <code>n_distinct</code> ≦ <code>10d</code>≦ <code>10n</code></li>
</ul>
</li>
</ul>
<p>以下のページの著者も同様の課題を報告しており、 「<code>n_distinct=d*N/n</code> というナイーブな式の方が精度が高いのでは」と提言しています。<br>https://www.postgresql.org/message-id/4338f834-dee9-2eb8-0577-10abe9d39e2d%40postgrespro.ru</p>
<p>この式をそのまま利用することは難しいと思いますが、少なくとも <code>d</code> がある程度大きいのであれば、上記のナイーブな式を用いた方が精度が良くなるのではないかと感じました。</p>
]]></content>
    <summary type="html">私が現場で行った PostgreSQL のパフォーマンスチューニングについて、原因調査から解決までのプロセスを共有します。この記事が、「なぜか適切な実行計画が選ばれない、インデックスが使われない」といった同様の問題に直面している方の助けになれば幸いです。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="PostgreSQL" scheme="https://future-architect.github.io/tags/PostgreSQL/"/>
    <category term="実行計画" scheme="https://future-architect.github.io/tags/%E5%AE%9F%E8%A1%8C%E8%A8%88%E7%94%BB/"/>
  </entry>
  <entry>
    <title>pg_dumpによる統計情報ダンプ検証</title>
    <link href="https://future-architect.github.io/articles/20251009a/"/>
    <id>https://future-architect.github.io/articles/20251009a/</id>
    <published>2025-10-08T15:00:00.000Z</published>
    <updated>2025-10-08T15:00:00.000Z</updated>
    <author><name>岩堀敦</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20251009a/top.jpg" alt="" width="800" height="664">

<p>PostgreSQL18連載の3本目の記事です。</p>
<p>この度、PosgreSQLメジャーバージョンアップに伴い、pg_dumpに統計情報のバックアップ・リストアが追加されました。</p>
<p>PostgreSQLのpg_dumpは、データ削除前のバックアップや他環境へのデータ移行などで広く利用されている機能だと思います。しかし、PosgreSQL18以前のpg_dumpには統計情報が含まれないため、あくまでオブジェクト・データのバックアップとして利用されるが多かったのではないかと考えます。</p>
<div class="note-container note-info"><span class="note-icon"></span><div>

<p>サードパーティ系の拡張モジュールを利用することで、PosgreSQL18以前のバージョンにおいても統計情報のバックアップできます。</p>
</div></div>

<p>今回、PosgreSQLメジャーバージョンアップで、バックアップ・リストアに統計情報が含まれたことにより、より高い精度で本番環境等の実環境を再現できるようになり、pg_dumpを活用できるシーンも増えるのではないかと考えます。</p>
<p>本記事では統計情報を含むpg_dumpの有用性について検証してまいります。</p>
<h2 id="検証①：pg-dumpに統計情報を含むことによる影響は？">検証①：pg_dumpに統計情報を含むことによる影響は？</h2><p>pg_dumpに統計情報が含まれることは活用の幅も広がり、メリットではありますが、実行時間やバックアップファイルサイズが著しく増加すれば、有用性に欠けると考えます。</p>
<p>PosgreSQL18とPosgreSQL18以前（今回はPosgreSQL 16）でpg_dumpによるバックアップ・リストアを実施し、実行時間・ファイルサイズの比較検証を行います。</p>
<h3 id="検証条件">検証条件</h3><h4 id="シナリオ">シナリオ</h4><p>バージョン間の条件を近づけるため、バージョンごとに以下のシナリオで検証します。<br>また、各バージョンごとに「a.10テーブル」「b.50テーブル」「c.100テーブル」の3パターンのテーブル数で検証します。</p>
<p>【PosgreSQL18】</p>
<ol>
<li>pg_dump：スキーマレベル論理バックアップ（対象：スキーマ・オブジェクト・データ・統計情報）</li>
<li>restore：バックアップファイルによるスキーマリストア</li>
</ol>
<p>【PosgreSQL18以前】</p>
<ol>
<li>pg_dump：論理バックアップ（対象：スキーマ・オブジェクト・データ）</li>
<li>restore：バックアップファイルによるスキーマリストア</li>
<li>analyze：統計情報取得</li>
</ol>
<h4 id="テスト用オブジェクト">テスト用オブジェクト</h4><p>バックアップ・リストア対象として用意するオブジェクトは以下とします。<br>なお、データは1テーブルあたり100万件とします。</p>
<p>テーブル</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">カラム名 (列名)</th>
<th align="left">データ型</th>
<th align="left">PK</th>
<th align="left">NULL許容</th>
<th align="left">カーディナリティ</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>id01</code></td>
<td align="left"><code>bigint</code></td>
<td align="left">〇</td>
<td align="left">NO</td>
<td align="left">1,000,000</td>
</tr>
<tr>
<td align="left"><code>id02</code></td>
<td align="left"><code>text</code></td>
<td align="left"></td>
<td align="left">NO</td>
<td align="left">2</td>
</tr>
<tr>
<td align="left"><code>id03</code></td>
<td align="left"><code>text</code></td>
<td align="left"></td>
<td align="left">NO</td>
<td align="left">3</td>
</tr>
<tr>
<td align="left"><code>id04</code></td>
<td align="left"><code>text</code></td>
<td align="left"></td>
<td align="left">NO</td>
<td align="left">4</td>
</tr>
<tr>
<td align="left"><code>id04</code></td>
<td align="left"><code>text</code></td>
<td align="left"></td>
<td align="left">NO</td>
<td align="left">5</td>
</tr>
</tbody></table></div>
<details><summary>検証手順</summary>

<ol>
<li>データベース作成</li>
</ol>
  <figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">create</span> database &#123;データベース名&#125;;</span><br></pre></td></tr></table></figure>

<ol start="2">
<li>スキーマ作成</li>
</ol>
  <figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">create</span> schema &#123;スキーマ名&#125;;</span><br></pre></td></tr></table></figure>

<ol start="3">
<li>テーブル・PK作成</li>
</ol>
<p>  意図しないタイミングで統計情報が更新されるのを避けるため、テーブルレベルでautovacuumを無効化しています。</p>
  <div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1klhblc-1" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">create table</span> &#123;テーブル名&#125; (id01 <span class="type">bigint</span> <span class="keyword">not null</span> , id02 text <span class="keyword">not null</span> , id03 text <span class="keyword">not null</span> , id04 text <span class="keyword">not null</span> , id05 text <span class="keyword">not null</span>) <span class="keyword">with</span>(autovacuum_enabled <span class="operator">=</span> <span class="literal">false</span>, toast.autovacuum_enabled <span class="operator">=</span> <span class="literal">false</span>);</span><br><span class="line"><span class="keyword">alter table</span> table01 <span class="keyword">add constraint</span> pk_table01 <span class="keyword">primary key</span> (id01);</span><br></pre></td></tr></table></figure></div>

<ol start="4">
<li>データ作成</li>
</ol>
  <div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1klhblc-2" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">insert into</span> table01 (<span class="keyword">select</span> generate_series(<span class="number">1</span>,<span class="number">1000000</span>),generate_series(<span class="number">1</span>,<span class="number">1000000</span>)<span class="operator">%</span><span class="number">2</span>,generate_series(<span class="number">1</span>,<span class="number">1000000</span>)<span class="operator">%</span><span class="number">3</span>,generate_series(<span class="number">1</span>,<span class="number">1000000</span>)<span class="operator">%</span><span class="number">4</span>,generate_series(<span class="number">1</span>,<span class="number">1000000</span>)<span class="operator">%</span><span class="number">5</span>);</span><br></pre></td></tr></table></figure></div>

<ol start="5">
<li>統計情報取得※【PosgreSQL18】のみ</li>
</ol>
<p>  autovacuumを無効化しているため、バックアップ対象となる統計情報を取得させます。</p>
  <figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">analyze &#123;対象オブジェクト全てを指定&#125;;</span><br></pre></td></tr></table></figure>

<ol start="6">
<li>pg_dump<br>  windows上にインスタンスを立てているため、Powershellよりpg_dumpを実施しています。<br>  今回は論理バックアップの内容がわかるよう平文にてバックアップを取得します。</li>
</ol>
<p>【PosgreSQL18】</p>
  <div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1klhblc-3" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">powershell -Command <span class="string">&quot;Measure-Command &#123; pg_dump -f &#123;バックアップファイル名&#125; -h &#123;ホスト名&#125; -p &#123;ポート&#125; -U &#123;ユーザ名&#125; -n &#123;スキーマ名&#125; -d &#123;データベース名&#125; -W --format=p -E &quot;</span>UTF8<span class="string">&quot; --verbose --statistics &#125;&quot;</span></span><br></pre></td></tr></table></figure></div>

<p>【PosgreSQL18以前】</p>
<p>  <code>--statistics</code>は付与できない。</p>
  <div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1klhblc-4" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">powershell -Command <span class="string">&quot;Measure-Command &#123; pg_dump -f &#123;バックアップファイル名&#125; -h &#123;ホスト名&#125; -p &#123;ポート&#125; -U &#123;ユーザ名&#125; -n &#123;スキーマ名&#125; -d &#123;データベース名&#125; -W --format=p -E &quot;</span>UTF8<span class="string">&quot; --verbose &#125;&quot;</span></span><br></pre></td></tr></table></figure></div>

<p>  今回は実行時間を取得したいので<code>Measure-Command</code>を使用しています。</p>
  <figure class="highlight text"><table><tr><td class="code"><pre><span class="line">Days              : 0</span><br><span class="line">Hours             : 0</span><br><span class="line">Minutes           : 0</span><br><span class="line">Seconds           : 5</span><br><span class="line">Milliseconds      : 676</span><br><span class="line">Ticks             : 56764202</span><br><span class="line">TotalDays         : 6.56993078703704E-05</span><br><span class="line">TotalHours        : 0.00157678338888889</span><br><span class="line">TotalMinutes      : 0.0946070033333333</span><br><span class="line">TotalSeconds      : 5.6764202</span><br><span class="line">TotalMilliseconds : 5676.4202</span><br></pre></td></tr></table></figure>

<ol start="7">
<li>スキーマ削除</li>
</ol>
  <figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">drop</span> schema &#123;スキーマ名&#125; cascade;</span><br></pre></td></tr></table></figure>

<ol start="8">
<li>リストア</li>
</ol>
<p>  windows上にインスタンスを立てているため、PowershellよりDBにログインし、リストアを実施しています。</p>
  <div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1klhblc-5" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">psql -h &#123;ホスト名&#125; -p &#123;ポート&#125; -U &#123;ユーザ名&#125; -d &#123;データベース名&#125;</span><br><span class="line">\timing</span><br><span class="line">\i &#123;バックアップファイル名&#125;;</span><br></pre></td></tr></table></figure></div>

<p>今回は実行時間を取得したいので<code>\timing</code>を使用しています。</p>
  <figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">（略）</span><br><span class="line">CREATE TABLE</span><br><span class="line">時間: 3.321 ミリ秒</span><br><span class="line">ALTER TABLE</span><br><span class="line">時間: 0.479 ミリ秒</span><br><span class="line">CREATE TABLE</span><br><span class="line">時間: 4.801 ミリ秒</span><br><span class="line">ALTER TABLE</span><br><span class="line">時間: 0.641 ミリ秒</span><br><span class="line">COPY 1000000</span><br><span class="line">時間: 816.210 ミリ秒</span><br><span class="line">COPY 1000000</span><br><span class="line">時間: 721.280 ミリ秒</span><br><span class="line">COPY 1000000</span><br><span class="line">（略）</span><br></pre></td></tr></table></figure>

<ol start="9">
<li>統計情報取得※【PosgreSQL18以前】のみ</li>
</ol>
  <figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">analyze &#123;対象オブジェクト全てを指定&#125;;</span><br></pre></td></tr></table></figure>

</details>

<h3 id="検証①結果">検証①結果</h3><h4 id="実行時間">実行時間</h4><ul>
<li>pg_dumpの実行時間はPostgres18が優位である。</li>
<li>restore（+analyze）の実行時間にも致命的な差はない。</li>
</ul>
<div class="scroll"><table>
<thead>
<tr>
<th align="center">バージョン</th>
<th align="center">パターン<br>(総データ量)</th>
<th align="center">pg_dump<br>(ms)</th>
<th align="center">restore<br>(ms)</th>
<th align="center">analyze<br>(ms)</th>
<th align="center">total<br>(ms)</th>
</tr>
</thead>
<tbody><tr>
<td align="center">PosgreSQL16</td>
<td align="center">10テーブル<br>(1,000万件)</td>
<td align="center">10,292.248</td>
<td align="center">19,134.755</td>
<td align="center">1,179.920</td>
<td align="center">30,606.923</td>
</tr>
<tr>
<td align="center">PosgreSQL16</td>
<td align="center">50テーブル<br>(5,000万件)</td>
<td align="center">38,700.804</td>
<td align="center">113,306.861</td>
<td align="center">7,801.671</td>
<td align="center">159,809.336</td>
</tr>
<tr>
<td align="center">PosgreSQL16</td>
<td align="center">100テーブル<br>(10,000万件)</td>
<td align="center">147,478.958</td>
<td align="center">318,141.068</td>
<td align="center">19,085.712</td>
<td align="center">484,705.738</td>
</tr>
<tr>
<td align="center">PosgreSQL18</td>
<td align="center">10テーブル<br>(1,000万件)</td>
<td align="center">5,676.420</td>
<td align="center">19,507.097</td>
<td align="center">-</td>
<td align="center">25,183.517</td>
</tr>
<tr>
<td align="center">PosgreSQL18</td>
<td align="center">50テーブル<br>(5,000万件)</td>
<td align="center">24,826.907</td>
<td align="center">195,836.231</td>
<td align="center">-</td>
<td align="center">220,663.138</td>
</tr>
<tr>
<td align="center">PosgreSQL18</td>
<td align="center">100テーブル<br>(10,000万件)</td>
<td align="center">69,300.5253</td>
<td align="center">341,310.349</td>
<td align="center">-</td>
<td align="center">410,610.8743</td>
</tr>
</tbody></table></div>
<h4 id="バックアップファイルサイズ">バックアップファイルサイズ</h4><ul>
<li>統計情報データの分ファイルサイズとしては純増していると思われるが、軽微な範囲であると考える。</li>
</ul>
<div class="scroll"><table>
<thead>
<tr>
<th align="center">バージョン</th>
<th align="center">パターン<br>(総データ量)</th>
<th align="center">ファイルサイズ(byte)</th>
</tr>
</thead>
<tbody><tr>
<td align="center">Postgres16</td>
<td align="center">10テーブル<br>(1,000万件)</td>
<td align="center">148,897,705</td>
</tr>
<tr>
<td align="center">Postgres16</td>
<td align="center">50テーブル<br>(5,000万件)</td>
<td align="center">744,485,345</td>
</tr>
<tr>
<td align="center">Postgres16</td>
<td align="center">100テーブル<br>(10,000万件)</td>
<td align="center">1,488,969,904</td>
</tr>
<tr>
<td align="center">Postgres18</td>
<td align="center">10テーブル<br>(1,000万件)</td>
<td align="center">148,951,114</td>
</tr>
<tr>
<td align="center">Postgres18</td>
<td align="center">50テーブル<br>(5,000万件)</td>
<td align="center">744,657,089</td>
</tr>
<tr>
<td align="center">Postgres18</td>
<td align="center">100テーブル<br>(10,000万件)</td>
<td align="center">1,489,313,091</td>
</tr>
</tbody></table></div>
<h4 id="反省点（まずは反省から。今後、アップデートしていきます）">反省点（まずは反省から。今後、アップデートしていきます）</h4><ul>
<li>純粋なバージョンアップによる比較をするためには、Postgres17を採用すべきであった。これではPostgres17アップデートによる影響なのか、Postgres18アップデートによる影響なのか判断ができない</li>
<li>値を比較するには、試行回数が足りていない</li>
</ul>
<h4 id="pg-dump考察">pg_dump考察</h4><p>Postgres18バックアップファイルを確認すると、以下のように統計情報が含まれていることがわかる。</p>
<p>また、テーブルレベルの統計情報は<code>pg_restore_relation_stats</code>、カラムレベルの統計情報は<code>pg_restore_attribute_stats</code>により、統計情報が更新されている。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1klhblc-6" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-6" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">（略）</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> pg_catalog.pg_restore_relation_stats(</span><br><span class="line">	<span class="string">&#x27;version&#x27;</span>, <span class="string">&#x27;180000&#x27;</span>::<span class="type">integer</span>,</span><br><span class="line">	<span class="string">&#x27;schemaname&#x27;</span>, <span class="string">&#x27;test&#x27;</span>,</span><br><span class="line">	<span class="string">&#x27;relname&#x27;</span>, <span class="string">&#x27;table01&#x27;</span>,</span><br><span class="line">	<span class="string">&#x27;relpages&#x27;</span>, <span class="string">&#x27;5406&#x27;</span>::<span class="type">integer</span>,</span><br><span class="line">	<span class="string">&#x27;reltuples&#x27;</span>, <span class="string">&#x27;1e+06&#x27;</span>::<span class="type">real</span>,</span><br><span class="line">	<span class="string">&#x27;relallvisible&#x27;</span>, <span class="string">&#x27;0&#x27;</span>::<span class="type">integer</span>,</span><br><span class="line">	<span class="string">&#x27;relallfrozen&#x27;</span>, <span class="string">&#x27;0&#x27;</span>::<span class="type">integer</span></span><br><span class="line">);</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> pg_catalog.pg_restore_attribute_stats(</span><br><span class="line">	<span class="string">&#x27;version&#x27;</span>, <span class="string">&#x27;180000&#x27;</span>::<span class="type">integer</span>,</span><br><span class="line">	<span class="string">&#x27;schemaname&#x27;</span>, <span class="string">&#x27;test&#x27;</span>,</span><br><span class="line">	<span class="string">&#x27;relname&#x27;</span>, <span class="string">&#x27;table01&#x27;</span>,</span><br><span class="line">	<span class="string">&#x27;attname&#x27;</span>, <span class="string">&#x27;id01&#x27;</span>,</span><br><span class="line">	<span class="string">&#x27;inherited&#x27;</span>, <span class="string">&#x27;f&#x27;</span>::<span class="type">boolean</span>,</span><br><span class="line">	<span class="string">&#x27;null_frac&#x27;</span>, <span class="string">&#x27;0&#x27;</span>::<span class="type">real</span>,</span><br><span class="line">	<span class="string">&#x27;avg_width&#x27;</span>, <span class="string">&#x27;8&#x27;</span>::<span class="type">integer</span>,</span><br><span class="line">	<span class="string">&#x27;n_distinct&#x27;</span>, <span class="string">&#x27;-1&#x27;</span>::<span class="type">real</span>,</span><br><span class="line">	<span class="string">&#x27;histogram_bounds&#x27;</span>, <span class="string">&#x27;&#123;56,10524,21232,30718,41153,50377,60264,70120,80019,89398,99574,109434,119523,129696,139684,149877,161659,171036,181245,191695,201052,211025,220695,231533,241040,251121,261035,272801,282385,291622,302144,311922,320997,331276,341649,352215,362449,372253,383367,393098,402659,412876,422222,431857,441934,452435,462050,471877,481111,491169,502147,512482,522629,533297,542697,552015,562928,573165,583118,592605,602734,612603,622786,632162,641168,651381,660752,671539,681549,691029,701074,711518,721946,732664,743058,752998,763483,773288,783463,793519,802897,812524,822721,832245,841333,851908,860870,870866,880738,889926,898894,909459,919903,929828,939844,949636,960208,970440,980545,989514,999967&#125;&#x27;</span>::text,</span><br><span class="line">	<span class="string">&#x27;correlation&#x27;</span>, <span class="string">&#x27;1&#x27;</span>::<span class="type">real</span></span><br><span class="line">);</span><br><span class="line">（略）</span><br></pre></td></tr></table></figure></div>

<p>テーブルレベル統計情報（pg_class）</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="center">列</th>
<th align="left">説明</th>
</tr>
</thead>
<tbody><tr>
<td align="center">relpages</td>
<td align="left">テーブルのディスク上のページ表現のサイズ</td>
</tr>
<tr>
<td align="center">reltuples</td>
<td align="left">テーブル内の有効な行数</td>
</tr>
<tr>
<td align="center">relallvisible</td>
<td align="left">テーブルの可視性マップですべて可視とマークされているページの数</td>
</tr>
<tr>
<td align="center">relallfrozen</td>
<td align="left">テーブルの可視性マップで「すべて凍結」とマークされているページの数</td>
</tr>
</tbody></table></div>
<p>参考：52.11.  pg_class</p>
<p>カラムレベル統計情報（pg_stats）</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="center">列</th>
<th align="left">説明</th>
</tr>
</thead>
<tbody><tr>
<td align="center">null_frac</td>
<td align="left">NULLの列エントリの割合</td>
</tr>
<tr>
<td align="center">avg_width</td>
<td align="left">列のエントリの平均幅（バイト単位）</td>
</tr>
<tr>
<td align="center">n_distinct</td>
<td align="left">列内の固有値の推定数</td>
</tr>
<tr>
<td align="center">histogram_bounds</td>
<td align="left">列の値をほぼ均等な母集団のグループに分割する値のリスト</td>
</tr>
<tr>
<td align="center">correlation</td>
<td align="left">物理的な行順序と列値の論理的な順序との間の統計的な相関関係</td>
</tr>
</tbody></table></div>
<p>参考：53.29.  pg_stats</p>
<p>データベースオブジェクト統計操作関数</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="center">関数名</th>
<th align="left">説明</th>
</tr>
</thead>
<tbody><tr>
<td align="center">pg_restore_relation_stats</td>
<td align="left">テーブルレベルの統計情報を更新</td>
</tr>
<tr>
<td align="center">pg_restore_attribute_stats</td>
<td align="left">列レベルの統計情報を作成または更新</td>
</tr>
</tbody></table></div>
<p>参考：9.28. System Administration Functions</p>
<h4 id="restore考察">restore考察</h4><p>リストアフローを分解し、各所の実行時間を洗い出す。※環境変数設定・スキーマ作成は含まない。</p>
<ul>
<li>テーブル・カラム統計情報更新、インデックス統計情報更新は純増している。</li>
<li>インデックス作成時にも実行時間の増加傾向が見受けられる。★今後の検証課題とする。</li>
</ul>
<div class="scroll"><table>
<thead>
<tr>
<th align="center">バージョン<br>パターン</th>
<th align="center">TABLE<br>作成<br>(ms)</th>
<th align="center">データ<br>作成<br>(ms)</th>
<th align="center">統計情報<br>更新<br>TABLE<br>(ms)</th>
<th align="center">INDEX<br>作成<br>(ms)</th>
<th align="center">統計情報<br>更新<br>INDEX<br>(ms)</th>
</tr>
</thead>
<tbody><tr>
<td align="center">PosgreSQL16<br>10テーブル</td>
<td align="center">82.982</td>
<td align="center">9950.638</td>
<td align="center">-</td>
<td align="center">9092.244</td>
<td align="center">-</td>
</tr>
<tr>
<td align="center">PosgreSQL16<br>50テーブル</td>
<td align="center">316.995</td>
<td align="center">58,569.127</td>
<td align="center">-</td>
<td align="center">54,412.186</td>
<td align="center">-</td>
</tr>
<tr>
<td align="center">PosgreSQL16<br>100テーブル</td>
<td align="center">746.560</td>
<td align="center">133,915.813</td>
<td align="center">-</td>
<td align="center">183,468.043</td>
<td align="center">-</td>
</tr>
<tr>
<td align="center">PosgreSQL16<br>10テーブル</td>
<td align="center">58.679</td>
<td align="center">8059.094</td>
<td align="center">527.813</td>
<td align="center">10846.512</td>
<td align="center">5.288</td>
</tr>
<tr>
<td align="center">PosgreSQL16<br>50テーブル</td>
<td align="center">286.436</td>
<td align="center">63196.114</td>
<td align="center">989.952</td>
<td align="center">131313.686</td>
<td align="center">39.404</td>
</tr>
<tr>
<td align="center">PosgreSQL16<br>100テーブル</td>
<td align="center">640.463</td>
<td align="center">134743.767</td>
<td align="center">1298.477</td>
<td align="center">204548.495</td>
<td align="center">63.719</td>
</tr>
</tbody></table></div>
<h3 id="検証①結論">検証①結論</h3><ul>
<li>pg_dumpに統計情報を含む形でも実行時間・バックアップファイルサイズが著しく増加することはないため、今後問題なく活用できると考える</li>
</ul>
<h2 id="検証②：オブジェクト・データ・統計情報をリストアすれば、バックアップ元の実行計画は再現される？">検証②：オブジェクト・データ・統計情報をリストアすれば、バックアップ元の実行計画は再現される？</h2><p>オブジェクト・データ・統計情報が再現されれば、理論上プランナは同じ実行計画を生成すると想定できる。<br>しかし、あくまで理論上であるため、検証します。</p>
<h3 id="検証条件-1">検証条件</h3><h4 id="シナリオ-1">シナリオ</h4><p>以下シナリオの②と⑤の実行計画を比較する。</p>
<p>①オブジェクト作成・データ作成・統計情報を取得<br>②実行計画を取得<br>③論理バックアップ（対象：スキーマ・オブジェクト・データ） を取得<br>④スキーマを削除・バックアップファイルをリストア<br>⑤実行計画を取得</p>
<h4 id="テスト用オブジェクト-1">テスト用オブジェクト</h4><p>バックアップ・リストア対象として用意するオブジェクトは以下とします。<br>なお、データは1テーブルあたり100万件とします。</p>
<p>テーブル（table01）</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">カラム名 (列名)</th>
<th align="left">データ型</th>
<th align="left">PK</th>
<th align="left">NULL許容</th>
<th align="left">カーディナリティ</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>id01</code></td>
<td align="left"><code>bigint</code></td>
<td align="left">〇</td>
<td align="left">NO</td>
<td align="left">1,000,000</td>
</tr>
<tr>
<td align="left"><code>id02</code></td>
<td align="left"><code>text</code></td>
<td align="left"></td>
<td align="left">NO</td>
<td align="left">1,000,000</td>
</tr>
<tr>
<td align="left"><code>id03</code></td>
<td align="left"><code>text</code></td>
<td align="left"></td>
<td align="left">NO</td>
<td align="left">1,000,000</td>
</tr>
<tr>
<td align="left"><code>id04</code></td>
<td align="left"><code>text</code></td>
<td align="left"></td>
<td align="left">NO</td>
<td align="left">1,000,000</td>
</tr>
<tr>
<td align="left"><code>id04</code></td>
<td align="left"><code>text</code></td>
<td align="left"></td>
<td align="left">NO</td>
<td align="left">1,000,000</td>
</tr>
</tbody></table></div>
<p>インデックス</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">インデックス名</th>
<th align="left">データ型</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>table01_idx1</code></td>
<td align="left"><code>id02</code></td>
</tr>
</tbody></table></div>
<p>クエリ<br>table01_idx1を利用するようなクエリとします。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> table01 <span class="keyword">where</span> id02 <span class="operator">=</span> <span class="string">&#x27;1&#x27;</span>;</span><br></pre></td></tr></table></figure>

<details><summary>検証手順</summary>

<ol>
<li>データベース作成</li>
</ol>
  <figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">create</span> database &#123;データベース名&#125;;</span><br></pre></td></tr></table></figure>

<ol start="2">
<li>スキーマ作成</li>
</ol>
  <figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">create</span> schema &#123;スキーマ名&#125;;</span><br></pre></td></tr></table></figure>

<ol start="3">
<li>テーブル・PK作成<br>  意図しないタイミングで統計情報が更新されるのを避けるため、テーブルレベルでautovacuumを無効化しています。</li>
</ol>
  <div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1klhblc-7" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-7" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">create table</span> &#123;テーブル名&#125; (id01 <span class="type">bigint</span> <span class="keyword">not null</span> , id02 text <span class="keyword">not null</span> , id03 text <span class="keyword">not null</span> , id04 text <span class="keyword">not null</span> , id05 text <span class="keyword">not null</span>) <span class="keyword">with</span>(autovacuum_enabled <span class="operator">=</span> <span class="literal">false</span>, toast.autovacuum_enabled <span class="operator">=</span> <span class="literal">false</span>);</span><br><span class="line"><span class="keyword">alter table</span> table01 <span class="keyword">add constraint</span> pk_table01 <span class="keyword">primary key</span> (id01);</span><br></pre></td></tr></table></figure></div>

<ol start="4">
<li>データ作成</li>
</ol>
  <div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1klhblc-8" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-8" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">insert into</span> table01 (<span class="keyword">select</span> generate_series(<span class="number">1</span>,<span class="number">1000000</span>),generate_series(<span class="number">1</span>,<span class="number">1000000</span>),generate_series(<span class="number">1</span>,<span class="number">1000000</span>),generate_series(<span class="number">1</span>,<span class="number">1000000</span>),generate_series(<span class="number">1</span>,<span class="number">1000000</span>));</span><br></pre></td></tr></table></figure></div>

<ol start="5">
<li>統計情報取得<br>  autovacuumを無効化しているため、バックアップ対象となる統計情報を取得させます。</li>
</ol>
  <figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">analyze &#123;対象オブジェクト全てを指定&#125;;</span><br></pre></td></tr></table></figure>

<ol start="6">
<li>実行計画取得</li>
</ol>
  <div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1klhblc-9" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-9" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">explain analyze <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> table01 <span class="keyword">where</span> id02 <span class="operator">=</span> <span class="string">&#x27;1&#x27;</span>;</span><br></pre></td></tr></table></figure></div>

<ol start="7">
<li>pg_dump<br>  windows上にインスタンスを立てているため、Powershellよりpg_dumpを実施しています。<br>  今回は論理バックアップの内容がわかるよう平文にてバックアップを取得します。</li>
</ol>
  <div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1klhblc-10" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-10" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">powershell -Command <span class="string">&quot;Measure-Command &#123; pg_dump -f &#123;バックアップファイル名&#125; -h &#123;ホスト名&#125; -p &#123;ポート&#125; -U &#123;ユーザ名&#125; -n &#123;スキーマ名&#125; -d &#123;データベース名&#125; -W --format=p -E &quot;</span>UTF8<span class="string">&quot; --verbose --statistics &#125;&quot;</span></span><br></pre></td></tr></table></figure></div>

<ol start="8">
<li>スキーマ削除</li>
</ol>
  <figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">drop</span> schema &#123;スキーマ名&#125; cascade;</span><br></pre></td></tr></table></figure>

<ol start="9">
<li>リストア<br>  windows上にインスタンスを立てているため、PowershellよりDBにログインし、リストアを実施しています。</li>
</ol>
  <div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1klhblc-11" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-11" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">psql -h &#123;ホスト名&#125; -p &#123;ポート&#125; -U &#123;ユーザ名&#125; -d &#123;データベース名&#125;</span><br><span class="line">\i &#123;バックアップファイル名&#125;;</span><br></pre></td></tr></table></figure></div>

<ol start="10">
<li>実行計画取得</li>
</ol>
  <div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1klhblc-12" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-12" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">explain analyze <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> table01 <span class="keyword">where</span> id02 <span class="operator">=</span> <span class="string">&#x27;1&#x27;</span>;</span><br></pre></td></tr></table></figure></div>

</details>

<h3 id="結果">結果</h3><p>実行計画は概ね同様であると考えます。</p>
<p>ただ、同一のオブジェクト・データ・統計情報であるため、再現したわけではなく、ただ同じ条件下で、同じ実行計画を生成したとも考えられる。<br>よって、バックアップ元の実行計画が再現されているとは言い切れないと判断する。</p>
<h4 id="②バックアップ元実行計画">②バックアップ元実行計画</h4><div class="code-block"><figure class="highlight txt"><input type="checkbox" id="code-wrap-1klhblc-13" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-13" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">Index Scan using table01_idx1 on table01  (cost=0.42..8.44 rows=1 width=32) (actual time=0.038..0.039 rows=1.00 loops=1)</span><br><span class="line">  Index Cond: (id02 = &#x27;1&#x27;::text)</span><br><span class="line">  Index Searches: 1</span><br><span class="line">  Buffers: shared hit=4</span><br><span class="line">Planning:</span><br><span class="line">  Buffers: shared hit=34</span><br><span class="line">Planning Time: 0.107 ms</span><br><span class="line">Execution Time: 0.060 ms</span><br></pre></td></tr></table></figure></div>

<h4 id="⑤リストア先実行計画">⑤リストア先実行計画</h4><div class="code-block"><figure class="highlight txt"><input type="checkbox" id="code-wrap-1klhblc-14" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-14" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">Index Scan using table01_idx1 on table01  (cost=0.42..8.44 rows=1 width=32) (actual time=0.080..0.081 rows=1.00 loops=1)</span><br><span class="line">  Index Cond: (id02 = &#x27;1&#x27;::text)</span><br><span class="line">  Index Searches: 1</span><br><span class="line">  Buffers: shared read=4</span><br><span class="line">Planning:</span><br><span class="line">  Buffers: shared hit=27 read=1</span><br><span class="line">Planning Time: 1.288 ms</span><br><span class="line">Execution Time: 0.109 ms</span><br></pre></td></tr></table></figure></div>

<h2 id="検証②追加検証：データ状況の異なるテーブルに統計情報のみリストアすれば、バックアップ元の実行計画は再現される？">検証②追加検証：データ状況の異なるテーブルに統計情報のみリストアすれば、バックアップ元の実行計画は再現される？</h2><p>バックアップ元のデータ状況とは異なるテーブルに統計情報のみをリストアした場合、理論上はバックアップ元の統計情報を基に実行計画を生成する。</p>
<h3 id="検証条件-2">検証条件</h3><h4 id="シナリオ-2">シナリオ</h4><p>以下シナリオの②と⑤と⑦の実行計画を比較する。</p>
<p>①オブジェクト作成・データ作成・統計情報を取得<br>②実行計画を取得<br>③論理バックアップ（対象：スキーマ・オブジェクト・データ） を取得<br>④スキーマを削除・バックアップファイルをリストア<br>⑤実行計画を取得<br>⑥統計情報を生成<br>⑦実行計画を取得</p>
<h4 id="テスト用オブジェクト-2">テスト用オブジェクト</h4><p>今回は統計情報のみバックアップ・リストアするため、バックアップ元・リストア先のテーブル・データそれぞれ用意します。</p>
<p>バックアップ元のテーブル（table01）<br>データ：2件</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">カラム名 (列名)</th>
<th align="left">データ型</th>
<th align="left">PK</th>
<th align="left">NULL許容</th>
<th align="left">カーディナリティ</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>id01</code></td>
<td align="left"><code>bigint</code></td>
<td align="left">〇</td>
<td align="left">NO</td>
<td align="left">2</td>
</tr>
<tr>
<td align="left"><code>id02</code></td>
<td align="left"><code>text</code></td>
<td align="left"></td>
<td align="left">NO</td>
<td align="left">2</td>
</tr>
<tr>
<td align="left"><code>id03</code></td>
<td align="left"><code>text</code></td>
<td align="left"></td>
<td align="left">NO</td>
<td align="left">2</td>
</tr>
<tr>
<td align="left"><code>id04</code></td>
<td align="left"><code>text</code></td>
<td align="left"></td>
<td align="left">NO</td>
<td align="left">2</td>
</tr>
<tr>
<td align="left"><code>id04</code></td>
<td align="left"><code>text</code></td>
<td align="left"></td>
<td align="left">NO</td>
<td align="left">2</td>
</tr>
</tbody></table></div>
<p>リストア元のテーブル（table01）<br>データ：100万件</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">カラム名 (列名)</th>
<th align="left">データ型</th>
<th align="left">PK</th>
<th align="left">NULL許容</th>
<th align="left">カーディナリティ</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>id01</code></td>
<td align="left"><code>bigint</code></td>
<td align="left">〇</td>
<td align="left">NO</td>
<td align="left">2</td>
</tr>
<tr>
<td align="left"><code>id02</code></td>
<td align="left"><code>text</code></td>
<td align="left"></td>
<td align="left">NO</td>
<td align="left">2 <strong>(内、999,999件の値が1）</strong></td>
</tr>
<tr>
<td align="left"><code>id03</code></td>
<td align="left"><code>text</code></td>
<td align="left"></td>
<td align="left">NO</td>
<td align="left">2</td>
</tr>
<tr>
<td align="left"><code>id04</code></td>
<td align="left"><code>text</code></td>
<td align="left"></td>
<td align="left">NO</td>
<td align="left">2</td>
</tr>
<tr>
<td align="left"><code>id04</code></td>
<td align="left"><code>text</code></td>
<td align="left"></td>
<td align="left">NO</td>
<td align="left">2</td>
</tr>
</tbody></table></div>
<p>インデックス</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">インデックス名</th>
<th align="left">データ型</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>table01_idx1</code></td>
<td align="left"><code>id02</code></td>
</tr>
</tbody></table></div>
<p>クエリ<br>table01_idx1を利用するようなクエリとします。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> table01 <span class="keyword">where</span> id02 <span class="operator">=</span> <span class="string">&#x27;1&#x27;</span>;</span><br></pre></td></tr></table></figure>

<details><summary>検証手順</summary>

<ol>
<li>データベース作成</li>
</ol>
  <figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">create</span> database &#123;データベース名&#125;;</span><br></pre></td></tr></table></figure>

<ol start="2">
<li>スキーマ作成</li>
</ol>
  <figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">create</span> schema &#123;スキーマ名&#125;;</span><br></pre></td></tr></table></figure>

<ol start="3">
<li>テーブル・PK作成<br>  意図しないタイミングで統計情報が更新されるのを避けるため、テーブルレベルでautovacuumを無効化しています。</li>
</ol>
  <div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1klhblc-15" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-15" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">create table</span> &#123;テーブル名&#125; (id01 <span class="type">bigint</span> <span class="keyword">not null</span> , id02 text <span class="keyword">not null</span> , id03 text <span class="keyword">not null</span> , id04 text <span class="keyword">not null</span> , id05 text <span class="keyword">not null</span>) <span class="keyword">with</span>(autovacuum_enabled <span class="operator">=</span> <span class="literal">false</span>, toast.autovacuum_enabled <span class="operator">=</span> <span class="literal">false</span>);</span><br><span class="line"><span class="keyword">alter table</span> table01 <span class="keyword">add constraint</span> pk_table01 <span class="keyword">primary key</span> (id01);</span><br></pre></td></tr></table></figure></div>

<ol start="4">
<li>データ作成</li>
</ol>
  <div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1klhblc-16" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-16" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">insert into</span> table01 (<span class="keyword">select</span> generate_series(<span class="number">1</span>,<span class="number">1000000</span>),generate_series(<span class="number">1</span>,<span class="number">1000000</span>),generate_series(<span class="number">1</span>,<span class="number">1000000</span>),generate_series(<span class="number">1</span>,<span class="number">1000000</span>),generate_series(<span class="number">1</span>,<span class="number">1000000</span>));</span><br></pre></td></tr></table></figure></div>

<ol start="5">
<li>統計情報取得<br>  autovacuumを無効化しているため、バックアップ対象となる統計情報を取得させます。</li>
</ol>
  <figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">analyze &#123;対象オブジェクト全てを指定&#125;;</span><br></pre></td></tr></table></figure>

<ol start="6">
<li>実行計画取得</li>
</ol>
  <div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1klhblc-17" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-17" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">explain analyze <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> table01 <span class="keyword">where</span> id02 <span class="operator">=</span> <span class="string">&#x27;1&#x27;</span>;</span><br></pre></td></tr></table></figure></div>

<ol start="7">
<li>pg_dump<br>  windows上にインスタンスを立てているため、Powershellよりpg_dumpを実施しています。<br>  今回は論理バックアップの内容がわかるよう平文にてバックアップを取得します。</li>
</ol>
  <div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1klhblc-18" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-18" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">powershell -Command <span class="string">&quot;Measure-Command &#123; pg_dump -f &#123;バックアップファイル名&#125; -h &#123;ホスト名&#125; -p &#123;ポート&#125; -U &#123;ユーザ名&#125; -n &#123;スキーマ名&#125; -d &#123;データベース名&#125; -W --format=p -E &quot;</span>UTF8<span class="string">&quot; --verbose --statistics-only &#125;&quot;</span></span><br></pre></td></tr></table></figure></div>

<ol start="8">
<li>スキーマ削除</li>
</ol>
  <figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">drop</span> schema &#123;スキーマ名&#125; cascade;</span><br></pre></td></tr></table></figure>

<ol start="9">
<li>リストア<br>  windows上にインスタンスを立てているため、PowershellよりDBにログインし、リストアを実施しています。</li>
</ol>
  <div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1klhblc-19" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-19" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">psql -h &#123;ホスト名&#125; -p &#123;ポート&#125; -U &#123;ユーザ名&#125; -d &#123;データベース名&#125;</span><br><span class="line">\i &#123;バックアップファイル名&#125;;</span><br></pre></td></tr></table></figure></div>

<ol start="10">
<li>実行計画取得</li>
</ol>
  <div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1klhblc-20" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-20" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">explain analyze <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> table01 <span class="keyword">where</span> id02 <span class="operator">=</span> <span class="string">&#x27;1&#x27;</span>;</span><br></pre></td></tr></table></figure></div>

<ol start="11">
<li>統計情報取得</li>
</ol>
  <figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">analyze &#123;対象オブジェクト全てを指定&#125;;</span><br></pre></td></tr></table></figure>

<ol start="12">
<li>実行計画取得</li>
</ol>
  <div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1klhblc-21" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-21" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">explain analyze <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> table01 <span class="keyword">where</span> id02 <span class="operator">=</span> <span class="string">&#x27;1&#x27;</span>;</span><br></pre></td></tr></table></figure></div>

</details>

<h3 id="結果-1">結果</h3><ul>
<li>⑤リストア先実行計画（統計情報取得前）を見る限り、プランナは取得されるのは1件であると想定している。しかし、それに対して実際は999,999件取得できているため、indexscanに切り替えて実行している。<br>想定通り、バックアップ元の統計情報に基づいて、実行計画を生成するしている様子がうかがえる。</li>
</ul>
<h4 id="②バックアップ元実行計画-1">②バックアップ元実行計画</h4><div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1klhblc-22" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-22" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">Seq Scan <span class="keyword">on</span> table01  (cost<span class="operator">=</span><span class="number">0.00</span>.<span class="number">.1</span><span class="number">.02</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">1</span> width<span class="operator">=</span><span class="number">16</span>) (actual <span class="type">time</span><span class="operator">=</span><span class="number">0.022</span>.<span class="number">.0</span><span class="number">.023</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">1.00</span> loops<span class="operator">=</span><span class="number">1</span>)</span><br><span class="line">  <span class="keyword">Filter</span>: (id02 <span class="operator">=</span> <span class="string">&#x27;1&#x27;</span>::text)</span><br><span class="line">  <span class="keyword">Rows</span> Removed <span class="keyword">by</span> <span class="keyword">Filter</span>: <span class="number">1</span></span><br><span class="line">  Buffers: shared hit<span class="operator">=</span><span class="number">1</span></span><br><span class="line">Planning:</span><br><span class="line">  Buffers: shared hit<span class="operator">=</span><span class="number">27</span> read<span class="operator">=</span><span class="number">1</span></span><br><span class="line">Planning <span class="type">Time</span>: <span class="number">1.491</span> ms</span><br><span class="line">Execution <span class="type">Time</span>: <span class="number">0.039</span> ms</span><br></pre></td></tr></table></figure></div>

<h4 id="⑤リストア先実行計画（統計情報取得前）">⑤リストア先実行計画（統計情報取得前）</h4><div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1klhblc-23" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-23" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">Index Scan <span class="keyword">using</span> table01_idx1 <span class="keyword">on</span> table01  (cost<span class="operator">=</span><span class="number">0.41</span>.<span class="number">.8</span><span class="number">.43</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">1</span> width<span class="operator">=</span><span class="number">16</span>) (actual <span class="type">time</span><span class="operator">=</span><span class="number">0.678</span>.<span class="number">.181</span><span class="number">.715</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">999999.00</span> loops<span class="operator">=</span><span class="number">1</span>)</span><br><span class="line">  Index Cond: (id02 <span class="operator">=</span> <span class="string">&#x27;1&#x27;</span>::text)</span><br><span class="line">  Index Searches: <span class="number">1</span></span><br><span class="line">  Buffers: shared hit<span class="operator">=</span><span class="number">7352</span> read<span class="operator">=</span><span class="number">844</span></span><br><span class="line">Planning:</span><br><span class="line">  Buffers: shared hit<span class="operator">=</span><span class="number">36</span> read<span class="operator">=</span><span class="number">1</span></span><br><span class="line">Planning <span class="type">Time</span>: <span class="number">0.794</span> ms</span><br><span class="line">Execution <span class="type">Time</span>: <span class="number">215.249</span> ms</span><br></pre></td></tr></table></figure></div>

<h4 id="⑦リストア先実行計画（統計情報取得後）">⑦リストア先実行計画（統計情報取得後）</h4><div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1klhblc-24" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1klhblc-24" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">Seq Scan <span class="keyword">on</span> table01  (cost<span class="operator">=</span><span class="number">0.00</span>.<span class="number">.19852</span><span class="number">.00</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">1000000</span> width<span class="operator">=</span><span class="number">28</span>) (actual <span class="type">time</span><span class="operator">=</span><span class="number">0.019</span>.<span class="number">.93</span><span class="number">.264</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">999999.00</span> loops<span class="operator">=</span><span class="number">1</span>)</span><br><span class="line">  <span class="keyword">Filter</span>: (id02 <span class="operator">=</span> <span class="string">&#x27;1&#x27;</span>::text)</span><br><span class="line">  <span class="keyword">Rows</span> Removed <span class="keyword">by</span> <span class="keyword">Filter</span>: <span class="number">1</span></span><br><span class="line">  Buffers: shared hit<span class="operator">=</span><span class="number">7352</span></span><br><span class="line">Planning:</span><br><span class="line">  Buffers: shared hit<span class="operator">=</span><span class="number">34</span> dirtied<span class="operator">=</span><span class="number">2</span></span><br><span class="line">Planning <span class="type">Time</span>: <span class="number">1.653</span> ms</span><br><span class="line">Execution <span class="type">Time</span>: <span class="number">125.505</span> ms</span><br></pre></td></tr></table></figure></div>

<h3 id="検証②結論">検証②結論</h3><ul>
<li>追加検証を見ても、バックアップ元の統計情報がリストア先に反映されていると判断できる</li>
</ul>
<h4 id="結論">結論</h4><ul>
<li>pg_dumpに統計情報を含むことによる劣化はなく、問題なく活用できる</li>
<li>統計情報のバックアップ・リストア自体も問題なく機能している</li>
<li>あくまで論理バックアップであり、デッドタプル等の物理的な部分まで再現できるわけではないため、リストア先で問題がなかったからバックアップ元でも問題ないと言い切るべきではない</li>
<li>ただし、従来のpg_dumpによるバックアップ・リストアと比較して、より高い精度でバックアップ元環境を再現できるようになっているため、<strong>”検証”</strong> を行う上ではより有用性の高い機能になっていると考える</li>
</ul>
]]></content>
    <summary type="html">この度、PosgreSQLメジャーバージョンアップに伴い、pg_dumpに統計情報のバックアップ・リストアが追加されました。PostgreSQLのpg_dumpは、データ削除前のバックアップや他環境へのデータ移行などで広く利用されている機能だと思います。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="PostgreSQL" scheme="https://future-architect.github.io/tags/PostgreSQL/"/>
    <category term="PostgreSQL18" scheme="https://future-architect.github.io/tags/PostgreSQL18/"/>
  </entry>
  <entry>
    <title>PostgreSQL18でのEXPLAINの更新を見る、あわせてEXPLAINを振り返る</title>
    <link href="https://future-architect.github.io/articles/20251008a/"/>
    <id>https://future-architect.github.io/articles/20251008a/</id>
    <published>2025-10-07T15:00:00.000Z</published>
    <updated>2025-10-07T15:00:00.000Z</updated>
    <author><name>山本竜玄</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20251008a/top.jpg" alt="" width="800" height="664">

<p>本記事は、PostgreSQL18連載の2本目の記事です。</p>
<h2 id="はじめに">はじめに</h2><p>こんにちは。山本竜玄です。</p>
<p>データベースのパフォーマンス問題に直面したとき、クエリが遅い原因の特定は難しい課題です。テーブルスキャンの方法、インデックスの利用状況、結合処理の手法など、データベース内部の動作を理解する必要があります。</p>
<p>PostgreSQLのEXPLAINは、こうした内部動作を可視化する強力なツールです。2025年9月にリリースされたPostgreSQL 18では、バッファ使用量の自動表示など、パフォーマンス分析をより手軽にする改善が加えられました。</p>
<p>この記事では、PostgreSQL 18の新機能と、あわせて基礎的な使い方まで記載します。既にEXPLAINを使ったことがある方は、「PostgreSQL 18でのEXPLAIN機能強化」セクション以外は適宜スキップください。</p>
<h2 id="PostgreSQL-18でのEXPLAIN機能強化">PostgreSQL 18でのEXPLAIN機能強化</h2><p>PostgreSQL 18では、EXPLAIN ANALYZEがより実用的になる改善が行われています。公式リリースノートによると、いくつかの重要な機能追加がありました。</p>
<h3 id="1-バッファ使用量のデフォルト表示">1. バッファ使用量のデフォルト表示</h3><p>最も大きな変更点として、PostgreSQL 18からEXPLAIN ANALYZEを実行すると、バッファ使用量が自動的に表示されるようになりました。これまではBUFFERSオプションを明示的に指定する必要がありましたが、18からは標準で出力されます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> large_table <span class="keyword">WHERE</span> id <span class="operator">&gt;</span> <span class="number">1000</span>;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight plaintext"><input type="checkbox" id="code-wrap-1a9w1s3-2" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                   QUERY PLAN</span><br><span class="line">-----------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Seq Scan on large_table  (cost=0.00..189.00 rows=9000 width=18) (actual time=0.052..0.797 rows=9000.00 loops=1)</span><br><span class="line">   Filter: (id &gt; 1000)</span><br><span class="line">   Rows Removed by Filter: 1000</span><br><span class="line">   Buffers: shared hit=64</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=20</span><br><span class="line"> Planning Time: 0.129 ms</span><br><span class="line"> Execution Time: 1.116 ms</span><br><span class="line">(8 rows)</span><br></pre></td></tr></table></figure></div>

<p>このクエリを実行すると、<code>Buffers: shared hit=64</code>のように、ヒット数、読み込み数、ダーティ化したブロック数などが自動的に表示されます。I&#x2F;O活動を把握する上で非常に重要な情報なので、デフォルトになったことで使いやすさが向上しています。</p>
<h3 id="2-インデックスルックアップ回数の表示">2. インデックスルックアップ回数の表示</h3><p>PostgreSQL 18から、EXPLAIN ANALYZEで各インデックススキャンノードが実行したルックアップの回数が表示されるようになりました。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> orders <span class="keyword">WHERE</span> customer_id <span class="operator">=</span> <span class="number">42</span>;</span><br></pre></td></tr></table></figure>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-3" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                            QUERY PLAN</span><br><span class="line">-----------------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Bitmap Heap Scan on orders  (cost=5.28..70.90 rows=129 width=18) (actual time=0.018..0.071 rows=129.00 loops=1)</span><br><span class="line">   Recheck Cond: (customer_id = 42)</span><br><span class="line">   Heap Blocks: exact=56</span><br><span class="line">   Buffers: shared hit=58</span><br><span class="line">   -&gt;  Bitmap Index Scan on orders_customer_idx  (cost=0.00..5.25 rows=129 width=0) (actual time=0.008..0.008 rows=129.00 loops=1)</span><br><span class="line">         Index Cond: (customer_id = 42)</span><br><span class="line">         Index Searches: 1</span><br><span class="line">         Buffers: shared hit=2</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=36</span><br><span class="line"> Planning Time: 0.128 ms</span><br><span class="line"> Execution Time: 0.087 ms</span><br><span class="line">(12 rows)</span><br></pre></td></tr></table></figure></div>

<p>実行結果には <code>Index Searches: 1</code> のような行が追加され、インデックスを何回検索したかが明確になります。特にスキップスキャンが使われる場合、複数回の検索が行われることがあるので、この情報でインデックスの動作をより詳細に理解できます。</p>
<h3 id="3-WAL-buffers-fullの追加">3. WAL buffers fullの追加</h3><p>PostgreSQL 18から、WAL統計に新しい情報が追加されました。WALバッファがフルになった回数（buffers full）が表示されるようになり、WALバッファのサイズ調整が必要かどうかを判断できます。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">EXPLAIN (ANALYZE, WAL, BUFFERS)</span><br><span class="line"><span class="keyword">UPDATE</span> wal_test <span class="keyword">SET</span> data <span class="operator">=</span> md5(data);</span><br></pre></td></tr></table></figure>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-4" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"> Update on wal_test  (cost=0.00..217.35 rows=0 width=0) (actual time=48.481..48.482 rows=0.00 loops=1)</span><br><span class="line">   Buffers: shared hit=60412 dirtied=110 written=117</span><br><span class="line">   WAL: records=30135 bytes=2413984 buffers full=268</span><br><span class="line">   -&gt;  Seq Scan on wal_test  (cost=0.00..217.35 rows=10668 width=38) (actual time=0.025..12.371 rows=10000.00 loops=1)</span><br><span class="line">         Buffers: shared hit=84</span><br><span class="line"> Execution Time: 48.626 ms</span><br><span class="line">(6 rows)</span><br></pre></td></tr></table></figure></div>

<p><code>buffers full=268</code>は、WALバッファがフルになり、ディスクへの書き込みが発生した回数を示します。この値が大きい場合、wal_buffersパラメータを増やすことで性能が改善する可能性があります。</p>
<p>I&#x2F;O統計やその他のWAL統計の詳細については、BUFFERSオプションおよびWALオプションセクションで解説します。</p>
<h3 id="4-その他のEXPLAIN機能強化">4. その他のEXPLAIN機能強化</h3><p>PostgreSQL 18では、上記の主要な機能に加えて、特定のノードでの詳細情報も強化されています。</p>
<h4 id="小数点での行数表示">小数点での行数表示</h4><p>PostgreSQL 18から、1行未満の推定値も小数点で正確に表現されるようになりました。</p>
<p>例えば、Nested Loop Joinの内側でフィルタ条件により平均0.15行が返される場合、これまでは<code>rows=0</code>または<code>rows=1</code>と丸められていましたが、PostgreSQL 18では<code>rows=0.15</code>と正確に表示されます。</p>
<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-5" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">-&gt;  Index Scan using customers_pkey on customers c  (cost=0.27..0.35 rows=1 width=16) (actual time=0.001..0.001 rows=0.15 loops=438)</span><br><span class="line">      Index Cond: (id = o.customer_id)</span><br><span class="line">      Filter: (city = &#x27;Tokyo&#x27;::text)</span><br><span class="line">      Rows Removed by Filter: 1</span><br></pre></td></tr></table></figure></div>

<p>これにより、確率の低いフィルタ条件での推定精度が向上し、プランナーがより正確なコスト計算を行えるようになりました。</p>
<h4 id="ウィンドウ関数の引数詳細">ウィンドウ関数の引数詳細</h4><p>ウィンドウ関数を使用する際に、より詳細な情報が表示されるようになりました。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-6" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-6" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN (ANALYZE, VERBOSE, BUFFERS)</span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">    category,</span><br><span class="line">    product_name,</span><br><span class="line">    amount,</span><br><span class="line">    <span class="built_in">ROW_NUMBER</span>() <span class="keyword">OVER</span> (<span class="keyword">PARTITION</span> <span class="keyword">BY</span> category <span class="keyword">ORDER</span> <span class="keyword">BY</span> amount <span class="keyword">DESC</span>) <span class="keyword">as</span> rank,</span><br><span class="line">    <span class="built_in">AVG</span>(amount) <span class="keyword">OVER</span> (<span class="keyword">PARTITION</span> <span class="keyword">BY</span> category) <span class="keyword">as</span> avg_amount</span><br><span class="line"><span class="keyword">FROM</span> sales</span><br><span class="line"><span class="keyword">ORDER</span> <span class="keyword">BY</span> category, rank;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-7" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-7" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                               QUERY PLAN</span><br><span class="line">-----------------------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Incremental Sort  (cost=102.22..158.29 rows=1000 width=64) (actual time=1.589..2.132 rows=1000.00 loops=1)</span><br><span class="line">   Output: category, product_name, amount, (row_number() OVER w1), (avg(amount) OVER w2)</span><br><span class="line">   Sort Key: sales.category, (row_number() OVER w1)</span><br><span class="line">   Presorted Key: sales.category</span><br><span class="line">   Full-sort Groups: 3  Sort Method: quicksort  Average Memory: 29kB  Peak Memory: 29kB</span><br><span class="line">   Pre-sorted Groups: 3  Sort Method: quicksort  Average Memory: 45kB  Peak Memory: 45kB</span><br><span class="line">   Buffers: shared hit=12</span><br><span class="line">   -&gt;  WindowAgg  (cost=80.46..103.83 rows=1000 width=64) (actual time=1.199..1.821 rows=1000.00 loops=1)</span><br><span class="line">         Output: category, product_name, amount, (row_number() OVER w1), avg(amount) OVER w2</span><br><span class="line">         Window: w2 AS (PARTITION BY sales.category)</span><br><span class="line">         Storage: Memory  Maximum Storage: 37kB</span><br><span class="line">         Buffers: shared hit=9</span><br><span class="line">         -&gt;  WindowAgg  (cost=68.85..88.83 rows=1000 width=32) (actual time=0.958..1.313 rows=1000.00 loops=1)</span><br><span class="line">               Output: category, amount, product_name, row_number() OVER w1</span><br><span class="line">               Window: w1 AS (PARTITION BY sales.category ORDER BY sales.amount ROWS UNBOUNDED PRECEDING)</span><br><span class="line">               Storage: Memory  Maximum Storage: 17kB</span><br><span class="line">               Buffers: shared hit=9</span><br><span class="line">               -&gt;  Sort  (cost=68.83..71.33 rows=1000 width=24) (actual time=0.949..0.991 rows=1000.00 loops=1)</span><br><span class="line">                     Output: category, amount, product_name</span><br><span class="line">                     Sort Key: sales.category, sales.amount DESC</span><br><span class="line">                     Sort Method: quicksort  Memory: 69kB</span><br><span class="line">                     Buffers: shared hit=9</span><br><span class="line">                     -&gt;  Seq Scan on public.sales  (cost=0.00..19.00 rows=1000 width=24) (actual time=0.007..0.095 rows=1000.00 loops=1)</span><br><span class="line">                           Output: category, amount, product_name</span><br><span class="line">                           Buffers: shared hit=9</span><br><span class="line"> Execution Time: 2.192 ms</span><br><span class="line">(26 rows)</span><br></pre></td></tr></table></figure></div>

<p>ウィンドウ関数の定義が<code>Window: w1 AS (PARTITION BY sales.category ORDER BY sales.amount ROWS UNBOUNDED PRECEDING)</code>のように詳細に表示されます。また、<code>Storage: Memory  Maximum Storage: 17kB</code>でメモリ使用量も確認できます。</p>
<p>これにより、複雑なウィンドウ関数を使用する場合でも、内部でどのように処理されているかが明確になりました。</p>
<h4 id="メモリ-ディスク使用量の詳細表示">メモリ&#x2F;ディスク使用量の詳細表示</h4><p>Window Aggregateノード、Materialノード、CTEノードで、メモリとディスク使用量の詳細が表示されるようになりました。上記のWindow Aggregateの例では、<code>Storage: Memory  Maximum Storage: 17kB</code>のように、メモリ内で処理が完結していることが分かります。</p>
<p>データ量が大きくメモリに収まらない場合、<code>Storage: Disk</code>のように表示され、ディスクにスピルしたことが明示されます。これにより、work_memの調整が必要かどうかを判断できます。</p>
<h2 id="EXPLAINの基礎知識">EXPLAINの基礎知識</h2><p>PostgreSQL 18での新機能を理解したところで、EXPLAINの基本から確認していきます。既に使ったことがある方も、改めて基礎を押さえることで、より深い理解につながります。</p>
<h3 id="1-EXPLAINとは">1. EXPLAINとは</h3><p>EXPLAINは、PostgreSQLのクエリプランナーが生成する実行計画を表示するコマンドです。クエリの前にEXPLAINを付けるだけで、データベースがどのようにクエリを実行するつもりなのかが分かります。</p>
<p>実行計画は、テーブルのスキャン方法、インデックスの使用有無、複数テーブルの結合方法など、クエリ実行の戦略を示します。この情報を読み解くことで、パフォーマンスの問題を特定し、改善策を考えられます。</p>
<h3 id="2-基本的な構文">2. 基本的な構文</h3><p>最もシンプルな使い方は、SELECT文の前にEXPLAINを付けるだけです。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">EXPLAIN <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> users <span class="keyword">WHERE</span> age <span class="operator">&gt;</span> <span class="number">30</span>;</span><br></pre></td></tr></table></figure>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-8" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-8" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                        QUERY PLAN</span><br><span class="line">-----------------------------------------------------------</span><br><span class="line"> Seq Scan on users  (cost=0.00..199.00 rows=8247 width=23)</span><br><span class="line">   Filter: (age &gt; 30)</span><br><span class="line">(2 rows)</span><br></pre></td></tr></table></figure></div>

<p>これだけで実行計画が表示されます。実際にクエリは実行されず、プランナーが予測した計画だけが返されます。</p>
<p>実際にクエリを実行して、実測値を含めた情報を得たい場合は、ANALYZEオプションを追加します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-9" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-9" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> users <span class="keyword">WHERE</span> age <span class="operator">&gt;</span> <span class="number">30</span>;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-10" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-10" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                QUERY PLAN</span><br><span class="line">-----------------------------------------------------------------------------------------------------------</span><br><span class="line"> Seq Scan on users  (cost=0.00..199.00 rows=8247 width=23) (actual time=0.006..1.146 rows=8247.00 loops=1)</span><br><span class="line">   Filter: (age &gt; 30)</span><br><span class="line">   Rows Removed by Filter: 1753</span><br><span class="line">   Buffers: shared hit=74</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=63</span><br><span class="line"> Planning Time: 0.438 ms</span><br><span class="line"> Execution Time: 1.573 ms</span><br><span class="line">(8 rows)</span><br></pre></td></tr></table></figure></div>

<p>ANALYZEを使うと、推定値だけでなく実際の実行時間や処理行数が表示されるので、推定と実測のギャップを確認できます。PostgreSQL 18では、バッファ情報（<code>Buffers: shared hit=74</code>）も自動的に表示されます。</p>
<h3 id="3-実行計画の読み方">3. 実行計画の読み方</h3><p>EXPLAINの出力は、慣れるまで複雑に見えます。基本的な見方を押さえておきましょう。</p>
<p>実行計画はツリー構造で表現されます。一番下にあるのがスキャンノードで、テーブルから実際にデータを読み取る部分です。その上に、結合やソート、集約などの処理ノードが積み重なっていきます。</p>
<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-11" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-11" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">QUERY PLAN</span><br><span class="line">---------------------------------------------------------</span><br><span class="line">Seq Scan on users  (cost=0.00..155.00 rows=10000 width=64)</span><br><span class="line">  Filter: (age &gt; 30)</span><br></pre></td></tr></table></figure></div>

<p>この例では、usersテーブルに対してシーケンシャルスキャン（全行スキャン）が行われ、age &gt; 30のフィルタ条件が適用されています。</p>
<h4 id="コスト（cost）の意味">コスト（cost）の意味</h4><p><code>cost=0.00..155.00</code> という部分は、そのノードの実行コストの推定値です。左側の数値が起動コスト、右側が総コストを表します。</p>
<ul>
<li>起動コスト: 最初の行を返すまでにかかるコストです。ソート処理では、すべてのデータを読み込んでソートしてから最初の行を返すため、起動コストが高くなります</li>
<li>総コスト: そのノードがすべての処理を完了するまでのコストです。通常はこちらの値が重要になります</li>
</ul>
<p>コストの単位は、ディスクページの読み取りを基準とした抽象的な値です。設定パラメータ seq_page_cost のデフォルト値が1.0で、他のコストパラメータはこれを基準に設定されています。</p>
<h4 id="行数推定（rows）">行数推定（rows）</h4><p><code>rows=10000</code> は、そのノードが出力すると推定される行数です。プランナーがテーブルの統計情報を基に計算した値で、実際の行数とは異なることがあります。</p>
<p>この推定値が大きくずれていると、プランナーが最適でない実行計画を選択する原因になります。そのため、定期的にANALYZEコマンドで統計情報を更新することが重要です。</p>
<h4 id="処理幅（width）">処理幅（width）</h4><p><code>width=64</code> は、1行あたりの平均バイト数の推定値です。メモリ使用量の計算などに使われます。</p>
<h3 id="ノードの種類と役割">ノードの種類と役割</h3><p>実行計画には様々な種類のノードが登場します。主要なものを理解しておくと、計画が読みやすくなります。</p>
<p>スキャンノードは、データを取得する部分です。Seq Scan（シーケンシャルスキャン）、Index Scan（インデックススキャン）、Bitmap Scan（ビットマップスキャン）などがあります。</p>
<ul>
<li>結合ノード: 複数のテーブルを結合する処理を表します。Nested Loop、Hash Join、Merge Joinの3種類が主に使われます</li>
<li>集約ノード: GROUP BYやSUM、COUNTなどの集約処理を行います。HashAggregateやGroupAggregateといった種類があります。</li>
<li>ソートノード: ORDER BYなどでデータをソートする処理です。メモリ内で完結する場合と、ディスクを使う場合で性能が大きく変わります。</li>
</ul>
<p>これらの基本を押さえておくと、実行計画の全体像が見えてきます。</p>
<h2 id="EXPLAINのオプション徹底解説">EXPLAINのオプション徹底解説</h2><p>EXPLAINには多くのオプションが用意されていて、必要な情報に応じて使い分けられます。各オプションの詳細を見ていきましょう。</p>
<h3 id="ANALYZE">ANALYZE</h3><p>ANALYZEオプションは、クエリを実際に実行し、実測値を含めた詳細な統計情報を表示します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-12" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-12" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> orders <span class="keyword">WHERE</span> order_date <span class="operator">=</span> <span class="string">&#x27;2024-01-15&#x27;</span>;</span><br></pre></td></tr></table></figure></div>

<p>実行結果には、推定値に加えて実際の実行時間（actual time）と実際の行数（actual rows）が表示されます。</p>
<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-13" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-13" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">Index Scan using orders_date_idx on orders  (cost=0.14..8.16 rows=1 width=26) (actual time=0.014..0.040 rows=150.00 loops=1)</span><br><span class="line">   Index Cond: (order_date = &#x27;2024-01-15&#x27;::date)</span><br><span class="line">   Index Searches: 1</span><br><span class="line">   Buffers: shared hit=3</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=91</span><br><span class="line"> Planning Time: 0.462 ms</span><br><span class="line"> Execution Time: 0.098 ms</span><br><span class="line">(8 rows)</span><br></pre></td></tr></table></figure></div>

<p><code>actual time=0.014..0.040</code> は、実際にかかった時間をミリ秒で表しています。左側が最初の行を返すまでの時間、右側が全行を返すまでの時間です。</p>
<p><code>rows=150.00</code> は実際に返された行数で、推定の <code>rows=1</code> と大きく異なる場合、統計情報の更新が必要です。</p>
<p><code>loops=1</code> は、このノードが実行された回数です。ネストしたループの内側にあるノードは、複数回実行されることがあります。</p>
<p>注意点として、ANALYZEを使うとクエリが実際に実行されるため、INSERT、UPDATE、DELETEなどのデータ更新クエリでは、データが変更されてしまいます。そのような場合は、トランザクション内で実行してROLLBACKすることで、変更を元に戻せます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-14" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-14" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">BEGIN</span>;</span><br><span class="line">EXPLAIN ANALYZE <span class="keyword">UPDATE</span> products <span class="keyword">SET</span> stock <span class="operator">=</span> stock <span class="operator">-</span> <span class="number">1</span> <span class="keyword">WHERE</span> product_id <span class="operator">=</span> <span class="number">123</span>;</span><br><span class="line"><span class="keyword">ROLLBACK</span>;</span><br></pre></td></tr></table></figure></div>

<h3 id="VERBOSE">VERBOSE</h3><p>VERBOSEオプションは、実行計画の追加情報を表示します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-15" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-15" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN VERBOSE <span class="keyword">SELECT</span> name, age <span class="keyword">FROM</span> users <span class="keyword">WHERE</span> city <span class="operator">=</span> <span class="string">&#x27;Tokyo&#x27;</span>;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-16" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-16" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                            QUERY PLAN</span><br><span class="line">------------------------------------------------------------------</span><br><span class="line"> Seq Scan on public.users  (cost=0.00..199.00 rows=1697 width=13)</span><br><span class="line">   Output: name, age</span><br><span class="line">   Filter: (users.city = &#x27;Tokyo&#x27;::text)</span><br><span class="line">(3 rows)</span><br></pre></td></tr></table></figure></div>

<p>通常のEXPLAINでは省略される情報が含まれます。各ノードの出力カラムリスト、テーブル名やカラム名のスキーマ修飾、式中の変数のテーブルエイリアスなどが表示されます。</p>
<p>実行計画の内部動作をより詳しく理解したい場合や、複雑なクエリのデバッグに役立ちます。</p>
<h3 id="BUFFERS">BUFFERS</h3><p>BUFFERSオプションは、バッファの使用状況を表示します。PostgreSQL 18では、EXPLAIN ANALYZEを使うと自動的にバッファ情報が含まれるようになりましたが、明示的な指定もできます。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">EXPLAIN (ANALYZE, BUFFERS)</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> test_data <span class="keyword">WHERE</span> <span class="keyword">value</span> <span class="operator">&gt;</span> <span class="number">500</span>;</span><br></pre></td></tr></table></figure>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-17" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-17" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                  QUERY PLAN</span><br><span class="line">---------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Seq Scan on test_data  (cost=0.00..189.00 rows=5021 width=17) (actual time=0.122..2.451 rows=5013.00 loops=1)</span><br><span class="line">   Filter: (value &gt; 500)</span><br><span class="line">   Rows Removed by Filter: 4987</span><br><span class="line">   Buffers: shared read=64</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=47 read=18 dirtied=4</span><br><span class="line"> Planning Time: 0.491 ms</span><br><span class="line"> Execution Time: 2.651 ms</span><br><span class="line">(8 rows)</span><br></pre></td></tr></table></figure></div>

<p>出力には、shared blocksとlocal blocks、temp blocksの統計が含まれます。</p>
<ul>
<li>shared blocks: 通常のテーブルやインデックスのデータです。<code>shared hit</code> はキャッシュから読み取った回数、<code>shared read</code> はディスクから読み取った回数を示します</li>
<li>local blocks: 一時テーブルなど、セッション固有のデータを表します</li>
<li>temp blocks: ソートやハッシュなどで使われる短期間のワーキングデータです</li>
</ul>
<p><code>shared hit</code> の割合が高いほど、データがキャッシュに載っていて高速に処理できています。<code>shared read</code> が多い場合、I&#x2F;Oがボトルネックになっています。</p>
<p>track_io_timingパラメータを有効にすると、I&#x2F;Oにかかった時間（ミリ秒）も表示されます。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">SET</span> track_io_timing <span class="operator">=</span> <span class="keyword">on</span>;</span><br><span class="line"></span><br><span class="line">EXPLAIN (ANALYZE, BUFFERS)</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> large_table <span class="keyword">WHERE</span> id <span class="operator">&lt;</span> <span class="number">1000</span>;</span><br></pre></td></tr></table></figure>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-18" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-18" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                              QUERY PLAN</span><br><span class="line">---------------------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Index Scan using large_table_pkey on large_table  (cost=0.29..38.07 rows=959 width=14) (actual time=0.022..0.183 rows=999.00 loops=1)</span><br><span class="line">   Index Cond: (id &lt; 1000)</span><br><span class="line">   Index Searches: 1</span><br><span class="line">   Buffers: shared hit=2 read=8</span><br><span class="line">   I/O Timings: shared read=0.069</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=59 read=9</span><br><span class="line">   I/O Timings: shared read=0.133</span><br><span class="line"> Planning Time: 0.452 ms</span><br><span class="line"> Execution Time: 0.243 ms</span><br><span class="line">(10 rows)</span><br></pre></td></tr></table></figure></div>

<p>I&#x2F;O待ち時間が可視化されることで、ディスクI&#x2F;Oが実際のボトルネックかどうかを判断できます。ただし、track_io_timingはシステムコールのオーバーヘッドがあるため、環境によっては性能に影響します。pg_test_timingツールでオーバーヘッドを測定してから有効化することをおすすめします。</p>
<h3 id="WAL">WAL</h3><p>WALオプションは、Write-Ahead Logの生成情報を表示します。ANALYZEと組み合わせて使います。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">EXPLAIN (ANALYZE, WAL)</span><br><span class="line"><span class="keyword">UPDATE</span> products <span class="keyword">SET</span> price <span class="operator">=</span> price <span class="operator">*</span> <span class="number">1.05</span>;</span><br></pre></td></tr></table></figure>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-19" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-19" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                   QUERY PLAN</span><br><span class="line">----------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Update on products  (cost=0.00..2.50 rows=0 width=0) (actual time=0.522..0.523 rows=0.00 loops=1)</span><br><span class="line">   Buffers: shared hit=411 dirtied=1 written=4</span><br><span class="line">   WAL: records=240 bytes=17820</span><br><span class="line">   -&gt;  Seq Scan on products  (cost=0.00..2.50 rows=100 width=22) (actual time=0.011..0.044 rows=100.00 loops=1)</span><br><span class="line">         Buffers: shared hit=1</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=54</span><br><span class="line"> Planning Time: 0.278 ms</span><br><span class="line"> Execution Time: 0.691 ms</span><br><span class="line">(9 rows)</span><br></pre></td></tr></table></figure></div>

<p>WALレコード数、フルページイメージ数、生成されたWALのバイト数が表示されます。</p>
<p>PostgreSQL 18からは、WALバッファがフルになった回数も表示されるようになりました。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">EXPLAIN (ANALYZE, WAL, BUFFERS)</span><br><span class="line"><span class="keyword">UPDATE</span> wal_test <span class="keyword">SET</span> data <span class="operator">=</span> md5(data);</span><br></pre></td></tr></table></figure>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-20" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-20" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                      QUERY PLAN</span><br><span class="line">-----------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Update on wal_test  (cost=0.00..217.35 rows=0 width=0) (actual time=48.481..48.482 rows=0.00 loops=1)</span><br><span class="line">   Buffers: shared hit=60412 dirtied=110 written=117</span><br><span class="line">   WAL: records=30135 bytes=2413984 buffers full=268</span><br><span class="line">   -&gt;  Seq Scan on wal_test  (cost=0.00..217.35 rows=10668 width=38) (actual time=0.025..12.371 rows=10000.00 loops=1)</span><br><span class="line">         Buffers: shared hit=84</span><br><span class="line"> Planning Time: 0.117 ms</span><br><span class="line"> Execution Time: 48.626 ms</span><br><span class="line">(7 rows)</span><br></pre></td></tr></table></figure></div>

<p><code>buffers full=268</code>は、WALバッファがフルになり、ディスクへの書き込みが発生した回数を示します。この値が大きい場合、wal_buffersパラメータを増やすことで性能が改善する可能性があります。</p>
<p>データ更新が多いワークロードでは、WAL生成量がI&#x2F;Oの負荷に直結するため、この情報でボトルネックを特定できます。</p>
<h3 id="TIMING">TIMING</h3><p>TIMINGオプションは、各ノードの実行時間計測を制御します。デフォルトはTRUEですが、FALSEに設定することで計測オーバーヘッドを減らせます。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">EXPLAIN (ANALYZE, TIMING <span class="literal">FALSE</span>)</span><br><span class="line"><span class="keyword">SELECT</span> <span class="built_in">COUNT</span>(<span class="operator">*</span>) <span class="keyword">FROM</span> huge_table;</span><br></pre></td></tr></table></figure>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-21" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-21" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                            QUERY PLAN</span><br><span class="line">--------------------------------------------------------------------------------------------------</span><br><span class="line"> Aggregate  (cost=18.50..18.51 rows=1 width=8) (actual rows=1.00 loops=1)</span><br><span class="line">   Buffers: shared hit=6</span><br><span class="line">   -&gt;  Seq Scan on huge_table  (cost=0.00..16.00 rows=1000 width=0) (actual rows=1000.00 loops=1)</span><br><span class="line">         Buffers: shared hit=6</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=57 read=1</span><br><span class="line"> Planning Time: 0.357 ms</span><br><span class="line"> Execution Time: 0.148 ms</span><br><span class="line">(8 rows)</span><br></pre></td></tr></table></figure></div>

<p>時間計測にはシステムコールのオーバーヘッドがあり、プラットフォームによっては顕著に遅くなることがあります。行数だけ確認したい場合は、TIMINGをオフにすると良いでしょう。</p>
<p>クエリ全体の実行時間は、TIMINGをオフにしても必ず計測されます。</p>
<h3 id="COSTS">COSTS</h3><p>COSTSオプションは、コスト推定値の表示を制御します。デフォルトはTRUEです。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">EXPLAIN (COSTS <span class="literal">FALSE</span>)</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> users <span class="keyword">WHERE</span> age <span class="operator">&gt;</span> <span class="number">30</span>;</span><br></pre></td></tr></table></figure>

<figure class="highlight text"><table><tr><td class="code"><pre><span class="line">      QUERY PLAN</span><br><span class="line">----------------------</span><br><span class="line"> Seq Scan on users</span><br><span class="line">   Filter: (age &gt; 30)</span><br><span class="line">(2 rows)</span><br></pre></td></tr></table></figure>

<p>コスト情報を省略することで、実行計画の構造だけをシンプルに確認できます。教育目的や、プレゼンテーション資料などで使うと見やすくなります。</p>
<h3 id="SETTINGS">SETTINGS</h3><p>SETTINGSオプションは、クエリ計画に影響を与える設定パラメータを表示します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-22" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-22" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">SET</span> work_mem <span class="operator">=</span> <span class="string">&#x27;16MB&#x27;</span>;</span><br><span class="line"></span><br><span class="line">EXPLAIN (SETTINGS)</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> orders <span class="keyword">JOIN</span> customers <span class="keyword">ON</span> orders.customer_id <span class="operator">=</span> customers.id;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-23" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-23" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                               QUERY PLAN</span><br><span class="line">-------------------------------------------------------------------------</span><br><span class="line"> Hash Join  (cost=16.25..36.90 rows=1000 width=69)</span><br><span class="line">   Hash Cond: (orders.customer_id = customers.id)</span><br><span class="line">   -&gt;  Seq Scan on orders  (cost=0.00..18.00 rows=1000 width=27)</span><br><span class="line">   -&gt;  Hash  (cost=10.00..10.00 rows=500 width=42)</span><br><span class="line">         -&gt;  Seq Scan on customers  (cost=0.00..10.00 rows=500 width=42)</span><br><span class="line"> Settings: work_mem = &#x27;16MB&#x27;</span><br><span class="line">(6 rows)</span><br></pre></td></tr></table></figure></div>

<p>デフォルト値と異なる設定のみが表示されるため、なぜ特定の実行計画が選択されたのか理解する助けになります。</p>
<h3 id="FORMAT">FORMAT</h3><p>FORMATオプションは、出力形式を指定します。TEXT、XML、JSON、YAMLの4種類から選べます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-24" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-24" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN (FORMAT JSON)</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> products <span class="keyword">WHERE</span> category <span class="operator">=</span> <span class="string">&#x27;electronics&#x27;</span>;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight json"><input type="checkbox" id="code-wrap-1a9w1s3-25" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-25" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;Plan&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;Node Type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Seq Scan&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;Parallel Aware&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;Async Capable&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;Relation Name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;products&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;Alias&quot;</span><span class="punctuation">:</span> <span class="string">&quot;products&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;Startup Cost&quot;</span><span class="punctuation">:</span> <span class="number">0.00</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;Total Cost&quot;</span><span class="punctuation">:</span> <span class="number">3.25</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;Plan Rows&quot;</span><span class="punctuation">:</span> <span class="number">32</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;Plan Width&quot;</span><span class="punctuation">:</span> <span class="number">30</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;Disabled&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;Filter&quot;</span><span class="punctuation">:</span> <span class="string">&quot;(category = &#x27;electronics&#x27;::text)&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure></div>

<p>TEXT形式は人間が読みやすいデフォルトの形式です。</p>
<p>JSON形式は、プログラムで解析する場合に便利です。pgAdminなどのツールは、JSON形式を解析してグラフィカルな実行計画を表示します。</p>
<p>XML形式とYAML形式も、ツールでの自動処理に適しています。</p>
<h3 id="SUMMARY">SUMMARY</h3><p>SUMMARYオプションは、実行計画後のサマリー情報を制御します。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">EXPLAIN (ANALYZE, SUMMARY)</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> large_table;</span><br></pre></td></tr></table></figure>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-26" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-26" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                      QUERY PLAN</span><br><span class="line">----------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Seq Scan on large_table  (cost=0.00..1541.00 rows=100000 width=14) (actual time=0.013..9.698 rows=100000.00 loops=1)</span><br><span class="line">   Buffers: shared hit=6 read=535</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=54 dirtied=1</span><br><span class="line"> Planning Time: 0.236 ms</span><br><span class="line"> Execution Time: 13.349 ms</span><br><span class="line">(6 rows)</span><br></pre></td></tr></table></figure></div>

<p>ANALYZEを使う場合はデフォルトでサマリーが表示されますが、明示的な制御もできます。Planning TimeとExecution Timeの合計時間が確認できます。</p>
<h3 id="MEMORY">MEMORY</h3><p>MEMORYオプションは、クエリ計画フェーズでのメモリ消費量を表示します。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">EXPLAIN (MEMORY)</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> complex_view;</span><br></pre></td></tr></table></figure>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-27" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-27" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                         QUERY PLAN</span><br><span class="line">-------------------------------------------------------------</span><br><span class="line"> Seq Scan on users u  (cost=0.00..199.00 rows=9108 width=23)</span><br><span class="line">   Filter: (age &gt; 25)</span><br><span class="line"> Planning:</span><br><span class="line">   Memory: used=14kB  allocated=16kB</span><br><span class="line">(4 rows)</span><br></pre></td></tr></table></figure></div>

<p>プランナーが使用したメモリの正確な量と、アロケーションオーバーヘッドを含めた総メモリ量が表示されます。複雑なクエリのプランニングでメモリ不足が疑われる場合に有用です。</p>
<h3 id="SERIALIZE">SERIALIZE</h3><p>SERIALIZEオプションは、PostgreSQL 17で追加されたオプションで、クエリ出力データのシリアライズコストを計測します。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">EXPLAIN (ANALYZE, SERIALIZE TEXT)</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> large_table;</span><br></pre></td></tr></table></figure>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-28" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-28" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                  QUERY PLAN</span><br><span class="line">---------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Seq Scan on large_table  (cost=0.00..189.00 rows=10000 width=18) (actual time=0.004..0.708 rows=10000.00 loops=1)</span><br><span class="line">   Buffers: shared hit=64</span><br><span class="line"> Planning Time: 0.012 ms</span><br><span class="line"> Serialization: time=0.125 ms  output=400kB  format=text</span><br><span class="line"> Execution Time: 1.045 ms</span><br><span class="line">(5 rows)</span><br></pre></td></tr></table></figure></div>

<p>データをテキストやバイナリ形式に変換する時間を計測します。通常の実行では含まれているが、EXPLAINでは省略されるコストを可視化できます。</p>
<p>SERIALIZE NONE（デフォルト）、SERIALIZE TEXT、SERIALIZE BINARYの3種類があります。</p>
<p>TOASTされた大きな値を持つカラムや、出力関数が重いデータ型を扱う場合、シリアライズがボトルネックになることがあります。</p>
<p>BUFFERSオプションと組み合わせると、シリアライズ時のバッファアクセスもカウントされます。</p>
<h3 id="GENERIC-PLAN">GENERIC_PLAN</h3><p>GENERIC_PLANオプションは、パラメータ化されたクエリの汎用プランを表示します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-29" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-29" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN (GENERIC_PLAN)</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> orders <span class="keyword">WHERE</span> customer_id <span class="operator">=</span> $<span class="number">1</span> <span class="keyword">AND</span> order_date <span class="operator">&gt;</span> $<span class="number">2</span>;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-30" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-30" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                       QUERY PLAN</span><br><span class="line">--------------------------------------------------------</span><br><span class="line"> Seq Scan on orders  (cost=0.00..23.00 rows=1 width=27)</span><br><span class="line">   Filter: ((order_date &gt; $2) AND (customer_id = $1))</span><br><span class="line">(2 rows)</span><br></pre></td></tr></table></figure></div>

<p>プリペアドステートメントでは、パラメータの具体的な値に依存しない汎用プランと、特定の値に最適化されたカスタムプランの2種類があります。</p>
<p>GENERIC_PLANを使うと、パラメータに依存しない汎用プランが表示されるため、様々なパラメータ値でどのような計画が使われるか確認できます。</p>
<p>ANALYZEとは組み合わせられません。パラメータの型は、明示的にキャストすることで指定できます。</p>
<p>これらのオプションを適切に組み合わせることで、様々な視点から実行計画を分析できます。</p>
<h2 id="スキャンノードの詳細">スキャンノードの詳細</h2><p>実行計画の最も基本となるのが、テーブルからデータを読み取るスキャンノードです。PostgreSQLは複数のスキャン方法を持っていて、データ量や条件に応じて最適なものが選ばれます。</p>
<h3 id="Sequential-Scan（シーケンシャルスキャン）">Sequential Scan（シーケンシャルスキャン）</h3><p>シーケンシャルスキャンは、テーブルの全行を先頭から順番に読み取る方法です。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-31" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-31" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> users;</span><br><span class="line"></span><br><span class="line">Seq Scan <span class="keyword">on</span> users  (cost<span class="operator">=</span><span class="number">0.00</span>.<span class="number">.155</span><span class="number">.00</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">10000</span> width<span class="operator">=</span><span class="number">64</span>)</span><br></pre></td></tr></table></figure></div>

<p>インデックスを使わず、ディスク上のページを順番にスキャンします。全行を読み取る必要がある場合や、テーブルの大部分を読む場合は、シーケンシャルスキャンが最速になることが多いです。</p>
<p>ランダムアクセスよりも順次アクセスの方が、ディスクI&#x2F;Oの効率が良いためです。特にSSDでは、シーケンシャルリードの速度が非常に高いので、小さなテーブルではインデックスよりも有利になります。</p>
<p>WHERE句でフィルタ条件がある場合、Filter行として表示されます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-32" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-32" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> users <span class="keyword">WHERE</span> age <span class="operator">&gt;</span> <span class="number">30</span>;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-33" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-33" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                QUERY PLAN</span><br><span class="line">-----------------------------------------------------------------------------------------------------------</span><br><span class="line"> Seq Scan on users  (cost=0.00..199.00 rows=8247 width=23) (actual time=0.254..2.636 rows=8247.00 loops=1)</span><br><span class="line">   Filter: (age &gt; 30)</span><br><span class="line">   Rows Removed by Filter: 1753</span><br><span class="line">   Buffers: shared read=74</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=63</span><br><span class="line"> Planning Time: 0.254 ms</span><br><span class="line"> Execution Time: 3.000 ms</span><br><span class="line">(8 rows)</span><br></pre></td></tr></table></figure></div>

<p>フィルタはスキャン後に適用されるため、全行を読み取ってから条件に合う行だけを返します。<code>Rows Removed by Filter: 1753</code>は、フィルタで除外された行数を示しています。</p>
<h3 id="Index-Scan（インデックススキャン）">Index Scan（インデックススキャン）</h3><p>インデックススキャンは、インデックスを使ってテーブルの行を検索します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-34" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-34" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> users <span class="keyword">WHERE</span> user_id <span class="operator">=</span> <span class="number">123</span>;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-35" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-35" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                         QUERY PLAN</span><br><span class="line">-----------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Index Scan using users_user_id_idx on users  (cost=0.29..8.30 rows=1 width=27) (actual time=0.031..0.031 rows=1.00 loops=1)</span><br><span class="line">   Index Cond: (user_id = 123)</span><br><span class="line">   Index Searches: 1</span><br><span class="line">   Buffers: shared hit=1 read=2</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=87 read=1</span><br><span class="line"> Planning Time: 0.399 ms</span><br><span class="line"> Execution Time: 0.071 ms</span><br><span class="line">(8 rows)</span><br></pre></td></tr></table></figure></div>

<p>インデックスで該当する行の位置を特定し、テーブルから実際のデータを取得します。少数の行を取得する場合に効率的です。PostgreSQL 18では、<code>Index Searches: 1</code>のようにインデックスルックアップ回数が表示されます。</p>
<p>Index Condは、インデックスで直接評価できる条件です。インデックスのキーに対する条件がここに表示されます。</p>
<p>インデックスで絞り込めない追加の条件がある場合、Filter行としても表示されます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-36" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-36" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> users <span class="keyword">WHERE</span> user_id <span class="operator">=</span> <span class="number">123</span> <span class="keyword">AND</span> name <span class="operator">=</span> <span class="string">&#x27;Alice&#x27;</span>;</span><br><span class="line"></span><br><span class="line">Index Scan <span class="keyword">using</span> users_pkey <span class="keyword">on</span> users  (cost<span class="operator">=</span><span class="number">0.29</span>.<span class="number">.8</span><span class="number">.31</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">1</span> width<span class="operator">=</span><span class="number">64</span>)</span><br><span class="line">  Index Cond: (user_id <span class="operator">=</span> <span class="number">123</span>)</span><br><span class="line">  <span class="keyword">Filter</span>: (name <span class="operator">=</span> <span class="string">&#x27;Alice&#x27;</span>::text)</span><br></pre></td></tr></table></figure></div>

<p>ORDER BY句がインデックスの順序と一致する場合、ソート処理が不要になるため、インデックススキャンが選ばれやすくなります。</p>
<p>PostgreSQL 18では、Index Scans行にインデックスルックアップの回数が表示されるようになりました。</p>
<h3 id="Index-Only-Scan（インデックスオンリースキャン）">Index Only Scan（インデックスオンリースキャン）</h3><p>インデックスオンリースキャンは、必要なデータがすべてインデックスに含まれている場合に使われます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-37" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-37" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> user_id <span class="keyword">FROM</span> users <span class="keyword">WHERE</span> user_id <span class="keyword">BETWEEN</span> <span class="number">100</span> <span class="keyword">AND</span> <span class="number">200</span>;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-38" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-38" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                             QUERY PLAN</span><br><span class="line">-------------------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Index Only Scan using users_user_id_idx on users  (cost=0.29..6.30 rows=101 width=4) (actual time=0.014..0.110 rows=101.00 loops=1)</span><br><span class="line">   Index Cond: ((user_id &gt;= 100) AND (user_id &lt;= 200))</span><br><span class="line">   Heap Fetches: 202</span><br><span class="line">   Index Searches: 1</span><br><span class="line">   Buffers: shared hit=206</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=3</span><br><span class="line"> Planning Time: 0.065 ms</span><br><span class="line"> Execution Time: 0.122 ms</span><br><span class="line">(9 rows)</span><br></pre></td></tr></table></figure></div>

<p>テーブル本体にアクセスする必要がないため、通常のインデックススキャンよりも高速です。</p>
<p>PostgreSQLのMVCC（Multi-Version Concurrency Control）により、インデックスには可視性情報が含まれていません。そのため、行の可視性を確認するために、テーブルのVisibility Mapを参照します。</p>
<p>VACUUMを実行すると、Heap Fetchesが減少します。</p>
<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-39" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-39" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">-- VACUUM実行後</span><br><span class="line">                                                              QUERY PLAN</span><br><span class="line">--------------------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Index Only Scan using users_user_id_idx on users  (cost=0.29..10.32 rows=102 width=4) (actual time=0.024..0.061 rows=101.00 loops=1)</span><br><span class="line">   Index Cond: ((user_id &gt;= 100) AND (user_id &lt;= 200))</span><br><span class="line">   Heap Fetches: 50</span><br><span class="line">   Index Searches: 1</span><br><span class="line">   Buffers: shared hit=5</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=43</span><br><span class="line"> Planning Time: 0.263 ms</span><br><span class="line"> Execution Time: 0.112 ms</span><br><span class="line">(9 rows)</span><br></pre></td></tr></table></figure></div>

<p>Heap Fetchesは、Visibility Mapで確認できず、実際にテーブルにアクセスした行数です。この値が大きい場合、VACUUMでVisibility Mapを更新すると性能が向上します。</p>
<p>カバリングインデックス（必要なカラムをすべて含むインデックス）を作成することで、インデックスオンリースキャンを活用できます。</p>
<h3 id="Bitmap-Scan（ビットマップスキャン）">Bitmap Scan（ビットマップスキャン）</h3><p>ビットマップスキャンは、中程度の行数を取得する場合に使われる方式です。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-40" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-40" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> users <span class="keyword">WHERE</span> age <span class="keyword">BETWEEN</span> <span class="number">20</span> <span class="keyword">AND</span> <span class="number">30</span>;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-41" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-41" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                           QUERY PLAN</span><br><span class="line">--------------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Bitmap Heap Scan on users  (cost=26.22..199.47 rows=1750 width=27) (actual time=0.130..0.587 rows=1750.00 loops=1)</span><br><span class="line">   Recheck Cond: ((age &gt;= 20) AND (age &lt;= 30))</span><br><span class="line">   Heap Blocks: exact=75</span><br><span class="line">   Buffers: shared hit=75 read=3</span><br><span class="line">   -&gt;  Bitmap Index Scan on users_age_idx  (cost=0.00..25.79 rows=1750 width=0) (actual time=0.091..0.091 rows=1750.00 loops=1)</span><br><span class="line">         Index Cond: ((age &gt;= 20) AND (age &lt;= 30))</span><br><span class="line">         Index Searches: 1</span><br><span class="line">         Buffers: shared read=3</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=101 read=1</span><br><span class="line"> Planning Time: 0.504 ms</span><br><span class="line"> Execution Time: 0.698 ms</span><br><span class="line">(12 rows)</span><br></pre></td></tr></table></figure></div>

<p>ビットマップスキャンは2段階で動作します。Bitmap Index Scanで条件に合う行の位置をビットマップに記録し、Bitmap Heap Scanでビットマップを使ってテーブルから行を取得します。</p>
<p>ビットマップに記録された位置は、物理的な順序でソートされます。これにより、テーブルへのアクセスが順序的になり、ランダムアクセスのコストを削減できます。</p>
<p>複数のインデックス条件がある場合、BitmapAndやBitmapOrノードで結合できます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-42" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-42" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> users <span class="keyword">WHERE</span> age <span class="operator">&gt;</span> <span class="number">20</span> <span class="keyword">AND</span> status <span class="operator">=</span> <span class="string">&#x27;active&#x27;</span>;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-43" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-43" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                            QUERY PLAN</span><br><span class="line">-----------------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Bitmap Heap Scan on users  (cost=42.04..253.90 rows=3301 width=35) (actual time=0.099..0.634 rows=3302.00 loops=1)</span><br><span class="line">   Recheck Cond: (status = &#x27;active&#x27;::text)</span><br><span class="line">   Filter: (age &gt; 20)</span><br><span class="line">   Rows Removed by Filter: 22</span><br><span class="line">   Heap Blocks: exact=91</span><br><span class="line">   Buffers: shared hit=95</span><br><span class="line">   -&gt;  Bitmap Index Scan on users_status_idx  (cost=0.00..41.21 rows=3324 width=0) (actual time=0.080..0.080 rows=3324.00 loops=1)</span><br><span class="line">         Index Cond: (status = &#x27;active&#x27;::text)</span><br><span class="line">         Index Searches: 1</span><br><span class="line">         Buffers: shared hit=4</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=130</span><br><span class="line"> Planning Time: 0.381 ms</span><br><span class="line"> Execution Time: 0.772 ms</span><br><span class="line">(14 rows)</span><br></pre></td></tr></table></figure></div>

<p>ビットマップのメモリが不足すると、「lossy」モードになります。この場合、ビットマップはページ単位でしか情報を保持できず、Recheck Condでページ内の各行を再確認する必要があります。</p>
<h3 id="その他のスキャンタイプ">その他のスキャンタイプ</h3><p>TID Scanは、行のタプルID（ctid）を直接指定して取得する方法です。内部的な処理や、システムカタログの特殊な操作で使われることがあります。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-44" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-44" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> users <span class="keyword">WHERE</span> ctid <span class="operator">=</span> <span class="string">&#x27;(0,102)&#x27;</span>;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-45" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-45" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                            QUERY PLAN</span><br><span class="line">---------------------------------------------------------------------------------------------------</span><br><span class="line"> Tid Scan on users  (cost=0.00..4.01 rows=1 width=35) (actual time=0.011..0.011 rows=1.00 loops=1)</span><br><span class="line">   TID Cond: (ctid = &#x27;(0,102)&#x27;::tid)</span><br><span class="line">   Buffers: shared hit=1</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=116</span><br><span class="line"> Planning Time: 0.496 ms</span><br><span class="line"> Execution Time: 0.047 ms</span><br><span class="line">(7 rows)</span><br></pre></td></tr></table></figure></div>

<p>VALUES Scanは、VALUES句から直接データを生成します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-46" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-46" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> (<span class="keyword">VALUES</span> (<span class="number">1</span>, <span class="string">&#x27;Alice&#x27;</span>), (<span class="number">2</span>, <span class="string">&#x27;Bob&#x27;</span>), (<span class="number">3</span>, <span class="string">&#x27;Charlie&#x27;</span>)) <span class="keyword">AS</span> t(id, name);</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-47" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-47" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                QUERY PLAN</span><br><span class="line">-----------------------------------------------------------------------------------------------------------</span><br><span class="line"> Values Scan on &quot;*VALUES*&quot;  (cost=0.00..0.04 rows=3 width=36) (actual time=0.014..0.015 rows=3.00 loops=1)</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=3</span><br><span class="line"> Planning Time: 0.097 ms</span><br><span class="line"> Execution Time: 0.036 ms</span><br><span class="line">(5 rows)</span><br></pre></td></tr></table></figure></div>

<p>Function Scanは、セット返却関数からデータを取得します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-48" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-48" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> generate_series(<span class="number">1</span>, <span class="number">100</span>);</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-49" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-49" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                     QUERY PLAN</span><br><span class="line">---------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Function Scan on generate_series  (cost=0.00..1.00 rows=100 width=4) (actual time=0.017..0.022 rows=100.00 loops=1)</span><br><span class="line"> Planning Time: 0.045 ms</span><br><span class="line"> Execution Time: 0.057 ms</span><br><span class="line">(3 rows)</span><br></pre></td></tr></table></figure></div>

<p>CTE Scanは、WITH句で定義されたCommon Table Expression（共通テーブル式）からデータを読み取ります。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-50" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-50" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">WITH</span> active_users <span class="keyword">AS</span> (</span><br><span class="line">  <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> users <span class="keyword">WHERE</span> status <span class="operator">=</span> <span class="string">&#x27;active&#x27;</span></span><br><span class="line">)</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> active_users <span class="keyword">WHERE</span> age <span class="operator">&gt;</span> <span class="number">30</span>;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-51" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-51" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                             QUERY PLAN</span><br><span class="line">-----------------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Bitmap Heap Scan on users  (cost=41.90..253.76 rows=2742 width=35) (actual time=0.107..0.636 rows=2744.00 loops=1)</span><br><span class="line">   Recheck Cond: (status = &#x27;active&#x27;::text)</span><br><span class="line">   Filter: (age &gt; 30)</span><br><span class="line">   Rows Removed by Filter: 580</span><br><span class="line">   Heap Blocks: exact=91</span><br><span class="line">   Buffers: shared hit=95</span><br><span class="line">   -&gt;  Bitmap Index Scan on users_status_idx  (cost=0.00..41.21 rows=3324 width=0) (actual time=0.088..0.088 rows=3324.00 loops=1)</span><br><span class="line">         Index Cond: (status = &#x27;active&#x27;::text)</span><br><span class="line">         Index Searches: 1</span><br><span class="line">         Buffers: shared hit=4</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=133</span><br><span class="line"> Planning Time: 0.403 ms</span><br><span class="line"> Execution Time: 0.759 ms</span><br><span class="line">(14 rows)</span><br></pre></td></tr></table></figure></div>

<p>スキャン方法の選択は、テーブルサイズ、取得する行数の割合、インデックスの有無、統計情報などに基づいて、プランナーが自動的に判断します。</p>
<h2 id="結合の実行計画">結合の実行計画</h2><p>複数のテーブルを結合するクエリでは、結合方法の選択がパフォーマンスに大きく影響します。PostgreSQLには3つの主要な結合方式があります。</p>
<h3 id="Nested-Loop-Join（ネステッドループ結合）">Nested Loop Join（ネステッドループ結合）</h3><p>ネステッドループ結合は、最もシンプルな方式です。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-52" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-52" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">SET</span> enable_hashjoin <span class="operator">=</span> off;</span><br><span class="line"><span class="keyword">SET</span> enable_mergejoin <span class="operator">=</span> off;</span><br><span class="line"></span><br><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> c.name, <span class="built_in">COUNT</span>(o.id), <span class="built_in">SUM</span>(o.amount)</span><br><span class="line"><span class="keyword">FROM</span> orders o</span><br><span class="line"><span class="keyword">JOIN</span> customers c <span class="keyword">ON</span> o.customer_id <span class="operator">=</span> c.id</span><br><span class="line"><span class="keyword">WHERE</span> c.city <span class="operator">=</span> <span class="string">&#x27;Tokyo&#x27;</span></span><br><span class="line"><span class="keyword">GROUP</span> <span class="keyword">BY</span> c.name;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-53" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-53" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                                     QUERY PLAN</span><br><span class="line">----------------------------------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> HashAggregate  (cost=198.56..199.44 rows=71 width=52) (actual time=1.263..1.280 rows=65.00 loops=1)</span><br><span class="line">   Group Key: c.name</span><br><span class="line">   Batches: 1  Memory Usage: 56kB</span><br><span class="line">   Buffers: shared hit=1322</span><br><span class="line">   -&gt;  Nested Loop  (cost=0.28..197.49 rows=142 width=22) (actual time=0.050..1.179 rows=154.00 loops=1)</span><br><span class="line">         Buffers: shared hit=1322</span><br><span class="line">         -&gt;  Seq Scan on orders o  (cost=0.00..18.00 rows=1000 width=14) (actual time=0.006..0.085 rows=1000.00 loops=1)</span><br><span class="line">               Buffers: shared hit=8</span><br><span class="line">         -&gt;  Memoize  (cost=0.28..0.36 rows=1 width=16) (actual time=0.001..0.001 rows=0.15 loops=1000)</span><br><span class="line">               Cache Key: o.customer_id</span><br><span class="line">               Cache Mode: logical</span><br><span class="line">               Hits: 562  Misses: 438  Evictions: 0  Overflows: 0  Memory Usage: 33kB</span><br><span class="line">               Buffers: shared hit=1314</span><br><span class="line">               -&gt;  Index Scan using customers_pkey on customers c  (cost=0.27..0.35 rows=1 width=16) (actual time=0.001..0.001 rows=0.15 loops=438)</span><br><span class="line">                     Index Cond: (id = o.customer_id)</span><br><span class="line">                     Filter: (city = &#x27;Tokyo&#x27;::text)</span><br><span class="line">                     Rows Removed by Filter: 1</span><br><span class="line">                     Index Searches: 438</span><br><span class="line">                     Buffers: shared hit=1314</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=195</span><br><span class="line"> Planning Time: 0.503 ms</span><br><span class="line"> Execution Time: 1.386 ms</span><br><span class="line">(23 rows)</span><br></pre></td></tr></table></figure></div>

<p>外側のテーブル（orders）から1行ずつ取得し、各行について内側のテーブル（customers）を検索します。内側は、外側の行数分だけ繰り返し実行されます。</p>
<p>上記の例では、ordersで1000行が取得され、各行についてcustomersがIndex Scanで検索されます（loops&#x3D;1000）。Memoizeノードがキャッシュを提供し、重複するcustomer_idへのアクセスを高速化しています。</p>
<p><code>rows=0.15</code>のように、PostgreSQL 18から行数が小数点で表示されるようになりました（詳細はセクション2-4参照）。</p>
<p>少数の行を結合する場合や、内側のテーブルにインデックスがある場合に効率的です。外側の行数が少ないほど、内側の繰り返し回数も減るため、高速になります。</p>
<h3 id="Hash-Join（ハッシュ結合）">Hash Join（ハッシュ結合）</h3><p>ハッシュ結合は、中規模から大規模のデータセットに適しています。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-54" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-54" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> c.name, <span class="built_in">COUNT</span>(o.id), <span class="built_in">SUM</span>(o.amount)</span><br><span class="line"><span class="keyword">FROM</span> orders o</span><br><span class="line"><span class="keyword">JOIN</span> customers c <span class="keyword">ON</span> o.customer_id <span class="operator">=</span> c.id</span><br><span class="line"><span class="keyword">GROUP</span> <span class="keyword">BY</span> c.name;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-55" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-55" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                           QUERY PLAN</span><br><span class="line">--------------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> HashAggregate  (cost=49.40..55.65 rows=500 width=52) (actual time=0.983..1.101 rows=438.00 loops=1)</span><br><span class="line">   Group Key: c.name</span><br><span class="line">   Batches: 1  Memory Usage: 321kB</span><br><span class="line">   Buffers: shared hit=18</span><br><span class="line">   -&gt;  Hash Join  (cost=21.25..41.90 rows=1000 width=22) (actual time=0.213..0.538 rows=1000.00 loops=1)</span><br><span class="line">         Hash Cond: (o.customer_id = c.id)</span><br><span class="line">         Buffers: shared hit=18</span><br><span class="line">         -&gt;  Seq Scan on orders o  (cost=0.00..18.00 rows=1000 width=14) (actual time=0.007..0.091 rows=1000.00 loops=1)</span><br><span class="line">               Buffers: shared hit=8</span><br><span class="line">         -&gt;  Hash  (cost=15.00..15.00 rows=500 width=16) (actual time=0.188..0.188 rows=500.00 loops=1)</span><br><span class="line">               Buckets: 1024  Batches: 1  Memory Usage: 34kB</span><br><span class="line">               Buffers: shared hit=10</span><br><span class="line">               -&gt;  Seq Scan on customers c  (cost=0.00..15.00 rows=500 width=16) (actual time=0.007..0.063 rows=500.00 loops=1)</span><br><span class="line">                     Buffers: shared hit=10</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=214</span><br><span class="line"> Planning Time: 0.639 ms</span><br><span class="line"> Execution Time: 1.255 ms</span><br><span class="line">(18 rows)</span><br></pre></td></tr></table></figure></div>

<p>内側（customers）のデータをメモリ上のハッシュテーブルに格納し、外側（orders）の各行に対してハッシュテーブルを検索します。</p>
<p>ハッシュテーブルの構築にはコストがかかりますが、構築後の検索は非常に高速です。両方のテーブルを1回ずつスキャンするだけで済むため、大量データの結合に向いています。</p>
<p>ハッシュテーブルがwork_memに収まらない場合、バッチ処理に分割されます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-56" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-56" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">Hash <span class="keyword">Join</span>  (cost<span class="operator">=</span><span class="number">200.00</span>.<span class="number">.1500</span><span class="number">.00</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">10000</span> width<span class="operator">=</span><span class="number">88</span>)</span><br><span class="line">  Hash Cond: (o.customer_id <span class="operator">=</span> c.id)</span><br><span class="line">  <span class="operator">-</span><span class="operator">&gt;</span> Seq Scan <span class="keyword">on</span> orders o  (cost<span class="operator">=</span><span class="number">0.00</span>.<span class="number">.1000</span><span class="number">.00</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">10000</span> width<span class="operator">=</span><span class="number">44</span>)</span><br><span class="line">  <span class="operator">-</span><span class="operator">&gt;</span> Hash  (cost<span class="operator">=</span><span class="number">150.00</span>.<span class="number">.150</span><span class="number">.00</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">5000</span> width<span class="operator">=</span><span class="number">44</span>)</span><br><span class="line">        Buckets: <span class="number">8192</span>  Batches: <span class="number">4</span>  Memory Usage: <span class="number">245</span>kB</span><br><span class="line">        <span class="operator">-</span><span class="operator">&gt;</span> Seq Scan <span class="keyword">on</span> customers c  (cost<span class="operator">=</span><span class="number">0.00</span>.<span class="number">.150</span><span class="number">.00</span> <span class="keyword">rows</span><span class="operator">=</span><span class="number">5000</span> width<span class="operator">=</span><span class="number">44</span>)</span><br></pre></td></tr></table></figure></div>

<p>Batchesが1より大きい場合、一時ファイルが使われます。この場合、work_memを増やすことで性能が向上します。</p>
<p>PostgreSQL 18では、ハッシュ結合の性能とメモリ使用量が改善されています。</p>
<h3 id="Merge-Join（マージ結合）">Merge Join（マージ結合）</h3><p>マージ結合は、両方のテーブルがソート済みの場合に効率的な方式です。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-57" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-57" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> c.name, <span class="built_in">COUNT</span>(o.id), <span class="built_in">SUM</span>(o.amount)</span><br><span class="line"><span class="keyword">FROM</span> orders o</span><br><span class="line"><span class="keyword">JOIN</span> customers c <span class="keyword">ON</span> o.customer_id <span class="operator">=</span> c.id</span><br><span class="line"><span class="keyword">GROUP</span> <span class="keyword">BY</span> c.name;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-58" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-58" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                                       QUERY PLAN</span><br><span class="line">--------------------------------------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> HashAggregate  (cost=905.06..967.56 rows=5000 width=53) (actual time=14.043..15.897 rows=4327.00 loops=1)</span><br><span class="line">   Group Key: c.name</span><br><span class="line">   Batches: 1  Memory Usage: 2193kB</span><br><span class="line">   Buffers: shared hit=9823 read=19</span><br><span class="line">   -&gt;  Merge Join  (cost=0.57..830.06 rows=10000 width=23) (actual time=0.036..8.866 rows=10000.00 loops=1)</span><br><span class="line">         Merge Cond: (o.customer_id = c.id)</span><br><span class="line">         Buffers: shared hit=9823 read=19</span><br><span class="line">         -&gt;  Index Scan using orders_customer_idx on orders o  (cost=0.29..498.28 rows=10000 width=14) (actual time=0.019..4.794 rows=10000.00 loops=1)</span><br><span class="line">               Index Searches: 1</span><br><span class="line">               Buffers: shared hit=9756 read=19</span><br><span class="line">         -&gt;  Index Scan using customers_pkey on customers c  (cost=0.28..194.28 rows=5000 width=17) (actual time=0.014..1.251 rows=5000.00 loops=1)</span><br><span class="line">               Index Searches: 1</span><br><span class="line">               Buffers: shared hit=67</span><br><span class="line"> Planning Time: 0.850 ms</span><br><span class="line"> Execution Time: 16.550 ms</span><br><span class="line">(17 rows)</span><br></pre></td></tr></table></figure></div>

<p>両方のテーブルを結合キーの順にスキャンし、マージしながら結合します。ソート済みのデータを前提とするため、インデックススキャンを使うか、事前にソートが必要です。</p>
<p>ソート済みでない場合、Sortノードが追加されます。</p>
<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-59" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-59" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                               QUERY PLAN</span><br><span class="line">-----------------------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Merge Join  (cost=828.67..891.62 rows=198 width=21) (actual time=2.418..2.497 rows=189.00 loops=1)</span><br><span class="line">   Merge Cond: (c.id = o.customer_id)</span><br><span class="line">   Buffers: shared hit=67</span><br><span class="line">   -&gt;  Index Scan using customers_pkey on customers c  (cost=0.28..11.02 rows=99 width=17) (actual time=0.004..0.021 rows=99.00 loops=1)</span><br><span class="line">         Index Cond: (id &lt; 100)</span><br><span class="line">         Index Searches: 1</span><br><span class="line">         Buffers: shared hit=3</span><br><span class="line">   -&gt;  Sort  (cost=828.39..853.39 rows=10000 width=4) (actual time=2.409..2.421 rows=190.00 loops=1)</span><br><span class="line">         Sort Key: o.customer_id</span><br><span class="line">         Sort Method: quicksort  Memory: 385kB</span><br><span class="line">         Buffers: shared hit=64</span><br><span class="line">         -&gt;  Seq Scan on orders o  (cost=0.00..164.00 rows=10000 width=4) (actual time=0.013..0.880 rows=10000.00 loops=1)</span><br><span class="line">               Buffers: shared hit=64</span><br><span class="line"> Planning Time: 0.619 ms</span><br><span class="line"> Execution Time: 2.609 ms</span><br><span class="line">(17 rows)</span><br></pre></td></tr></table></figure></div>

<h3 id="結合方法の選択基準について">結合方法の選択基準について</h3><p>プランナーは、統計情報とコスト計算に基づいて、最適な結合方法を選択します。</p>
<p>外側のテーブルが非常に小さく、内側のテーブルにインデックスがある場合は、ネステッドループが選ばれやすいです。</p>
<p>中規模から大規模のデータで、等値結合（&#x3D;）の場合は、ハッシュ結合が選ばれることが多いです。</p>
<p>両方のテーブルがソート済み、または結合キーにインデックスがある場合は、マージ結合が選ばれることがあります。</p>
<p>enable_nestloop、enable_hashjoin、enable_mergejoinパラメータで、各結合方式を一時的に無効化できます。これは主にデバッグや、プランナーの判断を理解するために使います。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-60" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-60" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">SET</span> enable_hashjoin <span class="operator">=</span> off;</span><br><span class="line">EXPLAIN <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> orders o <span class="keyword">JOIN</span> customers c <span class="keyword">ON</span> o.customer_id <span class="operator">=</span> c.id;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-61" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-61" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                           QUERY PLAN</span><br><span class="line">-------------------------------------------------------------------------------------------------</span><br><span class="line"> Merge Join  (cost=0.57..830.06 rows=10000 width=67)</span><br><span class="line">   Merge Cond: (o.customer_id = c.id)</span><br><span class="line">   -&gt;  Index Scan using orders_customer_idx on orders o  (cost=0.29..498.28 rows=10000 width=18)</span><br><span class="line">   -&gt;  Index Scan using customers_pkey on customers c  (cost=0.28..194.28 rows=5000 width=49)</span><br><span class="line">(4 rows)</span><br></pre></td></tr></table></figure></div>

<p>ハッシュ結合を無効化すると、マージ結合が選択されました。実行計画を見て、どの結合方式が選ばれたか確認し、必要に応じてインデックスの追加や統計情報の更新を検討すると良いでしょう。</p>
<h2 id="パフォーマンスチューニングの基本">パフォーマンスチューニングの基本</h2><p>EXPLAINの読み方を理解したところで、実際のパフォーマンスチューニングにどう活用するか基本的な例をあげて見ていきます。</p>
<h3 id="1-遅いクエリの特定方法">1. 遅いクエリの特定方法</h3><p>どのクエリが遅いのかを特定する必要があります。アプリケーションのログやPostgreSQLのスロークエリログから、実行時間が長いクエリをリストアップします。</p>
<p>log_min_duration_statementパラメータを設定することで、指定した時間以上かかったクエリをログに記録できます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-62" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-62" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">SET</span> log_min_duration_statement <span class="operator">=</span> <span class="number">1000</span>;  <span class="comment">-- 1秒以上のクエリをログに記録</span></span><br></pre></td></tr></table></figure></div>

<p>pg_stat_statementsエクステンションを使うと、実行されたすべてのクエリの統計情報を収集できます。平均実行時間、総実行時間、実行回数などから、最適化すべきクエリを特定できます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-63" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-63" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> query, calls, total_exec_time, mean_exec_time</span><br><span class="line"><span class="keyword">FROM</span> pg_stat_statements</span><br><span class="line"><span class="keyword">ORDER</span> <span class="keyword">BY</span> total_exec_time <span class="keyword">DESC</span></span><br><span class="line">LIMIT <span class="number">10</span>;</span><br></pre></td></tr></table></figure></div>

<p>遅いクエリが特定できたら、EXPLAIN ANALYZEで実行計画を確認します。</p>
<h3 id="2-インデックスの追加・改善">2. インデックスの追加・改善</h3><p>実行計画を見て、シーケンシャルスキャンが使われている場合、インデックスの追加を検討します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-64" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-64" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> orders <span class="keyword">WHERE</span> customer_id <span class="operator">=</span> <span class="number">123</span>;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-65" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-65" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                QUERY PLAN</span><br><span class="line">----------------------------------------------------------------------------------------------------------</span><br><span class="line"> Seq Scan on orders  (cost=0.00..1887.00 rows=99 width=18) (actual time=0.037..5.005 rows=104.00 loops=1)</span><br><span class="line">   Filter: (customer_id = 123)</span><br><span class="line">   Rows Removed by Filter: 99896</span><br><span class="line">   Buffers: shared hit=637</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=27 read=1</span><br><span class="line"> Planning Time: 0.134 ms</span><br><span class="line"> Execution Time: 5.025 ms</span><br><span class="line">(8 rows)</span><br></pre></td></tr></table></figure></div>

<p>この例では、10万行をスキャンして、99896行がフィルタで除外されています。customer_idにインデックスを作成すると改善できます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-66" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-66" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> INDEX orders_customer_idx <span class="keyword">ON</span> orders(customer_id);</span><br><span class="line"></span><br><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> orders <span class="keyword">WHERE</span> customer_id <span class="operator">=</span> <span class="number">123</span>;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-67" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-67" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                            QUERY PLAN</span><br><span class="line">----------------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Bitmap Heap Scan on orders  (cost=5.06..269.41 rows=99 width=18) (actual time=0.049..0.118 rows=104.00 loops=1)</span><br><span class="line">   Recheck Cond: (customer_id = 123)</span><br><span class="line">   Heap Blocks: exact=93</span><br><span class="line">   Buffers: shared hit=93 read=2</span><br><span class="line">   -&gt;  Bitmap Index Scan on orders_customer_idx  (cost=0.00..5.04 rows=99 width=0) (actual time=0.037..0.037 rows=104.00 loops=1)</span><br><span class="line">         Index Cond: (customer_id = 123)</span><br><span class="line">         Index Searches: 1</span><br><span class="line">         Buffers: shared read=2</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=49 read=2</span><br><span class="line"> Planning Time: 0.240 ms</span><br><span class="line"> Execution Time: 0.174 ms</span><br><span class="line">(12 rows)</span><br></pre></td></tr></table></figure></div>

<p>実行時間が5.025ミリ秒から0.174ミリ秒に大幅に改善されました。</p>
<p>複数のカラムで絞り込む場合、複合インデックスが有効なことがあります。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-68" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-68" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> INDEX orders_customer_date_idx <span class="keyword">ON</span> orders(customer_id, order_date);</span><br></pre></td></tr></table></figure></div>

<p>ただし、インデックスはストレージと更新コストを消費するため、必要なものだけを作成することが大切です。</p>
<h3 id="3-統計情報の更新">3. 統計情報の更新</h3><p>実行計画の推定行数と実際の行数が大きく異なる場合、統計情報が古くなっています。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-69" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-69" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> products <span class="keyword">WHERE</span> category <span class="operator">=</span> <span class="string">&#x27;electronics&#x27;</span>;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-70" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-70" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                QUERY PLAN</span><br><span class="line">-----------------------------------------------------------------------------------------------------------</span><br><span class="line"> Seq Scan on products  (cost=0.00..99.74 rows=8 width=356) (actual time=0.012..1.251 rows=5100.00 loops=1)</span><br><span class="line">   Filter: (category = &#x27;electronics&#x27;::text)</span><br><span class="line">   Rows Removed by Filter: 4900</span><br><span class="line">   Buffers: shared hit=79</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=10 read=1</span><br><span class="line"> Planning Time: 0.100 ms</span><br><span class="line"> Execution Time: 1.460 ms</span><br><span class="line">(8 rows)</span><br></pre></td></tr></table></figure></div>

<p>推定が8行なのに実際は5100行返されています。この場合、ANALYZEコマンドで統計情報を更新します。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">ANALYZE products;</span><br></pre></td></tr></table></figure>

<p>定期的なVACUUMとANALYZEの実行は、プランナーが正確な実行計画を立てるために重要です。autovacuumが有効になっていれば、自動的に実行されますが、大量のデータ変更があった後は、手動で実行することも検討します。</p>
<p>default_statistics_targetパラメータで、統計情報の詳細度を調整できます。デフォルトは100ですが、特定のカラムで精度を上げたい場合、カラム単位で設定を増やせます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-71" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-71" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">ALTER TABLE</span> products <span class="keyword">ALTER</span> <span class="keyword">COLUMN</span> category <span class="keyword">SET</span> STATISTICS <span class="number">1000</span>;</span><br><span class="line">ANALYZE products;</span><br></pre></td></tr></table></figure></div>

<h3 id="4-コスト設定の調整">4. コスト設定の調整</h3><p>プランナーのコスト計算は、いくつかのパラメータで制御されています。環境に合わせて調整することで、より適切な実行計画が選ばれます。</p>
<p>seq_page_costとrandom_page_costは、それぞれシーケンシャルアクセスとランダムアクセスのコストを表します。デフォルトではseq_page_cost&#x3D;1.0、random_page_cost&#x3D;4.0です。</p>
<p>SSDを使っている場合、ランダムアクセスのコストがHDDよりも低いため、random_page_costを下げることで、インデックススキャンが選ばれやすくなります。公式ドキュメントでは1.1が例示されています。<br>(https://www.postgresql.org/docs/18/runtime-config-query.html#GUC-RANDOM-PAGE-COST)</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">SET</span> random_page_cost <span class="operator">=</span> <span class="number">1.1</span>;  <span class="comment">-- SSD環境での調整例</span></span><br></pre></td></tr></table></figure>

<p>effective_cache_sizeは、OSやPostgreSQLが使える総キャッシュサイズの推定値です。この値を適切に設定することで、インデックススキャンとシーケンシャルスキャンの選択精度が向上します。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">SET</span> effective_cache_size <span class="operator">=</span> <span class="string">&#x27;4GB&#x27;</span>;</span><br></pre></td></tr></table></figure>

<p>work_memは、ソートやハッシュテーブルなどの作業用メモリです。複雑なクエリで一時ファイルへのスピルが発生している場合、work_memを増やすことで性能が改善します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1a9w1s3-72" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-72" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">EXPLAIN ANALYZE <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> large_table <span class="keyword">ORDER</span> <span class="keyword">BY</span> data;</span><br></pre></td></tr></table></figure></div>

<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1a9w1s3-73" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1a9w1s3-73" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">                                                       QUERY PLAN</span><br><span class="line">-------------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> Sort  (cost=848.39..873.39 rows=10000 width=37) (actual time=10.893..11.284 rows=10000.00 loops=1)</span><br><span class="line">   Sort Key: data</span><br><span class="line">   Sort Method: quicksort  Memory: 931kB</span><br><span class="line">   Buffers: shared hit=84</span><br><span class="line">   -&gt;  Seq Scan on large_table  (cost=0.00..184.00 rows=10000 width=37) (actual time=0.006..0.556 rows=10000.00 loops=1)</span><br><span class="line">         Buffers: shared hit=84</span><br><span class="line"> Planning:</span><br><span class="line">   Buffers: shared hit=34 read=3</span><br><span class="line"> Planning Time: 0.228 ms</span><br><span class="line"> Execution Time: 11.588 ms</span><br><span class="line">(10 rows)</span><br></pre></td></tr></table></figure></div>

<p>この例では、Sort Methodがquicksortでメモリ内で完結していますが、データ量がさらに多い場合、Sort Methodがexternal mergeになり、Diskが使われます。work_memを増やすと、より大きなデータセットをメモリ内でソートできるようになります。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">SET</span> work_mem <span class="operator">=</span> <span class="string">&#x27;256MB&#x27;</span>;</span><br></pre></td></tr></table></figure>

<p>ただし、work_memはクエリごと・ノードごとに割り当てられるため、値を大きくしすぎるとメモリ不足になるリスクがあります。</p>
<h2 id="まとめ">まとめ</h2><p>PostgreSQL 18では、EXPLAINが大幅に強化され、パフォーマンス分析がより容易になりました。</p>
<p>実際のパフォーマンスチューニングでは、EXPLAIN ANALYZEで実行計画を確認し、ボトルネックを特定することが第一歩です。適切なインデックスの追加、統計情報の更新、設定パラメータの調整などを組み合わせることで、大幅な性能改善が可能になります。</p>
<p>EXPLAINは、データベースのパフォーマンスを理解し、最適化する上で欠かせないツールです。PostgreSQL 18の強化された機能を活用して、より効率的なクエリチューニングを行っていきましょう。</p>
<h2 id="参考文献">参考文献</h2><ul>
<li>PostgreSQL 18 Released! - https://www.postgresql.org/about/news/postgresql-18-released-3142/</li>
<li>PostgreSQL 18 Release Notes - https://www.postgresql.org/docs/18/release-18.html</li>
<li>EXPLAIN Documentation - https://www.postgresql.org/docs/current/sql-explain.html</li>
<li>Using EXPLAIN - https://www.postgresql.org/docs/current/using-explain.html</li>
<li>Performance Tips - https://www.postgresql.org/docs/current/performance-tips.html</li>
<li>Run-time Statistics - https://www.postgresql.org/docs/current/runtime-config-statistics.html</li>
<li>PostgreSQL 18 New Features - https://neon.com/postgresql/postgresql-18-new-features</li>
<li>PostgreSQL 18 Enhanced EXPLAIN - https://neon.com/postgresql/postgresql-18/enhanced-explain</li>
<li>What’s New in PostgreSQL 18 - https://www.sqlpassion.at/archive/2025/09/29/whats-new-in-postgresql-18/</li>
<li>Crunchy Data: Get Excited About Postgres 18 - https://www.crunchydata.com/blog/get-excited-about-postgres-18</li>
<li>Going down the rabbit hole of Postgres 18 features - https://xata.io/blog/going-down-the-rabbit-hole-of-postgres-18-features</li>
<li>Waiting for PostgreSQL 18 – Enable BUFFERS with EXPLAIN ANALYZE by default - https://www.depesz.com/2025/01/15/waiting-for-postgresql-18-enable-buffers-with-explain-analyze-by-default/</li>
</ul>
]]></content>
    <summary type="html">PostgreSQL 18の新機能と、あわせて基礎的な使い方まで記載します。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="PostgreSQL" scheme="https://future-architect.github.io/tags/PostgreSQL/"/>
    <category term="PostgreSQL18" scheme="https://future-architect.github.io/tags/PostgreSQL18/"/>
    <category term="実行計画" scheme="https://future-architect.github.io/tags/%E5%AE%9F%E8%A1%8C%E8%A8%88%E7%94%BB/"/>
  </entry>
  <entry>
    <title>PostgreSQL連載始まります &amp; v18で対応したUUIDv7とv4の比較</title>
    <link href="https://future-architect.github.io/articles/20251006a/"/>
    <id>https://future-architect.github.io/articles/20251006a/</id>
    <published>2025-10-05T15:00:00.000Z</published>
    <updated>2025-10-05T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<p>PostgreSQL 18がリリースされました。気になる新機能やパフォーマンスアップなどが盛りだくさんです。当ブログでは今回のアップデートに限らずデータベース一般ネタも含めた連載記事を執筆します。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">日付</th>
<th align="left">執筆者</th>
<th align="left">タイトル</th>
</tr>
</thead>
<tbody><tr>
<td align="left">10&#x2F;6（月）</td>
<td align="left">澁川喜規</td>
<td align="left">v18で対応したUUIDv7とv4の比較（この記事です）</td>
</tr>
<tr>
<td align="left">10&#x2F;7（火）</td>
<td align="left">山本竜玄</td>
<td align="left">explainをマスターするぜ</td>
</tr>
<tr>
<td align="left">10&#x2F;8（水）</td>
<td align="left">真野隼記</td>
<td align="left">仮想生成列</td>
</tr>
<tr>
<td align="left">10&#x2F;9（木）</td>
<td align="left">岩堀敦</td>
<td align="left">pg_dump</td>
</tr>
<tr>
<td align="left">10&#x2F;10（金）</td>
<td align="left">市川裕也</td>
<td align="left">現場で行った性能チューニング</td>
</tr>
<tr>
<td align="left">10&#x2F;14（火）</td>
<td align="left">村田 靖拓</td>
<td align="left">B-treeインデックスのスキップスキャン</td>
</tr>
</tbody></table></div>
<h2 id="UUID-v7">UUID v7</h2><p>PostgreSQL 18ではUUIDv7生成に対応しました。今までのUUID v4(完全ランダム)は主キーとして使うと、ソート順で扱おうとするPostgreSQLの内部構造のB-Treeと相性が悪く、さまざまなノードへのアクセスが必要になるため、相性が悪いとされていました。RFC-9562で標準化されたUUID v7は先頭がタイムスタンプであり、キーをソートすると、必ず作成した順序になります。そのため、生成順にデータを挿入したとしてもパフォーマンスが落ちにくくなる、だからいいんだ、ということのようです。</p>
<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-28l9or-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-28l9or-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"> 0                   1                   2                   3</span><br><span class="line"> 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1</span><br><span class="line">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class="line">|                           unix_ts_ms                          |</span><br><span class="line">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class="line">|          unix_ts_ms           |  ver  |       rand_a          |</span><br><span class="line">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class="line">|var|                        rand_b                             |</span><br><span class="line">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class="line">|                            rand_b                             |</span><br><span class="line">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br></pre></td></tr></table></figure></div>

<p>B-Treeはその名の通り木構造です。ソート順でデータが並ぶため、近いデータへのアクセスであればキャッシュ効率も上がります。</p>
<img fetchpriority="high" src="/images/2025/20251006a/スクリーンショット_2025-10-03_18.03.47.png" alt="スクリーンショット_2025-10-03_18.03.47.png" width="844" height="261">

<ul>
<li>Wikpedia B木より引用</li>
</ul>
<h2 id="実際に検証してみる">実際に検証してみる</h2><p>実際に速度が変わるかをプログラムを作って検証しました。Dockerのpostgres:18-trixieイメージに対して検証プログラムから100万行ほどのレコードを投入しています。</p>
<ul>
<li>通信自体の往復が支配的にならないように（I&#x2F;Oの差が出やすいように）、100件ずつ入れるようにした</li>
<li>UUIDはローカルで生成して送る方式と、DBの関数で生成する方式の両方を試した。ローカル生成は生成時間を抜いた時間で計測した</li>
</ul>
<p>検証コードはこちらに置いてあります。検証コードはアイコンが可愛いと話題のKiroで作成しています。</p>
<p>こちらが結果です。アプリで生成してから送る方式だと20%ぐらい時間が短くなりました。DB側で発番する場合はそのぶんちょっと時間が伸びるのでアプリ発番のものと同じグラフに載せない方が良いかもしれませんが、まあだいたい傾向としてはv7の方が早そう、というところが見えました。</p>
<p>Macbook AirのM3で計測しましたが、ファンがなく温度が上がると目に見えて性能が変わるので、条件違いのケースを1通り実行して、また繰り返して、というのを3回行って平均しました。まあそんな感じなのであまり細かいスコアは気にしないでください。</p>
<img src="/images/2025/20251006a/スクリーンショット_2025-10-03_17.39.45.png" alt="スクリーンショット_2025-10-03_17.39.45.png" width="735" height="450" loading="lazy">

<p>時間順のデータの範囲アクセスとか、直近のデータを頻繁にアクセスする場合にデータが一部のツリーに集まるので変更したい箇所のディスクキャッシュが効きやすくなりI&#x2F;Oパフォーマンスがあがります。これは書き込みのときだけではなく、読み込みにも効きます。</p>
<p>20レコードを検索するクエリーを1万回投げた時の処理時間の結果が以下のグラフです。100万行のデータのうち直近の5%(5万件)の範囲のIDをランダムに20個ピックアップし、SELECTで探すテストになっています。10%ほどUUID v7を使った方が高速にはなっています。なお、直近1%(1万件)の範囲で探索するとさらに10%ほど早くなりそうでした。</p>
<img src="/images/2025/20251006a/スクリーンショット_2025-10-03_18.38.25.png" alt="スクリーンショット_2025-10-03_18.38.25.png" width="753" height="455" loading="lazy">

<p>なお、「IDがランダムでも、範囲アクセスするキーがインデックスされていれば問題ないのでは？」と思われるかもしれません。たしかに「どのデータがマッチするか」はインデックスが作成されていれば高速にアクセスできるはずですが、インデックスを元に実際のデータを参照する場合にB-Treeをたどって参照するはずで、配置場所が集中していたらそこの部分が高速になる、ということのはずです。</p>
<h2 id="実はPostgreSQL-18以前でもUUIDv7は使える">実はPostgreSQL 18以前でもUUIDv7は使える</h2><p>上記の検証プログラムはPostgreSQL 17でもuuid v7のDB発番以外は使えます。</p>
<p>UUIDを使うメリットというのは、極めて重複しにくいキーをデータベース以外で作れるという点にあります。SERIALを使う場合、かならずデータベースへのアクセスが必要になります。ウェブフロントエンドから何かデータを登録する、それも一度にコミットできずに何回かに分けてデータを登録して完成させるようなケースを考えてみます。フロントエンドでキーを発番してもらい、それを使うのか、フロントエンドだけで発番し、仮データとして登録して最後にまとめてコミットするか、という違いが出ます。特に親子関係があり、親のキーを子供に渡さなければならないみたいなケースでやりとりが増えると大変です。</p>
<p>UUIDv7を使うケースで、今回のベンチマークアプリのようにv7形式のUUIDを事前に発行してそれをUUID型の主キーのデータのコミットに使うというやり方で、UUIDのメリットを受けつつ、パフォーマンスも劣化を減らすということが可能です。あくまでも、今回v18でできるようになったは「DB側での発番」であって、事前発番なら前のバージョンでも使えます。ここ大事です。</p>
<p>なお、今回は非同期I&#x2F;O対応で高速になったというのも見かけたのですが、同じプログラムで17と18で比べたところ、そこまで大きな差はなかったです。気持ち10%ぐらいは早くなった？</p>
<h2 id="まとめ">まとめ</h2><p>UUID v7のパフォーマンスを検証しました。v18の目玉機能的に言われることが多いのですが、前節で書いた通り、アプリ側で発番するのであればv18でなく使えます。今回増えたのはあくまでもDB側での発番です。</p>
<p>もちろん「v18になったら主キーはどんどんUUID v7にしていこう」というのは早計です。</p>
<ul>
<li>人間が目で見て扱う場合にはUUIDはユーザーフレンドリーとは言い難いのでSERIALが良いケースもある</li>
<li>型番や社員番号などのビジネス的に意味のあるナチュラルキーがユニーク性が担保できるのであればマスターデータなどではそちらを使うべき</li>
<li>セキュリティ的に日付でキーの範囲がだいぶ下がってしまうのでIDの推測可能性や絞り込みがだいぶ簡単になってしまうため、挿入や検索が多少劣化してもUUID v4が良いケースもある</li>
</ul>
]]></content>
    <summary type="html">PostgreSQL 18ではUUIDv7生成に対応しました。今までのUUID v4(完全ランダム)は主キーとして使うと、ソート順で扱おうとするPostgreSQLの内部構造のB-Treeと相性が悪く...</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="PostgreSQL" scheme="https://future-architect.github.io/tags/PostgreSQL/"/>
    <category term="PostgreSQL18" scheme="https://future-architect.github.io/tags/PostgreSQL18/"/>
    <category term="UUID" scheme="https://future-architect.github.io/tags/UUID/"/>
    <category term="インデックス" scheme="https://future-architect.github.io/tags/%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9/"/>
  </entry>
  <entry>
    <title>半年がかりのパーサー移行を成功に導いた戦略 ～Rust製SQLフォーマッター開発の裏側～</title>
    <link href="https://future-architect.github.io/articles/20251001a/"/>
    <id>https://future-architect.github.io/articles/20251001a/</id>
    <published>2025-09-30T15:00:00.000Z</published>
    <updated>2025-09-30T15:00:00.000Z</updated>
    <author><name>仲泰志</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>こんにちは！フューチャーでアルバイトをしている仲です。Rust 製 SQL フォーマッター uroboroSQL-fmt の開発に携わっています。</p>
<p>先日リリースされた uroboroSQL-fmt ver.1.0.0 では、フォーマッターの中核機能であるパーサーが新しい実装へと切り替わっています。</p>
<p>本記事では、実に半年ほどかけて実現したパーサーの移行の裏側についてお話しします。なぜパーサーを置き換えるに至ったのか説明したのちに、どのようにして安全に移行したのか、具体的な設計や検証の戦略を交えて紹介していきます。</p>
<h2 id="旧パーサー-vs-新パーサー">旧パーサー vs. 新パーサー</h2><p>これまでの uroboroSQL-fmt ではパーサーとして tree-sitter-sql を利用していましたが、開発を進める中でいくつかの課題が明らかになっていました。そのため、今回リリースされた Ver.1.0.0 では postgresql-cst-parser という新しいパーサーへ移行しています。</p>
<p>今回のアップデートについては以下のシリーズ記事でも詳しく解説しています。</p>
<ul>
<li>リリース概要: Pure Rustで生まれ変わったPostgreSQL公式構文準拠SQLフォーマッター「uroborosql-fmt」をリリース🎉</li>
<li>新パーサーの技術詳細: PostgreSQL 全構文対応の Pure Rust な CST パーサーを作ってみた</li>
</ul>
<p>移行先の新しいパーサーである postgresql-cst-parser は、 PostgreSQL が内部に持つ Bison の文法定義を利用して Rust のパーサーとして利用できるようにしたツールです。フューチャー社員である山田さんによって開発されました。詳しくはPostgreSQL 全構文対応の Pure Rust な CST パーサーを作ってみた をご覧ください。</p>
<p>本節では、旧パーサーの課題と新パーサーの利点について次に示す3つの観点に基づいて説明します。以下で詳しく説明しますが、新しいパーサーに移行することで、表のとおりすべての課題が解決しています。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="center">観点</th>
<th align="center">tree-sitter-sql</th>
<th align="center">postgresql-cst-parser</th>
</tr>
</thead>
<tbody><tr>
<td align="center">文法追従コスト</td>
<td align="center">逐一修正が必要</td>
<td align="center">PostgreSQL バージョンアップ時のみ対応すればよい</td>
</tr>
<tr>
<td align="center">WebAssembly 化の容易性</td>
<td align="center">低い（ビルドが複雑）</td>
<td align="center">高い（ビルドがシンプル）</td>
</tr>
<tr>
<td align="center">パーサーのサイズ</td>
<td align="center">構文追加で膨れ上がりやすい</td>
<td align="center">現実的なサイズに収まる</td>
</tr>
</tbody></table></div>
<h3 id="1-文法追従コスト">1. 文法追従コスト</h3><h4 id="旧パーサーの課題">旧パーサーの課題</h4><p>tree-sitter-sql は PostgreSQL が持つ全ての構文を網羅しているわけではありません。そのためフォーマッターが新しいSQL構文に対応しようとすると、まずパーサー（tree-sitter-sql）自体にその構文を追加する修正が必要でした。これには既存の文法を壊さないよう慎重な検討が求められるだけでなく tree-sitter へ知識も必要となるため、開発におけるボトルネックとなっていました。</p>
<h4 id="新パーサーによる解決策">新パーサーによる解決策</h4><p>postgresql-cst-parser はPostgreSQL本体の文法定義から生成されているパーサーです。そのため、原理的に PostgreSQL のほぼ全ての構文を最初からサポートしています。これにより、フォーマッターに新機能を追加する際にパーサーへ手を入れる必要がなくなり、開発者はフォーマット処理そのものに集中できるようになりました。</p>
<img fetchpriority="high" src="/images/2025/20251001a/新パーサー版文法追従コスト図.avif" alt="新パーサー版文法追従コスト図" width="1070" height="456">

<h3 id="2-WebAssembly-化の容易性">2. WebAssembly 化の容易性</h3><h4 id="旧パーサーの課題-1">旧パーサーの課題</h4><p>uroboroSQL-fmt は WebAssembly 版を提供していますが、tree-sitter は内部に C 言語への依存があるため、wasm-bindgen に代表される Rust 向けのお手軽かつ一般的なツールチェーンをそのまま適用できません。<br>そのため旧版では WebAssembly 版を提供するために Emscripten を利用していましたが、ビルドには tree-sitter-sql とフォーマッターを個別にビルドするという複雑な手順が必要でした。（詳細については C&#x2F;C++を呼び出しているRustのWASM化 をご覧ください。）</p>
<h4 id="新パーサーによる解決策-1">新パーサーによる解決策</h4><p>postgresql-cst-parser は 100% Rustで実装された Pure Rust のライブラリです。そのため、 Rust の標準的なツールチェーンで WebAssembly 化でき、 wasm-bindgen も利用可能です。wasm-bindgen を利用する場合、ビルドプロセスは <code>cargo build</code> と wasm-bindgen の実行だけで完結する単純な構成になるうえ、 JavaScript から呼び出す場合のコードも大幅に簡素化できます。</p>
<h3 id="3-パーサーのサイズ">3. パーサーのサイズ</h3><h4 id="旧パーサーの課題-2">旧パーサーの課題</h4><p>tree-sitter-sql は、対応する構文を増やすほど生成されるパーサーのファイルサイズが際限なく大きくなるという問題を抱えていました。フォーク元である m-novikov&#x2F;tree-sitter-sql では現在挙がっている PR をすべてマージするとパーサーのサイズが 83MB にもなるという指摘がなされており、パーサーのサイズに悩まされている様子がうかがえます。このような事情もあり、uroboroSQL-fmtでは使われない一部の構文を削ることでサイズを抑制しつつ、必要な構文への対応を追加するという構成をとっていました。</p>
<h4 id="新パーサーによる解決策-2">新パーサーによる解決策</h4><p>postgresql-cst-parser は、PostgreSQLの全構文をサポートしながらも、パーサーのサイズを現実的な範囲に抑えることができます。これにより、ファイルサイズを過度に心配することなく、全てのSQL構文をフォーマット対象とすることが可能になりました。</p>
<h2 id="移行を実現した実装戦術">移行を実現した実装戦術</h2><p>ここからは、新しいパーサーへの移行をどのように実現したのか、具体的な実装レベルでの戦術をご紹介します。影響範囲の大きいパーサーの置き換えを安全に進めるため、「互換API層の実装」「CSTの整形」「独立した並走実装」という3つのアプローチを取りました。これらの戦術が奏功し、移行作業時に大きな問題は発生しませんでした。また、リリースから3週間が経過した現在も移行に起因する不具合は1件も確認されていません。</p>
<h3 id="1-互換API層の実装">1. 互換API層の実装</h3><p>パーサーが持つインターフェースの差異が移行作業に及ぼす影響を低減するため、 postgresql-cst-parser に tree-sitter 互換の API を用意しました。<br>フォーマット処理の実装にあたり頻出する処理を tree-sitter の場合とできるだけ同じ手触りになるようにしています。</p>
<p>具体的には、<code>goto_parent</code>・<code>goto_first_child</code>・<code>goto_next_sibling</code>といった命令的なノード走査用の API を新たに実装したり、ノードのソースコード上の位置を示す形式を tree-sitter と統一したりしました。</p>
<h3 id="2-CSTの整形">2. CSTの整形</h3><p>postgresql-cst-parser は Bison の文法定義とほとんど同一の構造を持つCSTを返すため、そのままではフォーマッターとして扱いづらいことがあります。その場合はパーサーが返す木を整形しています。</p>
<p>例えば <code>target_list</code> のようなリストを表す構文は、grammar では再帰的に定義されます。</p>
<div class="code-block"><figure class="highlight plaintext"><input type="checkbox" id="code-wrap-1um6vip-1" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1um6vip-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">target_list:</span><br><span class="line">			target_el								&#123; $$ = list_make1($1); &#125;</span><br><span class="line">			| target_list &#x27;,&#x27; target_el				&#123; $$ = lappend($1, $3); &#125;</span><br><span class="line">		;</span><br></pre></td></tr></table></figure></div>

<p>パーサーはこの文法定義に従って再帰的な構造によってリストを表現しますが、このような場合は CST をフラット化することでフォーマッター側から扱いやすくしています。</p>
<img src="/images/2025/20251001a/再帰的なリスト構造をフラットなリストに変換する整形処理の例.png" alt="再帰的なリスト構造をフラットなリストに変換する整形処理の例" width="973" height="384" loading="lazy">

<h3 id="3-独立した並走実装">3. 独立した並走実装</h3><p>移行作業中は旧パーサー用の処理を丸ごと消して書き換え始めたりはせず、旧パーサーを扱う処理と新パーサーを扱う処理を共存させて実装を進めていきました。これには、次のような意図がありました。</p>
<ul>
<li>フォーマットオプションによって新旧パーサーの切り替えを可能にすることで挙動の比較を簡単にする</li>
<li>パーサー関連の処理以外のバグ修正などを取り込みやすくしておく</li>
</ul>
<p>uroboroSQL-fmt の処理の流れは次のようになっており、主な処理はモジュールとして分割されています。基本的にはパーサーが返す CST を走査してフォーマッター用の木構造に変換する <code>visitor</code> モジュールと、フォーマッター用の木構造を定義し、フォーマット結果の書き出しまで行う <code>cst</code> モジュールから構成されます。（図はパーサー置き換え前のものです）</p>
<img src="/images/2025/20251001a/旧版モジュール構成図.png" alt="旧版モジュール構成図" width="1046" height="202" loading="lazy">

<p>次の図は、パーサー移行中の構成を示したものです。移行にあたっては、<code>visitor</code> モジュールと並ぶ <code>new_visitor</code> モジュールを新たに作成し、新パーサーに依存する処理はこのモジュールに閉じる形で実装しました。これにより移行中は新旧版のフォーマッターを共存させてフォーマットオプションでのパーサー選択を可能にする構成としていました。</p>
<img src="/images/2025/20251001a/移行作業中モジュール構成図.png" alt="移行作業中モジュール構成図" width="1048" height="333" loading="lazy">

<p>また、移行作業終了時には旧パーサー用のモジュール（<code>visitor</code>）を丸ごと削除することで簡単にクリーンアップができます。</p>
<img src="/images/2025/20251001a/移行作業終了時モジュール構成図.png" alt="移行作業終了時モジュール構成図" width="1200" height="431" loading="lazy">

<h2 id="安全に移行を進めるための検証設計">安全に移行を進めるための検証設計</h2><p>前節で述べたような実装戦術と並行し、移行の安全性を担保するための検証も入念に行いました。</p>
<p>パーサーという根幹部分の置き換えでは意図しないリグレッション発生のリスクが常に伴います。そこで、実装によって生じうるリスクを確実に潰していくため、「安全に小さく進める」という方針のもとで検証プロセスを設計しました。</p>
<p>具体的には、次のような三段構えとしました。</p>
<ol>
<li>段階的なE2Eテストで外部仕様を固定する</li>
<li>カバレッジ計測で進捗と抜け漏れを可視化する</li>
<li>実データ（社内の複数プロジェクトに存在する大量のSQL）でリグレッションを洗い出す</li>
</ol>
<p>それぞれについて以下で詳しく説明します。</p>
<h3 id="1-段階的なE2Eテストの利用">1. 段階的なE2Eテストの利用</h3><p>移行作業を安全かつ着実に進めるため、既存のテストケースとは独立した移行用のテストを新設し、それを起点にテスト駆動での実装を進める方針を取りました。</p>
<p>このテストでは入力と期待値にそれぞれ SQL 文を用意し、フォーマッターの挙動をエンドツーエンドで確認しながら実装に伴ってテストケースを増やしていきます。小さくはじめて、徐々に対応範囲を広げていくイメージです。</p>
<p>具体的には <code>select</code> のような最も単純なSQLから始めて、 <code>select 1;</code> → <code>select a;</code> → <code>select a,b;</code> → <code>select a,b from t;</code> のように機能を一段ずつ拡張していきました。それぞれのテストケースはこのように独立したファイルとして管理し、テストから読み込んで利用しています。</p>
<img src="/images/2025/20251001a/VSCodeの画面のキャプチャ.png" alt="VSCodeの画面のキャプチャ。テストケースのファイルが画面左半分のエクスプローラーで並んでいる。画面右半分は実際のテストケースあるSQLファイルの内容が表示されている。" width="606" height="359" loading="lazy">

<p>また、テストごとの結果を個別に表示して、すでに実装した機能に及ぼす影響を確認できるようにしています。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">Testing: 001_select</span><br><span class="line">✅ Test passed</span><br><span class="line"></span><br><span class="line">Testing: 002_select_semicolon</span><br><span class="line">✅ Test passed</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line">Testing: 071_insert_select_paren</span><br><span class="line">✅ Test passed</span><br><span class="line"></span><br><span class="line">Testing: 072_insert_values_without_column</span><br><span class="line">✅ Test passed</span><br><span class="line"></span><br><span class="line">Test Report:</span><br><span class="line">Total <span class="built_in">test</span> cases:   72 cases</span><br><span class="line">✅ Passed       :   72 cases</span><br><span class="line">❌ Failed       :    0 cases</span><br><span class="line">💥 Errors       :    0 cases</span><br><span class="line"><span class="built_in">test</span> test_normal_cases ... ok</span><br></pre></td></tr></table></figure>

<p>さらに、フォーマット結果の差分がすぐに把握できるよう、 <code>similar</code> クレートを活用した Diff 表示機能なども導入していました。</p>
<img src="/images/2025/20251001a/フォーマッ.png" alt="フォーマッ" width="727" height="294" loading="lazy">

<h3 id="2-カバレッジ管理">2. カバレッジ管理</h3><p>移行の拡大に伴い、進捗報告とタスクの整理が課題になることが見込まれました。そのため「現在の進捗がどの程度か」や「次に何を実装すべきか」の目安を判断する指標としてカバレッジ計測を取り入れました。</p>
<p>ここでのカバレッジは「既存のテストケースを新パーサーの実装がパスする割合」のことを指しています。既存のテストケース群に対して新パーサーでのフォーマット処理を実行し、その結果を逐一集計していました。</p>
<p>次のようなカバレッジレポート表示を実装することで進捗が一目でわかります。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1um6vip-2" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1um6vip-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">Coverage Report:</span><br><span class="line">Total <span class="built_in">test</span> cases:   83 cases</span><br><span class="line">✅ Supported    :   54 cases,   65.1%</span><br><span class="line">⏭️ Skipped      :   18 cases,   21.7%</span><br><span class="line">❌ Unsupported  :   11 cases,   13.3%</span><br><span class="line"></span><br><span class="line">By Category:</span><br><span class="line">  2way_sql              :   0/8   (   0.0%) [Skipped:   8]</span><br><span class="line">  2way_sql(doma)        :   0/5   (   0.0%) [Skipped:   5]</span><br><span class="line">  2way_sql(go-twowaysql):   0/5   (   0.0%) [Skipped:   5]</span><br><span class="line">  comment               :   8/10  (  80.0%) [Skipped:   0]</span><br><span class="line">  delete                :   3/4   (  75.0%) [Skipped:   0]</span><br><span class="line">  insert                :   3/7   (  42.9%) [Skipped:   0]</span><br><span class="line">  <span class="keyword">select</span>                :  36/38  (  94.7%) [Skipped:   0]</span><br><span class="line">  update                :   4/6   (  66.7%) [Skipped:   0]</span><br></pre></td></tr></table></figure></div>

<p>また、カバレッジ計測時に生じたエラーの原因を収集・分類することで次に対応すべき機能を決めたりしていました。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1um6vip-3" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1um6vip-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">Failed Cases (by error <span class="built_in">type</span>):</span><br><span class="line"></span><br><span class="line">Syntax Errors:</span><br><span class="line">  testfiles/src/comment/many_comments.sql - ❌ Syntax error: visit_a_expr_or_b_expr(): Unexpected syntax. node: C_COMMENT</span><br><span class="line">  testfiles/src/comment/paren_with_comment.sql - ❌ Syntax error: visit_a_expr_or_b_expr(): Unexpected syntax. node: C_COMMENT</span><br><span class="line"></span><br><span class="line">Validation Errors:</span><br><span class="line">  testfiles/src/insert/insert_select.sql - ❌ Validation error: different kind token: Errors have occurred near the following token</span><br><span class="line"></span><br><span class="line">Unimplemented Features:</span><br><span class="line">  testfiles/src/delete/with.sql - ❌ Unimplemented: visit_preparable_stmt: UpdateStmt is not implemented</span><br><span class="line">  testfiles/src/insert/insert_on_conflict.sql - ❌ Unimplemented: visit_insert_stmt(): opt_on_conflict is not implemented</span><br><span class="line">  testfiles/src/insert/with.sql - ❌ Unimplemented: visit_preparable_stmt: UpdateStmt is not implemented</span><br><span class="line">  testfiles/src/select/with.sql - ❌ Unimplemented: visit_preparable_stmt: UpdateStmt is not implemented</span><br><span class="line">  testfiles/src/update/with.sql - ❌ Unimplemented: visit_preparable_stmt: UpdateStmt is not implemented</span><br><span class="line"></span><br><span class="line">Other Errors:</span><br><span class="line">  testfiles/src/insert/insert_returning.sql - Formatting result does not match</span><br><span class="line">  testfiles/src/select/complement_alias.sql - Formatting result does not match</span><br><span class="line">  testfiles/src/update/update_returning.sql - Formatting result does not match</span><br></pre></td></tr></table></figure></div>

<h3 id="3-実データでの検証">3. 実データでの検証</h3><p>移行作業の最終段では、社内で実際に利用されている SQL を用いてリグレッションを検証しました。5400 件ほどのSQLファイルを新旧フォーマッターでそれぞれフォーマットしてしてエラーを集計・分析し、デグレを洗い出しつつ修正対応を進めました。</p>
<p>パーサーが違えば返すCSTも異なるため既存のテストケースでは不足だろうとの試算はあったものの、実際の検証では実に半数ほどのSQLで1つ以上のエラーが見つかりました。</p>
<p>修正の都度検証を実施し、発生したエラーを集計して確認して影響範囲の大きいものから対処していくことで着実にリグレッションを減らしていきました。</p>
<img src="/images/2025/20251001a/修正に伴い.png" alt="修正に伴い" width="1200" height="493" loading="lazy">

<h2 id="新旧フォーマッターの比較">新旧フォーマッターの比較</h2><p>最後に、新旧フォーマッターの実行ファイルのサイズとパフォーマンスについて比較した結果を報告します。</p>
<h3 id="1-実行ファイルのサイズ">1. 実行ファイルのサイズ</h3><p>実行ファイルのサイズ比較結果を以下に示します。新パーサーを利用しているバージョンのフォーマッター（グラフ右）では、旧パーサーの場合（グラフ中央）に比べて0.6MBほど増加しています。</p>
<p>ただし、旧パーサーである tree-sitter-sql は対応文法追加のためにあまり使われない構文を削ることでサイズを抑えている事情があります。サイズを抑える前の tree-sitter-sql を利用する場合は9.6MB（グラフ左）となり、2倍近い数値です。この点を踏まえれば、ファイルサイズを大幅に増やすことなくすべての文法に対応できたという意味で好ましい現象であると考えています。</p>
<img src="/images/2025/20251001a/フォーマッターの実行ファイルサイズを比較するグラフ.png" alt="フォーマッターの実行ファイルサイズを比較するグラフ。tree-sitter-sql(fork元)版は9.6MB、tree-sitter-sql 版は4.5MB、postgresql-cst-parser 版は5.1MBとなっている" width="724" height="480" loading="lazy">

<h3 id="2-パフォーマンス">2. パフォーマンス</h3><p>フォーマッターの性能をより実態に即して評価するため、社内で実際に使われているSQLファイル約5400件を用いて、新旧パーサーの1ファイルあたりの処理時間を比較しました。</p>
<p>計測結果の統計値は以下の通りです。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="center"></th>
<th align="right">旧版 (ms)</th>
<th align="right">新版 (ms)</th>
<th align="right">新版 &#x2F; 旧版</th>
</tr>
</thead>
<tbody><tr>
<td align="center">平均値</td>
<td align="right">1.822</td>
<td align="right">3.744</td>
<td align="right">2.1 倍</td>
</tr>
<tr>
<td align="center">中央値</td>
<td align="right">0.872</td>
<td align="right">1.965</td>
<td align="right">2.3 倍</td>
</tr>
<tr>
<td align="center">最小値</td>
<td align="right">0.033</td>
<td align="right">0.224</td>
<td align="right">6.8 倍</td>
</tr>
<tr>
<td align="center">最大値</td>
<td align="right">78.587</td>
<td align="right">160.410</td>
<td align="right">2.0 倍</td>
</tr>
</tbody></table></div>
<p>結果を見ると、平均値・中央値ともに新パーサーは旧版に比べ処理に約2倍強の時間がかかる傾向が見られます。</p>
<p>最も性能差が大きかったケース（最小値）では約6.8倍の時間がかかっていますが、これはもともとの処理時間が非常に短いSQLのため比率が大きくなったもので、絶対時間としては0.224ミリ秒とごくわずかです。また、最も時間のかかったケース（最大値）でも処理時間は約160ミリ秒でした。</p>
<p>このような結果は PostgreSQL の全構文への対応に対するトレードオフであると捉えています。多数のファイルで計測した結果からも実用上のパフォーマンスは十分に維持できており、ユーザー体験を損なうものではないと考えています。</p>
<h2 id="さいごに">さいごに</h2><p>本記事では、uroboroSQL-fmt のパーサーを tree-sitter-sql から postgresql-cst-parser へと移行したプロジェクトについてご紹介しました。</p>
<p>テストや継続的な計測・検証について工夫して実装しながら実現していく作業は技術的にも非常に面白く、大規模な書き換えを楽しみながらやり遂げることができました。</p>
<p>パーサーの問題を克服した uroboroSQL-fmt ですが、フォーマッターとしてはまだまだ対応できていない構文も多く残っています。バグ報告や機能要望など GitHub にて歓迎していますのでぜひ一度お試しください。</p>
]]></content>
    <summary type="html">uroboroSQL-fmt ver.1.0.0 では、フォーマッターの中核機能であるパーサーが新しい実装へと切り替わっています。本記事では、実に半年ほどかけて実現したパーサーの移行の裏側についてお話しします。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="Rust" scheme="https://future-architect.github.io/tags/Rust/"/>
    <category term="uroboroSQL" scheme="https://future-architect.github.io/tags/uroboroSQL/"/>
    <category term="フォーマッター" scheme="https://future-architect.github.io/tags/%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%82%BF%E3%83%BC/"/>
    <category term="構文解析" scheme="https://future-architect.github.io/tags/%E6%A7%8B%E6%96%87%E8%A7%A3%E6%9E%90/"/>
  </entry>
  <entry>
    <title>PostgreSQL 全構文対応の Pure Rust な CST パーサーを作ってみた</title>
    <link href="https://future-architect.github.io/articles/20250930a/"/>
    <id>https://future-architect.github.io/articles/20250930a/</id>
    <published>2025-09-29T15:00:00.000Z</published>
    <updated>2025-09-29T15:00:00.000Z</updated>
    <author><name>山田修路</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>こんにちは、Core Technology Groupの山田です。<br>PostgreSQL のフォーマッターである uroborosql-fmt<sup id="fnref:1">1</sup> の開発に携わっています。</p>
<p>先日リリースされたuroboroSQL-fmt ver1.0.0では、過去バージョンの課題を解決するために、新たに自作したパーサーを利用するように変更しました。</p>
<p>自作したパーサーであるpostgresql-cst-parserはこちらのリポジトリに置いています。</p>
<ol>
<li>tanzaku&#x2F;postgresql-cst-parser</li>
<li>future-architect&#x2F;postgresql-cst-parser</li>
</ol>
<p>個人リポジトリの方は実験的に作成していた際のリポジトリで汎用的なパーサーとしての機能のみ実装されており、当社の organization 配下の方はフォーマッターへ組み込むために機能拡張したものです。</p>
<p>今回は過去uroborosql-fmtで使用していたパーサーの課題の紹介と、その課題を解決するためにパーサーを自作した話を紹介します。</p>
<p>今回のアップデートについては、以下のシリーズ記事でも詳しく解説しています。</p>
<ul>
<li>リリース概要: Pure Rustで生まれ変わったPostgreSQL公式構文準拠SQLフォーマッター「uroborosql-fmt」をリリース🎉</li>
<li>パーサーの置き換え戦略: 半年がかりのパーサー移行を成功に導いた戦略 ～Rust製SQLフォーマッター開発の裏側～</li>
</ul>
<div class="note-container note-info"><span class="note-icon"></span><div>

<p>本記事のAppendixではflex・bisonの定義ファイルの構造、2WaySQLのエラー回復について説明していますが、発展的な内容であるため、興味のある方以外は読み飛ばしていただいて問題ありません。</p>
</div></div>

<h2 id="背景">背景</h2><p>uroborosql-fmt では tree-sitter-sql という CST パーサーを用いてパースしていました<sup id="fnref:2">2</sup>。</p>
<p>開発初期には当時存在したパーサーを調査し最も有望そうなものを選定しましたが、開発を進めるにつれて tree-sitter-sql の課題<sup id="fnref:3">3</sup>が浮き彫りになってきました。</p>
<h3 id="1-サポートされている文法が少なく-grammar-の改善にも工数がかかる">1. サポートされている文法が少なく grammar の改善にも工数がかかる</h3><p>tree-sitter-sql でサポートされている文法が少なく、フォーマッター改修の際に tree-sitter-sql の変更が伴うことによる工数増や、そもそも tree-sitter-sql に手を入れられる人でないと uroborosql-fmt の改善ができないといった問題がありました。</p>
<h3 id="2-wasm-にコンパイルする際に-wasm-bindgen-を使用できない">2. wasm にコンパイルする際に wasm-bindgen を使用できない</h3><p>tree-sitter は C のコードを出力するため、 FFI で uroborosql-fmt からCの関数を呼び出すつくりになっています。 C への FFI を含むため wasm32-unknown-unknown ターゲットでビルドできず、 wasm-bindgen を利用できませんでした。そこで emscripten でビルドすることで wasm にコンパイルしていますが、以下の2点の理由でいまいちでした。</p>
<ol>
<li>emscripten でビルドしているのでjavascriptから呼び出す際にシンプルにかけない</li>
<li>uroborosql-fmt 全体をemscriptenターゲットでビルドするとエラーになるため、tree-sitter-sqlパッケージのみ単体で先にビルドしておくという特殊な手順で何とか回避している</li>
</ol>
<p>これらの課題を満たすライブラリが存在しなかった<sup id="fnref:4">4</sup>ため自作してみました。</p>
<h2 id="方針">方針</h2><p>PostgreSQL のパーサーを Rust に porting することで全ての文法をカバーするパーサーを作成できるのではないかと考え、その方針で進めました。</p>
<h3 id="PostgreSQLのパーサーの流れ">PostgreSQLのパーサーの流れ</h3><p>PostgreSQL のパーサー<sup id="fnref:5">5</sup>はこのような構成だと認識しています。</p>
<ol>
<li>入力の読み込み</li>
<li>字句解析器による字句解析<sup id="fnref:6">6</sup></li>
<li>前段で作成したトークンの書換え<sup id="fnref:7">7</sup></li>
<li>構文解析器で構文解析<sup id="fnref:8">8</sup></li>
</ol>
<p>パース過程のイメージ画像<br><img fetchpriority="high" src="/images/2025/20250930a/2025-02-28_14h09_26.png" alt="2025-02-28_14h09_26.png" width="983" height="855"></p>
<h3 id="porting-の方針">porting の方針</h3><p>以下の方針で porting を行いました。</p>
<ol>
<li>字句解析器の定義ファイル (scan.l) は規模が小さく更新頻度も高くないため、手で Rust に porting</li>
<li>トークンの書換え処理も同様に手で Rust に porting</li>
<li>構文解析器は定義ファイル (gram.y) をパースし、文法定義から構文解析表を生成しパーサーを生成</li>
<li>構文解析表は比較的大きいサイズなので、wasmにした際のサイズを抑えるために圧縮</li>
</ol>
<h2 id="対応">対応</h2><p>具体的な対応内容を紹介します。</p>
<h3 id="1-PostgreSQL-のソースにパッチ適用">1. PostgreSQL のソースにパッチ適用</h3><p>PostgreSQL の素の字句解析器ではコメントはスキップされてしまうため、コメントもトークン化できるよう PostgreSQL のソースに libpg_query のパッチを適用しました。</p>
<h3 id="2-字句解析器の-C-のソースを-porting">2. 字句解析器の C のソースを porting</h3><p>字句解析器の定義ファイル (scan.l) に含まれる C のコードを Rust に porting していきました。また、字句解析器の処理で キーワードか否かを判定しているところ があるため、kwlist.hをパースしキーワードの一覧を作成なども行っています。<br>その後、定義やルールを読み込んで、porting した Rust コードとあわせて字句解析器を生成しました。</p>
<p>flex の定義ファイルの構造についての簡単な説明は、Appendix.1 flex の定義ファイルの構造をご覧ください。</p>
<h3 id="3-構文解析器の-porting">3. 構文解析器の porting</h3><p>構文解析器の定義ファイル (gram.y) については一切変更を入れず、ファイルの内容を読み込んで構文解析表<sup id="fnref:9">9</sup>を生成しました。<br>また、構文解析表を基にLR構文解析を実施する処理は bison の porting ではなく独自にLALR法を実装しました。</p>
<p>bison の定義ファイルの構造についての簡単な説明は、Appendix. 2 bison の定義ファイルの構造をご覧ください。</p>
<h3 id="4-構文解析表の圧縮">4. 構文解析表の圧縮</h3><p>構文解析表はサイズがやや大きく、wasm のサイズを小さくするために圧縮して埋め込み、実行時に展開しています。</p>
<h2 id="完成">完成</h2><p>こちらでデモを用意しているので、興味のある方はぜひ試してみてください。<br>gram.y で定義された grammar 通りの CST が出来るため決して使い勝手のよい形のツリーではありませんが、PostgreSQLの全ての構文をカバーしており、フォーマッターに利用するものとしては十分なものが出来たと考えています。</p>
<p>また当初想定していなかったメリットとして、2WaySQL で出てくる通常のパーサーではパースエラーとなるような SQL も自然とパースする仕組みを作れるようになりました。<br>興味があれば Appendix.3. 2WaySQL のエラー回復をご参照ください。</p>
<h2 id="さいごに">さいごに</h2><p>最初は実験的なコードの予定でしたが、uroborosql-fmt をより発展させていくためにパーサーを置き替えることになり、約半年かけて置き換えを実施しました。自作のパーサーに置き替えることで PostgreSQL のバージョンアップに追従するコストや保守コストなどがかかってくるため負の側面が大きいことも理解していますが、よりよいツールを開発し、生産性や品質を高めるために頑張っていきたいと思っています。</p>
<p>crateもリリースしておいたので、興味があればぜひ使ってみてください。</p>
<ul>
<li>postgresql-cst-parser - crates.io: Rust Package Registry</li>
</ul>
<h2 id="Appendix">Appendix</h2><h3 id="1-flex-の定義ファイルの構造">1. flex の定義ファイルの構造</h3><p>flex の定義ファイルは以下のような構造になっています。<br>※ PostgreSQL の定義ファイルの雰囲気を理解できる最低限の内容に絞っています。</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">定義セクション</span><br><span class="line">%%</span><br><span class="line">ルールセクション</span><br><span class="line">%%</span><br><span class="line">ユーザーコード</span><br></pre></td></tr></table></figure>

<h4 id="定義セクション">定義セクション</h4><p>定義セクションでは、字句解析器の状態と、ルールセクションで使用するパターンを定義できます。</p>
<p><code>%x</code> で始まる行は字句解析器の状態定義です。<br>各状態の説明は以下に記載があります。<br>postgres&#x2F;src&#x2F;backend&#x2F;parser&#x2F;scan.l at REL_16_STABLE · postgres&#x2F;postgres</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">%x xb</span><br><span class="line">%x xc</span><br><span class="line">%x xd</span><br><span class="line">%x xh</span><br><span class="line">%x xq</span><br><span class="line">%x xqs</span><br><span class="line">%x xe</span><br><span class="line">%x xdolq</span><br><span class="line">%x xui</span><br><span class="line">%x xus</span><br><span class="line">%x xeu</span><br></pre></td></tr></table></figure>

<p>ルールセクションで使用するパターンの定義は以下のように行うことができます。<br>この例では、数字一桁と数字複数桁の定義である decdigit, decinteger の定義が行われています。</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">decdigit		[0-9]</span><br><span class="line">decinteger		&#123;decdigit&#125;(_?&#123;decdigit&#125;)*</span><br></pre></td></tr></table></figure>

<h4 id="ルールセクション">ルールセクション</h4><p>ルールセクションは字句解析器のメインの部分で、状態ごとにルールにマッチした際のアクションを定義できます。<br>アクションはCのコードになっており、この中で字句解析器の状態を変更したり、トークンを受理できます。<br>特に状態が指定されていない場合は、INITIALというデフォルト状態のパターンとして認識されます。</p>
<div class="code-block"><figure class="highlight c"><input type="checkbox" id="code-wrap-ttb36-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-ttb36-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">/* 単一パターンの書き方 */</span></span><br><span class="line">パターン		アクション</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 同一状態の複数パターンをまとめて記述する書き方 */</span></span><br><span class="line">&lt;状態名&gt;&#123;</span><br><span class="line">パターン		アクション</span><br><span class="line">パターン		アクション</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 特定状態の単一パターンのみ記述する書き方 */</span></span><br><span class="line">&lt;状態名<span class="number">1</span>,状態名<span class="number">2</span>,...&gt;パターン	アクション</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 複数のパターンのアクションをまとめる書き方 */</span></span><br><span class="line">&lt;状態名&gt;パターン<span class="number">1</span>	|</span><br><span class="line">&lt;状態名&gt;パターン<span class="number">2</span>	アクション</span><br></pre></td></tr></table></figure></div>

<h5 id="状態遷移の例">状態遷移の例</h5><p>アクション部で <code>BEGIN(遷移先の状態)</code> と書くことで、別の状態に遷移できます。<br>以下の例では <code>&#123;xcstart&#125;</code> のパターンにマッチした際に、<code>BEGIN(xc)</code> を呼び出すことでC形式のコメント内部を表す xc に遷移しています。</p>
<div class="code-block"><figure class="highlight plaintext"><input type="checkbox" id="code-wrap-ttb36-2" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-ttb36-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">&#123;xcstart&#125;		&#123;</span><br><span class="line">					/* Set location in case of syntax error in comment */</span><br><span class="line">					SET_YYLLOC();</span><br><span class="line">					yyextra-&gt;xcdepth = 0;</span><br><span class="line">					BEGIN(xc);</span><br><span class="line">					/* Put back any characters past slash-star; see above */</span><br><span class="line">					yyless(2);</span><br><span class="line">				&#125;</span><br></pre></td></tr></table></figure></div>

<h5 id="余談">余談</h5><p>定義ファイルを見ると、これまで知らなかった書き方を知ることがあります。</p>
<p>一例ですが <code>select n&#39;1&#39;;</code> のように文字列の前に <code>n</code> というプレフィックスを付与することが定義できるというのは、この作業を通じて知りました。</p>
<p>この仕様はドキュメントでは見つけられませんでしたが、flex の定義ファイルの以下の部分を眺めることで知ることができ、こういったマイナーな書き方の発見もパーサー作成の面白さの1つだなと感じました。</p>
<div class="code-block"><figure class="highlight plaintext"><input type="checkbox" id="code-wrap-ttb36-3" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-ttb36-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">/* National character */</span><br><span class="line">xnstart			[nN]&#123;quote&#125;</span><br><span class="line"></span><br><span class="line">&#123;xnstart&#125;		&#123;</span><br><span class="line">					/* National character.</span><br><span class="line">					 * We will pass this along as a normal character string,</span><br><span class="line">					 * but preceded with an internally-generated &quot;NCHAR&quot;.</span><br><span class="line">					 */</span><br><span class="line">					int		kwnum;</span><br><span class="line"></span><br><span class="line">					SET_YYLLOC();</span><br><span class="line">					yyless(1);	/* eat only &#x27;n&#x27; this time */</span><br><span class="line"></span><br><span class="line">					kwnum = ScanKeywordLookup(&quot;nchar&quot;,</span><br><span class="line">											  yyextra-&gt;keywordlist);</span><br><span class="line">					if (kwnum &gt;= 0)</span><br><span class="line">					&#123;</span><br><span class="line">						yylval-&gt;keyword = GetScanKeyword(kwnum,</span><br><span class="line">														 yyextra-&gt;keywordlist);</span><br><span class="line">						return yyextra-&gt;keyword_tokens[kwnum];</span><br><span class="line">					&#125;</span><br><span class="line">					else</span><br><span class="line">					&#123;</span><br><span class="line">						/* If NCHAR isn&#x27;t a keyword, just return &quot;n&quot; */</span><br><span class="line">						yylval-&gt;str = pstrdup(&quot;n&quot;);</span><br><span class="line">						return IDENT;</span><br><span class="line">					&#125;</span><br><span class="line">				&#125;</span><br></pre></td></tr></table></figure></div>

<h4 id="参考">参考</h4><ol>
<li>Lexical Analysis With Flex, for Flex 2.6.2: Format https://westes.github.io/flex/manual/Format.html#Format</li>
<li>Flex: 3. Flex記述言語 https://web.sfc.wide.ad.jp/~sagawa/gnujdoc/flex-2.5.4/flex-ja_3.html</li>
<li>Flex - Flex記述言語 https://www.asahi-net.or.jp/~wg5k-ickw/html/online/flex-2.5.4/flex_5.html</li>
</ol>
<h3 id="2-bison-の定義ファイルの構造">2. bison の定義ファイルの構造</h3><p>bison の定義ファイルは以下のような構造になっています。<br>※ PostgreSQL の定義ファイルの雰囲気を理解できる最低限の内容に絞っています。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line">%&#123;</span><br><span class="line">C宣言部（C declarations）</span><br><span class="line">%&#125;</span><br><span class="line"></span><br><span class="line">Bison宣言部（Bison declarations）</span><br><span class="line"></span><br><span class="line">%%</span><br><span class="line">文法規則部（Grammar rules）</span><br><span class="line">%%</span><br><span class="line"></span><br><span class="line">追加のCプログラム部（Additional C code）</span><br></pre></td></tr></table></figure>

<p>上の構造はこちら に記載されています。</p>
<h4 id="Bison宣言部">Bison宣言部</h4><h5 id="1-tokenの宣言">1. tokenの宣言</h5><p>パーサーで使用するトークンを宣言できます。</p>
<div class="code-block"><figure class="highlight plaintext"><input type="checkbox" id="code-wrap-ttb36-4" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-ttb36-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">%token &lt;str&gt;	IDENT UIDENT FCONST SCONST USCONST BCONST XCONST Op</span><br><span class="line">%token &lt;ival&gt;	ICONST PARAM</span><br><span class="line">%token			TYPECAST DOT_DOT COLON_EQUALS EQUALS_GREATER</span><br><span class="line">%token			LESS_EQUALS GREATER_EQUALS NOT_EQUALS</span><br></pre></td></tr></table></figure></div>

<h5 id="2-結合性の宣言">2. 結合性の宣言</h5><p>結合性を宣言できます。<br>%leftで左結合、%rightで右結合、%nonassocで無結合を宣言できます。<br>また優先度が低い順に記載されており、この例では OR より AND の方が優先度が高いことがわかります。</p>
<div class="code-block"><figure class="highlight plaintext"><input type="checkbox" id="code-wrap-ttb36-5" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-ttb36-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">/* Precedence: lowest to highest */</span><br><span class="line">%left		UNION EXCEPT</span><br><span class="line">%left		INTERSECT</span><br><span class="line">%left		OR</span><br><span class="line">%left		AND</span><br><span class="line">%right		NOT</span><br><span class="line">%nonassoc	IS ISNULL NOTNULL	/* IS sets precedence for IS NULL, etc */</span><br><span class="line">%nonassoc	&#x27;&lt;&#x27; &#x27;&gt;&#x27; &#x27;=&#x27; LESS_EQUALS GREATER_EQUALS NOT_EQUALS</span><br><span class="line">%nonassoc	BETWEEN IN_P LIKE ILIKE SIMILAR NOT_LA</span><br><span class="line">%nonassoc	ESCAPE			/* ESCAPE must be just above LIKE/ILIKE/SIMILAR */</span><br></pre></td></tr></table></figure></div>

<h4 id="文法規則部">文法規則部</h4><p>以下のような形式で文法規則が与えられます。</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">非終端記号: ルール1 &#123; アクション1 &#125;</span><br><span class="line">         | ルール2 &#123; アクション2 &#125;</span><br></pre></td></tr></table></figure>

<p>具体例は以下のようになります。</p>
<div class="code-block"><figure class="highlight plaintext"><input type="checkbox" id="code-wrap-ttb36-6" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-ttb36-6" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">parse_toplevel:</span><br><span class="line">			stmtmulti</span><br><span class="line">			&#123;</span><br><span class="line">				pg_yyget_extra(yyscanner)-&gt;parsetree = $1;</span><br><span class="line">				(void) yynerrs;		/* suppress compiler warning */</span><br><span class="line">			&#125;</span><br><span class="line">			| MODE_TYPE_NAME Typename</span><br><span class="line">			&#123;</span><br><span class="line">				pg_yyget_extra(yyscanner)-&gt;parsetree = list_make1($2);</span><br><span class="line">			&#125;</span><br><span class="line">			...</span><br><span class="line">		;</span><br></pre></td></tr></table></figure></div>

<p>各ルールは優先度を持ち、優先度はそのルールにでてくる最後の終端記号と同じになります。<br>デフォルトとは異なる優先度を設定する場合、以下のように <code>%prec</code> で指定できます。</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">SelectStmt: select_no_parens			%prec UMINUS</span><br><span class="line">			| select_with_parens		%prec UMINUS</span><br><span class="line">		;</span><br></pre></td></tr></table></figure>

<h4 id="参考-1">参考</h4><ol>
<li>Bison 3.8.1 https://www.gnu.org/software/bison/manual/bison.html</li>
<li>Bison 1.28 - Bison文法ファイル https://guppy.eng.kagawa-u.ac.jp/2019/Compiler/bison-1.2.8/bison-ja_6.html</li>
<li>Bison入門: 1. Bisonの概念 https://web.sfc.wide.ad.jp/~sagawa/gnujdoc/bison-1.28/bison-ja_4.html</li>
</ol>
<h3 id="3-2WaySQL-のエラー回復">3. 2WaySQL のエラー回復</h3><h4 id="2WaySQLとは">2WaySQLとは</h4><p>コメントで制御フローやバインド変数などを記述し、動的に SQL を生成するような実行方法です。動的生成に関わる部分がコメントで記載されており、単体の SQL 文としても実行できるため 2Way と呼ばれています。<br>上述の通り 2WaySQL は基本的には単体の SQL ファイルとして実行可能になっていますが、時に単体では SQL として実行できないようなケースも存在します。</p>
<p>当社で開発、公開しているOSSである uroboroSQL も 2WaySQL が利用可能なライブラリで、uroborosql-fmt は 2WaySQL のフォーマットもサポートしています。以下の説明では uroboroSQL の 2WaySQL 前提としています。</p>
<h4 id="1-バインドパラメータのサンプル値漏れ">1. バインドパラメータのサンプル値漏れ</h4><p>uroboroSQLでは、以下のようにバインドパラメータをセットできます。この SQL を uroboroSQL を使わずに実行した場合は name カラムに foo という値がセットされ、uroboroSQL で実行した場合には ‘foo’ は無視され name にバインドした値で置き替えられます。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line">	<span class="comment">/*name*/</span><span class="string">&#x27;foo&#x27;</span> <span class="keyword">as</span> name</span><br><span class="line">;</span><br></pre></td></tr></table></figure>

<p>この ‘foo’ のように、バインドパラメータ等の直後の仮置きの値のことをサンプル値と呼んでいます。<br>サンプル値が無くても uroboroSQL では実行できてしまうため、以下のようにサンプル値のないSQLが作成されてしまうことがあります。サンプル値漏れのSQLは一般的なパーサーではエラーになってしまいますが、新パーサーではサンプル値漏れでエラーになるケースでエラー回復できます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-ttb36-7" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-ttb36-7" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line">	<span class="comment">/*name*/</span> <span class="keyword">as</span> name <span class="comment">-- バインドパラメータのサンプル値が欠けている</span></span><br><span class="line">;</span><br></pre></td></tr></table></figure></div>

<h4 id="2-置換文字列のサンプル値漏れ">2. 置換文字列のサンプル値漏れ</h4><p>以下のような置換文字列でも同様にサンプル値が漏れていることがありますが、こちらも同様にエラーを回復できます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-ttb36-8" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-ttb36-8" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line">  <span class="operator">*</span></span><br><span class="line"><span class="keyword">from</span>    <span class="comment">/*$table_name*/</span> <span class="comment">-- 置換文字列のサンプル値が欠けている</span></span><br><span class="line">;</span><br></pre></td></tr></table></figure></div>

<h4 id="3-不要なカンマ">3. 不要なカンマ</h4><p>uroboroSQL では、以下のようにselectやorder byの直後に余計なカンマがあっても実行できます<sup id="fnref:10">10</sup>。サンプル値漏れと同様に通常のパーサーではエラーになりますが、新パーサーではこちらもエラー回復し、さらに余計な <code>,</code> を保持した CST を構築できます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-ttb36-9" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-ttb36-9" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line"><span class="comment">/*IF detail*/</span></span><br><span class="line">,  first_name <span class="comment">-- 不要なカンマがあり、多くのパーサーではパースできない</span></span><br><span class="line">,  last_name</span><br><span class="line">,  birth_date</span><br><span class="line">,  gender</span><br><span class="line"><span class="comment">/*END*/</span></span><br><span class="line">,  emp_no</span><br><span class="line"><span class="keyword">from</span></span><br><span class="line">  employee  emp</span><br><span class="line"><span class="keyword">order</span> <span class="keyword">by</span></span><br><span class="line"><span class="comment">/*IF detail*/</span></span><br><span class="line">,  birth_date <span class="comment">-- 不要なカンマがあり、多くのパーサーではパースできない</span></span><br><span class="line"><span class="comment">/*END*/</span></span><br><span class="line">,  emp_no</span><br></pre></td></tr></table></figure></div>

<h4 id="4-不要なand-or">4. 不要なand&#x2F;or</h4><p>uroboroSQL では、以下のように where の直後に余計な and&#x2F;or があっても実行できます。新パーサーではこちらもエラー回復し、余計な and&#x2F;or を保持した CST を構築できます。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line">	<span class="number">1</span></span><br><span class="line"><span class="keyword">from</span></span><br><span class="line">  employee  emp</span><br><span class="line"><span class="keyword">where</span></span><br><span class="line"><span class="keyword">and</span> emp.id <span class="operator">=</span> <span class="number">1</span> <span class="comment">-- 余計なand/orが先頭にある</span></span><br></pre></td></tr></table></figure>



<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">新しいSQLフォーマッターであるuroboroSQL-fmtをリリースしました | フューチャー技術ブログ</span> ↩</li><li id="fn:2"><span style="vertical-align: top; padding-right: 10px;">2.</span><span style="vertical-align: top;">Engineer Camp2022 RustでSQLフォーマッタ作成（前編） | フューチャー技術ブログ</span> ↩</li><li id="fn:3"><span style="vertical-align: top; padding-right: 10px;">3.</span><span style="vertical-align: top;">ANTLR や tree-sitter などを見ると様々な言語のパーサーやグラマーはインターネットには溢れており、パースすることは一軒容易に見えるかもしれません。しかし私の所属するチームでの経験から考えると、対象言語の文法を網羅し実用的なパフォーマンスで動作するパーサーは、公式で提供されているパーサー以外では非常に少ないのではないかと思います。（少なくとも私は出会ったことがありません）</span> ↩</li><li id="fn:4"><span style="vertical-align: top; padding-right: 10px;">4.</span><span style="vertical-align: top;">本パーサーの開発当時、PostgreSQL の CST パーサーでデファクトと言えるものは存在しないようで、 supabase でも独自に CST パーサーを開発 しているようです。ただ、こちらは PostgreSQL の C ソースを含むpg_query.rsを利用していることから wasm にビルドするのは容易でないと考えています。</span> ↩</li><li id="fn:5"><span style="vertical-align: top; padding-right: 10px;">5.</span><span style="vertical-align: top;">PL/pgSQL は別の grammar が存在するので、本パーサーでは現在サポートしていません。</span> ↩</li><li id="fn:6"><span style="vertical-align: top; padding-right: 10px;">6.</span><span style="vertical-align: top;">字句解析器は flex を用いて scan.l から生成</span> ↩</li><li id="fn:7"><span style="vertical-align: top; padding-right: 10px;">7.</span><span style="vertical-align: top;">書換え箇所はこちらです postgres/src/backend/parser/parser.c at REL_16_STABLE · postgres/postgres</span> ↩</li><li id="fn:8"><span style="vertical-align: top; padding-right: 10px;">8.</span><span style="vertical-align: top;">構文解析器は bison を用いて gram.y から生成</span> ↩</li><li id="fn:9"><span style="vertical-align: top; padding-right: 10px;">9.</span><span style="vertical-align: top;">アクションは無視しているため本来はエラーになるべきSQLがエラーにならなかったりするのですが、それは許容しています</span> ↩</li><li id="fn:10"><span style="vertical-align: top; padding-right: 10px;">10.</span><span style="vertical-align: top;">詳しくは uroboroSQL のドキュメントを参照ください。</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">先日リリースされたuroboroSQL-fmt ver1.0.0では、過去バージョンの課題を解決するために、新たに自作したパーサーを利用するように変更しました。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="2WaySQL" scheme="https://future-architect.github.io/tags/2WaySQL/"/>
    <category term="PostgreSQL" scheme="https://future-architect.github.io/tags/PostgreSQL/"/>
    <category term="Rust" scheme="https://future-architect.github.io/tags/Rust/"/>
    <category term="構文解析" scheme="https://future-architect.github.io/tags/%E6%A7%8B%E6%96%87%E8%A7%A3%E6%9E%90/"/>
  </entry>
  <entry>
    <title>Pure Rustで生まれ変わったPostgreSQL公式構文準拠SQLフォーマッター「uroborosql-fmt」をリリース🎉</title>
    <link href="https://future-architect.github.io/articles/20250929a/"/>
    <id>https://future-architect.github.io/articles/20250929a/</id>
    <published>2025-09-28T15:00:00.000Z</published>
    <updated>2025-09-28T15:00:00.000Z</updated>
    <author><name>川渕皓太</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><img fetchpriority="high" src="/images/2025/20250929a/top.png" alt="" width="630" height="229">

<p>コアテクノロジーグループの川渕です。</p>
<p>先日、uroborosql-fmtの新バージョンv1.0.0をリリースしました 🎉</p>
<p>このツールは当社が公開しているPostgreSQL向けのSQLコーディング規約に基づき、SQL文をフォーマットするツールです。</p>
<p>現在、以下の3種類の方法でご利用いただけます。</p>
<ul>
<li>ブラウザツール</li>
<li>VSCode拡張</li>
<li>CLIツール</li>
</ul>
<p>詳しい利用方法は利用方法の章で説明しています。</p>
<p>※ uroborosql-fmtのライセンスはBUSLですが、競合会社含め開発環境での利用は自由ですのでお気軽にご使用ください。</p>
<p>また、今回のアップデートについては、以下のシリーズ記事でも詳しく解説しています。</p>
<ul>
<li>PostgreSQL 全構文対応の Pure Rust な CST パーサーを作ってみた</li>
<li>半年がかりのパーサー移行を成功に導いた戦略 ～Rust製SQLフォーマッター開発の裏側～</li>
</ul>
<h2 id="当社のSQLフォーマッター開発の歩みと課題の変遷">当社のSQLフォーマッター開発の歩みと課題の変遷</h2><p>当社ではこれまでにもSQLフォーマッターを開発・公開してきました。</p>
<p>それらのフォーマッターにはそれぞれ課題がありましたが、今回の新バージョンではこれまでの課題を<strong>全て克服</strong>しています。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">年</th>
<th align="left">名前</th>
<th align="left">特徴</th>
<th align="left">課題</th>
<th align="left">関連ブログ</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>2017</strong></td>
<td align="left"><strong>uroboroSQL formatter</strong></td>
<td align="left">・Pythonで開発<br>・トークンベースで解析（パースなし）</td>
<td align="left">・lexerベースのため複数RDB構文対応といった自由度は高い反面、複雑な構文はサポートが困難 (postgresql以外のRDBを使う方は未だお薦めできます)<br>・PythonのためVSCode拡張化やwasm化が難しい</td>
<td align="left">・SQL開発者を幸せにする！？ Sublime Text 3でも使える uroboroSQL Formatter を公開しました &#124; フューチャー技術ブログ</td>
</tr>
<tr>
<td align="left"><strong>2020</strong></td>
<td align="left"><strong>ANTLR+TypeScript版SQLフォーマッター</strong></td>
<td align="left">・TypeScriptで開発（VSCode拡張化が容易）<br>・ANTLR4のパース結果を利用</td>
<td align="left">・ANTLR4のパーサーが非常に低速なことにより、フォーマッター全体の動作が低速</td>
<td align="left">・Engineer Camp2020でSQLフォーマッターを開発しました &#124; フューチャー技術ブログ</td>
</tr>
<tr>
<td align="left"><strong>2022</strong></td>
<td align="left"><strong>uroborosql-fmt v0.1.0</strong></td>
<td align="left">・Rustで開発（wasm化やVSCode拡張化が可能）<br>・<code>tree-sitter-sql</code>のパース結果を利用しており高速</td>
<td align="left">・<code>tree-sitter-sql</code>の文法が不完全で、PostgreSQLの追従・修正コストがかかる<br>・文法を増やすとバイナリサイズが大きくなる<br>・<code>tree-sitter-sql</code>のC言語依存によりwasm化が複雑</td>
<td align="left">・新しいSQLフォーマッターであるuroboroSQL-fmtをリリースしました &#124; フューチャー技術ブログ</td>
</tr>
<tr>
<td align="left"><strong>2025</strong></td>
<td align="left"><strong>uroborosql-fmt v1.0.0</strong></td>
<td align="left">・Rustで開発（wasm化やVSCode拡張化が可能）<br>・公式PostgreSQL文法に準拠したパーサー(postgresql-cst-parser)のパース結果を利用しているため、<strong>文法の追従コストが低い</strong><br>・postgresql-cst-parserは十分に高速であり、<strong>パーサー自体も高速</strong><br>・Pure Rustであるため、<strong>wasm化が容易</strong></td>
<td align="left"></td>
<td align="left"></td>
</tr>
</tbody></table></div>
<h2 id="パーサー移行の背景とメリット">パーサー移行の背景とメリット</h2><p>uroborosql-fmt v0.1.0ではパーサーとして tree-sitter-sqlを利用していましたが、開発を進めるにつれて以下3点の課題が判明しました。</p>
<ul>
<li>サポートされている文法が少なく、grammarの改善に工数がかかる<ul>
<li>v0.1.0における新規構文サポートでは、フォーマットプロセスの改善に加えてパースプロセスの改善(grammarの改善)が必要であり、新規構文サポートに工数がかかっていた</li>
</ul>
</li>
<li>対応文法を増やすとパーサーのサイズが大きくなり、結果的にフォーマッターのバイナリサイズも大きくなる<ul>
<li>フォーク元tree-sitter-sqlでは、PRを全てマージすると83MBになるらしい… (参考issue)</li>
</ul>
</li>
<li>tree-sitterは内部的にC言語を利用しているため、wasmにコンパイルする際にwasm-bindgenが利用できず、wasmへのコンパイルが複雑化していた<ul>
<li>関連ブログ: C&#x2F;C++を呼び出しているRustのWASM化 | フューチャー技術ブログ</li>
</ul>
</li>
</ul>
<p>これらの課題に対応するため、当社の山田がPostgreSQLの公式のパーサーをRustに移植した<strong>postgresql-cst-parser</strong>を作成しました。uroborosql-fmt v1.0.0ではそのパーサーを利用するようにしています。</p>
<ul>
<li>future-architect&#x2F;postgresql-cst-parser - GitHub</li>
</ul>
<p>パーサーの変更により、v0.1.0でのパーサー起因の課題が全て解消されました。</p>
<ul>
<li>公式パーサーから移植しているので、パーサー自体は全てのPostgreSQL文法に対応している</li>
<li>公式の構文定義ファイルからRust製パーサーを自動生成しているため、文法の追従が容易</li>
<li>全てのPostgreSQL文法に対応しているにも関わらず、バイナリサイズが約6MBとコンパクト</li>
<li>Pure Rust (C言語を含まない) であるため、wasm-bindgenを用いてwasmにコンパイルできる</li>
</ul>
<p>この変更によって、新規構文サポートのためのパースプロセスの改善工数は<strong>ゼロ</strong>になり、フォーマットプロセスの改善のみで新規構文をサポートできるようになりました :tada:</p>
<img src="/images/2025/20250929a/image.png" alt="image.png" width="1070" height="456" loading="lazy">

<p>パーサーの動作は以下のデモページから確認できます。</p>
<ul>
<li>PostgreSQL CST Parser</li>
</ul>

<img src="/images/2025/20250929a/image_2.png" alt="image.png" width="1200" height="750" loading="lazy">


<p>新パーサーの詳細については後日ブログで公開する予定です。</p>
<h2 id="利用方法">利用方法</h2><p>現在以下の3種類の方法で利用できます。</p>
<ol>
<li>ブラウザツール</li>
<li>VSCode拡張</li>
<li>CLIツール</li>
</ol>
<h3 id="1-ブラウザツール">1. ブラウザツール</h3><p>デモページからブラウザ上で試すことができます。使い方の詳細はデモページをご覧ください。</p>

<img src="/images/2025/20250929a/image_3.png" alt="image.png" width="1200" height="750" loading="lazy">


<h3 id="2-VSCode拡張">2. VSCode拡張</h3><ol>
<li><p>uroborosql-fmtの拡張機能をインストールしてください</p>
</li>
<li><p>settings.jsonに以下の設定を入れてください</p>
 <div class="code-block"><figure class="highlight json"><input type="checkbox" id="code-wrap-10db3q7-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-10db3q7-1" title="コードの折り返しを切り替える"></label><figcaption><span>settings.json</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;[sql]&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;editor.defaultFormatter&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Future.uroborosql-fmt&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div>
</li>
<li><p>SQLファイルを開き、コマンドパレットから<code>Format Document</code>か、<code>format sql</code>を実行してください<br><code>format sql</code>では選択範囲のフォーマットをサポートしています。</p>
</li>
</ol>
<img src="/images/2025/20250929a/image_4.png" alt="image.png" width="1200" height="705" loading="lazy">

<h3 id="3-CLIツール">3. CLIツール</h3><ol>
<li><p>Rustをインストールしてください。(インストール方法)</p>
</li>
<li><p>以下を実行してください</p>
 <div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-10db3q7-2" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-10db3q7-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">cargo install --git https://github.com/future-architect/uroborosql-fmt</span><br></pre></td></tr></table></figure></div>
</li>
<li><p>以下のようなコマンドでuroborosql-fmtを実行できるようになります (オプションの詳細はCLIツールのREADMEをご覧ください)</p>
 <figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">uroborosql-fmt-cli --write target.sql</span><br></pre></td></tr></table></figure></li>
</ol>
<h2 id="今後の展望">今後の展望</h2><p>今後は対応構文の拡張とLinter機能を開発する予定です。</p>
<p>Linter機能は具体的に、以下のような警告・エラーをVSCode上で表示する機能を想定しています。</p>
<ul>
<li>大きすぎるIN句はパフォーマンスに影響を与える可能性があるため警告 (コーディング規約)</li>
<li>存在しないカラム・テーブルを参照している場合はエラー</li>
<li>NullableなカラムをINNER JOINしている場合は警告</li>
</ul>
<h2 id="さいごに">さいごに</h2><p>まだまだ開発途上であり、フォーマットできない構文も多くあります。<br>不具合や要望等ございましたらお気軽に以下リポジトリまでissueやPRを頂ければと思います。</p>
<ul>
<li>future-architect&#x2F;uroborosql-fmt - GitHub</li>
</ul>
<p>postgresql-cst-parser開発の話、パーサー移行に伴うフォーマッター移行作業の話の2本の記事を公開予定です。お楽しみに！</p>
<ul>
<li>PostgreSQL 全構文対応の Pure Rust な CST パーサーを作ってみた</li>
<li>半年がかりのパーサー移行を成功に導いた戦略 ～Rust製SQLフォーマッター開発の裏側～</li>
</ul>
]]></content>
    <summary type="html">roborosql-fmtの新バージョンv1.0.0をリリースしました。当社のSQLフォーマッター開発の歩みと課題の変遷について紹介します</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="PostgreSQL" scheme="https://future-architect.github.io/tags/PostgreSQL/"/>
    <category term="Rust" scheme="https://future-architect.github.io/tags/Rust/"/>
    <category term="WebAssembly" scheme="https://future-architect.github.io/tags/WebAssembly/"/>
    <category term="uroboroSQL" scheme="https://future-architect.github.io/tags/uroboroSQL/"/>
    <category term="フォーマッター" scheme="https://future-architect.github.io/tags/%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%82%BF%E3%83%BC/"/>
  </entry>
  <entry>
    <title>PostgreSQLの全文検索機能を試してみる</title>
    <link href="https://future-architect.github.io/articles/20250829a/"/>
    <id>https://future-architect.github.io/articles/20250829a/</id>
    <published>2025-08-28T15:00:00.000Z</published>
    <updated>2025-08-28T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20250829a/top.png" alt="" width="600" height="600">

<p>夏の自由研究2025ブログ連載の4日目です。</p>
<p>技術コンサルをしているお客さんとPrismaのドキュメントの読書会をしていて、全文検索機能がPrismaにも、PostgreSQLにも標準で用意されているということを知りました。PostgreSQLで全文検索はというと、PGroongaとか、pg_bigmを使うとかがトップ出てくるし、そもそも検索をしたくなったらElasticSearch使う、みたいに思っていました。</p>
<p>標準で全文検索もできるなら運用コストもだいぶ下げられそうです。かつて、Python製ドキュメントツールの、ブラウザで動く全文検索エンジンの日本語対応をやってみたり、FM-indexという高速文字列解析の世界という書籍で紹介されていたアルゴリズムを使ったブラウザで動く検索エンジンを作ったり、転置インデックスをS3に置く検索エンジンを作ってみたり<del>貧乏</del>低コスト検索エンジンの第一人者(自称)としては試してみたいところです。</p>
<p>ものは試しでやってみました。</p>
<h2 id="PostgreSQLの全文検索機能">PostgreSQLの全文検索機能</h2><p>PostgreSQLの全文検索では、<code>LIKE</code>とか<code>ILIKE</code>で検索するみたいに、 <code>@@</code>で転置インデックスを検索する演算子が提供されており、それを呼び出します。検索するフィールドは<code>to_tsvector()</code>、検索ワードは<code>to_tsquery()</code>関数に渡して前処理をするところがポイントですかね。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1w1390a-1" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1w1390a-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> title</span><br><span class="line"><span class="keyword">FROM</span> pgweb</span><br><span class="line"><span class="keyword">WHERE</span> to_tsvector(title <span class="operator">||</span> <span class="string">&#x27; &#x27;</span> <span class="operator">||</span> body) @@ to_tsquery(<span class="string">&#x27;create &amp; table&#x27;</span>)</span><br><span class="line"><span class="keyword">ORDER</span> <span class="keyword">BY</span> last_mod_date <span class="keyword">DESC</span></span><br><span class="line">LIMIT <span class="number">10</span>;</span><br></pre></td></tr></table></figure></div>

<p>テーブルの中にもtsvectorを作ってあげます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1w1390a-2" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1w1390a-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> articles (</span><br><span class="line">    id SERIAL <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    title TEXT <span class="keyword">NOT NULL</span>,</span><br><span class="line">    content TEXT <span class="keyword">NOT NULL</span>,</span><br><span class="line">    file_path TEXT <span class="keyword">NOT NULL</span> <span class="keyword">UNIQUE</span>,</span><br><span class="line">    author TEXT <span class="keyword">NOT NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">    created_at <span class="type">TIMESTAMP</span> <span class="keyword">WITH</span> <span class="type">TIME</span> ZONE <span class="keyword">DEFAULT</span> NOW(),</span><br><span class="line">    processed_text TEXT <span class="keyword">NOT NULL</span>,</span><br><span class="line">    search_vector tsvector GENERATED ALWAYS <span class="keyword">AS</span> (</span><br><span class="line">        to_tsvector(<span class="string">&#x27;simple&#x27;</span>, <span class="built_in">coalesce</span>(title, <span class="string">&#x27;&#x27;</span>) <span class="operator">||</span> <span class="string">&#x27; &#x27;</span> <span class="operator">||</span> <span class="built_in">coalesce</span>(processed_text, <span class="string">&#x27;&#x27;</span>))</span><br><span class="line">    ) STORED</span><br><span class="line">);</span><br></pre></td></tr></table></figure></div>

<p>なお、全文検索エンジンあるあるテーマが日本語対応で、大抵は英語のようにスペース区切りの単語で分割し、転置インデックスという、単語→ドキュメントのインデックスを作り、それを元に検索をするという仕組みです。その過程で、英語やドイツ語などの言語ごとに正規化（ステミング）、冠詞などのノイズになる単語(stop word)をフィルタリングなど、自然言語ごとの前処理を行います。そのあたりの設定はここに書かれています。</p>
<p>標準でサポートしている言語は17.5で以下のような感じです。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1w1390a-3" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1w1390a-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">/usr/local/share/postgresql/tsearch_data <span class="comment"># ls *.stop</span></span><br><span class="line">danish.stop      finnish.stop     hungarian.stop   norwegian.stop   spanish.stop</span><br><span class="line">dutch.stop       french.stop      italian.stop     portuguese.stop  swedish.stop</span><br><span class="line">english.stop     german.stop      nepali.stop      russian.stop     turkish.stop</span><br></pre></td></tr></table></figure></div>

<p>デフォルトでは日本語のようなスペース区切りではない言語は対応できません。当然日本語の文法ルールもなく、stop wordの辞書もありません。この機能も、昔は日本語対応のtextsearch_jaというモジュールがあったようですが、今はメンテナンスされていないようです。仮にされていたとしても、DB運用はクラウドにお任せ時代なので最初から導入されている方が運用は楽でしょう。</p>
<p>なんとかして使ってみる方法として、事前に、DBの外でスペース区切りにしてからテーブルに入れてみるという前処理をする方法を試してみます。</p>
<h2 id="GoでPostgreSQLの全文検索で日本語検索してみる">GoでPostgreSQLの全文検索で日本語検索してみる</h2><p>Goでは分かち書きのライブラリとして定評のあるgithub.com&#x2F;ikawaha&#x2F;kagome&#x2F;v2を使います。</p>
<p>kagomeではこんな感じではこんな感じに…</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1w1390a-4" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1w1390a-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">kagome</span></span><br><span class="line">シグナルを送信した</span><br><span class="line">シグナル        名詞,一般,*,*,*,*,シグナル,シグナル,シグナル</span><br><span class="line">を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ</span><br><span class="line">送信    名詞,サ変接続,*,*,*,*,送信,ソウシン,ソーシン</span><br><span class="line">し      動詞,自立,*,*,サ変・スル,連用形,する,シ,シ</span><br><span class="line">た      助動詞,*,*,*,特殊・タ,基本形,た,タ,タ</span><br></pre></td></tr></table></figure></div>

<p>助詞とか助動詞はひかっかりまくるので削除し、動詞などは原形にすることで、「送信する」「送信した」などの表記揺れでもひっかかるようになります。分かち書きでは品詞もわかるのでこれを使って、助詞、助動詞、副詞などを除外すれば良いでしょう。簡単ですね。</p>
<h2 id="実装">実装</h2><p>ソースコードはgithub.com&#x2F;shibukawa&#x2F;pgtfsです。生成AIでサッと作りました。ライセンスはUnlicenseです。full text searchだとftsのはずですが、スペルを間違ったのでtfsになってます。CLIツールとなっています。実証実験のコードなので、インデックスのメンテナンスとか考えずに、一発投入のみ。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1w1390a-5" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1w1390a-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">PostgreSQLの起動</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker compose up</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">DBを初期化して./articles以下のテキストファイルをDBに投入</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">./pgtfs init</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">検索</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">./pgtfs search <span class="string">&quot;検索用語&quot;</span></span></span><br></pre></td></tr></table></figure></div>

<p>サンプルドキュメントも生成AIが用意してくれました。</p>
<div class="code-block"><figure class="highlight md"><input type="checkbox" id="code-wrap-1w1390a-6" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1w1390a-6" title="コードの折り返しを切り替える"></label><figcaption><span>golang.md</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="section"># Goプログラミング言語</span></span><br><span class="line"></span><br><span class="line">Goは、Googleが開発したプログラミング言語です。</span><br><span class="line"></span><br><span class="line"><span class="section">## 特徴</span></span><br><span class="line"><span class="bullet">-</span> シンプルな文法</span><br><span class="line"><span class="bullet">-</span> 高速なコンパイル</span><br><span class="line"><span class="bullet">-</span> 優れた並行処理機能</span><br><span class="line"><span class="bullet">-</span> ガベージコレクション</span><br><span class="line"></span><br><span class="line"><span class="section">## 活用分野</span></span><br><span class="line">Goは以下の分野で広く使用されています：</span><br><span class="line"><span class="bullet">-</span> Webサービス開発</span><br><span class="line"><span class="bullet">-</span> クラウドインフラストラクチャ</span><br><span class="line"><span class="bullet">-</span> コマンドラインツール</span><br><span class="line"><span class="bullet">-</span> マイクロサービス</span><br><span class="line"></span><br><span class="line">ゴルーチンとチャネルを使った並行プログラミングが魅力的です。</span><br></pre></td></tr></table></figure></div>

<p>内部では次のように区切られてスペース区切りにされて保存されます。</p>
<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1w1390a-7" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1w1390a-7" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">#|Go|プログラミング|言語|Go|Google|開発|し|プログラミング|言語|</span><br><span class="line">##|特徴|-|シンプル|文法|-|高速|コンパイル|-|優れ|並行|処理|機能|</span><br><span class="line">-|ガベージコレクション|##|活用|分野|Go|以下|分野|広く|使用|さ|れ|い|</span><br><span class="line">-|Web|サービス|開発|-|クラウドインフラストラクチャ|-|コマンドラインツール|</span><br><span class="line">-|マイクロ|サービス|ゴルーチン|チャネル|使っ|並行|プログラミング|魅力|的</span><br></pre></td></tr></table></figure></div>

<p>「Web開発」で検索します。この単語は文中にはありません。「Webサービス開発」ならあります。うまく分かち書きされると単語がヒットしてくれるはず!!!</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">./pgtfs search <span class="string">&quot;Web開発&quot;</span></span></span><br><span class="line">Searching for: &quot;Web開発&quot;</span><br><span class="line">Found 1 articles:</span><br><span class="line"></span><br><span class="line">=== Result 1 (Rank: 0.0989) ===</span><br><span class="line">Title: golang</span><br><span class="line">File: articles/golang.md</span><br></pre></td></tr></table></figure>

<p>きた！</p>
<h2 id="より使いやすい検索にするには">より使いやすい検索にするには</h2><p>分かち書きとstop wordは最低限です。実際にElastichsearchで使われるKuromojiではそれ以外に漢数字を算用数字にしたりさまざまなフィルタを提供しています。</p>
<ul>
<li>ZOZO TECH BLOG: Elasticsearchで日本語検索を扱うためのマッピング定義</li>
</ul>
<p>これにより、表記揺れでもヒットしやすいようになります。PostgreSQLの検索機能も類義語辞書が持てるようになっているため、日本語でも頑張って集めることでさらに精度を上げる余地があります。最終的にはベクトル検索を実装して類似文書検索ですかね。いつかはやってみたい。</p>
<h2 id="まとめ">まとめ</h2><p>かんたんな前処理でPostgreSQLの標準の検索機能が使えました。これでコストを増やさずに全文検索機能が簡単に組み込めるでしょう。bigmなんかは正規の単語の区切りではないところでもひっかかってしまったりというのがあり、やはりきちんと分かち書きをした検索エンジンが使いたいですよね？どのような言語でもたいてい分かち書きのライブラリはあると思うので言語問わず利用できると思います。</p>
<p>PostgreSQLにはPub&#x2F;Sub機能もあるので、DBさえあれば他のマネージドサービスは要らない、という時代がそのうち来るんじゃないかと思っているところです。</p>
]]></content>
    <summary type="html">全文検索機能がPrismaにも標準で用意されているということを知りました。PostgreSQLで全文検索はというと、PGroongaとか、pg_bigmを使うとかがトップ出てくるし、そもそも検索をしたくなったらElasticsearch使う、みたいに思っていました。標準で全文検索もできるなら運用コストもだいぶ下げられそうです。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="Go" scheme="https://future-architect.github.io/tags/Go/"/>
    <category term="PostgreSQL" scheme="https://future-architect.github.io/tags/PostgreSQL/"/>
    <category term="全文検索" scheme="https://future-architect.github.io/tags/%E5%85%A8%E6%96%87%E6%A4%9C%E7%B4%A2/"/>
  </entry>
  <entry>
    <title>PostgreSQL設計ガイドラインのご紹介</title>
    <link href="https://future-architect.github.io/articles/20250530a/"/>
    <id>https://future-architect.github.io/articles/20250530a/</id>
    <published>2025-05-29T15:00:00.000Z</published>
    <updated>2025-05-29T15:00:00.000Z</updated>
    <author><name>宮崎将太</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>フューチャー社内の有志メンバーでPostgreSQL DB設計ガイドラインを作成しました。</p>
<ul>
<li>PostgreSQL設計ガイドライン | Future Enterprise Arch Guidelines</li>
</ul>
<p>形になってから数ヶ月寝かせており、ある程度社内の指摘を取り込むことができたのでこのタイミングで告知します。</p>

<img fetchpriority="high" src="/images/2025/20250530a/image.png" alt="" width="1200" height="800">


<h2 id="よくあるDB設計規約との差別化ポイント">よくあるDB設計規約との差別化ポイント</h2><p>単にDB設計ガイドラインというと何を今更？感もあるので、命名規則や型桁など一般的な内容に加え、以下の点でよくあるDB設計ガイドラインから一歩踏み込んだコンテンツとなるよう心がけました。</p>
<h3 id="論理設計への踏み込み">論理設計への踏み込み</h3><p>単なるテーブル定義やデータ型選択にとどまらず、より高度な論理設計の原則に焦点を当てています。</p>
<h4 id="マスタ-トラン-ワーク">マスタ&#x2F;トラン&#x2F;ワーク</h4><p>データベース設計において、データの種類に応じてテーブルを明確に分離することは設計効率と保守性を高める上で重要ですが、意外とその定義は曖昧であり、しばしば初学者の障壁となります。本ガイドラインでは以下の通り明確に分類を定めています。</p>
<ul>
<li><strong>マスタテーブル:</strong> ビジネスの中核となる静的な情報（例：顧客情報、商品カタログ）を格納</li>
<li><strong>トランザクションテーブル（トランテーブル）:</strong> ビジネスプロセスで発生する動的な出来事（例：注文履歴、在庫変動）を記録</li>
<li><strong>ワークテーブル:</strong> 一時的な処理や中間結果を保持するために使用</li>
</ul>
<p>これらのデータの特性を踏まえ、それぞれについて下記の言及しています。</p>
<ul>
<li>テーブル設計</li>
<li>インデックス戦略</li>
<li>アクセスパターン</li>
</ul>
<h4 id="スナップショット属性、導出属性">スナップショット属性、導出属性</h4><p>RDBにおけるテーブルデザインといえば、基本は正規化するべしというのがスタンダードな考え方です。<br>一方で、下記のように敢えて非正規化し、カラムとして保持させるべきケースもあります。</p>
<ul>
<li><strong>スナップショット属性:</strong> 特定の時点におけるエンティティの状態を記録し、履歴管理や監査に使用する</li>
<li><strong>導出属性:</strong> 既存の属性から計算される情報であり、例えば、注文日の属する四半期や、顧客の累積購入金額などが該当する。</li>
</ul>
<p>本ガイドラインではこれらの属性の適切な管理方法について、以下の指針を示しています。</p>
<ul>
<li>いつスナップショットを取得すべきか</li>
<li>導出属性を物理的に格納するか（性能とのトレードオフ）</li>
</ul>
<h4 id="業務日付">業務日付</h4><p>中規模以上のシステムになると、以下の様なケースに耐えるためシステム日付とは別に業務日付を保持させることがあります。</p>
<ul>
<li>店舗の営業時間が26時などの場合に、コンピューターの持つ日付（システム日付）とずれた営業日単位で登録／集計を可能とするため</li>
<li>日をまたぐバッチ処理や画面操作（システムメンテナンスなどを想定）に対して、データ整合性を保ちやすくする</li>
<li>障害調査や結合テスト／負荷検証などで、特定の日付におけるテストを再現しやすくする</li>
</ul>
<p>小規模なWebアプリケーションではあまり発生しない概念ですが、本ガイドラインでは業務日付を使用するべきケース、そうでないケース、使用する場合の推奨データ型や注意点を記載しています。</p>
<h4 id="世代管理">世代管理</h4><p>データの変更履歴を管理する世代管理は、データ復旧や過去の状態参照において重要な役割を果たします。これを実現するための具体的な方法として、以下のものが考えられます。</p>
<ul>
<li>タイムスタンプ付きの履歴テーブルの利用</li>
<li>論理削除フラグの活用</li>
</ul>
<p>データの重要度や変更頻度に応じて適切な世代管理戦略を選択するための基準を示しています。</p>
<h3 id="その他設計パターンへの踏み込み">その他設計パターンへの踏み込み</h3><h4 id="排他制御">排他制御</h4><p>複数のトランザクションが同時に同じデータにアクセスする状況下で、データの整合性を保つための排他制御は非常に重要です。<br>本ガイドラインでは、以下の点について解説しています。</p>
<ul>
<li>PostgreSQLを利用する場合のロック機能の適切な利用方法</li>
<li>デッドロックを回避するためのプラクティス</li>
</ul>
<h4 id="マルチテナント">マルチテナント</h4><p>複数の顧客や組織が同一のアプリケーションやデータベースインスタンスを共有するアーキテクチャはマルチテナントアーキテクチャと呼ばれ、コスト効率に優れています。<br>世間的に広く認知されているものの、実際に設計・実装しようとすると、以下の点が重要な課題となります。</p>
<ul>
<li>テナント間のデータ隔離</li>
<li>セキュリティ確保</li>
<li>性能</li>
</ul>
<p>本ガイドラインでは、複数考えられるマルチテナントアーキテクチャ設計パターンのメリット・デメリットを比較し、ケース毎に推奨される設計方針を打ち出しています。</p>
<h4 id="キャッシュ戦略">キャッシュ戦略</h4><p>データベースへの負荷を軽減し、アプリケーションの応答性を向上させるためには、適切なキャッシュ戦略が不可欠です。<br>本ガイドラインでは、アプリケーションの要件やデータ特性に応じて、効果的なキャッシュ戦略を選択し、実装するための指針を示しています。</p>
<h3 id="クラウド（AWS）への踏み込み">クラウド（AWS）への踏み込み</h3><p>より具体的なガイドラインとなるよう特にAmazon Auroraを中心にクラウド特有の考慮事項に焦点を当てています。</p>
<h4 id="バージョン管理">バージョン管理</h4><p>基本的にはLTSを選択することになり、少なくとも3年間はLTSを使用できますが、その後はしばしば強制的なメジャーバージョンアップが要求されることもあります。<br>そう何度も実施する作業ではないので対処に迷いがちなので、今回は特に以下の観点で記載しました。</p>
<ul>
<li>アップグレード計画の策定</li>
<li>アップグレード時の注意点</li>
</ul>
<h4 id="サイジング">サイジング</h4><p>アプリケーションの性能要件を満たすためには、適切な RDS インスタンスタイプ（CPU、メモリ、ストレージ）を選択することが重要です。サイジングが不適切だと、以下の問題につながる可能性があります。</p>
<ul>
<li>性能不足</li>
<li>コストの浪費</li>
</ul>
<p>本ガイドラインでは特に以下の点について言及しています。</p>
<ul>
<li>ワークロードの特性（例：OLTP、OLAP）に応じたインスタンスタイプの推奨</li>
<li>初期サイジングの見積もり方法</li>
<li>負荷テストの重要性</li>
</ul>
<h4 id="スケーリング戦略">スケーリング戦略</h4><p>アプリケーションの負荷変動に対応するため、データベースのスケーリング戦略を事前に検討しておく必要があります。Aruoraでは以下の機能を提供されています。</p>
<ul>
<li>リードレプリカによる読み取り負荷の分散</li>
<li>インスタンスタイプの変更による垂直スケーリング</li>
</ul>
<p>本ガイドラインでは、以下の点について解説しています。</p>
<ul>
<li>これらのスケーリングオプションの適切な選択と設定</li>
<li>スケーリング時の注意点</li>
</ul>
<h2 id="まとめ">まとめ</h2><p>冒頭に記述したとおり、単にPostgreSQLの基本的な機能や設計原則を解説するだけでなく、より実践的で高度なトピックにまで踏み込見ました。</p>
<p>PostgreSQL自体のupdateやAWSその他クラウドサービスの動向をウォッチしつつ継続的にupdateを行っていく予定です。</p>
]]></content>
    <summary type="html">フューチャー社内の有志メンバーでPostgreSQLDB設計ガイドラインを作成しました。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="AWS" scheme="https://future-architect.github.io/tags/AWS/"/>
    <category term="PostgreSQL" scheme="https://future-architect.github.io/tags/PostgreSQL/"/>
    <category term="RDS" scheme="https://future-architect.github.io/tags/RDS/"/>
    <category term="ガイドライン" scheme="https://future-architect.github.io/tags/%E3%82%AC%E3%82%A4%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3/"/>
    <category term="データモデル" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%83%A2%E3%83%87%E3%83%AB/"/>
  </entry>
  <entry>
    <title>OracleDB マルチテーブル・インサートにおけるIDENTITY列とSEQUENCEの挙動の違い</title>
    <link href="https://future-architect.github.io/articles/20250520a/"/>
    <id>https://future-architect.github.io/articles/20250520a/</id>
    <published>2025-05-19T15:00:00.000Z</published>
    <updated>2025-05-19T15:00:00.000Z</updated>
    <author><name>姫路康太郎</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20250520a/oracle-database-logo.png" alt="" width="500" height="271">

<p>春の入門祭り2025の19本目の記事です。</p>
<h2 id="はじめに">はじめに</h2><p>こんにちは、Cyber Security Innovation Group（以降CSIG）の姫路康太郎です。2025年2月から新卒としてプロジェクトに配属され、認可整理のチームでアジャイル開発をしています。</p>
<p>本記事ではまず、OracleDBにおける主要な採番方法であるSEQUENCEとIDENTITY列について、基本的な使い方を説明します。続いて、複数のテーブルへ同時にデータを投入する際に利用できるOracleDB特有のマルチテーブル・インサート構文 (<code>INSERT ALL</code>) における、それぞれの採番の実装方法と挙動の違いに焦点を当てて解説します。</p>
<p>OracleDBでの開発に携わる方や、効率的な採番方法に関心のある方にとって、本記事が少しでもお役に立てれば幸いです。</p>
<h2 id="OracleDBにおける採番方法の紹介">OracleDBにおける採番方法の紹介</h2><p>初めに、軽くIDENTITY列とSEQUENCEについて紹介します。</p>
<h3 id="SEQUENCE">SEQUENCE</h3><p>SEQUENCEは一意の整数値を生成するために使用されるスキーマオブジェクトであり、特定のテーブルとは独立したデータベースオブジェクトです。それにより、複数のテーブルで共有したり、SEQUENCEオブジェクト単体を操作できます。この点で後述するIDENTITY列とは異なり、IDENTITY列よりも柔軟性があると言えます。</p>
<h3 id="IDENTITY列">IDENTITY列</h3><p>IDENTITY列は、テーブルの特定の列に対して自動的に一意な数値を生成する機能です。内部的にSEQUENCEオブジェクトを利用して実現されており、IDENTITY列を定義すると対応するシーケンスを暗黙的に作成し、そのシーケンスから値を取得して列に自動的に設定します。そのため、SEQUENCEよりシンプルに使うことができます。</p>
<h3 id="SEQUENCEの使い方">SEQUENCEの使い方</h3><p>SEQUENCEオブジェクトは独立したオブジェクトとして作成します。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> SEQUENCE TEST_SEQ;</span><br></pre></td></tr></table></figure>

<p>INSERTをする際に、作成しておいたSEQUENCEオブジェクトを明示的に呼び出すことで採番します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-dmn1uw-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-dmn1uw-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">INSERT INTO</span> M_TEST(ID,<span class="keyword">VALUE</span>) <span class="keyword">VALUES</span>(TEST_SEQ.nextval,<span class="string">&#x27;data&#x27;</span>);</span><br></pre></td></tr></table></figure></div>

<h3 id="IDENTITY列の使い方">IDENTITY列の使い方</h3><p>IDENTITY列は、特定のテーブルの特定のカラムに定義します。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span>  M_TEST (</span><br><span class="line">    ID NUMBER GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span>,</span><br><span class="line">    <span class="keyword">VALUE</span> VARCHAR2(<span class="number">32</span>)</span><br><span class="line">);</span><br></pre></td></tr></table></figure>

<p>INSERTをする際に、採番列の値を明示的に示さなくても暗黙的に採番されます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-dmn1uw-2" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-dmn1uw-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">INSERT INTO</span> M_TEST(<span class="keyword">VALUE</span>) <span class="keyword">VALUES</span> (<span class="string">&#x27;data&#x27;</span>);</span><br></pre></td></tr></table></figure></div>

<h2 id="２つのテーブルに共通の番号を採番をする">２つのテーブルに共通の番号を採番をする</h2><p>いくつか方法はあると思いますが、本記事では、採番にSEQUENCE &#x2F; IDENTITY列を利用し、２つのテーブル同時の<code>INSERT</code>に、マルチテーブル・インサート構文<code>INSERT ALL</code>を利用して投入することを考えます。投入するデータは<code>M_TEST</code>の様に<code>ID</code>と<code>VALUE</code>のカラムを持った複数レコードからなるテーブルを想定しています。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="operator">&gt;&gt;</span> <span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> M_TEST</span><br><span class="line"></span><br><span class="line">    ID  <span class="keyword">VALUE</span></span><br><span class="line">  <span class="comment">----  -----</span></span><br><span class="line">     <span class="number">1</span>  data1</span><br><span class="line">     <span class="number">2</span>  data2</span><br><span class="line">     <span class="number">3</span>  data3</span><br></pre></td></tr></table></figure>

<h3 id="マルチテーブル・インサート">マルチテーブル・インサート</h3><p>OracleDB特有の構文で、複数のテーブルに同時にデータを投入できます。<code>INSERT ALL</code>を使う基本的な構文は以下のとおりです。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-dmn1uw-3" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-dmn1uw-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">INSERT</span> <span class="keyword">ALL</span></span><br><span class="line"><span class="keyword">INTO</span> TABLE1 (col1, col2, …) <span class="keyword">VALUES</span> (val1_1, val1_2, …)</span><br><span class="line"><span class="keyword">INTO</span> TABLE2 (col1, col2, …) <span class="keyword">VALUES</span> (val2_1, val2_2, …)</span><br><span class="line"><span class="keyword">SELECT</span>文;</span><br></pre></td></tr></table></figure></div>

<p>※詳細は以下の記事を参考ください。</p>
<ul>
<li>公式リファレンスにおけるマルチテーブルインサートの説明</li>
<li>【Oracle】複数のデータをまとめてINSERTする方法を解説！</li>
<li>マルチテーブル・インサートで同一テーブルに複数データ挿入してみる</li>
</ul>
<h3 id="SEQUENCEを使ったマルチテーブル・インサート">SEQUENCEを使ったマルチテーブル・インサート</h3><p>SEQUENCEオブジェクトを利用した場合、同じSEQUENCEオブジェクトから番号を呼び出すことで、共通の番号を採番できます。そのため、2つのテーブル間でIDは正しく保たれることとなります。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-dmn1uw-4" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-dmn1uw-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">INSERT</span> <span class="keyword">ALL</span></span><br><span class="line"><span class="keyword">INTO</span> M_TEST_SEQ1 (ID, <span class="keyword">VALUE</span>) <span class="keyword">VALUES</span> (TEST_SEQ.nextval, <span class="keyword">VALUE</span>)</span><br><span class="line"><span class="keyword">INTO</span> M_TEST_SEQ2 (ID, <span class="keyword">VALUE</span>) <span class="keyword">VALUES</span> (TEST_SEQ.nextval, <span class="keyword">VALUE</span>)</span><br><span class="line"><span class="keyword">SELECT</span> <span class="keyword">VALUE</span> <span class="keyword">FROM</span> M_TEST;</span><br></pre></td></tr></table></figure></div>

<h4 id="SEQUENCEを使ったマルチテーブル・インサートの落とし穴">SEQUENCEを使ったマルチテーブル・インサートの落とし穴</h4><p><code>INSERT ALL</code>でSEQUENCEを使って複数のテーブルに採番を行う際には、採番するテーブルに対して <strong>すべてに</strong> <code>nextval</code>（SEQUENCEを増加させて次の値を返す）を使用します。公式によるとこれが<strong>正規の方法</strong>のようです。この<code>nextval</code>を使用するという点が、私の直感と異なっていたので、共有しようと思いました。</p>
<p>直感的には、上記コードで<code>M_TEST_SEQ2</code>に採番する際、<code>currval</code>（SEQUENCEの現在の値を返す）を使うのではないかと感じました。直前の行で<code>nextval</code>を実行しているため、次のVALUE句では<code>nextval</code>されたSEQUENCEを取得するものと考えたからです。</p>
<p>実際に<code>currval</code>で実行したみたところ、1つの環境では成功したものの、別の環境では失敗したので、安全のためにも公式の説明から読み取れる<code>nextval</code>を推奨します。</p>
<h3 id="IDENTITYを使ったマルチテーブル・インサート">IDENTITYを使ったマルチテーブル・インサート</h3><p>IDENTITY列を利用した場合、データ投入先の２つのテーブルに、それぞれにIDENTITY列を定義することになります。つまり、２つのテーブルがそれぞれ固有のSEQUENCEオブジェクトを持つことになります。そのため、2つのテーブル間で<code>ID</code>の対応を正しく保つためには採番をずらさないための設計や、採番のずれを許容することが必要となります。<code>ID</code>がずれる可能性を考慮すると、IDENTITY列の利用はあまりいい手段ではなさそうです。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">INSERT</span> <span class="keyword">ALL</span></span><br><span class="line"><span class="keyword">INTO</span> M_TEST_ID1 (<span class="keyword">VALUE</span>) <span class="keyword">VALUES</span> (<span class="keyword">VALUE</span>)</span><br><span class="line"><span class="keyword">INTO</span> M_TEST_ID2 (<span class="keyword">VALUE</span>) <span class="keyword">VALUES</span> (<span class="keyword">VALUE</span>)</span><br><span class="line"><span class="keyword">SELECT</span> <span class="keyword">VALUE</span> <span class="keyword">FROM</span> M_TEST;</span><br></pre></td></tr></table></figure>

<p>※IDENTITY列の採番方法には、<code>GENERATED ALWAYS AS IDENTITY</code>、<code>GENERATED BY DEFAULT AS IDENTITY</code>の2種類があります。詳しく説明している記事の紹介にとどめて、詳細な説明は割愛させていただきますが、本記事では<code>GENERATED BY DEFAULT AS IDENTITY</code>を利用しました。</p>
<ul>
<li>Oracle も 12c から自動採番ができるようになった</li>
<li>12c新機能「Identity Column」の検証①</li>
</ul>
<p>また参考までに、PostgreSQLのIDENTITY列と、基本的な使い方や振る舞いは同様のようです。</p>
<ul>
<li>PostgreSQLで連番を自動生成するIDENTITY列。SERIALとどちらを使うべきか</li>
</ul>
<h2 id="おわりに">おわりに</h2><p>マルチテーブル・インサートにおける採番方法について触れ、マルチテーブル・インサート構文でIDENTITY列とSEQUENCEはどちらとも利用できることが分かりました。そのうえで、採番にずれが生じないSEQUENCEを利用することが良いと考えましたが、皆さんはどのように考えられますか？</p>
<p>何か疑問点や問題点がある場合には、遠慮なくご指摘いただけますと幸いです。</p>
]]></content>
    <summary type="html">OracleDBにおける主要な採番方法であるSEQUENCEとIDENTITY列について、基本的な使い方を説明します。続いて、複数のテーブルへ同時にデータを投入する際に利用できるOracleDB特有のマルチテーブル・インサート構文における、それぞれの採番の実装方法と挙動の違いに焦点を当てて解説します</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="IDENTITY" scheme="https://future-architect.github.io/tags/IDENTITY/"/>
    <category term="Oracle" scheme="https://future-architect.github.io/tags/Oracle/"/>
    <category term="SQL" scheme="https://future-architect.github.io/tags/SQL/"/>
  </entry>
  <entry>
    <title>DynamoDBコスト削減のための基本的な施策4点</title>
    <link href="https://future-architect.github.io/articles/20250512a/"/>
    <id>https://future-architect.github.io/articles/20250512a/</id>
    <published>2025-05-11T15:00:00.000Z</published>
    <updated>2025-05-11T15:00:00.000Z</updated>
    <author><name>八木雅斗</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20250512a/top.jpg" alt="" width="800" height="450">

<h2 id="はじめに">はじめに</h2><p>製造・エネルギー事業部所属の八木です。</p>
<p>AWSコストの内訳で大きな割合を占めていた数百億オーダーのデータ（&#x3D;Item）を保存しているDynamoDBのコストを削減する機会があり、その中で行った施策を春の入門祭り2025 にあわせて共有します。</p>
<h2 id="前提">前提</h2><ul>
<li>テーブルのキャパシティモードはオンデマンドで利用しています</li>
</ul>
<h2 id="1-テーブルを再作成してデータ削除コストを減らす">1. テーブルを再作成してデータ削除コストを減らす</h2><p>DynamoDBのストレージ料金を削減するために、100億オーダーの過去データを削除することになりました。</p>
<p>DynamoDBのテーブルからデータを削除するには、DeleteItemを利用することになり、削除するデータごとに書き込みキャパシティユニット（WCU）を消費します。</p>
<ul>
<li>DynamoDB の読み込みと書き込みのオペレーション - Amazon DynamoDB</li>
</ul>
<p>つまり、削除するデータ量に応じてコストが増加するため、大量のデータを削除するには、その分大きなコストが必要になります。</p>
<p>さらに、開発&#x2F;検証環境のテーブルにおいても本番環境と同様にDeleteItemでデータ削除すると、さらに追加の費用が必要となります。</p>
<p>今回、この削除費用を避けるために、開発&#x2F;検証環境で全データを1度リセットしても問題ないテーブルに関しては、テーブルを削除＆再作成をすることでWCUを消費せずにストレージ容量を削減することにしました。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>削除方法</th>
<th>削除対象</th>
<th>コスト(WCUの消費)</th>
</tr>
</thead>
<tbody><tr>
<td>DeleteItem</td>
<td>個別データ</td>
<td>発生する</td>
</tr>
<tr>
<td>テーブル削除＆再作成</td>
<td>全データ</td>
<td>発生しない</td>
</tr>
</tbody></table></div>
<p>本番環境ではもちろんテーブルを削除＆再作成することは難しいと思いますが、開発&#x2F;検証環境でデータをリセットしても問題ないテーブルがある場合は、データ削除コストの節約のための選択肢として実施して良いかもしれません。</p>
<h2 id="2-データ保持期限をTTLで設定">2. データ保持期限をTTLで設定</h2><p>DynamoDBでは以下のように、データの保持期限を設定するTTL（Time To Live）という機能があります。</p>
<blockquote>
<p>TTL では、項目がいつ不要になるかを示す有効期限タイムスタンプを項目ごとに定義できます。DynamoDB は、書き込みスループットを消費することなく、有効期限が切れてから数日以内に期限切れの項目を自動的に削除します。<br>https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/TTL.html</p>
</blockquote>
<p>TTLによるデータの自動削除はWCUを消費しないため、テーブルに保存しておく必要のある期間が分かっているデータは、設定しておくのが良いと思います。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>削除方法</th>
<th>コスト(WCUの消費)</th>
</tr>
</thead>
<tbody><tr>
<td>TTL</td>
<td>発生しない</td>
</tr>
<tr>
<td>DeleteItem</td>
<td>発生する</td>
</tr>
</tbody></table></div>
<h2 id="3-利用していないGSIの削除">3. 利用していないGSIの削除</h2><p>GSIを1つ貼ると、ストレージのコストがテーブル1つ分増加します。また、データの書き込み時にもGSI用のテーブルを更新する必要が発生し、テーブルに貼るGSIの分だけ、ストレージコストや書き込みコストが増加します。</p>
<p>そのため、利用されていないGSIが貼られていないかを確認し、あれば削除していきましょう。</p>
<ul>
<li>DynamoDB のグローバルセカンダリインデックスの使用 - Amazon DynamoDB</li>
<li>コンセプトから学ぶAmazon DynamoDB【GSI篇】 | DevelopersIO</li>
</ul>
<h2 id="4-不要なバックアップの棚卸し">4. 不要なバックアップの棚卸し</h2><p>マネジメントコンソール画面からDynamoDBテーブルのバックアップを作成すると、オンデマンドバックアップが作成され、保存コストが発生します。</p>
<ul>
<li>オンデマンドキャパシティーの料金 - Amazon DynamoDB | AWS</li>
</ul>
<p>DynamoDBのマネジメントコンソールを操作する際に、「テーブル」や「項目を探索」の画面と比較すると、「バックアップ」の画面に遷移することは少ないと思います。そのため、不要になったバックアップの削除が漏れると、なかなかバックアップが残っていることに気づかれず、そのまま保存されたままになってしまう危険性があります。</p>
<p>意外とコスト削減に繋がる過去の産物が見つかるかもしれないので、不要なバックアップが残り続けていないかをチェックしてみましょう。</p>
<h2 id="おわりに">おわりに</h2><p>AWS DynamoDBのコスト削減に向けた具体的な施策を紹介しました。特に大規模なデータを扱うDynamoDB環境においては、わずかな工夫が大きなコスト削減に繋がる可能性があります。</p>
<p>他にも、ユースケースによってはテーブルクラスやキャパシティモードの変更なども効果的かもしれません。</p>
<p>DynamoDBのコスト最適化に取り組む方々の一助となれば幸いです。</p>
]]></content>
    <summary type="html">AWSコストの内訳で大きな割合を占めていた数百億オーダーのデータ（=Item）を保存しているDynamoDBのコストを削減する機会があり、その中で行った施策を「春の入門祭り2025」にあわせて共有します。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="AWS" scheme="https://future-architect.github.io/tags/AWS/"/>
    <category term="DynamoDB" scheme="https://future-architect.github.io/tags/DynamoDB/"/>
    <category term="コスト削減" scheme="https://future-architect.github.io/tags/%E3%82%B3%E3%82%B9%E3%83%88%E5%89%8A%E6%B8%9B/"/>
  </entry>
  <entry>
    <title>Engineer Camp 2024: Rust でのSQLフォーマッタ開発</title>
    <link href="https://future-architect.github.io/articles/20241210a/"/>
    <id>https://future-architect.github.io/articles/20241210a/</id>
    <published>2024-12-09T15:00:00.000Z</published>
    <updated>2024-12-09T15:00:00.000Z</updated>
    <author><name>仲泰志</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>Engineer Camp 2024 に参加した仲です。今回のインターンシップではRust製SQLフォーマッタを開発しました。この記事では、期間中に取り組んだ内容について紹介します！</p>
<p>過去の関連インターン記事はこちらです：</p>
<ul>
<li>Engineer Camp2022 RustでSQLフォーマッタ作成（前編）</li>
<li>Engineer Camp2022 RustでSQLフォーマッタ作成（後編）</li>
</ul>
<h2 id="インターン内容">インターン内容</h2><p>フューチャーが開催している夏のインターンシップ「Engineer Camp 2024」では、社内のプロジェクトに参画し4週間にわたり開発業務を体験します。今年は全19コースの募集がありました。</p>
<p>私が参加したコースは （2）Rust製SQLフォーマッタの開発 です。Rust製のSQLフォーマッタである uroboroSQL-fmt やVSCode拡張等の周辺ツールに対し、不具合修正や機能追加を実施しました。</p>
<p>開発対象である uroboroSQL-fmt は、フューチャーのSQLコーディング規約に基づいてコードを整形するフォーマッタです。詳細は新しいSQLフォーマッターであるuroboroSQL-fmtをリリースしましたの記事をご覧ください。</p>
<ul>
<li>https://github.com/future-architect/uroborosql-fmt</li>
</ul>
<p>実際に取り組んだタスクは様々ありますが、ここではそのうち2つをピックアップしてご紹介します。</p>
<h3 id="1-VSCode拡張への機能追加">1. VSCode拡張への機能追加</h3><p>uroboroSQL-fmtにはVSCode拡張機能が存在します。</p>
<ul>
<li>https://marketplace.visualstudio.com/items?itemName=Future.uroborosql-fmt</li>
</ul>
<p>インターンでは、このVSCode拡張へ以下の2つの機能を追加しました。</p>
<ol>
<li>VSCode の設定を元に、フォーマッタの設定ファイル（<code>.uroborosqlfmtrc.json</code>）を出力する<strong>export</strong>機能</li>
<li>フォーマッタの設定ファイルで指定したフォーマットオプションを、VSCode の設定（<code>settings.json</code>）へ反映する<strong>import</strong>機能</li>
</ol>
<h4 id="機能デモ">機能デモ</h4><p>それぞれの機能を紹介します。</p>
<h5 id="1-export-機能">1. export 機能</h5><p>コマンドパレットから export コマンドを呼び出すことで、VSCodeの設定をフォーマッタの設定ファイル <code>.uroborosqlfmtrc.json</code> に反映できます。</p>
<img fetchpriority="high" src="/images/2024/20241210a/export-demo.avif" alt="コマンドパレットでexportコマンドを実行" width="1200" height="675">

<h5 id="2-import-機能">2. import 機能</h5><p>コマンドパレットから import コマンドを呼び出すことで、フォーマッタの設定ファイル <code>.uroborosqlfmtrc.json</code> の内容をVSCodeの設定に反映できます。</p>
<img src="/images/2024/20241210a/import-demo.avif" alt="コマンドパレットでimportコマンドを実行" width="1200" height="675" loading="lazy">

<h4 id="本機能のユースケース">本機能のユースケース</h4><h5 id="1-マルチリポジトリ構成での設定共有">1. マルチリポジトリ構成での設定共有</h5><p>昨今では、マルチリポジトリ構成での開発は珍しくありません。マルチリポジトリ構成においてVSCodeの設定は各リポジトリに適したものを用意しますが、SQLフォーマッタの設定は関連リポジトリ全体で共有することが望ましいでしょう。そのような場合の設定共有は、フォーマッタ用の設定ファイルを各リポジトリに配置することで対応できます。</p>
<p>しかし、フォーマッタ用の設定ファイルはただのjsonファイルなのでエディタで直接編集するのは手間がかかります。</p>
<p>そこで、今回追加したexport機能により、VSCodeの設定編集UIで作成した設定からフォーマッタ用の設定ファイルを出力し、各リポジトリに配布して共有することが可能となります。</p>
<p>また、VSCodeで<code>uroborosql-fmt.configurationFilePath</code> を設定すれば、プロジェクトのルートにない設定ファイルも参照できます。</p>
<img src="/images/2024/20241210a/usecase-1-share.png" alt="usecase-1-share.png" width="1200" height="618" loading="lazy">

<h5 id="2-既存のフォーマッタ用の設定ファイルに変更を加える場合">2. 既存のフォーマッタ用の設定ファイルに変更を加える場合</h5><p>VSCodeの設定編集UIは、取りうる値をドロップダウンで選択できたり、設定項目のドキュメントを読みながら編集ができます。import 機能を用いることでフォーマッタ用の設定ファイルをVSCodeの設定に取り込むことができ、設定の変更が容易になります。</p>
<p>適切な設定が用意できたら、export 機能でフォーマッタ用の設定ファイルを更新できます。</p>
<img src="/images/2024/20241210a/image.png" alt="image.png" width="1123" height="336" loading="lazy">

<h3 id="2-2WaySQL-との格闘">2. 2WaySQL との格闘</h3><p>uroboroSQL-fmt の特徴の1つとして、2WaySQLに対応していることが挙げられます。2WaySQLをサポートするには通常のSQLとは異なる考慮が必要であるため、uroboroSQL-fmtでは2WaySQL用のフォーマット処理が用意されています。インターン期間中に2WaySQL関連の不具合が見つかりましたが、その1つは原理的に解決が難しいものでした。結局インターン期間でその不具合を完全に解決するには至らず、部分的に解決する方法をとりました。</p>
<p>このセクションではその不具合について説明するとともに、どんな解決策をとったかについて記します。</p>
<h4 id="2WaySQL-とは">2WaySQL とは</h4><p>2WaySQL とは、バインドパラメータや制御構文を利用して実行できるSQLの拡張構文のようなものです。uroboroSQL-fmt は uroboroSQL、go-twowaysql、Doma といった2WaySQLをサポートしています。</p>
<h4 id="uroboroSQL-fmt-の-2WaySQL-フォーマット戦略">uroboroSQL-fmt の 2WaySQL フォーマット戦略</h4><p>2WaySQLは通常のSQLとして不正な構文になることがあるため、通常のSQLのパーサではハンドリングできないケースがあります。uroboroSQL-fmt はこの問題を解消するために、2WaySQLに対してはパーサで読み込む前の段階でテキストを分解し、フォーマット後にマージして再構築するという戦略をとっています。これにより、フォーマット処理のコアロジックやパーサジェネレータの文法定義は通常のSQLをターゲットにしたままで、フォーマッタ全体として2WaySQLに対応することを可能にしています。</p>
<p>2WaySQLのフォーマット処理についてはこれらの記事が詳しいです：</p>
<ul>
<li>uroborosql-fmtにおける2WaySQLフォーマット (前編: フォーマット方法編)</li>
<li>uroborosql-fmtにおける2WaySQLフォーマット (後編: 結果検証編)</li>
</ul>
<h4 id="実際の不具合">実際の不具合</h4><p>インターン期間中に遭遇したフォーマッタの不具合として、2WaySQLにおいて <code>as</code> キーワードの縦揃えが崩れてしまうというものがありました。</p>
<img src="/images/2024/20241210a/as-alignment-actual-expected.png" alt="as-alignment-actual-expected.png" width="871" height="294" loading="lazy">

<h4 id="不具合が起こる仕組み">不具合が起こる仕組み</h4><p>この不具合が発生した理由について、例を使って詳しく追ってみます。</p>
<p>2WaySQLのフォーマットにおけるSQLの分割およびマージは2WaySQLが持つ制御構文（<code>/* IF ... */</code>や<code>/* ELSE */</code>）に基づいて行単位で実施されます。（詳細は前述の記事に譲ります）</p>
<figure class="highlight sql"><figcaption><span>分割前.sql</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line">	<span class="string">&#x27;a&#x27;</span> <span class="keyword">as</span> a</span><br><span class="line"><span class="comment">/*IF true*/</span></span><br><span class="line">,	<span class="string">&#x27;b&#x27;</span> <span class="keyword">as</span> b</span><br><span class="line"><span class="comment">/*ELSE*/</span></span><br><span class="line">,	<span class="string">&#x27;ccccccccccccccc&#x27;</span> <span class="keyword">as</span> c</span><br><span class="line"><span class="comment">/*END*/</span></span><br><span class="line">;</span><br></pre></td></tr></table></figure>

<p>上のようなSQL（分割前.sql）は下に示すように、<code>IF</code> 節の中身を持つSQL（first.sql）と<code>ELSE</code>節の中身を持つSQL（second.sql）に分割されます。</p>
<figure class="highlight sql"><figcaption><span>first.sql</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line">	<span class="string">&#x27;a&#x27;</span> <span class="keyword">as</span> a</span><br><span class="line"><span class="comment">/*IF true*/</span></span><br><span class="line">,	<span class="string">&#x27;b&#x27;</span> <span class="keyword">as</span> b</span><br><span class="line"><span class="comment">/*END*/</span></span><br><span class="line">;</span><br></pre></td></tr></table></figure>

<figure class="highlight sql"><figcaption><span>second.sql</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line">	<span class="string">&#x27;a&#x27;</span> <span class="keyword">as</span> a</span><br><span class="line"><span class="comment">/*ELSE*/</span></span><br><span class="line">,	<span class="string">&#x27;ccccccccccccccc&#x27;</span> <span class="keyword">as</span> c</span><br><span class="line"><span class="comment">/*END*/</span></span><br><span class="line">;</span><br></pre></td></tr></table></figure>

<p>first.sql と second.sql のそれぞれをフォーマットすると次のようになります。<code>as</code> キーワードに注目すると、フォーマット処理によって縦揃えが行われていることを確認できます。</p>
<figure class="highlight sql"><figcaption><span>first.sql</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line">	<span class="string">&#x27;a&#x27;</span>			<span class="keyword">as</span>	a</span><br><span class="line"><span class="comment">/*IF true*/</span></span><br><span class="line">,	<span class="string">&#x27;b&#x27;</span>			<span class="keyword">as</span>	b</span><br><span class="line"><span class="comment">/*END*/</span></span><br><span class="line">;</span><br></pre></td></tr></table></figure>

<figure class="highlight sql"><figcaption><span>second.sql</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line">	<span class="string">&#x27;a&#x27;</span>					<span class="keyword">as</span>	a</span><br><span class="line"><span class="comment">/*ELSE*/</span></span><br><span class="line">,	<span class="string">&#x27;ccccccccccccccc&#x27;</span>	<span class="keyword">as</span>	c</span><br><span class="line"><span class="comment">/*END*/</span></span><br><span class="line">;</span><br></pre></td></tr></table></figure>

<p><code>as</code> の縦揃え位置は <code>as</code> キーワードの前に現れる要素（<code>&#39;a&#39;</code> など）の長さによって変化しますが、分割されたそれぞれのSQLは他方のSQLの情報を持ちません。すると、マージされたSQLでは次のように、<code>IF</code>節のインデントと<code>ELSE</code>節のインデントがずれる、という現象が発生してしまいます。</p>
<figure class="highlight sql"><figcaption><span>merged.sql</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line">	<span class="string">&#x27;a&#x27;</span>	<span class="keyword">as</span>	a</span><br><span class="line"><span class="comment">/*IF true*/</span></span><br><span class="line">,	<span class="string">&#x27;b&#x27;</span>	<span class="keyword">as</span>	b</span><br><span class="line"><span class="comment">/*ELSE*/</span></span><br><span class="line">,	<span class="string">&#x27;ccccccccccccccc&#x27;</span>	<span class="keyword">as</span>	c</span><br><span class="line"><span class="comment">/*END*/</span></span><br><span class="line">;</span><br></pre></td></tr></table></figure>

<h4 id="対処">対処</h4><p>2WaySQLの分割処理およびマージ処理はテキストの行単位で実施されます。そのため縦揃えのように複数行にまたがる情報の保持が必要な処理では、このような問題の発生を防ぐことができません。さらにフォーマッタのコアロジックはできる限り2WaySQLの知識を持たない方針で実装されているため、問題の解決には2WaySQLを扱う処理のアーキテクチャを大きく変える必要がありそうということがわかりました。</p>
<p>2WaySQLを扱うアーキテクチャを新たに検討し実装するのはそれなりに大規模な変更となることが見込まれます。そのような変更をインターン期間中で実施するのは難しいと判断したため、今回はこの問題を<strong>部分的に</strong>解消する修正を入れることにしました。具体的には、通常のSQLとしてパースできる2WaySQLについては分割・マージ処理を行わず、そのままSQLとしてフォーマットするようにしました。</p>
<p>たとえば今回の例に使用したSQLは2WaySQLの機能を使うものの、通常のSQLとしてもパースが可能です。</p>
<figure class="highlight sql"><figcaption><span>通常のSQLとしても文法的に正しいSQL.sql</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line">	<span class="string">&#x27;a&#x27;</span> <span class="keyword">as</span> a</span><br><span class="line"><span class="comment">/*IF true*/</span></span><br><span class="line">,	<span class="string">&#x27;b&#x27;</span> <span class="keyword">as</span> b</span><br><span class="line"><span class="comment">/*ELSE*/</span></span><br><span class="line">,	<span class="string">&#x27;ccccccccccccccc&#x27;</span> <span class="keyword">as</span> c</span><br><span class="line"><span class="comment">/*END*/</span></span><br><span class="line">;</span><br></pre></td></tr></table></figure>

<p>このようなSQLを通常のSQLとしてフォーマットする今回の修正により、下図のように縦揃えのそろったフォーマットが行われるようになりました。</p>
<figure class="highlight sql"><figcaption><span>フォーマット後.sql</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line">	<span class="string">&#x27;a&#x27;</span>					<span class="keyword">as</span>	a</span><br><span class="line"><span class="comment">/*IF true*/</span></span><br><span class="line">,	<span class="string">&#x27;b&#x27;</span>					<span class="keyword">as</span>	b</span><br><span class="line"><span class="comment">/*ELSE*/</span></span><br><span class="line">,	<span class="string">&#x27;ccccccccccccccc&#x27;</span>	<span class="keyword">as</span>	c</span><br><span class="line"><span class="comment">/*END*/</span></span><br><span class="line">;</span><br></pre></td></tr></table></figure>

<p>もちろん、通常のSQLとしてのパースができない2WaySQLでは、依然として同様の問題が発生し得ます。</p>
<p>例えば次に示す2WaySQLは、前述したSQLに from 句と where 句を付加したものです。2WaySQLの条件分岐機能を使っている where 句に注目すると、通常のSQLとして解釈するためには where 句のセパレータ <code>and</code> または <code>or</code> が不足していることがわかります。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-7vxtxv-1" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-7vxtxv-1" title="コードの折り返しを切り替える"></label><figcaption><span>通常のSQLとしては不正な2WaySQL（フォーマット前）.sql</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line">	<span class="string">&#x27;a&#x27;</span> <span class="keyword">as</span> a</span><br><span class="line"><span class="comment">/*IF true*/</span></span><br><span class="line">,	<span class="string">&#x27;b&#x27;</span> <span class="keyword">as</span> b</span><br><span class="line"><span class="comment">/*ELSE*/</span></span><br><span class="line">,	<span class="string">&#x27;ccccccccccccccc&#x27;</span> <span class="keyword">as</span> c</span><br><span class="line"><span class="comment">/*END*/</span></span><br><span class="line"><span class="keyword">from</span></span><br><span class="line">	some_table t</span><br><span class="line"><span class="keyword">where</span></span><br><span class="line"><span class="comment">/*IF some_condition */</span></span><br><span class="line">	some_column <span class="operator">=</span> <span class="string">&#x27;value&#x27;</span>  <span class="comment">-- 通常のSQLとして解釈するには、この位置にセパレータ（and, or）が必要</span></span><br><span class="line"><span class="comment">/*ELSE*/</span></span><br><span class="line">	some_column <span class="operator">=</span> <span class="string">&#x27;other_value&#x27;</span></span><br><span class="line"><span class="comment">/*END*/</span></span><br><span class="line">;</span><br></pre></td></tr></table></figure></div>

<p>これをフォーマットすると次のようになり、<code>as</code> キーワードの縦揃えが再び崩れていることが確認できます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-7vxtxv-2" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-7vxtxv-2" title="コードの折り返しを切り替える"></label><figcaption><span>通常のSQLとしては不正な2WaySQL（フォーマット後）.sql</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span></span><br><span class="line">	<span class="string">&#x27;a&#x27;</span>	<span class="keyword">as</span>	a</span><br><span class="line"><span class="comment">/*IF true*/</span></span><br><span class="line">,	<span class="string">&#x27;b&#x27;</span>	<span class="keyword">as</span>	b</span><br><span class="line"><span class="comment">/*ELSE*/</span></span><br><span class="line">,	<span class="string">&#x27;ccccccccccccccc&#x27;</span>	<span class="keyword">as</span>	c</span><br><span class="line"><span class="comment">/*END*/</span></span><br><span class="line"><span class="keyword">from</span></span><br><span class="line">	some_table	t</span><br><span class="line"><span class="keyword">where</span></span><br><span class="line"><span class="comment">/*IF some_condition */</span></span><br><span class="line">	some_column	<span class="operator">=</span>	<span class="string">&#x27;value&#x27;</span>	<span class="comment">-- 通常のSQLとして解釈するには、この位置にセパレータ（and, or）が必要</span></span><br><span class="line"><span class="comment">/*ELSE*/</span></span><br><span class="line">	some_column	<span class="operator">=</span>	<span class="string">&#x27;other_value&#x27;</span></span><br><span class="line"><span class="comment">/*END*/</span></span><br><span class="line">;</span><br></pre></td></tr></table></figure></div>

<p>このような2WaySQLの扱いについては現在検討中です。今後の修正にご期待ください。</p>
<h3 id="インターンの感想">インターンの感想</h3><p>Rust と静的解析をやりたいという動機で参加したインターンでしたが、仕事の進め方や見積もり方、自分の貢献の伝え方など、至るところに思わぬ学びがたくさんありました。毎日の定例ミーティングや最終発表を通して、人に何かを説明する際の情報のまとめ方・表現のやりかたを鍛えることができたように思います。</p>
<p>開発者の体験を良くする仕組みは私が興味とするところなので、今回そのような具体的なツールに携われたことは非常に大きな経験になりました。すでにあるコードベースを読み解き、影響範囲を検証しながら修正を加えていく作業がとても楽しかったです。</p>
<h2 id="さいごに">さいごに</h2><p>インターンではRust製SQLフォーマッタを開発しました。関わっていただいた社員のみなさん、そして同じインターン生のみなさんにもとても感謝しています。ありがとうございました！</p>
]]></content>
    <summary type="html">Engineer Camp 2024 に参加した仲です。今回のインターンシップではRust製SQLフォーマッタを開発しました。この記事では、期間中に取り組んだ内容について紹介します！]</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="2WaySQL" scheme="https://future-architect.github.io/tags/2WaySQL/"/>
    <category term="Rust" scheme="https://future-architect.github.io/tags/Rust/"/>
    <category term="SQL" scheme="https://future-architect.github.io/tags/SQL/"/>
    <category term="VSCode" scheme="https://future-architect.github.io/tags/VSCode/"/>
    <category term="インターン" scheme="https://future-architect.github.io/tags/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%B3/"/>
    <category term="インターン2024" scheme="https://future-architect.github.io/tags/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%B32024/"/>
    <category term="フォーマッター" scheme="https://future-architect.github.io/tags/%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%82%BF%E3%83%BC/"/>
  </entry>
  <entry>
    <title>PostgreSQLで連番を自動生成するIDENTITY列。SERIALとどちらを使うべきか</title>
    <link href="https://future-architect.github.io/articles/20241113a/"/>
    <id>https://future-architect.github.io/articles/20241113a/</id>
    <published>2024-11-12T15:00:00.000Z</published>
    <updated>2024-11-12T15:00:00.000Z</updated>
    <author><name>真野隼記</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20241113a/elephant.png" alt="" width="540" height="557">

<h2 id="はじめに">はじめに</h2><p>Technology Innovation Group真野です。</p>
<p>2017&#x2F;10&#x2F;5リリースのPostgreSQL 10にて、インサート時に自動で連番を割り当てる <code>GENERATED AS IDENTITY</code> という構文がサポートされました。PostgreSQLの連番作成機能と言えば、 <code>SERIAL</code> <code>BIGSERIAL</code> 型が有名ですが、<code>IDENTITY</code>の方がSQL標準準拠です。</p>
<p><code>SERIAL</code>も<code>IDENTITY</code> のどちらも内部的にはシーケンスを利用していますが、<code>IDENTITY</code>の方が手動で連番カラムに値を指定しにくい機能があり（※後述します）、新規の開発案件であれば <code>IDENTITY</code> を利用すると良いでしょう。</p>
<p>その上で <code>IDENTITY</code> に設定したカラムの挙動について不明点があったので調べてみました。最初に基礎情報をまとめ、調査事項の順で説明します。</p>
<p>なお、調査に用いたPostgreSQLバージョンは <code>17.0</code> です。</p>
<p><strong>2024&#x2F;11&#x2F;13 追記しました:</strong></p>
<ul>
<li>「作成されたシーケンスの名称」章のシーケンス名取得の方法を追記</li>
<li>「シーケンス名の上限63文字を超過したテーブル、カラム名の場合」章を追加</li>
<li>「テーブル名を変更した時シーケンス名はどうなるか」章を追加</li>
<li>「カラム名を変更した時シーケンス名はどうなるか」章を追加</li>
<li>「独自に作成したシーケンスとの紐づけ方法」を追加</li>
</ul>
<h2 id="記事のサマリ">記事のサマリ</h2><ul>
<li>新規構築なら連番の自動採番はSERIAL／BIGSERIALの代わりに <code>GENERATED ALWAYS AS IDENTITY</code> の利用がベター</li>
<li>DEFAULTキーワードは利用せず、省略する</li>
<li>暗黙的に作成されるシーケンスは、テーブル名やカラム名のリネームに追随しないので、合わせてリネームする運用にする</li>
<li>気になった部分の調査事項と結果は下表</li>
</ul>
<div class="scroll"><table>
<thead>
<tr>
<th>調査項目</th>
<th>結果</th>
</tr>
</thead>
<tbody><tr>
<td><code>COPY</code> の挙動</td>
<td>IDENTITYを無効なしで実行可能</td>
</tr>
<tr>
<td>シーケンスリセット</td>
<td><code>RESTART IDENTITY</code> オプションで可能</td>
</tr>
<tr>
<td>パーティションテーブルでの利用</td>
<td>利用できる</td>
</tr>
<tr>
<td>作成されたシーケンスの名称</td>
<td>{テーブル名}_{カラム名}_seq</td>
</tr>
<tr>
<td>作成されたシーケンスを削除したらどうなるか</td>
<td>削除不可</td>
</tr>
<tr>
<td>シーケンス名の上限63文字を超過したテーブル、カラム名の場合</td>
<td>それぞれ29文字上限でオミットされて生成</td>
</tr>
<tr>
<td>テーブル名を変更した時シーケンス名はどうなるか</td>
<td>変化なし</td>
</tr>
<tr>
<td>カラム名を変更した時シーケンス名はどうなるか</td>
<td>変化なし</td>
</tr>
<tr>
<td>独自に作成したシーケンスとの紐づけ方法</td>
<td>できない</td>
</tr>
<tr>
<td>文字列型とGENERATED AS IDENTITYの組み合わせ</td>
<td>設定不可</td>
</tr>
<tr>
<td>SERIAL型とGENERATED AS IDENTITYの組み合わせ</td>
<td>設定不可</td>
</tr>
</tbody></table></div>
<h2 id="IDENTITY列の基本">IDENTITY列の基本</h2><p>型の後に、 <code>GENERATED BY DEFAULT AS IDENTITY</code> といった構文で指定します。下記で<code>color_id</code>がIDENTITY列になります。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> color (</span><br><span class="line">    color_id <span class="type">BIGINT</span> GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    color_name <span class="type">VARCHAR</span> <span class="keyword">NOT NULL</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure></div>

<p>テーブルの状態は以下です。<code>GENERATED AS IDENTITY</code> を付けると暗黙的にNOT NULL制約がつくことも分かります。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-2" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">-</span># \d color;</span><br><span class="line">                                   <span class="keyword">Table</span> &quot;public.color&quot;</span><br><span class="line">   <span class="keyword">Column</span>   <span class="operator">|</span>       Type        <span class="operator">|</span> <span class="keyword">Collation</span> <span class="operator">|</span> Nullable <span class="operator">|</span>             <span class="keyword">Default</span></span><br><span class="line"><span class="comment">------------+-------------------+-----------+----------+----------------------------------</span></span><br><span class="line"> color_id   <span class="operator">|</span> <span class="type">bigint</span>            <span class="operator">|</span>           <span class="operator">|</span> <span class="keyword">not null</span> <span class="operator">|</span> generated <span class="keyword">by</span> <span class="keyword">default</span> <span class="keyword">as</span> <span class="keyword">identity</span></span><br><span class="line"> color_name <span class="operator">|</span> <span class="type">character</span> <span class="type">varying</span> <span class="operator">|</span>           <span class="operator">|</span> <span class="keyword">not null</span> <span class="operator">|</span></span><br><span class="line">Indexes:</span><br><span class="line">    &quot;color_pkey&quot; <span class="keyword">PRIMARY KEY</span>, btree (color_id)</span><br></pre></td></tr></table></figure></div>

<p>このテーブルで<code>color_id</code> を未指定にして、2件データを登録します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-3" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">INSERT INTO</span> color (color_name) <span class="keyword">VALUES</span> (<span class="string">&#x27;Orange&#x27;</span>);</span><br><span class="line"><span class="keyword">INSERT INTO</span> color (color_name) <span class="keyword">VALUES</span> (<span class="string">&#x27;Red&#x27;</span>);</span><br></pre></td></tr></table></figure></div>

<p>結果を見ると、連番が1, 2, …と入っていることが分かります。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> color;</span><br><span class="line"> color_id <span class="operator">|</span> color_name</span><br><span class="line"><span class="comment">----------+------------</span></span><br><span class="line">        <span class="number">1</span> <span class="operator">|</span> Orange</span><br><span class="line">        <span class="number">2</span> <span class="operator">|</span> Red</span><br><span class="line">(<span class="number">2</span> <span class="keyword">rows</span>)</span><br></pre></td></tr></table></figure>

<p>自動的に連番が登録される便利機能ですが、実は明示的に値を登録できてしまいます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-4" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">INSERT INTO</span> color (color_id, color_name) <span class="keyword">VALUES</span> (<span class="number">3</span>, <span class="string">&#x27;Blue&#x27;</span>);</span><br><span class="line"><span class="keyword">INSERT INTO</span> color (color_id, color_name) <span class="keyword">VALUES</span> (<span class="number">4</span>, <span class="string">&#x27;Brown&#x27;</span>);</span><br></pre></td></tr></table></figure></div>

<p>結果は以下の通り。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> color;</span><br><span class="line"> color_id <span class="operator">|</span> color_name</span><br><span class="line"><span class="comment">----------+------------</span></span><br><span class="line">        <span class="number">1</span> <span class="operator">|</span> Orange</span><br><span class="line">        <span class="number">2</span> <span class="operator">|</span> Red</span><br><span class="line">        <span class="number">3</span> <span class="operator">|</span> Blue</span><br><span class="line">        <span class="number">4</span> <span class="operator">|</span> Brown</span><br></pre></td></tr></table></figure>

<p>一度、明示的にIDENTITY列に値を指定してしまうと、再び未指定でインサートした場合に、重複した値が入り、場合によっては一位制約違反になってしまう可能性があります。これはSERIAL／BIGSERIAL型でも同様のお困りごとでした。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-5" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">INSERT INTO</span> color (color_name) <span class="keyword">VALUES</span> (<span class="string">&#x27;Black&#x27;</span>);</span><br><span class="line">ERROR:  duplicate key <span class="keyword">value</span> violates <span class="keyword">unique</span> <span class="keyword">constraint</span> &quot;color_pkey&quot;</span><br><span class="line">DETAIL:  Key (color_id)<span class="operator">=</span>(<span class="number">3</span>) already exists.</span><br></pre></td></tr></table></figure></div>

<p>さて、IDENTITY列にはオプションがありまして、 <code>BY DEFAULT</code> の代わりに <code>ALWAYS</code> が指定できます。これを利用すると、IDENTITY列に明示的に値を指定できなくなります。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-6" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-6" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">DROP</span> <span class="keyword">TABLE</span> color;</span><br><span class="line"><span class="keyword">CREATE TABLE</span> color (</span><br><span class="line">    color_id <span class="type">BIGINT</span> GENERATED ALWAYS <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    color_name <span class="type">VARCHAR</span> <span class="keyword">NOT NULL</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure></div>

<p>color_idに3を指定してインサートとしようとするとエラーが出て止められます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-7" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-7" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">INSERT INTO</span> color (color_id, color_name) <span class="keyword">VALUES</span> (<span class="number">3</span>, <span class="string">&#x27;Blue&#x27;</span>);</span><br><span class="line">ERROR:  cannot <span class="keyword">insert</span> a non<span class="operator">-</span><span class="keyword">DEFAULT</span> <span class="keyword">value</span> <span class="keyword">into</span> <span class="keyword">column</span> &quot;color_id&quot;</span><br><span class="line">DETAIL:  <span class="keyword">Column</span> &quot;color_id&quot; <span class="keyword">is</span> an <span class="keyword">identity</span> <span class="keyword">column</span> defined <span class="keyword">as</span> GENERATED ALWAYS.</span><br><span class="line">HINT:  Use OVERRIDING <span class="keyword">SYSTEM</span> <span class="keyword">VALUE</span> <span class="keyword">to</span> override.</span><br></pre></td></tr></table></figure></div>

<p>脱出ハッチも用意されており、<code>OVERRIDING SYSTEM VALUE</code> を利用すると強制的に上書きもできます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-8" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-8" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">INSERT INTO</span> color (color_name) <span class="keyword">VALUES</span> (<span class="string">&#x27;Orange&#x27;</span>);</span><br><span class="line"><span class="keyword">INSERT INTO</span> color (color_name) <span class="keyword">VALUES</span> (<span class="string">&#x27;Red&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- OVERRIDING SYSTEM VALUEを利用（エラーにせず、IDENTITYに明示的な値を登録可能）</span></span><br><span class="line"><span class="keyword">INSERT INTO</span> color (color_id, color_name) OVERRIDING <span class="keyword">SYSTEM</span> <span class="keyword">VALUE</span> <span class="keyword">VALUES</span> (<span class="number">3</span>, <span class="string">&#x27;Blue&#x27;</span>);</span><br><span class="line"><span class="keyword">INSERT INTO</span> color (color_id, color_name) OVERRIDING <span class="keyword">SYSTEM</span> <span class="keyword">VALUE</span> <span class="keyword">VALUES</span> (<span class="number">4</span>, <span class="string">&#x27;Brown&#x27;</span>);</span><br></pre></td></tr></table></figure></div>

<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> color;</span><br><span class="line"> color_id <span class="operator">|</span> color_name</span><br><span class="line"><span class="comment">----------+------------</span></span><br><span class="line">        <span class="number">1</span> <span class="operator">|</span> Orange</span><br><span class="line">        <span class="number">2</span> <span class="operator">|</span> Red</span><br><span class="line">        <span class="number">3</span> <span class="operator">|</span> Blue</span><br><span class="line">        <span class="number">4</span> <span class="operator">|</span> Brown</span><br><span class="line">(<span class="number">4</span> <span class="keyword">rows</span>)</span><br></pre></td></tr></table></figure>

<p>通常は、 <code>OVERRIDING SYSTEM VALUE</code> をうっかり付けて登録してしまう開発者はごく限られていると想定すると、<code>GENERATED BY DEFAULT AS IDENTITY</code> をSERIAL型の代わりに利用する方が、誤登録を発生させずベターだと思います。</p>
<p>まとめると以下です。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>項目</th>
<th>説明</th>
</tr>
</thead>
<tbody><tr>
<td>GENERATED BY DEFAULT AS IDENTITY</td>
<td>SERIAL型と同等。自動採番列に登録可能</td>
</tr>
<tr>
<td>GENERATED ALWAYS AS IDENTITY</td>
<td>SERIALと同等だが、<code>OVERRIDING SYSTEM VALUE</code>を付けないことには登録不可</td>
</tr>
</tbody></table></div>
<p>余談ですが、該当カラムに明示的にインサートしていることを示しつつ、値が自動採番を用いることを示したい場合は <code>DEFAULT</code> キーワードを利用できます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-9" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-9" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- DEFAULT を指定</span></span><br><span class="line"><span class="keyword">INSERT INTO</span> color (color_id, color_name) <span class="keyword">VALUES</span> (<span class="keyword">DEFAULT</span>, <span class="string">&#x27;Black&#x27;</span>);</span><br></pre></td></tr></table></figure></div>

<p>結果です。無事登録できています。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> color;</span><br><span class="line"> color_id <span class="operator">|</span> color_name</span><br><span class="line"><span class="comment">----------+------------</span></span><br><span class="line">        <span class="number">1</span> <span class="operator">|</span> Orange</span><br><span class="line">        <span class="number">2</span> <span class="operator">|</span> Red</span><br><span class="line">        <span class="number">3</span> <span class="operator">|</span> Blue</span><br><span class="line">        <span class="number">4</span> <span class="operator">|</span> Brown</span><br><span class="line">        <span class="number">5</span> <span class="operator">|</span> Black</span><br><span class="line">(<span class="number">5</span> <span class="keyword">rows</span>)</span><br></pre></td></tr></table></figure>

<p>どこまでドライバ／ライブラリ／コード生成／解析などのツールが対応しているか不明で、使い所も今イチわかりませんが、チーム内でIDENTITY列にインサートする際、「省略する／DEFAULTを指定する」のどちらかは統一したほうが良いでしょう。私は省略で良いかなと思いますが、みなさんはどうお考えでしょうか？</p>
<h2 id="GENERATED-ALWAYS-AS-IDENTITY-に対する調査">GENERATED ALWAYS AS IDENTITY に対する調査</h2><p>前章の通り、自動連番生成列だと <code>GENERATED ALWAYS AS IDENTITY</code> がベターな選択だという前提で、以下を調査しました。</p>
<h3 id="1-COPY-の挙動">1. COPY の挙動</h3><p>データ移行などで大量のデータ登録に <code>COPY</code> を用いることが多いでしょう。まずIDENTITY列が未指定の場合で動かします。FROMに <code>STDIN</code> を指定することで標準入力で動かすことができるので、これで検証します。最後に<code>COPY 5</code>とあり、正常終了したことがわかります。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-10" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-10" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">COPY</span> color (color_name) <span class="keyword">FROM</span> STDIN <span class="keyword">WITH</span> (FORMAT csv);</span><br><span class="line">Enter data <span class="keyword">to</span> be copied followed <span class="keyword">by</span> a newline.</span><br><span class="line"><span class="keyword">End</span> <span class="keyword">with</span> a backslash <span class="keyword">and</span> a <span class="keyword">period</span> <span class="keyword">on</span> a line <span class="keyword">by</span> itself, <span class="keyword">or</span> an EOF signal.</span><br><span class="line"><span class="operator">&gt;&gt;</span> Orange</span><br><span class="line"><span class="operator">&gt;&gt;</span> Red</span><br><span class="line"><span class="operator">&gt;&gt;</span> Blue</span><br><span class="line"><span class="operator">&gt;&gt;</span> Brown</span><br><span class="line"><span class="operator">&gt;&gt;</span> Black</span><br><span class="line"><span class="operator">&gt;&gt;</span> \.</span><br><span class="line"><span class="operator">&gt;&gt;</span> <span class="operator">&gt;&gt;</span> <span class="operator">&gt;&gt;</span> <span class="operator">&gt;&gt;</span> <span class="keyword">COPY</span> <span class="number">5</span></span><br></pre></td></tr></table></figure></div>

<p>テーブルの結果です。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> color;</span><br><span class="line"> color_id <span class="operator">|</span> color_name</span><br><span class="line"><span class="comment">----------+------------</span></span><br><span class="line">        <span class="number">1</span> <span class="operator">|</span> Orange</span><br><span class="line">        <span class="number">2</span> <span class="operator">|</span> Red</span><br><span class="line">        <span class="number">3</span> <span class="operator">|</span> Blue</span><br><span class="line">        <span class="number">4</span> <span class="operator">|</span> Brown</span><br><span class="line">        <span class="number">5</span> <span class="operator">|</span> Black</span><br><span class="line">(<span class="number">5</span> <span class="keyword">rows</span>)</span><br></pre></td></tr></table></figure>

<p>続いて、IDENTITY列に値を指定します。CSVなどからデータ移行する場合はこのようなケースもあるでしょう。こちらも正常終了します（！）。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-11" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-11" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">COPY</span> color (color_id, color_name) <span class="keyword">FROM</span> STDIN <span class="keyword">WITH</span> (FORMAT csv);</span><br><span class="line">Enter data <span class="keyword">to</span> be copied followed <span class="keyword">by</span> a newline.</span><br><span class="line"><span class="keyword">End</span> <span class="keyword">with</span> a backslash <span class="keyword">and</span> a <span class="keyword">period</span> <span class="keyword">on</span> a line <span class="keyword">by</span> itself, <span class="keyword">or</span> an EOF signal.</span><br><span class="line"><span class="operator">&gt;&gt;</span> <span class="number">21</span>,Orange</span><br><span class="line"><span class="operator">&gt;&gt;</span> <span class="number">22</span>,Red</span><br><span class="line"><span class="operator">&gt;&gt;</span> <span class="number">23</span>,Blue</span><br><span class="line"><span class="operator">&gt;&gt;</span> <span class="number">24</span>,Brown</span><br><span class="line"><span class="operator">&gt;&gt;</span> <span class="number">25</span>,Black</span><br><span class="line"><span class="operator">&gt;&gt;</span> \.</span><br><span class="line"><span class="keyword">COPY</span> <span class="number">5</span></span><br></pre></td></tr></table></figure></div>

<p>テーブルの結果です。指定した値でcolor_idが登録されていますね。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> color;</span><br><span class="line"> color_id <span class="operator">|</span> color_name</span><br><span class="line"><span class="comment">----------+------------</span></span><br><span class="line">       <span class="number">21</span> <span class="operator">|</span> Orange</span><br><span class="line">       <span class="number">22</span> <span class="operator">|</span> Red</span><br><span class="line">       <span class="number">23</span> <span class="operator">|</span> Blue</span><br><span class="line">       <span class="number">24</span> <span class="operator">|</span> Brown</span><br><span class="line">       <span class="number">25</span> <span class="operator">|</span> Black</span><br><span class="line">(<span class="number">5</span> <span class="keyword">rows</span>)</span><br></pre></td></tr></table></figure>

<p>COPYの場合、<code>IEDNTITY列</code> をALTER文で取り除く必要があるかと思いましたが、不要なようです。逆に嬉しいなと感じました。</p>
<p>ドキュメントのどこかに書いていそうだなと探したら、CREATE TABLEやCOPYのページにちゃんと書いてありました。</p>
<ul>
<li>https://www.postgresql.jp/document/16/html/sql-createtable.html#SQL-CREATETABLE-PARMS-GENERATED-IDENTITY</li>
<li>https://www.postgresql.org/docs/current/sql-copy.html#:~:text=For%20identity%20columns</li>
</ul>
<h3 id="2-シーケンスリセット">2. シーケンスリセット</h3><p>単体（E2E）テストなどで、事前／事後データをTRUNCATEして次のテストに備えることはよくあります。この時、TRUNCATEと同時に連番もリセットしたいことが多いでしょう。SERIAL型と同様に、<code>RESET IDENTITY</code> オプションが利用できます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-12" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-12" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- 何かしらINSERTしてコミット</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- RESTART IDENTITY オプションでシーケンスもリセット</span></span><br><span class="line"><span class="keyword">TRUNCATE</span> <span class="keyword">TABLE</span> color RESTART <span class="keyword">IDENTITY</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 別のテストでインサート</span></span><br><span class="line"><span class="keyword">INSERT INTO</span> color (color_name) <span class="keyword">VALUES</span> (<span class="string">&#x27;Orange&#x27;</span>);</span><br><span class="line"><span class="keyword">INSERT INTO</span> color (color_name) <span class="keyword">VALUES</span> (<span class="string">&#x27;Red&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 連番はリセットされ1から始まる</span></span><br><span class="line"> color_id <span class="operator">|</span> color_name</span><br><span class="line"><span class="comment">----------+------------</span></span><br><span class="line">        <span class="number">1</span> <span class="operator">|</span> Orange</span><br><span class="line">        <span class="number">2</span> <span class="operator">|</span> Red</span><br></pre></td></tr></table></figure></div>

<p>参考: https://www.postgresql.jp/docs/16/sql-truncate.html</p>
<p>もちろん、以下のように <code>setval()</code>でシーケンス値のリセットも可能です。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-13" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-13" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> setval(<span class="string">&#x27;color_color_id_seq&#x27;</span>, <span class="number">1</span>, <span class="literal">false</span>);</span><br></pre></td></tr></table></figure></div>

<h3 id="3-パーティションテーブルでの利用">3. パーティションテーブルでの利用</h3><p>パーティションテーブルで利用可能か、試しています。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-14" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-14" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- コメントテーブルを作成</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> comment (</span><br><span class="line">    comment_id <span class="type">BIGINT</span> GENERATED ALWAYS <span class="keyword">AS</span> <span class="keyword">IDENTITY</span>,</span><br><span class="line">    content TEXT <span class="keyword">NOT NULL</span>,</span><br><span class="line">    comment_date <span class="type">DATE</span>,</span><br><span class="line">    <span class="keyword">CONSTRAINT</span> comment_pk <span class="keyword">PRIMARY KEY</span> (comment_date, comment_id)</span><br><span class="line">) <span class="keyword">PARTITION</span> <span class="keyword">BY</span> <span class="keyword">RANGE</span> (comment_date);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- パーティションテーブルの作成</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> comment_2024 <span class="keyword">PARTITION</span> <span class="keyword">OF</span> comment</span><br><span class="line">  <span class="keyword">FOR</span> <span class="keyword">VALUES</span>  <span class="keyword">FROM</span> (<span class="string">&#x27;2024-01-01&#x27;</span>) <span class="keyword">TO</span> (<span class="string">&#x27;2025-01-01&#x27;</span>);</span><br></pre></td></tr></table></figure></div>

<p>テーブル状態は次のようになりました。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-15" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-15" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">\d comment;</span><br><span class="line">                     Partitioned <span class="keyword">table</span> &quot;public.comment&quot;</span><br><span class="line">    <span class="keyword">Column</span>    <span class="operator">|</span>  Type  <span class="operator">|</span> <span class="keyword">Collation</span> <span class="operator">|</span> Nullable <span class="operator">|</span>           <span class="keyword">Default</span></span><br><span class="line"><span class="comment">--------------+--------+-----------+----------+------------------------------</span></span><br><span class="line"> comment_id   <span class="operator">|</span> <span class="type">bigint</span> <span class="operator">|</span>           <span class="operator">|</span> <span class="keyword">not null</span> <span class="operator">|</span> generated always <span class="keyword">as</span> <span class="keyword">identity</span></span><br><span class="line"> content      <span class="operator">|</span> text   <span class="operator">|</span>           <span class="operator">|</span> <span class="keyword">not null</span> <span class="operator">|</span></span><br><span class="line"> comment_date <span class="operator">|</span> <span class="type">date</span>   <span class="operator">|</span>           <span class="operator">|</span> <span class="keyword">not null</span> <span class="operator">|</span></span><br><span class="line"><span class="keyword">Partition</span> key: <span class="keyword">RANGE</span> (comment_date)</span><br><span class="line">Indexes:</span><br><span class="line">    &quot;comment_pk&quot; <span class="keyword">PRIMARY KEY</span>, btree (comment_date, comment_id)</span><br><span class="line">Number <span class="keyword">of</span> partitions: <span class="number">1</span> (Use \d<span class="operator">+</span> <span class="keyword">to</span> list them.)</span><br></pre></td></tr></table></figure></div>

<p>続いて、データ登録します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-16" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-16" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">INSERT INTO</span> comment (content, comment_date) <span class="keyword">VALUES</span> (<span class="string">&#x27;Orange&#x27;</span>, <span class="string">&#x27;2024-05-15&#x27;</span>);</span><br><span class="line"><span class="keyword">INSERT INTO</span> comment (content, comment_date) <span class="keyword">VALUES</span> (<span class="string">&#x27;Red&#x27;</span>, <span class="string">&#x27;2024-05-16&#x27;</span>);</span><br><span class="line"><span class="keyword">INSERT INTO</span> comment (content, comment_date) <span class="keyword">VALUES</span> (<span class="string">&#x27;Blue&#x27;</span>, <span class="string">&#x27;2024-05-16&#x27;</span>);</span><br></pre></td></tr></table></figure></div>

<p>テーブルは以下のように登録されました。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> comment;</span><br><span class="line"> comment_id <span class="operator">|</span> content <span class="operator">|</span> comment_date</span><br><span class="line"><span class="comment">------------+---------+--------------</span></span><br><span class="line">          <span class="number">1</span> <span class="operator">|</span> Orange  <span class="operator">|</span> <span class="number">2024</span><span class="number">-05</span><span class="number">-15</span></span><br><span class="line">          <span class="number">2</span> <span class="operator">|</span> Red     <span class="operator">|</span> <span class="number">2024</span><span class="number">-05</span><span class="number">-16</span></span><br><span class="line">          <span class="number">3</span> <span class="operator">|</span> Blue    <span class="operator">|</span> <span class="number">2024</span><span class="number">-05</span><span class="number">-16</span></span><br><span class="line">(<span class="number">3</span> <span class="keyword">rows</span>)</span><br></pre></td></tr></table></figure>

<p>さっと利用した感じ、特に課題は無いかなと思います。</p>
<h3 id="4-作成されたシーケンスの名称">4. 作成されたシーケンスの名称</h3><p>下記のようなSQLで抽出できます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-17" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-17" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"> <span class="keyword">SELECT</span></span><br><span class="line">    t.relname <span class="keyword">as</span> table_name,</span><br><span class="line">    a.attname <span class="keyword">as</span> column_name,</span><br><span class="line">    s.relname <span class="keyword">as</span> sequence_name</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">    pg_class s</span><br><span class="line"><span class="keyword">JOIN</span></span><br><span class="line">    pg_depend d <span class="keyword">ON</span> d.objid <span class="operator">=</span> s.oid</span><br><span class="line"><span class="keyword">JOIN</span></span><br><span class="line">    pg_class t <span class="keyword">ON</span> d.refobjid <span class="operator">=</span> t.oid</span><br><span class="line"><span class="keyword">JOIN</span></span><br><span class="line">    pg_attribute a <span class="keyword">ON</span> a.attnum <span class="operator">=</span> d.refobjsubid <span class="keyword">AND</span> a.attrelid <span class="operator">=</span> t.oid</span><br><span class="line"><span class="keyword">WHERE</span></span><br><span class="line">    s.relkind <span class="operator">=</span> <span class="string">&#x27;S&#x27;</span></span><br><span class="line">;</span><br></pre></td></tr></table></figure></div>

<p>結果は次の通り、 <code>color_color_id_seq</code>。{テーブル名}_{カラム名}_seq という体系で生成されるようです。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"> table_name <span class="operator">|</span> column_name <span class="operator">|</span>   sequence_name</span><br><span class="line"><span class="comment">------------+-------------+--------------------</span></span><br><span class="line"> color      <span class="operator">|</span> color_id    <span class="operator">|</span> color_color_id_seq</span><br><span class="line">(<span class="number">1</span> <span class="type">row</span>)</span><br></pre></td></tr></table></figure>

<p>上記のSQLは少し長いので、 <code>pg_get_serial_sequence(table text, column text)</code> というシステムカタログ情報関数も用意されています。</p>
<ul>
<li>https://www.postgresql.jp/document/16/html/functions-info.html#FUNCTIONS-INFO-CATALOG</li>
</ul>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-18" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-18" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> pg_get_serial_sequence(<span class="string">&#x27;color&#x27;</span>, <span class="string">&#x27;color_id&#x27;</span>) <span class="keyword">AS</span> sequence_name;</span><br></pre></td></tr></table></figure></div>

<p>結果です。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line">       sequence_name</span><br><span class="line"><span class="comment">---------------------------</span></span><br><span class="line"> public.color_color_id_seq</span><br><span class="line">(<span class="number">1</span> <span class="type">row</span>)</span><br></pre></td></tr></table></figure>

<h3 id="5-作成されたシーケンスを削除したらどうなるか">5. 作成されたシーケンスを削除したらどうなるか</h3><p>誤ってIDENTITY列が内部的に使用するシーケンスオブジェクトを削除したら、不正な状態にならないかテストです。当然、失敗します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-19" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-19" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span>#  <span class="keyword">drop</span> sequence color_color_id_seq;</span><br><span class="line">ERROR:  cannot <span class="keyword">drop</span> sequence color_color_id_seq because <span class="keyword">column</span> color_id <span class="keyword">of</span> <span class="keyword">table</span> color requires it</span><br><span class="line">HINT:  You can <span class="keyword">drop</span> <span class="keyword">column</span> color_id <span class="keyword">of</span> <span class="keyword">table</span> color instead.</span><br></pre></td></tr></table></figure></div>

<p>もし、このシーケンスを削除したい場合は、colorテーブルのcolor_id列を削除する必要があるとあります。親切なメッセージですね。</p>
<h3 id="6-シーケンス名の上限63文字を超過したテーブル、カラム名の場合">6. シーケンス名の上限63文字を超過したテーブル、カラム名の場合</h3><p>PostgreSQLではシーケンスに限らず、識別子の最長は63文字です。</p>
<ul>
<li>https://www.postgresql.jp/document/16/html/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS</li>
</ul>
<p>そのため、IDENTITYで自動で生成されるシーケンス名の体系が、{テーブル名}_{カラム名}_seq だとすると、超過した場合にどう命名されるか気になりました。</p>
<p>試してみます。テーブル名が36文字、カラム名が39文字です。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-20" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-20" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> looooooooooooooooooooooooooooooooong (</span><br><span class="line">    looooooooooooooooooooooooooooooooong_id <span class="type">BIGINT</span> GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    color_name <span class="type">VARCHAR</span> <span class="keyword">NOT NULL</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure></div>

<p>シーケンス名を確認すると、次の結果となりました。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-21" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-21" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">SELECT</span> pg_get_serial_sequence(<span class="string">&#x27;looooooooooooooooooooooooooooooooong&#x27;</span>, <span class="string">&#x27;looooooooooooooooooooooooooooooooong_id&#x27;</span>) <span class="keyword">AS</span> sequence_name;</span><br><span class="line">                             sequence_name</span><br><span class="line"><span class="comment">------------------------------------------------------------------------</span></span><br><span class="line"> public.loooooooooooooooooooooooooooo_loooooooooooooooooooooooooooo_seq</span><br></pre></td></tr></table></figure></div>

<p>テーブル名、カラム名が長いと最長で29文字で前方からオミットされて生成されるようです。<strong>エラーにならない！</strong> 点は注意が必要です。</p>
<p>直接シーケンス名を指定して <code>setval()</code> するときに困ることが多いかなと思いますので、注意が必要です。</p>
<h3 id="7-テーブル名を変更した時シーケンス名はどうなるか">7. テーブル名を変更した時シーケンス名はどうなるか</h3><p>シーケンス名は自動生成されますが、ALTERでテーブル名を変えた場合にどうなるか確かめます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-22" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-22" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">ALTER TABLE</span> looooooooooooooooooooooooooooooooong RENAME <span class="keyword">TO</span> color;</span><br></pre></td></tr></table></figure></div>

<p>結果は以下の通り、変化無しです。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-23" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-23" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">SELECT</span> pg_get_serial_sequence(<span class="string">&#x27;color&#x27;</span>, <span class="string">&#x27;looooooooooooooooooooooooooooooooong_id&#x27;</span>) <span class="keyword">AS</span> sequence_name;</span><br><span class="line">                             sequence_name</span><br><span class="line"><span class="comment">------------------------------------------------------------------------</span></span><br><span class="line"> public.loooooooooooooooooooooooooooo_loooooooooooooooooooooooooooo_seq</span><br><span class="line">(<span class="number">1</span> <span class="type">row</span>)</span><br></pre></td></tr></table></figure></div>

<p>そのため、テーブル名を変更した場合は、シーケンス名もリネームするような運用を行った方が良いでしょう。</p>
<h3 id="8-カラム名を変更した時シーケンス名はどうなるか">8. カラム名を変更した時シーケンス名はどうなるか</h3><p>7と同様に、カラム名を変更した場合にシーケンス名がどうなるか確認します。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-24" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-24" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">ALTER TABLE</span> color RENAME <span class="keyword">COLUMN</span> looooooooooooooooooooooooooooooooong_id <span class="keyword">TO</span> color_id;</span><br></pre></td></tr></table></figure></div>

<p>結果は以下の通り、テーブル名と同様、カラム名の変更も変化ありません。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-25" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-25" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">SELECT</span> pg_get_serial_sequence(<span class="string">&#x27;color&#x27;</span>, <span class="string">&#x27;color_id&#x27;</span>) <span class="keyword">AS</span> sequence_name;</span><br><span class="line">                             sequence_name</span><br><span class="line"><span class="comment">------------------------------------------------------------------------</span></span><br><span class="line"> public.loooooooooooooooooooooooooooo_loooooooooooooooooooooooooooo_seq</span><br><span class="line">(<span class="number">1</span> <span class="type">row</span>)</span><br></pre></td></tr></table></figure></div>

<p>結論も7と同様、テーブル名／カラム名が変更した場合は、シーケンス名もリネームする運用を行うとベターでしょう。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-26" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-26" title="コードの折り返しを切り替える"></label><figcaption><span>シーケンスのリネーム</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">ALTER</span> SEQUENCE loooooooooooooooooooooooooooo_loooooooooooooooooooooooooooo_seq RENAME <span class="keyword">TO</span> color_color_id_seq;</span><br></pre></td></tr></table></figure></div>

<h3 id="9-独自に作成したシーケンスとの紐づけ方法">9. 独自に作成したシーケンスとの紐づけ方法</h3><p>SERIAL型であれば、以下のように指定すると独自のシーケンスと紐づけることができました。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-27" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-27" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- 独自シーケンス</span></span><br><span class="line"><span class="keyword">CREATE</span> SEQUENCE custom_color_seq;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- DEAULTでシーケンスと紐づける</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> color (</span><br><span class="line">    color_id <span class="type">BIGINT</span> <span class="keyword">NOT NULL</span> <span class="keyword">DEFAULT</span> nextval(<span class="string">&#x27;custom_color_seq&#x27;</span>) <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    color_name <span class="type">VARCHAR</span> <span class="keyword">NOT NULL</span></span><br><span class="line">);</span><br><span class="line"><span class="keyword">ALTER</span> SEQUENCE custom_color_seq OWNED <span class="keyword">BY</span> color.color_id;</span><br></pre></td></tr></table></figure></div>

<p>BIGINTを指定していて、BIGSERIALを使っていないじゃない？と思うかもしれません。しかしドキュメントにも記載通り、以下の2つの構文は同義ですので、これが言えます。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> tablename (</span><br><span class="line">    colname SERIAL</span><br><span class="line">);</span><br></pre></td></tr></table></figure>

<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-28" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-28" title="コードの折り返しを切り替える"></label><figcaption><span>SERIAL型の裏側</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> SEQUENCE tablename_colname_seq <span class="keyword">AS</span> <span class="type">integer</span>;</span><br><span class="line"><span class="keyword">CREATE TABLE</span> tablename (</span><br><span class="line">    colname <span class="type">integer</span> <span class="keyword">NOT NULL</span> <span class="keyword">DEFAULT</span> nextval(<span class="string">&#x27;tablename_colname_seq&#x27;</span>)</span><br><span class="line">);</span><br><span class="line"><span class="keyword">ALTER</span> SEQUENCE tablename_colname_seq OWNED <span class="keyword">BY</span> tablename.colname;</span><br></pre></td></tr></table></figure></div>

<p>IDENTITY列に関しては、手動で作成したシーケンスとIDENTITY列を紐づける構文は、ドキュメントを探した時点では存在しませんでした（予め作成したシーケンスを複数の用途で共有して使いたいといったユースケースは実現できなさそうです）。</p>
<p>名称だけの話であれば可能です。作成時にシーケンスオプションで指定するか、先述の通り、 <code>ALTER SEQUENCE RENAME</code> で変更できます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-29" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-29" title="コードの折り返しを切り替える"></label><figcaption><span>シーケンスオプションで指定</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> color (</span><br><span class="line">    color_id <span class="type">BIGINT</span> GENERATED ALWAYS <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> (SEQUENCE NAME custom_color_seq) <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    color_name <span class="type">VARCHAR</span> <span class="keyword">NOT NULL</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure></div>

<p>また、次のように名称以外も、シーケンスオプションで指定できます（ALTERで変更も可能）です。一般的なユースケースでは、困ることは無いかなと思います。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-30" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-30" title="コードの折り返しを切り替える"></label><figcaption><span>シーケンスの開始値、キャッシュ値などを指定</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> color (</span><br><span class="line">    color_id <span class="type">INT</span> GENERATED ALWAYS <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> (<span class="keyword">START</span> <span class="keyword">WITH</span> <span class="number">10</span> INCREMENT <span class="keyword">BY</span> <span class="number">1</span> CACHE <span class="number">100</span>),</span><br><span class="line">    color_name <span class="type">VARCHAR</span> <span class="keyword">NOT NULL</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure></div>

<h3 id="10-文字列型とGENERATED-AS-IDENTITYの組み合わせ">10. 文字列型とGENERATED AS IDENTITYの組み合わせ</h3><p>文字列型（text型）にGENERATED ALWAYS AS IDENTITYを指定すると、いい感じの型変換により ‘1’、’2’、…といった採番がされないかと思いついたので試しました。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-31" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-31" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">CREATE TABLE</span> color (</span><br><span class="line">    color_id text GENERATED ALWAYS <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    color_name <span class="type">VARCHAR</span> <span class="keyword">NOT NULL</span></span><br><span class="line">);</span><br><span class="line">ERROR:  <span class="keyword">identity</span> <span class="keyword">column</span> type must be <span class="type">smallint</span>, <span class="type">integer</span>, <span class="keyword">or</span> <span class="type">bigint</span></span><br></pre></td></tr></table></figure></div>

<p>無事エラーで、これは対応していないようです。型としては、<code>smallint</code> <code>integer</code> <code>bigint</code> のみ対応。</p>
<h3 id="11-SERIAL型とGENERATED-AS-IDENTITYの組み合わせ">11. SERIAL型とGENERATED AS IDENTITYの組み合わせ</h3><p>SERIAL型であれば、型としては <code>integer</code> 型なので、いけるのではと一応チャレンジしました。結果は以下のエラーです。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-bfhv6r-32" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-bfhv6r-32" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">CREATE TABLE</span> color (</span><br><span class="line">    color_id SERIAL GENERATED <span class="keyword">BY</span> <span class="keyword">DEFAULT</span> <span class="keyword">AS</span> <span class="keyword">IDENTITY</span> <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    color_name <span class="type">VARCHAR</span> <span class="keyword">NOT NULL</span></span><br><span class="line">);</span><br><span class="line">ERROR:  <span class="keyword">both</span> <span class="keyword">default</span> <span class="keyword">and</span> <span class="keyword">identity</span> specified <span class="keyword">for</span> <span class="keyword">column</span> &quot;color_id&quot; <span class="keyword">of</span> <span class="keyword">table</span> &quot;color&quot;</span><br></pre></td></tr></table></figure></div>

<p>2つIDENTITYが指定されているね、というエラーメッセージです（当然のことながら）手堅くブロックしてくれています。助かりますね。</p>
<h2 id="まとめ">まとめ</h2><p>PostgreSQLの自動採番機能であるIDENTITY列について試しました。すでにSERIAL／BIGSERIAL型を利用している稼働中のシステムであれば、あえて乗り換えるメリットは小さいでしょう。</p>
<p>新規構築分に関しては、ほぼSERIAL&#x2F;BIGSERIALの使い勝手と同等で、<code>GENERATED ALWAYS</code> を利用することで誤登録を防ぐことができるという意味で、積極的に利用していく方針で良いのでは？と感じました。</p>
<p>何か他のハマりどころがあれば、Xなどでコメントいただけると幸いです。ありがとうございました。</p>
]]></content>
    <summary type="html">PostgreSQLのIDENTITYに設定したカラムの挙動について不明点があったので調べてみました</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="IDENTITY" scheme="https://future-architect.github.io/tags/IDENTITY/"/>
    <category term="PostgreSQL" scheme="https://future-architect.github.io/tags/PostgreSQL/"/>
  </entry>
  <entry>
    <title>PostgreSQL17リリース: 排他制約がパーティションの親テーブルに定義できるようになった</title>
    <link href="https://future-architect.github.io/articles/20241106a/"/>
    <id>https://future-architect.github.io/articles/20241106a/</id>
    <published>2024-11-05T15:00:00.000Z</published>
    <updated>2024-11-05T15:00:00.000Z</updated>
    <author><name>真野隼記</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20241106a/top.png" alt="top.png" width="761" height="366">

<p>PostgreSQL 17のリリース記念連載の2本目です。</p>
<h2 id="はじめに">はじめに</h2><p>Technology Innovation Group真野です。</p>
<p>リリースノートの「E.1.3.2. Utility Commands」に記載がある、パーティションテーブルに対して宣言的に排他制約を設定できるようになったアップデートについて取り上げます。</p>
<blockquote>
<p>Allow exclusion constraints on partitioned tables (Paul A. Jungwirth) §<br>As long as exclusion constraints compare partition key columns for equality, other columns can use exclusion constraint-specific comparisons.</p>
<p>パーティショニングされたテーブルに排他制約を許可する（Paul A. Jungwirth）<br>排他制約がパーティションキー列に対して等価を比較する限り、他の列は排他制約特有の比較を使用できます。<br>https://www.postgresql.org/docs/17/release-17.html#RELEASE-17-UTILITY</p>
</blockquote>
<h2 id="排他制約とは何か？">排他制約とは何か？</h2><p>PostgreSQL 9.0 で追加された機能で、複雑な条件が指定できる一意制約のようなものと理解すると良いかなと思います。</p>
<ul>
<li>https://www.postgresql.jp/docs/16/ddl-constraints.html#DDL-CONSTRAINTS-EXCLUSION</li>
<li>https://www.postgresql.jp/docs/16/sql-createtable.html#SQL-CREATETABLE-EXCLUDE</li>
</ul>
<p>典型的なユースケースは会議室など限られたリソースの予約システムでしょう。</p>
<p>以下は会議室予約の、特定の部屋が同一時間帯に貸し出されないように、排他制約を付与した例です。 <code>EXCLUDE USING gist (...)</code> の部分が対象です。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-ua7ayt-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-ua7ayt-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- btree_gistを利用するために、拡張を有効にする</span></span><br><span class="line"><span class="comment">-- https://www.postgresql.jp/docs/16/btree-gist.html</span></span><br><span class="line"><span class="keyword">CREATE</span> EXTENSION IF <span class="keyword">NOT</span> <span class="keyword">EXISTS</span> btree_gist;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 同一時間帯に、同一部屋を貸し出されないようにする例</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> reservations (</span><br><span class="line">    id BIGSERIAL <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">    room_id <span class="type">INT</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    start_time <span class="type">TIMESTAMP</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    end_time <span class="type">TIMESTAMP</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    EXCLUDE <span class="keyword">USING</span> GIST (</span><br><span class="line">        room_id <span class="keyword">WITH</span> <span class="operator">=</span>,</span><br><span class="line">        tsrange(start_time, end_time) <span class="keyword">WITH</span> <span class="operator">&amp;&amp;</span></span><br><span class="line">    )</span><br><span class="line">);</span><br></pre></td></tr></table></figure></div>

<p><code>gist</code> はインデックス種別のことで、地理空間データや範囲型に対して効率が良いとされています。B-treeもEXCLUDE内で指定できるそうですが、一意制約以上に高速で動かないため意味がないとドキュメントにあります。</p>
<p>tsrangeは9.2から追加された範囲型です。重なり検出する演算子 <code>&amp;&amp;</code> （&#x3D;重なりがあることを示す）などと一緒に使います。</p>
<p><code>room_id WITH =</code> で同じ部屋IDが等しいという条件と合わせて、特定の部屋が同一時間帯に存在しないことを制約として示しています。</p>
<p>他にも、PostGISを用いた地理系の処理で、ジオフェンシングのように特定の領域が重複しないような制約も排他制約で実現できます。このように時間（範囲）や空間の重複を弾くために存在するのが排他制約です。</p>
<h2 id="国内でも使用実績がある？">国内でも使用実績がある？</h2><p>わたしは排他制約自体を、リリースノートを読んでいて初めて存在を知ったのですが、2017時点でそーだいさんなど、多くの方々が便利さを伝えているので、おそらく実績も多数かなと思います。</p>
<ul>
<li>PostgreSQLで排他制約がめっちゃ便利！！</li>
<li>Re: PostgreSQLで排他制約がめっちゃ便利！！</li>
</ul>
<p>そーだいさんの記事だと、<code>tsrange</code> で直接カラム定義しており、こちらを利用するほうが一般的には良いでしょう。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> schedule</span><br><span class="line">(</span><br><span class="line">    schedule_id SERIAL <span class="keyword">PRIMARY KEY</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    room_name TEXT <span class="keyword">NOT NULL</span>,</span><br><span class="line">    reservation_time tsrange <span class="keyword">NOT NULL</span>,</span><br><span class="line">    EXCLUDE <span class="keyword">USING</span> GIST (reservation_time <span class="keyword">WITH</span> <span class="operator">&amp;&amp;</span>)</span><br><span class="line">);</span><br></pre></td></tr></table></figure>

<h2 id="16以前のバージョンでは、パーティションテーブルの親側に定義することはできなかった">16以前のバージョンでは、パーティションテーブルの親側に定義することはできなかった</h2><p>16より前のバージョンは、以下のように <code>PARTITON BY</code> と <code>EXCLUDE USING</code> を同時に宣言できませんでした。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-ua7ayt-2" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-ua7ayt-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">postgres<span class="operator">=</span># <span class="keyword">select</span> version();</span><br><span class="line">                                                       version</span><br><span class="line"><span class="comment">---------------------------------------------------------------------------------------------------------------------</span></span><br><span class="line"> PostgreSQL <span class="number">16.4</span> (Debian <span class="number">16.4</span><span class="number">-1.</span>pgdg120<span class="operator">+</span><span class="number">2</span>) <span class="keyword">on</span> x86_64<span class="operator">-</span>pc<span class="operator">-</span>linux<span class="operator">-</span>gnu, compiled <span class="keyword">by</span> gcc (Debian <span class="number">12.2</span><span class="number">.0</span><span class="number">-14</span>) <span class="number">12.2</span><span class="number">.0</span>, <span class="number">64</span><span class="operator">-</span>bit</span><br><span class="line">(<span class="number">1</span> <span class="type">row</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- パーティションテーブルで排他制約を宣言</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">CREATE TABLE</span> reservations (</span><br><span class="line">    id BIGSERIAL <span class="keyword">NOT NULL</span>,</span><br><span class="line">    room_id <span class="type">INT</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    reservation_date <span class="type">date</span>,</span><br><span class="line">    start_time <span class="type">TIMESTAMP</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    end_time <span class="type">TIMESTAMP</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    <span class="keyword">CONSTRAINT</span> reservations_pkey <span class="keyword">PRIMARY KEY</span> (reservation_date, id),</span><br><span class="line">    EXCLUDE <span class="keyword">USING</span> GIST (</span><br><span class="line">        reservation_date <span class="keyword">WITH</span> <span class="operator">=</span>,</span><br><span class="line">        room_id <span class="keyword">WITH</span> <span class="operator">=</span>,</span><br><span class="line">        tsrange(start_time, end_time) <span class="keyword">WITH</span> <span class="operator">&amp;&amp;</span></span><br><span class="line">    )</span><br><span class="line">) <span class="keyword">PARTITION</span> <span class="keyword">BY</span> <span class="keyword">RANGE</span> (reservation_date);</span><br><span class="line">ERROR:  exclusion constraints <span class="keyword">are</span> <span class="keyword">not</span> supported <span class="keyword">on</span> partitioned tables</span><br><span class="line">LINE <span class="number">8</span>:     EXCLUDE <span class="keyword">USING</span> GIST (</span><br></pre></td></tr></table></figure></div>

<p><code>exclusion constraints are not supported on partitioned tables</code> とあるのがエラー部分です。</p>
<h2 id="16以前の回避方法">16以前の回避方法</h2><p>16以前のバージョンでは、回避策として子テーブルそれぞれに排他制約を追加していく必要がありました。</p>
<p>以下が <code>reservations_20241101</code> などのパーティションを作成して、それに対して排他制約を個別に定義する例です。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-ua7ayt-3" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-ua7ayt-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> reservations (</span><br><span class="line">    id BIGSERIAL <span class="keyword">NOT NULL</span>,</span><br><span class="line">    room_id <span class="type">INT</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    reservation_date <span class="type">date</span>,</span><br><span class="line">    start_time <span class="type">TIMESTAMP</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    end_time <span class="type">TIMESTAMP</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    <span class="keyword">CONSTRAINT</span> reservations_pkey <span class="keyword">PRIMARY KEY</span> (reservation_date, id)</span><br><span class="line">) <span class="keyword">PARTITION</span> <span class="keyword">BY</span> <span class="keyword">RANGE</span> (reservation_date);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- パーティションを作成</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> reservations_20241101 <span class="keyword">PARTITION</span> <span class="keyword">OF</span> reservations</span><br><span class="line"><span class="keyword">FOR</span> <span class="keyword">VALUES</span> <span class="keyword">FROM</span> (<span class="string">&#x27;2024-11-01&#x27;</span>) <span class="keyword">TO</span> (<span class="string">&#x27;2024-11-02&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE TABLE</span> reservations_20241102 <span class="keyword">PARTITION</span> <span class="keyword">OF</span> reservations</span><br><span class="line"><span class="keyword">FOR</span> <span class="keyword">VALUES</span> <span class="keyword">FROM</span> (<span class="string">&#x27;2024-11-02&#x27;</span>) <span class="keyword">TO</span> (<span class="string">&#x27;2024-11-03&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 排他制約をそれぞれの子パーティションテーブルに設定</span></span><br><span class="line"><span class="keyword">ALTER TABLE</span> reservations_20241101</span><br><span class="line"><span class="keyword">ADD CONSTRAINT</span> reservations_20241101_exclude EXCLUDE <span class="keyword">USING</span> GIST (</span><br><span class="line">    room_id <span class="keyword">WITH</span> <span class="operator">=</span>,</span><br><span class="line">    tsrange(start_time, end_time) <span class="keyword">WITH</span> <span class="operator">&amp;&amp;</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">ALTER TABLE</span> reservations_20241102</span><br><span class="line"><span class="keyword">ADD CONSTRAINT</span> reservations_20241102_exclude EXCLUDE <span class="keyword">USING</span> GIST (</span><br><span class="line">    room_id <span class="keyword">WITH</span> <span class="operator">=</span>,</span><br><span class="line">    tsrange(start_time, end_time) <span class="keyword">WITH</span> <span class="operator">&amp;&amp;</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure></div>

<p>テーブルの状態は以下です。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-ua7ayt-4" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-ua7ayt-4" title="コードの折り返しを切り替える"></label><figcaption><span>\d+結果</span></figcaption><table><tr><td class="code"><pre><span class="line">postgres=# \d+ reservations</span><br><span class="line">                                                                Partitioned table <span class="string">&quot;public.reservations&quot;</span></span><br><span class="line">      Column      |            Type             | Collation | Nullable |                 Default                  | Storage | Compression | Stats target | Description</span><br><span class="line">------------------+-----------------------------+-----------+----------+------------------------------------------+---------+-------------+--------------+-------------</span><br><span class="line"> <span class="built_in">id</span>               | bigint                      |           | not null | nextval(<span class="string">&#x27;reservations_id_seq&#x27;</span>::regclass) | plain   |             |              |</span><br><span class="line"> room_id          | <span class="built_in">integer</span>                     |           | not null |                                          | plain   |             |              |</span><br><span class="line"> reservation_date | <span class="built_in">date</span>                        |           | not null |                                          | plain   |             |              |</span><br><span class="line"> start_time       | timestamp without <span class="keyword">time</span> zone |           | not null |                                          | plain   |             |              |</span><br><span class="line"> end_time         | timestamp without <span class="keyword">time</span> zone |           | not null |                                          | plain   |             |              |</span><br><span class="line">Partition key: RANGE (reservation_date)</span><br><span class="line">Indexes:</span><br><span class="line">    <span class="string">&quot;reservations_pkey&quot;</span> PRIMARY KEY, btree (reservation_date, <span class="built_in">id</span>)</span><br><span class="line">Partitions: reservations_20241101 FOR VALUES FROM (<span class="string">&#x27;2024-11-01&#x27;</span>) TO (<span class="string">&#x27;2024-11-02&#x27;</span>),</span><br><span class="line">            reservations_20241102 FOR VALUES FROM (<span class="string">&#x27;2024-11-02&#x27;</span>) TO (<span class="string">&#x27;2024-11-03&#x27;</span>)</span><br><span class="line">````</span><br><span class="line"></span><br><span class="line">実際にデータを登録してみます。</span><br><span class="line"></span><br><span class="line">```sql psqlでの実行例</span><br><span class="line">-- 正常に挿入されるデータ</span><br><span class="line">postgres=# INSERT INTO reservations (room_id, reservation_date, start_time, end_time)</span><br><span class="line">  VALUES (1, <span class="string">&#x27;2024-11-01&#x27;</span>, <span class="string">&#x27;2024-11-01 10:00:00&#x27;</span>, <span class="string">&#x27;2024-11-01 11:00:00&#x27;</span>);</span><br><span class="line">INSERT 0 1</span><br><span class="line">postgres=# INSERT INTO reservations (room_id, reservation_date, start_time, end_time)</span><br><span class="line">  VALUES (1, <span class="string">&#x27;2024-11-02&#x27;</span>, <span class="string">&#x27;2024-11-02 11:00:00&#x27;</span>, <span class="string">&#x27;2024-11-02 12:00:00&#x27;</span>);</span><br><span class="line">INSERT 0 1</span><br><span class="line"></span><br><span class="line">-- 時間帯が重複しているため、エラーが発生するデータ</span><br><span class="line">postgres=# INSERT INTO reservations (room_id, reservation_date, start_time, end_time)</span><br><span class="line">  VALUES (1, <span class="string">&#x27;2024-11-01&#x27;</span>, <span class="string">&#x27;2024-11-01 10:30:00&#x27;</span>, <span class="string">&#x27;2024-11-01 11:30:00&#x27;</span>);</span><br><span class="line">ERROR:  conflicting key value violates exclusion constraint <span class="string">&quot;reservations_20241101_exclude&quot;</span></span><br><span class="line">DETAIL:  Key (room_id, tsrange(start_time, end_time))=(1, [<span class="string">&quot;2024-11-01 10:30:00&quot;</span>,<span class="string">&quot;2024-11-01 11:30:00&quot;</span>)) conflicts with existing key (room_id, tsrange(start_time, end_time))=(1, [<span class="string">&quot;2024-11-01 10:00:00&quot;</span>,<span class="string">&quot;2024-11-01 11:00:00&quot;</span>))</span><br><span class="line"></span><br><span class="line">-- テーブル状態を確認</span><br><span class="line">postgres=# <span class="keyword">select</span> * from reservations;</span><br><span class="line"> <span class="built_in">id</span> | room_id | reservation_date |     start_time      |      end_time</span><br><span class="line">----+---------+------------------+---------------------+---------------------</span><br><span class="line">  1 |       1 | 2024-11-01       | 2024-11-01 10:00:00 | 2024-11-01 11:00:00</span><br><span class="line">  2 |       1 | 2024-11-02       | 2024-11-02 11:00:00 | 2024-11-02 12:00:00</span><br><span class="line">(2 rows)</span><br></pre></td></tr></table></figure></div>

<p>最後のINSERTだけが失敗して、整合性が保たれていることがわかります。</p>
<h2 id="PostgreSQL17からは、親テーブル側に宣言できるようになった">PostgreSQL17からは、親テーブル側に宣言できるようになった</h2><p>次のように、<code>CREATE TABLE</code> に <code>EXCLUDE USING</code> と<code>PARTITON BY</code> のどちらも指定できるようになりました。パーティションテーブル側それぞれに排他制約を指定しなくて済むので、より直感的になりました。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-ua7ayt-5" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-ua7ayt-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> reservations (</span><br><span class="line">    id BIGSERIAL <span class="keyword">NOT NULL</span>,</span><br><span class="line">    room_id <span class="type">INT</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    reservation_date <span class="type">date</span>,</span><br><span class="line">    start_time <span class="type">TIMESTAMP</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    end_time <span class="type">TIMESTAMP</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    <span class="keyword">CONSTRAINT</span> reservations_pkey <span class="keyword">PRIMARY KEY</span> (reservation_date, id),</span><br><span class="line">    EXCLUDE <span class="keyword">USING</span> GIST (</span><br><span class="line">        reservation_date <span class="keyword">WITH</span> <span class="operator">=</span>,</span><br><span class="line">        room_id <span class="keyword">WITH</span> <span class="operator">=</span>,</span><br><span class="line">        tsrange(start_time, end_time) <span class="keyword">WITH</span> <span class="operator">&amp;&amp;</span></span><br><span class="line">    )</span><br><span class="line">) <span class="keyword">PARTITION</span> <span class="keyword">BY</span> <span class="keyword">RANGE</span> (reservation_date);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- パーティション作成</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> reservations_20241101 <span class="keyword">PARTITION</span> <span class="keyword">OF</span> reservations</span><br><span class="line"><span class="keyword">FOR</span> <span class="keyword">VALUES</span> <span class="keyword">FROM</span> (<span class="string">&#x27;2024-11-01&#x27;</span>) <span class="keyword">TO</span> (<span class="string">&#x27;2024-11-02&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE TABLE</span> reservations_20241102 <span class="keyword">PARTITION</span> <span class="keyword">OF</span> reservations</span><br><span class="line"><span class="keyword">FOR</span> <span class="keyword">VALUES</span> <span class="keyword">FROM</span> (<span class="string">&#x27;2024-11-02&#x27;</span>) <span class="keyword">TO</span> (<span class="string">&#x27;2024-11-03&#x27;</span>);</span><br></pre></td></tr></table></figure></div>

<p>制約としては、必ずパーティションキー（今回だと <code>reservation_date</code>） をイコール条件で履いた制約の追加する必要があります。</p>
<p>テーブルの状態は以下です。親テーブル側にも排他制約の情報が追加されていますね。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-ua7ayt-6" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-ua7ayt-6" title="コードの折り返しを切り替える"></label><figcaption><span>\d+結果</span></figcaption><table><tr><td class="code"><pre><span class="line">postgres=# \d+ reservations</span><br><span class="line">                                                                Partitioned table <span class="string">&quot;public.reservations&quot;</span></span><br><span class="line">      Column      |            Type             | Collation | Nullable |                 Default                  | Storage | Compression | Stats target | Description</span><br><span class="line">------------------+-----------------------------+-----------+----------+------------------------------------------+---------+-------------+--------------+-------------</span><br><span class="line"> <span class="built_in">id</span>               | bigint                      |           | not null | nextval(<span class="string">&#x27;reservations_id_seq&#x27;</span>::regclass) | plain   |             |              |</span><br><span class="line"> room_id          | <span class="built_in">integer</span>                     |           | not null |                                          | plain   |             |              |</span><br><span class="line"> reservation_date | <span class="built_in">date</span>                        |           | not null |                                          | plain   |             |              |</span><br><span class="line"> start_time       | timestamp without <span class="keyword">time</span> zone |           | not null |                                          | plain   |             |              |</span><br><span class="line"> end_time         | timestamp without <span class="keyword">time</span> zone |           | not null |                                          | plain   |             |              |</span><br><span class="line">Partition key: RANGE (reservation_date)</span><br><span class="line">Indexes:</span><br><span class="line">    <span class="string">&quot;reservations_pkey&quot;</span> PRIMARY KEY, btree (reservation_date, <span class="built_in">id</span>)</span><br><span class="line">    <span class="string">&quot;reservations_reservation_date_room_id_tsrange_excl&quot;</span> EXCLUDE USING gist (reservation_date WITH =, room_id WITH =, tsrange(start_time, end_time) WITH &amp;&amp;)</span><br><span class="line">Partitions: reservations_20241101 FOR VALUES FROM (<span class="string">&#x27;2024-11-01&#x27;</span>) TO (<span class="string">&#x27;2024-11-02&#x27;</span>),</span><br><span class="line">            reservations_20241102 FOR VALUES FROM (<span class="string">&#x27;2024-11-02&#x27;</span>) TO (<span class="string">&#x27;2024-11-03&#x27;</span>)</span><br></pre></td></tr></table></figure></div>

<p>さきほどと同様に、実際にデータを登録してみます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-ua7ayt-7" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-ua7ayt-7" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- 正常に挿入されるデータ</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">INSERT INTO</span> reservations (room_id, reservation_date, start_time, end_time)</span><br><span class="line">  <span class="keyword">VALUES</span> (<span class="number">1</span>, <span class="string">&#x27;2024-11-01&#x27;</span>, <span class="string">&#x27;2024-11-01 10:00:00&#x27;</span>, <span class="string">&#x27;2024-11-01 11:00:00&#x27;</span>);</span><br><span class="line"><span class="keyword">INSERT</span> <span class="number">0</span> <span class="number">1</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">INSERT INTO</span> reservations (room_id, reservation_date, start_time, end_time)</span><br><span class="line">  <span class="keyword">VALUES</span> (<span class="number">1</span>, <span class="string">&#x27;2024-11-02&#x27;</span>, <span class="string">&#x27;2024-11-02 11:00:00&#x27;</span>, <span class="string">&#x27;2024-11-02 12:00:00&#x27;</span>);</span><br><span class="line"><span class="keyword">INSERT</span> <span class="number">0</span> <span class="number">1</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 時間帯が重複しているため、エラーが発生するデータ</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">INSERT INTO</span> reservations (room_id, reservation_date, start_time, end_time)</span><br><span class="line">  <span class="keyword">VALUES</span> (<span class="number">1</span>, <span class="string">&#x27;2024-11-01&#x27;</span>, <span class="string">&#x27;2024-11-01 10:30:00&#x27;</span>, <span class="string">&#x27;2024-11-01 11:30:00&#x27;</span>);</span><br><span class="line">ERROR:  conflicting key <span class="keyword">value</span> violates exclusion <span class="keyword">constraint</span> &quot;reservations_20241101_reservation_date_room_id_tsrange_excl&quot;</span><br><span class="line">DETAIL:  Key (reservation_date, room_id, tsrange(start_time, end_time))<span class="operator">=</span>(<span class="number">2024</span><span class="number">-11</span><span class="number">-01</span>, <span class="number">1</span>, [&quot;2024-11-01 10:30:00&quot;,&quot;2024-11-01 11:30:00&quot;)) conflicts <span class="keyword">with</span> existing key (reservation_date, room_id, tsrange(start_time, end_time))<span class="operator">=</span>(<span class="number">2024</span><span class="number">-11</span><span class="number">-01</span>, <span class="number">1</span>, [&quot;2024-11-01 10:00:00&quot;,&quot;2024-11-01 11:00:00&quot;)).</span><br><span class="line"></span><br><span class="line"><span class="comment">-- テーブル状態を確認</span></span><br><span class="line">postgres<span class="operator">=</span># <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> reservations;</span><br><span class="line"> id <span class="operator">|</span> room_id <span class="operator">|</span> reservation_date <span class="operator">|</span>     start_time      <span class="operator">|</span>      end_time</span><br><span class="line"><span class="comment">----+---------+------------------+---------------------+---------------------</span></span><br><span class="line">  <span class="number">1</span> <span class="operator">|</span>       <span class="number">1</span> <span class="operator">|</span> <span class="number">2024</span><span class="number">-11</span><span class="number">-01</span>       <span class="operator">|</span> <span class="number">2024</span><span class="number">-11</span><span class="number">-01</span> <span class="number">10</span>:<span class="number">00</span>:<span class="number">00</span> <span class="operator">|</span> <span class="number">2024</span><span class="number">-11</span><span class="number">-01</span> <span class="number">11</span>:<span class="number">00</span>:<span class="number">00</span></span><br><span class="line">  <span class="number">2</span> <span class="operator">|</span>       <span class="number">1</span> <span class="operator">|</span> <span class="number">2024</span><span class="number">-11</span><span class="number">-02</span>       <span class="operator">|</span> <span class="number">2024</span><span class="number">-11</span><span class="number">-02</span> <span class="number">11</span>:<span class="number">00</span>:<span class="number">00</span> <span class="operator">|</span> <span class="number">2024</span><span class="number">-11</span><span class="number">-02</span> <span class="number">12</span>:<span class="number">00</span>:<span class="number">00</span></span><br><span class="line">(<span class="number">2</span> <span class="keyword">rows</span>)</span><br></pre></td></tr></table></figure></div>

<p>動作も16時点と同様、最後のINSERTだけが失敗して、整合性が保たれていることがわかります。</p>
<p>めちゃくちゃ便利！</p>
<h2 id="まとめ">まとめ</h2><p>PostgreSQLの排他制約を試しました。17のアップデートで、パーティションテーブルでより排他制約を利用しやすくなりました。</p>
]]></content>
    <summary type="html">パーティションテーブルに対して宣言的に排他制約を設定できるようになったアップデートについて取り上げます。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="PostgreSQL" scheme="https://future-architect.github.io/tags/PostgreSQL/"/>
    <category term="PostgreSQL17" scheme="https://future-architect.github.io/tags/PostgreSQL17/"/>
    <category term="パーティション" scheme="https://future-architect.github.io/tags/%E3%83%91%E3%83%BC%E3%83%86%E3%82%A3%E3%82%B7%E3%83%A7%E3%83%B3/"/>
  </entry>
  <entry>
    <title>PostgreSQL17リリース：to_regtypemod関数と型修飾子について</title>
    <link href="https://future-architect.github.io/articles/20241023b/"/>
    <id>https://future-architect.github.io/articles/20241023b/</id>
    <published>2024-10-22T15:00:01.000Z</published>
    <updated>2024-10-22T15:00:01.000Z</updated>
    <author><name>山本竜玄</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>PostgreSQL17リリース記念連載の1日目です。</p>
<p>Healthcare Innovation Group(HIG)所属の山本です。</p>
<p>先月の2024年9月、PostgreSQL 17がリリースされました。今回のリリースではバーフォーマンスの改善や、JSON周りなど大奥のアップデートが追加されていますが、その中でもデータ型文字列からデータ型の型修飾子(typemod)を取得する関数<code>to_regtypemod</code>が新規に追加されました。</p>
<p>本記事では、<code>to_regtypemod</code>の使用方法や従来の手法との比較を通じて、活用方法やその有用性、背景となる型修飾子自体について記載します。</p>
<h2 id="0-サンプルデータ">0. サンプルデータ</h2><p>本記事を下記にあたっての検証では、具体例などをイメージしやすくするために、以下のようにサンプルデータを作成しています。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="comment">-- データベースの作成</span></span><br><span class="line"><span class="keyword">CREATE</span> DATABASE healthcare_data;</span><br></pre></td></tr></table></figure>

<figure class="highlight console"><table><tr><td class="code"><pre><span class="line">-- 作成したデータベースに接続</span><br><span class="line">\c healthcare_data;</span><br></pre></td></tr></table></figure>

<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1dcklj5-1" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- カスタム型の定義</span></span><br><span class="line"><span class="keyword">CREATE</span> TYPE address_type <span class="keyword">AS</span> (</span><br><span class="line">    street <span class="type">VARCHAR</span>(<span class="number">100</span>),</span><br><span class="line">    city <span class="type">VARCHAR</span>(<span class="number">50</span>),</span><br><span class="line">    postal_code <span class="type">VARCHAR</span>(<span class="number">10</span>)</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- テーブルの作成</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> patient_records (</span><br><span class="line">    patient_id <span class="type">VARCHAR</span>(<span class="number">50</span>),</span><br><span class="line">    blood_sugar <span class="type">NUMERIC</span>(<span class="number">5</span>, <span class="number">2</span>),</span><br><span class="line">    test_results <span class="type">NUMERIC</span>(<span class="number">5</span>, <span class="number">2</span>)[],</span><br><span class="line">    address address_type</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- サンプルデータの挿入</span></span><br><span class="line"><span class="keyword">INSERT INTO</span> patient_records (patient_id, blood_sugar, test_results, address)</span><br><span class="line"><span class="keyword">VALUES</span> </span><br><span class="line">(<span class="string">&#x27;P001&#x27;</span>, <span class="number">98.76</span>, <span class="keyword">ARRAY</span>[<span class="number">95.5</span>, <span class="number">100.2</span>], (<span class="string">&#x27;123 Main St&#x27;</span>, <span class="string">&#x27;Tokyo&#x27;</span>, <span class="string">&#x27;100-0001&#x27;</span>)),</span><br><span class="line">(<span class="string">&#x27;P002&#x27;</span>, <span class="number">110.45</span>, <span class="keyword">ARRAY</span>[<span class="number">105.1</span>, <span class="number">108.9</span>], (<span class="string">&#x27;456 Oak St&#x27;</span>, <span class="string">&#x27;Osaka&#x27;</span>, <span class="string">&#x27;530-0001&#x27;</span>)),</span><br><span class="line">(<span class="string">&#x27;P003&#x27;</span>, <span class="number">92.35</span>, <span class="keyword">ARRAY</span>[<span class="number">90.5</span>, <span class="number">93.2</span>], (<span class="string">&#x27;789 Pine St&#x27;</span>, <span class="string">&#x27;Nagoya&#x27;</span>, <span class="string">&#x27;450-0001&#x27;</span>));</span><br></pre></td></tr></table></figure></div>

<p>作成後としては、以下のようになります。配列型、自作型などがあることがポイントです。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1dcklj5-2" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">healthcare_data=# select * from patient_records ;</span><br><span class="line"> patient_id | blood_sugar |  test_results   |             address</span><br><span class="line">------------+-------------+-----------------+---------------------------------</span><br><span class="line"> P001       |       98.76 | &#123;95.50,100.20&#125;  | (&quot;123 Main St&quot;,Tokyo,100-0001)</span><br><span class="line"> P002       |      110.45 | &#123;105.10,108.90&#125; | (&quot;456 Oak St&quot;,Osaka,530-0001)</span><br><span class="line"> P003       |       92.35 | &#123;90.50,93.20&#125;   | (&quot;789 Pine St&quot;,Nagoya,450-0001)</span><br><span class="line">(3 rows)</span><br></pre></td></tr></table></figure></div>

<h2 id="1-はじめに">1.はじめに</h2><p>この章では、サンプルデータで具体例を示しつつ、<code>to_regtypemod</code>それ自体や背景情報を説明します。</p>
<h3 id="1-1-to-regtypemodとは？">1.1 to_regtypemodとは？</h3><p>https://pgpedia.info/t/to_regtypemod.html</p>
<p>(※執筆時点ではPostgreSQLの公式ドキュメントにないので、pgpediaを引用させていただいています)</p>
<p><code>to_regtypemod</code>は、PostgreSQL 17で新たに追加されたシステム関数で、文字列で指定されたデータ型から型修飾子（typemod）を取得できます。</p>
<p>例えば、VARCHAR(32)やNUMERIC(5, 2)のようなデータ型には、文字列の長さ(32の部分)や数値の精度(5の部分)・スケール(2の部分)が指定されています。to_regtypemodは、これらの修飾子を内部表現の形式で返す関数です。</p>
<h3 id="1-2-基本的な使用方法">1.2 基本的な使用方法</h3><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">to_regtypemod(text) → integer</span><br></pre></td></tr></table></figure>

<p>戻り値は以下の挙動となります。</p>
<ol>
<li>データ型に型修飾子がある場合、その<strong>内部表現</strong>が返される</li>
<li>修飾子がない場合は-1返される</li>
<li>存在しないデータ型が指定されたが、文法的に有効な場合はNULLを返す</li>
<li>文法エラーがある場合はERRORを発生させる</li>
</ol>
<h3 id="1-3-サンプルデータによるto-regtypemodの検証">1.3 サンプルデータによるto_regtypemodの検証</h3><p>イメージしやすくするため、それぞれ実際に確認してみます。特に、「内部表現が返される」ということが引っかかりポイントな気がします。</p>
<h4 id="VARCHARの例">VARCHARの例</h4><div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1dcklj5-3" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">healthcare_data=# SELECT to_regtypemod(&#x27;varchar(50)&#x27;);</span><br><span class="line"> to_regtypemod</span><br><span class="line">---------------</span><br><span class="line">            54</span><br><span class="line">(1 row)</span><br></pre></td></tr></table></figure></div>

<p>このように内部表現の値で返されます。<br>この値は、<code>to_regtype</code>関数と<code>format_type</code>関数と併用することで、見てわかる直感的な表現となります。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1dcklj5-4" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">healthcare_data=# WITH datatype AS (</span><br><span class="line">  SELECT</span><br><span class="line">    &#x27;varchar(50)&#x27; AS type</span><br><span class="line">)</span><br><span class="line">SELECT</span><br><span class="line">  to_regtype(d.type),</span><br><span class="line">  to_regtypemod(d.type),</span><br><span class="line">  format_type(</span><br><span class="line">    to_regtype(d.type),</span><br><span class="line">    to_regtypemod(d.type)</span><br><span class="line">  )</span><br><span class="line">FROM</span><br><span class="line">  datatype d;</span><br><span class="line">    to_regtype     | to_regtypemod |      format_type</span><br><span class="line">-------------------+---------------+-----------------------</span><br><span class="line"> character varying |            54 | character varying(50)</span><br><span class="line">(1 row)</span><br></pre></td></tr></table></figure></div>

<h4 id="NUMERIC型でのスケール指定">NUMERIC型でのスケール指定</h4><p>numeric型で、scale(小数点の右側の小数部分の桁数)が指定されている場合には以下のようになります。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1dcklj5-5" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">healthcare_data=# SELECT to_regtypemod(&#x27;numeric(5,2)&#x27;);</span><br><span class="line"> to_regtypemod</span><br><span class="line">---------------</span><br><span class="line">        327686</span><br><span class="line">(1 row)</span><br><span class="line"></span><br></pre></td></tr></table></figure></div>

<figure class="highlight console"><table><tr><td class="code"><pre><span class="line">healthcare_data=# WITH datatype AS (</span><br><span class="line">  SELECT &#x27;numeric(5, 2)&#x27; AS type</span><br><span class="line">)</span><br><span class="line">SELECT format_type(</span><br><span class="line">         to_regtype(d.type),</span><br><span class="line">         to_regtypemod(d.type)</span><br><span class="line">       )</span><br><span class="line">  FROM datatype d;</span><br><span class="line"> format_type</span><br><span class="line">--------------</span><br><span class="line"> numeric(5,2)</span><br><span class="line">(1 row)</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h4 id="配列型の場合">配列型の場合</h4><p>配列の場合にも、以下のように取得できています。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line">healthcare_data=# WITH datatype AS (</span><br><span class="line">  SELECT &#x27;numeric(5, 2)[]&#x27; AS type</span><br><span class="line">)</span><br><span class="line">SELECT format_type(</span><br><span class="line">         to_regtype(d.type),</span><br><span class="line">         to_regtypemod(d.type)</span><br><span class="line">       )</span><br><span class="line">  FROM datatype d;</span><br><span class="line"> format_type</span><br><span class="line">--------------</span><br><span class="line"> numeric(5,2)[]</span><br><span class="line">(1 row)</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h4 id="修飾子が存在しない場合の例">修飾子が存在しない場合の例</h4><p>次に、値が不正やない場合の挙動を確認しましょう。</p>
<p>修飾子が存在しない場合、以下のように<code>-1</code>が返されていますね。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1dcklj5-6" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-6" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">healthcare_data=# SELECT to_regtypemod(&#x27;numeric&#x27;);</span><br><span class="line"> to_regtypemod</span><br><span class="line">---------------</span><br><span class="line">            -1</span><br><span class="line">(1 row)</span><br><span class="line"></span><br></pre></td></tr></table></figure></div>

<h4 id="カスタム型の場合">カスタム型の場合</h4><p>サンプルで定義した、カスタムの型を指定するとどうなるでしょうか？</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="comment">-- 患者の住所情報を管理するためのカスタム型を定義</span></span><br><span class="line"><span class="keyword">CREATE</span> TYPE address_type <span class="keyword">AS</span> (</span><br><span class="line">    street <span class="type">VARCHAR</span>(<span class="number">100</span>),</span><br><span class="line">    city <span class="type">VARCHAR</span>(<span class="number">50</span>),</span><br><span class="line">    postal_code <span class="type">VARCHAR</span>(<span class="number">10</span>)</span><br><span class="line">);</span><br></pre></td></tr></table></figure>

<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1dcklj5-7" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-7" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">healthcare_data=# SELECT to_regtypemod(&#x27;address_type&#x27;);</span><br><span class="line"> to_regtypemod</span><br><span class="line">---------------</span><br><span class="line">            -1</span><br><span class="line">(1 row)</span><br><span class="line"></span><br></pre></td></tr></table></figure></div>

<p><code>-1</code>で返されてしまいました。カスタム型の定義の際に、修飾子を指定していないからですね。</p>
<p>PostgreSQL 17時点の型宣言では、カスタム型の中でも複合型(上記のように、複数フィールドを指定するもの)に型修飾子を付与する構文は存在しないので、同様のことをしたい場合にはトリガーを作成することになるのではないでしょうか？</p>
<h4 id="存在しない型の場合">存在しない型の場合</h4><p>存在しない型だが文法的に有効な場合には、以下のようにNULLが戻ります。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1dcklj5-8" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-8" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">healthcare_data=# SELECT to_regtypemod(&#x27;tekitounakata(200)&#x27;);</span><br><span class="line"> to_regtypemod</span><br><span class="line">---------------</span><br><span class="line"></span><br><span class="line">(1 row)</span><br><span class="line"></span><br></pre></td></tr></table></figure></div>

<p>文法エラーでは、以下のようにエラーが発生します。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1dcklj5-9" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-9" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">healthcare_data=# SELECT to_regtypemod(&#x27;!&#x27;);</span><br><span class="line">ERROR:  syntax error at or near &quot;!&quot;</span><br><span class="line">LINE 1: SELECT to_regtypemod(&#x27;!&#x27;);</span><br><span class="line">        ^</span><br><span class="line">CONTEXT:  invalid type name &quot;!&quot;</span><br><span class="line"></span><br></pre></td></tr></table></figure></div>

<h2 id="2-型修飾子-typemod）とは？">2. 型修飾子(typemod）とは？</h2><p>ここまでで、<br><code>to_regtypemod</code>を用いて、文字列で指定されたデータ型から型修飾子（typemod）を取得する簡単な事例を紹介してきました。</p>
<p>そもそも、型修飾子（typemod）とはなんなのか、もう少し詳細に見ていきます。</p>
<h3 id="2-1-型修飾子の概要">2.1 型修飾子の概要</h3><p>型修飾子（typemod）は、PostgreSQLのデータ型に追加の制約を付与するための仕組みです。</p>
<p>データ型そのものだけでは、例えばNUMERICを指定しても最大長までは許容されてしまうので、実際の用途に合わせたデータのサイズや精度、その他の具体的な制約を表現できません。型修飾子は、これらの制約を定義するために用いられます。</p>
<p>実際にNUMERIC型を例に挙動を確認していきます。</p>
<p>https://www.postgresql.jp/document/16/html/datatype-numeric.html</p>
<p>NUMERIC型では、型修飾子としてprecisionとscaleを指定可能で、省略もできます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1dcklj5-10" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-10" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">-- サンプルテーブルの作成</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> sample_patient_info (</span><br><span class="line">    patient_id SERIAL <span class="keyword">PRIMARY KEY</span>,         </span><br><span class="line">    age <span class="type">NUMERIC</span>,                               <span class="comment">-- 型修飾子を指定しない整数型</span></span><br><span class="line">    heart_rate <span class="type">NUMERIC</span>(<span class="number">3</span>),                 <span class="comment">-- 精度を指定した整数型（最大3桁）</span></span><br><span class="line">    blood_pressure <span class="type">NUMERIC</span>(<span class="number">5</span>, <span class="number">2</span>)           <span class="comment">-- 位取りを指定した整数型（最大5桁、小数点以下2桁）</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure></div>

<p>ageには型修飾子を指定していないので、以下のように大きな数字を入れることができます。46億年なので、だいたい地球の誕生と同じくらいの年齢ですね。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1dcklj5-11" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-11" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">healthcare_data=# INSERT INTO sample_patient_info (age, heart_rate, blood_pressure)</span><br><span class="line">VALUES</span><br><span class="line">(4600000000, 120, 120.75);</span><br><span class="line">INSERT 0 1</span><br></pre></td></tr></table></figure></div>

<p>heart_rateに、桁数以上の数字を指定してみると、以下のようにエラーとなります。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1dcklj5-12" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-12" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">healthcare_data=# INSERT INTO sample_patient_info (age, heart_rate, blood_pressure)</span><br><span class="line">VALUES</span><br><span class="line">(120, 1200, 120.75);</span><br><span class="line">ERROR:  numeric field overflow</span><br><span class="line">DETAIL:  A field with precision 3, scale 0 must round to an absolute value less than 10^3.</span><br><span class="line"></span><br></pre></td></tr></table></figure></div>

<p>一方で、位取りの桁数以上を指定した場合には、エラーとならずに数字が丸められます。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1dcklj5-13" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-13" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">healthcare_data=# INSERT INTO sample_patient_info (age, heart_rate, blood_pressure)</span><br><span class="line">VALUES</span><br><span class="line">(120, 120, 120.759);</span><br><span class="line">INSERT 0 1</span><br><span class="line"></span><br><span class="line">healthcare_data=# select * from sample_patient_info;</span><br><span class="line"> patient_id |       age        | heart_rate | blood_pressure</span><br><span class="line">------------+------------------+------------+----------------</span><br><span class="line">          1 |              120 |        120 |         120.76</span><br><span class="line">(1 rows)</span><br><span class="line"></span><br></pre></td></tr></table></figure></div>

<p>小数点の精度が問われないようなシステムであれば問題ないですが、例えば検査値など勝手に丸められると大事故になりかねません。</p>
<p>一般的なフロントエンド、バックエンド、DBのようなアプリケーション構成であれば画面部分でチェックすることも重要ですが、DBのマイグレーションや移行などでもこれらの観点は重要です。</p>
<p>この一端として、typemodについても意識する必要があると思います。</p>
<h3 id="2-2-型修飾子の内部表現">2.2 型修飾子の内部表現</h3><p>せっかくのリリース連載なので、より内部的なデータや実装も眺めていきます。</p>
<p>前項までで登場している列ごとに定義した型修飾子の情報は、</p>
<p>PostgreSQLの<code>pg_attribute</code>テーブルの<code>atttypmod</code>カラム(int4, 32bit)などで管理されます。</p>
<p>このフィールドには、カラムごとの型修飾子の内部表現が整数として保存されています。このatttypmodを使って、カラムがどのような制約を持つかを確認できます。</p>
<p>サンプルデータのテーブルに確認すると、以下のようになっています。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1dcklj5-14" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-14" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">healthcare_data=# SELECT attname, atttypid, atttypmod FROM pg_attribute WHERE attrelid = &#x27;patient_records&#x27;::regclass;</span><br><span class="line">   attname    | atttypid | atttypmod</span><br><span class="line">--------------+----------+-----------</span><br><span class="line"> tableoid     |       26 |        -1</span><br><span class="line"> cmax         |       29 |        -1</span><br><span class="line"> xmax         |       28 |        -1</span><br><span class="line"> cmin         |       29 |        -1</span><br><span class="line"> xmin         |       28 |        -1</span><br><span class="line"> ctid         |       27 |        -1</span><br><span class="line"> patient_id   |     1043 |        54</span><br><span class="line"> blood_sugar  |     1700 |    327686</span><br><span class="line"> test_results |     1231 |    327686</span><br><span class="line"> address      |    16391 |        -1</span><br><span class="line">(10 rows)</span><br><span class="line"></span><br></pre></td></tr></table></figure></div>

<p>以下のようにしてto_regtypemodで取得した型修飾子と、同一の内部表現値ですね。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1dcklj5-15" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-15" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">healthcare_data=# SELECT to_regtypemod(&#x27;numeric(5,2)&#x27;);</span><br><span class="line"> to_regtypemod</span><br><span class="line">---------------</span><br><span class="line">        327686</span><br><span class="line">(1 row)</span><br></pre></td></tr></table></figure></div>

<p>ご覧の通り、typemodの表現には内部表現が用いられているので、よほどの職人でなければ目で見てどの型修飾子が付与されているのかの判定は難しいです。</p>
<p>この内部表現は、以下のようにformat_type関数を適用することで、値を内部表現から戻すことができます。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line">healthcare_data=# SELECT format_type(</span><br><span class="line">         1700,</span><br><span class="line">         327686</span><br><span class="line">       )</span><br><span class="line">;</span><br><span class="line"> format_type</span><br><span class="line">--------------</span><br><span class="line"> numeric(5,2)</span><br><span class="line">(1 row)</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>せっかくなので、format_type関数の内部実装までおって確認をしましょう。</p>
<p>format_typeについては、postgresリポジトリにおける以下のファイルで実装されています。</p>
<p>https://github.com/postgres/postgres/blob/master/src/backend/utils/adt/format_type.c#L390</p>
<div class="code-block"><figure class="highlight c"><input type="checkbox" id="code-wrap-1dcklj5-16" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-16" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * Add typmod decoration to the basic type name</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="type">static</span> <span class="type">char</span> *</span><br><span class="line"><span class="title function_">printTypmod</span><span class="params">(<span class="type">const</span> <span class="type">char</span> *typname, int32 typmod, Oid typmodout)</span></span><br><span class="line">&#123;</span><br><span class="line">	<span class="type">char</span>	   *res;</span><br><span class="line"></span><br><span class="line">	<span class="comment">/* Shouldn&#x27;t be called if typmod is -1 */</span></span><br><span class="line">	Assert(typmod &gt;= <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">	<span class="keyword">if</span> (typmodout == InvalidOid)</span><br><span class="line">	&#123;</span><br><span class="line">		<span class="comment">/* Default behavior: just print the integer typmod with parens */</span></span><br><span class="line">		res = psprintf(<span class="string">&quot;%s(%d)&quot;</span>, typname, (<span class="type">int</span>) typmod);</span><br><span class="line">	&#125;</span><br><span class="line">	<span class="keyword">else</span></span><br><span class="line">	&#123;</span><br><span class="line">		<span class="comment">/* Use the type-specific typmodout procedure */</span></span><br><span class="line">		<span class="type">char</span>	   *tmstr;</span><br><span class="line"></span><br><span class="line">		tmstr = DatumGetCString(OidFunctionCall1(typmodout,</span><br><span class="line">												 Int32GetDatum(typmod)));</span><br><span class="line">		res = psprintf(<span class="string">&quot;%s%s&quot;</span>, typname, tmstr);</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	<span class="keyword">return</span> res;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>上記は関連部分の抜粋ですが、</p>
<p><code>typmodout</code>で指定されている関数を呼び出して、<code>typmod</code>からの文字列へ変換をしているようです。<br>この<code>typmodout</code>については、以下のように<code>pg_attribute</code>テーブルと<code>pg_type</code>テーブルを結合することで確認できます。</p>
<p>今回は例として、atttypmod&#x3D;327686のものを確認します。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1dcklj5-17" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-17" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">healthcare_data=# WITH attribute_info AS (</span><br><span class="line">  SELECT</span><br><span class="line">    a.attname,</span><br><span class="line">    a.atttypid,</span><br><span class="line">    a.atttypmod,</span><br><span class="line">    t.typname,</span><br><span class="line">    t.typmodout -- pg_typeテーブルからtypmodout関数を取得</span><br><span class="line">  FROM</span><br><span class="line">    pg_attribute a</span><br><span class="line">  JOIN</span><br><span class="line">    pg_type t ON a.atttypid = t.oid</span><br><span class="line">  WHERE</span><br><span class="line">    a.attrelid = &#x27;patient_records&#x27;::regclass</span><br><span class="line">)</span><br><span class="line">SELECT</span><br><span class="line">  attname,</span><br><span class="line">  atttypid,</span><br><span class="line">  atttypmod,</span><br><span class="line">  CASE</span><br><span class="line">    WHEN atttypmod &gt;= 0 THEN</span><br><span class="line">      pg_catalog.format_type(atttypid, atttypmod) -- format_type関数でtypmodを表示</span><br><span class="line">    ELSE</span><br><span class="line">      &#x27;N/A&#x27; -- typmodがない場合は &#x27;N/A&#x27; と表示</span><br><span class="line">  END AS formatted_typemod,</span><br><span class="line">  typmodout -- typmodout関数のOID</span><br><span class="line">FROM</span><br><span class="line">  attribute_info;</span><br><span class="line">   attname    | atttypid | atttypmod |   formatted_typemod   |    typmodout</span><br><span class="line">--------------+----------+-----------+-----------------------+------------------</span><br><span class="line"> tableoid     |       26 |        -1 | N/A                   | -</span><br><span class="line"> ctid         |       27 |        -1 | N/A                   | -</span><br><span class="line"> xmin         |       28 |        -1 | N/A                   | -</span><br><span class="line"> xmax         |       28 |        -1 | N/A                   | -</span><br><span class="line"> cmin         |       29 |        -1 | N/A                   | -</span><br><span class="line"> cmax         |       29 |        -1 | N/A                   | -</span><br><span class="line"> patient_id   |     1043 |        54 | character varying(50) | varchartypmodout</span><br><span class="line"> blood_sugar  |     1700 |    327686 | numeric(5,2)          | numerictypmodout</span><br><span class="line"> test_results |     1231 |    327686 | numeric(5,2)[]        | numerictypmodout</span><br><span class="line"> address      |    16391 |        -1 | N/A                   | -</span><br><span class="line">(10 rows)</span><br><span class="line"></span><br></pre></td></tr></table></figure></div>

<p><code>typmodout</code>で指定されている関数のうち、確認対象としては<code>numerictypmodout</code>が呼び出される関数であることを確認できました。これは、以下の<code>numeric.c</code>で実装されています。</p>
<p>https://github.com/postgres/postgres/blob/master/src/backend/utils/adt/numeric.c#L1368</p>
<p>以下が関連箇所の抜粋です。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line">Datum</span><br><span class="line"><span class="title function_">numerictypmodout</span><span class="params">(PG_FUNCTION_ARGS)</span></span><br><span class="line">&#123;</span><br><span class="line">	int32		typmod = PG_GETARG_INT32(<span class="number">0</span>);</span><br><span class="line">	<span class="type">char</span>	   *res = (<span class="type">char</span> *) palloc(<span class="number">64</span>);</span><br><span class="line"></span><br><span class="line">	<span class="keyword">if</span> (is_valid_numeric_typmod(typmod))</span><br><span class="line">		<span class="built_in">snprintf</span>(res, <span class="number">64</span>, <span class="string">&quot;(%d,%d)&quot;</span>,</span><br><span class="line">				 numeric_typmod_precision(typmod),</span><br><span class="line">				 numeric_typmod_scale(typmod));</span><br><span class="line">	<span class="keyword">else</span></span><br><span class="line">		*res = <span class="string">&#x27;\0&#x27;</span>;</span><br><span class="line"></span><br><span class="line">	PG_RETURN_CSTRING(res);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<div class="code-block"><figure class="highlight c"><input type="checkbox" id="code-wrap-1dcklj5-18" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1dcklj5-18" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * numeric_typmod_precision() -</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> *	Extract the precision from a numeric typmod --- see make_numeric_typmod().</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="type">static</span> <span class="keyword">inline</span> <span class="type">int</span></span><br><span class="line"><span class="title function_">numeric_typmod_precision</span><span class="params">(int32 typmod)</span></span><br><span class="line">&#123;</span><br><span class="line">	<span class="keyword">return</span> ((typmod - VARHDRSZ) &gt;&gt; <span class="number">16</span>) &amp; <span class="number">0xffff</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * numeric_typmod_scale() -</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> *	Extract the scale from a numeric typmod --- see make_numeric_typmod().</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> *	Note that the scale may be negative, so we must do sign extension when</span></span><br><span class="line"><span class="comment"> *	unpacking it.  We do this using the bit hack (x^1024)-1024, which sign</span></span><br><span class="line"><span class="comment"> *	extends an 11-bit two&#x27;s complement number x.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="type">static</span> <span class="keyword">inline</span> <span class="type">int</span></span><br><span class="line"><span class="title function_">numeric_typmod_scale</span><span class="params">(int32 typmod)</span></span><br><span class="line">&#123;</span><br><span class="line">	<span class="keyword">return</span> (((typmod - VARHDRSZ) &amp; <span class="number">0x7ff</span>) ^ <span class="number">1024</span>) - <span class="number">1024</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>ようやく復元の処理にたどり着けましたね。VARHDRSZについては、データ全体のバイト数を表現しており、4バイトとなります。</p>
<p>まとめると、NUMERIC型と型修飾子で表現された<code>327686</code>とい<br>う値をbit表現に変換してヘッダーを引いた場合は、以下のようなイメージで復元をされます。</p>
<img fetchpriority="high" src="/images/2024/20241023b/image.png" alt="image.png" width="518" height="105">

<p>型修飾子と文字列の相互変換というものは、それぞれ対象としたい型ごとのビット表現を把握して実装する必要があるので、自作するのであればかなり手間になりそうということは伝わったのではないでしょうか？</p>
<p><code>to_regtypemod</code>関数の追加より前では、実際にテーブルに追加して<code>pg_attribute</code>テーブルと<code>pg_type</code>から良い感じで取得すれば、文字列(‘numeric(5,2)’のようなもの)からの型修飾子の取得自体は可能です。</p>
<p>また、精度・位取り自体を取得したい場合にも正規表現などを工夫すれば不可能ではないです。</p>
<p>ですが、これらの作業はやりたいことのわりには手間がかかり、かつ不具合のリスクやアップデートへの対応力は高いとは言えないと思います。</p>
<p>これを文字列 -&gt; <code>typemod</code>の形式に直接変換できるという意味で、<code>to_regtypemod</code>関数には大きな価値があると感じます。</p>
<h2 id="3-具体の活用事例を考える">3. 具体の活用事例を考える</h2><p><code>to_regtypemod</code>自体は文字列からtypemodを取得する関数であるため、外部システムやテストとのやりとりが最も活用される事例ではないかと思います。</p>
<p>実際にこの機能の追加の契機となっている、以下のメーリングリストでは、postgresの単体テストツールであるpgTAPでデータ型のアサーションが困難であったとのことで、機能のパッチを提案しています。</p>
<p>https://www.postgresql.org/message-id/DF2324CA-2673-4ABE-B382-26B5770B6AA3@justatheory.com</p>
<p>データベースのテスト実装などでもそうですし、あるいは他システムとのマイグレーションの事前検証(MySQL -&gt; PostgreSQLとか)、スキーマ間の移行などにも役立つかもしれません。</p>
<h2 id="4-まとめ">4. まとめ</h2><p>PostgreSQL 17で新規に追加された<code>to_regtypemod</code>関数では、文字列(‘numeric(5,2)’のようなもの)から型修飾子(typemod)を直接取得できます。</p>
<p>従来の方法では、実際にデータベースに登録して<code>pg_attribute</code>テーブルと<code>pg_type</code>テーブルなどを組み合わせて取得するであったり、正規表現を工夫するなどの方法も考えられますが、いささか煩雑です。</p>
<p>医療や金融システムなど、型修飾子レベルで正確なデータ型管理が求められる分野で <code>to_regtypemod</code> を活用することで、データの整合性を高いレベルで維持しつつ、開発・運用の効率化を図ることができるのではないでしょうか？</p>
<h2 id="参考文献">参考文献</h2><ul>
<li>https://pgpedia.info/t/to_regtypemod.html</li>
<li>https://www.postgresql.jp/document/16/html/datatype-numeric.html</li>
<li>https://github.com/postgres/postgres</li>
<li>https://www.postgresql.org/message-id/DF2324CA-2673-4ABE-B382-26B5770B6AA3@justatheory.com</li>
</ul>
]]></content>
    <summary type="html">PostgreSQL 17ではバーフォーマンスの改善や、JSON周りなど大奥のアップデートが追加されていますが、その中でもデータ型文字列からデータ型の型修飾子を取得する関数to_regtypemodが新規に追加されました。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="PostgreSQL" scheme="https://future-architect.github.io/tags/PostgreSQL/"/>
    <category term="PostgreSQL17" scheme="https://future-architect.github.io/tags/PostgreSQL17/"/>
    <category term="SQL" scheme="https://future-architect.github.io/tags/SQL/"/>
  </entry>
  <entry>
    <title>PostgreSQL17リリース記念連載を始めます</title>
    <link href="https://future-architect.github.io/articles/20241023a/"/>
    <id>https://future-architect.github.io/articles/20241023a/</id>
    <published>2024-10-22T15:00:00.000Z</published>
    <updated>2024-10-22T15:00:00.000Z</updated>
    <author><name>真野隼記</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20241023a/top.png" alt="top.png" width="761" height="366">

<h2 id="はじめに">はじめに</h2><p>Technology Innovation Group真野です。</p>
<p>2024年9月26日、PostgreSQL 17がリリースされました。</p>
<ul>
<li>PostgreSQL 17 Released!</li>
</ul>
<p>それを記念して、PostgreSQL17のプレスキットやリリースノートから記事ごとにトピックを抜粋して、ブログリレーを行います。</p>
<h2 id="スケジュール">スケジュール</h2><div class="scroll"><table>
<thead>
<tr>
<th>日付</th>
<th>担当者</th>
<th>タイトル</th>
</tr>
</thead>
<tbody><tr>
<td>10&#x2F;23</td>
<td>山本竜玄</td>
<td>to_regtypemod関数と型修飾子について</td>
</tr>
<tr>
<td>11&#x2F;06</td>
<td>真野隼記</td>
<td>排他制約について</td>
</tr>
</tbody></table></div>
<h2 id="PostgreSQL-17-アップデート概要">PostgreSQL 17 アップデート概要</h2><p>主な更新内容をプレスキットからまとめます。</p>
<ul>
<li>VACUUMが性能改善で、従来の1&#x2F;20のメモリ量で動作するようになり、速度向上を実現</li>
<li>WALの処理改善で、書き込みスループットが最大で2倍向上する可能性</li>
<li>新しいストリーミングI&#x2F;Oインターフェースで、シーケンシャルスキャンやANALYZEの速度向上</li>
<li>B-Treeインデックスを使用した、IN句の性能向上</li>
<li>NOT NULL制約の最適化</li>
<li>共通テーブル式（いわゆるWITH句）での実行計画の改善</li>
<li>JSON周り<ul>
<li>SQL&#x2F;JSON コンストラクタのサポート</li>
<li>クエリ関数（JSON_EXISTS、JSON_QUERY、JSON_VALUE）のサポート</li>
<li>jsonpath式の追加</li>
</ul>
</li>
<li>MERGE文でRETURNINGの利用が可能</li>
<li>COPYが最大2倍の性の向上と、ON_ERRORオプションの追加</li>
<li>パーティションテーブルでの識別子、排他制約が利用可能となった</li>
<li><code>postgres_fdw</code> でEXISTSやIN句をリモートサーバ側にプッシュダウンできるようになった</li>
<li>論理レプリケーションスロットを削除する必要がなくなった</li>
<li>論理レプリケーションのフェイルオーバ制御が追加された</li>
<li>TLS オプション sslnegotiation が追加された</li>
<li>pg_combinebackup ユーティリティが追加された</li>
<li>pg_dumpに–fiiterオプションが追加された</li>
<li>EXPLAINに、I&#x2F;Oブロック読み書き時間が表示されるようになったのと、SERIALIZE、MEMORYオプションが追加された</li>
<li>インデックスのバキューム進行状況をpg_stat_progress_vacuumで確認できるようになった</li>
<li>pg_wait_events システムビューが追加され、アクティブなセッションの待機を分析できるようになった</li>
</ul>
<h2 id="さいごに">さいごに</h2><p>フューチャーではPostgreSQLは大人気で、おそらく一番利用が多いDBMSです。今回はリリース記念という名目ですが、業務で得たPostgreSQLの知見を引き続き発信していきます。</p>
]]></content>
    <summary type="html">PostgreSQL 17がリリースされたことを記念し、ブログ連載を始めます</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="PostgreSQL" scheme="https://future-architect.github.io/tags/PostgreSQL/"/>
    <category term="PostgreSQL17" scheme="https://future-architect.github.io/tags/PostgreSQL17/"/>
    <category term="インデックス" scheme="https://future-architect.github.io/tags/%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9/"/>
  </entry>
  <entry>
    <title>PostgreSQL拡張機能のPLV8を使ってみた</title>
    <link href="https://future-architect.github.io/articles/20240830a/"/>
    <id>https://future-architect.github.io/articles/20240830a/</id>
    <published>2024-08-29T15:00:00.000Z</published>
    <updated>2024-08-29T15:00:00.000Z</updated>
    <author><name>岸本卓也</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20240830a/plv8-eyecatch.png" alt="plv8-eyecatch.png" width="1200" height="437">

<h2 id="はじめに">はじめに</h2><p>こんにちは、TIGの岸本卓也です。夏の自由研究連載2024 シリーズです。</p>
<p>これまでPostgreSQLで手続き型処理を実装することにがっつりと向き合うことがなかったため手続き型処理の実装言語はPL&#x2F;pgSQL一択だと思いこんでいたのですが、実は複数の選択肢がありました。PostgreSQL標準で提供されている選択肢はPL&#x2F;pgSQL、PL&#x2F;Tcl、PL&#x2F;Perl、PL&#x2F;Pythonですが、サードパーティ提供も含めると多数の言語が使えるます。</p>
<p>その中でもJavaScriptで実装できるPLV8は手馴染みが良さそうで興味を惹かれたので試してみることにしました。</p>
<h2 id="DBの準備">DBの準備</h2><p>適当にPostgreSQLデータベースとサンプルDBを作成しておきます。</p>
<h3 id="DBインスタンス">DBインスタンス</h3><p>Amazon RDS for PostgreSQLを利用してPostgreSQL 16.3のデータベースを作成しました。大手のクラウドベンダーではマネージドサービスのDBでもPLV8に対応しています。</p>
<p>cf. クラウドベンダーのマネージドPostgreSQLデータベースにおける対応拡張機能</p>
<ul>
<li>AWS: Amazon RDS for PostgreSQL</li>
<li>Azure: Azure Database for PostgreSQL</li>
<li>GCP: Cloud SQL for PostgreSQL</li>
</ul>
<p>PLV8拡張機能をインストールします。(cf. Installing PLV8)</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="comment">-- 利用可能な拡張機能を確認</span></span><br><span class="line"><span class="keyword">select</span> <span class="operator">*</span></span><br><span class="line"><span class="keyword">from</span> pg_available_extensions</span><br><span class="line"><span class="keyword">where</span> name <span class="keyword">like</span> <span class="string">&#x27;pl%&#x27;</span></span><br><span class="line"><span class="keyword">order</span> <span class="keyword">by</span> name;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- PLV8拡張機能をインストール</span></span><br><span class="line"><span class="keyword">create</span> extension plv8;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- インストールできたことを確認 その1</span></span><br><span class="line"><span class="keyword">select</span> <span class="operator">*</span></span><br><span class="line"><span class="keyword">from</span> pg_extension</span><br><span class="line"><span class="keyword">order</span> <span class="keyword">by</span> extname;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- インストールできたことを確認 その2</span></span><br><span class="line"><span class="keyword">select</span> plv8_version();</span><br></pre></td></tr></table></figure>

<h3 id="サンプルDB">サンプルDB</h3><p>今回は PostgreSQL wiki に掲載されている Pagila を利用しました。</p>
<h2 id="DO-ブロックでの実行">DO ブロックでの実行</h2><p>PLV8は <code>DO</code> による無名コードブロック実行にも対応しているため、手始めに DO ブロックで試してみます。</p>
<h3 id="素朴なSQL実行の例">素朴なSQL実行の例</h3><div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1iwdeg6-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">do $$</span><br><span class="line">    <span class="operator">/</span><span class="operator">/</span> <span class="keyword">SQL</span>を実行して結果を取得</span><br><span class="line">    const inactives <span class="operator">=</span> plv8.execute(`</span><br><span class="line">        <span class="keyword">select</span> <span class="built_in">count</span>(<span class="number">1</span>)::<span class="type">integer</span> <span class="keyword">as</span> inactive_count</span><br><span class="line">        <span class="keyword">from</span> customer c</span><br><span class="line">        <span class="keyword">where</span> c.active <span class="operator">=</span> <span class="number">0</span></span><br><span class="line">    `)</span><br><span class="line">    plv8.elog(NOTICE, `There <span class="keyword">is</span> $&#123;inactives[<span class="number">0</span>].inactive_count&#125; inactive members.`)</span><br><span class="line">$$ <span class="keyword">language</span> plv8;</span><br></pre></td></tr></table></figure></div>

<p>実行結果は以下です。特に問題なく期待通りの結果が得られました。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1iwdeg6-2" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">pagila=&gt; <span class="keyword">do</span> $$</span><br><span class="line">pagila$&gt;     // SQLを実行して結果を取得</span><br><span class="line">pagila$&gt;     const inactives = plv8.execute(`</span><br><span class="line">pagila$&gt;         <span class="keyword">select</span> count(1)::<span class="built_in">integer</span> as inactive_count</span><br><span class="line">pagila$&gt;         from customer c</span><br><span class="line">pagila$&gt;         <span class="built_in">where</span> c.active = 0</span><br><span class="line">pagila$&gt;     `)</span><br><span class="line">pagila$&gt;     plv8.elog(NOTICE, `There is <span class="variable">$&#123;inactives[0].inactive_count&#125;</span> inactive members.`)</span><br><span class="line">pagila$&gt; $$ language plv8;</span><br><span class="line">NOTICE:  There is 15 inactive members.</span><br><span class="line">DO</span><br></pre></td></tr></table></figure></div>

<h3 id="SELECT-結果をカーソルで取得する例">SELECT 結果をカーソルで取得する例</h3><p><code>plv8.execute</code> ではSQL実行結果を一度に取得しますが、カーソルを使って逐次取得できます。カーソルで取得する方法を試してみます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1iwdeg6-3" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">do $$</span><br><span class="line">    let plan, <span class="keyword">cursor</span></span><br><span class="line">    try &#123;</span><br><span class="line">        <span class="operator">/</span><span class="operator">/</span> プリペアド文を作成</span><br><span class="line">        plan <span class="operator">=</span> plv8.prepare(`</span><br><span class="line">            <span class="keyword">select</span> c.customer_id, c.first_name</span><br><span class="line">            <span class="keyword">from</span> customer c</span><br><span class="line">            <span class="keyword">where</span> <span class="keyword">exists</span> (</span><br><span class="line">                <span class="keyword">select</span> <span class="number">1</span></span><br><span class="line">                <span class="keyword">from</span> rental r</span><br><span class="line">                <span class="keyword">where</span> c.customer_id <span class="operator">=</span> r.customer_id</span><br><span class="line">                <span class="keyword">group</span> <span class="keyword">by</span> r.customer_id</span><br><span class="line">                <span class="keyword">having</span> <span class="built_in">count</span>(<span class="number">1</span>) <span class="operator">&gt;</span> $<span class="number">1</span></span><br><span class="line">            )</span><br><span class="line">            <span class="keyword">order</span> <span class="keyword">by</span> c.customer_id</span><br><span class="line">        `, [<span class="string">&#x27;integer&#x27;</span>])</span><br><span class="line"></span><br><span class="line">        <span class="operator">/</span><span class="operator">/</span> プリペアド文を実行してカーソルを取得</span><br><span class="line">        <span class="keyword">cursor</span> <span class="operator">=</span> plan.cursor([<span class="number">40</span>])</span><br><span class="line">        let customer</span><br><span class="line">        <span class="operator">/</span><span class="operator">/</span> カーソルを使ってループ</span><br><span class="line">        while (customer <span class="operator">=</span> cursor.fetch()) &#123;</span><br><span class="line">            plv8.elog(NOTICE, `id: $&#123;customer.customer_id&#125;, name: $&#123;customer.first_name&#125;`)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; finally &#123;</span><br><span class="line">        <span class="operator">/</span><span class="operator">/</span> カーソルとプリペアド文は解放が必要</span><br><span class="line">        <span class="keyword">cursor</span>?.<span class="keyword">close</span>()</span><br><span class="line">        plan?.<span class="keyword">free</span>()</span><br><span class="line">    &#125;</span><br><span class="line">$$ <span class="keyword">language</span> plv8;</span><br></pre></td></tr></table></figure></div>

<p>実行結果は以下です。こちらも特に問題なく期待通りの結果が得られました。</p>
<details>
<summary>実行結果</summary>

<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1iwdeg6-4" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">pagila=&gt; <span class="keyword">do</span> $$</span><br><span class="line">pagila$&gt;     <span class="built_in">let</span> plan, cursor</span><br><span class="line">pagila$&gt;     try &#123;</span><br><span class="line">pagila$&gt;         // プリペアド文を作成</span><br><span class="line">pagila$&gt;         plan = plv8.prepare(`</span><br><span class="line">pagila$&gt;             <span class="keyword">select</span> c.customer_id, c.first_name</span><br><span class="line">pagila$&gt;             from customer c</span><br><span class="line">pagila$&gt;             <span class="built_in">where</span> exists (</span><br><span class="line">pagila$&gt;                 <span class="keyword">select</span> 1</span><br><span class="line">pagila$&gt;                 from rental r</span><br><span class="line">pagila$&gt;                 <span class="built_in">where</span> c.customer_id = r.customer_id</span><br><span class="line">pagila$&gt;                 group by r.customer_id</span><br><span class="line">pagila$&gt;                 having count(1) &gt; <span class="variable">$1</span></span><br><span class="line">pagila$&gt;             )</span><br><span class="line">pagila$&gt;             order by c.customer_id</span><br><span class="line">pagila$&gt;         `, [<span class="string">&#x27;integer&#x27;</span>])</span><br><span class="line">pagila$&gt;</span><br><span class="line">pagila$&gt;         // プリペアド文を実行してカーソルを取得</span><br><span class="line">pagila$&gt;         cursor = plan.cursor([40])</span><br><span class="line">pagila$&gt;         <span class="built_in">let</span> customer</span><br><span class="line">pagila$&gt;         // カーソルを使ってループ</span><br><span class="line">pagila$&gt;         <span class="keyword">while</span> (customer = cursor.fetch()) &#123;</span><br><span class="line">pagila$&gt;             plv8.elog(NOTICE, `<span class="built_in">id</span>: <span class="variable">$&#123;customer.customer_id&#125;</span>, name: <span class="variable">$&#123;customer.first_name&#125;</span>`)</span><br><span class="line">pagila$&gt;         &#125;</span><br><span class="line">pagila$&gt;     &#125; finally &#123;</span><br><span class="line">pagila$&gt;         // カーソルとプリペアド文は解放が必要</span><br><span class="line">pagila$&gt;         cursor?.close()</span><br><span class="line">pagila$&gt;         plan?.free()</span><br><span class="line">pagila$&gt;     &#125;</span><br><span class="line">pagila$&gt; $$ language plv8;</span><br><span class="line">NOTICE:  <span class="built_in">id</span>: 75, name: TAMMY</span><br><span class="line">NOTICE:  <span class="built_in">id</span>: 144, name: CLARA</span><br><span class="line">NOTICE:  <span class="built_in">id</span>: 148, name: ELEANOR</span><br><span class="line">NOTICE:  <span class="built_in">id</span>: 236, name: MARCIA</span><br><span class="line">NOTICE:  <span class="built_in">id</span>: 526, name: KARL</span><br><span class="line">DO</span><br></pre></td></tr></table></figure></div>

</details>

<h2 id="関数として作成して実行">関数として作成して実行</h2><h3 id="CREATE-FUNCTION-文を手作成">CREATE FUNCTION 文を手作成</h3><p>カーソルで取得する例のSQLを関数にして実行してみます。DO ブロックの代わりに CREATE FUNCTION にするだけなので、ついでに閾値を関数の引数で渡すように変更してみます。</p>
<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1iwdeg6-5" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">create</span> <span class="keyword">function</span> print_top_customers(frequency <span class="type">integer</span>) <span class="keyword">returns</span> void <span class="keyword">as</span> $$</span><br><span class="line"><span class="comment">-- ---------- 処理はほぼ同じなため記載省略 ----------</span></span><br><span class="line">$$ <span class="keyword">language</span> plv8 stable;</span><br></pre></td></tr></table></figure></div>

<details>
<summary>実行結果</summary>

<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1iwdeg6-6" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-6" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">pagila=&gt; create <span class="keyword">function</span> print_top_customers(frequency <span class="built_in">integer</span>) returns void as $$</span><br><span class="line">pagila$&gt;     <span class="built_in">let</span> plan, cursor</span><br><span class="line">pagila$&gt;     try &#123;</span><br><span class="line">pagila$&gt;         // プリペアド文を作成</span><br><span class="line">pagila$&gt;         plan = plv8.prepare(`</span><br><span class="line">pagila$&gt;             <span class="keyword">select</span> c.customer_id, c.first_name</span><br><span class="line">pagila$&gt;             from customer c</span><br><span class="line">pagila$&gt;             <span class="built_in">where</span> exists (</span><br><span class="line">pagila$&gt;                 <span class="keyword">select</span> 1</span><br><span class="line">pagila$&gt;                 from rental r</span><br><span class="line">pagila$&gt;                 <span class="built_in">where</span> c.customer_id = r.customer_id</span><br><span class="line">pagila$&gt;                 group by r.customer_id</span><br><span class="line">pagila$&gt;                 having count(1) &gt; <span class="variable">$1</span></span><br><span class="line">pagila$&gt;             )</span><br><span class="line">pagila$&gt;             order by c.customer_id</span><br><span class="line">pagila$&gt;         `, [<span class="string">&#x27;integer&#x27;</span>])</span><br><span class="line">pagila$&gt;</span><br><span class="line">pagila$&gt;         // プリペアド文を実行してカーソルを取得</span><br><span class="line">pagila$&gt;         cursor = plan.cursor([frequency])</span><br><span class="line">pagila$&gt;         <span class="built_in">let</span> customer</span><br><span class="line">pagila$&gt;         // カーソルを使ってループ</span><br><span class="line">pagila$&gt;         <span class="keyword">while</span> (customer = cursor.fetch()) &#123;</span><br><span class="line">pagila$&gt;             plv8.elog(NOTICE, `<span class="built_in">id</span>: <span class="variable">$&#123;customer.customer_id&#125;</span>, name: <span class="variable">$&#123;customer.first_name&#125;</span>`)</span><br><span class="line">pagila$&gt;         &#125;</span><br><span class="line">pagila$&gt;     &#125; finally &#123;</span><br><span class="line">pagila$&gt;         // カーソルとプリペアド文は解放が必要</span><br><span class="line">pagila$&gt;         cursor?.close()</span><br><span class="line">pagila$&gt;         plan?.free()</span><br><span class="line">pagila$&gt;     &#125;</span><br><span class="line">pagila$&gt; $$ language plv8 stable;</span><br><span class="line">CREATE FUNCTION</span><br><span class="line">pagila=&gt; <span class="keyword">select</span> print_top_customers(40);</span><br><span class="line">NOTICE:  <span class="built_in">id</span>: 75, name: TAMMY</span><br><span class="line">NOTICE:  <span class="built_in">id</span>: 144, name: CLARA</span><br><span class="line">NOTICE:  <span class="built_in">id</span>: 148, name: ELEANOR</span><br><span class="line">NOTICE:  <span class="built_in">id</span>: 236, name: MARCIA</span><br><span class="line">NOTICE:  <span class="built_in">id</span>: 526, name: KARL</span><br><span class="line"> print_top_customers</span><br><span class="line">---------------------</span><br><span class="line"></span><br><span class="line">(1 行)</span><br></pre></td></tr></table></figure></div>

</details></p>

<p>DO ブロックでの実行と同じ結果が得られました。</p>
<h3 id="JavaScript実装をバンドルして-CREATE-FUNCTION-SQL-を生成">JavaScript実装をバンドルして CREATE FUNCTION SQL を生成</h3><p>前の例では CREATE FUNCTION 文を手作成しました。そのSQLにおいて1行目と最終行以外はJSの実装です。であれば、JS部分は独立して開発して最後に CREATE FUNCTION 文を生成できれば色々捗りそうです。それをやってくれるツールであるPLV8ifyが公式ドキュメントで紹介されていますので、ここからはPLV8ifyを使った関数の開発を試してみます。</p>
<h4 id="PLV8関数開発環境の構築">PLV8関数開発環境の構築</h4><p>適当なNode.js環境をインストールしておきます。今回はv20.16.0のNode.jsを利用しました。</p>
<p>開発用のフォルダで以下のコマンドによりプロジェクトを作成し、利用するパッケージをインストールします。PLV8ifyによる変換はTypeScriptしか対応していないのでTypeScriptの開発環境を整えています。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1iwdeg6-7" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-7" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">npm init -y</span><br><span class="line">npm pkg <span class="built_in">set</span> <span class="built_in">type</span>=module</span><br><span class="line">npm install -D eslint @eslint/js @stylistic/eslint-plugin @types/eslint__js typescript typescript-eslint plv8ify @types/plv8-internal vitest</span><br><span class="line">npm pkg <span class="built_in">set</span> scripts.test=vitest</span><br><span class="line">npm install dayjs</span><br></pre></td></tr></table></figure></div>

<p>上記でインストールされたパッケージのバージョンは以下の通り (<code>npm list --depth=0</code> の出力) です。</p>
<details>
<summary>パッケージバージョン</summary>

<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">+-- @eslint/js@9.9.1</span><br><span class="line">+-- @stylistic/eslint-plugin@2.7.1</span><br><span class="line">+-- @types/eslint__js@8.42.3</span><br><span class="line">+-- @types/plv8-internal@3.1.2</span><br><span class="line">+-- dayjs@1.11.13</span><br><span class="line">+-- eslint@9.9.1</span><br><span class="line">+-- plv8ify@0.0.59</span><br><span class="line">+-- typescript-eslint@8.3.0</span><br><span class="line">+-- typescript@5.5.4</span><br><span class="line">`-- vitest@2.0.5</span><br></pre></td></tr></table></figure>

</details>

<p>なお、 <code>plv8ify</code> は実際には上記でインストールされたバージョンそのものではなく、バグと思われる挙動や利便性向上をローカルで修正したものを使用しました。このため、ここより後の例ではTypeScriptで実装した関数に対して生成される CREATE FUNCTION 文の関数定義では関数名と引数名が snake_case 化されるようにしています。</p>
<h4 id="PLV8関数のTS実装例">PLV8関数のTS実装例</h4><p>前の手作成 CREATE FUNCTION の例をTS実装にし、ついでに検索結果をログではなく戻り値として返却するように変更したものがこちらです。</p>
<div class="code-block"><figure class="highlight ts"><input type="checkbox" id="code-wrap-1iwdeg6-8" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-8" title="コードの折り返しを切り替える"></label><figcaption><span>fetch_top_customers.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@plv</span>8ify_param &#123;integer&#125; frequency</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@plv</span>8ify_return &#123;setof record&#125;</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@plv</span>8ify_volatility STABLE</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">fetchTopCustomers</span>(<span class="params"><span class="attr">frequency</span>: <span class="built_in">number</span></span>): <span class="built_in">void</span> &#123;</span><br><span class="line">  <span class="keyword">let</span> plan!: <span class="title class_">PreparedPlan</span>, cursor!: <span class="title class_">Cursor</span>;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    plan = plv8.<span class="title function_">prepare</span>(<span class="string">`</span></span><br><span class="line"><span class="string">      select c.customer_id, c.first_name</span></span><br><span class="line"><span class="string">      from customer c</span></span><br><span class="line"><span class="string">      where exists (</span></span><br><span class="line"><span class="string">        select 1</span></span><br><span class="line"><span class="string">        from rental r</span></span><br><span class="line"><span class="string">        where c.customer_id = r.customer_id</span></span><br><span class="line"><span class="string">        group by r.customer_id</span></span><br><span class="line"><span class="string">        having count(1) &gt; $1</span></span><br><span class="line"><span class="string">      )</span></span><br><span class="line"><span class="string">      order by c.customer_id</span></span><br><span class="line"><span class="string">    `</span>, [<span class="string">&#x27;integer&#x27;</span>]);</span><br><span class="line"></span><br><span class="line">    cursor = plan.<span class="title function_">cursor</span>([frequency]);</span><br><span class="line">    <span class="keyword">let</span> <span class="attr">customer</span>: <span class="title class_">SQLRow</span>;</span><br><span class="line">    <span class="keyword">while</span> ((customer = cursor.<span class="title function_">fetch</span>())) &#123;</span><br><span class="line">      plv8.<span class="title function_">return_next</span>(&#123; <span class="attr">id</span>: customer.<span class="property">customer_id</span>, <span class="attr">name</span>: customer.<span class="property">first_name</span> &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">finally</span> &#123;</span><br><span class="line">    cursor?.<span class="title function_">close</span>();</span><br><span class="line">    plan?.<span class="title function_">free</span>();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p><code>plv8ify</code> で CREATE FUNCTION SQL を生成し、</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1iwdeg6-9" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-9" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">npx plv8ify generate --input-file src/fetch_top_customers.ts</span><br></pre></td></tr></table></figure></div>

<p>関数作成後にSQLを実行しました。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1iwdeg6-10" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-10" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">pagila=&gt; \i plv8ify-dist/fetch_top_customers.plv8.sql</span><br><span class="line">psql:plv8ify-dist/fetch_top_customers.plv8.sql:1: NOTICE:  <span class="keyword">function</span> fetch_top_customers(pg_catalog.int4) does not exist, skipping</span><br><span class="line">DROP FUNCTION</span><br><span class="line">CREATE FUNCTION</span><br><span class="line">pagila=&gt; <span class="keyword">select</span> * from fetch_top_customers(40) as (<span class="built_in">id</span> <span class="built_in">integer</span>, name varchar);</span><br><span class="line"> <span class="built_in">id</span>  |  name</span><br><span class="line">-----+---------</span><br><span class="line">  75 | TAMMY</span><br><span class="line"> 144 | CLARA</span><br><span class="line"> 148 | ELEANOR</span><br><span class="line"> 236 | MARCIA</span><br><span class="line"> 526 | KARL</span><br><span class="line">(5 行)</span><br></pre></td></tr></table></figure></div>

<p>手作成 CREATE FUNCTION の例と同じ結果が得られました。</p>
<h4 id="実践的な例">実践的な例</h4><p>より実践的な例としてPL&#x2F;pgSQLの関数をPLV8関数に実装し直してみます。対象はサンプルDBにある <code>rewards_report</code> 関数です。ただし素の <code>rewards_report</code> 関数は <code>CURRENT_DATE</code> が使われていてテストしにくいため、 <code>CURRENT_DATE</code> 部分を引数で指定できるように変更した以下の関数を対象とします。</p>
<details>
<summary>引数追加版 <code>rewards_report</code> 関数</summary>

<div class="code-block"><figure class="highlight sql"><input type="checkbox" id="code-wrap-1iwdeg6-11" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-11" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">FUNCTION</span> public.rewards_report(min_monthly_purchases <span class="type">integer</span>, min_dollar_amount_purchased <span class="type">numeric</span>, today <span class="type">date</span>) <span class="keyword">RETURNS</span> SETOF public.customer</span><br><span class="line">    <span class="keyword">LANGUAGE</span> plpgsql SECURITY DEFINER</span><br><span class="line">    <span class="keyword">AS</span> $_$</span><br><span class="line"><span class="keyword">DECLARE</span></span><br><span class="line">    last_month_start <span class="type">DATE</span>;</span><br><span class="line">    last_month_end <span class="type">DATE</span>;</span><br><span class="line">rr RECORD;</span><br><span class="line">tmpSQL TEXT;</span><br><span class="line"><span class="keyword">BEGIN</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Some sanity checks... */</span></span><br><span class="line">    IF min_monthly_purchases <span class="operator">=</span> <span class="number">0</span> <span class="keyword">THEN</span></span><br><span class="line">        RAISE EXCEPTION <span class="string">&#x27;Minimum monthly purchases parameter must be &gt; 0&#x27;</span>;</span><br><span class="line">    <span class="keyword">END</span> IF;</span><br><span class="line">    IF min_dollar_amount_purchased <span class="operator">=</span> <span class="number">0.00</span> <span class="keyword">THEN</span></span><br><span class="line">        RAISE EXCEPTION <span class="string">&#x27;Minimum monthly dollar amount purchased parameter must be &gt; $0.00&#x27;</span>;</span><br><span class="line">    <span class="keyword">END</span> IF;</span><br><span class="line"></span><br><span class="line">    last_month_start :<span class="operator">=</span> today <span class="operator">-</span> <span class="string">&#x27;3 month&#x27;</span>::<span class="type">interval</span>;</span><br><span class="line">    last_month_start :<span class="operator">=</span> to_date((<span class="built_in">extract</span>(<span class="keyword">YEAR</span> <span class="keyword">FROM</span> last_month_start) <span class="operator">||</span> <span class="string">&#x27;-&#x27;</span> <span class="operator">||</span> <span class="built_in">extract</span>(<span class="keyword">MONTH</span> <span class="keyword">FROM</span> last_month_start) <span class="operator">||</span> <span class="string">&#x27;-01&#x27;</span>),<span class="string">&#x27;YYYY-MM-DD&#x27;</span>);</span><br><span class="line">    last_month_end :<span class="operator">=</span> LAST_DAY(last_month_start);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/*</span></span><br><span class="line"><span class="comment">    Create a temporary storage area for Customer IDs.</span></span><br><span class="line"><span class="comment">    */</span></span><br><span class="line">    <span class="keyword">CREATE</span> TEMPORARY <span class="keyword">TABLE</span> tmpCustomer (customer_id <span class="type">INTEGER</span> <span class="keyword">NOT NULL</span> <span class="keyword">PRIMARY KEY</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/*</span></span><br><span class="line"><span class="comment">    Find all customers meeting the monthly purchase requirements</span></span><br><span class="line"><span class="comment">    */</span></span><br><span class="line"></span><br><span class="line">    tmpSQL :<span class="operator">=</span> <span class="string">&#x27;INSERT INTO tmpCustomer (customer_id)</span></span><br><span class="line"><span class="string">        SELECT p.customer_id</span></span><br><span class="line"><span class="string">        FROM payment AS p</span></span><br><span class="line"><span class="string">        WHERE DATE(p.payment_date) BETWEEN &#x27;</span><span class="operator">||</span>quote_literal(last_month_start) <span class="operator">||</span><span class="string">&#x27; AND &#x27;</span><span class="operator">||</span> quote_literal(last_month_end) <span class="operator">||</span> <span class="string">&#x27;</span></span><br><span class="line"><span class="string">        GROUP BY customer_id</span></span><br><span class="line"><span class="string">        HAVING SUM(p.amount) &gt; &#x27;</span><span class="operator">||</span> min_dollar_amount_purchased <span class="operator">||</span> <span class="string">&#x27;</span></span><br><span class="line"><span class="string">        AND COUNT(customer_id) &gt; &#x27;</span> <span class="operator">||</span>min_monthly_purchases ;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">EXECUTE</span> tmpSQL;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/*</span></span><br><span class="line"><span class="comment">    Output ALL customer information of matching rewardees.</span></span><br><span class="line"><span class="comment">    Customize output as needed.</span></span><br><span class="line"><span class="comment">    */</span></span><br><span class="line">    <span class="keyword">FOR</span> rr <span class="keyword">IN</span> <span class="keyword">EXECUTE</span> <span class="string">&#x27;SELECT c.* FROM tmpCustomer AS t INNER JOIN customer AS c ON t.customer_id = c.customer_id&#x27;</span> LOOP</span><br><span class="line">        <span class="keyword">RETURN</span> NEXT rr;</span><br><span class="line">    <span class="keyword">END</span> LOOP;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Clean up */</span></span><br><span class="line">    tmpSQL :<span class="operator">=</span> <span class="string">&#x27;DROP TABLE tmpCustomer&#x27;</span>;</span><br><span class="line">    <span class="keyword">EXECUTE</span> tmpSQL;</span><br><span class="line"></span><br><span class="line"><span class="keyword">RETURN</span>;</span><br><span class="line"><span class="keyword">END</span></span><br><span class="line">$_$;</span><br></pre></td></tr></table></figure></div>

</details>

<p>上記PL&#x2F;pgSQL関数を以下のようにTS実装しました。</p>
<details>
<summary>引数追加版 <code>rewards_report</code> のTS実装</summary>

<div class="code-block"><figure class="highlight ts"><input type="checkbox" id="code-wrap-1iwdeg6-12" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-12" title="コードの折り返しを切り替える"></label><figcaption><span>v8_rewards_report.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@plv</span>8ify_param &#123;integer&#125; minMonthlyPurchases</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@plv</span>8ify_param &#123;numeric&#125; minDollarAmountPurchased</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@plv</span>8ify_param &#123;date&#125; today</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@plv</span>8ify_return &#123;setof public.customer&#125;</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@plv</span>8ify_volatility VOLATILE</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">v8RewardsReport</span>(<span class="params"></span></span><br><span class="line"><span class="params">  <span class="attr">minMonthlyPurchases</span>: <span class="built_in">number</span>,</span></span><br><span class="line"><span class="params">  <span class="attr">minDollarAmountPurchased</span>: <span class="built_in">number</span>,</span></span><br><span class="line"><span class="params">  <span class="attr">today</span>: <span class="title class_">Date</span>,</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">void</span> &#123;</span><br><span class="line">  <span class="comment">// Some sanity checks...</span></span><br><span class="line">  <span class="keyword">if</span> (minMonthlyPurchases === <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;Minimum monthly purchases parameter must be &gt; 0&#x27;</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> (minDollarAmountPurchased === <span class="number">0.00</span>) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;Minimum monthly dollar amount purchased parameter must be &gt; $0.00&#x27;</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> lastMonthStart = plv8.<span class="title function_">execute</span>(<span class="string">`</span></span><br><span class="line"><span class="string">    SELECT DATE_TRUNC(&#x27;month&#x27;, $1::timestamp - &#x27;3 month&#x27;::interval) AS val</span></span><br><span class="line"><span class="string">  `</span>, [today])[<span class="number">0</span>].<span class="property">val</span>;</span><br><span class="line">  <span class="keyword">const</span> lastMonthEnd = plv8.<span class="title function_">execute</span>(<span class="string">`</span></span><br><span class="line"><span class="string">    SELECT LAST_DAY($1) AS val</span></span><br><span class="line"><span class="string">  `</span>, [lastMonthStart])[<span class="number">0</span>].<span class="property">val</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Create a temporary storage area for Customer IDs.</span></span><br><span class="line">  plv8.<span class="title function_">execute</span>(<span class="string">`</span></span><br><span class="line"><span class="string">    CREATE TEMPORARY TABLE tmpCustomer (customer_id INTEGER NOT NULL PRIMARY KEY)</span></span><br><span class="line"><span class="string">  `</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Find all customers meeting the monthly purchase requirements</span></span><br><span class="line">  plv8.<span class="title function_">execute</span>(<span class="string">`</span></span><br><span class="line"><span class="string">    INSERT INTO tmpCustomer (customer_id)</span></span><br><span class="line"><span class="string">    SELECT p.customer_id</span></span><br><span class="line"><span class="string">    FROM payment AS p</span></span><br><span class="line"><span class="string">    WHERE DATE(p.payment_date) BETWEEN $1 AND $2</span></span><br><span class="line"><span class="string">    GROUP BY customer_id</span></span><br><span class="line"><span class="string">    HAVING SUM(p.amount) &gt; <span class="subst">$&#123;minDollarAmountPurchased&#125;</span></span></span><br><span class="line"><span class="string">    AND COUNT(customer_id) &gt; <span class="subst">$&#123;minMonthlyPurchases&#125;</span></span></span><br><span class="line"><span class="string">  `</span>, [lastMonthStart, lastMonthEnd]);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Output ALL customer information of matching rewardees.</span></span><br><span class="line">  <span class="comment">// Customize output as needed.</span></span><br><span class="line">  <span class="keyword">let</span> plan!: <span class="title class_">PreparedPlan</span>, cursor!: <span class="title class_">Cursor</span>;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    plan = plv8.<span class="title function_">prepare</span>(<span class="string">`</span></span><br><span class="line"><span class="string">      SELECT c.* FROM tmpCustomer AS t INNER JOIN customer AS c ON t.customer_id = c.customer_id</span></span><br><span class="line"><span class="string">    `</span>);</span><br><span class="line">    cursor = plan.<span class="title function_">cursor</span>();</span><br><span class="line">    <span class="keyword">let</span> <span class="attr">customer</span>: <span class="title class_">SQLRow</span>;</span><br><span class="line">    <span class="keyword">while</span> ((customer = cursor.<span class="title function_">fetch</span>())) &#123;</span><br><span class="line">      plv8.<span class="title function_">return_next</span>(customer);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">finally</span> &#123;</span><br><span class="line">    cursor?.<span class="title function_">close</span>();</span><br><span class="line">    plan?.<span class="title function_">free</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Clean up</span></span><br><span class="line">  plv8.<span class="title function_">execute</span>(<span class="string">&#x27;DROP TABLE tmpCustomer&#x27;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

</details>

<p>前の例と同様に CREATE FUNCTION SQL を生成し関数を作成してSQLを実行しました。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1iwdeg6-13" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-13" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">pagila=&gt; <span class="keyword">select</span> customer_id, first_name</span><br><span class="line">pagila-&gt; from v8_rewards_report(10, 40, <span class="string">&#x27;2022-06-22&#x27;</span>);</span><br><span class="line"> customer_id | first_name</span><br><span class="line">-------------+------------</span><br><span class="line">         147 | JOANNE</span><br><span class="line">         158 | VERONICA</span><br><span class="line">         179 | DANA</span><br><span class="line">         366 | BRANDON</span><br><span class="line">         381 | BOBBY</span><br><span class="line">         410 | CURTIS</span><br><span class="line">         416 | JEFFERY</span><br><span class="line">         526 | KARL</span><br><span class="line">(8 行)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">pagila=&gt; <span class="keyword">select</span> customer_id, first_name</span><br><span class="line">pagila-&gt; from rewards_report(10, 40, <span class="string">&#x27;2022-06-22&#x27;</span>);</span><br><span class="line"> customer_id | first_name</span><br><span class="line">-------------+------------</span><br><span class="line">         147 | JOANNE</span><br><span class="line">         158 | VERONICA</span><br><span class="line">         179 | DANA</span><br><span class="line">         366 | BRANDON</span><br><span class="line">         381 | BOBBY</span><br><span class="line">         410 | CURTIS</span><br><span class="line">         416 | JEFFERY</span><br><span class="line">         526 | KARL</span><br><span class="line">(8 行)</span><br></pre></td></tr></table></figure></div>

<p>PLV8版の関数でもPL&#x2F;pgSQL版の関数と同じ結果が得られました。</p>
<h4 id="外部ライブラリの利用">外部ライブラリの利用</h4><p><code>rewards_report</code> 関数は <code>LAST_DAY</code> 関数を呼び出していますが、前の例では既存の関数をSQLで呼び出していました。PLV8関数からPLV8関数を呼び出す場合はSQL実行よりも簡単に呼び出す手段があるため、 <code>LAST_DAY</code> 関数もPLV8化してみます。この関数は日付を扱うため、日時を扱う外部ライブラリの利用も試してみます。今回はDay.jsを利用し、以下のようにTS実装しました。</p>
<div class="code-block"><figure class="highlight ts"><input type="checkbox" id="code-wrap-1iwdeg6-14" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-14" title="コードの折り返しを切り替える"></label><figcaption><span>v8_last_day2.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> dayjs <span class="keyword">from</span> <span class="string">&#x27;dayjs&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@plv</span>8ify_param &#123;timestamp with time zone&#125; date</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@plv</span>8ify_return &#123;date&#125;</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">v8LastDay2</span>(<span class="params"></span></span><br><span class="line"><span class="params">  <span class="attr">date</span>: <span class="title class_">Date</span>,</span></span><br><span class="line"><span class="params"></span>): <span class="title class_">Date</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">dayjs</span>(date).<span class="title function_">endOf</span>(<span class="string">&#x27;month&#x27;</span>).<span class="title function_">toDate</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>このTS実装から生成した CREATE FUNCTION SQL を確認すると、インポートしている <code>dayjs</code> はインライン化されることが分かります。</p>
<p>このPLV8関数版を使うように変更した <code>rewards_report</code> 関数のTS実装は以下です。 <code>LAST_DAY</code> 関数のPLV8化と同様にDay.jsを利用する変更を加えていますが、大部分は前掲の関数実装と同じなため差分だけ掲載します。</p>
<div class="code-block"><figure class="highlight diff"><input type="checkbox" id="code-wrap-1iwdeg6-15" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-15" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment">--- v8_rewards_report.ts        2024-08-29 13:01:54.774358100 +0900</span></span><br><span class="line"><span class="comment">+++ v8_rewards_report3.ts       2024-08-29 12:20:48.789519300 +0900</span></span><br><span class="line"><span class="meta">@@ -1,3 +1,7 @@</span></span><br><span class="line"><span class="addition">+import dayjs from &#x27;dayjs&#x27;;</span></span><br><span class="line"><span class="addition">+</span></span><br><span class="line"><span class="addition">+const lastDay = plv8.find_function(&#x27;v8_last_day2&#x27;);</span></span><br><span class="line"><span class="addition">+</span></span><br><span class="line"> /**</span><br><span class="line">  * @plv8ify_param &#123;integer&#125; minMonthlyPurchases</span><br><span class="line">  * @plv8ify_param &#123;numeric&#125; minDollarAmountPurchased</span><br><span class="line"><span class="meta">@@ -5,7 +9,7 @@</span></span><br><span class="line">  * @plv8ify_return &#123;setof public.customer&#125;</span><br><span class="line">  * @plv8ify_volatility VOLATILE</span><br><span class="line">  */</span><br><span class="line"><span class="deletion">-export function v8RewardsReport(</span></span><br><span class="line"><span class="addition">+export function v8RewardsReport3(</span></span><br><span class="line">   minMonthlyPurchases: number,</span><br><span class="line">   minDollarAmountPurchased: number,</span><br><span class="line">   today: Date,</span><br><span class="line"><span class="meta">@@ -18,12 +22,8 @@</span></span><br><span class="line">     throw new Error(&#x27;Minimum monthly dollar amount purchased parameter must be &gt; $0.00&#x27;);</span><br><span class="line">   &#125;</span><br><span class="line"></span><br><span class="line"><span class="deletion">-  const lastMonthStart = plv8.execute(`</span></span><br><span class="line"><span class="deletion">-    SELECT DATE_TRUNC(&#x27;month&#x27;, $1::timestamp - &#x27;3 month&#x27;::interval) AS val</span></span><br><span class="line"><span class="deletion">-  `, [today])[0].val;</span></span><br><span class="line"><span class="deletion">-  const lastMonthEnd = plv8.execute(`</span></span><br><span class="line"><span class="deletion">-    SELECT LAST_DAY($1) AS val</span></span><br><span class="line"><span class="deletion">-  `, [lastMonthStart])[0].val;</span></span><br><span class="line"><span class="addition">+  const lastMonthStart = dayjs(today).subtract(3, &#x27;month&#x27;).startOf(&#x27;month&#x27;);</span></span><br><span class="line"><span class="addition">+  const lastMonthEnd = lastDay(lastMonthStart);</span></span><br><span class="line"></span><br><span class="line">   // Create a temporary storage area for Customer IDs.</span><br><span class="line">   plv8.execute(`</span><br></pre></td></tr></table></figure></div>

<p>このTS実装から生成した CREATE FUNCTION SQL を確認すると、 <code>v8_last_day2.ts</code> と同様に <code>dayjs</code> はインライン化されることが分かります。</p>
<p>これらの関数を作成してSQLを実行しました。</p>
<details>
<summary>実行結果</summary>

<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1iwdeg6-16" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-16" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">pagila=&gt; <span class="keyword">select</span> customer_id, first_name</span><br><span class="line">pagila-&gt; from v8_rewards_report3(10, 40, <span class="string">&#x27;2022-06-22&#x27;</span>);</span><br><span class="line"> customer_id | first_name</span><br><span class="line">-------------+------------</span><br><span class="line">         147 | JOANNE</span><br><span class="line">         158 | VERONICA</span><br><span class="line">         179 | DANA</span><br><span class="line">         366 | BRANDON</span><br><span class="line">         381 | BOBBY</span><br><span class="line">         410 | CURTIS</span><br><span class="line">         416 | JEFFERY</span><br><span class="line">         526 | KARL</span><br><span class="line">(8 行)</span><br></pre></td></tr></table></figure></div>

</details>

<p>PL&#x2F;pgSQL版の関数と同じ結果が得られました。</p>
<h4 id="Unit-test">Unit test</h4><p>PostgreSQL関数を問題なくTS実装できることが確認できたので、次はTS実装のテストを試します。今回はテストフレームワークとモック作成にVitestを利用しました。シンプルな例として、サンプルDBにある <code>inventory_in_stock</code> 関数をPLV8化した以下のTS実装を使います。</p>
<div class="code-block"><figure class="highlight ts"><input type="checkbox" id="code-wrap-1iwdeg6-17" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-17" title="コードの折り返しを切り替える"></label><figcaption><span>v8_inventory_in_stock.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@plv</span>8ify_param &#123;integer&#125; inventoryId</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">v8InventoryInStock</span>(<span class="params"></span></span><br><span class="line"><span class="params">  <span class="attr">inventoryId</span>: <span class="built_in">number</span>,</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">boolean</span> &#123;</span><br><span class="line">  <span class="comment">// AN ITEM IS IN-STOCK IF THERE ARE EITHER NO ROWS IN THE rental TABLE</span></span><br><span class="line">  <span class="comment">// FOR THE ITEM OR ALL ROWS HAVE return_date POPULATED</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="attr">rentals</span>: <span class="built_in">number</span> = plv8.<span class="title function_">execute</span>(<span class="string">`</span></span><br><span class="line"><span class="string">    SELECT count(*) AS val</span></span><br><span class="line"><span class="string">    FROM rental</span></span><br><span class="line"><span class="string">    WHERE inventory_id = <span class="subst">$&#123;inventoryId&#125;</span></span></span><br><span class="line"><span class="string">  `</span>)[<span class="number">0</span>].<span class="property">val</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (rentals === <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="attr">out</span>: <span class="built_in">number</span> = plv8.<span class="title function_">execute</span>(<span class="string">`</span></span><br><span class="line"><span class="string">    SELECT COUNT(rental_id) AS val</span></span><br><span class="line"><span class="string">    FROM inventory LEFT JOIN rental USING(inventory_id)</span></span><br><span class="line"><span class="string">    WHERE inventory.inventory_id = <span class="subst">$&#123;inventoryId&#125;</span></span></span><br><span class="line"><span class="string">    AND rental.return_date IS NULL</span></span><br><span class="line"><span class="string">  `</span>)[<span class="number">0</span>].<span class="property">val</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> out &lt;= <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>CREATE FUNCTION SQL を生成し関数を作成してSQLを実行すると、PLV8版とPL&#x2F;pgSQL版で同じ結果が得られます。</p>
<details>
<summary>実行結果</summary>

<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1iwdeg6-18" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-18" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">pagila=&gt; <span class="keyword">select</span></span><br><span class="line">pagila-&gt;     iid,</span><br><span class="line">pagila-&gt;     inventory_in_stock(iid) as in_stock,</span><br><span class="line">pagila-&gt;     v8_inventory_in_stock(iid) as in_stock_v8</span><br><span class="line">pagila-&gt; from generate_series(1, 10) as iid;</span><br><span class="line"> iid | in_stock | in_stock_v8</span><br><span class="line">-----+----------+-------------</span><br><span class="line">   1 | t        | t</span><br><span class="line">   2 | t        | t</span><br><span class="line">   3 | t        | t</span><br><span class="line">   4 | t        | t</span><br><span class="line">   5 | t        | t</span><br><span class="line">   6 | f        | f</span><br><span class="line">   7 | t        | t</span><br><span class="line">   8 | t        | t</span><br><span class="line">   9 | f        | f</span><br><span class="line">  10 | t        | t</span><br><span class="line">(10 行)</span><br></pre></td></tr></table></figure></div>

</details>

<p>この <code>v8InventoryInStock</code> 関数をテストする以下のテストスクリプトを作成しました。このテストではグローバルな <code>plv8</code> オブジェクトはモックを作成してテストできるようにしました。</p>
<div class="code-block"><figure class="highlight js"><input type="checkbox" id="code-wrap-1iwdeg6-19" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-19" title="コードの折り返しを切り替える"></label><figcaption><span>v8_inventory_in_stock.test.js</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; expect, test, vi &#125; <span class="keyword">from</span> <span class="string">&#x27;vitest&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; v8InventoryInStock &#125; <span class="keyword">from</span> <span class="string">&#x27;./v8_inventory_in_stock&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title function_">test</span>(<span class="string">&#x27;貸出履歴ありだがすべて返却済みなら、在庫あり&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> plv8 = &#123;</span><br><span class="line">    <span class="attr">execute</span>: vi.<span class="title function_">fn</span>(),</span><br><span class="line">  &#125;;</span><br><span class="line">  globalThis.<span class="property">plv8</span> = plv8;</span><br><span class="line"></span><br><span class="line">  plv8.<span class="property">execute</span></span><br><span class="line">    .<span class="title function_">mockReturnValueOnce</span>(([&#123; <span class="attr">val</span>: <span class="number">1</span> &#125;]))</span><br><span class="line">    .<span class="title function_">mockReturnValueOnce</span>(([&#123; <span class="attr">val</span>: <span class="number">0</span> &#125;]));</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> inventoryId = <span class="number">1</span>;</span><br><span class="line">  <span class="title function_">expect</span>(<span class="title function_">v8InventoryInStock</span>(inventoryId)).<span class="title function_">toBe</span>(<span class="literal">true</span>);</span><br><span class="line">  <span class="title function_">expect</span>(plv8.<span class="property">execute</span>)</span><br><span class="line">    .<span class="title function_">toHaveBeenNthCalledWith</span>(<span class="number">1</span>, <span class="string">`</span></span><br><span class="line"><span class="string">    SELECT count(*) AS val</span></span><br><span class="line"><span class="string">    FROM rental</span></span><br><span class="line"><span class="string">    WHERE inventory_id = <span class="subst">$&#123;inventoryId&#125;</span></span></span><br><span class="line"><span class="string">  `</span>)</span><br><span class="line">    .<span class="title function_">toHaveBeenNthCalledWith</span>(<span class="number">2</span>, <span class="string">`</span></span><br><span class="line"><span class="string">    SELECT COUNT(rental_id) AS val</span></span><br><span class="line"><span class="string">    FROM inventory LEFT JOIN rental USING(inventory_id)</span></span><br><span class="line"><span class="string">    WHERE inventory.inventory_id = <span class="subst">$&#123;inventoryId&#125;</span></span></span><br><span class="line"><span class="string">    AND rental.return_date IS NULL</span></span><br><span class="line"><span class="string">  `</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure></div>

<p>このテストを実行すると以下の通り成功することが確認できました。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1iwdeg6-20" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1iwdeg6-20" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">✓ src/v8_inventory_in_stock.test.js (1)</span><br><span class="line">  ✓ 貸出履歴ありだがすべて返却済みなら、在庫あり</span><br><span class="line"></span><br><span class="line">Test Files  1 passed (1)</span><br><span class="line">     Tests  1 passed (1)</span><br><span class="line">  Start at  12:22:59</span><br><span class="line">  Duration  600ms (transform 151ms, setup 0ms, collect 123ms, tests 2ms, environment 0ms, prepare 146ms)</span><br></pre></td></tr></table></figure></div>

<h2 id="まとめ">まとめ</h2><p>JavaScriptでPostgreSQLの手続き型処理を実装できる拡張機能PLV8を試した結果の感想は以下です。</p>
<ul>
<li><p>良い点</p>
<ul>
<li>Node.jsのエコシステム、各種ツールが使える。エディタ、Linter、ライブラリ、テスト、など</li>
<li>フロントエンドの開発スキルが流用できる。</li>
</ul>
</li>
<li><p>懸念点</p>
<ul>
<li>PLV8ifyの場合はインポートしたモジュールがPostgreSQL関数ごとにインライン化される。<ul>
<li>関数定義が肥大化しそう。</li>
<li>モジュールの変更時はそれに依存する関数を生成&amp;作成し直す必要があるため、どのような単位でPostgreSQL関数化するかは要配慮と考えられる。</li>
</ul>
</li>
<li>今回試した範囲ではまったく性能的な懸念はなかったが、PL&#x2F;pgSQLに対して性能的にどうなのかは分からない。</li>
<li>SQL実行やPostgreSQL組み込み関数の呼び出しはPL&#x2F;pgSQLの方が簡単に書ける。</li>
</ul>
</li>
</ul>
<p>JavaScript (TypeScript) で開発できるというのは私は楽しく楽に実装できました。これは以下からくるものだと思います。</p>
<ul>
<li>エディタなどのJS開発支援機能による効率的な実装</li>
<li>Linterに指摘してもらえる安心感</li>
<li>テストしやすさからくる安心感</li>
</ul>
<p>注意すべき点はありそうなものの十分実用できそうな印象です。いざとなればPL&#x2F;pgSQLに切り替えるという選択肢も取れるので、チャンスがあれば実際のPJでも導入してみたいと思います。</p>
]]></content>
    <summary type="html">PostgreSQLで手続き型処理を実装することにがっつりと向き合うことがなかったため手続き型処理の実装言語はPL/pgSQL一択だと思いこんでいたのですが、実は複数の選択肢がありました。PL/pgSQL、PL/Tcl、PL/Perl、PL/Pythonや、サードパーティ提供も含めると多数の言語が使えるようです使えるようです。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="JavaScript" scheme="https://future-architect.github.io/tags/JavaScript/"/>
    <category term="PostgreSQL" scheme="https://future-architect.github.io/tags/PostgreSQL/"/>
    <category term="TypeScript" scheme="https://future-architect.github.io/tags/TypeScript/"/>
    <category term="Vite" scheme="https://future-architect.github.io/tags/Vite/"/>
  </entry>
  <entry>
    <title>【合格記】Google Cloud Professional Cloud Database Engineer認定資格を振り返る</title>
    <link href="https://future-architect.github.io/articles/20240730a/"/>
    <id>https://future-architect.github.io/articles/20240730a/</id>
    <published>2024-07-29T15:00:00.000Z</published>
    <updated>2024-07-29T15:00:00.000Z</updated>
    <author><name>岸下優介</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20240730a/image.png" alt="png" width="544" height="543">

<h2 id="はじめに">はじめに</h2><p>Google Cloud認定資格全冠を目指すべく、Professional Cloud Database Engineer Certification（PCDB）を受けてきました。無事に合格できたので、ざっくりとした所感を書いていきます。</p>
<p>また本試験はGoogle Cloudパートナー企業向けのバウチャーを活用して受験しました。大変感謝しております！</p>
<p>Google Cloud 認定資格関連の過去記事：</p>
<ul>
<li>【合格記】Google Cloud Professional Developer認定資格を振り返る</li>
<li>【合格体験記】Google Cloudの入門試験：Cloud Digital Leader</li>
<li>【合格記】Google Cloud Professional Cloud Security Engineer認定資格を振り返る</li>
<li>【合格記】Google Cloud Professional Data Engineer認定資格を振り返る</li>
<li>【合格記】Google Cloud Professional Machine Learning Engineer認定資格を振り返る</li>
<li>Google Cloud Professional Cloud Architectの再認定に合格しました</li>
<li>GCP Professional Cloud Network Engineer に合格しました</li>
<li>GCP Associate Cloud Engineer 合格記</li>
</ul>
<h2 id="試験と出題範囲">試験と出題範囲</h2><p>公式の出題範囲と、実際に自分が受けた際の所感は以下になります。</p>
<h3 id="スケーラブルで可用性の高いクラウド-データベース-ソリューションを設計する">スケーラブルで可用性の高いクラウド データベース ソリューションを設計する</h3><ul>
<li>Google CloudのDBサービスとその特性を理解する<ul>
<li>Relational DB<ul>
<li>Cloud SQL</li>
<li>Spanner</li>
<li>AlloyDB</li>
</ul>
</li>
<li>NoSQL<ul>
<li>Firestore</li>
<li>Bigtable</li>
</ul>
</li>
<li>インメモリ<ul>
<li>Memorystore</li>
</ul>
</li>
<li>その他<ul>
<li>MongoDB Atlas</li>
<li>Google Cloud Partner Services</li>
</ul>
</li>
</ul>
</li>
<li>シチュエーション別でDBサービスを選ぶ<ul>
<li>オープンソースのDBサービスを利用したい場合は？</li>
<li>99.99％の可用性を求められる場合は？</li>
<li>グローバル展開が予測されている場合は？</li>
<li>スケーリングは要る？ 要らない？</li>
</ul>
</li>
<li>DBを構成するデータの種別でサービスを選ぶ<ul>
<li>トランザクションデータ</li>
<li>ストリーミングデータ</li>
<li>リアルタイム</li>
</ul>
</li>
</ul>
<h3 id="データ-ソリューションを移行する">データ ソリューションを移行する</h3><ul>
<li>Database Migration Serviceマジ便利<ul>
<li>他クラウドのDBやオンプレからGoogle Cloudへの移行が容易に可能</li>
<li>ダウンタイムを最小限に抑えた移行を実現</li>
<li>サポートされているサービスは理解しておく<ul>
<li>Supported source and destination databases</li>
</ul>
</li>
</ul>
</li>
<li>OracleはBare Metal Solutionを使う<ul>
<li>但し、最近のトレンドとしてはOracle on Google Cloud</li>
</ul>
</li>
</ul>
<h3 id="複数のデータベース-ソリューションにまたがるソリューションを管理する">複数のデータベース ソリューションにまたがるソリューションを管理する</h3><ul>
<li>トラブルシューティング<ul>
<li>Query Insightsを利用したクエリのパフォーマンス分析</li>
<li>ディスクの拡張</li>
<li>DBにホットスポットが発生したときの対策</li>
</ul>
</li>
<li>データベースのセキュリティ<ul>
<li>Cloud SQL Auth Proxyを使う</li>
<li>IAMは最小権限の法則に従う</li>
<li>Private IPを利用し、VPC内で接続を完結させる</li>
<li>VPC Service Controlsを利用して接続経路を絞る</li>
</ul>
</li>
</ul>
<h3 id="Google-Cloud-にスケーラブルで可用性の高いデータベースをデプロイする">Google Cloud にスケーラブルで可用性の高いデータベースをデプロイする</h3><ul>
<li>ディザスタリカバリ戦略に沿った構成を組む<ul>
<li>復旧時間目標（RTO）</li>
<li>普及時点目標（RPO）</li>
</ul>
</li>
<li>DBとの接続方法<ul>
<li>IAMで制御したい場合は？</li>
<li>Cloud RunなどサーバレスとCloud SQLを接続する場合は？</li>
</ul>
</li>
<li>IaCツールを利用した開発環境の整備<ul>
<li>Terraformなどを利用して一律のフォーマットでデプロイすることで構成のミスを防ぐ</li>
</ul>
</li>
<li>高可用性を実現する構成<ul>
<li>プライマリインスタンスとリードレプリカのzone構成</li>
<li>フェイルオーバー時にリードレプリカをプライマリへ昇格させる</li>
</ul>
</li>
</ul>
<h3 id="全体的な所感">全体的な所感</h3><p>正直のところ、認定資格の中ではPCDBが最も容易な試験だったと感じました。理由としては、</p>
<ul>
<li>DBというサービスに絞っているため、他試験よりも出題されるサービスの範囲が狭い</li>
<li>サービス毎で用途を明確にしているため、問題で提示されるシチュエーション（データ種別、可用性、サービスの展開先など）から判断し易い</li>
<li>Google Cloudの知識が無くても、DBに関する見識があれば解けてしまう問題もちらほら存在する</li>
</ul>
<p>などが挙げられると思います。<br>もし既にProfessional Cloud Architectを合格されていて、ある程度Google CloudのDBサービスの見識をお持ちの場合、次に受ける試験としてはPCDBが良いのではないかなと思いました。</p>
<p>ただし、現状では試験が<strong>英語での受験</strong>しか選択できないことが少々ネックかもしれません。</p>
<h2 id="勉強方法">勉強方法</h2><p>どの試験もそうですが、4～5択から正解を選ぶ選択式試験なので模擬試験などで場数をこなすことが大事だと思います。</p>
<ul>
<li>Google Cloud公式提供の模擬試験を受験する</li>
<li>Udemyなどのオンライン学習サービスで模擬試験を購入し勉強する<ul>
<li>https://www.udemy.com/course/2024google-cloud-professional-cloud-database-engineer</li>
</ul>
</li>
</ul>
<p>正解の選択肢を暗記するというよりは、間違った問題に対してドキュメントを読むことが大切です。なぜその選択肢が正解なのかを理解することで他の問題にも応用できるようになっていきます。</p>
<blockquote>
<p>Cloud Database Engineer 試験を受験するには、Google Cloud データベース ソリューションの実務経験が 2 年以上あることが推奨されます。構築を開始するには、一部のプロダクトの Google Cloud の無料枠を最大で 1 か月間、無料でお試しいただけます。</p>
</blockquote>
<p>公式にも記載されている通りGoogle Cloudでは無料枠があるので、場合によっては自分で実際に環境を構築して触ってみるとより理解が深まります。</p>
<p>自分もGoogle CloudのDB関連では以下の記事を執筆しておりますので、是非参考にしてみてください。</p>
<p>VPC外からCloud SQL Auth Proxyを利用したPrivate IP Cloud SQLへの接続</p>
<h2 id="まとめ">まとめ</h2><p>PCDBを受けた際の所感を記載させて頂きました。資格試験は目標を立てられるためモチベが維持しやすく、Google Cloud Professional認定資格に限っては合格後のグッズプレゼント特典もあるので、良い感じにニンジンを吊るされた状態で完走できて最高です。</p>
<p>またこれは個人的な感想ですが、<strong>自分がPCDBを受験した問題の範囲</strong>ではAlloyDBの問題が一問も無かったり、英語しか試験が用意されてなかったりと全体的にアップデートされていない感じがあります🫠<br>もしかするとそのうちアップデートが入るかもなので、もしこの記事を参考にされる場合は早めの受験をおススメします！</p>
]]></content>
    <summary type="html">Google Cloud認定資格全冠を目指すべく、Professional Cloud Database Engineer Certification（PCDB）を受けてきました。無事に合格することができたので、本記事ではざっくりとした所感を書いていきたいと思います。</summary>
    <category term="DB" scheme="https://future-architect.github.io/categories/DB/"/>
    <category term="GoogleCloud" scheme="https://future-architect.github.io/tags/GoogleCloud/"/>
    <category term="PCDB" scheme="https://future-architect.github.io/tags/PCDB/"/>
    <category term="合格記" scheme="https://future-architect.github.io/tags/%E5%90%88%E6%A0%BC%E8%A8%98/"/>
  </entry>
</feed>
