diff --git a/h2/src/main/org/h2/expression/condition/ConditionIn.java b/h2/src/main/org/h2/expression/condition/ConditionIn.java index 4d055259be..c7d6be4811 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,37 @@ public void createIndexConditions(SessionLocal session, TableFilter filter) { } else if (left instanceof ExpressionList) { ExpressionList list = (ExpressionList) left; if (!list.isArray()) { - createIndexConditions(filter, list); + // 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, ExpressionList list) { + /** + * 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) { + if (!e.isEverything(visitor)) { + return; + } + } + 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); diff --git a/h2/src/main/org/h2/expression/condition/ConditionInConstantSet.java b/h2/src/main/org/h2/expression/condition/ConditionInConstantSet.java index 1ab4c1a751..84266886ac 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,37 @@ public void createIndexConditions(SessionLocal session, TableFilter filter) { } else if (left instanceof ExpressionList) { ExpressionList list = (ExpressionList) left; if (!list.isArray()) { - createIndexConditions(filter, list); + // 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, ExpressionList list) { + /** + * 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) { + if (!e.isEverything(visitor)) { + return; + } + } + 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); diff --git a/h2/src/main/org/h2/index/IndexCondition.java b/h2/src/main/org/h2/index/IndexCondition.java index 2be3564dd7..b9fbc3cb3d 100644 --- a/h2/src/main/org/h2/index/IndexCondition.java +++ b/h2/src/main/org/h2/index/IndexCondition.java @@ -14,17 +14,23 @@ 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.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; +import org.h2.value.ValueRow; + +import static org.h2.util.HasSQL.TRACE_SQL_FLAGS; /** - * 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. * @@ -65,71 +71,97 @@ public class IndexCondition { public static final int SPATIAL_INTERSECTS = 16; private final Column column; + private final Column[] columns; + private final boolean compoundColumns; + /** * see constants in {@link Comparison} */ 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) { + private IndexCondition(int compareType, ExpressionColumn column, Column[] columns, Expression expression, + List list, Query query) { + this.compareType = compareType; - this.column = column == null ? null : column.getColumn(); + if (column != null) { + this.column = column.getColumn(); + this.columns = null; + this.compoundColumns = false; + } else if (columns !=null) { + this.column = null; + this.columns = columns; + this.compoundColumns = true; + } else { + this.column = null; + this.columns = null; + this.compoundColumns = false; + } 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); } /** - * Create an index condition with the compare type IN_ARRAY and with the - * given parameters. + * 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) { + 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); + } + + /** + * 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 @@ -137,9 +169,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); } /** @@ -162,10 +192,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) { + ValueRow v = (ValueRow) e.getValue(session); + v = Column.convert(session, columns, v); + 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 +244,27 @@ 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 DbException.getInternalError("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 +292,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 +307,7 @@ public String getSQL(int sqlFlags) { if (expression != null) { expression.getSQL(builder, sqlFlags, Expression.AUTO_PARENTHESES); } - return builder.toString(); + return builder; } /** @@ -266,7 +327,15 @@ 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: @@ -323,7 +392,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) { @@ -360,9 +429,35 @@ public int getCompareType() { * Get the referenced column. * * @return the column + * @throws DbException if {@link #isCompoundColumns()} is {@code true} */ public Column getColumn() { - return column; + if (!isCompoundColumns()) { + return column; + } + throw DbException.getInternalError("The getColumn() method cannot be with multiple columns."); + } + + /** + * Get the referenced columns. + * + * @return the column array + * @throws DbException if {@link #isCompoundColumns()} is {@code false} + */ + public Column[] getColumns() { + if (isCompoundColumns()) { + return columns; + } + throw DbException.getInternalError("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 compoundColumns; } /** @@ -414,9 +509,66 @@ 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("column=").append(column).append(", compareType="); + StringBuilder builder = new StringBuilder(); + if (!isCompoundColumns()) { + builder.append("column=").append(column); + } else { + builder.append("columns="); + Column.writeColumns(builder, columns, TRACE_SQL_FLAGS); + } + 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 33232aed51..7395517f10 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 DbException.getInternalError("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,34 @@ 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; + } + 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 false; + } + 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 +357,11 @@ private void nextCursor() { while (inResult.next()) { Value v = inResult.currentRow()[0]; if (v != ValueNull.INSTANCE) { + if (inColumn instanceof Column[]) { + v = Column.convert(session, (Column[]) inColumn, (ValueRow) v); + } else { + v = ((Column) inColumn).convert(session, v); + } find(v); break; } @@ -317,9 +370,20 @@ 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; + ValueRow converted = Column.convert(session, columns, ((ValueRow) v)); + Value[] values = converted.getList(); + for (int i = columns.length; --i >= 0; ) { + start.setValue(columns[i].getColumnId(), values[i]); + } + } + 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/Column.java b/h2/src/main/org/h2/table/Column.java index beee700c92..9340254e2c 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; @@ -25,10 +26,12 @@ 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; import org.h2.value.ValueNull; +import org.h2.value.ValueRow; import org.h2.value.ValueUuid; /** @@ -187,6 +190,36 @@ 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; + } + } + if (copy == null) + return valueRow; + TypeInfo typeInfo = TypeInfo.getTypeInfo(Value.ROW, 0, 0, new ExtTypeInfoRow(columns)); + return ValueRow.get(typeInfo, copy); + } + /** * Returns whether this column is an identity column. * diff --git a/h2/src/main/org/h2/table/TableFilter.java b/h2/src/main/org/h2/table/TableFilter.java index a45058234e..26c2f89ca0 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); + } } } } @@ -320,14 +332,54 @@ 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()) { - Column col = condition.getColumn(); - if (col.getColumnId() >= 0) { - if (index.getColumnIndex(col) < 0) { + 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()) { + 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. + compoundIndexConditionFound = true; + continue; + } + // 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) { + int columnIndex = index.getColumnIndex(col); + if (columnIndex == 0) { + // The first column of the index always matches. + continue; + } + 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) + indexConditions.remove(i); + i--; + } } } } 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/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/db/TestCompoundIndexSearch.java b/h2/src/test/org/h2/test/db/TestCompoundIndexSearch.java new file mode 100644 index 0000000000..73e5f570d8 --- /dev/null +++ b/h2/src/test/org/h2/test/db/TestCompoundIndexSearch.java @@ -0,0 +1,202 @@ +/* + * 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); + compoundInNoIndexAndNull(conn); + compoundInAgainstCompoundIndex(conn); + compoundInAgainstCompoundIndexUnordered(conn); + compoundInAgainstSimpleIndex(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 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);" + ); + 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 ((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 (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 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(); + 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(); + } + + /** + * 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. + */ + 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(); + } + +}