Fabian Hueske created FLINK-41410:
-------------------------------------

             Summary: Push nested field accesses below joins to reduce join 
shuffle and state size
                 Key: FLINK-41410
                 URL: https://issues.apache.org/jira/browse/FLINK-41410
             Project: Flink
          Issue Type: Improvement
          Components: Table SQL / Legacy Planner
            Reporter: Fabian Hueske
            Assignee: Fabian Hueske


h2. Problem

When a query accesses only a few leaf fields of a ROW column on either side of 
a join, the whole ROW is carried into the join. The nested field access is 
evaluated in a Calc/Project *above* the join, so the full struct is shuffled 
and stored in the join state, even though only a leaf (or a few) is needed. The 
same is true for nested field accesses in the join condition.

Existing rules push projections through joins only at the granularity of 
top-level columns (e.g. {{{}FlinkProjectJoinTransposeRule{}}}), and Calcite's 
{{PushProjector}} records a field access like {{a.b}} as a reference to the 
whole column {{{}a{}}}. For wide structs this can dominate the row size, which 
directly increases the network volume of the join's Exchange and the size of 
its state.
h2. Reproduction
{code:sql}
CREATE TABLE L (id INT, nested ROW<name STRING, `value` INT, other STRING>) 
WITH (...);
CREATE TABLE R (id INT, info   ROW<city STRING, zip INT, other STRING>)     
WITH (...);

SELECT L.nested.name, R.info.city FROM L JOIN R ON L.id = R.id;
{code}
Current plan (abridged): the full structs go through the Exchange and into the 
join.
{noformat}
Calc(select=[nested.name AS name, info.city AS city])
+- Join(joinType=[InnerJoin], where=[(id = id0)], select=[id, nested, id0, 
info], ...)
   :- Exchange(distribution=[hash[id]])
   :  +- TableSourceScan(table=[[L, project=[id, nested]]])
   +- Exchange(distribution=[hash[id]])
      +- TableSourceScan(table=[[R, project=[id, info]]])
{noformat}
Expected plan (with a source supporting nested projection pushdown): only the 
referenced leaves enter the join.
{noformat}
Calc(select=[nested_name AS name, info_city AS city])
+- Join(joinType=[InnerJoin], where=[(id = id0)], select=[id, nested_name, id0, 
info_city], ...)
   :- Exchange(distribution=[hash[id]])
   :  +- TableSourceScan(table=[[L, project=[id, nested_name]]])
   +- Exchange(distribution=[hash[id]])
      +- TableSourceScan(table=[[R, project=[id, info_city]]])
{noformat}
The benefit does not depend on the source supporting nested projection 
push-down: even if the scan still reads the whole struct, the leaf is extracted 
in a Calc below the join, so the Exchange and the join state only carry the 
leaf.
h2. Proposal

Add a rule that, for a Project above a Join, collects the nested leaf accesses 
on each side (including those in the join condition), pushes them below the 
join as flat top-level columns, and rewrites the join condition and the top 
projection accordingly. Whole-struct references keep the struct whole. Outer 
joins widen the pushed leaves to nullable on the null-producing side. Semi/anti 
joins are not touched.

The rule is guarded by a new option 
{{table.optimizer.nested-projection-join-transpose-enabled}} (default 
{{{}false{}}}).
h2. Compatibility

Enabling the option changes the row type entering the join operator, and 
therefore the layout of its state. A job planned without the option cannot be 
restored from a savepoint after enabling it (and vice versa). This is why the 
option defaults to false and should be documented accordingly (release note).
h2. Related
 * FLINK-33322: ProjectWatermarkAssignerTransposeRule does not support nested 
projection push-down
 * FLINK-39804: nested projection not pushed to the source when a filter sits 
between project and scan
 * FLINK-32940: projection push-down through UNNEST / UDTF

These cover other places where nested pruning is lost; this issue is only about 
joins.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to