MySQL 쿼리 튜닝 기초

업무에 바로 쓰는 SQL 튜닝 책에 있는 내용을 기반으로 해당 글을 작성한다.

테스트를 위한 데이터는 책에서 제공한
https://github.com/7ieon/SQLtune 을 사용하였다.

우선 튜닝 기초로서 스키마나 인덱스를 수정하지 않고,
단순 쿼리 수정을 통한 쿼리 튜닝을 진행하려고 한다.

1. 기본키를 변형하는 쿼리

사원번호 1100으로 시작하면서 사원번호가 5자리인 사원을 출력하는 쿼리

개선 전

SELECT *
FROM 사원
WHERE SUBSTRING(사원번호,1,4) = 1100
AND LENGTH(사원번호) = 5;
-> 10 rows in set (0.15 sec)
실행 계획
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | 사원   | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 299822 |   100.00 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+

원인 : 좌변 가공이 일어나 인덱스를 제대로 타지 못하고 full scan이 발생

개선 후

SELECT *
FROM 사원
WHERE 사원번호 BETWEEN 11000 AND 11009;
-> 10 rows in set (0.00 sec)
실행 계획
+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table  | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra       |
+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | 사원   | NULL       | range | PRIMARY       | PRIMARY | 4       | NULL |   10 |   100.00 | Using where |
+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+

개선 : 사원번호 인덱스(pk)를 탈 수 있도록 쿼리 개선

2. 사용이 불필요한 함수를 포함하는 쿼리

사원 테이블에서 성별마다 몇 명의 사원이 있는지 출력하는 쿼리

개선 전

SELECT IFNULL(성별, 'NO DATA') AS 성별, COUNT(1) 건수
FROM 사원
GROUP BY IFNULL(성별, 'NO DATA');
-> 2 rows in set (0.28 sec)
실행 계획
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+------------------------------+
| id | select_type | table  | partitions | type  | possible_keys | key          | key_len | ref  | rows   | filtered | Extra                        |
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+------------------------------+
|  1 | SIMPLE      | 사원   | NULL       | index | I_성별_성     | I_성별_성    | 51      | NULL | 299822 |   100.00 | Using index; Using temporary |
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+------------------------------+

원인 : IFNULL 함수를 사용하여 임시테이블 사용

개선 후

SELECT 성별, COUNT(1) 건수
FROM 사원
GROUP BY 성별;
-> 2 rows in set (0.08 sec)
실행 계획
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+-------------+
| id | select_type | table  | partitions | type  | possible_keys | key          | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | 사원   | NULL       | index | I_성별_성     | I_성별_성    | 51      | NULL | 299822 |   100.00 | Using index |
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+-------------+

개선 : 성별은 not null 컬럼으로 불필요한 함수의 사용을 제거

3. 형변환으로 인덱스를 활용하지 못하는 쿼리

급여 테이블에서 유효한 급여 정보를 조회하고자 count를 출력하는 쿼리

개선 전

SELECT COUNT(1)
FROM 급여
WHERE 사용여부 = 1;
-> 1 row in set (0.41 sec)
실행 계획
+----+-------------+--------+------------+-------+----------------+----------------+---------+------+---------+----------+--------------------------+
| id | select_type | table  | partitions | type  | possible_keys  | key            | key_len | ref  | rows    | filtered | Extra                    |
+----+-------------+--------+------------+-------+----------------+----------------+---------+------+---------+----------+--------------------------+
|  1 | SIMPLE      | 급여   | NULL       | index | I_사용여부     | I_사용여부     | 4       | NULL | 2838065 |    10.00 | Using where; Using index |
+----+-------------+--------+------------+-------+----------------+----------------+---------+------+---------+----------+--------------------------+

원인 : 사용여부는 char(1) 컬럼이나 정수형 값으로 조회하여 내부에서 형변환이 발생

개선 후

SELECT COUNT(1)
FROM 급여
WHERE 사용여부 = '1';
-> 1 row in set (0.02 sec)
실행 계획
+----+-------------+--------+------------+------+----------------+----------------+---------+-------+-------+----------+-------------+
| id | select_type | table  | partitions | type | possible_keys  | key            | key_len | ref   | rows  | filtered | Extra       |
+----+-------------+--------+------------+------+----------------+----------------+---------+-------+-------+----------+-------------+
|  1 | SIMPLE      | 급여   | NULL       | ref  | I_사용여부     | I_사용여부     | 4       | const | 82824 |   100.00 | Using index |
+----+-------------+--------+------------+------+----------------+----------------+---------+-------+-------+----------+-------------+

개선 : 형변환이 발생하지 않도록 문자형인 ‘1’로 조회

4. 열을 결합하여 사용하는 쿼리

사원 중 성별과 성을 결합한 결과가 ‘M Radwan’인 데이터를 조회하는 쿼리

개선 전

SELECT *
FROM 사원
WHERE CONCAT(성별,' ',) =  'M Radwan';
-> 102 rows in set (0.14 sec)
실행 계획
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | 사원   | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 299822 |   100.00 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+

원인 : 좌변 가공을 하여 full table scan이 발생

개선 후

SELECT *
FROM 사원
WHERE 성별 = 'M'
AND  = 'Radwan';
-> 102 rows in set (0.01 sec)
실행 계획
+----+-------------+--------+------------+------+---------------+--------------+---------+-------------+------+----------+-------+
| id | select_type | table  | partitions | type | possible_keys | key          | key_len | ref         | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+--------------+---------+-------------+------+----------+-------+
|  1 | SIMPLE      | 사원   | NULL       | ref  | I_성별_성     | I_성별_성    | 51      | const,const |  102 |   100.00 | NULL  |
+----+-------------+--------+------------+------+---------------+--------------+---------+-------------+------+----------+-------+

개선 : 좌변 가공을 하지않게 변경하여 인덱스 사용

5. 습관적으로 중복을 제거하는 쿼리

부서 관리자의 정보를 중복 제거하여 조회하는 쿼리

개선 전

SELECT DISTINCT 사원.사원번호, 사원.이름, 사원., 부서관리자.부서번호
FROM 사원
JOIN 부서관리자
ON 사원.사원번호 = 부서관리자.사원번호;
-> 24 rows in set (0.01 sec)
실행 계획
+----+-------------+-----------------+------------+--------+---------------+----------------+---------+-------------------------------------+------+----------+------------------------------+
| id | select_type | table           | partitions | type   | possible_keys | key            | key_len | ref                                 | rows | filtered | Extra                        |
+----+-------------+-----------------+------------+--------+---------------+----------------+---------+-------------------------------------+------+----------+------------------------------+
|  1 | SIMPLE      | 부서관리자      | NULL       | index  | PRIMARY       | I_부서번호     | 12      | NULL                                |   24 |   100.00 | Using index; Using temporary |
|  1 | SIMPLE      | 사원            | NULL       | eq_ref | PRIMARY       | PRIMARY        | 4       | tuning.부서관리자.사원번호          |    1 |   100.00 | NULL                         |
+----+-------------+-----------------+------------+--------+---------------+----------------+---------+-------------------------------------+------+----------+------------------------------+

원인 : distinct 키워드는 나열된 열들을 정렬 후 중복 데이터를 삭제함.
따라서 distinct를 사용하는 것만으로도 정렬 작업이 포함됨.

사원 테이블의 사원번호는 pk로 중복된 데이터가 없음.
또한 실행계획에서 type이 eq_ref를 확인할 수 있으므로 distinct를 사용할 필요가 없다.

개선 후

SELECT 사원.사원번호, 사원.이름, 사원., 부서관리자.부서번호
FROM 사원
JOIN 부서관리자
ON 사원.사원번호 = 부서관리자.사원번호;
-> 24 rows in set (0.00 sec)
실행 계획
+----+-------------+-----------------+------------+--------+---------------+----------------+---------+-------------------------------------+------+----------+-------------+
| id | select_type | table           | partitions | type   | possible_keys | key            | key_len | ref                                 | rows | filtered | Extra       |
+----+-------------+-----------------+------------+--------+---------------+----------------+---------+-------------------------------------+------+----------+-------------+
|  1 | SIMPLE      | 부서관리자      | NULL       | index  | PRIMARY       | I_부서번호     | 12      | NULL                                |   24 |   100.00 | Using index |
|  1 | SIMPLE      | 사원            | NULL       | eq_ref | PRIMARY       | PRIMARY        | 4       | tuning.부서관리자.사원번호          |    1 |   100.00 | NULL        |
+----+-------------+-----------------+------------+--------+---------------+----------------+---------+-------------------------------------+------+----------+-------------+

개선 : distinct를 제거하여 임시 테이블 사용하지 않게 변경

6. 다수 쿼리를 UNION 연산자로 합치는 쿼리

성이 Baba이면서 성별이 M인 사원 + 성이 Baba이면서 성별이 F인 데이터를 조회

개선 전

SELECT 'M' AS 성별, 사원번호
FROM 사원
WHERE 성별 = 'M'
AND  = 'Baba'

UNION

SELECT 'F' AS 성별, 사원번호
FROM 사원
WHERE 성별 = 'F'
AND  = 'Baba';
-> 226 rows in set (0.00 sec)
실행 계획
+----+--------------+------------+------------+------+---------------+--------------+---------+-------------+------+----------+-----------------+
| id | select_type  | table      | partitions | type | possible_keys | key          | key_len | ref         | rows | filtered | Extra           |
+----+--------------+------------+------------+------+---------------+--------------+---------+-------------+------+----------+-----------------+
|  1 | PRIMARY      | 사원       | NULL       | ref  | I_성별_성     | I_성별_성    | 51      | const,const |  135 |   100.00 | Using index     |
|  2 | UNION        | 사원       | NULL       | ref  | I_성별_성     | I_성별_성    | 51      | const,const |   91 |   100.00 | Using index     |
|  3 | UNION RESULT | <union1,2> | NULL       | ALL  | NULL          | NULL         | NULL    | NULL        | NULL |     NULL | Using temporary |
+----+--------------+------------+------------+------+---------------+--------------+---------+-------------+------+----------+-----------------+

원인 : 중복된 데이터가 없으나 UNION을 사용하여 중복제거를 위한 임시테이블을 사용

개선 후

SELECT 성별, 사원번호
FROM 사원
WHERE  = 'Baba';
-> 226 rows in set (0.00 sec)
실행 계획
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+-------+----------+----------------------------------------+
| id | select_type | table  | partitions | type  | possible_keys | key          | key_len | ref  | rows  | filtered | Extra                                  |
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+-------+----------+----------------------------------------+
|  1 | SIMPLE      | 사원   | NULL       | range | I_성별_성     | I_성별_성    | 51      | NULL | 29982 |   100.00 | Using where; Using index for skip scan |
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+-------+----------+----------------------------------------+

개선 : 책에서는 성 단일 인덱스가 없어 성별_성 인덱스를 활용하여 UNION 대신 UNION ALL로 조회하지만
성별이 M, F밖에 존재하지않고 조회하려는 성이 동일하므로 단일 쿼리로 조회하는 것이 더 좋다고 판단함

7. 인덱스 고려 없이 열을 사용하는 쿼리

성과 성별 순서로 그루핑하여 몇 건의 데이터가 있는지 구하는 쿼리

개선 전

SELECT , 성별, COUNT(1) AS 카운트
FROM 사원
GROUP BY , 성별;
-> 3274 rows in set (0.38 sec)
실행 계획
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+------------------------------+
| id | select_type | table  | partitions | type  | possible_keys | key          | key_len | ref  | rows   | filtered | Extra                        |
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+------------------------------+
|  1 | SIMPLE      | 사원   | NULL       | index | I_성별_성     | I_성별_성    | 51      | NULL | 299822 |   100.00 | Using index; Using temporary |
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+------------------------------+

원인 : 현재 인덱스가 (성별, 성)으로 생성되어 있는 상태이나 반대로 group by를 조회하여 임시테이블 사용

개선 후

SELECT , 성별, COUNT(1) AS 카운트
FROM 사원
GROUP BY 성별, ;
-> 3274 rows in set (0.09 sec)
실행 계획
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+-------------+
| id | select_type | table  | partitions | type  | possible_keys | key          | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | 사원   | NULL       | index | I_성별_성     | I_성별_성    | 51      | NULL | 299822 |   100.00 | Using index |
+----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+-------------+

개선 : 인덱스 순으로 group by를 해도 결과가 변하지 않으므로 순서를 변경하여 인덱스를 제대로 탈 수 있게 함.

8. 엉뚱한 인덱스를 사용하는 쿼리

입사일자가 1989년이면서 사원번호가 100,000번 넘어가는 사원 조회

개선 전

SELECT 사원번호
FROM 사원
WHERE 입사일자 LIKE '1989%'
AND 사원번호 > 100000;
-> 20001 rows in set (0.09 sec)
실행 계획
+----+-------------+--------+------------+-------+------------------------+---------+---------+------+--------+----------+-------------+
| id | select_type | table  | partitions | type  | possible_keys          | key     | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+--------+------------+-------+------------------------+---------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | 사원   | NULL       | range | PRIMARY,I_입사일자     | PRIMARY | 4       | NULL | 149911 |    11.11 | Using where |
+----+-------------+--------+------------+-------+------------------------+---------+---------+------+--------+----------+-------------+

원인 : 입사일자로 필터링시 28394/300024건.
사원번호로 필터링시 210024/300024건으로 입사일자로 먼저 필터링하는게 좋아 보이는 상황

개선 후

SELECT 사원번호
FROM 사원 USE INDEX (I_입사일자)
WHERE 입사일자 LIKE '1989%'
AND 사원번호 > 100000;
-> 20001 rows in set (0.17 sec)
실행 계획
+----+-------------+--------+------------+-------+----------------+----------------+---------+------+-------+----------+----------------------------------------+
| id | select_type | table  | partitions | type  | possible_keys  | key            | key_len | ref  | rows  | filtered | Extra                                  |
+----+-------------+--------+------------+-------+----------------+----------------+---------+------+-------+----------+----------------------------------------+
|  1 | SIMPLE      | 사원   | NULL       | range | I_입사일자     | I_입사일자     | 7       | NULL | 99930 |   100.00 | Using where; Using index for skip scan |
+----+-------------+--------+------------+-------+----------------+----------------+---------+------+-------+----------+----------------------------------------+

원인 : 하지만 실제 쿼리 실행 시, pk를 타는 것보다 더 오래걸리는 현상이 발생

개선 후2

SELECT 사원번호
FROM 사원
WHERE 입사일자 BETWEEN '1989-01-01' AND '1989-12-31'
AND 사원번호 > 100000;
-> 20001 rows in set (0.02 sec)
실행 계획
+----+-------------+--------+------------+-------+------------------------+----------------+---------+------+-------+----------+--------------------------+
| id | select_type | table  | partitions | type  | possible_keys          | key            | key_len | ref  | rows  | filtered | Extra                    |
+----+-------------+--------+------------+-------+------------------------+----------------+---------+------+-------+----------+--------------------------+
|  1 | SIMPLE      | 사원   | NULL       | range | PRIMARY,I_입사일자     | I_입사일자     | 7       | NULL | 49824 |    50.00 | Using where; Using index |
+----+-------------+--------+------------+-------+------------------------+----------------+---------+------+-------+----------+--------------------------+

개선 : date컬럼을 like검색 하지않고 between이나 부등호 조건을 사용하여 성능을 향상

9. 동등 조건으로 인덱스를 사용하는 쿼리

B 출입문으로 출입한 이력이 있는 정보를 조회하는 쿼리

개선 전

SELECT *
FROM 사원출입기록
WHERE 출입문 = 'B';
-> 300000 rows in set (0.35 sec)
실행 계획
+----+-------------+--------------------+------------+------+---------------+-------------+---------+-------+--------+----------+-------+
| id | select_type | table              | partitions | type | possible_keys | key         | key_len | ref   | rows   | filtered | Extra |
+----+-------------+--------------------+------------+------+---------------+-------------+---------+-------+--------+----------+-------+
|  1 | SIMPLE      | 사원출입기록       | NULL       | ref  | I_출입문      | I_출입문    | 4       | const | 329467 |   100.00 | NULL  |
+----+-------------+--------------------+------------+------+---------------+-------------+---------+-------+--------+----------+-------+

원인 : 인덱스는 제대로 타고 있으나 전체 데이터 중 약 50%를 조회하여 랜덤 액세스가 다수 발생하는 상황

개선 후

SELECT *
FROM 사원출입기록 IGNORE INDEX (I_출입문)
WHERE  출입문 = 'B';
-> 300000 rows in set (0.28 sec)
실행 계획
+----+-------------+--------------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table              | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+--------------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | 사원출입기록       | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 658935 |    10.00 | Using where |
+----+-------------+--------------------+------------+------+---------------+------+---------+------+--------+----------+-------------+

개선 : 이 경우 오히려 table full scan이 랜덤 액세스가 발생하지 않아 더 빠른경우

10. 작은 테이블이 먼저 조인에 참여하는 쿼리

부서사원_매핑과 부서 테이블을 조인하여 부서 시작일자가 2002-03-01 이후인 사원을 조회

개선 전

SELECT 매핑.사원번호, 부서.부서번호
FROM 부서사원_매핑 매핑, 부서
WHERE 매핑.부서번호 = 부서.부서번호
AND 매핑.시작일자 >= '2002-03-01';
-> 1341 rows in set (0.37 sec)
실행 계획
+----+-------------+--------+------------+-------+----------------+----------------+---------+----------------------------+-------+----------+-------------+
| id | select_type | table  | partitions | type  | possible_keys  | key            | key_len | ref                        | rows  | filtered | Extra       |
+----+-------------+--------+------------+-------+----------------+----------------+---------+----------------------------+-------+----------+-------------+
|  1 | SIMPLE      | 부서   | NULL       | index | PRIMARY        | UI_부서명      | 122     | NULL                       |     9 |   100.00 | Using index |
|  1 | SIMPLE      | 매핑   | NULL       | ref   | I_부서번호     | I_부서번호     | 12      | tuning.부서.부서번호       | 41191 |    33.33 | Using where |
+----+-------------+--------+------------+-------+----------------+----------------+---------+----------------------------+-------+----------+-------------+

원인 : 우선 매핑 테이블에 시작일자 인덱스가 없고,
매핑 테이블은 33만건, 부서 테이블은 9건으로
이 경우 사이즈가 적은 부서가 driving 테이블이 되어버림

개선 후

SELECT STRAIGHT_JOIN 매핑.사원번호, 부서.부서번호
FROM 부서사원_매핑 매핑, 부서
WHERE 매핑.부서번호 = 부서.부서번호
AND 매핑.시작일자 >= '2002-03-01';
-> 1341 rows in set (0.10 sec)
실행 계획
+----+-------------+--------+------------+--------+----------------+---------+---------+----------------------------+--------+----------+-------------+
| id | select_type | table  | partitions | type   | possible_keys  | key     | key_len | ref                        | rows   | filtered | Extra       |
+----+-------------+--------+------------+--------+----------------+---------+---------+----------------------------+--------+----------+-------------+
|  1 | SIMPLE      | 매핑   | NULL       | ALL    | I_부서번호     | NULL    | NULL    | NULL                       | 329534 |    33.33 | Using where |
|  1 | SIMPLE      | 부서   | NULL       | eq_ref | PRIMARY        | PRIMARY | 12      | tuning.매핑.부서번호       |      1 |   100.00 | Using index |
+----+-------------+--------+------------+--------+----------------+---------+---------+----------------------------+--------+----------+-------------+

개선 : 하지만 where문으로 필터링이 진행되면 1341건으로 데이터 건수가 많이 줄어들어 이 경우는 매핑 테이블이 driving이 되는게 좋은 경우.
위와 같이 straight_join 힌트를 사용하면 from절에 명시된 순서대로 조인에 참가하도록 고정할 수 있다.

11. 메인 테이블에 계속 의존하는 쿼리

사원번호가 450,000보다 크고 최대 연봉이 100,000보다 큰 데이터 출력

개선 전

SELECT 사원.사원번호, 사원.이름, 사원.
FROM 사원
WHERE 사원번호 > 450000
AND ( SELECT MAX(연봉) FROM 급여 WHERE 사원번호 = 사원.사원번호 ) 
> 100000;
-> 3155 rows in set (0.20 sec)
실행 계획
+----+--------------------+--------+------------+-------+---------------+---------+---------+----------------------------+--------+----------+-------------+
| id | select_type        | table  | partitions | type  | possible_keys | key     | key_len | ref                        | rows   | filtered | Extra       |
+----+--------------------+--------+------------+-------+---------------+---------+---------+----------------------------+--------+----------+-------------+
|  1 | PRIMARY            | 사원   | NULL       | range | PRIMARY       | PRIMARY | 4       | NULL                       | 104330 |   100.00 | Using where |
|  2 | DEPENDENT SUBQUERY | 급여   | NULL       | ref   | PRIMARY       | PRIMARY | 4       | tuning.사원.사원번호       |      9 |   100.00 | NULL        |
+----+--------------------+--------+------------+-------+---------------+---------+---------+----------------------------+--------+----------+-------------+

원인 : 해당 쿼리는 의존성을 가진 서브쿼리

개선 후

SELECT 사원.사원번호, 사원.이름, 사원.
FROM 사원, 급여
WHERE 사원.사원번호 > 450000
AND 사원.사원번호 = 급여.사원번호
GROUP BY 사원.사원번호
HAVING MAX(급여.연봉) > 100000;
-> 3155 rows in set (0.17 sec)
실행 계획
+----+-------------+--------+------------+-------+-------------------------------------+---------+---------+----------------------------+--------+----------+-------------+
| id | select_type | table  | partitions | type  | possible_keys                       | key     | key_len | ref                        | rows   | filtered | Extra       |
+----+-------------+--------+------------+-------+-------------------------------------+---------+---------+----------------------------+--------+----------+-------------+
|  1 | SIMPLE      | 사원   | NULL       | range | PRIMARY,I_입사일자,I_성별_성        | PRIMARY | 4       | NULL                       | 104330 |   100.00 | Using where |
|  1 | SIMPLE      | 급여   | NULL       | ref   | PRIMARY                             | PRIMARY | 4       | tuning.사원.사원번호       |      9 |   100.00 | NULL        |
+----+-------------+--------+------------+-------+-------------------------------------+---------+---------+----------------------------+--------+----------+-------------+

개선 : 조인으로 풀어서 처리

12. 불필요한 조인을 수행하는 쿼리

A출입문으로 출입한 사원이 몇명인지 구하는 쿼리

개선 전

SELECT COUNT(DISTINCT 사원.사원번호) as 데이터건수
FROM 사원, (SELECT 사원번호 FROM 사원출입기록 WHERE 출입문 = 'A') 기록
WHERE 사원.사원번호 = 기록.사원번호;
-> 1 row in set (0.33 sec)
실행 계획
+----+-------------+--------------------+------------+--------+---------------+-------------+---------+----------------------------------------+--------+----------+-------------+
| id | select_type | table              | partitions | type   | possible_keys | key         | key_len | ref                                    | rows   | filtered | Extra       |
+----+-------------+--------------------+------------+--------+---------------+-------------+---------+----------------------------------------+--------+----------+-------------+
|  1 | SIMPLE      | 사원출입기록       | NULL       | ref    | I_출입문      | I_출입문    | 4       | const                                  | 329467 |   100.00 | Using index |
|  1 | SIMPLE      | 사원               | NULL       | eq_ref | PRIMARY       | PRIMARY     | 4       | tuning.사원출입기록.사원번호           |      1 |   100.00 | Using index |
+----+-------------+--------------------+------------+--------+---------------+-------------+---------+----------------------------------------+--------+----------+-------------+

원인 : 내부적으로 join으로 풀려서 처리하나 조인이 데이터 존재확인만을 위해 사용

개선 후

SELECT COUNT(DISTINCT 사원.사원번호) as 데이터건수
FROM 사원
WHERE EXISTS 
(SELECT 1
FROM 사원출입기록 기록
WHERE 출입문 = 'A'
AND 기록.사원번호 = 사원.사원번호);
-> 1 row in set (0.22 sec)
실행 계획
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+----------------------------+--------+----------+-------------+
| id | select_type  | table       | partitions | type   | possible_keys       | key                 | key_len | ref                        | rows   | filtered | Extra       |
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+----------------------------+--------+----------+-------------+
|  1 | SIMPLE       | 사원        | NULL       | index  | PRIMARY             | I_입사일자          | 3       | NULL                       | 299822 |   100.00 | Using index |
|  1 | SIMPLE       | <subquery2> | NULL       | eq_ref | <auto_distinct_key> | <auto_distinct_key> | 4       | tuning.사원.사원번호       |      1 |   100.00 | NULL        |
|  2 | MATERIALIZED | 기록        | NULL       | ref    | I_출입문            | I_출입문            | 4       | const                      | 329467 |   100.00 | Using index |
+----+--------------+-------------+------------+--------+---------------------+---------------------+---------+----------------------------+--------+----------+-------------+

개선 : EXISTS를 활용하여 개선

정리

정리하자면 좋은 쿼리를 위해 다음을 고려해보자.

  1. 좌변 가공을 하지말자.
  2. UNION 보다는 UNION ALL을 사용하자.
  3. 데이터 타입을 같게 하여 형변환이 일어나지 않게 하자.
  4. 불필요한 작업을 제거하자. (not null 컬럼에 ifnull, unique에 distinct 등)
  5. group by는 인덱스 순서를 맞춰서 사용하자.
  6. 가능하면 like보다는 범위 조건을 사용하자.
  7. 카디널리티에 따라 인덱스를 사용하지 않거나, 힌트를 사용하는 것도 고려하자.

Continue reading


© 2021. All rights reserved.

Powered by Hydejack v9.1.5