From "Montreal" as the origin point, I’m trying to find the shortest path to go to another city. But I’m having difficulty when a city is looped.
demo at db<>fiddle
create table flights(id,src,dst,cost)as values
(0,'Montreal','NY', 742.94)
,(1,'Montreal','Detroit', 362.47)
,(2,'NY', 'Detroit', 936.60)
,(3,'Detroit', 'LA', 64.32 )
,(4,'NY', 'Montreal',94.26 )
,(5,'Detroit', 'NY', 213.86)
,(6,'LA', 'Detroit', 490.88);
If I’m going to LA, there’s 2 ways:
- 3 steps (Montreal-NY-Detroit-LA)
- 2 steps (Montreal-Detroit-LA),
I want that the shortest, 2-step way to LA via (only) Detroit to be shown.
I tried this code and I was expecting that the shortest steps would be shown:
WITH RECURSIVE ShortestFlight AS (
SELECT src
, dst
, cost
, cost AS total_cost
, 1 AS steps
FROM Flights
WHERE src = 'Montreal'
UNION
SELECT f.src
, f.dst
, f.cost
, sf.total_cost + f.cost AS total_cost
, sf.steps + 1
FROM Flights f
JOIN ShortestFlight sf
ON f.src = sf.dst
WHERE NOT EXISTS (
SELECT FROM ShortestFlight
WHERE sf2.dst = f.dst
AND sf2.steps <= sf.steps + 1
)
)
SELECT *
FROM ShortestFlight
ORDER BY steps
, total_cost;
but I get the error on
ERROR: recursive reference to query "shortestflight" must not appear within a subquery LINE 21: SELECT FROM ShortestFlight ^
I would like to know an alternative so my code works.
2
Answers
Pick the shortest path using a
distinct on
(ororder by..limit 1
, or window/aggregate functions). As already pointed out by @Laurenz Albe, you also need to add cycle detection:demo at db<>fiddle
It skips the longer path but most importantly, this code doesn’t fall into an infinite loop. Without cycle detection there was nothing that would stop the CTE from falling into a Detroit-Montreal-Detroit loop (or any pair with a return connection available), for eternity or until it times out or hits a stack limit.
While you can use recursive CTEs to implement routing algorithms, there’s
pgrouting
extension that can handle that way more effectively. If you add airport locations you can even upgrade Dijkstra to A*.As already pointed out by @LaurenzAlbe and @Zegrek, you need to add cycle detection.
You will not be able to check for a loop using a subquery to a recursive subquery. You can eliminate loops by manually composing a path and checking for a point in that path, or using
CYCLE
option for recursive query.I also think that it is better to determine the "short" route ranking by a window function.
You may have several paths with the same number of steps. Among them, choose the path with the lowest cost.
Or immediately choose the path with the lowest cost, regardless of the number of steps.
I have added additional data to the example to show these options.
See example
Source data:
Output with least steps and cost
Output with least cost
All path’s, ordered by step,cost
fiddle