Skip to content

Commit fd3922d

Browse files
author
hongli.wwj
committed
[core] Apply LIMIT during merge-file split read when safe.
1 parent 94b468a commit fd3922d

8 files changed

Lines changed: 722 additions & 49 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.paimon.reader;
20+
21+
import javax.annotation.Nullable;
22+
23+
import java.io.IOException;
24+
import java.util.concurrent.atomic.AtomicLong;
25+
26+
import static org.apache.paimon.utils.Preconditions.checkArgument;
27+
28+
/** A {@link RecordReader} that stops after reading a given number of records. */
29+
public final class LimitRecordReader<T> implements RecordReader<T> {
30+
31+
private final RecordReader<T> reader;
32+
private final long limit;
33+
private final AtomicLong recordCount = new AtomicLong(0);
34+
35+
public LimitRecordReader(RecordReader<T> reader, long limit) {
36+
checkArgument(limit > 0, "Limit must be positive.");
37+
this.reader = reader;
38+
this.limit = limit;
39+
}
40+
41+
public static <T> RecordReader<T> limit(RecordReader<T> reader, @Nullable Integer limit) {
42+
if (limit == null || limit <= 0) {
43+
return reader;
44+
}
45+
return new LimitRecordReader<>(reader, limit);
46+
}
47+
48+
@Override
49+
@Nullable
50+
public RecordIterator<T> readBatch() throws IOException {
51+
if (recordCount.get() >= limit) {
52+
return null;
53+
}
54+
RecordIterator<T> iterator = reader.readBatch();
55+
if (iterator == null) {
56+
return null;
57+
}
58+
return new LimitRecordIterator<>(iterator);
59+
}
60+
61+
@Override
62+
public void close() throws IOException {
63+
reader.close();
64+
}
65+
66+
private class LimitRecordIterator<T> implements RecordIterator<T> {
67+
68+
private final RecordIterator<T> iterator;
69+
70+
private LimitRecordIterator(RecordIterator<T> iterator) {
71+
this.iterator = iterator;
72+
}
73+
74+
@Override
75+
@Nullable
76+
public T next() throws IOException {
77+
if (recordCount.get() >= limit) {
78+
return null;
79+
}
80+
T next = iterator.next();
81+
if (next != null) {
82+
recordCount.incrementAndGet();
83+
}
84+
return next;
85+
}
86+
87+
@Override
88+
public void releaseBatch() {
89+
iterator.releaseBatch();
90+
}
91+
}
92+
}

paimon-core/src/main/java/org/apache/paimon/operation/MergeFileSplitRead.java

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package org.apache.paimon.operation;
2020

2121
import org.apache.paimon.CoreOptions;
22+
import org.apache.paimon.CoreOptions.MergeEngine;
2223
import org.apache.paimon.KeyValue;
2324
import org.apache.paimon.KeyValueFileStore;
2425
import org.apache.paimon.data.BinaryRow;
@@ -41,6 +42,7 @@
4142
import org.apache.paimon.mergetree.compact.MergeFunctionWrapper;
4243
import org.apache.paimon.mergetree.compact.ReducerMergeFunctionWrapper;
4344
import org.apache.paimon.predicate.Predicate;
45+
import org.apache.paimon.reader.LimitRecordReader;
4446
import org.apache.paimon.reader.ReaderSupplier;
4547
import org.apache.paimon.reader.RecordReader;
4648
import org.apache.paimon.schema.TableSchema;
@@ -54,6 +56,9 @@
5456
import org.apache.paimon.utils.ProjectedRow;
5557
import org.apache.paimon.utils.UserDefinedSeqComparator;
5658

59+
import org.slf4j.Logger;
60+
import org.slf4j.LoggerFactory;
61+
5762
import javax.annotation.Nullable;
5863

5964
import java.io.IOException;
@@ -74,6 +79,8 @@
7479
*/
7580
public class MergeFileSplitRead implements SplitRead<KeyValue> {
7681

82+
private static final Logger LOG = LoggerFactory.getLogger(MergeFileSplitRead.class);
83+
7784
private final TableSchema tableSchema;
7885
private final FileIO fileIO;
7986
private final KeyValueFileReaderFactory.Builder readerFactoryBuilder;
@@ -82,14 +89,19 @@ public class MergeFileSplitRead implements SplitRead<KeyValue> {
8289
private final MergeSorter mergeSorter;
8390
private final List<String> sequenceFields;
8491
private final boolean sequenceOrder;
92+
private final CoreOptions coreOptions;
8593

8694
@Nullable private RowType readKeyType;
8795
@Nullable private RowType outerReadType;
8896

8997
@Nullable private List<Predicate> filtersForKeys;
9098
@Nullable private List<Predicate> filtersForAll;
9199

100+
@Nullable private Integer limit;
101+
92102
private boolean forceKeepDelete = false;
103+
private boolean mergeReadLimitLogEmitted = false;
104+
private boolean applyMergeReadLimit = true;
93105

94106
public MergeFileSplitRead(
95107
CoreOptions options,
@@ -100,6 +112,7 @@ public MergeFileSplitRead(
100112
MergeFunctionFactory<KeyValue> mfFactory,
101113
KeyValueFileReaderFactory.Builder readerFactoryBuilder) {
102114
this.tableSchema = schema;
115+
this.coreOptions = options;
103116
this.readerFactoryBuilder = readerFactoryBuilder;
104117
this.fileIO = readerFactoryBuilder.fileIO();
105118
this.keyComparator = keyComparator;
@@ -177,6 +190,26 @@ public MergeFileSplitRead forceKeepDelete() {
177190
return this;
178191
}
179192

193+
@Override
194+
public MergeFileSplitRead withLimit(@Nullable Integer limit) {
195+
this.limit = limit;
196+
return this;
197+
}
198+
199+
@Override
200+
public MergeFileSplitRead withApplyMergeReadLimit(boolean applyMergeReadLimit) {
201+
this.applyMergeReadLimit = applyMergeReadLimit;
202+
return this;
203+
}
204+
205+
@Override
206+
public boolean isMergeReadLimitActive() {
207+
if (!applyMergeReadLimit || limit == null || limit <= 0) {
208+
return false;
209+
}
210+
return mergeReadLimitDisabledReason() == null;
211+
}
212+
180213
@Override
181214
public MergeFileSplitRead withFilter(Predicate predicate) {
182215
if (predicate == null) {
@@ -312,6 +345,7 @@ public RecordReader<KeyValue> createMergeReader(
312345
reader = new DropDeleteReader(reader);
313346
}
314347

348+
reader = LimitRecordReader.limit(reader, effectiveReadLimit());
315349
return projectOuter(projectKey(reader));
316350
}
317351

@@ -334,7 +368,92 @@ public RecordReader<KeyValue> createNoMergeReader(
334368
suppliers.add(() -> readerFactory.createRecordReader(file));
335369
}
336370

337-
return projectOuter(ConcatRecordReader.create(suppliers));
371+
return LimitRecordReader.limit(
372+
projectOuter(ConcatRecordReader.create(suppliers)), effectiveReadLimit());
373+
}
374+
375+
/**
376+
* Limit on merge read is only safe when filters are fully applied before truncation. This
377+
* aligns with {@link KeyValueFileStoreScan#limitPushdownEnabled()} and additionally rejects
378+
* non-primary-key filters, which are not pushed down to overlapping L0 sections (see {@link
379+
* #withFilter(Predicate)}).
380+
*/
381+
@Nullable
382+
private Integer effectiveReadLimit() {
383+
if (!applyMergeReadLimit) {
384+
return null;
385+
}
386+
if (limit == null || limit <= 0) {
387+
return null;
388+
}
389+
390+
String disabledReason = mergeReadLimitDisabledReason();
391+
if (disabledReason != null) {
392+
logMergeReadLimitOnce(disabledReason);
393+
return null;
394+
}
395+
396+
logMergeReadLimitOnce(null);
397+
return limit;
398+
}
399+
400+
/** Returns why merge read limit is unsafe, or {@code null} if limit can be applied. */
401+
@Nullable
402+
private String mergeReadLimitDisabledReason() {
403+
if (forceKeepDelete) {
404+
return "forceKeepDelete is enabled";
405+
}
406+
407+
MergeEngine mergeEngine = coreOptions.mergeEngine();
408+
if (mergeEngine == MergeEngine.PARTIAL_UPDATE) {
409+
return "merge-engine is partial-update";
410+
}
411+
if (mergeEngine == MergeEngine.AGGREGATE) {
412+
return "merge-engine is aggregation";
413+
}
414+
415+
if (coreOptions.deletionVectorsEnabled()) {
416+
return "deletion-vectors is enabled";
417+
}
418+
419+
if (hasNonPrimaryKeyFilter()) {
420+
return "non-primary-key filter is present";
421+
}
422+
423+
return null;
424+
}
425+
426+
private void logMergeReadLimitOnce(@Nullable String disabledReason) {
427+
if (mergeReadLimitLogEmitted) {
428+
return;
429+
}
430+
mergeReadLimitLogEmitted = true;
431+
if (disabledReason != null) {
432+
LOG.info(
433+
"Merge read limit {} is disabled: {}. Limit will not be applied during merge read.",
434+
limit,
435+
disabledReason);
436+
} else {
437+
LOG.info("Applying merge read limit {} during merge read.", limit);
438+
}
439+
}
440+
441+
private boolean hasNonPrimaryKeyFilter() {
442+
if (filtersForAll == null || filtersForAll.isEmpty()) {
443+
return false;
444+
}
445+
446+
List<String> primaryKeys = tableSchema.trimmedPrimaryKeys();
447+
Set<String> nonPrimaryKeys =
448+
tableSchema.fieldNames().stream()
449+
.filter(name -> !primaryKeys.contains(name))
450+
.collect(Collectors.toSet());
451+
for (Predicate filter : filtersForAll) {
452+
if (containsFields(filter, nonPrimaryKeys)) {
453+
return true;
454+
}
455+
}
456+
return false;
338457
}
339458

340459
/**

paimon-core/src/main/java/org/apache/paimon/operation/SplitRead.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ default SplitRead<T> withLimit(@Nullable Integer limit) {
5353
return this;
5454
}
5555

56+
/** Whether merge-read limit optimization may be applied per split. */
57+
default SplitRead<T> withApplyMergeReadLimit(boolean applyMergeReadLimit) {
58+
return this;
59+
}
60+
61+
/** Returns whether per-split merge-read limit is currently active for this reader. */
62+
default boolean isMergeReadLimitActive() {
63+
return false;
64+
}
65+
5666
/** Create a {@link RecordReader} from split. */
5767
RecordReader<T> createReader(Split split) throws IOException;
5868

@@ -83,6 +93,29 @@ public SplitRead<R> withFilter(@Nullable Predicate predicate) {
8393
return this;
8494
}
8595

96+
@Override
97+
public SplitRead<R> withTopN(@Nullable TopN topN) {
98+
read.withTopN(topN);
99+
return this;
100+
}
101+
102+
@Override
103+
public SplitRead<R> withLimit(@Nullable Integer limit) {
104+
read.withLimit(limit);
105+
return this;
106+
}
107+
108+
@Override
109+
public SplitRead<R> withApplyMergeReadLimit(boolean applyMergeReadLimit) {
110+
read.withApplyMergeReadLimit(applyMergeReadLimit);
111+
return this;
112+
}
113+
114+
@Override
115+
public boolean isMergeReadLimitActive() {
116+
return read.isMergeReadLimitActive();
117+
}
118+
86119
@Override
87120
public RecordReader<R> createReader(Split split) throws IOException {
88121
return splitConvert.apply(split);

0 commit comments

Comments
 (0)