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

amashenkov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 36bd44ce78f IGNITE-28296 SQL. Prefer table scan for unbounded scan 
with no sort required. (#7818)
36bd44ce78f is described below

commit 36bd44ce78f2a82534ed8957af4b8c81e31a74b7
Author: Andrew V. Mashenkov <[email protected]>
AuthorDate: Tue Mar 24 14:40:33 2026 +0300

    IGNITE-28296 SQL. Prefer table scan for unbounded scan with no sort 
required. (#7818)
---
 .../internal/sql/engine/rel/AbstractIndexScan.java |  5 +--
 .../engine/rel/ProjectableFilterableTableScan.java |  2 +-
 .../internal/sql/engine/planner/PlannerTest.java   | 42 ++++++++++++++++++++--
 3 files changed, 44 insertions(+), 5 deletions(-)

diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/AbstractIndexScan.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/AbstractIndexScan.java
index efb0d83b327..34b626a86b5 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/AbstractIndexScan.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/AbstractIndexScan.java
@@ -157,6 +157,7 @@ public abstract class AbstractIndexScan extends 
ProjectableFilterableTableScan {
         double indexRowPassThroughCost = IgniteCost.ROW_PASS_THROUGH_COST * 
IgniteCost.INDEX_ROW_SCAN_MULTIPLIER;
 
         if (condition == null) {
+            rows = Math.max(1.0d, rows);
             cost = rows * indexRowPassThroughCost;
         } else {
             double selectivity = 1;
@@ -170,9 +171,9 @@ public abstract class AbstractIndexScan extends 
ProjectableFilterableTableScan {
                 cost = Math.max(Math.log(rows), 1) * 
IgniteCost.ROW_COMPARISON_COST;
             }
 
-            rows *= selectivity;
+            rows = Math.max(rows * selectivity, 1);
 
-            cost += Math.max(rows, 1) * (IgniteCost.ROW_COMPARISON_COST + 
indexRowPassThroughCost);
+            cost += rows * (IgniteCost.ROW_COMPARISON_COST + 
indexRowPassThroughCost);
         }
 
         return planner.getCostFactory().makeCost(rows, cost, 0);
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/ProjectableFilterableTableScan.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/ProjectableFilterableTableScan.java
index 3a3f61b02e4..15696ab99da 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/ProjectableFilterableTableScan.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/rel/ProjectableFilterableTableScan.java
@@ -173,7 +173,7 @@ public abstract class ProjectableFilterableTableScan 
extends TableScan {
             cost += rows * IgniteCost.ROW_COMPARISON_COST;
         }
 
-        return planner.getCostFactory().makeCost(rows, cost, 0);
+        return planner.getCostFactory().makeCost(rows, Math.max(cost, 1.0d), 
0);
     }
 
     /** {@inheritDoc} */
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/PlannerTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/PlannerTest.java
index 2c730abb358..3d02ea5df9d 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/PlannerTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/PlannerTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.sql.engine.planner;
 
+import static java.util.function.Predicate.not;
 import static org.apache.calcite.tools.Frameworks.newConfigBuilder;
 import static 
org.apache.ignite.internal.sql.engine.planner.CorrelatedSubqueryPlannerTest.createTestTable;
 import static 
org.apache.ignite.internal.sql.engine.util.Commons.FRAMEWORK_CONFIG;
@@ -55,10 +56,12 @@ import 
org.apache.ignite.internal.sql.engine.prepare.PlanningContext;
 import org.apache.ignite.internal.sql.engine.rel.IgniteConvention;
 import org.apache.ignite.internal.sql.engine.rel.IgniteFilter;
 import org.apache.ignite.internal.sql.engine.rel.IgniteHashJoin;
+import org.apache.ignite.internal.sql.engine.rel.IgniteIndexScan;
 import org.apache.ignite.internal.sql.engine.rel.IgniteMergeJoin;
 import org.apache.ignite.internal.sql.engine.rel.IgniteRel;
 import org.apache.ignite.internal.sql.engine.rel.IgniteSort;
 import org.apache.ignite.internal.sql.engine.rel.IgniteTableScan;
+import org.apache.ignite.internal.sql.engine.schema.IgniteIndex.Collation;
 import org.apache.ignite.internal.sql.engine.schema.IgniteSchema;
 import org.apache.ignite.internal.sql.engine.schema.IgniteTable;
 import org.apache.ignite.internal.sql.engine.trait.IgniteDistribution;
@@ -114,7 +117,7 @@ public class PlannerTest extends AbstractPlannerTest {
                 + "   WHERE VAL = 10::VARCHAR) \n"
                 + "WHERE VAL = 10::VARCHAR";
 
-        assertPlan(sql, publicSchema, 
Predicate.not(nodeOrAnyChild(isInstanceOf(IgniteFilter.class)))
+        assertPlan(sql, publicSchema, 
not(nodeOrAnyChild(isInstanceOf(IgniteFilter.class)))
                 .and(nodeOrAnyChild(isInstanceOf(IgniteTableScan.class)
                         .and(scan -> scan.condition() != null)
                 )));
@@ -216,7 +219,7 @@ public class PlannerTest extends AbstractPlannerTest {
                                 .and(hasChildThat(isTableScan("EMP")))
                                 .and(hasChildThat(isTableScan("DEPT")))
                         ))
-                
).and(Predicate.not(nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class)))),
+                
).and(not(nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class)))),
                 "CorrelatedNestedLoopJoin");
     }
 
@@ -427,4 +430,39 @@ public class PlannerTest extends AbstractPlannerTest {
                 .size(100)
                 .distribution(distribution);
     }
+
+    /** Ensure we we prefer table scan in cases where index scan is useless. */
+    @Test
+    public void preferTableScanForEmptyTable() throws Exception {
+        IgniteSchema publicSchema = createSchema(
+                TestBuilders.table()
+                        .name("T1")
+                        .distribution(TestBuilders.affinity(0, nextTableId(), 
Integer.MIN_VALUE))
+                        .distribution(IgniteDistributions.single())
+                        .addKeyColumn("PK", NativeTypes.INT32)
+                        .addColumn("VAL1", NativeTypes.INT32)
+                        .addColumn("VAL2", NativeTypes.INT32)
+                        .size(0)
+                        .sortedIndex().name("IDX1").addColumn("VAL2", 
Collation.ASC_NULLS_LAST).end()
+                        .build(),
+                TestBuilders.table()
+                        .name("T2")
+                        .distribution(TestBuilders.affinity(0, nextTableId(), 
Integer.MIN_VALUE))
+                        .addColumn("PK", NativeTypes.INT32)
+                        .addColumn("VAL1", NativeTypes.INT32)
+                        .addColumn("VAL2", NativeTypes.INT32)
+                        .size(0)
+                        .sortedIndex().name("IDX2").addColumn("VAL2", 
Collation.ASC_NULLS_LAST).end()
+                        .build()
+        );
+
+        // Join on non-indexed columns.
+        assertPlan("SELECT * FROM T1 LEFT JOIN T2 ON t1.val1 = t2.val1", 
publicSchema, isInstanceOf(IgniteHashJoin.class)
+                
.and(not(nodeOrAnyChild(isInstanceOf(IgniteIndexScan.class)))));
+        // Order by non-indexed column
+        assertPlan("SELECT * FROM T1 ORDER BY val1", publicSchema, 
isInstanceOf(IgniteSort.class)
+                .and(nodeOrAnyChild(isTableScan("T1"))));
+        // Condition on non-indexed column.
+        assertPlan("SELECT * FROM T1", publicSchema, 
nodeOrAnyChild(isTableScan("T1")));
+    }
 }

Reply via email to