This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 6af757af51 [core] Reuse projected entries in manifest run merge (#9241)
6af757af51 is described below

commit 6af757af515541da555259dff8b75a2ad686c937
Author: YeJunHao <[email protected]>
AuthorDate: Mon Aug 17 10:26:29 2026 +0800

    [core] Reuse projected entries in manifest run merge (#9241)
---
 .../paimon/manifest/ProjectedManifestEntry.java    |   1 +
 .../operation/ManifestEntryExternalSort.java       |   8 +-
 .../paimon/operation/ManifestEntryRunMerge.java    | 700 ++++++++++++---------
 .../operation/ManifestEntryRunMergeEntry.java      | 339 ----------
 .../operation/ManifestEntryRunMergePlan.java       | 173 +++--
 .../paimon/operation/ManifestFileSorter.java       | 410 ++++++------
 .../manifest/ProjectedManifestEntryTest.java       |  19 +
 7 files changed, 705 insertions(+), 945 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/manifest/ProjectedManifestEntry.java
 
b/paimon-core/src/main/java/org/apache/paimon/manifest/ProjectedManifestEntry.java
index 591d86759e..1d99c8bd91 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/manifest/ProjectedManifestEntry.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/manifest/ProjectedManifestEntry.java
@@ -151,6 +151,7 @@ public final class ProjectedManifestEntry implements 
ManifestEntry {
                                                         DataFileMeta.LEVEL,
                                                         DataFileMeta.SCHEMA_ID,
                                                         
DataFileMeta.FIRST_ROW_ID,
+                                                        
DataFileMeta.MAX_SEQUENCE_NUMBER,
                                                         
DataFileMeta.EXTRA_FILES,
                                                         
DataFileMeta.EMBEDDED_FILE_INDEX,
                                                         
DataFileMeta.EXTERNAL_PATH)))));
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java
 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java
index 5c9b83772e..c261389093 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java
@@ -24,6 +24,7 @@ import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.data.serializer.InternalRowSerializer;
 import org.apache.paimon.disk.IOManager;
+import org.apache.paimon.manifest.CollectedDeletes;
 import org.apache.paimon.manifest.CompactFileIdentifierSet;
 import org.apache.paimon.manifest.FileEntry.ReusableIdentifier;
 import org.apache.paimon.manifest.ManifestAvroWriter;
@@ -84,9 +85,10 @@ public class ManifestEntryExternalSort {
             ExternalSortConfig config,
             ManifestFile manifestFile,
             List<ManifestFileMeta> newFilesForAbort,
-            CompactFileIdentifierSet deleteEntries,
+            CollectedDeletes deletes,
             @Nullable Integer manifestReadParallelism)
             throws Exception {
+        ReusableIdentifier identifier = new ReusableIdentifier();
         try (EntrySorter sorter = new EntrySorter(sortKey, config)) {
             scanEntries(
                     section,
@@ -94,13 +96,15 @@ public class ManifestEntryExternalSort {
                     manifestReadParallelism,
                     entry -> {
                         if (entry.isAdd()
-                                && (deleteEntries.isEmpty() || 
!deleteEntries.contains(entry))) {
+                                && (deletes.isEmpty() || 
!deletes.isDeleted(entry, identifier))) {
                             sorter.write(entry);
                         }
                     });
             List<ManifestFileMeta> files = 
sorter.writeToManifest(manifestFile);
             newFilesForAbort.addAll(files);
             return files;
+        } finally {
+            identifier.release();
         }
     }
 
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java
 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java
index 1b9d4f8db6..18cd08dfb3 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMerge.java
@@ -20,31 +20,36 @@ package org.apache.paimon.operation;
 
 import org.apache.paimon.data.BinaryArray;
 import org.apache.paimon.data.BinaryRow;
-import org.apache.paimon.data.GenericRow;
-import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.format.SimpleStatsCollector;
-import org.apache.paimon.io.DataFileMeta;
-import org.apache.paimon.manifest.CompactFileIdentifierSet;
-import org.apache.paimon.manifest.DeletedRowIdSet;
-import org.apache.paimon.manifest.FileKind;
+import org.apache.paimon.manifest.CollectedDeletes;
+import org.apache.paimon.manifest.FileEntry.ReusableIdentifier;
 import org.apache.paimon.manifest.ManifestAvroReader;
 import org.apache.paimon.manifest.ManifestAvroReader.RawBlock;
 import org.apache.paimon.manifest.ManifestAvroReader.RowIterator;
 import org.apache.paimon.manifest.ManifestAvroWriter.EncodedBlockMeta;
-import org.apache.paimon.manifest.ManifestEntry;
 import org.apache.paimon.manifest.ManifestFile;
 import org.apache.paimon.manifest.ManifestFileMeta;
+import org.apache.paimon.manifest.PartitionDictionary;
+import org.apache.paimon.manifest.ProjectedManifestEntry;
+import org.apache.paimon.memory.MemorySegment;
+import org.apache.paimon.memory.MemorySegmentUtils;
 import org.apache.paimon.stats.SimpleStats;
 import org.apache.paimon.stats.SimpleStatsConverter;
-import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.ByteArrayKey;
+import org.apache.paimon.utils.ByteArrayLookupKey;
 import org.apache.paimon.utils.Pair;
 
 import javax.annotation.Nullable;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
 
 import static 
org.apache.paimon.utils.ManifestReadThreadPool.sequentialBatchedExecute;
@@ -57,91 +62,9 @@ final class ManifestEntryRunMerge {
     private static final long MAX_IN_MEMORY_FRAGMENTED_ENTRIES = 25_000L;
     private static final int MAX_STREAM_CURSORS = 128;
     private static final int MAX_STREAM_READ_AMPLIFICATION = 8;
-    static final int KIND = 0;
-    static final int PARTITION = 1;
-    static final int BUCKET = 2;
-    static final int FILE = 3;
-    static final int FILE_NAME = 0;
-    static final int ROW_COUNT = 1;
-    static final int LEVEL = 2;
-    static final int SCHEMA_ID = 3;
-    static final int FIRST_ROW_ID = 4;
-    static final int MAX_SEQUENCE_NUMBER = 5;
-    static final int EXTRA_FILES = 6;
-    static final int EMBEDDED_FILE_INDEX = 7;
-    static final int EXTERNAL_PATH = 8;
-    static final int FILE_FIELD_COUNT = 9;
-    private static final String[] ENTRY_FILE_FIELD_NAMES = {
-        DataFileMeta.FILE_NAME,
-        DataFileMeta.ROW_COUNT,
-        DataFileMeta.LEVEL,
-        DataFileMeta.SCHEMA_ID,
-        DataFileMeta.FIRST_ROW_ID,
-        DataFileMeta.MAX_SEQUENCE_NUMBER,
-        DataFileMeta.EXTRA_FILES,
-        DataFileMeta.EMBEDDED_FILE_INDEX,
-        DataFileMeta.EXTERNAL_PATH
-    };
-    private static final InternalRow.FieldGetter[] ENTRY_FILE_GETTERS = 
entryFileGetters();
-    static final RowType ENTRY_LAYOUT = entryLayout();
-    private static final int FULL_KIND =
-            ManifestEntry.MANIFEST_ROW_TYPE.getFieldIndex(ManifestEntry.KIND);
-    private static final int FULL_PARTITION =
-            
ManifestEntry.MANIFEST_ROW_TYPE.getFieldIndex(ManifestEntry.PARTITION);
-    private static final int FULL_BUCKET =
-            
ManifestEntry.MANIFEST_ROW_TYPE.getFieldIndex(ManifestEntry.BUCKET);
-    private static final int FULL_FILE =
-            ManifestEntry.MANIFEST_ROW_TYPE.getFieldIndex(ManifestEntry.FILE);
 
     private ManifestEntryRunMerge() {}
 
-    private static InternalRow.FieldGetter[] entryFileGetters() {
-        InternalRow.FieldGetter[] getters =
-                new InternalRow.FieldGetter[ENTRY_FILE_FIELD_NAMES.length];
-        for (int field = 0; field < getters.length; field++) {
-            int position = 
DataFileMeta.SCHEMA.getFieldIndex(ENTRY_FILE_FIELD_NAMES[field]);
-            getters[field] =
-                    InternalRow.createFieldGetter(
-                            DataFileMeta.SCHEMA.getTypeAt(position), position);
-        }
-        return getters;
-    }
-
-    private static RowType entryLayout() {
-        List<DataField> fields = new ArrayList<>();
-        
fields.add(ManifestEntry.MANIFEST_ROW_TYPE.getField(ManifestEntry.KIND));
-        
fields.add(ManifestEntry.MANIFEST_ROW_TYPE.getField(ManifestEntry.PARTITION));
-        
fields.add(ManifestEntry.MANIFEST_ROW_TYPE.getField(ManifestEntry.BUCKET));
-        fields.add(
-                ManifestEntry.MANIFEST_ROW_TYPE
-                        .getField(ManifestEntry.FILE)
-                        .newType(
-                                DataFileMeta.SCHEMA.project(
-                                        DataFileMeta.FILE_NAME,
-                                        DataFileMeta.ROW_COUNT,
-                                        DataFileMeta.LEVEL,
-                                        DataFileMeta.SCHEMA_ID,
-                                        DataFileMeta.FIRST_ROW_ID,
-                                        DataFileMeta.MAX_SEQUENCE_NUMBER,
-                                        DataFileMeta.EXTRA_FILES,
-                                        DataFileMeta.EMBEDDED_FILE_INDEX,
-                                        DataFileMeta.EXTERNAL_PATH)));
-        return new RowType(false, fields);
-    }
-
-    static GenericRow projectEntryLayout(
-            GenericRow fullRow, GenericRow reuse, GenericRow reuseFile) {
-        reuse.setField(KIND, fullRow.getByte(FULL_KIND));
-        reuse.setField(PARTITION, fullRow.getBinary(FULL_PARTITION));
-        reuse.setField(BUCKET, fullRow.getInt(FULL_BUCKET));
-        InternalRow fullFile = fullRow.getRow(FULL_FILE, 
DataFileMeta.SCHEMA.getFieldCount());
-        for (int field = 0; field < ENTRY_FILE_GETTERS.length; field++) {
-            reuseFile.setField(field, 
ENTRY_FILE_GETTERS[field].getFieldOrNull(fullFile));
-        }
-        reuse.setField(FILE, reuseFile);
-        return reuse;
-    }
-
     /**
      * Returns null when the input is too fragmented for a bounded streaming 
merge. The caller must
      * fall back to the spillable external sorter in that case.
@@ -153,26 +76,24 @@ final class ManifestEntryRunMerge {
             RowType partitionType,
             ManifestFile manifestFile,
             List<ManifestFileMeta> newFilesForAbort,
-            CompactFileIdentifierSet deletedIdentifiers,
-            DeletedRowIdSet deletedRowIds,
+            CollectedDeletes deletes,
             int maxNumFileHandles,
             @Nullable Integer manifestReadParallelism)
             throws Exception {
-        ManifestEntryRunMergeEntry.Filter filter =
-                new ManifestEntryRunMergeEntry.Filter(deletedIdentifiers, 
deletedRowIds, true);
         ManifestEntryRunMergePlan plan =
                 discoverRuns(
                         section,
                         sortKey,
                         partitionType,
                         manifestFile,
-                        filter,
+                        deletes,
+                        false,
                         maxNumFileHandles,
                         manifestReadParallelism);
         if (plan == null) {
             return null;
         }
-        return plan.mergeToManifest(sortKey, manifestFile, filter, 
newFilesForAbort);
+        return plan.mergeToManifest(sortKey, manifestFile, newFilesForAbort);
     }
 
     /**
@@ -189,38 +110,24 @@ final class ManifestEntryRunMerge {
             int maxNumFileHandles,
             @Nullable Integer manifestReadParallelism)
             throws Exception {
-        CompactFileIdentifierSet deletedIdentifiers = new 
CompactFileIdentifierSet();
-        DeletedRowIdSet deletedRowIds = new DeletedRowIdSet();
-        ManifestEntryRunMergeEntry.Filter.Minor filter =
-                new ManifestEntryRunMergeEntry.Filter.Minor(
-                        deletedIdentifiers, deletedRowIds, true);
+        CollectedDeletes deletes = new CollectedDeletes(true);
         try {
-            ManifestEntryRunMergePlan plan;
-            try {
-                plan =
-                        discoverRuns(
-                                section,
-                                sortKey,
-                                partitionType,
-                                manifestFile,
-                                filter,
-                                maxNumFileHandles,
-                                manifestReadParallelism);
-            } finally {
-                deletedRowIds.releaseRangeIndex();
-            }
+            ManifestEntryRunMergePlan plan =
+                    discoverRuns(
+                            section,
+                            sortKey,
+                            partitionType,
+                            manifestFile,
+                            deletes,
+                            true,
+                            maxNumFileHandles,
+                            manifestReadParallelism);
             if (plan == null) {
                 return null;
             }
-            return plan.mergeMinorToManifest(
-                    sortKey,
-                    manifestFile,
-                    filter,
-                    deletedIdentifiers,
-                    deletedRowIds,
-                    newFilesForAbort);
+            return plan.mergeMinorToManifest(sortKey, manifestFile, 
newFilesForAbort);
         } finally {
-            deletedIdentifiers.release();
+            deletes.release();
         }
     }
 
@@ -230,12 +137,13 @@ final class ManifestEntryRunMerge {
             ManifestFileSorter.RowIdEntrySortKey sortKey,
             RowType partitionType,
             ManifestFile manifestFile,
-            ManifestEntryRunMergeEntry.Filter filter,
+            CollectedDeletes deletes,
+            boolean minor,
             int maxNumFileHandles,
             @Nullable Integer manifestReadParallelism)
             throws Exception {
-        ManifestEntryRunMergeEntry.PartitionDictionary partitions =
-                new ManifestEntryRunMergeEntry.PartitionDictionary(sortKey);
+        SortPartitionDictionary partitions =
+                new SortPartitionDictionary(sortKey::comparePartitions);
         List<ManifestEntryRunMergePlan.Source.Spec> sources = new 
ArrayList<>();
         int streamCursorCount = 0;
         long inMemoryEntries = 0;
@@ -243,36 +151,88 @@ final class ManifestEntryRunMerge {
         if (section.size() <= 1
                 || (manifestReadParallelism != null && manifestReadParallelism 
<= 1)) {
             for (ManifestFileMeta meta : section) {
-                Discovery.DiscoveredManifest manifest =
-                        discoverManifestRuns(meta, manifestFile, 
partitionType, partitions, filter);
+                CollectedDeletes discoveryDeletes =
+                        minor ? new CollectedDeletes(deletes.useRowIdFilter()) 
: deletes;
+                Discovery.DiscoveredManifest manifest;
+                try {
+                    manifest =
+                            discoverManifestRuns(
+                                    meta,
+                                    manifestFile,
+                                    partitionType,
+                                    partitions,
+                                    discoveryDeletes,
+                                    minor);
+                } catch (Exception e) {
+                    if (minor) {
+                        discoveryDeletes.release();
+                    }
+                    throw e;
+                }
+                if (minor) {
+                    try {
+                        deletes.combine(discoveryDeletes);
+                    } finally {
+                        discoveryDeletes.release();
+                    }
+                }
                 if (manifest.requiresExternalSort) {
                     return null;
                 }
                 discovered.add(manifest);
             }
         } else {
-            Function<ManifestFileMeta, List<Discovery.DiscoveredManifest>> 
reader =
-                    meta -> {
-                        try {
-                            return Collections.singletonList(
-                                    discoverManifestRuns(
-                                            meta, manifestFile, partitionType, 
partitions, filter));
-                        } catch (Exception e) {
-                            throw new RuntimeException(
-                                    "Failed to discover sorted Avro runs in " 
+ meta.fileName(), e);
-                        }
-                    };
-            for (Discovery.DiscoveredManifest manifest :
+            boolean requiresExternalSort = false;
+            Function<ManifestFileMeta, List<Pair<Discovery.DiscoveredManifest, 
CollectedDeletes>>>
+                    reader =
+                            meta -> {
+                                CollectedDeletes discoveryDeletes =
+                                        minor
+                                                ? new 
CollectedDeletes(deletes.useRowIdFilter())
+                                                : deletes;
+                                try {
+                                    return Collections.singletonList(
+                                            Pair.of(
+                                                    discoverManifestRuns(
+                                                            meta,
+                                                            manifestFile,
+                                                            partitionType,
+                                                            partitions,
+                                                            discoveryDeletes,
+                                                            minor),
+                                                    discoveryDeletes));
+                                } catch (Exception e) {
+                                    if (minor) {
+                                        discoveryDeletes.release();
+                                    }
+                                    throw new RuntimeException(
+                                            "Failed to discover sorted Avro 
runs in "
+                                                    + meta.fileName(),
+                                            e);
+                                }
+                            };
+            for (Pair<Discovery.DiscoveredManifest, CollectedDeletes> scan :
                     sequentialBatchedExecute(reader, section, 
manifestReadParallelism)) {
-                discovered.add(manifest);
+                requiresExternalSort |= scan.getLeft().requiresExternalSort;
+                if (minor) {
+                    try {
+                        deletes.combine(scan.getRight());
+                    } finally {
+                        scan.getRight().release();
+                    }
+                }
+                discovered.add(scan.getLeft());
+            }
+            // Drain every task in the bounded discovery batch before falling 
back. Returning from
+            // the lazy iterator early would leave already submitted manifest 
scans running beside
+            // the external sorter and duplicate their I/O and retained memory.
+            if (requiresExternalSort) {
+                return null;
             }
         }
         for (int manifestIndex = 0; manifestIndex < section.size(); 
manifestIndex++) {
             ManifestFileMeta meta = section.get(manifestIndex);
             Discovery.DiscoveredManifest manifest = 
discovered.get(manifestIndex);
-            if (manifest.requiresExternalSort) {
-                return null;
-            }
             if (manifest.fragmented) {
                 long entryCount = meta.numAddedFiles() + 
meta.numDeletedFiles();
                 inMemoryEntries += entryCount;
@@ -289,26 +249,34 @@ final class ManifestEntryRunMerge {
                 return null;
             }
         }
+        if (minor) {
+            deletes.toImmutable();
+        }
         partitions.finish();
         for (Discovery.DiscoveredManifest manifest : discovered) {
-            manifest.finishFiltering(filter);
+            manifest.finishFiltering(deletes, minor);
             manifest.updatePartitionRanks(partitions);
         }
-        return new ManifestEntryRunMergePlan(sources, partitions);
+        return new ManifestEntryRunMergePlan(sources, partitions, deletes, 
minor);
     }
 
     private static Discovery.DiscoveredManifest discoverManifestRuns(
             ManifestFileMeta meta,
             ManifestFile manifestFile,
             RowType partitionType,
-            ManifestEntryRunMergeEntry.PartitionDictionary partitions,
-            ManifestEntryRunMergeEntry.Filter filter)
+            SortPartitionDictionary partitions,
+            CollectedDeletes deletes,
+            boolean minor)
             throws Exception {
+        ReusableIdentifier identifier = new ReusableIdentifier();
         try (ManifestAvroReader reader =
                 manifestFile.scanAvroBlocks(meta.fileName(), meta.fileSize())) 
{
-            return discoverManifestRuns(meta, reader, partitionType, 
partitions, filter);
+            return discoverManifestRuns(
+                    meta, reader, partitionType, partitions, deletes, minor, 
identifier);
         } catch (UnsupportedOperationException unsupported) {
             return Discovery.DiscoveredManifest.requiresExternalSort();
+        } finally {
+            identifier.release();
         }
     }
 
@@ -316,26 +284,32 @@ final class ManifestEntryRunMerge {
             ManifestFileMeta meta,
             ManifestAvroReader reader,
             RowType partitionType,
-            ManifestEntryRunMergeEntry.PartitionDictionary partitions,
-            ManifestEntryRunMergeEntry.Filter filter)
+            SortPartitionDictionary partitions,
+            CollectedDeletes deletes,
+            boolean minor,
+            ReusableIdentifier identifier)
             throws Exception {
         SimpleStatsConverter partitionStatsConverter = new 
SimpleStatsConverter(partitionType);
         List<ManifestEntryRunMergePlan.Source.ManifestRunSpec> runs = new 
ArrayList<>();
         List<Discovery.BlockInfo> blocks = new ArrayList<>();
-        ManifestEntryRunMergeEntry.Key previous = new 
ManifestEntryRunMergeEntry.Key();
-        ManifestEntryRunMergeEntry.Key current = new 
ManifestEntryRunMergeEntry.Key();
+        SortKey previous = new SortKey();
+        SortKey current = new SortKey();
         boolean hasPrevious = false;
         long runStart = 0;
         long position = 0;
         long entryCount = meta.numAddedFiles() + meta.numDeletedFiles();
         boolean fragmented = false;
+        ProjectedManifestEntry entry = 
ProjectedManifestEntry.ENTRY_LAYOUT_PROJECTION.createEntry();
         while (reader.hasNext()) {
             RawBlock rawBlock = reader.next();
-            RowIterator rows = rawBlock.toRows(ENTRY_LAYOUT);
+            RowIterator rows =
+                    
rawBlock.toRows(ProjectedManifestEntry.ENTRY_LAYOUT_PROJECTION.projectedType());
             while (rows.hasNext()) {
-                GenericRow row = rows.next();
-                current.replace(row, partitions);
-                filter.observe(row, current);
+                entry.replace(rows.next());
+                current.replace(entry, partitions);
+                if (minor && entry.isDelete()) {
+                    deletes.add(entry, deletes.useRowIdFilter(), false);
+                }
                 if (fragmented) {
                     position++;
                     continue;
@@ -350,7 +324,7 @@ final class ManifestEntryRunMerge {
                                     partitionType));
                 }
                 Discovery.BlockInfo block = blocks.get(blocks.size() - 1);
-                block.collectForSort(row, current, partitions, filter);
+                block.collectForSort(entry, current, partitions, deletes, 
identifier, minor);
                 boolean inversion =
                         hasPrevious && compareDiscoveryKeys(previous, current, 
partitions) > 0;
                 if (inversion) {
@@ -374,7 +348,7 @@ final class ManifestEntryRunMerge {
                 }
                 position++;
                 if (rows.recordIndex() + 1 == rawBlock.recordCount()) {
-                    ManifestEntryRunMergeEntry.Key stableLastKey = 
current.stableCopy();
+                    SortKey stableLastKey = current.stableCopy();
                     block.finishSort(position, stableLastKey, 
partitionStatsConverter);
                     previous.copyFrom(stableLastKey);
                 } else {
@@ -413,23 +387,17 @@ final class ManifestEntryRunMerge {
     }
 
     private static int compareDiscoveryKeys(
-            ManifestEntryRunMergeEntry.Key left,
-            ManifestEntryRunMergeEntry.Key right,
-            ManifestEntryRunMergeEntry.PartitionDictionary partitions) {
+            SortKey left, SortKey right, SortPartitionDictionary partitions) {
         return compareRemainingKeys(
                 left, right, partitions.compareIds(left.partitionId, 
right.partitionId));
     }
 
-    static int compareMergeKeys(
-            ManifestEntryRunMergeEntry.Key left, 
ManifestEntryRunMergeEntry.Key right) {
+    static int compareMergeKeys(SortKey left, SortKey right) {
         return compareRemainingKeys(
                 left, right, Integer.compare(left.partitionRank, 
right.partitionRank));
     }
 
-    private static int compareRemainingKeys(
-            ManifestEntryRunMergeEntry.Key left,
-            ManifestEntryRunMergeEntry.Key right,
-            int comparison) {
+    private static int compareRemainingKeys(SortKey left, SortKey right, int 
comparison) {
         if (comparison == 0) {
             comparison = Byte.compare(left.kind, right.kind);
         }
@@ -437,10 +405,10 @@ final class ManifestEntryRunMerge {
             comparison = Long.compare(left.firstRowId, right.firstRowId);
         }
         if (comparison == 0) {
-            comparison = Long.compare(left.rangeEnd, right.rangeEnd);
+            comparison = Long.compare(left.lastRowId, right.lastRowId);
         }
         if (comparison == 0) {
-            comparison = Long.compare(left.reverseSequence, 
right.reverseSequence);
+            comparison = Long.compare(left.descendingSequenceKey, 
right.descendingSequenceKey);
         }
         if (comparison == 0) {
             comparison = compareBytes(left, right);
@@ -448,12 +416,15 @@ final class ManifestEntryRunMerge {
         return comparison;
     }
 
-    private static int compareBytes(
-            ManifestEntryRunMergeEntry.Key left, 
ManifestEntryRunMergeEntry.Key right) {
+    private static int compareBytes(SortKey left, SortKey right) {
         int minLength = Math.min(left.fileNameLength, right.fileNameLength);
         for (int i = 0; i < minLength; i++) {
-            int leftByte = left.fileNameBytes[left.fileNameOffset + i] & 0xFF;
-            int rightByte = right.fileNameBytes[right.fileNameOffset + i] & 
0xFF;
+            int leftByte =
+                    MemorySegmentUtils.getByte(left.fileNameSegments, 
left.fileNameOffset + i)
+                            & 0xFF;
+            int rightByte =
+                    MemorySegmentUtils.getByte(right.fileNameSegments, 
right.fileNameOffset + i)
+                            & 0xFF;
             if (leftByte != rightByte) {
                 return leftByte - rightByte;
             }
@@ -461,6 +432,80 @@ final class ManifestEntryRunMerge {
         return left.fileNameLength - right.fileNameLength;
     }
 
+    static final class SortKey {
+
+        int partitionId;
+        int partitionRank;
+        byte kind;
+        long firstRowId;
+        long lastRowId;
+        long descendingSequenceKey;
+        MemorySegment[] fileNameSegments;
+        int fileNameOffset;
+        int fileNameLength;
+        byte[] ownedFileNameBytes;
+        MemorySegment[] ownedFileNameSegments;
+
+        static SortKey viewOf(ProjectedManifestEntry entry, 
SortPartitionDictionary partitions) {
+            SortKey key = new SortKey();
+            key.replace(entry, partitions);
+            return key;
+        }
+
+        void replace(ProjectedManifestEntry entry, SortPartitionDictionary 
partitions) {
+            long firstRowId = entry.file().nonNullFirstRowId();
+            this.partitionId = partitions.id(entry.partitionBytes());
+            this.partitionRank = partitions.rank(partitionId);
+            this.kind = entry.kind().toByteValue();
+            this.firstRowId = firstRowId;
+            this.lastRowId = firstRowId + entry.file().rowCount() - 1L;
+            this.descendingSequenceKey = Long.MAX_VALUE - 
entry.file().maxSequenceNumber();
+            BinaryString fileName = entry.file().fileNameBinary();
+            this.fileNameSegments = fileName.getSegments();
+            this.fileNameOffset = fileName.getOffset();
+            this.fileNameLength = fileName.getSizeInBytes();
+        }
+
+        void copyFrom(SortKey key) {
+            this.partitionId = key.partitionId;
+            this.partitionRank = key.partitionRank;
+            this.kind = key.kind;
+            this.firstRowId = key.firstRowId;
+            this.lastRowId = key.lastRowId;
+            this.descendingSequenceKey = key.descendingSequenceKey;
+            ensureFileNameCapacity(key.fileNameLength);
+            MemorySegmentUtils.copyToBytes(
+                    key.fileNameSegments,
+                    key.fileNameOffset,
+                    ownedFileNameBytes,
+                    0,
+                    key.fileNameLength);
+            this.fileNameSegments = ownedFileNameSegments;
+            this.fileNameOffset = 0;
+            this.fileNameLength = key.fileNameLength;
+        }
+
+        SortKey stableCopy() {
+            SortKey copy = new SortKey();
+            copy.copyFrom(this);
+            return copy;
+        }
+
+        private void ensureFileNameCapacity(int length) {
+            if (ownedFileNameBytes == null || ownedFileNameBytes.length < 
length) {
+                ownedFileNameBytes = new byte[length];
+                ownedFileNameSegments =
+                        new MemorySegment[] 
{MemorySegment.wrap(ownedFileNameBytes)};
+            }
+        }
+
+        void clear() {
+            fileNameSegments = null;
+            ownedFileNameBytes = null;
+            ownedFileNameSegments = null;
+        }
+    }
+
     /** Results and Avro block metadata collected while discovering natural 
manifest runs. */
     static final class Discovery {
 
@@ -500,15 +545,15 @@ final class ManifestEntryRunMerge {
                         Collections.emptyList(), Collections.emptyList(), 
false, true);
             }
 
-            void 
updatePartitionRanks(ManifestEntryRunMergeEntry.PartitionDictionary partitions) 
{
+            void updatePartitionRanks(SortPartitionDictionary partitions) {
                 for (BlockInfo block : blocks) {
                     block.updatePartitionRanks(partitions);
                 }
             }
 
-            void finishFiltering(ManifestEntryRunMergeEntry.Filter filter) {
+            void finishFiltering(CollectedDeletes deletes, boolean minor) {
                 for (BlockInfo block : blocks) {
-                    block.finishFiltering(filter);
+                    block.finishFiltering(deletes, minor);
                 }
             }
         }
@@ -517,159 +562,240 @@ final class ManifestEntryRunMerge {
 
             final long ordinal;
             final long start;
-            final @Nullable ManifestEntryRunMergeEntry.Key firstKey;
-            boolean eligible;
+            final @Nullable SortKey firstKey;
             boolean sorted = true;
             long end;
-            @Nullable ManifestEntryRunMergeEntry.Key lastKey;
-            long addedFiles;
-            long deletedFiles;
-            long schemaId = Long.MIN_VALUE;
-            int minBucket = Integer.MAX_VALUE;
-            int maxBucket = Integer.MIN_VALUE;
-            int minLevel = Integer.MAX_VALUE;
-            int maxLevel = Integer.MIN_VALUE;
-            long minRowId = Long.MAX_VALUE;
-            long maxRowId = Long.MIN_VALUE;
-            final boolean singleFieldSortedPartitionStats;
-            @Nullable SimpleStatsCollector partitionStats;
-            final RowType partitionType;
-            @Nullable BinaryRow nullPartition;
-            long nullPartitionCount;
-            @Nullable BinaryRow minNonNullPartition;
-            @Nullable BinaryRow maxNonNullPartition;
-            EncodedBlockMeta metadata;
+            @Nullable SortKey lastKey;
+            long minRowId;
+            long maxRowId;
+            @Nullable BlockMetadataAccumulator metadataAccumulator;
+            @Nullable EncodedBlockMeta metadata;
 
             BlockInfo(
                     long ordinal,
                     long start,
-                    boolean eligible,
-                    ManifestEntryRunMergeEntry.Key firstKey,
+                    boolean rawBlockCopySupported,
+                    SortKey firstKey,
                     RowType partitionType) {
                 this.ordinal = ordinal;
                 this.start = start;
-                this.eligible = eligible;
                 this.firstKey = firstKey;
-                this.partitionType = partitionType;
-                this.singleFieldSortedPartitionStats =
-                        eligible && firstKey != null && 
partitionType.getFieldCount() == 1;
-                this.partitionStats =
-                        eligible && firstKey != null && 
!singleFieldSortedPartitionStats
-                                ? new SimpleStatsCollector(partitionType)
+                this.metadataAccumulator =
+                        rawBlockCopySupported && firstKey != null
+                                ? new BlockMetadataAccumulator(partitionType)
                                 : null;
             }
 
             void collectForSort(
-                    GenericRow record,
-                    ManifestEntryRunMergeEntry.Key key,
-                    ManifestEntryRunMergeEntry.PartitionDictionary partitions,
-                    ManifestEntryRunMergeEntry.Filter filter) {
-                if (!eligible) {
+                    ProjectedManifestEntry entry,
+                    SortKey key,
+                    SortPartitionDictionary partitions,
+                    CollectedDeletes deletes,
+                    ReusableIdentifier identifier,
+                    boolean minor) {
+                if (metadataAccumulator == null) {
                     return;
                 }
-                if (!filter.copyable(record, key)) {
-                    eligible = false;
-                    releasePartitionStats();
+                if (!deletes.copyable(entry, identifier, minor)) {
+                    metadataAccumulator = null;
                     return;
                 }
-                collectEntryStats(record, key);
-                BinaryRow partition = partitions.partition(key.partitionId);
-                if (singleFieldSortedPartitionStats) {
-                    if (partition.isNullAt(0)) {
-                        nullPartition = partition;
-                        nullPartitionCount++;
-                    } else {
-                        if (minNonNullPartition == null) {
-                            minNonNullPartition = partition;
-                        }
-                        maxNonNullPartition = partition;
-                    }
-                } else {
-                    partitionStats.collect(partition);
+                metadataAccumulator.collect(entry, key, 
partitions.partition(key.partitionId));
+            }
+
+            void finishSort(
+                    long end, SortKey lastKey, SimpleStatsConverter 
partitionStatsConverter) {
+                this.end = end;
+                this.lastKey = lastKey;
+                if (metadataAccumulator != null && sorted) {
+                    minRowId = metadataAccumulator.minRowId;
+                    maxRowId = metadataAccumulator.maxRowId;
+                    metadata = 
metadataAccumulator.finish(partitionStatsConverter);
+                }
+                metadataAccumulator = null;
+            }
+
+            boolean copyable(long runStart, long runEnd) {
+                return metadata != null && start >= runStart && end <= runEnd;
+            }
+
+            void finishFiltering(CollectedDeletes deletes, boolean minor) {
+                if (metadata != null
+                        && minor
+                        && (!deletes.useRowIdFilter()
+                                || deletes.intersectsRowIds(minRowId, 
maxRowId))) {
+                    metadata = null;
                 }
             }
 
-            private void collectEntryStats(GenericRow record, 
ManifestEntryRunMergeEntry.Key key) {
-                InternalRow file = ManifestEntryRunMergeEntry.file(record);
-                if (key.kind == FileKind.ADD.toByteValue()) {
+            void updatePartitionRanks(SortPartitionDictionary partitions) {
+                checkState(firstKey != null && lastKey != null, "Manifest 
block has no sort keys.");
+                firstKey.partitionRank = partitions.rank(firstKey.partitionId);
+                lastKey.partitionRank = partitions.rank(lastKey.partitionId);
+            }
+        }
+
+        /** Mutable statistics retained only while the current Avro block is 
being inspected. */
+        private static final class BlockMetadataAccumulator {
+
+            private long addedFiles;
+            private long deletedFiles;
+            private long schemaId = Long.MIN_VALUE;
+            private int minBucket = Integer.MAX_VALUE;
+            private int maxBucket = Integer.MIN_VALUE;
+            private int minLevel = Integer.MAX_VALUE;
+            private int maxLevel = Integer.MIN_VALUE;
+            private long minRowId = Long.MAX_VALUE;
+            private long maxRowId = Long.MIN_VALUE;
+            private final BlockPartitionStats partitionStats;
+
+            private BlockMetadataAccumulator(RowType partitionType) {
+                this.partitionStats = new BlockPartitionStats(partitionType);
+            }
+
+            private void collect(ProjectedManifestEntry entry, SortKey key, 
BinaryRow partition) {
+                if (entry.isAdd()) {
                     addedFiles++;
                 } else {
                     deletedFiles++;
                 }
-                schemaId = Math.max(schemaId, file.getLong(SCHEMA_ID));
-                int bucket = record.getInt(BUCKET);
+                schemaId = Math.max(schemaId, entry.file().schemaId());
+                int bucket = entry.bucket();
                 minBucket = Math.min(minBucket, bucket);
                 maxBucket = Math.max(maxBucket, bucket);
-                int level = file.getInt(LEVEL);
+                int level = entry.file().level();
                 minLevel = Math.min(minLevel, level);
                 maxLevel = Math.max(maxLevel, level);
                 minRowId = Math.min(minRowId, key.firstRowId);
-                maxRowId = Math.max(maxRowId, key.rangeEnd);
+                maxRowId = Math.max(maxRowId, key.lastRowId);
+                partitionStats.collect(partition);
             }
 
-            void finishSort(
-                    long end,
-                    ManifestEntryRunMergeEntry.Key lastKey,
-                    SimpleStatsConverter partitionStatsConverter) {
-                this.end = end;
-                this.lastKey = lastKey;
-                if (eligible && sorted) {
-                    SimpleStats encodedPartitionStats;
-                    if (singleFieldSortedPartitionStats) {
-                        BinaryRow min =
-                                minNonNullPartition == null ? nullPartition : 
minNonNullPartition;
-                        BinaryRow max =
-                                maxNonNullPartition == null ? nullPartition : 
maxNonNullPartition;
-                        checkState(min != null && max != null, "Manifest block 
has no partition.");
-                        encodedPartitionStats =
-                                new SimpleStats(
-                                        min,
-                                        max,
-                                        BinaryArray.fromLongArray(new Long[] 
{nullPartitionCount}));
-                    } else {
-                        checkState(
-                                partitionStats != null, "Manifest block has no 
partition stats.");
-                        encodedPartitionStats =
-                                
partitionStatsConverter.toBinaryAllMode(partitionStats.extract());
+            private EncodedBlockMeta finish(SimpleStatsConverter 
partitionStatsConverter) {
+                return new EncodedBlockMeta(
+                        addedFiles,
+                        deletedFiles,
+                        schemaId,
+                        minBucket,
+                        maxBucket,
+                        minLevel,
+                        maxLevel,
+                        minRowId,
+                        maxRowId,
+                        partitionStats.finish(partitionStatsConverter));
+            }
+        }
+
+        /** Partition statistics for one sorted Avro block. */
+        private static final class BlockPartitionStats {
+
+            private final boolean singleField;
+            private final @Nullable SimpleStatsCollector collector;
+            private @Nullable BinaryRow nullPartition;
+            private @Nullable BinaryRow minNonNullPartition;
+            private @Nullable BinaryRow maxNonNullPartition;
+            private long nullCount;
+
+            private BlockPartitionStats(RowType partitionType) {
+                this.singleField = partitionType.getFieldCount() == 1;
+                this.collector = singleField ? null : new 
SimpleStatsCollector(partitionType);
+            }
+
+            private void collect(BinaryRow partition) {
+                if (!singleField) {
+                    checkState(collector != null, "Manifest block has no 
partition collector.");
+                    collector.collect(partition);
+                    return;
+                }
+                if (partition.isNullAt(0)) {
+                    nullPartition = partition;
+                    nullCount++;
+                } else {
+                    if (minNonNullPartition == null) {
+                        minNonNullPartition = partition;
                     }
-                    metadata =
-                            new EncodedBlockMeta(
-                                    addedFiles,
-                                    deletedFiles,
-                                    schemaId,
-                                    minBucket,
-                                    maxBucket,
-                                    minLevel,
-                                    maxLevel,
-                                    minRowId,
-                                    maxRowId,
-                                    encodedPartitionStats);
+                    maxNonNullPartition = partition;
                 }
-                releasePartitionStats();
             }
 
-            private void releasePartitionStats() {
-                partitionStats = null;
-                nullPartition = null;
-                minNonNullPartition = null;
-                maxNonNullPartition = null;
+            private SimpleStats finish(SimpleStatsConverter converter) {
+                if (!singleField) {
+                    checkState(collector != null, "Manifest block has no 
partition collector.");
+                    return converter.toBinaryAllMode(collector.extract());
+                }
+                BinaryRow min = minNonNullPartition == null ? nullPartition : 
minNonNullPartition;
+                BinaryRow max = maxNonNullPartition == null ? nullPartition : 
maxNonNullPartition;
+                checkState(min != null && max != null, "Manifest block has no 
partition.");
+                return new SimpleStats(min, max, BinaryArray.fromLongArray(new 
Long[] {nullCount}));
             }
+        }
+    }
 
-            boolean copyable(long runStart, long runEnd) {
-                return metadata != null && start >= runStart && end <= runEnd;
-            }
+    /** Concurrent partition dictionary and ordering used only by manifest run 
merge. */
+    static final class SortPartitionDictionary {
 
-            void finishFiltering(ManifestEntryRunMergeEntry.Filter filter) {
-                if (metadata != null && 
!filter.copyableAfterDiscovery(minRowId, maxRowId)) {
-                    metadata = null;
+        private final Comparator<BinaryRow> comparator;
+        private final PartitionDictionary partitions = new 
PartitionDictionary();
+        private final Map<ByteArrayKey, Integer> ids = new 
ConcurrentHashMap<>();
+        private final ThreadLocal<ByteArrayLookupKey> lookup =
+                ThreadLocal.withInitial(ByteArrayLookupKey::new);
+        private int[] ranks;
+
+        SortPartitionDictionary(Comparator<BinaryRow> comparator) {
+            this.comparator = comparator;
+        }
+
+        int id(byte[] bytes) {
+            ByteArrayLookupKey lookupKey = lookup.get();
+            lookupKey.reset(bytes);
+            try {
+                Integer existing = ids.get(lookupKey);
+                if (existing != null) {
+                    return existing;
                 }
+                synchronized (this) {
+                    existing = ids.get(lookupKey);
+                    if (existing != null) {
+                        return existing;
+                    }
+                    checkState(ranks == null, "Manifest scan found an unknown 
partition.");
+                    byte[] canonical = Arrays.copyOf(bytes, bytes.length);
+                    int id = partitions.id(canonical);
+                    ids.put(new ByteArrayKey(canonical), id);
+                    return id;
+                }
+            } finally {
+                lookupKey.clear();
             }
+        }
 
-            void 
updatePartitionRanks(ManifestEntryRunMergeEntry.PartitionDictionary partitions) 
{
-                checkState(firstKey != null && lastKey != null, "Manifest 
block has no sort keys.");
-                firstKey.partitionRank = partitions.rank(firstKey.partitionId);
-                lastKey.partitionRank = partitions.rank(lastKey.partitionId);
+        void finish() {
+            int partitionCount = ids.size();
+            List<Integer> order = new ArrayList<>(partitionCount);
+            for (int id = 0; id < partitionCount; id++) {
+                order.add(id);
+            }
+            order.sort((left, right) -> compareIds(left, right));
+            ranks = new int[partitionCount];
+            int rank = 0;
+            for (int position = 0; position < order.size(); position++) {
+                if (position > 0 && compareIds(order.get(position - 1), 
order.get(position)) != 0) {
+                    rank++;
+                }
+                ranks[order.get(position)] = rank;
             }
         }
+
+        int compareIds(int left, int right) {
+            return comparator.compare(partitions.partition(left), 
partitions.partition(right));
+        }
+
+        int rank(int id) {
+            return ranks == null ? 0 : ranks[id];
+        }
+
+        BinaryRow partition(int id) {
+            return partitions.partition(id);
+        }
     }
 }
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java
 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java
deleted file mode 100644
index 5e8c318e9f..0000000000
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergeEntry.java
+++ /dev/null
@@ -1,339 +0,0 @@
-/*
- * 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.paimon.operation;
-
-import org.apache.paimon.data.BinaryRow;
-import org.apache.paimon.data.BinaryString;
-import org.apache.paimon.data.GenericRow;
-import org.apache.paimon.data.InternalRow;
-import org.apache.paimon.manifest.CompactFileIdentifierSet;
-import org.apache.paimon.manifest.DeletedRowIdSet;
-import org.apache.paimon.manifest.FileEntry.ReusableIdentifier;
-import org.apache.paimon.manifest.FileKind;
-import org.apache.paimon.manifest.ProjectedManifestEntry;
-import org.apache.paimon.memory.MemorySegmentUtils;
-import org.apache.paimon.utils.ByteArrayKey;
-import org.apache.paimon.utils.ByteArrayLookupKey;
-import org.apache.paimon.utils.SerializationUtils;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-import static org.apache.paimon.utils.Preconditions.checkState;
-
-/** Entry-level state shared by manifest run discovery and merge execution. */
-final class ManifestEntryRunMergeEntry {
-
-    private ManifestEntryRunMergeEntry() {}
-
-    static final class Key {
-
-        int partitionId;
-        int partitionRank;
-        byte kind;
-        boolean hasRowId;
-        long firstRowId;
-        long rangeEnd;
-        long reverseSequence;
-        byte[] fileNameBytes;
-        int fileNameOffset;
-        int fileNameLength;
-
-        static Key viewOf(ProjectedManifestEntry entry, PartitionDictionary 
partitions) {
-            Key key = new Key();
-            key.replace(entry, partitions);
-            return key;
-        }
-
-        void replace(ProjectedManifestEntry entry, PartitionDictionary 
partitions) {
-            long firstRowId = entry.file().nonNullFirstRowId();
-            this.partitionId = partitions.id(entry.partitionBytes());
-            this.partitionRank = partitions.rank(partitionId);
-            this.kind = entry.kind().toByteValue();
-            this.hasRowId = true;
-            this.firstRowId = firstRowId;
-            this.rangeEnd = firstRowId + entry.file().rowCount() - 1L;
-            this.reverseSequence = Long.MAX_VALUE - 
entry.file().maxSequenceNumber();
-            this.fileNameBytes = entry.file().fileNameBinary().toBytes();
-            this.fileNameOffset = 0;
-            this.fileNameLength = fileNameBytes.length;
-        }
-
-        void replace(GenericRow record, PartitionDictionary partitions) {
-            InternalRow file = file(record);
-            checkState(
-                    !file.isNullAt(ManifestEntryRunMerge.FIRST_ROW_ID),
-                    "First row id should not be null.");
-            this.partitionId = 
partitions.id(record.getBinary(ManifestEntryRunMerge.PARTITION));
-            this.partitionRank = partitions.rank(partitionId);
-            this.kind = record.getByte(ManifestEntryRunMerge.KIND);
-            this.hasRowId = true;
-            this.firstRowId = file.getLong(ManifestEntryRunMerge.FIRST_ROW_ID);
-            this.rangeEnd = firstRowId + 
file.getLong(ManifestEntryRunMerge.ROW_COUNT) - 1L;
-            this.reverseSequence =
-                    Long.MAX_VALUE - 
file.getLong(ManifestEntryRunMerge.MAX_SEQUENCE_NUMBER);
-            BinaryString fileName = 
file.getString(ManifestEntryRunMerge.FILE_NAME);
-            this.fileNameBytes =
-                    MemorySegmentUtils.copyToBytes(
-                            fileName.getSegments(),
-                            fileName.getOffset(),
-                            fileName.getSizeInBytes());
-            this.fileNameOffset = 0;
-            this.fileNameLength = fileNameBytes.length;
-        }
-
-        void replaceForCompaction(GenericRow record) {
-            InternalRow file = file(record);
-            this.kind = record.getByte(ManifestEntryRunMerge.KIND);
-            this.hasRowId = !file.isNullAt(ManifestEntryRunMerge.FIRST_ROW_ID);
-            if (hasRowId) {
-                this.firstRowId = 
file.getLong(ManifestEntryRunMerge.FIRST_ROW_ID);
-                this.rangeEnd = firstRowId + 
file.getLong(ManifestEntryRunMerge.ROW_COUNT) - 1L;
-            }
-        }
-
-        void copyFrom(Key key) {
-            this.partitionId = key.partitionId;
-            this.partitionRank = key.partitionRank;
-            this.kind = key.kind;
-            this.hasRowId = key.hasRowId;
-            this.firstRowId = key.firstRowId;
-            this.rangeEnd = key.rangeEnd;
-            this.reverseSequence = key.reverseSequence;
-            this.fileNameBytes = key.fileNameBytes;
-            this.fileNameOffset = key.fileNameOffset;
-            this.fileNameLength = key.fileNameLength;
-        }
-
-        Key stableCopy() {
-            Key copy = new Key();
-            copy.copyFrom(this);
-            copy.fileNameBytes =
-                    Arrays.copyOfRange(
-                            fileNameBytes, fileNameOffset, fileNameOffset + 
fileNameLength);
-            copy.fileNameOffset = 0;
-            return copy;
-        }
-
-        void clear() {
-            fileNameBytes = null;
-        }
-    }
-
-    /** Interns variable-width partition bytes once and assigns 
comparator-compatible ranks. */
-    static final class PartitionDictionary {
-
-        final ManifestFileSorter.RowIdEntrySortKey sortKey;
-        final Map<ByteArrayKey, Integer> ids = new ConcurrentHashMap<>();
-        final ThreadLocal<ByteArrayLookupKey> lookup =
-                ThreadLocal.withInitial(ByteArrayLookupKey::new);
-        volatile BinaryRow[] partitions = new BinaryRow[16];
-        int partitionCount;
-        int[] ranks;
-
-        PartitionDictionary(ManifestFileSorter.RowIdEntrySortKey sortKey) {
-            this.sortKey = sortKey;
-        }
-
-        PartitionDictionary() {
-            this.sortKey = null;
-        }
-
-        int id(byte[] bytes) {
-            return id(bytes, 0, bytes.length);
-        }
-
-        int id(byte[] bytes, int offset, int length) {
-            ByteArrayLookupKey lookupKey = lookup.get();
-            lookupKey.reset(bytes, offset, length);
-            try {
-                Integer existing = ids.get(lookupKey);
-                if (existing != null) {
-                    return existing;
-                }
-                synchronized (this) {
-                    existing = ids.get(lookupKey);
-                    if (existing != null) {
-                        return existing;
-                    }
-                    checkState(ranks == null, "Full manifest scan found an 
unknown partition.");
-                    byte[] canonical = Arrays.copyOfRange(bytes, offset, 
offset + length);
-                    int id = partitionCount;
-                    if (id == partitions.length) {
-                        partitions = Arrays.copyOf(partitions, 
partitions.length << 1);
-                    }
-                    partitions[id] = 
SerializationUtils.deserializeBinaryRow(canonical);
-                    ids.put(new ByteArrayKey(canonical), id);
-                    partitionCount = id + 1;
-                    return id;
-                }
-            } finally {
-                lookupKey.clear();
-            }
-        }
-
-        int compareIds(int left, int right) {
-            checkState(sortKey != null, "Partition dictionary has no sort 
key.");
-            return sortKey.comparePartitions(partitions[left], 
partitions[right]);
-        }
-
-        void finish() {
-            List<Integer> order = new ArrayList<>(partitionCount);
-            for (int id = 0; id < partitionCount; id++) {
-                order.add(id);
-            }
-            order.sort((left, right) -> compareIds(left, right));
-            ranks = new int[partitionCount];
-            int rank = 0;
-            for (int position = 0; position < order.size(); position++) {
-                if (position > 0 && compareIds(order.get(position - 1), 
order.get(position)) != 0) {
-                    rank++;
-                }
-                ranks[order.get(position)] = rank;
-            }
-        }
-
-        int rank(int id) {
-            return ranks == null ? 0 : ranks[id];
-        }
-
-        BinaryRow partition(int id) {
-            return partitions[id];
-        }
-    }
-
-    static class Filter {
-
-        final CompactFileIdentifierSet deletedIdentifiers;
-        final DeletedRowIdSet deletedRowIds;
-        final boolean useRowIdFilter;
-        final ThreadLocal<IdentifierEncoder> identifier =
-                ThreadLocal.withInitial(IdentifierEncoder::new);
-
-        Filter(
-                CompactFileIdentifierSet deletedIdentifiers,
-                DeletedRowIdSet deletedRowIds,
-                boolean useRowIdFilter) {
-            this.deletedIdentifiers = deletedIdentifiers;
-            this.deletedRowIds = deletedRowIds;
-            this.useRowIdFilter = useRowIdFilter;
-        }
-
-        boolean include(ProjectedManifestEntry entry) {
-            return entry.isAdd() && !deletedIdentifiers.contains(entry);
-        }
-
-        boolean include(GenericRow record, Key key) {
-            return key.kind == FileKind.ADD.toByteValue() && 
!isDeleted(record, key);
-        }
-
-        boolean copyable(GenericRow record, Key key) {
-            return include(record, key);
-        }
-
-        void observe(GenericRow record, Key key) {}
-
-        boolean copyableAfterDiscovery(long minRowId, long maxRowId) {
-            return true;
-        }
-
-        ReusableIdentifier identifier(GenericRow record) {
-            return identifier.get().replace(record);
-        }
-
-        boolean isDeleted(GenericRow record, Key key) {
-            // RowID is only a cheap negative filter. The complete identifier 
remains the
-            // authoritative match, and is also sufficient for manifests which 
predate RowID.
-            if (useRowIdFilter) {
-                checkState(key.hasRowId, "First row id should not be null.");
-                if (!deletedRowIds.contains(key.firstRowId)) {
-                    return false;
-                }
-            }
-            return deletedIdentifiers.contains(identifier(record));
-        }
-
-        static final class Minor extends Filter {
-
-            Minor(
-                    CompactFileIdentifierSet deletedIdentifiers,
-                    DeletedRowIdSet deletedRowIds,
-                    boolean useRowIdFilter) {
-                super(deletedIdentifiers, deletedRowIds, useRowIdFilter);
-            }
-
-            @Override
-            boolean include(ProjectedManifestEntry entry) {
-                return true;
-            }
-
-            @Override
-            boolean include(GenericRow record, Key key) {
-                return true;
-            }
-
-            @Override
-            boolean copyable(GenericRow record, Key key) {
-                return key.kind == FileKind.ADD.toByteValue();
-            }
-
-            @Override
-            void observe(GenericRow record, Key key) {
-                if (key.kind != FileKind.DELETE.toByteValue()) {
-                    return;
-                }
-                ReusableIdentifier reusable = identifier(record);
-                synchronized (this) {
-                    deletedIdentifiers.add(reusable);
-                    if (useRowIdFilter) {
-                        checkState(key.hasRowId, "First row id should not be 
null.");
-                        deletedRowIds.add(key.firstRowId);
-                    }
-                }
-            }
-
-            @Override
-            boolean copyableAfterDiscovery(long minRowId, long maxRowId) {
-                // A DELETE preserves the deleted ADD's globally unique first 
RowID. A range hit may
-                // be a false positive and only disables block copying; a miss 
proves the block has
-                // no deleted ADD.
-                return useRowIdFilter && !deletedRowIds.intersects(minRowId, 
maxRowId);
-            }
-        }
-
-        private static final class IdentifierEncoder {
-
-            final ProjectedManifestEntry entry =
-                    
ProjectedManifestEntry.Projection.create(ManifestEntryRunMerge.ENTRY_LAYOUT)
-                            .createEntry();
-            final ReusableIdentifier identifier = new ReusableIdentifier();
-
-            ReusableIdentifier replace(GenericRow record) {
-                return identifier.replaceWithPartition(entry.replace(record));
-            }
-        }
-    }
-
-    static InternalRow file(GenericRow record) {
-        return record.getRow(ManifestEntryRunMerge.FILE, 
ManifestEntryRunMerge.FILE_FIELD_COUNT);
-    }
-}
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java
 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java
index eb7702b8aa..997bfb362b 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java
@@ -23,8 +23,8 @@ import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.data.InternalRow;
 import org.apache.paimon.data.serializer.InternalRowSerializer;
 import org.apache.paimon.format.avro.AvroRawBlock;
+import org.apache.paimon.manifest.CollectedDeletes;
 import org.apache.paimon.manifest.CompactFileIdentifierSet;
-import org.apache.paimon.manifest.DeletedRowIdSet;
 import org.apache.paimon.manifest.FileEntry.ReusableIdentifier;
 import org.apache.paimon.manifest.FileKind;
 import org.apache.paimon.manifest.ManifestAvroReader;
@@ -53,25 +53,31 @@ import static 
org.apache.paimon.utils.Preconditions.checkState;
 final class ManifestEntryRunMergePlan {
 
     final List<Source.Spec> sources;
-    final ManifestEntryRunMergeEntry.PartitionDictionary partitions;
+    final ManifestEntryRunMerge.SortPartitionDictionary partitions;
+    final CollectedDeletes deletes;
+    final boolean minor;
 
     ManifestEntryRunMergePlan(
-            List<Source.Spec> sources, 
ManifestEntryRunMergeEntry.PartitionDictionary partitions) {
+            List<Source.Spec> sources,
+            ManifestEntryRunMerge.SortPartitionDictionary partitions,
+            CollectedDeletes deletes,
+            boolean minor) {
         this.sources = sources;
         this.partitions = partitions;
+        this.deletes = deletes;
+        this.minor = minor;
     }
 
     List<ManifestFileMeta> mergeToManifest(
             ManifestFileSorter.RowIdEntrySortKey sortKey,
             ManifestFile manifestFile,
-            ManifestEntryRunMergeEntry.Filter filter,
             List<ManifestFileMeta> newFilesForAbort)
             throws Exception {
         List<Cursor> cursors = new ArrayList<>(sources.size());
         Exception failure = null;
         try {
             for (Source.Spec source : sources) {
-                Cursor cursor = source.open(manifestFile, sortKey, filter, 
partitions);
+                Cursor cursor = source.open(manifestFile, sortKey, deletes, 
minor, partitions);
                 cursors.add(cursor);
                 cursor.advance();
             }
@@ -100,16 +106,13 @@ final class ManifestEntryRunMergePlan {
     Pair<List<ManifestFileMeta>, List<ManifestFileMeta>> mergeMinorToManifest(
             ManifestFileSorter.RowIdEntrySortKey sortKey,
             ManifestFile manifestFile,
-            ManifestEntryRunMergeEntry.Filter filter,
-            CompactFileIdentifierSet deletedIdentifiers,
-            DeletedRowIdSet deletedRowIds,
             List<ManifestFileMeta> newFilesForAbort)
             throws Exception {
         List<Cursor> cursors = new ArrayList<>(sources.size());
         Exception failure = null;
         try {
             for (Source.Spec source : sources) {
-                Cursor cursor = source.open(manifestFile, sortKey, filter, 
partitions);
+                Cursor cursor = source.open(manifestFile, sortKey, deletes, 
minor, partitions);
                 cursors.add(cursor);
                 cursor.advance();
             }
@@ -118,8 +121,7 @@ final class ManifestEntryRunMergePlan {
                 return Pair.of(Collections.emptyList(), 
Collections.emptyList());
             }
             Pair<List<ManifestFileMeta>, List<ManifestFileMeta>> files =
-                    writeMinorSelected(
-                            selectionTree, manifestFile, deletedIdentifiers, 
deletedRowIds);
+                    writeMinorSelected(selectionTree, manifestFile, deletes);
             newFilesForAbort.addAll(files.getLeft());
             newFilesForAbort.addAll(files.getRight());
             return files;
@@ -169,10 +171,7 @@ final class ManifestEntryRunMergePlan {
     }
 
     private static Pair<List<ManifestFileMeta>, List<ManifestFileMeta>> 
writeMinorSelected(
-            SelectionTree selectionTree,
-            ManifestFile manifestFile,
-            CompactFileIdentifierSet deletedIdentifiers,
-            DeletedRowIdSet deletedRowIds)
+            SelectionTree selectionTree, ManifestFile manifestFile, 
CollectedDeletes deletes)
             throws Exception {
         ManifestAvroWriter addWriter = manifestFile.createAvroWriter();
         ManifestAvroWriter deleteWriter = manifestFile.createAvroWriter();
@@ -192,15 +191,11 @@ final class ManifestEntryRunMergePlan {
 
                 cursor.materializeCurrent();
                 if (cursor.key().kind == FileKind.ADD.toByteValue()) {
-                    if (!deletedRowIds.contains(cursor.key().firstRowId)) {
-                        writeCurrent(addWriter, cursor);
+                    ReusableIdentifier identifier = cursor.identifier();
+                    if (deletes.isDeleted(cursor.current(), identifier)) {
+                        matchedEntries.add(identifier);
                     } else {
-                        ReusableIdentifier identifier = cursor.identifier();
-                        if (deletedIdentifiers.contains(identifier)) {
-                            matchedEntries.add(identifier);
-                        } else {
-                            writeCurrent(addWriter, cursor);
-                        }
+                        writeCurrent(addWriter, cursor);
                     }
                 } else {
                     ReusableIdentifier identifier = cursor.identifier();
@@ -270,8 +265,9 @@ final class ManifestEntryRunMergePlan {
             Cursor open(
                     ManifestFile manifestFile,
                     ManifestFileSorter.RowIdEntrySortKey sortKey,
-                    ManifestEntryRunMergeEntry.Filter filter,
-                    ManifestEntryRunMergeEntry.PartitionDictionary partitions)
+                    CollectedDeletes deletes,
+                    boolean minor,
+                    ManifestEntryRunMerge.SortPartitionDictionary partitions)
                     throws Exception;
         }
 
@@ -311,11 +307,12 @@ final class ManifestEntryRunMergePlan {
             public Cursor open(
                     ManifestFile manifestFile,
                     ManifestFileSorter.RowIdEntrySortKey sortKey,
-                    ManifestEntryRunMergeEntry.Filter filter,
-                    ManifestEntryRunMergeEntry.PartitionDictionary partitions)
+                    CollectedDeletes deletes,
+                    boolean minor,
+                    ManifestEntryRunMerge.SortPartitionDictionary partitions)
                     throws Exception {
                 return new PrimitiveManifestRunCursor(
-                        manifestFile, meta, start, end, blocks, filter, 
partitions);
+                        manifestFile, meta, start, end, blocks, deletes, 
minor, partitions);
             }
         }
 
@@ -331,10 +328,12 @@ final class ManifestEntryRunMergePlan {
             public Cursor open(
                     ManifestFile manifestFile,
                     ManifestFileSorter.RowIdEntrySortKey sortKey,
-                    ManifestEntryRunMergeEntry.Filter filter,
-                    ManifestEntryRunMergeEntry.PartitionDictionary partitions)
+                    CollectedDeletes deletes,
+                    boolean minor,
+                    ManifestEntryRunMerge.SortPartitionDictionary partitions)
                     throws Exception {
-                return new InMemoryManifestCursor(manifestFile, meta, sortKey, 
filter, partitions);
+                return new InMemoryManifestCursor(
+                        manifestFile, meta, sortKey, deletes, minor, 
partitions);
             }
         }
     }
@@ -351,7 +350,7 @@ final class ManifestEntryRunMergePlan {
         @Nullable
         EncodedEntry metadata();
 
-        ManifestEntryRunMergeEntry.Key key();
+        ManifestEntryRunMerge.SortKey key();
 
         @Nullable
         ByteBuffer encodedRecord();
@@ -366,7 +365,7 @@ final class ManifestEntryRunMergePlan {
             return false;
         }
 
-        default ManifestEntryRunMergeEntry.Key blockLastKey() {
+        default ManifestEntryRunMerge.SortKey blockLastKey() {
             throw new UnsupportedOperationException();
         }
 
@@ -392,10 +391,16 @@ final class ManifestEntryRunMergePlan {
 
         final ManifestAvroReader reader;
         final boolean encodedRecordsCompatible;
-        final ManifestEntryRunMergeEntry.Filter filter;
-        final ManifestEntryRunMergeEntry.PartitionDictionary partitions;
-        final ManifestEntryRunMergeEntry.Key key = new 
ManifestEntryRunMergeEntry.Key();
+        final CollectedDeletes deletes;
+        final boolean minor;
+        final ManifestEntryRunMerge.SortPartitionDictionary partitions;
+        final ManifestEntryRunMerge.SortKey key = new 
ManifestEntryRunMerge.SortKey();
         final EncodedEntry metadata = new EncodedEntry();
+        final ProjectedManifestEntry projectedEntry =
+                ProjectedManifestEntry.ENTRY_LAYOUT_PROJECTION.createEntry();
+        final ProjectedManifestEntry fullEntry =
+                ProjectedManifestEntry.fullProjection().createEntry();
+        final ReusableIdentifier identifier = new ReusableIdentifier();
         final List<ManifestEntryRunMerge.Discovery.BlockInfo> blocks;
         final long runStart;
         final long runEnd;
@@ -406,10 +411,8 @@ final class ManifestEntryRunMergePlan {
         boolean current;
         @Nullable RawBlock currentRawBlock;
         @Nullable RowIterator currentRows;
-        @Nullable GenericRow currentRow;
         @Nullable GenericRow currentSourceRow;
-        @Nullable GenericRow compactRow;
-        @Nullable GenericRow compactFile;
+        @Nullable ProjectedManifestEntry currentEntry;
         @Nullable ManifestEntryRunMerge.Discovery.BlockInfo currentBlock;
         boolean closed;
 
@@ -419,17 +422,14 @@ final class ManifestEntryRunMergePlan {
                 long start,
                 long end,
                 List<ManifestEntryRunMerge.Discovery.BlockInfo> blocks,
-                ManifestEntryRunMergeEntry.Filter filter,
-                ManifestEntryRunMergeEntry.PartitionDictionary partitions)
+                CollectedDeletes deletes,
+                boolean minor,
+                ManifestEntryRunMerge.SortPartitionDictionary partitions)
                 throws Exception {
             this.reader = manifestFile.scanAvroBlocks(meta.fileName(), 
meta.fileSize());
             this.encodedRecordsCompatible = reader.rawBlockCopySupported();
-            if (!encodedRecordsCompatible) {
-                this.compactRow =
-                        new 
GenericRow(ManifestEntryRunMerge.ENTRY_LAYOUT.getFieldCount());
-                this.compactFile = new 
GenericRow(ManifestEntryRunMerge.FILE_FIELD_COUNT);
-            }
-            this.filter = filter;
+            this.deletes = deletes;
+            this.minor = minor;
             this.partitions = partitions;
             this.blocks = blocks;
             this.runStart = start;
@@ -469,24 +469,22 @@ final class ManifestEntryRunMergePlan {
                         currentRows != null && currentRows.hasNext(),
                         "Manifest block ends before its discovered boundary.");
                 currentSourceRow = currentRows.next();
-                currentRow =
+                currentEntry =
                         encodedRecordsCompatible
-                                ? currentSourceRow
-                                : ManifestEntryRunMerge.projectEntryLayout(
-                                        currentSourceRow, compactRow, 
compactFile);
+                                ? projectedEntry.replace(currentSourceRow)
+                                : fullEntry.replace(currentSourceRow);
                 decodedRemaining--;
-                key.replace(currentRow, partitions);
-                if (filter.include(currentRow, key)) {
+                key.replace(currentEntry, partitions);
+                if (minor || deletes.copyable(currentEntry, identifier, 
false)) {
                     current = true;
-                    InternalRow file = 
ManifestEntryRunMergeEntry.file(currentRow);
                     metadata.replace(
                             key.kind,
                             partitions.partition(key.partitionId),
-                            currentRow.getInt(ManifestEntryRunMerge.BUCKET),
-                            file.getInt(ManifestEntryRunMerge.LEVEL),
-                            file.getLong(ManifestEntryRunMerge.SCHEMA_ID),
+                            currentEntry.bucket(),
+                            currentEntry.file().level(),
+                            currentEntry.file().schemaId(),
                             key.firstRowId,
-                            file.getLong(ManifestEntryRunMerge.ROW_COUNT));
+                            currentEntry.file().rowCount());
                     return true;
                 }
             }
@@ -496,8 +494,8 @@ final class ManifestEntryRunMergePlan {
             rawBlock = false;
             current = false;
             currentRows = null;
-            currentRow = null;
             currentSourceRow = null;
+            currentEntry = null;
             while (blockIndex < blocks.size()) {
                 ManifestEntryRunMerge.Discovery.BlockInfo info = 
blocks.get(blockIndex);
                 if (info.start >= runEnd) {
@@ -524,7 +522,8 @@ final class ManifestEntryRunMergePlan {
                 currentRows =
                         currentRawBlock.toRows(
                                 encodedRecordsCompatible
-                                        ? ManifestEntryRunMerge.ENTRY_LAYOUT
+                                        ? 
ProjectedManifestEntry.ENTRY_LAYOUT_PROJECTION
+                                                .projectedType()
                                         : ManifestEntry.MANIFEST_ROW_TYPE);
                 for (long i = 0; i < prefix; i++) {
                     checkState(
@@ -548,7 +547,7 @@ final class ManifestEntryRunMergePlan {
 
         @Override
         public ProjectedManifestEntry current() {
-            return null;
+            return current ? currentEntry : null;
         }
 
         @Override
@@ -557,7 +556,7 @@ final class ManifestEntryRunMergePlan {
         }
 
         @Override
-        public ManifestEntryRunMergeEntry.Key key() {
+        public ManifestEntryRunMerge.SortKey key() {
             return key;
         }
 
@@ -574,7 +573,7 @@ final class ManifestEntryRunMergePlan {
         @Override
         public ReusableIdentifier identifier() {
             checkState(current, "Manifest entry has not been materialized.");
-            return filter.identifier(currentRow);
+            return identifier.replaceWithPartition(currentEntry);
         }
 
         @Override
@@ -583,7 +582,7 @@ final class ManifestEntryRunMergePlan {
         }
 
         @Override
-        public ManifestEntryRunMergeEntry.Key blockLastKey() {
+        public ManifestEntryRunMerge.SortKey blockLastKey() {
             return currentBlock.lastKey;
         }
 
@@ -617,30 +616,28 @@ final class ManifestEntryRunMergePlan {
             currentRows =
                     currentRawBlock.toRows(
                             encodedRecordsCompatible
-                                    ? ManifestEntryRunMerge.ENTRY_LAYOUT
+                                    ? 
ProjectedManifestEntry.ENTRY_LAYOUT_PROJECTION.projectedType()
                                     : ManifestEntry.MANIFEST_ROW_TYPE);
             checkState(currentRows.hasNext(), "Manifest block cannot be 
decompressed.");
             currentSourceRow = currentRows.next();
-            currentRow =
+            currentEntry =
                     encodedRecordsCompatible
-                            ? currentSourceRow
-                            : ManifestEntryRunMerge.projectEntryLayout(
-                                    currentSourceRow, compactRow, compactFile);
+                            ? projectedEntry.replace(currentSourceRow)
+                            : fullEntry.replace(currentSourceRow);
             decodedRemaining--;
-            key.replace(currentRow, partitions);
+            key.replace(currentEntry, partitions);
             checkState(
-                    filter.include(currentRow, key),
+                    minor || deletes.copyable(currentEntry, identifier, false),
                     "Copyable manifest block contains a filtered entry.");
             current = true;
-            InternalRow file = ManifestEntryRunMergeEntry.file(currentRow);
             metadata.replace(
                     key.kind,
                     partitions.partition(key.partitionId),
-                    currentRow.getInt(ManifestEntryRunMerge.BUCKET),
-                    file.getInt(ManifestEntryRunMerge.LEVEL),
-                    file.getLong(ManifestEntryRunMerge.SCHEMA_ID),
+                    currentEntry.bucket(),
+                    currentEntry.file().level(),
+                    currentEntry.file().schemaId(),
                     key.firstRowId,
-                    file.getLong(ManifestEntryRunMerge.ROW_COUNT));
+                    currentEntry.file().rowCount());
             blockIndex++;
         }
 
@@ -653,10 +650,11 @@ final class ManifestEntryRunMergePlan {
             current = false;
             currentRawBlock = null;
             currentRows = null;
-            currentRow = null;
             currentSourceRow = null;
-            compactRow = null;
-            compactFile = null;
+            currentEntry = null;
+            projectedEntry.clear();
+            fullEntry.clear();
+            identifier.release();
             currentBlock = null;
             rawBlock = false;
             key.clear();
@@ -676,8 +674,9 @@ final class ManifestEntryRunMergePlan {
                 ManifestFile manifestFile,
                 ManifestFileMeta meta,
                 ManifestFileSorter.RowIdEntrySortKey sortKey,
-                ManifestEntryRunMergeEntry.Filter filter,
-                ManifestEntryRunMergeEntry.PartitionDictionary partitions)
+                CollectedDeletes deletes,
+                boolean minor,
+                ManifestEntryRunMerge.SortPartitionDictionary partitions)
                 throws Exception {
             long entryCount = meta.numAddedFiles() + meta.numDeletedFiles();
             this.entries = new ArrayList<>((int) entryCount);
@@ -688,14 +687,14 @@ final class ManifestEntryRunMergePlan {
                     manifestFile.scan(meta.fileName(), 
ProjectedManifestEntry.fullProjection())) {
                 while (iterator.hasNext()) {
                     ProjectedManifestEntry entry = iterator.next();
-                    if (!filter.include(entry)) {
+                    if (!minor && !deletes.copyable(entry, identifier, false)) 
{
                         continue;
                     }
                     BinaryRow row = 
serializer.toBinaryRow(entry.fullRow()).copy();
                     entries.add(
                             new StoredEntry(
                                     row,
-                                    ManifestEntryRunMergeEntry.Key.viewOf(
+                                    ManifestEntryRunMerge.SortKey.viewOf(
                                             view.replace(row), partitions)));
                 }
             }
@@ -732,7 +731,7 @@ final class ManifestEntryRunMergePlan {
         }
 
         @Override
-        public ManifestEntryRunMergeEntry.Key key() {
+        public ManifestEntryRunMerge.SortKey key() {
             return entries.get(position).key;
         }
 
@@ -758,9 +757,9 @@ final class ManifestEntryRunMergePlan {
     private static final class StoredEntry {
 
         final BinaryRow row;
-        final ManifestEntryRunMergeEntry.Key key;
+        final ManifestEntryRunMerge.SortKey key;
 
-        StoredEntry(BinaryRow row, ManifestEntryRunMergeEntry.Key key) {
+        StoredEntry(BinaryRow row, ManifestEntryRunMerge.SortKey key) {
             this.row = row;
             this.key = key;
         }
@@ -821,7 +820,7 @@ final class ManifestEntryRunMergePlan {
             return comparison < 0 || (comparison == 0 && left < right) ? left 
: right;
         }
 
-        boolean blockPrecedesOthers(int cursor, ManifestEntryRunMergeEntry.Key 
blockLastKey) {
+        boolean blockPrecedesOthers(int cursor, ManifestEntryRunMerge.SortKey 
blockLastKey) {
             for (int other = 0; other < cursors.size(); other++) {
                 if (other == cursor || !cursors.get(other).hasCurrent()) {
                     continue;
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 eda776751d..76c5b0ef5c 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
@@ -26,8 +26,7 @@ import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.data.InternalRow;
 import org.apache.paimon.disk.IOManager;
-import org.apache.paimon.manifest.CompactFileIdentifierSet;
-import org.apache.paimon.manifest.DeletedRowIdSet;
+import org.apache.paimon.manifest.CollectedDeletes;
 import org.apache.paimon.manifest.ManifestEntry;
 import org.apache.paimon.manifest.ManifestFile;
 import org.apache.paimon.manifest.ManifestFileMeta;
@@ -75,8 +74,7 @@ public class ManifestFileSorter {
         final ManifestSortKey sortKey;
         final RowType partitionType;
         final ManifestEntryExternalSort.ExternalSortConfig externalSortConfig;
-        final CompactFileIdentifierSet deleteEntries;
-        final DeletedRowIdSet deletedRowIds;
+        final CollectedDeletes deletes;
         /**
          * Manifest files that need unsorted compaction.
          *
@@ -96,8 +94,7 @@ public class ManifestFileSorter {
                 ManifestSortKey sortKey,
                 RowType partitionType,
                 ManifestEntryExternalSort.ExternalSortConfig 
externalSortConfig,
-                CompactFileIdentifierSet deleteEntries,
-                DeletedRowIdSet deletedRowIds,
+                CollectedDeletes deletes,
                 Map<ManifestFileMeta, Boolean> defaultCompactFiles,
                 List<ManifestAdjacentSortedRun> levelRuns,
                 List<ManifestAdjacentSortedRun> pickedRuns) {
@@ -106,8 +103,7 @@ public class ManifestFileSorter {
             this.sortKey = sortKey;
             this.partitionType = partitionType;
             this.externalSortConfig = externalSortConfig;
-            this.deleteEntries = deleteEntries;
-            this.deletedRowIds = deletedRowIds;
+            this.deletes = deletes;
             this.defaultCompactFiles = defaultCompactFiles;
             this.levelRuns = levelRuns;
             this.pickedRuns = pickedRuns;
@@ -122,8 +118,7 @@ public class ManifestFileSorter {
     /** Result of classifying manifest files. */
     static class ClassifyResult {
         final List<ManifestFileMeta> lsmFiles;
-        final CompactFileIdentifierSet deleteEntries;
-        final DeletedRowIdSet deletedRowIds;
+        final CollectedDeletes deletes;
         /**
          * Manifest files that need unsorted compaction.
          *
@@ -136,32 +131,14 @@ public class ManifestFileSorter {
 
         ClassifyResult(
                 List<ManifestFileMeta> lsmFiles,
-                CompactFileIdentifierSet deleteEntries,
-                DeletedRowIdSet deletedRowIds,
+                CollectedDeletes deletes,
                 Map<ManifestFileMeta, Boolean> compactWithoutSort) {
             this.lsmFiles = lsmFiles;
-            this.deleteEntries = deleteEntries;
-            this.deletedRowIds = deletedRowIds;
+            this.deletes = deletes;
             this.compactWithoutSort = compactWithoutSort;
         }
     }
 
-    /** Binary identifiers and partition values collected from DELETE entries. 
*/
-    private static class DeletedEntryInfo {
-        final CompactFileIdentifierSet identifiers;
-        final DeletedRowIdSet rowIds;
-        final Set<BinaryRow> partitions;
-
-        private DeletedEntryInfo(
-                CompactFileIdentifierSet identifiers,
-                DeletedRowIdSet rowIds,
-                Set<BinaryRow> partitions) {
-            this.identifiers = identifiers;
-            this.rowIds = rowIds;
-            this.partitions = partitions;
-        }
-    }
-
     /**
      * Try to sort-rewrite the merged manifest list by a configured partition 
field. If the sort
      * field cannot be resolved, the input is returned as-is.
@@ -268,64 +245,68 @@ public class ManifestFileSorter {
                         sortedRunSizeRatio,
                         externalSortConfig,
                         manifestReadParallelism);
-        List<ManifestAdjacentSortedRun> levelRuns = ctx.levelRuns;
-        List<ManifestAdjacentSortedRun> pickedRuns = ctx.pickedRuns;
-
-        if (pickedRuns.isEmpty() && ctx.defaultCompactFiles.isEmpty()) {
-            LOG.debug(
-                    "Manifest sort full compact skipped: no runs picked and no 
defaultCompactFiles.");
-            return Optional.empty();
-        }
+        try {
+            List<ManifestAdjacentSortedRun> levelRuns = ctx.levelRuns;
+            List<ManifestAdjacentSortedRun> pickedRuns = ctx.pickedRuns;
+
+            if (pickedRuns.isEmpty() && ctx.defaultCompactFiles.isEmpty()) {
+                LOG.debug(
+                        "Manifest sort full compact skipped: no runs picked 
and no defaultCompactFiles.");
+                return Optional.empty();
+            }
 
-        LOG.info(
-                "Manifest sort full compact: input={} files, lsm={} runs, 
picked={} runs, "
-                        + "defaultCompactFiles={}.",
-                input.size(),
-                levelRuns.size(),
-                pickedRuns.size(),
-                ctx.defaultCompactFiles.size());
-
-        // Step 3: Collect reused files (not picked) and picked files
-        Set<ManifestAdjacentSortedRun> pickedSet = new HashSet<>(pickedRuns);
-        List<ManifestFileMeta> result = new ArrayList<>();
-        for (ManifestAdjacentSortedRun run : levelRuns) {
-            if (!pickedSet.contains(run)) {
-                result.addAll(run.files());
+            LOG.info(
+                    "Manifest sort full compact: input={} files, lsm={} runs, 
picked={} runs, "
+                            + "defaultCompactFiles={}.",
+                    input.size(),
+                    levelRuns.size(),
+                    pickedRuns.size(),
+                    ctx.defaultCompactFiles.size());
+
+            // Step 3: Collect reused files (not picked) and picked files
+            Set<ManifestAdjacentSortedRun> pickedSet = new 
HashSet<>(pickedRuns);
+            List<ManifestFileMeta> result = new ArrayList<>();
+            for (ManifestAdjacentSortedRun run : levelRuns) {
+                if (!pickedSet.contains(run)) {
+                    result.addAll(run.files());
+                }
             }
-        }
-        List<ManifestFileMeta> pickedFiles = new ArrayList<>();
-        for (ManifestAdjacentSortedRun run : pickedRuns) {
-            pickedFiles.addAll(run.files());
-        }
-        pickedFiles.addAll(ctx.defaultCompactFiles.keySet());
+            List<ManifestFileMeta> pickedFiles = new ArrayList<>();
+            for (ManifestAdjacentSortedRun run : pickedRuns) {
+                pickedFiles.addAll(run.files());
+            }
+            pickedFiles.addAll(ctx.defaultCompactFiles.keySet());
 
-        // Step 4: Split into sections and merge small adjacent sections
-        List<Section> sections = splitIntoSections(pickedFiles, ctx);
-        sections = mergeSmallAdjacentSections(sections, suggestedMetaSize);
+            // Step 4: Split into sections and merge small adjacent sections
+            List<Section> sections = splitIntoSections(pickedFiles, ctx);
+            sections = mergeSmallAdjacentSections(sections, suggestedMetaSize);
 
-        LOG.info(
-                "Manifest sort full compact: pickedFiles={}, sections={}.",
-                pickedFiles.size(),
-                sections.size());
+            LOG.info(
+                    "Manifest sort full compact: pickedFiles={}, sections={}.",
+                    pickedFiles.size(),
+                    sections.size());
 
-        // Step 5: Rewrite sections
-        FullCompactOutput output = new FullCompactOutput(result);
-        rewriteSections(
-                sections,
-                output,
-                newFilesForAbort,
-                ctx,
-                manifestFile,
-                suggestedMetaSize,
-                suggestedMinMetaCount,
-                maxRewriteSize,
-                manifestReadParallelism);
+            // Step 5: Rewrite sections
+            FullCompactOutput output = new FullCompactOutput(result);
+            rewriteSections(
+                    sections,
+                    output,
+                    newFilesForAbort,
+                    ctx,
+                    manifestFile,
+                    suggestedMetaSize,
+                    suggestedMinMetaCount,
+                    maxRewriteSize,
+                    manifestReadParallelism);
 
-        LOG.info(
-                "Manifest sort full compact completed: input={}, 
resultFiles={}.",
-                input.size(),
-                result.size());
-        return Optional.of(result);
+            LOG.info(
+                    "Manifest sort full compact completed: input={}, 
resultFiles={}.",
+                    input.size(),
+                    result.size());
+            return Optional.of(result);
+        } finally {
+            ctx.deletes.release();
+        }
     }
 
     /**
@@ -365,95 +346,99 @@ public class ManifestFileSorter {
                         sortedRunSizeRatio,
                         externalSortConfig,
                         manifestReadParallelism);
-        List<ManifestAdjacentSortedRun> levelRuns = ctx.levelRuns;
-        List<ManifestAdjacentSortedRun> pickedRuns = ctx.pickedRuns;
-
-        if (pickedRuns.isEmpty() && ctx.defaultCompactFiles.isEmpty()) {
-            LOG.debug(
-                    "Manifest sort minor compact skipped: no runs picked and 
no defaultCompactFiles.");
-            return input;
-        }
-
-        LOG.info(
-                "Manifest sort minor compact: input={} files, lsm={} runs, 
picked={} runs, "
-                        + "defaultCompactFiles={}.",
-                input.size(),
-                levelRuns.size(),
-                pickedRuns.size(),
-                ctx.defaultCompactFiles.size());
-
-        // Step 2: Build fileName -> index mapping and initialize 2D result
-        Map<String, Integer> fileNameToIndex = new HashMap<>();
-        List<List<ManifestFileMeta>> result = new ArrayList<>(input.size());
-        for (int i = 0; i < input.size(); i++) {
-            fileNameToIndex.put(input.get(i).fileName(), i);
-            result.add(new ArrayList<>());
-        }
-
-        // Step 3: Collect reused files and picked files
-        Set<ManifestAdjacentSortedRun> pickedSet = new HashSet<>(pickedRuns);
-        for (ManifestAdjacentSortedRun run : levelRuns) {
-            if (!pickedSet.contains(run)) {
-                for (ManifestFileMeta file : run.files()) {
-                    Integer idx = fileNameToIndex.get(file.fileName());
-                    if (idx != null) {
-                        result.get(idx).add(file);
+        try {
+            List<ManifestAdjacentSortedRun> levelRuns = ctx.levelRuns;
+            List<ManifestAdjacentSortedRun> pickedRuns = ctx.pickedRuns;
+
+            if (pickedRuns.isEmpty() && ctx.defaultCompactFiles.isEmpty()) {
+                LOG.debug(
+                        "Manifest sort minor compact skipped: no runs picked 
and no defaultCompactFiles.");
+                return input;
+            }
+
+            LOG.info(
+                    "Manifest sort minor compact: input={} files, lsm={} runs, 
picked={} runs, "
+                            + "defaultCompactFiles={}.",
+                    input.size(),
+                    levelRuns.size(),
+                    pickedRuns.size(),
+                    ctx.defaultCompactFiles.size());
+
+            // Step 2: Build fileName -> index mapping and initialize 2D result
+            Map<String, Integer> fileNameToIndex = new HashMap<>();
+            List<List<ManifestFileMeta>> result = new 
ArrayList<>(input.size());
+            for (int i = 0; i < input.size(); i++) {
+                fileNameToIndex.put(input.get(i).fileName(), i);
+                result.add(new ArrayList<>());
+            }
+
+            // Step 3: Collect reused files and picked files
+            Set<ManifestAdjacentSortedRun> pickedSet = new 
HashSet<>(pickedRuns);
+            for (ManifestAdjacentSortedRun run : levelRuns) {
+                if (!pickedSet.contains(run)) {
+                    for (ManifestFileMeta file : run.files()) {
+                        Integer idx = fileNameToIndex.get(file.fileName());
+                        if (idx != null) {
+                            result.get(idx).add(file);
+                        }
                     }
                 }
             }
-        }
 
-        List<ManifestFileMeta> pickedFiles = new ArrayList<>();
-        for (ManifestAdjacentSortedRun run : pickedRuns) {
-            pickedFiles.addAll(run.files());
-        }
-        pickedFiles.addAll(ctx.defaultCompactFiles.keySet());
-
-        // Step 4: Compute index range
-        int minIdx = Integer.MAX_VALUE;
-        int maxIdx = Integer.MIN_VALUE;
-        for (ManifestFileMeta meta : pickedFiles) {
-            Integer idx = fileNameToIndex.get(meta.fileName());
-            if (idx != null) {
-                minIdx = Math.min(minIdx, idx);
-                maxIdx = Math.max(maxIdx, idx);
+            List<ManifestFileMeta> pickedFiles = new ArrayList<>();
+            for (ManifestAdjacentSortedRun run : pickedRuns) {
+                pickedFiles.addAll(run.files());
             }
-        }
-        Pair<Integer, Integer> indexRange = Pair.of(minIdx, maxIdx);
+            pickedFiles.addAll(ctx.defaultCompactFiles.keySet());
+
+            // Step 4: Compute index range
+            int minIdx = Integer.MAX_VALUE;
+            int maxIdx = Integer.MIN_VALUE;
+            for (ManifestFileMeta meta : pickedFiles) {
+                Integer idx = fileNameToIndex.get(meta.fileName());
+                if (idx != null) {
+                    minIdx = Math.min(minIdx, idx);
+                    maxIdx = Math.max(maxIdx, idx);
+                }
+            }
+            Pair<Integer, Integer> indexRange = Pair.of(minIdx, maxIdx);
 
-        // Step 5: Split into sections and merge small adjacent sections
-        List<Section> sections = splitIntoSections(pickedFiles, ctx);
-        sections = mergeSmallAdjacentSections(sections, suggestedMetaSize);
+            // Step 5: Split into sections and merge small adjacent sections
+            List<Section> sections = splitIntoSections(pickedFiles, ctx);
+            sections = mergeSmallAdjacentSections(sections, suggestedMetaSize);
 
-        LOG.info(
-                "Manifest sort minor compact: pickedFiles={}, sections={}.",
-                pickedFiles.size(),
-                sections.size());
+            LOG.info(
+                    "Manifest sort minor compact: pickedFiles={}, 
sections={}.",
+                    pickedFiles.size(),
+                    sections.size());
 
-        // Step 6: Rewrite sections
-        MinorCompactOutput output = new MinorCompactOutput(result, indexRange, 
fileNameToIndex);
-        rewriteSections(
-                sections,
-                output,
-                newFilesForAbort,
-                ctx,
-                manifestFile,
-                suggestedMetaSize,
-                suggestedMinMetaCount,
-                maxRewriteSize,
-                manifestReadParallelism);
+            // Step 6: Rewrite sections
+            MinorCompactOutput output = new MinorCompactOutput(result, 
indexRange, fileNameToIndex);
+            rewriteSections(
+                    sections,
+                    output,
+                    newFilesForAbort,
+                    ctx,
+                    manifestFile,
+                    suggestedMetaSize,
+                    suggestedMinMetaCount,
+                    maxRewriteSize,
+                    manifestReadParallelism);
 
-        // Step 7: Flatten 2D result into a single list
-        List<ManifestFileMeta> flatResult = new ArrayList<>();
-        for (List<ManifestFileMeta> subList : result) {
-            flatResult.addAll(subList);
-        }
+            // Step 7: Flatten 2D result into a single list
+            List<ManifestFileMeta> flatResult = new ArrayList<>();
+            for (List<ManifestFileMeta> subList : result) {
+                flatResult.addAll(subList);
+            }
 
-        LOG.info(
-                "Manifest sort minor compact completed: input={}, 
resultFiles={}.",
-                input.size(),
-                flatResult.size());
-        return flatResult;
+            LOG.info(
+                    "Manifest sort minor compact completed: input={}, 
resultFiles={}.",
+                    input.size(),
+                    flatResult.size());
+            return flatResult;
+        } finally {
+            ctx.deletes.release();
+        }
     }
 
     /**
@@ -508,8 +493,7 @@ public class ManifestFileSorter {
                 sortKey,
                 partitionType,
                 externalSortConfig,
-                classification.deleteEntries,
-                classification.deletedRowIds,
+                classification.deletes,
                 classification.compactWithoutSort,
                 levelRuns,
                 pickedRuns);
@@ -535,7 +519,7 @@ public class ManifestFileSorter {
      * <p>Non-full compaction: small files go to defaultCompactFiles for 
minor-style merge; the rest
      * are returned as lsmFiles.
      *
-     * @return classification containing lsmFiles, deleteEntries, and 
defaultCompactFiles
+     * @return classification containing lsmFiles, collected DELETEs, and 
defaultCompactFiles
      */
     static ClassifyResult classifyManifests(
             List<ManifestFileMeta> input,
@@ -565,28 +549,26 @@ public class ManifestFileSorter {
         // Initialize classification containers and read delete entries
         Map<ManifestFileMeta, Boolean> defaultCompactFiles = new 
LinkedHashMap<>();
         List<ManifestFileMeta> lsmFiles = new LinkedList<>(input);
-        CompactFileIdentifierSet classifiedDeleteEntries = new 
CompactFileIdentifierSet();
-        DeletedRowIdSet deletedRowIds = new DeletedRowIdSet();
-        Set<BinaryRow> deletePartitions = Collections.emptySet();
+        CollectedDeletes deletes;
         PartitionPredicate predicate = null;
         if (fullCompaction) {
-            DeletedEntryInfo deletedEntries =
+            deletes =
                     readDeletedEntries(
                             manifestFile, input, runMergeOptimizeEnabled, 
manifestReadParallelism);
-            classifiedDeleteEntries = deletedEntries.identifiers;
-            deletedRowIds = deletedEntries.rowIds;
-            deletePartitions = deletedEntries.partitions;
 
             // Build partition predicate from delete entries for overlap 
detection.
-            if (classifiedDeleteEntries.isEmpty()) {
+            if (deletes.isEmpty()) {
                 predicate = PartitionPredicate.ALWAYS_FALSE;
             } else {
                 if (partitionType.getFieldCount() > 0) {
-                    predicate = PartitionPredicate.fromMultiple(partitionType, 
deletePartitions);
+                    predicate =
+                            PartitionPredicate.fromMultiple(partitionType, 
deletes.partitions());
                 } else {
                     predicate = PartitionPredicate.ALWAYS_TRUE;
                 }
             }
+        } else {
+            deletes = new CollectedDeletes(runMergeOptimizeEnabled);
         }
 
         // Classify each file based on size and delete-partition overlap
@@ -607,18 +589,15 @@ public class ManifestFileSorter {
             }
         }
 
-        return new ClassifyResult(
-                lsmFiles, classifiedDeleteEntries, deletedRowIds, 
defaultCompactFiles);
+        return new ClassifyResult(lsmFiles, deletes.toImmutable(), 
defaultCompactFiles);
     }
 
-    private static DeletedEntryInfo readDeletedEntries(
+    private static CollectedDeletes readDeletedEntries(
             ManifestFile manifestFile,
             List<ManifestFileMeta> manifestFiles,
             boolean runMergeOptimizeEnabled,
             @Nullable Integer manifestReadParallelism) {
-        CompactFileIdentifierSet identifiers = new CompactFileIdentifierSet();
-        DeletedRowIdSet rowIds = new DeletedRowIdSet();
-        Set<BinaryRow> partitions = new HashSet<>();
+        CollectedDeletes deletes = new 
CollectedDeletes(runMergeOptimizeEnabled);
         List<ManifestFileMeta> filesWithDeletes = new ArrayList<>();
         for (ManifestFileMeta meta : manifestFiles) {
             if (meta.numDeletedFiles() > 0) {
@@ -629,44 +608,29 @@ public class ManifestFileSorter {
         if (filesWithDeletes.size() <= 1
                 || (manifestReadParallelism != null && manifestReadParallelism 
<= 1)) {
             for (ManifestFileMeta meta : filesWithDeletes) {
-                collectDeletedEntries(
-                        meta,
-                        manifestFile,
-                        identifiers,
-                        rowIds,
-                        partitions,
-                        runMergeOptimizeEnabled,
-                        false);
+                CollectedDeletes local =
+                        collectDeletedEntries(meta, manifestFile, 
runMergeOptimizeEnabled);
+                deletes.combine(local);
+                local.release();
             }
         } else {
-            Function<ManifestFileMeta, List<Boolean>> reader =
-                    meta -> {
-                        collectDeletedEntries(
-                                meta,
-                                manifestFile,
-                                identifiers,
-                                rowIds,
-                                partitions,
-                                runMergeOptimizeEnabled,
-                                true);
-                        return Collections.singletonList(Boolean.TRUE);
-                    };
-            for (Boolean ignored :
+            Function<ManifestFileMeta, List<CollectedDeletes>> reader =
+                    meta ->
+                            Collections.singletonList(
+                                    collectDeletedEntries(
+                                            meta, manifestFile, 
runMergeOptimizeEnabled));
+            for (CollectedDeletes local :
                     sequentialBatchedExecute(reader, filesWithDeletes, 
manifestReadParallelism)) {
-                // Iteration waits for each bounded batch of parallel reads.
+                deletes.combine(local);
+                local.release();
             }
         }
-        return new DeletedEntryInfo(identifiers, rowIds, partitions);
+        return deletes;
     }
 
-    private static void collectDeletedEntries(
-            ManifestFileMeta meta,
-            ManifestFile manifestFile,
-            CompactFileIdentifierSet identifiers,
-            DeletedRowIdSet rowIds,
-            Set<BinaryRow> partitions,
-            boolean runMergeOptimizeEnabled,
-            boolean synchronize) {
+    private static CollectedDeletes collectDeletedEntries(
+            ManifestFileMeta meta, ManifestFile manifestFile, boolean 
runMergeOptimizeEnabled) {
+        CollectedDeletes deletes = new 
CollectedDeletes(runMergeOptimizeEnabled);
         try (CloseableIterator<ProjectedManifestEntry> entries =
                 manifestFile.scan(
                         meta.fileName(), 
ProjectedManifestEntry.DELETE_ENTRY_PROJECTION)) {
@@ -675,27 +639,14 @@ public class ManifestFileSorter {
                 if (!entry.isDelete()) {
                     continue;
                 }
-                BinaryRow partition = entry.partition().copy();
-                if (synchronize) {
-                    synchronized (identifiers) {
-                        identifiers.add(entry);
-                        if (runMergeOptimizeEnabled) {
-                            rowIds.add(entry.file().nonNullFirstRowId());
-                        }
-                        partitions.add(partition);
-                    }
-                } else {
-                    identifiers.add(entry);
-                    if (runMergeOptimizeEnabled) {
-                        rowIds.add(entry.file().nonNullFirstRowId());
-                    }
-                    partitions.add(partition);
-                }
+                deletes.add(entry, runMergeOptimizeEnabled, true);
             }
         } catch (Exception e) {
+            deletes.release();
             throw new RuntimeException(
                     String.format("Failed to scan manifest file '%s'.", 
meta.fileName()), e);
         }
+        return deletes;
     }
 
     /**
@@ -1089,7 +1040,7 @@ public class ManifestFileSorter {
         }
         // Flush tail only if delete entries exist or file count >= minCount.
         if (!candidates.isEmpty()) {
-            if (!ctx.deleteEntries.isEmpty() || candidates.size() >= 
suggestedMinMetaCount) {
+            if (!ctx.deletes.isEmpty() || candidates.size() >= 
suggestedMinMetaCount) {
                 rewriteSection(
                         candidates,
                         output,
@@ -1153,8 +1104,7 @@ public class ManifestFileSorter {
                             ctx.partitionType,
                             manifestFile,
                             sortNewFiles,
-                            ctx.deleteEntries,
-                            ctx.deletedRowIds,
+                            ctx.deletes,
                             ctx.externalSortConfig.maxNumFileHandles,
                             manifestReadParallelism);
         }
@@ -1166,7 +1116,7 @@ public class ManifestFileSorter {
                             ctx.externalSortConfig,
                             manifestFile,
                             sortNewFiles,
-                            ctx.deleteEntries,
+                            ctx.deletes,
                             manifestReadParallelism);
         }
         if (!sorted.isEmpty()) {
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/manifest/ProjectedManifestEntryTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/manifest/ProjectedManifestEntryTest.java
index c78e782367..ede3a07402 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/manifest/ProjectedManifestEntryTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/manifest/ProjectedManifestEntryTest.java
@@ -266,6 +266,25 @@ public class ProjectedManifestEntryTest {
         assertThat(entry.isDelete()).isFalse();
     }
 
+    @Test
+    void testEntryLayoutProjectionContainsRunMergeFields() {
+        RowType projectedType = 
ProjectedManifestEntry.ENTRY_LAYOUT_PROJECTION.projectedType();
+        RowType projectedFileType =
+                (RowType) 
projectedType.getTypeAt(projectedType.getFieldIndex(ManifestEntry.FILE));
+
+        assertThat(projectedFileType.getFieldNames())
+                .containsExactly(
+                        DataFileMeta.FILE_NAME,
+                        DataFileMeta.ROW_COUNT,
+                        DataFileMeta.LEVEL,
+                        DataFileMeta.SCHEMA_ID,
+                        DataFileMeta.FIRST_ROW_ID,
+                        DataFileMeta.MAX_SEQUENCE_NUMBER,
+                        DataFileMeta.EXTRA_FILES,
+                        DataFileMeta.EMBEDDED_FILE_INDEX,
+                        DataFileMeta.EXTERNAL_PATH);
+    }
+
     private static ProjectedManifestEntry.Projection projection(
             boolean includeBucket, String... projectedFileFields) {
         RowType manifestType = ManifestEntry.MANIFEST_ROW_TYPE;

Reply via email to