gustavodemorais commented on code in PR #29570:
URL: https://github.com/apache/flink/pull/29570#discussion_r4379179508
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/AbstractStreamingJoinOperator.java:
##########
@@ -102,6 +132,102 @@ public void close() throws Exception {
}
}
+ /**
+ * Returns the stored record that the given record replaces, or null if
there is none. An upsert
+ * input updates a record by sending only the new version, without
retracting the old one.
+ *
+ * <p>The lookup only happens if it can change the result.
+ */
+ protected @Nullable RowData replacedRecord(
+ RowData record, JoinRecordStateView stateView, boolean isLeft,
boolean otherIsOuter)
+ throws Exception {
+ final boolean inputIsUpsertOrUnknown =
+ isLeft ? leftIsUpsertOrUnknown : rightIsUpsertOrUnknown;
+ // with at most one record per join key, the count of the other side
is right without it
+ final boolean mayCountTwice = otherIsOuter &&
!joinKeyContainsUniqueKey(isLeft);
+ final boolean mayMatchDifferently = hasNonEquiCondition;
+ if (!inputIsUpsertOrUnknown || !(mayCountTwice ||
mayMatchDifferently)) {
+ return null;
+ }
+ return stateView.getRecord(record);
+ }
+
+ /**
+ * Returns the full record to retract.
+ *
+ * <p>A DELETE may contain only the unique key, e.g. from an upsert
source, but a non-equi
+ * condition needs all fields. In that case, the stored record with the
same unique key is
+ * returned, or the given retraction if nothing is stored.
+ */
+ protected RowData retractedRecord(
+ RowData retraction,
+ RowKind retractionKind,
+ JoinRecordStateView stateView,
+ boolean isLeft)
+ throws Exception {
+ final boolean inputHasKeyOnlyDeletesOrUnknown =
+ isLeft ? leftHasKeyOnlyDeletesOrUnknown :
rightHasKeyOnlyDeletesOrUnknown;
+ final boolean mayBeKeyOnlyDelete =
+ retractionKind == RowKind.DELETE
+ && inputHasKeyOnlyDeletesOrUnknown
+ && hasUniqueKey(isLeft);
+ if (!mayBeKeyOnlyDelete || !hasNonEquiCondition) {
+ return retraction;
+ }
+ final RowData storedRecord = stateView.getRecord(retraction);
+ return storedRecord != null ? storedRecord : retraction;
Review Comment:
When nothing is stored, a key-only DELETE falls back to the retraction with
NULL non-key fields. With a NULL-tolerant condition like `r.k = l.k AND (r.v IS
NULL OR r.v = l.v)` and a right PK `(k, id)`, a key-only `-D(k1, r9)` that was
never stored matches every left row of `k1`, so the semi join sends -D (anti:
+I) although the real match `(k1, r1, A)` is still in state. Could we return
null here and skip the other side in that case?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/StreamingSemiAntiJoinOperator.java:
##########
@@ -177,52 +209,81 @@ public void processElement2(StreamRecord<RowData>
element) throws Exception {
RowKind inputRowKind = input.getRowKind();
input.setRowKind(RowKind.INSERT); // erase RowKind for later state
updating
- Iterator<OuterRecord> associatedRecords =
- AbstractStreamingJoinOperator.iterator(
- input, false, leftRecordStateView, joinCondition);
if (isAccumulateMsg) { // record is accumulate
+ final RowData replacedRecord = replacedRecord(input,
rightRecordStateView, false, true);
+ if (replacedRecord != null && hasNonEquiCondition) {
+ removeLostMatches(input, replacedRecord);
+ }
// do not store a record that can never match
if (!joinCondition.currentKeyCannotMatch()) {
rightRecordStateView.addRecord(input);
}
+ Iterator<OuterRecord> associatedRecords =
+ AbstractStreamingJoinOperator.iterator(
+ input, false, leftRecordStateView, joinCondition);
while (associatedRecords.hasNext()) {
OuterRecord outerRecord = associatedRecords.next();
RowData other = outerRecord.record;
if (outerRecord.numOfAssociations == 0) {
- if (isAntiJoin) {
- // send -D[other]
- other.setRowKind(RowKind.DELETE);
- } else {
- // send +I/+U[other] (using input RowKind)
- other.setRowKind(inputRowKind);
- }
- collector.collect(other);
- // set header back to INSERT, because we will update the
other row to state
- other.setRowKind(RowKind.INSERT);
+ // send -D[other] if anti join, +I/+U[other] (using input
RowKind) if semi join
+ collect(other, isAntiJoin ? RowKind.DELETE : inputRowKind);
} // ignore when number > 0
- leftRecordStateView.updateNumOfAssociations(
- other, outerRecord.numOfAssociations + 1);
+ if (isNewMatch(outerRecord, replacedRecord, false)) {
+ leftRecordStateView.updateNumOfAssociations(
+ other, outerRecord.numOfAssociations + 1);
+ }
}
} else { // retract input
- rightRecordStateView.retractRecord(input);
+ final RowData retractedRecord =
+ retractedRecord(input, inputRowKind, rightRecordStateView,
false);
+ rightRecordStateView.retractRecord(retractedRecord);
+ Iterator<OuterRecord> associatedRecords =
+ AbstractStreamingJoinOperator.iterator(
+ retractedRecord, false, leftRecordStateView,
joinCondition);
while (associatedRecords.hasNext()) {
- OuterRecord outerRecord = associatedRecords.next();
- RowData other = outerRecord.record;
- if (outerRecord.numOfAssociations == 1) {
- if (!isAntiJoin) {
- // send -D/-U[other] (using input RowKind)
- other.setRowKind(inputRowKind);
- } else {
- // send +I[other]
- other.setRowKind(RowKind.INSERT);
- }
- collector.collect(other);
- // set RowKind back, because we will update the other row
to state
- other.setRowKind(RowKind.INSERT);
- } // ignore when number > 0
- leftRecordStateView.updateNumOfAssociations(
- other, outerRecord.numOfAssociations - 1);
+ removeMatch(associatedRecords.next(), inputRowKind);
}
}
}
+
+ /** Removes the matches of the left records that only the replaced right
record matched. */
+ private void removeLostMatches(RowData record, RowData replacedRecord)
throws Exception {
+ final Iterator<OuterRecord> matchesOfReplacedRecord =
+ AbstractStreamingJoinOperator.iterator(
+ replacedRecord, false, leftRecordStateView,
joinCondition);
+ while (matchesOfReplacedRecord.hasNext()) {
+ final OuterRecord other = matchesOfReplacedRecord.next();
+ if (!matches(record, other.record, false)) {
+ removeMatch(other, RowKind.DELETE);
+ }
+ }
+ }
+
+ /**
+ * Removes a match of the left record. If it was the last one, a semi join
retracts the record
+ * with the given RowKind and an anti join sends it.
+ */
+ private void removeMatch(OuterRecord other, RowKind retractionKind) throws
Exception {
+ if (other.numOfAssociations == 1) {
+ // send -D/-U[other] if semi join, +I[other] if anti join
+ collect(other.record, isAntiJoin ? RowKind.INSERT :
retractionKind);
+ } // ignore when number > 1
+ leftRecordStateView.updateNumOfAssociations(other.record,
other.numOfAssociations - 1);
Review Comment:
A retraction can only remove a match that was counted, so could we return
early when `other.numOfAssociations <= 0` instead of writing a negative count?
(not sure)
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/StreamingSemiAntiJoinOperator.java:
##########
@@ -177,52 +209,81 @@ public void processElement2(StreamRecord<RowData>
element) throws Exception {
RowKind inputRowKind = input.getRowKind();
input.setRowKind(RowKind.INSERT); // erase RowKind for later state
updating
- Iterator<OuterRecord> associatedRecords =
- AbstractStreamingJoinOperator.iterator(
- input, false, leftRecordStateView, joinCondition);
if (isAccumulateMsg) { // record is accumulate
+ final RowData replacedRecord = replacedRecord(input,
rightRecordStateView, false, true);
+ if (replacedRecord != null && hasNonEquiCondition) {
+ removeLostMatches(input, replacedRecord);
+ }
// do not store a record that can never match
if (!joinCondition.currentKeyCannotMatch()) {
rightRecordStateView.addRecord(input);
}
+ Iterator<OuterRecord> associatedRecords =
+ AbstractStreamingJoinOperator.iterator(
+ input, false, leftRecordStateView, joinCondition);
while (associatedRecords.hasNext()) {
OuterRecord outerRecord = associatedRecords.next();
RowData other = outerRecord.record;
if (outerRecord.numOfAssociations == 0) {
- if (isAntiJoin) {
- // send -D[other]
- other.setRowKind(RowKind.DELETE);
- } else {
- // send +I/+U[other] (using input RowKind)
- other.setRowKind(inputRowKind);
- }
- collector.collect(other);
- // set header back to INSERT, because we will update the
other row to state
- other.setRowKind(RowKind.INSERT);
+ // send -D[other] if anti join, +I/+U[other] (using input
RowKind) if semi join
+ collect(other, isAntiJoin ? RowKind.DELETE : inputRowKind);
} // ignore when number > 0
- leftRecordStateView.updateNumOfAssociations(
- other, outerRecord.numOfAssociations + 1);
+ if (isNewMatch(outerRecord, replacedRecord, false)) {
+ leftRecordStateView.updateNumOfAssociations(
+ other, outerRecord.numOfAssociations + 1);
+ }
}
} else { // retract input
- rightRecordStateView.retractRecord(input);
+ final RowData retractedRecord =
+ retractedRecord(input, inputRowKind, rightRecordStateView,
false);
+ rightRecordStateView.retractRecord(retractedRecord);
+ Iterator<OuterRecord> associatedRecords =
+ AbstractStreamingJoinOperator.iterator(
+ retractedRecord, false, leftRecordStateView,
joinCondition);
while (associatedRecords.hasNext()) {
- OuterRecord outerRecord = associatedRecords.next();
- RowData other = outerRecord.record;
- if (outerRecord.numOfAssociations == 1) {
- if (!isAntiJoin) {
- // send -D/-U[other] (using input RowKind)
- other.setRowKind(inputRowKind);
- } else {
- // send +I[other]
- other.setRowKind(RowKind.INSERT);
- }
- collector.collect(other);
- // set RowKind back, because we will update the other row
to state
- other.setRowKind(RowKind.INSERT);
- } // ignore when number > 0
- leftRecordStateView.updateNumOfAssociations(
- other, outerRecord.numOfAssociations - 1);
+ removeMatch(associatedRecords.next(), inputRowKind);
}
}
}
+
+ /** Removes the matches of the left records that only the replaced right
record matched. */
+ private void removeLostMatches(RowData record, RowData replacedRecord)
throws Exception {
Review Comment:
nit: with a non-equi condition a right upsert now iterates all left rows of
the key twice, once here and once in the accumulate loop. Could we do it in one
pass, evaluating both `matches(replaced, l)` and `matches(input, l)` per left
row?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/AbstractStreamingJoinOperator.java:
##########
@@ -55,6 +60,21 @@ public abstract class AbstractStreamingJoinOperator extends
AbstractStreamOperat
protected final JoinInputSideSpec leftInputSideSpec;
protected final JoinInputSideSpec rightInputSideSpec;
+ // changelog mode of the left input, null for compiled plans before Flink
2.4
+ @Nullable protected final RuntimeChangelogMode leftInputChangelogMode;
Review Comment:
nit: `leftInputChangelogMode`/`rightInputChangelogMode` are never read, only
the booleans derived from them. Could we drop the fields?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/StreamingSemiAntiJoinOperator.java:
##########
@@ -177,52 +209,81 @@ public void processElement2(StreamRecord<RowData>
element) throws Exception {
RowKind inputRowKind = input.getRowKind();
input.setRowKind(RowKind.INSERT); // erase RowKind for later state
updating
- Iterator<OuterRecord> associatedRecords =
- AbstractStreamingJoinOperator.iterator(
- input, false, leftRecordStateView, joinCondition);
if (isAccumulateMsg) { // record is accumulate
+ final RowData replacedRecord = replacedRecord(input,
rightRecordStateView, false, true);
+ if (replacedRecord != null && hasNonEquiCondition) {
+ removeLostMatches(input, replacedRecord);
+ }
// do not store a record that can never match
if (!joinCondition.currentKeyCannotMatch()) {
rightRecordStateView.addRecord(input);
}
+ Iterator<OuterRecord> associatedRecords =
+ AbstractStreamingJoinOperator.iterator(
+ input, false, leftRecordStateView, joinCondition);
while (associatedRecords.hasNext()) {
OuterRecord outerRecord = associatedRecords.next();
RowData other = outerRecord.record;
if (outerRecord.numOfAssociations == 0) {
- if (isAntiJoin) {
- // send -D[other]
- other.setRowKind(RowKind.DELETE);
- } else {
- // send +I/+U[other] (using input RowKind)
- other.setRowKind(inputRowKind);
- }
- collector.collect(other);
- // set header back to INSERT, because we will update the
other row to state
- other.setRowKind(RowKind.INSERT);
+ // send -D[other] if anti join, +I/+U[other] (using input
RowKind) if semi join
+ collect(other, isAntiJoin ? RowKind.DELETE : inputRowKind);
} // ignore when number > 0
- leftRecordStateView.updateNumOfAssociations(
- other, outerRecord.numOfAssociations + 1);
+ if (isNewMatch(outerRecord, replacedRecord, false)) {
+ leftRecordStateView.updateNumOfAssociations(
+ other, outerRecord.numOfAssociations + 1);
+ }
}
} else { // retract input
- rightRecordStateView.retractRecord(input);
+ final RowData retractedRecord =
+ retractedRecord(input, inputRowKind, rightRecordStateView,
false);
Review Comment:
A DELETE for a right row that is not in state still decrements the left
count to -1, and since `isNewMatch` never increments a non-zero count when the
join key contains the right unique key, the left row never comes back. With
`lines` PK `order_id` (upsert, key-only deletes) and `SELECT * FROM orders o
WHERE EXISTS (SELECT 1 FROM lines l WHERE l.order_id = o.order_id)`:
```
L +I(Ord#1, LineOrd#1, ADDR)
R -D(LineOrd#1) -- never inserted
R +I(LineOrd#1, AIR) -- expected +I[Ord#1], nothing emitted
R +U(LineOrd#1, SHIP)
R -D(LineOrd#1)
R +I(LineOrd#1, AIR) -- still nothing
```
The anti join is stuck the same way. Before the change the next upsert
brought the count back. Could we skip the retraction when the right side has a
unique key and nothing is stored for it, and add a harness test with a DELETE
before the first INSERT for semi and anti?
##########
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/StreamingSemiAntiJoinOperator.java:
##########
@@ -109,29 +122,37 @@ public void open() throws Exception {
@Override
public void processElement1(StreamRecord<RowData> element) throws
Exception {
RowData input = element.getValue();
+ boolean isAccumulateMsg = RowDataUtil.isAccumulateMsg(input);
+ RowKind inputRowKind = input.getRowKind();
+ input.setRowKind(RowKind.INSERT); // erase RowKind for later state
updating
+
+ final RowData record =
+ isAccumulateMsg
+ ? input
+ : retractedRecord(input, inputRowKind,
leftRecordStateView, true);
Iterator<OuterRecord> associatedRecords =
AbstractStreamingJoinOperator.iterator(
- input, true, rightRecordStateView, joinCondition);
- if (!associatedRecords.hasNext()) {
- if (isAntiJoin) {
- collector.collect(input);
- }
- } else { // there are matched rows on the other side
- if (!isAntiJoin) {
- collector.collect(input);
+ record, true, rightRecordStateView, joinCondition);
+ final boolean recordIsSent = isAntiJoin != associatedRecords.hasNext();
+ // count before the state of the other side is read again, which may
reuse the iterable
+ final int numOfAssociations = isAccumulateMsg ?
Iterators.size(associatedRecords) : 0;
+ if (isAccumulateMsg && !recordIsSent) {
+ final RowData replacedRecord = replacedRecord(input,
leftRecordStateView, true, false);
+ if (replacedRecord != null && isSent(replacedRecord)) {
Review Comment:
Suggestion: the left state already stores the replaced record's
`numOfAssociations`, so could we decide whether it was sent from that count
(`isAntiJoin ? count == 0 : count > 0`) and drop `isSent`? It saves a second
scan of the right state, and it also retracts correctly when the right state
changed without retractions (TTL, or counts restored from before the fix).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]