/** * Copyright (C) 2009-2013 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.sql.optimizer; import com.foundationdb.sql.parser.*; import com.foundationdb.sql.StandardException; import com.foundationdb.ais.model.Column; import com.foundationdb.ais.model.Index; import com.foundationdb.ais.model.IndexColumn; import com.foundationdb.ais.model.Table; import java.util.*; /** Eliminate DISTINCT from SELECT when result is already distinct. * * Derby has a somewhat different version of this, but it does not try * multiple result tables or outer joins. * * It would be nicer if this could be an actual rule, but it really * has to run before ASTStatementLoader to keep from trying to sort on * all the extra columns. * * Likewise, if this runs before SubqueryFlattener, it can remove a * DISTINCT from a derived table and allow it to flatten. If it runs * after, it will not be stopped by derived tables that do get * flattened. Really, the two need to cooperate more closely. */ public class DistinctEliminator { private SQLParserContext parserContext; private FromBaseTable unjoinedTable; public DistinctEliminator(SQLParserContext parserContext) { this.parserContext = parserContext; } public DMLStatementNode eliminate(DMLStatementNode stmt) throws StandardException { ResultSetNode resultSet = stmt.getResultSetNode(); if (resultSet.getNodeType() == NodeTypes.SELECT_NODE) { selectNode((SelectNode)resultSet); } return stmt; } protected void selectNode(SelectNode selectNode) throws StandardException { boolean foundSubquery = false; for (FromTable fromTable : selectNode.getFromList()) { if (fromTable instanceof FromSubquery) { ResultSetNode subquery = ((FromSubquery)fromTable).getSubquery(); if (subquery instanceof SelectNode) { selectNode((SelectNode)subquery); } foundSubquery = true; } } // May have eliminated from subquery, but can't from main one. if (foundSubquery) return; // Nothing more to do if not distinct or if grouped. if (!selectNode.isDistinct() || (selectNode.getGroupByList() != null)) return; unjoinedTable = null; ResultColumnList resultColumns = selectNode.getResultColumns(); AndNode whereConditions = (AndNode)selectNode.getWhereClause(); for (FromTable fromTable : selectNode.getFromList()) { if (!isTableDistinct(fromTable, resultColumns, whereConditions, null)) { return; } } // Some table needs to not just be joined, so that don't get fooled by // SELECT 1 FROM t1,t2 WHERE t1.pk = t2.pk if (unjoinedTable != null) // Everything looks distinct already. selectNode.clearDistinct(); } protected boolean isTableDistinct(FromTable fromTable, ResultColumnList resultColumns, AndNode whereConditions, AndNode joinConditions) throws StandardException { if (fromTable instanceof FromBaseTable) { TableBinding binding = (TableBinding)fromTable.getUserData(); if (binding == null) return false; return isTableDistinct((FromBaseTable)fromTable, binding, resultColumns, whereConditions, joinConditions); } else if (fromTable instanceof JoinNode) { return isJoinDistinct((JoinNode)fromTable, resultColumns, whereConditions); } else return false; } protected boolean isJoinDistinct(JoinNode join, ResultColumnList resultColumns, AndNode whereConditions) throws StandardException { ResultSetNode left = join.getLeftResultSet(); ResultSetNode right = join.getRightResultSet(); if (!((left instanceof FromTable) && (right instanceof FromTable))) return false; FromTable leftTable = (FromTable)left; FromTable rightTable = (FromTable)right; ValueNode joinClause = join.getJoinClause(); if ((joinClause != null) && !(joinClause instanceof AndNode)) return false; AndNode joinConditions = (AndNode)joinClause; if (join instanceof HalfOuterJoinNode) { if (((HalfOuterJoinNode)join).isRightOuterJoin()) { return (isTableDistinct(leftTable, resultColumns, null, joinConditions) && isTableDistinct(rightTable, resultColumns, whereConditions, null)); } else { return (isTableDistinct(leftTable, resultColumns, whereConditions, null) && isTableDistinct(rightTable, resultColumns, null, joinConditions)); } } else return (isTableDistinct(leftTable, resultColumns, whereConditions, joinConditions) && isTableDistinct(rightTable, resultColumns, whereConditions, joinConditions)); } protected boolean isTableDistinct(FromBaseTable table, TableBinding binding, ResultColumnList resultColumns, AndNode whereConditions, AndNode joinConditions) throws StandardException { boolean found = false; for (Index index : ((Table)binding.getTable()).getIndexes()) { if (!index.isUnique()) continue; Set<FromTable> joinTables = null; Set<FromTable> columnJoinTables = new HashSet<>(); boolean handled = true, joined = false; for (IndexColumn indexColumn : index.getKeyColumns()) { Column column = indexColumn.getColumn(); // A table's contribution is distinct if every column // in some unique index is not nullable and appears in // the select list. More joining (with the same // condition) won't introduce duplicates. if (!binding.isNullable() && !column.getNullable() && columnInResult(column, resultColumns)) { continue; } // A table is unique (occurs zero or one times) if // every column of some unique index participates in // an equality constraint either with a constant or // with a single other table. // At least some cases of unique index columns joined // to two or more other tables don't keep it unique, // such as: // (1,2) (2,3) (3,1) // (1,2) (2,4) (4,1) // So don't bother with those cases. columnJoinTables.clear(); if (!(columnInConditions(column, whereConditions, columnJoinTables) || columnInConditions(column, joinConditions, columnJoinTables))) { columnJoinTables.remove(table); if (columnJoinTables.isEmpty()) { handled = false; break; } joined = true; if (joinTables == null) joinTables = new HashSet<>(columnJoinTables); else { joinTables.retainAll(columnJoinTables); if (joinTables.isEmpty()) { handled = false; break; } } } } if (handled) { found = true; if (!joined && (unjoinedTable == null)) unjoinedTable = table; if (unjoinedTable != null) break; // If needed to join, try other indexes; they might not with same table. } } return found; } // Does the given column appear in the result set directly? protected boolean columnInResult(Column column, ResultColumnList resultColumns) throws StandardException { for (ResultColumn resultColumn : resultColumns) { if (isColumnReference(resultColumn.getExpression(), column)) { return true; } } return false; } // Is there some equality condition on this column in these conditions, // either to a constant (return true) or some other table(s) (return them)? protected boolean columnInConditions(Column column, AndNode conditions, Set<FromTable> joinTables) throws StandardException { if (conditions != null) { while (true) { ValueNode leftOperand = conditions.getLeftOperand(); ValueNode rightOperand = conditions.getRightOperand(); if (leftOperand.getNodeType() == NodeTypes.BINARY_EQUALS_OPERATOR_NODE) { BinaryComparisonOperatorNode equals = (BinaryComparisonOperatorNode)leftOperand; ValueNode otherOperand = null; if (isColumnReference(equals.getLeftOperand(), column)) otherOperand = equals.getRightOperand(); else if (isColumnReference(equals.getRightOperand(), column)) otherOperand = equals.getLeftOperand(); if ((otherOperand instanceof ConstantNode) || (otherOperand instanceof ParameterNode)) return true; else if (otherOperand instanceof ColumnReference) { ColumnBinding columnBinding = (ColumnBinding) ((ColumnReference)otherOperand).getUserData(); if (columnBinding != null) joinTables.add(columnBinding.getFromTable()); } } if (rightOperand instanceof AndNode) conditions = (AndNode)rightOperand; else break; } } return false; } // This is a reference to the given column? protected boolean isColumnReference(ValueNode value, Column column) throws StandardException { if (value instanceof ColumnReference) { ColumnBinding columnBinding = (ColumnBinding) ((ColumnReference)value).getUserData(); if ((columnBinding != null) && (column == columnBinding.getColumn())) { return true; } } return false; } }