Created
January 10, 2020 08:16
-
-
Save tkers/9a891070418685ac42d13be2966cdbee to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License, | |
* Version 1.0, and under the Eclipse Public License, Version 1.0 | |
* (http://h2database.com/html/license.html). | |
* Initial Developer: H2 Group | |
*/ | |
package org.h2.command.dml; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.HashMap; | |
import java.util.HashSet; | |
import org.h2.api.Trigger; | |
import org.h2.command.CommandInterface; | |
import org.h2.constant.ErrorCode; | |
import org.h2.constant.SysProperties; | |
import org.h2.engine.Constants; | |
import org.h2.engine.Database; | |
import org.h2.engine.Session; | |
import org.h2.expression.Comparison; | |
import org.h2.expression.ConditionAndOr; | |
import org.h2.expression.Expression; | |
import org.h2.expression.ExpressionColumn; | |
import org.h2.expression.ExpressionVisitor; | |
import org.h2.expression.Parameter; | |
import org.h2.expression.Wildcard; | |
import org.h2.index.Cursor; | |
import org.h2.index.Index; | |
import org.h2.index.IndexType; | |
import org.h2.message.DbException; | |
import org.h2.result.LocalResult; | |
import org.h2.result.ResultInterface; | |
import org.h2.result.ResultTarget; | |
import org.h2.result.Row; | |
import org.h2.result.SearchRow; | |
import org.h2.result.SortOrder; | |
import org.h2.table.Column; | |
import org.h2.table.ColumnResolver; | |
import org.h2.table.IndexColumn; | |
import org.h2.table.Table; | |
import org.h2.table.TableFilter; | |
import org.h2.util.New; | |
import org.h2.util.StatementBuilder; | |
import org.h2.util.StringUtils; | |
import org.h2.util.ValueHashMap; | |
import org.h2.value.Value; | |
import org.h2.value.ValueArray; | |
import org.h2.value.ValueNull; | |
/** | |
* This class represents a simple SELECT statement. | |
* | |
* For each select statement, | |
* visibleColumnCount <= distinctColumnCount <= expressionCount. | |
* The expression list count could include ORDER BY and GROUP BY expressions | |
* that are not in the select list. | |
* | |
* The call sequence is init(), mapColumns() if it's a subquery, prepare(). | |
* | |
* @author Thomas Mueller | |
* @author Joel Turkel (Group sorted query) | |
*/ | |
public class Select extends Query { | |
private TableFilter topTableFilter; | |
private ArrayList<TableFilter> filters = New.arrayList(); | |
private ArrayList<TableFilter> topFilters = New.arrayList(); | |
private ArrayList<Expression> expressions; | |
private Expression[] expressionArray; | |
private Expression having; | |
private Expression condition; | |
private int visibleColumnCount, distinctColumnCount; | |
private ArrayList<SelectOrderBy> orderList; | |
private ArrayList<Expression> group; | |
private int[] groupIndex; | |
private boolean[] groupByExpression; | |
private HashMap<Expression, Object> currentGroup; | |
private int havingIndex; | |
private boolean isGroupQuery, isGroupSortedQuery; | |
private boolean isForUpdate, isForUpdateMvcc; | |
private double cost; | |
private boolean isQuickAggregateQuery, isDistinctQuery; | |
private boolean isPrepared, checkInit; | |
private boolean sortUsingIndex; | |
private SortOrder sort; | |
private int currentGroupRowId; | |
public Select(Session session) { | |
super(session); | |
} | |
public void init() { | |
if (SysProperties.CHECK && checkInit) { | |
DbException.throwInternalError(); | |
} | |
expandColumnList(); | |
visibleColumnCount = expressions.size(); | |
ArrayList<String> expressionSQL; | |
if (orderList != null || group != null) { | |
expressionSQL = New.arrayList(); | |
for (int i = 0; i < visibleColumnCount; i++) { | |
Expression expr = expressions.get(i); | |
expr = expr.getNonAliasExpression(); | |
String sql = expr.getSQL(); | |
expressionSQL.add(sql); | |
} | |
} else { | |
expressionSQL = null; | |
} | |
if (orderList != null) { | |
initOrder(session, expressions, expressionSQL, orderList, visibleColumnCount, distinct, filters); | |
} | |
distinctColumnCount = expressions.size(); | |
if (having != null) { | |
expressions.add(having); | |
havingIndex = expressions.size() - 1; | |
having = null; | |
} else { | |
havingIndex = -1; | |
} | |
Database db = session.getDatabase(); | |
// first the select list (visible columns), | |
// then 'ORDER BY' expressions, | |
// then 'HAVING' expressions, | |
// and 'GROUP BY' expressions at the end | |
if (group != null) { | |
int size = group.size(); | |
int expSize = expressionSQL.size(); | |
groupIndex = new int[size]; | |
for (int i = 0; i < size; i++) { | |
Expression expr = group.get(i); | |
String sql = expr.getSQL(); | |
int found = -1; | |
for (int j = 0; j < expSize; j++) { | |
String s2 = expressionSQL.get(j); | |
if (db.equalsIdentifiers(s2, sql)) { | |
found = j; | |
break; | |
} | |
} | |
if (found < 0) { | |
// special case: GROUP BY a column alias | |
for (int j = 0; j < expSize; j++) { | |
Expression e = expressions.get(j); | |
if (db.equalsIdentifiers(sql, e.getAlias())) { | |
found = j; | |
break; | |
} | |
} | |
} | |
if (found < 0) { | |
int index = expressions.size(); | |
groupIndex[i] = index; | |
expressions.add(expr); | |
} else { | |
groupIndex[i] = found; | |
} | |
} | |
groupByExpression = new boolean[expressions.size()]; | |
for (int gi : groupIndex) { | |
groupByExpression[gi] = true; | |
} | |
group = null; | |
} | |
// map columns in select list and condition | |
for (TableFilter f : filters) { | |
for (Expression expr : expressions) { | |
expr.mapColumns(f, 0); | |
} | |
if (condition != null) { | |
condition.mapColumns(f, 0); | |
} | |
} | |
if (havingIndex >= 0) { | |
Expression expr = expressions.get(havingIndex); | |
SelectListColumnResolver res = new SelectListColumnResolver(this); | |
expr.mapColumns(res, 0); | |
} | |
checkInit = true; | |
} | |
public void prepare() { | |
if (isPrepared) { | |
// sometimes a subquery is prepared twice (CREATE TABLE AS SELECT) | |
return; | |
} | |
if (SysProperties.CHECK && !checkInit) { | |
DbException.throwInternalError("not initialized"); | |
} | |
if (orderList != null) { | |
sort = prepareOrder(orderList, expressions.size()); | |
orderList = null; | |
} | |
for (int i = 0; i < expressions.size(); i++) { | |
Expression e = expressions.get(i); | |
expressions.set(i, e.optimize(session)); | |
} | |
if (condition != null) { | |
condition = condition.optimize(session); | |
for (TableFilter f : filters) { | |
// outer joins: must not add index conditions such as | |
// "c is null" - example: | |
// create table parent(p int primary key) as select 1; | |
// create table child(c int primary key, pc int); | |
// insert into child values(2, 1); | |
// select p, c from parent | |
// left outer join child on p = pc where c is null; | |
if (!f.isJoinOuter() && !f.isJoinOuterIndirect()) { | |
condition.createIndexConditions(session, f); | |
} | |
} | |
} | |
if (isGroupQuery && groupIndex == null && havingIndex < 0 && filters.size() == 1) { | |
if (condition == null) { | |
Table t = filters.get(0).getTable(); | |
ExpressionVisitor optimizable = ExpressionVisitor.getOptimizableVisitor(t); | |
isQuickAggregateQuery = isEverything(optimizable); | |
} | |
} | |
cost = preparePlan(); | |
if (distinct && session.getDatabase().getSettings().optimizeDistinct && | |
!isGroupQuery && filters.size() == 1 && | |
expressions.size() == 1 && condition == null) { | |
Expression expr = expressions.get(0); | |
expr = expr.getNonAliasExpression(); | |
if (expr instanceof ExpressionColumn) { | |
Column column = ((ExpressionColumn) expr).getColumn(); | |
int selectivity = column.getSelectivity(); | |
Index columnIndex = topTableFilter.getTable().getIndexForColumn(column, true); | |
if (columnIndex != null && selectivity != Constants.SELECTIVITY_DEFAULT && selectivity < 20) { | |
// the first column must be ascending | |
boolean ascending = columnIndex.getIndexColumns()[0].sortType == SortOrder.ASCENDING; | |
Index current = topTableFilter.getIndex(); | |
// if another index is faster | |
if (columnIndex.canFindNext() && ascending && | |
(current == null || current.getIndexType().isScan() || columnIndex == current)) { | |
IndexType type = columnIndex.getIndexType(); | |
// hash indexes don't work, and unique single column indexes don't work | |
if (!type.isHash() && (!type.isUnique() || columnIndex.getColumns().length > 1)) { | |
topTableFilter.setIndex(columnIndex); | |
isDistinctQuery = true; | |
} | |
} | |
} | |
} | |
} | |
if (sort != null && !isQuickAggregateQuery && !isGroupQuery) { | |
Index index = getSortIndex(); | |
if (index != null) { | |
Index current = topTableFilter.getIndex(); | |
if (current.getIndexType().isScan() || current == index) { | |
topTableFilter.setIndex(index); | |
if (!topTableFilter.hasInComparisons()) { | |
// in(select ...) and in(1,2,3) my return the key in another order | |
sortUsingIndex = true; | |
} | |
} else if (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; | |
break; | |
} | |
if (sortColumns[i].sortType != currentColumns[i].sortType) { | |
swapIndex = true; | |
} | |
} | |
if (swapIndex) { | |
topTableFilter.setIndex(index); | |
sortUsingIndex = true; | |
} | |
} | |
} | |
} | |
if (!isQuickAggregateQuery && isGroupQuery && getGroupByExpressionCount() > 0) { | |
Index index = getGroupSortedIndex(); | |
Index current = topTableFilter.getIndex(); | |
if (index != null && (current.getIndexType().isScan() || current == index)) { | |
topTableFilter.setIndex(index); | |
isGroupSortedQuery = true; | |
} | |
} | |
expressionArray = new Expression[expressions.size()]; | |
expressions.toArray(expressionArray); | |
isPrepared = true; | |
} | |
/** | |
* Add a table to the query. | |
* | |
* @param filter the table to add | |
* @param isTop if the table can be the first table in the query plan | |
*/ | |
public void addTableFilter(TableFilter filter, boolean isTop) { | |
// Oracle doesn't check on duplicate aliases | |
// String alias = filter.getAlias(); | |
// if(filterNames.contains(alias)) { | |
// throw Message.getSQLException( | |
// ErrorCode.DUPLICATE_TABLE_ALIAS, alias); | |
// } | |
// filterNames.add(alias); | |
filters.add(filter); | |
if (isTop) { | |
topFilters.add(filter); | |
} | |
} | |
public ArrayList<TableFilter> getTopFilters() { | |
return topFilters; | |
} | |
public void setExpressions(ArrayList<Expression> expressions) { | |
this.expressions = expressions; | |
} | |
/** | |
* Called if this query contains aggregate functions. | |
*/ | |
public void setGroupQuery() { | |
isGroupQuery = true; | |
} | |
public void setGroupBy(ArrayList<Expression> group) { | |
this.group = group; | |
} | |
public HashMap<Expression, Object> getCurrentGroup() { | |
return currentGroup; | |
} | |
public int getCurrentGroupRowId() { | |
return currentGroupRowId; | |
} | |
public void setOrder(ArrayList<SelectOrderBy> order) { | |
orderList = order; | |
} | |
/** | |
* Add a condition to the list of conditions. | |
* | |
* @param cond the condition to add | |
*/ | |
public void addCondition(Expression cond) { | |
if (condition == null) { | |
condition = cond; | |
} else { | |
condition = new ConditionAndOr(ConditionAndOr.AND, cond, condition); | |
} | |
} | |
private void queryGroupSorted(int columnCount, ResultTarget result) { | |
int rowNumber = 0; | |
setCurrentRowNumber(0); | |
Value[] previousKeyValues = null; | |
while (topTableFilter.next()) { | |
setCurrentRowNumber(rowNumber + 1); | |
if (condition == null || Boolean.TRUE.equals(condition.getBooleanValue(session))) { | |
rowNumber++; | |
Value[] keyValues = new Value[groupIndex.length]; | |
// update group | |
for (int i = 0; i < groupIndex.length; i++) { | |
int idx = groupIndex[i]; | |
Expression expr = expressions.get(idx); | |
keyValues[i] = expr.getValue(session); | |
} | |
if (previousKeyValues == null) { | |
previousKeyValues = keyValues; | |
currentGroup = New.hashMap(); | |
} else if (!Arrays.equals(previousKeyValues, keyValues)) { | |
addGroupSortedRow(previousKeyValues, columnCount, result); | |
previousKeyValues = keyValues; | |
currentGroup = New.hashMap(); | |
} | |
currentGroupRowId++; | |
for (int i = 0; i < columnCount; i++) { | |
if (groupByExpression == null || !groupByExpression[i]) { | |
Expression expr = expressions.get(i); | |
expr.updateAggregate(session); | |
} | |
} | |
} | |
} | |
if (previousKeyValues != null) { | |
addGroupSortedRow(previousKeyValues, columnCount, result); | |
} | |
} | |
private void addGroupSortedRow(Value[] keyValues, int columnCount, ResultTarget result) { | |
Value[] row = new Value[columnCount]; | |
for (int j = 0; groupIndex != null && j < groupIndex.length; j++) { | |
row[groupIndex[j]] = keyValues[j]; | |
} | |
for (int j = 0; j < columnCount; j++) { | |
if (groupByExpression != null && groupByExpression[j]) { | |
continue; | |
} | |
Expression expr = expressions.get(j); | |
row[j] = expr.getValue(session); | |
} | |
if (isHavingNullOrFalse(row)) { | |
return; | |
} | |
row = keepOnlyDistinct(row, columnCount); | |
result.addRow(row); | |
} | |
private Value[] keepOnlyDistinct(Value[] row, int columnCount) { | |
if (columnCount == distinctColumnCount) { | |
return row; | |
} | |
// remove columns so that 'distinct' can filter duplicate rows | |
Value[] r2 = new Value[distinctColumnCount]; | |
System.arraycopy(row, 0, r2, 0, distinctColumnCount); | |
return r2; | |
} | |
private boolean isHavingNullOrFalse(Value[] row) { | |
if (havingIndex >= 0) { | |
Value v = row[havingIndex]; | |
if (v == ValueNull.INSTANCE) { | |
return true; | |
} | |
if (!Boolean.TRUE.equals(v.getBoolean())) { | |
return true; | |
} | |
} | |
return false; | |
} | |
private Index getGroupSortedIndex() { | |
if (groupIndex == null || groupByExpression == null) { | |
return null; | |
} | |
ArrayList<Index> indexes = topTableFilter.getTable().getIndexes(); | |
if (indexes != null) { | |
for (int i = 0, size = indexes.size(); i < size; i++) { | |
Index index = indexes.get(i); | |
if (index.getIndexType().isScan()) { | |
continue; | |
} | |
if (isGroupSortedIndex(topTableFilter, index)) { | |
return index; | |
} | |
} | |
} | |
return null; | |
} | |
private boolean isGroupSortedIndex(TableFilter tableFilter, Index index) { | |
// check that all the GROUP BY expressions are part of the index | |
Column[] indexColumns = index.getColumns(); | |
// also check that the first columns in the index are grouped | |
boolean[] grouped = new boolean[indexColumns.length]; | |
outerLoop: | |
for (int i = 0, size = expressions.size(); i < size; i++) { | |
if (!groupByExpression[i]) { | |
continue; | |
} | |
Expression expr = expressions.get(i).getNonAliasExpression(); | |
if (!(expr instanceof ExpressionColumn)) { | |
return false; | |
} | |
ExpressionColumn exprCol = (ExpressionColumn) expr; | |
for (int j = 0; j < indexColumns.length; ++j) { | |
if (tableFilter == exprCol.getTableFilter()) { | |
if (indexColumns[j].equals(exprCol.getColumn())) { | |
grouped[j] = true; | |
continue outerLoop; | |
} | |
} | |
} | |
// We didn't find a matching index column | |
// for one group by expression | |
return false; | |
} | |
// check that the first columns in the index are grouped | |
// good: index(a, b, c); group by b, a | |
// bad: index(a, b, c); group by a, c | |
for (int i = 1; i < grouped.length; i++) { | |
if (!grouped[i - 1] && grouped[i]) { | |
return false; | |
} | |
} | |
return true; | |
} | |
private int getGroupByExpressionCount() { | |
if (groupByExpression == null) { | |
return 0; | |
} | |
int count = 0; | |
for (boolean b : groupByExpression) { | |
if (b) { | |
++count; | |
} | |
} | |
return count; | |
} | |
private void queryGroup(int columnCount, LocalResult result) { | |
ValueHashMap<HashMap<Expression, Object>> groups = ValueHashMap.newInstance(); | |
int rowNumber = 0; | |
setCurrentRowNumber(0); | |
ValueArray defaultGroup = ValueArray.get(new Value[0]); | |
while (topTableFilter.next()) { | |
setCurrentRowNumber(rowNumber + 1); | |
if (condition == null || Boolean.TRUE.equals(condition.getBooleanValue(session))) { | |
Value key; | |
rowNumber++; | |
if (groupIndex == null) { | |
key = defaultGroup; | |
} else { | |
Value[] keyValues = new Value[groupIndex.length]; | |
// update group | |
for (int i = 0; i < groupIndex.length; i++) { | |
int idx = groupIndex[i]; | |
Expression expr = expressions.get(idx); | |
keyValues[i] = expr.getValue(session); | |
} | |
key = ValueArray.get(keyValues); | |
} | |
HashMap<Expression, Object> values = groups.get(key); | |
if (values == null) { | |
values = new HashMap<Expression, Object>(); | |
groups.put(key, values); | |
} | |
currentGroup = values; | |
currentGroupRowId++; | |
int len = columnCount; | |
for (int i = 0; i < len; i++) { | |
if (groupByExpression == null || !groupByExpression[i]) { | |
Expression expr = expressions.get(i); | |
expr.updateAggregate(session); | |
} | |
} | |
if (sampleSize > 0 && rowNumber >= sampleSize) { | |
break; | |
} | |
} | |
} | |
if (groupIndex == null && groups.size() == 0) { | |
groups.put(defaultGroup, new HashMap<Expression, Object>()); | |
} | |
ArrayList<Value> keys = groups.keys(); | |
for (Value v : keys) { | |
ValueArray key = (ValueArray) v; | |
currentGroup = groups.get(key); | |
Value[] keyValues = key.getList(); | |
Value[] row = new Value[columnCount]; | |
for (int j = 0; groupIndex != null && j < groupIndex.length; j++) { | |
row[groupIndex[j]] = keyValues[j]; | |
} | |
for (int j = 0; j < columnCount; j++) { | |
if (groupByExpression != null && groupByExpression[j]) { | |
continue; | |
} | |
Expression expr = expressions.get(j); | |
row[j] = expr.getValue(session); | |
} | |
if (isHavingNullOrFalse(row)) { | |
continue; | |
} | |
row = keepOnlyDistinct(row, columnCount); | |
result.addRow(row); | |
} | |
} | |
/** | |
* 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) | |
* | |
* @return the index if one is found | |
*/ | |
private Index getSortIndex() { | |
if (sort == null) { | |
return null; | |
} | |
ArrayList<Column> sortColumns = New.arrayList(); | |
for (int idx : sort.getIndexes()) { | |
if (idx < 0 || idx >= expressions.size()) { | |
throw DbException.getInvalidValueException("ORDER BY", idx + 1); | |
} | |
Expression expr = expressions.get(idx); | |
expr = expr.getNonAliasExpression(); | |
if (expr.isConstant()) { | |
continue; | |
} | |
if (!(expr instanceof ExpressionColumn)) { | |
return null; | |
} | |
ExpressionColumn exprCol = (ExpressionColumn) expr; | |
if (exprCol.getTableFilter() != topTableFilter) { | |
return null; | |
} | |
sortColumns.add(exprCol.getColumn()); | |
} | |
Column[] sortCols = sortColumns.toArray(new Column[sortColumns.size()]); | |
int[] sortTypes = sort.getSortTypes(); | |
if (sortCols.length == 0) { | |
// sort just on constants - can use scan index | |
return topTableFilter.getTable().getScanIndex(session); | |
} | |
ArrayList<Index> list = topTableFilter.getTable().getIndexes(); | |
if (list != null) { | |
for (int i = 0, size = list.size(); i < size; i++) { | |
Index index = list.get(i); | |
if (index.getCreateSQL() == null) { | |
// can't use the scan index | |
continue; | |
} | |
if (index.getIndexType().isHash()) { | |
continue; | |
} | |
IndexColumn[] indexCols = index.getIndexColumns(); | |
if (indexCols.length < sortCols.length) { | |
continue; | |
} | |
boolean ok = true; | |
for (int j = 0; j < sortCols.length; j++) { | |
// the index and the sort order must start | |
// with the exact same columns | |
IndexColumn idxCol = indexCols[j]; | |
Column sortCol = sortCols[j]; | |
if (idxCol.column != sortCol) { | |
ok = false; | |
break; | |
} | |
if (idxCol.sortType != sortTypes[j]) { | |
// NULL FIRST for ascending and NULLS LAST | |
// for descending would actually match the default | |
ok = false; | |
break; | |
} | |
} | |
if (ok) { | |
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; | |
} | |
} | |
return null; | |
} | |
private void queryDistinct(ResultTarget result, long limitRows) { | |
// limitRows must be long, otherwise we get an int overflow | |
// if limitRows is at or near Integer.MAX_VALUE | |
// limitRows is never 0 here | |
if (limitRows > 0 && offsetExpr != null) { | |
int offset = offsetExpr.getValue(session).getInt(); | |
if (offset > 0) { | |
limitRows += offset; | |
} | |
} | |
int rowNumber = 0; | |
setCurrentRowNumber(0); | |
Index index = topTableFilter.getIndex(); | |
SearchRow first = null; | |
int columnIndex = index.getColumns()[0].getColumnId(); | |
while (true) { | |
setCurrentRowNumber(rowNumber + 1); | |
Cursor cursor = index.findNext(session, first, null); | |
if (!cursor.next()) { | |
break; | |
} | |
SearchRow found = cursor.getSearchRow(); | |
Value value = found.getValue(columnIndex); | |
if (first == null) { | |
first = topTableFilter.getTable().getTemplateSimpleRow(true); | |
} | |
first.setValue(columnIndex, value); | |
Value[] row = { value }; | |
result.addRow(row); | |
rowNumber++; | |
if ((sort == null || sortUsingIndex) && limitRows > 0 && rowNumber >= limitRows) { | |
break; | |
} | |
if (sampleSize > 0 && rowNumber >= sampleSize) { | |
break; | |
} | |
} | |
} | |
private void queryFlat(int columnCount, ResultTarget result, long limitRows) { | |
// limitRows must be long, otherwise we get an int overflow | |
// if limitRows is at or near Integer.MAX_VALUE | |
// limitRows is never 0 here | |
if (limitRows > 0 && offsetExpr != null) { | |
int offset = offsetExpr.getValue(session).getInt(); | |
if (offset > 0) { | |
limitRows += offset; | |
} | |
} | |
int rowNumber = 0; | |
setCurrentRowNumber(0); | |
ArrayList<Row> forUpdateRows = null; | |
if (isForUpdateMvcc) { | |
forUpdateRows = New.arrayList(); | |
} | |
while (topTableFilter.next()) { | |
setCurrentRowNumber(rowNumber + 1); | |
if (condition == null || Boolean.TRUE.equals(condition.getBooleanValue(session))) { | |
Value[] row = new Value[columnCount]; | |
for (int i = 0; i < columnCount; i++) { | |
Expression expr = expressions.get(i); | |
row[i] = expr.getValue(session); | |
} | |
if (isForUpdateMvcc) { | |
topTableFilter.lockRowAdd(forUpdateRows); | |
} | |
result.addRow(row); | |
rowNumber++; | |
if ((sort == null || sortUsingIndex) && limitRows > 0 && result.getRowCount() >= limitRows) { | |
break; | |
} | |
if (sampleSize > 0 && rowNumber >= sampleSize) { | |
break; | |
} | |
} | |
} | |
if (isForUpdateMvcc) { | |
topTableFilter.lockRows(forUpdateRows); | |
} | |
} | |
private void queryQuick(int columnCount, ResultTarget result) { | |
Value[] row = new Value[columnCount]; | |
for (int i = 0; i < columnCount; i++) { | |
Expression expr = expressions.get(i); | |
row[i] = expr.getValue(session); | |
} | |
result.addRow(row); | |
} | |
public ResultInterface queryMeta() { | |
LocalResult result = new LocalResult(session, expressionArray, visibleColumnCount); | |
result.done(); | |
return result; | |
} | |
protected LocalResult queryWithoutCache(int maxRows, ResultTarget target) { | |
int limitRows = maxRows == 0 ? -1 : maxRows; | |
if (limitExpr != null) { | |
Value v = limitExpr.getValue(session); | |
int l = v == ValueNull.INSTANCE ? -1 : v.getInt(); | |
if (limitRows < 0) { | |
limitRows = l; | |
} else if (l >= 0) { | |
limitRows = Math.min(l, limitRows); | |
} | |
} | |
int columnCount = expressions.size(); | |
LocalResult result = null; | |
if (target == null || !session.getDatabase().getSettings().optimizeInsertFromSelect) { | |
result = createLocalResult(result); | |
} | |
if (sort != null && (!sortUsingIndex || distinct)) { | |
result = createLocalResult(result); | |
result.setSortOrder(sort); | |
} | |
if (distinct && !isDistinctQuery) { | |
result = createLocalResult(result); | |
result.setDistinct(); | |
} | |
if (randomAccessResult) { | |
result = createLocalResult(result); | |
result.setRandomAccess(); | |
} | |
if (isGroupQuery && !isGroupSortedQuery) { | |
result = createLocalResult(result); | |
} | |
if (limitRows >= 0 || offsetExpr != null) { | |
result = createLocalResult(result); | |
} | |
topTableFilter.startQuery(session); | |
topTableFilter.reset(); | |
boolean exclusive = isForUpdate && !isForUpdateMvcc; | |
if (isForUpdateMvcc) { | |
if (isGroupQuery) { | |
throw DbException.getUnsupportedException("FOR UPDATE && GROUP"); | |
} else if (distinct) { | |
throw DbException.getUnsupportedException("FOR UPDATE && DISTINCT"); | |
} else if (isQuickAggregateQuery) { | |
throw DbException.getUnsupportedException("FOR UPDATE && AGGREGATE"); | |
} else if (topTableFilter.getJoin() != null) { | |
throw DbException.getUnsupportedException("FOR UPDATE && JOIN"); | |
} else if (topTableFilter.getJoin() != null) { | |
throw DbException.getUnsupportedException("FOR UPDATE && JOIN"); | |
} | |
} | |
topTableFilter.lock(session, exclusive, exclusive); | |
ResultTarget to = result != null ? result : target; | |
if (limitRows != 0) { | |
if (isQuickAggregateQuery) { | |
queryQuick(columnCount, to); | |
} else if (isGroupQuery) { | |
if (isGroupSortedQuery) { | |
queryGroupSorted(columnCount, to); | |
} else { | |
queryGroup(columnCount, result); | |
} | |
} else if (isDistinctQuery) { | |
queryDistinct(to, limitRows); | |
} else { | |
queryFlat(columnCount, to, limitRows); | |
} | |
} | |
if (offsetExpr != null) { | |
result.setOffset(offsetExpr.getValue(session).getInt()); | |
} | |
if (limitRows >= 0) { | |
result.setLimit(limitRows); | |
} | |
if (result != null) { | |
result.done(); | |
if (target != null) { | |
while (result.next()) { | |
target.addRow(result.currentRow()); | |
} | |
result.close(); | |
return null; | |
} | |
return result; | |
} | |
return null; | |
} | |
private LocalResult createLocalResult(LocalResult old) { | |
return old != null ? old : new LocalResult(session, expressionArray, visibleColumnCount); | |
} | |
private void expandColumnList() { | |
Database db = session.getDatabase(); | |
// the expressions may change within the loop | |
for (int i = 0; i < expressions.size(); i++) { | |
Expression expr = expressions.get(i); | |
if (!expr.isWildcard()) { | |
continue; | |
} | |
String schemaName = expr.getSchemaName(); | |
String tableAlias = expr.getTableAlias(); | |
if (tableAlias == null) { | |
int temp = i; | |
expressions.remove(i); | |
for (TableFilter filter : filters) { | |
Wildcard c2 = new Wildcard(filter.getTable().getSchema().getName(), filter.getTableAlias()); | |
expressions.add(i++, c2); | |
} | |
i = temp - 1; | |
} else { | |
TableFilter filter = null; | |
for (TableFilter f : filters) { | |
if (db.equalsIdentifiers(tableAlias, f.getTableAlias())) { | |
if (schemaName == null || db.equalsIdentifiers(schemaName, f.getSchemaName())) { | |
filter = f; | |
break; | |
} | |
} | |
} | |
if (filter == null) { | |
throw DbException.get(ErrorCode.TABLE_OR_VIEW_NOT_FOUND_1, tableAlias); | |
} | |
Table t = filter.getTable(); | |
String alias = filter.getTableAlias(); | |
expressions.remove(i); | |
Column[] columns = t.getColumns(); | |
for (Column c : columns) { | |
if (filter.isNaturalJoinColumn(c)) { | |
continue; | |
} | |
ExpressionColumn ec = new ExpressionColumn(session.getDatabase(), null, alias, c.getName()); | |
expressions.add(i++, ec); | |
} | |
i--; | |
} | |
} | |
} | |
public double getCost() { | |
return cost; | |
} | |
public HashSet<Table> getTables() { | |
HashSet<Table> set = New.hashSet(); | |
for (TableFilter filter : filters) { | |
set.add(filter.getTable()); | |
} | |
return set; | |
} | |
public void fireBeforeSelectTriggers() { | |
for (int i = 0, size = filters.size(); i < size; i++) { | |
TableFilter filter = filters.get(i); | |
filter.getTable().fire(session, Trigger.SELECT, true); | |
} | |
} | |
private double preparePlan() { | |
TableFilter[] topArray = topFilters.toArray(new TableFilter[topFilters.size()]); | |
for (TableFilter t : topArray) { | |
t.setFullCondition(condition); | |
} | |
Optimizer optimizer = new Optimizer(topArray, condition, session); | |
optimizer.optimize(); | |
topTableFilter = optimizer.getTopFilter(); | |
double planCost = optimizer.getCost(); | |
setEvaluatableRecursive(topTableFilter); | |
topTableFilter.prepare(); | |
return planCost; | |
} | |
private void setEvaluatableRecursive(TableFilter f) { | |
for (; f != null; f = f.getJoin()) { | |
f.setEvaluatable(f, true); | |
if (condition != null) { | |
condition.setEvaluatable(f, true); | |
} | |
TableFilter n = f.getNestedJoin(); | |
if (n != null) { | |
setEvaluatableRecursive(n); | |
} | |
Expression on = f.getJoinCondition(); | |
if (on != null) { | |
if (!on.isEverything(ExpressionVisitor.EVALUATABLE_VISITOR)) { | |
if (session.getDatabase().getSettings().nestedJoins) { | |
// need to check that all added are bound to a table | |
on = on.optimize(session); | |
if (!f.isJoinOuter() && !f.isJoinOuterIndirect()) { | |
f.removeJoinCondition(); | |
addCondition(on); | |
} | |
} else { | |
if (f.isJoinOuter()) { | |
// this will check if all columns exist - it may or may not throw an exception | |
on = on.optimize(session); | |
// it is not supported even if the columns exist | |
throw DbException.get(ErrorCode.UNSUPPORTED_OUTER_JOIN_CONDITION_1, on.getSQL()); | |
} | |
f.removeJoinCondition(); | |
// need to check that all added are bound to a table | |
on = on.optimize(session); | |
addCondition(on); | |
} | |
} | |
} | |
on = f.getFilterCondition(); | |
if (on != null) { | |
if (!on.isEverything(ExpressionVisitor.EVALUATABLE_VISITOR)) { | |
f.removeFilterCondition(); | |
addCondition(on); | |
} | |
} | |
// this is only important for subqueries, so they know | |
// the result columns are evaluatable | |
for (Expression e : expressions) { | |
e.setEvaluatable(f, true); | |
} | |
} | |
} | |
public String getPlanSQL() { | |
// can not use the field sqlStatement because the parameter | |
// indexes may be incorrect: ? may be in fact ?2 for a subquery | |
// but indexes may be set manually as well | |
Expression[] exprList = expressions.toArray(new Expression[expressions.size()]); | |
StatementBuilder buff = new StatementBuilder("SELECT"); | |
if (distinct) { | |
buff.append(" DISTINCT"); | |
} | |
for (int i = 0; i < visibleColumnCount; i++) { | |
buff.appendExceptFirst(","); | |
buff.append('\n'); | |
buff.append(StringUtils.indent(exprList[i].getSQL(), 4, false)); | |
} | |
buff.append("\nFROM "); | |
TableFilter filter = topTableFilter; | |
if (filter != null) { | |
buff.resetCount(); | |
int i = 0; | |
do { | |
buff.appendExceptFirst("\n"); | |
buff.append(filter.getPlanSQL(i++ > 0)); | |
filter = filter.getJoin(); | |
} while (filter != null); | |
} else { | |
buff.resetCount(); | |
int i = 0; | |
for (TableFilter f : topFilters) { | |
do { | |
buff.appendExceptFirst("\n"); | |
buff.append(f.getPlanSQL(i++ > 0)); | |
f = f.getJoin(); | |
} while (f != null); | |
} | |
} | |
if (condition != null) { | |
buff.append("\nWHERE ").append(StringUtils.unEnclose(condition.getSQL())); | |
} | |
if (groupIndex != null) { | |
buff.append("\nGROUP BY "); | |
buff.resetCount(); | |
for (int gi : groupIndex) { | |
Expression g = exprList[gi]; | |
g = g.getNonAliasExpression(); | |
buff.appendExceptFirst(", "); | |
buff.append(StringUtils.unEnclose(g.getSQL())); | |
} | |
} | |
if (group != null) { | |
buff.append("\nGROUP BY "); | |
buff.resetCount(); | |
for (Expression g : group) { | |
buff.appendExceptFirst(", "); | |
buff.append(StringUtils.unEnclose(g.getSQL())); | |
} | |
} | |
if (having != null) { | |
// could be set in addGlobalCondition | |
// in this case the query is not run directly, just getPlanSQL is | |
// called | |
Expression h = having; | |
buff.append("\nHAVING ").append(StringUtils.unEnclose(h.getSQL())); | |
} else if (havingIndex >= 0) { | |
Expression h = exprList[havingIndex]; | |
buff.append("\nHAVING ").append(StringUtils.unEnclose(h.getSQL())); | |
} | |
if (sort != null) { | |
buff.append("\nORDER BY ").append(sort.getSQL(exprList, visibleColumnCount)); | |
} | |
if (orderList != null) { | |
buff.append("\nORDER BY "); | |
buff.resetCount(); | |
for (SelectOrderBy o : orderList) { | |
buff.appendExceptFirst(", "); | |
buff.append(StringUtils.unEnclose(o.getSQL())); | |
} | |
} | |
if (limitExpr != null) { | |
buff.append("\nLIMIT ").append(StringUtils.unEnclose(limitExpr.getSQL())); | |
if (offsetExpr != null) { | |
buff.append(" OFFSET ").append(StringUtils.unEnclose(offsetExpr.getSQL())); | |
} | |
} | |
if (sampleSize != 0) { | |
buff.append("\nSAMPLE_SIZE ").append(sampleSize); | |
} | |
if (isForUpdate) { | |
buff.append("\nFOR UPDATE"); | |
} | |
if (isQuickAggregateQuery) { | |
buff.append("\n/* direct lookup */"); | |
} | |
if (isDistinctQuery) { | |
buff.append("\n/* distinct */"); | |
} | |
if (sortUsingIndex) { | |
buff.append("\n/* index sorted */"); | |
} | |
if (isGroupQuery) { | |
if (isGroupSortedQuery) { | |
buff.append("\n/* group sorted */"); | |
} | |
} | |
// buff.append("\n/* cost: " + cost + " */"); | |
return buff.toString(); | |
} | |
public void setHaving(Expression having) { | |
this.having = having; | |
} | |
public int getColumnCount() { | |
return visibleColumnCount; | |
} | |
public TableFilter getTopTableFilter() { | |
return topTableFilter; | |
} | |
public ArrayList<Expression> getExpressions() { | |
return expressions; | |
} | |
public void setForUpdate(boolean b) { | |
this.isForUpdate = b; | |
if (session.getDatabase().getSettings().selectForUpdateMvcc && session.getDatabase().isMultiVersion()) { | |
isForUpdateMvcc = b; | |
} | |
} | |
public void mapColumns(ColumnResolver resolver, int level) { | |
for (Expression e : expressions) { | |
e.mapColumns(resolver, level); | |
} | |
if (condition != null) { | |
condition.mapColumns(resolver, level); | |
} | |
} | |
public void setEvaluatable(TableFilter tableFilter, boolean b) { | |
for (Expression e : expressions) { | |
e.setEvaluatable(tableFilter, b); | |
} | |
if (condition != null) { | |
condition.setEvaluatable(tableFilter, b); | |
} | |
} | |
/** | |
* Check if this is an aggregate query with direct lookup, for example a | |
* query of the type SELECT COUNT(*) FROM TEST or | |
* SELECT MAX(ID) FROM TEST. | |
* | |
* @return true if a direct lookup is possible | |
*/ | |
public boolean isQuickAggregateQuery() { | |
return isQuickAggregateQuery; | |
} | |
public void addGlobalCondition(Parameter param, int columnId, int comparisonType) { | |
addParameter(param); | |
Expression comp; | |
Expression col = expressions.get(columnId); | |
col = col.getNonAliasExpression(); | |
if (col.isEverything(ExpressionVisitor.QUERY_COMPARABLE_VISITOR)) { | |
comp = new Comparison(session, comparisonType, col, param); | |
} else { | |
// this condition will always evaluate to true, but need to | |
// add the parameter, so it can be set later | |
comp = new Comparison(session, Comparison.EQUAL_NULL_SAFE, param, param); | |
} | |
comp = comp.optimize(session); | |
boolean addToCondition = true; | |
if (isGroupQuery) { | |
addToCondition = false; | |
for (int i = 0; groupIndex != null && i < groupIndex.length; i++) { | |
if (groupIndex[i] == columnId) { | |
addToCondition = true; | |
break; | |
} | |
} | |
if (!addToCondition) { | |
if (havingIndex >= 0) { | |
having = expressions.get(havingIndex); | |
} | |
if (having == null) { | |
having = comp; | |
} else { | |
having = new ConditionAndOr(ConditionAndOr.AND, having, comp); | |
} | |
} | |
} | |
if (addToCondition) { | |
if (condition == null) { | |
condition = comp; | |
} else { | |
condition = new ConditionAndOr(ConditionAndOr.AND, condition, comp); | |
} | |
} | |
} | |
public void updateAggregate(Session s) { | |
for (Expression e : expressions) { | |
e.updateAggregate(s); | |
} | |
if (condition != null) { | |
condition.updateAggregate(s); | |
} | |
if (having != null) { | |
having.updateAggregate(s); | |
} | |
} | |
public boolean isEverything(ExpressionVisitor visitor) { | |
switch(visitor.getType()) { | |
case ExpressionVisitor.DETERMINISTIC: { | |
if (isForUpdate) { | |
return false; | |
} | |
for (int i = 0, size = filters.size(); i < size; i++) { | |
TableFilter f = filters.get(i); | |
if (!f.getTable().isDeterministic()) { | |
return false; | |
} | |
} | |
break; | |
} | |
case ExpressionVisitor.SET_MAX_DATA_MODIFICATION_ID: { | |
for (int i = 0, size = filters.size(); i < size; i++) { | |
TableFilter f = filters.get(i); | |
long m = f.getTable().getMaxDataModificationId(); | |
visitor.addDataModificationId(m); | |
} | |
break; | |
} | |
case ExpressionVisitor.EVALUATABLE: { | |
if (!session.getDatabase().getSettings().optimizeEvaluatableSubqueries) { | |
return false; | |
} | |
break; | |
} | |
case ExpressionVisitor.GET_DEPENDENCIES: { | |
for (int i = 0, size = filters.size(); i < size; i++) { | |
TableFilter f = filters.get(i); | |
Table table = f.getTable(); | |
visitor.addDependency(table); | |
table.addDependencies(visitor.getDependencies()); | |
} | |
break; | |
} | |
default: | |
} | |
ExpressionVisitor v2 = visitor.incrementQueryLevel(1); | |
boolean result = true; | |
for (int i = 0, size = expressions.size(); i < size; i++) { | |
Expression e = expressions.get(i); | |
if (!e.isEverything(v2)) { | |
result = false; | |
break; | |
} | |
} | |
if (result && condition != null && !condition.isEverything(v2)) { | |
result = false; | |
} | |
if (result && having != null && !having.isEverything(v2)) { | |
result = false; | |
} | |
return result; | |
} | |
public boolean isReadOnly() { | |
return isEverything(ExpressionVisitor.READONLY_VISITOR); | |
} | |
public boolean isCacheable() { | |
return !isForUpdate; | |
} | |
public int getType() { | |
return CommandInterface.SELECT; | |
} | |
public boolean allowGlobalConditions() { | |
if (offsetExpr == null && (limitExpr == null || sort == null)) { | |
return true; | |
} | |
return false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment