Understanding Postgres 19 Property Graphs rimas silkaitis
Understanding Postgres 19 Property Graphs<br>Jul 5, 2026
Last post I was talking about Relational Deep Learning, which starts from the claim that your database schema is already a graph. Rows are nodes, foreign keys are edges, and a model can learn on that structure directly. The catch in that work is that the graph lives in Python. RelBench pulls your tables out into pandas, builds the graph in memory as a PyTorch object, and trains on it there. The database is just where the data happened to be sitting. Even you when you go look at the documentation for PyG (PyTorch Geometric), a library that makes it easy to write and train graph neural networks, the examples are for flat files or in memory.
With the new Property Graph feature in in Postgres 19, you can declare which tables are nodes and which are edges, then pattern-match over them with MATCH predicate.
I spent a while poking at Postgres property graphs on a Formula 1 dataset that’s referenced as part of the Relational Deep Learning benchmark. This post is about what property graphs in Postgres actually are, where they fit, what they compile down to, and the one thing that I learned: a single table can be a vertex and an edge at the same time.
You can check out the code I used to learn more about property graphs here: https://github.com/neovintage/relational-deep-learning-with-pg19
How I think about a relational schema as a graph
Take any normalized schema. You have dimension tables holding entities (drivers, constructors, circuits) and fact tables recording events (a race result, a qualifying session). The foreign keys wire them together. A results row has a driver_id, a race_id, and a constructor_id. Each of those is a pointer to a row in another table.
That represenation is a graph. Every row is a potential node in the node with every foreign key representing a potential edge. Your ER diagram is the schema-level version of it while the actual data is the instance-level version.
“Give me the constructor for each result” is a graph traversal that you write as results JOIN constructors ON .... SQL/PGQ does not add the graph. It adds a way to ask graph questions without spelling out the join.
What a property graph actually is
A property graph in Postgres is a named object you create over tables you already have (trimmed here to a few tables to keep it readable):
CREATE PROPERTY GRAPH f1<br>VERTEX TABLES (<br>drivers KEY (driver_id) LABEL driver<br>PROPERTIES (driver_id, code, nationality, dob),<br>races KEY (race_id) LABEL race<br>PROPERTIES (race_id, year, round, date),<br>results KEY (result_id) LABEL result<br>PROPERTIES (result_id, grid, position, points, status_id, date)<br>EDGE TABLES (<br>results_driver<br>SOURCE KEY (result_id) REFERENCES results (result_id)<br>DESTINATION KEY (driver_id) REFERENCES drivers (driver_id)<br>LABEL of_driver,<br>results_race<br>SOURCE KEY (result_id) REFERENCES results (result_id)<br>DESTINATION KEY (race_id) REFERENCES races (race_id)<br>LABEL in_race<br>);<br>VERTEX TABLES lists the tables whose rows are nodes. Each gets a KEY (its identity, almost always the primary key), a LABEL you’ll match on, and a PROPERTIES list of the columns you want queryable. EDGE TABLES lists the tables whose rows are connections, each wired with a SOURCE and a DESTINATION that reference vertex keys.
The important part: this moves no data. CREATE PROPERTY GRAPH is a declaration, an overlay on the foreign-key structure you already have. The rows stay in their tables. You are describing how to read them as a graph, not making a copy.
Then you query it with MATCH:
SELECT g.code, g.points<br>FROM GRAPH_TABLE (f1<br>MATCH (d IS driver)[IS of_driver]-(res IS result)-[IS in_race]->(ra IS race)<br>COLUMNS (d.code AS code, res.points AS points)<br>) AS g;<br>The pattern reads like a sentence: a driver, connected by an of_driver edge back from a result, which connects by in_race to a race. The COLUMNS clause is what gets projected out. Everything inside GRAPH_TABLE(...) is the graph query, and the outer SELECT treats the result like any other table.
There are exactly two kinds of thing in a property graph: vertices and edges. That’s the whole structural vocabulary. Labels and properties are attributes of those two kinds, not additional kinds. When you describe the graph in psql (more on that below), every element reports an “Element Kind” that is either vertex or edge, and there is no third option.
What it compiles to
A MATCH is not a separate execution engine bolted onto Postgres. It compiles to relational joins. Run EXPLAIN over that three-element pattern and you get:
Hash Join<br>Hash Cond: (results_race.race_id = races.race_id)<br>-> Hash Join<br>Hash Cond: (results.result_id = results_race.result_id)<br>-> Hash Join<br>Hash Cond: (results_driver.driver_id = drivers.driver_id)<br>-> Hash Join<br>Hash Cond: (results.result_id = results_driver.result_id)<br>...<br>Four hash joins over the base tables. The graph pattern I wrote turned into exactly the joins I would...