If you need a line per flight, the only solution that comes to mind is two recursive queries. The first builds flight routes with numbers 1, 1.1, 1.2, 1.1.1, etc .; seconds collect flights belonging to the same route. Pretty hard:
with cte1 (routepart, pos, src_apid, dst_apid) as ( select to_char(rownum) as routepart, 1 as pos, src_apid, dst_apid from routes where src_apid = 18 union all select cte1.routepart || '-' || rownum, pos + 1, r.src_apid, r.dst_apid from cte1 join routes r on r.src_apid = cte1.dst_apid where cte1.dst_apid <> 1403 ) cycle src_apid set cycle to 1 default 0 , cte2 (route, routepart, pos, src_apid, dst_apid) as ( select routepart as route, routepart, pos, src_apid, dst_apid from cte1 where dst_apid = 1403 union all select cte2.route, cte1.routepart, cte1.pos, cte1.src_apid, cte1.dst_apid from cte1 join cte2 on cte2.routepart like cte1.routepart || '%' and nvl(length(regexp_replace(cte2.routepart, '[[:digit:]]', '')), 0) = nvl(length(regexp_replace(cte1.routepart, '[[:digit:]]', '')), 0) + 1 ) cycle src_apid set cycle to 1 default 0 select pos, src_apid, dst_apid from ( select cte2.*, rank() over (order by length(regexp_replace(route, '[[:digit:]]', ''))) as rn from cte2 ) where rn = 1 order by route, pos;
Use ROW_NUMBER instead of RANK if you don't need links.