This is an automated email from the ASF dual-hosted git repository.
morrySnow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new df36be5a86d [fix](point query) Keep point-query scan when partition
pruning is empty (#67161)
df36be5a86d is described below
commit df36be5a86d916cb8b94b739482f744a0e129f1b
Author: zyp-V <[email protected]>
AuthorDate: Tue Sep 1 10:27:13 2026 +0800
[fix](point query) Keep point-query scan when partition pruning is empty
(#67161)
### What problem does this PR solve?
Problem Summary: Short-circuit point queries on partitioned tables were
rewritten to LogicalEmptyRelation when their partition predicates
matched no existing partition. This removed the LogicalOlapScan required
to initialize and cache ShortCircuitQueryContext, causing the
point-query path to fail or fall back to normal planning. Preserve a
partition-pruned scan with an empty selected partition list only for
short-circuit point queries, while retaining LogicalEmptyRelation for
ordinary queries and preserving the effective partition-predicate
marker.
### Release note
Keep short-circuit point-query planning active when no table partition
matches the query predicate.
Co-authored-by: zhangyipeng.0818 <[email protected]>
---
.../java/org/apache/doris/catalog/OlapTable.java | 21 +++-
.../LogicalResultSinkToShortCircuitPointQuery.java | 16 ++-
.../rules/rewrite/PruneOlapScanPartition.java | 12 +-
.../trees/plans/commands/ExecuteCommand.java | 15 ++-
.../apache/doris/qe/ShortCircuitQueryContext.java | 6 +-
.../java/org/apache/doris/qe/StmtExecutor.java | 10 +-
.../org/apache/doris/catalog/OlapTableTest.java | 29 +++++
.../rules/rewrite/ShortCircuitPointQueryTest.java | 118 +++++++++++++++++--
.../doris/qe/ShortCircuitQueryContextTest.java | 29 +++++
.../test_point_query_partition_not_exists.out | 21 ++++
.../test_point_query_partition_not_exists.groovy | 130 +++++++++++++++++++++
11 files changed, 381 insertions(+), 26 deletions(-)
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/OlapTable.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/OlapTable.java
index eec6e523ad1..a3eef1688b4 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/OlapTable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/OlapTable.java
@@ -125,6 +125,7 @@ import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
@@ -187,6 +188,10 @@ public class OlapTable extends Table implements
MTMVRelatedTableIf, GsonPostProc
@SerializedName(value = "itp", alternate = {"idToPartition"})
@Getter
protected ConcurrentHashMap<Long, Partition> idToPartition = new
ConcurrentHashMap<>();
+ // Incremented only when the formal partition id set changes. Prepared
short-circuit point query cache uses this
+ // to invalidate stale partition pruning metadata without reacting to
ordinary data writes. This is transient and
+ // rebuilt from zero after deserialization because prepared statement
cache is also in-memory.
+ private transient AtomicLong partitionTopologyVersion = new AtomicLong(0L);
// handled in postgsonprocess
@Getter
protected Map<String, Partition> nameToPartition = Maps.newTreeMap();
@@ -1335,8 +1340,11 @@ public class OlapTable extends Table implements
MTMVRelatedTableIf, GsonPostProc
}
public void addPartition(Partition partition) {
- idToPartition.put(partition.getId(), partition);
+ Partition previousPartition = idToPartition.put(partition.getId(),
partition);
nameToPartition.put(partition.getName(), partition);
+ if (previousPartition == null) {
+ bumpPartitionTopologyVersion();
+ }
}
// This is a private method.
@@ -1352,6 +1360,7 @@ public class OlapTable extends Table implements
MTMVRelatedTableIf, GsonPostProc
if (partition != null) {
idToPartition.remove(partition.getId());
nameToPartition.remove(partitionName);
+ bumpPartitionTopologyVersion();
RecyclePartitionParam recyclePartitionParam = new
RecyclePartitionParam();
fillInfo(partition, recyclePartitionParam);
dropPartitionCommon(dbId, isForceDrop, recyclePartitionParam,
partition, reserveTablets);
@@ -1649,6 +1658,14 @@ public class OlapTable extends Table implements
MTMVRelatedTableIf, GsonPostProc
return new ArrayList<>(idToPartition.keySet());
}
+ public long getPartitionTopologyVersion() {
+ return partitionTopologyVersion.get();
+ }
+
+ private void bumpPartitionTopologyVersion() {
+ partitionTopologyVersion.incrementAndGet();
+ }
+
public Set<String> getCopiedBfColumns() {
if (bfColumns == null) {
return null;
@@ -2079,6 +2096,7 @@ public class OlapTable extends Table implements
MTMVRelatedTableIf, GsonPostProc
@Override
public void gsonPostProcess() throws IOException {
+ partitionTopologyVersion = new AtomicLong(0L);
// HACK: the index id in MaterializedIndexMeta is not equals to the
index id
// saved in OlapTable, because the table restore from snapshot is not
reset
@@ -2239,6 +2257,7 @@ public class OlapTable extends Table implements
MTMVRelatedTableIf, GsonPostProc
idToPartition.put(newPartition.getId(), newPartition);
nameToPartition.put(newPartition.getName(), newPartition);
+ bumpPartitionTopologyVersion();
DataProperty dataProperty =
partitionInfo.getDataProperty(oldPartition.getId());
ReplicaAllocation replicaAlloc =
partitionInfo.getReplicaAllocation(oldPartition.getId());
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LogicalResultSinkToShortCircuitPointQuery.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LogicalResultSinkToShortCircuitPointQuery.java
index dfcd2ea289c..f839b81b601 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LogicalResultSinkToShortCircuitPointQuery.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LogicalResultSinkToShortCircuitPointQuery.java
@@ -19,6 +19,7 @@ package org.apache.doris.nereids.rules.rewrite;
import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.datasource.doris.RemoteOlapTable;
import org.apache.doris.nereids.StatementContext;
import org.apache.doris.nereids.rules.Rule;
import org.apache.doris.nereids.rules.RuleType;
@@ -31,6 +32,7 @@ import
org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
import org.apache.doris.qe.ConnectContext;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
@@ -60,11 +62,23 @@ public class LogicalResultSinkToShortCircuitPointQuery
implements RewriteRuleFac
&& expression.child(1).isLiteral());
}
- private boolean scanMatchShortCircuitCondition(LogicalOlapScan olapScan) {
+ @VisibleForTesting
+ boolean scanMatchShortCircuitCondition(LogicalOlapScan olapScan) {
if
(!ConnectContext.get().getSessionVariable().isEnableShortCircuitQuery()) {
return false;
}
+ // Lazy point-query pruning does not preserve explicit
PARTITION/TABLET restrictions.
+ // Keep these queries on the normal execution path so the physical
scan enforces them.
+ if (!olapScan.getManuallySpecifiedPartitions().isEmpty()
+ || !olapScan.getManuallySpecifiedTabletIds().isEmpty()) {
+ return false;
+ }
OlapTable olapTable = olapScan.getTable();
+ // Remote Doris metadata refresh replaces the RemoteOlapTable
instance. A prepared context retains the old
+ // instance, so its table-local topology version cannot observe remote
partition changes.
+ if (olapTable instanceof RemoteOlapTable) {
+ return false;
+ }
if (olapTable.hasVariantColumns()) {
return false;
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PruneOlapScanPartition.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PruneOlapScanPartition.java
index aa5f722587f..c89234b92fe 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PruneOlapScanPartition.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PruneOlapScanPartition.java
@@ -132,14 +132,20 @@ public class PruneOlapScanPartition implements
RewriteRuleFactory {
if (prunedPartitions == null) {
return Pair.of(null, Optional.empty());
}
+ boolean hasPartitionPredicate =
prunedPartitionsByFilters.hasPartitionPredicate
+ || !scan.getManuallySpecifiedPartitions().isEmpty()
+ || !scan.getManuallySpecifiedTabletIds().isEmpty();
if (prunedPartitions.isEmpty()) {
+ // Keep scan for short-circuit point queries even when no
partition matches.
+ // ShortCircuitQueryContext needs the scan to initialize and cache
the point-query path.
+ if (filter != null && ctx.statementContext.isShortCircuitQuery()) {
+ return Pair.of(scan.withSelectedPartitionIds(prunedPartitions,
hasPartitionPredicate),
+ Optional.empty());
+ }
return Pair.of(new LogicalEmptyRelation(
ConnectContext.get().getStatementContext().getNextRelationId(),
ctx.root.getOutput()), Optional.empty());
}
- boolean hasPartitionPredicate =
prunedPartitionsByFilters.hasPartitionPredicate
- || !scan.getManuallySpecifiedPartitions().isEmpty()
- || !scan.getManuallySpecifiedTabletIds().isEmpty();
return Pair.of(scan.withSelectedPartitionIds(prunedPartitions,
hasPartitionPredicate),
prunedPartitionsByFilters.prunedPartitionPredicate);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java
index 21f6c4e5063..dd7b551585c 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java
@@ -17,7 +17,6 @@
package org.apache.doris.nereids.trees.plans.commands;
-import org.apache.doris.analysis.Queriable;
import org.apache.doris.analysis.StatementBase;
import org.apache.doris.analysis.StmtType;
import org.apache.doris.analysis.TableScanParams;
@@ -46,6 +45,7 @@ import org.apache.doris.qe.PreparedStatementContext;
import org.apache.doris.qe.ShortCircuitQueryContext;
import org.apache.doris.qe.StmtExecutor;
+import com.google.common.base.Preconditions;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -181,11 +181,14 @@ public class ExecuteCommand extends Command {
// early above, has just been refreshed here, or is stale and we are
about to re-plan.
preparedStmtCtx.shortCircuitQueryContext = Optional.empty();
executor.execute();
- if (executor.getContext().getStatementContext().isShortCircuitQuery())
{
- // cache short-circuit plan
- preparedStmtCtx.shortCircuitQueryContext = Optional.of(
- new ShortCircuitQueryContext(executor.planner(),
(Queriable) executor.getParsedStmt()));
-
statementContext.setShortCircuitQueryContext(preparedStmtCtx.shortCircuitQueryContext.get());
+ StatementContext executedStatementContext =
executor.getContext().getStatementContext();
+ ShortCircuitQueryContext shortCircuitQueryContext =
+ executedStatementContext.getShortCircuitQueryContext();
+ if (shortCircuitQueryContext != null) {
+
Preconditions.checkState(executedStatementContext.isShortCircuitQuery());
+ // Publish the exact context used by this execution so its
topology generation stays
+ // bound to the cached partition pruner in the same planner scan
node.
+ preparedStmtCtx.shortCircuitQueryContext =
Optional.of(shortCircuitQueryContext);
}
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/qe/ShortCircuitQueryContext.java
b/fe/fe-core/src/main/java/org/apache/doris/qe/ShortCircuitQueryContext.java
index 2a4235e332d..99496be25c9 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/ShortCircuitQueryContext.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/ShortCircuitQueryContext.java
@@ -60,6 +60,7 @@ public class ShortCircuitQueryContext {
public final OlapTable tbl;
public final String tableName;
private final long fileCacheQueryLimitBytes;
+ private final long partitionTopologyVersion;
public final OlapScanNode scanNode;
public final Queriable analzyedQuery;
@@ -115,6 +116,7 @@ public class ShortCircuitQueryContext {
this.tbl = this.scanNode.getOlapTable();
this.tableName = this.scanNode.getTableNameInPlan();
this.schemaVersion = this.tbl.getBaseSchemaVersion();
+ this.partitionTopologyVersion = this.tbl.getPartitionTopologyVersion();
this.analzyedQuery = analzyedQuery;
}
@@ -130,6 +132,7 @@ public class ShortCircuitQueryContext {
this.tableName = tableName;
this.schemaVersion = schemaVersion;
this.fileCacheQueryLimitBytes = fileCacheQueryLimitBytes;
+ this.partitionTopologyVersion = tbl.getPartitionTopologyVersion();
this.scanNode = null;
this.analzyedQuery = null;
}
@@ -138,7 +141,8 @@ public class ShortCircuitQueryContext {
return !this.tbl.isDropped
&& this.tbl.getBaseSchemaVersion() == this.schemaVersion
&& Objects.equals(this.tableName, this.tbl.getName())
- && this.fileCacheQueryLimitBytes ==
ctx.getSessionVariable().fileCacheQueryLimitBytes;
+ && this.fileCacheQueryLimitBytes ==
ctx.getSessionVariable().fileCacheQueryLimitBytes
+ && this.tbl.getPartitionTopologyVersion() ==
this.partitionTopologyVersion;
}
public void sanitize() {
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
b/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
index 76456330951..797140bd408 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
@@ -1491,10 +1491,12 @@ public class StmtExecutor {
RowBatch batch;
CoordInterface coordBase = null;
if (statementContext.isShortCircuitQuery()) {
- ShortCircuitQueryContext shortCircuitQueryContext =
- statementContext.getShortCircuitQueryContext() != null
- ? statementContext.getShortCircuitQueryContext()
- : new ShortCircuitQueryContext(planner,
(Queriable) parsedStmt);
+ ShortCircuitQueryContext shortCircuitQueryContext =
statementContext.getShortCircuitQueryContext();
+ if (shortCircuitQueryContext == null) {
+ shortCircuitQueryContext = new
ShortCircuitQueryContext(planner, (Queriable) parsedStmt);
+ // ExecuteCommand publishes this same context after a
successful first prepared execution.
+
statementContext.setShortCircuitQueryContext(shortCircuitQueryContext);
+ }
coordBase = new PointQueryExecutor(shortCircuitQueryContext,
context.getSessionVariable().getMaxMsgSizeOfResultReceiver());
context.getState().setIsQuery(true);
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/catalog/OlapTableTest.java
b/fe/fe-core/src/test/java/org/apache/doris/catalog/OlapTableTest.java
index 4503cb58067..81090d8d7dd 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/catalog/OlapTableTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/catalog/OlapTableTest.java
@@ -76,6 +76,35 @@ public class OlapTableTest {
Assert.assertEquals(132L, stats.getIndexLength());
}
+ @Test
+ public void testPartitionTopologyVersionChangesWithPartitionIdSet() {
+ OlapTable olapTable = new OlapTable();
+ olapTable.setPartitionInfo(new SinglePartitionInfo());
+
+ long version = olapTable.getPartitionTopologyVersion();
+ addPartitionForTopologyVersionTest(olapTable, 1L, "p1");
+ Assert.assertEquals(version + 1,
olapTable.getPartitionTopologyVersion());
+
+ version = olapTable.getPartitionTopologyVersion();
+ olapTable.replacePartition(newPartitionForTopologyVersionTest(2L,
"p1"), new RecyclePartitionParam());
+ Assert.assertEquals(version + 1,
olapTable.getPartitionTopologyVersion());
+
+ version = olapTable.getPartitionTopologyVersion();
+ olapTable.dropPartitionAndReserveTablet("p1");
+ Assert.assertEquals(version + 1,
olapTable.getPartitionTopologyVersion());
+ }
+
+ private void addPartitionForTopologyVersionTest(OlapTable olapTable, long
partitionId, String partitionName) {
+ olapTable.getPartitionInfo().addPartition(partitionId, new
DataProperty(TStorageMedium.HDD),
+ new ReplicaAllocation((short) 1), false, true);
+ olapTable.addPartition(newPartitionForTopologyVersionTest(partitionId,
partitionName));
+ }
+
+ private Partition newPartitionForTopologyVersionTest(long partitionId,
String partitionName) {
+ MaterializedIndex index = new MaterializedIndex(partitionId,
MaterializedIndex.IndexState.NORMAL);
+ return new Partition(partitionId, partitionName, index, new
RandomDistributionInfo(1));
+ }
+
private Replica mockReplica(Replica.ReplicaState state, long dataSize,
long localSegmentSize,
long remoteSegmentSize, long localIndexSize, long remoteIndexSize)
{
Replica replica = Mockito.mock(Replica.class);
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ShortCircuitPointQueryTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ShortCircuitPointQueryTest.java
index e036b3a7c49..1b25c4728d7 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ShortCircuitPointQueryTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ShortCircuitPointQueryTest.java
@@ -17,7 +17,13 @@
package org.apache.doris.nereids.rules.rewrite;
+import org.apache.doris.catalog.Database;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.catalog.Tablet;
import org.apache.doris.common.FeConstants;
+import org.apache.doris.datasource.doris.RemoteOlapTable;
+import org.apache.doris.nereids.trees.expressions.StatementScopeIdGenerator;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.logical.LogicalEmptyRelation;
import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
@@ -31,8 +37,9 @@ import org.junit.jupiter.api.Test;
/**
* Regression test:
* For short-circuit point query, we should not rewrite LogicalOlapScan to
LogicalEmptyRelation
- * even if the table partitions are empty. Otherwise PreparedStatement could
not cache
- * ShortCircuitQueryContext and may downgrade to normal planning repeatedly.
+ * even if the table partitions are empty or partition pruning selects no
partitions.
+ * Current execution still needs the scan to initialize the point-query path;
PreparedStatement
+ * cacheability for empty selected partitions is handled separately.
*/
class ShortCircuitPointQueryTest extends TestWithFeService
implements MemoPatternMatchSupported {
@@ -53,24 +60,115 @@ class ShortCircuitPointQueryTest extends TestWithFeService
+ " \"light_schema_change\" = \"true\",\n"
+ " \"store_row_column\" = \"true\"\n"
+ ");");
+ createTable("CREATE TABLE `tbl_partitioned_point_query` (\n"
+ + " `order_id` bigint NOT NULL,\n"
+ + " `pay_date` date NOT NULL,\n"
+ + " `v1` varchar(30) NULL\n"
+ + ") ENGINE=OLAP\n"
+ + "UNIQUE KEY(`order_id`, `pay_date`)\n"
+ + "PARTITION BY RANGE(`pay_date`) (\n"
+ + " PARTITION `p20260805` VALUES [(\"2026-08-05\"),
(\"2026-08-06\"))\n"
+ + ")\n"
+ + "DISTRIBUTED BY HASH(`order_id`) BUCKETS 1\n"
+ + "PROPERTIES (\n"
+ + " \"replication_num\" = \"1\",\n"
+ + " \"enable_unique_key_merge_on_write\" = \"true\",\n"
+ + " \"light_schema_change\" = \"true\",\n"
+ + " \"store_row_column\" = \"true\"\n"
+ + ");");
}
@Test
void testShortCircuitPointQueryKeepOlapScanWhenTableEmpty() {
+ Plan plan = rewrite("select * from tbl_point_query where `key` = 1");
+
+
Assertions.assertTrue(connectContext.getStatementContext().isShortCircuitQuery());
+ Assertions.assertTrue(plan.anyMatch(p -> p instanceof
LogicalOlapScan));
+ Assertions.assertFalse(plan.anyMatch(p -> p instanceof
LogicalEmptyRelation));
+ }
+
+ @Test
+ void testShortCircuitPointQueryKeepOlapScanWhenNoPartitionMatches() {
+ Plan plan = rewrite("select * from tbl_partitioned_point_query "
+ + "where order_id = 1 and pay_date = '2026-08-04'");
+
+
Assertions.assertTrue(connectContext.getStatementContext().isShortCircuitQuery());
+ Assertions.assertTrue(plan.anyMatch(p -> p instanceof LogicalOlapScan
+ && ((LogicalOlapScan) p).isPartitionPruned()
+ && ((LogicalOlapScan) p).getSelectedPartitionIds().isEmpty()
+ && ((LogicalOlapScan) p).hasPartitionPredicate()));
+ Assertions.assertFalse(plan.anyMatch(p -> p instanceof
LogicalEmptyRelation));
+ }
+
+ @Test
+ void testNonPointQueryWithNoMatchingPartitionPrunesToEmptyRelation() {
+ Plan plan = rewrite("select * from tbl_partitioned_point_query "
+ + "where pay_date = '2026-08-04'");
+
+
Assertions.assertFalse(connectContext.getStatementContext().isShortCircuitQuery());
+ Assertions.assertTrue(plan.anyMatch(p -> p instanceof
LogicalEmptyRelation));
+ Assertions.assertFalse(plan.anyMatch(p -> p instanceof
LogicalOlapScan));
+ }
+
+ @Test
+ void
testShortCircuitPointQueryWithMatchingPartitionKeepsSelectedPartition() {
+ Plan plan = rewrite("select * from tbl_partitioned_point_query "
+ + "where order_id = 1 and pay_date = '2026-08-05'");
+
+
Assertions.assertTrue(connectContext.getStatementContext().isShortCircuitQuery());
+ Assertions.assertTrue(plan.anyMatch(p -> p instanceof LogicalOlapScan
+ && ((LogicalOlapScan) p).isPartitionPruned()
+ && !((LogicalOlapScan)
p).getSelectedPartitionIds().isEmpty()));
+ Assertions.assertFalse(plan.anyMatch(p -> p instanceof
LogicalEmptyRelation));
+ }
+
+ @Test
+ void testPointQueryWithManualPartitionDoesNotUseShortCircuit() {
+ rewrite("select * from tbl_partitioned_point_query
partition(p20260805) "
+ + "where order_id = 1 and pay_date = '2026-08-05'");
+
+
Assertions.assertFalse(connectContext.getStatementContext().isShortCircuitQuery());
+ }
+
+ @Test
+ void testPointQueryWithManualTabletDoesNotUseShortCircuit() throws
Exception {
+ long tabletId = getTabletId("p20260805");
+ rewrite("select * from tbl_partitioned_point_query tablet(" + tabletId
+ ") "
+ + "where order_id = 1 and pay_date = '2026-08-05'");
+
+
Assertions.assertFalse(connectContext.getStatementContext().isShortCircuitQuery());
+ }
+
+ @Test
+ void testRemoteOlapTableDoesNotUseShortCircuit() throws Exception {
+ Database database =
Env.getCurrentInternalCatalog().getDbOrMetaException("test");
+ OlapTable table = (OlapTable)
database.getTableOrMetaException("tbl_point_query");
+ RemoteOlapTable remoteTable = RemoteOlapTable.fromOlapTable(table);
+ LogicalOlapScan scan = new
LogicalOlapScan(StatementScopeIdGenerator.newRelationId(), remoteTable);
+
+
Assertions.assertTrue(connectContext.getSessionVariable().isEnableShortCircuitQuery());
+ Assertions.assertTrue(remoteTable.getEnableLightSchemaChange());
+ Assertions.assertTrue(remoteTable.getEnableUniqueKeyMergeOnWrite());
+ Assertions.assertTrue(remoteTable.storeRowColumn());
+ Assertions.assertFalse(new LogicalResultSinkToShortCircuitPointQuery()
+ .scanMatchShortCircuitCondition(scan));
+ }
+
+ private long getTabletId(String partitionName) throws Exception {
+ Database database =
Env.getCurrentInternalCatalog().getDbOrMetaException("test");
+ OlapTable table = (OlapTable)
database.getTableOrMetaException("tbl_partitioned_point_query");
+ Tablet tablet =
table.getPartition(partitionName).getBaseIndex().getTablets().iterator().next();
+ return tablet.getId();
+ }
+
+ private Plan rewrite(String sql) {
boolean originRunningUnitTest = FeConstants.runningUnitTest;
FeConstants.runningUnitTest = false;
try {
- String sql = "select * from tbl_point_query where `key` = 1";
- Plan plan = PlanChecker.from(connectContext)
+ return PlanChecker.from(connectContext)
.analyze(sql)
.rewrite()
.getPlan();
-
- // short-circuit flag should be set
-
Assertions.assertTrue(connectContext.getStatementContext().isShortCircuitQuery());
- // should still keep scan node for point query path
- Assertions.assertTrue(plan.anyMatch(p -> p instanceof
LogicalOlapScan));
- Assertions.assertFalse(plan.anyMatch(p -> p instanceof
LogicalEmptyRelation));
} finally {
FeConstants.runningUnitTest = originRunningUnitTest;
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/qe/ShortCircuitQueryContextTest.java
b/fe/fe-core/src/test/java/org/apache/doris/qe/ShortCircuitQueryContextTest.java
index e24a57ee465..b43f0157073 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/qe/ShortCircuitQueryContextTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/qe/ShortCircuitQueryContextTest.java
@@ -19,10 +19,18 @@ package org.apache.doris.qe;
import org.apache.doris.analysis.DescriptorTable;
import org.apache.doris.analysis.Queriable;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.KeysType;
+import org.apache.doris.catalog.MaterializedIndex;
import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.catalog.Partition;
+import org.apache.doris.catalog.PrimitiveType;
+import org.apache.doris.catalog.RandomDistributionInfo;
+import org.apache.doris.catalog.SinglePartitionInfo;
import org.apache.doris.planner.OlapScanNode;
import org.apache.doris.planner.Planner;
import org.apache.doris.thrift.TQueryOptions;
+import org.apache.doris.thrift.TStorageType;
import org.apache.thrift.TDeserializer;
import org.junit.jupiter.api.Assertions;
@@ -30,6 +38,7 @@ import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.Collections;
+import java.util.List;
public class ShortCircuitQueryContextTest {
private OlapTable table(String name, int schemaVersion) {
@@ -64,6 +73,26 @@ public class ShortCircuitQueryContextTest {
Assertions.assertFalse(context.isReusable(connectContext(0)));
}
+ @Test
+ public void testReusableRequiresSamePartitionTopologyVersion() {
+ long baseIndexId = 2L;
+ Column key = new Column("k", PrimitiveType.INT);
+ key.setIsKey(true);
+ List<Column> baseSchema = Collections.singletonList(key);
+ OlapTable table = new OlapTable(1L, "tbl", baseSchema,
KeysType.DUP_KEYS,
+ new SinglePartitionInfo(), new RandomDistributionInfo(1));
+ table.setIndexMeta(baseIndexId, "tbl", baseSchema, 10, 0, (short) 1,
+ TStorageType.COLUMN, KeysType.DUP_KEYS);
+ table.setBaseIndexId(baseIndexId);
+ ShortCircuitQueryContext context = new ShortCircuitQueryContext(table,
"tbl", 10, -1);
+
+ Assertions.assertTrue(context.isReusable(connectContext(-1)));
+ table.addPartition(new Partition(3L, "p1",
+ new MaterializedIndex(baseIndexId,
MaterializedIndex.IndexState.NORMAL),
+ new RandomDistributionInfo(1)));
+ Assertions.assertFalse(context.isReusable(connectContext(-1)));
+ }
+
@Test
public void testSerializedQueryOptionsKeepBitmapOpCountVersion() throws
Exception {
TQueryOptions queryOptions = new SessionVariable().toThrift();
diff --git
a/regression-test/data/point_query_p0/test_point_query_partition_not_exists.out
b/regression-test/data/point_query_p0/test_point_query_partition_not_exists.out
new file mode 100644
index 00000000000..dd5d8eb103c
--- /dev/null
+++
b/regression-test/data/point_query_p0/test_point_query_partition_not_exists.out
@@ -0,0 +1,21 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !direct_no_partition --
+
+-- !prepared_no_partition_first --
+
+-- !prepared_no_partition_repeat --
+
+-- !prepared_added_partition --
+1 2026-08-04 added
+
+-- !prepared_existing_partition --
+2 2026-08-05 existing
+
+-- !direct_manual_partition_conflict --
+
+-- !direct_manual_tablet_conflict --
+
+-- !prepared_manual_partition_conflict --
+
+-- !prepared_manual_tablet_conflict --
+
diff --git
a/regression-test/suites/point_query_p0/test_point_query_partition_not_exists.groovy
b/regression-test/suites/point_query_p0/test_point_query_partition_not_exists.groovy
new file mode 100644
index 00000000000..b373ec4d94b
--- /dev/null
+++
b/regression-test/suites/point_query_p0/test_point_query_partition_not_exists.groovy
@@ -0,0 +1,130 @@
+// 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.
+
+import com.mysql.cj.jdbc.ServerPreparedStatement
+
+import java.sql.Date
+
+suite("test_point_query_partition_not_exists") {
+ sql "CREATE DATABASE IF NOT EXISTS regression_test_point_query_p0"
+ sql "USE regression_test_point_query_p0"
+ sql "DROP TABLE IF EXISTS test_point_query_partition_not_exists"
+ sql """
+ CREATE TABLE test_point_query_partition_not_exists (
+ order_id BIGINT NOT NULL,
+ pay_date DATE NOT NULL,
+ value VARCHAR(30) NULL
+ ) ENGINE=OLAP
+ UNIQUE KEY(order_id, pay_date)
+ PARTITION BY RANGE(pay_date) (
+ PARTITION p20260805 VALUES [("2026-08-05"), ("2026-08-06")),
+ PARTITION p20260806 VALUES [("2026-08-06"), ("2026-08-07"))
+ )
+ DISTRIBUTED BY HASH(order_id) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true",
+ "light_schema_change" = "true",
+ "store_row_column" = "true"
+ )
+ """
+ sql """
+ INSERT INTO test_point_query_partition_not_exists VALUES
+ (2, '2026-08-05', 'existing'),
+ (3, '2026-08-06', 'excluded')
+ """
+ sql "SET enable_nereids_planner = true"
+ sql "SET enable_fallback_to_original_planner = false"
+
+ explain {
+ sql """
+ SELECT * FROM test_point_query_partition_not_exists
+ WHERE order_id = 1 AND pay_date = '2026-08-04'
+ """
+ contains "SHORT-CIRCUIT"
+ }
+
+ order_qt_direct_no_partition """
+ SELECT * FROM test_point_query_partition_not_exists
+ WHERE order_id = 1 AND pay_date = '2026-08-04'
+ """
+
+ String url = getServerPrepareJdbcUrl(
+ context.config.jdbcUrl, "regression_test_point_query_p0", false)
+ connect(context.config.jdbcUser, context.config.jdbcPassword, url) {
+ def stmt = prepareStatement """
+ SELECT * FROM test_point_query_partition_not_exists
+ WHERE order_id = ? AND pay_date = ?
+ """
+ assertEquals(ServerPreparedStatement, stmt.class)
+
+ stmt.setLong(1, 1)
+ stmt.setDate(2, Date.valueOf("2026-08-04"))
+ qe_prepared_no_partition_first stmt
+ qe_prepared_no_partition_repeat stmt
+
+ sql """
+ ALTER TABLE test_point_query_partition_not_exists
+ ADD PARTITION p20260804 VALUES [("2026-08-04"), ("2026-08-05"))
+ """
+ sql "INSERT INTO test_point_query_partition_not_exists VALUES (1,
'2026-08-04', 'added')"
+ stmt.setLong(1, 1)
+ stmt.setDate(2, Date.valueOf("2026-08-04"))
+ qe_prepared_added_partition stmt
+
+ stmt.setLong(1, 2)
+ stmt.setDate(2, Date.valueOf("2026-08-05"))
+ qe_prepared_existing_partition stmt
+ stmt.close()
+ }
+
+ def tablets = sql_return_maparray """
+ SHOW TABLETS FROM test_point_query_partition_not_exists
PARTITION(p20260805)
+ """
+ def p20260805TabletId = tablets[0].TabletId
+
+ order_qt_direct_manual_partition_conflict """
+ SELECT * FROM test_point_query_partition_not_exists
PARTITION(p20260805)
+ WHERE order_id = 3 AND pay_date = '2026-08-06'
+ """
+ order_qt_direct_manual_tablet_conflict """
+ SELECT * FROM test_point_query_partition_not_exists
TABLET(${p20260805TabletId})
+ WHERE order_id = 3 AND pay_date = '2026-08-06'
+ """
+
+ connect(context.config.jdbcUser, context.config.jdbcPassword, url) {
+ def partitionStmt = prepareStatement """
+ SELECT * FROM test_point_query_partition_not_exists
PARTITION(p20260805)
+ WHERE order_id = ? AND pay_date = ?
+ """
+ assertEquals(ServerPreparedStatement, partitionStmt.class)
+ partitionStmt.setLong(1, 3)
+ partitionStmt.setDate(2, Date.valueOf("2026-08-06"))
+ qe_prepared_manual_partition_conflict partitionStmt
+ partitionStmt.close()
+
+ def tabletStmt = prepareStatement """
+ SELECT * FROM test_point_query_partition_not_exists
TABLET(${p20260805TabletId})
+ WHERE order_id = ? AND pay_date = ?
+ """
+ assertEquals(ServerPreparedStatement, tabletStmt.class)
+ tabletStmt.setLong(1, 3)
+ tabletStmt.setDate(2, Date.valueOf("2026-08-06"))
+ qe_prepared_manual_tablet_conflict tabletStmt
+ tabletStmt.close()
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]