Repository navigation
perf(planner): look up a grouped node's columns after grouping by id - #101
Merged
Merged
Conversation
`WITH b, ...` grouped by every column of `b`, carrying them through the aggregate for each input row. After an expansion that's many rows per node: in IC5, about 800K `hasMember` rows carry a forum's title into 77K groups. When `b`'s label has a node index (unique integer ids) and its table is in memory, the grouping now uses `b`'s id alone, and a new NodeLookup operator adds `b`'s other columns afterwards through the node index, one lookup per group. With unique ids the groups are the same, and a null `b` from an unmatched OPTIONAL MATCH gets null columns. Columns nothing uses are pruned, as for the CSR's target lookup. Lance node tables keep the plain grouping: there each lookup is a read the grouping avoids. On GDC SF1 in memory: IC3 -52%, IC7 -11%, IC5 -7% (78.9 ms, 0.4.0: 77.6 ms), IC1 -6%; lazy mode unchanged. Part of #92 and #99.
Deploying lance-csr with
|
| Latest commit: |
b830f1a
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://93571d58.lance-csr.pages.dev |
| Branch Preview URL: | https://perf-late-node-properties.lance-csr.pages.dev |
prrao87
marked this pull request as ready for review
October 31, 2026 16:02
prrao87
added a commit
that referenced
this pull request
Oct 31, 2026
A patch release: nothing since 0.5.0 breaks the Python or Rust API, so RELEASE.md's rule gives a patch bump. It ships the two-hop count rewrite (#97), `optimize_csr()` (#98), per-chunk in-memory relationship filters (#100) and the node lookup after grouping by id (#101). Also refresh the version in the three lockfiles, which bump-my-version does not update.
This was referenced Oct 31, 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
Grouping by a node (
WITH forum, collect(friend) AS friends) grouped by every column of that node and carried them through the aggregate for every input row. After an expansion that's many rows per node: in IC5, about 800KhasMemberrows carry the forum'stitleinto 77K groups. While working on #92, carrying a string through that grouping cost 7 to 9 ms however it was done (grouping by it,max,head(collect())), which kept IC5 behind 0.4.0. #99 proposed the fix: group by the node's id and fetch its other properties afterwards, once per node.This implements it for in-memory node tables. It addresses #99 and #92's remaining memory-mode gap; #99 stays open for Lance node tables (see below).
What changed
b's label has a node index (unique integer ids) and its table is in memory,WITH b, ...groups byb's id, and a newNodeLookupoperator addsb's other columns after the aggregate. With unique ids the groups are identical to grouping by every column.NodeLookuplooks rows up through the node index, the same id-to-row map the CSR's fused target lookup uses, and gathers the wanted columns per batch. A null or unknown id gets null columns, so the null group from an unmatchedOPTIONAL MATCHkeeps null properties.explain()shows it asNodeLookupExec: key=b__id, take=b[b__name].explain()shows the new operator. The CSR guide gains a "Grouping by a node" section.Two variants tried first
Joining the grouped ids back to the node table scans the whole table for every fetch, which IC10 and IC1 (6 and 10 fetches in their plans) feel. Looking ids up through the node index fixed that; restricting it to in-memory tables removed the lazy-mode IC1 cost.
Validation
cargo fmt --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace,make test(1665 passed),make lint,docs/check_snippets.py,zensical build --strictandtypospass locally. CI is pending.NodeLookupExecwith a node index and not without; an unmatchedOPTIONAL MATCHgroups with null columns; 20K groups in memory and from Lance with deleted rows, each compared against the join path. ExistingWITHtests run withuse_csron and off against the same expected rows.Benchmarks
Apple M5 (10 cores), 24 GB, macOS 26.6.2, other apps closed. Release wheels built from
mainat 20c1a36 and from this branch, loaded withuv run --with;mainand branch alternated twice per mode, each run started below a 1-minute load of 3. Benchmark repositories: graph-benchmark-ldbc 5f528da, graph-benchmark-ldbc-complex f3e63fd. Every result check passed.mainmainrun gapIn memory: IC3 53.4 to 25.4 ms (-52%), IC7 16.2 to 14.4 ms (-11%), IC5 85.1 to 78.9 ms (-7%; 0.4.0: 77.6 ms), IC1 -6%, IC14 -5%. Nothing got slower beyond run-to-run noise in either mode. Lazy plans are unchanged from
main.Every query, both suites and modes (mean ms)
graph-benchmark-ldbc: memory
main(ms)mainrun gapGeomean branch/main 1.000; median main run gap 3.2%
graph-benchmark-ldbc: lazy
main(ms)mainrun gapGeomean branch/main 0.992; median main run gap 0.7%
graph-benchmark-ldbc-complex: memory
main(ms)mainrun gapGeomean branch/main 0.930; median main run gap 1.5%
graph-benchmark-ldbc-complex: lazy
main(ms)mainrun gapGeomean branch/main 0.994; median main run gap 1.1%
🤖 Generated with Claude Code