Skip to content

Commit 99685e8

Browse files
IGNITE-27433 SQL Calcite: Improve unique index rows count estimation
1 parent 3534e59 commit 99685e8

File tree

3 files changed

+131
-4
lines changed

3 files changed

+131
-4
lines changed

modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/AbstractIndexScan.java

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import org.apache.calcite.plan.RelOptPlanner;
2525
import org.apache.calcite.plan.RelOptTable;
2626
import org.apache.calcite.plan.RelTraitSet;
27+
import org.apache.calcite.rel.RelCollation;
28+
import org.apache.calcite.rel.RelFieldCollation;
2729
import org.apache.calcite.rel.RelInput;
2830
import org.apache.calcite.rel.RelWriter;
2931
import org.apache.calcite.rel.metadata.RelMetadataQuery;
@@ -34,6 +36,7 @@
3436
import org.apache.calcite.util.ImmutableBitSet;
3537
import org.apache.ignite.internal.processors.query.calcite.externalize.RelInputEx;
3638
import org.apache.ignite.internal.processors.query.calcite.metadata.cost.IgniteCost;
39+
import org.apache.ignite.internal.processors.query.calcite.prepare.bounds.MultiBounds;
3740
import org.apache.ignite.internal.processors.query.calcite.prepare.bounds.SearchBounds;
3841
import org.apache.ignite.internal.processors.query.calcite.schema.IgniteIndex;
3942
import org.apache.ignite.internal.processors.query.calcite.schema.IgniteTable;
@@ -108,6 +111,14 @@ public boolean isInlineScan() {
108111
return false;
109112
}
110113

114+
/** {@inheritDoc} */
115+
@Override public double estimateRowCount(RelMetadataQuery mq) {
116+
IgniteTable tbl = table.unwrap(IgniteTable.class);
117+
IgniteIndex idx = tbl.getIndex(idxName);
118+
119+
return adjustRowCountByUniqueBoundsScan(super.estimateRowCount(mq), idx.collation(), searchBounds);
120+
}
121+
111122
/** {@inheritDoc} */
112123
@Override public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
113124
double rows = table.getRowCount();
@@ -131,12 +142,14 @@ public boolean isInlineScan() {
131142

132143
if (searchBounds != null) {
133144
selectivity = mq.getSelectivity(this, RexUtil.composeConjunction(builder,
134-
Commons.transform(searchBounds, b -> b == null ? null : b.condition())));
145+
Commons.transform(searchBounds, b -> b == null ? null : b.condition())));
135146

136147
cost = Math.log(rows) * IgniteCost.ROW_COMPARISON_COST;
137-
}
138148

139-
rows *= selectivity;
149+
rows *= selectivity;
150+
151+
rows = adjustRowCountByUniqueBoundsScan(rows, idx.collation(), searchBounds);
152+
}
140153

141154
if (rows <= 0)
142155
rows = 1;
@@ -148,6 +161,30 @@ public boolean isInlineScan() {
148161
return planner.getCostFactory().makeCost(rows, cost, 0).plus(planner.getCostFactory().makeTinyCost());
149162
}
150163

164+
/** Rows count can't exceed count of exact bounds on unique index. */
165+
private static double adjustRowCountByUniqueBoundsScan(double rows, RelCollation collation, List<SearchBounds> bounds) {
166+
if (bounds == null)
167+
return rows;
168+
169+
long exactBounds = 1L;
170+
171+
for (RelFieldCollation fldCol : collation.getFieldCollations()) {
172+
SearchBounds bound = bounds.get(fldCol.getFieldIndex());
173+
174+
if (bound == null || bound.type() == SearchBounds.Type.RANGE)
175+
return rows;
176+
177+
if (bound.type() == SearchBounds.Type.MULTI) {
178+
if (((MultiBounds)bound).bounds().stream().anyMatch(b -> b.type() != SearchBounds.Type.EXACT))
179+
return rows;
180+
else
181+
exactBounds *= ((MultiBounds)bound).bounds().size();
182+
}
183+
}
184+
185+
return Math.min(rows, exactBounds);
186+
}
187+
151188
/** */
152189
public List<SearchBounds> searchBounds() {
153190
return searchBounds;

modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/CorrelatedNestedLoopJoinPlannerTest.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
import java.util.List;
2121
import java.util.function.Predicate;
22-
2322
import org.apache.calcite.plan.RelOptUtil;
2423
import org.apache.calcite.rel.RelNode;
2524
import org.apache.calcite.rel.core.JoinRelType;
@@ -155,6 +154,31 @@ public void testJoinPushExpressionRule() throws Exception {
155154
);
156155
}
157156

157+
/** Check that CNLJ is used for unique scans (when left hand return no more than 1 row). */
158+
@Test
159+
public void testJoinForUniqueScans() throws Exception {
160+
IgniteSchema publicSchema = createSchema(
161+
createTable("EMP", 2 * DEFAULT_TBL_SIZE,
162+
IgniteDistributions.affinity(1, "default", "hash"),
163+
"EMPNO", INTEGER, "DEPTNO", INTEGER, "NAME", VARCHAR)
164+
.addIndex("PK", 0)
165+
.addIndex("AFF", 1),
166+
createTable("DEPT", DEFAULT_TBL_SIZE,
167+
IgniteDistributions.affinity(0, "default", "hash"),
168+
"DEPTNO", INTEGER, "NAME", VARCHAR)
169+
.addIndex("PK", 0)
170+
);
171+
172+
// TODO https://issues.apache.org/jira/browse/IGNITE-16334
173+
// For query SELECT * FROM ... plan still not optimal and contains other join type instead of CNLJ.
174+
String sql = "SELECT count(*) FROM emp e JOIN dept d ON e.deptno = d.deptno WHERE e.empno = ?";
175+
176+
assertPlan(sql, publicSchema, hasChildThat(isInstanceOf(IgniteCorrelatedNestedLoopJoin.class)
177+
.and(input(0, isIndexScan("EMP", "PK")))
178+
.and(input(1, isIndexScan("DEPT", "PK")))
179+
));
180+
}
181+
158182
/** */
159183
private TestTable testTable(String name) {
160184
return createTable(name, IgniteDistributions.broadcast(), "ID", Integer.class, "JID", Integer.class, "VAL", String.class);

modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/IndexSearchBoundsPlannerTest.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import java.util.function.Predicate;
2323
import java.util.stream.IntStream;
2424
import org.apache.calcite.rel.RelCollations;
25+
import org.apache.calcite.rel.RelNode;
26+
import org.apache.calcite.rel.metadata.RelMetadataQuery;
2527
import org.apache.calcite.rel.type.RelDataType;
2628
import org.apache.calcite.rel.type.RelDataTypeFactory;
2729
import org.apache.calcite.rex.RexLiteral;
@@ -461,6 +463,53 @@ public void testBoundsComplex() throws Exception {
461463
);
462464
}
463465

466+
/** Tests row count estimation by bounds scan. */
467+
@Test
468+
public void testEstimateRowCount() throws Exception {
469+
int t1Size = 1_000;
470+
int t2Size = 5;
471+
472+
IgniteSchema schema = createSchema(createTable("T1", t1Size, IgniteDistributions.broadcast(),
473+
"ID1", SqlTypeName.INTEGER, "ID2", SqlTypeName.INTEGER, "ID3", SqlTypeName.INTEGER)
474+
.addIndex("IDX1", 0)
475+
.addIndex("IDX2", 1, 2),
476+
createTable("T2", t2Size, IgniteDistributions.broadcast(),
477+
"ID1", SqlTypeName.INTEGER, "ID2", SqlTypeName.INTEGER, "ID3", SqlTypeName.INTEGER)
478+
.addIndex("IDX1", 0, 1)
479+
);
480+
481+
assertPlan("SELECT * FROM t1 WHERE id1 = ?", schema, isIndexScan("T1", "IDX1")
482+
.and(estimatedRowCountBetween(1, 1)));
483+
484+
assertPlan("SELECT * FROM t1 WHERE id1 in (?, ?, ?)", schema, isIndexScan("T1", "IDX1")
485+
.and(estimatedRowCountBetween(3, 3)));
486+
487+
// Can't estimate row count by bounds containing range.
488+
assertPlan("SELECT * FROM t1 WHERE id1 > ?", schema, isIndexScan("T1", "IDX1")
489+
.and(estimatedRowCountBetween(2, t1Size)));
490+
491+
// Index scan on IDX2 for column ID2 is not unique, can't estimate row count by bounds.
492+
assertPlan("SELECT * FROM t1 WHERE id2 = ?", schema, isIndexScan("T1", "IDX2")
493+
.and(estimatedRowCountBetween(2, t1Size)));
494+
495+
assertPlan("SELECT * FROM t1 WHERE id2 = ? AND id3 = ?", schema, isIndexScan("T1", "IDX2")
496+
.and(estimatedRowCountBetween(1, 1)));
497+
498+
assertPlan("SELECT * FROM t1 WHERE id2 = ? AND id3 in (?, ?, ?)", schema, isIndexScan("T1", "IDX2")
499+
.and(estimatedRowCountBetween(3, 3)));
500+
501+
assertPlan("SELECT * FROM t1 WHERE id2 in (?, ?, ?) AND id3 in (?, ?, ?)", schema, isIndexScan("T1", "IDX2")
502+
.and(estimatedRowCountBetween(9, 9)));
503+
504+
// Can't estimate row count by bounds containing range.
505+
assertPlan("SELECT * FROM t1 WHERE id2 in (?, ?, ?) AND id3 between ? and ?", schema, isIndexScan("T1", "IDX2")
506+
.and(estimatedRowCountBetween(10, t1Size)));
507+
508+
// Row count for small table is limited by estimation by condition.
509+
assertPlan("SELECT * FROM t2 WHERE id1 in (?, ?, ?) and id2 in (?, ?, ?)", schema, isIndexScan("T2", "IDX1")
510+
.and(estimatedRowCountBetween(0, t2Size)));
511+
}
512+
464513
/** */
465514
private void assertBounds(String sql, Predicate<SearchBounds>... predicates) throws Exception {
466515
assertPlan(sql, publicSchema, nodeOrAnyChild(isInstanceOf(IgniteIndexScan.class)
@@ -516,4 +565,21 @@ private static boolean matchValue(Object val, RexNode bound) {
516565
return Objects.toString(val).equals(Objects.toString(
517566
bound instanceof RexLiteral ? ((RexLiteral)bound).getValueAs(val.getClass()) : bound));
518567
}
568+
569+
/** */
570+
protected <T extends RelNode> Predicate<T> estimatedRowCountBetween(double min, double max) {
571+
return node -> {
572+
RelMetadataQuery mq = node.getCluster().getMetadataQuery();
573+
574+
double rowCnt = node.estimateRowCount(mq);
575+
576+
if (rowCnt >= min && rowCnt <= max)
577+
return true;
578+
579+
lastErrorMsg = "Unexpected estimated row count [node=" + node + ", rowCnt=" + rowCnt + ']';
580+
581+
return false;
582+
};
583+
}
584+
519585
}

0 commit comments

Comments
 (0)