Repository navigation
perf(planner): count two-hop paths whose second hop is optional - #133
Merged
Merged
Conversation
This was referenced Nov 7, 2026
prrao87
marked this pull request as ready for review
November 7, 2026 02:28
prrao87
force-pushed
the
perf/count-undirected-hops
branch
from
November 7, 2026 02:31
b00fc09 to
c86627d
Compare
prrao87
force-pushed
the
perf/count-optional-hops
branch
from
November 7, 2026 02:31
dd25bc8 to
96785c1
Compare
MATCH (a)-[:R]->(b) OPTIONAL MATCH (b)-[:S]->(c) RETURN count(*) kept the join plan. Count it from edges too: group each hop per b, left-join the optional side, and count a b without a match once per first-hop edge (or not at all for count(c), which skips nulls). The optional clause's own WHERE filters its hop's edges, including conditions on b alone.
prrao87
force-pushed
the
perf/count-optional-hops
branch
from
November 7, 2026 02:34
96785c1 to
a312a10
Compare
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
MATCH (a)-[:FOLLOWS]->(b) OPTIONAL MATCH (b)-[:HAS_INTEREST]->(i) RETURN count(*)kept the join plan: the optional hop was expanded from the distinctb, left-joined back onto every first-hop row, and only then counted. The two-hop count rewrite can count it from degrees like an ordinary second hop. This is the other half of the "undirected hops andOPTIONAL MATCH" item in #78. Builds on #132 (merged), as both change the same rewrite.What changed
OPTIONAL MATCHfrom the first hop'sb, both hops are grouped perbas before and the optional side is left-joined. Abwithout a match still has one row per first-hop edge, with nulls: the count isΣ n1(b) × coalesce(n2(b), 1), orcoalesce(n2(b), 0)forcount(i)orcount(r2), which skip nulls.WHEREfilters the second hop's edges, including conditions onbalone, since abit rejects still keeps its null row. The first clause'sWHEREapplies to the first hop as before.b, or a hop that doesn't qualify on its own.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 and against hand-counted answers:
count(*),count(a)andcount(c)(which skips the null rows), the optional clause'sWHEREon its far end and onbalone, the first clause'sWHERE, and an optional hop over the first hop's own type, which may reuse its edge.graph-benchmark (100K
Person, 2.4MFOLLOWS), 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 withuv run --with. Answers are equal across builds and engines. "Before" is the undirected-hops branch this stacks on.MATCH (a)-[:FOLLOWS]->(b) OPTIONAL MATCH (b)-[:HAS_INTEREST]->(i) RETURN count(*)MATCH (a)-[:FOLLOWS]->(b) OPTIONAL MATCH (b)-[:FOLLOWS]->(c) RETURN count(c)Both suites, timed for the whole stack (this branch, with perf(planner): count same-type two-hop paths that share an end #131 and perf(planner): count two-hop paths over undirected hops #132) against
mainwith theCONTRIBUTING.mdharness: GDC SF1, graph-benchmark-ldbc 3afadb9 and graph-benchmark-ldbc-complex fc7b44c, both modes, alternatingmainand the stack twice each. Every test passes. Every query's physical plan is the same as onmain(captured throughexplain()), apart from the order in which IC3 prints the two filters of its inline{name, type}map, which changes between builds.mainmainrunsTwo rows are beyond the run gap:
mainand 19.1 to 20.0 ms here, with the shared-end and undirected builds in the same range.MATCH (c:Comment) WHERE c.content CONTAINS $fragment AND c.length > $min RETURN count(c.id), one scan and filter with no hop, which this change returns from at its first check. It is reproducible over three more alternating runs: 3.86 to 3.93 ms here, against 3.47 to 3.51 ms onmainand on the shared-end and undirected builds. Timed on its own in one process, planning (explain()) takes 0.27 ms on every build, and the 0.38 ms difference is all in executing the same plan, in code this change doesn't touch. I read it as code placement in this build: without LTO, generic Arrow and DataFusion kernels are compiled into this crate, and any change to it can move them. I haven't confirmed that with a control build.Every query, both suites and modes
graph-benchmark-ldbc: memory (ms, mean of two runs)
Geomean stack/main 1.016; median main run gap 1.7%
graph-benchmark-ldbc: lazy (ms, mean of two runs)
Geomean stack/main 0.999; median main run gap 0.7%
graph-benchmark-ldbc-complex: memory (ms, mean of two runs)
Geomean stack/main 1.019; median main run gap 0.6%
graph-benchmark-ldbc-complex: lazy (ms, mean of two runs)
Geomean stack/main 0.996; median main run gap 1.6%
Part of #78.
🤖 Generated with Claude Code