Skip to content

Commit 42b3d70

Browse files
committed
HHH-8370 - Optimize sql server IN predicate using derived table
1 parent 231163f commit 42b3d70

File tree

4 files changed

+171
-1
lines changed

4 files changed

+171
-1
lines changed

hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6208,4 +6208,25 @@ public boolean supportsRowValueConstructorSyntaxInInSubQuery() {
62086208
return supportsRowValueConstructorSyntaxInInList();
62096209
}
62106210

6211+
/**
6212+
* If the dialect supports {@link org.hibernate.dialect.Dialect#supportsRowValueConstructorSyntax() row values},
6213+
* does it allow using them in a derived table within an EXISTS predicate that emulates an IN-list?
6214+
* <p>
6215+
* For example:
6216+
* <pre>
6217+
* SELECT * FROM EntityTable T
6218+
* WHERE EXISTS (
6219+
* SELECT 1 FROM (VALUES (?, ?), (?, ?)) AS V(FIELD1, FIELD2)
6220+
* WHERE T.FIELD1 = V.FIELD1 AND T.FIELD2 = V.FIELD2
6221+
* )
6222+
* </pre>
6223+
* This pattern avoids the SQL length and performance issues of large disjunctions,
6224+
* and emulates tuple-based IN-list comparisons using a derived VALUES table.
6225+
*
6226+
* @return True if this SQL dialect is known to support row value constructors in
6227+
* derived tables for EXISTS-based IN-list emulation; false otherwise.
6228+
*/
6229+
public boolean supportsRowValueConstructorSyntaxInDerivedTableInList() {
6230+
return false;
6231+
}
62116232
}

hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1245,7 +1245,7 @@ public boolean supportsSimpleQueryGrouping() {
12451245

12461246
@Override
12471247
public boolean supportsRowValueConstructorSyntax() {
1248-
return false;
1248+
return getVersion().isSameOrAfter( 10 );
12491249
}
12501250

12511251
@Override
@@ -1263,4 +1263,8 @@ public boolean supportsRowValueConstructorSyntaxInInList() {
12631263
return false;
12641264
}
12651265

1266+
@Override
1267+
public boolean supportsRowValueConstructorSyntaxInDerivedTableInList() {
1268+
return getVersion().isSameOrAfter( 10 );
1269+
}
12661270
}

hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.function.Consumer;
2222
import java.util.function.Function;
2323
import java.util.function.Supplier;
24+
import java.util.stream.Collectors;
2425

2526
import org.hibernate.AssertionFailure;
2627
import org.hibernate.Internal;
@@ -7642,6 +7643,52 @@ public void visitInListPredicate(InListPredicate inListPredicate) {
76427643
getSqlTuple( listExpression )
76437644
.getExpressions().get( 0 );
76447645
}
7646+
else if ( dialect.supportsRowValueConstructorSyntaxInDerivedTableInList() ) {
7647+
if ( inListPredicate.isNegated() ) {
7648+
appendSql( "not " );
7649+
}
7650+
appendSql( "exists (select 1 from (values " );
7651+
7652+
List<SqlTuple> listTuples = listExpressions.stream()
7653+
.map( SqlTupleContainer::getSqlTuple )
7654+
.toList();
7655+
for ( int i = 0; i < listTuples.size(); i++ ) {
7656+
if ( i > 0 ) {
7657+
appendSql( ", " );
7658+
}
7659+
7660+
appendSql( OPEN_PARENTHESIS );
7661+
renderCommaSeparatedSelectExpression( listTuples.get(i).getExpressions() );
7662+
appendSql( CLOSE_PARENTHESIS );
7663+
}
7664+
7665+
List<Expression> expressions = lhsTuple.getExpressions()
7666+
.stream()
7667+
.filter( expression -> expression.getColumnReference() != null )
7668+
.collect( Collectors.toList() );
7669+
appendSql( ") as v(" );
7670+
for ( int i = 0; i < expressions.size(); i++ ) {
7671+
if ( i > 0 ) {
7672+
appendSql( ", " );
7673+
}
7674+
appendSql( expressions.get( i ).getColumnReference().getColumnExpression() );
7675+
}
7676+
7677+
appendSql( ") where " );
7678+
for ( int i = 0; i < expressions.size(); i++ ) {
7679+
if ( i > 0 ) {
7680+
appendSql( " and " );
7681+
}
7682+
7683+
Expression expression = expressions.get( i );
7684+
expression.accept( this );
7685+
appendSql( " = v." );
7686+
appendSql( expression.getColumnReference().getColumnExpression() );
7687+
}
7688+
7689+
appendSql( CLOSE_PARENTHESIS );
7690+
return;
7691+
}
76457692
else if ( !dialect.supportsRowValueConstructorSyntaxInInList() ) {
76467693
final ComparisonOperator comparisonOperator = inListPredicate.isNegated() ?
76477694
ComparisonOperator.NOT_EQUAL :
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.dialect.functional;
6+
7+
import org.hibernate.cfg.AvailableSettings;
8+
import org.hibernate.dialect.SQLServerDialect;
9+
import org.hibernate.orm.test.jpa.CompositeId;
10+
import org.hibernate.orm.test.jpa.EntityWithCompositeId;
11+
import org.hibernate.testing.jdbc.SQLStatementInspector;
12+
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
13+
import org.hibernate.testing.orm.junit.JiraKey;
14+
import org.hibernate.testing.orm.junit.Jpa;
15+
import org.hibernate.testing.orm.junit.RequiresDialect;
16+
import org.hibernate.testing.orm.junit.Setting;
17+
import org.junit.jupiter.api.Test;
18+
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
import static org.junit.jupiter.api.Assertions.assertTrue;
24+
25+
/**
26+
* @author Rob Green
27+
*/
28+
@JiraKey( value = "HHH-8370" )
29+
@RequiresDialect( value = SQLServerDialect.class, majorVersion = 10 )
30+
@Jpa(
31+
annotatedClasses = { EntityWithCompositeId.class },
32+
integrationSettings = {
33+
@Setting( name = AvailableSettings.USE_SQL_COMMENTS, value = "true" ),
34+
},
35+
useCollectingStatementInspector = true
36+
)
37+
public class SQLServerDialectCompositeTest {
38+
@Test
39+
public void testCompositeQueryWithInPredicate(EntityManagerFactoryScope scope) {
40+
final SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector();
41+
sqlStatementInterceptor.clear();
42+
43+
List<CompositeId> compositeIds = new ArrayList<>();
44+
compositeIds.add( new CompositeId( 1,2 ) );
45+
compositeIds.add( new CompositeId( 3,4 ) );
46+
47+
scope.inTransaction( entityManager -> {
48+
entityManager.createQuery( "SELECT e FROM EntityWithCompositeId e WHERE e.id IN (:ids)" )
49+
.setParameter( "ids", compositeIds )
50+
.getResultList();
51+
}
52+
);
53+
54+
var query = sqlStatementInterceptor.getSqlQueries().get( 0 );
55+
assertTrue(query.endsWith( "where exists (select 1 from (values (?,?), (?,?)) as v(id1, id2) where ewci1_0.id1 = v.id1 and ewci1_0.id2 = v.id2)" ));
56+
}
57+
58+
@Test
59+
public void testCompositeQueryWithNotInPredicate(EntityManagerFactoryScope scope) {
60+
final SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector();
61+
sqlStatementInterceptor.clear();
62+
63+
List<CompositeId> compositeIds = new ArrayList<>();
64+
compositeIds.add( new CompositeId( 1,2 ) );
65+
compositeIds.add( new CompositeId( 3,4 ) );
66+
67+
scope.inTransaction( entityManager -> {
68+
entityManager.createQuery( "SELECT e FROM EntityWithCompositeId e WHERE e.id NOT IN (:ids)" )
69+
.setParameter( "ids", compositeIds )
70+
.getResultList();
71+
}
72+
);
73+
74+
var query = sqlStatementInterceptor.getSqlQueries().get( 0 );
75+
assertTrue(query.endsWith( "where not exists (select 1 from (values (?,?), (?,?)) as v(id1, id2) where ewci1_0.id1 = v.id1 and ewci1_0.id2 = v.id2)" ));
76+
}
77+
78+
@Test
79+
public void testCompositeQueryWithMultiplePredicatesIncludingIn(EntityManagerFactoryScope scope) {
80+
final SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector();
81+
sqlStatementInterceptor.clear();
82+
83+
List<CompositeId> compositeIds = new ArrayList<>();
84+
compositeIds.add( new CompositeId( 1,2 ) );
85+
compositeIds.add( new CompositeId( 3,4 ) );
86+
87+
scope.inTransaction( entityManager -> {
88+
entityManager.createQuery("SELECT e FROM EntityWithCompositeId e WHERE e.description = :description AND e.id IN (:ids)")
89+
.setParameter( "ids", compositeIds )
90+
.setParameter( "description", "test" )
91+
.getResultList();
92+
}
93+
);
94+
95+
var query = sqlStatementInterceptor.getSqlQueries().get( 0 );
96+
assertTrue(query.endsWith( "where ewci1_0.description=? and exists (select 1 from (values (?,?), (?,?)) as v(id1, id2) where ewci1_0.id1 = v.id1 and ewci1_0.id2 = v.id2)" ));
97+
}
98+
}

0 commit comments

Comments
 (0)