Skip to content

Flow the WITH ORDINALITY struct as a record constructor value - #4696

Merged
g31pranjal merged 3 commits into
mainfrom
apple/g31pranjal/unnested/explode-ordinality-struct-value
Oct 2, 2026
Merged

g31pranjal merged 3 commits into
mainfrom
apple/g31pranjal/unnested/explode-ordinality-struct-value

Conversation

@g31pranjal

@g31pranjal g31pranjal commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Supersedes #4651, which GitHub marked as merged when this branch was messed up in the stack reordering

#4650, now merged, added the flowsRecordConstructorValue flag but left every explode flowing the opaque value. This PR turns it on only for EXPLODE WITH ORDINALS (LogicalOperator.generateCorrelatedFieldAccess). Wrapping the element of a plain explode in a struct of its own is #4623.

What changes in plans

Nothing in fdb-record-layer-core changes, and no core test needed updating. What moves is the expected plan of the five AT queries that also carry an IN: an IN over a value read out of a record is costed differently than one over an opaque value. The movement goes in both directions and is worth a look:

  • array-join-at:317, WHERE "at" IN (1, 2): was an explode of the IN list joined with a scan, and is now a single scan with a residual FILTER _._1 + 1 IN @c19. Cheaper.
  • array-join-at:305 and in-predicate:402, both IN on the primary key: same operators, but the unnesting moves across the in-join boundary — into the in-join in the first, out of it in the second.
  • in-predicate:382 and in-predicate:410, both IN on the unnested value: the IN list explode becomes the outer loop with the table scan inside it, i.e. one full scan per IN element. This is Cost model scores a nested data access the same as an unnested one, so an IN-join can be chosen over a single scan #4621 — the cost model scores a data access nested under an in-join the same as an unnested one.

#4650 gave an explode the flag that decides whether it flows the element and the
ordinal as a `RecordConstructorValue` or as one opaque value of the struct type,
but left every explode flowing the opaque value. Ask for the record constructor
where the SQL layer builds an `AT` unnesting, which is the only place that
produces an explode with ordinality. The record-layer constructors keep
defaulting to the opaque value, so nothing else changes shape.

Both shapes stand for the same data and evaluate identically -- the plan builds
its struct from the protobuf descriptor its declared type names -- but the
element and the ordinal are now reachable sub-values, which is what lets
anything be related to them.

The expected plans of the five `AT` queries that also carry an `IN` move, since
an `IN` over a value read out of a record is costed differently than one over an
opaque value. They move in both directions: `array-join-at`'s `WHERE "at" IN
(1, 2)` drops its join over the `IN` list for a single scan with a residual
filter, two queries with an `IN` on the primary key keep their operators but move
the unnesting across the in-join boundary, and `in-predicate`'s two queries with
an `IN` on the unnested value start scanning the table once per `IN` element,
which is #4621. No index match is lost, and all results still verify.

A plan serialized before the flag existed still deserializes to the opaque
value, so it keeps the shape, and the hash, it was planned with. A release that
predates #4650 drops the flag instead, so this cannot be released before #4650
is.
@g31pranjal g31pranjal added enhancement New feature or request DO NOT MERGE do not merge breaking change Changes that are not backwards compatible labels Oct 1, 2026
@g31pranjal
g31pranjal marked this pull request as ready for review October 1, 2026 15:04
@g31pranjal g31pranjal removed the DO NOT MERGE do not merge label Oct 2, 2026
@alecgrieser alecgrieser added the Run mixed-mode Label to add to Pull Requests to have it run mixed mode tests label Oct 2, 2026

@alecgrieser alecgrieser left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I did point out some plan differences, including ones where the plan got worse. But there's another example of essentially the same transformation, but in reverse, which makes me think this is random noise from plan hash differences. Which, is maybe something to be aware of, but also hard to argue should be a blocking change, though potentially someone with more planner expertise should weigh in.

You did mark this as a breaking change Changes that are not backwards compatible , which is fair enough. I've kicked off Run mixed-mode Label to add to Pull Requests to have it run mixed mode tests , and it would be good to quantify what the impact is there. We should update the minor version, though whether that happens in this PR or in a follow up to this and #4704 is tomato/tomato

- query: SELECT t.id, n, idx FROM array_table AS t, t.numbers AS n AT idx WHERE n IN (10, 20)
- supported_version: 4.12.5.0
- explain: "SCAN([IS ARRAY_TABLE]) | FLATMAP q0 -> { EXPLODE arrayDistinct(promote(@c23 AS ARRAY(LONG))) | FLATMAP q1 -> { EXPLODE q0.NUMBERS WITH ORDINALITY | FILTER _._0 EQUALS q1 AS q2 RETURN q2 } AS q2 RETURN (q0.ID AS ID, q2._0 AS N, q2._1 + 1 AS IDX) }"
- explain: "EXPLODE arrayDistinct(promote(@c23 AS ARRAY(LONG))) | FLATMAP q0 -> { SCAN([IS ARRAY_TABLE]) | FLATMAP q1 -> { EXPLODE q1.NUMBERS WITH ORDINALITY | FILTER _._0 EQUALS q0 AS q2 RETURN (q1.ID AS ID, q2._0 AS N, q2._1 + 1 AS IDX) } AS q3 RETURN q3 }"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This plan is worse, because we're now executing the SCAN([IS ARRAY_TABLE]) operator once per element in @c23 rather than once for the whole plan. I'm a little surprised that this change would cause that though unless this was relying on the plan hash tiebreaker or something.

- query: SELECT t.id, f, idx FROM array_table AS t, t.fruits AS f AT idx WHERE f IN ('apple', 'mango')
- supported_version: 4.12.5.0
- explain: "SCAN([IS ARRAY_TABLE]) | FLATMAP q0 -> { EXPLODE arrayDistinct(@c23) | FLATMAP q1 -> { EXPLODE q0.FRUITS WITH ORDINALITY | FILTER _._0 EQUALS q1 AS q2 RETURN q2 } AS q2 RETURN (q0.ID AS ID, q2._0 AS F, q2._1 + 1 AS IDX) }"
- explain: "EXPLODE arrayDistinct(@c23) | FLATMAP q0 -> { SCAN([IS ARRAY_TABLE]) | FLATMAP q1 -> { EXPLODE q1.FRUITS WITH ORDINALITY | FILTER _._0 EQUALS q0 AS q2 RETURN (q1.ID AS ID, q2._0 AS F, q2._1 + 1 AS IDX) } AS q3 RETURN q3 }"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This plan is also worse, for effectively the same reason.

FROM T1, T1."arr1_nn" AS "val" AT "at"
WHERE "at" IN (1, 2)
- explain: "EXPLODE arrayDistinct(@c19) | FLATMAP q0 -> { SCAN([IS T1]) | FLATMAP q1 -> { EXPLODE q1.arr1_nn WITH ORDINALITY | FILTER _._1 + 1 EQUALS q0 AS q2 RETURN (q1.id AS id, q2._0 AS val, q2._1 + 1 AS at) } AS q3 RETURN q3 }"
- explain: "SCAN([IS T1]) | FLATMAP q0 -> { EXPLODE q0.arr1_nn WITH ORDINALITY | FILTER _._1 + 1 IN @c19 AS q1 RETURN (q0.id AS id, q1._0 AS val, q1._1 + 1 AS at) }"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the sake of fairness, this plan is better, effectively because it makes the opposite transformation as: https://github.com/FoundationDB/fdb-record-layer/pull/4696/changes#r4166198242 Which is more evidence that this is random plan hash noise, I guess

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

📊 Metrics Diff Analysis Report

Summary

  • New queries: 0
  • Dropped queries: 0
  • Plan changed + metrics changed: 2
  • Plan unchanged + metrics changed: 0
ℹ️ 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.

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: 2 queries

Statistical Summary (Plan and Metrics Changed)

task_count:

  • Average change: -11.0
  • Median change: -11
  • Standard deviation: 0.0
  • Range: -11 to -11
  • Queries changed: 2
  • No regressions! 🎉

transform_count:

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

transform_yield_count:

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

insert_new_count:

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

Significant Regressions (Plan and 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/array-join-at.metrics.yaml:196: EXPLAIN SELECT "id", "val", "at" FROM T1, T1."arr1_nn" AS "val" AT "at" WHERE "at" IN (1, 2)
    • old explain: EXPLODE arrayDistinct(@c19) | FLATMAP q0 -> { SCAN([IS T1]) | FLATMAP q1 -> { EXPLODE q1.arr1_nn WITH ORDINALITY | FILTER _._1 + 1 EQUALS q0 AS q2 RETURN (q1.id AS id, q2._0 AS val, q2._1 + 1 AS at) } AS q3 RETURN q3 }
    • new explain: SCAN([IS T1]) | FLATMAP q0 -> { EXPLODE q0.arr1_nn WITH ORDINALITY | FILTER _._1 + 1 IN @c19 AS q1 RETURN (q0.id AS id, q1._0 AS val, q1._1 + 1 AS at) }
    • task_count: 1056 -> 1045 (-11)
    • transform_count: 421 -> 420 (-1)
    • transform_yield_count: 52 -> 51 (-1)
    • insert_new_count: 115 -> 114 (-1)
  • yaml-tests/src/test/resources/in-predicate.metrics.yaml:503: EXPLAIN SELECT t.id, n, idx FROM array_table AS t, t.numbers AS n AT idx WHERE idx IN (1, 3)
    • old explain: EXPLODE arrayDistinct(@c23) | FLATMAP q0 -> { SCAN([IS ARRAY_TABLE]) | FLATMAP q1 -> { EXPLODE q1.NUMBERS WITH ZERO BASED ORDINALITY | FILTER _._1 + 1 EQUALS q0 AS q2 RETURN (q1.ID AS ID, q2._0 AS N, q2._1 + 1 AS IDX) } AS q3 RETURN q3 }
    • new explain: EXPLODE arrayDistinct(@c23) | FLATMAP q0 -> { SCAN([IS ARRAY_TABLE]) | FLATMAP q1 -> { EXPLODE q1.NUMBERS WITH ORDINALITY | FILTER _._1 + 1 EQUALS q0 AS q2 RETURN (q1.ID AS ID, q2._0 AS N, q2._1 + 1 AS IDX) } AS q3 RETURN q3 }
    • task_count: 1060 -> 1049 (-11)
    • transform_count: 423 -> 422 (-1)
    • transform_yield_count: 54 -> 53 (-1)
    • insert_new_count: 115 -> 114 (-1)

@g31pranjal
g31pranjal merged commit 75cb456 into main Oct 2, 2026
14 of 22 checks passed
@g31pranjal
g31pranjal deleted the apple/g31pranjal/unnested/explode-ordinality-struct-value branch October 2, 2026 14:41
alecgrieser added a commit that referenced this pull request Oct 5, 2026
This updates the project's minor version to 4.16. This is to accommodate
the following breaking changes:

* #4696 and #4704 - Breaks plan continuation compatibility for queryies
using `WITH ORDINALITY` with versions prior to 4.15.2.0
* #4662 and #4663 - Removes two shaded artifacts from being published
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Changes that are not backwards compatible enhancement New feature or request Run mixed-mode Label to add to Pull Requests to have it run mixed mode tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants