
Inside PostgreSQL MATCH Queries: Syntax, Paths, and Parameters with pg_igraph
Cypher-like pattern matching inside PostgreSQL sounds convenient.
But there is an important distinction: pg_igraph is not taking a MATCH-shaped string and doing a few regex replacements before handing it back to SQL.
It has its own grammar.
That changes both what you can express and how you should think about using it.
In Part 1, I covered the core idea behind pg_igraph and why keeping graph traversal inside PostgreSQL can be useful.
This time, let's look at the query language itself.
1. MATCH is parsed, not rewritten
A basic traversal can look like this:
SELECT igraph_query('
MATCH (u:User)-[:FOLLOWS*1..3]->(friend)
WHERE u.id = 42
RETURN friend
');
The important part is not just the Cypher-like syntax.
Under the hood, pg_igraph runs the query through its own lexer and parser and builds an actual AST.
Conceptually, the flow is:
MATCH query
β
lexer
β
parser
β
AST
β
executor
β
result
That means the graph expression is treated as a real language construct rather than a string-rewrite shortcut.
For application code, that matters because traversal intent stays explicit.
You are describing the graph pattern you want, instead of manually assembling recursive SQL around it.
2. PATH covers the common shortest-path case
Not every graph query needs a full MATCH expression.
When the problem is simply:
Find a path from node A to node B through this relationship type.
pg_igraph provides a shorter form:
SELECT igraph_query('PATH FROM 100 TO 500 VIA FOLLOWS');
This avoids writing a larger traversal pattern when the intent is already clear.
A useful detail: FROM and TO take literal node IDs, while VIA takes a bare relationship name.
This form is intentionally narrow.
It is not a replacement for MATCH; it is a shorthand for one common path-query shape.
3. Parameters stay outside the graph expression
Hardcoding application values into graph queries is not something you want to scale.
pg_igraph supports JSON-backed parameters:
SELECT igraph_query(
'',
'MATCH (u:User)-[:FOLLOWS]->(f)
WHERE f.influence > &data.threshold
AND u.id != &data.exclude_id
RETURN f.name',
'{"data":{"threshold":100,"exclude_id":42}}'
);
The useful part is not only convenience.
Application values do not have to be interpolated directly into the graph expression itself.
The supported comparison operators include:
=
>
<
>=
<=
!=
That is enough for many product-level filters around graph traversal.
It is also worth keeping the boundary clear:
MATCH is not a general-purpose ORM query builder.
It exists specifically for the graph-shaped part of the query.
4. Know the response contract
One detail that is easy to miss is that not every command returns results in exactly the same shape.
Row-returning MATCH / RETURN queries return their data directly.
Commands such as:
CREATEDELETESETPATH
still use a status-style response such as:
{"status":"ok"}
This distinction matters when you wrap graph queries in application code.
Do not assume that every igraph_query() call has the same response envelope.
Treat row-returning queries and mutation/path operations as different API shapes.
5. MATCH is its own grammar
There is another architectural consequence.
Although you call it from PostgreSQL, the content inside igraph_query() is not standard SQL.
That means tooling that expects to parse SQL directly may not understand the graph expression.
Some ORMs, proxies, linters, or query inspection layers may simply see an opaque function argument.
That is not necessarily a problem.
It just means the abstraction boundary is important:
PostgreSQL remains the host database, while
MATCHhandles the graph-pattern part of the query.
If your surrounding tooling needs to inspect or rewrite every query as plain SQL, that is something to evaluate early.
When this is useful
This approach is especially interesting when:
- your source-of-truth data already lives in PostgreSQL,
- relationships matter to product behavior,
- recursive CTEs are becoming difficult to read,
- you want graph traversal without introducing another operational datastore.
A dedicated graph database can still be the better choice for deep graph analytics or specialized graph workloads.
But for product traversal inside an existing PostgreSQL system, keeping the graph layer close to the data can be a very practical trade-off.
Next: traversal patterns you can actually ship
In Part 3, I'll move from syntax to product queries:
- direct relationships,
- two-hop traversal,
- bounded variable-length expansion,
- and the production considerations that matter once the graph starts growing.
If you have ever hand-written a recursive CTE for something that was really a graph-pattern query, I'd be curious to see what the SQL eventually turned into.