-
Notifications
You must be signed in to change notification settings - Fork 3k
Flink: Apply row level filtering #7109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
flink/v1.16/flink/src/main/java/org/apache/iceberg/flink/FlinkSourceFilter.java
Show resolved
Hide resolved
flink/v1.16/flink/src/main/java/org/apache/iceberg/flink/source/FlinkSource.java
Outdated
Show resolved
Hide resolved
doki23
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, except for a potential NPE problem.
doki23
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great!
|
|
||
| public class FlinkSourceFilter implements FilterFunction<RowData> { | ||
|
|
||
| private final RowDataWrapper wrapper; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't have to make RowDataWrapper serializable. it can be lazily initialized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lazily initialized is ok, but it makes sense that making it serializable -- it's meaningful that we don't need to check the if statement per record.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm open to both. It is just one null check.
| } | ||
| return env.createInput(format, typeInfo).setParallelism(parallelism); | ||
|
|
||
| DataStreamSource<RowData> source = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This covers one scenario. there are two other scenarios.
- Use
FlinkInputFormatdirectly. e.g.StreamingReaderOperator.
private void processSplits() throws IOException {
FlinkInputSplit split = splits.poll();
if (split == null) {
currentSplitState = SplitState.IDLE;
return;
}
format.open(split);
try {
RowData nextElement = null;
while (!format.reachedEnd()) {
nextElement = format.nextRecord(nextElement);
sourceContext.collect(nextElement);
}
} finally {
currentSplitState = SplitState.IDLE;
format.close();
}
// Re-schedule to process the next split.
enqueueProcessSplits();
}
- new Flink FLIP-27
IcebergSource. Here is an example fromIcebergTableSourcethat shows how users can construct the DataStream. We can fix it inIcebergTableSource. but we can't control users' code to add the filter in theDataStream. Note that FLIP-27 source will be the future Flink source.
private DataStreamSource<RowData> createFLIP27Stream(StreamExecutionEnvironment env) {
SplitAssignerType assignerType =
readableConfig.get(FlinkConfigOptions.TABLE_EXEC_SPLIT_ASSIGNER_TYPE);
IcebergSource<RowData> source =
IcebergSource.forRowData()
.tableLoader(loader)
.assignerFactory(assignerType.factory())
.properties(properties)
.project(getProjectedSchema())
.limit(limit)
.filters(filters)
.flinkConfig(readableConfig)
.build();
DataStreamSource stream =
env.fromSource(
source,
WatermarkStrategy.noWatermarks(),
source.name(),
TypeInformation.of(RowData.class));
return stream;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another possible common place to evaluate the residual filter is probably RowDataFileScanTaskReader. This approach also brings a benefit that it doesn't change the shape of Flink DAG. E.g., users won't see an extra filter function/operator and wonder where does it come from.
982469e to
b876007
Compare
|
@stevenzwu Sorry for the long wait. I've updated the PR according to your suggestion. Looking at the FLIP27 tests, I think row filtering is already applied over there. I also took the liberty to change some public methods, since Flink 1.17 hasn't been released to the public. |
stevenzwu
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for making the change. It seems that performing the filtering at RowDataFileScanTaskReader is the right approach, as it is the common denominator.
flink/v1.17/flink/src/main/java/org/apache/iceberg/flink/FlinkSourceFilter.java
Outdated
Show resolved
Hide resolved
flink/v1.17/flink/src/main/java/org/apache/iceberg/flink/source/RowDataFileScanTaskReader.java
Outdated
Show resolved
Hide resolved
flink/v1.17/flink/src/test/java/org/apache/iceberg/flink/source/TestFlinkInputFormat.java
Outdated
Show resolved
Hide resolved
...k/v1.17/flink/src/test/java/org/apache/iceberg/flink/source/TestStreamingReaderOperator.java
Outdated
Show resolved
Hide resolved
...nk/src/main/java/org/apache/iceberg/flink/source/reader/AvroGenericRecordReaderFunction.java
Outdated
Show resolved
Hide resolved
| } | ||
|
|
||
| @Test | ||
| public void testBasicRowDataFiltering() throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this method won't be necessary if we add the new test method above to TestFlinkScan. Currently, TestFlinkScan#testFilterExp only covers filter with partition column. we can add the above test method as testResidualFilter where filter is constructed with non-partition column.
TestFlinkScan is the base class for both the old FlinkSource(covered by TestFlinkInputFormat and others) and the new FLIP-27 IcebergSource (covered by TestIcebergSourceBounded and others)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a test in TestFlinkScan that filters on a not-partitioned column
|
@stevenzwu thanks again for the review, could you do another pass? |
flink/v1.17/flink/src/test/java/org/apache/iceberg/flink/source/TestFlinkScan.java
Outdated
Show resolved
Hide resolved
flink/v1.17/flink/src/test/java/org/apache/iceberg/flink/source/TestFlinkInputFormat.java
Outdated
Show resolved
Hide resolved
flink/v1.17/flink/src/main/java/org/apache/iceberg/flink/source/RowDataFileScanTaskReader.java
Show resolved
Hide resolved
| DataFile dataFile = helper.writeFile(expectedRecords); | ||
| helper.appendToTable(dataFile); | ||
| List<Row> actual = | ||
| runWithFilter(Expressions.greaterThanOrEqual("data", "b"), "where data>='b'"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ideally, we also want to test the ignore case sensitive option with filter. but the current test structure seems very hard to do that. we need both filter and options
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. I got it working quite easily when using the expression. Do you know if Flink supports case-insensitive mode when it comes to Flink SQL? Seems to be an option issue: https://issues.apache.org/jira/browse/FLINK-16175
stevenzwu
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
added a comment for testing the case insensitive scenario. but seems difficult with the current test code structure.
| } | ||
|
|
||
| @Test | ||
| public void testFilterExpCaseInsensitive() throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: for now, maybe extract a private method to avoid test duplications. with Junit 5, this can be handled better.
|
Thanks @stevenzwu and @doki23 for the review! |
|
@Fokko thx for fixing this issue |
|
@Fokko can you also create the backport PR for 1.15 and 1.16? |
|
@stevenzwu sure thing! Here you go: #7397 |
* Flink: Apply row level filtering * Fix the tests * Add test for case-sensitive * Reduce duplication using a private method
| if (rowFilter != null) { | ||
| return CloseableIterable.filter(iter, rowFilter::filter); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Fokko @stevenzwu We have an internal request to add row filter dynamically, current impl requires the filter to be supplied at job startup time. After some investigation, I believe maybe we don't have to passing the filters all the way done to this class.. The task itself already has the row filter task.residual(). We could simply convert it to rowFilter here, such as:
// if (rowFilter != null) {
// return CloseableIterable.filter(iter, rowFilter::filter);
// }
if (task.residual() != null && !task.residual().isEquivalentTo(Expressions.alwaysTrue())) {
FlinkSourceFilter dataFilter =
new FlinkSourceFilter(this.projectedSchema, task.residual(), this.caseSensitive);
return CloseableIterable.filter(iter, dataFilter::filter);
}
WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe maybe we don't have to passing the filters all the way done to this class.. The task itself already has the row filter task.residual().
if that is the case, it is probably simpler.
add row filter dynamically,
can you explain what's the dynamic part and how is it related to the residual filter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you explain what's the dynamic part and how is it related to the residual filter?
The feature we are developing needs to add some additional scan filters at planIcebergSourceSplits phase, the additional scan filter is added to scanContext, which would be part of task.residual.
The additional filter itself is dynamically added.
if that is the case, it is probably simpler.
Then, if we can just use task.residual, do you think we should refactor this PR to use that? Is it possible to revert the class interface change in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@advancedxy That sounds like a great approach. If we only have to apply the residual, then we're also more efficient. That would be an improvement over the current approach.
For Flink, we apply partition pruning, filtering based on metrics, and row-group skipping, but no row-level filtering.
See the issue for more details
Resolves #7022
@stevenzwu would you have time to take a peek at this one?