Skip to content

Build a match candidate from an index defined on an unnested record type - #4641

Draft
g31pranjal wants to merge 21 commits into
apple/g31pranjal/unnested/explode-ordinality-matchingfrom
apple/g31pranjal/unnested/syn-relational-integration
Draft

g31pranjal wants to merge 21 commits into
apple/g31pranjal/unnested/explode-ordinality-matchingfrom
apple/g31pranjal/unnested/syn-relational-integration

Conversation

@g31pranjal

Copy link
Copy Markdown
Member

Makes an index defined on an UnnestedRecordType usable by the Cascades planner, and covers it end to end.

Three preconditions were missing, each one gating the next:

  • MetaDataPlanContext never collected the indexes of a synthetic record type, so such an index was not in the list a candidate could be expanded from.
  • IndexExpansionInfo described the base with the name-taking getPlannerType, which resolves stored record types only.
  • ValueIndexExpansionVisitor always built the base as a scan of the stored types plus a type filter, so SyntheticRecordType.expand was never called.

With those in place, matching and compensating against such a candidate works, and a covering scan can read the __positions tuple, which a ListKeyExpression places one level deeper in the index entry's key.

unnested-record-type-indexes.yamsql asserts the covering plan and the rows for seven queries over one such index: the query the index was defined for, the same unnesting without the ordering, a prefix of the key, the PartiQL spelling of the unnesting, and equalities binding one or both leading key columns.

Why this is marked do-not-merge

The branch carries the relational-side work of #4467 as a merge, because that is what generates the synthetic table from an index definition. #4467 lives on a fork, so this PR cannot be based on it. It is based on #4619 instead, and the diff therefore shows #4467's changes as well. Once #4467 merges, this branch will be rebased and the diff will narrow to its own commits.

The second commit refreshes plan expectations in array-join-at.yamsql and in-predicate.yamsql. Those mismatches are present on this branch without the candidate change, and belong to the branches those files came from; the commit is kept separate so it can be dropped on that rebase.

@g31pranjal g31pranjal added enhancement New feature or request DO NOT MERGE do not merge labels Sep 18, 2026
An index on an `UnnestedRecordType` could not be matched, because nothing led
the planner to it. Three things were missing, and each is a precondition of the
next.

`MetaDataPlanContext` collected the indexes of the queried stored types and the
universal ones, so an index defined on a synthetic type was never in the list a
candidate could be expanded from. It now also collects the indexes of every
synthetic type all of whose stored constituents the query reads.

`IndexExpansionInfo` described the base with the name-taking
`getPlannerType`, which resolves against stored record types only, so a
synthetic type could not be described at all. It now uses
`getPlannerTypeForRecordTypes`, which takes the already-resolved types
`recordTypesForIndex` hands back.

`ValueIndexExpansionVisitor` always built the base as a scan of the stored
types plus a type filter. For an index on a synthetic type the base is instead
that type's own expansion -- the graph that assembles its records -- so
`SyntheticRecordType.expand` is finally what the candidate ranges over.

Matching and compensating against such a candidate then needs the rest of the
change: the select the expansion builds carries the constituents' quantifiers and
the index's placeholders together, which is what lets a predicate over the
unnesting bind the scan.

A covering scan over such an index also has to read the `__positions` tuple,
which a `ListKeyExpression` places one level deeper in the entry's key rather
than flattening. `computeIndexEntryToLogicalRecord` therefore takes the
positions of the full key and derives the path to each datum from them.

The planner metrics of two yamsql files move with the new rule applications.
Seven queries over one index defined on an unnested synthetic type: the query the
index was defined for, the same unnesting without the ordering, a prefix of the
key, the PartiQL spelling of the unnesting, and equalities that bind one or both
of the key's leading columns. Each asserts the covering plan as well as the rows,
since answering these from a fan-out is what the index is meant to avoid.
The plans these two files assert move once the relational unnesting work is merged
in: a join over two unnestings picks the other order, and an IN over an ordinal
stays a filter instead of becoming an explode joined to the scan.

These expectations belong to the branches the files came from, so the change is
kept in its own commit, to be dropped when this branch is rebased onto them.
@g31pranjal
g31pranjal force-pushed the apple/g31pranjal/unnested/explode-ordinality-matching branch from c781f27 to f3be10b Compare September 18, 2026 15:12
@g31pranjal
g31pranjal force-pushed the apple/g31pranjal/unnested/syn-relational-integration branch from 9a0ac5f to 6db6c25 Compare September 18, 2026 15:12
@github-actions

Copy link
Copy Markdown

📊 Metrics Diff Analysis Report

Summary

  • New queries: 7
  • Dropped queries: 0
  • Plan changed + metrics changed: 7
  • Plan unchanged + metrics changed: 2
ℹ️ About this analysis

This automated analysis compares query planner metrics between the base branch and this PR. It categorizes changes into:

  • New queries: Queries added in this PR
  • Dropped queries: Queries removed in this PR. These should be reviewed to ensure we are not losing coverage.
  • Plan changed + metrics changed: The query plan has changed along with planner metrics.
  • Metrics only changed: Same plan but different metrics

The last category in particular may indicate planner regressions that should be investigated.

New Queries

Count of new queries by file:

  • yaml-tests/src/test/resources/unnested-record-type-indexes.metrics.yaml: 7

Plan and Metrics Changed

These queries experienced both plan and metrics changes. This generally indicates that there was some planner change
that means the planning for this query may be substantially different. Some amount of query plan metrics change is expected,
but the reviewer should still validate that these changes are not excessive.

Total: 7 queries

Statistical Summary (Plan and Metrics Changed)

task_count:

  • Average change: -7.4
  • Median change: -11
  • Standard deviation: 4.2
  • Range: -11 to -2
  • Queries changed: 7
  • No regressions! 🎉

task_count % change distribution (7 queries, bin = 2%):

     Range                                  n
----------  ------------------------------  ---
[-2%, +0%)  ██████████████████████████████  7

transform_count:

  • Average change: -1.0
  • Median change: -1
  • Standard deviation: 0.0
  • Range: -1 to -1
  • Queries changed: 4
  • No regressions! 🎉

transform_count % change distribution (4 queries, bin = 2%):

     Range                                  n
----------  ------------------------------  ---
[-2%, +0%)  ██████████████████████████████  4

transform_yield_count:

  • Average change: -1.0
  • Median change: -1
  • Standard deviation: 0.0
  • Range: -1 to -1
  • Queries changed: 4
  • No regressions! 🎉

transform_yield_count % change distribution (4 queries, bin = 2%):

     Range                                  n
----------  ------------------------------  ---
[-2%, +0%)  ██████████████████████████████  4

insert_new_count:

  • Average change: -1.0
  • Median change: -1
  • Standard deviation: 0.0
  • Range: -1 to -1
  • Queries changed: 4
  • No regressions! 🎉

insert_new_count % change distribution (4 queries, bin = 2%):

     Range                                  n
----------  ------------------------------  ---
[-2%, +0%)  ██████████████████████████████  4

There were no queries with significant regressions detected.

Minor Changes (Plan and Metrics Changed)

In addition, there were 7 queries with minor changes.

Only Metrics Changed

These queries experienced only metrics changes without any plan changes. If these metrics have substantially changed,
then a planner change has been made which affects planner performance but does not correlate with any new outcomes,
which could indicate a regression.

Total: 2 queries

Statistical Summary (Only Metrics Changed)

task_count:

  • Average change: -175.0
  • Median change: -175
  • Standard deviation: 0.0
  • Range: -175 to -175
  • Queries changed: 1
  • No regressions! 🎉

transform_count:

  • Average change: -57.0
  • Median change: -57
  • Standard deviation: 0.0
  • Range: -57 to -57
  • Queries changed: 1
  • No regressions! 🎉

transform_yield_count:

  • Average change: -9.0
  • Median change: -9
  • Standard deviation: 0.0
  • Range: -9 to -9
  • Queries changed: 1
  • No regressions! 🎉

insert_new_count:

  • Average change: -10.0
  • Average regression: +1.0
  • Median change: +1
  • Median regression: +1
  • Standard deviation: 11.0
  • Standard deviation of regressions: 0.0
  • Range: -21 to +1
  • Range of regressions: +1 to +1
  • Queries changed: 2
  • Queries regressed: 1

Significant Regressions (Only Metrics Changed)

There were 2 outliers detected. Outlier queries have a significant regression in at least one field. Statistically, this represents either an increase of more than two standard deviations above the mean or a large absolute increase (e.g., 100).

  • yaml-tests/src/test/resources/subquery-tests.metrics.yaml:88: EXPLAIN select sq.idr, sq.z from (select * from r where idr = 1) sq, (select f from sq.nr where f > 10) sq2 where sq.z = 10 AND sq2.f is not null
    • explain: ISCAN(IR [EQUALS promote(@c17 AS INT), EQUALS promote(@c31 AS INT)]) | FLATMAP q0 -> { EXPLODE q0.NR | FILTER _.F GREATER_THAN promote(@c31 AS INT) AND _.F NOT_NULL AS q1 RETURN (q0.IDR AS IDR, q0.Z AS Z) }
    • insert_new_count: 82 -> 83 (+1)
  • yaml-tests/src/test/resources/valid-identifiers.metrics.yaml:29: EXPLAIN select t.id from "foo.table$nested.repeated" as t where exists (select * from t."level0.field1" as b, b."level1$field.1" where "level2$array.field.1" = 10)
    • explain: COVERING(foo.table$nested.repeated.idx.field.1.1.1 [EQUALS promote(@c26 AS LONG)] -> [ID: KEY:[2]]) | MAP (_.ID AS ID)
    • task_count: 899 -> 724 (-175)
    • transform_count: 320 -> 263 (-57)
    • transform_yield_count: 69 -> 60 (-9)
    • insert_new_count: 94 -> 73 (-21)

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

Labels

DO NOT MERGE do not merge enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant