Repository navigation
perf(planner): count same-type two-hop paths that share an end - #131
Merged
Merged
Conversation
Two hops of one type that share an end, such as (a)-[:R]->(b)<-[:R]-(c), kept the join plan in the two-hop count rewrite because the paths whose far ends are the same node would use one edge twice. Group both hops by (b, far end) and then by b, and subtract the pairs with the same far end, so the count still follows the edges rather than the paths.
This was referenced Nov 7, 2026
Deploying lance-csr with
|
| Latest commit: |
fa2389f
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://1d3ef55b.lance-csr.pages.dev |
| Branch Preview URL: | https://perf-count-shared-end-hops.lance-csr.pages.dev |
prrao87
marked this pull request as ready for review
November 7, 2026 02:29
This was referenced Nov 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The two-hop count rewrite (#97, #113) counts
MATCH (a)-[:R]->(b)-[:S]->(c) RETURN count(*)from per-node degrees instead of building every path. Two hops of the same type that share an end, such as(a)-[:FOLLOWS]->(b)<-[:FOLLOWS]-(c)("pairs of people who follow the same person"), kept the join plan, which buildsΣ in(b)²rows only to count them. The reason was the correction: Cypher's rule that a path uses each edge once excludes every pair whose far ends are the same node, and finding those pairs needs both ends of each edge. This is one of the open items in #78.What changed
(b, far end)and then byb, which gives both sides' countsn1(b),n2(b)and the excluded pairsΣ_x k1(b, x) × k2(b, x)in one pass. The count isΣ_b n1(b) × n2(b)less those pairs.kparallel edges betweenbandxexcludek²pairs.degrees=), because the correction needs the far end. They read the CSR expansion's rows into a hash aggregate instead.WHEREcondition reads one hop, and the mean fan-outs say there are at least twice as many paths as edges.Validation
cargo test --workspace,cargo clippy --workspace --all-targets -- -D warningsandcargo fmt --all --checkpass.make test(1710 passed) andmake lintpass, as dozensical build --strictandtypos. CI is pending.New tests check the count against the join plan (forced with
count(*) + 0, which the rewrite doesn't match), with and without the CSR: hops into and out of the shared node, the shared node as the root of a comma pattern, parallel edges (k²pairs excluded), a self-loop at the shared node, and filters on either hop, on the shared node and on a relationship.graph-benchmark (100K
Person, 2.4MFOLLOWS, in memory), median ms of 11 runs after 3 warm-ups, two alternating processes per build; Apple M5, 24 GB, macOS 26.6.2; release wheels built from each commit and loaded withuv run --with. Answers are equal across builds and engines.mainjoinmainCSR(a)-[:FOLLOWS]->(b)<-[:FOLLOWS]-(c) RETURN count(*)(a)<-[:FOLLOWS]-(b)-[:FOLLOWS]->(c) RETURN count(*)WHERE b.age < 50 AND a.age > 25With the CSR these hops are a little slower than without it: they can't count in place, so the CSR expansion's rows go through the same hash aggregate as the join path's scan.
Both suites (GDC SF1, graph-benchmark-ldbc 3afadb9 and graph-benchmark-ldbc-complex fc7b44c, both modes): the physical plan of every query, captured through
explain()for each wheel, is the same as onmainfor this branch. None of their queries has this shape, so their timings can't change through it; the suites were timed once for the top of this stack, in perf(planner): count two-hop paths whose second hop is optional #133, every test passing.Part of #78.
🤖 Generated with Claude Code