Skip to content

Shrink plans for comprehension-heavy queries such as IC10 #18

Closed

Description

@prrao87

Problem

Queries built from pattern comprehensions, OPTIONAL MATCH, collect() and chained WITH produce very large physical plans. Two of them, IC10 and IC1, are where lance-csr trails other engines most; IC14 has the same plan growth, although it is already faster than the other engines.

Evidence

Physical operator counts on SF1, from explain():

Query Operators Notable lance-csr in memory Fastest other engine
IC10 217 38 AggregateExec, 32 CsrExpandExec 105.0 ms 13.7 ms (Ladybug)
IC14 225 49 RepartitionExec 64.4 ms 522.6 ms (Neo4j)
IC1 164 20 AggregateExec 26.8 ms 13.5 ms (Kuzu)

IC10 collects each friend's posts and counts those that share a tag with the person, with size([p IN posts WHERE (p)-[:postHasTag]->()<-[:hasInterest]-(person)]). An earlier change (seeding comprehensions from the original rows) cut IC14 from 641 to 225 operators and from 188 to 62 ms, so there is precedent for large wins here.

Suggested fix

Start with IC10: read its logical plan and find where the comprehension and the OPTIONAL MATCH ... collect() are planned as separate joins and aggregations over the same rows. Candidates are evaluating a pattern predicate inside a list comprehension as a semi-join over the unnested list, and merging aggregations that group by the same keys.

Acceptance criteria

  • IC10's operator count and latency before and after, in the pull request, with results unchanged.
  • No regression on the other IC queries or the 30-query workload.
  • Any new planning rule has focused tests.

Identified by profiling the LDBC IC benchmark; IC10's plan has not been analysed in detail.

Activity

  1. prrao87 commented on Oct 29, 2026

    @prrao87
    MemberAuthor

    Partial progress from #38, which counts size([x IN list WHERE ...]) without building the list. On current main (Apple M5, SF1, in memory), IC10 is down from 217 to 201 operators (AggregateExec 38 → 32, CsrExpandExec 32 → 24) and from 105.0 to 97.8 ms, still about 7x slower than Ladybug's 13.7 ms. IC1 is unchanged at 164 operators. The suggested fixes, a semi-join for the pattern predicate and merging aggregations with the same keys, are still open.

  2. prrao87 commented on Nov 2, 2026

    @prrao87
    MemberAuthor

    Progress from #108, which implements the semi-join suggested here and also stops list comprehensions from copying each row's list onto every element. Measured on SF1, Apple M5, against main at d3c0192, with four harness runs of each build per mode:

    main #108
    IC10 physical operators 209 63
    AggregateExec / CsrExpandExec / HashJoinExec 32 / 24 / 9 9 / 5 / 1
    IC10, memory 97.5 ms 24.2 ms
    IC10, lazy 130.1 ms 34.0 ms

    How it gets there:

    • Pattern predicates such as (p)-[:postHasTag]->()<-[:hasInterest]-(person) and NOT (friend)-[:knows]-(person) are answered per row by a new PathExists operator that walks the CSR, instead of re-planning everything before them as a seed and joining a mark back. Each predicate used to double the plan beneath it.
    • [p IN posts WHERE ...] unnests the list beside its positions, rebuilds a list read from a column once per row instead of grouping on it, and binds node elements through NodeLookup instead of a hash join against the whole node table.

    Still open here:

    • OPTIONAL MATCH still plans its input a second time as the seed of its optional side and joins back. It is now the largest remaining duplication in IC10. An outer mode for single-hop CSR expansion would remove it the same way PathExists removed the predicate copies.
    • IC10 is still about 1.8x Ladybug (24.2 vs 13.7 ms), and roughly half of the remaining time is planning.
    • Rebuilding an iterated list in order uses DataFusion's ordered array_agg, which has no grouped fast path. Dropping the order saved about 9 ms on IC10 in an experiment, but is incorrect in general.
    • IC14 got about 3% slower in memory mode in direct A/B runs (0.8 ms planning, 0.8 ms execution). Its [n IN nodes(path) | n.ID] now takes the new lowering, which adds a few expressions to an already large plan.
    • IC1 is unchanged (141 operators on current main, identical with perf(planner): walk pattern predicates on the csr and keep comprehension rows once #108). Neither change applies to it, and merging aggregations with the same keys is still open.

    🤖 Posted by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions