提交 990e81ce authored 作者: Noel Grandin's avatar Noel Grandin

choose better index when the index does not need to touch the table data

when we can satisfy a query from an index without touching table data,
then that index should have a lower cost than an index that DOES need
to retrieve table data
上级 0c6a8026
......@@ -5,11 +5,15 @@
*/
package org.h2.index;
import java.util.Arrays;
import java.util.HashSet;
import org.h2.api.ErrorCode;
import org.h2.engine.Constants;
import org.h2.engine.DbObject;
import org.h2.engine.Mode;
import org.h2.engine.Session;
import org.h2.expression.ExpressionVisitor;
import org.h2.message.DbException;
import org.h2.message.Trace;
import org.h2.result.Row;
......@@ -21,6 +25,7 @@ import org.h2.table.IndexColumn;
import org.h2.table.Table;
import org.h2.table.TableFilter;
import org.h2.util.MathUtils;
import org.h2.util.New;
import org.h2.util.StatementBuilder;
import org.h2.util.StringUtils;
import org.h2.value.Value;
......@@ -148,7 +153,7 @@ public abstract class BaseIndex extends SchemaObjectBase implements Index {
* b-tree range index. This is the estimated cost required to search one
* row, and then iterate over the given number of rows.
*
* @param masks the search mask
* @param masks the IndexCondition search masks, one for each Column in the table
* @param rowCount the number of rows in the index
* @param filters all joined table filters
* @param filter the current table filter index
......@@ -233,6 +238,26 @@ public abstract class BaseIndex extends SchemaObjectBase implements Index {
cost -= coveringCount;
}
}
// If we have two indexes with the same cost, and one of the indexes can satisfy the query
// without needing to read from the primary table, make that one slightly lower cost
HashSet<Column> set1 = New.hashSet();
for (int i=0; i < filters.length; i++) {
if (filters[i].getSelect() != null) {
filters[i].getSelect().isEverything(ExpressionVisitor.getColumnsVisitor(set1));
}
}
if (!set1.isEmpty()) {
HashSet<Column> set2 = New.hashSet();
for (Column c : set1) {
if (c.getTable() == getTable()) {
set2.add(c);
}
}
set2.removeAll(Arrays.asList(getColumns()));
if (set2.isEmpty()) {
cost -= 1;
}
}
return cost;
}
......
......@@ -219,8 +219,10 @@ public class MVPrimaryIndex extends BaseIndex {
public double getCost(Session session, int[] masks,
TableFilter[] filters, int filter, SortOrder sortOrder) {
try {
long cost = 10 * (dataMap.sizeAsLongMax() + Constants.COST_ROW_OFFSET);
return cost;
// we use 9 here and 10 in MVSecondaryIndex to biase the decision towards
// using a table-scan when it has similar cost to an index
return 9 * getCostRangeIndex(masks, dataMap.sizeAsLongMax(),
filters, filter, sortOrder);
} catch (IllegalStateException e) {
throw DbException.get(ErrorCode.OBJECT_CLOSED, e);
}
......
......@@ -6,7 +6,9 @@
package org.h2.table;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import org.h2.engine.Session;
import org.h2.expression.Expression;
import org.h2.expression.ExpressionVisitor;
......@@ -104,12 +106,17 @@ public class Plan {
* @return the cost
*/
public double calculateCost(Session session) {
if (session.getTrace().isDebugEnabled()) {
session.getTrace().debug("Plan : calculate cost for plan {0}", Arrays.toString(allFilters));
}
double cost = 1;
boolean invalidPlan = false;
for (int i = 0; i < allFilters.length; i++) {
TableFilter tableFilter = allFilters[i];
session.getTrace().debug("Plan : for table filter {0}", tableFilter);
PlanItem item = tableFilter.getBestPlanItem(session, allFilters, i);
planItems.put(tableFilter, item);
session.getTrace().debug("Plan : best plan item cost {0} index {1}", (int)item.cost, item.getIndex().getPlanSQL());
cost += cost * item.cost;
setEvaluatable(tableFilter, true);
Expression on = tableFilter.getJoinCondition();
......@@ -123,6 +130,7 @@ public class Plan {
if (invalidPlan) {
cost = Double.POSITIVE_INFINITY;
}
session.getTrace().debug("Plan : plan cost {0}", (int)cost);
for (TableFilter f : allFilters) {
setEvaluatable(f, false);
}
......
......@@ -697,11 +697,13 @@ public abstract class Table extends SchemaObjectBase {
PlanItem item = new PlanItem();
item.setIndex(getScanIndex(session));
item.cost = item.getIndex().getCost(session, null, filters, filter, null);
session.getTrace().debug("Table : potential plan item cost {0} index {1}", item.cost, item.getIndex().getPlanSQL());
ArrayList<Index> indexes = getIndexes();
if (indexes != null && masks != null) {
for (int i = 1, size = indexes.size(); i < size; i++) {
Index index = indexes.get(i);
double cost = index.getCost(session, masks, filters, filter, sortOrder);
session.getTrace().debug("Table : potential plan item cost {0} index {1}", cost, index.getPlanSQL());
if (cost < item.cost) {
item.cost = cost;
item.setIndex(index);
......
......@@ -62,6 +62,7 @@ public class TestOptimizations extends TestBase {
testNestedInSelect();
testInSelectJoin();
testMinMaxNullOptimization();
testUseCoveringIndex();
// testUseIndexWhenAllColumnsNotInOrderBy();
if (config.networked) {
return;
......@@ -1027,4 +1028,21 @@ public class TestOptimizations extends TestBase {
conn.close();
}
private void testUseCoveringIndex() throws SQLException {
deleteDb("optimizations");
Connection conn = getConnection("optimizations");
Statement stat = conn.createStatement();
stat.execute("CREATE TABLE TBL_A(id IDENTITY PRIMARY KEY NOT NULL, name VARCHAR NOT NULL, active BOOLEAN DEFAULT TRUE, UNIQUE KEY TBL_A_UK (name) )");
stat.execute("CREATE TABLE TBL_B(id IDENTITY PRIMARY KEY NOT NULL, tbl_a_id BIGINT NOT NULL, createDate TIMESTAMP DEFAULT NOW(), UNIQUE KEY TBL_B_UK (tbl_a_id, createDate), FOREIGN KEY (tbl_a_id) REFERENCES TBL_A(id) )");
stat.execute("INSERT INTO TBL_A (name) SELECT 'package_' || CAST(X as VARCHAR) FROM SYSTEM_RANGE(1, 100) WHERE X <= 100");
stat.execute("INSERT INTO TBL_B (tbl_a_id, createDate) SELECT CASE WHEN tbl_a_id = 0 THEN 1 ELSE tbl_a_id END, createDate FROM ( SELECT ROUND((RAND() * 100)) AS tbl_a_id, DATEADD('SECOND', X, NOW()) as createDate FROM SYSTEM_RANGE(1, 50000) WHERE X < 50000 )");
stat.execute("CREATE INDEX tbl_b_idx ON tbl_b(tbl_a_id, id)");
stat.execute("ANALYZE");
ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT MAX(b.id) as id FROM tbl_b b JOIN tbl_a a ON b.tbl_a_id = a.id GROUP BY b.tbl_a_id HAVING A.ACTIVE = TRUE");
rs.next();
assertContains(rs.getString(1), "/* PUBLIC.TBL_B_IDX: TBL_A_ID = A.ID */");
conn.close();
}
}
......@@ -228,7 +228,7 @@ public class TestTableEngines extends TestBase {
checkPlan(stat, "select * from t where a > 0 and b > ''", "IDX_B_A");
checkPlan(stat, "select * from t where b < ''", "IDX_B_A");
checkPlan(stat, "select * from t where b < '' and c < 1", "IDX_C_B_A");
checkPlan(stat, "select * from t where a = 0", "scan");
checkPlan(stat, "select * from t where a = 0", "IDX_C_B_A");
checkPlan(stat, "select * from t where a > 0 order by c, b", "IDX_C_B_A");
checkPlan(stat, "select * from t where a = 0 and c > 0", "IDX_C_B_A");
checkPlan(stat, "select * from t where a = 0 and b < 0", "IDX_B_A");
......@@ -535,11 +535,11 @@ public class TestTableEngines extends TestBase {
+ "/* SELECT A FROM PUBLIC.T /++ PUBLIC.\"scan\" ++/ */ "
+ "INNER JOIN PUBLIC.T /* batched:test PUBLIC.T_IDX_B: B = Z.A */ "
+ "ON 1=1 WHERE Z.A = T.B");
checkPlan(stat, "SELECT 1 FROM PUBLIC.T /* PUBLIC.\"scan\" */ "
checkPlan(stat, "SELECT 1 FROM PUBLIC.T /* PUBLIC.T_IDX_B */ "
+ "INNER JOIN ( SELECT A FROM PUBLIC.T ) Z "
+ "/* batched:view SELECT A FROM PUBLIC.T /++ batched:test PUBLIC.T_IDX_A: A IS ?1 ++/ "
+ "WHERE A IS ?1: A = T.B */ ON 1=1 WHERE Z.A = T.B");
checkPlan(stat, "SELECT 1 FROM PUBLIC.T /* PUBLIC.\"scan\" */ "
checkPlan(stat, "SELECT 1 FROM PUBLIC.T /* PUBLIC.T_IDX_A */ "
+ "INNER JOIN ( ((SELECT A FROM PUBLIC.T) UNION ALL (SELECT B FROM PUBLIC.U)) "
+ "UNION ALL (SELECT B FROM PUBLIC.T) ) Z /* batched:view "
+ "((SELECT A FROM PUBLIC.T /++ batched:test PUBLIC.T_IDX_A: A IS ?1 ++/ WHERE A IS ?1) "
......@@ -548,12 +548,12 @@ public class TestTableEngines extends TestBase {
+ "UNION ALL "
+ "(SELECT B FROM PUBLIC.T /++ batched:test PUBLIC.T_IDX_B: B IS ?1 ++/ WHERE B IS ?1)"
+ ": A = T.A */ ON 1=1 WHERE Z.A = T.A");
checkPlan(stat, "SELECT 1 FROM PUBLIC.T /* PUBLIC.\"scan\" */ "
checkPlan(stat, "SELECT 1 FROM PUBLIC.T /* PUBLIC.T_IDX_A */ "
+ "INNER JOIN ( SELECT U.A FROM PUBLIC.U INNER JOIN PUBLIC.T ON 1=1 WHERE U.B = T.B ) Z "
+ "/* batched:view SELECT U.A FROM PUBLIC.U /++ batched:fake PUBLIC.U_IDX_A: A IS ?1 ++/ "
+ "/++ WHERE U.A IS ?1 ++/ INNER JOIN PUBLIC.T /++ batched:test PUBLIC.T_IDX_B: B = U.B ++/ "
+ "ON 1=1 WHERE (U.A IS ?1) AND (U.B = T.B): A = T.A */ ON 1=1 WHERE Z.A = T.A");
checkPlan(stat, "SELECT 1 FROM PUBLIC.T /* PUBLIC.\"scan\" */ "
checkPlan(stat, "SELECT 1 FROM PUBLIC.T /* PUBLIC.T_IDX_A */ "
+ "INNER JOIN ( SELECT A FROM PUBLIC.U ) Z /* SELECT A FROM PUBLIC.U "
+ "/++ PUBLIC.U_IDX_A: A IS ?1 ++/ WHERE A IS ?1: A = T.A */ ON 1=1 WHERE T.A = Z.A");
checkPlan(stat, "SELECT 1 FROM "
......@@ -564,7 +564,7 @@ public class TestTableEngines extends TestBase {
+ "INNER JOIN PUBLIC.T /* batched:test PUBLIC.T_IDX_A: A = Z.A */ ON 1=1 WHERE T.A = Z.A");
checkPlan(stat, "SELECT 1 FROM "
+ "( SELECT U.A FROM PUBLIC.T INNER JOIN PUBLIC.U ON 1=1 WHERE T.B = U.B ) Z "
+ "/* SELECT U.A FROM PUBLIC.T /++ PUBLIC.\"scan\" ++/ "
+ "/* SELECT U.A FROM PUBLIC.T /++ PUBLIC.T_IDX_B ++/ "
+ "INNER JOIN PUBLIC.U /++ PUBLIC.U_IDX_B: B = T.B ++/ "
+ "ON 1=1 WHERE T.B = U.B */ INNER JOIN PUBLIC.T /* batched:test PUBLIC.T_IDX_A: A = Z.A */ "
+ "ON 1=1 WHERE Z.A = T.A");
......@@ -573,14 +573,14 @@ public class TestTableEngines extends TestBase {
+ "UNION "
+ "(SELECT A FROM PUBLIC.U /++ PUBLIC.\"scan\" ++/) */ "
+ "INNER JOIN PUBLIC.T /* batched:test PUBLIC.T_IDX_A: A = Z.A */ ON 1=1 WHERE Z.A = T.A");
checkPlan(stat, "SELECT 1 FROM PUBLIC.U /* PUBLIC.\"scan\" */ "
checkPlan(stat, "SELECT 1 FROM PUBLIC.U /* PUBLIC.U_IDX_B */ "
+ "INNER JOIN ( (SELECT A, B FROM PUBLIC.T) UNION (SELECT B, A FROM PUBLIC.U) ) Z "
+ "/* batched:view (SELECT A, B FROM PUBLIC.T /++ batched:test PUBLIC.T_IDX_B: B IS ?1 ++/ "
+ "WHERE B IS ?1) UNION (SELECT B, A FROM PUBLIC.U /++ PUBLIC.U_IDX_A: A IS ?1 ++/ "
+ "WHERE A IS ?1): B = U.B */ ON 1=1 /* WHERE U.B = Z.B */ "
+ "INNER JOIN PUBLIC.T /* batched:test PUBLIC.T_IDX_A: A = Z.A */ ON 1=1 "
+ "WHERE (U.B = Z.B) AND (Z.A = T.A)");
checkPlan(stat, "SELECT 1 FROM PUBLIC.U /* PUBLIC.\"scan\" */ "
checkPlan(stat, "SELECT 1 FROM PUBLIC.U /* PUBLIC.U_IDX_A */ "
+ "INNER JOIN ( SELECT A, B FROM PUBLIC.U ) Z "
+ "/* batched:fake SELECT A, B FROM PUBLIC.U /++ PUBLIC.U_IDX_A: A IS ?1 ++/ "
+ "WHERE A IS ?1: A = U.A */ ON 1=1 /* WHERE U.A = Z.A */ "
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论