Skip to content

perf(planner): count two-hop paths over undirected hops - #132

Merged
prrao87 merged 1 commit into
mainfrom
perf/count-undirected-hops
Nov 7, 2026
Merged

prrao87 merged 1 commit into
mainfrom
perf/count-undirected-hops

Conversation

@prrao87

@prrao87 prrao87 commented Nov 7, 2026 •

Copy link
Copy Markdown
Member

Why

Both counting rewrites, the two-hop count and the first-hop count grouped per node, gave up on any undirected hop, so MATCH (a)-[:FOLLOWS]-(b)-[:FOLLOWS]-(c) RETURN count(*) or RETURN b.id, count(a) over (a)-[:FOLLOWS]-(b) built every path or row before counting it. This is half of the "undirected hops and OPTIONAL MATCH" item in #78. Builds on #131 (merged), whose correction it reuses.

What changed

  • An undirected hop is counted like a directed one: its rows at b are b's neighbours either way round (a self-loop once, as the engine matches it), and its mean fan-out is the sum of both directions'.
  • For two hops of the same type where either is undirected, the planner's distinct-edges predicate compares endpoints in both orders. That excludes exactly the pairs whose far ends are the same node, the correction the shared-end case already subtracts.
  • The planner now recognises that predicate by the nodes it makes equal, all three (a directed chain's self-loops) or the two far ends, rather than by one fixed shape. Any other predicate still keeps the join plan.
  • The first-hop count (RETURN b.name, count(a) over (a)-[:R]-(b)...) also accepts an undirected first hop, and on the CSR still counts it in place.
  • The CSR guide and the optimizer internals page cover undirected hops.

Validation

  • cargo test --workspace, cargo clippy --workspace --all-targets -- -D warnings and cargo fmt --all --check pass. make test (1710 passed) and make lint pass, as do zensical build --strict and typos. CI is pending.

  • New tests check the count against the join plan and against hand-counted answers: undirected hops of different types, of the same type, and mixed with a directed hop; an undirected self-loop; edges joining the same two nodes in both directions, which never pair up; a filter on one hop; and an undirected first hop counted per node on the CSR, in place.

  • graph-benchmark (100K Person, 2.4M FOLLOWS, in memory), median ms as in perf(planner): count same-type two-hop paths that share an end #131; Apple M5, 24 GB, macOS 26.6.2; release wheels built from each commit and loaded with uv run --with. Answers are equal across builds and engines. "Before" is the shared-end branch this stacks on.

    Query Answer before, join before, CSR branch join branch CSR
    (a)-[:FOLLOWS]-(b)-[:FOLLOWS]-(c) RETURN count(*) 5,010,294,306 8,511.5 10,614.5 93.0 94.5
    (a)-[:FOLLOWS]-(b)-[:HAS_INTEREST]->(i) RETURN count(*) 12,134,572 18.8 59.9 15.9 13.8
    (a)-[:FOLLOWS]-(b) RETURN b.id, count(a), top 3 14.6 23.6 14.4 12.9
    q8, the chain (control, unchanged) 58,431,994 20.4 13.2 19.6 13.2
  • 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 on main for this branch, except that IC3's inline {name, type} map prints its two filters in the other order, an ordering that changes between builds. 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

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Deploying lance-csr with  Cloudflare Pages  Cloudflare Pages

Latest commit: c86627d
Status: ✅  Deploy successful!
Preview URL: https://b3a25789.lance-csr.pages.dev
Branch Preview URL: https://perf-count-undirected-hops.lance-csr.pages.dev

View logs

@prrao87
prrao87 marked this pull request as ready for review November 7, 2026 02:28
Base automatically changed from perf/count-shared-end-hops to main November 7, 2026 02:30
Undirected hops kept the join plan in both counting rewrites. Count them
from edges too: an undirected hop's rows at the middle node are its
neighbours either way round, and for same-type hops the planner's
distinct-edges predicate, which compares endpoints in both orders, then
excludes exactly the pairs with the same far end, which the shared-end
correction already subtracts. Recognise the predicate by the nodes it
makes equal rather than by one fixed shape.
@prrao87
prrao87 force-pushed the perf/count-undirected-hops branch from b00fc09 to c86627d Compare November 7, 2026 02:31
@prrao87
prrao87 merged commit 58192f8 into main Nov 7, 2026
8 checks passed
@prrao87
prrao87 deleted the perf/count-undirected-hops branch November 7, 2026 02:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant