diff --git "a/source/_posts/2025/20251006a_PostgreSQL\351\200\243\350\274\211\345\247\213\343\201\276\343\202\212\343\201\276\343\201\231_&_v18\343\201\247\345\257\276\345\277\234\343\201\227\343\201\237UUIDv7\343\201\250v4\343\201\256\346\257\224\350\274\203.md" "b/source/_posts/2025/20251006a_PostgreSQL\351\200\243\350\274\211\345\247\213\343\201\276\343\202\212\343\201\276\343\201\231_&_v18\343\201\247\345\257\276\345\277\234\343\201\227\343\201\237UUIDv7\343\201\250v4\343\201\256\346\257\224\350\274\203.md" new file mode 100644 index 00000000000..61b214bc659 --- /dev/null +++ "b/source/_posts/2025/20251006a_PostgreSQL\351\200\243\350\274\211\345\247\213\343\201\276\343\202\212\343\201\276\343\201\231_&_v18\343\201\247\345\257\276\345\277\234\343\201\227\343\201\237UUIDv7\343\201\250v4\343\201\256\346\257\224\350\274\203.md" @@ -0,0 +1,92 @@ +--- +title: "PostgreSQL連載始まります & v18で対応したUUIDv7とv4の比較" +date: 2025/10/06 00:00:00 +postid: a +tag: + - PostgreSQL + - PostgreSQL18 +category: + - DB +thumbnail: /images/2025/20251006a/thumbnail.png +author: 澁川喜規 +lede: "PostgreSQL 18ではUUIDv7生成に対応しました。今までのUUID v4(完全ランダム)は主キーとして使うと、ソート順で扱おうとするPostgreSQLの内部構造のB-Treeと相性が悪く..." +--- +[PostgreSQL 18がリリース](https://www.postgresql.org/about/news/postgresql-18-released-3142/)されました。気になる新機能やパフォーマンスアップなどが盛りだくさんです。当ブログでは今回のアップデートに限らずデータベース一般ネタも含めた連載記事を執筆します。 + +| 日付 | 執筆者 | タイトル | +| :--- | :--- | :--- | +| 10/6(月) | 澁川喜規 | v18で対応したUUIDv7とv4の比較 | +| 10/7(火) | 山本竜玄 | explainをマスターするぜ | +| 10/8(水) | 真野隼記 | 仮想生成列 | +| 10/9(木) | 岩堀敦 | 「pg_dump」 | +| 10/9(木) | 市川裕也 | 現場で行った性能チューニング | +| 10/10(金) | 村田 靖拓 | 「TBD」 | +| 10/13(月) | 平岡 浩一郎 | 「TBD」 | + +# UUID v7 + +PostgreSQL 18ではUUIDv7生成に対応しました。今までのUUID v4(完全ランダム)は主キーとして使うと、ソート順で扱おうとするPostgreSQLの内部構造のB-Treeと相性が悪く、さまざまなノードへのアクセスが必要になるため、相性が悪いとされていました。[RFC-9562](https://datatracker.ietf.org/doc/rfc9562/)で標準化されたUUID v7は先頭がタイムスタンプであり、キーをソートすると、必ず作成した順序になります。そのため、生成順にデータを挿入したとしてもパフォーマンスが落ちにくくなる、だからいいんだ、ということのようです。 + +```text + 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 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +B-Treeはその名の通り木構造です。ソート順でデータが並ぶため、近いデータへのアクセスであればキャッシュ効率も上がります。 + +スクリーンショット_2025-10-03_18.03.47.png + +* Wikpedia [B木](https://ja.wikipedia.org/wiki/B木)より引用 + +## 実際に検証してみる + +実際に速度が変わるかをプログラムを作って検証しました。Dockerのpostgres:18-trixieイメージに対して検証プログラムから100万行ほどのレコードを投入しています。 + +* 通信自体の往復が支配的にならないように(I/Oの差が出やすいように)、100件ずつ入れるようにした +* UUIDはローカルで生成して送る方式と、DBの関数で生成する方式の両方を試した。ローカル生成は生成時間を抜いた時間で計測した + +検証コードは[こちらに置いてあります](https://github.com/shibukawa/postgresql-uuidbench)。検証コードはアイコンが可愛いと話題のKiroで作成しています。 + +こちらが結果です。アプリで生成してから送る方式だと20%ぐらい時間が短くなりました。DB側で発番する場合はそのぶんちょっと時間が伸びるのでアプリ発番のものと同じグラフに載せない方が良いかもしれませんが、まあだいたい傾向としてはv7の方が早そう、というところが見えました。 + +Macbook AirのM3で計測しましたが、ファンがなく温度が上がると目に見えて性能が変わるので、条件違いのケースを1通り実行して、また繰り返して、というのを3回行って平均しました。まあそんな感じなのであまり細かいスコアは気にしないでください。 + +スクリーンショット_2025-10-03_17.39.45.png + +時間順のデータの範囲アクセスとか、直近のデータを頻繁にアクセスする場合にデータが一部のツリーに集まるので変更したい箇所のディスクキャッシュが効きやすくなりI/Oパフォーマンスがあがります。これは書き込みのときだけではなく、読み込みにも効きます。 + +20レコードを検索するクエリーを1万回投げた時の処理時間の結果が以下のグラフです。100万行のデータのうち直近の5%(5万件)の範囲のIDをランダムに20個ピックアップし、SELECTで探すテストになっています。10%ほどUUID v7を使った方が高速にはなっています。なお、直近1%(1万件)の範囲で探索するとさらに10%ほど早くなりそうでした。 + +スクリーンショット_2025-10-03_18.38.25.png + +なお、「IDがランダムでも、範囲アクセスするキーがインデックスされていれば問題ないのでは?」と思われるかもしれません。たしかに「どのデータがマッチするか」はインデックスが作成されていれば高速にアクセスできるはずですが、インデックスを元に実際のデータを参照する場合にB-Treeをたどって参照するはずで、配置場所が集中していたらそこの部分が高速になる、ということのはずです。 + +## 実はPostgreSQL 18以前でもUUIDv7は使える + +上記の検証プログラムはPostgreSQL 17でもuuid v7のDB発番以外は使えます。 + +UUIDを使うメリットというのは、極めて重複しにくいキーをデータベース以外で作れるという点にあります。SERIALを使う場合、かならずデータベースへのアクセスが必要になります。ウェブフロントエンドから何かデータを登録する、それも一度にコミットできずに何回かに分けてデータ登録を行って完成させるようなケースを考えてみると、フロントエンドでキーを発番してもらい、それを使うのか、フロントエンドだけで発番し、仮データとして登録して最後にまとめてコミット、みたいな方式にするか、という違いが出ます。特に親子関係があり、親のキーを子供に渡さなければならないみたいなケースでやりとりが増えると大変です。 + +UUIDv7を使うケースで、今回のベンチマークアプリのようにv7形式のUUIDを事前に発行してそれをUUID型の主キーのデータのコミットに使うというやり方で、UUIDのメリットを受けつつ、パフォーマンスも劣化を減らすということが可能です。あくまでも、今回v18でできるようになったは「DB側での発番」であって、事前発番なら前のバージョンでも使えます。ここ大事です。 + +なお、今回は非同期I/O対応で高速になったというのも見かけたのですが、同じプログラムで17と18で比べたところ、そこまで大きな差はなかったです。気持ち10%ぐらいは早くなった? + +## まとめ + +UUID v7のパフォーマンスの検証を行いました。v18の目玉機能的に言われることが多いのですが、前節で書いた通り、アプリ側で発番するのであればv18でなく使えます。今回増えたのはあくまでもDB側での発番です。 + +もちろん「v18になったら主キーはどんどんUUID v7にしていこう」というのは早計です。 + +* 人間が目で見て扱う場合にはUUIDはユーザーフレンドリーとは言い難いのでSERIALが良いケースもある +* 型番や社員番号などのビジネス的に意味のあるナチュラルキーがユニーク性が担保できるのであればマスターデータなどではそちらを使うべき +* セキュリティ的に日付でキーの範囲がだいぶ下がってしまうのでIDの推測可能性や絞り込みがだいぶ簡単になってしまうため、挿入や検索が多少劣化してもUUID v4が良いケースもある + diff --git a/source/images/2025/20251006a/thumbnail.png b/source/images/2025/20251006a/thumbnail.png new file mode 100644 index 00000000000..ae538cffb2c Binary files /dev/null and b/source/images/2025/20251006a/thumbnail.png differ diff --git "a/source/images/2025/20251006a/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210_2025-10-03_17.39.45.png" "b/source/images/2025/20251006a/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210_2025-10-03_17.39.45.png" new file mode 100644 index 00000000000..108242cece3 Binary files /dev/null and "b/source/images/2025/20251006a/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210_2025-10-03_17.39.45.png" differ diff --git "a/source/images/2025/20251006a/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210_2025-10-03_18.03.47.png" "b/source/images/2025/20251006a/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210_2025-10-03_18.03.47.png" new file mode 100644 index 00000000000..7d4d0093ede Binary files /dev/null and "b/source/images/2025/20251006a/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210_2025-10-03_18.03.47.png" differ diff --git "a/source/images/2025/20251006a/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210_2025-10-03_18.38.25.png" "b/source/images/2025/20251006a/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210_2025-10-03_18.38.25.png" new file mode 100644 index 00000000000..4b35a9541df Binary files /dev/null and "b/source/images/2025/20251006a/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210_2025-10-03_18.38.25.png" differ