MySQL 은 클라이언트가 요청을 하게 되면 SQL 엔진이 4가지 과정을 거쳐서 응답을 주게 된다

그 중 실행계획은 옵티마이저 와 관련이 있다
즉, 실행계획은 옵티마이저가 쿼리를 어떻게 처리할 것인지 계산해낸 결과
먼저 실습하기 앞서 EXPLAIN 으로 실행 계획을 확인하는 정보들은 아주 많다
그러므로 type / key / Extra 한정해서 확인해보겠다
type
key
Extra
예제 데이터
테이블 생성
CREATE TABLE departments (
dept_no CHAR(4) PRIMARY KEY,
name VARCHAR(40) NOT NULL
);
CREATE TABLE employees (
emp_no INT PRIMARY KEY,
birth_date DATE NOT NULL,
first_name VARCHAR(14) NOT NULL,
last_name VARCHAR(16) NOT NULL,
gender ENUM('M', 'F') NOT NULL,
hire_date DATE NOT NULL
);
CREATE TABLE dept_emp (
emp_no INT,
dept_no CHAR(4),
from_date DATE NOT NULL,
to_date DATE NOT NULL,
PRIMARY KEY (emp_no, dept_no),
FOREIGN KEY (emp_no) REFERENCES employees(emp_no),
FOREIGN KEY (dept_no) REFERENCES departments(dept_no)
);
부서 추가
INSERT INTO departments (dept_no, name) VALUES
('D001', 'Engineering'),
('D002', 'Tool Design'),
('D003', 'Marketing'),
('D004', 'Finance'),
('D005', 'Human Resources'),
('D006', 'Production'),
('D007', 'Development'),
('D008', 'Research'),
('D009', 'Customer Service');
사원 추가
DELIMITER $$
CREATE PROCEDURE populate_employees()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 300000 DO
INSERT INTO employees (
emp_no, birth_date, first_name, last_name, gender, hire_date
)
VALUES (
i,
DATE_SUB(CURDATE(), INTERVAL FLOOR(RAND() * 15000 + 8000) DAY),
CONCAT('First', i),
CONCAT('Last', i),
IF(RAND() > 0.5, 'M', 'F'),
DATE_SUB(CURDATE(), INTERVAL FLOOR(RAND() * 5000) DAY)
);
SET i = i + 1;
END WHILE;
END$$
DELIMITER ;
CALL populate_employees();
사원_부서 추가
DELIMITER $$
CREATE PROCEDURE populate_dept_emp()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 330000 DO
INSERT IGNORE INTO dept_emp (
emp_no, dept_no, from_date, to_date
)
VALUES (
FLOOR(RAND() * 300000) + 1,
CONCAT('D00', FLOOR(RAND() * 9 + 1)),
DATE_SUB(CURDATE(), INTERVAL FLOOR(RAND() * 5000 + 5000) DAY),
DATE_SUB(CURDATE(), INTERVAL FLOOR(RAND() * 2000) DAY)
);
SET i = i + 1;
END WHILE;
END$$
DELIMITER ;
CALL populate_dept_emp();
EXPLAIN SELECT * FROM employees;

type: ALLWHERE) 이 인덱스와 무관하거나, 있어도 활용할 수 없는 등key: NullExtra: NullEXPLAIN SELECT * FROM employees WHERE employees.emp_no = 1;

type: constPK 또는 UK 조건으로 ROW 가 유일하게 결정될 때 사용key: PRIMARYExtra: NullEXPLAIN SELECT * FROM dept_emp WHERE dept_no = 'D005';

type: ref=) 를 사용할때 보임dept_no = "D005" 조건으로 인덱스를 타고 여러 ROW 를 가져오는 상황key: dept_nodept_no 에 대해 인덱스가 걸려있다는 의미Extra = Using index condition즉, 인덱스를 통해 조회할 때 필터 조건을 스토리지 레벨까지 내려보내서 처리하는 최적화 방식을 의미한다
단순히 인덱스를 타고 ROW 를 찾은 후 MySQL 서버단에서 필터링하는 것이 아닌,
인덱스 조건을 스토리지 엔진에서 먼저 처리하므로 속도가 빨라진다
(InnoDB 스토리지 엔진이 ICP 를 지원할 경우 자동으로 활용됨)
참고
왜 “Index Condition Pushdown” 이라는 이름인 걸까 ?
간단히 이야기하면 MySQL 이 인덱스 조건을 “더 아래로 밀어 넣는다” 라는 의미의 최적화 기법을 설명한 용어
Index Condition
- WHERE 조건 절에서 인덱스 컬럼에 걸린 조건 (ex. dept_no = "D005")
Pushdown
- 이 조건을 MySQL 의 스토리지 엔진 레벨까지 내려보내서 먼저 평가하는 동작
즉, 조건을 인덱스에서 판단 가능한 건 미리 판단해서 필요 없는 ROW 는 아예 읽지도 않음 → 성능 향상
MySQL 5.5 버전까지는 조건이 인덱스에 포함된 필드라도 인덱스 범위 조건으로 사용할 수 없는 경우에는 스토리지 엔진 조건 자체를 전달 조차 못함 (ex.
LIKE "%TEST%")
hire_date) 컬럼에 대한 범위 검색hire_date) 컬럼에 대한 인덱스는 없음EXPLAIN SELECT * FROM employees WHERE hire_date > '2022-01-01'

type: ALLWHERE) 이 인덱스와 무관하거나, 있어도 활용할 수 없는 등key: NullExtra: Using where이제 사원일자 (hire_date) 컬럼에 대해 인덱스를 생성 후 범위 검색 실행
CREATE index IX_employees_hire_date ON employees (hire_date);
EXPLAIN SELECT * FROM employees WHERE hire_date > '2022-01-01'

이 실행 결과를 보면 여전히 풀 테이블 스캔 (Full Table Scan) 을 한 걸로 보여진다
인덱스가 생성되지 않았나?
라고 생각할 수 있는데 possible_keys = IX_employees_hire_date 인 것을 보면 인덱스가 생성 되어 있고 키로 사용할 가능성이 있는 것으로 판단하고 있긴 하다
왜 풀 테이블 스캔을 처리하는걸까?
여러가지 추측을 할 수 있는데 우선
information_schema.tables / information_schema.statistics 을 통해 통계 정보를 볼 수 있다ANALYZE TABLE employees 로 통계를 갱신하는 것도 방법혹은 인덱스를 활용해도 전체 레코드의 너무 많은 부분을 탐색하는 경우
인덱스가 걸린 컬럼의 유니크 정도가 낮은 경우 등 (카디널리티가 작은 경우)
참고
커버링 인덱스 란 ?
- 인덱스만 보고도 쿼리를 처리할 수 있는 상황을 말한다
즉, 테이블까지 랜덤 I/O 하지 않고도 인덱스만으로 결과를 낼 수 있는 경우
예시로 사원번호(emp_no) 에 대한 인덱스를 걸고 조회할 때 사원번호만 조회한다면
SELECT emp_no FROM employees WHERE emp_no > 10
- 인덱스에 필터 조건 + SELECT 대상 컬럼 모두 포함됨
- 테이블 접근 생략 (랜덤 I/O X)
- 인덱스만으로 빠르게 처리 가능
추가로 PK 혹은 커버링 인덱스 접근 방식은 랜덤 I/O 를 줄이거나 순차 I/O 로 조회할 수 있다
(DB 의 인덱스 구조, 디스크 형식에 따라 다를 수 있음)
그러면 이제 사원(employees) 테이블에 10만건의 데이터를 넣고 실행해보자
데이터를 더 추가해서 아주 데이터 많은 경우에도 인덱스를 사용하지 않고 풀 테이블 스캔을 하는지
사원 데이터 60만건 추가
DELIMITER $$
DROP PROCEDURE IF EXISTS populate_more_employees$$
CREATE PROCEDURE populate_more_employees()
BEGIN
DECLARE i INT DEFAULT 300001;
WHILE i <= 859999 DO
INSERT INTO employees (
emp_no, birth_date, first_name, last_name, gender, hire_date
) VALUES (
i,
DATE_ADD('1970-01-01', INTERVAL FLOOR(RAND() * 15000) DAY),
CONCAT('First', i),
CONCAT('Last', i),
IF(RAND() > 0.5, 'M', 'F'),
DATE_ADD('1995-01-01', INTERVAL FLOOR(RAND() * 10000) DAY)
);
SET i = i + 1;
END WHILE;
END$$
DELIMITER ;
-- 실행
CALL populate_more_employees();
EXPLAIN SELECT * FROM employees WHERE hire_date > '2022-01-01'

type: range> / < / BETWEEN / IN(…) 등의 조건에서 발생key: IX_employees_hire_datehire_date) 에 대해 생성한 인덱스를 활용Extra: Using index conditiondept_emp) 테이블 과 사원 (employees) 를 JoinEXPLAIN SELECT * FROM dept_emp JOIN employees ON dept_emp.emp_no = employees.emp_no AND dept_emp.dept_no = 'D005';

dept_emp)type: ref=) 를 사용할때 보임key: dept_nodept_no 인덱스Extra: Using index conditionemployees)type: eq_ref
PRIMAY_KEY 조인을 의미하며 하나의 레코드만 매칭된다key: PRIMARY
Extra: Null
동작 방식
Index lookup on dept_emp using dept_no)
dept_nodept_emp 테이블에서 dept_no='D005' 조건을 인덱스로 빠르게 가져옴.Single-row index lookup on employees using PRIMARY)
dept_emp 테이블에서 dept_no='D005' 조건을 인덱스로 빠르게 가져옴.Nested loop inner join위 컬럼에는 각 종류에 따라 성능에 최적화된 순서가 존재한다

위 그림이 각 type 에 대한 종류인데
아래로 갈수록 성능에 안좋은 영향이 생기는 type 이다
index 와 ALL 에 대해서는 주의깊게 사용해야 한다type: index위 그림은 인덱스 (Non-Clustered) 테이블에 대한 풀 스캔을 의미한다
인덱스 테이블 (Non-Clustered Leaf) 들을 순차적으로 쭉 순회한다 → 사실 이 행동 자체는 크게 문제되지 않는다
그러나 인덱스 테이블 (Non-Clustered) 에 없는 컬럼을 조회한다고 한다면 실제 데이터 레코드까지 찾아가서 참조해야하는 경우 중간에 Disk I/O 가 발생한다
그러므로 인덱스 테이블 풀 스캔을 할때는

위 그림처럼 인덱스 테이블을 순차적으로 쭉 순회하지만 필터링 조건에 의해 몇개만 데이터 레코드까지 참조하는 경우
혹은

위에서 이야기했던 커버링 인덱스를 통해 실제 데이터 레코드까지 조회하지 않고 인덱스 테이블만으로 결과물을 도출할 수 있는 경우에는 Disk I/O 가 많이 발생하지 않으므로 괜찮은 성능을 보인다
그래서 다른 커넥션으로 똑같은 쿼리를 보내도 MySQL 은 새로운 실행 계획을 계산해야한다
이 과정에서 옵티마이저는 직접 인덱스 몇개를 탐색해 샘플링하는 작업을 하게되는데 이를 Index Dive 라고 한다
그렇지만 실제로 보내는 쿼리들은 조합이 아주 다양하고 많은 인덱스를 사용해야하는 경우에는 실행 계획을 수립하는 과정에서 굉장히 많은 비용이 소모된다
심지어는 쿼리 자체를 수행하는 것 보다 실행 계획을 수립하는데 리소스가 더 많이 들기도 하는 경우가 있다
혹은
실행 계획 수립에는 할당된 시스템 메모리 비용이 정해져 있는데 해당 메모리를 초과해서 실행 계획을 수립하다가 실행 계획 자체를 포기하고 풀 테이블 스캔을 하기도 한다고 함
참조)