Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ join dim_with_pk for system_time as of t1.proctime as t2
-- 执行计划:声明了 pk 后的维表,通过 pk 连接时左流的 upsertKey 属性得以保留,从而节省了高开销的物化节点
Sink(table=[default_catalog.default_database.sink_with_pk], fields=[a, b, c])
+- Calc(select=[a, b, c])
+- LookupJoin(table=[default_catalog.default_database.dim_with_pk], joinType=[InnerJoin], lookup=[a=a], select=[a, b, a, c])
+- LookupJoin(table=[default_catalog.default_database.dim_with_pk, project=[a, c]], joinType=[InnerJoin], lookup=[a=a], select=[a, b, a, c])
+- DropUpdateBefore
+- TableSourceScan(table=[[default_catalog, default_database, cdc, project=[a, b], metadata=[]]], fields=[a, b])
```
Expand All @@ -246,7 +246,7 @@ join dim_without_pk for system_time as of t1.proctime as t2
-- 不启用 `TRY_RESOLVE` 模式时在运行时可能产生错误,当启用 `TRY_RESOLVE` 时的执行计划
Sink(table=[default_catalog.default_database.sink_with_pk], fields=[a, b, c], upsertMaterialize=[true])
+- Calc(select=[a, b, c])
+- LookupJoin(table=[default_catalog.default_database.dim_without_pk], joinType=[InnerJoin], lookup=[a=a], select=[a, b, a, c], upsertMaterialize=[true])
+- LookupJoin(table=[default_catalog.default_database.dim_without_pk, project=[a, c]], joinType=[InnerJoin], lookup=[a=a], select=[a, b, a, c], upsertMaterialize=[true])
+- TableSourceScan(table=[[default_catalog, default_database, cdc, project=[a, b], metadata=[]]], fields=[a, b])
```
尽管第二个查询可以通过启用 `TRY_RESOLVE` 选项(增加物化)来解决正确性问题,但成本高昂,与声明了主键的第一个查询相比,会多出两个更昂贵的物化操作。
Expand Down
4 changes: 2 additions & 2 deletions docs/content/docs/concepts/sql-table-concepts/determinism.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ join dim_with_pk for system_time as of t1.proctime as t2
-- plan: the upsertKey of left stream is reserved when lookup table with a pk definition and use it as lookup key, so that the high cost materialization can be omitted.
Sink(table=[default_catalog.default_database.sink_with_pk], fields=[a, b, c])
+- Calc(select=[a, b, c])
+- LookupJoin(table=[default_catalog.default_database.dim_with_pk], joinType=[InnerJoin], lookup=[a=a], select=[a, b, a, c])
+- LookupJoin(table=[default_catalog.default_database.dim_with_pk, project=[a, c]], joinType=[InnerJoin], lookup=[a=a], select=[a, b, a, c])
+- DropUpdateBefore
+- TableSourceScan(table=[[default_catalog, default_database, cdc, project=[a, b], metadata=[]]], fields=[a, b])
```
Expand All @@ -257,7 +257,7 @@ join dim_without_pk for system_time as of t1.proctime as t2
-- execution plan when `TRY_RESOLVE` is enabled(may encounter errors at runtime when `TRY_RESOLVE` mode is not enabled):
Sink(table=[default_catalog.default_database.sink_with_pk], fields=[a, b, c], upsertMaterialize=[true])
+- Calc(select=[a, b, c])
+- LookupJoin(table=[default_catalog.default_database.dim_without_pk], joinType=[InnerJoin], lookup=[a=a], select=[a, b, a, c], upsertMaterialize=[true])
+- LookupJoin(table=[default_catalog.default_database.dim_without_pk, project=[a, c]], joinType=[InnerJoin], lookup=[a=a], select=[a, b, a, c], upsertMaterialize=[true])
+- TableSourceScan(table=[[default_catalog, default_database, cdc, project=[a, b], metadata=[]]], fields=[a, b])
```
Though the second case can be solved by adding materialization if `TRY_RESOLVE` is enabled, but the cost is very high, there will be two more costly materialization compared to the one with primary key.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
package org.apache.flink.table.planner.plan.nodes.physical.common

import org.apache.flink.table.api.{TableConfig, TableException}
import org.apache.flink.table.catalog.{ObjectIdentifier, UniqueConstraint}
import org.apache.flink.table.catalog.UniqueConstraint
import org.apache.flink.table.connector.ChangelogMode
import org.apache.flink.table.planner.calcite.FlinkTypeFactory
import org.apache.flink.table.planner.plan.nodes.FlinkRelNode
Expand Down Expand Up @@ -187,14 +187,18 @@ abstract class CommonPhysicalLookupJoin(
case None =>
resultFieldNames.mkString(", ")
}
val tableIdentifier: ObjectIdentifier = temporalTable match {
case t: TableSourceTable => t.contextResolvedTable.getIdentifier
case t: LegacyTableSourceTable[_] => t.tableIdentifier
// Two lookup joins on the same table with different push-downs are different operators. The
// scan gets the spec digests from RelOptTable#getQualifiedName; the lookup join has to add them.
val tableDigest: String = temporalTable match {
case t: TableSourceTable =>
val specDigests = t.getSpecDigests.asScala.toSeq.filterNot(_.endsWith("=[]"))
(t.contextResolvedTable.getIdentifier.asSummaryString +: specDigests).mkString(", ")
Comment on lines +194 to +195

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this is weird...
probably better with having empty collections in plans...

sorry for back and forth however we should not rely on 3rd party toString implementstion

case t: LegacyTableSourceTable[_] => t.tableIdentifier.asSummaryString
}

super
.explainTerms(pw)
.item("table", tableIdentifier.asSummaryString())
.item("table", tableDigest)
.item("joinType", JoinTypeUtil.getFlinkJoinType(joinType))
.item("lookup", lookupKeys)
.itemIf("where", whereString, whereString.nonEmpty)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.flink.table.planner.plan.nodes.exec.common;

import org.apache.flink.table.planner.plan.nodes.exec.stream.StreamExecLookupJoin;
import org.apache.flink.table.test.program.SinkTestStep;
import org.apache.flink.table.test.program.SourceTestStep;
import org.apache.flink.table.test.program.TableTestProgram;

import static org.apache.flink.table.planner.plan.nodes.exec.common.LookupJoinTestPrograms.CUSTOMERS_BEFORE_DATA;
import static org.apache.flink.table.planner.plan.nodes.exec.common.LookupJoinTestPrograms.CUSTOMERS_SCHEMA;
import static org.apache.flink.table.planner.plan.nodes.exec.common.LookupJoinTestPrograms.ORDERS_BEFORE_DATA;
import static org.apache.flink.table.planner.plan.nodes.exec.common.LookupJoinTestPrograms.ORDERS_SCHEMA;
import static org.apache.flink.table.planner.plan.nodes.exec.common.LookupJoinTestPrograms.SINK_SCHEMA;

/** {@link TableTestProgram} definitions for semantic testing {@link StreamExecLookupJoin}. */
public class LookupJoinSemanticTestPrograms {

/** A semantic test cannot take a source that carries after-restore data. */
static final SourceTestStep CUSTOMERS =
SourceTestStep.newBuilder("customers_t")
.addOption("disable-lookup", "false") // static/lookup table
.addOption("filterable-fields", "age")
.addSchema(CUSTOMERS_SCHEMA)
.producedValues(CUSTOMERS_BEFORE_DATA)
.build();

static final SourceTestStep ORDERS =
SourceTestStep.newBuilder("orders_t")
.addOption("filterable-fields", "customer_id")
.addSchema(ORDERS_SCHEMA)
.producedValues(ORDERS_BEFORE_DATA)
.build();

private static String filteredLookupJoin(String filter) {
return "SELECT "
+ "O.order_id, "
+ "O.total, "
+ "C.id, "
+ "C.name, "
+ "C.age, "
+ "C.city, "
+ "C.state, "
+ "C.zipcode "
+ "FROM orders_t as O "
+ "JOIN customers_t FOR SYSTEM_TIME AS OF O.proc_time AS C "
+ "ON O.customer_id = C.id AND "
+ filter;
}

/**
* Both branches select the same columns from the same dim table and differ only in the filter
* pushed into it, so the two lookup joins are indistinguishable unless the pushed-down filter
* is part of the lookup join's digest. See FLINK-36808.
*/
private static String unionOfTwoFilteredLookupJoins(String firstFilter, String secondFilter) {
return "INSERT INTO sink_t "
+ filteredLookupJoin(firstFilter)
+ " UNION ALL "
+ filteredLookupJoin(secondFilter);
}

private static SinkTestStep sink() {
return SinkTestStep.newBuilder("sink_t")
.addSchema(SINK_SCHEMA)
.consumedValues(
"+I[1, 44.44, 3, Claire, 37, Austin, Texas, 73301]",
"+I[2, 100.02, 5, Jake, 42, New York City, New York, 10001]",
"+I[4, 92.61, 2, Alice, 32, San Francisco, California, 95016]",
"+I[5, 12.78, 2, Alice, 32, San Francisco, California, 95016]")
.build();
}

public static final TableTestProgram LOOKUP_JOIN_UNION_DIFFERENT_FILTERS =
TableTestProgram.of(
"lookup-join-union-different-filters",
"validates two lookup joins on the same table with different pushed-down filters are not merged, with the matching filter first")
.setupTableSource(CUSTOMERS)
.setupTableSource(ORDERS)
.setupTableSink(sink())
.runSql(unionOfTwoFilteredLookupJoins("C.age > 30", "C.age > 100"))
.build();

public static final TableTestProgram LOOKUP_JOIN_UNION_DIFFERENT_FILTERS_REVERSED =
TableTestProgram.of(
"lookup-join-union-different-filters-reversed",
"validates the same with the non-matching filter first, where a wrong merge returns nothing at all")
.setupTableSource(CUSTOMERS)
.setupTableSource(ORDERS)
.setupTableSink(sink())
.runSql(unionOfTwoFilteredLookupJoins("C.age > 100", "C.age > 30"))
.build();
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,22 +84,30 @@ public class LookupJoinTestPrograms {
.producedAfterRestore(CUSTOMERS_AFTER_DATA)
.build();

static final String[] ORDERS_SCHEMA =
new String[] {
"order_id INT",
"customer_id INT",
"total DOUBLE",
"order_time STRING",
"proc_time AS PROCTIME()"
};

static final Row[] ORDERS_BEFORE_DATA =
new Row[] {
Row.of(1, 3, 44.44, "2020-10-10 00:00:01"),
Row.of(2, 5, 100.02, "2020-10-10 00:00:02"),
Row.of(4, 2, 92.61, "2020-10-10 00:00:04"),
Row.of(3, 1, 23.89, "2020-10-10 00:00:03"),
Row.of(6, 4, 7.65, "2020-10-10 00:00:06"),
Row.of(5, 2, 12.78, "2020-10-10 00:00:05")
};

static final SourceTestStep ORDERS =
SourceTestStep.newBuilder("orders_t")
.addOption("filterable-fields", "customer_id")
.addSchema(
"order_id INT",
"customer_id INT",
"total DOUBLE",
"order_time STRING",
"proc_time AS PROCTIME()")
.producedBeforeRestore(
Row.of(1, 3, 44.44, "2020-10-10 00:00:01"),
Row.of(2, 5, 100.02, "2020-10-10 00:00:02"),
Row.of(4, 2, 92.61, "2020-10-10 00:00:04"),
Row.of(3, 1, 23.89, "2020-10-10 00:00:03"),
Row.of(6, 4, 7.65, "2020-10-10 00:00:06"),
Row.of(5, 2, 12.78, "2020-10-10 00:00:05"))
.addSchema(ORDERS_SCHEMA)
.producedBeforeRestore(ORDERS_BEFORE_DATA)
.producedAfterRestore(
Row.of(7, 6, 17.58, "2020-10-10 00:00:07"), // new customer
Row.of(9, 1, 143.21, "2020-10-10 00:00:08") // updated zip code
Expand All @@ -110,20 +118,9 @@ public class LookupJoinTestPrograms {
SourceTestStep.newBuilder("orders_cdc_t")
.addOption("filterable-fields", "customer_id")
.addOption("changelog-mode", "I,UA,UB,D")
.addSchema(
"order_id INT",
"customer_id INT",
"total DOUBLE",
"order_time STRING",
"proc_time AS PROCTIME()")
.addSchema(ORDERS_SCHEMA)
.addSchema("PRIMARY KEY (order_id) NOT ENFORCED")
.producedBeforeRestore(
Row.of(1, 3, 44.44, "2020-10-10 00:00:01"),
Row.of(2, 5, 100.02, "2020-10-10 00:00:02"),
Row.of(4, 2, 92.61, "2020-10-10 00:00:04"),
Row.of(3, 1, 23.89, "2020-10-10 00:00:03"),
Row.of(6, 4, 7.65, "2020-10-10 00:00:06"),
Row.of(5, 2, 12.78, "2020-10-10 00:00:05"))
.producedBeforeRestore(ORDERS_BEFORE_DATA)
.producedAfterRestore(
Row.ofKind(RowKind.DELETE, 3, 1, 23.89, "2020-10-10 00:00:03"),
Row.ofKind(RowKind.INSERT, 3, 1, 33.01, "2020-10-10 01:01:06"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.flink.table.planner.plan.nodes.exec.stream;

import org.apache.flink.table.planner.plan.nodes.exec.common.LookupJoinSemanticTestPrograms;
import org.apache.flink.table.planner.plan.nodes.exec.testutils.SemanticTestBase;
import org.apache.flink.table.test.program.TableTestProgram;

import java.util.List;

/** Semantic tests for {@link StreamExecLookupJoin}. */
public class LookupJoinSemanticTests extends SemanticTestBase {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

there is JoinSemanticTests
not sure if we need a dedicated class for LookupJoin

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

JoinSemanticTests is for StreamExecJoin only. Its eight programs are all plain stream-stream joins, there is no SYSTEM_TIME or LOOKUP in any of them. Lateral snapshot join and multi join are also joins and both got their own class rather than going in there, so I kept this one.

You did make me look at where the programs live though. They were in LookupJoinTestPrograms, which is the restore-test file, and the two sources I added only exist because a semantic test cannot take restore data. Moved them to LookupJoinSemanticTestPrograms, same split FLINK-38720 did for joins.


@Override
public List<TableTestProgram> programs() {
return List.of(
LookupJoinSemanticTestPrograms.LOOKUP_JOIN_UNION_DIFFERENT_FILTERS,
LookupJoinSemanticTestPrograms.LOOKUP_JOIN_UNION_DIFFERENT_FILTERS_REVERSED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ on t1.a = t2.a and ndFunc(t2.b) > 100]]>
<![CDATA[
Sink(table=[default_catalog.default_database.sink_without_pk], fields=[a, b, c])
+- Calc(select=[a, b, c])
+- LookupJoin(table=[default_catalog.default_database.dim_with_pk], joinType=[InnerJoin], lookup=[a=a], where=[>(ndFunc(b), 100)], select=[a, b, c, a], upsertKey=[[0]])
+- LookupJoin(table=[default_catalog.default_database.dim_with_pk, project=[a, b]], joinType=[InnerJoin], lookup=[a=a], where=[>(ndFunc(b), 100)], select=[a, b, c, a], upsertKey=[[0]])
+- TableSourceScan(table=[[default_catalog, default_database, cdc, project=[a, b, c], metadata=[]]], fields=[a, b, c])

advice[1]: [WARNING] There exists non deterministic function: 'ndFunc' in condition: '>(ndFunc($1), 100)' which may cause wrong result in update pipeline.
related rel plan:
LookupJoin(table=[default_catalog.default_database.dim_with_pk], joinType=[InnerJoin], lookup=[a=a], where=[>(ndFunc(b), 100)], select=[a, b, c, a], upsertKey=[[0]], changelogMode=[I,UB,UA,D])
LookupJoin(table=[default_catalog.default_database.dim_with_pk, project=[a, b]], joinType=[InnerJoin], lookup=[a=a], where=[>(ndFunc(b), 100)], select=[a, b, c, a], upsertKey=[[0]], changelogMode=[I,UB,UA,D])
+- TableSourceScan(table=[[default_catalog, default_database, cdc, project=[a, b, c], metadata=[]]], fields=[a, b, c], changelogMode=[I,UB,UA,D], upsertKeys=[[a]])


Expand All @@ -54,7 +54,7 @@ LookupJoin(table=[default_catalog.default_database.dim_with_pk], joinType=[Inner
<![CDATA[
Sink(table=[default_catalog.default_database.sink_without_pk], fields=[a, b, c])
+- Calc(select=[a, b, c])
+- LookupJoin(table=[default_catalog.default_database.dim_with_pk], joinType=[InnerJoin], lookup=[a=a], select=[a, c, a0, b], upsertKey=[[0]])
+- LookupJoin(table=[default_catalog.default_database.dim_with_pk, project=[a, b]], joinType=[InnerJoin], lookup=[a=a], select=[a, c, a0, b], upsertKey=[[0]])
+- TableSourceScan(table=[[default_catalog, default_database, cdc, project=[a, c], metadata=[]]], fields=[a, c])

advice[1]: [WARNING] You might want to enable upsert materialization for look up join operator by configuring ('table.optimizer.non-deterministic-update.strategy' to 'TRY_RESOLVE') to resolve the correctness issue caused by 'Non-Deterministic Updates' (NDU) in a changelog pipeline.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ LogicalProject(f2=[$1], f3=[$5])
<Resource name="optimized exec plan">
<![CDATA[
Calc(select=[f2, f3])
+- LookupJoin(table=[TimeTravelCatalog.default.t1], joinType=[LeftOuterJoin], lookup=[f1=f1], select=[f1, f2, f10, f3])
+- LookupJoin(table=[TimeTravelCatalog.default.t1, project=[f1, f3]], joinType=[LeftOuterJoin], lookup=[f1=f1], select=[f1, f2, f10, f3])
+- TableSourceScan(table=[[TimeTravelCatalog, default, t1]], fields=[f1, f2], version=[1672538400000])
]]>
</Resource>
Expand Down
Loading