From 2b14525fec1c909aae083f62da492443ad0dd608 Mon Sep 17 00:00:00 2001 From: kiss034 Date: Wed, 18 Oct 2023 14:20:21 +0200 Subject: [PATCH 01/12] Improving search by compound indexes. --- .../h2/expression/condition/ConditionIn.java | 41 ++---- .../condition/ConditionInConstantSet.java | 45 ++----- h2/src/main/org/h2/index/IndexCondition.java | 118 ++++++++++++++++-- h2/src/main/org/h2/index/IndexCursor.java | 67 +++++++++- h2/src/main/org/h2/table/TableFilter.java | 41 +++++- h2/src/main/org/h2/value/ValueRow.java | 27 ++++ 6 files changed, 248 insertions(+), 91 deletions(-) diff --git a/h2/src/main/org/h2/expression/condition/ConditionIn.java b/h2/src/main/org/h2/expression/condition/ConditionIn.java index 4d055259be..2a2fb6aa94 100644 --- a/h2/src/main/org/h2/expression/condition/ConditionIn.java +++ b/h2/src/main/org/h2/expression/condition/ConditionIn.java @@ -165,44 +165,19 @@ public void createIndexConditions(SessionLocal session, TableFilter filter) { } else if (left instanceof ExpressionList) { ExpressionList list = (ExpressionList) left; if (!list.isArray()) { - createIndexConditions(filter, list); + createIndexConditions(filter); } } } - private void createIndexConditions(TableFilter filter, ExpressionList list) { - int c = list.getSubexpressionCount(); - for (int i = 0; i < c; i++) { - Expression e = list.getSubexpression(i); - if (e instanceof ExpressionColumn) { - ExpressionColumn l = (ExpressionColumn) e; - if (filter == l.getTableFilter()) { - ArrayList subList = new ArrayList<>(valueList.size()); - for (Expression row : valueList) { - if (row instanceof ExpressionList) { - ExpressionList r = (ExpressionList) row; - if (r.isArray() || r.getSubexpressionCount() != c) { - return; - } - subList.add(r.getSubexpression(i)); - } else if (row instanceof ValueExpression) { - Value v = row.getValue(null); - if (v.getValueType() != Value.ROW) { - return; - } - Value[] values = ((ValueRow) v).getList(); - if (c != values.length) { - return; - } - subList.add(ValueExpression.get(values[i])); - } else { - return; - } - } - createIndexConditions(filter, l, subList); - } - } + private void createIndexConditions(TableFilter filter) { + // We do not check filter here, because the IN condition can contain columns from multiple tables. + ExpressionVisitor visitor = ExpressionVisitor.getNotFromResolverVisitor(filter); + for (Expression e : valueList) { + if (!e.isEverything(visitor)) + return; } + filter.addIndexCondition(IndexCondition.getCompoundInList((ExpressionList) left, valueList)); } private static void createIndexConditions(TableFilter filter, ExpressionColumn l, // diff --git a/h2/src/main/org/h2/expression/condition/ConditionInConstantSet.java b/h2/src/main/org/h2/expression/condition/ConditionInConstantSet.java index 1ab4c1a751..5899030bad 100644 --- a/h2/src/main/org/h2/expression/condition/ConditionInConstantSet.java +++ b/h2/src/main/org/h2/expression/condition/ConditionInConstantSet.java @@ -139,48 +139,19 @@ public void createIndexConditions(SessionLocal session, TableFilter filter) { } else if (left instanceof ExpressionList) { ExpressionList list = (ExpressionList) left; if (!list.isArray()) { - createIndexConditions(filter, list); + createIndexConditions(filter); } } } - private void createIndexConditions(TableFilter filter, ExpressionList list) { - int c = list.getSubexpressionCount(); - for (int i = 0; i < c; i++) { - Expression e = list.getSubexpression(i); - if (e instanceof ExpressionColumn) { - ExpressionColumn l = (ExpressionColumn) e; - if (filter == l.getTableFilter()) { - ArrayList subList = new ArrayList<>(valueList.size()); - for (Expression row : valueList) { - if (row instanceof ExpressionList) { - ExpressionList r = (ExpressionList) row; - if (r.isArray() || r.getSubexpressionCount() != c) { - return; - } - subList.add(r.getSubexpression(i)); - } else if (row instanceof ValueExpression) { - Value v = row.getValue(null); - if (v.getValueType() != Value.ROW) { - return; - } - Value[] values = ((ValueRow) v).getList(); - if (c != values.length) { - return; - } - subList.add(ValueExpression.get(values[i])); - } else { - return; - } - } - TypeInfo type = l.getType(); - for (Expression expression : subList) { - type = TypeInfo.getHigherType(type, expression.getType()); - } - createIndexConditions(filter, l, subList, type); - } - } + private void createIndexConditions(TableFilter filter) { + // We do not check filter here, because the IN condition can contain columns from multiple tables. + ExpressionVisitor visitor = ExpressionVisitor.getNotFromResolverVisitor(filter); + for (Expression e : valueList) { + if (!e.isEverything(visitor)) + return; } + filter.addIndexCondition(IndexCondition.getCompoundInList((ExpressionList) left, valueList)); } private static void createIndexConditions(TableFilter filter, ExpressionColumn l, ArrayList valueList, diff --git a/h2/src/main/org/h2/index/IndexCondition.java b/h2/src/main/org/h2/index/IndexCondition.java index 2be3564dd7..bad44c3654 100644 --- a/h2/src/main/org/h2/index/IndexCondition.java +++ b/h2/src/main/org/h2/index/IndexCondition.java @@ -14,6 +14,7 @@ import org.h2.engine.SessionLocal; import org.h2.expression.Expression; import org.h2.expression.ExpressionColumn; +import org.h2.expression.ExpressionList; import org.h2.expression.ExpressionVisitor; import org.h2.expression.condition.Comparison; import org.h2.message.DbException; @@ -22,6 +23,7 @@ import org.h2.table.TableType; import org.h2.value.Value; import org.h2.value.ValueArray; +import org.h2.value.ValueRow; /** * A index condition object is made for each condition that can potentially use @@ -64,7 +66,12 @@ public class IndexCondition { */ public static final int SPATIAL_INTERSECTS = 16; - private final Column column; + /** + * Contains a {@link Column} or {@code Column[]} depending on the condition type. + * @see #isCompoundColumns() + */ + private final Object column; + /** * see constants in {@link Comparison} */ @@ -85,6 +92,24 @@ private IndexCondition(int compareType, ExpressionColumn column, this.expression = expression; } + /** + * @param compareType the comparison type, see constants in {@link Comparison} + */ + private IndexCondition(int compareType, ExpressionList columns, Expression expression) { + this.compareType = compareType; + if (columns == null) + this.column = null; + else { + int listSize = columns.getSubexpressionCount(); + Column[] result = new Column[listSize]; + for (int i = listSize; --i >= 0; ) { + result[i] = ((ExpressionColumn) columns.getSubexpression(i)).getColumn(); + } + this.column = result; + } + this.expression = expression; + } + /** * Create an index condition with the given parameters. * @@ -115,6 +140,19 @@ public static IndexCondition getInList(ExpressionColumn column, return cond; } + /** + * Create a compound index condition with the compare type IN_LIST and with the given parameters. + * + * @param columns the columns + * @param list the expression list + * @return the index condition + */ + public static IndexCondition getCompoundInList(ExpressionList columns, List list) { + IndexCondition cond = new IndexCondition(Comparison.IN_LIST, columns, null); + cond.expressionList = list; + return cond; + } + /** * Create an index condition with the compare type IN_ARRAY and with the * given parameters. @@ -162,10 +200,21 @@ public Value getCurrentValue(SessionLocal session) { public Value[] getCurrentValueList(SessionLocal session) { TreeSet valueSet = new TreeSet<>(session.getDatabase().getCompareMode()); if (compareType == Comparison.IN_LIST) { - for (Expression e : expressionList) { - Value v = e.getValue(session); - v = column.convert(session, v); - valueSet.add(v); + if (isCompoundColumns()) { + Column[] columns = getColumns(); + for (Expression e : expressionList) { + Value v = e.getValue(session); + v = ((ValueRow) v).convert(session, columns); + valueSet.add(v); + } + } + else { + Column column = getColumn(); + for (Expression e : expressionList) { + Value v = e.getValue(session); + v = column.convert(session, v); + valueSet.add(v); + } } } else if (compareType == Comparison.IN_ARRAY) { Value v = expression.getValue(session); @@ -203,6 +252,25 @@ public String getSQL(int sqlFlags) { return "FALSE"; } StringBuilder builder = new StringBuilder(); + builder = isCompoundColumns() ? buildSql(sqlFlags, builder) : buildSql(sqlFlags, getColumn(), builder); + return builder.toString(); + } + + private StringBuilder buildSql(int sqlFlags, StringBuilder builder) { + if (compareType == Comparison.IN_LIST) { + builder.append(" IN("); + for (int i = 0, s = expressionList.size(); i < s; i++) { + if (i > 0) + builder.append(", "); + builder.append(expressionList.get(i).getSQL(sqlFlags)); + } + return builder.append(')'); + } + else + throw new IllegalStateException("Multiple columns can only be used with compound IN lists."); + } + + private StringBuilder buildSql(int sqlFlags, Column column, StringBuilder builder) { column.getSQL(builder, sqlFlags); switch (compareType) { case Comparison.EQUAL: @@ -230,8 +298,7 @@ public String getSQL(int sqlFlags) { Expression.writeExpressions(builder.append(" IN("), expressionList, sqlFlags).append(')'); break; case Comparison.IN_ARRAY: - return expression.getSQL(builder.append(" = ANY("), sqlFlags, Expression.AUTO_PARENTHESES).append(')') - .toString(); + return expression.getSQL(builder.append(" = ANY("), sqlFlags, Expression.AUTO_PARENTHESES).append(')'); case Comparison.IN_QUERY: builder.append(" IN("); builder.append(expressionQuery.getPlanSQL(sqlFlags)); @@ -246,7 +313,7 @@ public String getSQL(int sqlFlags) { if (expression != null) { expression.getSQL(builder, sqlFlags, Expression.AUTO_PARENTHESES); } - return builder.toString(); + return builder; } /** @@ -266,7 +333,14 @@ public int getMask(ArrayList indexConditions) { case Comparison.IN_ARRAY: case Comparison.IN_QUERY: if (indexConditions.size() > 1) { - if (TableType.TABLE != column.getTable().getTableType()) { + if (isCompoundColumns()) { + Column[] columns = getColumns(); + for (int i = columns.length; --i >= 0; ) { + if (TableType.TABLE != columns[i].getTable().getTableType()) + return 0; + } + } + else if (TableType.TABLE != getColumn().getTable().getTableType()) { // if combined with other conditions, // IN(..) can only be used for regular tables // test case: @@ -360,9 +434,33 @@ public int getCompareType() { * Get the referenced column. * * @return the column + * @throws IllegalStateException if {@link #isCompoundColumns()} is {@code true} */ public Column getColumn() { - return column; + if (column instanceof Column) + return (Column) column; + throw new IllegalStateException("The getColumn() method cannot be with multiple columns."); + } + + /** + * Get the referenced columns. + * + * @return the column array + * @throws IllegalStateException if {@link #isCompoundColumns()} is {@code false} + */ + public Column[] getColumns() { + if (column instanceof Column[]) + return (Column[]) column; + throw new IllegalStateException("The getColumns() method cannot be with a single column."); + } + + /** + * Check if the expression contains multiple columns + * + * @return true if it contains multiple columns + */ + public boolean isCompoundColumns() { + return column instanceof Column[]; } /** diff --git a/h2/src/main/org/h2/index/IndexCursor.java b/h2/src/main/org/h2/index/IndexCursor.java index 33232aed51..6085a9e3d0 100644 --- a/h2/src/main/org/h2/index/IndexCursor.java +++ b/h2/src/main/org/h2/index/IndexCursor.java @@ -20,6 +20,7 @@ import org.h2.value.Value; import org.h2.value.ValueGeometry; import org.h2.value.ValueNull; +import org.h2.value.ValueRow; /** * The filter used to walk through an index. This class supports IN(..) @@ -39,7 +40,11 @@ public class IndexCursor implements Cursor { private SearchRow start, end, intersects; private Cursor cursor; - private Column inColumn; + /** + * Contains a {@link Column} or {@code Column[]} depending on the condition type. + * @see IndexCondition#isCompoundColumns() + */ + private Object inColumn; private int inListIndex; private Value[] inList; private ResultInterface inResult; @@ -87,6 +92,21 @@ public void prepare(SessionLocal s, ArrayList indexConditions) { if (index.isFindUsingFullTableScan()) { continue; } + if (condition.isCompoundColumns()) { + Column[] columns = condition.getColumns(); + if (condition.getCompareType() == Comparison.IN_LIST) { + if (start == null && end == null) { + if (canUseIndexForIn(columns)) { + this.inColumn = columns; + inList = condition.getCurrentValueList(s); + inListIndex = 0; + } + } + continue; + } + else + throw new IllegalStateException("Multiple columns can only be used with compound IN lists."); + } Column column = condition.getColumn(); switch (condition.getCompareType()) { case Comparison.IN_LIST: @@ -135,7 +155,7 @@ public void prepare(SessionLocal s, ArrayList indexConditions) { } // An X=? condition will produce less rows than // an X IN(..) condition, unless the X IN condition can use the index. - if ((isStart || isEnd) && !canUseIndexFor(inColumn)) { + if ((isStart || isEnd) && !canUseIndexFor((Column) inColumn)) { inColumn = null; inList = null; inResult = null; @@ -189,6 +209,26 @@ private boolean canUseIndexFor(Column column) { return idxCol == null || idxCol.column == column; } + private boolean canUseIndexForIn(Column[] columns) { + if ( inColumn != null ) { + // only one IN(..) condition can be used at the same time + return false; + } + // The first column of the index must match this column, + // or it must be a VIEW index (where the column is null). + // Multiple IN conditions with views are not supported, see + // IndexCondition.getMask. + IndexColumn[] cols = index.getIndexColumns(); + if (cols == null || cols.length != columns.length) + return true; + for (int i = 0; i < cols.length; i++) { + IndexColumn idxCol = cols[i]; + if (idxCol != null && idxCol.column != columns[i]) + return false; + } + return true; + } + private SearchRow getSpatialSearchRow(SearchRow row, int columnId, Value v) { if (row == null) { row = table.getTemplateRow(); @@ -309,6 +349,10 @@ private void nextCursor() { while (inResult.next()) { Value v = inResult.currentRow()[0]; if (v != ValueNull.INSTANCE) { + if (inColumn instanceof Column[]) + v = ((ValueRow) v).convert(session, (Column[]) inColumn); + else + v = ((Column) inColumn).convert(session, v); find(v); break; } @@ -317,9 +361,22 @@ private void nextCursor() { } private void find(Value v) { - v = inColumn.convert(session, v); - int id = inColumn.getColumnId(); - start.setValue(id, v); + if (inColumn instanceof Column[]) { + Column[] columns = (Column[]) inColumn; + Value[] values = ((ValueRow) v).getList(); + for (int i = columns.length; --i >= 0; ) { + Column column = columns[i]; + Value vv = column.convert(session, values[i]); + int id = column.getColumnId(); + start.setValue(id, vv); + } + } + else { + Column column = (Column) inColumn; + v = column.convert(session, v); + int id = column.getColumnId(); + start.setValue(id, v); + } cursor = index.find(session, start, start); } diff --git a/h2/src/main/org/h2/table/TableFilter.java b/h2/src/main/org/h2/table/TableFilter.java index a45058234e..3488157b03 100644 --- a/h2/src/main/org/h2/table/TableFilter.java +++ b/h2/src/main/org/h2/table/TableFilter.java @@ -232,9 +232,21 @@ public PlanItem getBestPlanItem(SessionLocal s, TableFilter[] filters, int filte masks = null; break; } - int id = condition.getColumn().getColumnId(); - if (id >= 0) { - masks[id] |= condition.getMask(indexConditions); + if (condition.isCompoundColumns()) { + // Set the op mask in case of compound columns as well. + Column[] columns = condition.getColumns(); + for (int i = 0, n = columns.length; i < n; i++) { + int id = columns[i].getColumnId(); + if (id >= 0) { + masks[id] |= condition.getMask(indexConditions); + } + } + } + else { + int id = condition.getColumn().getColumnId(); + if (id >= 0) { + masks[id] |= condition.getMask(indexConditions); + } } } } @@ -323,13 +335,30 @@ public void prepare() { for (int i = 0; i < indexConditions.size(); i++) { IndexCondition condition = indexConditions.get(i); if (!condition.isAlwaysFalse()) { - Column col = condition.getColumn(); - if (col.getColumnId() >= 0) { - if (index.getColumnIndex(col) < 0) { + if (condition.isCompoundColumns()) { + // Checking if all columns are indexed. + Column[] columns = condition.getColumns(); + boolean indexed = true; + for (int j = 0; j < columns.length; j++) { + Column col = columns[j]; + indexed = col.getColumnId() >= 0 && index.getColumnIndex(col) >= 0; + if (!indexed) + break; + } + if (!indexed) { // Not all columns are indexed so removing the current condition indexConditions.remove(i); i--; } } + else { + Column col = condition.getColumn(); + if (col.getColumnId() >= 0) { + if (index.getColumnIndex(col) < 0) { + indexConditions.remove(i); + i--; + } + } + } } } if (nestedJoin != null) { diff --git a/h2/src/main/org/h2/value/ValueRow.java b/h2/src/main/org/h2/value/ValueRow.java index cf9a4de155..72059011ef 100644 --- a/h2/src/main/org/h2/value/ValueRow.java +++ b/h2/src/main/org/h2/value/ValueRow.java @@ -10,6 +10,9 @@ import org.h2.engine.Constants; import org.h2.message.DbException; import org.h2.result.SimpleResult; +import org.h2.table.Column; + +import java.util.Arrays; /** * Row value. @@ -163,4 +166,28 @@ public boolean equals(Object other) { return true; } + /** + * Converts the values in a ValueRow based on the passed column info. + * Creates a new instance if any of the contained item must be converted. Otherwise, returns {@code this}. + * + * @param provider the cast information provider + * @param columns the column info list used for the conversation + * @return a ValueRow which contains the converted values + * + * @see Column#convert(CastDataProvider, Value) + */ + public ValueRow convert(CastDataProvider provider, Column[] columns) { + Value[] copy = null; + for (int i = values.length; --i >= 0; ) { + Value v = values[i]; + Value nv = columns[i].convert(provider, v); + if (v != nv) { + if (copy == null) + copy = Arrays.copyOf(values, values.length); + copy[i] = nv; + } + } + return copy == null ? this : get(type, copy); + } + } From 48b72bfd8a3ce92004a7fa731793dd63f282836e Mon Sep 17 00:00:00 2001 From: kiss034 Date: Fri, 27 Oct 2023 09:27:25 +0200 Subject: [PATCH 02/12] Fixing review findings. --- h2/src/main/org/h2/index/IndexCondition.java | 57 ++++++++++++-------- h2/src/main/org/h2/index/IndexCursor.java | 21 ++++---- h2/src/main/org/h2/table/Column.java | 29 ++++++++++ h2/src/main/org/h2/value/ValueRow.java | 27 ---------- 4 files changed, 73 insertions(+), 61 deletions(-) diff --git a/h2/src/main/org/h2/index/IndexCondition.java b/h2/src/main/org/h2/index/IndexCondition.java index bad44c3654..52201f5eaf 100644 --- a/h2/src/main/org/h2/index/IndexCondition.java +++ b/h2/src/main/org/h2/index/IndexCondition.java @@ -66,11 +66,9 @@ public class IndexCondition { */ public static final int SPATIAL_INTERSECTS = 16; - /** - * Contains a {@link Column} or {@code Column[]} depending on the condition type. - * @see #isCompoundColumns() - */ - private final Object column; + private final Column column; + private final Column[] columns; + private final boolean compoundColumns; /** * see constants in {@link Comparison} @@ -89,6 +87,8 @@ private IndexCondition(int compareType, ExpressionColumn column, Expression expression) { this.compareType = compareType; this.column = column == null ? null : column.getColumn(); + this.columns = null; + this.compoundColumns = false; this.expression = expression; } @@ -97,16 +97,18 @@ private IndexCondition(int compareType, ExpressionColumn column, */ private IndexCondition(int compareType, ExpressionList columns, Expression expression) { this.compareType = compareType; - if (columns == null) - this.column = null; - else { + this.column = null; + if (columns == null) { + this.columns = null; + } else { int listSize = columns.getSubexpressionCount(); Column[] result = new Column[listSize]; for (int i = listSize; --i >= 0; ) { result[i] = ((ExpressionColumn) columns.getSubexpression(i)).getColumn(); } - this.column = result; + this.columns = result; } + this.compoundColumns = true; this.expression = expression; } @@ -203,8 +205,8 @@ public Value[] getCurrentValueList(SessionLocal session) { if (isCompoundColumns()) { Column[] columns = getColumns(); for (Expression e : expressionList) { - Value v = e.getValue(session); - v = ((ValueRow) v).convert(session, columns); + ValueRow v = (ValueRow) e.getValue(session); + v = Column.convert(session, columns, v); valueSet.add(v); } } @@ -266,8 +268,9 @@ private StringBuilder buildSql(int sqlFlags, StringBuilder builder) { } return builder.append(')'); } - else - throw new IllegalStateException("Multiple columns can only be used with compound IN lists."); + else { + throw DbException.getInternalError("Multiple columns can only be used with compound IN lists."); + } } private StringBuilder buildSql(int sqlFlags, Column column, StringBuilder builder) { @@ -434,24 +437,26 @@ public int getCompareType() { * Get the referenced column. * * @return the column - * @throws IllegalStateException if {@link #isCompoundColumns()} is {@code true} + * @throws DbException if {@link #isCompoundColumns()} is {@code true} */ public Column getColumn() { - if (column instanceof Column) - return (Column) column; - throw new IllegalStateException("The getColumn() method cannot be with multiple columns."); + if (!isCompoundColumns()) { + return column; + } + throw DbException.getInternalError("The getColumn() method cannot be with multiple columns."); } /** * Get the referenced columns. * * @return the column array - * @throws IllegalStateException if {@link #isCompoundColumns()} is {@code false} + * @throws DbException if {@link #isCompoundColumns()} is {@code false} */ public Column[] getColumns() { - if (column instanceof Column[]) - return (Column[]) column; - throw new IllegalStateException("The getColumns() method cannot be with a single column."); + if (isCompoundColumns()) { + return columns; + } + throw DbException.getInternalError("The getColumns() method cannot be with a single column."); } /** @@ -460,7 +465,7 @@ public Column[] getColumns() { * @return true if it contains multiple columns */ public boolean isCompoundColumns() { - return column instanceof Column[]; + return compoundColumns; } /** @@ -514,7 +519,13 @@ public boolean isEvaluatable() { @Override public String toString() { - StringBuilder builder = new StringBuilder("column=").append(column).append(", compareType="); + StringBuilder builder = new StringBuilder(); + if (!isCompoundColumns()) { + builder.append("column=").append(column); + } else { + builder.append("columns=").append(columns); + } + builder.append(", compareType="); return compareTypeToString(builder, compareType) .append(", expression=").append(expression) .append(", expressionList=").append(expressionList) diff --git a/h2/src/main/org/h2/index/IndexCursor.java b/h2/src/main/org/h2/index/IndexCursor.java index 6085a9e3d0..f073503b57 100644 --- a/h2/src/main/org/h2/index/IndexCursor.java +++ b/h2/src/main/org/h2/index/IndexCursor.java @@ -103,9 +103,9 @@ public void prepare(SessionLocal s, ArrayList indexConditions) { } } continue; + } else { + throw DbException.getInternalError("Multiple columns can only be used with compound IN lists."); } - else - throw new IllegalStateException("Multiple columns can only be used with compound IN lists."); } Column column = condition.getColumn(); switch (condition.getCompareType()) { @@ -210,7 +210,7 @@ private boolean canUseIndexFor(Column column) { } private boolean canUseIndexForIn(Column[] columns) { - if ( inColumn != null ) { + if (inColumn != null) { // only one IN(..) condition can be used at the same time return false; } @@ -349,10 +349,11 @@ private void nextCursor() { while (inResult.next()) { Value v = inResult.currentRow()[0]; if (v != ValueNull.INSTANCE) { - if (inColumn instanceof Column[]) - v = ((ValueRow) v).convert(session, (Column[]) inColumn); - else + if (inColumn instanceof Column[]) { + v = Column.convert(session, (Column[]) inColumn, (ValueRow) v); + } else { v = ((Column) inColumn).convert(session, v); + } find(v); break; } @@ -363,12 +364,10 @@ private void nextCursor() { private void find(Value v) { if (inColumn instanceof Column[]) { Column[] columns = (Column[]) inColumn; - Value[] values = ((ValueRow) v).getList(); + ValueRow converted = Column.convert(session, columns, ((ValueRow) v)); + Value[] values = converted.getList(); for (int i = columns.length; --i >= 0; ) { - Column column = columns[i]; - Value vv = column.convert(session, values[i]); - int id = column.getColumnId(); - start.setValue(id, vv); + start.setValue(columns[i].getColumnId(), values[i]); } } else { diff --git a/h2/src/main/org/h2/table/Column.java b/h2/src/main/org/h2/table/Column.java index beee700c92..8bbb4c4a13 100644 --- a/h2/src/main/org/h2/table/Column.java +++ b/h2/src/main/org/h2/table/Column.java @@ -6,6 +6,7 @@ package org.h2.table; import java.sql.ResultSetMetaData; +import java.util.Arrays; import java.util.Objects; import org.h2.api.ErrorCode; @@ -29,6 +30,7 @@ import org.h2.value.Typed; import org.h2.value.Value; import org.h2.value.ValueNull; +import org.h2.value.ValueRow; import org.h2.value.ValueUuid; /** @@ -187,6 +189,33 @@ public Value convert(CastDataProvider provider, Value v) { } } + + /** + * Converts the values in a ValueRow based on the passed column info. + * Creates a new instance if any of the contained item must be converted. Otherwise, returns the same {@code valueRow}. + * + * @param provider the cast information provider + * @param columns the column info list used for the conversation + * @param valueRow the holder of the values + * @return a ValueRow which contains the converted values + * + * @see Column#convert(CastDataProvider, Value) + */ + public static ValueRow convert(CastDataProvider provider, Column[] columns, ValueRow valueRow) { + Value[] copy = null; + Value[] values = valueRow.getList(); + for (int i = values.length; --i >= 0; ) { + Value v = values[i]; + Value nv = columns[i].convert(provider, v); + if (v != nv) { + if (copy == null) + copy = Arrays.copyOf(values, values.length); + copy[i] = nv; + } + } + return copy == null ? valueRow : ValueRow.get(valueRow.getType(), copy); + } + /** * Returns whether this column is an identity column. * diff --git a/h2/src/main/org/h2/value/ValueRow.java b/h2/src/main/org/h2/value/ValueRow.java index 72059011ef..cf9a4de155 100644 --- a/h2/src/main/org/h2/value/ValueRow.java +++ b/h2/src/main/org/h2/value/ValueRow.java @@ -10,9 +10,6 @@ import org.h2.engine.Constants; import org.h2.message.DbException; import org.h2.result.SimpleResult; -import org.h2.table.Column; - -import java.util.Arrays; /** * Row value. @@ -166,28 +163,4 @@ public boolean equals(Object other) { return true; } - /** - * Converts the values in a ValueRow based on the passed column info. - * Creates a new instance if any of the contained item must be converted. Otherwise, returns {@code this}. - * - * @param provider the cast information provider - * @param columns the column info list used for the conversation - * @return a ValueRow which contains the converted values - * - * @see Column#convert(CastDataProvider, Value) - */ - public ValueRow convert(CastDataProvider provider, Column[] columns) { - Value[] copy = null; - for (int i = values.length; --i >= 0; ) { - Value v = values[i]; - Value nv = columns[i].convert(provider, v); - if (v != nv) { - if (copy == null) - copy = Arrays.copyOf(values, values.length); - copy[i] = nv; - } - } - return copy == null ? this : get(type, copy); - } - } From c57a62f0f054a30e654f1534415abe906a257a6c Mon Sep 17 00:00:00 2001 From: kiss034 Date: Mon, 30 Oct 2023 10:06:42 +0100 Subject: [PATCH 03/12] Fixing the index condition handling. --- .../h2/expression/condition/ConditionIn.java | 55 +++++++++++++++++- .../condition/ConditionInConstantSet.java | 58 ++++++++++++++++++- h2/src/main/org/h2/table/TableFilter.java | 8 ++- 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/h2/src/main/org/h2/expression/condition/ConditionIn.java b/h2/src/main/org/h2/expression/condition/ConditionIn.java index 2a2fb6aa94..c7f67b4652 100644 --- a/h2/src/main/org/h2/expression/condition/ConditionIn.java +++ b/h2/src/main/org/h2/expression/condition/ConditionIn.java @@ -6,6 +6,8 @@ package org.h2.expression.condition; import java.util.ArrayList; +import java.util.List; + import org.h2.engine.SessionLocal; import org.h2.expression.Expression; import org.h2.expression.ExpressionColumn; @@ -165,12 +167,22 @@ public void createIndexConditions(SessionLocal session, TableFilter filter) { } else if (left instanceof ExpressionList) { ExpressionList list = (ExpressionList) left; if (!list.isArray()) { - createIndexConditions(filter); + // First we create a compound index condition. + createCompoundIndexCondition(filter); + // If there is no compound index, then the TableFilter#prepare() method will drop this condition. + // Then we create a unique index condition for each column. + createUniqueIndexConditions(filter, list); + // If there are two or more index conditions, IndexCursor will only use the first one. + // See: IndexCursor#canUseIndexForIn(Column) } } } - private void createIndexConditions(TableFilter filter) { + /** + * Creates a compound index condition containing every item in the expression list. + * @see IndexCondition#getCompoundInList(ExpressionList, List) + */ + private void createCompoundIndexCondition(TableFilter filter) { // We do not check filter here, because the IN condition can contain columns from multiple tables. ExpressionVisitor visitor = ExpressionVisitor.getNotFromResolverVisitor(filter); for (Expression e : valueList) { @@ -180,6 +192,45 @@ private void createIndexConditions(TableFilter filter) { filter.addIndexCondition(IndexCondition.getCompoundInList((ExpressionList) left, valueList)); } + /** + * Creates a unique index condition for every item in the expression list. + * @see IndexCondition#getInList(ExpressionColumn, List) + */ + private void createUniqueIndexConditions(TableFilter filter, ExpressionList list) { + int c = list.getSubexpressionCount(); + for (int i = 0; i < c; i++) { + Expression e = list.getSubexpression(i); + if (e instanceof ExpressionColumn) { + ExpressionColumn l = (ExpressionColumn) e; + if (filter == l.getTableFilter()) { + ArrayList subList = new ArrayList<>(valueList.size()); + for (Expression row : valueList) { + if (row instanceof ExpressionList) { + ExpressionList r = (ExpressionList) row; + if (r.isArray() || r.getSubexpressionCount() != c) { + return; + } + subList.add(r.getSubexpression(i)); + } else if (row instanceof ValueExpression) { + Value v = row.getValue(null); + if (v.getValueType() != Value.ROW) { + return; + } + Value[] values = ((ValueRow) v).getList(); + if (c != values.length) { + return; + } + subList.add(ValueExpression.get(values[i])); + } else { + return; + } + } + createIndexConditions(filter, l, subList); + } + } + } + } + private static void createIndexConditions(TableFilter filter, ExpressionColumn l, // ArrayList valueList) { ExpressionVisitor visitor = ExpressionVisitor.getNotFromResolverVisitor(filter); diff --git a/h2/src/main/org/h2/expression/condition/ConditionInConstantSet.java b/h2/src/main/org/h2/expression/condition/ConditionInConstantSet.java index 5899030bad..2a41c46c0c 100644 --- a/h2/src/main/org/h2/expression/condition/ConditionInConstantSet.java +++ b/h2/src/main/org/h2/expression/condition/ConditionInConstantSet.java @@ -6,6 +6,7 @@ package org.h2.expression.condition; import java.util.ArrayList; +import java.util.List; import java.util.TreeSet; import org.h2.engine.SessionLocal; @@ -139,12 +140,22 @@ public void createIndexConditions(SessionLocal session, TableFilter filter) { } else if (left instanceof ExpressionList) { ExpressionList list = (ExpressionList) left; if (!list.isArray()) { - createIndexConditions(filter); + // First we create a compound index condition. + createCompoundIndexCondition(filter); + // If there is no compound index, then the TableFilter#prepare() method will drop this condition. + // Then we create a unique index condition for each column. + createUniqueIndexConditions(filter, list); + // If there are two or more index conditions, IndexCursor will only use the first one. + // See: IndexCursor#canUseIndexForIn(Column) } } } - private void createIndexConditions(TableFilter filter) { + /** + * Creates a compound index condition containing every item in the expression list. + * @see IndexCondition#getCompoundInList(ExpressionList, List) + */ + private void createCompoundIndexCondition(TableFilter filter) { // We do not check filter here, because the IN condition can contain columns from multiple tables. ExpressionVisitor visitor = ExpressionVisitor.getNotFromResolverVisitor(filter); for (Expression e : valueList) { @@ -154,6 +165,49 @@ private void createIndexConditions(TableFilter filter) { filter.addIndexCondition(IndexCondition.getCompoundInList((ExpressionList) left, valueList)); } + /** + * Creates a unique index condition for every item in the expression list. + * @see IndexCondition#getInList(ExpressionColumn, List) + */ + private void createUniqueIndexConditions(TableFilter filter, ExpressionList list) { + int c = list.getSubexpressionCount(); + for (int i = 0; i < c; i++) { + Expression e = list.getSubexpression(i); + if (e instanceof ExpressionColumn) { + ExpressionColumn l = (ExpressionColumn) e; + if (filter == l.getTableFilter()) { + ArrayList subList = new ArrayList<>(valueList.size()); + for (Expression row : valueList) { + if (row instanceof ExpressionList) { + ExpressionList r = (ExpressionList) row; + if (r.isArray() || r.getSubexpressionCount() != c) { + return; + } + subList.add(r.getSubexpression(i)); + } else if (row instanceof ValueExpression) { + Value v = row.getValue(null); + if (v.getValueType() != Value.ROW) { + return; + } + Value[] values = ((ValueRow) v).getList(); + if (c != values.length) { + return; + } + subList.add(ValueExpression.get(values[i])); + } else { + return; + } + } + TypeInfo type = l.getType(); + for (Expression expression : subList) { + type = TypeInfo.getHigherType(type, expression.getType()); + } + createIndexConditions(filter, l, subList, type); + } + } + } + } + private static void createIndexConditions(TableFilter filter, ExpressionColumn l, ArrayList valueList, TypeInfo type) { TypeInfo colType = l.getType(); diff --git a/h2/src/main/org/h2/table/TableFilter.java b/h2/src/main/org/h2/table/TableFilter.java index 3488157b03..4b1091ae16 100644 --- a/h2/src/main/org/h2/table/TableFilter.java +++ b/h2/src/main/org/h2/table/TableFilter.java @@ -349,11 +349,13 @@ public void prepare() { indexConditions.remove(i); i--; } - } - else { + } else { Column col = condition.getColumn(); if (col.getColumnId() >= 0) { - if (index.getColumnIndex(col) < 0) { + if (index.getColumnIndex(col) != 0) { + // If this is a simple index, then the column index must be 0 on a match. + // If this is a compound index, we can use it only if the first index column is the searched one. + // See: IndexCursor#canUseIndexFor(column) indexConditions.remove(i); i--; } From 1e4c3e80d9e67a233fe262ead1ef2ffea98e2924 Mon Sep 17 00:00:00 2001 From: kiss034 Date: Mon, 30 Oct 2023 13:37:44 +0100 Subject: [PATCH 04/12] Adjusting test cases. --- .../h2/test/scripts/compatibility/compatibility.sql | 1 + h2/src/test/org/h2/test/scripts/indexes.sql | 12 ++++++++---- h2/src/test/org/h2/test/scripts/predicates/null.sql | 6 ++++++ .../h2/test/scripts/queries/query-optimisations.sql | 6 +++++- h2/src/test/org/h2/test/scripts/testScript.sql | 8 +++++++- 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/h2/src/test/org/h2/test/scripts/compatibility/compatibility.sql b/h2/src/test/org/h2/test/scripts/compatibility/compatibility.sql index ad9294fcfa..9c39bd1362 100644 --- a/h2/src/test/org/h2/test/scripts/compatibility/compatibility.sql +++ b/h2/src/test/org/h2/test/scripts/compatibility/compatibility.sql @@ -779,6 +779,7 @@ INSERT INTO TEST (SELECT X, X + 1, X + 2 FROM SYSTEM_RANGE(1, 5)); EXPLAIN UPDATE TEST T SET V = V.V FROM (VALUES (1, 2, 4)) V(ID1, ID2, V) WHERE (T.ID1, T.ID2) = (V.ID1, V.ID2); >> MERGE INTO "PUBLIC"."TEST" "T" /* PUBLIC.PRIMARY_KEY_2: ID1 = V.ID1 AND ID2 = V.ID2 */ USING (VALUES (1, 2, 4)) "V"("ID1", "ID2", "V") /* table scan */ ON (ROW ("T"."ID1", "T"."ID2") = ROW ("V"."ID1", "V"."ID2")) WHEN MATCHED THEN UPDATE SET "V" = "V"."V" +-- TODO:kiss034: This should be fixed later. UPDATE TEST T SET V = V.V FROM (VALUES (1, 2, 4)) V(ID1, ID2, V) WHERE (T.ID1, T.ID2) = (V.ID1, V.ID2); > update count: 1 diff --git a/h2/src/test/org/h2/test/scripts/indexes.sql b/h2/src/test/org/h2/test/scripts/indexes.sql index 44363d5693..21f2af3e67 100644 --- a/h2/src/test/org/h2/test/scripts/indexes.sql +++ b/h2/src/test/org/h2/test/scripts/indexes.sql @@ -321,10 +321,12 @@ EXPLAIN SELECT * FROM TEST WHERE A = 0; >> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A1: A = 0 */ WHERE "A" = 0 EXPLAIN SELECT * FROM TEST WHERE A = 0 AND B >= 0; ->> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A_B: A = 0 AND B >= 0 */ WHERE ("A" = 0) AND ("B" >= 0) +>> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A_B: A = 0 */ WHERE ("A" = 0) AND ("B" >= 0) +-- B is not the first column of the T_A_B index, so it cannot be used as an index condition. EXPLAIN SELECT * FROM TEST WHERE A > 0 AND B >= 0; ->> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A_B: A > 0 AND B >= 0 */ WHERE ("A" > 0) AND ("B" >= 0) +>> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A_B: A > 0 */ WHERE ("A" > 0) AND ("B" >= 0) +-- B is not the first column of the T_A_B index, so it cannot be used as an index condition. INSERT INTO TEST (SELECT X / 100, X, X FROM SYSTEM_RANGE(1, 3000)); > update count: 3000 @@ -333,10 +335,12 @@ EXPLAIN SELECT * FROM TEST WHERE A = 0; >> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A1: A = 0 */ WHERE "A" = 0 EXPLAIN SELECT * FROM TEST WHERE A = 0 AND B >= 0; ->> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A_B: A = 0 AND B >= 0 */ WHERE ("A" = 0) AND ("B" >= 0) +>> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A_B: A = 0 */ WHERE ("A" = 0) AND ("B" >= 0) +-- B is not the first column of the T_A_B index, so it cannot be used as an index condition. EXPLAIN SELECT * FROM TEST WHERE A > 0 AND B >= 0; ->> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A_B: A > 0 AND B >= 0 */ WHERE ("A" > 0) AND ("B" >= 0) +>> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A_B: A > 0 */ WHERE ("A" > 0) AND ("B" >= 0) +-- B is not the first column of the T_A_B index, so it cannot be used as an index condition. -- Test that creation order of indexes has no effect CREATE INDEX T_A2 ON TEST(A); diff --git a/h2/src/test/org/h2/test/scripts/predicates/null.sql b/h2/src/test/org/h2/test/scripts/predicates/null.sql index 1f76166b9f..2f542b5550 100644 --- a/h2/src/test/org/h2/test/scripts/predicates/null.sql +++ b/h2/src/test/org/h2/test/scripts/predicates/null.sql @@ -136,6 +136,7 @@ SELECT * FROM TEST T1 JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T EXPLAIN SELECT * FROM TEST T1 JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T2.B) IS NULL; >> SELECT "T1"."A", "T1"."B", "T2"."A", "T2"."B" FROM "PUBLIC"."TEST" "T2" /* PUBLIC.TEST_A_B_IDX: A IS NULL AND B IS NULL */ /* WHERE ROW (T2.A, T2.B) IS NULL */ INNER JOIN "PUBLIC"."TEST" "T1" /* PUBLIC.TEST_A_B_IDX: A = T2.A AND B = T2.B */ ON 1=1 WHERE (ROW ("T2"."A", "T2"."B") IS NULL) AND (ROW ("T1"."A", "T1"."B") = ROW ("T2"."A", "T2"."B")) +-- TODO:kiss034: This should be fixed later. SELECT * FROM TEST T1 LEFT JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T2.B) IS NULL; > A B A B @@ -147,6 +148,7 @@ SELECT * FROM TEST T1 LEFT JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2 EXPLAIN SELECT * FROM TEST T1 LEFT JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T2.B) IS NULL; >> SELECT "T1"."A", "T1"."B", "T2"."A", "T2"."B" FROM "PUBLIC"."TEST" "T1" /* PUBLIC.TEST_A_B_IDX */ LEFT OUTER JOIN "PUBLIC"."TEST" "T2" /* PUBLIC.TEST_A_B_IDX: A = T1.A AND B = T1.B */ ON ROW ("T1"."A", "T1"."B") = ROW ("T2"."A", "T2"."B") WHERE ROW ("T2"."A", "T2"."B") IS NULL +-- TODO:kiss034: This should be fixed later. SELECT * FROM TEST T1 JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T2.B) IS NOT NULL; > A B A B @@ -156,6 +158,7 @@ SELECT * FROM TEST T1 JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T EXPLAIN SELECT * FROM TEST T1 JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T2.B) IS NOT NULL; >> SELECT "T1"."A", "T1"."B", "T2"."A", "T2"."B" FROM "PUBLIC"."TEST" "T1" /* PUBLIC.TEST_A_B_IDX */ INNER JOIN "PUBLIC"."TEST" "T2" /* PUBLIC.TEST_A_B_IDX: A = T1.A AND B = T1.B */ ON 1=1 WHERE (ROW ("T2"."A", "T2"."B") IS NOT NULL) AND (ROW ("T1"."A", "T1"."B") = ROW ("T2"."A", "T2"."B")) +-- TODO:kiss034: This should be fixed later. SELECT * FROM TEST T1 LEFT JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T2.B) IS NOT NULL; > A B A B @@ -165,6 +168,7 @@ SELECT * FROM TEST T1 LEFT JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2 EXPLAIN SELECT * FROM TEST T1 LEFT JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T2.B) IS NOT NULL; >> SELECT "T1"."A", "T1"."B", "T2"."A", "T2"."B" FROM "PUBLIC"."TEST" "T1" /* PUBLIC.TEST_A_B_IDX */ LEFT OUTER JOIN "PUBLIC"."TEST" "T2" /* PUBLIC.TEST_A_B_IDX: A = T1.A AND B = T1.B */ ON ROW ("T1"."A", "T1"."B") = ROW ("T2"."A", "T2"."B") WHERE ROW ("T2"."A", "T2"."B") IS NOT NULL +-- TODO:kiss034: This should be fixed later. EXPLAIN SELECT A, B FROM TEST WHERE (A, NULL) IS NULL; >> SELECT "A", "B" FROM "PUBLIC"."TEST" /* PUBLIC.TEST_A_B_IDX: A IS NULL */ WHERE "A" IS NULL @@ -180,9 +184,11 @@ EXPLAIN SELECT A, B FROM TEST WHERE NOT (A, NULL) IS NOT NULL; EXPLAIN SELECT A, B FROM TEST WHERE (A, NULL, B) IS NULL; >> SELECT "A", "B" FROM "PUBLIC"."TEST" /* PUBLIC.TEST_A_B_IDX: A IS NULL AND B IS NULL */ WHERE ROW ("A", "B") IS NULL +-- TODO:kiss034: This should be fixed later. EXPLAIN SELECT A, B FROM TEST WHERE (A, NULL, B, NULL) IS NULL; >> SELECT "A", "B" FROM "PUBLIC"."TEST" /* PUBLIC.TEST_A_B_IDX: A IS NULL AND B IS NULL */ WHERE ROW ("A", "B") IS NULL +-- TODO:kiss034: This should be fixed later. DROP TABLE TEST; > ok diff --git a/h2/src/test/org/h2/test/scripts/queries/query-optimisations.sql b/h2/src/test/org/h2/test/scripts/queries/query-optimisations.sql index 87de62c705..41633fd2d7 100644 --- a/h2/src/test/org/h2/test/scripts/queries/query-optimisations.sql +++ b/h2/src/test/org/h2/test/scripts/queries/query-optimisations.sql @@ -16,7 +16,8 @@ insert into person select convert(x,varchar) as firstname, (convert(x,varchar) | -- can directly use the index. -- explain analyze SELECT * FROM person WHERE firstname IN ('FirstName1', 'FirstName2') AND lastname='LastName1'; ->> SELECT "PUBLIC"."PERSON"."FIRSTNAME", "PUBLIC"."PERSON"."LASTNAME" FROM "PUBLIC"."PERSON" /* PUBLIC.PERSON_1: FIRSTNAME IN('FirstName1', 'FirstName2') AND LASTNAME = 'LastName1' */ /* scanCount: 1 */ WHERE ("FIRSTNAME" IN('FirstName1', 'FirstName2')) AND ("LASTNAME" = 'LastName1') +>> SELECT "PUBLIC"."PERSON"."FIRSTNAME", "PUBLIC"."PERSON"."LASTNAME" FROM "PUBLIC"."PERSON" /* PUBLIC.PERSON_1: FIRSTNAME IN('FirstName1', 'FirstName2') */ /* scanCount: 1 */ WHERE ("FIRSTNAME" IN('FirstName1', 'FirstName2')) AND ("LASTNAME" = 'LastName1') +-- lastname is neither indexed per se, nor the first column of the person_1 index, so it cannot be used as an index condition. CREATE TABLE TEST(A SMALLINT PRIMARY KEY, B SMALLINT); > ok @@ -111,11 +112,13 @@ SELECT T1.ID, T2.V AS LV FROM (SELECT ID, MAX(V) AS LV FROM T GROUP BY ID) AS T1 > 1 2 > 2 2 > rows (ordered): 2 +-- TODO:kiss034: This should be fixed later. EXPLAIN SELECT T1.ID, T2.V AS LV FROM (SELECT ID, MAX(V) AS LV FROM T GROUP BY ID) AS T1 INNER JOIN T AS T2 ON T2.ID = T1.ID AND T2.V = T1.LV WHERE T1.ID IN (1, 2) ORDER BY ID; >> SELECT "T1"."ID", "T2"."V" AS "LV" FROM "PUBLIC"."T" "T2" /* PUBLIC.T.tableScan */ INNER JOIN ( SELECT "ID", MAX("V") AS "LV" FROM "PUBLIC"."T" GROUP BY "ID" ) "T1" /* SELECT ID, MAX(V) AS LV FROM PUBLIC.T /* PUBLIC.T.tableScan */ WHERE ID IS NOT DISTINCT FROM ?1 GROUP BY ID HAVING MAX(V) IS NOT DISTINCT FROM ?2: ID = T2.ID AND LV = T2.V */ ON 1=1 WHERE ("T1"."ID" IN(1, 2)) AND ("T2"."ID" = "T1"."ID") AND ("T2"."V" = "T1"."LV") ORDER BY 1 +-- TODO:kiss034: This should be fixed later. DROP TABLE T; > ok @@ -294,6 +297,7 @@ CREATE INDEX T1_A_B_IDX ON T1(A, B); EXPLAIN SELECT * FROM T1 WHERE (A, B) = (1, 2); >> SELECT "PUBLIC"."T1"."A", "PUBLIC"."T1"."B" FROM "PUBLIC"."T1" /* PUBLIC.T1_A_B_IDX: A = 1 AND B = 2 */ WHERE ROW ("A", "B") = ROW (1, 2) +-- TODO:kiss034: This should be fixed later. EXPLAIN SELECT * FROM T1 WHERE (A, B) > (1, 2); >> SELECT "PUBLIC"."T1"."A", "PUBLIC"."T1"."B" FROM "PUBLIC"."T1" /* PUBLIC.T1_A_B_IDX: A >= 1 */ WHERE ROW ("A", "B") > ROW (1, 2) diff --git a/h2/src/test/org/h2/test/scripts/testScript.sql b/h2/src/test/org/h2/test/scripts/testScript.sql index bbce5b403e..ec5fe93edb 100644 --- a/h2/src/test/org/h2/test/scripts/testScript.sql +++ b/h2/src/test/org/h2/test/scripts/testScript.sql @@ -1531,7 +1531,8 @@ EXPLAIN SELECT * FROM TEST WHERE FLAG; >> SELECT "PUBLIC"."TEST"."ID", "PUBLIC"."TEST"."FLAG", "PUBLIC"."TEST"."NAME" FROM "PUBLIC"."TEST" /* PUBLIC.IDX_FLAG: FLAG = TRUE */ WHERE "FLAG" EXPLAIN SELECT * FROM TEST WHERE FLAG AND NAME>'I'; ->> SELECT "PUBLIC"."TEST"."ID", "PUBLIC"."TEST"."FLAG", "PUBLIC"."TEST"."NAME" FROM "PUBLIC"."TEST" /* PUBLIC.IDX_FLAG: FLAG = TRUE AND NAME > 'I' */ WHERE "FLAG" AND ("NAME" > 'I') +>> SELECT "PUBLIC"."TEST"."ID", "PUBLIC"."TEST"."FLAG", "PUBLIC"."TEST"."NAME" FROM "PUBLIC"."TEST" /* PUBLIC.IDX_FLAG: FLAG = TRUE */ WHERE "FLAG" AND ("NAME" > 'I') +-- NAME is neither indexed per se, nor the first column of the IDX_FLAG index, so it cannot be used as an index condition. DROP TABLE TEST; > ok @@ -4423,6 +4424,11 @@ SELECT * FROM TEST2COL WHERE A=0 AND B=0; EXPLAIN SELECT * FROM TEST2COL WHERE A=0 AND B=0; >> SELECT "PUBLIC"."TEST2COL"."A", "PUBLIC"."TEST2COL"."B", "PUBLIC"."TEST2COL"."C" FROM "PUBLIC"."TEST2COL" /* PUBLIC.PRIMARY_KEY_E: A = 0 AND B = 0 */ WHERE ("A" = 0) AND ("B" = 0) +-- B is neither indexed per se, nor the first column of the PRIMARY KEY, so it cannot be used as an index condition. + +EXPLAIN SELECT * FROM TEST2COL WHERE (A, B)=(0, 0); +>> SELECT "PUBLIC"."TEST2COL"."A", "PUBLIC"."TEST2COL"."B", "PUBLIC"."TEST2COL"."C" FROM "PUBLIC"."TEST2COL" /* PUBLIC.PRIMARY_KEY_E: ROW (0, 0) */ WHERE ROW ("A", "B") = (0, 0) +-- TODO:kiss034: This should be fixed later. SELECT * FROM TEST2COL WHERE A=0; > A B C From 0dbfc32bd34016b392acbf8460305e1646d42c89 Mon Sep 17 00:00:00 2001 From: kiss034 Date: Tue, 14 Nov 2023 11:36:44 +0100 Subject: [PATCH 05/12] Fixing TableFilter.prepare(). --- h2/src/main/org/h2/table/TableFilter.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/h2/src/main/org/h2/table/TableFilter.java b/h2/src/main/org/h2/table/TableFilter.java index 4b1091ae16..0344df232b 100644 --- a/h2/src/main/org/h2/table/TableFilter.java +++ b/h2/src/main/org/h2/table/TableFilter.java @@ -352,9 +352,13 @@ public void prepare() { } else { Column col = condition.getColumn(); if (col.getColumnId() >= 0) { - if (index.getColumnIndex(col) != 0) { - // If this is a simple index, then the column index must be 0 on a match. - // If this is a compound index, we can use it only if the first index column is the searched one. + int columnIndex = index.getColumnIndex(col); + if (columnIndex == 0) // The first column of the index always matches. + continue; + int compareType = condition.getCompareType(); + if (columnIndex < 0 || compareType == Comparison.IN_LIST ) { + // The index does not contain the column, or this is an IN() condition which can be used + // only if the first index column is the searched one. // See: IndexCursor#canUseIndexFor(column) indexConditions.remove(i); i--; From 1b9cb56568c53c94f85915b5d218780963ede0e5 Mon Sep 17 00:00:00 2001 From: kiss034 Date: Tue, 14 Nov 2023 11:37:57 +0100 Subject: [PATCH 06/12] Adjusting test cases. --- h2/src/test/org/h2/test/scripts/indexes.sql | 12 ++++-------- .../h2/test/scripts/queries/query-optimisations.sql | 3 +-- h2/src/test/org/h2/test/scripts/testScript.sql | 7 ++----- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/h2/src/test/org/h2/test/scripts/indexes.sql b/h2/src/test/org/h2/test/scripts/indexes.sql index 21f2af3e67..44363d5693 100644 --- a/h2/src/test/org/h2/test/scripts/indexes.sql +++ b/h2/src/test/org/h2/test/scripts/indexes.sql @@ -321,12 +321,10 @@ EXPLAIN SELECT * FROM TEST WHERE A = 0; >> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A1: A = 0 */ WHERE "A" = 0 EXPLAIN SELECT * FROM TEST WHERE A = 0 AND B >= 0; ->> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A_B: A = 0 */ WHERE ("A" = 0) AND ("B" >= 0) --- B is not the first column of the T_A_B index, so it cannot be used as an index condition. +>> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A_B: A = 0 AND B >= 0 */ WHERE ("A" = 0) AND ("B" >= 0) EXPLAIN SELECT * FROM TEST WHERE A > 0 AND B >= 0; ->> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A_B: A > 0 */ WHERE ("A" > 0) AND ("B" >= 0) --- B is not the first column of the T_A_B index, so it cannot be used as an index condition. +>> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A_B: A > 0 AND B >= 0 */ WHERE ("A" > 0) AND ("B" >= 0) INSERT INTO TEST (SELECT X / 100, X, X FROM SYSTEM_RANGE(1, 3000)); > update count: 3000 @@ -335,12 +333,10 @@ EXPLAIN SELECT * FROM TEST WHERE A = 0; >> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A1: A = 0 */ WHERE "A" = 0 EXPLAIN SELECT * FROM TEST WHERE A = 0 AND B >= 0; ->> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A_B: A = 0 */ WHERE ("A" = 0) AND ("B" >= 0) --- B is not the first column of the T_A_B index, so it cannot be used as an index condition. +>> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A_B: A = 0 AND B >= 0 */ WHERE ("A" = 0) AND ("B" >= 0) EXPLAIN SELECT * FROM TEST WHERE A > 0 AND B >= 0; ->> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A_B: A > 0 */ WHERE ("A" > 0) AND ("B" >= 0) --- B is not the first column of the T_A_B index, so it cannot be used as an index condition. +>> SELECT "PUBLIC"."TEST"."A", "PUBLIC"."TEST"."B", "PUBLIC"."TEST"."C" FROM "PUBLIC"."TEST" /* PUBLIC.T_A_B: A > 0 AND B >= 0 */ WHERE ("A" > 0) AND ("B" >= 0) -- Test that creation order of indexes has no effect CREATE INDEX T_A2 ON TEST(A); diff --git a/h2/src/test/org/h2/test/scripts/queries/query-optimisations.sql b/h2/src/test/org/h2/test/scripts/queries/query-optimisations.sql index 41633fd2d7..0acb2e2e99 100644 --- a/h2/src/test/org/h2/test/scripts/queries/query-optimisations.sql +++ b/h2/src/test/org/h2/test/scripts/queries/query-optimisations.sql @@ -16,8 +16,7 @@ insert into person select convert(x,varchar) as firstname, (convert(x,varchar) | -- can directly use the index. -- explain analyze SELECT * FROM person WHERE firstname IN ('FirstName1', 'FirstName2') AND lastname='LastName1'; ->> SELECT "PUBLIC"."PERSON"."FIRSTNAME", "PUBLIC"."PERSON"."LASTNAME" FROM "PUBLIC"."PERSON" /* PUBLIC.PERSON_1: FIRSTNAME IN('FirstName1', 'FirstName2') */ /* scanCount: 1 */ WHERE ("FIRSTNAME" IN('FirstName1', 'FirstName2')) AND ("LASTNAME" = 'LastName1') --- lastname is neither indexed per se, nor the first column of the person_1 index, so it cannot be used as an index condition. +>> SELECT "PUBLIC"."PERSON"."FIRSTNAME", "PUBLIC"."PERSON"."LASTNAME" FROM "PUBLIC"."PERSON" /* PUBLIC.PERSON_1: FIRSTNAME IN('FirstName1', 'FirstName2') AND LASTNAME = 'LastName1' */ /* scanCount: 1 */ WHERE ("FIRSTNAME" IN('FirstName1', 'FirstName2')) AND ("LASTNAME" = 'LastName1') CREATE TABLE TEST(A SMALLINT PRIMARY KEY, B SMALLINT); > ok diff --git a/h2/src/test/org/h2/test/scripts/testScript.sql b/h2/src/test/org/h2/test/scripts/testScript.sql index ec5fe93edb..a58d966176 100644 --- a/h2/src/test/org/h2/test/scripts/testScript.sql +++ b/h2/src/test/org/h2/test/scripts/testScript.sql @@ -1531,8 +1531,7 @@ EXPLAIN SELECT * FROM TEST WHERE FLAG; >> SELECT "PUBLIC"."TEST"."ID", "PUBLIC"."TEST"."FLAG", "PUBLIC"."TEST"."NAME" FROM "PUBLIC"."TEST" /* PUBLIC.IDX_FLAG: FLAG = TRUE */ WHERE "FLAG" EXPLAIN SELECT * FROM TEST WHERE FLAG AND NAME>'I'; ->> SELECT "PUBLIC"."TEST"."ID", "PUBLIC"."TEST"."FLAG", "PUBLIC"."TEST"."NAME" FROM "PUBLIC"."TEST" /* PUBLIC.IDX_FLAG: FLAG = TRUE */ WHERE "FLAG" AND ("NAME" > 'I') --- NAME is neither indexed per se, nor the first column of the IDX_FLAG index, so it cannot be used as an index condition. +>> SELECT "PUBLIC"."TEST"."ID", "PUBLIC"."TEST"."FLAG", "PUBLIC"."TEST"."NAME" FROM "PUBLIC"."TEST" /* PUBLIC.IDX_FLAG: FLAG = TRUE AND NAME > 'I' */ WHERE "FLAG" AND ("NAME" > 'I') DROP TABLE TEST; > ok @@ -4424,11 +4423,9 @@ SELECT * FROM TEST2COL WHERE A=0 AND B=0; EXPLAIN SELECT * FROM TEST2COL WHERE A=0 AND B=0; >> SELECT "PUBLIC"."TEST2COL"."A", "PUBLIC"."TEST2COL"."B", "PUBLIC"."TEST2COL"."C" FROM "PUBLIC"."TEST2COL" /* PUBLIC.PRIMARY_KEY_E: A = 0 AND B = 0 */ WHERE ("A" = 0) AND ("B" = 0) --- B is neither indexed per se, nor the first column of the PRIMARY KEY, so it cannot be used as an index condition. EXPLAIN SELECT * FROM TEST2COL WHERE (A, B)=(0, 0); ->> SELECT "PUBLIC"."TEST2COL"."A", "PUBLIC"."TEST2COL"."B", "PUBLIC"."TEST2COL"."C" FROM "PUBLIC"."TEST2COL" /* PUBLIC.PRIMARY_KEY_E: ROW (0, 0) */ WHERE ROW ("A", "B") = (0, 0) --- TODO:kiss034: This should be fixed later. +>> SELECT "PUBLIC"."TEST2COL"."A", "PUBLIC"."TEST2COL"."B", "PUBLIC"."TEST2COL"."C" FROM "PUBLIC"."TEST2COL" /* PUBLIC.PRIMARY_KEY_E: A = 0 AND B = 0 */ WHERE ROW ("A", "B") = ROW (0, 0) SELECT * FROM TEST2COL WHERE A=0; > A B C From 5b80dd9ec5fd350c32a628784e355f24dc0b425c Mon Sep 17 00:00:00 2001 From: kiss034 Date: Wed, 15 Nov 2023 12:02:16 +0100 Subject: [PATCH 07/12] Refactoring the constructors of the IndexCondition class. --- h2/src/main/org/h2/index/IndexCondition.java | 73 ++++++++------------ 1 file changed, 29 insertions(+), 44 deletions(-) diff --git a/h2/src/main/org/h2/index/IndexCondition.java b/h2/src/main/org/h2/index/IndexCondition.java index 52201f5eaf..9efca6a677 100644 --- a/h2/src/main/org/h2/index/IndexCondition.java +++ b/h2/src/main/org/h2/index/IndexCondition.java @@ -26,7 +26,7 @@ import org.h2.value.ValueRow; /** - * A index condition object is made for each condition that can potentially use + * An index condition object is made for each condition that can potentially use * an index. This class does not extend expression, but in general there is one * expression that maps to each index condition. * @@ -76,70 +76,61 @@ public class IndexCondition { private final int compareType; private final Expression expression; - private List expressionList; - private Query expressionQuery; + private final List expressionList; + private final Query expressionQuery; /** * @param compareType the comparison type, see constants in * {@link Comparison} */ - private IndexCondition(int compareType, ExpressionColumn column, - Expression expression) { - this.compareType = compareType; - this.column = column == null ? null : column.getColumn(); - this.columns = null; - this.compoundColumns = false; - this.expression = expression; - } + private IndexCondition(int compareType, ExpressionColumn column, ExpressionList columns, Expression expression, + List list, Query query) { - /** - * @param compareType the comparison type, see constants in {@link Comparison} - */ - private IndexCondition(int compareType, ExpressionList columns, Expression expression) { this.compareType = compareType; - this.column = null; - if (columns == null) { + if (column != null) { + this.column = column.getColumn(); this.columns = null; - } else { + this.compoundColumns = false; + } else if (columns !=null) { + this.column = null; int listSize = columns.getSubexpressionCount(); Column[] result = new Column[listSize]; for (int i = listSize; --i >= 0; ) { result[i] = ((ExpressionColumn) columns.getSubexpression(i)).getColumn(); } this.columns = result; + this.compoundColumns = true; + } else { + this.column = null; + this.columns = null; + this.compoundColumns = false; } - this.compoundColumns = true; this.expression = expression; + this.expressionList = list; + this.expressionQuery = query; } /** * Create an index condition with the given parameters. * - * @param compareType the comparison type, see constants in - * {@link Comparison} + * @param compareType the comparison type, see constants in {@link Comparison} * @param column the column * @param expression the expression * @return the index condition */ - public static IndexCondition get(int compareType, ExpressionColumn column, - Expression expression) { - return new IndexCondition(compareType, column, expression); + public static IndexCondition get(int compareType, ExpressionColumn column, Expression expression) { + return new IndexCondition(compareType, column, null, expression, null, null); } /** - * Create an index condition with the compare type IN_LIST and with the - * given parameters. + * Create an index condition with the compare type IN_LIST and with the given parameters. * * @param column the column * @param list the expression list * @return the index condition */ - public static IndexCondition getInList(ExpressionColumn column, - List list) { - IndexCondition cond = new IndexCondition(Comparison.IN_LIST, column, - null); - cond.expressionList = list; - return cond; + public static IndexCondition getInList(ExpressionColumn column, List list) { + return new IndexCondition(Comparison.IN_LIST, column, null, null, list, null); } /** @@ -150,26 +141,22 @@ public static IndexCondition getInList(ExpressionColumn column, * @return the index condition */ public static IndexCondition getCompoundInList(ExpressionList columns, List list) { - IndexCondition cond = new IndexCondition(Comparison.IN_LIST, columns, null); - cond.expressionList = list; - return cond; + return new IndexCondition(Comparison.IN_LIST, null, columns, null, list, null); } /** - * Create an index condition with the compare type IN_ARRAY and with the - * given parameters. + * Create an index condition with the compare type IN_ARRAY and with the given parameters. * * @param column the column * @param array the array * @return the index condition */ public static IndexCondition getInArray(ExpressionColumn column, Expression array) { - return new IndexCondition(Comparison.IN_ARRAY, column, array); + return new IndexCondition(Comparison.IN_ARRAY, column, null, array, null, null); } /** - * Create an index condition with the compare type IN_QUERY and with the - * given parameters. + * Create an index condition with the compare type IN_QUERY and with the given parameters. * * @param column the column * @param query the select statement @@ -177,9 +164,7 @@ public static IndexCondition getInArray(ExpressionColumn column, Expression arra */ public static IndexCondition getInQuery(ExpressionColumn column, Query query) { assert query.isRandomAccessResult(); - IndexCondition cond = new IndexCondition(Comparison.IN_QUERY, column, null); - cond.expressionQuery = query; - return cond; + return new IndexCondition(Comparison.IN_QUERY, column, null, null, null, query); } /** @@ -400,7 +385,7 @@ public boolean isStart() { * Check if this index condition is of the type column smaller or equal to * value. * - * @return true if this is a end condition + * @return true if this is an end condition */ public boolean isEnd() { switch (compareType) { From e7b8be5b1163aaf17ff16e2386d94fc427c8dafa Mon Sep 17 00:00:00 2001 From: kiss034 Date: Wed, 15 Nov 2023 12:23:41 +0100 Subject: [PATCH 08/12] Removing unnecessary TODOs. --- .../org/h2/test/scripts/compatibility/compatibility.sql | 1 - h2/src/test/org/h2/test/scripts/predicates/null.sql | 6 ------ .../org/h2/test/scripts/queries/query-optimisations.sql | 3 --- 3 files changed, 10 deletions(-) diff --git a/h2/src/test/org/h2/test/scripts/compatibility/compatibility.sql b/h2/src/test/org/h2/test/scripts/compatibility/compatibility.sql index 9c39bd1362..ad9294fcfa 100644 --- a/h2/src/test/org/h2/test/scripts/compatibility/compatibility.sql +++ b/h2/src/test/org/h2/test/scripts/compatibility/compatibility.sql @@ -779,7 +779,6 @@ INSERT INTO TEST (SELECT X, X + 1, X + 2 FROM SYSTEM_RANGE(1, 5)); EXPLAIN UPDATE TEST T SET V = V.V FROM (VALUES (1, 2, 4)) V(ID1, ID2, V) WHERE (T.ID1, T.ID2) = (V.ID1, V.ID2); >> MERGE INTO "PUBLIC"."TEST" "T" /* PUBLIC.PRIMARY_KEY_2: ID1 = V.ID1 AND ID2 = V.ID2 */ USING (VALUES (1, 2, 4)) "V"("ID1", "ID2", "V") /* table scan */ ON (ROW ("T"."ID1", "T"."ID2") = ROW ("V"."ID1", "V"."ID2")) WHEN MATCHED THEN UPDATE SET "V" = "V"."V" --- TODO:kiss034: This should be fixed later. UPDATE TEST T SET V = V.V FROM (VALUES (1, 2, 4)) V(ID1, ID2, V) WHERE (T.ID1, T.ID2) = (V.ID1, V.ID2); > update count: 1 diff --git a/h2/src/test/org/h2/test/scripts/predicates/null.sql b/h2/src/test/org/h2/test/scripts/predicates/null.sql index 2f542b5550..1f76166b9f 100644 --- a/h2/src/test/org/h2/test/scripts/predicates/null.sql +++ b/h2/src/test/org/h2/test/scripts/predicates/null.sql @@ -136,7 +136,6 @@ SELECT * FROM TEST T1 JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T EXPLAIN SELECT * FROM TEST T1 JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T2.B) IS NULL; >> SELECT "T1"."A", "T1"."B", "T2"."A", "T2"."B" FROM "PUBLIC"."TEST" "T2" /* PUBLIC.TEST_A_B_IDX: A IS NULL AND B IS NULL */ /* WHERE ROW (T2.A, T2.B) IS NULL */ INNER JOIN "PUBLIC"."TEST" "T1" /* PUBLIC.TEST_A_B_IDX: A = T2.A AND B = T2.B */ ON 1=1 WHERE (ROW ("T2"."A", "T2"."B") IS NULL) AND (ROW ("T1"."A", "T1"."B") = ROW ("T2"."A", "T2"."B")) --- TODO:kiss034: This should be fixed later. SELECT * FROM TEST T1 LEFT JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T2.B) IS NULL; > A B A B @@ -148,7 +147,6 @@ SELECT * FROM TEST T1 LEFT JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2 EXPLAIN SELECT * FROM TEST T1 LEFT JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T2.B) IS NULL; >> SELECT "T1"."A", "T1"."B", "T2"."A", "T2"."B" FROM "PUBLIC"."TEST" "T1" /* PUBLIC.TEST_A_B_IDX */ LEFT OUTER JOIN "PUBLIC"."TEST" "T2" /* PUBLIC.TEST_A_B_IDX: A = T1.A AND B = T1.B */ ON ROW ("T1"."A", "T1"."B") = ROW ("T2"."A", "T2"."B") WHERE ROW ("T2"."A", "T2"."B") IS NULL --- TODO:kiss034: This should be fixed later. SELECT * FROM TEST T1 JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T2.B) IS NOT NULL; > A B A B @@ -158,7 +156,6 @@ SELECT * FROM TEST T1 JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T EXPLAIN SELECT * FROM TEST T1 JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T2.B) IS NOT NULL; >> SELECT "T1"."A", "T1"."B", "T2"."A", "T2"."B" FROM "PUBLIC"."TEST" "T1" /* PUBLIC.TEST_A_B_IDX */ INNER JOIN "PUBLIC"."TEST" "T2" /* PUBLIC.TEST_A_B_IDX: A = T1.A AND B = T1.B */ ON 1=1 WHERE (ROW ("T2"."A", "T2"."B") IS NOT NULL) AND (ROW ("T1"."A", "T1"."B") = ROW ("T2"."A", "T2"."B")) --- TODO:kiss034: This should be fixed later. SELECT * FROM TEST T1 LEFT JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T2.B) IS NOT NULL; > A B A B @@ -168,7 +165,6 @@ SELECT * FROM TEST T1 LEFT JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2 EXPLAIN SELECT * FROM TEST T1 LEFT JOIN TEST T2 ON (T1.A, T1.B) = (T2.A, T2.B) WHERE (T2.A, T2.B) IS NOT NULL; >> SELECT "T1"."A", "T1"."B", "T2"."A", "T2"."B" FROM "PUBLIC"."TEST" "T1" /* PUBLIC.TEST_A_B_IDX */ LEFT OUTER JOIN "PUBLIC"."TEST" "T2" /* PUBLIC.TEST_A_B_IDX: A = T1.A AND B = T1.B */ ON ROW ("T1"."A", "T1"."B") = ROW ("T2"."A", "T2"."B") WHERE ROW ("T2"."A", "T2"."B") IS NOT NULL --- TODO:kiss034: This should be fixed later. EXPLAIN SELECT A, B FROM TEST WHERE (A, NULL) IS NULL; >> SELECT "A", "B" FROM "PUBLIC"."TEST" /* PUBLIC.TEST_A_B_IDX: A IS NULL */ WHERE "A" IS NULL @@ -184,11 +180,9 @@ EXPLAIN SELECT A, B FROM TEST WHERE NOT (A, NULL) IS NOT NULL; EXPLAIN SELECT A, B FROM TEST WHERE (A, NULL, B) IS NULL; >> SELECT "A", "B" FROM "PUBLIC"."TEST" /* PUBLIC.TEST_A_B_IDX: A IS NULL AND B IS NULL */ WHERE ROW ("A", "B") IS NULL --- TODO:kiss034: This should be fixed later. EXPLAIN SELECT A, B FROM TEST WHERE (A, NULL, B, NULL) IS NULL; >> SELECT "A", "B" FROM "PUBLIC"."TEST" /* PUBLIC.TEST_A_B_IDX: A IS NULL AND B IS NULL */ WHERE ROW ("A", "B") IS NULL --- TODO:kiss034: This should be fixed later. DROP TABLE TEST; > ok diff --git a/h2/src/test/org/h2/test/scripts/queries/query-optimisations.sql b/h2/src/test/org/h2/test/scripts/queries/query-optimisations.sql index 0acb2e2e99..87de62c705 100644 --- a/h2/src/test/org/h2/test/scripts/queries/query-optimisations.sql +++ b/h2/src/test/org/h2/test/scripts/queries/query-optimisations.sql @@ -111,13 +111,11 @@ SELECT T1.ID, T2.V AS LV FROM (SELECT ID, MAX(V) AS LV FROM T GROUP BY ID) AS T1 > 1 2 > 2 2 > rows (ordered): 2 --- TODO:kiss034: This should be fixed later. EXPLAIN SELECT T1.ID, T2.V AS LV FROM (SELECT ID, MAX(V) AS LV FROM T GROUP BY ID) AS T1 INNER JOIN T AS T2 ON T2.ID = T1.ID AND T2.V = T1.LV WHERE T1.ID IN (1, 2) ORDER BY ID; >> SELECT "T1"."ID", "T2"."V" AS "LV" FROM "PUBLIC"."T" "T2" /* PUBLIC.T.tableScan */ INNER JOIN ( SELECT "ID", MAX("V") AS "LV" FROM "PUBLIC"."T" GROUP BY "ID" ) "T1" /* SELECT ID, MAX(V) AS LV FROM PUBLIC.T /* PUBLIC.T.tableScan */ WHERE ID IS NOT DISTINCT FROM ?1 GROUP BY ID HAVING MAX(V) IS NOT DISTINCT FROM ?2: ID = T2.ID AND LV = T2.V */ ON 1=1 WHERE ("T1"."ID" IN(1, 2)) AND ("T2"."ID" = "T1"."ID") AND ("T2"."V" = "T1"."LV") ORDER BY 1 --- TODO:kiss034: This should be fixed later. DROP TABLE T; > ok @@ -296,7 +294,6 @@ CREATE INDEX T1_A_B_IDX ON T1(A, B); EXPLAIN SELECT * FROM T1 WHERE (A, B) = (1, 2); >> SELECT "PUBLIC"."T1"."A", "PUBLIC"."T1"."B" FROM "PUBLIC"."T1" /* PUBLIC.T1_A_B_IDX: A = 1 AND B = 2 */ WHERE ROW ("A", "B") = ROW (1, 2) --- TODO:kiss034: This should be fixed later. EXPLAIN SELECT * FROM T1 WHERE (A, B) > (1, 2); >> SELECT "PUBLIC"."T1"."A", "PUBLIC"."T1"."B" FROM "PUBLIC"."T1" /* PUBLIC.T1_A_B_IDX: A >= 1 */ WHERE ROW ("A", "B") > ROW (1, 2) From 89363a2cdad2fc78b9a3cc0e49c866e9f4318d57 Mon Sep 17 00:00:00 2001 From: kiss034 Date: Thu, 16 Nov 2023 17:42:24 +0100 Subject: [PATCH 09/12] Fixing review findings. --- .../main/org/h2/expression/condition/ConditionIn.java | 3 ++- .../expression/condition/ConditionInConstantSet.java | 3 ++- h2/src/main/org/h2/index/IndexCondition.java | 11 ++++++++--- h2/src/main/org/h2/index/IndexCursor.java | 6 ++++-- h2/src/main/org/h2/table/Column.java | 6 +++++- h2/src/main/org/h2/table/TableFilter.java | 10 ++++++---- h2/src/test/org/h2/test/TestAll.java | 2 ++ h2/src/test/org/h2/test/scripts/testScript.sql | 3 --- 8 files changed, 29 insertions(+), 15 deletions(-) diff --git a/h2/src/main/org/h2/expression/condition/ConditionIn.java b/h2/src/main/org/h2/expression/condition/ConditionIn.java index c7f67b4652..c7d6be4811 100644 --- a/h2/src/main/org/h2/expression/condition/ConditionIn.java +++ b/h2/src/main/org/h2/expression/condition/ConditionIn.java @@ -186,8 +186,9 @@ private void createCompoundIndexCondition(TableFilter filter) { // We do not check filter here, because the IN condition can contain columns from multiple tables. ExpressionVisitor visitor = ExpressionVisitor.getNotFromResolverVisitor(filter); for (Expression e : valueList) { - if (!e.isEverything(visitor)) + if (!e.isEverything(visitor)) { return; + } } filter.addIndexCondition(IndexCondition.getCompoundInList((ExpressionList) left, valueList)); } diff --git a/h2/src/main/org/h2/expression/condition/ConditionInConstantSet.java b/h2/src/main/org/h2/expression/condition/ConditionInConstantSet.java index 2a41c46c0c..84266886ac 100644 --- a/h2/src/main/org/h2/expression/condition/ConditionInConstantSet.java +++ b/h2/src/main/org/h2/expression/condition/ConditionInConstantSet.java @@ -159,8 +159,9 @@ private void createCompoundIndexCondition(TableFilter filter) { // We do not check filter here, because the IN condition can contain columns from multiple tables. ExpressionVisitor visitor = ExpressionVisitor.getNotFromResolverVisitor(filter); for (Expression e : valueList) { - if (!e.isEverything(visitor)) + if (!e.isEverything(visitor)) { return; + } } filter.addIndexCondition(IndexCondition.getCompoundInList((ExpressionList) left, valueList)); } diff --git a/h2/src/main/org/h2/index/IndexCondition.java b/h2/src/main/org/h2/index/IndexCondition.java index 9efca6a677..04672d329d 100644 --- a/h2/src/main/org/h2/index/IndexCondition.java +++ b/h2/src/main/org/h2/index/IndexCondition.java @@ -25,6 +25,8 @@ import org.h2.value.ValueArray; import org.h2.value.ValueRow; +import static org.h2.util.HasSQL.TRACE_SQL_FLAGS; + /** * An index condition object is made for each condition that can potentially use * an index. This class does not extend expression, but in general there is one @@ -247,8 +249,9 @@ private StringBuilder buildSql(int sqlFlags, StringBuilder builder) { if (compareType == Comparison.IN_LIST) { builder.append(" IN("); for (int i = 0, s = expressionList.size(); i < s; i++) { - if (i > 0) + if (i > 0) { builder.append(", "); + } builder.append(expressionList.get(i).getSQL(sqlFlags)); } return builder.append(')'); @@ -324,8 +327,9 @@ public int getMask(ArrayList indexConditions) { if (isCompoundColumns()) { Column[] columns = getColumns(); for (int i = columns.length; --i >= 0; ) { - if (TableType.TABLE != columns[i].getTable().getTableType()) + if (TableType.TABLE != columns[i].getTable().getTableType()) { return 0; + } } } else if (TableType.TABLE != getColumn().getTable().getTableType()) { @@ -508,7 +512,8 @@ public String toString() { if (!isCompoundColumns()) { builder.append("column=").append(column); } else { - builder.append("columns=").append(columns); + builder.append("columns="); + Column.writeColumns(builder, columns, TRACE_SQL_FLAGS); } builder.append(", compareType="); return compareTypeToString(builder, compareType) diff --git a/h2/src/main/org/h2/index/IndexCursor.java b/h2/src/main/org/h2/index/IndexCursor.java index f073503b57..6301d6f8e1 100644 --- a/h2/src/main/org/h2/index/IndexCursor.java +++ b/h2/src/main/org/h2/index/IndexCursor.java @@ -219,12 +219,14 @@ private boolean canUseIndexForIn(Column[] columns) { // Multiple IN conditions with views are not supported, see // IndexCondition.getMask. IndexColumn[] cols = index.getIndexColumns(); - if (cols == null || cols.length != columns.length) + if (cols == null || cols.length != columns.length) { return true; + } for (int i = 0; i < cols.length; i++) { IndexColumn idxCol = cols[i]; - if (idxCol != null && idxCol.column != columns[i]) + if (idxCol != null && idxCol.column != columns[i]) { return false; + } } return true; } diff --git a/h2/src/main/org/h2/table/Column.java b/h2/src/main/org/h2/table/Column.java index 8bbb4c4a13..9340254e2c 100644 --- a/h2/src/main/org/h2/table/Column.java +++ b/h2/src/main/org/h2/table/Column.java @@ -26,6 +26,7 @@ import org.h2.util.HasSQL; import org.h2.util.ParserUtil; import org.h2.util.StringUtils; +import org.h2.value.ExtTypeInfoRow; import org.h2.value.TypeInfo; import org.h2.value.Typed; import org.h2.value.Value; @@ -213,7 +214,10 @@ public static ValueRow convert(CastDataProvider provider, Column[] columns, Valu copy[i] = nv; } } - return copy == null ? valueRow : ValueRow.get(valueRow.getType(), copy); + if (copy == null) + return valueRow; + TypeInfo typeInfo = TypeInfo.getTypeInfo(Value.ROW, 0, 0, new ExtTypeInfoRow(columns)); + return ValueRow.get(typeInfo, copy); } /** diff --git a/h2/src/main/org/h2/table/TableFilter.java b/h2/src/main/org/h2/table/TableFilter.java index 0344df232b..6ae4a2cde3 100644 --- a/h2/src/main/org/h2/table/TableFilter.java +++ b/h2/src/main/org/h2/table/TableFilter.java @@ -342,8 +342,9 @@ public void prepare() { for (int j = 0; j < columns.length; j++) { Column col = columns[j]; indexed = col.getColumnId() >= 0 && index.getColumnIndex(col) >= 0; - if (!indexed) + if (!indexed) { break; + } } if (!indexed) { // Not all columns are indexed so removing the current condition indexConditions.remove(i); @@ -353,10 +354,11 @@ public void prepare() { Column col = condition.getColumn(); if (col.getColumnId() >= 0) { int columnIndex = index.getColumnIndex(col); - if (columnIndex == 0) // The first column of the index always matches. + if (columnIndex == 0) { + // The first column of the index always matches. continue; - int compareType = condition.getCompareType(); - if (columnIndex < 0 || compareType == Comparison.IN_LIST ) { + } + if (columnIndex < 0 || condition.getCompareType() == Comparison.IN_LIST ) { // The index does not contain the column, or this is an IN() condition which can be used // only if the first index column is the searched one. // See: IndexCursor#canUseIndexFor(column) diff --git a/h2/src/test/org/h2/test/TestAll.java b/h2/src/test/org/h2/test/TestAll.java index 0153a7e2b1..cdb849b6f2 100644 --- a/h2/src/test/org/h2/test/TestAll.java +++ b/h2/src/test/org/h2/test/TestAll.java @@ -35,6 +35,7 @@ import org.h2.test.db.TestCompatibility; import org.h2.test.db.TestCompatibilityOracle; import org.h2.test.db.TestCompatibilitySQLServer; +import org.h2.test.db.TestCompoundIndexSearch; import org.h2.test.db.TestCsv; import org.h2.test.db.TestDateStorage; import org.h2.test.db.TestDeadlock; @@ -732,6 +733,7 @@ private void test() throws SQLException { addTest(new TestInit()); addTest(new TestIndex()); addTest(new TestIndexHints()); + addTest(new TestCompoundIndexSearch()); addTest(new TestLargeBlob()); addTest(new TestLinkedTable()); addTest(new TestListener()); diff --git a/h2/src/test/org/h2/test/scripts/testScript.sql b/h2/src/test/org/h2/test/scripts/testScript.sql index a58d966176..bbce5b403e 100644 --- a/h2/src/test/org/h2/test/scripts/testScript.sql +++ b/h2/src/test/org/h2/test/scripts/testScript.sql @@ -4424,9 +4424,6 @@ SELECT * FROM TEST2COL WHERE A=0 AND B=0; EXPLAIN SELECT * FROM TEST2COL WHERE A=0 AND B=0; >> SELECT "PUBLIC"."TEST2COL"."A", "PUBLIC"."TEST2COL"."B", "PUBLIC"."TEST2COL"."C" FROM "PUBLIC"."TEST2COL" /* PUBLIC.PRIMARY_KEY_E: A = 0 AND B = 0 */ WHERE ("A" = 0) AND ("B" = 0) -EXPLAIN SELECT * FROM TEST2COL WHERE (A, B)=(0, 0); ->> SELECT "PUBLIC"."TEST2COL"."A", "PUBLIC"."TEST2COL"."B", "PUBLIC"."TEST2COL"."C" FROM "PUBLIC"."TEST2COL" /* PUBLIC.PRIMARY_KEY_E: A = 0 AND B = 0 */ WHERE ROW ("A", "B") = ROW (0, 0) - SELECT * FROM TEST2COL WHERE A=0; > A B C > - - ----- From a8ba33ac42cc2702efa80070815ae2776633333e Mon Sep 17 00:00:00 2001 From: kiss034 Date: Thu, 16 Nov 2023 17:43:28 +0100 Subject: [PATCH 10/12] Introducing the TestCompoundIndexSearch class. --- .../h2/test/db/TestCompoundIndexSearch.java | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 h2/src/test/org/h2/test/db/TestCompoundIndexSearch.java diff --git a/h2/src/test/org/h2/test/db/TestCompoundIndexSearch.java b/h2/src/test/org/h2/test/db/TestCompoundIndexSearch.java new file mode 100644 index 0000000000..c73a448a08 --- /dev/null +++ b/h2/src/test/org/h2/test/db/TestCompoundIndexSearch.java @@ -0,0 +1,150 @@ +/* + * Copyright 2004-2023 H2 Group. Multiple-Licensed under the MPL 2.0, + * and the EPL 1.0 (https://h2database.com/html/license.html). + * Initial Developer: H2 Group + */ +package org.h2.test.db; + +import org.h2.test.TestBase; +import org.h2.test.TestDb; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; + +/** + * Test various queries against compound indexes. + */ +public class TestCompoundIndexSearch extends TestDb { + + private static final String DB_NAME = "compoundIndexSearch"; + + /** + * Run just this test. + * + * @param a ignored + */ + public static void main(String... a) throws Exception { + TestBase.createCaller().init().testFromMain(); + } + + @Override + public void test() throws Exception { + Connection conn = prepare(); + + simpleInAgainstSimpleIndexCheck(conn); + simpleInAgainstFirstCompoundIndex(conn); + simpleInAgainstSecondCompoundIndex(conn); + compoundInAgainstCompoundIndex(conn); + compoundEqAgainstCompoundIndex(conn); + multipleEqAgainstCompoundIndex(conn); + + conn.close(); + deleteDb(DB_NAME); + } + + private Connection prepare() throws Exception { + deleteDb(DB_NAME); + Connection conn = getConnection(DB_NAME); + Statement stat = conn.createStatement(); + stat.execute("CREATE TABLE test (a INT, b INT, c INT, d INT);"); + stat.execute("CREATE INDEX idx_a ON test(a);"); + stat.execute("CREATE INDEX idx_b_c ON test(b, c);"); + stat.execute("INSERT INTO test (a, b, c, d) VALUES " + + "(1, 1, 1, 1), " + + "(1, 1, 2, 2), " + + "(1, 3, 3, 3), " + + "(2, 2, 1, 4), " + + "(2, 3, 2, 1), " + + "(2, 3, 3, 2), " + + "(3, 2, 1, 3), " + + "(3, 2, 2, 4), " + + "(3, 3, 3, 1), " + + "(4, 1, 1, 2);" + ); + stat.close(); + return conn; + } + + /** + * Executes a query with a simple IN condition against an indexed column. + */ + private void simpleInAgainstSimpleIndexCheck(Connection conn) throws Exception { + Statement stat = conn.createStatement(); + ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT b, c FROM test WHERE (a) IN (1, 4)"); + rs.next(); + assertEquals(rs.getString(1).replaceAll("[\\r\\n\\s]+", " "), + "SELECT \"B\", \"C\" FROM \"PUBLIC\".\"TEST\" /* PUBLIC.IDX_A: A IN(1, 4) */ " + + "/* scanCount: 5 */ WHERE \"A\" IN(1, 4)"); + stat.close(); + } + + /** + * Executes a query with a simple IN condition against a compound index. The lookup column is the first component + * of the index, so the lookup works as it was a simple index. + */ + private void simpleInAgainstFirstCompoundIndex(Connection conn) throws Exception { + Statement stat = conn.createStatement(); + ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT b, c FROM test WHERE b IN (1, 2)"); + rs.next(); + assertContains(rs.getString(1).replaceAll("[\\r\\n\\s]+", " "), + "SELECT \"B\", \"C\" FROM \"PUBLIC\".\"TEST\" /* PUBLIC.IDX_B_C: B IN(1, 2) */ " + + "/* scanCount: 7 */ WHERE \"B\" IN(1, 2)"); + stat.close(); + } + + /** + * Executes a query with a simple IN condition against a compound index. The lookup column is the second component + * of the index, so a full table scan happens. + */ + private void simpleInAgainstSecondCompoundIndex(Connection conn) throws Exception { + Statement stat = conn.createStatement(); + ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT b, c FROM test WHERE c IN (1, 2)"); + rs.next(); + assertContains(rs.getString(1).replaceAll("[\\r\\n\\s]+", " "), + "SELECT \"B\", \"C\" FROM \"PUBLIC\".\"TEST\" /* PUBLIC.IDX_B_C */ " + + "/* scanCount: 11 */ WHERE \"C\" IN(1, 2)"); + stat.close(); + } + + /** + * Executes a query with a compound IN condition against a compound index. + */ + private void compoundInAgainstCompoundIndex(Connection conn) throws Exception { + Statement stat = conn.createStatement(); + ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT b, c FROM test WHERE (b, c) IN ((1, 1), (2, 2))"); + rs.next(); + assertContains(rs.getString(1).replaceAll("[\\r\\n\\s]+", " "), + "SELECT \"B\", \"C\" FROM \"PUBLIC\".\"TEST\" " + + "/* PUBLIC.IDX_B_C: IN(ROW (1, 1), ROW (2, 2)) AND B IN(1, 2) */ " + + "/* scanCount: 4 */ WHERE ROW (\"B\", \"C\") IN(ROW (1, 1), ROW (2, 2))"); + stat.close(); + } + + /** + * Executes a query with a compound EQ condition against a compound index. + */ + private void compoundEqAgainstCompoundIndex(Connection conn) throws Exception { + Statement stat = conn.createStatement(); + ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT b, c FROM test WHERE (b, c) = (1, 1)"); + rs.next(); + assertContains(rs.getString(1).replaceAll("[\\r\\n\\s]+", " "), + "SELECT \"B\", \"C\" FROM \"PUBLIC\".\"TEST\" /* PUBLIC.IDX_B_C: B = 1 AND C = 1 */ " + + "/* scanCount: 3 */ WHERE ROW (\"B\", \"C\") = ROW (1, 1)"); + stat.close(); + } + + /** + * Executes a query with multiple EQ conditions against a compound index. + */ + private void multipleEqAgainstCompoundIndex(Connection conn) throws Exception { + Statement stat = conn.createStatement(); + ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT b, c FROM test WHERE b=1 AND c=1"); + rs.next(); + assertContains(rs.getString(1).replaceAll("[\\r\\n\\s]+", " "), + "SELECT \"B\", \"C\" FROM \"PUBLIC\".\"TEST\" /* PUBLIC.IDX_B_C: B = 1 AND C = 1 */ " + + "/* scanCount: 3 */ WHERE (\"B\" = 1) AND (\"C\" = 1)"); + stat.close(); + } + +} From 705d96504422e096da773b3c1e89ed55f1d8ca03 Mon Sep 17 00:00:00 2001 From: kiss034 Date: Thu, 23 Nov 2023 08:06:22 +0100 Subject: [PATCH 11/12] Preparing IndexCondition to deal with queries where the columns not used in the right order. --- h2/src/main/org/h2/index/IndexCondition.java | 69 ++++++++++++++++--- h2/src/main/org/h2/index/IndexCursor.java | 14 ++-- h2/src/main/org/h2/table/TableFilter.java | 35 ++++++---- h2/src/main/org/h2/value/ValueRow.java | 35 ++++++++++ .../h2/test/db/TestCompoundIndexSearch.java | 61 ++++++++++------ 5 files changed, 167 insertions(+), 47 deletions(-) diff --git a/h2/src/main/org/h2/index/IndexCondition.java b/h2/src/main/org/h2/index/IndexCondition.java index 04672d329d..b9fbc3cb3d 100644 --- a/h2/src/main/org/h2/index/IndexCondition.java +++ b/h2/src/main/org/h2/index/IndexCondition.java @@ -16,10 +16,12 @@ import org.h2.expression.ExpressionColumn; import org.h2.expression.ExpressionList; import org.h2.expression.ExpressionVisitor; +import org.h2.expression.ValueExpression; import org.h2.expression.condition.Comparison; import org.h2.message.DbException; import org.h2.result.ResultInterface; import org.h2.table.Column; +import org.h2.table.IndexColumn; import org.h2.table.TableType; import org.h2.value.Value; import org.h2.value.ValueArray; @@ -85,7 +87,7 @@ public class IndexCondition { * @param compareType the comparison type, see constants in * {@link Comparison} */ - private IndexCondition(int compareType, ExpressionColumn column, ExpressionList columns, Expression expression, + private IndexCondition(int compareType, ExpressionColumn column, Column[] columns, Expression expression, List list, Query query) { this.compareType = compareType; @@ -95,12 +97,7 @@ private IndexCondition(int compareType, ExpressionColumn column, ExpressionList this.compoundColumns = false; } else if (columns !=null) { this.column = null; - int listSize = columns.getSubexpressionCount(); - Column[] result = new Column[listSize]; - for (int i = listSize; --i >= 0; ) { - result[i] = ((ExpressionColumn) columns.getSubexpression(i)).getColumn(); - } - this.columns = result; + this.columns = columns; this.compoundColumns = true; } else { this.column = null; @@ -143,7 +140,13 @@ public static IndexCondition getInList(ExpressionColumn column, List * @return the index condition */ public static IndexCondition getCompoundInList(ExpressionList columns, List list) { - return new IndexCondition(Comparison.IN_LIST, null, columns, null, list, null); + int listSize = columns.getSubexpressionCount(); + Column[] cols = new Column[listSize]; + for (int i = listSize; --i >= 0; ) { + cols[i] = ((ExpressionColumn) columns.getSubexpression(i)).getColumn(); + } + + return new IndexCondition(Comparison.IN_LIST, null, cols, null, list, null); } /** @@ -506,6 +509,56 @@ public boolean isEvaluatable() { .isEverything(ExpressionVisitor.EVALUATABLE_VISITOR); } + /** + * Creates a copy of this index condition but using the {@link Index#getIndexColumns() columns} of the {@code index}. + * @param index a non-null Index + * @return a new IndexCondition with the specified columns, or {@code null} if the index does not match with this + * condition. + */ + public IndexCondition cloneWithIndexColumns(Index index) { + if (!isCompoundColumns()) { + throw DbException.getInternalError("The cloneWithColumns() method cannot be with a single column."); + } + + IndexColumn[] indexColumns = index.getIndexColumns(); + int length = indexColumns.length; + if (length != columns.length) { + return null; + } + + int[] newOrder = new int[length]; + int found = 0; + for (int i = 0; i < length; i++) { + if (indexColumns[i] == null || indexColumns[i].column == null) { + return null; + } + for (int j = 0; j < this.columns.length; j++) { + if (columns[j] == indexColumns[i].column) { + newOrder[j] = i; + found++; + } + } + } + if (found != length) { + return null; + } + + Column[] newColumns = new Column[length]; + for(int i = 0; i < length; i++) { + newColumns[i] = columns[newOrder[i]]; + } + + List newList = new ArrayList<>(length); + for (Expression expression: expressionList) { + ValueExpression valueExpression = (ValueExpression) expression; + ValueRow valueRow = (ValueRow) valueExpression.getValue(null); + ValueRow newRow = valueRow.cloneWithOrder(newOrder); + newList.add(ValueExpression.get(newRow)); + } + + return new IndexCondition(Comparison.IN_LIST, null, newColumns, null, newList, null); + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); diff --git a/h2/src/main/org/h2/index/IndexCursor.java b/h2/src/main/org/h2/index/IndexCursor.java index 6301d6f8e1..fdc62adac9 100644 --- a/h2/src/main/org/h2/index/IndexCursor.java +++ b/h2/src/main/org/h2/index/IndexCursor.java @@ -214,10 +214,16 @@ private boolean canUseIndexForIn(Column[] columns) { // only one IN(..) condition can be used at the same time return false; } - // The first column of the index must match this column, - // or it must be a VIEW index (where the column is null). - // Multiple IN conditions with views are not supported, see - // IndexCondition.getMask. + return canUseIndexForIn(index, columns); + } + + /** + * Return {@code true} if {@link Index#getIndexColumns()} and the {@code columns} parameter contains the same + * elements in the same order. All column of the index must match the column in the {@code columns} array, or + * it must be a VIEW index (where the column is null). + * @see IndexCondition#getMask(ArrayList) + */ + public static boolean canUseIndexForIn(Index index, Column[] columns) { IndexColumn[] cols = index.getIndexColumns(); if (cols == null || cols.length != columns.length) { return true; diff --git a/h2/src/main/org/h2/table/TableFilter.java b/h2/src/main/org/h2/table/TableFilter.java index 6ae4a2cde3..e63484ddd1 100644 --- a/h2/src/main/org/h2/table/TableFilter.java +++ b/h2/src/main/org/h2/table/TableFilter.java @@ -332,24 +332,33 @@ private void setScanIndexes() { public void prepare() { // forget all unused index conditions // the indexConditions list may be modified here + boolean compoundIndexConditionFound = false; for (int i = 0; i < indexConditions.size(); i++) { IndexCondition condition = indexConditions.get(i); if (!condition.isAlwaysFalse()) { - if (condition.isCompoundColumns()) { - // Checking if all columns are indexed. - Column[] columns = condition.getColumns(); - boolean indexed = true; - for (int j = 0; j < columns.length; j++) { - Column col = columns[j]; - indexed = col.getColumnId() >= 0 && index.getColumnIndex(col) >= 0; - if (!indexed) { - break; - } + if (compoundIndexConditionFound) { + // A compound index condition is already found. We cannot use other indexes with it, so removing + // everything else. The compound condition was added first. + // See: ConditionIn#createIndexConditions(SessionLocal, TableFilter) + indexConditions.remove(i); + i--; + } else if (condition.isCompoundColumns()) { + // Checking the columns match with the index. + if (IndexCursor.canUseIndexForIn(index, condition.getColumns())) { + // The condition uses the exact columns in the right order. + compoundIndexConditionFound = true; + continue; } - if (!indexed) { // Not all columns are indexed so removing the current condition - indexConditions.remove(i); - i--; + // Trying to fix the order of the condition columns. + IndexCondition fixedCondition = condition.cloneWithIndexColumns(index); + if (fixedCondition != null) { + indexConditions.set(i, fixedCondition); + compoundIndexConditionFound = true; + continue; } + // Index condition cannot be used. + indexConditions.remove(i); + i--; } else { Column col = condition.getColumn(); if (col.getColumnId() >= 0) { diff --git a/h2/src/main/org/h2/value/ValueRow.java b/h2/src/main/org/h2/value/ValueRow.java index cf9a4de155..304ac0d559 100644 --- a/h2/src/main/org/h2/value/ValueRow.java +++ b/h2/src/main/org/h2/value/ValueRow.java @@ -11,6 +11,10 @@ import org.h2.message.DbException; import org.h2.result.SimpleResult; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + /** * Row value. */ @@ -142,6 +146,37 @@ public StringBuilder getSQL(StringBuilder builder, int sqlFlags) { return builder.append(')'); } + /** + * Creates a copy of this row but the new instance will contain the {@link #values} according to + * {@code newOrder}.
+ * E.g.: ROW('a', 'b').cloneWithOrder([1, 0]) returns ROW('b', 'a') + * @param newOrder array of indexes to create the new values array + */ + public ValueRow cloneWithOrder(int[] newOrder) { + int length = values.length; + if (newOrder.length != values.length) { + throw DbException.getInternalError("Length of the new orders is different than values count."); + } + + Value[] newValues = new Value[length]; + for (int i = 0; i < length; i++) { + newValues[i] = values[newOrder[i]]; + } + + ExtTypeInfoRow typeInfoRow = (ExtTypeInfoRow) type.getExtTypeInfo(); + Object[] fields = typeInfoRow.getFields().toArray(); + LinkedHashMap newFields = new LinkedHashMap<>(length); + for (int i = 0; i < length; i++) { + Map.Entry field = (Map.Entry) fields[newOrder[i]]; + newFields.put(field.getKey(), field.getValue()); + } + ExtTypeInfoRow newTypeInfoRow = new ExtTypeInfoRow(newFields); + TypeInfo newType = new TypeInfo(type.getValueType(), type.getDeclaredPrecision(), + type.getDeclaredScale(), newTypeInfoRow); + + return new ValueRow(newType, newValues); + } + @Override public boolean equals(Object other) { if (!(other instanceof ValueRow)) { diff --git a/h2/src/test/org/h2/test/db/TestCompoundIndexSearch.java b/h2/src/test/org/h2/test/db/TestCompoundIndexSearch.java index c73a448a08..b8947b0432 100644 --- a/h2/src/test/org/h2/test/db/TestCompoundIndexSearch.java +++ b/h2/src/test/org/h2/test/db/TestCompoundIndexSearch.java @@ -36,6 +36,7 @@ public void test() throws Exception { simpleInAgainstFirstCompoundIndex(conn); simpleInAgainstSecondCompoundIndex(conn); compoundInAgainstCompoundIndex(conn); + compoundInAgainstCompoundIndex2(conn); compoundEqAgainstCompoundIndex(conn); multipleEqAgainstCompoundIndex(conn); @@ -47,20 +48,20 @@ private Connection prepare() throws Exception { deleteDb(DB_NAME); Connection conn = getConnection(DB_NAME); Statement stat = conn.createStatement(); - stat.execute("CREATE TABLE test (a INT, b INT, c INT, d INT);"); + stat.execute("CREATE TABLE test (a INT, b INT, c CHAR, d INT);"); stat.execute("CREATE INDEX idx_a ON test(a);"); stat.execute("CREATE INDEX idx_b_c ON test(b, c);"); stat.execute("INSERT INTO test (a, b, c, d) VALUES " + - "(1, 1, 1, 1), " + - "(1, 1, 2, 2), " + - "(1, 3, 3, 3), " + - "(2, 2, 1, 4), " + - "(2, 3, 2, 1), " + - "(2, 3, 3, 2), " + - "(3, 2, 1, 3), " + - "(3, 2, 2, 4), " + - "(3, 3, 3, 1), " + - "(4, 1, 1, 2);" + "(1, 1, '1', 1), " + + "(1, 1, '2', 2), " + + "(1, 3, '3', 3), " + + "(2, 2, '1', 4), " + + "(2, 3, '2', 1), " + + "(2, 3, '3', 2), " + + "(3, 2, '1', 3), " + + "(3, 2, '2', 4), " + + "(3, 3, '3', 1), " + + "(4, 1, '1', 2);" ); stat.close(); return conn; @@ -99,11 +100,11 @@ private void simpleInAgainstFirstCompoundIndex(Connection conn) throws Exception */ private void simpleInAgainstSecondCompoundIndex(Connection conn) throws Exception { Statement stat = conn.createStatement(); - ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT b, c FROM test WHERE c IN (1, 2)"); + ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT b, c FROM test WHERE c IN ('1', '2')"); rs.next(); assertContains(rs.getString(1).replaceAll("[\\r\\n\\s]+", " "), "SELECT \"B\", \"C\" FROM \"PUBLIC\".\"TEST\" /* PUBLIC.IDX_B_C */ " + - "/* scanCount: 11 */ WHERE \"C\" IN(1, 2)"); + "/* scanCount: 11 */ WHERE \"C\" IN('1', '2')"); stat.close(); } @@ -112,12 +113,28 @@ private void simpleInAgainstSecondCompoundIndex(Connection conn) throws Exceptio */ private void compoundInAgainstCompoundIndex(Connection conn) throws Exception { Statement stat = conn.createStatement(); - ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT b, c FROM test WHERE (b, c) IN ((1, 1), (2, 2))"); + ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT b, c FROM test WHERE (b, c) IN ((2, '1'), (3, '2'))"); rs.next(); assertContains(rs.getString(1).replaceAll("[\\r\\n\\s]+", " "), "SELECT \"B\", \"C\" FROM \"PUBLIC\".\"TEST\" " + - "/* PUBLIC.IDX_B_C: IN(ROW (1, 1), ROW (2, 2)) AND B IN(1, 2) */ " + - "/* scanCount: 4 */ WHERE ROW (\"B\", \"C\") IN(ROW (1, 1), ROW (2, 2))"); + "/* PUBLIC.IDX_B_C: IN(ROW (2, '1'), ROW (3, '2')) */ " + + "/* scanCount: 4 */ WHERE ROW (\"B\", \"C\") IN(ROW (2, '1'), ROW (3, '2'))"); + stat.close(); + } + + /** + * Executes a query with a compound IN condition against a compound index, but the condition columns are in different + * order than in the index.
+ * condition (c, b) vs index (b, c) + */ + private void compoundInAgainstCompoundIndex2(Connection conn) throws Exception { + Statement stat = conn.createStatement(); + ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT b, c FROM test WHERE (c, b) IN (('1', 2), ('2', 3))"); + rs.next(); + assertContains(rs.getString(1).replaceAll("[\\r\\n\\s]+", " "), + "SELECT \"B\", \"C\" FROM \"PUBLIC\".\"TEST\" " + + "/* PUBLIC.IDX_B_C: IN(ROW (2, '1'), ROW (3, '2')) */ " + + "/* scanCount: 4 */ WHERE ROW (\"C\", \"B\") IN(ROW ('1', 2), ROW ('2', 3))"); stat.close(); } @@ -126,11 +143,11 @@ private void compoundInAgainstCompoundIndex(Connection conn) throws Exception { */ private void compoundEqAgainstCompoundIndex(Connection conn) throws Exception { Statement stat = conn.createStatement(); - ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT b, c FROM test WHERE (b, c) = (1, 1)"); + ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT b, c FROM test WHERE (b, c) = (1, '1')"); rs.next(); assertContains(rs.getString(1).replaceAll("[\\r\\n\\s]+", " "), - "SELECT \"B\", \"C\" FROM \"PUBLIC\".\"TEST\" /* PUBLIC.IDX_B_C: B = 1 AND C = 1 */ " + - "/* scanCount: 3 */ WHERE ROW (\"B\", \"C\") = ROW (1, 1)"); + "SELECT \"B\", \"C\" FROM \"PUBLIC\".\"TEST\" /* PUBLIC.IDX_B_C: B = 1 AND C = '1' */ " + + "/* scanCount: 3 */ WHERE ROW (\"B\", \"C\") = ROW (1, '1')"); stat.close(); } @@ -139,11 +156,11 @@ private void compoundEqAgainstCompoundIndex(Connection conn) throws Exception { */ private void multipleEqAgainstCompoundIndex(Connection conn) throws Exception { Statement stat = conn.createStatement(); - ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT b, c FROM test WHERE b=1 AND c=1"); + ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT b, c FROM test WHERE b=1 AND c='1'"); rs.next(); assertContains(rs.getString(1).replaceAll("[\\r\\n\\s]+", " "), - "SELECT \"B\", \"C\" FROM \"PUBLIC\".\"TEST\" /* PUBLIC.IDX_B_C: B = 1 AND C = 1 */ " + - "/* scanCount: 3 */ WHERE (\"B\" = 1) AND (\"C\" = 1)"); + "SELECT \"B\", \"C\" FROM \"PUBLIC\".\"TEST\" /* PUBLIC.IDX_B_C: B = 1 AND C = '1' */ " + + "/* scanCount: 3 */ WHERE (\"B\" = 1) AND (\"C\" = '1')"); stat.close(); } From 76f34d04b9e354f9641b7c66234f5fd4069ef01a Mon Sep 17 00:00:00 2001 From: kiss034 Date: Thu, 23 Nov 2023 15:14:56 +0100 Subject: [PATCH 12/12] Fixing test errors. --- h2/src/main/org/h2/index/IndexCursor.java | 2 +- h2/src/main/org/h2/table/TableFilter.java | 6 +++ .../h2/test/db/TestCompoundIndexSearch.java | 39 ++++++++++++++++++- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/h2/src/main/org/h2/index/IndexCursor.java b/h2/src/main/org/h2/index/IndexCursor.java index fdc62adac9..7395517f10 100644 --- a/h2/src/main/org/h2/index/IndexCursor.java +++ b/h2/src/main/org/h2/index/IndexCursor.java @@ -226,7 +226,7 @@ private boolean canUseIndexForIn(Column[] columns) { public static boolean canUseIndexForIn(Index index, Column[] columns) { IndexColumn[] cols = index.getIndexColumns(); if (cols == null || cols.length != columns.length) { - return true; + return false; } for (int i = 0; i < cols.length; i++) { IndexColumn idxCol = cols[i]; diff --git a/h2/src/main/org/h2/table/TableFilter.java b/h2/src/main/org/h2/table/TableFilter.java index e63484ddd1..26c2f89ca0 100644 --- a/h2/src/main/org/h2/table/TableFilter.java +++ b/h2/src/main/org/h2/table/TableFilter.java @@ -343,6 +343,12 @@ public void prepare() { indexConditions.remove(i); i--; } else if (condition.isCompoundColumns()) { + if ( index.getIndexType().isScan() ) { + // This is only a pseudo index. + indexConditions.remove(i); + i--; + continue; + } // Checking the columns match with the index. if (IndexCursor.canUseIndexForIn(index, condition.getColumns())) { // The condition uses the exact columns in the right order. diff --git a/h2/src/test/org/h2/test/db/TestCompoundIndexSearch.java b/h2/src/test/org/h2/test/db/TestCompoundIndexSearch.java index b8947b0432..73e5f570d8 100644 --- a/h2/src/test/org/h2/test/db/TestCompoundIndexSearch.java +++ b/h2/src/test/org/h2/test/db/TestCompoundIndexSearch.java @@ -35,8 +35,10 @@ public void test() throws Exception { simpleInAgainstSimpleIndexCheck(conn); simpleInAgainstFirstCompoundIndex(conn); simpleInAgainstSecondCompoundIndex(conn); + compoundInNoIndexAndNull(conn); compoundInAgainstCompoundIndex(conn); - compoundInAgainstCompoundIndex2(conn); + compoundInAgainstCompoundIndexUnordered(conn); + compoundInAgainstSimpleIndex(conn); compoundEqAgainstCompoundIndex(conn); multipleEqAgainstCompoundIndex(conn); @@ -127,7 +129,7 @@ private void compoundInAgainstCompoundIndex(Connection conn) throws Exception { * order than in the index.
* condition (c, b) vs index (b, c) */ - private void compoundInAgainstCompoundIndex2(Connection conn) throws Exception { + private void compoundInAgainstCompoundIndexUnordered(Connection conn) throws Exception { Statement stat = conn.createStatement(); ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT b, c FROM test WHERE (c, b) IN (('1', 2), ('2', 3))"); rs.next(); @@ -138,6 +140,39 @@ private void compoundInAgainstCompoundIndex2(Connection conn) throws Exception { stat.close(); } + /** + * Executes a query with a compound IN condition. Creates a table on the fly without any indexes. The table and the + * query both contain NULL values. + */ + private void compoundInNoIndexAndNull(Connection conn) throws Exception { + Statement stat = conn.createStatement(); + stat.execute("CREATE TABLE TEST_NULL(A INT, B INT) AS (VALUES (1, 1), (1, 2), (2, 1), (2, NULL));"); + ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT * FROM TEST_NULL " + + "WHERE (A, B) IN ((1, 1), (2, 1), (2, 2), (2, NULL))"); + rs.next(); + assertContains(rs.getString(1).replaceAll("[\\r\\n\\s]+", " "), + "SELECT \"PUBLIC\".\"TEST_NULL\".\"A\", \"PUBLIC\".\"TEST_NULL\".\"B\" " + + "FROM \"PUBLIC\".\"TEST_NULL\" /* PUBLIC.TEST_NULL.tableScan */ " + + "/* scanCount: 5 */ WHERE ROW (\"A\", \"B\") " + + "IN(ROW (1, 1), ROW (2, 1), ROW (2, 2), ROW (2, NULL))"); + stat.execute("DROP TABLE TEST_NULL;"); + stat.close(); + } + + /** + * Executes a query with a compound IN condition against a simple index. + */ + private void compoundInAgainstSimpleIndex(Connection conn) throws Exception { + Statement stat = conn.createStatement(); + ResultSet rs = stat.executeQuery("EXPLAIN ANALYZE SELECT a, d FROM test WHERE (a, d) IN ((1, 3), (2, 4))"); + rs.next(); + assertContains(rs.getString(1).replaceAll("[\\r\\n\\s]+", " "), + "SELECT \"A\", \"D\" FROM \"PUBLIC\".\"TEST\" " + + "/* PUBLIC.IDX_A: A IN(1, 2) */ " + + "/* scanCount: 7 */ WHERE ROW (\"A\", \"D\") IN(ROW (1, 3), ROW (2, 4))"); + stat.close(); + } + /** * Executes a query with a compound EQ condition against a compound index. */