데이터베이스

조회 성능 개선하기 ( ① 쿼리 최적화 )

dodop 2022. 4. 28. 00:07

 

 

이번엔 저번 화면 응답 개선에 이어 조회 성능을 개선하는 것을 배웠다!

먼저 쿼리 최적화에 대해서 정리하자. 

 

 

 

 

 

쿼리 동작 방식 

출처 NEXTSTEP 프로젝트 공방

  • 쿼리 캐싱 
    • KEY : SQL문, VALUE : 쿼리의 실행결과 인 Map
    • 데이터 변경시 모두 삭제되어 동시처리 성능저하를 일으키기 때문에 MySQL 8.0부터 사라졌다 
  • Parsing
    • 사용자로부터 요청된 SQL을 작게 만들어 서버가 이해할 수 있는 수준으로 분리 
  • Preprocessor ( 전처리기 )
    • 해당쿼리의 문법을 확인하여 오류가 있다면 처리중단 (일괄 처리내에 있으면 일괄처리 전체 중단)
    • 실제 존재하지 않거나 권한상 사용할 수 없는 개체의 토큰은 이 단계에서 걸러짐
  • Optimization
    • 실행계획(Exception Plan)은 이 단계에서의 출력을 의미
    • 사용자의 요청으로 들어온 쿼리 문장을 어떻게 저렴한 비용으로 가장 빠르게 처리할 지 결정하는 역할 
    • 쿼리분석 : WHERE 절의 검색 조건인지 JOIN 조건인지 판단
    • 인덱스 선택 : 각테이블의 조건과 인덱스 통계정보를 이용해 사용할 인덱스 선택
    • 조인처리 : 여러 테이블의 조인이 있는 경우 어떤 순서로 테이블 읽을 지 결정
  • Execution Engine
    • 만들어진 계획대로 각 핸들러에게 요청해서 받은 결과를 또 다른 핸들러 요청의 입력으로 연결하는 역할 
  • Handler (Storage Engine)
    • 실행엔진의 요청에 따라 데이터를 디스크로 저장하고 디스크로부터 읽어오는 역할 담당 
    • Sequential Access
      • 물리적으로 인접한 페이지를 차례대로 읽는 순차 접근 방법
      • 다중페이지 읽기 방식
      • ex)인접리스트 
    • Random Access 
      • 물리적으로 떨어진 페이지들에 임의로 접근하는 방식
      • 다중페이지 읽기 불가능
      • 데이터의 접근 수행시간이 오래걸림
      • ex)배열 

 

 

 

 

 

쿼리 최적화 체크리스트  
  • SELECT 시에는 꼭 필요한 칼럼만 불러오기 
    • 불필요한 불러오면 DB는 불필요하게 그만큼의 로드를 부담하게 된다. 
  • 조건에는 별도의 연산을 걸지 않고 값으로 조건 걸기
    •  연산을 걸게되면 Full Table Scan을 이용하여 모든 Cell을 탐색하고 수식을 이용한 조건 충족을 판단하게 된다.
  • LIKE 사용시 와일드 카드 문자열 (%)은 String 앞부분에 배치하지 않기 
    •  문자열 앞에 %을 걸게되면 Full Table Scan을 하게된다. 
  • DISTINCT를 사용한 중복값 제거 연산은 최대한 사용하지 않기 
    • 시간이 많이 걸린다. 
  • GROUP BY 연산 시에 HAVING 보다는 WHERE절 사용
    • WHERE절이 HAVING 절보다 먼저 실행되기 때문에 미리 데이터 크기를 줄일 수 있다. 
  • 3개 인상의 테이블을 INNER JOIN시에는 크기가 가장 작은 테이블을 FROM 절에 배치하기
    • 드라이븐 테이블의 액세스를 최소화 할 수 있다. 
    •  대부분의 Query Planner에서 INNER JOIN의 경우 효과적인 순서대로 바꿔주기는 하지만 Planning의 비용 또한 생각하여 미리 최적화된 INNER JOIN 순서를 입력단계에서 조정해두자. 
  • 자주 사용되는 데이터 형식에 대해서는 미리 전처리 테이블을 따로 보관/관리하기
    •  이 부분은 DB의 실시간이 필수적(운영계)이지 않은 분석계에서 많이 쓰인다. 
  • ORDER BY는 연산 중간에 사용하지 말기 
  • LIMIT 활용하기 

 

 

 

페이징 쿼리 

페이징 쿼리란 테이블의 레코드를 일정 단위로 잘라서 조회하는 것을 뜻한다(LIMIT).

## id 순서대로 정렬하고 20번째부터 10개를 읽는다
SELECT * FROM subway.programmer ORDER BY id LIMIT 20, 10;

그런데 LIMIT을 이용하여 조회할 경우 원하는 범위의 데이터만을 읽는 것이 아니라 처음부터 원하는 범위까지 다 읽고 앞에 부분을 버려 성능 저하된다. 아래와 같이 WHERE 조건을 사용하도록 하도록 하자. 

SELECT * FROM subway.programmer WHERE subway.programmer.id >= 20 ORDER BY id LIMIT 0, 10;

 

JPQL은 다음과 같이 쿼리에 조건을 추가해준다. 

@RestController
public class StationController {
    private StationService stationService;
    public StationController(StationService stationService) {
        this.stationService = stationService;
    }
    
    @GetMapping(value = "/stations", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<List<StationResponse>> showStations() {
        int page = 1;
        return ResponseEntity.ok().body(stationService.findAllStations(page));
    }
}
public class StationService {
    private StationRepository stationRepository;

    public static final int STATION_SIZE_PER_PAGE = 5;

    public StationService(StationRepository stationRepository) {
        this.stationRepository = stationRepository;
    }
    
    @Transactional(readOnly = true)
    public List<StationResponse> findAllStations(int page) {
        Pageable pageable = PageRequest.of(0, STATION_SIZE_PER_PAGE, Sort.by("id"));
        Page<Station> stations = stationRepository.findAllByPage((long) (page * STATION_SIZE_PER_PAGE), pageable);

        return stations.stream()
            .map(StationResponse::of)
            .collect(Collectors.toList());
    }
}
public interface StationRepository extends JpaRepository<Station, Long> {

    @Query("SELECT s FROM Station s WHERE s.id >= :offset")
    Page<Station> findAllByPage(Long offset, Pageable pageable);

}

 

 

 

 

쿼리 최적화 예시 
SELECT A.사원번호, A.이름, A.연봉, 직급.직급명, 사원출입기록.입출입시간, 사원출입기록.지역
FROM 
	(
	SELECT 사원.사원번호, 사원.이름, 급여.연봉 
	FROM 
		사원 
	JOIN 부서관리자 
		ON 사원.사원번호 = 부서관리자.사원번호 
	JOIN 부서
		ON 부서관리자.부서번호 = 부서.부서번호
	JOIN 급여 
		ON 사원.사원번호 = 급여.사원번호 
	WHERE  부서관리자.종료일자 >= NOW() 
	AND 부서.비고 = 'active' 
	AND 급여.종료일자 >= NOW()
	ORDER BY 연봉 DESC
	LIMIT 5
	) AS A
JOIN 직급 
	ON A.사원번호 = 직급.사원번호
JOIN 사원출입기록 
	ON A.사원번호 = 사원출입기록.사원번호 
WHERE 직급.종료일자 >= NOW()
AND 사원출입기록.입출입구분 = 'o'

최적화를 적용한 쿼리는 위와 같다. 

 

 

 

 

 

 

 

 

(참고한 사이트)

NextStep 프로젝트 공방 

https://edu.nextstep.camp/

 

NEXTSTEP

 

edu.nextstep.camp

https://medium.com/watcha/%EC%BF%BC%EB%A6%AC-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%B2%AB%EA%B1%B8%EC%9D%8C-%EB%B3%B4%EB%8B%A4-%EB%B9%A0%EB%A5%B8-%EC%BF%BC%EB%A6%AC%EB%A5%BC-%EC%9C%84%ED%95%9C-7%EA%B0%80%EC%A7%80-%EC%B2%B4%ED%81%AC-%EB%A6%AC%EC%8A%A4%ED%8A%B8-bafec9d2c073

 

✅ 쿼리 최적화 첫걸음 — 보다 빠른 쿼리를 위한 7가지 체크 리스트

DB에 대한 데이터 분석가의 에티켓

medium.com

https://12bme.tistory.com/73

 

[MySQL] 쿼리 실행 구조 및 쿼리 캐시

쿼리 실행 구조 쿼리 실행구조는 기능별로 다음과 같이 나눠질 수 있습니다. 1) 파서 파서는 사용자 요청으로 들어온 쿼리 문장을 토큰(MySQL이 인식할 수 있는 최소 단위의 어휘나 기호)으로 분리

12bme.tistory.com