<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://masahikosawada.github.io/ja/feed.xml" rel="self" type="application/atom+xml" /><link href="https://masahikosawada.github.io/ja/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-08-18T16:03:21+09:00</updated><id>https://masahikosawada.github.io/feed.xml</id><title type="html">MasahikoSawada</title><subtitle>PostgreSQLメジャーコントリビュータ／コミッタ Masahiko Sawada のブログ。 VACUUM、MVCC、論理レプリケーション、トランザクション管理、インデックスなど PostgreSQL の内部構造とソースコードを日本語で解説しています。</subtitle><author><name>Masahiko Sawada</name></author><entry xml:lang="ja"><title type="html">PostgreSQLのWALを対話的に読むTUIツール(pg_walview)を作ってみた</title><link href="https://masahikosawada.github.io/ja/2026/08/17/pg_walview/" rel="alternate" type="text/html" title="PostgreSQLのWALを対話的に読むTUIツール(pg_walview)を作ってみた" /><published>2026-08-17T00:00:00+09:00</published><updated>2026-08-17T00:00:00+09:00</updated><id>https://masahikosawada.github.io/2026/08/17/pg_walview</id><content type="html" xml:base="https://masahikosawada.github.io/2026/08/17/pg_walview/"><![CDATA[<p>以前、<a href="https://github.com/MasahikoSawada/pg_walview">pg_walview</a>という、PostgreSQLのWAL（Write-Ahead Log）ファイルを対話的に読むためのTUIツールを作りました。<code class="language-plaintext highlighter-rouge">pg_waldump</code>のように出力を一度に吐き出すのではなく、ターミナル上でカーソルを動かしながらWALレコードを1つずつ眺めていくツールです。Rustで書いています。</p>

<video src="/images/2026-08-18/pg_walview-demo.mp4" poster="/images/2026-08-18/pg_walview-demo-poster.jpg" width="1200" height="600" autoplay="" muted="" loop="" playsinline="" preload="none" style="max-width:100%;height:auto" aria-label="pg_walviewでWALファイルを開き、カーソルを動かしながらWALレコードの詳細とHEXダンプを表示している様子">
</video>

<p><code class="language-plaintext highlighter-rouge">pg_waldump</code>は<code class="language-plaintext highlighter-rouge">-x</code>でXID、<code class="language-plaintext highlighter-rouge">-r</code>でリソースマネージャ、<code class="language-plaintext highlighter-rouge">-R</code>や<code class="language-plaintext highlighter-rouge">-B</code>でリレーションやブロック、という具合に絞り込みは一通りできます。ですが、絞り込むと今度はその前後で何が起きていたかが見えなくなります。<code class="language-plaintext highlighter-rouge">-x</code>を付けて実行して、やっぱり周りが気になって<code class="language-plaintext highlighter-rouge">-x</code>を外して実行して、というのを何度も繰り返していました。だったら全部読み込んでおいて、その場で視点だけ切り替えられればいいのでは、と思ったのがきっかけです。</p>

<h2 id="画面の構成">画面の構成</h2>

<p>画面は3つのペインに分かれていて、<code class="language-plaintext highlighter-rouge">Tab</code>でフォーカスを移動します。</p>

<h3 id="wal-records左">WAL records（左）</h3>

<p>LSN、XID、レコード長、FPIの有無、リソースマネージャ、説明の一覧です。</p>

<p>一番やりたかったのが左端のグラフ線です。カーソルを合わせたレコードと同じXIDを持つレコードが、線で繋がって表示されます。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    0/100 13840   171  Heap      UPDATE
 ┏━ 0/100 13841  8135  Heap      LOCK        ← このXIDの最初のレコード
 ┃  0/100 13839  8135  Heap      LOCK
 ┣━ 0/100 13841   171  Heap      UPDATE      ← カーソル位置
 ┃  0/100 13840  8135  Heap      LOCK
 ┃  0/100 13839   171  Heap      UPDATE
 ┣━ 0/100 13841    64  Btree     INSERT_LEAF
 ┗━ 0/100 13841    34  Transact  COMMIT      ← このXIDの最後のレコード
</code></pre></div></div>

<p>他のトランザクションのレコードは間に挟まったまま残ります。なので、絞り込まずに特定のトランザクションだけを目で追えます。<code class="language-plaintext highlighter-rouge">s</code>と<code class="language-plaintext highlighter-rouge">r</code>で同じXIDの次／前のレコードにジャンプすることもできます<sup id="fnref:xid" role="doc-noteref"><a href="#fn:xid" class="footnote" rel="footnote">1</a></sup>。</p>

<p>線の色は、そのXIDの最後のレコードを見て決めています。要はそのトランザクションがどう終わったかです。</p>

<table>
  <thead>
    <tr>
      <th>状態</th>
      <th>判定条件</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>コミット済み</td>
      <td><code class="language-plaintext highlighter-rouge">COMMIT</code> または <code class="language-plaintext highlighter-rouge">COMMIT_PREPARED</code> で終わっている</td>
    </tr>
    <tr>
      <td>アボート</td>
      <td><code class="language-plaintext highlighter-rouge">ABORT</code> または <code class="language-plaintext highlighter-rouge">ABORT_PREPARED</code> で終わっている</td>
    </tr>
    <tr>
      <td>保留中</td>
      <td><code class="language-plaintext highlighter-rouge">PREPARE</code> など、コミットでもアボートでもないTransactionレコードで終わっている</td>
    </tr>
    <tr>
      <td>未完結</td>
      <td>Transactionレコードで終わっていない</td>
    </tr>
  </tbody>
</table>

<p>最後の「未完結」が地味に便利です。そのトランザクションがこのWALファイル内では完結していない、つまり続きが次のセグメントにあるということなので、次のファイルを開くべきかどうかがわかります。</p>

<p>ちなみに使っているのはANSIの16色だけです。実際にどう見えるかは端末のテーマ次第。<code class="language-plaintext highlighter-rouge">NO_COLOR</code>を設定すれば色は落ちます。</p>

<h3 id="details右上">DETAILS（右上）</h3>

<p>選択中のレコードの詳細です。<code class="language-plaintext highlighter-rouge">XLogRecord</code>のヘッダ、ブロック参照（<code class="language-plaintext highlighter-rouge">RelFileLocator</code>、フォーク、ブロック番号、フラグ）、そしてリソースマネージャごとにデコードした構造体のフィールドが並びます。</p>

<p>アコーディオンになっていて、<code class="language-plaintext highlighter-rouge">Enter</code>でブロックごとの詳細を開閉できます。Heapの<code class="language-plaintext highlighter-rouge">UPDATE</code>なら<code class="language-plaintext highlighter-rouge">xl_heap_header</code>の<code class="language-plaintext highlighter-rouge">t_infomask</code>や<code class="language-plaintext highlighter-rouge">t_infomask2</code>をフラグ名に展開して表示します。<code class="language-plaintext highlighter-rouge">HEAP_XMAX_INVALID</code>が立っているかどうか、みたいな確認をその場でできるので便利です。</p>

<h3 id="hex-dump右下">HEX DUMP（右下）</h3>

<p>WALセグメント全体のバイト列です。各行はLSNとファイルオフセットの両方で位置を示すので、レコードがどのページのどこに載っているかを見ながら読めます。</p>

<p>色が付くのは選択中のレコードのバイトだけです。しかも<code class="language-plaintext highlighter-rouge">XLogRecord</code>のヘッダ、ディスクリプタ、FPI、ブロックデータ、メインデータ、と構造ごとに色が分かれます。ページ境界をまたぐレコードは、実際にファイル上で占めている複数の断片として色が付きます。間に挟まるページヘッダはレコードの一部ではないので、そこには色を付けていません。</p>

<p>DETAILSでカーソルを合わせている項目は、ダンプ側でも強調されます。なのでアコーディオンがそのままバイト列のナビゲータになります。デコード結果と生バイトを並べて見られるのは、<code class="language-plaintext highlighter-rouge">*desc.c</code>相当の実装<sup id="fnref:desc" role="doc-noteref"><a href="#fn:desc" class="footnote" rel="footnote">2</a></sup>を書いているときのデバッグにも効きました。</p>

<h2 id="使い方">使い方</h2>

<p><code class="language-plaintext highlighter-rouge">pg_config</code>にパスが通っていればそのままビルドできます。ビルド時にPostgreSQL本体のヘッダから<code class="language-plaintext highlighter-rouge">XLogRecord</code>などの定義を取り込んでいるので、ヘッダが必要です<sup id="fnref:version" role="doc-noteref"><a href="#fn:version" class="footnote" rel="footnote">3</a></sup>。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/MasahikoSawada/pg_walview.git
<span class="nb">cd </span>pg_walview
cargo build <span class="nt">--release</span>

<span class="c"># PostgreSQLを独自の場所にインストールしている場合</span>
<span class="nv">PG_INCLUDE_DIR</span><span class="o">=</span>/path/to/pgsql/include/server cargo build <span class="nt">--release</span>
</code></pre></div></div>

<p>あとはWALファイルのパスを渡すだけです。サーバに接続するわけではないので、稼働中のインスタンスでなくても、コピーしてきたWALファイルさえあれば読めます。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pg_walview /path/to/pg_wal/000000010000000000000001
</code></pre></div></div>

<p>主なキーバインドはこちら。</p>

<table>
  <thead>
    <tr>
      <th>キー</th>
      <th>動作</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">j</code> / <code class="language-plaintext highlighter-rouge">k</code></td>
      <td>次／前のレコードへ移動</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">g</code> / <code class="language-plaintext highlighter-rouge">G</code></td>
      <td>最初／最後のレコードへ移動</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">s</code> / <code class="language-plaintext highlighter-rouge">r</code></td>
      <td>同じXIDの次／前のレコードへジャンプ</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Space</code> / <code class="language-plaintext highlighter-rouge">-</code></td>
      <td>ページ送り／ページ戻し</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Tab</code></td>
      <td>ペインの切り替え</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">q</code></td>
      <td>終了</td>
    </tr>
  </tbody>
</table>

<h2 id="制限">制限</h2>

<ul>
  <li>一度に1つのセグメントファイルしか開けません。トランザクションがセグメントをまたぐと追いきれません。グラフ線が「未完結」になるのは、この制限の裏返しでもあります</li>
  <li>ファイル内の全レコードをメモリに読み込みます。セグメントは既定で16MBなので今のところ困っていませんが、<code class="language-plaintext highlighter-rouge">--wal-segsize</code>を大きくしている環境では効いてくるかもしれません。なおセグメントサイズ自体はロングページヘッダから読むので、非デフォルトでも読めます</li>
  <li>リソースマネージャごとのデコーダが揃っていません。Heap、Heap2、Btree、Transaction、XLOGあたりは書きましたが、GiST、GIN、SP-GiSTなどはまだmain dataのバイト数を表示するだけです</li>
  <li>圧縮されたFPI（Full Page Image）を展開できません。圧縮方式の表示まではしますが、中身までは見られません</li>
</ul>

<h2 id="おわりに">おわりに</h2>

<p>WALの中身を見る手段としては<code class="language-plaintext highlighter-rouge">pg_waldump</code>も<a href="https://www.postgresql.org/docs/18/pgwalinspect.html">pg_walinspect</a>もありますし、機能面では当然そちらのほうが揃っています。pg_walviewは「対話的に動き回れる」という一点だけに絞ったツールです。</p>

<p>自分の場合、ロジカルレプリケーション関連のデバッグで、このトランザクションはどこでコミットされたのか、このレコードとこのレコードの間に何が挟まっているのか、といったことを追う機会が多くあります。その用途では作ってよかったと思っています。</p>

<p>ソースは<a href="https://github.com/MasahikoSawada/pg_walview">GitHub</a>に置いてあります。MITライセンスです。ContributionもWelcomeです！</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:xid" role="doc-endnote">
      <p>ただしXIDが0、1、2のときはジャンプを無効にしています。それぞれ<code class="language-plaintext highlighter-rouge">InvalidTransactionId</code>、<code class="language-plaintext highlighter-rouge">BootstrapTransactionId</code>、<code class="language-plaintext highlighter-rouge">FrozenTransactionId</code>ですが、XIDを持たないレコードは大量にあるので、繋いでも意味がないためです。 <a href="#fnref:xid" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:desc" role="doc-endnote">
      <p>リソースマネージャごとのデコード部分は、PostgreSQL本体の<code class="language-plaintext highlighter-rouge">src/backend/access/rmgrdesc/</code>にある<code class="language-plaintext highlighter-rouge">*desc.c</code>と同じ役割です。本体の構造に寄せておいたほうが、あとで追従しやすいはず、という判断でそうしています。 <a href="#fnref:desc" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:version" role="doc-endnote">
      <p>なので、ビルドしたヘッダのバージョンのWALしか読めません。別バージョンのセグメントを開いた場合は、誤読せずにページマジックの不一致としてエラーになります。<code class="language-plaintext highlighter-rouge">--version</code>を付けると、そのバイナリがどのPostgreSQL向けにビルドされたものかを表示します。 <a href="#fnref:version" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Masahiko Sawada</name></author><category term="PostgreSQL" /><category term="WAL" /><category term="Rust" /><summary type="html"><![CDATA[PostgreSQLのWALファイルを対話的に読むためのTUIツール pg_walview を作りました。 同じXIDのレコードをグラフ線で繋いで表示するので、絞り込まずに特定のトランザクションを 目で追えます。画面の構成と使い方を紹介します。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://masahikosawada.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://masahikosawada.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="ja"><title type="html">テキサス大学オースティン校のオンラインCS修士課程に通い始めた話</title><link href="https://masahikosawada.github.io/ja/2026/01/13/UT_Austin_MSCS/" rel="alternate" type="text/html" title="テキサス大学オースティン校のオンラインCS修士課程に通い始めた話" /><published>2026-01-13T00:00:00+09:00</published><updated>2026-01-13T00:00:00+09:00</updated><id>https://masahikosawada.github.io/2026/01/13/UT_Austin_MSCS</id><content type="html" xml:base="https://masahikosawada.github.io/2026/01/13/UT_Austin_MSCS/"><![CDATA[<p>qoonelcodeさんによる<a href="https://note.com/ykmj/n/n3bc7a0388d07?sub_rt=share_pb">テキサス大学オースティン校(UT Austin)のコンピュータ修士課程に進学した話</a>を読んで、同窓生がいた嬉しさと共に、自分のケースも共有することでなにか役に立つかなと思ったので書いてみます。</p>

<p>まずは自分の簡単なバックグランドから：</p>

<ul>
  <li>10年以上前に日本の大学（理系）を卒業</li>
  <li>ソフトウェアエンジニアとして10年以上の経験あり</li>
  <li>アメリカ在住2年目</li>
</ul>

<h2 id="ut-austinのmscsを選んだ理由">UT AustinのMSCSを選んだ理由</h2>

<p>学部生時代は、大学院の選択肢は自分の中に全くありませんでした。大学院に進んで勉強したことは当時の自分にはありませんでしたし、「第一希望の就職先から内定をもらったし早く働き始めたい」と考えていたからです。</p>

<p>就職してしばらくした頃に、オンラインでアメリカの大学院に通うことができると知り興味を持ちました。ですが、英語が壊滅的にできなかったため(大学卒業時に受けたTOEICは300点でした)、TOEFLの基準を満たすための準備が大変で、モチベーションが上がってはTOEFL対策をし、でも途中で他のことに興味が出てきて中断する、を何年も繰り返していました。そうこうしているうちに、12年以上のデータベース一本のキャリアが出来上がっていました。データベースという一つの強みを持てたのと同時に、他の分野も他の分野も学んでみたいという気持ちが強くなってきました。キャリアの幅を広げるきっかけになると良いなとも思いましたし、色々な方向に知見を一度広げてからデータベース業界に戻ってくることも自分を成長させるうえで魅力的な選択肢だと思いました。</p>

<p>様々な学校を検討しましたが、私が重視した点は以下の通りです：</p>

<ul>
  <li>理論や数学がしっかり学べる
    <ul>
      <li>自分に全く知見がないAIの分野を理論から学べる所も◯。</li>
    </ul>
  </li>
  <li>学費が安い
    <ul>
      <li>全部で$10,000なのはアメリカの大学としては格安。</li>
    </ul>
  </li>
  <li>オンラインコースがある
    <ul>
      <li>通学することも検討したのですが、仕事しながらは無理だと考えました。</li>
    </ul>
  </li>
</ul>

<h2 id="出願準備成績英語sop">出願準備（成績、英語、SOP）</h2>

<h3 id="gpa">GPA</h3>

<p>学部生時代の成績は専門科目であれば平均でGPA 3.9くらい(全体で3.7くらい)だったのでクリアしていました。</p>

<p>一方で、Algorithms and Complexity (CS331)に相当する科目は履修していなかったり、学部生時代に履修した科目では微妙にカバーしきれていない点は懸念でした。しかし、PostgreSQLの開発において、Vacuumや論理レプリケーションの最適化をする際に、色々なデータ構造やアルゴリズムの比較を行った実績を具体的に記述することで、「アカデミックな単位はないが、実務レベルで同等の知識はある」とアピールしました。</p>

<h3 id="英語">英語</h3>

<p>TOEFL対策は色々試したのですがどれも続かず、その間にTOEFLの試験内容も変わっていってしまいとても苦戦しました。未だに自分にあった学習方法やリソースが見つけられていません。自分の実力を測ってみようと久しぶりにTOEFLを受けてみたら基準点を超えていました。実務で英語を使っていたおかげで、自分でも気づかないうちに力がついていたのかもしれません。また、TOEFLの基準点は79点以上(またはIELTS 6.5以上)なので、他のCSオンライン修士コースよりも低めです。</p>

<p>業務では英語を使いながら少しずつ上達しています。まずは自分の今の実力を知ろうと、久しぶりにTOEFLを受けてみたら運よく基準点を超えていました。</p>

<h3 id="statement-of-purpose-sop">Statement of Purpose (SoP)</h3>

<p>ChatGPTを壁打ち相手に使いながら、これまでの実績をアピールしつつ、なぜUT Austinに入りたいのか、そしてUT Austinで得る経験を将来にどうつなげたいのかを書きました。自分の場合は、PostgreSQLにAdaptive Radix Treeを実装した経験を通してデータ構造や仕組みを理論から理解することの重要性を感じた事、そして、経験則だけでなく、体系的なコンピュータサイエンス（CS）の理論を学ぶことで、アカデミアの研究と産業界の実装を両方がわかる人になりたい（アカデミアにも還元していきたい）、といった旨を書きました。</p>

<h2 id="最後に">最後に</h2>

<p>1月から始まった春学期では、以前から取りたかった理論系の科目「Automated Logical Reasoning」を履修しています。この授業をやりきった後、データベースの見え方が少し変わったり、何か新しいアイデアが生まれたりするかもしれないと思うと、今からとても楽しみです。</p>

<p>英語へのコンプレックスや、10年以上のブランク、そして仕事・育児との両立など、不安要素は数え切れないほどありましたが、一歩踏み出してみて本当によかったと感じています。まだ始まったばかりですが、「理論を学びたい」という初心を忘れず、卒業目指して地道に頑張っていこうと思います。興味があればぜひ海外大学院のオンラインコースに出願してみてください。また、質問があればDMください。</p>]]></content><author><name>Masahiko Sawada</name></author><category term="Diary" /><summary type="html"><![CDATA[テキサス大学オースティン校（UT Austin）のオンラインCS修士課程（MSCSO）に社会人エンジニアとして入学した話です。出願の経緯、費用、働きながら通うことにした理由をまとめています。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://masahikosawada.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://masahikosawada.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="ja"><title type="html">2025年振り返り</title><link href="https://masahikosawada.github.io/ja/2025/12/30/2025/" rel="alternate" type="text/html" title="2025年振り返り" /><published>2025-12-30T00:00:00+09:00</published><updated>2025-12-30T00:00:00+09:00</updated><id>https://masahikosawada.github.io/2025/12/30/2025</id><content type="html" xml:base="https://masahikosawada.github.io/2025/12/30/2025/"><![CDATA[<h2 id="postgresql">PostgreSQL</h2>

<p>2025年9月にリリースされたPostgreSQL 18も、現在開発中のPostgreSQL 19もLogical Replication関連の開発がメインだった。自分が開発した機能では、<code class="language-plaintext highlighter-rouge">wal_level</code>の<code class="language-plaintext highlighter-rouge">replica</code>と<code class="language-plaintext highlighter-rouge">logical</code>を再起動なしで自動的に切り替えるようになる機能の開発に一番時間を割いたと思う(<a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=67c20979ce7">67c20979ce7</a>)。他にも、<code class="language-plaintext highlighter-rouge">update_deleted</code>コンフリクトを確実に検出するための機能(<a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=228c3708685">228c3708685</a>)や、Logical Replicationにシーケンスを対応させる機能(<a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=5509055d695">5509055d695</a>、<a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=f0b3573c3aa">f0b3573c3aa</a>、<a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=96b37849734">96b37849734</a>)はレビュアーとして参画した。</p>

<p>あとは、CPUアーキテクチャによって<code class="language-plaintext highlighter-rouge">char</code>の符号が異なることが原因でGINインデックスのスキャン結果が変わってしまう問題を直せたのも良かったし(<a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=dfd8e6c73ee">dfd8e6c73ee</a>, <a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=1aab6805919">1aab6805919</a>, <a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=a8238f87f98">a8238f87f98</a>, <a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=30666d1857d">30666d1857d</a>, <a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=44fe30fdab6">44fe30fdab6</a>)、CVEのバグ修正のレビューに参加できたのも良い経験となった<a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=627acc3caa7">627acc3caa7</a>。</p>

<p>2025年では62件コミットできたようだ:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git log <span class="nt">--author</span> <span class="s2">"Masahiko Sawada"</span> <span class="nt">--since</span><span class="o">=</span>2025-01-01 <span class="nt">--oneline</span> | <span class="nb">wc</span> <span class="nt">-l</span>
62
</code></pre></div></div>

<p>来年は100件以上を目指そう。</p>

<h2 id="pgconfdev-2025">PGConf.dev 2025</h2>

<p>PGConf.dev 2025では「The Present and Future of VACUUM of PostgreSQL」という発表をした(<a href="https://www.pgevents.ca/events/pgconfdev2025/sessions/session/281/slides/85/The_Present_and_Future_of_Vacuum_in_PostgreSQL.pdf">スライド</a>、<a href="https://www.youtube.com/watch?v=kRMQlz7JW4U">動画</a>)。また、Advanced Patch Feedback Sessionというセッションにも参加して、対面でパッチについて議論しながら、開発が先に進めるようにコミッタ視点でアドバイスをする、ということもしてきた。その他にも、Code Of Conduct Committeeのメンバに入れてもらえたり、Lightning Talksの運営をやったりと、運営側としても参加できたカンファレンスだった。</p>

<h2 id="メンター">メンター</h2>

<p>PostgreSQLコミュニティでは、<a href="https://www.postgresql.org/message-id/CA+TgmoZfS_rYU_OjOFFxhbqNS5FyoX5dv1MU7-qE_SoxRzfWLw@mail.gmail.com">hacker mentoring program</a>というのがあり、PostgreSQL Committerが希望者に付いて一緒に開発したりアドバイスをする取り組みがある。今年は自分もCommitterとして参加して、1人メンティーを持つことになった。</p>

<p>また、PostgreSQLの開発に興味があるけどやり方が分からなず参加できない、という人のために個人的になにかできることはないかと考えXで募集してみた。</p>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">PostgreSQLのLogical Replicationの改善に興味がある人いないかな。簡単なバックログもあるので未経験でもアドバイスしながら一緒にPostgreSQL開発を経験できます。</p>&mdash; Masahiko Sawada (@masahiko_sawada) <a href="https://twitter.com/masahiko_sawada/status/1978179819020972219?ref_src=twsrc%5Etfw">October 14, 2025</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<p>すると、9人も興味を示していただき、1人1時間ほどのハンズオンを通して、PostgreSQL開発コミュニティの事やソースコードの構成など説明したり、私の方で用意したパッチネタについて議論するなどをした。参加いただいた方々の満足度はわからないが、そのハンズオン以降、2名の方がめでたくPostgreSQL Contributorとなった(リリースノートに名前が載ります)。その後、日本以外の方からも連絡をもらったりしたのですが、中々自分の時間が取れず現在は一旦休止しています。</p>

<h2 id="rust">Rust</h2>

<p>Rustの勉強を始めた。並行してRust製のOSSを使うことも始めたいな色々と探していたらRust製でbash/POSIX互換を謳っている<a href="https://github.com/reubeno/brush">brush</a>という良さげなシェルがあったので、これを使い始めた。Tab補完周りでバグを見つけたのでIssueと<a href="https://github.com/reubeno/brush/pull/870">PR</a>を出したらめでたくMergeしてもらい、Rust製OSSへのContributionを経験できた。他にもいくつか見つけた問題があるのでこれからもContributionを続けていきたい。</p>

<p>あとは、Rustのコンパイラ(rustc)にも興味があり、ソースコードや関連する資料を見始めた。</p>

<h2 id="ut-austin-mscs">UT Austin MSCS</h2>

<p>2025年9月からテキサス大学(UT Austin)のCS修士課程(オンライン)を始めてみました。オンラインCS修士課程だと一番有名なのはジョージア工科大学のOMSCSですが、UT Austinでも比較的低価格で受けることができます。CS修士課程ではあるのですが、どちらからというと理論や数学系の勉強をしたかったのでUT Austinのコースを取りました。実際やってみると体力的に1学期1授業が限界な感じ。</p>

<h2 id="アメリカでの生活">アメリカでの生活</h2>

<p>2024年からアメリカに住み始めたので今年は(一時帰国した7月を除くと)ほぼ1年丸々アメリカにいたことになります。仕事の内容は渡米前後で変わらないので良くも悪くも変化無し。開発チームとタイムゾーンが一緒になれたのは一番良かったことの一つかもしれない。突然やってくるレイオフの波、物価高、たまにニュースで見る危ない事件などに怯えることもありますが、キャンプやったりバーベキューやったりしてなんだかんだ楽しくやっています。来年にH1-Bに切り替えられるのでちょっとは安心かも（それまでにレイオフに合わなければ）。</p>

<h2 id="2026年に向けて">2026年に向けて</h2>

<p>Logical Replication周りはまだまだ改善点が多いので引き続き開発していきます。</p>]]></content><author><name>Masahiko Sawada</name></author><category term="Diary" /><summary type="html"><![CDATA[2025年の振り返りです。PostgreSQL 18と19でのLogical Replication関連の開発、wal_levelの自動切り替え機能、GINインデックスのバグ修正など、1年間の活動をまとめています。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://masahikosawada.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://masahikosawada.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="ja"><title type="html">論理レプリケーションでのレプリケーションラグの原因を調べてみた</title><link href="https://masahikosawada.github.io/ja/2025/12/17/Logical-Replication-Lags/" rel="alternate" type="text/html" title="論理レプリケーションでのレプリケーションラグの原因を調べてみた" /><published>2025-12-17T00:00:00+09:00</published><updated>2025-12-17T00:00:00+09:00</updated><id>https://masahikosawada.github.io/2025/12/17/Logical-Replication-Lags</id><content type="html" xml:base="https://masahikosawada.github.io/2025/12/17/Logical-Replication-Lags/"><![CDATA[<p>これは<a href="https://qiita.com/advent-calendar/2025/postgresql">PostgreSQL Advent calendar 2025</a>の17日目の記事です。</p>

<p>物理レプリケーションと論理レプリケーションの性能を比較するために、色々実験をしていたら少し嵌ってしまったのでその備忘録です。</p>

<p>実験では、トランザクションがCommitされてからそれが受信側で適用するまでの時間を計測しました。具体的には、同期レプリケーションを利用し、<a href="https://www.postgresql.jp/document/17/html/runtime-config-wal.html#GUC-SYNCHRONOUS-COMMIT"><code class="language-plaintext highlighter-rouge">synchronous_commit</code></a>を変更しながらトランザクションの完了時間を比較します。<code class="language-plaintext highlighter-rouge">synchronous_commit</code>パラメータを使うと「何を持って受信側で変更の適用が完了したとみなすか」(例えば、メモリ上に書かれた時点で完了とするなど)をトランザクション単位で制御することができますす。発生させたトランザクションは、1000行INSERTするような軽いトランザクションです。</p>

<p>結果はこちら:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>remote_write</th>
      <th>on</th>
      <th>remote_apply</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>物理レプリケーション</td>
      <td>2.551 ms</td>
      <td>3.858 ms</td>
      <td>4.788 ms</td>
    </tr>
    <tr>
      <td>論理レプリケーション</td>
      <td>5.744 ms</td>
      <td>405.534 ms</td>
      <td>5.377 ms</td>
    </tr>
  </tbody>
</table>

<p>物理レプリケーションの結果は想定通りでした。スタンバイで適用をより待つように<code class="language-plaintext highlighter-rouge">synchronous_commit</code>を設定する(remote_write -&gt; on -&gt; remote_apply)と、トランザクション完了までより長い時間がかかります。</p>

<p>一方、論理レプリケーションでは、実行時間が<code class="language-plaintext highlighter-rouge">on</code> &gt; <code class="language-plaintext highlighter-rouge">remote_apply</code>であり、<code class="language-plaintext highlighter-rouge">on</code>の時にかなり長い時間がかかっている結果となり、ちょっと意外でした。</p>

<p>物理レプリケーション、論理レプリケーションは基本的には似たように動いているはずなので、適用完了とみなしたLSNがどのように管理されているのかを調査しました。それらは<code class="language-plaintext highlighter-rouge">pg_stat_replication</code>ビューの<code class="language-plaintext highlighter-rouge">sent_lsn</code>、<code class="language-plaintext highlighter-rouge">write_lsn</code>、<code class="language-plaintext highlighter-rouge">flush_lsn</code>、<code class="language-plaintext highlighter-rouge">replay_lsn</code>列の値に対応します。<code class="language-plaintext highlighter-rouge">sent_lsn</code>、<code class="language-plaintext highlighter-rouge">write_lsn</code>、<code class="language-plaintext highlighter-rouge">flush_lsn</code>、<code class="language-plaintext highlighter-rouge">replay_lsn</code>は、どこまでの変更を送信完了なのかやどこまでの変更が複製完了なのかを示すLSN（Log Sequence Number）です。これらは、レプリケーションのラグを計測するのにとても便利で、例えば、<code class="language-plaintext highlighter-rouge">pg_current_wal_lsn() - sent_lsn</code>を計算することで、どれくらい変更の送信が遅れているのかがわかります。`</p>

<p>それぞれの列の値はどのようにこうしんされるのでしょうか。</p>

<h2 id="物理レプリケーションの場合">物理レプリケーションの場合</h2>

<p>物理レプリケーションの場合は、プライマリはWALが書かれた順番通りに送り、スタンバイはそれを適用するので単純です。</p>

<ol>
  <li>プライマリがWALを読み送る（<code class="language-plaintext highlighter-rouge">sent_lsn</code>を更新）</li>
  <li>スタンバイが受信しWALを書く（<code class="language-plaintext highlighter-rouge">write_lsn</code>を更新）</li>
  <li>スタンバイがWALをFlushする（<code class="language-plaintext highlighter-rouge">flush_lsn</code>を更新）</li>
  <li>スタンバイでWALが適用される（<code class="language-plaintext highlighter-rouge">replay_lsn</code>を更新）</li>
</ol>

<p>プライマリに通知する各LSNは以下の通りになります:</p>

<ul>
  <li>write_lsn: スタンバイが書いた最新のLSN</li>
  <li>flush_lsn: スタンバイがFlushした最新のLSN</li>
  <li>replay_lsn: スタンバイが適用した最新のLSN</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">pg_stat_replicationビュー</code>でのそれぞれの列値は、<code class="language-plaintext highlighter-rouge">sent_lsn</code> -&gt; <code class="language-plaintext highlighter-rouge">write_lsn</code> -&gt; <code class="language-plaintext highlighter-rouge">flush_lsn</code> -&gt; <code class="language-plaintext highlighter-rouge">replay_lsn</code>の順に増えていきます。</p>

<h2 id="論理レプリケーションの場合">論理レプリケーションの場合</h2>

<p>論理レプリケーションでは、パブリッシャ（送信側）はWALをデコードし、コミット順に並び替えてからトランザクション単位で送り、サブスクライバ（受信側）ではトランザクション単位で適用するので多少複雑です。サブスクライバでは、「パブリッシャで発生したトランザクションのCommit LSN<sup id="fnref:commit_LSN" role="doc-noteref"><a href="#fn:commit_LSN" class="footnote" rel="footnote">1</a></sup>」と「それをローカルで適用して得られたCommit LSN」をペアで管理します。例えば、パブリッシャで3つのトランザクションが発生し、それぞれのCommit LSNが100, 120, 200だった場合、それぞれのトランザクションをサブスクライバで適用して、以下のようにCommit LSNのペアを管理します：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{remote commit-lsn = 100, local commit-lsn = 123}
{remote commit-lsn = 120, local commit-lsn = 1234}
{remote commit-lsn = 200, local commit-lsn = 2345}
</code></pre></div></div>

<p>サブスクライバがどこまでのWALを適用したのかをパブリッシャへ通知する際は、上記の情報を元に「どのトランザクションまでをCommitしたのか」、「どのトランザクションまでをCommitしそのWALをFlushしたのか」を計算します。例えば、サブスクライバでの最新のFlush済みLSNが2000だった場合、remote commit-lsn = 120まではCommitが完了しており、remote commit-lsn = 200はCommit済みだけどまだFlushされていない、という事になります。パブリッシャに通知するLSNは、</p>

<ul>
  <li>write_lsn: 最新の受信済みLSN</li>
  <li>flush_lsn: 最新のCommit済みかつFlush済みの(remote)トランザクションのCommit LSN</li>
  <li>replay_lsn: 最新のCommit済み（かつ未Flush）の(remote)トランザクションのCommit LSN</li>
</ul>

<p>つまり、物理レプリケーションとは違い、データを受信(write)→それを適用(replay)→適用したWALをFlushという流れて処理するので、<code class="language-plaintext highlighter-rouge">pg_stat_replicationビュー</code>でのそれぞれの列値は、<code class="language-plaintext highlighter-rouge">sent_lsn</code> -&gt; <code class="language-plaintext highlighter-rouge">write_lsn</code> -&gt; <code class="language-plaintext highlighter-rouge">replay_lsn</code> -&gt; <code class="language-plaintext highlighter-rouge">flush_lsn</code>の順に増えていきます。</p>

<h2 id="サブスクライバではデフォルトで非同期コミットを利用している">サブスクライバではデフォルトで非同期コミットを利用している</h2>

<p>ソースコードやドキュメントを確認していると、サブスクライバでパブリッシャからの変更を受け取り適用するワーカー（Apply Worker）はデフォルトでは非同期コミットを利用していることがわかりました。つまり、先程の実験でサブスクライバでは、Apply Workerが変更を適用することで発生したWALレコードを、WAL WriterプロセスがFlushするまで待つ必要があった訳です。その遅延は最大で<code class="language-plaintext highlighter-rouge">wal_writer_delay</code>の3倍で、<code class="language-plaintext highlighter-rouge">wal_writer_delay</code>はデフォルトで200msです。</p>

<p>実際に<code class="language-plaintext highlighter-rouge">wal_writer_delay = 10ms</code>に変更すると<code class="language-plaintext highlighter-rouge">synchronous_commit = on</code>での結果が変わりました:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>remote_write</th>
      <th>on</th>
      <th>remote_apply</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>論理レプリケーション(wal_writer_delay = 200ms)</td>
      <td>5.744 ms</td>
      <td>405.534 ms</td>
      <td>5.377 ms</td>
    </tr>
    <tr>
      <td>論理レプリケーション(wal_writer_delay = 10ms)</td>
      <td>5.352 ms</td>
      <td>26.813 ms</td>
      <td>5.289 ms</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">wal_writer_delay</code>を変更してより早くWALをFlushすることで待ち時間を少なくする代わりに、<code class="language-plaintext highlighter-rouge">CREATE SUBSCRIPTION</code>に用意されている<code class="language-plaintext highlighter-rouge">synchronous_commit</code>オプションを利用することも可能です。<a href="https://www.postgresql.jp/document/17/html/sql-createsubscription.html#SQL-CREATESUBSCRIPTION-PARAMS-WITH-SYNCHRONOUS-COM">ドキュメント</a>にもしっかり書かれています:</p>

<blockquote>
  <p>同期論理レプリケーションを行う場合は別の設定が適切かもしれません。 論理レプリケーションのワーカーは書き込みおよび吐き出しの位置をパブリッシャーに報告しますが、同期レプリケーションを行っているときは、パブリッシャーは実際に吐き出しがされるのを待ちます。 これはつまり、サブスクリプションが同期レプリケーションで使われている時に、サブスクライバーのsynchronous_commitをoffに設定すると、パブリッシャーでのCOMMITの遅延が増大するかもしれない、ということを意味します。 この場合、synchronous_commitをlocalまたはそれ以上に設定することが有利になりえます。</p>
</blockquote>

<p>試してみると:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>remote_write</th>
      <th>on</th>
      <th>remote_apply</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>論理レプリケーション(synchronous_commitオプション = off)</td>
      <td>5.744 ms</td>
      <td>405.534 ms</td>
      <td>5.377 ms</td>
    </tr>
    <tr>
      <td>論理レプリケーション(synchronous_commitオプション = on)</td>
      <td>5.829 ms</td>
      <td>5.851 ms</td>
      <td>5.874 ms</td>
    </tr>
  </tbody>
</table>

<p>という感じで納得行く結果を得ることができました。<code class="language-plaintext highlighter-rouge">wal_writer_delay</code>はインスタンス全体に影響を与えてしまうので、<code class="language-plaintext highlighter-rouge">synchronous_commit</code>オプションで調整するの方が推奨です。</p>

<h2 id="まとめ">まとめ</h2>

<p>最初からドキュメントを読んでいればよかったですが、調査を通して論理レプリケーションのflush_lsnとreplay_lsn更新の仕方の違いなども知ることができたので良かったです。次回は、本来やりたかった物理レプリケーションと論理レプリケーションの性能比較をやっていきます。</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:commit_LSN" role="doc-endnote">
      <p>トランザクションのCommit WALレコードのLSN <a href="#fnref:commit_LSN" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Masahiko Sawada</name></author><category term="PostgreSQL" /><category term="Logical Replication" /><summary type="html"><![CDATA[PostgreSQLの論理レプリケーションでレプリケーションラグが大きくなる原因を調べました。synchronous_commitを変えながら物理レプリケーションと比較し、サブスクライバがデフォルトで非同期コミットを使う点に行き着きます。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://masahikosawada.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://masahikosawada.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="ja"><title type="html">UUIDの生成速度を上げる取り組み</title><link href="https://masahikosawada.github.io/ja/2025/10/30/UUID-performance/" rel="alternate" type="text/html" title="UUIDの生成速度を上げる取り組み" /><published>2025-10-30T00:00:00+09:00</published><updated>2025-10-30T00:00:00+09:00</updated><id>https://masahikosawada.github.io/2025/10/30/UUID-performance</id><content type="html" xml:base="https://masahikosawada.github.io/2025/10/30/UUID-performance/"><![CDATA[<p>以前PostgreSQL 18でUUIDv7がサポートされたという<a href="https://masahikosawada.github.io/ja/2025/09/04/UUIDv7-in-PostgreSQL/">記事</a>を書きました。今回は現在取り組んでいるUUIDv7の生成を早くするための改善について、その背景や検証内容についてです。</p>

<h2 id="背景">背景</h2>

<p>UUIDの生成速度が気になったきっかけは、PostgreSQLで色々なUUIDv7生成方法を比較していた時に、PostgreSQL 18で導入される予定の<code class="language-plaintext highlighter-rouge">uuidv7()</code>関数とpgrxで自前で作ったUUIDv7生成関数の性能比較をしていたときでした。</p>

<p>PostgreSQL 18の<code class="language-plaintext highlighter-rouge">uuidv7()</code>関数はC言語で実装されていて、自作のpgrxのUUIDv7（便宜上<code class="language-plaintext highlighter-rouge">pgrx_uuiv7()</code>と呼びます）はRustのuuid createを利用して実装しています<sup id="fnref:pgrx-uuidv7" role="doc-noteref"><a href="#fn:pgrx-uuidv7" class="footnote" rel="footnote">1</a></sup>。</p>

<p>UUIDv7を100万件生成するのにかかった時間は以下のとおりです:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>uuidv7()</th>
      <th>pgrx_uuidv7()</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>実行時間(ms)</td>
      <td>2203.124</td>
      <td>264.688</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">uuidv7()</code>でも秒間約45万個のUUIDv7が生成できていてる一方で、<code class="language-plaintext highlighter-rouge">pgrx_uuidv7()</code>は約10倍近く速い結果となりました。この生成速度の違いを調べた所、ランダムデータ生成が実行時間の大半を占めていて、それぞれの方式で異なるランダムデータ生成方法が使用されていることがわかりました。</p>

<h2 id="postgresqlのランダムデータ生成方法">PostgreSQLのランダムデータ生成方法</h2>

<p>PostgreSQLは以下の2つ種類のランダムデータ生成方法をサポートしています（Linuxの場合）：</p>

<ol>
  <li>OpenSSLの<code class="language-plaintext highlighter-rouge">RAND_bytes()</code>関数を使う</li>
  <li>/dev/urandomを読む</li>
</ol>

<p>PostgreSQLではビルド時にどちらの方法を利用するか決めていて、OpenSSLを利用してビルドした場合(<code class="language-plaintext highlighter-rouge">--with-openssl</code>を指定)は常に1を選択することになります。2はFallBackとして用意されています。</p>

<p>先程の性能測定では、OpenSSLを利用しないでビルドしたPostgreSQLを利用したので2の方法を利用していました。2は<a href="https://github.com/postgres/postgres/blob/master/src/port/pg_strong_random.c#L150">コード</a>を見ると分かる通り、<code class="language-plaintext highlighter-rouge">/dev/urandom</code>を<code class="language-plaintext highlighter-rouge">open()</code>して<code class="language-plaintext highlighter-rouge">read()</code>し、最後に<code class="language-plaintext highlighter-rouge">close()</code>するという、移植性の高い方法ではありますが性能的には良くありません。</p>

<p>一方、OpenSSLを利用すると<a href="https://github.com/postgres/postgres/blob/master/src/port/pg_strong_random.c#L90"><code class="language-plaintext highlighter-rouge">RAND_bytes()</code>関数を使用</a>してランダムデータを生成します。PostgreSQLをOpenSSLを有効にしてビルドし直してもう一度UUIDv7の生成速度を測定してます。</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>uuidv7() (/dev/urandomを利用)</th>
      <th>uuidv7() (OpenSSLのRAND_bytes()を利用)</th>
      <th>pgrx_uuidv7()</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>実行時間(ms)</td>
      <td>2203.124</td>
      <td>759.296</td>
      <td>264.688</td>
    </tr>
  </tbody>
</table>

<p>OpenSSLは<code class="language-plaintext highlighter-rouge">/dev/urandom</code>よりはかなり速いが、<code class="language-plaintext highlighter-rouge">pgrx_uuidv7()</code>よりは遅いという結果になりました。世の中のほぼすべてのPostgreSQLはOpenSSLを有効にしてビルドされたものだと思うので、UUIDの生成速度で困ることはほとんどないでしょう。</p>

<p>しかし、<code class="language-plaintext highlighter-rouge">pgrx_uuidv7()</code>とはまだ3倍近くの差があります。一度検証を始めたからには、この差を埋める方法が見つかるまでさらに検証を進めていきます。</p>

<h2 id="uuid-createではgetrandomを使っていた">uuid createでは<code class="language-plaintext highlighter-rouge">getrandom()</code>を使っていた</h2>

<p><code class="language-plaintext highlighter-rouge">pgrx_uuidv()</code>の元になっている<code class="language-plaintext highlighter-rouge">Uuid::now_v7()</code>を調べた所、おそらく最終的には(Linuxでは)<code class="language-plaintext highlighter-rouge">getrandom()</code>関数を使ってランダムデータを生成しているようです<sup id="fnref:source" role="doc-noteref"><a href="#fn:source" class="footnote" rel="footnote">2</a></sup>。<a href="https://man7.org/linux/man-pages/man2/getrandom.2.html"><code class="language-plaintext highlighter-rouge">getrandom()</code></a>はランダムデータを生成するglibcの関数であり、同名のシステムコールを呼びます。</p>

<p><code class="language-plaintext highlighter-rouge">getrandom()</code>関数の<code class="language-plaintext highlighter-rouge">flags</code>に<code class="language-plaintext highlighter-rouge">GRND_NONBLOCK</code>を指定することで<code class="language-plaintext highlighter-rouge">/dev/urandom</code>と同じソースからランダムデータを生成することができるようです。<code class="language-plaintext highlighter-rouge">/dev/urandom</code>を<code class="language-plaintext highlighter-rouge">open()</code>したり<code class="language-plaintext highlighter-rouge">read()</code>する必要がないので高速に動きます。古いLinuxではサポートされていないシステムコールなので注意が必要です。</p>

<h2 id="実際どれくらい違うのか">実際どれくらい違うのか？</h2>

<p>簡単なCプログラムを書いて、それぞれ方式でののランダムデータ生成速度を測定してみました。各方式では、以下のようにランダムデータを生成しています。</p>

<ol>
  <li>urandom: <code class="language-plaintext highlighter-rouge">/dev/urandom</code>を直接読む</li>
  <li><code class="language-plaintext highlighter-rouge">getrandom()</code>関数を呼ぶ</li>
  <li>OpenSSLの<code class="language-plaintext highlighter-rouge">RAND_bytes()</code>関数を呼ぶ</li>
</ol>

<p>生成するデータサイズを変えながら、データ生成にかかった時間を計測しました（単位はナノ秒）:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./bench
        len    urandom  getrandom   openssl
         16       1932         61      1505
         64       2067        160       427
        256       2507        505       492
       1024       4346       1807       592
</code></pre></div></div>

<p>生成データが小さい場合(len&lt;=64)はgetrandomの方が圧倒的に性能が良く、生成するデータが大きくなっていくとOpenSSLの方が良くなる、という結果でした。PostgreSQLのUUIDv7実装では62 bitsのランダムデータを格納しているので、<code class="language-plaintext highlighter-rouge">getrandom()</code>を利用していた<code class="language-plaintext highlighter-rouge">pgrx_uuidv7()</code>が一番早かったのは納得です。</p>

<h2 id="postgresqlでもgetrandomが使えるのか">PostgreSQLでも<code class="language-plaintext highlighter-rouge">getrandom()</code>が使えるのか？</h2>

<p>より速いUUID生成性能を得るために、開発コミュニティに提案したのが<a href="https://www.postgresql.org/message-id/CAD21AoAjb2TP%2BUj-fOr7s1cjv2Eq65BaUYi8xNMumcAXiYFM9Q%40mail.gmail.com">こちら</a>です。</p>

<p>セキュリティ、性能、互換性などを中心に議論し、以下のような方針で進めようと思っています。</p>

<ul>
  <li>Packagerがビルドオプションで使用するランダムデータ生成方法を選択できるようにする。
    <ul>
      <li>OpenSSLを有効にするけどランダムデータ生成だけは<code class="language-plaintext highlighter-rouge">getrandom()</code>を使う、みたいなことが可能になる。</li>
      <li>実際のシステムでこれを使うユースケースはおそらくほぼないけど、OpenSSLを無効にしてビルドされたPostgreSQLに対するテストの高速化が期待できる</li>
      <li>とはいえ、セキュリティ面（特にFIPS準拠など<sup id="fnref:fips" role="doc-noteref"><a href="#fn:fips" class="footnote" rel="footnote">3</a></sup>）を考えるとOpenSSLのRAND_bytes()を使うのがもっとも望ましいというのは変わらない。</li>
      <li>これまでとの互換性を保つためにも、「OpenSSLが有効ならランダムデータ生成にはRAND_bytes()を使う」という動作をデフォルトにする。</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">getrandom()</code>が生成するランダムデータでもセキュリティ的な要件を満たせるケースはあるので、UUID生成のレイヤにてユーザが使用するランダムデータ生成方法を選択できるようにする</li>
</ul>

<p>議論のポイントとしては「セキュリティ＞速度」なので、OpenSSLを有効にしたビルドではこれまでの動作とは変えないようにしながら、ユースケースに応じてユーザが設定できるようにする、という点です。</p>

<p>特に最後の点は、<code class="language-plaintext highlighter-rouge">getrandom()</code>のvDSO実装によりさらなる性能的なメリットが得られることが議論を後押ししました。</p>

<h2 id="getrandomのvdso実装でuuid生成を比べてみる">getrandom()のvDSO実装でUUID生成を比べてみる</h2>

<p>詳しいことはわかりませんが、新し目のLinuxカーネルではvDSO(Virtual Dynamic Shared Object)という仕組みを利用して、ユーザ空間で<code class="language-plaintext highlighter-rouge">getrandom</code>システムコール相当の処理ができるようなったようです。コンテキストスイッチも不要かつ、カーネル空間→ユーザ空間へのコピーも不要なのでとても高速化されているとのことです。これは、Linux 6.11以降 + glibc 2.40以降で利用可能で、特に数百バイト程度のランダムデータ生成時にこの方式が利用されます。</p>

<p>実は先程の性能検証結果で使ったマシンにはRed Hat Enterprise Linux 10.0がインストールをされていて、vDSOのgetrandomを利用していました<sup id="fnref:rhel10" role="doc-noteref"><a href="#fn:rhel10" class="footnote" rel="footnote">4</a></sup>。なので少量のランダムデータ生成では圧倒的に早かったということです。一応、getrandomシステムコールとの差を比べてみると、以下のような結果になりました。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./bench
            len        urandom      getrandom  getrandom_sys        openssl
             16           1921             61            366           1536
             64           2063            160            501            430
            256           2506            506            953            490
           1024           4351           1801           2780            593
</code></pre></div></div>

<h2 id="vdso実装のgetrandomを使ってuuidを生成してみる">vDSO実装のgetrandomを使ってUUIDを生成してみる</h2>

<p>最後にgetrandomを使ってPostgreSQLでUUIDv7を生成すると、どれくらい高速になるかを検証してみます。</p>

<p>この検証には、先程紹介したPostgreSQLコミュニティに提案中のパッチが必要となります。</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>uuidv7() (/dev/urandomを利用)</th>
      <th>uuidv7() (OpenSSLのRAND_bytes()を利用)</th>
      <th>uuidv7() (getrandom)を利用</th>
      <th>pgrx_uuidv7() (参考)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>実行時間(ms)</td>
      <td>2183.191</td>
      <td>766.671</td>
      <td>196.876</td>
      <td>260.512</td>
    </tr>
  </tbody>
</table>

<p>ついにPostgreSQLの<code class="language-plaintext highlighter-rouge">uuidv7()</code>が最も早くなりました！秒間約500万件のUUIDv7が生成できています。ちなみに、同環境では、シーケンスの値を<code class="language-plaintext highlighter-rouge">nextval()</code>関数で100万回取得するのに352.519msかかったので、シーケンスの払い出しよりも高速になったと言えます<sup id="fnref:sequence" role="doc-noteref"><a href="#fn:sequence" class="footnote" rel="footnote">5</a></sup>。</p>

<p>UUID生成速度を上げるためにより速いランダムデータ生成方法を利用する、という話でした。</p>

<h2 id="参考資料">参考資料</h2>

<ul>
  <li><a href="https://lwn.net/Articles/919008/">A vDSO implementation of getrandom()</a></li>
  <li><a href="https://www.phoronix.com/news/glibc-getrandom-vDSO-Merged">GNU C Library Merges Support for getrandom vDSO</a></li>
  <li><a href="https://lwn.net/Articles/978601/">implement getrandom() in vDSO</a></li>
</ul>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:pgrx-uuidv7" role="doc-endnote">
      <p>前回のポストにコードを乗せています <a href="#fnref:pgrx-uuidv7" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:source" role="doc-endnote">
      <p>https://github.com/rust-lang/rust/blob/master/library/std/src/random.rs <a href="#fnref:source" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fips" role="doc-endnote">
      <p>getrandom()はCSPRNGでRAND_bytes()はDRPRNG <a href="#fnref:fips" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:rhel10" role="doc-endnote">
      <p>Linux kernel 6.12.0 + glibc-2.39-43ですが、Red Hat Engerprise LinuxではvDSO対応パッチをバックポートしているようです。 <a href="#fnref:rhel10" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:sequence" role="doc-endnote">
      <p>シーケンスの払い出しはシーケンス自体の更新＋WALもあるので <a href="#fnref:sequence" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Masahiko Sawada</name></author><category term="PostgreSQL" /><category term="UUID" /><summary type="html"><![CDATA[PostgreSQLのUUIDv7の生成速度を上げるための改善について、その背景と検証内容を紹介します。C実装のuuidv7()とRust（pgrx）実装の性能を比較し、どこがボトルネックになっているかを掘り下げます。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://masahikosawada.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://masahikosawada.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="ja"><title type="html">PostgreSQL 18がUUIDv7をサポート</title><link href="https://masahikosawada.github.io/ja/2025/09/04/UUIDv7-in-PostgreSQL/" rel="alternate" type="text/html" title="PostgreSQL 18がUUIDv7をサポート" /><published>2025-09-04T00:00:00+09:00</published><updated>2025-09-04T00:00:00+09:00</updated><id>https://masahikosawada.github.io/2025/09/04/UUIDv7-in-PostgreSQL</id><content type="html" xml:base="https://masahikosawada.github.io/2025/09/04/UUIDv7-in-PostgreSQL/"><![CDATA[<p>UUIDv7は<a href="https://www.rfc-editor.org/rfc/rfc9562.html">RFC 9562</a>で定義されました。UUIDには8つのバージョンがあり、どれもサイズは128ビットですが、格納するデータがそれぞれバージョンで異なります。私もUUIDv7を知るまでは、UUIDといえばランダムデータのイメージでしたが、それはバージョン4のUUIDで、本記事で紹介するバージョン7のUUID（UUIDv7）はタイムスタンプをデータの先頭に持つためソート可能であるのが大きな特徴です。</p>

<p>実際に比較してみると違いは一目瞭然です:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">=#</span> <span class="k">select</span> <span class="n">uuidv4</span><span class="p">(),</span> <span class="n">uuidv7</span><span class="p">()</span> <span class="k">from</span> <span class="n">generate_series</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">5</span><span class="p">);</span>
                <span class="n">uuidv4</span>                <span class="o">|</span>                <span class="n">uuidv7</span>
<span class="c1">--------------------------------------+--------------------------------------</span>
 <span class="mi">216</span><span class="n">aae4c</span><span class="o">-</span><span class="mi">6</span><span class="n">b02</span><span class="o">-</span><span class="mi">4</span><span class="n">bea</span><span class="o">-</span><span class="n">bd84</span><span class="o">-</span><span class="mi">7</span><span class="n">fea9617e0cc</span> <span class="o">|</span> <span class="mi">01991632</span><span class="o">-</span><span class="n">e3a0</span><span class="o">-</span><span class="mi">7468</span><span class="o">-</span><span class="n">ac42</span><span class="o">-</span><span class="mi">26</span><span class="n">afbc51df65</span>
 <span class="mi">55372</span><span class="n">a64</span><span class="o">-</span><span class="mi">0351</span><span class="o">-</span><span class="mi">40</span><span class="n">a1</span><span class="o">-</span><span class="n">a32c</span><span class="o">-</span><span class="mi">318</span><span class="n">e581f4561</span> <span class="o">|</span> <span class="mi">01991632</span><span class="o">-</span><span class="n">e3a0</span><span class="o">-</span><span class="mi">748</span><span class="n">a</span><span class="o">-</span><span class="n">bb4c</span><span class="o">-</span><span class="mi">9882</span><span class="n">ddaf0721</span>
 <span class="n">e154c7a7</span><span class="o">-</span><span class="mi">4</span><span class="n">a6b</span><span class="o">-</span><span class="mi">4446</span><span class="o">-</span><span class="mi">96</span><span class="n">be</span><span class="o">-</span><span class="mi">6</span><span class="n">a1cb84b773e</span> <span class="o">|</span> <span class="mi">01991632</span><span class="o">-</span><span class="n">e3a0</span><span class="o">-</span><span class="mi">749</span><span class="n">f</span><span class="o">-</span><span class="n">b429</span><span class="o">-</span><span class="n">d2ef56b9683e</span>
 <span class="mi">59</span><span class="n">adba82</span><span class="o">-</span><span class="mi">8</span><span class="n">a88</span><span class="o">-</span><span class="mi">4</span><span class="n">f4f</span><span class="o">-</span><span class="n">b859</span><span class="o">-</span><span class="mi">036430</span><span class="n">d09045</span> <span class="o">|</span> <span class="mi">01991632</span><span class="o">-</span><span class="n">e3a0</span><span class="o">-</span><span class="mi">74</span><span class="n">b5</span><span class="o">-</span><span class="mi">9</span><span class="n">a86</span><span class="o">-</span><span class="mi">8107</span><span class="n">f1851200</span>
 <span class="n">be096cb4</span><span class="o">-</span><span class="n">ee48</span><span class="o">-</span><span class="mi">4</span><span class="n">bf8</span><span class="o">-</span><span class="n">ae30</span><span class="o">-</span><span class="mi">3</span><span class="n">ca6d09af786</span> <span class="o">|</span> <span class="mi">01991632</span><span class="o">-</span><span class="n">e3a0</span><span class="o">-</span><span class="mi">74</span><span class="n">c9</span><span class="o">-</span><span class="n">a08a</span><span class="o">-</span><span class="mi">1</span><span class="n">ec588e5a60d</span>
<span class="p">(</span><span class="mi">5</span> <span class="k">rows</span><span class="p">)</span>
</code></pre></div></div>

<p>UUIDv7は、例えばデータベースの主キーとして使用した時に多くのメリットがあります。PostgreSQLでは主キーインデックスにはBtreeインデックスが使われます。そのため、大量にデータをロードした場合でも挿入されるUUIDのデータは常に昇順になるので、インデックス更新の局所性が高く、性能的に有利です。また、PostgreSQLではFull Page Writes（FPW）を抑える効果もあります。</p>

<p>SERIAL型（シーケンス）とUUID型（UUIDv4とUUIDv7）に主キーをつけた状態で500万件INSERTした結果は以下のとおりです(PostgreSQL 18 Betaで検証):</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>SERIAL</th>
      <th>UUIDv4</th>
      <th>UUIDv7</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Druation</td>
      <td>8.452 s</td>
      <td>42.24 s</td>
      <td>16.922 s</td>
    </tr>
  </tbody>
</table>

<h2 id="postgresqlとuuid">PostgreSQLとUUID</h2>

<p>PostgreSQLはSQLのデータ型として<a href="https://www.postgresql.jp/document/17/html/datatype-uuid.html"><code class="language-plaintext highlighter-rouge">uuid</code>型</a>を持っていて、（PostgreSQL 17現在）UUIDを生成する方法は大きく2つあります。</p>

<p>一つは、組み込みの<a href="https://www.postgresql.jp/document/17/html/functions-uuid.html"><code class="language-plaintext highlighter-rouge">gen_random_uuid()</code>SQL関数</a>を利用する方法です。これはバージョン4のUUIDを生成します（バージョンの詳細については後述）。PostgreSQL独自の実装を利用しています。</p>

<p>もう一つは、<a href="https://www.postgresql.jp/document/17/html/uuid-ossp.html">uuid-ossp contribモジュール</a>を使う方法です。UUID生成を外部ライブラリによって行うのですが、プラットフォームによって利用するライブラリはことなります。LinuxやmacOSではlibuuidを利用します。バージョン1 ~ 5まで生成可能です。</p>

<p>上記の通り、PostgreSQL 17現在、UUIDv7の生成をサポートしていないので、UUIDv7を利用したい場合は、公開されているextensionを利用する、もしくは自分で実装する必要があります。githubで探すと以下のextensionが見つかりました:</p>

<ul>
  <li><a href="https://github.com/fboulnois/pg_uuidv7">pg_uuidv7</a>
    <ul>
      <li>C言語で実装</li>
    </ul>
  </li>
  <li><a href="https://github.com/dverite/postgres-uuidv7-sql">postgres-uuidv7-sql</a>
    <ul>
      <li>SQLで実装。なので、Extensionとして登録しなくても<code class="language-plaintext highlighter-rouge">CREATE FUNCTION</code>を使えば利用可能</li>
    </ul>
  </li>
</ul>

<p>pg_tle + PL/RustでUUIDv7を生成する関数を作るブロクもあります：</p>

<p><a href="https://aws.amazon.com/blogs/database/implement-uuidv7-in-amazon-rds-for-postgresql-using-trusted-language-extensions/">https://aws.amazon.com/blogs/database/implement-uuidv7-in-amazon-rds-for-postgresql-using-trusted-language-extensions/</a></p>

<p>PL/Rustは利用できるcrateが限られているためRustのuuid crateは利用できません。自作ExtensionでUUIDv7を生成する関数を作りたい場合は、おそらくpgrx<sup id="fnref:pgrx" role="doc-noteref"><a href="#fn:pgrx" class="footnote" rel="footnote">1</a></sup>を利用するのが一番簡単だと思います。UUIDv7の生成だけであれば以下のコードだけで可能です:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">pgrx</span><span class="p">::</span><span class="nn">prelude</span><span class="p">::</span><span class="o">*</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">uuid</span><span class="p">::</span><span class="n">Uuid</span><span class="p">;</span>

<span class="p">::</span><span class="nn">pgrx</span><span class="p">::</span><span class="nd">pg_module_magic!</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">version</span><span class="p">);</span>

<span class="nd">#[pg_extern]</span>
<span class="k">fn</span> <span class="nf">pgrx_uuidv7</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="nn">pgrx</span><span class="p">::</span><span class="n">Uuid</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">uuid</span> <span class="o">=</span> <span class="nn">Uuid</span><span class="p">::</span><span class="nf">now_v7</span><span class="p">();</span>

    <span class="nn">pgrx</span><span class="p">::</span><span class="nn">Uuid</span><span class="p">::</span><span class="nf">from_bytes</span><span class="p">(</span><span class="n">uuid</span><span class="nf">.into_bytes</span><span class="p">())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>PostgreSQL 18では<a href="https://www.postgresql.org/docs/devel/functions-uuid.html"><code class="language-plaintext highlighter-rouge">uuidv7()</code>SQL関数</a>が導入されるため、すべてのPostgreSQLがユーザがUUIDv7を利用できるようになります(<a href="https://github.com/postgres/postgres/commit/78c5e141e9c139fc2ff36a220334e4aa25e1b0eb">コミットログ</a>)！</p>

<p>後方互換性のため<code class="language-plaintext highlighter-rouge">gen_random_uuid()</code>はこれまで通りUUIDv4を生成する関数として存在します。<code class="language-plaintext highlighter-rouge">uuidv7()</code>にあわせて<code class="language-plaintext highlighter-rouge">uuidv4()</code>も追加されましたが<code class="language-plaintext highlighter-rouge">gen_random_uuid()</code>のエイリアスです。</p>

<h2 id="uuidv7のフォーマット">UUIDv7のフォーマット</h2>

<p>UUIDv7のフォーマットはRFCに以下のように記載されています:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 0                   1                   2                   3
 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           unix_ts_ms                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          unix_ts_ms           |  ver  |       rand_a          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                        rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
</code></pre></div></div>

<p>すべてのUUIDにはデータ自体にそのバージョンが記載されています（<code class="language-plaintext highlighter-rouge">ver</code>の部分）。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>           Version
              |
              v
01937a3a-3d34-74d0-a1b7-e1f1b53064d8
</code></pre></div></div>

<p>UUIDv7では、バージョンの前にミリ秒制度のタイムスタンプを入れ、バージョンの後にランダムデータ(正確には+variant)を入れる、というのがざっくりとしたフォーマットです。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  timestamp      random data(+var)
|-----------|  |-------------------|
01937a3a-3d34-74d0-a1b7-e1f1b53064d8

</code></pre></div></div>

<h3 id="uuidv7の単調増加性">UUIDv7の単調増加性</h3>

<p>ミリ秒精度のタイムスタンプでは精度が足りないというユースケースのために、RFCでは、<code class="language-plaintext highlighter-rouge">rand_a</code>の部分を（またはそれに加えて<code class="language-plaintext highlighter-rouge">rand_b</code>も)生成されたデータの単調増加性を維持すための追加データとして利用することを許可しています。そして、どのように使うことができるかの方法もいくつかRFCで<a href="https://www.rfc-editor.org/rfc/rfc9562.html#name-monotonicity-and-counters">紹介されています</a>。</p>

<p>UUIDv7生成関数の実装毎に異なる使い方をしていますが、高頻度なUUID生成が行われる環境でもデータの単調増加性を維持すためにどのように<code class="language-plaintext highlighter-rouge">rand_a</code>と<code class="language-plaintext highlighter-rouge">rand_b</code>を使うかはとても重要です。例えば、「ミリ秒精度のタイムスタンプ＋あとは全部ランダムデータ」といった一番単純フォーマットを利用したUUIDv7では、1秒間に1000個以上のUUIDが生成された場合、先頭のタイムスタンプはすべて同じ値になるので、生成されたUUIDv7のデータの単調増加性は保証されません。つまり、秒間1000個以上のUUIDのを生成する可能性あるシステムでは、そのようなUUIDv7生成関数を利用すると、UUIDv7の利点を活かしきることができません。ユースケースに応じて利用するUUIDv7生成関数を見極める必要があります<sup id="fnref:pg_uuidv7_analysis" role="doc-noteref"><a href="#fn:pg_uuidv7_analysis" class="footnote" rel="footnote">2</a></sup>。</p>

<h2 id="postgresqlのuuidv7の実装">PostgreSQLのUUIDv7の実装</h2>

<p>PostgreSQLのUUIDv7実装では、RFCで記載されている<a href="https://www.rfc-editor.org/rfc/rfc9562.html#name-monotonicity-and-counters">Method 3(Replace Leftmost Random Bits with Increased Clock Precision)</a>の方法を取り入れました。具体的には、<code class="language-plaintext highlighter-rouge">rand_a</code>の部分にミリ秒以下のタイムスタンプを入れ、全体で60(=48+12)ビットをタイムスタンプに使っています。これにより、秒間約400万個のUUID生成に耐えることが可能です。さらに、同一プロセス内ではUUID生成事に<code class="language-plaintext highlighter-rouge">rand_a</code>の部分が必ず増加するように調整しているので、それ以上の高頻度でのUUID生成でも<strong>単一プロセスから生成されるUUIDv7のデータは単調増加していることが保証</strong>されています。</p>

<p>また、引数に<code class="language-plaintext highlighter-rouge">interval</code>値を入れることができ、UUIDデータに格納されるタイムスタンプを指定した期間だけずらすことも可能です。</p>

<p>ソースコードは<a href="https://github.com/postgres/postgres/blob/master/src/backend/utils/adt/uuid.c#L601">ここ</a>です。</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:pgrx" role="doc-endnote">
      <p>RustでPostgreSQLのExtensionを作成するためのフレームワーク <a href="#fnref:pgrx" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:pg_uuidv7_analysis" role="doc-endnote">
      <p>例えば<a href="https://github.com/fboulnois/pg_uuidv7/blob/main/pg_uuidv7.c#L35">pg_uuidv7の実装</a>を見ると「ミリ秒精度のタイムスタンプ＋ランダムデータ」ということがわかります <a href="#fnref:pg_uuidv7_analysis" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Masahiko Sawada</name></author><category term="PostgreSQL" /><category term="UUID" /><summary type="html"><![CDATA[PostgreSQL 18がRFC 9562のUUIDv7をサポートしました。タイムスタンプを先頭に持つためソート可能というUUIDv7の特徴と、uuidv7()関数の使い方、UUIDv4との違いを実際のSQLで比較しながら解説します。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://masahikosawada.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://masahikosawada.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="ja"><title type="html">PostgreSQL 17で新しく実装されたradix treeを使ってインメモリのキーバリューストア作ってみた</title><link href="https://masahikosawada.github.io/ja/2024/12/18/pg_memstore-extension/" rel="alternate" type="text/html" title="PostgreSQL 17で新しく実装されたradix treeを使ってインメモリのキーバリューストア作ってみた" /><published>2024-12-18T00:00:00+09:00</published><updated>2024-12-18T00:00:00+09:00</updated><id>https://masahikosawada.github.io/2024/12/18/pg_memstore-extension</id><content type="html" xml:base="https://masahikosawada.github.io/2024/12/18/pg_memstore-extension/"><![CDATA[<p>これは<a href="https://qiita.com/advent-calendar/2024/postgresql">PostgreSQL Advent calendar 2024</a>の18日目の記事です。</p>

<h2 id="radixtreeh">radixtree.h</h2>

<p>先日リリースされたPostgreSQL 17では、Vacuumの実行速度やメモリ使用量が大きく改善されています。内部的な情報なのでリリースノートでは言及されていませんが、その改善の立役者となったのはPostgreSQL 17で新しく実装されたradix treeです。以前はTIDの配列を使ってゴミタプルのTIDを管理していたのですが、PostgreSQL 17からはゴミタプルのTIDをradix treeに入れることにより、Vacuumがより早く、より省メモリで動くようになりました。radix treeの実装は<a href="https://db.in.tum.de/~leis/papers/ART.pdf">こちらの論文</a>をベースにしており、いくつか最適化を入れています。ソースコードに興味がある方は<a href="https://github.com/postgres/postgres/blob/master/src/include/lib/radixtree.h">こちら</a>。</p>

<p>インメモリでなにかのデータを保持したい場合、PostgreSQLのソースコードにはハッシュテーブルをはじめとしたいくつかのデータ構造が実装されています。</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">src/backend/utils/hash/dynahash.c</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/include/lib/simplehash.h</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/include/lib/radixtree.h</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/backend/lib/dshash.c</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/backend/lib/integerset.c</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/backend/lib/rbtree.c</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/backend/lib/ilist.c</code></li>
</ul>

<p>例えば、<code class="language-plaintext highlighter-rouge">dynahash.c</code>はPostgreSQL内部ではロック情報の管理など、様々なところで使われており、<code class="language-plaintext highlighter-rouge">dshash.c</code>はPostgreSQLの稼働統計情報を保持するコードで使われています。</p>

<p>それぞれ、データ構造としての特徴が異なることはもちろんですが、PostgreSQLでの実装という観点での特徴も異なります。例えば、<code class="language-plaintext highlighter-rouge">dynahash.c</code>はプロセスのローカルメモリ上にも共有メモリ上にもハッシュテーブルを作ることが可能ですが、ハッシュテーブルのサイズは固定です。一方で、<code class="language-plaintext highlighter-rouge">dshash.c</code>は共有メモリ上にのみハッシュテーブルを作ることができ、ハッシューテーブルは自動的に成長しますが、ハッシュテーブルの値（バリュー）は固定長ですし、ハッシューテーブルは成長はしますが小さくはなりません。これらの実装依存の特徴は将来変わる可能性があります。</p>

<p><code class="language-plaintext highlighter-rouge">radixtree.h</code>の実装としての特徴は以下の通りです。</p>

<ul>
  <li>キーは<code class="language-plaintext highlighter-rouge">uint64</code>で固定</li>
  <li>バリューは可変長もサポートしてる</li>
  <li>ローカルメモリにも共有メモリにも作成可能</li>
  <li>自動的に成長、縮退する</li>
</ul>

<p>特に、可変長のバリューを持つことができ、共有メモリ上に作成できることは（今のところ）大きな特徴だと言えます。この特徴を使ってインメモリのキーバリューストアを作ってみました。</p>

<h2 id="pg_memstore">pg_memstore</h2>

<p><a href="https://github.com/MasahikoSawada/pg_memstore"><code class="language-plaintext highlighter-rouge">pg_memstore</code></a>は、PostgreSQLの拡張機能（Extension）で、共有メモリ上に作成したradix treeをベースとしたキーバリューストアです。可変長のバリューが持てる特徴を活かし、バリューにはjsonbデータが格納できます。</p>

<p>最初は簡単なSET、GETだけをサポートする予定だったのですが、作っていたら色々面白くなってきて、ファイルへのダンプ・リストア、WALサポートも実装してみました。</p>

<p>インストールはリポジトリ内の<a href="https://github.com/MasahikoSawada/pg_memstore">README</a>をご参照ください。</p>

<p><code class="language-plaintext highlighter-rouge">CREATE EXTENSION pg_memstore</code>を実行したら早速使ってみましょう。まずはシンプルな操作です。</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">=#</span> <span class="k">SELECT</span> <span class="n">memstore</span><span class="p">.</span><span class="k">set</span><span class="p">(</span><span class="s1">'key-1'</span><span class="p">,</span> <span class="s1">'{"a": 1}'</span><span class="p">);</span> <span class="c1">-- return false if a new key</span>
 <span class="k">set</span>
<span class="c1">-----</span>
 <span class="n">f</span>
<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>

<span class="o">=#</span> <span class="k">SELECT</span> <span class="n">memstore</span><span class="p">.</span><span class="k">get</span><span class="p">(</span><span class="s1">'key-1'</span><span class="p">);</span>
   <span class="k">get</span>
<span class="c1">----------</span>
 <span class="p">{</span><span class="nv">"a"</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span>
<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>

<span class="o">=#</span> <span class="k">SELECT</span> <span class="n">memstore</span><span class="p">.</span><span class="k">set</span><span class="p">(</span><span class="s1">'key-1'</span><span class="p">,</span> <span class="s1">'{"a": 999}'</span><span class="p">);</span> <span class="c1">-- update the value</span>
 <span class="k">set</span>
<span class="c1">-----</span>
 <span class="n">t</span>
<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>

<span class="o">=#</span> <span class="k">SELECT</span> <span class="n">memstore</span><span class="p">.</span><span class="k">get</span><span class="p">(</span><span class="s1">'key-1'</span><span class="p">);</span>
    <span class="k">get</span>
<span class="c1">------------</span>
 <span class="p">{</span><span class="nv">"a"</span><span class="p">:</span> <span class="mi">999</span><span class="p">}</span>
<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>

<span class="o">=#</span> <span class="k">SELECT</span> <span class="n">memstore</span><span class="p">.</span><span class="k">set</span><span class="p">(</span><span class="s1">'key-2'</span><span class="p">,</span> <span class="s1">'{"b": [1, 2, 3]}'</span><span class="p">);</span>
 <span class="k">set</span>
<span class="c1">-----</span>
 <span class="n">f</span>
<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>

<span class="o">=#</span> <span class="k">SELECT</span> <span class="o">*</span> <span class="k">from</span> <span class="n">memstore</span><span class="p">.</span><span class="n">list</span><span class="p">();</span>
       <span class="k">key</span>      <span class="o">|</span>      <span class="n">value</span>
<span class="c1">----------------+------------------</span>
 <span class="err">\</span><span class="n">x6b65792d317e</span> <span class="o">|</span> <span class="p">{</span><span class="nv">"a"</span><span class="p">:</span> <span class="mi">999</span><span class="p">}</span>
 <span class="err">\</span><span class="n">x6b65792d327e</span> <span class="o">|</span> <span class="p">{</span><span class="nv">"b"</span><span class="p">:</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">]}</span>
<span class="p">(</span><span class="mi">2</span> <span class="k">rows</span><span class="p">)</span>

<span class="o">=#</span> <span class="k">SELECT</span> <span class="n">memstore</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="s1">'key-2'</span><span class="p">);</span>
 <span class="k">delete</span>
<span class="c1">--------</span>
 <span class="n">t</span>
<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>

<span class="o">=#</span> <span class="k">SELECT</span> <span class="o">*</span> <span class="k">from</span> <span class="n">memstore</span><span class="p">.</span><span class="n">list</span><span class="p">();</span>
       <span class="k">key</span>      <span class="o">|</span>      <span class="n">value</span>
<span class="c1">----------------+------------------</span>
 <span class="err">\</span><span class="n">x6b65792d317e</span> <span class="o">|</span> <span class="p">{</span><span class="nv">"a"</span><span class="p">:</span> <span class="mi">999</span><span class="p">}</span>
<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>
</code></pre></div></div>

<p>共有メモリ上に作られているので、他のセッションからもアクセス可能です：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">=#</span> <span class="k">SELECT</span> <span class="n">memstore</span><span class="p">.</span><span class="k">get</span><span class="p">(</span><span class="s1">'key-1'</span><span class="p">);</span>
    <span class="k">get</span>
<span class="c1">------------</span>
 <span class="p">{</span><span class="nv">"a"</span><span class="p">:</span> <span class="mi">999</span><span class="p">}</span>
<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>

<span class="o">=#</span> <span class="err">\</span><span class="k">c</span> <span class="o">-</span>
<span class="o">=#</span> <span class="k">SELECT</span> <span class="n">memstore</span><span class="p">.</span><span class="k">get</span><span class="p">(</span><span class="s1">'key-1'</span><span class="p">);</span>
    <span class="k">get</span>
<span class="c1">------------</span>
 <span class="p">{</span><span class="nv">"a"</span><span class="p">:</span> <span class="mi">999</span><span class="p">}</span>
<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>
</code></pre></div></div>

<p>メモリ使用量を確認することもできます：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">=#</span> <span class="k">SELECT</span> <span class="n">memstore</span><span class="p">.</span><span class="n">memory_usage</span><span class="p">();</span>
 <span class="n">memory_usage</span>
<span class="c1">--------------</span>
       <span class="mi">262144</span>
<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">memstore.save()</code>でディスク上にダンプし、<code class="language-plaintext highlighter-rouge">memstore.load()</code>でロードすることも可能です。</p>

<p>また、<code class="language-plaintext highlighter-rouge">pg_memstore.wal_logging = true</code>に設定すると、<code class="language-plaintext highlighter-rouge">memstore.set()</code>と<code class="language-plaintext highlighter-rouge">memstore.delete()</code>の情報がWALに書かれるので、レプリケーションのスタンバイサーバでも同じデータを持つことができます。</p>

<h2 id="実装してみた所感">実装してみた所感</h2>

<p><code class="language-plaintext highlighter-rouge">pg_memstore</code>を実装したことで2つPostgreSQLのバグを見つけることができました。1つは<a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=724890ffb75c703afc1e0287f5a66b94c2998799">修正済み</a>ですが、もう一つは<a href="https://www.postgresql.org/message-id/CAD21AoBB2U47V%3DF%2BwQRB1bERov_of5%3DBOZGaybjaV8FLQyqG3Q%40mail.gmail.com">議論中</a>です。READMEにも書いてありますが、PostgreSQL本体でこのバグが直るまで、<code class="language-plaintext highlighter-rouge">memstore.list()</code>と<code class="language-plaintext highlighter-rouge">memstore.save()</code>は使えません。</p>

<p>キーバリューの情報をシャットダウン時にディスクに保存するために「CHECKPOINT時やサーバシャットダウン時に、DSA上に作成したデータをファイルに書く」みたいな挙動をやりたかったのですが、現在のPostgreSQLではできなさそうです。最初に検討したのは、「シャットダウン時にradix treeに入っているデータを一つずつファイルに書く」ですが、これはpostmasterがやる必要があります。しかし、現在postmasterがDSA上のデータを触ることはできないようです（正確にはその内部で使っているDSMにアクセスできない）。次に、checkpointerがCHECKPOINT時にそれをやる方法を考えたのですが、現在CHECKPOINT時にhook等でコードを入れ込むことはできません、この辺りは将来改善できるかもなと思いました。</p>]]></content><author><name>Masahiko Sawada</name></author><category term="PostgreSQL" /><category term="radixtree" /><summary type="html"><![CDATA[PostgreSQL 17で新しく実装されたradix tree（radixtree.h）を使って、インメモリのキーバリューストア拡張 pg_memstore を作ってみました。Vacuumの高速化を支えたradix treeの使い方を実例で解説します。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://masahikosawada.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://masahikosawada.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="ja"><title type="html">PostgreSQLでmusl libcを使う方法</title><link href="https://masahikosawada.github.io/ja/2024/10/14/musl-libc-postgresql/" rel="alternate" type="text/html" title="PostgreSQLでmusl libcを使う方法" /><published>2024-10-14T00:00:00+09:00</published><updated>2024-10-14T00:00:00+09:00</updated><id>https://masahikosawada.github.io/2024/10/14/musl-libc-postgresql</id><content type="html" xml:base="https://masahikosawada.github.io/2024/10/14/musl-libc-postgresql/"><![CDATA[<p>有名な標準Cライブラリの実装はglibc。<a href="https://www.musl-libc.org/">musl libc</a>(マッスル)は別の標準Cライブラリの実装。ライセンスはMITライセンスで、シンプルな実装、小さいバイナリ、が特徴らしい。Alpine Linuxでも使われているとの事。</p>

<p>glibcとの比較は<a href="https://www.etalabs.net/compare_libcs.html">こちら</a>。</p>

<p>このmusl libcを使うPostgreSQLをビルドする方法のメモ。</p>

<h2 id="musl-libcの準備">musl libcの準備</h2>

<p>ソースコードは<a href="https://musl.libc.org/">ここ</a>から取得可能。<a href="https://musl.cc/">musl.cc</a>というビルド済みのバイナリをダウンロード出来る所もあるらしいが、今回はソースコードをビルドした。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>wget https://musl.libc.org/releases/musl-1.2.5.tar.gz
<span class="nv">$ </span><span class="nb">tar </span>zxf musl-1.2.5.tar.gz
<span class="nv">$ </span><span class="nb">cd </span>musl-1.2.5
<span class="nv">$ </span>./configure <span class="nt">--prefix</span><span class="o">=</span>/home/masahiko/musl <span class="nt">--syslibdir</span><span class="o">=</span>/home/masahiko/musl-lib/
<span class="nv">$ </span>make
<span class="nv">$ </span>make <span class="nb">install</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--prefix</code>と<code class="language-plaintext highlighter-rouge">--syslibdir</code>オプションで、musl libcがインストールされるディレクトリとダイナミックリンカがインストールされるディレクトリを指定する。ビルドは20秒程度で終わった。</p>

<p>インストール先の<code class="language-plaintext highlighter-rouge">bin</code>ディレクトリに<code class="language-plaintext highlighter-rouge">musl-gcc</code>というプログラムがあればOK。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">ls</span> /home/masahiko/musl/bin
musl-gcc
</code></pre></div></div>

<p>これはgccのラッパー(中身はシェルスクリプト)で、これを使ってプログラムをコンパイルするとmusl libcにリンクするようになるらしい。</p>

<h2 id="postgresqlのビルド">PostgreSQLのビルド</h2>

<p>開発版のHEADを使ってビルドする。</p>

<h3 id="下準備">下準備</h3>

<p>下準備として、先程インストールした<code class="language-plaintext highlighter-rouge">musl-gcc</code>をPATHに含めておく。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span>/home/masahiko/musl/bin:<span class="nv">$PATH</span>
</code></pre></div></div>

<p>また、<code class="language-plaintext highlighter-rouge">/usr/include/linux</code>、<code class="language-plaintext highlighter-rouge">/usr/include/asm</code>、<code class="language-plaintext highlighter-rouge">/usr/include/asm-generic</code>をディレクトリごと、musl libcをインストールしたところにコピーする。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cd</span> /home/masahiko/musl/include
<span class="nv">$ </span><span class="nb">cp</span> <span class="nt">-rs</span> /usr/include/linux linux/
<span class="nv">$ </span><span class="nb">cp</span> <span class="nt">-rs</span> /usr/include/asm asm/
<span class="nv">$ </span><span class="nb">cp</span> <span class="nt">-rs</span> /usr/include/asm-generic asm-generic/
</code></pre></div></div>

<p>これをする理由は後ほど。</p>

<h3 id="ビルド">ビルド</h3>

<p>PostgreSQLのソースコードをダウンロードして、ビルドする。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git clone git://git.postgresql.org/git/postgresql.git
<span class="nv">$ </span><span class="nb">cd </span>postgresql
<span class="nv">$ </span>./configure <span class="nt">--prefix</span><span class="o">=</span>/home/masahiko/pgsql <span class="nv">CC</span><span class="o">=</span>musl-cc <span class="nt">--without-readline</span> <span class="nt">--without-icu</span> <span class="nt">--witout-zlib</span>
<span class="nv">$ </span>make
<span class="nv">$ </span>make <span class="nb">install</span>
</code></pre></div></div>

<h3 id="ccmusl-ccについて"><code class="language-plaintext highlighter-rouge">CC=musl-cc</code>について</h3>

<p>CCには使用するコンパイラを指定できる。</p>

<h3 id="--without-xxxの指定について"><code class="language-plaintext highlighter-rouge">--without-XXX</code>の指定について</h3>

<p>PostgreSQLがデフォルトでreadline, zlib, icuを有効にしてビルドする（configureの場合）。例えば、readlineのヘッダファイルは<code class="language-plaintext highlighter-rouge">/usr/include/readline</code>にあるけど、<code class="language-plaintext highlighter-rouge">/usr/include</code>にはglibcのヘッダファイルもある。なので<code class="language-plaintext highlighter-rouge">/usr/include</code>を探しに行くように設定すると<code class="language-plaintext highlighter-rouge">musl-gcc</code>を使ったビルドができなかった。なので、これらのライブラリは無効にした状態でビルドする。今回はお試しなのでこれでOK。</p>

<p>下準備として<code class="language-plaintext highlighter-rouge">/usr/include/linux</code>等をディレクトリごとコピーしたのはこれに対応するため。これは<a href="https://www.openwall.com/lists/musl/2017/11/23/1">推奨された方法ではない</a>らしいけど今回はこれでOKとした。おそらく、readline等も同じようにすればビルド出来るのだと思う。</p>

<p>readline等はなくてもPostgreSQLのビルドは出来るけど、<code class="language-plaintext highlighter-rouge">/usr/include/linux</code>等はPostgreSQLのビルドに必須だったので今回はこの方法を使った。これがないと、自分の環境は以下のエラーが出た。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>musl-gcc <span class="nt">-Wall</span> <span class="nt">-Wmissing-prototypes</span> <span class="nt">-Wpointer-arith</span> <span class="nt">-Wdeclaration-after-statement</span> <span class="nt">-Werror</span><span class="o">=</span>vla <span class="nt">-Wendif-labels</span> <span class="nt">-Wmissing-format-attribute</span> <span class="nt">-Wimplicit-fallthrough</span><span class="o">=</span>3 <span class="nt">-Wcast-function-type</span> <span class="nt">-Wshadow</span><span class="o">=</span>compatible-
<span class="nb">local</span> <span class="nt">-Wformat-security</span> <span class="nt">-fno-strict-aliasing</span> <span class="nt">-fwrapv</span> <span class="nt">-fexcess-precision</span><span class="o">=</span>standard <span class="nt">-Wno-format-truncation</span> <span class="nt">-Wno-stringop-truncation</span> <span class="nt">-O2</span> <span class="nt">-I</span>../../../src/interfaces/libpq <span class="nt">-I</span>../../../src/include  <span class="nt">-D_GNU_SOURCE</span>
   <span class="nt">-c</span> <span class="nt">-o</span> pg_combinebackup.o pg_combinebackup.c
pg_combinebackup.c:24:10: fatal error: linux/fs.h: No such file or directory
   24 | <span class="c">#include &lt;linux/fs.h&gt;</span>
      |          ^~~~~~~~~~~~
</code></pre></div></div>

<h3 id="ビルド中のwarning">ビルド中のWARNING</h3>

<p>自分の環境では、以下のようなWARNINGが出た。ビルド自体はできたので問題なし。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pg_get_line.c: In <span class="k">function </span>_pg_get_line_append_:
pg_get_line.c:129:27: warning: _<span class="o">({</span>anonymous<span class="o">})</span>_ may be used uninitialized <span class="o">[</span><span class="nt">-Wmaybe-uninitialized</span><span class="o">]</span>
  129 |         <span class="k">if</span> <span class="o">(</span>prompt_ctx <span class="o">&amp;&amp;</span> sigsetjmp<span class="o">(</span><span class="k">*</span><span class="o">((</span>sigjmp_buf <span class="k">*</span><span class="o">)</span> prompt_ctx-&gt;jmpbuf<span class="o">)</span>, 1<span class="o">)</span> <span class="o">!=</span> 0<span class="o">)</span>
      |                           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
</code></pre></div></div>

<h2 id="確認">確認</h2>

<p>PostgreSQLがビルドできたら、ちゃんとmusl libcにリンクするようになっているかを確認する。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ldd bin/postgres
        linux-vdso.so.1 <span class="o">(</span>0x00007ffd0bdf6000<span class="o">)</span>
        libc.so <span class="o">=&gt;</span> /home/masahiko/musl/lib/libc.so <span class="o">(</span>0x00007f21be639000<span class="o">)</span>
</code></pre></div></div>

<p>musl libcはglibcに完全互換しているわけではなく、<a href="https://wiki.musl-libc.org/functional-differences-from-glibc.html">若干挙動が違ったりする</a>とのことだけど、<code class="language-plaintext highlighter-rouge">make check-world</code>も通って一安心。</p>

<p>musl libcの方がライブラリとしてはglibcより小さいけど、性能はglibcの方が早いとのことなので、性能面も比較してみたい。</p>]]></content><author><name>Masahiko Sawada</name></author><category term="PostgreSQL" /><summary type="html"><![CDATA[標準Cライブラリにglibcではなくmusl libcを使ってPostgreSQLをビルドする方法のメモです。musl libcの準備からconfigureのオプション指定、ビルド時にはまった点までを紹介します。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://masahikosawada.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://masahikosawada.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="ja"><title type="html">PostgreSQLで「圧縮＋読み取り専用テーブル」をテーブルAMを使って自作してみた</title><link href="https://masahikosawada.github.io/ja/2024/03/07/Implementing_Table_AM_for_Archving_Tables/" rel="alternate" type="text/html" title="PostgreSQLで「圧縮＋読み取り専用テーブル」をテーブルAMを使って自作してみた" /><published>2024-03-07T00:00:00+09:00</published><updated>2024-03-07T00:00:00+09:00</updated><id>https://masahikosawada.github.io/2024/03/07/Implementing_Table_AM_for_Archving_Tables</id><content type="html" xml:base="https://masahikosawada.github.io/2024/03/07/Implementing_Table_AM_for_Archving_Tables/"><![CDATA[<p>先日、<a href="https://github.com/MasahikoSawada/pgroad">pgroad</a>という趣味で作っているPostgreSQL用のTable AMを公開しました。pgroadは、PostgreSQLに新しいテーブル形式であるroadを追加する拡張機能です。roadはRead Only Archived Dataの略で、使用頻度が低くなったけど削除はできない既存のテーブルを、コンパクトな読み取り専用テーブルに変換する形で使います。</p>

<p><strong>趣味実装かつ、作成途中なのでプロダクション環境では利用しないでください！</strong></p>

<h2 id="動作サンプル">動作サンプル</h2>

<p><code class="language-plaintext highlighter-rouge">CREATE EXTENSION</code>で<code class="language-plaintext highlighter-rouge">pgroad</code>をデータベースに登録します(あらかじめ<code class="language-plaintext highlighter-rouge">shared_preload_libraies</code>へ<code class="language-plaintext highlighter-rouge">pgroad</code>の追加する必要があります)。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>=# CREATE EXTENSION pgroad;
CREATE EXTENSION
</code></pre></div></div>

<p>サンプルデータとしてpgbenchでテーブルを作成します。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ pgbench -i -s 100
dropping old tables...
creating tables...
generating data (client-side)...
vacuuming...
creating primary keys...
done in 10.43 s (drop tables 0.00 s, create tables 0.01 s, client-side generate 7.41 s, vacuum 0.16 s, primary keys 2.86 s).
$ psql
=# \dt+ pgbench_accounts
                                         List of relations
 Schema |       Name       | Type  |  Owner   | Persistence | Access method |  Size   | Description
--------+------------------+-------+----------+-------------+---------------+---------+-------------
 public | pgbench_accounts | table | masahiko | permanent   | heap          | 1281 MB |
(1 row)
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">pgbench_accounts</code>テーブルは現在<code class="language-plaintext highlighter-rouge">heap</code>テーブルですが、これを<code class="language-plaintext highlighter-rouge">road</code>テーブルに変換します。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>=# ALTER TABLE pgbench_accounts SET ACCESS METHOD road;
ALTER TABLE
=# \dt+ pgbench_accounts
                                         List of relations
 Schema |       Name       | Type  |  Owner   | Persistence | Access method |  Size  | Description
--------+------------------+-------+----------+-------------+---------------+--------+-------------
 public | pgbench_accounts | table | masahiko | permanent   | road          | 118 MB |
(1 row)
</code></pre></div></div>

<p>テーブルサイズが1281MB → 118MBに小さくなったことがわかります。一度<code class="language-plaintext highlighter-rouge">road</code>テーブルへ変換すると、新しいデータを変更、追加、削除することはできません。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>=# DELETE FROM pgbench_accounts ;
ERROR:  road_tuple_delete is not supported
=# INSERT INTO pgbench_accounts (aid) VALUES (1);
ERROR:  cannot insert tuple directly into a ROAD table
HINT:  Use ALTER TABLE ... SET ACCESS METHOD or CREATE TABLE ... AS to insert tuples
</code></pre></div></div>

<h2 id="アーキテクチャ">アーキテクチャ</h2>

<p>作りは非常に単純です。既存のテーブルをスキャンしながら、16KBページ（メモリ上）にタプルを格納していき、ページが満杯になったら圧縮して<code class="language-plaintext highlighter-rouge">road</code>テーブルに書き込みます。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+-------------------+     +------------+  compression   +---------+
|                   | --&gt; |   chunk    | -------------&gt; |xxxxxxxxx| \
|                   |     |  (16kB)    |                +---------+   \
|                   |     +------------+                                \     +--------------------+
|  original table   |                                                    \    |                    |
| (heap, 8kB pages) |     +------------+  compression   +---------+       `-&gt; |     new table      |
|                   | --&gt; |   chunk    | -------------&gt; |xxxxxxxxx| --------&gt; |  (road, 8kB pages) |
|                   |     |  (16kB)    |                +---------+       .-&gt; |                    |
|                   |     +------------+                                 /    |                    |
|                   |                                                   /     +--------------------+
|                   |     +------------+ compression    +---------+   /
|                   | --&gt; |   chunk    | -------------&gt; |xxxxxxxxx| /
|                   |     |  (16kB)    |                +---------+
|                   |     +------------+
+-------------------+
</code></pre></div></div>

<p>今のところ<code class="language-plaintext highlighter-rouge">road</code>テーブル内のタプルはHeap Tupleを利用しています。ですが、Heap Tupleのヘッダには<code class="language-plaintext highlighter-rouge">road</code>が必要としていないデータがいくつかあるので(xminやxmaxなど)、独自のタプルフォーマットをサポートしてみたいなと思っています。</p>

<p>圧縮方法はデフォルトは<code class="language-plaintext highlighter-rouge">pglz</code>。PostgreSQL本体が対応していれば<code class="language-plaintext highlighter-rouge">lz4</code>も指定可能です。</p>

<h2 id="サポートしている機能">サポートしている機能</h2>

<ul>
  <li>テーブルの作成</li>
  <li>インデックスの作成（BRINは未対応）</li>
  <li>検索
    <ul>
      <li>Seq Scan</li>
      <li>Index Scan</li>
    </ul>
  </li>
  <li>TOAST</li>
  <li>WAL</li>
</ul>

<h2 id="roadテーブルの作成">ROADテーブルの作成</h2>

<p>「既存のテーブルを<code class="language-plaintext highlighter-rouge">road</code>へ変換する」というユースケースに絞って作成したので、<code class="language-plaintext highlighter-rouge">road</code>テーブルを作成する方法は以下の2つ限られています：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ALTER TABLE ... SET ACCESS METHOD road</code></li>
  <li><code class="language-plaintext highlighter-rouge">CREATE TABLE ... USING road AS ...</code></li>
</ul>

<p>また、トランザクション内では作成できません。</p>

<h3 id="processutility_hookの利用">ProcessUtility_hookの利用</h3>

<p>特定のDDLコマンドが実行されたかどうかを知るには<code class="language-plaintext highlighter-rouge">ProcessUtility_hook</code>が利用できます。PostgreSQLにはhookポイントと呼ばれる箇所がいくつかあり、拡張機能内で自身の関数を差し込むことが可能です。<code class="language-plaintext highlighter-rouge">ProcessUtility_hook</code>はPostgreSQLが提供するHookポイントの一つで、DDLが実行されるときに呼ばれます。</p>

<p>pgroadではこの<code class="language-plaintext highlighter-rouge">ProcessUtility_hook</code>を使って、実行されたSQLが<code class="language-plaintext highlighter-rouge">CREATE TABLE AS</code>もしくは<code class="language-plaintext highlighter-rouge">ALTER TABLE ... SET ACCESS METHOD road</code>かどうかを判別しています。</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">void</span>
<span class="nf">road_ProcessUtility</span><span class="p">(</span><span class="n">PlannedStmt</span> <span class="o">*</span><span class="n">pstmt</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">queryString</span><span class="p">,</span>
                    <span class="n">bool</span> <span class="n">readOnlyTree</span><span class="p">,</span>
                    <span class="n">ProcessUtilityContext</span> <span class="n">context</span><span class="p">,</span> <span class="n">ParamListInfo</span> <span class="n">params</span><span class="p">,</span>
                    <span class="n">QueryEnvironment</span> <span class="o">*</span><span class="n">queryEnv</span><span class="p">,</span>
                    <span class="n">DestReceiver</span> <span class="o">*</span><span class="n">dest</span><span class="p">,</span> <span class="n">QueryCompletion</span> <span class="o">*</span><span class="n">qc</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">NodeTag</span>     <span class="n">tag</span> <span class="o">=</span> <span class="n">nodeTag</span><span class="p">(</span><span class="n">pstmt</span><span class="o">-&gt;</span><span class="n">utilityStmt</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">tag</span> <span class="o">==</span> <span class="n">T_CreateTableAsStmt</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">RoadInsertState</span><span class="p">.</span><span class="n">called_in_ctas</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
        <span class="n">Assert</span><span class="p">(</span><span class="o">!</span><span class="n">RoadInsertState</span><span class="p">.</span><span class="n">called_in_atsam</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">tag</span> <span class="o">==</span> <span class="n">T_AlterTableStmt</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">AlterTableStmt</span> <span class="o">*</span><span class="n">atstmt</span> <span class="o">=</span> <span class="p">(</span><span class="n">AlterTableStmt</span> <span class="o">*</span><span class="p">)</span> <span class="n">pstmt</span><span class="o">-&gt;</span><span class="n">utilityStmt</span><span class="p">;</span>
        <span class="n">ListCell</span>   <span class="o">*</span><span class="n">cell</span><span class="p">;</span>

        <span class="n">foreach</span><span class="p">(</span><span class="n">cell</span><span class="p">,</span> <span class="n">atstmt</span><span class="o">-&gt;</span><span class="n">cmds</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">AlterTableCmd</span> <span class="o">*</span><span class="n">cmd</span> <span class="o">=</span> <span class="p">(</span><span class="n">AlterTableCmd</span> <span class="o">*</span><span class="p">)</span> <span class="n">lfirst</span><span class="p">(</span><span class="n">cell</span><span class="p">);</span>

            <span class="k">if</span> <span class="p">(</span><span class="n">cmd</span><span class="o">-&gt;</span><span class="n">subtype</span> <span class="o">==</span> <span class="n">AT_SetAccessMethod</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="n">Relation</span>    <span class="n">rel</span> <span class="o">=</span> <span class="n">relation_openrv</span><span class="p">(</span><span class="n">atstmt</span><span class="o">-&gt;</span><span class="n">relation</span><span class="p">,</span> <span class="n">ShareLock</span><span class="p">);</span>

                <span class="cm">/*
                 * Are we about to change the access method of the relation to
                 * ROAD table AM?
                 */</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">strcmp</span><span class="p">(</span><span class="n">cmd</span><span class="o">-&gt;</span><span class="n">name</span><span class="p">,</span> <span class="s">"road"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span>
                <span class="p">{</span>
                    <span class="cm">/* Remember the original table's OID */</span>
                    <span class="n">RoadInsertState</span><span class="p">.</span><span class="n">atsam_relid</span> <span class="o">=</span> <span class="n">RelationGetRelid</span><span class="p">(</span><span class="n">rel</span><span class="p">);</span>

                    <span class="n">RoadInsertState</span><span class="p">.</span><span class="n">called_in_atsam</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
                <span class="p">}</span>

                <span class="n">RelationClose</span><span class="p">(</span><span class="n">rel</span><span class="p">);</span>

                <span class="k">break</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="n">Assert</span><span class="p">(</span><span class="o">!</span><span class="n">RoadInsertState</span><span class="p">.</span><span class="n">called_in_ctas</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="n">prev_ProcessUtility</span><span class="p">(</span><span class="n">pstmt</span><span class="p">,</span> <span class="n">queryString</span><span class="p">,</span> <span class="nb">false</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span>
                        <span class="n">params</span><span class="p">,</span> <span class="n">queryEnv</span><span class="p">,</span> <span class="n">dest</span><span class="p">,</span> <span class="n">qc</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>この関数で「どのようなDDLが実行されたのか」を覚えておき、後に呼ばれるINSERTのためのコールバックで実際のエラーを出しています。</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">void</span>
<span class="nf">road_tuple_insert</span><span class="p">(</span><span class="n">Relation</span> <span class="n">relation</span><span class="p">,</span> <span class="n">TupleTableSlot</span> <span class="o">*</span><span class="n">slot</span><span class="p">,</span>
                  <span class="n">CommandId</span> <span class="n">cid</span><span class="p">,</span> <span class="kt">int</span> <span class="n">options</span><span class="p">,</span> <span class="n">BulkInsertState</span> <span class="n">bistate</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">RoadInsertStateData</span> <span class="o">*</span><span class="n">state</span><span class="p">;</span>
    <span class="n">ItemPointerData</span> <span class="n">tid</span><span class="p">;</span>
    <span class="n">RowNumber</span>   <span class="n">rownum</span><span class="p">;</span>
    <span class="n">bool</span>        <span class="n">shouldFree</span><span class="p">;</span>
    <span class="n">HeapTuple</span>   <span class="n">tuple</span> <span class="o">=</span> <span class="n">ExecFetchSlotHeapTuple</span><span class="p">(</span><span class="n">slot</span><span class="p">,</span> <span class="nb">true</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">shouldFree</span><span class="p">);</span>

    <span class="n">state</span> <span class="o">=</span> <span class="n">road_get_insert_state</span><span class="p">(</span><span class="n">relation</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="n">state</span><span class="o">-&gt;</span><span class="n">called_in_atsam</span> <span class="o">||</span> <span class="n">state</span><span class="o">-&gt;</span><span class="n">called_in_ctas</span><span class="p">))</span>
        <span class="n">ereport</span><span class="p">(</span><span class="n">ERROR</span><span class="p">,</span>
                <span class="p">(</span><span class="n">errmsg</span><span class="p">(</span><span class="s">"cannot insert tuple directly into a ROAD table"</span><span class="p">),</span>
                 <span class="n">errhint</span><span class="p">(</span><span class="s">"Use %s or %s to insert tuples"</span><span class="p">,</span>
                         <span class="s">"ALTER TABLE ... SET ACCESS METHOD"</span><span class="p">,</span>
                         <span class="s">"CREATE TABLE ... AS"</span><span class="p">)));</span>
</code></pre></div></div>

<h2 id="最後に趣味テーブルamのすすめ">最後に：趣味テーブルAMのすすめ</h2>

<p>PostgreSQLの自作テーブルAMの実体は「コールバックのまとまり」であり、自作テーブルAMがサポートしたいテーブルに関する機能（例えば、シーケンシャルスキャン、インデックススキャン、インデックス構築など）に応じて、必要なコールバックを実装する必要があります。そして、テーブルAMはPostgreSQLの中でうまく抽象化されており、他のコンポーネントとは独立して実装することが可能です。PostgreSQL本体がトランザクションやバッファマネージャなどの機能を提供しているので、テーブルAM内でそれを使うか使わないかは、実装者が選択することができます。例えば、PostgreSQL本体のバッファマネージャの機能を利用することで、テーブルAM開発者は、共有バッファより下の層を意識することなく実装できます。</p>

<p>ただし、テーブルAMを実装するとなると、コールバックの実装以外にも考えることはたくさんあります。</p>

<ul>
  <li>データを保存する形式はどうするか？
    <ul>
      <li>行指向、列指向、ページフォーマットなど</li>
    </ul>
  </li>
  <li>テーブルが同時に更新された場合どうするか？
    <ul>
      <li>ロックの粒度（行レベル、テーブルレベルなど）</li>
      <li>ロックマネージャ自体はPostgreSQL本体を提供するものを利用できます</li>
    </ul>
  </li>
  <li>リカバリに対応するのか？
    <ul>
      <li>レプリケーションへの対応も一緒に考得る必要があるかも</li>
    </ul>
  </li>
  <li>トランザクションの途中で失敗、またはROLLBACKした時どうするか？
    <ul>
      <li>Vacuumみたいに後でまとめてゴミを回収する、それともundoログ的なものを実装する、など</li>
    </ul>
  </li>
  <li>ページサイズよりも大きいデータへの対応
    <ul>
      <li>PostgreSQL本体が提供するTOASTの仕組みを使うことは可能</li>
    </ul>
  </li>
</ul>

<p>これらをじっくり考えて理想のテーブルAMを作るもの良しですが、とりあえず作ってみたいという場合は、ユースケースをできるだけ絞ることをおすすめします。ユースケースを絞ることで制約が増えますが、考慮しなくてはいけないことが減り、実装量も減り、実装もシンプルになり易いです。まずは「単純で実用的ではないかもしれないけど動く物」を作ることで、モチベーションも上がります。テーブルAMに限らずですが、手を動かして実装してみるその過程自体が一番大切なので、実際中身は何でも良いと思います。</p>

<p>pgroadは以下のような制約があります。思い切ってこれらの制約をつけることで実装が格段に楽になりました。</p>

<ul>
  <li>テーブルの作成は既存のテーブルからの変更のみ
    <ul>
      <li>既存テーブルに排他ロックを取った状態でしかroadテーブルはつくられない
        <ul>
          <li>同時更新を考慮しなくて良い</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>明示的なトランザクションブロック内では作れない
    <ul>
      <li>途中で失敗したらテーブル全体がなくなる</li>
      <li>SAVEPOINTも気にしなくて良い</li>
      <li>CURORやCommandIdも気にしなくて良い</li>
    </ul>
  </li>
  <li>読み取り専用（INSERT、UPDATE、DELETEを禁止）
    <ul>
      <li>実装するコールバックが減る</li>
      <li>更新途中でトランザクションがAbortした場合を考えなくて良い</li>
      <li>テーブル内にゴミ（不可視なタプル）が存在することはない</li>
    </ul>
  </li>
</ul>

<p>pgroadはアーカイブ用のTable AMでしたが、他にも実装してみると面白そうなアイディアがいくつかあります。</p>

<ul>
  <li>自動的に全てのタプルにRowIDが付くテーブル</li>
  <li>Heapがベースだけど<a href="https://www.pdl.cmu.edu/PDL-FTP/Database/pax.pdf">PAXページ</a>を実装したテーブル</li>
  <li>WALしか出さないテーブル</li>
</ul>

<p>などなど。</p>

<p>ぜひ自作Table AMの作成を通して、PostgreSQLの拡張機能開発に挑戦してみてください！</p>]]></content><author><name>Masahiko Sawada</name></author><category term="PostgreSQL" /><category term="Table AM" /><summary type="html"><![CDATA[PostgreSQLのTable Access Method（Table AM）を使って、圧縮された読み取り専用テーブルを追加する拡張機能 pgroad を自作しました。Table AMのコールバック実装と、アーカイブ用途での使い方を解説します。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://masahikosawada.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://masahikosawada.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="ja"><title type="html">PostgreSQLでトランザクションIDをできるだけ早く消費する方法</title><link href="https://masahikosawada.github.io/ja/2023/12/21/Burning-XIDs/" rel="alternate" type="text/html" title="PostgreSQLでトランザクションIDをできるだけ早く消費する方法" /><published>2023-12-21T00:00:00+09:00</published><updated>2023-12-21T00:00:00+09:00</updated><id>https://masahikosawada.github.io/2023/12/21/Burning-XIDs</id><content type="html" xml:base="https://masahikosawada.github.io/2023/12/21/Burning-XIDs/"><![CDATA[<p>本記事は<a href="https://qiita.com/advent-calendar/2023/postgresql">PostgreSQL Advent Calendar 2023</a> 21日目の記事です。</p>

<p>PostgreSQLのトランザクションID（以下XID）は内部的には「単調増加する32bitの非負整数値」なので、2^32-1に達した後は0に戻ります。PostgreSQLでは、データベースに変更を加えるトランザクションに対して1つXIDが割り当てられます。XIDの大小関係を利用してテーブル内の行の可視性判断[^visibility]をしているので、XIDが上限に達して周回してしまうとそのロジックが壊れてしまいます。</p>

<p>そこでPostgreSQLでは周回が起こる前（具体的にはXIDを2^31(≒約20億)個消費する前）に、aggressive vacuumと呼ばれる安全装置のようなものが自動的に走り、XIDを更に消費できるようにします（詳細は<a href="https://www.postgresql.jp/document/14/html/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND">公式ドキュメント</a>をご参照ください）。ここ数年、この安全装置周りの改善に取り組んでいたので、安全装置自体をテストや、安全装置の動作完了が間に合わなかった場合のテストなどすることが多くありました。ただ、XIDと安全装置の性質上、XIDを大量に消費しないと適切なテストができず、これには時間がかかります。</p>

<p>例えばナイーブにやろうとすると、約20億のトランザクションを実行する必要があります。また、新しいXIDを発行する際には各XIDにステータスを持つようなデータ、例えばCLOGやCommit Timestampなどの領域も合わせて拡張されます。それらの動作をテストするときにも、実際にXIDを消費することが重要になる時があります。</p>

<p>そこで、早くXID消費するための色々な方法を紹介します。</p>

<h2 id="1-ストアドプロシージャを使ってトランザクションを消費する">1. ストアドプロシージャを使ってトランザクションを消費する</h2>

<p>次のようなユーザ定義関数を使って10億XIDを消費します。内部的にはサブトランザクションを10億個生成することで、XIDを消費しています。</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">PROCEDURE</span> <span class="n">consume_xids</span><span class="p">(</span><span class="n">cnt</span> <span class="nb">int</span><span class="p">)</span>
<span class="k">AS</span> <span class="err">$$</span>
<span class="k">DECLARE</span> <span class="n">i</span> <span class="nb">int</span><span class="p">;</span>
<span class="k">BEGIN</span>
	<span class="k">FOR</span> <span class="n">i</span> <span class="k">in</span> <span class="mi">1</span><span class="p">..</span><span class="n">cnt</span> <span class="n">LOOP</span>
		<span class="k">EXECUTE</span> <span class="s1">'SELECT txid_current()'</span><span class="p">;</span>
		<span class="k">COMMIT</span><span class="p">;</span>
	<span class="k">END</span> <span class="n">LOOP</span><span class="p">;</span>
<span class="k">END</span><span class="p">;</span>
<span class="err">$$</span>
<span class="k">LANGUAGE</span> <span class="n">plpgsql</span><span class="p">;</span>
</code></pre></div></div>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">=#</span> <span class="k">select</span> <span class="n">pg_current_xact_id</span><span class="p">();</span>

<span class="o">=#</span> <span class="k">call</span> <span class="n">consume_xid</span><span class="p">(</span><span class="mi">100</span><span class="n">_000_000</span><span class="p">);</span>
<span class="nb">Time</span><span class="p">:</span> <span class="mi">797958</span><span class="p">.</span><span class="mi">576</span> <span class="n">ms</span> <span class="p">(</span><span class="mi">13</span><span class="p">:</span><span class="mi">17</span><span class="p">.</span><span class="mi">959</span><span class="p">)</span>

<span class="o">=#</span> <span class="k">select</span> <span class="n">pg_current_xact_id</span><span class="p">();</span>
</code></pre></div></div>

<p>13分と結構時間がかかりました。XIDの生成は排他ロックを取るので並列化しても大きな改善は見込めません。</p>

<h2 id="2-pg_resetwalを使う">2. pg_resetwalを使う</h2>

<p><code class="language-plaintext highlighter-rouge">pg_resetwal</code>というPostgreSQL内部情報を初期化するツールを使い、次のXIDを強制的に設定します。内部情報を書き換えているだけなので、<code class="language-plaintext highlighter-rouge">pg_resetwal</code>は一瞬で完了するはずです。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ psql -c "SELECT pg_current_xact_id()"
$ pg_ctl stop
$ pg_resetwal -x 2000027648
$ pg_ctl start
$ psql -c "SELECT pg_current_xact_id()"
</code></pre></div></div>

<p>XIDを<code class="language-plaintext highlighter-rouge">2000027648</code>に進めるのは、次のXIDをCLOGのページ（8kB）境界にするためです。そうしないと、起動するときにトランザクションのステータス（CommitとかAbortとか）にアクセスできない旨のエラーがでてしまいます。これは、サーバ起動時に現在のXIDのステータスがCLOGページの境界にない場合、そのページ内のまだ使っていないXIDのStatusを0に初期化するがあるためです（詳細は<code class="language-plaintext highlighter-rouge">TrimCLOG()</code>参照）。さらに、XIDをかなり大きくスキップしているのでそれに対応するCLOGページはまだできておらず、ファイルアクセスエラーになります。</p>

<p>これは、pg_resetwalを使うすべてのケースに当てはまるので注意が必要です（ただし通常のユースケースでは、こんなに大きくXIDをスキップさせないので問題にならない）。</p>

<p>CLOGのページ境界に次のXIDを持っていくために、8kBのCLOGページに32768個のXIDについての情報を格納できるので、<code class="language-plaintext highlighter-rouge">32768 * (2000000000/32768 + 1) = 2000027648</code>と計算しています。</p>

<p><code class="language-plaintext highlighter-rouge">pg_resetwal</code>でXIDを大幅にスキップした後にあとは<code class="language-plaintext highlighter-rouge">txid_current()</code>等の関数でXIDを消費します。</p>

<p>簡単に、かつ高速にXIDを進めることができるますが、”リアル”なユースケースではないというのが欠点です。XIDを消費していく中で実行されていく処理（例えばCLOGの拡張など）はスキップしてます。再起動も必要だし、新しいXIDは狙い撃ちする必要があります。ページサイズにも依存します。</p>

<h2 id="3-内部的にxidをスキップする">3. 内部的にXIDをスキップする</h2>

<p>今度は自分で作った関数で内部的にXIDを設定してみます。</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">PG_FUNCTION_INFO_V1</span><span class="p">(</span><span class="n">set_next_xid</span><span class="p">);</span>
<span class="n">Datum</span>
<span class="nf">set_next_xid</span><span class="p">(</span><span class="n">PG_FUNCTION_ARGS</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">TransactionId</span> <span class="n">next_xid</span> <span class="o">=</span> <span class="n">PG_GETARG_TRANSACTIONID</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
    <span class="n">TransactionId</span> <span class="n">xid</span><span class="p">;</span>
    <span class="n">uint32</span> <span class="n">epoch</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">TransactionIdIsNormal</span><span class="p">(</span><span class="n">next_xid</span><span class="p">))</span>
        <span class="n">elog</span><span class="p">(</span><span class="n">ERROR</span><span class="p">,</span> <span class="s">"cannot set invalid transaction id"</span><span class="p">);</span>

    <span class="n">LWLockAcquire</span><span class="p">(</span><span class="n">XidGenLock</span><span class="p">,</span> <span class="n">LW_EXCLUSIVE</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">TransactionIdPrecedes</span><span class="p">(</span><span class="n">next_xid</span><span class="p">,</span>
                              <span class="n">XidFromFullTransactionId</span><span class="p">(</span><span class="n">TransamVariables</span><span class="o">-&gt;</span><span class="n">nextXid</span><span class="p">)))</span>
    <span class="p">{</span>
        <span class="n">LWLockRelease</span><span class="p">(</span><span class="n">XidGenLock</span><span class="p">);</span>
        <span class="n">elog</span><span class="p">(</span><span class="n">ERROR</span><span class="p">,</span> <span class="s">"cannot set transaction id older than the current transaction id"</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="cm">/*
     * If the new XID is past xidVacLimit, start trying to force autovacuum
     * cycles.
     */</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">TransactionIdFollowsOrEquals</span><span class="p">(</span><span class="n">next_xid</span><span class="p">,</span> <span class="n">TransamVariables</span><span class="o">-&gt;</span><span class="n">xidVacLimit</span><span class="p">))</span>
    <span class="p">{</span>
        <span class="cm">/* For safety, we release XidGenLock while sending signal */</span>
        <span class="n">LWLockRelease</span><span class="p">(</span><span class="n">XidGenLock</span><span class="p">);</span>
        <span class="n">SendPostmasterSignal</span><span class="p">(</span><span class="n">PMSIGNAL_START_AUTOVAC_LAUNCHER</span><span class="p">);</span>
        <span class="n">LWLockAcquire</span><span class="p">(</span><span class="n">XidGenLock</span><span class="p">,</span> <span class="n">LW_EXCLUSIVE</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="n">ExtendCLOG</span><span class="p">(</span><span class="n">next_xid</span><span class="p">);</span>
    <span class="n">ExtendCommitTs</span><span class="p">(</span><span class="n">next_xid</span><span class="p">);</span>
    <span class="n">ExtendSUBTRANS</span><span class="p">(</span><span class="n">next_xid</span><span class="p">);</span>

    <span class="cm">/* Construct the new XID */</span>
    <span class="n">epoch</span> <span class="o">=</span> <span class="n">EpochFromFullTransactionId</span><span class="p">(</span><span class="n">TransamVariables</span><span class="o">-&gt;</span><span class="n">nextXid</span><span class="p">);</span>
    <span class="n">xid</span> <span class="o">=</span> <span class="n">XidFromFullTransactionId</span><span class="p">(</span><span class="n">TransamVariables</span><span class="o">-&gt;</span><span class="n">nextXid</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">unlikely</span><span class="p">(</span><span class="n">xid</span> <span class="o">&gt;</span> <span class="n">next_xid</span><span class="p">))</span>
        <span class="o">++</span><span class="n">epoch</span><span class="p">;</span>
    <span class="n">TransamVariables</span><span class="o">-&gt;</span><span class="n">nextXid</span> <span class="o">=</span>
        <span class="n">FullTransactionIdFromEpochAndXid</span><span class="p">(</span><span class="n">epoch</span><span class="p">,</span> <span class="n">next_xid</span><span class="p">);</span>

    <span class="n">LWLockRelease</span><span class="p">(</span><span class="n">XidGenLock</span><span class="p">);</span>

    <span class="n">PG_RETURN_VOID</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>C言語でユーザ定義関数を書く必要があるますが、1つ前の方法とは異なり、サーバの再起動は不要になりました。さらに、新しいXID付近のCLOGやCommitTsも拡張するし、新しく設定するXIDが十分に古い場合は、aggressive vacuumをしてもらうためにautovacuum launcherを起こすようにもなっています。この関数も一瞬で完了するはずです。</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">=#</span> <span class="k">select</span> <span class="n">txid_current</span><span class="p">();</span>
 <span class="n">txid_current</span>
<span class="c1">--------------</span>
          <span class="mi">737</span>
<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>

<span class="nb">Time</span><span class="p">:</span> <span class="mi">0</span><span class="p">.</span><span class="mi">850</span> <span class="n">ms</span>
<span class="o">=#</span> <span class="k">select</span> <span class="n">set_next_xid</span><span class="p">(</span><span class="s1">'999981056'</span><span class="p">::</span><span class="n">xid</span><span class="p">);</span>
 <span class="n">set_next_xid</span>
<span class="c1">-----------lang: jp</span>
<span class="c1">---</span>

<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>

<span class="nb">Time</span><span class="p">:</span> <span class="mi">0</span><span class="p">.</span><span class="mi">483</span> <span class="n">ms</span>
<span class="o">=#</span> <span class="k">select</span> <span class="n">txid_current</span><span class="p">();</span>
 <span class="n">txid_current</span>
<span class="c1">--------------</span>
    <span class="mi">999981056</span>
<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>

<span class="nb">Time</span><span class="p">:</span> <span class="mi">0</span><span class="p">.</span><span class="mi">926</span> <span class="n">ms</span>
</code></pre></div></div>

<p>しかし、<code class="language-plaintext highlighter-rouge">ExtendCLOG()</code>等の関数は、受け取ったXIDがページ内の境界となるようなXIDである場合にのみ対応するページを作成するため、先程の方法と同様、設定するXIDは狙い撃ちする必要があります。また、この関数はXIDを進めているというよりも”ジャンプしている”という感じです。新しく設定したXID周辺のCLOGを拡張することはできますが、そこまでのXIDについては何もしていません。</p>

<h2 id="4-内部的にxidを高速に進める">4. 内部的にXIDを高速に”進める”</h2>

<p>最後に紹介するのは、最近masterブランチ（開発用ブランチ）に<a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=e255b646a16b45823c338dadf787813fc9e191dc">コミット</a>した<code class="language-plaintext highlighter-rouge">xid_wraparound</code>というテスト用の拡張で採用している方法です。</p>

<p><code class="language-plaintext highlighter-rouge">xid_wraparound</code>は、リグレッションテスト用に作られましたが、そこで定義されているSQL関数は、<code class="language-plaintext highlighter-rouge">xid_wraparound</code>の拡張をインストールすればどの環境でも使えます。</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">=#</span> <span class="k">CREATE</span> <span class="n">EXTENSION</span> <span class="n">xid_wraparound</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="n">EXTENSION</span>

<span class="o">=#</span> <span class="err">\</span><span class="n">dx</span> <span class="n">xid_wraparound</span>
                 <span class="n">List</span> <span class="k">of</span> <span class="n">installed</span> <span class="n">extensions</span>
      <span class="n">Name</span>      <span class="o">|</span> <span class="k">Version</span> <span class="o">|</span> <span class="k">Schema</span> <span class="o">|</span>       <span class="n">Description</span>
<span class="c1">----------------+---------+--------+--------------------------</span>
 <span class="n">xid_wraparound</span> <span class="o">|</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span>     <span class="o">|</span> <span class="k">public</span> <span class="o">|</span> <span class="n">Tests</span> <span class="k">for</span> <span class="n">XID</span> <span class="n">wraparound</span>
<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>
<span class="o">=#</span> <span class="err">\</span><span class="n">dx</span><span class="o">+</span> <span class="n">xid_wraparound</span>
<span class="n">Objects</span> <span class="k">in</span> <span class="n">extension</span> <span class="nv">"xid_wraparound"</span>
        <span class="k">Object</span> <span class="n">description</span>
<span class="c1">-----------------------------------</span>
 <span class="k">function</span> <span class="n">consume_xids</span><span class="p">(</span><span class="nb">bigint</span><span class="p">)</span>
 <span class="k">function</span> <span class="n">consume_xids_until</span><span class="p">(</span><span class="n">xid8</span><span class="p">)</span>
<span class="p">(</span><span class="mi">2</span> <span class="k">rows</span><span class="p">)</span>
</code></pre></div></div>

<p>`これまでの方法と異なるのは、XIDを”スキップしながら進めている”という所です。CLOGやCommitTsを拡張する必要があるXID（コード内では”interesting xids”と呼ばれている）周辺では通常通りのやり方でXIDを消費しますが、それ以外のところは1つ前に紹介した方法のように、内部的にXIDを設定してスキップしています。</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">=#</span> <span class="k">select</span> <span class="n">txid_current</span><span class="p">();</span>
 <span class="n">txid_current</span>
<span class="c1">--------------</span>
          <span class="mi">737</span>
<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>

<span class="o">=#</span> <span class="k">select</span> <span class="n">consume_xids</span><span class="p">(</span><span class="s1">'1000000000'</span><span class="p">);</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">10000071</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">10000809</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">20000880</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">20001618</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">30001689</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">30002427</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">40002498</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">40003236</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">50003230</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">50003968</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">60003297</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">60004035</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">70003998</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">70004736</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">80004096</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">80004834</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">90004766</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">90005504</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">100004895</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">100005633</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">110005534</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">110006272</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">120005694</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">120006432</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">130006302</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">130007040</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">140006493</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">140007231</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">150007070</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">150007808</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">160007292</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">160008030</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">170007838</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">170008576</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">180008091</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">180008829</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">190008606</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">190009344</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">200008890</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">200009628</span>
<span class="p">:</span>
<span class="p">:</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">960038174</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">960038912</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">970038423</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">970039161</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">980038942</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">980039680</span>
<span class="n">NOTICE</span><span class="p">:</span>  <span class="n">consumed</span> <span class="mi">990039222</span> <span class="o">/</span> <span class="mi">1000000000</span> <span class="n">XIDs</span><span class="p">,</span> <span class="n">latest</span> <span class="mi">0</span><span class="p">:</span><span class="mi">990039960</span>
 <span class="n">consume_xids</span>
<span class="c1">--------------</span>
   <span class="mi">1000000738</span>
<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>

<span class="nb">Time</span><span class="p">:</span> <span class="mi">2893</span><span class="p">.</span><span class="mi">244</span> <span class="n">ms</span> <span class="p">(</span><span class="mi">00</span><span class="p">:</span><span class="mi">02</span><span class="p">.</span><span class="mi">893</span><span class="p">)</span>
<span class="o">=#</span> <span class="k">select</span> <span class="n">txid_current</span><span class="p">();</span>
 <span class="n">txid_current</span>
<span class="c1">--------------</span>
   <span class="mi">1000000739</span>
<span class="p">(</span><span class="mi">1</span> <span class="k">row</span><span class="p">)</span>
</code></pre></div></div>

<p>実行速度もそれなりに早いです。そこそこはやくXIDを消費しつつ、XID消費に伴う処理も従来通りの方法で行われるので、より”リアル”なXID消費をシミュレーションできます。</p>

<p>autovacuum launcherを起こす処理は入れていないので、autovacuum launcherがaggressive vacuumのために起きるタイミングは、設定したXIDや<code class="language-plaintext highlighter-rouge">autovacuum_naptime</code>に依存します。このあたりは将来変わる可能性があります。</p>

<p>ぜひこの方法を使って快適なXID消費ライフを！</p>]]></content><author><name>Masahiko Sawada</name></author><category term="PostgreSQL" /><category term="Vacuum" /><summary type="html"><![CDATA[PostgreSQLのトランザクションID（XID）をできるだけ早く消費する方法をいくつか紹介します。XID周回とaggressive vacuumのテストのために、約20億トランザクションを現実的な時間で消費するための手法を比較します。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://masahikosawada.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://masahikosawada.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>