Collapse date joins (#1726)

* Collapse date joins

* Add changelog

* Address review comments

* Add whitespace

* fix test

Co-authored-by: Ken Stevens <ken.stevens@sympatico.ca>
This commit is contained in:
James Agnew 2020-02-25 17:17:13 -05:00 committed by GitHub
parent f1646fd184
commit b2b0ff22b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 142 additions and 9 deletions

View File

@ -0,0 +1,6 @@
---
type: perf
issue: 1726
title: When performing date range searches in the JPA server, the server was generating extra
unneccessary joins in the generated SQL. This has been streamlined, which should result in
faster searches when performing date ranges.

View File

@ -39,13 +39,17 @@ import javax.persistence.criteria.Join;
import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Predicate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
@Component @Component
@Scope("prototype") @Scope("prototype")
public class PredicateBuilderDate extends BasePredicateBuilder implements IPredicateBuilder { public class PredicateBuilderDate extends BasePredicateBuilder implements IPredicateBuilder {
private static final Logger ourLog = LoggerFactory.getLogger(PredicateBuilderDate.class); private static final Logger ourLog = LoggerFactory.getLogger(PredicateBuilderDate.class);
private Map<String, Join<ResourceTable, ResourceIndexedSearchParamDate>> myJoinMap;
PredicateBuilderDate(SearchBuilder theSearchBuilder) { PredicateBuilderDate(SearchBuilder theSearchBuilder) {
super(theSearchBuilder); super(theSearchBuilder);
} }
@ -56,7 +60,18 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
List<? extends IQueryParameterType> theList, List<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation operation) { SearchFilterParser.CompareOperation operation) {
Join<ResourceTable, ResourceIndexedSearchParamDate> join = createJoin(SearchBuilderJoinEnum.DATE, theParamName); boolean newJoin = false;
if (myJoinMap == null) {
myJoinMap = new HashMap<>();
}
String key = theResourceName + " " + theParamName;
Join<ResourceTable, ResourceIndexedSearchParamDate> join = myJoinMap.get(key);
if (join == null) {
join = createJoin(SearchBuilderJoinEnum.DATE, theParamName);
myJoinMap.put(key, join);
newJoin = true;
}
if (theList.get(0).getMissing() != null) { if (theList.get(0).getMissing() != null) {
Boolean missing = theList.get(0).getMissing(); Boolean missing = theList.get(0).getMissing();
@ -77,7 +92,14 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
} }
Predicate orPredicates = myBuilder.or(toArray(codePredicates)); Predicate orPredicates = myBuilder.or(toArray(codePredicates));
if (newJoin) {
Predicate identityAndValuePredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, orPredicates);
myQueryRoot.addPredicate(identityAndValuePredicate);
} else {
myQueryRoot.addPredicate(orPredicates); myQueryRoot.addPredicate(orPredicates);
}
return orPredicates; return orPredicates;
} }
@ -86,12 +108,13 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
String theParamName, String theParamName,
CriteriaBuilder theBuilder, CriteriaBuilder theBuilder,
From<?, ResourceIndexedSearchParamDate> theFrom) { From<?, ResourceIndexedSearchParamDate> theFrom) {
return createPredicateDate(theParam, Predicate predicateDate = createPredicateDate(theParam,
theResourceName, theResourceName,
theParamName, theParamName,
theBuilder, theBuilder,
theFrom, theFrom,
null); null);
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, predicateDate);
} }
private Predicate createPredicateDate(IQueryParameterType theParam, private Predicate createPredicateDate(IQueryParameterType theParam,
@ -99,7 +122,7 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
String theParamName, String theParamName,
CriteriaBuilder theBuilder, CriteriaBuilder theBuilder,
From<?, ResourceIndexedSearchParamDate> theFrom, From<?, ResourceIndexedSearchParamDate> theFrom,
SearchFilterParser.CompareOperation operation) { SearchFilterParser.CompareOperation theOperation) {
Predicate p; Predicate p;
if (theParam instanceof DateParam) { if (theParam instanceof DateParam) {
@ -109,7 +132,7 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
p = createPredicateDateFromRange(theBuilder, p = createPredicateDateFromRange(theBuilder,
theFrom, theFrom,
range, range,
operation); theOperation);
} else { } else {
// TODO: handle missing date param? // TODO: handle missing date param?
p = null; p = null;
@ -119,12 +142,12 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
p = createPredicateDateFromRange(theBuilder, p = createPredicateDateFromRange(theBuilder,
theFrom, theFrom,
range, range,
operation); theOperation);
} else { } else {
throw new IllegalArgumentException("Invalid token type: " + theParam.getClass()); throw new IllegalArgumentException("Invalid token type: " + theParam.getClass());
} }
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, p); return p;
} }
private Predicate createPredicateDateFromRange(CriteriaBuilder theBuilder, private Predicate createPredicateDateFromRange(CriteriaBuilder theBuilder,

View File

@ -3258,6 +3258,105 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
} }
} }
@Test
public void testSearchWithDateAndReusesExistingJoin() {
// Add a search parameter to Observation.issued, so that between that one
// and the existing one on Observation.effective, we have 2 date search parameters
// on the same resource
{
SearchParameter sp = new SearchParameter();
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
sp.addBase("Observation");
sp.setType(Enumerations.SearchParamType.DATE);
sp.setCode("issued");
sp.setExpression("Observation.issued");
mySearchParameterDao.create(sp);
mySearchParamRegistry.forceRefresh();
}
// Dates are reversed on these two observations
IIdType obsId1;
{
Observation obs = new Observation();
obs.setIssuedElement(new InstantType("2020-06-06T12:00:00Z"));
obs.setEffective(new InstantType("2019-06-06T12:00:00Z"));
obsId1 = myObservationDao.create(obs).getId().toUnqualifiedVersionless();
}
IIdType obsId2;
{
Observation obs = new Observation();
obs.setIssuedElement(new InstantType("2019-06-06T12:00:00Z"));
obs.setEffective(new InstantType("2020-06-06T12:00:00Z"));
obsId2 = myObservationDao.create(obs).getId().toUnqualifiedVersionless();
}
// Add two with a period
IIdType obsId3;
{
Observation obs = new Observation();
obs.setEffective(new Period().setStartElement(new DateTimeType("2000-06-06T12:00:00Z")).setEndElement(new DateTimeType("2001-06-06T12:00:00Z")));
obsId3 = myObservationDao.create(obs).getId().toUnqualifiedVersionless();
}
IIdType obsId4;
{
Observation obs = new Observation();
obs.setEffective(new Period().setStartElement(new DateTimeType("2001-01-01T12:00:00Z")).setEndElement(new DateTimeType("2002-01-01T12:00:00Z")));
obsId4 = myObservationDao.create(obs).getId().toUnqualifiedVersionless();
}
// Two AND instances of 1 SP
{
myCaptureQueriesListener.clear();
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronous(true);
params.add("issued", new DateParam("ge2020-06-05"));
params.add("issued", new DateParam("lt2020-06-07"));
List<IIdType> patients = toUnqualifiedVersionlessIds(myObservationDao.search(params));
assertThat(patients.toString(), patients, contains(obsId1));
String searchQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search query:\n{}", searchQuery);
assertEquals(searchQuery, 1, StringUtils.countMatches(searchQuery.toLowerCase(), "join"));
assertEquals(searchQuery, 1, StringUtils.countMatches(searchQuery.toLowerCase(), "hash_identity"));
assertEquals(searchQuery, 2, StringUtils.countMatches(searchQuery.toLowerCase(), "sp_value_low"));
}
// Two AND instances of 1 SP and 1 instance of another
{
myCaptureQueriesListener.clear();
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronous(true);
params.add("issued", new DateParam("ge2020-06-05"));
params.add("issued", new DateParam("lt2020-06-07"));
params.add("date", new DateParam("gt2019-06-05"));
params.add("date", new DateParam("lt2019-06-07"));
List<IIdType> patients = toUnqualifiedVersionlessIds(myObservationDao.search(params));
assertThat(patients.toString(), patients, contains(obsId1));
String searchQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search query:\n{}", searchQuery);
assertEquals(searchQuery, 2, StringUtils.countMatches(searchQuery.toLowerCase(), "join"));
assertEquals(searchQuery, 2, StringUtils.countMatches(searchQuery.toLowerCase(), "hash_identity"));
assertEquals(searchQuery, 4, StringUtils.countMatches(searchQuery.toLowerCase(), "sp_value_low"));
}
// Period search
{
myCaptureQueriesListener.clear();
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronous(true);
params.add("date", new DateParam("lt2002-01-01T12:00:00Z"));
List<IIdType> patients = toUnqualifiedVersionlessIds(myObservationDao.search(params));
assertThat(patients.toString(), patients, containsInAnyOrder(obsId3, obsId4));
String searchQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search query:\n{}", searchQuery);
assertEquals(searchQuery, 1, StringUtils.countMatches(searchQuery.toLowerCase(), "join"));
assertEquals(searchQuery, 1, StringUtils.countMatches(searchQuery.toLowerCase(), "hash_identity"));
assertEquals(searchQuery, 1, StringUtils.countMatches(searchQuery.toLowerCase(), "sp_value_low"));
}
}
@Test @Test
public void testSearchWithFetchSizeDefaultMaximum() { public void testSearchWithFetchSizeDefaultMaximum() {
myDaoConfig.setFetchSizeDefaultMaximum(5); myDaoConfig.setFetchSizeDefaultMaximum(5);

View File

@ -21,16 +21,16 @@ package ca.uhn.fhir.jpa.migrate.tasks.api;
*/ */
import ca.uhn.fhir.jpa.migrate.DriverTypeEnum; import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;
import ca.uhn.fhir.jpa.migrate.tasks.SchemaInitializationProvider;
import java.util.List; import java.util.List;
public interface ISchemaInitializationProvider { public interface ISchemaInitializationProvider {
List<String> getSqlStatements(DriverTypeEnum theDriverType); List<String> getSqlStatements(DriverTypeEnum theDriverType);
String getSchemaExistsIndicatorTable(); String getSchemaExistsIndicatorTable();
String getSchemaDescription(); String getSchemaDescription();
SchemaInitializationProvider setSchemaDescription(String theSchemaDescription); ISchemaInitializationProvider setSchemaDescription(String theSchemaDescription);
} }

View File

@ -45,6 +45,11 @@ public class InitializeSchemaTaskTest extends BaseTest {
return "TEST"; return "TEST";
} }
@Override
public ISchemaInitializationProvider setSchemaDescription(String theSchemaDescription) {
return this;
}
@Override @Override
public boolean equals(Object theO) { public boolean equals(Object theO) {
if (this == theO) return true; if (this == theO) return true;