プログラミング
PostgreSQL 19 インタラクティブツアー
PostgreSQL 19 Interactive Tour (victoriametrics.com)
要約
PostgreSQL 19ベータ版の新機能を紹介する記事で、SQL/PGQによるプロパティグラフクエリ、範囲列に対するTemporal UPDATE/DELETE、およびINSERT ... ON CONFLICT DO SELECTによるリターン機能に焦点を当てています。各機能は実行可能な例とともに解説されており、実際の動作を体験しながら理解を深めることができます。
全文翻訳
ブログ / PostgreSQL 19 インタラクティブツアー
PostgreSQL 19 インタラクティブツアー
PostgreSQL
共有:
PostgreSQL 19 はベータ版であり、一般提供は2026年9月または10月頃に予定されているため、新機能について事前に学習を始めるのに良い時期です。公式リリースノートは、各変更のコミットや貢献者まで含めて詳細にまとめられた権威ある記録です。この記事は、それらのエントリの中からいくつかを選択し、それぞれを実行可能な例に変えて、新しい動作が実際どのように機能するかを見ることができるようにした、ハンズオンの補足記事です。
新機能の詳細に入る前に、文脈を設定しましょう。
この記事は、PostgreSQLライセンスの下でライセンスされている公式リリースノートとPostgreSQLソースコードに基づいています。これは網羅的なリストではありません。網羅的なリストについては公式リリースノートを参照してください。
以下の各例は、PostgreSQL 19 ベータ 3 (2026-08-13リリース) で実行され、そのサーバーが実際に出力したものが表示されています。
リンクは、各機能のドキュメント(𝗗)、最も関連性の高いコミット(𝗖)、および作成者(𝗔)を指しています。動機、使用方法、実装の詳細については、それらをチェックしてください。作成者(𝗔)は、リリースノートでその機能についてクレジットされている人々であり、通常は単一の主要作成者ではなくパッチ作成者を意味します。
文脈が設定されたので、新機能の探索を始めましょう。
プロパティグラフクエリ
これはリリースのアナウンスの目玉です。PostgreSQL 19 は、SQL:2023 のプロパティグラフ部分である SQL/PGQ を実装します。既存のテーブル上にプロパティグラフを宣言し、自分でJOINを記述する代わりにパターンマッチングでクエリできるようになります。
2つの通常のテーブル、その上に1つのグラフ:
CREATE PROPERTY GRAPH social VERTEX TABLES ( person KEY (id) LABEL person PROPERTIES (id, name) ) EDGE TABLES ( follows KEY (follower, followee) SOURCE KEY (follower) REFERENCES person (id) DESTINATION KEY (followee) REFERENCES person (id) LABEL follows );
何もコピーされません。social は、「person行は頂点、follows行はエッジ」と言うビューのようなオブジェクトです。これで、-[...]-> が有向エッジである GRAPH_TABLE を使ってパターンをマッチングできます:
SELECT * FROM GRAPH_TABLE (social MATCH (a IS person)-[IS follows]->(b IS person) COLUMNS (a.name AS follower, b.name AS followee) ) ORDER BY follower, followee;
┌──────────┬──────────┐
│ follower │ followee │
├──────────┼──────────┤
│ Ada │ Bo │
│ Ada │ Dee │
│ Bo │ Cleo │
│ Cleo │ Dee │
└──────────┴──────────┘
(4 rows)
その利点はマルチホップパターンです。2つのエッジを連鎖させることで、自己結合なしで友人の友人を見つけることができます。空の()は「名前を気にしない任意の頂点」を意味します:
SELECT * FROM GRAPH_TABLE (social MATCH (a IS person WHERE a.name = 'Ada')-[IS follows]->()-[IS follows]->(c IS person) COLUMNS (a.name AS start, c.name AS friend_of_friend) );
┌───────┬──────────────────┐
│ start │ friend_of_friend │
├───────┼──────────────────┤
│ Ada │ Cleo │
└───────┴──────────────────┘
(1 row)
ここには新しい実行エンジンはありません。それがポイントです。GRAPH_TABLE は通常の関係クエリに書き換えられるため、プランナー、統計情報、インデックスの選択など、すでに知っているものがすべて引き続き適用されます:
EXPLAIN (COSTS OFF) SELECT * FROM GRAPH_TABLE (social MATCH (a IS person)-[IS follows]->(b IS person) COLUMNS (a.name AS follower, b.name AS followee) );
┌───────────────────────────────────────────────────┐
│ QUERY PLAN │
├───────────────────────────────────────────────────┤
│ Hash Join │
│ Hash Cond: (follows.followee = person_1.id) │
│ -> Hash Join │
│ Hash Cond: (follows.follower = person.id) │
│ -> Seq Scan on follows │
│ -> Hash │
│ -> Seq Scan on person │
│ -> Hash │
│ -> Seq Scan on person person_1 │
└───────────────────────────────────────────────────┘
(9 rows)
グラフデータベースからの移行を計画する前に知っておくべき制限が1つあります。この最初のカットには可変長パスがありません。Quantifier (例: -[IS follows]->{1,3}) は解析されますが、element pattern quantifier is not supported というエラーで拒否されるため、パターンはすべてのホップを明示的に指定する必要があります。
𝗗 Graph Queries, Property Graphs, CREATE PROPERTY GRAPH
𝗖 2f094e7, c5b3253, a0dd070
𝗔 Peter Eisentraut, Ashutosh Bapat
Temporal updates and deletes
UPDATE および DELETE の新しい FOR PORTION OF 句は、範囲列のスライスに対して操作を行います。有効期間全体を書き換える代わりに、サブ期間を指定すると、PostgreSQL が行を分割してくれます。
テーブルは単一行から始まります。これは2026年全体で有効な1つの価格です。7月のみ変更し、再度クエリします:
SELECT * FROM price ORDER BY valid_at;
UPDATE price FOR PORTION OF valid_at FROM '2026-07-01' TO '2026-08-01' SET amount = 7.99;
SELECT * FROM price ORDER BY valid_at;
┌────────┬─────────────────────────┬────────┐
│ sku │ valid_at │ amount │
├────────┼─────────────────────────┼────────┤
│ widget │ [2026-01-01,2027-01-01) │ 9.99 │
└────────┴─────────────────────────┴────────┘
(1 row)
UPDATE 1
┌────────┬─────────────────────────┬────────┐
│ sku │ valid_at │ amount │
├────────┼─────────────────────────┼────────┤
│ widget │ [2026-01-01,2026-07-01) │ 9.99 │
│ widget │ [2026-07-01,2026-08-01) │ 7.99 │
│ widget │ [2026-08-01,2027-01-01) │ 9.99 │
└────────┴─────────────────────────┴────────┘
(3 rows)
1行入力され、3行出力され、変更されなかった期間は古い価格を保持します。
DELETE も同様に動作し、分割する代わりにトリミングします。このスニペットは、変更されていない完全な年の行から始まります。12月から年末までの期間を削除すると、それ以前の期間が残ります:
SELECT * FROM price ORDER BY valid_at;
DELETE FROM price FOR PORTION OF valid_at FROM '2026-12-01' TO NULL;
SELECT * FROM price ORDER BY valid_at;
┌────────┬─────────────────────────┬────────┐
│ sku │ valid_at │ amount │
├────────┼─────────────────────────┼────────┤
│ widget │ [2026-01-01,2027-01-01) │ 9.99 │
└────────┴─────────────────────────┴────────┘
(1 row)
DELETE 1
┌────────┬─────────────────────────┬────────┐
│ sku │ valid_at │ amount │
├────────┼─────────────────────────┼────────┤
│ widget │ [2026-01-01,2026-12-01) │ 9.99 │
└────────┴─────────────────────────┴────────┘
(1 row)
NULL を境界として使用すると、境界がないことを意味します。したがって、TO NULL は「12月から以降」を意味します。これは、範囲列に履歴を保持している場合に読む価値のある、Temporal Tables に関する新しいドキュメントの章と並んで提供されます。
𝗗 Temporal Tables, UPDATE
𝗖 8e72d91, b6ccd30
𝗔 Paul A. Jungwirth
Upsert が失った行を返す
INSERT ... ON CONFLICT DO NOTHING ... RETURNING には常に厄介な穴がありました。競合した行は結果に単純に存在しないため、「すでに存在した」と「発生しなかった」を区別できませんでした。これを修正するために、ON CONFLICT DO UPDATE を使用できましたが、これは競合した各行に書き込みたい場合にのみ機能します。
PostgreSQL 19 は ON CONFLICT DO SELECT を追加しました。これは、既存の行を変更せずに返します。ここでは、widget はすでに qty = 7 で存在しています:
INSERT INTO inventory VALUES ('widget', 1), ('gadget', 3) ON CONFLICT (sku) DO SELECT RETURNING sku, qty;
┌────────┬─────┐
│ sku │ qty │
├────────┼─────┤
│ widget │ 7 │
│ gadget │ 3 │
└────────┴─────┘
(2 rows)
INSERT 0 2
両方の行が返され、widget は挿入しようとした 1 ではなく 7 (テーブルにすでに存在する値) を報告します。
DO SELECT はロック句も取ることができるため、FOR UPDATE は競合した行をロックし、その後の処理を決定できるようにします。また、2種類の行を区別する方法も提供します。行をロックするとその xmax がスタンプされるため、xmax = 0 のトリックは、どの行が実際に挿入されたかを示します:
INSERT INTO inventory VALUES ('widget', 1), ('gadget', 3) ON CONFLICT (sku) DO SELECT FOR UPDATE RETURNING sku, qty, xmax = 0 AS was_inserted;
┌────────┬─────┬──────────────┐
│ sku │ qty │ was_inserted │
├────────┼─────┼──────────────┤
│ widget │ 7 │ f │
│ gadget │ 3 │ t │
└────────┴─────┴──────────────┘
(2 rows)
INSERT 0 2
それを機能させるのはロック句です。ロック句がない場合、ロックはスタンプされず、競合した行の xmax も 0 のままになり、すべての行が was_inserted = t を主張します。任意のロック句で十分です。FOR SHARE も含まれます。
𝗗 INSERT
𝗖 8832709
𝗔 Andreas Karlsson, Marko Tiikkaja, Viktor Holmberg
Window 関数が NULL をスキップできるようになりました
lead(), lag(), first_value(), last_value(), および