morrySnow commented on code in PR #66530:
URL: https://github.com/apache/doris/pull/66530#discussion_r3773488732


##########
fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonScanPlanProvider.java:
##########
@@ -2246,4 +2269,76 @@ private static String escapeJson(String s) {
     private ConnectorStorageContext storage() {
         return context.getStorageContext();
     }
+
+    /**
+     * Statement-scoped cache key for one Paimon scan.
+     *
+     * <p>Includes every input that changes the planned split list: table 
identity, the branch pin,
+     * the whole scan-options map (snapshot / tag / incremental / options 
pins), the projected
+     * columns in order, the pushed filter and the COUNT pushdown flag. System 
tables are excluded
+     * upstream, and session variables are statement-constant, so both stay 
out of the key.
+     */
+    private static final class PaimonScanReuseKey {
+        private final String databaseName;
+        private final String tableName;
+        private final String branchName;
+        private final Map<String, String> scanOptions;
+        private final List<String> columnNames;
+        private final Optional<ConnectorExpression> filter;
+        private final boolean countPushdown;
+
+        private PaimonScanReuseKey(PaimonTableHandle handle, 
ConnectorScanRequest request) {
+            // Catalog and query isolation are provided by the statement-scope 
memo key. System
+            // tables are bypassed in planScan before this key is built, so 
sysTableName is always
+            // null here; if the system-table bypass is ever relaxed, add it 
back.
+            this.databaseName = handle.getDatabaseName();
+            this.tableName = handle.getTableName();
+            this.branchName = handle.getBranchName();
+            this.scanOptions = handle.getScanOptions() == null
+                    ? Collections.emptyMap()
+                    : Collections.unmodifiableMap(new 
HashMap<>(handle.getScanOptions()));
+            this.columnNames = request.getColumns().stream()
+                    .map(PaimonScanReuseKey::toPaimonColumnName)
+                    .collect(Collectors.toList());
+            this.filter = request.getFilter();

Review Comment:
   Same issue as the Iceberg key: `ConnectorExpression` has no 
`equals`/`hashCode`, so `Optional<ConnectorExpression>` equality here is 
identity-based. Two duplicate relations with an equal pushed filter (each scan 
node builds its own expression tree) will produce different keys, so the reuse 
misses for filtered scans — which for Paimon is the common case since partition 
pruning is predicate-driven — and each distinct instance's plan is retained in 
the statement scope until close. Since the filter participates in `scan.plan()` 
split enumeration, it must stay in the key, but the comparison needs to be 
structural (or canonicalized) for the memo to actually hit.



##########
fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanProvider.java:
##########
@@ -2922,4 +2953,70 @@ private Table resolveSysTable(ConnectorSession session, 
IcebergTableHandle handl
     private ConnectorStorageContext storage() {
         return context.getStorageContext();
     }
+
+    /**
+     * Statement-scoped cache key for one Iceberg scan.
+     *
+     * <p>Includes every input that changes the planned file list: table 
identity, the typed
+     * time-travel pin (snapshot id / ref / schema id), the rewrite-file 
scope, the pushed filter
+     * and the COUNT pushdown flag. System tables are excluded upstream, and 
session variables are
+     * statement-constant, so both stay out of the key.
+     */
+    private static final class IcebergScanReuseKey {
+        private final String dbName;
+        private final String tableName;
+        private final long snapshotId;
+        private final String ref;
+        private final long schemaId;
+        private final List<String> rewriteFileScope;
+        private final Optional<ConnectorExpression> filter;
+        private final boolean countPushdown;
+
+        private IcebergScanReuseKey(IcebergTableHandle handle, 
ConnectorScanRequest request) {
+            this.dbName = handle.getDbName();
+            this.tableName = handle.getTableName();
+            this.snapshotId = handle.getSnapshotId();
+            this.ref = handle.getRef();
+            this.schemaId = handle.getSchemaId();
+            // System tables are bypassed in planScan before this key is 
built, so sysTableName is
+            // always null here; if the system-table bypass is ever relaxed, 
add it back.
+            this.rewriteFileScope = handle.getRewriteFileScope() == null
+                    ? Collections.emptyList()
+                    : handle.getRewriteFileScope().stream().sorted()
+                            .collect(Collectors.toList());
+            this.filter = request.getFilter();

Review Comment:
   `ConnectorExpression` (spi/pushdown) does not override `equals`/`hashCode` 
(it only declares `getChildren()`), so `Optional<ConnectorExpression>` equality 
in this key is reference-based, not structural. Each scan node builds its own 
filter tree via `PluginDrivenScanNode.buildRemainingFilter()`, so two duplicate 
relations carrying the same pushed filter almost never produce equal keys:
   
   1. The memo misses for exactly the filtered case this PR targets (e.g. 
self-join with a common predicate; Paimon's filter-driven partition pruning). 
Reuse only works when the filter is empty or the literally same instance is 
passed — the new unit tests pass the same `request` object twice, so they 
cannot catch this.
   2. Each distinct filter instance's full range list is then retained in the 
statement scope until statement end, where previously it was released after 
`getSplits` — a memory regression for statements with many distinct filtered 
scans of the same table.
   
   This is conservative (no wrong reuse), but the reuse silently does not 
happen for filtered scans. Consider giving `ConnectorExpression` structural 
equality (or canonicalizing the filter in the key), or at least documenting the 
limitation and its memory consequence.



##########
fe/fe-connector/fe-connector-hive/src/main/java/org/apache/doris/connector/hive/HiveScanPlanProvider.java:
##########
@@ -725,4 +759,72 @@ private static final class PartitionScanInfo {
     private ConnectorStorageContext storage() {
         return context.getStorageContext();
     }
+
+    /**
+     * Statement-scoped cache key for one Hive scan.
+     *
+     * <p>Includes every input that changes the planned split list: table 
identity, the file formats
+     * (input format / serialization lib / JSON single-column gate), the 
partition keys and the
+     * pruned partition set (each partition's location and values). ACID 
tables are excluded
+     * upstream, and session variables are statement-constant, so both stay 
out of the key.
+     */
+    private static final class HiveScanReuseKey {
+        private final String dbName;
+        private final String tableName;
+        private final String location;
+        private final String inputFormat;
+        private final String serializationLib;
+        private final boolean firstColumnIsString;
+        private final List<String> partitionKeyNames;
+        private final List<HmsPartitionInfo> prunedPartitions;
+
+        private HiveScanReuseKey(HiveTableHandle handle) {
+            // Catalog and query isolation are provided by the statement-scope 
memo key. The table
+            // location identifies the data source of unpartitioned tables, 
whose prunedPartitions
+            // is null.
+            this.dbName = handle.getDbName();
+            this.tableName = handle.getTableName();
+            this.location = handle.getLocation();
+            this.inputFormat = handle.getInputFormat();
+            this.serializationLib = handle.getSerializationLib();
+            this.firstColumnIsString = handle.isFirstColumnString();
+            this.partitionKeyNames = handle.getPartitionKeyNames() == null
+                    ? Collections.emptyList()
+                    : Collections.unmodifiableList(new 
ArrayList<>(handle.getPartitionKeyNames()));
+            this.prunedPartitions = handle.getPrunedPartitions() == null
+                    ? null
+                    : Collections.unmodifiableList(new 
ArrayList<>(handle.getPrunedPartitions()));

Review Comment:
   Two low-severity notes on the key construction:
   
   1. The defensive `new ArrayList<>(...)` copy plus `Objects.hash(...)` over 
the entire pruned partition list runs on every `planScan` call (including memo 
hits, since the key is built before lookup). For tables with very large pruned 
sets (up to `MAX_PARTITIONS` = 100000) each duplicated relation pays an 
O(partitions) copy and hash per call.
   
   2. `HmsPartitionInfo.equals`/`hashCode` cover only `values` + `location`, 
but `convertPartitions` derives the per-partition format (`splittable`, and 
thus the split list) from `part.getInputFormat()`/`getSerializationLib()`, 
which are not in the equality. In practice the same HMS partition identity 
implies the same format, so this is unlikely to produce a wrong reuse — 
flagging only so the key's javadoc claim of covering the file formats stays 
accurate.



##########
fe/fe-connector/fe-connector-hive/src/main/java/org/apache/doris/connector/hive/HiveScanPlanProvider.java:
##########
@@ -131,6 +133,28 @@ public boolean usesHiveParquetInt96TimeZone() {
 
     @Override
     public List<ConnectorScanRange> planScan(ConnectorSession session, 
ConnectorScanRequest request) {
+        HiveTableHandle hiveHandle = (HiveTableHandle) 
request.getTableHandle();
+        if (session == null) {
+            return doPlanScan(session, request);
+        }
+        if (hiveHandle.isTransactional()) {
+            // ACID / INSERT_ONLY reads open a per-scan read transaction with 
a write-id snapshot and
+            // a shared metastore lock; reusing the planned ranges would skip 
that transaction.
+            return doPlanScan(session, request);
+        }
+        // Statement-scoped reuse: within one statement the identical scan 
(same table, same
+        // partition set, same formats) plans once and every duplicated 
relation shares the result.
+        // The scope is NONE for offline planning and tests, in which case the 
loader runs on every
+        // call. Session variables are constant within a statement and 
deliberately absent.
+        String memoKey = "hive.scan-reuse:" + session.getCatalogId() + ":" + 
session.getQueryId();
+        Map<HiveScanReuseKey, List<ConnectorScanRange>> scanReuse = 
session.getStatementScope().computeIfAbsent(
+                memoKey, () -> new ConcurrentHashMap<>());
+        HiveScanReuseKey reuseKey = new HiveScanReuseKey(hiveHandle);
+        return scanReuse.computeIfAbsent(reuseKey,
+                key -> Collections.unmodifiableList(doPlanScan(session, 
request)));

Review Comment:
   Retention note: every distinct (unreused) scan now pins its full range list 
inside the statement scope until `closeAll()` at query finish, instead of being 
released after each node's `getSplits`. For a statement with many distinct 
external scans this is newly retained memory. For Hive specifically, the 
large-scan path (partition-batch mode, `planScanForPartitionBatch`) is 
deliberately not memoized, so on that path the memo only ever serves 
planning/EXPLAIN calls while execution streams batches — the memoized full plan 
is retained for the whole statement without ever being reused. Worth confirming 
this is acceptable; if not, the `planScan` memo could be skipped when the table 
qualifies for batch scan.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to