모든 내용은 Real MySQL 8.0 1권에서 가져왔습니다!!
어떤 경우 인덱스를 사용하게 유도할지, 사용하지 못하게 할지 판단하려면 MySQL의 스토리지 엔진이 어떻게 인덱스를 사용해 실제 레코드를 읽는지 알아야 한다. 대표적인 세 가지 방법을 알아보자
인덱스 레인지 스캔
은 인덱스 접근 방법 가운데 가장 대표적인 접근 방법으로, 나머지 두 방식보다 빠른 방법이다.(레코드 한 건만 읽는 경우와 한 건 이상 읽는 경우를 모두 묶어 인덱스 레인지 스캔
이라 하겠다.)SELECT * FROM employees WHERE first_name BETWEEN 'Ebbe' AND 'Gad';
[Real MySQL 8.0 그림 8.8]
[Real MySQL 8.0 그림 8.9]
인덱스 레인지 스캔은 다음 3단계 과정을 거친다
1. 인덱스 탐색(Index seek): 인덱스에서 조건을 만족하는 값이 저장된 위치를 찾는다.
2. 인덱스 스캔(Index scan): 1번에서 탐색된 위치부터 필요한 만큼 인덱스를 차례로 쭉 읽는다.
3. 2번에서 읽은 인덱스 키와 레코드 주소를 이용해 레코드가 저장된 Page를 가져오고, 최종 레코드를 읽어온다.
쿼리가 필요로 하는 데이터에 따라 3번 과정은 필요하지 않을 수도 있는데, 이를 커버링 인덱스
라고 한다. 커버링 인덱스로 처리되는 쿼리는 디스크의 레코드를 읽지 않아 성능이 빨라진다.
[Real MySQL 8.0 그림 8.10]
GROUP BY
또는 MAX()
, MIN()
함수를 최적화 하는 경우 사용됨SELECT dept_no, MIN(emp_no)
FROM dept_emp
WHERE dep_no BETWEEN 'd002' AND 'd004'
GROUP BY dept_no;
dept_emp테이블은 dept_no, emp_no 두 column으로 인덱스가 생성돼 있고, (dept_no, emp_no) 조합으로 정렬되어 있다 하자.
dept_no 그룹 별로 첫 번째 레코드의 emp_no만 읽으면 되기 때문에 WHERE 조건을 만족하는 범위 전체를 다 스캔할 필요가 없다는 것을 옵티마이저는 알고 있다. 따라서 조건에 만족하지 않는 레코드는 무시하고 다음 레코드로 이동한다.
ALTER TABLE employees
ADD INDEX ix_gender_birthdate (gender, birth_date);
위와 같은 인덱스를 사용하려면 WHERE절에 gender column에 대한 비교 조건이 필수이다.
// 인덱스를 사용하지 못하는 쿼리
SELECT * FROM employees WHERE birth_date >= '1965-02-01';
// 인덱스를 사용할 수 있는 쿼리
SELECT * FROM employees WHERE gender = 'M' AND birth_date >= '1965-02-01';
하지만 MySQL 8.0부터 도입된 인덱스 스킵 스캔(Index skip scan)
기능으로 옵티마이저가 gender column을 건너뛰어 birth_date column만으로 인덱스 검색을 할 수 있게 되었다.
이전에 이와 비슷한 루스 인덱스 스캔(Loose index scan)
이란 기능이 있었지만 group by 작업을 처리하기 위해 인덱스를 사용하는 경우에만 적용할 수 있었다.
인덱스 스킵 스캔
은 WHERE 조건절의 검색을 위해 사용 가능하도록 용도가 훨씬 넓어진 것이다.
인덱스 스킵 스캔
을 끄고 아래 쿼리의 실행계획을 살펴보자.
EXPLAIN
SELECT gender, birth_date
FROM employees
WHERE birth_date >= '1965-02-01';
index라는 type은 '풀 인덱스 스캔'을 의미한다. 인덱스를 효율적으로 활용하지 못했다는 의미가 된다. 만약 SELECT 문에 요구하는 column이 인덱스에 없는 column을 요구했다면 '테이블 풀 스캔'이 일어났을 것이다.
인덱스 스킵 스캔
을 활성화하고 다시 쿼리 실행계획을 살펴보자.
range라는 type으로 실행계획이 변경된 것을 알 수 있다. 이는 인덱스에서 꼭 필요한 부분만 읽었다는 것을 의미한다. MySQL 옵티마이저는 인덱스 스킵 스캔
을 실행하기 위해 gender column에서 유니크한 값을 모두 조회하고 gender column 조건을 쿼리에 추가해 다시 실행하는 형태로 처리한다. 아래 그림을 보고 어떻게 처리되는지 살펴보자.
[Real MySQL 8.0 1권 그림 8.12]
gender column은 'M'과 'F' 두 가지 값만 가진다. 따라서 아래와 같이 변형된 쿼리를 이용해 탐색하도록 옵티마이저가 최적화해준다.
SELECT gender, birth_date FROM employees WHERE gender = 'M' AND birth_date >= '1965-02-01';
SELECT gender, birth_date FROM employees WHERE gender = 'F' AND birth_date >= '1965-02-01';
MySQL 서버는 루스 인덱스 스캔과 동일한 방식으로 인덱스를 읽으면서 인덱스에 존재하는 모든 값을 먼저 추출하고, 그 결과를 이용해 인덱스 스킵 스캔을 실행한다.
하지만 아직 MySQL 8.0에 도입된 인덱스 스킵 스캔은 아래와 같은 단점이 있다.
첫 번째 단점은 만약 유니크한 값의 개수가 많다면 인덱스에서 스캔해야 할 시작 지점을 검색하는 작업이 많이 필요하게 된다(유니크한 값이 1000개라면 시작 지점을 1000번 찾아야 함). 따라서 인덱스 스킵 스캔은 인덱스 선행 column이 가진 유니크한 값의 개수가 소량일 때만 적용 가능한 최적화이다.
두 번째 제약 조건은 아래와 같은 쿼리를 사용했을때 발생할 수 있는 문제이다.
EXPLAIN
SELECT *
FROM employees
WHERE birth_date >= '1965-02-01';
인덱스에 포함된 gender, birth_date 이외의 column을 SELECT 절에서 요구했기 때문에 실행계획의 type이 'all'로 테이블 풀 스캔이 일어났음을 알 수 있다. 하지만 이 문제는 MySQL 옵티마이저가 개선되면 해결될 수 있는 문제이다.