Problem solving
Reachable, how far, and by what route
Grain: one row per reachable pair · Class: Graph · Signals: reachable, can get to, connected to, within N hops, shortest route, cheapest route, via · Operators:
CLOSURE,PATH,TRACE
The problem
"From each airport, where can I get to with any number of connections? Which are within two hops? And what is the cheapest itinerary between two cities, and which airports does it pass through?"
How to recognise it
The question is about following a directed edge repeatedly — reachable, can get to, within N hops, the route via, the cheapest way. What makes it a graph problem rather than a join is the chain: a single join finds the direct edges, but reachable means any number of hops. Three operators answer three sharpenings of the question, and the tell is how much you need to know about the connection:
- Just whether you can get there —
CLOSURE(every reachable pair, any distance). - Within a hop budget, and how far —
PATH(a hop window, stamping each pair with its shortest distance). - The best weighted route, and its stops —
TRACE(the cheapest/highest route as an ordered node sequence).
For undirected "who belongs together", see Things that belong together (CLUSTER); this recipe is directed.
The data
Direct flights with a fare. Edges is the two-column projection the closure operators want.
Flights := [
| origin | dest | cost |
|--------|------|------|
| JFK | ORD | 200 |
| ORD | LAX | 150 |
| JFK | LAX | 500 |
| ORD | DEN | 100 |
| DEN | LAX | 80 |
];
Edges := { π origin, dest (Flights) };
Flights := [
| origin | dest | cost |
|--------|------|------|
| JFK | ORD | 200 |
| ORD | LAX | 150 |
| JFK | LAX | 500 |
| ORD | DEN | 100 |
| DEN | LAX | 80 |
];
Edges := { PROJECT origin, dest (Flights) };
Recipe 1: everything reachable (CLOSURE)
CLOSURE from, to follows the edge to a fixpoint and returns every pair connected by one or more hops — the direct edges plus every derived one.
query { τ origin, dest (CLOSURE origin, dest (Edges)) };
query { SORT origin, dest (CLOSURE origin, dest (Edges)) };
origin dest
────── ────
DEN LAX
JFK DEN
JFK LAX
JFK ORD
ORD DEN
ORD LAX
(6 rows)
"Can I get from JFK to anywhere?" is then a plain σ on the result — and the optimiser folds that σ into the closure as a single-source search, so scoping to one origin is an ordinary selection, not new syntax:
query { σ origin = "JFK" (CLOSURE origin, dest (Edges)) };
query { SELECT origin = "JFK" (CLOSURE origin, dest (Edges)) };
origin dest
────── ────
JFK ORD
JFK LAX
JFK DEN
(3 rows)
RCLOSURE additionally pairs each node with itself, for when "stay put" is a valid route.
Recipe 2: within a hop budget, with distance (PATH)
When the question caps the distance — within two hops — and wants to know how far, PATH takes a hop window and stamps each pair with its shortest distance:
query { τ hops, dest (σ origin = "JFK" (PATH origin, dest HOPS 1 TO 2 AS hops (Edges))) };
query { SORT hops, dest (SELECT origin = "JFK" (PATH origin, dest HOPS 1 TO 2 AS hops (Edges))) };
origin dest hops
────── ──── ────
JFK LAX 1
JFK ORD 1
JFK DEN 2
(3 rows)
The distance is the shortest path length, so (from, to) is a candidate key — a pair never appears at two depths. As with CLOSURE, a σ on an endpoint seeds the traversal at that node.
Recipe 3: the optimal route, and its stops (TRACE)
Reachability says whether; TRACE says how, and how much. Give it a weight column and MINIMIZE/MAXIMIZE, and it returns the best route for every reachable pair as an ordered array of nodes:
query { τ origin, dest (TRACE origin, dest VIA cost MINIMIZE AS route (Flights)) };
query { SORT origin, dest (TRACE origin, dest VIA cost MINIMIZE AS route (Flights)) };
origin dest cost route
────── ──── ──── ───────────────
DEN LAX 80 [DEN, LAX]
JFK DEN 300 [JFK, ORD, DEN]
JFK LAX 350 [JFK, ORD, LAX]
JFK ORD 200 [JFK, ORD]
ORD DEN 100 [ORD, DEN]
ORD LAX 150 [ORD, LAX]
(6 rows)
TRACE uses the whole Flights relation (it needs the weight), not the two-column Edges. Explode the route into rows with μ route (…) when you want one stop per row.
Variations
- Undirected traversal — union the reversed edges into the input first, or use the
↔separator (CLOSURE from ↔ to,PATH from ↔ to), which reads each edge both ways. - Weighted reachability without the route —
--provenance --semiring tropicalgives shortest-path costs throughCLOSUREwithout materialising the paths. - A rule the two-column form cannot express — extra columns, a filter or a join each round — is general recursion (
FIX).
Pitfalls
CLOSUREandPATHare two-column, and directed. Project to exactly the edge columns first (Edgesabove), and remember the edge direction —from → tois not the same graph asto → from. Use↔for undirected.CLOSUREhas no distance;PATHhas no route;TRACEhas both cost and route. Reaching forCLOSUREwhen you needed the hop count, orPATHwhen you needed the actual stops, is the common miss — pick by how much you need to know.- Scope with a σ, not by hand. The start node is deliberately not part of the syntax:
σ origin = cabove the operator is folded into a single-source search. A predicate on the distance or an inequality stays as a residual σ (correct, just not pushed). - Dense graphs are large, and these block. All-pairs reachability can be quadratic; the operators buffer and never push down, and
--max-fixpoint-roundsguards a runaway. TRACEweights should be whole units for an exact total — scale money to cents, since floating-point sums can differ in their last bits.
Check it
- Put a cycle in the edges and confirm
CLOSUREstill terminates (a derived pair is never re-added) — closure over cyclic data is exactly where a hand-rolled join loops forever. - Put a pair reachable two ways (JFK→LAX direct and via ORD) and confirm
CLOSURElists it once,PATHreports the shortest distance, andTRACEkeeps the cheapest route. - Check a node just outside the hop window is excluded by
PATHand included byCLOSURE.
Related
- Things that belong together — undirected connectivity with
CLUSTER, the other half of the graph class. - When the rule is recursive —
FIX, the general form these specialise. - As columns, as a list, as a tree —
TREEfolds the same parent edges into nested documents;μexplodes aTRACEroute. - Reference pages (
docs/reference):closure,path,trace.