From 665baa87204fdf5a5d3a1334c3d8e7264828c946 Mon Sep 17 00:00:00 2001 From: jianguotian <18464293+jianguotian@users.noreply.github.com> Date: Sun, 13 Sep 2026 23:33:44 +0800 Subject: [PATCH 1/3] [core] Support bucket-first sorting for manifest files --- docs/generated/core_configuration.html | 6 ++ .../java/org/apache/paimon/CoreOptions.java | 14 +++ .../operation/ManifestCompactDryRun.java | 3 +- .../paimon/operation/ManifestFileSorter.java | 94 +++++++++++++++++-- .../paimon/schema/SchemaValidation.java | 5 + .../paimon/manifest/ManifestFileMetaTest.java | 60 ++++++++++++ .../operation/ManifestEntryRunMergeTest.java | 74 +++++++++++++++ .../paimon/schema/SchemaValidationTest.java | 17 ++++ 8 files changed, 262 insertions(+), 11 deletions(-) diff --git a/docs/generated/core_configuration.html b/docs/generated/core_configuration.html index ac5730017523..fb313aef8dd9 100644 --- a/docs/generated/core_configuration.html +++ b/docs/generated/core_configuration.html @@ -1059,6 +1059,12 @@ Integer Level threshold of lookup to generate remote lookup files. Level files below this threshold will not generate remote lookup files. + +
manifest-sort.bucket-first
+ false + Boolean + Sort manifest entries by bucket before the configured partition field. This improves manifest pruning for bucket-key point lookups spanning many partitions, at the cost of wider partition ranges in each manifest. +
manifest-sort.enabled
false diff --git a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java index 7b0665c50296..83e330f81e0e 100644 --- a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java +++ b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java @@ -589,6 +589,16 @@ public InlineElement getDescription() { "Partition field name to sort manifest entries by. Validated by" + " schema validation, if not configured, defaults to the first partition field."); + public static final ConfigOption MANIFEST_SORT_BUCKET_FIRST = + key("manifest-sort.bucket-first") + .booleanType() + .defaultValue(false) + .withDescription( + "Sort manifest entries by bucket before the configured partition" + + " field. This improves manifest pruning for bucket-key point" + + " lookups spanning many partitions, at the cost of wider" + + " partition ranges in each manifest."); + public static final ConfigOption MANIFEST_SORT_MAX_REWRITE_SIZE = key("manifest-sort.max-rewrite-size") .memoryType() @@ -3223,6 +3233,10 @@ public String manifestSortPartitionField() { return options.get(MANIFEST_SORT_PARTITION_FIELD); } + public boolean manifestSortBucketFirst() { + return options.get(MANIFEST_SORT_BUCKET_FIRST); + } + public long manifestSortMaxRewriteSize() { return options.get(MANIFEST_SORT_MAX_REWRITE_SIZE).getBytes(); } diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestCompactDryRun.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestCompactDryRun.java index 4ab06f52bfdc..56a31a02ac5e 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestCompactDryRun.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestCompactDryRun.java @@ -119,7 +119,8 @@ private static List buildLevelSortedRunsForDryRun( options.dataEvolutionEnabled(), manifests, options.manifestSortPartitionField(), - partitionType); + partitionType, + options.manifestSortBucketFirst()); ManifestFileSorter.ClassifyResult classifyResult = ManifestFileSorter.classifyManifests( manifests, diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileSorter.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileSorter.java index 76c5b0ef5ca0..ba3856849e5d 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileSorter.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileSorter.java @@ -155,6 +155,7 @@ static List trySortCompaction( @Nullable IOManager ioManager) throws Exception { String sortPartitionField = options.manifestSortPartitionField(); + boolean sortBucketFirst = options.manifestSortBucketFirst(); boolean runMergeOptimizeEnabled = options.manifestMergeOptimizeEnabled(); long suggestedMetaSize = options.manifestTargetSize().getBytes(); int suggestedMinMetaCount = options.manifestMergeMinCount(); @@ -173,6 +174,7 @@ static List trySortCompaction( manifestFile, partitionType, sortPartitionField, + sortBucketFirst, options.dataEvolutionEnabled(), runMergeOptimizeEnabled, suggestedMetaSize, @@ -192,6 +194,7 @@ static List trySortCompaction( manifestFile, partitionType, sortPartitionField, + sortBucketFirst, options.dataEvolutionEnabled(), runMergeOptimizeEnabled, suggestedMetaSize, @@ -215,6 +218,7 @@ private static Optional> tryFullCompaction( ManifestFile manifestFile, RowType partitionType, String sortPartitionField, + boolean sortBucketFirst, boolean dataEvolutionEnabled, boolean runMergeOptimizeEnabled, long suggestedMetaSize, @@ -238,6 +242,7 @@ private static Optional> tryFullCompaction( manifestFile, partitionType, sortPartitionField, + sortBucketFirst, dataEvolutionEnabled, runMergeOptimizeEnabled, suggestedMetaSize, @@ -321,6 +326,7 @@ private static List tryMinorCompaction( ManifestFile manifestFile, RowType partitionType, String sortPartitionField, + boolean sortBucketFirst, boolean dataEvolutionEnabled, boolean runMergeOptimizeEnabled, long suggestedMetaSize, @@ -339,6 +345,7 @@ private static List tryMinorCompaction( manifestFile, partitionType, sortPartitionField, + sortBucketFirst, dataEvolutionEnabled, runMergeOptimizeEnabled, suggestedMetaSize, @@ -453,6 +460,7 @@ private static CompactionContext prepareCompaction( ManifestFile manifestFile, RowType partitionType, String sortPartitionField, + boolean sortBucketFirst, boolean dataEvolutionEnabled, boolean runMergeOptimizeEnabled, long suggestedMetaSize, @@ -464,7 +472,13 @@ private static CompactionContext prepareCompaction( boolean useRunMergeOptimize = rowIdSort && runMergeOptimizeEnabled; // Step 1: Resolve sort key. Data evolution tables prefer RowID ranges when available. - ManifestSortKey sortKey = createSortKey(rowIdSort, sortPartitionField, partitionType); + ManifestSortKey sortKey = + createSortKey( + dataEvolutionEnabled, + input, + sortPartitionField, + partitionType, + sortBucketFirst); // Step 2: Classify manifests into LSM files and collect delete entries. ClassifyResult classification = @@ -1183,14 +1197,42 @@ static ManifestSortKey createSortKey( List input, String sortPartitionField, RowType partitionType) { + return createSortKey(dataEvolutionEnabled, input, sortPartitionField, partitionType, false); + } + + static ManifestSortKey createSortKey( + boolean dataEvolutionEnabled, + List input, + String sortPartitionField, + RowType partitionType, + boolean sortBucketFirst) { + if (dataEvolutionEnabled && sortBucketFirst) { + throw new IllegalArgumentException( + String.format( + "'%s' is not supported when '%s' is enabled.", + CoreOptions.MANIFEST_SORT_BUCKET_FIRST.key(), + CoreOptions.DATA_EVOLUTION_ENABLED.key())); + } + return createSortKey( dataEvolutionEnabled && ManifestFileMeta.allContainsRowId(input), sortPartitionField, - partitionType); + partitionType, + sortBucketFirst, + sortBucketFirst + && input.stream() + .allMatch( + meta -> + meta.minBucket() != null + && meta.maxBucket() != null)); } private static ManifestSortKey createSortKey( - boolean rowIdSort, String sortPartitionField, RowType partitionType) { + boolean rowIdSort, + String sortPartitionField, + RowType partitionType, + boolean sortBucketFirst, + boolean compareManifestBuckets) { if (rowIdSort) { // RowID sorting uses the configured partition field as the primary key when specified, // otherwise it uses the full partition row to preserve partition locality. It then @@ -1219,7 +1261,12 @@ private static ManifestSortKey createSortKey( RecordComparator fieldComparator = CodeGenUtils.newRecordComparator( partitionType.getFieldTypes(), new int[] {sortFieldIndex}); - return new PartitionSortKey(fieldComparator, partitionType, sortFieldIndex); + return new PartitionSortKey( + fieldComparator, + partitionType, + sortFieldIndex, + sortBucketFirst, + compareManifestBuckets); } private static int[] createPartitionSortFields( @@ -1282,36 +1329,62 @@ private static class PartitionSortKey implements ManifestSortKey { private final RowType externalSortRowType; private final int[] externalSortKeyFields; private final int sortFieldNum; + private final boolean compareManifestBuckets; private PartitionSortKey( - RecordComparator fieldComparator, RowType partitionType, int sortFieldIndex) { + RecordComparator fieldComparator, + RowType partitionType, + int sortFieldIndex, + boolean sortBucketFirst, + boolean compareManifestBuckets) { this.fieldComparator = fieldComparator; + this.compareManifestBuckets = compareManifestBuckets; DataType sortFieldType = partitionType.getTypeAt(sortFieldIndex); this.sortFieldGetter = InternalRow.createFieldGetter(sortFieldType, sortFieldIndex); - this.sortFieldNum = 3; + this.sortFieldNum = 4; this.externalSortRowType = DataTypes.ROW( sortFieldType, + DataTypes.INT(), DataTypes.TINYINT(), DataTypes.STRING(), ManifestEntry.MANIFEST_ROW_TYPE); - this.externalSortKeyFields = createSequentialFields(sortFieldNum); + this.externalSortKeyFields = + sortBucketFirst ? new int[] {1, 0, 2, 3} : new int[] {0, 2, 3}; } @Override public int compareMin(ManifestFileMeta a, ManifestFileMeta b) { + if (compareManifestBuckets) { + int bucketComparison = Integer.compare(a.minBucket(), b.minBucket()); + if (bucketComparison != 0) { + return bucketComparison; + } + } return fieldComparator.compare( a.partitionStats().minValues(), b.partitionStats().minValues()); } @Override public int compareMax(ManifestFileMeta a, ManifestFileMeta b) { + if (compareManifestBuckets) { + int bucketComparison = Integer.compare(a.maxBucket(), b.maxBucket()); + if (bucketComparison != 0) { + return bucketComparison; + } + } return fieldComparator.compare( a.partitionStats().maxValues(), b.partitionStats().maxValues()); } @Override public boolean isAfterMax(ManifestFileMeta file, ManifestFileMeta maxFile) { + if (compareManifestBuckets) { + int bucketComparison = Integer.compare(file.minBucket(), maxFile.maxBucket()); + if (bucketComparison != 0) { + return bucketComparison > 0; + } + } return fieldComparator.compare( file.partitionStats().minValues(), maxFile.partitionStats().maxValues()) >= 0; @@ -1331,13 +1404,14 @@ public int[] externalSortKeyFields() { public void replaceExternalSortRow( GenericRow row, ManifestEntry entry, InternalRow binaryManifestRow) { row.setField(0, sortFieldGetter.getFieldOrNull(entry.partition())); - row.setField(1, entry.kind().toByteValue()); + row.setField(1, entry.bucket()); + row.setField(2, entry.kind().toByteValue()); row.setField( - 2, + 3, entry instanceof ProjectedManifestEntry ? ((ProjectedManifestEntry) entry).file().fileNameBinary() : BinaryString.fromString(entry.file().fileName())); - row.setField(3, binaryManifestRow); + row.setField(4, binaryManifestRow); } @Override diff --git a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java index 996fceaa97bf..71a0f5842495 100644 --- a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java +++ b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java @@ -2021,6 +2021,11 @@ public static void validatePkClusteringOverride(CoreOptions options) { private static void validateManifestSort(TableSchema schema, CoreOptions options) { if (options.manifestSortEnabled()) { + checkArgument( + !options.dataEvolutionEnabled() || !options.manifestSortBucketFirst(), + "'%s' is not supported when '%s' is enabled.", + CoreOptions.MANIFEST_SORT_BUCKET_FIRST.key(), + CoreOptions.DATA_EVOLUTION_ENABLED.key()); if (!options.dataEvolutionEnabled()) { checkArgument( !schema.partitionKeys().isEmpty(), diff --git a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java index c27d8c012fb3..94d64ec3c375 100644 --- a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java @@ -1286,6 +1286,60 @@ public void testManifestSortWithOverlappingPartitions() { } } + @Test + public void testManifestSortPreservesExistingOrderWhenBucketFirstDisabled() { + List input = + Arrays.asList( + makeManifest(makeBucketEntry("a-3", 0, 3), makeBucketEntry("a-1", 0, 1)), + makeManifest(makeBucketEntry("b-2", 0, 2), makeBucketEntry("b-0", 0, 0))); + + Options testOptions = new Options(); + testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true); + testOptions.set(CoreOptions.MANIFEST_TARGET_FILE_SIZE.key(), "1G"); + testOptions.set(CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(), "1B"); + List merged = + ManifestFileMerger.merge( + input, + manifestFile, + getPartitionType(), + CoreOptions.fromMap(testOptions.toMap())); + + assertEquivalentEntries(input, merged); + assertThat(readEntries(merged)) + .extracting(ManifestEntry::bucket) + .containsExactly(1, 3, 0, 2); + } + + @Test + public void testManifestSortCanUseBucketAsPrimaryKey() { + List input = + Arrays.asList( + makeManifest( + makeBucketEntry("a-b1-p1", 1, 1), makeBucketEntry("a-b0-p0", 0, 0)), + makeManifest( + makeBucketEntry("b-b1-p0", 0, 1), + makeBucketEntry("b-b0-p1", 1, 0))); + + Options testOptions = new Options(); + testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true); + testOptions.set(CoreOptions.MANIFEST_SORT_BUCKET_FIRST, true); + testOptions.set(CoreOptions.MANIFEST_TARGET_FILE_SIZE.key(), "1G"); + testOptions.set(CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(), "1B"); + List merged = + ManifestFileMerger.merge( + input, + manifestFile, + getPartitionType(), + CoreOptions.fromMap(testOptions.toMap())); + + assertEquivalentEntries(input, merged); + List entries = readEntries(merged); + assertThat(entries).extracting(ManifestEntry::bucket).containsExactly(0, 0, 1, 1); + assertThat(entries) + .extracting(entry -> entry.partition().getInt(0)) + .containsExactly(0, 1, 0, 1); + } + @Test public void testManifestSortMinorCompactionRespectsMergeMinCount() { List input = new ArrayList<>(); @@ -2708,6 +2762,12 @@ public void testBoundaryEqualityHandling() { } } + /** Create a ManifestEntry with an explicit bucket. */ + private ManifestEntry makeBucketEntry(String fileName, int partition, int bucket) { + ManifestEntry entry = makeEntry(true, fileName, partition); + return ManifestEntry.create(entry.kind(), entry.partition(), bucket, 240, entry.file()); + } + /** Create a ManifestEntry with a 3-field partition row (region, dt, hour). */ private ManifestEntry makeMultiPartEntry( boolean isAdd, String fileName, int region, int dt, int hour) { diff --git a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergeTest.java b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergeTest.java index 2d08a158d5ca..c311eeec6eb4 100644 --- a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergeTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergeTest.java @@ -38,6 +38,7 @@ import org.junit.jupiter.api.io.TempDir; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -111,6 +112,26 @@ void testLargeFragmentedManifestsUseRunMerge() throws Exception { .collect(Collectors.toList())); } + @Test + void testBucketFirstManifestComparisonFallsBackForLegacyMetadata() { + ManifestFileMeta first = makeManifest(bucketEntry("first", 2, 0)); + ManifestFileMeta legacy = copyWithoutBucketStats(makeManifest(bucketEntry("legacy", 1, 2))); + ManifestFileMeta last = makeManifest(bucketEntry("last", 0, 1)); + + ManifestFileSorter.ManifestSortKey sortKey = + ManifestFileSorter.createSortKey( + false, Arrays.asList(first, legacy, last), null, partitionType, true); + + assertThat(sortKey.compareMin(first, legacy)).isPositive(); + assertThat(sortKey.compareMin(legacy, last)).isPositive(); + assertThat(sortKey.compareMin(first, last)).isPositive(); + + ManifestFileSorter.ManifestSortKey bucketSortKey = + ManifestFileSorter.createSortKey( + false, Arrays.asList(first, last), null, partitionType, true); + assertThat(bucketSortKey.compareMin(first, last)).isNegative(); + } + private ManifestEntry rowIdEntry(String fileName, long firstRowId) { return ManifestEntry.create( FileKind.ADD, @@ -141,6 +162,59 @@ private ManifestEntry rowIdEntry(String fileName, long firstRowId) { null)); } + private ManifestEntry bucketEntry(String fileName, int partitionValue, int bucket) { + BinaryRow entryPartition = new BinaryRow(1); + BinaryRowWriter writer = new BinaryRowWriter(entryPartition); + writer.writeInt(0, partitionValue); + writer.complete(); + + return ManifestEntry.create( + FileKind.ADD, + entryPartition, + bucket, + 240, + DataFileMeta.create( + fileName, + 0, + 1, + entryPartition, + entryPartition, + StatsTestUtils.newEmptySimpleStats(), + StatsTestUtils.newEmptySimpleStats(), + 0, + 0, + 0, + 0, + Collections.emptyList(), + Timestamp.fromEpochMillis(200000), + 0L, + null, + FileSource.APPEND, + null, + null, + null, + Collections.singletonList("f0"), + null)); + } + + private ManifestFileMeta copyWithoutBucketStats(ManifestFileMeta meta) { + return new ManifestFileMeta( + meta.fileName(), + meta.fileSize(), + meta.numAddedFiles(), + meta.numDeletedFiles(), + meta.partitionStats(), + meta.schemaId(), + null, + null, + meta.minLevel(), + meta.maxLevel(), + meta.minRowId(), + meta.maxRowId(), + meta.totalBuckets(), + meta.extraFiles()); + } + @Override protected ManifestFile getManifestFile() { return manifestFile; diff --git a/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java b/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java index c9588f4552fa..b4fa41be10b0 100644 --- a/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java @@ -2047,6 +2047,23 @@ void testManifestSortValidation() { options6, ""))) .hasMessageContaining("is not a partition field"); + + // Test 7: bucket-first sorting is incompatible with RowID sorting + Map options7 = new HashMap<>(options4); + options7.put(CoreOptions.MANIFEST_SORT_BUCKET_FIRST.key(), "true"); + assertThatThrownBy( + () -> + validateTableSchema( + new TableSchema( + 1, + fields, + 10, + emptyList(), + emptyList(), + options7, + ""))) + .hasMessageContaining(CoreOptions.MANIFEST_SORT_BUCKET_FIRST.key()) + .hasMessageContaining(DATA_EVOLUTION_ENABLED.key()); } @Test From 1cdca5e4798c42fecaac475ca24b8939d7da926d Mon Sep 17 00:00:00 2001 From: jianguotian <18464293+jianguotian@users.noreply.github.com> Date: Mon, 14 Sep 2026 08:47:18 +0800 Subject: [PATCH 2/3] [core] Add spill coverage for bucket-first manifest sorting --- .../paimon/manifest/ManifestFileMetaTest.java | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java index 94d64ec3c375..a0ca91e20fe7 100644 --- a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java @@ -1446,26 +1446,29 @@ public void testManifestSortMaxRewriteSizeSmallerThanTargetFileSizeStillRewrites .isTrue(); } - @Test - public void testManifestSortWithSpillableExternalSortBuffer() { + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void testManifestSortWithSpillableExternalSortBuffer(boolean bucketFirst) { List input = new ArrayList<>(); for (int manifest = 0; manifest < 4; manifest++) { List entries = new ArrayList<>(); for (int i = 0; i < 80; i++) { int partition = manifest % 2 == 0 ? 79 - i : i; + int bucket = Math.floorMod(manifest * 31 + i * 17, 4); entries.add( - makeEntry( - true, + makeBucketEntry( String.format( "spill-manifest-%02d-entry-%03d-payload-padding-%040d", manifest, i, i), - partition)); + partition, + bucket)); } input.add(makeManifest(entries.toArray(new ManifestEntry[0]))); } Options testOptions = new Options(); testOptions.set("manifest-sort.enabled", "true"); + testOptions.set(CoreOptions.MANIFEST_SORT_BUCKET_FIRST, bucketFirst); testOptions.set("manifest.full-compaction-threshold-size", "1B"); testOptions.set("page-size", "1kb"); testOptions.set("sort-spill-buffer-size", "4kb"); @@ -1479,15 +1482,25 @@ public void testManifestSortWithSpillableExternalSortBuffer() { CoreOptions.fromMap(testOptions.toMap())); assertEquivalentEntries(input, merged); - for (ManifestFileMeta meta : merged) { - List entries = manifestFile.read(meta.fileName(), meta.fileSize()); - for (int i = 1; i < entries.size(); i++) { - int prevPartition = entries.get(i - 1).partition().getInt(0); - int currPartition = entries.get(i).partition().getInt(0); - assertThat(currPartition) - .as("Entries within a manifest should be sorted after spill") - .isGreaterThanOrEqualTo(prevPartition); + List entries = readEntries(merged); + for (int i = 1; i < entries.size(); i++) { + ManifestEntry previous = entries.get(i - 1); + ManifestEntry current = entries.get(i); + int comparison = 0; + if (bucketFirst) { + comparison = Integer.compare(previous.bucket(), current.bucket()); + } + if (comparison == 0) { + comparison = + Integer.compare( + previous.partition().getInt(0), current.partition().getInt(0)); + } + if (comparison == 0) { + comparison = previous.file().fileName().compareTo(current.file().fileName()); } + assertThat(comparison) + .as("Entries should use the configured sort order after spill") + .isLessThanOrEqualTo(0); } } From fb445e6ad1b3dd37db544fe327919ca78cce727c Mon Sep 17 00:00:00 2001 From: jianguotian <18464293+jianguotian@users.noreply.github.com> Date: Mon, 14 Sep 2026 09:19:14 +0800 Subject: [PATCH 3/3] [core] Use bucket-first manifest sorting by default --- docs/generated/core_configuration.html | 6 - .../java/org/apache/paimon/CoreOptions.java | 14 -- .../operation/ManifestCompactDryRun.java | 2 +- .../paimon/operation/ManifestFileSorter.java | 162 +++++++++++------- .../paimon/schema/SchemaValidation.java | 5 - .../paimon/manifest/ManifestFileMetaTest.java | 16 +- .../operation/ManifestEntryRunMergeTest.java | 2 +- .../paimon/schema/SchemaValidationTest.java | 17 -- 8 files changed, 111 insertions(+), 113 deletions(-) diff --git a/docs/generated/core_configuration.html b/docs/generated/core_configuration.html index fb313aef8dd9..ac5730017523 100644 --- a/docs/generated/core_configuration.html +++ b/docs/generated/core_configuration.html @@ -1059,12 +1059,6 @@ Integer Level threshold of lookup to generate remote lookup files. Level files below this threshold will not generate remote lookup files. - -
manifest-sort.bucket-first
- false - Boolean - Sort manifest entries by bucket before the configured partition field. This improves manifest pruning for bucket-key point lookups spanning many partitions, at the cost of wider partition ranges in each manifest. -
manifest-sort.enabled
false diff --git a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java index 83e330f81e0e..7b0665c50296 100644 --- a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java +++ b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java @@ -589,16 +589,6 @@ public InlineElement getDescription() { "Partition field name to sort manifest entries by. Validated by" + " schema validation, if not configured, defaults to the first partition field."); - public static final ConfigOption MANIFEST_SORT_BUCKET_FIRST = - key("manifest-sort.bucket-first") - .booleanType() - .defaultValue(false) - .withDescription( - "Sort manifest entries by bucket before the configured partition" - + " field. This improves manifest pruning for bucket-key point" - + " lookups spanning many partitions, at the cost of wider" - + " partition ranges in each manifest."); - public static final ConfigOption MANIFEST_SORT_MAX_REWRITE_SIZE = key("manifest-sort.max-rewrite-size") .memoryType() @@ -3233,10 +3223,6 @@ public String manifestSortPartitionField() { return options.get(MANIFEST_SORT_PARTITION_FIELD); } - public boolean manifestSortBucketFirst() { - return options.get(MANIFEST_SORT_BUCKET_FIRST); - } - public long manifestSortMaxRewriteSize() { return options.get(MANIFEST_SORT_MAX_REWRITE_SIZE).getBytes(); } diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestCompactDryRun.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestCompactDryRun.java index 56a31a02ac5e..6f1e5f8e4076 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestCompactDryRun.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestCompactDryRun.java @@ -120,7 +120,7 @@ private static List buildLevelSortedRunsForDryRun( manifests, options.manifestSortPartitionField(), partitionType, - options.manifestSortBucketFirst()); + options.bucket() > 0); ManifestFileSorter.ClassifyResult classifyResult = ManifestFileSorter.classifyManifests( manifests, diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileSorter.java b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileSorter.java index ba3856849e5d..51f51dabfe30 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileSorter.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileSorter.java @@ -155,7 +155,7 @@ static List trySortCompaction( @Nullable IOManager ioManager) throws Exception { String sortPartitionField = options.manifestSortPartitionField(); - boolean sortBucketFirst = options.manifestSortBucketFirst(); + boolean bucketed = options.bucket() > 0; boolean runMergeOptimizeEnabled = options.manifestMergeOptimizeEnabled(); long suggestedMetaSize = options.manifestTargetSize().getBytes(); int suggestedMinMetaCount = options.manifestMergeMinCount(); @@ -174,7 +174,7 @@ static List trySortCompaction( manifestFile, partitionType, sortPartitionField, - sortBucketFirst, + bucketed, options.dataEvolutionEnabled(), runMergeOptimizeEnabled, suggestedMetaSize, @@ -194,7 +194,7 @@ static List trySortCompaction( manifestFile, partitionType, sortPartitionField, - sortBucketFirst, + bucketed, options.dataEvolutionEnabled(), runMergeOptimizeEnabled, suggestedMetaSize, @@ -218,7 +218,7 @@ private static Optional> tryFullCompaction( ManifestFile manifestFile, RowType partitionType, String sortPartitionField, - boolean sortBucketFirst, + boolean bucketed, boolean dataEvolutionEnabled, boolean runMergeOptimizeEnabled, long suggestedMetaSize, @@ -242,7 +242,7 @@ private static Optional> tryFullCompaction( manifestFile, partitionType, sortPartitionField, - sortBucketFirst, + bucketed, dataEvolutionEnabled, runMergeOptimizeEnabled, suggestedMetaSize, @@ -326,7 +326,7 @@ private static List tryMinorCompaction( ManifestFile manifestFile, RowType partitionType, String sortPartitionField, - boolean sortBucketFirst, + boolean bucketed, boolean dataEvolutionEnabled, boolean runMergeOptimizeEnabled, long suggestedMetaSize, @@ -345,7 +345,7 @@ private static List tryMinorCompaction( manifestFile, partitionType, sortPartitionField, - sortBucketFirst, + bucketed, dataEvolutionEnabled, runMergeOptimizeEnabled, suggestedMetaSize, @@ -460,7 +460,7 @@ private static CompactionContext prepareCompaction( ManifestFile manifestFile, RowType partitionType, String sortPartitionField, - boolean sortBucketFirst, + boolean bucketed, boolean dataEvolutionEnabled, boolean runMergeOptimizeEnabled, long suggestedMetaSize, @@ -474,11 +474,7 @@ private static CompactionContext prepareCompaction( // Step 1: Resolve sort key. Data evolution tables prefer RowID ranges when available. ManifestSortKey sortKey = createSortKey( - dataEvolutionEnabled, - input, - sortPartitionField, - partitionType, - sortBucketFirst); + dataEvolutionEnabled, input, sortPartitionField, partitionType, bucketed); // Step 2: Classify manifests into LSM files and collect delete entries. ClassifyResult classification = @@ -1205,34 +1201,8 @@ static ManifestSortKey createSortKey( List input, String sortPartitionField, RowType partitionType, - boolean sortBucketFirst) { - if (dataEvolutionEnabled && sortBucketFirst) { - throw new IllegalArgumentException( - String.format( - "'%s' is not supported when '%s' is enabled.", - CoreOptions.MANIFEST_SORT_BUCKET_FIRST.key(), - CoreOptions.DATA_EVOLUTION_ENABLED.key())); - } - - return createSortKey( - dataEvolutionEnabled && ManifestFileMeta.allContainsRowId(input), - sortPartitionField, - partitionType, - sortBucketFirst, - sortBucketFirst - && input.stream() - .allMatch( - meta -> - meta.minBucket() != null - && meta.maxBucket() != null)); - } - - private static ManifestSortKey createSortKey( - boolean rowIdSort, - String sortPartitionField, - RowType partitionType, - boolean sortBucketFirst, - boolean compareManifestBuckets) { + boolean bucketed) { + boolean rowIdSort = dataEvolutionEnabled && ManifestFileMeta.allContainsRowId(input); if (rowIdSort) { // RowID sorting uses the configured partition field as the primary key when specified, // otherwise it uses the full partition row to preserve partition locality. It then @@ -1261,12 +1231,14 @@ private static ManifestSortKey createSortKey( RecordComparator fieldComparator = CodeGenUtils.newRecordComparator( partitionType.getFieldTypes(), new int[] {sortFieldIndex}); - return new PartitionSortKey( - fieldComparator, - partitionType, - sortFieldIndex, - sortBucketFirst, - compareManifestBuckets); + if (bucketed) { + boolean compareManifestBuckets = + input.stream() + .allMatch(meta -> meta.minBucket() != null && meta.maxBucket() != null); + return new BucketSortKey( + fieldComparator, partitionType, sortFieldIndex, compareManifestBuckets); + } + return new PartitionSortKey(fieldComparator, partitionType, sortFieldIndex); } private static int[] createPartitionSortFields( @@ -1329,28 +1301,98 @@ private static class PartitionSortKey implements ManifestSortKey { private final RowType externalSortRowType; private final int[] externalSortKeyFields; private final int sortFieldNum; - private final boolean compareManifestBuckets; private PartitionSortKey( + RecordComparator fieldComparator, RowType partitionType, int sortFieldIndex) { + this.fieldComparator = fieldComparator; + DataType sortFieldType = partitionType.getTypeAt(sortFieldIndex); + this.sortFieldGetter = InternalRow.createFieldGetter(sortFieldType, sortFieldIndex); + this.sortFieldNum = 3; + this.externalSortRowType = + DataTypes.ROW( + sortFieldType, + DataTypes.TINYINT(), + DataTypes.STRING(), + ManifestEntry.MANIFEST_ROW_TYPE); + this.externalSortKeyFields = createSequentialFields(sortFieldNum); + } + + @Override + public int compareMin(ManifestFileMeta a, ManifestFileMeta b) { + return fieldComparator.compare( + a.partitionStats().minValues(), b.partitionStats().minValues()); + } + + @Override + public int compareMax(ManifestFileMeta a, ManifestFileMeta b) { + return fieldComparator.compare( + a.partitionStats().maxValues(), b.partitionStats().maxValues()); + } + + @Override + public boolean isAfterMax(ManifestFileMeta file, ManifestFileMeta maxFile) { + return fieldComparator.compare( + file.partitionStats().minValues(), maxFile.partitionStats().maxValues()) + >= 0; + } + + @Override + public RowType externalSortRowType() { + return externalSortRowType; + } + + @Override + public int[] externalSortKeyFields() { + return externalSortKeyFields; + } + + @Override + public void replaceExternalSortRow( + GenericRow row, ManifestEntry entry, InternalRow binaryManifestRow) { + row.setField(0, sortFieldGetter.getFieldOrNull(entry.partition())); + row.setField(1, entry.kind().toByteValue()); + row.setField( + 2, + entry instanceof ProjectedManifestEntry + ? ((ProjectedManifestEntry) entry).file().fileNameBinary() + : BinaryString.fromString(entry.file().fileName())); + row.setField(3, binaryManifestRow); + } + + @Override + public InternalRow binaryManifestRow(BinaryRow row) { + return row.getRow(sortFieldNum, ManifestEntry.MANIFEST_ROW_TYPE.getFieldCount()); + } + } + + private static class BucketSortKey implements ManifestSortKey { + + private final PartitionSortKey partitionSortKey; + private final InternalRow.FieldGetter sortFieldGetter; + private final RowType externalSortRowType; + private final int[] externalSortKeyFields; + private final int sortFieldNum; + private final boolean compareManifestBuckets; + + private BucketSortKey( RecordComparator fieldComparator, RowType partitionType, int sortFieldIndex, - boolean sortBucketFirst, boolean compareManifestBuckets) { - this.fieldComparator = fieldComparator; + this.partitionSortKey = + new PartitionSortKey(fieldComparator, partitionType, sortFieldIndex); this.compareManifestBuckets = compareManifestBuckets; DataType sortFieldType = partitionType.getTypeAt(sortFieldIndex); this.sortFieldGetter = InternalRow.createFieldGetter(sortFieldType, sortFieldIndex); this.sortFieldNum = 4; this.externalSortRowType = DataTypes.ROW( - sortFieldType, DataTypes.INT(), + sortFieldType, DataTypes.TINYINT(), DataTypes.STRING(), ManifestEntry.MANIFEST_ROW_TYPE); - this.externalSortKeyFields = - sortBucketFirst ? new int[] {1, 0, 2, 3} : new int[] {0, 2, 3}; + this.externalSortKeyFields = createSequentialFields(sortFieldNum); } @Override @@ -1361,8 +1403,7 @@ public int compareMin(ManifestFileMeta a, ManifestFileMeta b) { return bucketComparison; } } - return fieldComparator.compare( - a.partitionStats().minValues(), b.partitionStats().minValues()); + return partitionSortKey.compareMin(a, b); } @Override @@ -1373,8 +1414,7 @@ public int compareMax(ManifestFileMeta a, ManifestFileMeta b) { return bucketComparison; } } - return fieldComparator.compare( - a.partitionStats().maxValues(), b.partitionStats().maxValues()); + return partitionSortKey.compareMax(a, b); } @Override @@ -1385,9 +1425,7 @@ public boolean isAfterMax(ManifestFileMeta file, ManifestFileMeta maxFile) { return bucketComparison > 0; } } - return fieldComparator.compare( - file.partitionStats().minValues(), maxFile.partitionStats().maxValues()) - >= 0; + return partitionSortKey.isAfterMax(file, maxFile); } @Override @@ -1403,8 +1441,8 @@ public int[] externalSortKeyFields() { @Override public void replaceExternalSortRow( GenericRow row, ManifestEntry entry, InternalRow binaryManifestRow) { - row.setField(0, sortFieldGetter.getFieldOrNull(entry.partition())); - row.setField(1, entry.bucket()); + row.setField(0, entry.bucket()); + row.setField(1, sortFieldGetter.getFieldOrNull(entry.partition())); row.setField(2, entry.kind().toByteValue()); row.setField( 3, diff --git a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java index 71a0f5842495..996fceaa97bf 100644 --- a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java +++ b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java @@ -2021,11 +2021,6 @@ public static void validatePkClusteringOverride(CoreOptions options) { private static void validateManifestSort(TableSchema schema, CoreOptions options) { if (options.manifestSortEnabled()) { - checkArgument( - !options.dataEvolutionEnabled() || !options.manifestSortBucketFirst(), - "'%s' is not supported when '%s' is enabled.", - CoreOptions.MANIFEST_SORT_BUCKET_FIRST.key(), - CoreOptions.DATA_EVOLUTION_ENABLED.key()); if (!options.dataEvolutionEnabled()) { checkArgument( !schema.partitionKeys().isEmpty(), diff --git a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java index a0ca91e20fe7..898f238231ed 100644 --- a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java @@ -1287,7 +1287,7 @@ public void testManifestSortWithOverlappingPartitions() { } @Test - public void testManifestSortPreservesExistingOrderWhenBucketFirstDisabled() { + public void testManifestSortPreservesExistingOrderForUnawareBucketTable() { List input = Arrays.asList( makeManifest(makeBucketEntry("a-3", 0, 3), makeBucketEntry("a-1", 0, 1)), @@ -1311,7 +1311,7 @@ public void testManifestSortPreservesExistingOrderWhenBucketFirstDisabled() { } @Test - public void testManifestSortCanUseBucketAsPrimaryKey() { + public void testManifestSortUsesBucketAsPrimaryKeyForBucketedTable() { List input = Arrays.asList( makeManifest( @@ -1322,7 +1322,7 @@ public void testManifestSortCanUseBucketAsPrimaryKey() { Options testOptions = new Options(); testOptions.set(CoreOptions.MANIFEST_SORT_ENABLED, true); - testOptions.set(CoreOptions.MANIFEST_SORT_BUCKET_FIRST, true); + testOptions.set(CoreOptions.BUCKET, 4); testOptions.set(CoreOptions.MANIFEST_TARGET_FILE_SIZE.key(), "1G"); testOptions.set(CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(), "1B"); List merged = @@ -1448,7 +1448,7 @@ public void testManifestSortMaxRewriteSizeSmallerThanTargetFileSizeStillRewrites @ParameterizedTest @ValueSource(booleans = {false, true}) - public void testManifestSortWithSpillableExternalSortBuffer(boolean bucketFirst) { + public void testManifestSortWithSpillableExternalSortBuffer(boolean bucketed) { List input = new ArrayList<>(); for (int manifest = 0; manifest < 4; manifest++) { List entries = new ArrayList<>(); @@ -1468,7 +1468,9 @@ public void testManifestSortWithSpillableExternalSortBuffer(boolean bucketFirst) Options testOptions = new Options(); testOptions.set("manifest-sort.enabled", "true"); - testOptions.set(CoreOptions.MANIFEST_SORT_BUCKET_FIRST, bucketFirst); + if (bucketed) { + testOptions.set(CoreOptions.BUCKET, 4); + } testOptions.set("manifest.full-compaction-threshold-size", "1B"); testOptions.set("page-size", "1kb"); testOptions.set("sort-spill-buffer-size", "4kb"); @@ -1487,7 +1489,7 @@ public void testManifestSortWithSpillableExternalSortBuffer(boolean bucketFirst) ManifestEntry previous = entries.get(i - 1); ManifestEntry current = entries.get(i); int comparison = 0; - if (bucketFirst) { + if (bucketed) { comparison = Integer.compare(previous.bucket(), current.bucket()); } if (comparison == 0) { @@ -1499,7 +1501,7 @@ public void testManifestSortWithSpillableExternalSortBuffer(boolean bucketFirst) comparison = previous.file().fileName().compareTo(current.file().fileName()); } assertThat(comparison) - .as("Entries should use the configured sort order after spill") + .as("Entries should use the table's sort order after spill") .isLessThanOrEqualTo(0); } } diff --git a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergeTest.java b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergeTest.java index c311eeec6eb4..1142f2fa1664 100644 --- a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergeTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestEntryRunMergeTest.java @@ -113,7 +113,7 @@ void testLargeFragmentedManifestsUseRunMerge() throws Exception { } @Test - void testBucketFirstManifestComparisonFallsBackForLegacyMetadata() { + void testBucketedManifestComparisonFallsBackForLegacyMetadata() { ManifestFileMeta first = makeManifest(bucketEntry("first", 2, 0)); ManifestFileMeta legacy = copyWithoutBucketStats(makeManifest(bucketEntry("legacy", 1, 2))); ManifestFileMeta last = makeManifest(bucketEntry("last", 0, 1)); diff --git a/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java b/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java index b4fa41be10b0..c9588f4552fa 100644 --- a/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java @@ -2047,23 +2047,6 @@ void testManifestSortValidation() { options6, ""))) .hasMessageContaining("is not a partition field"); - - // Test 7: bucket-first sorting is incompatible with RowID sorting - Map options7 = new HashMap<>(options4); - options7.put(CoreOptions.MANIFEST_SORT_BUCKET_FIRST.key(), "true"); - assertThatThrownBy( - () -> - validateTableSchema( - new TableSchema( - 1, - fields, - 10, - emptyList(), - emptyList(), - options7, - ""))) - .hasMessageContaining(CoreOptions.MANIFEST_SORT_BUCKET_FIRST.key()) - .hasMessageContaining(DATA_EVOLUTION_ENABLED.key()); } @Test