Repository navigation
perf(planner): look up grouped node columns from lance tables - #140
Merged
Merged
Conversation
Grouping by a node read from a lazily loaded Lance table carried all its columns through the aggregate, once per expanded row. When a CSR hop fetches those columns and nothing before the grouping reads them, group by the node's id and look the columns up afterwards, as in-memory node tables already do. The hop then fetches only ids. Past 1/8 of the table, the lookup reads its columns in one sequential pass. Also narrow a lookup to the columns the projection right above it uses, which drops lookups whose columns are discarded (IC5, IC14). Closes #99
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) after a hop that fans out carries every column of the node through the grouping, once per input row. #101 fixed this for in-memory node tables: it groups by the node's id and adds the other columns after the aggregate with aNodeLookup. Lance node tables in lazy mode kept the plain grouping, because turning the lookup on read the columns from disk a second time and made IC1 11% slower. This PR enables the lookup for Lance node tables only where it replaces a read instead of adding one, which resolves #99.What changed
WHEREorDISTINCTdoes), the query groups by the id and looks the columns up afterwards. Pruning then removes the columns from the hop, so they are read once per node instead of once per edge. Columns read by a scan, as in IC1, or used before the grouping, as in IC10, keep the plain grouping.OPTIONAL MATCHjoin's left side fetchedtitlefor 77K forums and threw it away; in memory-mode IC14 eight of nine lookups were like this. Both modes benefit.explain()showsNodeLookupExecin more lazy plans. The CSR guide's "Grouping by a node" section and the internals pages describe when Lance tables get the lookup.IC5 in lazy mode, before and after (the
hasMemberhop under the grouping):Variants tried first
The first version also looked up IC10's
friendandcity, whose columns aWHEREand aWITH DISTINCTread before the grouping, so the hops kept fetching them and the lookups read them again. IC10's +4.4% held in 5 alternating processes per build (ranges 29.0 to 29.8 ms onmain, 30.2 to 32.0 ms on the branch). Moving the hop's per-chunk read-once check into a function shared with the lookup cost lazy ldbc query5 2.6% against a control build identical but for that change (+0.5%), so the hop keeps its inline check and only the lookup uses the shared one.Validation
cargo fmt --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace(705 passed),make test(1718 passed),make lint,zensical build --strictandtypospass locally.docs/check_snippets.pypasses for the pages this PR changes;cypher.md:596andsql.md:122fail identically onmain. CI is pending.WHEREorDISTINCTreads its columns first; and a lookup projected away under anOPTIONAL MATCHjoin being dropped. Each compares against the join path.main. Plans changed only in lazy IC1, IC5, IC7 and IC12 and memory IC1, IC3, IC4, IC5 and IC14 (plus a reorderedANDin memory IC3's filter, which varies run to run).Benchmarks
Apple M5 (10 cores), 24 GB, macOS 26.6.2. Release wheels built from
mainat 8fa7a80 and from this branch, loaded withuv run --withperCONTRIBUTING.md;mainand branch alternated twice per mode. Benchmark repositories: graph-benchmark-ldbc 3afadb9, graph-benchmark-ldbc-complex fc7b44c. Every result check passed. The 1-minute load average stayed between 2.1 and 4.0 during the runs with only the desktop app open.mainmainrun gapIn lazy mode: IC5 111.0 to 101.2 ms (-8.8%) and IC7 14.4 to 13.5 ms (-6.3%), both beyond the run gaps and seen in all four rounds of this work. IC1, the query that blocked #101 in lazy mode, gets one small lookup and is unchanged (-0.1%).
graph-benchmark-ldbc in memory mode is 1 to 2% slower on nearly every query, in this run and the three before it. None of its plans changed, the code those plans run is the same as on
main, and the same queries in lazy mode, which plan the same way, show no shift (1.001). So this is most likely build-to-build variance, but I haven't confirmed that with a second build ofmain.Every query, both suites and modes (mean ms)
graph-benchmark-ldbc: memory
main(ms)mainrun gapGeomean branch/
main1.017; medianmainrun gap 2.5%graph-benchmark-ldbc: lazy
main(ms)mainrun gapGeomean branch/
main1.001; medianmainrun gap 0.6%graph-benchmark-ldbc-complex: memory
main(ms)mainrun gapGeomean branch/
main1.000; medianmainrun gap 1.6%graph-benchmark-ldbc-complex: lazy
main(ms)mainrun gapGeomean branch/
main0.988; medianmainrun gap 0.5%🤖 Generated with Claude Code