Skip to content

perf(schema-compiler): cache back-alias members per query#11322

Open
hank-sq wants to merge 3 commits into
cube-js:masterfrom
hank-sq:perf/cache-back-alias-members
Open

perf(schema-compiler): cache back-alias members per query#11322
hank-sq wants to merge 3 commits into
cube-js:masterfrom
hank-sq:perf/cache-back-alias-members

Conversation

@hank-sq

@hank-sq hank-sq commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Check List

  • Tests have been run in packages where changes have been made if available
  • Linter has been run for changed code
  • Tests for the changes have been added if not covered yet
  • Docs have been added / updated if required (not required for this internal planner optimization)

Description of Changes Made

FILTER_PARAMS expansion can call allBackAliasMembersExceptSegments() once per filter expression. Each call currently traverses every member in the query, so schemas with many filter parameters and wide queries repeat the same expensive work hundreds of times during SQL generation.

This change memoizes the completed alias map for each BaseQuery instance through an internal WeakMap used by FILTER_PARAMS and FILTER_GROUP expansion. It deliberately bypasses the cache while the join graph is still being built and during nested alias-gathering traversals, then freezes the successfully computed internal result before reuse. The exported allBackAliasMembersExceptSegments() method retains its existing behavior of returning a fresh, mutable map.

The cache is query-instance scoped: no value is shared through compilerCache, queryCache, users, security contexts, or requests. It becomes collectible with its owning BaseQuery.

Correctness coverage

The new regression suite verifies:

  • completed-query internal memoization without changing the exported helper's fresh, mutable return contract
  • isolation between separate BaseQuery instances, even with shared compilers/query caches
  • pre-join and nested alias-gathering bypasses, including nested traversal after cache population
  • failed computations are not cached
  • byte-identical generated SQL and parameters with and without memoization, using non-empty view aliases
  • 50 repeated FILTER_PARAMS expansions perform 50 lookups but only one expensive alias traversal

Performance reproduction

A generated minimal schema with 100 dimensions and 500 FILTER_PARAMS expansions was tested both directly through PostgresQuery and end-to-end through a source-built local Cube server's /cubejs-api/v1/sql endpoint. E2E requests varied filter values to avoid whole-query cache hits.

  • Schema-compiler microbenchmark median: 243.88 ms → 1.70 ms (143× faster)
  • Local Cube /sql median: 587.45 ms → 65.79 ms (8.9× faster)

A joined-schema benchmark with 10 FILTER_PARAMS per cube shows the optimization scaling beyond the synthetic single-cube case:

  • 2 joined cubes: 25.75 ms → 8.43 ms (3.1× faster)
  • 3 joined cubes: 43.69 ms → 9.63 ms (4.5× faster)

Small single-cube queries showed no meaningful regression: queries with 0–1 FILTER_PARAMS added only 0.3–1.1 µs (under 0.3%). All cached/uncached comparisons produced identical SQL or endpoint responses.

Separately, we have been running a patched build with this change against our production workload and have observed substantial query-planning improvements:

  • p50 (averaged over the observation window): 1.14 s → 52 ms
  • p100 (averaged over the observation window): 3.14 s → 81.9 ms
  • p100 maximum over the observation window: 12 s → 1.89 s

This is an atypical, FILTER_PARAMS-heavy setup with more than 100 filter parameters on each cube, so these measurements are workload-specific rather than representative of every Cube deployment.

LTS backport request

We believe this change should be included in the Cube 1.6 LTS line (lts/v1.6). It addresses severe query-planning latency in FILTER_PARAMS-heavy schemas while preserving generated SQL and parameter behavior, and the production measurements above show a material impact.

Validation

  • yarn tsc
  • yarn lint in packages/cubejs-schema-compiler (passes; existing warnings only)
  • focused Jest suite for the new cache tests
  • related base-query and join-hints-cache-pollution regression suites
  • full schema-compiler unit run: 674 tests passed; the only local failures were two pre-existing macOS ANSI-color snapshot cases in error-reporter.test.ts
  • source-built local Cube server E2E on Node 22 using the REST SQL endpoint

Made with Cursor

Avoid repeating an expensive member traversal for every FILTER_PARAMS expansion while keeping intermediate planning phases uncached.

Signed-off-by: Hank Ditton <hank@squareup.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions github-actions Bot added javascript Pull requests that update Javascript code pr:community Contribution from Cube.js community members. labels Jul 21, 2026
Ensure nested alias gathering still recomputes phase-local results after the completed-query cache has been populated.

Signed-off-by: Hank Ditton <hank@squareup.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep memoization internal so exported callers still receive fresh mutable maps, and verify full SQL and parameter parity with non-empty aliases.

Signed-off-by: Hank Ditton <hank@squareup.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@hank-sq
hank-sq marked this pull request as ready for review July 23, 2026 23:17
@hank-sq
hank-sq requested a review from a team as a code owner July 23, 2026 23:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update Javascript code pr:community Contribution from Cube.js community members.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant