Skip to content

Commit

Permalink
HHH-14153 further optimization for single-table HQL update
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gavinking authored and dreab8 committed Aug 27, 2020
1 parent 264e71a commit 4236970
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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;
Expand All @@ -33,8 +34,9 @@
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;
Expand All @@ -61,8 +63,6 @@
import antlr.TokenStreamException;
import antlr.collections.AST;

import static java.util.Collections.singleton;

/**
* A QueryTranslator that uses an Antlr-based parser.
*
Expand Down Expand Up @@ -609,13 +609,14 @@ else if ( walker.getStatementType() == HqlSqlTokenTypes.UPDATE ) {
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 ) {
Expand All @@ -625,6 +626,16 @@ else if ( walker.getStatementType() == HqlSqlTokenTypes.INSERT ) {
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 ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,7 @@ public int execute(QueryParameters parameters, SharedSessionContractImplementor
);
}

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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,17 +24,17 @@
* @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 );
}
}
}
Original file line number Diff line number Diff line change
@@ -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 );
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ public class TableBasedUpdateHandlerImpl
private final String[] updates;
private final ParameterSpecification[][] assignmentParameterSpecifications;

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

0 comments on commit 4236970

Please sign in to comment.