Postgres 19のプロパティグラフを理解する
(neovintage.org)- Postgres 19のプロパティグラフは、既存のテーブルを頂点とエッジとして宣言し、
MATCHで固定された関係パターンを検索するSQL/PGQ機能で、データをコピーしたり別のグラフ実行エンジンを作ったりしない - グラフパターンはリレーショナル結合にコンパイルされ、既存のオプティマイザ・インデックス・統計をそのまま使うため、遅いトラバーサルも通常の結合と同じ方法で分析・最適化できる
- ディメンションテーブルは頂点、純粋な結合テーブルはエッジに自然に対応するが、複数の外部キーと独自の属性を持つファクトテーブルは、イベントを表すハブ頂点としてモデル化するのが適している
- 1つのテーブルを異なるエイリアスで頂点と複数のエッジに同時に宣言できるため、
resultsの既存の外部キーを使った別個のエッジテーブルを作成・保存する必要がない - Postgres 19は可変長パスをサポートしていないため、最短経路・Nホップ到達性・PageRankには向かず、関係構造が事前に分かっている固定長の探索に適している
リレーショナルスキーマをグラフとして読む方法
- 正規化されたリレーショナルスキーマでは、
drivers、constructors、circuitsのようなディメンションテーブルがエンティティを保持し、ファクトテーブルがレース結果や予選セッションのようなイベントを記録する - 各行は潜在的な頂点であり、外部キーは別の行を指す潜在的なエッジである
- ER図はスキーマレベルのグラフに相当する
- 実際の行と外部キー関係はインスタンスレベルのグラフに相当する
- 「各結果に該当するconstructorを探す」という要求は、リレーショナルSQLでは
results JOIN constructors ON ...と書くグラフトラバーサルである - SQL/PGQはグラフそのものを新たに追加するのではなく、結合を直接列挙する代わりにグラフの形で質問を書けるようにする
- RelBench方式は、テーブルをpandasに取り込んでメモリ上でPyTorchグラフを構成するが、Postgresのプロパティグラフはデータベース内の既存テーブル関係をそのまま使う
- PyGの例も、主にフラットファイルやメモリ内グラフを使っている
- Formula 1データセットで実験したコードはGitHubリポジトリで確認できる
プロパティグラフの構成
CREATE PROPERTY GRAPHは既存テーブルの上に作る名前付きの宣言オブジェクトであるVERTEX TABLESは、行を頂点として読むテーブルを指定するEDGE TABLESは、行を接続関係として読むテーブルを指定する
- 各頂点テーブルには次の要素を定義する
KEY: 頂点の識別子で、多くの場合は主キーを使うLABEL:MATCHで使う頂点タイプ名PROPERTIES: グラフクエリでアクセスできる列の一覧
- 各エッジテーブルには
SOURCEとDESTINATIONを指定し、2つの値はそれぞれ頂点のキーを参照する CREATE PROPERTY GRAPHはデータを移動したりコピーしたりしない- 行は元のテーブルにそのまま残る
- 宣言は、既存の外部キー構造をグラフとして読む方法だけを定義する
- プロパティグラフの構造要素は頂点とエッジの2種類だけである
- ラベルとプロパティは頂点またはエッジに属する属性である
psqlでは各要素のElement Kindはvertexまたはedgeのどちらかである
MATCHで固定パターンを問い合わせる
- グラフクエリは
GRAPH_TABLE(...)の中にMATCHパターンを書く方式である - 次のパターンは、driverからresultを経てraceへつながる関係を表す
MATCH (d IS driver)<-[IS of_driver]-(res IS result)-[IS in_race]->(ra IS race)
COLUMNS句は、グラフクエリが外部へ返す列を指定する- 外側の
SELECTでは、GRAPH_TABLE(...)の結果を通常のテーブルのように照会できる - パターンに方向とラベルが含まれており、関係を文章のように読めるため、同じ多重結合より構造を把握しやすい
実行時にリレーショナル結合へ変換される構造
MATCHはPostgresに別途付け加えられたグラフ実行エンジンではなく、リレーショナル結合にコンパイルされる構文である- driver–result–raceパターンに
EXPLAINを実行すると、基盤テーブルを対象にした4つのハッシュ結合が現れる - 手書きの結合と同じく、次の要素を使う
- 既存のPostgresオプティマイザ
- 既存のインデックス
- 既存の統計
- グラフトラバーサルが遅いなら、原因と最適化方法も通常の結合が遅い場合と同じである
psqlでも、テーブルに似たコマンドでグラフを調査できる\dG: プロパティグラフの一覧を表示する\d f1: 各頂点とエッジ、基盤テーブル、要素の種類、エッジの出発・到着頂点を示す\d+ f1:CREATE PROPERTY GRAPH文全体を再構成し、エッジテーブルの主キーから推論したエッジキーも含める
キーとプロパティの違い
- 頂点の
KEYに指定した列が自動的にプロパティになるわけではない driver_idを頂点キーとしてだけ指定した後にd.driver_id = 1でフィルタすると、その列はクエリ可能なプロパティではないため失敗する- キーは頂点を識別するが、クエリには公開しない
- ID列でフィルタしたり返したりするには、
PROPERTIES一覧に明示的に追加する必要がある
Postgres 19の可変長パス制約
- Postgres 19は、エッジを1〜3回たどるような要素パターン量指定子をサポートしていない
ERROR: element pattern quantifier is not supported
- 2ホップを探索するには、
MATCHにエッジパターンを2つ明示する必要がある - 任意長のパスはプロパティグラフ構文では表現できない
- 深さが開かれている探索は、基盤テーブルに対する再帰CTEで処理する必要があり、その場合はプロパティグラフ構文の外に出る
既存テーブルを頂点とエッジにマッピングする
-
ディメンションテーブルは頂点
drivers、constructors、circuitsのように、安定した主キーと属性を持つエンティティテーブルは頂点に直接対応する- 主キーを頂点キーとして使い、必要な列をプロパティとして公開すればよい
-
純粋な結合テーブルはエッジ
student_courses(student_id, course_id)のような多対多ブリッジテーブルは、2つのエンティティを結びつけることが本来の役割なので、エッジに自然に対応する- 片方の外部キーを出発頂点、もう片方の外部キーを到着頂点として宣言する
- 結合テーブルの各行そのものが1つの関係なので、グラフ用の別データ変換は不要である
- この構造では
studentsとcoursesが頂点で、student_coursesがenrolled_inエッジになる
-
ファクトテーブルはイベント頂点
results行はdriver、race、constructorという3つのエンティティを指しながら、grid、position、points、statusのような独自データも持つ- SQL/PGQのエッジは出発点1つと到着点1つを持つ二項関係なので、3つの外部キーを持つ行全体を単一のエッジにすることはできない
- ファクト行自体が分析対象なら、そのテーブルを頂点として宣言する方が適している
results頂点はイベントとその属性を保持するresults_driver、results_race、results_constructorのような細いエッジが外部エンティティと接続する- このモデルは
driver <- result -> raceのようなハブ構造を作り、result頂点で止まってフィルタしたり属性を参照したりできる - 関係そのものが関心対象ならエッジとして、固有の属性を持つ特定の行やイベントが関心対象なら頂点としてモデル化する
- 結合テーブルは関係を表し、ファクトテーブルはイベントを表す
1つのテーブルを頂点とエッジとして同時に使う
- 「頂点またはエッジ」という区分は基盤テーブルではなく、グラフ内の各要素宣言に適用される
- 1つのテーブルを異なるエイリアスで
VERTEX TABLESとEDGE TABLESの両方に宣言できる resultsをresult頂点として使いながら、同じテーブルを次のエッジエイリアスとして再利用できるresults AS res_driver: resultからdriverへ接続するresults AS res_race: resultからraceへ接続するresults AS res_constr: resultからconstructorへ接続する
- 各エッジエイリアスは、
resultsにすでに存在する主キーと外部キー列を使う - 別個の
results_driver、results_race、results_constructorテーブルを作る必要はなく、エイリアスはデータを保存しない - 外部キーが3つあるファクトテーブルは、1つの多終端エッジではなく、それぞれ出発点と到着点を1つずつ持つ3つのエッジエイリアスとして宣言しなければならない
- この方式は、3つの物理テーブルやビューの代わりに、1つの基盤テーブルに対する3つの宣言的エイリアスを使う
プロパティ名と型の衝突
PROPERTIESを省略すると、SQL/PGQはテーブルのすべての列をプロパティとして公開するresultsとqualifyingの両方にnumber列があるが、それぞれdouble precisionとbigintである場合、次のエラーが発生する
ERROR: property "number" data type mismatch: double precision vs. bigint
- グラフ全体で同じ名前のプロパティは1つの型を持つ必要があるため、型が異なる同名列は衝突する
- 必要な列だけを
PROPERTIESに明示すれば、衝突する列をグラフから除外できる - キーが自動的にプロパティにならない問題と型衝突は、どちらも明示的なプロパティ許可リストで解決できる
適したクエリと適さないクエリ
-
固定された関係パターン
- プロパティグラフは「Xとこの特定のパターンで接続された対象を探せ」という質問に適している
- Formula 1グラフでは、次のようなクエリを書ける
- 特定のdriverがどのconstructorでレースしたかを探索する
- driverからresultとraceを経て別のresultとdriverへつながるパターンで競争相手を探す
- スタート順位が10位圏外でdriverがItalianである結果のように、構造と属性条件を合わせてフィルタする
- 関係の形を事前に知っており、その構造が固定され有限であるとき、検索・フィルタリング・集計構文の可読性が高まる
- 複数回の自己結合が必要なクエリも、1つの
MATCHパターンとして読みやすく表現できる
-
パスそのものが未知の問題
- 次の問題はPostgres 19のプロパティグラフには向いていない
- 2人のdriver間の最短経路
- Nホップ以内で到達できるすべての対象
- 深さを事前に分からない接続探索
- こうしたクエリには可変長トラバーサルが必要なため、基盤テーブルに対する再帰CTEを使う必要がある
- PageRank、コミュニティ検出、中心性計算のようなグラフアルゴリズムもパターンマッチングとは別の問題なので、この機能の範囲には含まれない
- プロパティグラフはパス構造が分かっているときに接続対象を探すが、XとYがどんなパスで接続されているか分からない状態でパスを発見したり、グラフの構造的重要度を計算したりはできない
- 次の問題はPostgres 19のプロパティグラフには向いていない
導入前に判断すべき点
- SQL/PGQは既存の外部キー構造の上に置かれる宣言的オーバーレイであり、別途選択しない限り追加データを保存しない
- 主な利点は2つある
- 固定形のトラバーサルをリレーショナル結合より読みやすく書ける
- スキーマをグラフとして文書化する名前付きオブジェクトを提供する
- 最大の制約は可変長パスがなく、深い探索や開かれた探索を再帰SQLで処理しなければならない点である
- テーブル自体が本質的に頂点またはエッジであるわけではなく、各グラフでその行を頂点・エッジ、またはその両方として読むかを決められる
- Postgres 19で既存スキーマに対して固定されたグラフパターンを問い合わせたい場合、別のグラフデータベースへデータを移す前に
CREATE PROPERTY GRAPHとMATCHを検討できる - 未知の深さを探索するには再帰CTEが必要だが、どの方式を使ってもデータがPostgresを離れる必要はない
- 実際に導入する前には、必要な性能が出るかを自分でテストすべきである
まだコメントはありません。