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.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.hibernate.HibernateException;
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.MultiTableDeleteExecutor;
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.UpdateExecutor;
import org.hibernate.hql.internal.ast.exec.IdSubselectUpdateExecutor;
import org.hibernate.hql.internal.ast.tree.AggregatedSelectExpression;
import org.hibernate.hql.internal.ast.tree.FromElement;
import org.hibernate.hql.internal.ast.tree.QueryNode;
@ -61,8 +63,6 @@ import antlr.RecognitionException;
import antlr.TokenStreamException;
import antlr.collections.AST;
import static java.util.Collections.singleton;
/**
* A QueryTranslator that uses an Antlr-based parser.
*
@ -609,13 +609,14 @@ public class QueryTranslatorImpl implements FilterTranslator {
final FromElement fromElement = walker.getFinalFromClause().getFromElement();
final Queryable persister = fromElement.getQueryable();
boolean affectsExtraTables = persister.isMultiTable()
&& !singleton( persister.getTableName() ).containsAll( walker.getQuerySpaces() );
if ( affectsExtraTables ) {
if ( persister.isMultiTable() && affectsExtraTables( walker, persister ) ) {
return new MultiTableUpdateExecutor( walker );
}
else if ( persister.isMultiTable() && walker.getQuerySpaces().size() > 1 ) {
return new IdSubselectUpdateExecutor( walker );
}
else {
return new UpdateExecutor( walker );
return new SimpleUpdateExecutor( walker );
}
}
else if ( walker.getStatementType() == HqlSqlTokenTypes.INSERT ) {
@ -625,6 +626,16 @@ public class QueryTranslatorImpl implements FilterTranslator {
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
public ParameterTranslations getParameterTranslations() {
if ( paramTranslations == null ) {

View File

@ -64,9 +64,7 @@ public abstract class BasicExecutor implements StatementExecutor {
);
}
int doExecute(String sql, QueryParameters parameters,
List<ParameterSpecification> parameterSpecifications,
SharedSessionContractImplementor session)
int doExecute(String sql, QueryParameters parameters, List<ParameterSpecification> parameterSpecifications, SharedSessionContractImplementor session)
throws HibernateException{
try {
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;
import antlr.RecognitionException;
@ -18,17 +24,17 @@ import java.util.List;
* @author Gavin King
*/
public class InsertExecutor extends BasicExecutor {
public InsertExecutor(HqlSqlWalker walker) {
super( ( (InsertStatement) walker.getAST() ).getIntoClause().getQueryable() );
try {
SqlGenerator gen = new SqlGenerator( walker.getSessionFactoryHelper().getFactory() );
gen.statement( walker.getAST() );
sql = gen.getSQL();
gen.getParseErrorHandler().throwQueryException();
parameterSpecifications = gen.getCollectedParameters();
}
catch ( RecognitionException e ) {
throw QuerySyntaxException.convert( e );
}
}
public InsertExecutor(HqlSqlWalker walker) {
super( ( (InsertStatement) walker.getAST() ).getIntoClause().getQueryable() );
try {
SqlGenerator gen = new SqlGenerator( walker.getSessionFactoryHelper().getFactory() );
gen.statement( walker.getAST() );
sql = gen.getSQL();
gen.getParseErrorHandler().throwQueryException();
parameterSpecifications = gen.getCollectedParameters();
}
catch ( RecognitionException e ) {
throw QuerySyntaxException.convert( e );
}
}
}

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 ParameterSpecification[][] assignmentParameterSpecifications;
@SuppressWarnings("unchecked")
public TableBasedUpdateHandlerImpl(
SessionFactoryImplementor factory,
HqlSqlWalker walker,
@ -90,7 +89,7 @@ public class TableBasedUpdateHandlerImpl
}
if ( affected ) {
updates[tableIndex] = update.toStatementString();
assignmentParameterSpecifications[tableIndex] = parameterList.toArray( new ParameterSpecification[parameterList.size()] );
assignmentParameterSpecifications[tableIndex] = parameterList.toArray( new ParameterSpecification[0] );
}
}
}