<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Data Bene - Ayoub Kazar</title>
  <subtitle>Relational database, open-source and scalable.</subtitle>
  <link href="https://www.data-bene.io/en/authors/ayoub-kazar/feed.xml" rel="self" type="application/atom+xml" />
  <updated>2026-10-21T00:00:00Z</updated>
  <id>https://www.data-bene.io/en/authors/ayoub-kazar/feed.xml</id>
    <entry>
      <title>PostgreSQL as a graph database, part 0: intro, graphs, graph databases</title>
      <link href="https://www.data-bene.io/en/blog/postgresql-as-a-graph-database/" />
      <updated>2026-10-19T00:00:00Z</updated>
      <id>https://www.data-bene.io/en/blog/postgresql-as-a-graph-database/</id>
     <content type="html"><![CDATA[ <p><em>This is the first post in a series exploring graph databases and PostgreSQL’s evolving role in the graph landscape. Over four posts, we’ll move from foundational concepts through PostgreSQL’s SQL/PGQ implementation details and its future, benchmark comparisons against PostgreSQL graph solutions (e.g Apache AGE) , and finally the broader trajectory of relational systems absorbing graph workloads.<br>
The series assumes no prior graph database expertise.</em></p>
<h2 id="what-is-a-graph"><a class="heading-anchor" href="#what-is-a-graph">What Is a Graph?</a></h2>
<p><strong>A graph is a set of things and a set of connections between them</strong>. That is the whole definition.<br>
The things are called <em>vertices</em> (or nodes) and the connections are called <em>edges</em> (or relationships).</p>
<p>Formally, a graph $G $ is modeled as a tuple $(V, E)$, where $V $ is a set of vertices and $E $  is a subset of the Cartesian product of $V $ with itself, representing a set of edges:<br>
$E \subseteq V \times V$</p>
<p>We find 3 kinds of graphs: directed, undirected, and weighted.</p>
<ol class="list">
<li>
<p>For a <strong>directed graph</strong>, an edge $E $ equals the ordered pair $(u, v)$ in $E $, where $u $ is the source vertex and $V $ is the destination vertex; an example is a Twitter follow, where the relationship has a specific direction:<br>
edge $e = (u, v) \in E$</p>
</li>
<li>
<p>For an <strong>undirected graph</strong>, an edge $E $ is an unordered pair containing $u $ and $V $ in $E $; an example is a Facebook friendship, where the relationship is mutual:<br>
edge $e = \{ u, v \} \in E$</p>
</li>
<li>
<p>A <strong>weighted graph</strong> extends this structure as a triple $(V, E, w)$, where $w $ is a function that asigns each edge in $E $ a real number (a weight), which is useful for representing distances, costs, or strengths:<br>
triple $(V, E, w)$ where $w: E \to \mathbb{R}$</p>
</li>
</ol>
<!--Formally, a graph $G $ is modeled as a tuple $(V, E)$ where $V $ is a set of vertices and $E \subseteq V \times V$ is a set of edges. We write $|V| = n$ and $|E| = m$. For a directed graph, an edge $e = (u, v) \in E$ is an ordered pair where $u $ is the source vertex and $V $ is the destination (think Twitter follows, where the relationship has a direction). For an undirected graph, an edge $e = \{u, v\} \in E$ is an unordered pair (think Facebook friendships, where the relationship is mutual). A weighted graph extends this as a triple $(V, E, w)$ where $w: E \to \mathbb{R}$ maps each edge to a real-valued weight, useful for representing distances, costs, strengths…-->
<h2 id="what-can-be-a-graph"><a class="heading-anchor" href="#what-can-be-a-graph">What Can Be a Graph?</a></h2>
<p>The power of the graph model lies in its universality. Once you start looking, graphs are everywhere.</p>
<ul class="list">
<li><strong>Social networks</strong> represent friendship, followership, and interaction as edges connecting user vertices.<br>
<em>Does Alice know Bob through other people?</em><br>
<em>Is basically walking the graph and finding this relationship.</em></li>
</ul>
<ul class="list">
<li>
<p><strong>Fraud detection systems</strong> model transactions and accounts as graphs, exploiting the fact that fraudulent rings are just cycles or dense cluster of accounts and transactions.<br>
<em>The relationships between entities often reveal fraud more reliably than the entities themselves.</em></p>
</li>
<li>
<p><strong>Supply chain and logistics</strong> applications use graphs to model dependencies between suppliers, warehouses, and shipments.</p>
</li>
<li>
<p>Biologists represent <strong>protein interaction networks and drug-target</strong> relationships.<br>
<img src="/assets/images/blog/protein_network_transparent.png" alt="protein_network_transparent" src="/assets/images/blog/protein_network_transparent.png" alt="" width="80%" eleventy:widths="650,960,1400"></p>
</li>
<li>
<p><strong>Networks</strong> use graphs to represent topologies.</p>
</li>
</ul>
<p><img src="/assets/images/blog/stp_topology_transparent.png" alt="stp_topology_transparent" src="/assets/images/blog/stp_topology_transparent.png" alt="" width="80%" eleventy:widths="650,960,1400"></p>
<ul class="list">
<li><strong>The web</strong> is another example.<br>
<em>Pages are vertices, hyperlinks are directed edges, and the resulting graph is one of the largest and most studied structures in computing</em>:</li>
</ul>
<p><img src="/assets/images/blog/web_graph_transparent.png" alt="web_graph_transparent" src="/assets/images/blog/web_graph_transparent.png" alt="" width="80%" eleventy:widths="650,960,1400"></p>
<h2 id="modeling-as-a-graph"><a class="heading-anchor" href="#modeling-as-a-graph">Modeling as a graph</a></h2>
<p>What the above domain examples share are <strong>Properties</strong> that make a <strong>Graph</strong> an intuitive solution for us human beings. Relationships are as important as the entities themselves.<br>
The schema is often irregular or evolving. <strong>Queries</strong> frequently follow chains of connections whose length or shape is not known in advance.</p>
<p><strong>Graphs are also effective because they are easy to reason about visually</strong>. Human beings understand things better with examples, and a graph makes relationships explicit. That visual and structural clarity often translates into data models and algorithms that are easier to explain, and question.</p>
<p>The model defined at the start of this post, a set of vertices and a set of edges with no data attached to either of them, underlies much of classical graph theory and is the basis for graph processing frameworks and graph database management systems. It is, however, too limited for most real-world database scenarios, because it carries no information beyond structure. The richer models used in graph databases are built on top of it.</p>
<ul class="list">
<li>The <strong>Labeled Property Graph (LPG)</strong> is the standard conceptual model used by most graph database systems. It enriches the simple graph with two mechanisms: <strong>labels</strong>, which classify vertices and edges into types, and <strong>properties</strong>, which attach arbitrary key-value data to individual vertices and edges.<br>
A vertex might have the label <code>Person</code> and properties <code>name='Alice' and age=21</code>.<br>
An edge might have the label <code>KNOWS</code> and a property <code>since = &quot;09.08.2007&quot;</code>.</li>
</ul>
<p><a href="https://dl.acm.org/doi/10.1145/3604932#sec-3-3-2" target="_blank" rel="noopener"><img src="/assets/images/blog/csur-2022-0455-f02.png" alt="Labeled Property Graph example, Adapted from Besta et al" src="/assets/images/blog/csur-2022-0455-f02.png" alt="" width="80%" eleventy:widths="650,960,1400"></a></p>
<p>There are other variants of LPG implemented by others systems with minor extensions or restrictions, <a href="https://neo4j.com/" target="_blank" rel="noopener">Neo4j</a> for example supports an arbitrary number of labels per vertex but restricts edges to exactly one label.</p>
<ul class="list">
<li>The <strong>Resource Description Framework (RDF)</strong> takes a different approach. RDF represents information as collections of triples subject, predicate, object $(s, p, o)$.<br>
While RDF-based systems are a meaningful part of the graph database landscape, they follow a different data model than LPG and <strong>are outside the scope of this series</strong>. We mention it only for context, as this article series is focused on labeled property graph databases.</li>
</ul>
<h2 id="the-many-forms-of-graph-databases"><a class="heading-anchor" href="#the-many-forms-of-graph-databases">The Many Forms of Graph Databases</a></h2>
<p>The graph database landscape is heterogeneous. Systems differ fundamentally in how they store graphs, what data models they support, and what query languages they expose.</p>
<p><a href="https://dl.acm.org/doi/10.1145/3604932#sec-5" target="_blank" rel="noopener"><img src="/assets/images/blog/graph_db_models_transparent.png" alt="Overview of different graph storage backends" src="/assets/images/blog/graph_db_models_transparent.png" alt="" width="80%" eleventy:widths="650,960,1400"></a></p>
<p><strong>Native graph databases</strong> are built from the ground up around the graph model. Their storage layouts and query engines are designed with graph access patterns as the primary concern: they aim to “follow pointers”, or otherwise access adjacency directly, so that traversal “does not become a JOIN”.<br>
Neo4j is the most prominent example. For a long time its storage engine used fixed-size records with direct pointers between them, and this design was mostly how people described native graph storage.<br>
After nearly 15 years, in 2024, Neo4j introduced a block format that colocates related data into variable-length blocks inside pages <sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>. Although a late realization (for the better), it implies that “native equals pointer chasing” description is no longer fully accurate.<br>
The useful definition therefore should be behavioral, meaning native systems try to make adjacency access direct, whether through pointers, colocated blocks, or other graph-specific layouts.</p>
<p><strong>Non-native graph databases</strong> take an existing mature backend; a relational, document or key-value store and build graph capabilities on top of it <sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>. Both classes are graph databases in the sense that they store, query, and manage graph data, but their internals are fundamentally different implying which graph-specific optimizations are available and which are left to the backend.<br>
In practice, the difference is more about where the engineering effort goes, for example non-native systems inherit decades of work in concurrency control, indexing, query optimization and many other DBMS components.</p>
<hr>
<p><strong>A survey</strong> of approximately one hundred graph database systems reveals that roughly 66% are non-native. The distribution of underlying data models breaks down as: 34% native graph storage, 24% relational databases, 24% key-value or wide-column stores, and 18% document stores. This makes sense, one would want to make an existing DBMS capable of supporting graph queries if possible, providing users with the best possible interface.</p>
<p>This is precisely where PostgreSQL enters the picture. As a mature relational database, it represents a massive share of that non-native ecosystem through extensions and new SQL standards.</p>
<!-- *That is enough vocabulary to start. In the next post, we look at SQL/PGQ in PostgreSQL: what it does, how it is implemented, and where it stands today.* -->
<h2 id="key-takeaways"><a class="heading-anchor" href="#key-takeaways">Key Takeaways</a></h2>
<p>Before we jump into the technical implementation, let’s lock in the core concepts from this introduction:</p>
<ul class="list">
<li><strong>Graphs are universal:</strong> At their core, they are just vertices (nodes) and edges (relationships), making them ideal for complex, interconnected data like social networks or fraud rings.</li>
<li><strong>LPG is the industry standard:</strong> The Labeled Property Graph model extends simple graphs by allowing you to attach labels and key-value properties to both nodes and edges.</li>
<li><strong>Native vs. Non-Native is behavioral:</strong> Native databases (like Neo4j) optimize for direct adjacency access, while non-native databases leverage existing enterprise-grade engines (relational, document) to handle graph workloads.</li>
<li><strong>The landscape is hybrid:</strong> Over 66% of graph database solutions are built on top of non-graph backends, proving that you don’t always need to deploy a new native database to query graph structures.</li>
</ul>
<h2 id="whats-next"><a class="heading-anchor" href="#whats-next">What’s Next?</a></h2>
<p>Now that we have the foundational vocabulary down, we can explore how to bring this model into the relational world. <a href="/en/blog/postgresql-as-a-graph-database-part-1/">In <strong>Part 1</strong></a>, we will dive straight into <strong>SQL/PGQ in PostgreSQL</strong>: what this new standard brings to the table, how it is implemented under the hood, and its current limitations.</p>
<!-- 👉 **What is your current stack?** Are you running a native graph database, or are you looking to extend your existing relational database? Let me know in the comments below! -->
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p><a href="https://neo4j.com/docs/operations-manual/current/database-internals/store-formats/" target="_blank" rel="noopener">Neo4j store formats</a> <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Several pictures borrowed from <a href="https://dl.acm.org/doi/10.1145/3604932" target="_blank" rel="noopener">Besta, M. et al. (2023). Demystifying Graph Databases: Analysis and Taxonomy of Data Organization, System Designs, and Graph Queries. <em>ACM Computing Surveys</em>, 56(2), Art. 31.</a> <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
 ]]></content>
			<author>
				<name>Ayoub Kazar</name>
			</author>
    </entry>
    <entry>
      <title>PostgreSQL as a graph database, part 1: SQL/PGQ in PostgreSQL</title>
      <link href="https://www.data-bene.io/en/blog/postgresql-as-a-graph-database-part-1/" />
      <updated>2026-10-21T00:00:00Z</updated>
      <id>https://www.data-bene.io/en/blog/postgresql-as-a-graph-database-part-1/</id>
     <content type="html"><![CDATA[ <p><em>This is the second post in a series exploring graph databases and PostgreSQL. The <a href="/en/blog/postgresql-as-a-graph-database/">first post</a> covered what graphs are, what can be modeled as a graph, and the landscape of graph database systems. This post moves from the general to the specific: how we query graphs, why SQL can be an awkward fit, and what SQL/PGQ changes. It ends with the PostgreSQL implementation, why it was reverted, and a few notes on the main features still to come.</em></p>
<h2 id="query-languages-for-graphs"><a class="heading-anchor" href="#query-languages-for-graphs">Query languages for graphs</a></h2>
<p>In <a href="/en/blog/postgresql-as-a-graph-database/">the previous post</a>, we ended on the observation that graph databases come in many forms, and that they differ in how they store graphs and what data models they support. They also differ in how you question them.</p>
<p>Unlike the relational world, which converged on SQL early and kept it as the dominant standard for decades, the graph ecosystem produced a proliferation of query languages:</p>
<ul class="list">
<li>Cypher<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>, created at Neo4j, represents patterns as ASCII art:<br>
<code>(a:Person)-[:KNOWS]-&gt;(b:Person)</code>;</li>
<li>Gremlin<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>, from Apache TinkerPop, is imperative and step-based:<br>
<code>g.V().hasLabel('Person').out('KNOWS')</code>;</li>
<li>PGQL<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>, from Oracle, embedded graph patterns inside a SQL-like shell;</li>
<li>And in 2024, GQL<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup> was ratified as ISO/IEC 39075, the first new database query language to reach ISO standardization since SQL itself.</li>
</ul>
<p>For this series, the relevant one is SQL/PGQ<sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup>, <strong>standardized as part of SQL:2023</strong>. It is an extension of SQL that lets you define property graphs over existing tables and query them with GQL- and PGQL-like pattern syntax inside a standard <code>SELECT</code>.</p>
<h2 id="how-good-is-sql-for-graphs"><a class="heading-anchor" href="#how-good-is-sql-for-graphs">How good is SQL for graphs ?</a></h2>
<p>Consider a social network stored in two tables: <code>person</code> and <code>knows</code>.<br>
A Cypher query asking for friends of friends looks like this:</p>
<pre class="language-cypher"><code class="language-cypher"> <span class="token keyword">MATCH</span> <span class="token punctuation">(</span>a<span class="token operator">:</span><span class="token class-name">Person</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token punctuation">[</span><span class="token operator">:</span><span class="token relationship property">KNOWS</span><span class="token punctuation">]</span><span class="token operator">-></span><span class="token punctuation">(</span>b<span class="token punctuation">)</span><span class="token operator">-</span><span class="token punctuation">[</span><span class="token operator">:</span><span class="token relationship property">KNOWS</span><span class="token punctuation">]</span><span class="token operator">-></span><span class="token punctuation">(</span>c<span class="token punctuation">)</span>
 <span class="token keyword">WHERE</span> a<span class="token punctuation">.</span>name <span class="token operator">=</span> <span class="token string">'Alice'</span>
<span class="token keyword">RETURN</span> c<span class="token punctuation">.</span>name</code></pre>
<p>The equivalent in plain SQL is a two-way self-join:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">SELECT</span> <span class="token keyword">DISTINCT</span> c<span class="token punctuation">.</span>name
  <span class="token keyword">FROM</span> person a
  <span class="token keyword">JOIN</span> knows k1 <span class="token keyword">ON</span> k1<span class="token punctuation">.</span>src <span class="token operator">=</span> a<span class="token punctuation">.</span>id
  <span class="token keyword">JOIN</span> person b <span class="token keyword">ON</span> k1<span class="token punctuation">.</span>dst <span class="token operator">=</span> b<span class="token punctuation">.</span>id
  <span class="token keyword">JOIN</span> knows k2 <span class="token keyword">ON</span> k2<span class="token punctuation">.</span>src <span class="token operator">=</span> b<span class="token punctuation">.</span>id
  <span class="token keyword">JOIN</span> person c <span class="token keyword">ON</span> k2<span class="token punctuation">.</span>dst <span class="token operator">=</span> c<span class="token punctuation">.</span>id
 <span class="token keyword">WHERE</span> a<span class="token punctuation">.</span>name <span class="token operator">=</span> <span class="token string">'Alice'</span><span class="token punctuation">;</span></code></pre>
<p>That is not bad. It is verbose, but it is expressible, the JOIN is explicit, and whether it is two hops or ten hops, each is a matter of writing more JOINs.</p>
<p>Let’s try something a bit more graph-centric now. Suppose we want all people reachable from Alice within five <code>KNOWS</code> hops. In SQL/PGQ (very similar to Cypher) that is a single pattern:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">SELECT</span> c<span class="token punctuation">.</span>name
  <span class="token keyword">FROM</span> GRAPH_TABLE<span class="token punctuation">(</span>
          social_graph
    <span class="token keyword">MATCH</span> <span class="token punctuation">(</span>a <span class="token operator">IS</span> Person <span class="token keyword">WHERE</span> a<span class="token punctuation">.</span>name <span class="token operator">=</span> <span class="token string">'Alice'</span><span class="token punctuation">)</span><span class="token operator">-</span><span class="token punctuation">[</span><span class="token operator">IS</span> KNOWS<span class="token punctuation">]</span><span class="token operator">-</span><span class="token operator">></span>{<span class="token number">1</span><span class="token punctuation">,</span><span class="token number">5</span>}<span class="token punctuation">(</span>c <span class="token operator">IS</span> Person<span class="token punctuation">)</span>
  <span class="token keyword">COLUMNS</span> <span class="token punctuation">(</span>c<span class="token punctuation">.</span>name<span class="token punctuation">)</span>
<span class="token punctuation">)</span> <span class="token keyword">AS</span> g<span class="token punctuation">;</span></code></pre>
<p>In SQL, things become a bit uglier:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">WITH</span> RECURSIVE reachable<span class="token punctuation">(</span>id<span class="token punctuation">,</span> depth<span class="token punctuation">,</span> path<span class="token punctuation">)</span> <span class="token keyword">AS</span> <span class="token punctuation">(</span>
    <span class="token keyword">SELECT</span> p<span class="token punctuation">.</span>id
         <span class="token punctuation">,</span> <span class="token number">0</span>
         <span class="token punctuation">,</span> ARRAY<span class="token punctuation">[</span>p<span class="token punctuation">.</span>id<span class="token punctuation">]</span>
      <span class="token keyword">FROM</span> person p
     <span class="token keyword">WHERE</span> p<span class="token punctuation">.</span>name <span class="token operator">=</span> <span class="token string">'Alice'</span>
    <span class="token keyword">UNION</span> <span class="token keyword">ALL</span>
    <span class="token keyword">SELECT</span> k<span class="token punctuation">.</span>dst
         <span class="token punctuation">,</span> r<span class="token punctuation">.</span>depth <span class="token operator">+</span> <span class="token number">1</span>
         <span class="token punctuation">,</span> r<span class="token punctuation">.</span>path <span class="token operator">||</span> k<span class="token punctuation">.</span>dst
      <span class="token keyword">FROM</span> reachable r
      <span class="token keyword">JOIN</span> knows k <span class="token keyword">ON</span> k<span class="token punctuation">.</span>src <span class="token operator">=</span> r<span class="token punctuation">.</span>id
     <span class="token keyword">WHERE</span> r<span class="token punctuation">.</span>depth <span class="token operator">&lt;</span> <span class="token number">5</span>
       <span class="token operator">AND</span> k<span class="token punctuation">.</span>dst <span class="token operator">&lt;></span> <span class="token keyword">ALL</span><span class="token punctuation">(</span>r<span class="token punctuation">.</span>path<span class="token punctuation">)</span>
<span class="token punctuation">)</span>
<span class="token keyword">SELECT</span> <span class="token keyword">DISTINCT</span> p<span class="token punctuation">.</span>name
  <span class="token keyword">FROM</span> reachable r
  <span class="token keyword">JOIN</span> person p <span class="token keyword">ON</span> p<span class="token punctuation">.</span>id <span class="token operator">=</span> r<span class="token punctuation">.</span>id
 <span class="token keyword">WHERE</span> r<span class="token punctuation">.</span>depth <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">;</span></code></pre>
<p>The author now encodes the depth limit, carries the path in an array to avoid cycles, and removes the seed row at the end. PostgreSQL 14 added <code>SEARCH</code> and <code>CYCLE</code> clauses<sup class="footnote-ref"><a href="#fn6" id="fnref6">[6]</a></sup> that hide some of this bookkeeping, but the query is still a recursive fixpoint computation that happens to produce the right answer.</p>
<p>Though this is a basic graph pattern, things get even trickier. For shortest paths, we would need to track distance and prune. Asking for a trail (no repeated edges), and we would have to track edges, not just nodes.</p>
<h2 id="defining-and-querying-a-graph-in-sql/pgq"><a class="heading-anchor" href="#defining-and-querying-a-graph-in-sql/pgq">Defining and querying a graph in SQL/PGQ</a></h2>
<p>From here on, we use a richer example, set in the Star Wars galaxy. Jedi fought on planets, and Sith fought at stations. The vertex tables are <code>jedi</code>, <code>sith</code>, <code>planet</code>, and <code>station</code>, each with an <code>id</code> and a <code>name</code>, plus a <code>title</code> for the fighters and a <code>region</code> for the places. Two edge tables, <code>fought_on_planet</code> and <code>fought_on_station</code>, each hold a source id, a destination id, and the name of the battle.</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span> jedi <span class="token punctuation">(</span>
    id     <span class="token keyword">BIGINT</span> <span class="token keyword">PRIMARY</span> <span class="token keyword">KEY</span><span class="token punctuation">,</span>
    name   <span class="token keyword">TEXT</span><span class="token punctuation">,</span>
    title  <span class="token keyword">TEXT</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span> sith <span class="token punctuation">(</span>
    id     <span class="token keyword">BIGINT</span> <span class="token keyword">PRIMARY</span> <span class="token keyword">KEY</span><span class="token punctuation">,</span>
    name   <span class="token keyword">TEXT</span><span class="token punctuation">,</span>
    title  <span class="token keyword">TEXT</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span> planet <span class="token punctuation">(</span>
    id      <span class="token keyword">BIGINT</span> <span class="token keyword">PRIMARY</span> <span class="token keyword">KEY</span><span class="token punctuation">,</span>
    name    <span class="token keyword">TEXT</span><span class="token punctuation">,</span>
    region  <span class="token keyword">TEXT</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span> station <span class="token punctuation">(</span>
    id      <span class="token keyword">BIGINT</span> <span class="token keyword">PRIMARY</span> <span class="token keyword">KEY</span><span class="token punctuation">,</span>
    name    <span class="token keyword">TEXT</span><span class="token punctuation">,</span>
    region  <span class="token keyword">TEXT</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span> fought_on_planet <span class="token punctuation">(</span>
    a       <span class="token keyword">BIGINT</span> <span class="token keyword">REFERENCES</span> jedi <span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">,</span>
    b       <span class="token keyword">BIGINT</span> <span class="token keyword">REFERENCES</span> planet <span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">,</span>
    battle  <span class="token keyword">TEXT</span><span class="token punctuation">,</span>
    <span class="token keyword">PRIMARY</span> <span class="token keyword">KEY</span> <span class="token punctuation">(</span>a<span class="token punctuation">,</span> b<span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span> fought_on_station <span class="token punctuation">(</span>
    a       <span class="token keyword">BIGINT</span> <span class="token keyword">REFERENCES</span> sith <span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">,</span>
    b       <span class="token keyword">BIGINT</span> <span class="token keyword">REFERENCES</span> station <span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">,</span>
    battle  <span class="token keyword">TEXT</span><span class="token punctuation">,</span>
    <span class="token keyword">PRIMARY</span> <span class="token keyword">KEY</span> <span class="token punctuation">(</span>a<span class="token punctuation">,</span> b<span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>The graph is declared over them:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">CREATE</span> PROPERTY GRAPH galaxy
    VERTEX <span class="token keyword">TABLES</span> <span class="token punctuation">(</span>
        jedi    <span class="token keyword">KEY</span> <span class="token punctuation">(</span>id<span class="token punctuation">)</span> LABEL force_user PROPERTIES <span class="token punctuation">(</span>id<span class="token punctuation">,</span> name<span class="token punctuation">,</span> title<span class="token punctuation">)</span><span class="token punctuation">,</span>
        sith    <span class="token keyword">KEY</span> <span class="token punctuation">(</span>id<span class="token punctuation">)</span> LABEL force_user PROPERTIES <span class="token punctuation">(</span>id<span class="token punctuation">,</span> name<span class="token punctuation">,</span> title<span class="token punctuation">)</span><span class="token punctuation">,</span>
        planet  <span class="token keyword">KEY</span> <span class="token punctuation">(</span>id<span class="token punctuation">)</span> LABEL place      PROPERTIES <span class="token punctuation">(</span>id<span class="token punctuation">,</span> name<span class="token punctuation">,</span> region<span class="token punctuation">)</span><span class="token punctuation">,</span>
        station <span class="token keyword">KEY</span> <span class="token punctuation">(</span>id<span class="token punctuation">)</span> LABEL place      PROPERTIES <span class="token punctuation">(</span>id<span class="token punctuation">,</span> name<span class="token punctuation">,</span> region<span class="token punctuation">)</span>
    <span class="token punctuation">)</span>
    EDGE <span class="token keyword">TABLES</span> <span class="token punctuation">(</span>
        fought_on_planet
            SOURCE <span class="token keyword">KEY</span> <span class="token punctuation">(</span>a<span class="token punctuation">)</span>      <span class="token keyword">REFERENCES</span> jedi <span class="token punctuation">(</span>id<span class="token punctuation">)</span>
            DESTINATION <span class="token keyword">KEY</span> <span class="token punctuation">(</span>b<span class="token punctuation">)</span> <span class="token keyword">REFERENCES</span> planet <span class="token punctuation">(</span>id<span class="token punctuation">)</span>
            LABEL fought_at     PROPERTIES <span class="token punctuation">(</span>battle<span class="token punctuation">)</span><span class="token punctuation">,</span>
        fought_on_station
            SOURCE <span class="token keyword">KEY</span> <span class="token punctuation">(</span>a<span class="token punctuation">)</span>      <span class="token keyword">REFERENCES</span> sith <span class="token punctuation">(</span>id<span class="token punctuation">)</span>
            DESTINATION <span class="token keyword">KEY</span> <span class="token punctuation">(</span>b<span class="token punctuation">)</span> <span class="token keyword">REFERENCES</span> station <span class="token punctuation">(</span>id<span class="token punctuation">)</span>
            LABEL fought_at     PROPERTIES <span class="token punctuation">(</span>battle<span class="token punctuation">)</span>
    <span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Labels group tables: <code>force_user</code> covers two vertex tables, <code>place</code> covers two, and <code>fought_at</code> covers both edge tables. The graph holds no data.<br>
In PostgreSQL it is a new kind of relation (<code>RELKIND_PROPGRAPH</code>) that lives in <code>pg_class</code> with no heap and no columns of its own, so it behaves like a view. Five new catalogs (<code>pg_propgraph_element</code>, <code>pg_propgraph_label</code> and three more) record its elements, labels, and properties.</p>
<p>Querying it:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">SELECT</span> <span class="token operator">*</span>
<span class="token keyword">FROM</span> GRAPH_TABLE <span class="token punctuation">(</span>
            galaxy
      <span class="token keyword">MATCH</span> <span class="token punctuation">(</span>a <span class="token operator">IS</span> force_user<span class="token punctuation">)</span><span class="token operator">-</span><span class="token punctuation">[</span><span class="token operator">IS</span> fought_at<span class="token punctuation">]</span><span class="token operator">-</span><span class="token operator">></span><span class="token punctuation">(</span>b <span class="token operator">IS</span> place<span class="token punctuation">)</span>
    <span class="token keyword">COLUMNS</span> <span class="token punctuation">(</span>a<span class="token punctuation">.</span>name <span class="token keyword">AS</span> fighter<span class="token punctuation">,</span> b<span class="token punctuation">.</span>name <span class="token keyword">AS</span> battlefield<span class="token punctuation">,</span> b<span class="token punctuation">.</span>region<span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<h3 id="how-the-rewrite-works"><a class="heading-anchor" href="#how-the-rewrite-works">How the rewrite works ?</a></h3>
<p>PostgreSQL has no new executor node. The parser turns <code>GRAPH_TABLE</code> into a new range table entry kind, <code>RTE_GRAPH_TABLE</code>. The rewriter, the same stage that expands views, then replaces it with an ordinary subquery.<br>
It does this in four steps:</p>
<ol class="list">
<li>The pattern is read left to right into a list of element patterns: vertices, edges in either direction, and nested sub-patterns.</li>
<li>Each unique variable name becomes one path factor. A variable that appears twice, as in a cycle that returns to <code>a</code>, shares one factor, and its predicates are <code>AND</code>ed.</li>
<li>Each label expression is resolved to the tables it covers, giving one path element per table.</li>
<li>Every combination of elements along the path is enumerated by a recursive DFS (Depth-First Search). A combination is valid only if each edge table’s source and destination keys really connect the two neighboring vertex tables. Each valid combination becomes a <code>JOIN</code> between edge and vertex tables on those keys, and all of them are combined with <code>UNION ALL</code>.</li>
</ol>
<p>In our query, <code>force_user</code> has two tables, <code>fought_at</code> has two, and <code>place</code> has two, so there are 2 × 2 × 2 = 8 combinations. Only two are valid: a Jedi on a planet, and a Sith at a station. The query becomes equivalent to this:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">SELECT</span> jedi<span class="token punctuation">.</span>name<span class="token punctuation">,</span> planet<span class="token punctuation">.</span>name<span class="token punctuation">,</span> planet<span class="token punctuation">.</span>region
  <span class="token keyword">FROM</span> jedi<span class="token punctuation">,</span> fought_on_planet<span class="token punctuation">,</span> planet
 <span class="token keyword">WHERE</span> jedi<span class="token punctuation">.</span>id <span class="token operator">=</span> fought_on_planet<span class="token punctuation">.</span>a
   <span class="token operator">AND</span> planet<span class="token punctuation">.</span>id <span class="token operator">=</span> fought_on_planet<span class="token punctuation">.</span>b
<span class="token keyword">UNION</span> <span class="token keyword">ALL</span>
<span class="token keyword">SELECT</span> sith<span class="token punctuation">.</span>name<span class="token punctuation">,</span> station<span class="token punctuation">.</span>name<span class="token punctuation">,</span> station<span class="token punctuation">.</span>region
  <span class="token keyword">FROM</span> sith<span class="token punctuation">,</span> fought_on_station<span class="token punctuation">,</span> station
 <span class="token keyword">WHERE</span> sith<span class="token punctuation">.</span>id <span class="token operator">=</span> fought_on_station<span class="token punctuation">.</span>a
   <span class="token operator">AND</span> station<span class="token punctuation">.</span>id <span class="token operator">=</span> fought_on_station<span class="token punctuation">.</span>b<span class="token punctuation">;</span></code></pre>
<p>From this point, the planner and executor see joins and a union, nothing graph-specific. This is somewhat valuable because we benefit from indexes, statistics, parallel query, and everything else PostgreSQL has, which recalls the argument we made in the previous blog in the series about graph capabilities on top of mature systems.</p>
<p><strong>There is a subtlety here that matters for how you read any benchmark comparing the two formalisms.</strong></p>
<p>In a fixed-length pattern, SQL/PGQ is obliged to touch the vertex tables. The pattern binds vertices, so the join to the vertex table is part of the semantics even when no vertex property is requested, because there are no foreign keys (FKs). If you model the same graph yourself in plain SQL and only need the edge traversal, provided that you define FKs, joining the edge tables alone is enough.</p>
<p><em>Note: There’s a recent non-committed effort from PostgreSQL hackers to remove inner joins based on foreign keys, which is beneficial for SQL/PGQ + FKs on base graph element tables rewrite<sup class="footnote-ref"><a href="#fn7" id="fnref7">[7]</a></sup>.</em></p>
<h3 id="whats-supported-and-whats-not"><a class="heading-anchor" href="#whats-supported-and-whats-not">What’s Supported and What’s Not</a></h3>
<p>The standard tracks 100 features. The committed implementation had 26 of them. It supports graph patterns with <code>WHERE</code>, vertex patterns, edge patterns in full and abbreviated form, label disjunction (<code>label1|label2</code>), property references, <code>GRAPH_TABLE</code>, the DDL including <code>ALTER</code> and <code>DROP</code>, several labels per element table, and views as element tables.</p>
<p>The most interesting ones out of the other 74 it lacks fall (mostly) into five mechanisms that the standard defines on top of a basic pattern:</p>
<table>
<thead>
<tr>
<th>Mechanism</th>
<th>What it decides</th>
<th>PostgreSQL</th>
</tr>
</thead>
<tbody>
<tr>
<td>Quantifiers: <code>{n,m}</code>, <code>{n}</code>, <code>+</code>, <code>*</code></td>
<td>How many hops a pattern spans</td>
<td>Not supported</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Path modes: <code>WALK</code>, <code>TRAIL</code>, <code>SIMPLE</code>, <code>ACYCLIC</code></td>
<td>Which paths are admissible (what may repeat)</td>
<td>Behaves as <code>WALK</code>, keywords rejected</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Path search prefixes: <code>ALL</code>, <code>ANY</code>, <code>ALL SHORTEST</code>, <code>SHORTEST n</code></td>
<td>Which admissible paths are returned</td>
<td>Only <code>ALL</code>, the default</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Match modes: <code>REPEATABLE ELEMENTS</code>, <code>DIFFERENT EDGES</code></td>
<td>Whether edges may bind twice</td>
<td>Only <code>REPEATABLE ELEMENTS</code></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Several comma-separated path patterns in one <code>MATCH</code></td>
<td>Joins patterns on shared variables</td>
<td>Existing patch, delayed until after quantifiers.</td>
</tr>
</tbody>
</table>
<p>PostgreSQL is not alone here. Oracle’s SQL/PGQ is similarly narrow, DuckPGQ adds only <code>ANY SHORTEST</code>, and Oracle’s older PGQL supports most of these features.</p>
<h3 id="the-revert"><a class="heading-anchor" href="#the-revert">The revert</a></h3>
<p>SQL/PGQ was committed on March 16, 2026, for PostgreSQL 19. In early September, a <code>pgsql-hackers</code> thread on some issues in its catalog representation/definition and object dependencies ended with a decision to take it out.<br>
Reviewers argued it should be reverted from 19 so the problems could be fixed properly and easily for 20, without backwards-compatibility pressure.</p>
<p>The feature is expected back in an upcoming version. The rewrite itself is not the part being redesigned, so it should survive the rework unchanged, which means further work, VLE (variable length edge), multi-path patterns, etc. can be built on top of the same foundation.</p>
<p>It is worth noting that none of this had to happen in the core at all. PostgreSQL has no parser hook, so an extension cannot add grammar, and parser hooks have been proposed and discussed repeatedly without landing. The latest mention was about how to release SQL/PGQ as an experimental feature<sup class="footnote-ref"><a href="#fn8" id="fnref8">[8]</a></sup>.<br>
That leaves SQL/PGQ with only two options: work inside core, or do what Apache AGE does and parse a string at runtime; of course, the latter is something for an extension, not a core feature.<br>
<strong>Had extensions been able to extend the parser, this could have been built and proven outside the tree, and only then considered for a contrib module or for core. That is not an argument that parser hooks should exist. It is an observation about where the cost of not having them ends up.</strong></p>
<h3 id="the-feature-everyone-asks-for"><a class="heading-anchor" href="#the-feature-everyone-asks-for">The feature everyone asks for</a></h3>
<p>Of the missing pieces, one dominates every conversation about graph queries: <strong>variable-length edges</strong> (VLE).<br>
It is the feature graph practitioners ask for first, and it is often the reason someone reaches for a graph query language instead of writing joins. The difficulty is proportional to the scale. A bounded pattern over two or three hops can be done with a similar rewrite to joins. A pattern that can reach hundreds of hops, or run unbounded until it exhausts the graph, is a different problem with its own memory, termination, and cycle-handling requirements.</p>
<p>The standard makes this harder than it looks, because it defines several mechanisms and features that interact with it. How far the pattern goes, which paths count as admissible, and which of those are returned are decided independently, and each combination implies a different execution strategy. All paths over a bounded depth, a single shortest path, and distinct reachable endpoints are different patterns we should think of in design. Of course, one can NOT support everything, but it’s important to keep such semantics in mind.</p>
<p>Implementations vary. One approach is another rewrite, this time to recursive CTEs, leaning on <code>SEARCH</code> and <code>CYCLE</code> clauses and doing a lot of the bookkeeping in the query itself for the different semantics. At the end of the day, it is set-based BFS (breadth-first search) expressed as joins; this is mostly okay but it won’t be any performant or flexible to work with in combination with any other features like path variables.</p>
<p>Another approach expands each possible length into its own <code>UNION ALL</code> arm, which cannot handle the unbounded case. This is a very bad idea; Oracle does this. Then there are executor-level strategies: DFS with path state held for backtracking, or BFS run directly in the executor (all with new executor nodes). Each is suited to a different goal, whether that is reachability, shortest path, or enumerating all possible paths. The future implementation will consider all of these, <strong>otherwise we can’t consider Postgres as a competitor, at all.</strong></p>
<h2 id="key-takeaways"><a class="heading-anchor" href="#key-takeaways">Key Takeaways</a></h2>
<p>SQL/PGQ rewrites graph patterns to joins — no new executor nodes, no graph engine. The planner handles the rest with indexes, statistics, and parallel query.</p>
<p>The committed implementation covered 26 of 100 standard features, enough to prove the approach but stopping before the hard problems. Quantifiers, path modes, and variable-length edges were intentionally deferred.</p>
<p>The revert was about timing. Catalogs and dependency tracking need a cleaner foundation for PostgreSQL 20. The rewrite engine itself is not being redesigned.</p>
<p>Variable-length edges management is the required feature. The standard defines multiple mechanisms on top of VLE that imply different execution strategies — set-based BFS via recursive CTEs, per-length UNION ALL arms, or executor-level DFS/BFS with new nodes. Each serves a different goal.</p>
<p>Without parser hooks, SQL/PGQ must live in core or use runtime string parsing.</p>
<h2 id="whats-next"><a class="heading-anchor" href="#whats-next">What’s Next?</a></h2>
<p>The next post looks at the other graph options in PostgreSQL: Apache AGE, pgRouting, and the practical case for or against each.<br>
After that, a benchmark comparing SQL/PGQ against Apache AGE.<br>
And finally, what the long-term picture looks like for graph queries in PostgreSQL, and why the trajectory points toward relational systems absorbing graph workloads.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p><a href="https://neo4j.com/docs/getting-started/cypher/" rel="noopener">https://neo4j.com/docs/getting-started/cypher/</a> <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p><a href="https://tinkerpop.apache.org/gremlin.html" rel="noopener">https://tinkerpop.apache.org/gremlin.html</a> <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p><a href="https://pgql-lang.org/" rel="noopener">https://pgql-lang.org/</a> <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p><a href="https://www.iso.org/standard/76120.html" rel="noopener">https://www.iso.org/standard/76120.html</a> <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn5" class="footnote-item"><p><a href="https://www.iso.org/standard/79473.html" rel="noopener">https://www.iso.org/standard/79473.html</a> <a href="#fnref5" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn6" class="footnote-item"><p><a href="https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-SEARCH" rel="noopener">https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-SEARCH</a> <a href="#fnref6" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn7" class="footnote-item"><p><a href="https://www.postgresql.org/message-id/CAMbWs49-v9FGX+B=tsCxUq_FPX09ZNvXjj+TaTx0m-MAVmY5TA@mail.gmail.com" rel="noopener">https://www.postgresql.org/message-id/CAMbWs49-v9FGX+B=tsCxUq_FPX09ZNvXjj+TaTx0m-MAVmY5TA@mail.gmail.com</a> <a href="#fnref7" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn8" class="footnote-item"><p><a href="https://www.postgresql.org/message-id/33d590a7-4e26-41a8-acd4-51720dddf969%40eisentraut.org" rel="noopener">https://www.postgresql.org/message-id/33d590a7-4e26-41a8-acd4-51720dddf969%40eisentraut.org</a> <a href="#fnref8" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
 ]]></content>
			<author>
				<name>Ayoub Kazar</name>
			</author>
    </entry>
</feed>
