Skip to main content
Version: 0.3.36

Graph Queries (GRAPH_TABLE)

GRAPH_TABLE is RaisinDB's implementation of SQL/PGQ (SQL:2023 property graph queries). It matches a pattern against the relations of a repository and returns the matches as a table that the rest of the query can filter, sort, group and join.

Syntax

SELECT *
FROM GRAPH_TABLE(
[graph_name]
MATCH pattern [, pattern ...]
[WHERE condition]
COLUMNS ( expression [AS alias] [, ...] )
) [AS alias]

Write GRAPH_TABLE( with no space before the parenthesis. graph_name is optional and is ignored for scoping; relations of every workspace on the current branch are searched. Give the expression an alias when the outer query refers to its columns (... ) AS g WHERE g.hops > 1).

Node patterns

-- every node that has at least one relation
SELECT * FROM GRAPH_TABLE(
MATCH (n)
COLUMNS (n.path, n.node_type)
);

-- nodes of one type
SELECT * FROM GRAPH_TABLE(
MATCH (n:Article)
COLUMNS (n.title, n.path)
);

-- with a filter
SELECT * FROM GRAPH_TABLE(
MATCH (n:Article)
WHERE n.status = 'published'
COLUMNS (n.title, n.created_at)
);

A single-node pattern is resolved from the relation index, so it returns only nodes that are the source or target of at least one relation. Use an ordinary SELECT to list every node of a type.

Labels

Node types are namespaced (news:Article, raisin:Folder). A label is the part after the colon, matched case-insensitively:

MATCH (n:Article)          -- matches news:Article and studio:Article
MATCH (n:`news:Article`) -- backticks pin the full type
MATCH (n:news:Article) -- parse error

(n:Article|Page) matches either label. Filtering on n.node_type in the WHERE clause is another way to pin a namespace.

Where filters go

Predicates belong in the WHERE clause of the GRAPH_TABLE, between MATCH and COLUMNS. A WHERE written inside a node or edge pattern ((n:Page WHERE …)) is a parse error; the message points at the supported form.

Relationship patterns

-- directed, typed
SELECT * FROM GRAPH_TABLE(
MATCH (a:Person)-[:follows]->(b:Person)
COLUMNS (a.name AS follower, b.name AS followed)
);

-- reverse direction
SELECT * FROM GRAPH_TABLE(
MATCH (a:Person)<-[:follows]-(b:Person)
WHERE a.path = '/alice'
COLUMNS (b.name AS follower)
);

-- bound edge variable
SELECT * FROM GRAPH_TABLE(
MATCH (a)-[r:follows]->(b)
WHERE r.weight > 1
COLUMNS (a.name, r.weight, b.name)
);

Relation types match exactly, including case ([:follows] does not match an edge stored as FOLLOWS). A type containing a hyphen must be backticked: [:`tagged-with`]. [:follows|likes] matches either type.

An edge pattern without an arrow, (a)-[:follows]-(b), is accepted but currently matches the stored direction only, the same as (a)-[:follows]->(b). Write two patterns when you need both directions.

Edge fields

An edge variable exposes two fields:

FieldTypeDescription
r.relation_typeTEXTThe type given in RELATE … TYPE
r.weightDOUBLEThe WEIGHT, or NULL when none was set

Relations carry no other properties; to weight edges by a domain value, write it into weight when you create them.

Path patterns and quantifiers

-- fixed length
SELECT * FROM GRAPH_TABLE(
MATCH (a:Page)-[:LINKS_TO]->(b)-[:LINKS_TO]->(c)
COLUMNS (a.title AS start, c.title AS end)
);

-- variable length
SELECT * FROM GRAPH_TABLE(
MATCH (a:Page)-[:LINKS_TO]->{1,3}(b:Page)
COLUMNS (a.title, b.title)
);

The quantifier follows the arrow:

QuantifierHops
->{2}exactly 2
->{1,3}1 to 3
->{2,}2 or more
->*0 or more
->+1 or more
->?0 or 1

An unbounded quantifier (*, +, {m,}) must be inside the scope of a path selector or a path restrictor; MATCH (a)-[:t]->*(b) on its own is a parse error, while MATCH ANY SHORTEST p = (a)-[:t]->*(b) and MATCH TRAIL (a)-[:t]->*(b) are accepted. Even then traversal stops at 10 hops. Bounded quantifiers need neither.

Every matching path is a row. A node reachable by two routes appears twice; use SELECT DISTINCT in the outer query to collapse them.

Cypher-style quantifier

The older spelling inside the brackets still parses and is mapped as follows:

Older formEquivalent
-[:t*2]->-[:t]->{2}
-[:t*1..3]->-[:t]->{1,3}
-[:t*2..]->-[:t]->{2,}
-[:t*]->-[:t]->{1,}

Note that * inside the brackets means one or more, while ->* means zero or more. The bracket form does not need a selector or restrictor and is capped at 10 hops.

Path variables

Bind the path to a variable to read it with the path accessors:

SELECT hops, stops
FROM GRAPH_TABLE(
MATCH p = (a:Person)-[:follows]->{1,3}(b:Person)
WHERE a.path = '/dave'
COLUMNS (path_length(p) AS hops, nodes(p) AS stops)
);
{"columns":["hops","stops"],
"rows":[{"hops":1,"stops":[{"id":"f3ea…","workspace":"social","node_type":"social:Person"},
{"id":"6637…","workspace":"social","node_type":"social:Person"}]},
{"hops":2,"stops":[]}]}

The variable can be written before the selector (p = ANY SHORTEST (...)) or after the restrictor (ANY SHORTEST TRAIL p = (...)). A path variable is not selectable on its own; COLUMNS (p) is rejected with a message naming the accessors.

Path accessors

AccessorReturns
path_length(p)hop count
nodes(p)array of {id, workspace, node_type} in order
edges(p)array of {source_id, source_workspace, target_id, target_workspace, relation_type, weight}
path_first(p)the first node
path_last(p)the last node
element_id(p)a stable string identifying the whole path
is_trail(p)whether no edge repeats
is_acyclic(p)whether no node repeats

element_id takes a path variable only; it is not defined for a plain edge variable.

Path selectors

A selector limits how many paths are returned per pair of endpoints:

SelectorMeaning
(none)every matching path
ANYone arbitrary path per endpoint pair
ANY SHORTESTone minimum-hop path per endpoint pair
ALL SHORTESTevery minimum-hop path per endpoint pair
ANY CHEAPESTone minimum-cost path (RaisinDB extension, requires COST)
-- fewest hops from dave to alice
SELECT hops FROM GRAPH_TABLE(
MATCH ANY SHORTEST p = (a:Person)-[:follows]->{1,6}(b:Person)
WHERE a.path = '/dave' AND b.path = '/alice'
COLUMNS (path_length(p) AS hops)
);
-- hops: 1

-- cheapest by edge weight
SELECT hops FROM GRAPH_TABLE(
MATCH ANY CHEAPEST p = (a:Stop)-[r:route COST r.weight]->{1,8}(b:Stop)
COLUMNS (path_length(p) AS hops)
);

COST needs a bound edge variable and must be r.weight (or a positive literal); COST on an anonymous edge, on another variable, or naming a field other than weight is a parse error. ANY CHEAPEST and COST go together: either without the other is an error. Every edge on a cheapest path must carry a positive weight; an unweighted edge on the way makes the query fail with a message naming the edge. SHORTEST k, SHORTEST k GROUP and ANY k are not implemented.

Path restrictors

RestrictorMeaning
WALKnodes and edges may repeat
TRAILno edge is traversed twice
ACYCLICno node is visited twice (default)

Write the selector before the restrictor: ANY SHORTEST TRAIL p = (...). SIMPLE is not implemented.

Properties and system fields

Inside WHERE and COLUMNS, a node variable exposes its system fields and any property by name; n.title and n.properties->>'title' are equivalent.

FieldDescription
idnode id
pathhierarchical path
namelast path segment
node_typefull node type, e.g. social:Person
workspaceworkspace the node lives in
parent_idparent node id
created_at, updated_attimestamps
SELECT * FROM GRAPH_TABLE(
MATCH (n:Article)
COLUMNS (n.id, n.workspace, n.node_type, n.path, n.name, n.title)
);

Without an alias a column is named variable_field (n.title becomes n_title).

Expressions in COLUMNS and WHERE

Comparisons, AND/OR/NOT, arithmetic on numeric fields and the graph algorithm functions are supported:

SELECT * FROM GRAPH_TABLE(
MATCH (a)-[r:follows]->(b)
WHERE r.weight > 1
COLUMNS (a.name, r.weight * 2 AS doubled)
);

General SQL functions such as UPPER() or PARENT() are not available inside GRAPH_TABLE; apply them in the outer query instead.

COUNT(x) and COLLECT(x) are accepted inside COLUMNS, but they aggregate over the entire match set (there is no per-node grouping inside the pattern). For per-node counts, aggregate in the outer query:

SELECT followed, COUNT(*) AS followers
FROM GRAPH_TABLE(
MATCH (a:Person)-[:follows]->(b:Person)
COLUMNS (b.name AS followed)
) AS g
GROUP BY followed
ORDER BY followers DESC;

Combining with SQL

-- filter and sort the matches
SELECT * FROM GRAPH_TABLE(
MATCH (a:Person)-[:follows]->(b:Person)
COLUMNS (a.name AS follower, b.name AS followed)
) AS g
ORDER BY follower
LIMIT 20;

-- join back to a workspace
SELECT g.follower, s.properties->>'city' AS city
FROM GRAPH_TABLE(
MATCH (a:Person)-[:follows]->(b:Person)
COLUMNS (a.name AS follower, a.id AS follower_id)
) AS g
JOIN 'social' s ON s.id = g.follower_id;

-- collapse duplicate paths
SELECT DISTINCT reached
FROM GRAPH_TABLE(
MATCH (a:Person)-[:follows]->{1,3}(b:Person)
WHERE a.path = '/dave'
COLUMNS (b.name AS reached)
) AS g;

Time-travel predicates (__revision) apply to workspace tables, not to GRAPH_TABLE; the pattern is always matched against the current state of the branch.

Multiple patterns

-- two patterns sharing a variable
SELECT * FROM GRAPH_TABLE(
MATCH
(a:Page)-[:LINKS_TO]->(b:Page),
(b)-[:LINKS_TO]->(c:Page)
COLUMNS (a.title, b.title, c.title)
);

-- the same as one chain
SELECT * FROM GRAPH_TABLE(
MATCH (a:Page)-[:LINKS_TO]->(b:Page)-[:LINKS_TO]->(c:Page)
WHERE a.id <> c.id
COLUMNS (a.title AS start, b.title AS middle, c.title AS end)
);

Examples

Pages linked from a page

SELECT linked_title
FROM GRAPH_TABLE(
MATCH (start:Page)-[:LINKS_TO]->(linked:Page)
WHERE start.title = 'Home'
COLUMNS (linked.title AS linked_title)
) AS results
ORDER BY linked_title;

Pages reachable in exactly two hops

SELECT DISTINCT end_title
FROM GRAPH_TABLE(
MATCH (start:Page)-[:LINKS_TO]->{2}(end:Page)
WHERE start.title = 'Home' AND start.id <> end.id
COLUMNS (end.title AS end_title)
) AS results;
SELECT page_title, COUNT(*) AS incoming_links
FROM GRAPH_TABLE(
MATCH (source:Page)-[:LINKS_TO]->(target:Page)
COLUMNS (target.title AS page_title)
) AS links
GROUP BY page_title
ORDER BY incoming_links DESC
LIMIT 10;
SELECT * FROM GRAPH_TABLE(
MATCH (a:Article)-[:`tagged-with`]->(t:Tag)<-[:`tagged-with`]-(b:Article)
WHERE a.path = '/articles/graph-intro' AND a.id <> b.id
COLUMNS (b.title AS related, t.name AS via_tag)
);

Members of a project, two node types

SELECT * FROM GRAPH_TABLE(
MATCH (person:User|Admin)-[:MEMBER_OF]->(project:Project)
COLUMNS (person.name, person.node_type AS role, project.name AS project_name)
);

See also