diff --git a/sqrl-planner/src/main/java/com/datasqrl/engine/database/relational/PostgresStatementFactory.java b/sqrl-planner/src/main/java/com/datasqrl/engine/database/relational/PostgresStatementFactory.java index 8e2b3e5302..e1b53ccc6a 100644 --- a/sqrl-planner/src/main/java/com/datasqrl/engine/database/relational/PostgresStatementFactory.java +++ b/sqrl-planner/src/main/java/com/datasqrl/engine/database/relational/PostgresStatementFactory.java @@ -147,8 +147,13 @@ public List extractTypeExtensions(List queries) { public JdbcStatement addIndex(IndexDefinition index) { var ddl = new CreateIndexDDL( - index.getName(), index.getTableName(), index.getColumnNames(), index.getType()); - return new GenericJdbcStatement(ddl.getIndexName(), Type.INDEX, ddl.getSql()); + index.getName(), + index.getTableName(), + index.getColumnNames(), + index.getDirections(), + index.getType()); + + return new GenericJdbcStatement(ddl.indexName(), Type.INDEX, ddl.getSql()); } /* diff --git a/sqrl-planner/src/main/java/com/datasqrl/engine/database/relational/ddl/CreateIndexDDL.java b/sqrl-planner/src/main/java/com/datasqrl/engine/database/relational/ddl/CreateIndexDDL.java index a23ebc60d3..d03e53b25e 100644 --- a/sqrl-planner/src/main/java/com/datasqrl/engine/database/relational/ddl/CreateIndexDDL.java +++ b/sqrl-planner/src/main/java/com/datasqrl/engine/database/relational/ddl/CreateIndexDDL.java @@ -22,15 +22,16 @@ import com.google.common.base.Preconditions; import java.util.List; import java.util.stream.Collectors; -import lombok.Value; +import java.util.stream.IntStream; +import org.apache.calcite.rel.RelFieldCollation.Direction; -@Value -public class CreateIndexDDL implements SqlDDLStatement { - - String indexName; - String tableName; - List columns; - IndexType type; +public record CreateIndexDDL( + String indexName, + String tableName, + List columns, + List directions, + IndexType type) + implements SqlDDLStatement { @Override public String getSql() { @@ -41,7 +42,7 @@ public String getSql() { "to_tsvector('english', %s )" .formatted( quoteIdentifier(columns).stream() - .map(col -> "coalesce(%s, '')".formatted(col)) + .map("coalesce(%s, '')"::formatted) .collect(Collectors.joining(" || ' ' || "))); indexType = "GIN"; break; @@ -59,7 +60,10 @@ public String getSql() { indexType = "HNSW"; break; default: - columnExpression = String.join(",", quoteIdentifier(columns)); + columnExpression = + IntStream.range(0, columns.size()) + .mapToObj(this::formatIndexColumn) + .collect(Collectors.joining(",")); indexType = type.name().toLowerCase(); } @@ -69,4 +73,9 @@ public String getSql() { quoteIdentifier(indexName), quoteIdentifier(tableName), indexType, columnExpression); return sql; } + + private String formatIndexColumn(int index) { + var sortOrder = directions.get(index).isDescending() ? " DESC" : ""; + return quoteIdentifier(columns.get(index)) + sortOrder; + } } diff --git a/sqrl-planner/src/main/java/com/datasqrl/plan/global/IndexDefinition.java b/sqrl-planner/src/main/java/com/datasqrl/plan/global/IndexDefinition.java index f268c080fb..493ca4772f 100644 --- a/sqrl-planner/src/main/java/com/datasqrl/plan/global/IndexDefinition.java +++ b/sqrl-planner/src/main/java/com/datasqrl/plan/global/IndexDefinition.java @@ -15,10 +15,13 @@ */ package com.datasqrl.plan.global; -import com.google.common.base.Preconditions; +import static com.google.common.base.Preconditions.checkArgument; + import java.util.List; import java.util.stream.Collectors; +import java.util.stream.IntStream; import lombok.Value; +import org.apache.calcite.rel.RelFieldCollation.Direction; @Value public class IndexDefinition implements Comparable { @@ -28,6 +31,7 @@ public class IndexDefinition implements Comparable { String tableName; List columns; List columnNames; + List directions; int partitionOffset; IndexType type; @@ -37,28 +41,59 @@ public IndexDefinition( List allFieldNames, int partitionOffset, IndexType type) { - Preconditions.checkArgument( + this( + tableName, + columns, + allFieldNames, + partitionOffset, + type, + columns.stream().map(column -> Direction.ASCENDING).toList()); + } + + public IndexDefinition( + String tableName, + List columns, + List allFieldNames, + int partitionOffset, + IndexType type, + List directions) { + + checkArgument( type.isPartitioned() ^ partitionOffset < 0, "Index must be partitioned XOR partition offset must be negative: %s | %s", type, partitionOffset); - Preconditions.checkArgument( + + checkArgument( partitionOffset <= columns.size(), "Invalid partition offset: %s | %s", partitionOffset, columns.size()); + + checkArgument( + columns.size() == directions.size(), + "Number of index column directions must match number of columns: %s | %s", + columns.size(), + directions.size()); + this.tableName = tableName; this.columns = columns; this.partitionOffset = partitionOffset; this.columnNames = columns.stream().map(allFieldNames::get).collect(Collectors.toList()); this.type = type; + this.directions = directions; } private IndexDefinition( - String tableName, List columns, List columnNames, IndexType type) { + String tableName, + List columns, + List columnNames, + List directions, + IndexType type) { this.tableName = tableName; this.columns = columns; this.columnNames = columnNames; + this.directions = directions; this.partitionOffset = -1; this.type = type; } @@ -68,12 +103,19 @@ public String getName() { + "_" + type.name().toLowerCase() + "_" - + columns.stream().map(i -> "c" + i).collect(Collectors.joining()); + + IntStream.range(0, columns.size()) + .mapToObj(i -> "c" + columns.get(i) + (directions.get(i).isDescending() ? "d" : "")) + .collect(Collectors.joining()); } public static IndexDefinition getPrimaryKeyIndex( String tableId, List primaryKeys, List pkNames) { - return new IndexDefinition(tableId, primaryKeys, pkNames, IndexType.BTREE); + return new IndexDefinition( + tableId, + primaryKeys, + pkNames, + primaryKeys.stream().map(column -> Direction.ASCENDING).toList(), + IndexType.BTREE); } public int numEqualityColumnsRequired() { diff --git a/sqrl-planner/src/main/java/com/datasqrl/plan/global/IndexSelector.java b/sqrl-planner/src/main/java/com/datasqrl/plan/global/IndexSelector.java index f09053d712..34b404c9cc 100644 --- a/sqrl-planner/src/main/java/com/datasqrl/plan/global/IndexSelector.java +++ b/sqrl-planner/src/main/java/com/datasqrl/plan/global/IndexSelector.java @@ -35,7 +35,6 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; -import java.util.stream.Collectors; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode.Include; @@ -111,28 +110,32 @@ public Map optimizeIndexes( public Optional> getIndexHints( String tableName, TableAnalysis tableAnalysis) { + var hints = tableAnalysis.getHints(); - List indexHints = - hints.getHints(IndexHint.class).collect(Collectors.toUnmodifiableList()); - if (!indexHints.isEmpty()) { - return Optional.of( - indexHints.stream() - .filter(idxHint -> idxHint.getIndexType() != null) // filter out no-index hints - .filter(idxHint -> config.supportedIndexTypes().contains(idxHint.getIndexType())) - .map( - idxHint -> - new IndexDefinition( - tableName, - idxHint.getColumnIndexes(), - tableAnalysis.getRowType().getFieldNames(), - idxHint.getIndexType().isPartitioned() - ? idxHint.getColumnNames().size() - : -1, - idxHint.getIndexType())) - .collect(Collectors.toUnmodifiableList())); - } else { + var indexHints = hints.getHints(IndexHint.class).toList(); + + if (indexHints.isEmpty()) { return Optional.empty(); } + + var indexDefinitions = + indexHints.stream() + .filter(idxHint -> idxHint.getIndexType() != null) // filter out no-index hints + .filter(idxHint -> config.supportedIndexTypes().contains(idxHint.getIndexType())) + .map( + idxHint -> + new IndexDefinition( + tableName, + idxHint.getColumnIndexes(), + tableAnalysis.getRowType().getFieldNames(), + idxHint.getIndexType().isPartitioned() + ? idxHint.getColumnNames().size() + : -1, + idxHint.getIndexType(), + idxHint.getDirections())) + .toList(); + + return Optional.of(indexDefinitions); } private Map optimizeIndexes( @@ -192,7 +195,7 @@ private Map optimizeIndexesWithCostMinimization( // Determine all index candidates Set candidates = new LinkedHashSet<>(); indexes.forEach(idx -> candidates.addAll(generateIndexCandidates(idx))); - Function initialCost = idx -> idx.getBaseCost(); + Function initialCost = QueryIndexSummary::getBaseCost; if (config.hasPrimaryKeyIndex() && table.getAnalysis().getPrimaryKey().isDefined()) { // The baseline cost is the cost of doing the lookup with the primary key index // we need to use the primary key on the physical table (i.e. from the statement) diff --git a/sqrl-planner/src/main/java/com/datasqrl/plan/global/IndexType.java b/sqrl-planner/src/main/java/com/datasqrl/plan/global/IndexType.java index 093e764548..4990b9a558 100644 --- a/sqrl-planner/src/main/java/com/datasqrl/plan/global/IndexType.java +++ b/sqrl-planner/src/main/java/com/datasqrl/plan/global/IndexType.java @@ -32,8 +32,6 @@ public boolean requiresAllColumns() { /** * A general index covers comparison operators and can cover multiple columns. If it is not a * general index, it is a function index that has a specific indexing method. - * - * @return */ public boolean isGeneralIndex() { return this == HASH || this == BTREE || this == PBTREE; @@ -43,6 +41,10 @@ public boolean isPartitioned() { return this == PBTREE; } + public boolean supportsSortOrder() { + return this == BTREE || this == PBTREE; + } + public static Optional fromName(String name) { for (IndexType indexType : IndexType.values()) { if (indexType.name().equalsIgnoreCase(name)) { diff --git a/sqrl-planner/src/main/java/com/datasqrl/plan/global/QueryIndexSummary.java b/sqrl-planner/src/main/java/com/datasqrl/plan/global/QueryIndexSummary.java index a6b8ee71c1..4310536673 100644 --- a/sqrl-planner/src/main/java/com/datasqrl/plan/global/QueryIndexSummary.java +++ b/sqrl-planner/src/main/java/com/datasqrl/plan/global/QueryIndexSummary.java @@ -53,18 +53,13 @@ public class QueryIndexSummary { private static final QueryIndexSummary EMPTY = new QueryIndexSummary(null, Set.of(), Set.of(), Set.of(), 1.0); - public static final String INDEX_NAME = "_index_"; - @Include NamedTable table; @Include Set equalityColumns; @Include Set inequalityColumns; @Include Set functionCalls; - // TODO: add support for sort orders - // List sorts; - /** Keeps track of the relative frequency of query conjunctions as we reduce them */ - double count = 1.0; + double count; public static List ofFilter( @NonNull NamedTable table, RexNode filter, SqrlRexUtil rexUtil) { @@ -104,16 +99,22 @@ public static List ofFilter( } public static Optional ofSort(@NonNull NamedTable table, RexNode node) { - if (node instanceof RexCall call) { - var idxFinder = new IndexableFinder(); - call.accept(idxFinder); - if (idxFinder.isIndexable && idxFinder.idxCall != null) { - return Optional.of( - new QueryIndexSummary( - table, Set.of(), Set.of(), ImmutableSet.of(idxFinder.idxCall), 1.0)); - } + if (node instanceof RexInputRef inputRef) { + return ofSort(table, inputRef.getIndex()); + } + + if (!(node instanceof RexCall call)) { + return Optional.empty(); + } + + var idxFinder = new IndexableFinder(); + call.accept(idxFinder); + if (!idxFinder.isIndexable || idxFinder.idxCall == null) { + return Optional.empty(); } - return Optional.empty(); + + return Optional.of( + new QueryIndexSummary(table, Set.of(), Set.of(), ImmutableSet.of(idxFinder.idxCall), 1.0)); } public static Optional ofSort(@NonNull NamedTable table, int columnIndex) { @@ -151,11 +152,11 @@ public double getCost(@NonNull IndexDefinition indexDef) { // See which of the indexable function calls are covered List coveredCalls = new ArrayList<>(); Set indexCols = ImmutableSet.copyOf(indexDef.getColumns()); - for (IndexableFunctionCall fcall : this.functionCalls) { - var function = fcall.function(); + for (IndexableFunctionCall fnCall : this.functionCalls) { + var function = fnCall.function(); if (function.getSupportedIndexes().contains(indexType) - && indexCols.containsAll(fcall.columnIndexes())) { - coveredCalls.add(fcall); + && indexCols.containsAll(fnCall.columnIndexes())) { + coveredCalls.add(fnCall); } } if (coveredCalls.isEmpty()) { diff --git a/sqrl-planner/src/main/java/com/datasqrl/planner/hint/IndexHint.java b/sqrl-planner/src/main/java/com/datasqrl/planner/hint/IndexHint.java index 5c22e03782..9e33fd9596 100644 --- a/sqrl-planner/src/main/java/com/datasqrl/planner/hint/IndexHint.java +++ b/sqrl-planner/src/main/java/com/datasqrl/planner/hint/IndexHint.java @@ -21,8 +21,10 @@ import com.datasqrl.planner.parser.SqrlHint; import com.datasqrl.planner.parser.StatementParserException; import com.google.auto.service.AutoService; +import java.util.ArrayList; import java.util.List; import lombok.Getter; +import org.apache.calcite.rel.RelFieldCollation.Direction; /** * Explicitly assign an index to a table that's persisted to a database engine. Overwrites the @@ -34,11 +36,16 @@ public class IndexHint extends ColumnNamesHint { public static final String HINT_NAME = "index"; private final IndexType indexType; + private final List directions; protected IndexHint( - ParsedObject source, IndexType indexType, List columnsNames) { + ParsedObject source, + IndexType indexType, + List columnsNames, + List directions) { super(source, Type.DAG, columnsNames); this.indexType = indexType; + this.directions = directions; } @AutoService(Factory.class) @@ -48,9 +55,10 @@ public static class IndexHintFactory implements Factory { public PlannerHint create(ParsedObject source) { var arguments = source.get().options(); if (arguments == null || arguments.isEmpty()) { - return new IndexHint(source, null, List.of()); // no hint + return new IndexHint(source, null, List.of(), List.of()); // no hint } - if (arguments.size() <= 1) { + + if (arguments.size() == 1) { throw new StatementParserException( ErrorLabel.GENERIC, source.getFileLocation(), @@ -64,12 +72,73 @@ public PlannerHint create(ParsedObject source) { "Unknown index type: %s", arguments.get(0)); } - return new IndexHint(source, optIndex.get(), arguments.subList(1, arguments.size())); + var indexType = optIndex.get(); + var columns = parseColumns(source, indexType, arguments.subList(1, arguments.size())); + + return new IndexHint(source, indexType, columns.names(), columns.directions()); } @Override public String getName() { return HINT_NAME; } + + private static ParsedColumns parseColumns( + ParsedObject source, IndexType indexType, List arguments) { + + var columnNames = new ArrayList(); + var directions = new ArrayList(); + + for (var argument : arguments) { + var terms = argument.trim().split("\\s+"); + + if (argument.isBlank() || terms.length > 2) { + throw invalidColumnSpecification(source, argument); + } + + columnNames.add(terms[0]); + + Direction direction = Direction.ASCENDING; + if (terms.length > 1) { + direction = parseDirection(source, argument, terms[1]); + } + + if (direction.isDescending() && !indexType.supportsSortOrder()) { + throw new StatementParserException( + ErrorLabel.GENERIC, + source.getFileLocation(), + "Descending index columns are only supported for BTREE and PBTREE indexes."); + } + + directions.add(direction); + } + + return new ParsedColumns(columnNames, directions); + } + + private static Direction parseDirection( + ParsedObject source, String argument, String direction) { + + if ("asc".equalsIgnoreCase(direction)) { + return Direction.ASCENDING; + } + + if ("desc".equalsIgnoreCase(direction)) { + return Direction.DESCENDING; + } + + throw invalidColumnSpecification(source, argument); + } + + private static StatementParserException invalidColumnSpecification( + ParsedObject source, String argument) { + return new StatementParserException( + ErrorLabel.GENERIC, + source.getFileLocation(), + "Invalid index column specification: %s. Expected a column name optionally followed by ASC or DESC.", + argument); + } + + private record ParsedColumns(List names, List directions) {} } } diff --git a/sqrl-planner/src/test/java/com/datasqrl/plan/global/IndexSelectorTest.java b/sqrl-planner/src/test/java/com/datasqrl/plan/global/IndexSelectorTest.java new file mode 100644 index 0000000000..294108a67b --- /dev/null +++ b/sqrl-planner/src/test/java/com/datasqrl/plan/global/IndexSelectorTest.java @@ -0,0 +1,82 @@ +/* + * Copyright © 2021 DataSQRL (contact@datasqrl.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datasqrl.plan.global; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.datasqrl.plan.global.IndexSelector.NamedTable; +import com.datasqrl.planner.analyzer.TableAnalysis; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.calcite.rel.RelFieldCollation.Direction; +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeField; +import org.apache.calcite.rex.RexInputRef; +import org.apache.flink.table.catalog.ObjectIdentifier; +import org.junit.jupiter.api.Test; + +class IndexSelectorTest { + + @Test + void givenProjectedColumnSort_whenSummarize_thenIdentifiesColumn() { + var fieldType = mock(RelDataType.class); + var field = mock(RelDataTypeField.class); + when(field.getType()).thenReturn(fieldType); + var rowType = mock(RelDataType.class); + when(rowType.getFieldList()).thenReturn(List.of(field)); + + var summary = + QueryIndexSummary.ofSort( + new NamedTable("orders", "orders", null, null), RexInputRef.of(0, rowType)) + .orElseThrow(); + + assertThat(summary.getInequalityColumns()).containsExactly(0); + } + + @Test + void givenSort_whenGenerateIndexCandidates_thenBtreeUsesDefaultSortOrder() { + var rowType = mock(RelDataType.class); + when(rowType.getFieldNames()).thenReturn(List.of("col_a")); + var relNode = mock(RelNode.class); + when(relNode.getRowType()).thenReturn(rowType); + var tableAnalysis = + TableAnalysis.builder() + .objectIdentifier(ObjectIdentifier.of("datasqrl", "public", "orders")) + .collapsedRelnode(relNode) + .originalRelnode(relNode) + .build(); + var table = new NamedTable("orders", "orders", tableAnalysis, null); + var summary = new QueryIndexSummary(table, Set.of(), Set.of(0), Set.of(), 1.0); + var config = mock(IndexSelectorConfig.class); + when(config.supportedIndexTypes()).thenReturn(EnumSet.of(IndexType.BTREE)); + when(config.maxIndexColumns(IndexType.BTREE)).thenReturn(1); + + var candidates = new IndexSelector(null, config, Map.of()).generateIndexCandidates(summary); + + assertThat(candidates) + .singleElement() + .satisfies( + index -> { + assertThat(index.getColumns()).containsExactly(0); + assertThat(index.getDirections()).containsExactly(Direction.ASCENDING); + }); + } +} diff --git a/sqrl-planner/src/test/java/com/datasqrl/planner/hint/IndexHintTest.java b/sqrl-planner/src/test/java/com/datasqrl/planner/hint/IndexHintTest.java new file mode 100644 index 0000000000..ec1db4b843 --- /dev/null +++ b/sqrl-planner/src/test/java/com/datasqrl/planner/hint/IndexHintTest.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2021 DataSQRL (contact@datasqrl.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datasqrl.planner.hint; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.datasqrl.error.ErrorLocation.FileLocation; +import com.datasqrl.plan.global.IndexType; +import com.datasqrl.planner.parser.ParsedObject; +import com.datasqrl.planner.parser.SqrlHint; +import com.datasqrl.planner.parser.StatementParserException; +import java.util.List; +import org.apache.calcite.rel.RelFieldCollation.Direction; +import org.junit.jupiter.api.Test; + +class IndexHintTest { + + private final IndexHint.IndexHintFactory factory = new IndexHint.IndexHintFactory(); + + @Test + void givenDescendingIndexColumn_whenCreate_thenRetainsSortDirections() { + var parsedHint = + SqrlHint.parse( + new ParsedObject<>( + "index(BTREE, col_a DESC, col_b asc, col_c)", FileLocation.START)) + .get(0); + + var hint = (IndexHint) factory.create(parsedHint); + + assertThat(hint.getIndexType()).isEqualTo(IndexType.BTREE); + assertThat(hint.getColumnNames()).containsExactly("col_a", "col_b", "col_c"); + assertThat(hint.getDirections()) + .containsExactly(Direction.DESCENDING, Direction.ASCENDING, Direction.ASCENDING); + } + + @Test + void givenInvalidIndexColumnDirection_whenCreate_thenThrows() { + var hint = + new ParsedObject<>( + new SqrlHint("index", List.of("BTREE", "col_a sideways")), FileLocation.START); + + assertThatThrownBy(() -> factory.create(hint)) + .isInstanceOf(StatementParserException.class) + .hasMessageContaining("Expected a column name optionally followed by ASC or DESC"); + } +} diff --git a/sqrl-testing/sqrl-testing-integration/src/test/resources/dagplanner/indexHints.sqrl b/sqrl-testing/sqrl-testing-integration/src/test/resources/dagplanner/indexHints.sqrl index ac39bdb1ec..46f059afb6 100644 --- a/sqrl-testing/sqrl-testing-integration/src/test/resources/dagplanner/indexHints.sqrl +++ b/sqrl-testing/sqrl-testing-integration/src/test/resources/dagplanner/indexHints.sqrl @@ -9,7 +9,7 @@ MyOrdersIndexHash := SELECT * FROM _Orders WHERE id > 10; /*+index */ MyOrdersNoIndex := SELECT * FROM _Orders WHERE id > 10; -/*+index(btree, time, id), index(hash, customerid) */ +/*+index(btree, time DESC, id), index(hash, customerid) */ MyOrdersTwoIndex := SELECT * FROM _Orders WHERE id > 10; MyOrdersNoHint := SELECT * FROM _Orders WHERE id > 10; diff --git a/sqrl-testing/sqrl-testing-integration/src/test/resources/snapshots/com/datasqrl/DAGPlannerTest/indexHints.txt b/sqrl-testing/sqrl-testing-integration/src/test/resources/snapshots/com/datasqrl/DAGPlannerTest/indexHints.txt index 801034a1f4..905e18f852 100644 --- a/sqrl-testing/sqrl-testing-integration/src/test/resources/snapshots/com/datasqrl/DAGPlannerTest/indexHints.txt +++ b/sqrl-testing/sqrl-testing-integration/src/test/resources/snapshots/com/datasqrl/DAGPlannerTest/indexHints.txt @@ -626,9 +626,9 @@ END "sql" : "CREATE INDEX IF NOT EXISTS \"MyOrdersIndexHash_2_hash_c1\" ON \"MyOrdersIndexHash_2\" USING hash (\"customerid\")" }, { - "name" : "MyOrdersTwoIndex_5_btree_c2c0", + "name" : "MyOrdersTwoIndex_5_btree_c2dc0", "type" : "INDEX", - "sql" : "CREATE INDEX IF NOT EXISTS \"MyOrdersTwoIndex_5_btree_c2c0\" ON \"MyOrdersTwoIndex_5\" USING btree (\"time\",\"id\")" + "sql" : "CREATE INDEX IF NOT EXISTS \"MyOrdersTwoIndex_5_btree_c2dc0\" ON \"MyOrdersTwoIndex_5\" USING btree (\"time\" DESC,\"id\")" }, { "name" : "MyOrdersTwoIndex_5_hash_c1",