This is an automated email from the ASF dual-hosted git repository.

snuyanzin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/master by this push:
     new 67594274366 [FLINK-41282][table-planner] Use each multi-join step's 
own join columns for upsert keys
67594274366 is described below

commit 675942743661d688202c246dd64b66cdce2c757b
Author: Anton Borisov <[email protected]>
AuthorDate: Fri Nov 6 12:02:02 2026 +0000

    [FLINK-41282][table-planner] Use each multi-join step's own join columns 
for upsert keys
    
    * [FLINK-41282][table-planner] Use each multi-join step's own join columns 
for upsert keys
    
    * Address review comments
    
    * Update 
flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdUpsertKeys.scala
    
    Co-authored-by: Sergey Nuyanzin <[email protected]>
    
    * Reword getLeftSideJoinKeyIndices description
    
    ---------
    
    Co-authored-by: Sergey Nuyanzin <[email protected]>
---
 .../physical/stream/StreamPhysicalMultiJoin.java   |   4 +
 .../plan/metadata/FlinkRelMdUpsertKeys.scala       |   4 +-
 .../nodes/exec/stream/MultiJoinRestoreTest.java    |   3 +-
 .../nodes/exec/stream/MultiJoinTestPrograms.java   |  73 +++
 .../planner/plan/stream/sql/MultiJoinTest.java     |  83 +++
 .../planner/plan/stream/sql/MultiJoinTest.xml      |  87 +++
 ...ree-way-join-on-fewer-columns-with-restore.json | 608 +++++++++++++++++++++
 .../savepoint/_metadata                            | Bin 0 -> 57818 bytes
 .../AttributeBasedJoinKeyExtractor.java            |  13 +
 .../join/stream/keyselector/JoinKeyExtractor.java  |  10 +
 10 files changed, 882 insertions(+), 3 deletions(-)

diff --git 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalMultiJoin.java
 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalMultiJoin.java
index 4d7aa3a519f..a5735f368e9 100644
--- 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalMultiJoin.java
+++ 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalMultiJoin.java
@@ -247,6 +247,10 @@ public class StreamPhysicalMultiJoin extends 
AbstractRelNode implements StreamPh
         return keyExtractor.getJoinKeyIndices(inputId);
     }
 
+    public int[] getLeftSideJoinKeyIndices(int inputId) {
+        return keyExtractor.getLeftSideJoinKeyIndices(inputId);
+    }
+
     private @Nullable Set<ImmutableBitSet> getUniqueKeys(RelNode input) {
         final FlinkRelMetadataQuery fmq =
                 
FlinkRelMetadataQuery.reuseOrCreate(input.getCluster().getMetadataQuery());
diff --git 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdUpsertKeys.scala
 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdUpsertKeys.scala
index ea94be9f61d..2532f0c256f 100644
--- 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdUpsertKeys.scala
+++ 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdUpsertKeys.scala
@@ -322,13 +322,13 @@ class FlinkRelMdUpsertKeys private extends 
MetadataHandler[UpsertKeys] {
     var leftFieldCount = inputs.get(0).getRowType.getFieldCount
     var leftUpsertKeys = fmq.getUpsertKeys(inputs.get(0))
 
-    val leftJoinKeyIndices = 
ImmutableBitSet.of(multiJoin.getJoinKeyIndices(0): _*)
-
     // Process each subsequent input as right side of join
     for (i <- 1 until inputs.size) {
       val rightInput = inputs.get(i)
       val rightUpsertKeys = fmq.getUpsertKeys(rightInput)
       val joinType = joinTypes.get(i)
+      // Only the left columns that input i's own condition references, not 
every previous join column.
+      val leftJoinKeyIndices = 
ImmutableBitSet.of(multiJoin.getLeftSideJoinKeyIndices(i): _*)
       val rightJoinKeyIndices = 
ImmutableBitSet.of(multiJoin.getJoinKeyIndices(i): _*)
 
       // We compute candidate unique keys that qualify as upsert keys if they 
include
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinRestoreTest.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinRestoreTest.java
index fb81aab8c4a..e9f47684f88 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinRestoreTest.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinRestoreTest.java
@@ -43,6 +43,7 @@ public class MultiJoinRestoreTest extends RestoreTestBase {
                 
MultiJoinTestPrograms.MULTI_JOIN_FOUR_WAY_COMPLEX_PRESERVES_UPSERT_KEY_WITH_RESTORE,
                 MultiJoinTestPrograms
                         
.MULTI_JOIN_TWO_WAY_LEFT_JOIN_PRESERVES_UPSERT_KEY_WITH_RESTORE,
-                
MultiJoinTestPrograms.MULTI_JOIN_THREE_WAY_JOIN_PRESERVES_UPSERT_KEY_WITH_RESTORE);
+                
MultiJoinTestPrograms.MULTI_JOIN_THREE_WAY_JOIN_PRESERVES_UPSERT_KEY_WITH_RESTORE,
+                
MultiJoinTestPrograms.MULTI_JOIN_THREE_WAY_JOIN_ON_FEWER_COLUMNS_WITH_RESTORE);
     }
 }
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinTestPrograms.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinTestPrograms.java
index f5abff08279..b084c0c8524 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinTestPrograms.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinTestPrograms.java
@@ -1391,6 +1391,79 @@ public class MultiJoinTestPrograms {
                                     + "  ON  o.user_id = p.user_id")
                     .build();
 
+    static final TableTestProgram 
MULTI_JOIN_THREE_WAY_JOIN_ON_FEWER_COLUMNS_WITH_RESTORE =
+            TableTestProgram.of(
+                            "three-way-join-on-fewer-columns-with-restore",
+                            "three way join on fewer columns with restore")
+                    
.setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
+                    .setupTableSource(
+                            SourceTestStep.newBuilder("UserRegions")
+                                    .addMode(ChangelogMode.upsert())
+                                    .addSchema(
+                                            "user_id STRING NOT NULL",
+                                            "region STRING NOT NULL",
+                                            "name STRING",
+                                            "CONSTRAINT `PRIMARY` PRIMARY KEY 
(`user_id`, `region`) NOT ENFORCED")
+                                    .producedBeforeRestore(
+                                            Row.ofKind(RowKind.INSERT, "u1", 
"US", "Ann"))
+                                    .producedAfterRestore(
+                                            Row.ofKind(RowKind.INSERT, "u1", 
"EU", "Ann"),
+                                            Row.ofKind(RowKind.DELETE, "u1", 
"EU", "Ann"))
+                                    .build())
+                    .setupTableSource(
+                            SourceTestStep.newBuilder("Orders")
+                                    .addMode(ChangelogMode.upsert())
+                                    .addSchema(
+                                            "order_id STRING NOT NULL",
+                                            "user_id STRING NOT NULL",
+                                            "product STRING",
+                                            "CONSTRAINT `PRIMARY` PRIMARY KEY 
(`order_id`, `user_id`) NOT ENFORCED")
+                                    .producedBeforeRestore(
+                                            Row.ofKind(RowKind.INSERT, "o1", 
"u1", "book"))
+                                    .producedAfterRestore(
+                                            Row.ofKind(RowKind.INSERT, "o2", 
"u1", "pen"))
+                                    .build())
+                    .setupTableSource(
+                            SourceTestStep.newBuilder("RegionAddresses")
+                                    .addMode(ChangelogMode.upsert())
+                                    .addSchema(
+                                            "user_id STRING NOT NULL",
+                                            "region STRING NOT NULL",
+                                            "location STRING",
+                                            "CONSTRAINT `PRIMARY` PRIMARY KEY 
(`user_id`, `region`) NOT ENFORCED")
+                                    .producedBeforeRestore(
+                                            Row.ofKind(RowKind.INSERT, "u1", 
"EU", "Berlin"),
+                                            Row.ofKind(RowKind.INSERT, "u1", 
"US", "NYC"))
+                                    .build())
+                    .setupTableSink(
+                            SinkTestStep.newBuilder("sink")
+                                    .addMode(ChangelogMode.upsert())
+                                    .addSchema(
+                                            "`order_id` STRING NOT NULL",
+                                            "`user_id` STRING NOT NULL",
+                                            "name STRING",
+                                            "location STRING",
+                                            "CONSTRAINT `PRIMARY` PRIMARY KEY 
(`order_id`, `user_id`) NOT ENFORCED")
+                                    // deleting the EU region must keep the 
orders' rows for the US
+                                    .consumedBeforeRestore("+I[o1, u1, Ann, 
NYC]")
+                                    .consumedAfterRestore("+I[o2, u1, Ann, 
NYC]")
+                                    .testMaterializedData()
+                                    .build())
+                    .runSql(
+                            "INSERT INTO `sink`\n"
+                                    + "SELECT\n"
+                                    + "    o.order_id,\n"
+                                    + "    o.user_id,\n"
+                                    + "    r.name,\n"
+                                    + "    a.location\n"
+                                    + "FROM UserRegions r\n"
+                                    + "JOIN Orders o\n"
+                                    + "  ON  r.user_id = o.user_id\n"
+                                    + "JOIN RegionAddresses a\n"
+                                    + "  ON  r.user_id = a.user_id AND 
r.region = a.region\n"
+                                    + "ON CONFLICT DO DEDUPLICATE")
+                    .build();
+
     public static final TableTestProgram
             MULTI_JOIN_WITH_TIME_ATTRIBUTES_IN_CONDITIONS_MATERIALIZATION =
                     TableTestProgram.of(
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.java
index fc387fddeb0..6a77fc3fcaf 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.java
@@ -176,6 +176,24 @@ public class MultiJoinTest extends TableTestBase {
                                 + "  user_id STRING PRIMARY KEY NOT ENFORCED,"
                                 + "  location STRING"
                                 + ") WITH ('connector' = 'values', 
'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE UserRegionsPK ("
+                                + "  user_id STRING NOT NULL,"
+                                + "  region STRING NOT NULL,"
+                                + "  name STRING,"
+                                + "  CONSTRAINT `PRIMARY` PRIMARY KEY 
(user_id, region) NOT ENFORCED"
+                                + ") WITH ('connector' = 'values', 
'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE RegionAddressPK ("
+                                + "  user_id STRING NOT NULL,"
+                                + "  region STRING NOT NULL,"
+                                + "  location STRING,"
+                                + "  CONSTRAINT `PRIMARY` PRIMARY KEY 
(user_id, region) NOT ENFORCED"
+                                + ") WITH ('connector' = 'values', 
'changelog-mode' = 'I,UA,D')");
     }
 
     @Test
@@ -865,6 +883,71 @@ public class MultiJoinTest extends TableTestBase {
                         + "    ON o.user_id = p.user_id");
     }
 
+    @Test
+    void testPreservesUpsertKeyThreeWayJoinOnFewerLeftColumnsDoesNot() {
+        // UserRegionsPK is unique on its join columns (user_id, region), but 
only user_id is joined
+        // to OrdersPK: an order matches the user's row of every region, so 
the orders key is not
+        // unique in the output and the sink needs upsertMaterialize.
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE sink_orders ("
+                                + "  `order_id` STRING NOT NULL,"
+                                + "  `user_id` STRING NOT NULL,"
+                                + "  name STRING,"
+                                + "  location STRING,"
+                                + "  CONSTRAINT `PRIMARY` PRIMARY KEY 
(`order_id`, `user_id`) NOT ENFORCED"
+                                + ") WITH ("
+                                + "  'connector' = 'values',"
+                                + "  'sink-insert-only' = 'false'"
+                                + ")");
+
+        util.verifyRelPlanInsert(
+                "\nINSERT INTO sink_orders\n"
+                        + "SELECT\n"
+                        + "    o.order_id,\n"
+                        + "    o.user_id,\n"
+                        + "    r.name,\n"
+                        + "    a.location\n"
+                        + "FROM UserRegionsPK r\n"
+                        + "JOIN OrdersPK o\n"
+                        + "    ON r.user_id = o.user_id\n"
+                        + "JOIN RegionAddressPK a\n"
+                        + "    ON r.user_id = a.user_id AND r.region = 
a.region\n"
+                        + "ON CONFLICT DO DEDUPLICATE");
+    }
+
+    @Test
+    void testPreservesUpsertKeyThreeWayJoinOnSecondInputColumns() {
+        // RegionAddressPK is joined on the key of UserRegionsPK, the second 
input, which stays
+        // unique after joining UsersPK on user_id: an address matches at most 
one row, so the
+        // address key is unique in the output and the sink needs no 
upsertMaterialize.
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE sink_addresses ("
+                                + "  `user_id` STRING NOT NULL,"
+                                + "  `region` STRING NOT NULL,"
+                                + "  name STRING,"
+                                + "  location STRING,"
+                                + "  CONSTRAINT `PRIMARY` PRIMARY KEY 
(`user_id`, `region`) NOT ENFORCED"
+                                + ") WITH ("
+                                + "  'connector' = 'values',"
+                                + "  'sink-insert-only' = 'false'"
+                                + ")");
+
+        util.verifyRelPlanInsert(
+                "\nINSERT INTO sink_addresses\n"
+                        + "SELECT\n"
+                        + "    a.user_id,\n"
+                        + "    a.region,\n"
+                        + "    u.name,\n"
+                        + "    a.location\n"
+                        + "FROM UsersPK u\n"
+                        + "JOIN UserRegionsPK r\n"
+                        + "    ON u.user_id = r.user_id\n"
+                        + "JOIN RegionAddressPK a\n"
+                        + "    ON r.user_id = a.user_id AND r.region = 
a.region");
+    }
+
     @Test
     void testPreservesUpsertKeyFourWayComplex() {
         util.tableEnv()
diff --git 
a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.xml
 
b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.xml
index 7a0fd28351b..7f34caf615f 100644
--- 
a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.xml
+++ 
b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.xml
@@ -1684,6 +1684,93 @@ 
Sink(table=[default_catalog.default_database.sink_three_way], fields=[user_id, o
       :  +- TableSourceScan(table=[[default_catalog, default_database, 
OrdersPK, project=[order_id, user_id]]], fields=[order_id, user_id])
       +- Exchange(distribution=[hash[user_id]])
          +- TableSourceScan(table=[[default_catalog, default_database, 
PaymentsPK, project=[payment_id, user_id]]], fields=[payment_id, user_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testPreservesUpsertKeyThreeWayJoinOnFewerLeftColumnsDoesNot">
+    <Resource name="sql">
+      <![CDATA[
+INSERT INTO sink_orders
+SELECT
+    o.order_id,
+    o.user_id,
+    r.name,
+    a.location
+FROM UserRegionsPK r
+JOIN OrdersPK o
+    ON r.user_id = o.user_id
+JOIN RegionAddressPK a
+    ON r.user_id = a.user_id AND r.region = a.region
+ON CONFLICT DO DEDUPLICATE]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalSink(table=[default_catalog.default_database.sink_orders], 
fields=[order_id, user_id, name, location])
++- LogicalProject(order_id=[$3], user_id=[$4], name=[$2], location=[$8])
+   +- LogicalJoin(condition=[AND(=($0, $6), =($1, $7))], joinType=[inner])
+      :- LogicalJoin(condition=[=($0, $4)], joinType=[inner])
+      :  :- LogicalTableScan(table=[[default_catalog, default_database, 
UserRegionsPK]])
+      :  +- LogicalTableScan(table=[[default_catalog, default_database, 
OrdersPK]])
+      +- LogicalTableScan(table=[[default_catalog, default_database, 
RegionAddressPK]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Sink(table=[default_catalog.default_database.sink_orders], fields=[order_id, 
user_id, name, location], upsertMaterialize=[true], 
conflictStrategy=[DEDUPLICATE])
++- Calc(select=[order_id, user_id0 AS user_id, name, location])
+   +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], 
inputUniqueKeys=[(user_id, region), (order_id, user_id), (user_id, region)], 
joinConditions=[=(user_id, user_id0), AND(=(user_id, user_id1), =(region, 
region0))], 
select=[user_id,region,name,order_id,user_id0,user_id1,region0,location], 
rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) region, 
VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) 
user_id0, VARCHAR(2147483647) user_ [...]
+      :- Exchange(distribution=[hash[user_id]])
+      :  +- ChangelogNormalize(key=[user_id, region])
+      :     +- Exchange(distribution=[hash[user_id, region]])
+      :        +- TableSourceScan(table=[[default_catalog, default_database, 
UserRegionsPK]], fields=[user_id, region, name])
+      :- Exchange(distribution=[hash[user_id]])
+      :  +- ChangelogNormalize(key=[order_id, user_id])
+      :     +- Exchange(distribution=[hash[order_id, user_id]])
+      :        +- TableSourceScan(table=[[default_catalog, default_database, 
OrdersPK, project=[order_id, user_id]]], fields=[order_id, user_id])
+      +- Exchange(distribution=[hash[user_id]])
+         +- ChangelogNormalize(key=[user_id, region])
+            +- Exchange(distribution=[hash[user_id, region]])
+               +- TableSourceScan(table=[[default_catalog, default_database, 
RegionAddressPK]], fields=[user_id, region, location])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testPreservesUpsertKeyThreeWayJoinOnSecondInputColumns">
+    <Resource name="sql">
+      <![CDATA[
+INSERT INTO sink_addresses
+SELECT
+    a.user_id,
+    a.region,
+    u.name,
+    a.location
+FROM UsersPK u
+JOIN UserRegionsPK r
+    ON u.user_id = r.user_id
+JOIN RegionAddressPK a
+    ON r.user_id = a.user_id AND r.region = a.region]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalSink(table=[default_catalog.default_database.sink_addresses], 
fields=[user_id, region, name, location])
++- LogicalProject(user_id=[$7], region=[$8], name=[$1], location=[$9])
+   +- LogicalJoin(condition=[AND(=($4, $7), =($5, $8))], joinType=[inner])
+      :- LogicalJoin(condition=[=($0, $4)], joinType=[inner])
+      :  :- LogicalTableScan(table=[[default_catalog, default_database, 
UsersPK]])
+      :  +- LogicalTableScan(table=[[default_catalog, default_database, 
UserRegionsPK]])
+      +- LogicalTableScan(table=[[default_catalog, default_database, 
RegionAddressPK]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Sink(table=[default_catalog.default_database.sink_addresses], fields=[user_id, 
region, name, location])
++- Calc(select=[user_id1 AS user_id, region0 AS region, name, location])
+   +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], 
inputUniqueKeys=[(user_id), (user_id, region), (user_id, region)], 
joinConditions=[=(user_id, user_id0), AND(=(user_id0, user_id1), =(region, 
region0))], select=[user_id,name,user_id0,region,user_id1,region0,location], 
rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, 
VARCHAR(2147483647) user_id0, VARCHAR(2147483647) region, VARCHAR(2147483647) 
user_id1, VARCHAR(2147483647) region0, VARCHAR(214748 [...]
+      :- Exchange(distribution=[hash[user_id]])
+      :  +- TableSourceScan(table=[[default_catalog, default_database, 
UsersPK, project=[user_id, name]]], fields=[user_id, name])
+      :- Exchange(distribution=[hash[user_id]])
+      :  +- TableSourceScan(table=[[default_catalog, default_database, 
UserRegionsPK, project=[user_id, region]]], fields=[user_id, region])
+      +- Exchange(distribution=[hash[user_id]])
+         +- TableSourceScan(table=[[default_catalog, default_database, 
RegionAddressPK]], fields=[user_id, region, location])
 ]]>
     </Resource>
   </TestCase>
diff --git 
a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-on-fewer-columns-with-restore/plan/three-way-join-on-fewer-columns-with-restore.json
 
b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-on-fewer-columns-with-restore/plan/three-way-join-on-fewer-columns-with-restore.json
new file mode 100644
index 00000000000..742c17d1feb
--- /dev/null
+++ 
b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-on-fewer-columns-with-restore/plan/three-way-join-on-fewer-columns-with-restore.json
@@ -0,0 +1,608 @@
+{
+  "flinkVersion" : "2.5",
+  "nodes" : [ {
+    "id" : 1,
+    "type" : "stream-exec-table-source-scan_2",
+    "scanTableSource" : {
+      "table" : {
+        "identifier" : "`default_catalog`.`default_database`.`UserRegions`",
+        "resolvedTable" : {
+          "schema" : {
+            "columns" : [ {
+              "name" : "user_id",
+              "dataType" : "VARCHAR(2147483647) NOT NULL"
+            }, {
+              "name" : "region",
+              "dataType" : "VARCHAR(2147483647) NOT NULL"
+            }, {
+              "name" : "name",
+              "dataType" : "VARCHAR(2147483647)"
+            } ],
+            "primaryKey" : {
+              "name" : "PRIMARY",
+              "type" : "PRIMARY_KEY",
+              "columns" : [ "user_id", "region" ]
+            }
+          }
+        }
+      }
+    },
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647) NOT NULL, `region` 
VARCHAR(2147483647) NOT NULL, `name` VARCHAR(2147483647)>",
+    "description" : "TableSourceScan(table=[[default_catalog, 
default_database, UserRegions]], fields=[user_id, region, name])"
+  }, {
+    "id" : 2,
+    "type" : "stream-exec-exchange_1",
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "HASH",
+        "keys" : [ 0, 1 ]
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647) NOT NULL, `region` 
VARCHAR(2147483647) NOT NULL, `name` VARCHAR(2147483647)>",
+    "description" : "Exchange(distribution=[hash[user_id, region]])"
+  }, {
+    "id" : 3,
+    "type" : "stream-exec-changelog-normalize_1",
+    "configuration" : {
+      "table.exec.mini-batch.enabled" : "false",
+      "table.exec.mini-batch.size" : "-1"
+    },
+    "uniqueKeys" : [ 0, 1 ],
+    "generateUpdateBefore" : true,
+    "state" : [ {
+      "index" : 0,
+      "ttl" : "0 ms",
+      "name" : "changelogNormalizeState"
+    } ],
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647) NOT NULL, `region` 
VARCHAR(2147483647) NOT NULL, `name` VARCHAR(2147483647)>",
+    "description" : "ChangelogNormalize(key=[user_id, region])"
+  }, {
+    "id" : 4,
+    "type" : "stream-exec-exchange_1",
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "HASH",
+        "keys" : [ 0 ]
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647) NOT NULL, `region` 
VARCHAR(2147483647) NOT NULL, `name` VARCHAR(2147483647)>",
+    "description" : "Exchange(distribution=[hash[user_id]])"
+  }, {
+    "id" : 5,
+    "type" : "stream-exec-table-source-scan_2",
+    "scanTableSource" : {
+      "table" : {
+        "identifier" : "`default_catalog`.`default_database`.`Orders`",
+        "resolvedTable" : {
+          "schema" : {
+            "columns" : [ {
+              "name" : "order_id",
+              "dataType" : "VARCHAR(2147483647) NOT NULL"
+            }, {
+              "name" : "user_id",
+              "dataType" : "VARCHAR(2147483647) NOT NULL"
+            }, {
+              "name" : "product",
+              "dataType" : "VARCHAR(2147483647)"
+            } ],
+            "primaryKey" : {
+              "name" : "PRIMARY",
+              "type" : "PRIMARY_KEY",
+              "columns" : [ "order_id", "user_id" ]
+            }
+          }
+        }
+      },
+      "abilities" : [ {
+        "type" : "ProjectPushDown",
+        "projectedFields" : [ [ 0 ], [ 1 ] ],
+        "producedType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, 
`user_id` VARCHAR(2147483647) NOT NULL> NOT NULL"
+      } ]
+    },
+    "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id` 
VARCHAR(2147483647) NOT NULL>",
+    "description" : "TableSourceScan(table=[[default_catalog, 
default_database, Orders, project=[order_id, user_id]]], fields=[order_id, 
user_id])"
+  }, {
+    "id" : 6,
+    "type" : "stream-exec-exchange_1",
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "HASH",
+        "keys" : [ 0, 1 ]
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id` 
VARCHAR(2147483647) NOT NULL>",
+    "description" : "Exchange(distribution=[hash[order_id, user_id]])"
+  }, {
+    "id" : 7,
+    "type" : "stream-exec-changelog-normalize_1",
+    "configuration" : {
+      "table.exec.mini-batch.enabled" : "false",
+      "table.exec.mini-batch.size" : "-1"
+    },
+    "uniqueKeys" : [ 0, 1 ],
+    "generateUpdateBefore" : true,
+    "state" : [ {
+      "index" : 0,
+      "ttl" : "0 ms",
+      "name" : "changelogNormalizeState"
+    } ],
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id` 
VARCHAR(2147483647) NOT NULL>",
+    "description" : "ChangelogNormalize(key=[order_id, user_id])"
+  }, {
+    "id" : 8,
+    "type" : "stream-exec-exchange_1",
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "HASH",
+        "keys" : [ 1 ]
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id` 
VARCHAR(2147483647) NOT NULL>",
+    "description" : "Exchange(distribution=[hash[user_id]])"
+  }, {
+    "id" : 9,
+    "type" : "stream-exec-table-source-scan_2",
+    "scanTableSource" : {
+      "table" : {
+        "identifier" : 
"`default_catalog`.`default_database`.`RegionAddresses`",
+        "resolvedTable" : {
+          "schema" : {
+            "columns" : [ {
+              "name" : "user_id",
+              "dataType" : "VARCHAR(2147483647) NOT NULL"
+            }, {
+              "name" : "region",
+              "dataType" : "VARCHAR(2147483647) NOT NULL"
+            }, {
+              "name" : "location",
+              "dataType" : "VARCHAR(2147483647)"
+            } ],
+            "primaryKey" : {
+              "name" : "PRIMARY",
+              "type" : "PRIMARY_KEY",
+              "columns" : [ "user_id", "region" ]
+            }
+          }
+        }
+      }
+    },
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647) NOT NULL, `region` 
VARCHAR(2147483647) NOT NULL, `location` VARCHAR(2147483647)>",
+    "description" : "TableSourceScan(table=[[default_catalog, 
default_database, RegionAddresses]], fields=[user_id, region, location])"
+  }, {
+    "id" : 10,
+    "type" : "stream-exec-exchange_1",
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "HASH",
+        "keys" : [ 0, 1 ]
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647) NOT NULL, `region` 
VARCHAR(2147483647) NOT NULL, `location` VARCHAR(2147483647)>",
+    "description" : "Exchange(distribution=[hash[user_id, region]])"
+  }, {
+    "id" : 11,
+    "type" : "stream-exec-changelog-normalize_1",
+    "configuration" : {
+      "table.exec.mini-batch.enabled" : "false",
+      "table.exec.mini-batch.size" : "-1"
+    },
+    "uniqueKeys" : [ 0, 1 ],
+    "generateUpdateBefore" : true,
+    "state" : [ {
+      "index" : 0,
+      "ttl" : "0 ms",
+      "name" : "changelogNormalizeState"
+    } ],
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647) NOT NULL, `region` 
VARCHAR(2147483647) NOT NULL, `location` VARCHAR(2147483647)>",
+    "description" : "ChangelogNormalize(key=[user_id, region])"
+  }, {
+    "id" : 12,
+    "type" : "stream-exec-exchange_1",
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "HASH",
+        "keys" : [ 0 ]
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647) NOT NULL, `region` 
VARCHAR(2147483647) NOT NULL, `location` VARCHAR(2147483647)>",
+    "description" : "Exchange(distribution=[hash[user_id]])"
+  }, {
+    "id" : 13,
+    "type" : "stream-exec-multi-join_1",
+    "joinTypes" : [ "INNER", "INNER", "INNER" ],
+    "joinConditions" : [ {
+      "kind" : "LITERAL",
+      "value" : true,
+      "type" : "BOOLEAN NOT NULL"
+    }, {
+      "kind" : "CALL",
+      "syntax" : "BINARY",
+      "internalName" : "$=$1",
+      "operands" : [ {
+        "kind" : "INPUT_REF",
+        "inputIndex" : 0,
+        "type" : "VARCHAR(2147483647) NOT NULL"
+      }, {
+        "kind" : "INPUT_REF",
+        "inputIndex" : 4,
+        "type" : "VARCHAR(2147483647) NOT NULL"
+      } ],
+      "type" : "BOOLEAN NOT NULL"
+    }, {
+      "kind" : "CALL",
+      "syntax" : "BINARY",
+      "internalName" : "$AND$1",
+      "operands" : [ {
+        "kind" : "CALL",
+        "syntax" : "BINARY",
+        "internalName" : "$=$1",
+        "operands" : [ {
+          "kind" : "INPUT_REF",
+          "inputIndex" : 0,
+          "type" : "VARCHAR(2147483647) NOT NULL"
+        }, {
+          "kind" : "INPUT_REF",
+          "inputIndex" : 5,
+          "type" : "VARCHAR(2147483647) NOT NULL"
+        } ],
+        "type" : "BOOLEAN NOT NULL"
+      }, {
+        "kind" : "CALL",
+        "syntax" : "BINARY",
+        "internalName" : "$=$1",
+        "operands" : [ {
+          "kind" : "INPUT_REF",
+          "inputIndex" : 1,
+          "type" : "VARCHAR(2147483647) NOT NULL"
+        }, {
+          "kind" : "INPUT_REF",
+          "inputIndex" : 6,
+          "type" : "VARCHAR(2147483647) NOT NULL"
+        } ],
+        "type" : "BOOLEAN NOT NULL"
+      } ],
+      "type" : "BOOLEAN NOT NULL"
+    } ],
+    "multiJoinCondition" : {
+      "kind" : "CALL",
+      "syntax" : "BINARY",
+      "internalName" : "$AND$1",
+      "operands" : [ {
+        "kind" : "CALL",
+        "syntax" : "BINARY",
+        "internalName" : "$=$1",
+        "operands" : [ {
+          "kind" : "INPUT_REF",
+          "inputIndex" : 0,
+          "type" : "VARCHAR(2147483647) NOT NULL"
+        }, {
+          "kind" : "INPUT_REF",
+          "inputIndex" : 4,
+          "type" : "VARCHAR(2147483647) NOT NULL"
+        } ],
+        "type" : "BOOLEAN NOT NULL"
+      }, {
+        "kind" : "CALL",
+        "syntax" : "BINARY",
+        "internalName" : "$=$1",
+        "operands" : [ {
+          "kind" : "INPUT_REF",
+          "inputIndex" : 0,
+          "type" : "VARCHAR(2147483647) NOT NULL"
+        }, {
+          "kind" : "INPUT_REF",
+          "inputIndex" : 5,
+          "type" : "VARCHAR(2147483647) NOT NULL"
+        } ],
+        "type" : "BOOLEAN NOT NULL"
+      }, {
+        "kind" : "CALL",
+        "syntax" : "BINARY",
+        "internalName" : "$=$1",
+        "operands" : [ {
+          "kind" : "INPUT_REF",
+          "inputIndex" : 1,
+          "type" : "VARCHAR(2147483647) NOT NULL"
+        }, {
+          "kind" : "INPUT_REF",
+          "inputIndex" : 6,
+          "type" : "VARCHAR(2147483647) NOT NULL"
+        } ],
+        "type" : "BOOLEAN NOT NULL"
+      } ],
+      "type" : "BOOLEAN NOT NULL"
+    },
+    "joinAttributeMap" : {
+      "0" : [ {
+        "leftInputId" : -1,
+        "leftFieldIndex" : -1,
+        "rightInputId" : 0,
+        "rightFieldIndex" : 0
+      }, {
+        "leftInputId" : -1,
+        "leftFieldIndex" : -1,
+        "rightInputId" : 0,
+        "rightFieldIndex" : 0
+      }, {
+        "leftInputId" : -1,
+        "leftFieldIndex" : -1,
+        "rightInputId" : 0,
+        "rightFieldIndex" : 1
+      } ],
+      "1" : [ {
+        "leftInputId" : 0,
+        "leftFieldIndex" : 0,
+        "rightInputId" : 1,
+        "rightFieldIndex" : 1
+      } ],
+      "2" : [ {
+        "leftInputId" : 0,
+        "leftFieldIndex" : 0,
+        "rightInputId" : 2,
+        "rightFieldIndex" : 0
+      }, {
+        "leftInputId" : 0,
+        "leftFieldIndex" : 1,
+        "rightInputId" : 2,
+        "rightFieldIndex" : 1
+      } ]
+    },
+    "inputUniqueKeys" : [ [ [ 0, 1 ] ], [ [ 0, 1 ] ], [ [ 0, 1 ] ] ],
+    "state" : [ {
+      "index" : 0,
+      "ttl" : "0 ms",
+      "name" : "input-state-0"
+    }, {
+      "index" : 1,
+      "ttl" : "0 ms",
+      "name" : "input-state-1"
+    }, {
+      "index" : 2,
+      "ttl" : "0 ms",
+      "name" : "input-state-2"
+    } ],
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "HASH",
+        "keys" : [ 0 ]
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    }, {
+      "requiredDistribution" : {
+        "type" : "HASH",
+        "keys" : [ 1 ]
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 1
+    }, {
+      "requiredDistribution" : {
+        "type" : "HASH",
+        "keys" : [ 0 ]
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 2
+    } ],
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647) NOT NULL, `region` 
VARCHAR(2147483647) NOT NULL, `name` VARCHAR(2147483647), `order_id` 
VARCHAR(2147483647) NOT NULL, `user_id0` VARCHAR(2147483647) NOT NULL, 
`user_id1` VARCHAR(2147483647) NOT NULL, `region0` VARCHAR(2147483647) NOT 
NULL, `location` VARCHAR(2147483647)>",
+    "description" : "MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, 
INNER], inputUniqueKeys=[(user_id, region), (order_id, user_id), (user_id, 
region)], joinConditions=[(user_id = user_id0), ((user_id = user_id1) AND 
(region = region0))], 
select=[user_id,region,name,order_id,user_id0,user_id1,region0,location], 
rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) region, 
VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) 
user_id0, VARCHAR(21 [...]
+  }, {
+    "id" : 14,
+    "type" : "stream-exec-calc_1",
+    "projection" : [ {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 3,
+      "type" : "VARCHAR(2147483647) NOT NULL"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 4,
+      "type" : "VARCHAR(2147483647) NOT NULL"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 2,
+      "type" : "VARCHAR(2147483647)"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 7,
+      "type" : "VARCHAR(2147483647)"
+    } ],
+    "condition" : null,
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id` 
VARCHAR(2147483647) NOT NULL, `name` VARCHAR(2147483647), `location` 
VARCHAR(2147483647)>",
+    "description" : "Calc(select=[order_id, user_id0 AS user_id, name, 
location])"
+  }, {
+    "id" : 15,
+    "type" : "stream-exec-sink_2",
+    "configuration" : {
+      "table.exec.sink.keyed-shuffle" : "AUTO",
+      "table.exec.sink.not-null-enforcer" : "ERROR",
+      "table.exec.sink.rowtime-inserter" : "ENABLED",
+      "table.exec.sink.type-length-enforcer" : "IGNORE",
+      "table.exec.sink.upsert-materialize" : "AUTO"
+    },
+    "dynamicTableSink" : {
+      "table" : {
+        "identifier" : "`default_catalog`.`default_database`.`sink`",
+        "resolvedTable" : {
+          "schema" : {
+            "columns" : [ {
+              "name" : "order_id",
+              "dataType" : "VARCHAR(2147483647) NOT NULL"
+            }, {
+              "name" : "user_id",
+              "dataType" : "VARCHAR(2147483647) NOT NULL"
+            }, {
+              "name" : "name",
+              "dataType" : "VARCHAR(2147483647)"
+            }, {
+              "name" : "location",
+              "dataType" : "VARCHAR(2147483647)"
+            } ],
+            "primaryKey" : {
+              "name" : "PRIMARY",
+              "type" : "PRIMARY_KEY",
+              "columns" : [ "order_id", "user_id" ]
+            }
+          }
+        }
+      }
+    },
+    "inputChangelogMode" : [ "INSERT", "UPDATE_BEFORE", "UPDATE_AFTER", 
"DELETE" ],
+    "requireUpsertMaterialize" : true,
+    "upsertMaterializeStrategy" : "ADAPTIVE",
+    "state" : [ {
+      "index" : 0,
+      "ttl" : "0 ms",
+      "name" : "sinkMaterializeState"
+    } ],
+    "conflictStrategy" : {
+      "behavior" : "DEDUPLICATE"
+    },
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id` 
VARCHAR(2147483647) NOT NULL, `name` VARCHAR(2147483647), `location` 
VARCHAR(2147483647)>",
+    "description" : "Sink(table=[default_catalog.default_database.sink], 
fields=[order_id, user_id, name, location], upsertMaterialize=[true], 
conflictStrategy=[DEDUPLICATE])"
+  } ],
+  "edges" : [ {
+    "source" : 1,
+    "target" : 2,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 2,
+    "target" : 3,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 3,
+    "target" : 4,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 5,
+    "target" : 6,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 6,
+    "target" : 7,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 7,
+    "target" : 8,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 9,
+    "target" : 10,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 10,
+    "target" : 11,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 11,
+    "target" : 12,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 4,
+    "target" : 13,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 8,
+    "target" : 13,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 12,
+    "target" : 13,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 13,
+    "target" : 14,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 14,
+    "target" : 15,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  } ]
+}
\ No newline at end of file
diff --git 
a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-on-fewer-columns-with-restore/savepoint/_metadata
 
b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-on-fewer-columns-with-restore/savepoint/_metadata
new file mode 100644
index 00000000000..6e708ab01a3
Binary files /dev/null and 
b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-on-fewer-columns-with-restore/savepoint/_metadata
 differ
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/keyselector/AttributeBasedJoinKeyExtractor.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/keyselector/AttributeBasedJoinKeyExtractor.java
index 6de9e570adc..aeede3d7546 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/keyselector/AttributeBasedJoinKeyExtractor.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/keyselector/AttributeBasedJoinKeyExtractor.java
@@ -170,6 +170,15 @@ public class AttributeBasedJoinKeyExtractor implements 
JoinKeyExtractor, Seriali
         return 
keyFieldIndices.stream().mapToInt(KeyExtractor::getFieldIndexInSourceRow).toArray();
     }
 
+    @Override
+    public int[] getLeftSideJoinKeyIndices(final int inputId) {
+        final List<KeyExtractor> keyExtractors = 
this.leftKeyExtractorsMap.get(inputId);
+        if (keyExtractors == null) {
+            return new int[0];
+        }
+        return 
keyExtractors.stream().mapToInt(KeyExtractor::getAbsoluteFieldIndex).toArray();
+    }
+
     @Override
     public RowType getCommonJoinKeyType() {
         // We return an empty RowType if no common join key is defined.
@@ -913,6 +922,10 @@ public class AttributeBasedJoinKeyExtractor implements 
JoinKeyExtractor, Seriali
             return fieldIndexInSourceRow;
         }
 
+        public int getAbsoluteFieldIndex() {
+            return absoluteFieldIndex;
+        }
+
         private void readObject(final java.io.ObjectInputStream in)
                 throws java.io.IOException, ClassNotFoundException {
             in.defaultReadObject();
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/keyselector/JoinKeyExtractor.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/keyselector/JoinKeyExtractor.java
index 4fb96d28ed1..28b44081225 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/keyselector/JoinKeyExtractor.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/keyselector/JoinKeyExtractor.java
@@ -83,6 +83,16 @@ public interface JoinKeyExtractor extends Serializable {
      */
     int[] getJoinKeyIndices(int inputId);
 
+    /**
+     * Gets the field indices in the joined row of the previous inputs that 
make up the left-side
+     * join key for a given input.
+     *
+     * @param inputId The ID of the input stream.
+     * @return An array of integers representing the field indices in the 
joined row that form the
+     *     left-side join key.
+     */
+    int[] getLeftSideJoinKeyIndices(int inputId);
+
     /**
      * Extracts the common key from an input row. The common key consists of 
attributes that are
      * part of all equi-join conditions in the multi-join sequence.

Reply via email to