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

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


The following commit(s) were added to refs/heads/master by this push:
     new 9c3f0e6d208 Prepare partition pruning predicates once per query 
(#19539)
9c3f0e6d208 is described below

commit 9c3f0e6d208f48627414fd3b7cb4e6b32e9db3f0
Author: Xiang Fu <[email protected]>
AuthorDate: Tue Oct 6 21:31:58 2026 -0700

    Prepare partition pruning predicates once per query (#19539)
    
    * Reuse partition predicate hashes across broker segments
    
    * Simplify partition function config matching
    
    * Remove partition pruning benchmark
    
    * Fix unused test import
    
    * Address partition pruning review feedback
    
    * Correct partition pruning review fixes and canonicalize cache keys
    
    * Simplify partition pruning cache state
    
    * Limit partition pruning optimization to query-local ID caching
    
    * Limit partition predicate preparation to large segment sets
    
    * Delegate partition ID reuse compatibility to partition functions
    
    * Clarify partition reuse contract and add query-level cache fallback
    
    * Use primitive partition ID sets and configurable pruning cutoff
    
    Keep scalar IDs for equality and single-ID IN prefixes, and retain lazy 
evaluation for remaining literals. Use a 32-candidate default based on bounded 
JMH comparisons.
    
    Configure pinot.broker.partition.pruning.cache.min.segments in cluster 
config, or append .events_OFFLINE for a live table override. For example, use a 
cluster default of 256 and an events_OFFLINE override of 32 to canary the lower 
cutoff. Removing the table key restores the cluster default; 
enablePartitionPruningCache=false still forces the original evaluator.
    
    * Avoid overlap with concurrent routing-manager tests
    
    * Correct partition pruning cache cutoff documentation
    
    * Support config-aware partition ID reuse
    
    * Cache partition function identities
    
    * Avoid per-query partition threshold lookup
    
    * Use partition function equality instead of interned identities
    
    * Avoid copying partition function configuration
    
    * Address partition pruning review feedback
    
    Use numeric preparation thresholds at cluster, table, and query levels. 
Reuse a small per-query list of function-specific prepared predicates.
    
    * Use function equality for query-local partition pruning
    
    Read the live preparation threshold once per query so cluster changes do 
not rebuild routing entries.
    
    * Avoid hashing functions in common partition-pruning queries
    
    Use a small query-local list and promote to a map after eight distinct 
functions. Keep lazy predicate evaluation and cover the promotion case.
    
    * Avoid preparing non-reusable partition functions
    
    * Reuse OR partition IDs after a miss
    
    Once every direct OR value has been visited on a nonmatching segment, reuse 
their union for later segment checks. Keep first-hit short circuiting and 
multi-partition AND semantics unchanged.
    
    How to reproduce: Compare 256, 512, and 1024 segment pruning for a nested 
OR, EQUALS, and first-hit IN filter. Whole-predicate eager preparation slows 
the first-hit IN control while the on-miss OR union speeds up the nested case.
    
    * Keep OR preparation off simple predicate hot paths
    
    Reuse existing prepared-predicate fields for OR state and move its union 
logic out of matches(). This keeps the EQUALS and first-hit IN paths lean while 
retaining the nested-filter gain.
    
    How to reproduce: Run the full-prune JMH fixture with 256, 512, and 1024 
independently constructed equal Modulo functions, comparing the pre-change lazy 
class to this revision for EQUALS, nested AND/OR, and first-hit IN(128).
    
    * Prepare partition pruning ID sets eagerly
    
    Compute the whole predicate once per equivalent partition function and 
query, intersecting AND candidates and unioning OR candidates. Retain the 
singleton-ID fast path and use the original evaluator when preparation throws, 
preserving its short-circuit error behavior.
    
    How to reproduce: For a single-valued Modulo partition column, a segment 
listing partitions 1 and 2 was retained for id=1 AND id=2 even though no row 
could match. The prepared intersection now prunes it. Tests also cover nested 
set operations and invalid literals after an empty intersection.
    
    * Make partition preparation opt-in and short-circuit unrestricted OR
---
 .../broker/broker/helix/BaseBrokerStarter.java     |   1 +
 .../helix/MultiClusterHelixBrokerStarter.java      |   1 +
 .../routing/manager/BaseBrokerRoutingManager.java  |  29 +-
 .../segmentpruner/SegmentPrunerFactory.java        |  27 +-
 .../SinglePartitionColumnSegmentPruner.java        | 186 ++++++
 .../routing/manager/BrokerRoutingManagerTest.java  |  33 ++
 .../SinglePartitionColumnSegmentPrunerTest.java    | 632 +++++++++++++++++++++
 .../function/ByteArrayPartitionFunction.java       |  24 +
 .../partition/function/FnvPartitionFunction.java   |  28 +
 .../function/HashCodePartitionFunction.java        |  24 +
 .../function/ModuloPartitionFunction.java          |  24 +
 .../function/Murmur3PartitionFunction.java         |  29 +
 .../function/MurmurPartitionFunction.java          |  26 +
 .../common/utils/config/QueryOptionsUtils.java     |   8 +
 .../partition/function/PartitionFunctionTest.java  | 123 +++-
 .../common/utils/config/QueryOptionsUtilsTest.java |  10 +
 .../segment/spi/partition/PartitionFunction.java   |  11 +
 .../pinot/spi/config/table/RoutingConfig.java      |  19 +-
 .../apache/pinot/spi/utils/CommonConstants.java    |   8 +
 19 files changed, 1231 insertions(+), 12 deletions(-)

diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java
index 18cac8d2825..79ad054c15b 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java
@@ -440,6 +440,7 @@ public abstract class BaseBrokerStarter implements 
ServiceStartable {
     _serverRoutingStatsManager = new ServerRoutingStatsManager(_brokerConf, 
_brokerMetrics);
     _serverRoutingStatsManager.init();
     initRoutingManager();
+    
_clusterConfigChangeHandler.registerClusterConfigChangeListener(_routingManager);
 
     final PinotConfiguration factoryConf = 
_brokerConf.subset(Broker.ACCESS_CONTROL_CONFIG_PREFIX);
     // Adding cluster name to the config so that it can be used by the 
AccessControlFactory
diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/MultiClusterHelixBrokerStarter.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/MultiClusterHelixBrokerStarter.java
index 661c640b520..a1e05842c37 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/MultiClusterHelixBrokerStarter.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/MultiClusterHelixBrokerStarter.java
@@ -230,6 +230,7 @@ public class MultiClusterHelixBrokerStarter extends 
BaseBrokerStarter {
         RemoteClusterBrokerRoutingManager routingManager =
             new RemoteClusterBrokerRoutingManager(clusterName, _brokerMetrics, 
_serverRoutingStatsManager, _brokerConf);
         routingManager.init(entry.getValue());
+        
_clusterConfigChangeHandler.registerClusterConfigChangeListener(routingManager);
         _remoteRoutingManagers.put(clusterName, routingManager);
       } catch (Exception e) {
         LOGGER.error("[multi-cluster] Failed to initialize routing manager for 
cluster '{}'", clusterName, e);
diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/BaseBrokerRoutingManager.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/BaseBrokerRoutingManager.java
index 25e5b784b7d..829e20c8e82 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/BaseBrokerRoutingManager.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/BaseBrokerRoutingManager.java
@@ -90,6 +90,7 @@ import 
org.apache.pinot.core.routing.timeboundary.TimeBoundaryStrategy;
 import org.apache.pinot.core.routing.timeboundary.TimeBoundaryStrategyService;
 import org.apache.pinot.core.transport.ServerInstance;
 import 
org.apache.pinot.core.transport.server.routing.stats.ServerRoutingStatsManager;
+import org.apache.pinot.spi.config.provider.PinotClusterConfigChangeListener;
 import org.apache.pinot.spi.config.table.ColumnPartitionConfig;
 import org.apache.pinot.spi.config.table.QueryConfig;
 import org.apache.pinot.spi.config.table.SegmentPartitionConfig;
@@ -127,7 +128,8 @@ import org.slf4j.LoggerFactory;
 ///
 /// TODO: Expose RoutingEntry class to get a consistent view in the broker 
request handler and save the redundant map
 ///       lookups.
-public abstract class BaseBrokerRoutingManager implements RoutingManager, 
ClusterChangeHandler {
+public abstract class BaseBrokerRoutingManager
+    implements RoutingManager, ClusterChangeHandler, 
PinotClusterConfigChangeListener {
   private static final Logger LOGGER = 
LoggerFactory.getLogger(BaseBrokerRoutingManager.class);
 
   protected final BrokerMetrics _brokerMetrics;
@@ -170,6 +172,9 @@ public abstract class BaseBrokerRoutingManager implements 
RoutingManager, Cluste
   private String _instanceConfigsPath;
   protected ZkHelixPropertyStore<ZNRecord> _propertyStore;
 
+  private volatile int _partitionPruningPreparationThreshold =
+      CommonConstants.Broker.DEFAULT_PARTITION_PRUNING_PREPARATION_THRESHOLD;
+
   /// Snapshot of `_enabledServerInstanceMap` restricted to 
enabled-minus-excluded servers. Replaced atomically under
   /// `_globalLock.writeLock()` whenever routing membership changes. Serves as 
the single source of truth for routable
   /// servers: its `keySet()` is passed to `InstanceSelector` for per-table 
selection, and callers that pick workers
@@ -209,6 +214,25 @@ public abstract class BaseBrokerRoutingManager implements 
RoutingManager, Cluste
     _propertyStore = helixManager.getHelixPropertyStore();
   }
 
+  @Override
+  public void onChange(Set<String> changedConfigs, Map<String, String> 
clusterConfigs) {
+    String key = 
CommonConstants.Broker.CONFIG_OF_PARTITION_PRUNING_PREPARATION_THRESHOLD;
+    int updatedThreshold = 
CommonConstants.Broker.DEFAULT_PARTITION_PRUNING_PREPARATION_THRESHOLD;
+    String value = clusterConfigs.get(key);
+    if (value != null) {
+      try {
+        updatedThreshold = Integer.parseInt(value);
+      } catch (NumberFormatException e) {
+        LOGGER.warn("Ignoring invalid partition pruning threshold: {}={}", 
key, value);
+      }
+    }
+    _partitionPruningPreparationThreshold = updatedThreshold;
+  }
+
+  int getPartitionPruningPreparationThreshold() {
+    return _partitionPruningPreparationThreshold;
+  }
+
   /// Sets a callback to be invoked when a server is re-enabled after being 
excluded.
   /// This is useful for resetting gRPC channel state to avoid exponential 
backoff delays.
   public void setServerReenableCallback(Consumer<ServerInstance> callback) {
@@ -812,7 +836,8 @@ public abstract class BaseBrokerRoutingManager implements 
RoutingManager, Cluste
       segmentSelector.init(idealState, externalView, 
preSelectedOnlineSegments);
 
       // Register segment pruners and initialize segment zk metadata fetcher.
-      List<SegmentPruner> segmentPruners = 
SegmentPrunerFactory.getSegmentPruners(tableConfig, _propertyStore);
+      List<SegmentPruner> segmentPruners = 
SegmentPrunerFactory.getSegmentPruners(tableConfig, _propertyStore,
+          this::getPartitionPruningPreparationThreshold);
 
       AdaptiveServerSelector adaptiveServerSelector =
           
AdaptiveServerSelectorFactory.getAdaptiveServerSelector(_serverRoutingStatsManager,
 _pinotConfig);
diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/segmentpruner/SegmentPrunerFactory.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/segmentpruner/SegmentPrunerFactory.java
index 6fc8a150c1a..1a084733226 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/segmentpruner/SegmentPrunerFactory.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/segmentpruner/SegmentPrunerFactory.java
@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.IntSupplier;
 import javax.annotation.Nullable;
 import org.apache.commons.collections4.MapUtils;
 import org.apache.helix.store.zk.ZkHelixPropertyStore;
@@ -36,6 +37,7 @@ import org.apache.pinot.spi.config.table.TableConfig;
 import org.apache.pinot.spi.config.table.TableType;
 import org.apache.pinot.spi.data.DateTimeFieldSpec;
 import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.utils.CommonConstants.Broker;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -51,6 +53,16 @@ public class SegmentPrunerFactory {
 
   public static List<SegmentPruner> getSegmentPruners(TableConfig tableConfig,
       ZkHelixPropertyStore<ZNRecord> propertyStore) {
+    return getSegmentPruners(tableConfig, propertyStore, 
Broker.DEFAULT_PARTITION_PRUNING_PREPARATION_THRESHOLD);
+  }
+
+  public static List<SegmentPruner> getSegmentPruners(TableConfig tableConfig,
+      ZkHelixPropertyStore<ZNRecord> propertyStore, int 
defaultPartitionPruningPreparationThreshold) {
+    return getSegmentPruners(tableConfig, propertyStore, () -> 
defaultPartitionPruningPreparationThreshold);
+  }
+
+  public static List<SegmentPruner> getSegmentPruners(TableConfig tableConfig,
+      ZkHelixPropertyStore<ZNRecord> propertyStore, IntSupplier 
defaultPreparationThreshold) {
     List<SegmentPruner> segmentPruners = new ArrayList<>();
     boolean needsEmptySegment = 
TableConfigUtils.needsEmptySegmentPruner(tableConfig);
     if (needsEmptySegment) {
@@ -65,7 +77,8 @@ public class SegmentPrunerFactory {
         List<SegmentPruner> configuredSegmentPruners = new 
ArrayList<>(segmentPrunerTypes.size());
         for (String segmentPrunerType : segmentPrunerTypes) {
           if 
(RoutingConfig.PARTITION_SEGMENT_PRUNER_TYPE.equalsIgnoreCase(segmentPrunerType))
 {
-            SegmentPruner partitionSegmentPruner = 
getPartitionSegmentPruner(tableConfig);
+            SegmentPruner partitionSegmentPruner =
+                getPartitionSegmentPruner(tableConfig, 
defaultPreparationThreshold);
             if (partitionSegmentPruner != null) {
               configuredSegmentPruners.add(partitionSegmentPruner);
             }
@@ -88,7 +101,8 @@ public class SegmentPrunerFactory {
         if ((tableType == TableType.OFFLINE && 
LEGACY_PARTITION_AWARE_OFFLINE_ROUTING.equalsIgnoreCase(
             routingTableBuilderName)) || (tableType == TableType.REALTIME
             && 
LEGACY_PARTITION_AWARE_REALTIME_ROUTING.equalsIgnoreCase(routingTableBuilderName)))
 {
-          SegmentPruner partitionSegmentPruner = 
getPartitionSegmentPruner(tableConfig);
+          SegmentPruner partitionSegmentPruner =
+              getPartitionSegmentPruner(tableConfig, 
defaultPreparationThreshold);
           if (partitionSegmentPruner != null) {
             segmentPruners.add(partitionSegmentPruner);
           }
@@ -99,7 +113,8 @@ public class SegmentPrunerFactory {
   }
 
   @Nullable
-  private static SegmentPruner getPartitionSegmentPruner(TableConfig 
tableConfig) {
+  private static SegmentPruner getPartitionSegmentPruner(TableConfig 
tableConfig,
+      IntSupplier defaultPreparationThreshold) {
     String tableNameWithType = tableConfig.getTableName();
     SegmentPartitionConfig segmentPartitionConfig = 
tableConfig.getIndexingConfig().getSegmentPartitionConfig();
     if (segmentPartitionConfig == null) {
@@ -114,8 +129,12 @@ public class SegmentPrunerFactory {
     Set<String> partitionColumns = columnPartitionMap.keySet();
     LOGGER.info("Using PartitionSegmentPruner on partition columns: {} for 
table: {}", partitionColumns,
         tableNameWithType);
+    RoutingConfig routingConfig = tableConfig.getRoutingConfig();
+    Integer tableMinSegments = routingConfig != null ? 
routingConfig.getPartitionPruningPreparationThreshold() : null;
+    IntSupplier preparationThreshold =
+        tableMinSegments != null ? () -> tableMinSegments : 
defaultPreparationThreshold;
     return partitionColumns.size() == 1 ? new 
SinglePartitionColumnSegmentPruner(tableNameWithType,
-        partitionColumns.iterator().next())
+        partitionColumns.iterator().next(), preparationThreshold)
         : new MultiPartitionColumnsSegmentPruner(tableNameWithType, 
partitionColumns);
   }
 
diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/segmentpruner/SinglePartitionColumnSegmentPruner.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/segmentpruner/SinglePartitionColumnSegmentPruner.java
index 3e672b88686..3bb79cf4a10 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/segmentpruner/SinglePartitionColumnSegmentPruner.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/segmentpruner/SinglePartitionColumnSegmentPruner.java
@@ -18,11 +18,18 @@
  */
 package org.apache.pinot.broker.routing.segmentpruner;
 
+import it.unimi.dsi.fastutil.ints.IntIterator;
+import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
+import it.unimi.dsi.fastutil.ints.IntSet;
+import it.unimi.dsi.fastutil.ints.IntSets;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.IntSupplier;
 import javax.annotation.Nullable;
 import org.apache.helix.model.ExternalView;
 import org.apache.helix.model.IdealState;
@@ -34,19 +41,35 @@ import org.apache.pinot.common.request.Expression;
 import org.apache.pinot.common.request.Function;
 import org.apache.pinot.common.request.Identifier;
 import org.apache.pinot.common.request.context.RequestContextUtils;
+import org.apache.pinot.common.utils.config.QueryOptionsUtils;
+import org.apache.pinot.segment.spi.partition.PartitionFunction;
+import org.apache.pinot.spi.utils.CommonConstants.Broker;
 import org.apache.pinot.sql.FilterKind;
 
 
 /// The `SinglePartitionColumnSegmentPruner` prunes segments based on their 
partition metadata stored in ZK. The
 /// pruner supports queries with filter (or nested filter) of EQUALITY and IN 
predicates.
 public class SinglePartitionColumnSegmentPruner implements SegmentPruner {
+  private static final int MAX_LINEAR_FUNCTIONS = 8;
   private final String _tableNameWithType;
   private final String _partitionColumn;
+  private final IntSupplier _preparationThreshold;
   private final Map<String, SegmentPartitionInfo> _partitionInfoMap = new 
ConcurrentHashMap<>();
 
   public SinglePartitionColumnSegmentPruner(String tableNameWithType, String 
partitionColumn) {
+    this(tableNameWithType, partitionColumn, 
Broker.DEFAULT_PARTITION_PRUNING_PREPARATION_THRESHOLD);
+  }
+
+  public SinglePartitionColumnSegmentPruner(String tableNameWithType, String 
partitionColumn,
+      int minSegmentsForPreparation) {
+    this(tableNameWithType, partitionColumn, () -> minSegmentsForPreparation);
+  }
+
+  public SinglePartitionColumnSegmentPruner(String tableNameWithType, String 
partitionColumn,
+      IntSupplier preparationThreshold) {
     _tableNameWithType = tableNameWithType;
     _partitionColumn = partitionColumn;
+    _preparationThreshold = preparationThreshold;
   }
 
   @Override
@@ -94,6 +117,12 @@ public class SinglePartitionColumnSegmentPruner implements 
SegmentPruner {
     if (filterExpression == null) {
       return segments;
     }
+    Integer queryMinSegments =
+        
QueryOptionsUtils.getPartitionPruningPreparationThreshold(brokerRequest.getPinotQuery().getQueryOptions());
+    int minSegmentsForPreparation = queryMinSegments != null ? 
queryMinSegments : _preparationThreshold.getAsInt();
+    if (minSegmentsForPreparation >= 0 && segments.size() >= 
minSegmentsForPreparation) {
+      return pruneWithPreparedPredicate(filterExpression, segments);
+    }
     Set<String> selectedSegments = new HashSet<>();
     for (String segment : segments) {
       SegmentPartitionInfo partitionInfo = _partitionInfoMap.get(segment);
@@ -105,6 +134,62 @@ public class SinglePartitionColumnSegmentPruner implements 
SegmentPruner {
     return selectedSegments;
   }
 
+  private Set<String> pruneWithPreparedPredicate(Expression filterExpression, 
Set<String> segments) {
+    Set<String> selectedSegments = new HashSet<>();
+    List<PreparedPartitionIds> predicates = new ArrayList<>(2);
+    Map<PartitionFunction, PreparedPartitionIds> predicateMap = null;
+    for (String segment : segments) {
+      SegmentPartitionInfo partitionInfo = _partitionInfoMap.get(segment);
+      if (partitionInfo == null || partitionInfo == 
SegmentPartitionUtils.INVALID_PARTITION_INFO) {
+        selectedSegments.add(segment);
+        continue;
+      }
+      PartitionFunction function = partitionInfo.getPartitionFunction();
+      if (!function.supportsPartitionIdPreparation()) {
+        if (isPartitionMatch(filterExpression, partitionInfo)) {
+          selectedSegments.add(segment);
+        }
+        continue;
+      }
+      PreparedPartitionIds predicate = null;
+      if (predicateMap == null) {
+        for (PreparedPartitionIds candidate : predicates) {
+          if (function.equals(candidate._partitionFunction)) {
+            predicate = candidate;
+            break;
+          }
+        }
+      } else {
+        predicate = predicateMap.get(function);
+      }
+      if (predicate == null) {
+        try {
+          predicate = new PreparedPartitionIds(function, 
preparePartitionIds(filterExpression, function), false);
+        } catch (RuntimeException e) {
+          // Preparation can reach a value or branch that per-segment 
short-circuiting would skip. Defer errors
+          // to the original evaluator, and remember the fallback so this 
function is not prepared again.
+          predicate = new PreparedPartitionIds(function, null, true);
+        }
+        if (predicateMap == null && predicates.size() < MAX_LINEAR_FUNCTIONS) {
+          predicates.add(predicate);
+        } else {
+          if (predicateMap == null) {
+            predicateMap = new HashMap<>();
+            for (PreparedPartitionIds candidate : predicates) {
+              predicateMap.put(candidate._partitionFunction, candidate);
+            }
+          }
+          predicateMap.put(function, predicate);
+        }
+      }
+      if (predicate._preparationFailed ? isPartitionMatch(filterExpression, 
partitionInfo)
+          : predicate.matches(partitionInfo.getPartitions())) {
+        selectedSegments.add(segment);
+      }
+    }
+    return selectedSegments;
+  }
+
   private boolean isPartitionMatch(Expression filterExpression, 
SegmentPartitionInfo partitionInfo) {
     Function function = filterExpression.getFunctionCall();
     FilterKind filterKind = FilterKind.valueOf(function.getOperator());
@@ -152,4 +237,105 @@ public class SinglePartitionColumnSegmentPruner 
implements SegmentPruner {
         return true;
     }
   }
+
+  /// Computes conservative candidate IDs for the whole predicate. Null 
represents every partition.
+  /// Partition columns are single-valued, so a row satisfying AND must belong 
to every child's candidate set.
+  @Nullable
+  private IntSet preparePartitionIds(Expression expression, PartitionFunction 
partitionFunction) {
+    Function function = expression.getFunctionCall();
+    FilterKind kind = FilterKind.valueOf(function.getOperator());
+    List<Expression> operands = function.getOperands();
+    switch (kind) {
+      case AND: {
+        IntSet ids = null;
+        for (Expression child : operands) {
+          IntSet childIds = preparePartitionIds(child, partitionFunction);
+          if (childIds != null) {
+            if (ids == null) {
+              ids = childIds;
+            } else {
+              // Singleton leaves are immutable; composite and IN sets can be 
intersected in place.
+              if (!(ids instanceof IntOpenHashSet)) {
+                ids = new IntOpenHashSet(ids);
+              }
+              ids.retainAll(childIds);
+            }
+          }
+        }
+        return ids;
+      }
+      case OR: {
+        IntSet ids = new IntOpenHashSet();
+        for (Expression child : operands) {
+          IntSet childIds = preparePartitionIds(child, partitionFunction);
+          if (childIds == null) {
+            return null;
+          }
+          ids.addAll(childIds);
+        }
+        return ids;
+      }
+      case EQUALS:
+      case IN: {
+        Identifier identifier = operands.get(0).getIdentifier();
+        if (identifier == null || 
!identifier.getName().equals(_partitionColumn)) {
+          return null;
+        }
+        int numValues = kind == FilterKind.EQUALS ? 1 : operands.size() - 1;
+        if (numValues == 1) {
+          return 
IntSets.singleton(partitionFunction.getPartition(RequestContextUtils.getStringValue(operands.get(1))));
+        }
+        IntSet ids = new IntOpenHashSet(Math.min(numValues, 
partitionFunction.getNumPartitions()));
+        for (int i = 1; i <= numValues; i++) {
+          
ids.add(partitionFunction.getPartition(RequestContextUtils.getStringValue(operands.get(i))));
+        }
+        return ids;
+      }
+      default:
+        return null;
+    }
+  }
+
+  /// Prepared IDs belong to one prune call. Their sets are read-only after 
construction and never shared by queries.
+  private static final class PreparedPartitionIds {
+    private final PartitionFunction _partitionFunction;
+    @Nullable
+    private final IntSet _partitionIds;
+    @Nullable
+    private final Integer _singlePartitionId;
+    private final boolean _preparationFailed;
+
+    private PreparedPartitionIds(PartitionFunction partitionFunction, 
@Nullable IntSet partitionIds,
+        boolean preparationFailed) {
+      _partitionFunction = partitionFunction;
+      _partitionIds = partitionIds;
+      _singlePartitionId = partitionIds != null && partitionIds.size() == 1 ? 
partitionIds.iterator().nextInt() : null;
+      _preparationFailed = preparationFailed;
+    }
+
+    private boolean matches(Set<Integer> partitions) {
+      if (_partitionIds == null) {
+        return true;
+      }
+      if (_singlePartitionId != null) {
+        return partitions.contains(_singlePartitionId);
+      }
+      // Probe the smaller set; a segment can itself contain many partitions.
+      if (_partitionIds.size() <= partitions.size()) {
+        IntIterator iterator = _partitionIds.iterator();
+        while (iterator.hasNext()) {
+          if (partitions.contains(iterator.nextInt())) {
+            return true;
+          }
+        }
+      } else {
+        for (int partition : partitions) {
+          if (_partitionIds.contains(partition)) {
+            return true;
+          }
+        }
+      }
+      return false;
+    }
+  }
 }
diff --git 
a/pinot-broker/src/test/java/org/apache/pinot/broker/routing/manager/BrokerRoutingManagerTest.java
 
b/pinot-broker/src/test/java/org/apache/pinot/broker/routing/manager/BrokerRoutingManagerTest.java
index 82668fd1ce1..57592fd309b 100644
--- 
a/pinot-broker/src/test/java/org/apache/pinot/broker/routing/manager/BrokerRoutingManagerTest.java
+++ 
b/pinot-broker/src/test/java/org/apache/pinot/broker/routing/manager/BrokerRoutingManagerTest.java
@@ -31,6 +31,7 @@ import org.apache.helix.HelixConstants.ChangeType;
 import org.apache.helix.HelixDataAccessor;
 import org.apache.helix.HelixManager;
 import org.apache.helix.PropertyKey;
+import org.apache.helix.model.ClusterConfig;
 import org.apache.helix.model.ExternalView;
 import org.apache.helix.model.IdealState;
 import org.apache.helix.model.InstanceConfig;
@@ -46,6 +47,7 @@ import 
org.apache.pinot.broker.routing.segmentpruner.SegmentPruner;
 import org.apache.pinot.broker.routing.segmentselector.SegmentSelector;
 import org.apache.pinot.broker.routing.tablesampler.TableSampler;
 import org.apache.pinot.broker.routing.timeboundary.TimeBoundaryManager;
+import org.apache.pinot.common.config.DefaultClusterConfigChangeHandler;
 import org.apache.pinot.common.metrics.BrokerGauge;
 import org.apache.pinot.common.metrics.BrokerMeter;
 import org.apache.pinot.common.metrics.BrokerMetrics;
@@ -73,6 +75,8 @@ import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import static 
org.apache.pinot.spi.utils.CommonConstants.Broker.CONFIG_OF_PARTITION_PRUNING_PREPARATION_THRESHOLD;
+import static 
org.apache.pinot.spi.utils.CommonConstants.Broker.DEFAULT_PARTITION_PRUNING_PREPARATION_THRESHOLD;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -806,6 +810,35 @@ public class BrokerRoutingManagerTest {
     
assertTrue(_routingManager.getRoutableServerInstanceMap().containsKey(SERVER_INSTANCE_ID));
   }
 
+  @Test
+  public void testPartitionPruningThresholdUpdates()
+      throws Exception {
+    String key = CONFIG_OF_PARTITION_PRUNING_PREPARATION_THRESHOLD;
+    _routingManager = spy(_routingManager);
+    putRoutingEntry(TEST_TABLE, createRoutingEntry(TEST_TABLE, null, null, 
Map.of()));
+    DefaultClusterConfigChangeHandler handler = new 
DefaultClusterConfigChangeHandler();
+    ClusterConfig config = new ClusterConfig("testCluster");
+    config.getRecord().setSimpleField(key, "128");
+    handler.onClusterConfigChange(config, null);
+    handler.registerClusterConfigChangeListener(_routingManager);
+    assertEquals(_routingManager.getPartitionPruningPreparationThreshold(), 
128);
+
+    config.getRecord().setSimpleField(key, "-1");
+    handler.onClusterConfigChange(config, null);
+    assertEquals(_routingManager.getPartitionPruningPreparationThreshold(), 
-1);
+
+    config.getRecord().setSimpleField(key, "invalid");
+    handler.onClusterConfigChange(config, null);
+    assertEquals(_routingManager.getPartitionPruningPreparationThreshold(),
+        DEFAULT_PARTITION_PRUNING_PREPARATION_THRESHOLD);
+
+    config.getRecord().getSimpleFields().clear();
+    handler.onClusterConfigChange(config, null);
+    assertEquals(_routingManager.getPartitionPruningPreparationThreshold(),
+        DEFAULT_PARTITION_PRUNING_PREPARATION_THRESHOLD);
+    verify(_routingManager, never()).buildRouting(TEST_TABLE);
+  }
+
   /// Creates a ZNRecord representing an enabled server instance.
   private ZNRecord createEnabledServerZNRecord(String instanceId) {
     ZNRecord record = new ZNRecord(instanceId);
diff --git 
a/pinot-broker/src/test/java/org/apache/pinot/broker/routing/segmentpruner/SinglePartitionColumnSegmentPrunerTest.java
 
b/pinot-broker/src/test/java/org/apache/pinot/broker/routing/segmentpruner/SinglePartitionColumnSegmentPrunerTest.java
new file mode 100644
index 00000000000..3d37d2ffd8a
--- /dev/null
+++ 
b/pinot-broker/src/test/java/org/apache/pinot/broker/routing/segmentpruner/SinglePartitionColumnSegmentPrunerTest.java
@@ -0,0 +1,632 @@
+/**
+ * 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.pinot.broker.routing.segmentpruner;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.annotation.Nullable;
+import org.apache.helix.zookeeper.datamodel.ZNRecord;
+import org.apache.pinot.common.metadata.segment.SegmentPartitionMetadata;
+import org.apache.pinot.common.request.BrokerRequest;
+import org.apache.pinot.common.request.Expression;
+import org.apache.pinot.common.request.PinotQuery;
+import org.apache.pinot.common.utils.request.RequestUtils;
+import org.apache.pinot.segment.spi.partition.PartitionFunction;
+import org.apache.pinot.segment.spi.partition.PartitionFunctionFactory;
+import org.apache.pinot.segment.spi.partition.PartitionIdNormalizer;
+import org.apache.pinot.segment.spi.partition.metadata.ColumnPartitionMetadata;
+import org.apache.pinot.spi.config.table.ColumnPartitionConfig;
+import org.apache.pinot.spi.config.table.RoutingConfig;
+import org.apache.pinot.spi.config.table.SegmentPartitionConfig;
+import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.pinot.spi.utils.CommonConstants.Broker;
+import 
org.apache.pinot.spi.utils.CommonConstants.Broker.Request.QueryOptionKey;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.expectThrows;
+
+
+/// Exercises query-local partition predicate preparation with real metadata 
initialization and refresh, without ZK.
+public class SinglePartitionColumnSegmentPrunerTest {
+  private static final String COLUMN = "memberId";
+  private static final String TABLE = "testTable_OFFLINE";
+
+  @Test
+  public void testThresholdThroughFactory() throws Exception {
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE).build();
+    tableConfig.getIndexingConfig().setSegmentPartitionConfig(
+        new SegmentPartitionConfig(Map.of(COLUMN, new 
ColumnPartitionConfig("PrunerCounting", 8))));
+    AtomicInteger clusterThreshold = new AtomicInteger(100);
+    tableConfig.setRoutingConfig(new RoutingConfig(null, List.of("partition"), 
null, null));
+    SegmentPruner livePruner = 
SegmentPrunerFactory.getSegmentPruners(tableConfig, null, 
clusterThreshold::get).get(0);
+    List<String> segments = List.of("first", "second");
+    List<ZNRecord> metadata = List.of(metadata("first", "PrunerCounting", 8, 
Set.of(1), null),
+        metadata("second", "PrunerCounting", 8, Set.of(2), null));
+    livePruner.init(null, null, segments, metadata);
+    BrokerRequest request = request(predicate("EQUALS", "1"));
+    CountingPartitionFunction.CALLS.set(0);
+    assertEquals(livePruner.prune(request, Set.of("first", "second")), 
Set.of("first"));
+    assertEquals(CountingPartitionFunction.CALLS.get(), 2);
+    clusterThreshold.set(2);
+    CountingPartitionFunction.CALLS.set(0);
+    assertEquals(livePruner.prune(request, Set.of("first", "second")), 
Set.of("first"));
+    assertEquals(CountingPartitionFunction.CALLS.get(), 1);
+
+    tableConfig.setRoutingConfig(new RoutingConfig(null, List.of("partition"), 
null, null, 2));
+    SegmentPruner pruner = SegmentPrunerFactory.getSegmentPruners(tableConfig, 
null, 100).get(0);
+    pruner.init(null, null, segments, metadata);
+    CountingPartitionFunction.CALLS.set(0);
+    assertEquals(pruner.prune(request, Set.of("first", "second")), 
Set.of("first"));
+    assertEquals(CountingPartitionFunction.CALLS.get(), 1);
+    
request.getPinotQuery().setQueryOptions(Map.of(QueryOptionKey.PARTITION_PRUNING_PREPARATION_THRESHOLD,
 "-1"));
+    CountingPartitionFunction.CALLS.set(0);
+    assertEquals(pruner.prune(request, Set.of("first", "second")), 
Set.of("first"));
+    assertEquals(CountingPartitionFunction.CALLS.get(), 2);
+  }
+
+  @Test
+  public void 
testComputesPartitionIdOnceAcrossDistinctMetadataInstancesPerQuery() throws 
Exception {
+    Map<String, ZNRecord> records = new LinkedHashMap<>();
+    Set<String> expected = new HashSet<>();
+    for (int i = 0; i < 256; i++) {
+      String segment = "segment_" + i;
+      records.put(segment, metadata(segment, "PrunerCounting", 8, Set.of(i % 
8), i % 2 == 0 ? null : Map.of()));
+      if (i % 8 == 3) {
+        expected.add(segment);
+      }
+    }
+    SinglePartitionColumnSegmentPruner pruner = pruner(records);
+    CountingPartitionFunction.CALLS.set(0);
+    BrokerRequest request = request(predicate("EQUALS", "3"));
+    assertEquals(pruner.prune(request, records.keySet()), expected);
+    assertEquals(CountingPartitionFunction.CALLS.get(), 1);
+    assertEquals(pruner.prune(request, records.keySet()), expected);
+    assertEquals(CountingPartitionFunction.CALLS.get(), 2, "Prepared IDs must 
not survive a prune call");
+    
request.getPinotQuery().setQueryOptions(Map.of(QueryOptionKey.PARTITION_PRUNING_PREPARATION_THRESHOLD,
 "-1"));
+    CountingPartitionFunction.CALLS.set(0);
+    assertEquals(pruner.prune(request, records.keySet()), expected);
+    assertEquals(CountingPartitionFunction.CALLS.get(), 256,
+        "Disabling predicate preparation must use per-segment evaluation");
+  }
+
+  @Test
+  public void testInterleavedFunctionConfigurationsAndPartitionCounts() throws 
Exception {
+    Map<String, ZNRecord> records = new LinkedHashMap<>();
+    records.put("a", metadata("a", "PrunerCounting", 8, Set.of(3), null));
+    records.put("sameFunction", metadata("sameFunction", "PrunerCounting", 8, 
Set.of(2), null));
+    records.put("b", metadata("b", "PrunerCounting", 8, Set.of(4), 
Map.of("offset", "1")));
+    records.put("c", metadata("c", "PrunerCounting", 8, Set.of(2), null));
+    records.put("d", metadata("d", "PrunerCounting", 8, Set.of(3), 
Map.of("offset", "1")));
+    records.put("e", metadata("e", "PrunerCounting", 16, Set.of(11), null));
+    Set<String> expected = new HashSet<>(Set.of("a", "b", "e"));
+    for (int i = 0; i < 250; i++) {
+      String segment = "tail_" + i;
+      records.put(segment, metadata(segment, "PrunerCounting", 8, Set.of(3), 
null));
+      expected.add(segment);
+    }
+    SinglePartitionColumnSegmentPruner pruner = pruner(records);
+    CountingPartitionFunction.CALLS.set(0);
+    assertEquals(pruner.prune(request(predicate("EQUALS", "11")), 
records.keySet()), expected);
+    assertEquals(CountingPartitionFunction.CALLS.get(), 3,
+        "Each distinct function should prepare its partition ID once");
+    records.put("b", metadata("b", "PrunerCounting", 16, Set.of(11), null));
+    records.put("d", metadata("d", "PrunerCounting", 8, Set.of(3), null));
+    expected.add("d");
+    CountingPartitionFunction.CALLS.set(0);
+    assertEquals(pruner(records).prune(request(predicate("EQUALS", "11")), 
records.keySet()), expected);
+    assertEquals(CountingPartitionFunction.CALLS.get(), 2,
+        "Different partition counts must not reuse partition IDs");
+  }
+
+  @Test
+  public void testMoreThanEightDistinctPartitionFunctions() throws Exception {
+    Map<String, ZNRecord> records = new LinkedHashMap<>();
+    Set<String> expected = new HashSet<>();
+    for (int offset = 0; offset < 9; offset++) {
+      String segment = "matching_" + offset;
+      records.put(segment, metadata(segment, "PrunerCounting", 16, 
Set.of(offset),
+          Map.of("offset", Integer.toString(offset))));
+      expected.add(segment);
+    }
+    for (int offset = 0; offset < 9; offset++) {
+      String segment = "nonmatching_" + offset;
+      records.put(segment, metadata(segment, "PrunerCounting", 16, 
Set.of(offset + 1),
+          Map.of("offset", Integer.toString(offset))));
+    }
+    CountingPartitionFunction.CALLS.set(0);
+    assertEquals(pruner(records, 1).prune(request(predicate("EQUALS", "0")), 
records.keySet()), expected);
+    assertEquals(CountingPartitionFunction.CALLS.get(), 9);
+  }
+
+  @Test
+  public void testEqualConfigurationsReusePartitionIds() throws Exception {
+    Map<String, ZNRecord> records = new LinkedHashMap<>();
+    for (int i = 0; i < 
Broker.DEFAULT_PARTITION_PRUNING_PREPARATION_THRESHOLD; i++) {
+      String segment = "segment_" + i;
+      records.put(segment, metadata(segment, "PrunerCounting", 8, Set.of(4), 
Map.of("offset", "1")));
+    }
+    SinglePartitionColumnSegmentPruner pruner = pruner(records);
+    CountingPartitionFunction.CALLS.set(0);
+    assertEquals(pruner.prune(request(predicate("EQUALS", "3")), 
records.keySet()), records.keySet());
+    assertEquals(CountingPartitionFunction.CALLS.get(), 1, "Equal nonempty 
configs should share computed IDs");
+    pruner.refreshSegment("segment_1", metadata("segment_1", "PrunerCounting", 
8, Set.of(4), Map.of("offset", "2")));
+    Set<String> expected = new HashSet<>(records.keySet());
+    expected.remove("segment_1");
+    CountingPartitionFunction.CALLS.set(0);
+    assertEquals(pruner.prune(request(predicate("EQUALS", "3")), 
records.keySet()), expected);
+    assertEquals(CountingPartitionFunction.CALLS.get(), 2, "A changed config 
must not reuse the first function's ID");
+  }
+
+  @Test
+  public void testLegacyFunctionKeepsFirstHitShortCircuit() throws Exception {
+    Map<String, ZNRecord> records = new LinkedHashMap<>();
+    for (int i = 0; i < 
Broker.DEFAULT_PARTITION_PRUNING_PREPARATION_THRESHOLD; i++) {
+      String segment = "segment_" + i;
+      records.put(segment, metadata(segment, "PrunerLegacy", 8, Set.of(0), 
null));
+    }
+    PartitionFunction first = 
PartitionFunctionFactory.getPartitionFunction("PrunerLegacy", 8, null);
+    assertFalse(first.supportsPartitionIdPreparation());
+    assertNotEquals(first, 
PartitionFunctionFactory.getPartitionFunction("PrunerLegacy", 8, null));
+    String[] values = new String[128];
+    Arrays.setAll(values, i -> Integer.toString(i));
+    SinglePartitionColumnSegmentPruner pruner = pruner(records);
+    CountingPartitionFunction.CALLS.set(0);
+    assertEquals(pruner.prune(request(predicate("IN", values)), 
records.keySet()), records.keySet());
+    assertEquals(CountingPartitionFunction.CALLS.get(), records.size(),
+        "Identity-only plugins should evaluate only the first matching value 
per segment");
+  }
+
+  @Test
+  public void testUnrestrictedOrSkipsLaterPartitionValues() throws Exception {
+    Map<String, ZNRecord> records = new LinkedHashMap<>();
+    for (int i = 0; i < 
Broker.DEFAULT_PARTITION_PRUNING_PREPARATION_THRESHOLD; i++) {
+      String segment = "segment_" + i;
+      records.put(segment, metadata(segment, "PrunerCounting", 8, Set.of(0), 
null));
+    }
+    String[] values = new String[128];
+    Arrays.setAll(values, i -> Integer.toString(i));
+    values[127] = "invalid-number";
+    Expression filter = function("OR",
+        function("EQUALS", RequestUtils.getIdentifierExpression("other"), 
RequestUtils.getLiteralExpression("x")),
+        predicate("IN", values));
+    SinglePartitionColumnSegmentPruner pruner = pruner(records);
+    CountingPartitionFunction.CALLS.set(0);
+    assertEquals(pruner.prune(request(filter), records.keySet()), 
records.keySet());
+    assertEquals(CountingPartitionFunction.CALLS.get(), 0,
+        "An unrestricted OR child should skip all later values, including 
invalid ones");
+  }
+
+  @Test
+  public void testDuplicateInPartitionsArePreparedOnce() throws Exception {
+    Map<String, ZNRecord> records = new LinkedHashMap<>();
+    records.put("first", metadata("first", "PrunerCounting", 8, Set.of(1), 
null));
+    records.put("configured", metadata("configured", "PrunerCounting", 8, 
Set.of(3), Map.of("offset", "1")));
+    records.put("second", metadata("second", "PrunerCounting", 8, Set.of(2), 
null));
+    records.put("miss", metadata("miss", "PrunerCounting", 8, Set.of(3, 4), 
null));
+    records.put("repeat", metadata("repeat", "PrunerCounting", 8, Set.of(1, 
2), null));
+    Set<String> expected = new HashSet<>(Set.of("first", "configured", 
"second", "repeat"));
+    for (int i = 0; i < 252; i++) {
+      String segment = "repeat_" + i;
+      records.put(segment, metadata(segment, "PrunerCounting", 8, Set.of(1, 
2), null));
+      expected.add(segment);
+    }
+    SinglePartitionColumnSegmentPruner pruner = pruner(records);
+    CountingPartitionFunction.CALLS.set(0);
+    // Every literal is prepared once for each function, even when several 
literals map to the same partition.
+    assertEquals(pruner.prune(request(predicate("IN", "1", "9", "17", "2")), 
records.keySet()),
+        expected);
+    assertEquals(CountingPartitionFunction.CALLS.get(), 8);
+    Set<String> firstHits = Set.of("first", "repeat");
+    CountingPartitionFunction.CALLS.set(0);
+    assertEquals(pruner(records, 1).prune(request(predicate("IN", "1", "9", 
"17", "2")), firstHits), firstHits);
+    assertEquals(CountingPartitionFunction.CALLS.get(), 4, "Preparation must 
include values after the first hit");
+    // A configured first segment must not seed IDs for later default-config 
segments.
+    records.put("first", metadata("first", "PrunerCounting", 8, Set.of(2), 
Map.of("offset", "1")));
+    assertEquals(pruner(records).prune(request(predicate("IN", "1", "9", "17", 
"2")), records.keySet()), expected);
+  }
+
+  @Test(dataProvider = "candidateCounts")
+  public void testLargeConfigurationsAndUnrelatedPredicates(int numSegments, 
@Nullable Integer queryMinSegments)
+      throws Exception {
+    String values = "first|" + "x".repeat(100_000);
+    Map<String, String> config = Map.of("columnValues", values, 
"columnValuesDelimiter", "|");
+    ZNRecord first = metadata("first", "BoundedColumnValue", 3, Set.of(1), 
config);
+    ZNRecord second = metadata("second", "BoundedColumnValue", 3, Set.of(2), 
config);
+    Map<String, ZNRecord> records = new LinkedHashMap<>(Map.of("first", first, 
"second", second));
+    for (int i = 2; i < numSegments; i++) {
+      String segment = "segment_" + i;
+      records.put(segment, metadata(segment, "BoundedColumnValue", 3, 
Set.of(2), config));
+    }
+    SinglePartitionColumnSegmentPruner pruner = pruner(records);
+    BrokerRequest request = request(predicate("EQUALS", "first"));
+    if (queryMinSegments != null) {
+      request.getPinotQuery().setQueryOptions(
+          Map.of(QueryOptionKey.PARTITION_PRUNING_PREPARATION_THRESHOLD, 
queryMinSegments.toString()));
+    }
+    assertEquals(pruner.prune(request, records.keySet()), Set.of("first"));
+    request.getPinotQuery().setFilterExpression(function("EQUALS", 
RequestUtils.getIdentifierExpression("other"),
+        RequestUtils.getLiteralExpression("value")));
+    assertEquals(pruner.prune(request, records.keySet()), records.keySet());
+    pruner.refreshSegment("first", metadata("first", "BoundedColumnValue", 3, 
Set.of(2), config));
+    request.getPinotQuery().setFilterExpression(predicate("EQUALS", "first"));
+    assertEquals(pruner.prune(request, records.keySet()), Set.of());
+  }
+
+  @Test
+  public void testMixedFunctionsNormalizersAndFunctionConfig() throws 
Exception {
+    Map<String, ZNRecord> moduloRecords = new LinkedHashMap<>();
+    moduloRecords.put("positive", metadata("positive", "Modulo", 8, Set.of(7), 
null));
+    moduloRecords.put("abs", metadata("abs", "Modulo", 8, Set.of(1), 
Map.of("partitionIdNormalizer", "ABS")));
+    moduloRecords.put("wrongAbs", metadata("wrongAbs", "Modulo", 8, Set.of(7), 
Map.of("partitionIdNormalizer", "ABS")));
+    Set<String> expected = new HashSet<>(Set.of("positive", "abs"));
+    for (int i = 0; i < 253; i++) {
+      String segment = "positive_" + i;
+      moduloRecords.put(segment, metadata(segment, "Modulo", 8, Set.of(7), 
null));
+      expected.add(segment);
+    }
+    assertEquals(pruner(moduloRecords).prune(request(predicate("EQUALS", 
"-1")), moduloRecords.keySet()),
+        expected);
+
+    String value = "80ff0102";
+    Map<String, String> rawConfig = Map.of("useRawBytes", "true");
+    int textPartition = 
PartitionFunctionFactory.getPartitionFunction("Murmur", 97, 
null).getPartition(value);
+    int rawPartition = PartitionFunctionFactory.getPartitionFunction("Murmur", 
97, rawConfig).getPartition(value);
+    assertNotEquals(textPartition, rawPartition, "Fixture must distinguish 
raw-byte and string hashing");
+    Map<String, ZNRecord> murmurRecords = new LinkedHashMap<>();
+    murmurRecords.put("text", metadata("text", "Murmur", 97, 
Set.of(textPartition), null));
+    murmurRecords.put("raw", metadata("raw", "Murmur", 97, 
Set.of(rawPartition), rawConfig));
+    murmurRecords.put("wrongRaw", metadata("wrongRaw", "Murmur", 97, 
Set.of(textPartition), rawConfig));
+    expected = new HashSet<>(Set.of("text", "raw"));
+    for (int i = 0; i < 253; i++) {
+      String segment = "text_" + i;
+      murmurRecords.put(segment, metadata(segment, "Murmur", 97, 
Set.of(textPartition), null));
+      expected.add(segment);
+    }
+    assertEquals(pruner(murmurRecords).prune(request(predicate("EQUALS", 
value)), murmurRecords.keySet()),
+        expected);
+
+    Map<String, ZNRecord> lookupRecords = new LinkedHashMap<>();
+    lookupRecords.put("first", metadata("first", "BoundedColumnValue", 3, 
Set.of(1),
+        Map.of("columnValues", "11|12", "columnValuesDelimiter", "|")));
+    lookupRecords.put("second", metadata("second", "BoundedColumnValue", 3, 
Set.of(2),
+        Map.of("columnValues", "12|11", "columnValuesDelimiter", "|")));
+    lookupRecords.put("modulo", metadata("modulo", "Modulo", 3, Set.of(2), 
null));
+    assertEquals(pruner(lookupRecords).prune(request(predicate("EQUALS", 
"11")), lookupRecords.keySet()),
+        lookupRecords.keySet());
+  }
+
+  @Test
+  public void testPreparedAndOrWithMultipleSegmentPartitions() throws 
Exception {
+    Map<String, ZNRecord> records = new LinkedHashMap<>();
+    records.put("miss", metadata("miss", "Modulo", 8, Set.of(7), null));
+    records.put("multiple", metadata("multiple", "Modulo", 8, Set.of(1, 2), 
null));
+    for (int i = 2; i < 
Broker.DEFAULT_PARTITION_PRUNING_PREPARATION_THRESHOLD; i++) {
+      String segment = "segment_" + i;
+      records.put(segment, metadata(segment, "Modulo", 8, Set.of(i % 8), 
null));
+    }
+    Expression filter = function("AND", function("OR", predicate("EQUALS", 
"1"), predicate("IN", "2", "3")),
+        function("EQUALS", RequestUtils.getIdentifierExpression("other"),
+            RequestUtils.getLiteralExpression("value")));
+    SinglePartitionColumnSegmentPruner pruner = pruner(records);
+    BrokerRequest prepared = request(filter);
+    BrokerRequest legacy = request(filter);
+    
legacy.getPinotQuery().setQueryOptions(Map.of(QueryOptionKey.PARTITION_PRUNING_PREPARATION_THRESHOLD,
 "-1"));
+    Set<String> expected = pruner.prune(legacy, records.keySet());
+    assertTrue(expected.contains("multiple"));
+    assertEquals(pruner.prune(prepared, records.keySet()), expected);
+    assertEquals(pruner.prune(request(function("AND", predicate("EQUALS", 
"1"), predicate("EQUALS", "2"))),
+        records.keySet()), Set.of(), "Different rows cannot satisfy an 
impossible single-valued conjunction");
+    Expression intersection = function("AND", predicate("IN", "1", "2"), 
predicate("IN", "2", "3"));
+    assertEquals(pruner.prune(request(intersection), records.keySet()),
+        pruner.prune(request(predicate("EQUALS", "2")), records.keySet()));
+    assertEquals(pruner.prune(request(function("OR", intersection, 
predicate("EQUALS", "4"))), records.keySet()),
+        pruner.prune(request(predicate("IN", "2", "4")), records.keySet()));
+    expectThrows(NumberFormatException.class, () -> 
pruner.prune(request(function("AND", predicate("EQUALS", "1"),
+        predicate("EQUALS", "2"), predicate("EQUALS", "invalid-number"))), 
records.keySet()));
+  }
+
+  @DataProvider
+  public Object[][] candidateCounts() {
+    int threshold = Broker.DEFAULT_PARTITION_PRUNING_PREPARATION_THRESHOLD;
+    return new Object[][]{{1, null}, {2, null}, {threshold - 1, null}, 
{threshold, null}, {threshold, -1}};
+  }
+
+  @Test(dataProvider = "candidateCounts")
+  public void testAndOrUnsupportedPredicatesAndShortCircuitErrors(int 
numSegments, @Nullable Integer queryMinSegments)
+      throws Exception {
+    Map<String, ZNRecord> records = new LinkedHashMap<>();
+    for (int i = 0; i < numSegments; i++) {
+      String segment = "segment_" + i;
+      records.put(segment, metadata(segment, "Modulo", 8, Set.of(1), null));
+    }
+    SinglePartitionColumnSegmentPruner pruner = pruner(records, 
queryMinSegments);
+    Set<String> segments = records.keySet();
+    Expression invalidValue = predicate("EQUALS", "invalid-number");
+    assertEquals(pruner.prune(request(predicate("IN", "1", "invalid-number")), 
segments), segments);
+    expectThrows(NumberFormatException.class,
+        () -> pruner.prune(request(predicate("IN", "2", "invalid-number")), 
segments));
+    assertEquals(pruner.prune(request(function("AND", predicate("EQUALS", 
"2"), invalidValue)), segments), Set.of());
+    assertEquals(pruner.prune(request(function("OR", predicate("EQUALS", "1"), 
invalidValue)), segments), segments);
+    Expression invalidOperator = function("INVALID_OPERATOR");
+    assertEquals(pruner.prune(request(function("OR", predicate("EQUALS", "1"), 
invalidOperator)), segments), segments);
+    assertEquals(pruner.prune(request(function("AND", predicate("EQUALS", 
"2"), invalidOperator)), segments), Set.of());
+    expectThrows(IllegalArgumentException.class,
+        () -> pruner.prune(request(function("OR", predicate("EQUALS", "2"), 
invalidOperator)), segments));
+    expectThrows(IllegalArgumentException.class, () -> 
pruner.prune(request(invalidOperator), segments));
+
+    Expression unsupported = predicate("GREATER_THAN", "100");
+    assertEquals(pruner.prune(request(function("AND", predicate("IN", "0", 
"1"), unsupported)), segments), segments);
+    assertEquals(pruner.prune(request(function("OR", predicate("EQUALS", "2"), 
unsupported)), segments), segments);
+    assertEquals(pruner.prune(request(function("NOT", predicate("EQUALS", 
"1"))), segments), segments);
+    assertEquals(pruner.prune(request(function("EQUALS", 
RequestUtils.getIdentifierExpression("other"),
+        RequestUtils.getLiteralExpression("invalid-number"))), segments), 
segments);
+    Expression transformedColumn = function("LOWER", 
RequestUtils.getIdentifierExpression(COLUMN));
+    assertEquals(pruner.prune(request(function("EQUALS", transformedColumn,
+        RequestUtils.getLiteralExpression("invalid-number"))), segments), 
segments);
+    BrokerRequest unfilteredRequest = new BrokerRequest();
+    unfilteredRequest.setPinotQuery(new PinotQuery());
+    assertSame(pruner.prune(unfilteredRequest, segments), segments);
+  }
+
+  @Test
+  public void testEmptyCandidatesDoNotEvaluateFilter() throws Exception {
+    SinglePartitionColumnSegmentPruner pruner = pruner(Map.of("one", 
metadata("one", "Modulo", 8, Set.of(1), null)));
+    assertEquals(pruner.prune(request(function("INVALID_OPERATOR")), 
Set.of()), Set.of());
+    assertEquals(pruner.prune(request(predicate("EQUALS", "invalid-number")), 
Set.of()), Set.of());
+  }
+
+  @Test(dataProvider = "candidateCounts")
+  public void testUnknownMetadataIsConservativeAndDoesNotEvaluateFilter(int 
numSegments,
+      @Nullable Integer queryMinSegments)
+      throws Exception {
+    SinglePartitionColumnSegmentPruner pruner = pruner(Map.of(), 
queryMinSegments);
+    ZNRecord invalid = new ZNRecord("invalid");
+    invalid.setSimpleField(CommonConstants.Segment.PARTITION_METADATA, 
"invalid-json");
+    pruner.init(null, null, List.of("missing", "empty", "invalid"),
+        Arrays.asList(null, new ZNRecord("empty"), invalid));
+    Set<String> segments = new HashSet<>(Set.of("missing", "empty", "invalid", 
"not-initialized"));
+    for (int i = segments.size(); i < numSegments; i++) {
+      segments.add("not-initialized_" + i);
+    }
+    assertEquals(pruner.prune(request(predicate("EQUALS", "invalid-number")), 
segments), segments);
+    assertEquals(pruner.prune(request(function("INVALID_OPERATOR")), 
segments), segments);
+  }
+
+  @Test(dataProvider = "candidateCounts")
+  public void testRefreshUsesCurrentPartitionsAndConfiguration(int 
numSegments, @Nullable Integer queryMinSegments)
+      throws Exception {
+    Map<String, ZNRecord> records = new LinkedHashMap<>();
+    for (int i = 0; i < numSegments; i++) {
+      String segment = "segment_" + i;
+      records.put(segment, metadata(segment, "PrunerCounting", 8, Set.of(3), 
null));
+    }
+    SinglePartitionColumnSegmentPruner pruner = pruner(records, 
queryMinSegments);
+    Set<String> segments = records.keySet();
+    BrokerRequest request = request(predicate("EQUALS", "3"));
+    assertEquals(pruner.prune(request, segments), segments);
+    for (String segment : segments) {
+      pruner.refreshSegment(segment, metadata(segment, "PrunerCounting", 8, 
Set.of(4), null));
+    }
+    assertEquals(pruner.prune(request, segments), Set.of());
+    for (String segment : segments) {
+      pruner.refreshSegment(segment, metadata(segment, "PrunerCounting", 8, 
Set.of(4), Map.of("offset", "1")));
+    }
+    assertEquals(pruner.prune(request, segments), segments);
+    for (String segment : segments) {
+      pruner.refreshSegment(segment, null);
+    }
+    assertEquals(pruner.prune(request(predicate("EQUALS", "invalid-number")), 
segments), segments);
+  }
+
+  @Test
+  public void testConcurrentQueriesKeepSeparatePreparedValues() throws 
Exception {
+    Map<String, ZNRecord> records = new LinkedHashMap<>();
+    for (int i = 0; i < 256; i++) {
+      String segment = "segment_" + i;
+      records.put(segment, metadata(segment, "Modulo", 8, Set.of(i % 8), 
null));
+    }
+    SinglePartitionColumnSegmentPruner pruner = pruner(records);
+    ExecutorService executor = Executors.newFixedThreadPool(4);
+    try {
+      List<Callable<Void>> queries = new ArrayList<>();
+      for (int i = 0; i < 32; i++) {
+        int partition = i % 8;
+        Set<String> expected = new HashSet<>();
+        for (int segment = partition; segment < 256; segment += 8) {
+          expected.add("segment_" + segment);
+        }
+        queries.add(() -> {
+          assertEquals(pruner.prune(request(predicate("EQUALS", 
Integer.toString(partition))), records.keySet()),
+              expected);
+          return null;
+        });
+      }
+      for (Future<Void> future : executor.invokeAll(queries)) {
+        future.get();
+      }
+    } finally {
+      executor.shutdownNow();
+    }
+  }
+
+  private static SinglePartitionColumnSegmentPruner pruner(Map<String, 
ZNRecord> records) {
+    SinglePartitionColumnSegmentPruner pruner = new 
SinglePartitionColumnSegmentPruner(TABLE, COLUMN);
+    pruner.init(null, null, new ArrayList<>(records.keySet()), new 
ArrayList<>(records.values()));
+    return pruner;
+  }
+
+  private static SinglePartitionColumnSegmentPruner pruner(Map<String, 
ZNRecord> records,
+      @Nullable Integer queryMinSegments) {
+    SinglePartitionColumnSegmentPruner pruner = new 
SinglePartitionColumnSegmentPruner(TABLE, COLUMN) {
+      @Override
+      public Set<String> prune(BrokerRequest request, Set<String> segments) {
+        if (queryMinSegments != null) {
+          request.getPinotQuery().setQueryOptions(
+              Map.of(QueryOptionKey.PARTITION_PRUNING_PREPARATION_THRESHOLD, 
queryMinSegments.toString()));
+        }
+        return super.prune(request, segments);
+      }
+    };
+    pruner.init(null, null, new ArrayList<>(records.keySet()), new 
ArrayList<>(records.values()));
+    return pruner;
+  }
+
+  private static ZNRecord metadata(String segment, String function, int count, 
Set<Integer> partitions,
+      @Nullable Map<String, String> config) throws Exception {
+    ZNRecord record = new ZNRecord(segment);
+    record.setSimpleField(CommonConstants.Segment.PARTITION_METADATA, new 
SegmentPartitionMetadata(Map.of(COLUMN,
+        new ColumnPartitionMetadata(function, count, partitions, 
config))).toJsonString());
+    return record;
+  }
+
+  private static Expression predicate(String operator, String... values) {
+    List<Expression> operands = new ArrayList<>();
+    operands.add(RequestUtils.getIdentifierExpression(COLUMN));
+    for (String value : values) {
+      operands.add(RequestUtils.getLiteralExpression(value));
+    }
+    return RequestUtils.getFunctionExpression(operator, operands);
+  }
+
+  private static Expression function(String operator, Expression... operands) {
+    return RequestUtils.getFunctionExpression(operator, List.of(operands));
+  }
+
+  private static BrokerRequest request(Expression filter) {
+    PinotQuery pinotQuery = new PinotQuery();
+    pinotQuery.setFilterExpression(filter);
+    BrokerRequest brokerRequest = new BrokerRequest();
+    brokerRequest.setPinotQuery(pinotQuery);
+    return brokerRequest;
+  }
+
+  /// Legacy plugin with identity equality and the default preparation 
capability, registered by the factory scan.
+  public static class LegacyPartitionFunction implements PartitionFunction {
+    private static final long serialVersionUID = 1L;
+    private final int _numPartitions;
+
+    public LegacyPartitionFunction(int numPartitions, @Nullable Map<String, 
String> config) {
+      _numPartitions = numPartitions;
+    }
+
+    @Override
+    public int getPartition(String value) {
+      CountingPartitionFunction.CALLS.incrementAndGet();
+      return Math.floorMod(Integer.parseInt(value), _numPartitions);
+    }
+
+    @Override
+    public String getName() {
+      return "PrunerLegacy";
+    }
+
+    @Override
+    public int getNumPartitions() {
+      return _numPartitions;
+    }
+
+    @Override
+    public PartitionIdNormalizer getPartitionIdNormalizer() {
+      return PartitionIdNormalizer.POSITIVE_MODULO;
+    }
+  }
+
+  /// Stateless partition function registered by the factory scan with 
observable hash calls. Exact-count tests require
+  /// sequential test methods; the concurrent-query fixture deliberately uses 
Modulo instead.
+  public static class CountingPartitionFunction implements PartitionFunction {
+    private static final long serialVersionUID = 1L;
+    private static final AtomicInteger CALLS = new AtomicInteger();
+    private final int _numPartitions;
+    private final int _offset;
+    private final Map<String, String> _functionConfig;
+
+    public CountingPartitionFunction(int numPartitions, @Nullable Map<String, 
String> config) {
+      _numPartitions = numPartitions;
+      _offset = config == null ? 0 : 
Integer.parseInt(config.getOrDefault("offset", "0"));
+      _functionConfig = config == null ? null : Map.copyOf(config);
+    }
+
+    @Override
+    public int getPartition(String value) {
+      CALLS.incrementAndGet();
+      return Math.floorMod(Integer.parseInt(value) + _offset, _numPartitions);
+    }
+
+    @Override
+    public String getName() {
+      return "PrunerCounting";
+    }
+
+    @Override
+    public int getNumPartitions() {
+      return _numPartitions;
+    }
+
+    @Override
+    public Map<String, String> getFunctionConfig() {
+      return _functionConfig;
+    }
+
+    @Override
+    public PartitionIdNormalizer getPartitionIdNormalizer() {
+      return PartitionIdNormalizer.POSITIVE_MODULO;
+    }
+
+    @Override
+    public boolean supportsPartitionIdPreparation() {
+      return true;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (this == other) {
+        return true;
+      }
+      if (other == null || getClass() != other.getClass()) {
+        return false;
+      }
+      CountingPartitionFunction that = (CountingPartitionFunction) other;
+      return _numPartitions == that._numPartitions && _offset == that._offset;
+    }
+
+    @Override
+    public int hashCode() {
+      return 31 * _numPartitions + _offset;
+    }
+  }
+}
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/partition/function/ByteArrayPartitionFunction.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/partition/function/ByteArrayPartitionFunction.java
index 90b1d030d86..138e0b083fd 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/partition/function/ByteArrayPartitionFunction.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/partition/function/ByteArrayPartitionFunction.java
@@ -64,6 +64,30 @@ public class ByteArrayPartitionFunction implements 
PartitionFunction {
     return _normalizer;
   }
 
+  @Override
+  public boolean supportsPartitionIdPreparation() {
+    return getClass() == ByteArrayPartitionFunction.class;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+    if (other == null || getClass() != other.getClass()) {
+      return false;
+    }
+    ByteArrayPartitionFunction that = (ByteArrayPartitionFunction) other;
+    return _numPartitions == that._numPartitions && _normalizer == 
that._normalizer;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = _numPartitions;
+    result = 31 * result + _normalizer.hashCode();
+    return result;
+  }
+
   // Keep it for backward-compatibility, use getName() instead
   @Override
   public String toString() {
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/partition/function/FnvPartitionFunction.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/partition/function/FnvPartitionFunction.java
index 83fe5a8d5c0..ca288ec581f 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/partition/function/FnvPartitionFunction.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/partition/function/FnvPartitionFunction.java
@@ -102,6 +102,34 @@ public class FnvPartitionFunction implements 
PartitionFunction {
     return _normalizer;
   }
 
+  @Override
+  public boolean supportsPartitionIdPreparation() {
+    return getClass() == FnvPartitionFunction.class;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+    if (other == null || getClass() != other.getClass()) {
+      return false;
+    }
+    FnvPartitionFunction that = (FnvPartitionFunction) other;
+    return _numPartitions == that._numPartitions && _normalizer == 
that._normalizer
+        && _variant == that._variant
+        && _useRawBytes == that._useRawBytes;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = _numPartitions;
+    result = 31 * result + _normalizer.hashCode();
+    result = 31 * result + _variant.hashCode();
+    result = 31 * result + Boolean.hashCode(_useRawBytes);
+    return result;
+  }
+
   // Keep it for backward-compatibility, use getName() instead
   @Override
   public String toString() {
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/partition/function/HashCodePartitionFunction.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/partition/function/HashCodePartitionFunction.java
index a6f5c5867d7..b36456bbb56 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/partition/function/HashCodePartitionFunction.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/partition/function/HashCodePartitionFunction.java
@@ -61,6 +61,30 @@ public class HashCodePartitionFunction implements 
PartitionFunction {
     return _normalizer;
   }
 
+  @Override
+  public boolean supportsPartitionIdPreparation() {
+    return getClass() == HashCodePartitionFunction.class;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+    if (other == null || getClass() != other.getClass()) {
+      return false;
+    }
+    HashCodePartitionFunction that = (HashCodePartitionFunction) other;
+    return _numPartitions == that._numPartitions && _normalizer == 
that._normalizer;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = _numPartitions;
+    result = 31 * result + _normalizer.hashCode();
+    return result;
+  }
+
   // Keep it for backward-compatibility, use getName() instead
   @Override
   public String toString() {
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/partition/function/ModuloPartitionFunction.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/partition/function/ModuloPartitionFunction.java
index 8d34d855a75..48c2b35577f 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/partition/function/ModuloPartitionFunction.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/partition/function/ModuloPartitionFunction.java
@@ -60,6 +60,30 @@ public class ModuloPartitionFunction implements 
PartitionFunction {
     return _normalizer;
   }
 
+  @Override
+  public boolean supportsPartitionIdPreparation() {
+    return getClass() == ModuloPartitionFunction.class;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+    if (other == null || getClass() != other.getClass()) {
+      return false;
+    }
+    ModuloPartitionFunction that = (ModuloPartitionFunction) other;
+    return _numPartitions == that._numPartitions && _normalizer == 
that._normalizer;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = _numPartitions;
+    result = 31 * result + _normalizer.hashCode();
+    return result;
+  }
+
   // Keep it for backward-compatibility, use getName() instead
   @Override
   public String toString() {
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/partition/function/Murmur3PartitionFunction.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/partition/function/Murmur3PartitionFunction.java
index 19b35c1aae3..1cd9012e8f4 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/partition/function/Murmur3PartitionFunction.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/partition/function/Murmur3PartitionFunction.java
@@ -114,6 +114,35 @@ public class Murmur3PartitionFunction implements 
PartitionFunction {
     return _normalizer;
   }
 
+  @Override
+  public boolean supportsPartitionIdPreparation() {
+    return getClass() == Murmur3PartitionFunction.class;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+    if (other == null || getClass() != other.getClass()) {
+      return false;
+    }
+    Murmur3PartitionFunction that = (Murmur3PartitionFunction) other;
+    return _numPartitions == that._numPartitions && _normalizer == 
that._normalizer
+        && _seed == that._seed && _useX64 == that._useX64
+        && _useRawBytes == that._useRawBytes;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = _numPartitions;
+    result = 31 * result + _normalizer.hashCode();
+    result = 31 * result + _seed;
+    result = 31 * result + Boolean.hashCode(_useX64);
+    result = 31 * result + Boolean.hashCode(_useRawBytes);
+    return result;
+  }
+
   // Keep it for backward-compatibility, use getName() instead
   @Override
   public String toString() {
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/partition/function/MurmurPartitionFunction.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/partition/function/MurmurPartitionFunction.java
index 5396fbe07cc..5a8383aa6a6 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/partition/function/MurmurPartitionFunction.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/partition/function/MurmurPartitionFunction.java
@@ -86,6 +86,32 @@ public class MurmurPartitionFunction implements 
PartitionFunction {
     return _normalizer;
   }
 
+  @Override
+  public boolean supportsPartitionIdPreparation() {
+    return getClass() == MurmurPartitionFunction.class;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+    if (other == null || getClass() != other.getClass()) {
+      return false;
+    }
+    MurmurPartitionFunction that = (MurmurPartitionFunction) other;
+    return _numPartitions == that._numPartitions && _normalizer == 
that._normalizer
+        && _useRawBytes == that._useRawBytes;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = _numPartitions;
+    result = 31 * result + _normalizer.hashCode();
+    result = 31 * result + Boolean.hashCode(_useRawBytes);
+    return result;
+  }
+
   // Keep it for backward-compatibility, use getName() instead
   @Override
   public String toString() {
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionsUtils.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionsUtils.java
index 3b0da6329eb..f5f8168ca5a 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionsUtils.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionsUtils.java
@@ -353,6 +353,14 @@ public class QueryOptionsUtils {
         queryOptions.get(QueryOptionKey.IN_PREDICATE_PRUNING_THRESHOLD));
   }
 
+  /// Query-level override for the minimum candidate segments needed to 
prepare the partition-pruning predicate.
+  /// A negative value disables preparation.
+  @Nullable
+  public static Integer getPartitionPruningPreparationThreshold(@Nullable 
Map<String, String> queryOptions) {
+    return queryOptions != null ? 
uncheckedParseInt(QueryOptionKey.PARTITION_PRUNING_PREPARATION_THRESHOLD,
+        
queryOptions.get(QueryOptionKey.PARTITION_PRUNING_PREPARATION_THRESHOLD)) : 
null;
+  }
+
   /// Returns whether materialized-view rewrite is allowed for this query. 
Defaults to `true`
   /// (absence ⇒ rewrite allowed) for backward compatibility with the 
pre-option behavior; the
   /// MV minion executor sets it to `false` so a materialization query is 
never rewritten back
diff --git 
a/pinot-common/src/test/java/org/apache/pinot/common/partition/function/PartitionFunctionTest.java
 
b/pinot-common/src/test/java/org/apache/pinot/common/partition/function/PartitionFunctionTest.java
index 6e55464c472..ffce3322e6b 100644
--- 
a/pinot-common/src/test/java/org/apache/pinot/common/partition/function/PartitionFunctionTest.java
+++ 
b/pinot-common/src/test/java/org/apache/pinot/common/partition/function/PartitionFunctionTest.java
@@ -28,10 +28,12 @@ import org.apache.pinot.spi.utils.BytesUtils;
 import org.apache.pinot.spi.utils.JsonUtils;
 import org.apache.pinot.spi.utils.hash.FnvHashFunctions;
 import org.apache.pinot.spi.utils.hash.MurmurHashFunctions;
+import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNotEquals;
 import static org.testng.Assert.assertTrue;
 import static org.testng.Assert.expectThrows;
@@ -42,6 +44,117 @@ public class PartitionFunctionTest {
   private static final int NUM_ROUNDS = 1000;
   private static final int MAX_NUM_PARTITIONS = 100;
 
+  @Test
+  public void testPartitionIdReuse() {
+    PartitionFunction function = new MurmurPartitionFunction(8, null);
+    PartitionFunction emptyConfig = new MurmurPartitionFunction(8, Map.of());
+    assertTrue(function.equals(emptyConfig));
+    assertTrue(emptyConfig.equals(function));
+    PartitionFunction third = new MurmurPartitionFunction(8, Map.of());
+    assertTrue(emptyConfig.equals(third));
+    assertTrue(function.equals(third));
+    assertFalse(function.equals(new MurmurPartitionFunction(16, null)));
+    assertFalse(function.equals(new Murmur3PartitionFunction(8, null)));
+
+    PartitionFunction configured = new MurmurPartitionFunction(8, 
Map.of("useRawBytes", "true"));
+    assertTrue(configured.equals(configured));
+    assertFalse(function.equals(configured));
+    assertFalse(configured.equals(function));
+    assertTrue(configured.equals(new MurmurPartitionFunction(8, 
Map.of("useRawBytes", "true"))));
+
+    PartitionFunction modulo = new ModuloPartitionFunction(8, null);
+    PartitionFunction abs = new ModuloPartitionFunction(8, 
Map.of("partitionIdNormalizer", "ABS"));
+    assertFalse(modulo.equals(abs));
+    assertFalse(abs.equals(modulo));
+  }
+
+  @DataProvider
+  public Object[][] reuseConfigurations() {
+    return new Object[][]{
+        {"Murmur", Map.of("useRawBytes", "true"), Map.of("useRawBytes", 
"TRUE"),
+            Map.of("useRawBytes", "false")},
+        {"Murmur3", Map.of("seed", "7"), Map.of("seed", "07", "variant", 
"x86_32"), Map.of("seed", "8")},
+        {"Murmur3", Map.of("variant", "x64_32"), Map.of("variant", "x64_32", 
"seed", "0"),
+            Map.of("variant", "x86_32")},
+        {"Murmur3", Map.of("useRawBytes", "true"), Map.of("useRawBytes", 
"TRUE"),
+            Map.of("useRawBytes", "false")},
+        {"FNV", Map.of("variant", "FNV1_64"), Map.of("variant", "FNV1_64", 
"useRawBytes", "false"),
+            Map.of("variant", "FNV1A_64")},
+        {"FNV", Map.of("useRawBytes", "true"), Map.of("useRawBytes", "TRUE"), 
Map.of("useRawBytes", "false")},
+        {"Modulo", Map.of("partitionIdNormalizer", "ABS"), 
Map.of("partitionIdNormalizer", " abs "),
+            Map.of("partitionIdNormalizer", "POSITIVE_MODULO")},
+        {"HashCode", Map.of(), Map.of("partitionIdNormalizer", 
"PRE_MODULO_ABS"),
+            Map.of("partitionIdNormalizer", "MASK")},
+        {"ByteArray", Map.of(), Map.of("partitionIdNormalizer", 
"PRE_MODULO_ABS"),
+            Map.of("partitionIdNormalizer", "MASK")}
+    };
+  }
+
+  @Test
+  public void testComplexFunctionsAndSubclasses() {
+    Map<String, String> config = Map.of("columnValues", "a|b", 
"columnValuesDelimiter", "|");
+    PartitionFunction bounded = new BoundedColumnValuePartitionFunction(3, 
config);
+    assertFalse(bounded.supportsPartitionIdPreparation());
+    assertTrue(bounded.equals(bounded));
+    assertFalse(bounded.equals(new BoundedColumnValuePartitionFunction(3, 
config)));
+    class CustomMurmur extends MurmurPartitionFunction {
+      CustomMurmur() {
+        super(8, null);
+      }
+    }
+    PartitionFunction subclass = new CustomMurmur();
+    assertFalse(subclass.supportsPartitionIdPreparation());
+    PartitionFunction another = new CustomMurmur();
+    assertTrue(subclass.equals(subclass));
+    assertTrue(subclass.equals(another));
+    assertFalse(subclass.equals(new MurmurPartitionFunction(8, null)));
+    assertFalse(new MurmurPartitionFunction(8, null).equals(subclass));
+  }
+
+  @Test
+  public void testHashCollisionDoesNotImplyEquality() {
+    // Increasing the partition count and decreasing the seed compensates in 
the 31-based hash.
+    PartitionFunction first = new Murmur3PartitionFunction(8, Map.of("seed", 
"961"));
+    PartitionFunction second = new Murmur3PartitionFunction(9, Map.of("seed", 
"0"));
+    assertEquals(first.hashCode(), second.hashCode());
+    assertNotEquals(first, second);
+    assertFalse(first.equals(second));
+  }
+
+  @Test(dataProvider = "reuseConfigurations")
+  public void testConfiguredPartitionIdReuse(String name, Map<String, String> 
config,
+      Map<String, String> equivalentConfig, Map<String, String> 
differentConfig) {
+    Map<String, String> mutableConfig = new HashMap<>(config);
+    PartitionFunction first = 
PartitionFunctionFactory.getPartitionFunction(name, 8, mutableConfig);
+    assertTrue(first.supportsPartitionIdPreparation());
+    PartitionFunction second = 
PartitionFunctionFactory.getPartitionFunction(name, 8, equivalentConfig);
+    PartitionFunction third = 
PartitionFunctionFactory.getPartitionFunction(name, 8, new 
HashMap<>(equivalentConfig));
+    assertTrue(first.equals(first));
+    assertEquals(first, second);
+    assertEquals(first.hashCode(), second.hashCode());
+    assertFalse(first.equals(null));
+    assertTrue(first.equals(second));
+    assertTrue(second.equals(first));
+    assertTrue(second.equals(third));
+    assertTrue(first.equals(third));
+    for (String value : new String[]{"00", "1234", "abcd"}) {
+      if (!name.equals("Modulo") || !value.equals("abcd")) {
+        assertEquals(first.getPartition(value), second.getPartition(value));
+      }
+    }
+    PartitionFunction different = 
PartitionFunctionFactory.getPartitionFunction(name, 8, differentConfig);
+    assertFalse(first.equals(different));
+    assertFalse(different.equals(first));
+    
assertFalse(first.equals(PartitionFunctionFactory.getPartitionFunction(name, 
16, config)));
+    Map<String, String> otherNormalizer = new HashMap<>(config);
+    otherNormalizer.put("partitionIdNormalizer",
+        first.getPartitionIdNormalizer().name().equals("MASK") ? "ABS" : 
"MASK");
+    
assertFalse(first.equals(PartitionFunctionFactory.getPartitionFunction(name, 8, 
otherNormalizer)));
+    mutableConfig.clear();
+    mutableConfig.putAll(differentConfig);
+    assertTrue(first.equals(second), "Caller mutations must not change the 
effective function");
+  }
+
   /// Unit test for [ModuloPartitionFunction].
   ///
   /// - Builds an instance of the [ModuloPartitionFunction].
@@ -143,6 +256,7 @@ public class PartitionFunctionTest {
       // Create partition function with function config present but no seed 
value present.
       PartitionFunction partitionFunction2 =
           PartitionFunctionFactory.getPartitionFunction(functionName, 
numPartitions, functionConfig);
+      testBasicProperties(partitionFunction2, functionName, numPartitions, 
functionConfig);
 
       // Get partition number with random value.
       int partitionNumWithNoSeedValue = 
partitionFunction2.getPartition(valueTobeHashed);
@@ -156,12 +270,14 @@ public class PartitionFunctionTest {
       // Create partition function with function config present but random 
seed value present in function config.
       PartitionFunction partitionFunction3 =
           PartitionFunctionFactory.getPartitionFunction(functionName, 
numPartitions, functionConfig);
+      testBasicProperties(partitionFunction3, functionName, numPartitions, 
functionConfig);
 
       // Create partition function with function config present with random 
seed value
       // and with variant provided as "x64_32" in function config.
       functionConfig.put("variant", "x64_32");
       PartitionFunction partitionFunction4 =
           PartitionFunctionFactory.getPartitionFunction(functionName, 
numPartitions, functionConfig);
+      testBasicProperties(partitionFunction4, functionName, numPartitions, 
functionConfig);
 
       // Put variant value as "x86_32" in function config.
       functionConfig.put("variant", "x86_32");
@@ -172,6 +288,7 @@ public class PartitionFunctionTest {
       // Create partition function with function config present with variant 
provided as "x86_32" in function config.
       PartitionFunction partitionFunction5 =
           PartitionFunctionFactory.getPartitionFunction(functionName, 
numPartitions, functionConfig);
+      testBasicProperties(partitionFunction5, functionName, numPartitions, 
functionConfig);
 
       // Partition number should be equal as partitionNumWithNullConfig and 
partitionNumWithNoSeedValue as this is
       // default behavior.
@@ -184,6 +301,7 @@ public class PartitionFunctionTest {
       // value in functionConfig.
       PartitionFunction partitionFunction6 =
           PartitionFunctionFactory.getPartitionFunction(functionName, 
numPartitions, functionConfig);
+      testBasicProperties(partitionFunction6, functionName, numPartitions, 
functionConfig);
 
       // Partition number should be equal as partitionNumWithNullConfig and 
partitionNumWithNoSeedValue as this is
       // default behavior.
@@ -201,11 +319,6 @@ public class PartitionFunctionTest {
       assertEquals(partitionFunction7.getPartition(valueTobeHashed), 
partitionNumWithNullConfig);
 
       testBasicProperties(partitionFunction1, functionName, numPartitions);
-      testBasicProperties(partitionFunction2, functionName, numPartitions, 
functionConfig);
-      testBasicProperties(partitionFunction3, functionName, numPartitions, 
functionConfig);
-      testBasicProperties(partitionFunction4, functionName, numPartitions, 
functionConfig);
-      testBasicProperties(partitionFunction5, functionName, numPartitions, 
functionConfig);
-      testBasicProperties(partitionFunction6, functionName, numPartitions, 
functionConfig);
       testBasicProperties(partitionFunction7, functionName, numPartitions, 
functionConfig);
 
       for (int j = 0; j < NUM_ROUNDS; j++) {
diff --git 
a/pinot-common/src/test/java/org/apache/pinot/common/utils/config/QueryOptionsUtilsTest.java
 
b/pinot-common/src/test/java/org/apache/pinot/common/utils/config/QueryOptionsUtilsTest.java
index 73d248c5c09..b432f47c948 100644
--- 
a/pinot-common/src/test/java/org/apache/pinot/common/utils/config/QueryOptionsUtilsTest.java
+++ 
b/pinot-common/src/test/java/org/apache/pinot/common/utils/config/QueryOptionsUtilsTest.java
@@ -120,6 +120,16 @@ public class QueryOptionsUtilsTest {
     assertNull(QueryOptionsUtils.getInPredicatePruningThreshold(Map.of()));
   }
 
+  @Test
+  public void shouldReadPartitionPruningPreparationThresholdOption() {
+    assertEquals(QueryOptionsUtils.getPartitionPruningPreparationThreshold(
+        Map.of(PARTITION_PRUNING_PREPARATION_THRESHOLD, "20")), 20);
+    assertEquals(QueryOptionsUtils.getPartitionPruningPreparationThreshold(
+        Map.of(PARTITION_PRUNING_PREPARATION_THRESHOLD, "-1")), -1);
+    
assertNull(QueryOptionsUtils.getPartitionPruningPreparationThreshold(Map.of()));
+    
assertNull(QueryOptionsUtils.getPartitionPruningPreparationThreshold(null));
+  }
+
   @Test(expectedExceptions = IllegalArgumentException.class)
   public void shouldRejectInvalidInPredicatePruningThreshold() {
     
QueryOptionsUtils.getInPredicatePruningThreshold(Map.of(IN_PREDICATE_PRUNING_THRESHOLD,
 "invalid"));
diff --git 
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/partition/PartitionFunction.java
 
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/partition/PartitionFunction.java
index 69e9c5cfffd..743105bc25a 100644
--- 
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/partition/PartitionFunction.java
+++ 
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/partition/PartitionFunction.java
@@ -30,6 +30,10 @@ import javax.annotation.Nullable;
 /// Implementations of this interface are assumed not to be stateful. That is, 
two invocations of
 /// `PartitionFunction.getPartition(value)` with the same value are expected 
to produce the same
 /// result. Implementations must also be safe for concurrent invocation by 
multiple threads.
+/// Functions with equal identity may reuse partition ids during broker 
pruning. Implementations that override
+/// `equals` and `hashCode` must include every setting that affects partition 
ids, including subclass state.
+/// The raw function-config map need not be compared when parsed fields 
already capture all partition-ID-affecting
+/// settings; unrelated config entries should not prevent reuse.
 public interface PartitionFunction extends Serializable {
 
   /// Method to compute and return partition id for the given value.
@@ -58,6 +62,13 @@ public interface PartitionFunction extends Serializable {
   /// @return Number of possible partitions.
   int getNumPartitions();
 
+  /// Whether query-local predicate preparation is useful for this function. 
Implementations must opt in only when
+  /// they provide cheap value equality across segments. The default preserves 
per-segment pruning for legacy plugins.
+  default boolean supportsPartitionIdPreparation() {
+    return false;
+  }
+
+  /// Returns the configuration used to construct this function, or null if 
none is exposed.
   @Nullable
   default Map<String, String> getFunctionConfig() {
     return null;
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/config/table/RoutingConfig.java 
b/pinot-spi/src/main/java/org/apache/pinot/spi/config/table/RoutingConfig.java
index b3cde3ea82b..d88428b0a09 100644
--- 
a/pinot-spi/src/main/java/org/apache/pinot/spi/config/table/RoutingConfig.java
+++ 
b/pinot-spi/src/main/java/org/apache/pinot/spi/config/table/RoutingConfig.java
@@ -42,16 +42,26 @@ public class RoutingConfig extends BaseJsonConfig {
   private final List<String> _segmentPrunerTypes;
   private final String _instanceSelectorType;
   private final Boolean _useFixedReplica;
+  private final Integer _partitionPruningPreparationThreshold;
 
-  @JsonCreator
   public RoutingConfig(@JsonProperty("routingTableBuilderName") @Nullable 
String routingTableBuilderName,
       @JsonProperty("segmentPrunerTypes") @Nullable List<String> 
segmentPrunerTypes,
       @JsonProperty("instanceSelectorType") @Nullable String 
instanceSelectorType,
       @JsonProperty("useFixedReplica") @Nullable Boolean useFixedReplica) {
+    this(routingTableBuilderName, segmentPrunerTypes, instanceSelectorType, 
useFixedReplica, null);
+  }
+
+  @JsonCreator
+  public RoutingConfig(@JsonProperty("routingTableBuilderName") @Nullable 
String routingTableBuilderName,
+      @JsonProperty("segmentPrunerTypes") @Nullable List<String> 
segmentPrunerTypes,
+      @JsonProperty("instanceSelectorType") @Nullable String 
instanceSelectorType,
+      @JsonProperty("useFixedReplica") @Nullable Boolean useFixedReplica,
+      @JsonProperty("partitionPruningPreparationThreshold") @Nullable Integer 
partitionPruningPreparationThreshold) {
     _routingTableBuilderName = routingTableBuilderName;
     _segmentPrunerTypes = segmentPrunerTypes;
     _instanceSelectorType = instanceSelectorType;
     _useFixedReplica = useFixedReplica;
+    _partitionPruningPreparationThreshold = 
partitionPruningPreparationThreshold;
   }
 
   @Nullable
@@ -73,4 +83,11 @@ public class RoutingConfig extends BaseJsonConfig {
   public Boolean getUseFixedReplica() {
     return _useFixedReplica;
   }
+
+  /// Returns the table-level override for the minimum candidate segments 
needed to prepare a partition-pruning
+  /// predicate. A negative value disables preparation. Null means use the 
cluster-level value.
+  @Nullable
+  public Integer getPartitionPruningPreparationThreshold() {
+    return _partitionPruningPreparationThreshold;
+  }
 }
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java 
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
index 7ab92db3953..70b97dbc3f5 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
@@ -339,6 +339,10 @@ public class CommonConstants {
   }
 
   public static class Broker {
+    /// Minimum candidate segments for preparing a partition-pruning 
predicate. A negative value disables preparation.
+    public static final String 
CONFIG_OF_PARTITION_PRUNING_PREPARATION_THRESHOLD =
+        "pinot.broker.partition.pruning.preparation.threshold";
+    public static final int DEFAULT_PARTITION_PRUNING_PREPARATION_THRESHOLD = 
32;
     public static final String ROUTING_TABLE_CONFIG_PREFIX = 
"pinot.broker.routing.table";
     public static final String ACCESS_CONTROL_CONFIG_PREFIX = 
"pinot.broker.access.control";
     /// Namespace for service credentials used by the broker when invoking 
Server admin APIs.
@@ -1029,6 +1033,10 @@ public class CommonConstants {
         /// Query-level override for `inpredicate.threshold`. Negative means 
always prune.
         public static final String IN_PREDICATE_PRUNING_THRESHOLD = 
"inPredicatePruningThreshold";
 
+        /// Query-level override for the minimum candidate segments needed to 
prepare the partition-pruning predicate.
+        /// A negative value disables preparation, but does not disable 
partition pruning itself.
+        public static final String PARTITION_PRUNING_PREPARATION_THRESHOLD = 
"partitionPruningPreparationThreshold";
+
         // When evaluating REGEXP_LIKE predicate on a dictionary encoded 
column:
         // - If dictionary size is smaller than this threshold, scan the 
dictionary to get the matching dictionary ids
         //   first, where inverted index can be applied if exists


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

Reply via email to