pgr_drivingDistance: get_depth() is O(reached × V) and dominates runtime on large graphs
Maintainers usually reply within 3 days
@jflangenbach-casageo is already working on this.
Since Oct 28, 2026.
- #3177 by @jflangenbach-casageo — open
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 62/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- cpp, postgresql
- Domain
- backend, performance
Research direction
Start in detail::get_depth() in include/dijkstra/drivingDist.hpp, which scans every vertex for each reached vertex; the proposed replacement memoizes depth in one O(V) pass and keeps the remove_details() handling. Reproduce with the chain-graph queries in the issue and compare the returned rows against the original build. Done when the depth output matches the old version on the grid tests and the pgTAP suite passes.
Written by the indexing model from the issue text.
Description
Title: pgr_drivingDistance: get_depth() is O(reached × V) and dominates runtime on large graphs
Problem
Since 3.6.0, pgr_drivingDistance (and pgr_withPointsDD) compute the depth column in detail::get_depth() (include/dijkstra/drivingDist.hpp). For every vertex found on the current depth level, the function scans all vertices of the graph to find its children:
for (const auto v : vertices) {
for (V p = 0; p < graph.num_vertices() ; ++p) {
...
if (!(predecessors[p] == v)) continue;
This makes the cost O(reached vertices × all vertices in the graph). The Dijkstra search itself stops at distance and is cheap. But get_depth() also iterates over every vertex that was loaded and never reached. Typical isochrone queries load a bounding area that is much larger than the reachable part, so get_depth() takes almost all of the runtime.
The loop also has no CHECK_FOR_INTERRUPTS(), so the query cannot be cancelled while it runs.
The code is unchanged from 3.6.0 through 4.0.2 and is the same on main and develop.
Versions
- pgRouting 4.0.1 (
pgr_full_version():4.0.1, Release, ..., Boost 1.83.0) - PostgreSQL 18, Ubuntu 24.04, PGDG packages
Minimal reproduction
A chain graph. The search always reaches the same 20,001 vertices. Only the number of loaded, unreachable vertices changes:
CREATE TEMP TABLE edges AS
SELECT g AS id, g AS source, g + 1 AS target, 1.0::float8 AS cost, -1.0::float8 AS reverse_cost
FROM generate_series(1, 2000000) g;
SELECT count(*) FROM pgr_drivingDistance('SELECT * FROM edges WHERE id <= 100000', 1, 20000);
SELECT count(*) FROM pgr_drivingDistance('SELECT * FROM edges WHERE id <= 1000000', 1, 20000);
SELECT count(*) FROM pgr_drivingDistance('SELECT * FROM edges', 1, 20000);
-- graph building only (start vertex does not exist)
SELECT count(*) FROM pgr_drivingDistance('SELECT * FROM edges', -1, 20000);
| Vertices in graph | Result rows | Time |
|---|---|---|
| 100,000 | 20,001 | 2.1 s |
| 1,000,000 | 20,001 | 19.2 s |
| 2,000,000 | 20,001 | 40.5 s |
| 2,000,000, building only | – | 1.7 s |
The result stays the same, but the runtime grows linearly with the number of unreachable vertices.
Real-world impact
These numbers come from a road network (car, cost in seconds, one start vertex):
| Isochrone | Vertices loaded | Vertices reached | Time total | Time loading + graph building |
|---|---|---|---|---|
| 10 min | 607,779 | 15,783 | 10.4 s | 0.7 s |
| 30 min | 1,685,400 | 247,916 | 9 min 26 s | not measured |
More than 90 % of the time is spent in get_depth(). The depth column is not used in these queries, but it cannot be switched off.
With the fix proposed below, the same 30-minute isochrone (Hamburg main station as start point) runs much faster. The times below are for our complete application functions, end to end. Each function selects the edges, calls pgr_drivingDistance once and builds the output geometry:
| 30 min isochrone, complete function | pgRouting 4.0.1, unchanged | with the fix below |
|---|---|---|
| Isochrone polygon | 9 min 6 s | 13 s |
| Catchment area as grid cells | 9 min 4 s | 11 s |
So in practice almost the whole runtime of these queries was spent in get_depth().
Proposed fix
Compute the depth in a single pass over the predecessors, depth(v) = depth(pred(v)) + 1, and memoize the results. This is O(V) and gives the same result, including the remove_details() handling for details = false:
template <typename G, typename V>
std::map<int64_t, int64_t> get_depth(
const G &graph,
V root,
const std::vector<double>& distances,
std::vector<V>& predecessors,
double distance,
bool details) {
std::map<int64_t, int64_t> depth;
if (predecessors.empty()) return depth;
if (predecessors.size() != distances.size()) return depth;
if (!details) {
remove_details(graph, distances, predecessors);
}
const int64_t UNKNOWN = -2;
const int64_t NOT_IN_TREE = -1;
std::vector<int64_t> d(predecessors.size(), UNKNOWN);
d[root] = 0;
std::vector<V> chain;
for (V v = 0; v < predecessors.size(); ++v) {
if (d[v] != UNKNOWN) continue;
if (v % 100000 == 0) CHECK_FOR_INTERRUPTS();
/* walk up the tree until a vertex with known depth is found */
chain.clear();
V u = v;
while (d[u] == UNKNOWN) {
if (predecessors[u] == u || !(distances[u] <= distance)) {
d[u] = NOT_IN_TREE;
break;
}
chain.push_back(u);
u = predecessors[u];
}
/* assign depths back down the chain */
int64_t current = d[u];
for (auto it = chain.rbegin(); it != chain.rend(); ++it) {
current = (current == NOT_IN_TREE) ? NOT_IN_TREE : current + 1;
d[*it] = current;
}
}
for (V v = 0; v < d.size(); ++v) {
if (d[v] >= 0) depth[graph[v].id] = d[v];
}
return depth;
}
The function above is a drop-in replacement for detail::get_depth() in include/dijkstra/drivingDist.hpp (v4.0.1). No other file or signature changes.
How we tested it
- Standalone harness: We compared the original and the new function on 300 random shortest-path trees, with and without
details, with points (negative ids) and with a distance limit. The returned maps and the modified predecessor vectors were identical in all cases. For a tree with 600,000 vertices and 15,783 reached, the original took 4.9 s and the new version 0.006 s. - Full build: We built pgRouting 4.0.1 with the change (Release, Ubuntu 24.04, PostgreSQL 18, Boost 1.83) and compared the complete result rows of the original and the patched library on a 300 × 300 grid with random costs and one-way edges:
| Query | Rows | Differences |
|---|---|---|
pgr_drivingDistance, one start vertex |
2,441 | 0 |
pgr_drivingDistance, several start vertices |
3,608 | 0 |
pgr_drivingDistance, several start vertices, equicost |
3,608 | 0 |
pgr_drivingDistance, undirected |
2,972 | 0 |
pgr_withPointsDD, details => true |
2,803 | 0 |
pgr_withPointsDD, details => false |
2,800 | 0 |
pgr_withPointsDD, details => false, equicost |
2,790 | 0 |
In the same build, the chain reproduction above (2,000,000 vertices) dropped from 40.9 s to 2.2 s.
We have not run the pgTAP suite with this change.
Everything needed is contained in this issue. Please feel free to use the code above under the project's license.
- Dominant language
- C++
- Stars
- 1.4k
- Forks
- 397
- Avg merge
- 2d 16h
- Merged PRs (30d)
- 8
Getting set up
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from pgRouting/pgrouting
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 3 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 3 days
-
Administrative Enhancement
Difficulty 1/5 Under an hour Newbie friendliness 80/100
Maintainers usually reply within 3 days
-
points_input Developers docPossibly taken @withaman69 claimed this 101 days ago. OpenC/C++ Developers Doc. good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
pgRouting/pgrouting#973 · 10 comments ·
Maintainers usually reply within 3 days
-
get_check_data developers docPossibly taken @withaman69 claimed this 102 days ago. OpenC/C++ Developers Doc. good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
pgRouting/pgrouting#972 · 7 comments ·
Maintainers usually reply within 3 days
All issues in pgRouting/pgrouting
Similar issues
-
std::fdim drops partial derivative w.r.t second argument in forward and reverse modePossibly taken @ADITYASINGH1206 claimed this today. Open
Difficulty 1/5 Under an hour Newbie friendliness 85/100
vgvassilev/clad#2534 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Maintainers usually reply within 1 day
-
HAP.cpp fixOpen
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
KLayout/klayout#2554 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day