HHH-14153 further optimization for single-table HQL update

This extends the optimization for single-table HQL bulk
updates to the case where the where clause touches multiple
tables and we can use a subselect to collect the ids that
we need to update.
This commit is contained in:
Gavin King 2020-08-23 13:54:44 +02:00 committed by Andrea Boriero
parent 264e71a916
commit 423697026d
7 changed files with 222 additions and 61 deletions

View File

@ -15,6 +15,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.hibernate.HibernateException; import org.hibernate.HibernateException;
import org.hibernate.MappingException; import org.hibernate.MappingException;
@ -33,8 +34,9 @@ import org.hibernate.hql.internal.ast.exec.DeleteExecutor;
import org.hibernate.hql.internal.ast.exec.InsertExecutor; import org.hibernate.hql.internal.ast.exec.InsertExecutor;
import org.hibernate.hql.internal.ast.exec.MultiTableDeleteExecutor; import org.hibernate.hql.internal.ast.exec.MultiTableDeleteExecutor;
import org.hibernate.hql.internal.ast.exec.MultiTableUpdateExecutor; import org.hibernate.hql.internal.ast.exec.MultiTableUpdateExecutor;
import org.hibernate.hql.internal.ast.exec.SimpleUpdateExecutor;
import org.hibernate.hql.internal.ast.exec.StatementExecutor; import org.hibernate.hql.internal.ast.exec.StatementExecutor;
import org.hibernate.hql.internal.ast.exec.UpdateExecutor; import org.hibernate.hql.internal.ast.exec.IdSubselectUpdateExecutor;
import org.hibernate.hql.internal.ast.tree.AggregatedSelectExpression; import org.hibernate.hql.internal.ast.tree.AggregatedSelectExpression;
import org.hibernate.hql.internal.ast.tree.FromElement; import org.hibernate.hql.internal.ast.tree.FromElement;
import org.hibernate.hql.internal.ast.tree.QueryNode; import org.hibernate.hql.internal.ast.tree.QueryNode;
@ -61,8 +63,6 @@ import antlr.RecognitionException;
import antlr.TokenStreamException; import antlr.TokenStreamException;
import antlr.collections.AST; import antlr.collections.AST;
import static java.util.Collections.singleton;
/** /**
* A QueryTranslator that uses an Antlr-based parser. * A QueryTranslator that uses an Antlr-based parser.
* *
@ -609,13 +609,14 @@ public class QueryTranslatorImpl implements FilterTranslator {
final FromElement fromElement = walker.getFinalFromClause().getFromElement(); final FromElement fromElement = walker.getFinalFromClause().getFromElement();
final Queryable persister = fromElement.getQueryable(); final Queryable persister = fromElement.getQueryable();
boolean affectsExtraTables = persister.isMultiTable() if ( persister.isMultiTable() && affectsExtraTables( walker, persister ) ) {
&& !singleton( persister.getTableName() ).containsAll( walker.getQuerySpaces() );
if ( affectsExtraTables ) {
return new MultiTableUpdateExecutor( walker ); return new MultiTableUpdateExecutor( walker );
} }
else if ( persister.isMultiTable() && walker.getQuerySpaces().size() > 1 ) {
return new IdSubselectUpdateExecutor( walker );
}
else { else {
return new UpdateExecutor( walker ); return new SimpleUpdateExecutor( walker );
} }
} }
else if ( walker.getStatementType() == HqlSqlTokenTypes.INSERT ) { else if ( walker.getStatementType() == HqlSqlTokenTypes.INSERT ) {
@ -625,6 +626,16 @@ public class QueryTranslatorImpl implements FilterTranslator {
throw new QueryException( "Unexpected statement type" ); throw new QueryException( "Unexpected statement type" );
} }
} }
private static boolean affectsExtraTables(HqlSqlWalker walker, Queryable persister) {
String[] tableNames = persister.getConstraintOrderedTableNameClosure();
return IntStream.range( 0, tableNames.length ).filter(
table -> walker.getAssignmentSpecifications().stream().anyMatch(
assign -> assign.affectsTable( tableNames[table] )
)
).count() > 1;
}
@Override @Override
public ParameterTranslations getParameterTranslations() { public ParameterTranslations getParameterTranslations() {
if ( paramTranslations == null ) { if ( paramTranslations == null ) {

View File

@ -64,9 +64,7 @@ public abstract class BasicExecutor implements StatementExecutor {
); );
} }
int doExecute(String sql, QueryParameters parameters, int doExecute(String sql, QueryParameters parameters, List<ParameterSpecification> parameterSpecifications, SharedSessionContractImplementor session)
List<ParameterSpecification> parameterSpecifications,
SharedSessionContractImplementor session)
throws HibernateException{ throws HibernateException{
try { try {
PreparedStatement st = null; PreparedStatement st = null;

View File

@ -0,0 +1,138 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.hql.internal.ast.exec;
import antlr.RecognitionException;
import org.hibernate.AssertionFailure;
import org.hibernate.HibernateException;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.MySQLDialect;
import org.hibernate.hql.internal.ast.HqlSqlWalker;
import org.hibernate.hql.internal.ast.QuerySyntaxException;
import org.hibernate.hql.internal.ast.SqlGenerator;
import org.hibernate.hql.internal.ast.tree.AssignmentSpecification;
import org.hibernate.hql.internal.ast.tree.UpdateStatement;
import org.hibernate.persister.entity.Queryable;
import org.hibernate.sql.Select;
import org.hibernate.sql.SelectValues;
import org.hibernate.sql.Update;
import java.util.List;
import java.util.stream.IntStream;
/**
* Executes HQL bulk updates against a single table, using a subselect
* against multiple tables to collect ids, which is needed when the
* where condition of the query touches columns from multiple tables.
*
* @author Gavin King
*/
public class IdSubselectUpdateExecutor extends BasicExecutor {
public IdSubselectUpdateExecutor(HqlSqlWalker walker) {
super( walker.getFinalFromClause().getFromElement().getQueryable() );
Dialect dialect = walker.getDialect();
UpdateStatement updateStatement = (UpdateStatement) walker.getAST();
List<AssignmentSpecification> assignments = walker.getAssignmentSpecifications();
String whereClause;
if ( updateStatement.getWhereClause().getNumberOfChildren() == 0 ) {
whereClause = "";
}
else {
try {
SqlGenerator gen = new SqlGenerator( walker.getSessionFactoryHelper().getFactory() );
gen.whereClause( updateStatement.getWhereClause() );
gen.getParseErrorHandler().throwQueryException();
whereClause = gen.getSQL().substring( 7 ); // strip the " where "
}
catch ( RecognitionException e ) {
throw new HibernateException( "Unable to generate id select for DML operation", e );
}
}
String tableAlias = updateStatement.getFromClause().getFromElement().getTableAlias();
String idSelect = generateIdSelect( tableAlias, whereClause, dialect, persister );
String[] tableNames = persister.getConstraintOrderedTableNameClosure();
String[][] columnNames = persister.getContraintOrderedTableKeyColumnClosure();
int[] affectedTables =
IntStream.range( 0, tableNames.length ).filter(
table -> assignments.stream().anyMatch(
assign -> assign.affectsTable( tableNames[table] )
)
).toArray();
if ( affectedTables.length > 1 ) {
throw new AssertionFailure("more than one affected table");
}
int affectedTable = affectedTables[0];
String tableName = tableNames[affectedTable];
String idColumnNames = String.join( ", ", columnNames[affectedTable] );
Update update = new Update( dialect ).setTableName( tableName );
if ( dialect instanceof MySQLDialect) {
//MySQL needs an extra subselect to hack the query optimizer
String selectedIdColumns = String.join( ", ", persister.getIdentifierColumnNames() );
update.setWhere( "(" + idColumnNames + ") in (select " + selectedIdColumns + " from (" + idSelect + ") as ht_ids)" );
}
else {
update.setWhere( "(" + idColumnNames + ") in (" + idSelect + ")" );
}
for ( AssignmentSpecification assignment: assignments ) {
update.appendAssignmentFragment( assignment.getSqlAssignmentFragment() );
}
sql = update.toStatementString();
// now collect the parameters from the whole query
// parameters included in the list
try {
SqlGenerator gen = new SqlGenerator( walker.getSessionFactoryHelper().getFactory() );
gen.statement( walker.getAST() );
parameterSpecifications = gen.getCollectedParameters();
}
catch ( RecognitionException e ) {
throw QuerySyntaxException.convert( e );
}
}
//TODO: this is a copy/paste of a method from AbstractTableBasedBulkIdHandler
private String generateIdSelect(String tableAlias, String whereClause, Dialect dialect, Queryable queryable) {
Select select = new Select( dialect );
SelectValues selectClause = new SelectValues( dialect ).addColumns(
tableAlias,
queryable.getIdentifierColumnNames(),
queryable.getIdentifierColumnNames()
);
select.setSelectClause( selectClause.render() );
String rootTableName = queryable.getTableName();
String fromJoinFragment = queryable.fromJoinFragment( tableAlias, true, false );
String whereJoinFragment = queryable.whereJoinFragment( tableAlias, true, false );
select.setFromClause( rootTableName + ' ' + tableAlias + fromJoinFragment );
if ( whereJoinFragment == null ) {
whereJoinFragment = "";
}
else {
whereJoinFragment = whereJoinFragment.trim();
if ( whereJoinFragment.startsWith( "and" ) ) {
whereJoinFragment = whereJoinFragment.substring( 4 );
}
}
if ( !whereClause.isEmpty() ) {
if ( whereJoinFragment.length() > 0 ) {
whereJoinFragment += " and ";
}
}
select.setWhereClause( whereJoinFragment + whereClause );
return select.toStatementString();
}
}

View File

@ -1,3 +1,9 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.hql.internal.ast.exec; package org.hibernate.hql.internal.ast.exec;
import antlr.RecognitionException; import antlr.RecognitionException;

View File

@ -0,0 +1,45 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.hql.internal.ast.exec;
import antlr.RecognitionException;
import org.hibernate.AssertionFailure;
import org.hibernate.hql.internal.ast.HqlSqlWalker;
import org.hibernate.hql.internal.ast.QuerySyntaxException;
import org.hibernate.hql.internal.ast.SqlGenerator;
/**
* Executes HQL bulk updates against a single table, where the
* query only touches columns from the table it's updating, and
* so we don't need to use a subselect.
*
* @author Gavin King
*/
public class SimpleUpdateExecutor extends BasicExecutor {
public SimpleUpdateExecutor(HqlSqlWalker walker) {
super( walker.getFinalFromClause().getFromElement().getQueryable() );
if ( persister.isMultiTable() && walker.getQuerySpaces().size() > 1 ) {
throw new AssertionFailure("not a simple update");
}
try {
SqlGenerator gen = new SqlGenerator( walker.getSessionFactoryHelper().getFactory() );
gen.statement( walker.getAST() );
gen.getParseErrorHandler().throwQueryException();
// workaround for a problem where HqlSqlWalker actually generates
// broken SQL with undefined aliases in the where clause, because
// that is what MultiTableUpdateExecutor is expecting to get
String alias = walker.getFinalFromClause().getFromElement().getTableAlias();
sql = gen.getSQL().replace( alias + ".", "" );
parameterSpecifications = gen.getCollectedParameters();
}
catch ( RecognitionException e ) {
throw QuerySyntaxException.convert( e );
}
}
}

View File

@ -1,36 +0,0 @@
package org.hibernate.hql.internal.ast.exec;
import antlr.RecognitionException;
import org.hibernate.hql.internal.antlr.HqlSqlTokenTypes;
import org.hibernate.hql.internal.ast.HqlSqlWalker;
import org.hibernate.hql.internal.ast.QuerySyntaxException;
import org.hibernate.hql.internal.ast.SqlGenerator;
import org.hibernate.param.ParameterSpecification;
import org.hibernate.persister.entity.Queryable;
import java.util.List;
/**
* Executes HQL bulk updates against a single table.
*
* @author Gavin King
*/
public class UpdateExecutor extends BasicExecutor {
public UpdateExecutor(HqlSqlWalker walker) {
super( walker.getFinalFromClause().getFromElement().getQueryable() );
try {
SqlGenerator gen = new SqlGenerator( walker.getSessionFactoryHelper().getFactory() );
gen.statement( walker.getAST() );
// workaround for a problem where HqlSqlWalker actually generates
// broken SQL with undefined aliases in the where clause, because
// that is what MultiTableUpdateExecutor is expecting to get
String alias = walker.getFinalFromClause().getFromElement().getTableAlias();
sql = gen.getSQL().replace( alias + ".", "" );
gen.getParseErrorHandler().throwQueryException();
parameterSpecifications = gen.getCollectedParameters();
}
catch ( RecognitionException e ) {
throw QuerySyntaxException.convert( e );
}
}
}

View File

@ -43,7 +43,6 @@ public class TableBasedUpdateHandlerImpl
private final String[] updates; private final String[] updates;
private final ParameterSpecification[][] assignmentParameterSpecifications; private final ParameterSpecification[][] assignmentParameterSpecifications;
@SuppressWarnings("unchecked")
public TableBasedUpdateHandlerImpl( public TableBasedUpdateHandlerImpl(
SessionFactoryImplementor factory, SessionFactoryImplementor factory,
HqlSqlWalker walker, HqlSqlWalker walker,
@ -90,7 +89,7 @@ public class TableBasedUpdateHandlerImpl
} }
if ( affected ) { if ( affected ) {
updates[tableIndex] = update.toStatementString(); updates[tableIndex] = update.toStatementString();
assignmentParameterSpecifications[tableIndex] = parameterList.toArray( new ParameterSpecification[parameterList.size()] ); assignmentParameterSpecifications[tableIndex] = parameterList.toArray( new ParameterSpecification[0] );
} }
} }
} }