본문 바로가기

Trouble Shooting/DataBase

복합 인덱스 적용기

 

 

 

이번 블로그에서는 개인 프로젝트에 복합 인덱스를 적용해보면서 느끼는 고찰에 대해 기록해보려고 한다.

 

 

배경

회원 엔터티가 프로젝트내의 존재한다.

구성은 userId, userPwd, userName, userNumber, userAddress, userEmail 이 존재하고 upadatedDate, createdDate 가 부수적으로 존재한다.

 

검색 API를 구성했다.

 

- 동적 쿼리로 구성

기본적으로 동적 쿼리로 구성해 조건에 있으면 해당 조건을 넣어 검색하고 없으면 해당 조건을 빼고 검색했다. 예를 들어, 이름과 주소로만 검색 가능하고 모든 조건을 넣어서도 검색이 가능하다.

또한, 동적 쿼리 구성시 BooleanExpression을 각 필드마다 null여부를 체크해서 반환해주는 메서드를 모두 만들고 좀 더 가독성을 높였다.

 

 

BooleanBuilder 와 BooleanExpression의 차이점

전자는 후자를 여러개 모아 형성된 객체이다. 특징으로는 처음에는 빈 상태로 시작해 and(), or(), not() 등의 메서드를 통해 후자를 추가해가는 방식이다. 둘 다 DB의 조건절로 사용이 충분히 가능하지만 가독성의 차이가 있다. 전자는 코드가 복잡해지고 추가적인 메서드가 들어 가독성이 조금 떨어진다. 반면, 후자 사용시 메서드로 빼내고 메서드명에 의미를 부여해 바로 where 절에 삽입 가능하다.

하지만 밑의 코드에서는 모두 null인경우 where 절이 아예 날라가므로 애초에 클라이언트로부터 입력을 받을 때 모두 null이면 오류를 발생시켜 해당 문제를 처리하였다.

 

 

 

- DTO로 결과 처리

Entity에 존재하는 모든 결과를 보내줄 필요가 없고 성능상 좋지도 않기에 Entity가 아닌 DTO로 바로 검색 결과로 받을 수 있게 설정을 하였다. 따라서 기존 코드는 아래와 같다.

 

- 코드

  @Override
  public List<SearchMemberDTO> searchMember(SearchMemberDTO member) {

    // member의 모든 필드가 null 인경우 컨트롤러에서 처리

    // 동적 쿼리 생성 및 결과 반환
    List<SearchMemberDTO> result = queryFactory
        .select(
            Projections.fields(SearchMemberDTO.class, memberEntity.userId, memberEntity.userName,
                memberEntity.userNumber, memberEntity.userAddress, memberEntity.userEmail))
        .from(memberEntity)
        .where(eqUserId(member.getUserId()), eqUserName(member.getUserName()),
            eqUserNumber(member.getUserNumber()), eqUserAddress(member.getUserAddress()),
            eqUserEmail(member.getUserEmail()))
        .fetch();
    

    return result;
  }

  // 동적 쿼리 메서드
  private BooleanExpression eqUserId(String userId) {
    if (StringUtils.isEmpty(userId)) {
      return null;
    }
    return memberEntity.userId.eq(userId);
  }

  private BooleanExpression eqUserName(String userName) {
    if (StringUtils.isEmpty(userName)) {
      return null;
    }
    return memberEntity.userName.eq(userName);
  }

  private BooleanExpression eqUserNumber(String userNumber) {
    if (StringUtils.isEmpty(userNumber)) {
      return null;
    }
    return memberEntity.userNumber.eq(userNumber);
  }

  private BooleanExpression eqUserAddress(String userAddress) {
    if (StringUtils.isEmpty(userAddress)) {
      return null;
    }
    return memberEntity.userAddress.eq(userAddress);
  }

  private BooleanExpression eqUserEmail(String userEmail) {
    if (StringUtils.isEmpty(userEmail)) {
      return null;
    }
    return memberEntity.userEmail.eq(userEmail);
  }

 

 

 

테스팅 배경

지금부터 과정은 아래와 같다.

기본적으로 아무 인덱싱 처리를 안했기에 클러스터형 인덱스만 처리가 되어있을 것이고

해당 배경에서 성능을 검사한 후, 복합 인덱스 처리 한 후 결과도 측정해 둘의 성능차이를 비교해보고자 한다.

그리고 마지막으로 복합 인덱스 처리 필드를 달리해서 성능 측정을 해보고자 한다.

 

요약

1. 클러스터형 인덱스만 처리시 성능 측정

2. 세컨더리 인덱스 처리시 성능 측정

3. 복합 인덱스처리시 성능 측정

4. 복합 인덱스 최적화

 

 

 

테스팅

- 클러스터형은 기본적으로 id 로 설정되어 있다.

- 물리적으로 영향을 주지만 속도면에서 위치에 따른 검색 속도 차이가 별로 나지 않으리라 예상된다.

- 예를 들어, 아이디가 a인 엔터티와 z인 엔터티의 검색 속도 차이는 크지 않을것이다.

 

 

공통조건

- 1초안에 1000명의 유저(스레드)가 접속한다. 

- 데이터이 크기는 10000개이다.

- Jmeter를 활용한다.

- DB는 아이디만 클러스터형으로 인덱스로 지정되어 있다.

- Mysql 사용 중

 

 

 

클러스터형 인덱스 처리

 

아이디 'a' 검색

아이디 'z' 검색

- 결과적으로 차이가 크게 나지 않는다.

- 예상대로, 인덱싱 지정시 속도에서 평균값으로 맞추며 다양한 유저들에게 쾌적한 환경을 제공하는 듯 보인다.

 

 

 

 

세컨더리 인덱스 처리

 

하지만 인덱싱 처리전에는 구조상 데이터 전체를 다 탐색하겠지만, 인덱싱 처리시 인덱스를 통해 처리를 하기에 속도가 향상 되었을 것이다.

이메일은 인덱싱 처리가 되어있지 않다. 따라서, 아래 결과는 속도에서 아이디 검색과 차이가 발생할 것이라 예상이 든다.

 

 

이메일 'a'로 검색

 

이메일 'z' 검색

확연히 아이디로 검색시 차이가 발생한다. 이는 예상대로 인덱싱 처리 후 검색시 전체적인 속도를 향상시킴을 의미한다.

아래 같은 검색 조건으로 다시 확인해보고자 한다. 

 

 

아래 결과는 세컨더리 인덱스 처리시 차이를 확인해보고자 한다.

예상대로, 속도가 향상되는지 확인해보겠다.

 

 

이메일 'a'로 검색

 

이메일 'z'로 검색

 

예상대로 시간이 단축되었고 약, 2~3초정도 사용자들에게 쾌적한 환경을 제공한다.

 

 

 

 

 

복합 인덱스 처리

 

이번에는 복합인덱스 처리시 차이에 대해서 설정해보려고 한다. 

우선, 아이디와 이메일을 중심으로 복합인덱스를 처리하지 않고 검색해본다.

 

 

아이디 'a', 이메일 'a' 검색

 

아이디 'z', 이메일 'z'검색

 

 

이번에는 복합인덱스로 (아이디, 이메일)로 처리 후 검색해보고자 한다.

 

아이디 'a', 이메일 'a' 검색

 

아이디 'z', 이메일 'z'검색

 

속도가 복합인덱스 처리와 처리 안할때 차이가 크지 않다. 이유는 이미 아이디와 이메일이라는 필드는 클러스터와 세컨더리 인덱스가 처리되어있기 때문이라는 생각이든다.

이 결과로 보면, 복합인덱스로 설정한 필드들이 모두 이미 인덱싱이 되어있다면 큰 의미가 없다는 것이라고 판단된다.

 

 

 

복합 인덱스 최적화

 

이번에는 복합 인덱스 최적화를 해보려고한다.

앞서 말했던 것처럼 필드는 (id, name, email, number, address) 이 있다.

우선, 앞서 생성한 인덱스는 모두 삭제한다.

 

두가지 조건을 통해 테스팅을 해보려고 한다.

- 그냥 무작위로 (name, number) 으로 복합인덱스를 생성한 경우

- 최적화를 진행 후 복합인덱스 생성

 

참고로 모든 필드를 복합 인덱스로 설정하는 것은 불가능하다. 

4개 이상시 오류가 발생한다.

12:33:22	CREATE INDEX idx_alllll ON member (user_name, user_number, user_address, user_id)	Error Code: 1071. Specified key was too long; max key length is 3072 bytes	0.0077 sec

 

 

(number, address) 복합 인덱스 처리

 

 

복합 인덱스 최적화 조건

 

조건은 아래와 같이 설정 후 사용하였다.

1. 인덱스는 하나의 비용이므로 최대한 효율 좋은 인덱스만 유지한다.

2. 같음, 정렬, 다중 키, 카디널리티 순으로 설정한다.

 

현재 검색시 정렬 및 다중 키는 없다. 따라서, 같음과 카디널리티를 판단해야하는데,

API에서 모두 같음만을 체크하므로 카디널리티만 판단을 했다.

카디널리티가 높을수록 복합인덱스에서는 먼저 설정되어야 한다.

즉, 유니크한 값이 많을수록 먼저 설정되어야 한다.

 

 

현재 카디널리티는 id > name > number > email > address 순이였다. 따라서 id, name으로 복합인덱스를 설정하고

검색을 진행하였다.

 

(id, name) 복합 인덱스 처리

 

 

하지만 별차이가 없다.

복합 인덱스의 최적화는 많은 데이터 환경에서 더 큰 차이가 있으리라 생각이든다. 따라서 많은 데이터 저장 후 다시 한번 실험을 해볼 생각이다.

 

 

 

 

성능 개선

필드는 아이디, 이름, 전화번호, 주소, 이메일 필드가 있다고 했다. 생각해보면, Id 필드가 검색 조건으로 주어진다면 이 필드만으로 fetchFirst를 통해 검색한다면 동일한 결과를 낸다. 또한 굳이, 다른 필드를 검색할 필요도 없어진다. 이유는 아이디는 DB의 유니크한 키로 설정해두었기 때문이다.

또한, 만약, 전화번호와 이메일 또한 처음 가입시 한 이메일 또는 전화번호당 하나의 아이디로 설정해두었다면 유니크한 값이 되기에 위와같이 설정이 가능할 것이다. 이러한 방법으로 속도를 좀 더 향상시킬 수 있다.

 

 

 

 

최종 코드

@Override
  public List<SearchMemberDTO> searchMember(SearchMemberDTO member) {

    // member의 모든 필드가 null 인경우 컨트롤러에서 처리
    
    if(member.getUserId() != null) {
      SearchMemberDTO dto = queryFactory
          .select(
              Projections.fields(SearchMemberDTO.class, memberEntity.userId, memberEntity.userName,
                  memberEntity.userNumber, memberEntity.userAddress, memberEntity.userEmail))
          .from(memberEntity)
          .where(eqUserId(member.getUserId()), eqUserName(member.getUserName()),
              eqUserNumber(member.getUserNumber()), eqUserAddress(member.getUserAddress()),
              eqUserEmail(member.getUserEmail()))
          .fetchFirst();
      List<SearchMemberDTO> result = new ArrayList<>();
      result.add(dto);
      return result;
    }

    // 동적 쿼리 생성 및 결과 반환
    List<SearchMemberDTO> result = queryFactory
        .select(
            Projections.fields(SearchMemberDTO.class, memberEntity.userId, memberEntity.userName,
                memberEntity.userNumber, memberEntity.userAddress, memberEntity.userEmail))
        .from(memberEntity)
        .where(eqUserId(member.getUserId()), eqUserName(member.getUserName()),
            eqUserNumber(member.getUserNumber()), eqUserAddress(member.getUserAddress()),
            eqUserEmail(member.getUserEmail()))
        .fetch();
    

    return result;
  }

  // 동적 쿼리 메서드
  private BooleanExpression eqUserId(String userId) {
    if (StringUtils.isEmpty(userId)) {
      return null;
    }
    return memberEntity.userId.eq(userId);
  }

  private BooleanExpression eqUserName(String userName) {
    if (StringUtils.isEmpty(userName)) {
      return null;
    }
    return memberEntity.userName.eq(userName);
  }

  private BooleanExpression eqUserNumber(String userNumber) {
    if (StringUtils.isEmpty(userNumber)) {
      return null;
    }
    return memberEntity.userNumber.eq(userNumber);
  }

  private BooleanExpression eqUserAddress(String userAddress) {
    if (StringUtils.isEmpty(userAddress)) {
      return null;
    }
    return memberEntity.userAddress.eq(userAddress);
  }

  private BooleanExpression eqUserEmail(String userEmail) {
    if (StringUtils.isEmpty(userEmail)) {
      return null;
    }
    return memberEntity.userEmail.eq(userEmail);
  }

 

 

 

'Trouble Shooting > DataBase' 카테고리의 다른 글

Indexing 적용해 성능 개선  (0) 2023.07.09
Redis Cache 성능 측정  (0) 2023.07.07