GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — [SQL] SQL compiler: combinatorial expression-lineage expansion for ROW_NUMBER over UNION ALL causes excessive memory usage and GC stalls · Issue #7870 · feldera/feldera

[SQL] SQL compiler: combinatorial expression-lineage expansion for ROW_NUMBER over UNION ALL causes excessive memory usage and GC stalls #7870

New issue
Copy link
New issue
Copy link
Open
Bug
Open
[SQL] SQL compiler: combinatorial expression-lineage expansion for ROW_NUMBER over UNION ALL causes excessive memory usage and GC stalls#7870
Bug
Copy link
Assignees
mihaibudiu
Labels
CalciteFix required in CalciteFix required in CalciteSQL compilerRelated to the SQL compilerRelated to the SQL compiler

Description

@Mrhs121
Mrhs121
opened on Nov 6, 2026
Issue body actions

Summary

A query combining many UNION ALL branches with ROW_NUMBER() causes excessive memory consumption during SQL compilation.

Using an unmodified upstream compiler, the attached anonymized SQL took approximately 369 seconds and reached 8.38 GiB peak RSS. A local prototype that avoids expanding expression lineage for window expressions compiled the same SQL in approximately 4.7–4.8 seconds, using around 522–531 MiB peak RSS.

All three runs generated byte-identical Rust source.

This happens during SQL compilation, without starting a Pipeline or ingesting any data.

Environment

  • Feldera revision: 0f3c32534cbc1ba00467010566b288cd7b0b6b4e
  • Calcite revision: a2e574ec271866eb352f98d7101a386a30dc8bc8
  • macOS, Apple M1 Max, 32 GiB RAM
  • Zulu Open JDK 21.0.2
  • JVM options: -Xmx8g -XX:ActiveProcessorCount=6
  • Default compiler optimization level
  • Runs executed serially on the same machine

oom-anonymized.sql.txt

Reproduction

Attach oom-anonymized.sql.txt and rename it to oom-anonymized.sql.

The attachment is 97,062 bytes and contains:

  • 25 tables
  • 91 views
  • 89 UNION ALL operators
  • A final ROW_NUMBER() window over the combined results, followed by filtering on the row number

Unused UDF declarations and connector configurations have been removed. Business identifiers and string literals have been anonymized.

Build the compiler at the revision above, then run:

env -u JAVA_TOOL_OPTIONS -u JDK_JAVA_OPTIONS -u _JAVA_OPTIONS \
  java -Xmx8g -XX:ActiveProcessorCount=6 \
  -Xlog:gc:file=gc.log:time,uptime,level,tags \
  -jar sql2dbsp-jar-with-dependencies.jar \
  oom-anonymized.sql \
  -o generated.rs

Input SHA-256:

884c6b0b17379844276ade8b3ec2118cf46650fd9acb73f2b8adfa27eed60f62

Results

Metric Unmodified upstream Local prototype Prototype repeat
Wall-clock time 369.27 s 4.84 s 4.68 s
Maximum process RSS 8.38 GiB 530.64 MiB 521.80 MiB
Full GC count 134 0 0
Total Full GC pause time 320.69 s 0 s 0 s
Exit code 0 0 0

The upstream run spent approximately 87% of its elapsed time in Full GC.

Importantly, this particular upstream run eventually succeeded; it did not terminate with a fatal OOM. An exit code of zero does not necessarily rule out an exception caught inside an optional optimizer step.

The generated Rust files were byte-identical across all three runs:

93ad810eea8e6097fe1bfb42cc4c96cf6c65068b88e281886127481d78a9c748

These measurements cover SQL-to-Rust source generation only, not native Rust compilation or runtime correctness.

Root-cause investigation

The investigation points to expression-lineage expansion, rather than input data volume or runtime state.

The relevant call chain is:

ExpandDisjunctionForTableRule.apply
  -> RelMetadataQuery.getExpressionLineage
  -> RelMdExpressionLineage.getExpressionLineage(Union)
  -> createAllPossibleExpressions
  -> createExpressions
  -> copying RexOver / RexWindow expressions and constructing their digests

The UNION lineage handler gathers possible origins for each referenced column. The expression-expansion code then enumerates combinations of those origins.

For a window expression referencing several columns, this can produce a combinatorial number of copied expressions, rather than work proportional to the number of UNION branches.

Calcite already returns unknown lineage for references to results produced by a relational Window node. However, a window represented as RexOver inside a Project or Calc expression can still enter this expansion path.

Additional heap evidence

In a separate diagnostic run of the original, pre-anonymization query with an earlier local compiler build and a 12 GiB heap, compilation encountered Java heap space.

Heap analysis showed:

  • One HashSet retained approximately 11.78 GiB, or 99.36% of the analyzed heap.
  • Its retained object graph contained approximately 24 million RexOver objects and 24 million RexWindow objects.
  • Associated strings, byte arrays, collections and hash-map entries accounted for much of the remaining memory.

That heap dump is supporting evidence from a separate run, not a measurement from the clean-upstream benchmark above.

Prototype mitigation

I tested a local RelMetadataQuery override that returns unknown lineage before dispatching queries whose expression contains a window:

@Override
public @Nullable Set<RexNode> getExpressionLineage(
        RelNode rel, RexNode expression) {
    if (RexOver.containsOver(expression)) {
        return null;
    }
    return super.getExpressionLineage(rel, expression);
}

The custom query is installed through the cluster's metadata-query supplier, so recursive lineage queries and query invalidation use the same guard.

This does not remove the window operation or globally disable optimizer rules. It withholds optional lineage metadata for window expressions while retaining ordinary column/scalar lineage behavior.

For the attached query, it eliminated the excessive allocation and Full GC while preserving the generated Rust output exactly. This is a result for this reproducer, not a general semantic-equivalence proof.

Would guarding window-expression lineage in Calcite be the preferred fix, or should Feldera apply this restriction at its metadata-query boundary?

Activity

  1. Mrhs121 commented on Nov 6, 2026

    @Mrhs121
    Mrhs121
    on Nov 6, 2026
    ContributorAuthor
    More actions

    @mihaibudiu PTAL

  2. mihaibudiu commented on Nov 6, 2026

    @mihaibudiu
    mihaibudiu
    on Nov 6, 2026
    Contributor
    More actions

    Frankly I think this is really a Calcite bug and it should be fixed in Calcite.

  3. mihaibudiu
    self-assigned this
    on Nov 6, 2026
  4. mihaibudiu
    added
    SQL compilerRelated to the SQL compilerRelated to the SQL compiler
    CalciteFix required in CalciteFix required in Calcite
    on Nov 6, 2026
  5. mihaibudiu
    added the
    Bug
    issue type on Nov 6, 2026
  6. mihaibudiu commented on Nov 6, 2026

    @mihaibudiu
    mihaibudiu
    on Nov 6, 2026
    Contributor
    More actions

    But we could probably take this mitigation in the meantime. I will investigate a bit what the consequences are first.

  7. Mrhs121 commented on Nov 6, 2026

    @Mrhs121
    Mrhs121
    on Nov 6, 2026
    ContributorAuthor
    More actions

    Frankly I think this is really a Calcite bug and it should be fixed in Calcite.

    Thanks for looking into this. I agree that the root fix belongs in Calcite. I also reproduced the same problem in Calcite. The root cause is in the lineage enumeration path. I’ll create a jira bug issue to track it.

    The window expression references 4 fields (3 partition + 1 order):

    UNION branches Window lineage expressions
    2 16
    4 256
    8 4,096
    16 65,536

    Candidate count is branches^4. The exponent 4 comes from the 4 referenced fields: lineage takes the Cartesian product of their candidate sources. It is not four-level UNION nesting or window recursion.

    RC:
    The same SQL with RexOver kept in Project yields 256 candidates for 4 branches. After converting it to a separate Window node, window lineage returns null/unknown, while passthrough columns still return 4 sources. Calcite handles the Window path but lacks equivalent protection on the RexOver path.

    Project/Calc forwards window-expression lineage; Union collects per-field sources; createAllPossibleExpressions() cross-combines and copies them. There is no RexOver guard and no candidate limit; the bloat limit applies only after the lineage query.

  8. mihaibudiu commented on Nov 6, 2026

    @mihaibudiu
    mihaibudiu
    on Nov 6, 2026
    Contributor
    More actions

    My plan is to create a program generator function which can be used as a test in Calcite.
    I won't mind if you do it.
    Several other optimizations in Calcite count steps and break if they exceed a budget; that could be a solution.

  9. Mrhs121 commented on Nov 6, 2026

    @Mrhs121
    Mrhs121
    on Nov 6, 2026
    ContributorAuthor
    More actions

    Thanks @mihaibudiu. I have opened CALCITE-7924 and submitted a proposed fix in apache/calcite#5396.

    The PR includes a parameterized program generator and a per-expansion work budget. It returns unknown lineage before enumerating an over-budget Cartesian product, while preserving existing behavior within the budget for both scalar and window expressions.

    Feedback on the approach would be appreciated!

  10. mihaibudiu commented on Nov 7, 2026

    @mihaibudiu
    mihaibudiu
    on Nov 7, 2026
    Contributor
    More actions

    I have reviewed your PR

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

Metadata

Metadata

Assignees

  • @mihaibudiu
    mihaibudiu

Labels

CalciteFix required in CalciteFix required in CalciteSQL compilerRelated to the SQL compilerRelated to the SQL compiler

Type

Bug

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    You are visitor 01402503 — GeezerVison does not store the pages you look up.
    Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.