Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for basic incremental index sorting #3656

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
177 changes: 126 additions & 51 deletions h2/src/main/org/h2/command/query/Select.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;
import org.h2.api.ErrorCode;
import org.h2.api.Trigger;
Expand Down Expand Up @@ -150,7 +153,13 @@ public class Select extends Query {
private boolean isForUpdate;
private double cost;
private boolean isQuickAggregateQuery, isDistinctQuery;
private boolean sortUsingIndex;

/**
* The length of the order-by prefix which is already sorted.
* A positive value triggers incremental sorting at the end.
* {@code Integer.MAX_VALUE} means that no final sort is needed.
*/
private int numIndexSortedColumns;

private boolean isGroupWindowStage2;

Expand Down Expand Up @@ -583,21 +592,24 @@ private Value[] constructGroupResultRow(Value[] keyValues, int columnCount) {
}

/**
* Get the index that matches the ORDER BY list, if one exists. This is to
* avoid running a separate ORDER BY if an index can be used. This is
* specially important for large result sets, if only the first few rows are
* important (LIMIT is used)
* Get an array of indexes that match a non-empty prefix of the ORDER BY list. This is to
* avoid running a separate ORDER BY if an index which covers all ORDER BY elements exists.
* Indexes which cover a prefix can be used to boost the performance of ORDER BY. This is
* specially important for large result sets, if only the first few rows are important
* (LIMIT is used).
*
* @return the index if one is found
* @return a list of {@code SortIndex} wrappers ordered by the length of the prefix covered
* in descending order. The first index can provide better performance than the latter.
*/
private Index getSortIndex() {
private List<SortIndex> getBestSortIndexes() {
katzyn marked this conversation as resolved.
Show resolved Hide resolved
katzyn marked this conversation as resolved.
Show resolved Hide resolved
if (sort == null) {
return null;
}
ArrayList<Column> sortColumns = Utils.newSmallArrayList();
int[] queryColumnIndexes = sort.getQueryColumnIndexes();
int queryIndexesLength = queryColumnIndexes.length;
int[] sortIndex = new int[queryIndexesLength];
boolean partial = false;
for (int i = 0, j = 0; i < queryIndexesLength; i++) {
int idx = queryColumnIndexes[i];
if (idx < 0 || idx >= expressions.size()) {
Expand All @@ -613,20 +625,33 @@ private Index getSortIndex() {
}
ExpressionColumn exprCol = (ExpressionColumn) expr;
if (exprCol.getTableFilter() != topTableFilter) {
return null;
// any index to be picked will only cover a partial number of columns
// there is no "multi-table" index
partial = true;
break;
}
sortColumns.add(exprCol.getColumn());
sortIndex[j++] = i;
}
Column[] sortCols = sortColumns.toArray(new Column[0]);
if (sortCols.length == 0) {
// sort just on constants - can use scan index
return topTableFilter.getTable().getScanIndex(session);
return partial ? null :
Collections.singletonList(
new SortIndex(topTableFilter.getTable().getScanIndex(session), Integer.MAX_VALUE));
}
if (sortCols.length == 1 && sortCols[0].getColumnId() == -1 && !partial) {
// special case: order by _ROWID_
Index index = topTableFilter.getTable().getScanIndex(session);
if (index.isRowIdIndex()) {
return Collections.singletonList(new SortIndex(index, Integer.MAX_VALUE));
}
}
ArrayList<Index> list = topTableFilter.getTable().getIndexes();
if (list != null) {
int[] sortTypes = sort.getSortTypesWithNullOrdering();
DefaultNullOrdering defaultNullOrdering = getDatabase().getDefaultNullOrdering();
List<SortIndex> candidates = new ArrayList<>();
loop: for (Index index : list) {
if (index.getCreateSQL() == null) {
// can't use the scan index
Expand All @@ -645,24 +670,27 @@ private Index getSortIndex() {
IndexColumn idxCol = indexCols[j];
Column sortCol = sortCols[j];
if (idxCol.column != sortCol) {
if (j > 0) {
// good candidate as it matches a part of the prefix
candidates.add(new SortIndex(index, j));
}
continue loop;
}
int sortType = sortTypes[sortIndex[j]];
if (sortCol.isNullable()
? defaultNullOrdering.addExplicitNullOrdering(idxCol.sortType) != sortType
: (idxCol.sortType & SortOrder.DESCENDING) != (sortType & SortOrder.DESCENDING)) {
if (j > 0) {
// good candidate as it matches a part of the prefix
candidates.add(new SortIndex(index, j));
}
continue loop;
}
}
return index;
}
}
if (sortCols.length == 1 && sortCols[0].getColumnId() == -1) {
// special case: order by _ROWID_
Index index = topTableFilter.getTable().getScanIndex(session);
if (index.isRowIdIndex()) {
return index;
candidates.add(new SortIndex(index, partial ? sortCols.length : Integer.MAX_VALUE));
}
candidates.sort(Comparator.comparingInt(SortIndex::getPrefixCovered).reversed());
return candidates;
}
return null;
}
Expand Down Expand Up @@ -701,7 +729,7 @@ private void queryDistinct(ResultTarget result, long offset, long limitRows, boo
continue;
}
result.addRow(value);
if ((sort == null || sortUsingIndex) && limitRows > 0 && rowNumber >= limitRows && !withTies) {
if ((sort == null || numIndexSortedColumns == Integer.MAX_VALUE) && limitRows > 0 && rowNumber >= limitRows && !withTies) {
break;
}
}
Expand All @@ -721,24 +749,42 @@ private LazyResult queryFlat(int columnCount, ResultTarget result, long offset,
if (result == null) {
return lazyResult;
}
if (limitRows < 0 || sort != null && !sortUsingIndex || withTies && !quickOffset) {
if (limitRows < 0 || sort != null && numIndexSortedColumns == 0 || withTies && !quickOffset) {
limitRows = Long.MAX_VALUE;
}
Value[] row = null;
while (result.getRowCount() < limitRows && lazyResult.next()) {
row = lazyResult.currentRow();
result.addRow(row);
}
if (limitRows != Long.MAX_VALUE && withTies && sort != null && row != null) {
// alreadySorted may not cover the whole order-by clause; fetch until you ensure that the prefix changes
if (limitRows != Long.MAX_VALUE && (withTies || numIndexSortedColumns != Integer.MAX_VALUE) && sort != null && row != null) {
Value[] expected = row;
boolean fixSort = numIndexSortedColumns == Integer.MAX_VALUE;
boolean fixWithTies = !withTies;
while (lazyResult.next()) {
row = lazyResult.currentRow();
if (sort.compare(expected, row) != 0) {
// we still need to fetch some records to ensure completeness in case of partial sorting
if (!fixSort) {
if (sort.compare(expected, row, numIndexSortedColumns) != 0) {
fixSort = true;
}
}
// we still need to fetch some records to ensure withTies is honored
if (fixSort && !fixWithTies && sort.compare(expected, row) != 0) {
fixWithTies = true;
}
if (fixSort && fixWithTies)
{
break;
}
result.addRow(row);
}
result.limitsWereApplied();

if (withTies)
{
result.limitsWereApplied();
}
}
return null;
}
Expand Down Expand Up @@ -780,10 +826,10 @@ protected ResultInterface queryWithoutCache(long maxRows, ResultTarget target) {
}
// Do not add rows before OFFSET to result if possible
boolean quickOffset = !fetchPercent;
if (sort != null && (!sortUsingIndex || isAnyDistinct())) {
if (sort != null && (numIndexSortedColumns != Integer.MAX_VALUE || isAnyDistinct())) {
result = createLocalResult(result);
result.setSortOrder(sort);
if (!sortUsingIndex) {
if (numIndexSortedColumns != Integer.MAX_VALUE) {
quickOffset = false;
}
}
Expand Down Expand Up @@ -1223,39 +1269,50 @@ public void preparePlan() {
}
}
if (sort != null && !isQuickAggregateQuery && !isGroupQuery) {
Index index = getSortIndex();
List<SortIndex> sortIndexes = getBestSortIndexes();
Index current = topTableFilter.getIndex();
if (index != null && current != null) {
if (current.getIndexType().isScan() || current == index) {
topTableFilter.setIndex(index);
if (!topTableFilter.hasInComparisons()) {
// in(select ...) and in(1,2,3) may return the key in
// another order
sortUsingIndex = true;
}
} else if (index.getIndexColumns() != null
&& index.getIndexColumns().length >= current
.getIndexColumns().length) {
IndexColumn[] sortColumns = index.getIndexColumns();
IndexColumn[] currentColumns = current.getIndexColumns();
boolean swapIndex = false;
for (int i = 0; i < currentColumns.length; i++) {
if (sortColumns[i].column != currentColumns[i].column) {
swapIndex = false;
if (sortIndexes != null && current != null) {
// the sort indexes are sorted by the length of the prefix they cover
// the first which matches should be picked
for (SortIndex sortIndex : sortIndexes) {
Index index = sortIndex.index;
if (current.getIndexType().isScan() || current == index) {
// free upgrade
topTableFilter.setIndex(index);
if (!topTableFilter.hasInComparisons()) {
// in(select ...) and in(1,2,3) may return the key in
// another order
numIndexSortedColumns = sortIndex.prefixCovered;
break;
}
if (sortColumns[i].sortType != currentColumns[i].sortType) {
swapIndex = true;
} else if (index.getIndexColumns() != null
&& index.getIndexColumns().length >= current
.getIndexColumns().length) {
// this sort index may have potential to be used as long as it
// satisfies the invariants from the currently picked index
IndexColumn[] sortColumns = index.getIndexColumns();
IndexColumn[] currentColumns = current.getIndexColumns();
boolean swapIndex = false;
for (int i = 0; i < currentColumns.length; i++) {
if (sortColumns[i].column != currentColumns[i].column) {
katzyn marked this conversation as resolved.
Show resolved Hide resolved
swapIndex = false;
break;
}
if (sortColumns[i].sortType != currentColumns[i].sortType) {
swapIndex = true;
}
}
if (swapIndex) {
// do the swap now as this is the best sort index we may use
topTableFilter.setIndex(index);
numIndexSortedColumns = sortIndex.prefixCovered;
break;
}
}
if (swapIndex) {
topTableFilter.setIndex(index);
sortUsingIndex = true;
}
}
}
if (sortUsingIndex && isForUpdate && !topTableFilter.getIndex().isRowIdIndex()) {
sortUsingIndex = false;
if (numIndexSortedColumns > 0 && isForUpdate && !topTableFilter.getIndex().isRowIdIndex()) {
katzyn marked this conversation as resolved.
Show resolved Hide resolved
numIndexSortedColumns = 0;
}
}
if (!isQuickAggregateQuery && isGroupQuery) {
Expand Down Expand Up @@ -1468,8 +1525,10 @@ public String getPlanSQL(int sqlFlags) {
if (isDistinctQuery) {
builder.append("\n/* distinct */");
}
if (sortUsingIndex) {
if (numIndexSortedColumns == Integer.MAX_VALUE) {
builder.append("\n/* index sorted */");
} else if (numIndexSortedColumns > 0) {
builder.append("\n/* incrementally index sorted */");
}
if (isGroupQuery) {
if (isGroupSortedQuery) {
Expand Down Expand Up @@ -1931,4 +1990,20 @@ protected Value[] fetchNextRow() {
}
}

static class SortIndex
{
Index index;
int prefixCovered;

public SortIndex(Index index, int prefixCovered) {
this.index = index;
this.prefixCovered = prefixCovered;
}

public Integer getPrefixCovered()
{
return prefixCovered;
}
}

}
14 changes: 13 additions & 1 deletion h2/src/main/org/h2/result/SortOrder.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,19 @@ public static void typeToString(StringBuilder builder, int type) {
*/
@Override
public int compare(Value[] a, Value[] b) {
for (int i = 0, len = queryColumnIndexes.length; i < len; i++) {
return compare(a, b, Integer.MAX_VALUE);
}

/**
* Compare two expression lists.
*
* @param a the first expression list
* @param b the second expression list
* @return the result of the comparison
*/
public int compare(Value[] a, Value[] b, int onlyFirst) {
int bound = Math.min(onlyFirst, queryColumnIndexes.length);
for (int i = 0; i < bound; i++) {
int idx = queryColumnIndexes[i];
int type = sortTypes[i];
Value ao = a[idx];
Expand Down
2 changes: 1 addition & 1 deletion h2/src/test/org/h2/test/db/TestTableEngines.java
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ private void testMultiColumnTreeSetIndex() throws SQLException {
checkPlan(stat, "select * from t order by c, b", "IDX_C_B_A");
checkPlan(stat, "select * from t order by b", "IDX_B_A");
checkPlan(stat, "select * from t order by b, a", "IDX_B_A");
checkPlan(stat, "select * from t order by b, c", "scan");
checkPlan(stat, "select * from t order by b, c", "IDX_B_A");
checkPlan(stat, "select * from t order by a, b", "scan");
checkPlan(stat, "select * from t order by a, c, b", "scan");

Expand Down
2 changes: 1 addition & 1 deletion h2/src/test/org/h2/test/scripts/queries/joins.sql
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ CREATE TABLE TESTB(ID IDENTITY);
> ok

explain SELECT TESTA.ID A, TESTB.ID B FROM TESTA, TESTB ORDER BY TESTA.ID, TESTB.ID;
>> SELECT "TESTA"."ID" AS "A", "TESTB"."ID" AS "B" FROM "PUBLIC"."TESTA" /* PUBLIC.TESTA.tableScan */ INNER JOIN "PUBLIC"."TESTB" /* PUBLIC.TESTB.tableScan */ ON 1=1 ORDER BY 1, 2
>> SELECT "TESTA"."ID" AS "A", "TESTB"."ID" AS "B" FROM "PUBLIC"."TESTA" /* PUBLIC.PRIMARY_KEY_4 */ INNER JOIN "PUBLIC"."TESTB" /* PUBLIC.TESTB.tableScan */ ON 1=1 ORDER BY 1, 2 /* incrementally index sorted */

DROP TABLE IF EXISTS TESTA, TESTB;
> ok
Expand Down
2 changes: 1 addition & 1 deletion h2/src/test/org/h2/test/scripts/queries/select.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1167,7 +1167,7 @@ SELECT A, 1 AS X, B FROM TEST ORDER BY A, X, B DESC;
> rows (ordered): 4

EXPLAIN SELECT A, 1 AS X, B FROM TEST ORDER BY A, X, B DESC;
>> SELECT "A", 1 AS "X", "B" FROM "PUBLIC"."TEST" /* PUBLIC.TEST.tableScan */ ORDER BY 1, 2, 3 DESC
>> SELECT "A", 1 AS "X", "B" FROM "PUBLIC"."TEST" /* PUBLIC.TEST_IDX */ ORDER BY 1, 2, 3 DESC /* incrementally index sorted */

DROP TABLE TEST;
> ok
Expand Down
2 changes: 1 addition & 1 deletion h2/src/test/org/h2/test/scripts/testScript.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2558,7 +2558,7 @@ INSERT INTO TEST VALUES(1, 'Hello');
> update count: 1

explain select t0.id, t1.id from test t0, test t1 order by t0.id, t1.id;
>> SELECT "T0"."ID", "T1"."ID" FROM "PUBLIC"."TEST" "T0" /* PUBLIC.TEST.tableScan */ INNER JOIN "PUBLIC"."TEST" "T1" /* PUBLIC.TEST.tableScan */ ON 1=1 ORDER BY 1, 2
>> SELECT "T0"."ID", "T1"."ID" FROM "PUBLIC"."TEST" "T0" /* PUBLIC.PRIMARY_KEY_2 */ INNER JOIN "PUBLIC"."TEST" "T1" /* PUBLIC.TEST.tableScan */ ON 1=1 ORDER BY 1, 2 /* incrementally index sorted */

INSERT INTO TEST VALUES(2, 'World');
> update count: 1
Expand Down