JPA 다양한 Join 방법 정리 (N+1, queryDSL, fetch join)

web/Spring|2019. 11. 4. 20:31

JPA를 사용하다 보면 join을 할 때가 많아진다. join을 어떠한 방법으로 하느냐에 따라서 수행되는 쿼리가 달라지고 성능에 문제가 발생하는 경우도 종종있다.

 

그래서 다양한 방식의 join 방식을 알아보고 방식에 따라 작업을 진행해 보자.

우선 사용될 entity 두 개를 설명하면 다음과 같다.

@Getter
@Entity
@Table(name = "wedul_classes")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@Builder
public class WedulClasses extends CommonEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long wedulClassesId;

    @OneToMany(mappedBy = "wedulClasses", fetch = FetchType.LAZY)
    private Set<WedulStudent> wedulStudentList = new LinkedHashSet<>();

    private String classesName;

    private String classesAddr;

}

@Getter
@Entity
@Table(name = "wedul_student")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@Builder
public class WedulStudent extends CommonEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long wedulStudentId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "wedul_classes_id")
    @JsonBackReference
    private WedulClasses wedulClasses;

    private String studentName;

    private int studentAge;

    @Enumerated(value = EnumType.STRING)
    private StudentType studentType;

}

이 두 엔티티는 class와 student의 관계로 1대 N의 관계를 가지고 있다.

이 과정에서 사용될 데이터는 임의로 추가했고 다음과 같다.

 

wedul_classe 테이블의 데이터

wedul_student 테이블의 데이터

사용 쿼리 툴) tadpole docker version

 

 

그럼 이 entity를 이용해서 데이터를 조인하여 조회해보자.

 

1. 단순 조회

우선 첫 번째로 JpaRepository 인터페이스 사용 시 기본적으로 제공하는 findAll을 사용해보자.

@Repository
public interface WedulClassesRepository extends JpaRepository<WedulClasses, Long> {

    Optional<WedulClasses> findByClassesName(String classesName);

}

이를 사용하여 데이터를 조회해보면 사용 되는 쿼리는 다음과 같다.

-- classes 목록을 조회하는 쿼리
select
    wedulclass0_.wedul_classes_id as wedul_cl1_0_
    ,wedulclass0_.create_at as create_a2_0_
    ,wedulclass0_.update_at as update_a3_0_
    ,wedulclass0_.classes_addr as classes_4_0_
    ,wedulclass0_.classes_name as classes_5_0_
  from
    wedul_classes wedulclass0_
;


-- 아래 쿼리들은 wedul_classes_id 개수별로 조회되는 쿼리
select
    wedulstude0_.wedul_classes_id as wedul_cl7_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_1_
    ,wedulstude0_.create_at as create_a2_1_1_
    ,wedulstude0_.update_at as update_a3_1_1_
    ,wedulstude0_.student_age as student_4_1_1_
    ,wedulstude0_.student_name as student_5_1_1_
    ,wedulstude0_.student_type as student_6_1_1_
    ,wedulstude0_.wedul_classes_id as wedul_cl7_1_1_
  from
    wedul_student wedulstude0_
  where
    wedulstude0_.wedul_classes_id = ?
;
select
    wedulstude0_.wedul_classes_id as wedul_cl7_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_1_
    ,wedulstude0_.create_at as create_a2_1_1_
    ,wedulstude0_.update_at as update_a3_1_1_
    ,wedulstude0_.student_age as student_4_1_1_
    ,wedulstude0_.student_name as student_5_1_1_
    ,wedulstude0_.student_type as student_6_1_1_
    ,wedulstude0_.wedul_classes_id as wedul_cl7_1_1_
  from
    wedul_student wedulstude0_
  where
    wedulstude0_.wedul_classes_id = ?
;
select
    wedulstude0_.wedul_classes_id as wedul_cl7_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_1_
    ,wedulstude0_.create_at as create_a2_1_1_
    ,wedulstude0_.update_at as update_a3_1_1_
    ,wedulstude0_.student_age as student_4_1_1_
    ,wedulstude0_.student_name as student_5_1_1_
    ,wedulstude0_.student_type as student_6_1_1_
    ,wedulstude0_.wedul_classes_id as wedul_cl7_1_1_
  from
    wedul_student wedulstude0_
  where
    wedulstude0_.wedul_classes_id = ?
;

쿼리를 자세히 보면 알겠지만 wedul_classes를 조회하는 쿼리와 그 wedul_classes 개수만큼 쿼리가 실행되는것을 볼 수 있다.

많이 들어 봤을 법한 N+1 문제가 발생한 것이다.

이 방식으로 쿼리 수행 시 N번의 쿼리가 발생해야 하기에 데이터 수만큼 쿼리가 실행되는 안좋은 부담을 안고 가야해서 좋지 않다.

 

2. left fetch join

위의 1번의 N+1 문제 해결로 고안된 방법 중 하나가 fetch join이다. 나는 left join을 하고자 하기에 left fetch join을 시도해보자. 우선 사용된 코드는 다음과 같다.

@Repository
public interface WedulClassesRepository extends JpaRepository<WedulClasses, Long> {
    @Query(value = "select DISTINCT c from WedulClasses c left join fetch c.wedulStudentList")
    List<WedulClasses> findAllWithStudent();
}

distinct가 붙은 이유는 카티션곱에 의해서 여러개의 결과값이 발생해 버리기 때문에 추가하였다.

그럼 사용된 쿼리도 확인해보자.

select
    distinct wedulclass0_.wedul_classes_id as wedul_cl1_0_0_
    ,wedulstude1_.wedul_student_id as wedul_st1_1_1_
    ,wedulclass0_.create_at as create_a2_0_0_
    ,wedulclass0_.update_at as update_a3_0_0_
    ,wedulclass0_.classes_addr as classes_4_0_0_
    ,wedulclass0_.classes_name as classes_5_0_0_
    ,wedulstude1_.create_at as create_a2_1_1_
    ,wedulstude1_.update_at as update_a3_1_1_
    ,wedulstude1_.student_age as student_4_1_1_
    ,wedulstude1_.student_name as student_5_1_1_
    ,wedulstude1_.student_type as student_6_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_0__
    ,wedulstude1_.wedul_student_id as wedul_st1_1_0__
  from
    wedul_classes wedulclass0_
      left outer join wedul_student wedulstude1_
        on wedulclass0_.wedul_classes_id = wedulstude1_.wedul_classes_id

left join을 해서 한번에 데이터를 가져올 수 있는 걸 확인 할 수 있지만 아쉽게도 Lazy로 데이터를 가져오지 못하고 Eager로 가져와야 한다.

 

3. EntityGraph

이제 3번째 방식으로 entity graph를 사용하여 실행시켜보자. 코드는 아래와 같다.

@EntityGraph(attributePaths = "wedulStudentList")
@Query("select c from WedulClasses c")
Page<WedulClasses> findEntityGraph(Pageable pageable);

실행되는 쿼리는 다음과 같아서 2번과 동일하다. (page를 사용한 것만 차이)

select
    wedulclass0_.wedul_classes_id as wedul_cl1_0_0_
    ,wedulstude1_.wedul_student_id as wedul_st1_1_1_
    ,wedulclass0_.create_at as create_a2_0_0_
    ,wedulclass0_.update_at as update_a3_0_0_
    ,wedulclass0_.classes_addr as classes_4_0_0_
    ,wedulclass0_.classes_name as classes_5_0_0_
    ,wedulstude1_.create_at as create_a2_1_1_
    ,wedulstude1_.update_at as update_a3_1_1_
    ,wedulstude1_.student_age as student_4_1_1_
    ,wedulstude1_.student_name as student_5_1_1_
    ,wedulstude1_.student_type as student_6_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_0__
    ,wedulstude1_.wedul_student_id as wedul_st1_1_0__
  from
    wedul_classes wedulclass0_
      left outer join wedul_student wedulstude1_
        on wedulclass0_.wedul_classes_id = wedulstude1_.wedul_classes_id
  order by
    wedulclass0_.update_at desc;

 

4. QueryDSL

Querydsl은 정적 타입을 이용해서 SQL과 같은 쿼리를 사용할 수 있도록 해주는 프레임워크로 HQL쿼리를 실행하게 도와준다.

설정 방식은 gradle 5 기준으로 다음과 같다.

plugins {
    id 'org.springframework.boot' version '2.2.0.RELEASE'
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
    id 'java'
}

group = 'com.wedul'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
    maven { url "https://plugins.gradle.org/m2/" }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compile group: "org.flywaydb", name: "flyway-core", version: '5.2.4'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'mysql:mysql-connector-java'
    compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.0'
    annotationProcessor 'org.projectlombok:lombok'
    testCompile group: 'org.mockito', name: 'mockito-all', version:'1.9.5'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // query dsl
    compile("com.querydsl:querydsl-apt")
    compile("com.querydsl:querydsl-jpa")
}

// querydsl 적용
def querydslSrcDir = 'src/main/generated'

querydsl {
    library = "com.querydsl:querydsl-apt"
    jpa = true
    querydslSourcesDir = querydslSrcDir
}

compileQuerydsl{
    options.annotationProcessorPath = configurations.querydsl
}

configurations {
    querydsl.extendsFrom compileClasspath
}

sourceSets {
    main {
        java {
            srcDirs = ['src/main/java', querydslSrcDir]
        }
    }
}

그리고 QueryDsl 사용을 위해 QueryDslRepositorySupport를 상속받아서 사용할 수 있는데 마지막에 distinct를 사용한 것은 2번 fetch 조인의 이유와 동일하다.

@Repository
public class WedulClassesQueryDsl extends QuerydslRepositorySupport {

    public WedulClassesQueryDsl() {
        super(WedulClasses.class);
    }

    public List<WedulClasses> findAllWithStudent() {
        QWedulClasses wedulClasses = QWedulClasses.wedulClasses;
        QWedulStudent wedulStudent = QWedulStudent.wedulStudent;

        return from(wedulClasses)
            .leftJoin(wedulClasses.wedulStudentList, wedulStudent)
            .fetchJoin()
            .distinct()
            .fetch();
    }

}

그럼 마찬가지로 실행되는 쿼리를 확인해보자.

select
    distinct wedulclass0_.wedul_classes_id as wedul_cl1_0_0_
    ,wedulstude1_.wedul_student_id as wedul_st1_1_1_
    ,wedulclass0_.create_at as create_a2_0_0_
    ,wedulclass0_.update_at as update_a3_0_0_
    ,wedulclass0_.classes_addr as classes_4_0_0_
    ,wedulclass0_.classes_name as classes_5_0_0_
    ,wedulstude1_.create_at as create_a2_1_1_
    ,wedulstude1_.update_at as update_a3_1_1_
    ,wedulstude1_.student_age as student_4_1_1_
    ,wedulstude1_.student_name as student_5_1_1_
    ,wedulstude1_.student_type as student_6_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_0__
    ,wedulstude1_.wedul_student_id as wedul_st1_1_0__
  from
    wedul_classes wedulclass0_
      left outer join wedul_student wedulstude1_
        on wedulclass0_.wedul_classes_id = wedulstude1_.wedul_classes_id

애도 2번, 3번과 동일한 쿼리가 작성되는 걸 확인할 수 있다.

기본적으로 단순하게 다대일 데이터를 가져오려고 하면 N+1 문제가 발생할 수 있기 때문에 조심해야하고 이를 해결하기 위해서는 다양한 방식의 문제 해결 방식이 있는걸 확인할 수 있었다.

무엇이 가장 좋은지는 본인이 판단하거나 상황에 맞게 사용하면 좋을 거 같다.

댓글()

스프링 부트에서 사용하는 JPA 기능 정리

web/JPA|2018. 11. 4. 23:53

스프링 프레임워크에서 제공하는 JPA는 별도의 구현 클래스 없이 인터페이스만을 사용할 수 있도록 제공한다. 제공되는 인터페이스 JpaRepository는 실행시점에 자동으로 인터페이스 내용을 연결하는 엔티티에 맞게 자동으로 구현해준다. 만약 스프링 JPA 인터페이스에서 제공하지 않는 기능을 사용하고 싶을 때는 메서드명을 특정한 규칙대로 만들어서 사용하면 인터페이스가 알아서 그 이름에 맞는 JPQL을 만들어서 실행해준다.


스프링 JPA 인터페이스는 Mysql같은 RDBMS 뿐만 아니라 Mongodb, Redis와 같은 NoSQL에도 동일한 인터페이스를 사용해서 기능을 사용할 수 있도록 제공해준다. 공통으로 사용할 수 있기에 아주 편리하다.


우선 스프링 부트에 JPA를 사용하기 위해서 Gradle에 라이브러리를 넣자.

1
compile ('org.springframework.boot:spring-boot-starter-data-jpa')
cs

 

기본적인 구조

JpaRepository를 상속받아 구현하고자 하는 인터페이스를 만들고 제네릭에 구현하려는 엔티티와 엔티티의 식별자 타입을 지정하여 인터페이스를 선언한다.

1
2
3
public interface StudentRepository extends JpaRepository<Student, Long> {
}
 
cs


주요 메서드 몇개만 정리해보자.

save() : 저장하거나 업데이트한다.

delete(entity) : em.remove() 호출하여 엔티티를 제거한다.

findOne(ID) : em.find() 호출하여 엔티티를 찾는다.

findAll() : 엔티티 모두를 조회한다. 


JpaRepository 인터페이스로 부족한 기능을 구현한 PagingAndSortingRepository CrudRepository 사용해서 보완할 있다.


JPA에서 메서드 사용하는 방법

JPA에서 엔티티에 맞는 메서드를 사용하는 방법은 크게 3가지이다.

- 메서드 이름으로 쿼리 생성

- 메서드 이름으로 JPA NamedQuery 호출

- @Query 어노테이션을 사용해서 레포지토리 인터페이스에 쿼리 직접 정의


1. 메서드 이름으로 쿼리 생성

메서드 이름으로 쿼리를 생성할 있는데 정식 Document 이용하면 자세히 나와있다

https://docs.spring.io/spring-data/jpa/docs/2.1.2.RELEASE/reference/html/#jpa.query-methods


2. 메서드 이름으로 JPA NamedQuery 호출

JPA NamedQuery 쿼리에 이름을 부여해서 사용하는 방법은 다음과 같이 엔티티에 선언해주면 된다.

1
2
3
4
5
@NamedQuery(
  name="student.findByName",
  query="select s from student s where s.name = :name")
public class Student {

}
cs


위와 같이 선언하고 실제 사용할 때는 entityManager에 아래와 같이 createQuery를 사용해서 쿼리를 호출하면 된다.

1
2
3
4
5
6
7
@PersistenceContext
private EntityManager entityManager;
 
public List<Student> findByUser(String name) {
  List<Student> students = entityManager.createQuery("student.findByName", Student.class).setParameter("name""wedul").getResultList();
}
 
cs


위와 같이 EntityManager를 사용할 수 있지만 스프링 JPA를 사용하여 간단하게 메소드 이름만으로 호출이 가능하다. 이렇게 호출하면 레포지토리에서 Student.쿼리메소드 형태로 쿼리를 찾는다. 만약 실행할 쿼리가 없으면 메서드 이름으로 쿼리를 자동으로 바꿔 동작한다.

1
2
3
4
5
public interface StudentRepository extends JpaRepository<Student, Long> {
  
  List<Student> findByUserName(@Param("name") String name);
  
}
cs


3. @Query 어노테이션을 사용해서 레포지토리 인터페이스에 쿼리 직접 정의

2번에서는 @Entity 클래스에서 정의한 쿼리를 레포지토리에서 메소드 형태로 접근하여 사용하였다.  이번에는 레포지토리에서 직접적으로 쿼리를 만들어서 조회하는 방식을 확인해보자.


@Query("select s from Student s where s.name = ?1")

Student findByName(String name);


인터페이스에 정의하는 메소드에 @Query 어노테이션을 붙혀서 정의하고 사용하면 된다. 바인딩 값은 1부터 시작한다. 스프링 데이터 JPA에서는 ?1 ?2 같은 위치기반 파라미터와 :name 같은 이름 기반 방식을 모두 사용가능하다.


페이징과 정렬

스프링 데이터 JPA에서 쿼리 메서드에 페이징과 정렬 기능을 사용할 있다. 파라미터로 Pageable 인터페이스를 사용할 경우에는 Page 또는 List 반환 받을 있다.

1
2
3
4
public interface StudentRepository extends JpaRepository<Student, Long> {
 
  Page<Student> findByNameStartingWith(String name, Pageable pageable);
}
cs


실제 사용할 때는 Pageable 인터페이스이기 때문에 구현체인 PageRequest 객체를 사용해서 사용한다.

// 파라미터 순서대로 페이지 번호, 사이즈, 정렬 기준 등으로 사용한다.

1
PageRequest pageRequest = new PageRequest(0, 10, new Sort(Sort.Direction.DESC, "name"));
cs


반환되는 값인 Page에서 제공하는 다양한 메소드를 사용해서 편하게 페이징과 소트 기능을 사용할 있다. 

컨트롤러에서 사용자 요청에게 전달되는 페이징 정보를 받기 위해서는 다음과 같이 Pageable 인터페이스를 받으면 되고 받을 속성값은 page, size, sort 사용해서 받는다. (/student?page=0&size=20&sort=name,desc&sort=address.city)


1
2
3
4
5
6
7
@GetMapping("/student")
public String list(Pageable pageable, Model model) {
  Page<Student> page = studentRepository.findByNameStartingWith("dd", pageable);
 
  return "dfdfdf";
}
 
cs


참고 싸이트

https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Page.html


출처. : 자바 ORM 표준 JPA 프로그래밍


댓글()
  1. ParkNoIl 2020.02.25 16:10 댓글주소  수정/삭제  댓글쓰기

    안녕하세요 선배님? 잘지내시나요?

    다름이 아니라 회사 내에서 응용프로그램(.exe파일)을 개발 해야하는데,

    JPA에 대해 공부도 할겸 적용을 해볼까하는데, 응용프로그램에 JPA를 적용 할수 있나요?

    JPA 관련 인강 및 구글링에서는 대부분 웹과 연동 하여 사용한 내용만 있어서요..

    답변 부탁드립니다. 감사합니다.

    • Favicon of https://wedul.site BlogIcon 위들 wedul 2020.02.29 22:29 신고 댓글주소  수정/삭제

      안녕하세요 잘지내죠??

      exe 프로그램을 자바로 만드시려면 rcp 아니면 swing으로 하실거 같은데

      jpa를 spring data를 사용하는 방식처럼은 사용하기 어려워도 https://wiki.eclipse.org/EclipseLink/Examples/JPA/RCP

      https://stackoverflow.com/questions/7491761/jpa-on-a-desktop-swing-application

      위 링크들과 같이 EntityManager를 잘 활용하면 어플리케이션에서도 사용할 수 있을거 같아요.

    • ParkNoIl 2020.03.01 08:50 댓글주소  수정/삭제

      선배님 답변 감사합니다.

Elasticsearch에서 Full text queries와 Term level queries 정리

Elasticsearch를 사용하다보면 term과 match 관련된 쿼리에 대해 헷갈리는 경우가 많다. Document에 있는 내용을 가져와서 정리해보자.


Full text queries

The high-level full text queries are usually used for running full text queries on full text fields like the body of an email. They understand how the field being queried is analyzed and will apply each field’sanalyzer (or search_analyzer) to the query string before executing.


match query

The standard query for performing full text queries, including fuzzy matching and phrase or proximity queries.

-> 전체 텍스트 쿼리를 수행하기 위한 기본 쿼리 (fuzzy 매치와 phrase, 근접 쿼리를 포함). 쉽게 말해 전체 텍스트에서 특정 부분이 포함되는지 여부를 확인하는데 fuzzy를 이용하여 유사도가 어느정도 되는 쿼리들도 조회 가능


match_phrase query

Like the match query but used for matching exact phrases or word proximity matches.

-> match와 같지만 정확한 구문이나 근접하게 매치되는 단어를 찾기위해 사용되는 쿼리


match_phrase_prefix query

The poor man’s search-as-you-type. Like the match_phrase query, but does a wildcard search on the final word.

-> match_phrase와 유사하지만 마지막 단어에서 와일드카드 사용됨. (자동완성 기능에서 자주 실행 됨)


multi_match query

The multi-field version of the match query.

-> match 쿼리의 다중 필드 버전


common terms query

A more specialized query which gives more preference to uncommon words.

-> Documentd에서 자주 발생되는 단어를 제외하고 진짜 의미있는 단어만 검색하기 위해서 사용될 때 사용하는 쿼리

-> cutoff_frequency 옵션을 사용해서 0.1%보다 빈도수가 많은 단어는 제외하고 검색하라고 할 수 있다.

"query": { "common": { "body": { "query": "this is bonsai cool", "cutoff_frequency": 0.001 } } }


query_string query

Supports the compact Lucene query string syntax, allowing you to specify AND|OR|NOT conditions and multi-field search within a single query string. For expert users only.

-> 컴팩트 한 Lucene 쿼리 문자열 구문을 지원하므로 단일 쿼리 문자열 내에서 AND | OR NOT 조건과 다중 필드 검색을 지정할 수 있습니다.

-> 검색이 실행될 필드를 지정하고 쿼리에 사용된 AND|OR|NOT 조건에 사용된 값들은 독립적으로 텍스트를 분석해서 결과를 도출한다.

-> 단 Lucene 파서를 사용하기 때문에 reserved character가 들어간 경우 검색이 안되니 반드시 \\ 이것을 붙혀서 escape 처리를 해줘야 한다.

"query": {
    "query_string" : {
        "default_field" : "content",
        "query" : "this AND that OR thus"
    }
}


simple_query_string query

A simpler, more robust version of the query_string syntax suitable for exposing directly to users.

-> query_string을 더 간단하게 사용할 수 있도록 제공된 쿼리




Term level queries

While the full text queries will analyze the query string before executing, the term-level queriesoperate on the exact terms that are stored in the inverted index, and will normalize terms before executing only for keyword fields with normalizer property.

These queries are usually used for structured data like numbers, dates, and enums, rather than full text fields. Alternatively, they allow you to craft low-level queries, foregoing the analysis process.


term query

Find documents which contain the exact term specified in the field specified.

-> 검색하고자 하는 필드에 정확한 텀을 포함하고 있는지 확인하는 쿼리


terms query

Find documents which contain any of the exact terms specified in the field specified.

-> 검색하고자 하는 필드에서 정확한 텀들을 포함하고 있는지 확인하는 쿼리


terms_set query

Find documents which match with one or more of the specified terms. The number of terms that must match depend on the specified minimum should match field or script.

-> 지정된 용어 중 하나 이상과 일치하는 문서를 찾는다. 지정된 최소값과 일치해야하는 용어의 수는 필드 또는 스크립트와 일치해야 한다.

"query": {
    "terms_set": {
        "codes" : {
            "terms" : ["abc", "def", "ghi"],
            "minimum_should_match_field": "required_matches"
        }
    }
}


range query

Find documents where the field specified contains values (dates, numbers, or strings) in the range specified.

-> 검색하고자 하는 필드에서 입력한 범위에 포함되는 날짜나, 숫자, 문자열등을 검색할 때 사용.


exists query

Find documents where the field specified contains any non-null value.

-> 검색하고자 하는 필드에 값이 있는지 여부 확인


prefix query

Find documents where the field specified contains terms which begin with the exact prefix specified.

->검색하고자 하는 필드에서 입력한 값으로 시작하는 문장이 있는지 검색할 때 사용


wildcard query

Find documents where the field specified contains terms which match the pattern specified, where the pattern supports single character wildcards (?) and multi-character wildcards (*)

-> 와이드카드 *, ?를 사용해서 검색하는 방법. 전체를 다 뒤져서 찾아야하기 때문에 성능에 아주 안좋은 결과를 초래한다.


regexp query

Find documents where the field specified contains terms which match the regular expression specified.

-> 정규식을 이용해서 정확한 문장 찾기


fuzzy query

Find documents where the field specified contains terms which are fuzzily similar to the specified term. Fuzziness is measured as a Levenshtein edit distance of 1 or 2.

-> 입력한 term과 비슷한 문자를 찾을 때 사용하는 쿼리 레벤스테인 거리 알고리즘을 사용하고 설정한 거리에 따라서 어느정도 유사도에 값을 찾을지 정할 수 있다.


type query

Find documents of the specified type.

-> 입력한 타입에 맞는 값을 찾는 쿼리

"query": {
    "query_string" : {
        "default_field" : "content",
        "query" : "this AND that OR thus"
    }
}


ids query

Find documents with the specified type and IDs.

-> 타입과 IDS를 가진 값을 찾는 쿼리

"query": {
    "ids" : {
        "type" : "_doc",
        "values" : ["1", "4", "100"]
    }
}












match_phrase와 term의 차이 참고 사이트

https://findstar.pe.kr/2018/01/19/understanding-query-on-elasticsearch/

댓글()

Elasticsearch query string 조회시 parse exception 에러 처리

elasticsearch에서 query_string로 데이터 조회시에 쿼리문으로 ) 특수문자가 포함하여 조회했다. 하지만 다음과 같이 문제가 발생했다.


1
2
3
4
5
6
7
8
9
10
11
{
  "error": {
    "root_cause": [
      {
        "type": "parse_exception",
        "reason": "parse_exception: Encountered \" \")\" \") \"\" at line 1, column 11.\nWas expecting one of:\n    <EOF> \n    <AND> ...\n    <OR> ...\n    <NOT> ...\n    \"+\" ...\n    \"-\" ...\n    <BAREOPER> ...\n    \"(\" ...\n    \"*\" ...\n    \"^\" ...\n    <QUOTED> ...\n    <TERM> ...\n    <FUZZY_SLOP> ...\n    <PREFIXTERM> ...\n    <WILDTERM> ...\n    <REGEXPTERM> ...\n    \"[\" ...\n    \"{\" ...\n    <NUMBER> ...\n    "
      }
    ],
    "type": "search_phase_execution_exception",
    "reason": "all shards failed",
    "phase": "query",
cs


확인해보니 + - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ / 포함된 문장을 query_string 통해서 조회하려고 하면 에러를 발생시킨다. 그래서 이를 해결하기 위해서 위에 reserved character들이 들어간 단어는 \\를 붙여주어야 한다.


이를 위한 자바스크립트는 다음과 같다.

1
2
3
async escapeReservedCharacter(query) {
  return query.replace(/([!*+&|()<>[\]{}^~?:\-="/\\])/g, '\\$1');
}
cs


이를 해결해서 query_string을 사용하면 문제가 해결된다.


참고 : https://stackoverflow.com/questions/26431958/escaping-lucene-characters-using-javascript-regex




댓글()

elasticsearch percolating 쿼리

엘라스틱 서치에서 일반적인 검색 기능은 특정 인덱스에 문서를 저장하고, 쿼리에 매칭되는 문서를 불러오는 방식으로 수행된다.

하지만 percolating 쿼리 방식은 그 반대로 동작한다. 쿼리를 사전에 저장하고, 새로 유입된 문서가 매칭되는 쿼리가 있는지 확인해 매칭되는 쿼리를 반환한다.

업무적으로 필요한 기능이어서 알아보던 중 알게되어서 정리해본다.

https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-percolate-query.html



인덱스 생성

아래 인덱스생성에 보면 두 가지 필드를 볼 수있다. 먼저 message 필드는 percolator에서 정의된 문서를 임시 인덱스로 인덱싱하기 전에 사전 처리하는 데 사용되는 필드이다. query 필드는 쿼리 문서를 인덱싱하는 데 사용된다. 실제 Elasticsearch 쿼리를 나타내는 json 객체를 보유한다. query 필드는 쿼리 dsl을 이해하고 이후에 percolate 쿼리에 정의 된 문서와 일치시키기 위해 쿼리를 저장한다.

이해하기 어려운데, 자세한 내용은 사용방법을 더 보면 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PUT wedul_product
{
  "mappings": {
    "seat": {
      "properties": {
        "message": {
          "type": "text"
        },
        "query": {
          "type": "percolator"
        }
      }
    }
  }
}
 
cs


쿼리 삽입
상품명을 가지고 있는 문서에서 특정 지역들에 대한 정보가 들어있는지 확인하기 위해서 지역정보 쿼리를 미리 넣어둔다. (춘천, 서울, 등등...)

1
2
3
4
5
6
7
8
POST /wedul_product/seat/?refresh
{
    "query" : {
        "match" : {
            "message" : "춘천"
        }
    }
}
cs



문서 매칭되는 쿼리 찾아보기
'위들아이패드 서울 지점'과 '맥북 부산시 AS 지점' 두 개를 검색해보고 매칭 결과를 확인해보자. 쿼리는 score를 고려하는 것과 percolate를 사용해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#쿼리 (score 없는 filter 사용)
GET /wedul_product/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "percolate": {
          "field": "query",
          "document": {
            "message": "위들아이패드 서울 지점"
          }
        }
      }
    }
  }
}
 
#결과
{
  "took": 55,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "wedul_product",
        "_type": "seat",
        "_id": "7dAz8mUBIvIDO7uZfj1s",
        "_score": 1,
        "_source": {
          "query": {
            "match": {
              "message": "서울"
            }
          }
        },
        "fields": {
          "_percolator_document_slot": [
            0
          ]
        }
      }
    ]
  }
}
 
# score가 고려된 percolate사용
GET /wedul_product/_search
{
  
  "query" : {
        "percolate" : {
            "field": "query",
            "document" : {
                "message" : "맥북 부산 시 AS 지점"
            }
        }
    }
}
 
#결과
{
  "took": 3,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.2876821,
    "hits": [
      {
        "_index": "wedul_product",
        "_type": "seat",
        "_id": "BtAz8mUBIvIDO7uZkz5Q",
        "_score": 0.2876821,
        "_source": {
          "query": {
            "match": {
              "message": "부산"
            }
          }
        },
        "fields": {
          "_percolator_document_slot": [
            0
          ]
        }
      }
    ]
  }
}
cs


이를 이용해서 다양한 것을 할 수 있을 것 같다.

네이버에서도 이 기능을 이용해서 로그 알림 기능을 만들었다. 참고하면 좋을 것 같다.
https://d2.naver.com/helloworld/1044388

댓글()

Elasticsearch 질의 DSL 정리

엘라스틱 서치를 공부하면서 봤던 DSL 쿼리를 정리해보자.


Query와 Filter의 차이

Query 일반적으로 Full Text Search(전문검색) 사용되고 필터는 YES/NO 조건의 바이너리 구분에 주로 사용된다.
쿼리는 scoring 계산되나 필터는 계산되지 않는다.
쿼리 결과는 캐싱되지 않고 필터 결과는 캐싱된다.
상대적으로 쿼리는 응답속도가 느리고 필터는 응답속도가 빠르다.


term
- term 색인이 나눠지면서 형태소로 나누어지는 저장되는 토큰등을 term이라고 한다term 쿼리는 주어진 질의문과 저장된 텀과 정확히 일치하는 문장을 찾는다.
- term으로 "name" : "cjung gglee" 라고 입력하게 되는경우에는 "cjung gglee"라는 하나의 term 찾기 때문에 결과가 나오지 않는다만약 2 이상의 term 같이 검색하고 싶을 때는 terms 쿼리를 이용해야 한다.

1
2
3
4
5
6
7
8
9
10
# terms의 사용으로 두가지 term으로 사용할 수 있다.
GET /bank/_search
{
  "query": {
    "terms": {
      "age": [3020]
    }    
  }
}
 
cs


match

match 쿼리도 term 쿼리와 마찬가지로 주어진 질의문을 색인된 term 비교해서 일치하는 도큐먼트를검색하는 질의다만 term 쿼리와 다르게 match 쿼리에서는 주어진 질의문 또한 형태소 분석을 거친 분석된 질의문으로 검색을 수행한다예를 들면 The And 검색하면 매치 쿼리는  질의문을 형태소 분석을 거쳐서 the and 질의문을 바꾸고  값을 term 비교해서 검색한다.

그리고 기본적으로 match 들어가는 데이터들은 or 검색으로 진행된다다시말하면 아래의 예에서는Diamond 또는 Street 또는 Bartlett 라는 term으로 검색한다이것을 and 바꾸고 싶은 경우에는 "operator" : "and" 옵션을 넣어주어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GET /bank/_search
{
  "query": {
    "match": {
      "address": "Diamond Street Bartlett"
    }    
  }
}
 
 
operator 적용
GET /bank/_search
{
  "query": {
    "match": {
      "address": {
        "query" : "Diamond Street Bartlett",
        "operator" : "and"
      }
    }    
  }
}
 
cs


이렇게 색인을 나누고 term을 구분할 때 사용하는 형태소 분석기를 설정하기 위해서  analyzer 선택할  있다. (굳이 지금은 볼필요가 없을 것 같아 생략.)


multi_match 
여러 필드에 대한 조건을 검색할 
blance age 필드에서 값을 조회한다
두개세개 등등의 필드에서 검색한다.

1
2
3
4
5
6
7
8
9
10
GET /bank/_search
{
  "query": {
    "multi_match": {
      "fields": ["balance", "age"],
      "query": 20
    }
  }
}
 
cs


(bool) 쿼리
조건문인  조합으로 적용해서 최종 검색 결과를 찾아내자.
bool 조건에는 must, must_not, should가 존재한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
GET /bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "address": {
              "value": "800"
            }
          }
        }
      ],
      "must_not": [
        {
          "term": {
            "state": {
              "value": "ID"
            }
          }
        }
      ]
    }
  }
}
 
cs


문자열 쿼리(q=)
URL 검색에서 q 매개변수에 다양한 질의문을 사용해서 검색을 수행했던 방식과 동일하게 사용할수 있는 방식이다.
여러 필드의 조건으로 검색 가능

1
2
3
4
5
6
7
8
9
10
GET /bank/_search
{
  "query": {
    "query_string": {
      "default_field": "address",
      "query": "Street 800"
    }
  }
}
 
cs


접두어 쿼리

1
2
3
4
5
6
7
8
9
10
11
GET /bank/_search
{
  "query": {
    "prefix": {
      "address": {
        "value": "800"
      }
    }
  }
}
 
cs


범위 쿼리 (날짜와 시간 데이터도 범위쿼리 사용가능)

1
2
3
4
5
6
7
8
9
10
11
12
GET /bank/_search
{
  "query": {
    "range": {
      "balance": {
        "gte": 10,
        "lte": 201212132
      }
    }
  }
}
 
cs


퍼지(fuzzy) 쿼리
주어진 질의문을 레벤슈타인 거리(Levenshtein distance) 알고리즘을 기반으로 유사한 단어의 검색을 지원
주소이름이 Noble 유사한 데이터들이 출력
특정 페이지 출력도 가능

1
2
3
4
5
6
7
8
9
GET /bank/_search
{
  "query": {
    "fuzzy": {
      "address": "Noble"
    }
  }
}
 
cs


Filter 쿼리
메모리도 캐싱되고 점수도 따지지 않기 때문에 단순 검색에서는 Filter 사용하라.
예전에는 filter 밖에 있을수 있었지만 이제는 query, bool 안에 있어야 사용가능

1
2
3
4
5
6
7
8
9
10
11
12
GET /bank/_search
{
  "query": {
    "bool": {
      "filter": {
        "terms": {
          "address": ["800", "Street"]
        }
      }
    }
  }
}
cs


댓글()

Spring boot hibernate jpa에서 Auditing 사용 - update, create 시간 자동 변경

web/Spring|2018. 10. 3. 23:55

엔티티를 만들고 데이터를 삽입하고 조작할 때 create date와 last modified date를 별도로 업데이트 해주면서 관리하였다.

하지만 이번에 JPA를 공부하면서 별도의 작업 없이 JPA의 Auditing 기능을 사용하면 데이터를 삽입하고 수정할 때 자동으로 날짜를 수정하도록 할 수 있는 기능이 있는 것을 확인했다.


1. Configuration

JPA Auditing을 사용하기 위해서는 기능을 자동으로 활성화 해주는 어노테이션을 붙혀주면 된다. 처음에는 @Configuration을 사용하는 클래스에 함께 선언해주었는데 정상적으로 적용이 되지 않아서 @SpringBootApplication을 사용하는 곳에 적용했더니 성공적으로 적용되었다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.wedul.springboottest;
 
import com.wedul.common.exception.ValidationException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.web.context.request.WebRequest;
 
import java.util.Map;
 
@SpringBootApplication
@EnableJpaAuditing
public class SpringboottestApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(SpringboottestApplication.class, args);
    }
 
    @Bean
    public ErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes() {
 
            @Override
            public Map<String, Object> getErrorAttributes(WebRequest webRequest,
                                                          boolean includeStackTrace) {
                Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
                Throwable error = getError(webRequest);
 
                // validatijon Exception에 경우 별도의 처리를 진행한 에러 데이터 추가
                if (error instanceof ValidationException) {
                    errorAttributes.put("errors", ((ValidationException)error).getErrors());
                }
                return errorAttributes;
            }
 
        };
    }
}
 
cs


2. Entity에 CreatedDate, LastModifiedDate

Auditing을 사용할 엔티티에는 몇가지 어노테이션이 사용된다.

먼저 설정을위해서 사용되는 @MappedSuperclass, @EntityListeners(AuditingEntityListener.class)이다. 첫 번재 어노테이션은 이후에 사용될 createdDate, modifiedDate와 같은 필드들을 컬럼으로 인식하게 도와주는 역할을 하고 두 번째 어노테이션은 해당 Entity에 Auditing기능을 포함한다라는 명시를 한다.

처음에는 필요한 Entity에만 CreatedDate, lastModifiedDate가 포함하도록 Entity마다 기재해주었다.하지만 많은 Entity에서 필요로 할 것 같고 필요할 때마다 새로 써주기가 비효율적인 것 같아서 추상클래스로 만들고 필요한 Entity에서 이를 상속받아서 사용하도록 하였다.



- 추상클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.wedul.common.dto;
 
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
 
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
 
/**
 * 모든 Entity들의 상위 클래스가 되어 Entity들의 createdDate, modifiedDate를 자동으로 관리
 *
 * @author wedul
 * @since 2018. 8. 14.
 **/
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class TimeEntity {
 
    @CreatedDate
    private LocalDateTime createdDate;
 
    @LastModifiedDate
    private LocalDateTime modifiedDate;
 
}
cs


- 상속받아서 사용하는 Product Entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.wedul.springboottest.product.dto;
 
import com.wedul.common.dto.BaseTimeEntity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
 
import javax.persistence.*;
import java.io.Serializable;
import java.sql.Timestamp;
 
/**
 * 상품 정보
 *
 * @author wedul
 * @since 2018. 08. 12
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity // class (hibernate)
@Table(name = "product")
public class ProductDto extends TimeEntity implements Serializable {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long productId;
 
    @Column(nullable = false, unique = true)
    private String productName;
 
    @Column(nullable = false)
    private long price;
 
    public ProductDto(ProductRequestDto req) {
        this.productName = req.getProductName();
        this.price = req.getPrice();
    }
 
}
 
cs


테스트 코드를 작성하여 정상적으로 시간값이 들어가고 또 변경되는지 확인해보자.

1. 처음 데이터 삽입시 시간값 입력 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
 * Product Test
 *
 * @author wedul
 * @since 2018. 8. 14.
 **/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest // rolleback 설정
@Rollback(value=true)
public class ProductTest {
 
    private MockMvc mockMvc;
    private final MediaType mediaType = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));
 
    @Autowired
    ProductCtrl productCtrl;
 
    @Autowired
    ProductServiceI productService;
 
    @Before // before 클래스는 한번만 실해되고 before는 각 테스트마다 실행된다.
    public void beforeClass() {
        this.mockMvc = standaloneSetup(productCtrl).build();
    }
 
    @Test
    public void insertTest_reqestBody() throws Exception {
 
        // Mock Request Builder
        MockHttpServletRequestBuilder req =
                post("/api/product/new")
                        .content(CommonUtil.getJsonStrFromObject(new ProductRequestDto("i-mac", 2220011L)))
                        .contentType(mediaType);
 
        // 테스트
        MvcResult result = mockMvc.perform(req).andExpect(status().isOk()).andReturn();
        ResultDto resultDto = CommonUtil.getObjectFromJsonStr(result.getResponse().getContentAsString(), ResultDto.class);
 
        assertTrue(resultDto.isResult());
    }
}
cs


정상적으로 날짜값이 들어가 있는 것을 확인 할 수있다. 이제 수정 테스트를 진행해보자.


2. 수정 후 last modified date 시간 변경여부 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void updateTest_requestBody() throws Exception {
    // Mock Request Builder
    MockHttpServletRequestBuilder req =
           put("/api/product/edit")
           .content(CommonUtil.getJsonStrFromObject(new ProductRequestDto(1,"macbook pro", 2312111L)))
           .contentType(mediaType);
 
    // 테스트
    MvcResult result = mockMvc.perform(req).andExpect(status().isOk()).andReturn();
    ResultDto resultDto = CommonUtil.getObjectFromJsonStr(result.getResponse().getContentAsString(), ResultDto.class);
 
    assertTrue(resultDto.isResult());
}
cs


업데이트 후에 정상적으로 modified_date만 변경된 것을 확인할 수있다.

좋은 기능인것같다. JPA를 공부하고 있는데 Mybatis보다 솔직히 불편하다 그런데 한번 잘 구축해놓으면 편하기는 하다. 그래도 무엇하나 변경된다면 결국 너무 손이 많이가고 러닝커브가 좀 심하다.

흠 더 사용해보고 실무에서 써봐야 이걸 왜 요새 사용하는지 더 이해할 수 있을 것 같다.

자세한 코드는 github 참조
https://github.com/weduls/spring_boot_test


댓글()
  1. Happy 2019.05.22 17:27 댓글주소  수정/삭제  댓글쓰기

    안녕하세요 글 잘보았습니다. :) 몇가지 궁금증이 생겨서 글을 쓰게 되었습니다.

    만일 특정 사용자가 서비스 로그인 이후에 로그아웃을 하고 다시 로그인을 하면 변경사항은 전혀 없습니다.

    그렇다면 임의로 제가 특정 컬럼을 변경시켜서 (더미컬럼 ?) 수정시간이 jpa auditing 에 의해서 변경되어야 하는건지. 궁금하네요.

    한편으로는 사용자에 해당하는 테이블에 현재 로그인 여부를 컬럼으로 추가해서 그렇게 수정하는 방법도 생각해볼 수 있습니다만 어떻게 하는게 효율적일지 몰라 이렇게 장문으로 글을 남깁니다.

    • Favicon of https://wedul.site BlogIcon 위들 wedul 2019.05.22 18:45 신고 댓글주소  수정/삭제

      안녕하세요!

      사용자의 마지막 로그인 시간을 별도의 컬럼으로 관리하시면 어떨까 싶습니다.

      그리고 현재 로그인중인 사용자를 테이블의 컬럼으로 관리하는건 힘들지 않을까 싶습니다.

Mysql 실행계획 설명

데이터베이스/mysql|2018. 10. 3. 23:43

프로그램의 성능을 높히기 위해서는 DB튜닝이 필요하다. 
Mysql에서 튜닝을 하기 위해서 제공하는 쿼리의 실행 계획에 대해 정리해보자.



Mysql의 데이터 처리 방식

우선 Mysql의 데이터 처리방식에 대해 정리해보자.

- Mysql은 단일 코어로 데이터를 처리하기 때문에 멀티코어로 scale out을 진행하는 것 보다 cpu 자체의 성능을 높히는 scale up을 하는 것이 더 효율적이다. 
- Oracle과 달리 mysql은 nested loop join 알고리즘만 사용한다.  
- Nested Loop Join은 선행 테이블의 검색 결과 값 하나하나 테이블 B와 조인하는 방식이다. 그래서 데이터 양이 적을 때는 상관이 없으나 데이터가 많은 테이블끼리 조인할 시 성능에 문제가 있을 수 있다. 그래서 내부적으로 join buffer를 사용하여 드라이빙 테이블에서 조인에 사용될 데이터를 찾아 join buffer에 채우고 조인 버퍼에서 조인 대상 B 테이블의 데이터를 스캔하면서 풀, 인덱스 스캔, 인덱스 범위 스캔등을 사용하여 테이블에 데이터와 조인한다. 



Mysql 쿼리 성능 진단 (for 최적화)
성능 진단을 위해서 사용하는 방법은 Explain을 사용하는 것이다.  

Explain을 사용해서 쿼리 실행계획을 살펴보면 하단에 그림과 같이 출력된다.


각 필드에 대한 설명은 다음과 같다. 
ID : Select 아이디 
Select_type : 참조 타입 
Table : 참조하는 테이블 
Type : 조인 타입 
Possible_keys : 데이터를 조회할 때 DB에서 사용할 수 있는 인덱스 리스트 
Key : 실제로 사용할 인덱스 
Key_len : 실제로 사용할 인덱스 길이 
Ref : Key 안의 인덱스와 비교하는 컬럼(상수) 
Rows : 쿼리 실행 시 조사하는 행 수 
Extra : 추가 정보 

이 필드중에 Select_type, type, Extra에 대해서만 잘 확인하면 좋은 쿼리를 작성할 수 있다.


Select_type 종류

구분
설명
예시
SIMPLE
UNION이나 서브쿼리가 없는 단순 SELECT를 의미한다. 
SELECT * FROM USER;
PRIMARY 
서브쿼리가 있을 때 가장 바깥쪽에 있는 SELECT 
SELECT * FROM (SELECT * FROM USER) t; 
DERIVED 
FROM절 안의 서브쿼리 
SELECT * FROM (SELECT * FROM USER) t; 
DEPENDENT SUBQUERY 
외부 쿼리와 상호 연관된 서브쿼리 
SELECT * FROM user u1 WHERE EXISTS ( 
    SELECT * FROM user u2 WHERE u1.user_id = u2.user_id 
);


Type
Type에는 system, const, ref... 등등 많이 있지만 성능상 문제가 되는 부분은 index, all이 두가지 타입이 문제다.
구분
설명
index 
인덱스를 처음부터 끝까지 찾아서 검색하는 경우로, 일반적으로 인덱스 풀스캔이라고 지칭 
all 
테이블 풀스캔으로 모든 부분을 스캔하는 것


Extra
쿼리 실행에 대한 추가적인 정보를 보여준다. 
하단의 대표적인 설명인 4가지중에서 특히 FileSort와 Using Temporary의 경우에는 쿼리 튜닝이 필요한 상태
구분
설명
Using Index 
인덱스를 이용해서 데이터를 추출
Using Where 
Where 조건으로 데이터를 추출.  (Type에서 All과 Index와 마찬가지로 성능에 문제) 
Using Filesort 
데이터의 정렬이 필요한 경우로써 데이터 양이 많을수록 성능에 직접적인 영향을 끼친다. 
Using Temporary 
내부적으로 Temporary Table을 사용하는 경우


Join 최적화 포인트
- Nested Loop 조인으로 되어있기 때문에 기준 테이블에서 조회되는 데이터양에 따라 연관 테이블의 데이터양이 결정되기 때문에 기준 테이블(왼쪽)의 데이터양을 줄이는 것이 관건. 
- Outer Join은 지양한다. 꼭 필요한 경우만 사용한다. 
- join시 조합 경우의 수를 줄이기 위해 복합 컬럼 index를 사용.


댓글()