MySQL 서버로 요청된 쿼리는 결과는 동일하지만 내부적으로 그 결과를 만들어내는 방법은 매우 다양하다.
이런 다양한 방법 중 어떤 방법이 최적이고 최소의 비용이 소모될지 결정해야 한다.
MySQL에서는 쿼리를 최적으로 실행하기 위해 각 테이블이 데이터가 어떤 분포로 저장돼 있는지 통계 정보를 참조하며, 그런 기본 데이터를 비교해 최적의 실행 계획을 수립하는 작업이 필요하다.
대부분의 DBMS에서 옵티마이저가 이러한 기능을 담당한다.
EPLAIN
명령어로 쿼리의 실행 계획 확인.쿼리는 아래와 같이 3단계로 수행된다.
👆 SQL 파싱(Parsing) - SQL 파서가 수행
요청된 SQL문장을 잘게 쪼개서 파스 트리로 분리한다.
✌ 최적화 및 실행 계획 수립 - 옵티마이저가 수행
파스 트리를 확인하며 어떤 테이블로부터 읽고 어떤 인덱스를 이용해 테이블을 읽을지 선택한다. 파스트리를 참조하며 처리하는 내용은 아래와 같다.
👌 실행계획 수행
2단계에서 결정된 테이블의 읽기 순서나 선택된 인덱스를 이용해 스토리지 엔진으로부터 데이터를 가져온다.
규칙 기반 최적화 (Rule-based optimizer)
- 초기 버전의 오라클에서 많이 사용했던 옵티마이저
- 사용자의 데이터 분포도는 매우 다양하기에 규칙 기반 최적화는 오래전부터 사용되지 않는다.
비용 기반 최적화 (Cost-based optimizer)
풀 테이블 스캔을 선택하는 경우
WHERE
, ON
절에 인덱스를 이용할 수 있는 적절한 조건이 없는 경우리드 어헤드란?
innodb_read_ahead_threshold
변수로 리드 어헤드 임계치 설정 가능기존 MySQL은 여러 쿼리를 동시해 수행하는 병렬 처리가 가능했다.
여기에 MySQL8.0버전부터 한개의 쿼리를 여러 스레드가 병렬 처리하는 기능도 추가됐다.
innodb_parallel_read_threads
변수를 통해 몇개의 스레드로 처리할지 변경 가능✅ 인덱스를 이용하는 방식
✅ Filesort를 이용하는 방식
✅ 인덱스를 사용못하는 케이스
GROUP BY
의 결과 또는 DISTINCT
같은 처리의 결과를 정렬해야 하는 경우UNION
의 결과와 같이 임시 테이블의 결과를 다시 정렬해야 하는 경우✅ MySQL은 정렬을 수행하기 위해 별도의 메모리 공간을 할당받아서 사용한다.
sort_buffer_size
라는 시스템 변수로 설정할 수 있다.일반적으로 256KB 미만, 8MB 이상부터 소트 버퍼 사이즈에 의한 성능변화가 미미하다.
소트버퍼를 크게 설정 시 메모리풀이 발생할 수 있으니 주의.
✅ 소트 버퍼의 크기
✅ OPTIMIZER TRACE
OPTIMIZER TRACE
기능으로 정렬방식을 확인 가능하다.첫번쨰 방식은 투패스정렬, 둘째,셋째 방식은 싱글패스정렬이다.
✅ 싱글패스 정렬 방식
SELECT
대상이 되는 컬럼 전부를 담아서 (정렬이 필요하지 않은 컬럼까지 전부 읽음) 정렬을 수행하는 방식✅ 투 패스 정렬 방식
SELECT
할 컬럼을 가져오는 정렬 방식.✅ 싱글패스 vs 투 패스
max_length_for_sort_data
시스템 변수에 설정된 값보다 클 때SELECT
대상에 포함될 때✅ 정렬 처리 방법
인덱스 활용하도록 유도. 최소한 드라이빙 테이블만 정렬해도 되는 수준으로 유도.
✅ 인덱스를 사용한 정렬
ORDER BY
에 명시된 컬럼이 드라이빙 테이블에 속하고, ORDER BY
의 순서대로 생성된 인덱스가 있어야 한다.✅ 스트리밍 방식
LIMIT
처럼 결과 건수를 제한하는 조건들은 쿼리의 전체 실행 시간을 상당히 줄여줄 수 있다.✅ 버퍼링 방식
ORDER BY
나 GROUP BY
같은 처리는 쿼리의 결과가 스트리밍 되는 것을 불가능하게 한다.LIMIT
처럼 결과 건수를 제한하는 조건이 있어도 성능 향상에 별로 도움이 되지 않는다.GROUP BY
또한 ORDER BY와 같이 쿼리가 스트리밍 된 처리를 할 수 없다.GROUP BY
절이 있는 쿼리에서는 필터링 역할을 하는 HAVING 절을 사용할 수 있는데, GROUP BY에 사용된 조건은 인덱스를 사용해서 처리될 수 없으므로 HAVING 절을 튜닝하려고 인덱스를 생성하거나 다른 방법을 고민할 필요는 없다.✅ 타이트 인덱스 스캔을 이용하는 GROUP BY
ORDER BY
의 경우와 마찬가지로 조인의 드라이빙 테이블에 속한 컬럼만 이용해 그루핑 할 때 GROUP BY
컬럼으로 이미 인덱스가 있다면 그 인덱스를 차례대로 읽으면서 그루핑 작업을 수행하고 그 결과로 조인을 처리한다.
GROUP BY가 인덱스를 사용해서 처리된다 하더라도 그룹 함수 등의 그룹 값을 처리해야 해서 임시 테이블이 필요할 때도 있다.
✅ 루스 인덱스 스캔을 이용하는 GROUP BY
✅ 임시 테이블을 사용하는 GROUP BY
인덱스를 전혀 사용할 수 없는 GROUP BY
로, 실행 계획의 Extra 컬럼에 Using temporary 메시지가 표기된다.
✅ ORDER BY NULL
GROUP BY
가 사용된 쿼리는 그루핑되는 컬럼을 기준으로 묵시적인 정렬까지 함께 수행했다.ORDER BY NULL
을 추가로 사용할 것을 권장했다. (불필요한 추가 정렬 작업을 수행하지 않아 성능 향상)✅ SELECT DISTINCT...
SELECT DISTINCT ... FROM ...
SELECT
되는 레코드 중에서 유니크한 레코드만 가져오고자 하면 SELECT DISTINCT
형태의 쿼리 문장을 사용한다.GROUP BY
와 동일하게 처리된다.DISTINCT
는 SELECT
하는 레코드를 유니크하게 SELECT
하는 것이지, 특정 컬럼만 유니크하게 조회하는 것이 아니다.🌵예제
SELECT DISTINCT first_name, last_name FROM employees;
SELECT DISTINCT (first_name), last_name FROM employees;
MySQL Syntex는 DISTICT가 함수가 아니기에 괄호를 무시처리한다.
✅ 집합 함수와 함께 사용된 DISTINCT
SELECT COUNT(DISTINCT s.salary) FROM ...
COUNT()
, MIN()
, MAX()
와 같은 집합 함수 내에서 DISTINCT
가 사용되는 경우, 그 집합 함수의 인자로 전달된 컬럼 값이 유니크한 것들을 가져온다.DISTINCT
가 인덱스를 사용하지 못할 때는 항상 임시 테이블이 필요하다.MySQL 엔진이 스토리지 엔진으로부터 받아온 레코드들을 정렬하거나 그루핑 할 때는 내부적인 임시 테이블을 사용한다.
CREATE TEMPORARY TABLE
명령으로 만든 임시 테이블과 다르다
✅ 메모리 임시 테이블과 디스크 임시 테이블
MySQL 8.0 이전 버전까의 경우 MEMORY 스토리지 엔진을 사용하며, 디스크에 저장될 때는 MyISAM 스토리지 엔진을 사용한다.
임시 테이블의 크기가 1GB보다 커지는 경우 메모리의 임시 테이블을 디스크로 기록한다.
임시테이블 사이즈는
temptable_max_ram
시스템 변수로 변경 가능
✅ 임시 테이블이 필요한 쿼리
ORDER BY
, GROUP BY
에 명시된 컬럼이 다른 쿼리ORDER BY
, GROUP BY
에 명시된 컬럼이 조인의 순서상 첫 번째 테이블이 아닌 쿼리DISTINCT
나 ORDER BY
가 동시에 쿼리에 존재하는 경우 또는 DISTINCT
가 인덱스로 처리되지 못하는 쿼리UNION
, UNION DISTINCT
가 사용된 쿼리 (UNION ALL
을 사용하는 쿼리는 MySQL 8.0부터는 임시 테이블을 사용하지 않도록 개선되었다)컬럼에 Using temporary라는 메시지가 표시되는지 확인하면 된다.
✅ 임시 테이블이 디스크에 생성되는 경우
UNION
, UNION ALL
에서 SELECT
되는 컬럼 중에서 길이가 512byte 이상의 크기의 컬럼이 있는 경우GROUP BY
나 DISTINCT
컬럼에서 512byte 이상의 크기의 컬럼이 있는 경우