Hibernate Query Plan Cache

Tina Jeong·2024년 6월 10일
1

Hibernate Query Plan Cache?

DB 쿼리 플랜 캐시와 별개로, Hibernate는 별도의 쿼리 플랜 캐시를 관리해요.

배경지식

JPQL 과 HQL, Criteria API는 SQM을 통해 실제 SQL로 변환돼요.

SQM은 Semantic Query Model
entity query parser의 역할을하고, JPQL and Criteria API를 둘다 처리할 수 있어요.

JPQL은 Java Persistence Query Language
테이블 대신 엔티티 객체를 대상으로 쿼리를 작성해요.

HQL은 Hibernate Query Language
JPQL과 매우 유사한 형태에요. JPA의 구현체인 Hibernate 프레임워크에서 사용하는 쿼리 언어에요.

Criteria API는 CriteriaQuery를 통해 동적으로 자바 코드를 작성할 수 있고, 컴파일타임에 오류를 감지할 수 있어요. QueryDSL과 동일한 특징을 가지고 있지만, QuryDSL이 더 가독성이 좋고, QueryDSL은 HQL로 변환되는 특징이 있어요

쿼리플랜캐시 생성/사용 과정

QueryDSL

return queryFactory
    .selectFrom(person)
    .where(person.firstName.eq(firstName), person.lastName.eq(lastName))
    .fetch();

HQL

select person
from Person person
where person.firstName = ?1 and person.lastName = ?2

SQL (MSSQL)

select
    p1_0.first_name,
    p1_0.id,
    p1_0.last_name 
from
    person p1_0 
where
    p1_0.first_name='Joseph' 
    and p1_0.last_name='Jeong' 

Key Point

  • 'Tina' , 'Jeong' 으로 파라미터 조건만 바꾸니 2번의 쿼리플랜캐시 생성은 생략됐어요.

  • 기존의 쿼리플랜캐시를 사용하는 것을 관찰할 수 있었어요.

  • 밑의 QueryInterpretationCacheStandardImpl.resolveSelectQueryPlan 메소드 참고


  1. isQueryPlanCacheable 쿼리 플랜캐싱이 가능한지 체크
public class QuerySqmImpl{ 
    ...
@Override
public boolean isQueryPlanCacheable() {
    return CRITERIA_HQL_STRING.equals( hql )
            // For criteria queries, query plan caching requires an explicit opt-in
            ? getQueryOptions().getQueryPlanCachingEnabled() == Boolean.TRUE
            : super.isQueryPlanCacheable();
}
  1. 쿼리 플랜캐싱 객체 생성
public class SqmSelectionQueryImpl<R> extends AbstractSelectionQuery<R>
		implements SqmSelectionQueryImplementor<R>, InterpretationsKeySource { 
private SelectQueryPlan<R> resolveSelectQueryPlan() {
    final QueryInterpretationCache.Key cacheKey = createInterpretationsKey( this );
    if ( cacheKey != null ) {
        return getSession().getFactory().getQueryEngine().getInterpretationCache()
                .resolveSelectQueryPlan( cacheKey, this::buildSelectQueryPlan ); // 여기 걸림
    }
    else {
        return buildSelectQueryPlan();
    }
}
  • 여기가 핵심!!!!
public class QueryInterpretationCacheStandardImpl implements QueryInterpretationCache { 
    private final BoundedConcurrentHashMap<Key, QueryPlan> queryPlanCache;
	private final BoundedConcurrentHashMap<Object, HqlInterpretation> hqlInterpretationCache;

    @Override
	public <R> SelectQueryPlan<R> resolveSelectQueryPlan(

        // 캐시된 쿼리 플랜이 있으면 반환
        final SelectQueryPlan<R> cached = (SelectQueryPlan<R>) queryPlanCache.get( key );
		if ( cached != null ) {
			if ( stats ) {
				statistics.queryPlanCacheHit( key.getQueryString() );
			}
			return cached;
		}
        // 아니면 쿼리 플랜 생성
        ...
        final SelectQueryPlan<R> plan = creator.get(); // creater는 QuerySqmImpl
		queryPlanCache.put( key.prepareForStore(), plan );
        return plan;
    }
} 
private SelectQueryPlan<R> buildSelectQueryPlan() { // 아까 메소드 참조 this::buildSelectQueryPlan로 여기 들어옴
    final SqmSelectStatement<R>[] concreteSqmStatements = QuerySplitter.split(
            (SqmSelectStatement<R>) getSqmStatement(),
            getSession().getFactory()
    );

    if ( concreteSqmStatements.length > 1 ) {
        return buildAggregatedSelectQueryPlan( concreteSqmStatements );
    }
    else { // 여기 걸림
        return buildConcreteSelectQueryPlan( concreteSqmStatements[0], getResultType(), getQueryOptions() );
    }
}
private <T> SelectQueryPlan<T> buildConcreteSelectQueryPlan(
        SqmSelectStatement<?> concreteSqmStatement,
        Class<T> resultType,
        QueryOptions queryOptions) {
    return new ConcreteSqmSelectQueryPlan<>(
            concreteSqmStatement,
            getQueryString(),
            getDomainParameterXref(),
            resultType,
            tupleMetadata,
            queryOptions
    );
}
  • 68~188 line ConcreteSqmSelectQueryPlan의 생성자를 통해 ConcreteSqmSelectQueryPlan 생성
  1. 쿼리 플랜 캐시를 통한 조회
protected List<R> doList() {
    final List<R> list = resolveSelectQueryPlan()
        .performList( executionContextForDoList( containsCollectionFetches, hasLimit, needsDistinct ) ); // 실제 Entity 객체 리스트가 담김
}
public class ConcreteSqmSelectQueryPlan<R> implements SelectQueryPlan<R> {
    ....
@Override
public List<R> performList(DomainQueryExecutionContext executionContext) {
    if ( executionContext.getQueryOptions().getEffectiveLimit().getMaxRowsJpa() == 0 ) {
        return Collections.emptyList();
    }
    return withCacheableSqmInterpretation( executionContext, null, listInterpreter ); // withCacheableSqmInterpretation에서 buildCacheableSqmInterpretation 호출
}
private static CacheableSqmInterpretation buildCacheableSqmInterpretation(
    		final SqmTranslation<SelectStatement> sqmInterpretation =
				sessionFactory.getQueryEngine().getSqmTranslatorFactory()
						.createSelectTranslator(
								sqm,
								executionContext.getQueryOptions(),
								domainParameterXref,
								executionContext.getQueryParameterBindings(),
								executionContext.getSession().getLoadQueryInfluencers(),
								sessionFactory,
								true
						)
						.translate();
            // DBMS 종류별로 SqlAstTranslator 구현체가 있고 실질적인 sql을 생성
            final SqlAstTranslator<JdbcOperationQuerySelect> selectTranslator =
				sessionFactory.getJdbcServices().getJdbcEnvironment().getSqlAstTranslatorFactory()
						.buildSelectTranslator(
								sessionFactory,
								sqmInterpretation.getSqlAst()
						);

Hibernate Query Plan Cache 요약

  • 초기 실행: 쿼리가 처음 실행될 때 쿼리 파싱 및 실행 계획 생성으로 부하 발생
  • 이후 실행: 동일한 쿼리 템플릿에 대해 캐시된 실행 계획을 재사용하여 빠른 쿼리 실행

  • 쿼리 템플릿에 플레이스홀더가 포함된 이유:
    • 쿼리 템플릿에는 변수 값 대신 플레이스홀더가 포함되어 있으며, 실제 쿼리 실행 시 변수 값이 바인딩됨
    • 데이터베이스와의 통신을 효율적으로 하고 보안을 강화하기 위한 중요한 전략

이슈 : 동일한 쿼리에 대해 실행계획이 달라지는 경우

Hibernate에서 동일한 쿼리에 대해 실행 계획이 달라지는 문제는 주로 파라미터의 변동성 때문에 발생해요.

  • 특히 IN 절과 같은 변동적인 파라미터를 사용할 때 주의

  • 파라미터 변동성:

    • IN 절에 사용되는 파라미터 리스트의 크기가 변동적일 경우, Hibernate는 각각의 다른 파라미터 리스트에 대해 새로운 쿼리 플랜을 생성.
    • 이는 각기 다른 쿼리 플랜이 캐시에 저장되어 메모리 사용량이 증가하고, 쿼리 플랜 캐시의 크기가 커지면서 OutOfMemory 에러가 발생할 수 있다. (기본 크기인 2048 넘어감)

해결 방안

파라미터 패딩 옵션 켜기:

  • hibernate.query.in_clause_parameter_padding 속성을 사용하여 IN 절의 파라미터 크기를 고정하기. -> 동일한 크기의 파라미터 리스트를 사용하여 쿼리 플랜의 재사용성을 높일수 있어요.
  • 위 옵션을 켜지 않으면, IN 절 쿼리가 QueryPlanCache를 모두 점유해서 메모리가 부족해질수 있고, 메모리가 남아돌더라도 캐시를 모두 IN 절이 점유해버리므로 별로 좋지 못하고 합니다.
  • RDBMS 의 경우 그 자체 execution plan cache 가 있는데, 옵션을 켜면 그에 대한 hit 율도 높아지게 된다고 합니다.
protected Map<String, Object> jpaProperties() {
    Map<String, Object> props = new HashMap<>();
    props.put("hibernate.session_factory.interceptor", interceptor);
    
    // 쿼리 플랜 최적화 설정
    props.put("hibernate.query.in_clause_parameter_padding", true);

    // execution plan cache 사이즈 하향
    props.put("hibernate.query.plan_cache_max_size", 256);
    props.put("hibernate.query.plan_parameter_metadata_max_size", 16);

    return props;
}

메모리 오류가 발생하면 2의 제곱기준으로 맞추어 하향조정하기

출처:

profile
Keep exploring, 계속 탐색하세요.

0개의 댓글