COUNT() vs SELECT
LIMIT 조건의 영향
ORM 사용 시 주의점
COUNT(*) vs COUNT(DISTINCT)
countALL 과 selectALL 쿼리 차이를 확인하기 위해 두개의 쿼리를 작동해보자
where 조건절에는 ix-fd 조건은 인덱스를 사용할 수 있지만
non-ix-fd 조건은 인덱스를 사용 X
결론적으로 두 개의 쿼리는 커버링 인덱스 실행 계획 사용 X
ix-fd 컬럼의 인덱스 이용해 대상 레코드 찾은 후에 데이터파일에서 레코드를 가져오고 non-ixfd 컬럼값이 b인지 비교
최종적 반환
두개의 쿼리는 동일한 성능
select count 는 결과값의 바이트수가 적어 네트워크 사용량이 적지만
select all 쿼리는 모든 결과를 보내야 되 결과 바이트가 커짐 네트워크 사용량 많아짐
실제 mytsql서버가 데이터파일과 버퍼풀의 페이지를 읽어서 가공하는 작업은 거의 동일한 양 수행하게 됨
많은 레코드를 가져와도 표시못해서 limit 으로 페이징으로 가져감
결론적으로 select ALL 하고 count는 바이트수가 그리 큰차이는 아님
반면 count는 where 조건제를 일치하는 모든 레코드를 잃어야해서 (위에페이징은 일부분만)
전체 건수를 확인할 수 있어서 selcet counter 쿼리시간은 처리해야 하는 레코드 건수에 따라서
엄청 느려질 수 있음
ex) 게시판 갯수 100만건 select ALL은 20건만 count ALL은 100만건 읽어 5만배 정도의 쿼리 시간이 더많이 걸리게
됨
쿼리 실행 과정
쿼리 처리 성능
LIMIT의 영향
성능 차이의 주요 원인
실제 예시
커버링 인덱스란
예시:
테이블: users(id, name, email, age)
인덱스: (name, email)
쿼리: SELECT name, email FROM users WHERE name = 'John';
count(*) 성능개선
정확한 레코드 건수를 확인해야 하는 경우에는 최고의 성능 튜닝은
커버링 인덱스 실행계획으로 쿼리를 처리해주게 하면 됨
ix-fd1 컬럼 ix-fd2 컬럼으로 인덱스 준비되있따하자
select count(*) where ix-fd1 = ? and ix-fd21=?
이 2개의 쿼리는 인덱스의 컬럼만으로 쿼리가 처리될 수 있기 때문에 테이블의 데이터를 읽지 않고
인덱스만 읽으면 쿼리 처리 완료
select count(*) where ix_fd1=? and non_ix_fd1=?
select count(non_ix_fd1) where ix_fd1= ?
이 두개의 쿼리는 non-ixfd1 컬럼을 사용하고 있는데 이 컬럼은
인덱스 정의 컬럼이 아니기 때문에 mysql 서버는 인덱서를 통해 1차 대상 레코더를 찾은 후
데이터 파일에서 non-ixfd1 컬럼읽어서 where 조건에 일치하는지 비교해봐야 반환여부 결정
다음 쿼리는 non-ixfd1컬럼이 안늘인? notnull인 레코드의 끈수를 카운트해야하기 때문에
여전히 데이터 파일을 읽어야함 커버링 인덱스 처리 안됨
커버링 인덱스로 쿼리의 실행 계획이 최적화될 수 있도록 함으로서 카운트 올 쿼리의 성능을 매우 빠르게 만듬
모든 쿼리를 커버링 인덱스로 튜닝하는건 적절하지 않음
일반적으로 서비스에서 카운트 올 쿼리라 하더라도 웨어 조건절에 많은 컬럼들의 비교조건이
사용되는데 이 모든 컬럼들을 인덱스에 추가하기에는 사실 성능적인 장점 보단 단점이 훨씬더 커질수 잇음
그래서 커버링 인덱스를 이용한 튜닝은 꼭 필요한 경우에만 권장
커버링 인덱스를 이용한 최적화
인덱스에 포함되지 않은 컬럼 사용 시
NULL이 아닌 레코드 카운트
커버링 인덱스 사용의 장단점:
장점: 매우 빠른 쿼리 처리
단점: 모든 조건 컬럼을 인덱스에 추가하면 성능 저하 가능성
꼭 필요한 경우에만 커버링 인덱스 사용 권장
모든 쿼리를 커버링 인덱스로 튜닝하는 것은 부적절
내부적으로 매우 다른 방식인 두 쿼리
countAll은 조건에 일치하는 레코드를 찾아 건수만 확인
count Distinct query는 많은 메모리 공간과 cpu 자원을 소모하는 편
중복된 레코드를 배제하고 유니크한 값들의 조합인 레코드 대해서만 레코드 건수 반환
mysql 서버는 중복 제거용 임시테이블 생성
where 조건ㅇ절에 일치하는 레코드를 찾아 임시 테이블에 저장해야 함.
이때 임시 테이블에 그냥 인서트하는게 아니라
먼저 임시 테이블에서 중복된 값이 있는지 셀럭터 먼저해서 확인하고 중복된 경우 무시 중복되지 않는경우 인서트
이렇게 반복 웨어 조건절에 일치하는 레코드를 찾아서 임시테이블에 인서트 하고
mysql server는 임시 테이블의 레코더 건수 확인해서 최종적으로 클라이언트로 반환
그래서 카운트 디스팅크를 위해서 mysql server는 레코드 건별로 셀럭트와 인서트를 한번 씩 더 실행
내부적으로 성능적으로 2~3배 더느려지는 작업방식
레코더 건수가 매우 많다면 mysql 서버는 너무 큰 임시테이블이 메모리에 상주하는것을 막기 위해
적절한 시점에 임시 테이블에 디스크로 다시 옮겨서 저장하는 작업도 처리하게 됨
이렇게 되면 메모리나 cpu 뿐만아니라 디스크 작업까지 과중되면서 쿼리의 성능이 더 느려짐
orm 사용한다해도 최종적으로 mysql server로 어떻게 실행되는 지확인해 보자
제너럴 로그 활성화해두고 제너럴 로그를 살펴보면서 성능 이슈를 발생하는 쿼리가 있는지 검토하는게 좋음
COUNT(*) vs COUNT(DISTINCT)
COUNT(DISTINCT)의 처리 과정
a. 임시 테이블 생성
b. 조건에 맞는 레코드를 찾아 임시 테이블에 저장
- 중복 체크 후 저장 (SELECT → INSERT)
c. 임시 테이블의 최종 레코드 수 반환
COUNT(DISTINCT)의 성능 특성
성능 모니터링
응용 프로그램 로직을 변경하지 않고 count-all-query는 커버링 인덱스 실행 계획을 사용하도록 하는 것이 유일한 튜닝 방법
응용 프로그램의 로직을 변경하면서 count-all-query를 튜닝할 수 있는 방법을 좀 더 살펴 보자
먼저 카운트 올 쿼리 최고 튜닝은 쿼리 자체 제거
의외로 이경우가 많다
페이지 번호 없이 이전 이후 페이지 이동 기능으로 구현 하면 손쉽게 카운트 올 쿼리 제거 가능
만약 카운트 올쿼리 제거 못하면
대략적인 건수 활용
처음 10개 페이지가 있는 것처럼 페이지 번호를 1부터 10번까지 표시해주고
실제 해당 페이지로 이동하면 countAllquery 날려서 결과 존재 여부를 페이지 존재여부를 갱신
즉 사용자가 7페이지 클릭하면 selectAllFromLimit 10, offset60 쿼리가 실행되게 되는데 결과가
10개 미만이면 7페이를 마지막 번호로 표시해주고 10개면 페이지 번호를 그대로 10까지 유지
이 때 7페이지 데이터 조회하는 결과가 한건도 없다면 카운트 all 쿼리를 실행해서 전체 레코드 건수를 확인하고
페이지 번호를 고쳐주면 되는데 이 경우 카운트 올 쿼리를 실행하지만 실제 레코드 건수는 많지 않기 때문에 성능 적으로 큰 우려가 되지않는다.
구글도 약간 이런 느낌 튜닝인듯?
쿼리 제거
대략적인 건수 활용
임의의 페이지 번호 표기
통계 정보 활용
이러한 방법들이 효과적인 이유
where 조건없는 count(*)
인덱스 활용하여 최적화 대상
타입 ORM을 이용해서 샵 테이블과 코멘트 테이블을 조인해 정렬한 뒤
20건만 조회하는 쿼리를 실행해 보자
잘 만든 쿼리처럼 보인다
샵테이블과 코멘트 테이블은 1대 n 관계이고 distinct-alliance 의 코멘트 id 컬럼은 기본키인 id 컬럼
실제 이쿼리는 distinct 는 불필요함 기본키는 유니크하니까
type orm 서브쿼리까지하면서 distinct 이용해 쿼리작성하지만
실제는 쓸모없는작업 기본키가있으니까!
오른쪽 처럼 짜야지 의도대로 라면
이 쿼리는 페이징 쿼리로 사용되는 것 인데 페이징 작업을 위해서
사용 되는 것인데 countALl 쿼리가 필요하고 타입 orm 에서는 이렇게 마지막에 getCount 함수를 호출하도록함
이 경우 어떤 문제 쿼리를 생성했을까?
COUNT(*) 쿼리 튜닝 방법 구분:
a) 쿼리 제거가 좋은 경우
인덱스를 활용한 최적화가 좋은 경우
ORM 사용 시 주의사항:
a) 불필요한 DISTINCT 사용
COUNT 쿼리 최적화