diff --git "a/\354\227\205\353\254\264\354\227\220 \353\260\224\353\241\234 \354\223\260\353\212\224 SQL \355\212\234\353\213\235/\354\244\200\353\240\254/3\354\236\245.md" "b/\354\227\205\353\254\264\354\227\220 \353\260\224\353\241\234 \354\223\260\353\212\224 SQL \355\212\234\353\213\235/\354\244\200\353\240\254/3\354\236\245.md" new file mode 100644 index 0000000..ec6ebc5 --- /dev/null +++ "b/\354\227\205\353\254\264\354\227\220 \353\260\224\353\241\234 \354\223\260\353\212\224 SQL \355\212\234\353\213\235/\354\244\200\353\240\254/3\354\236\245.md" @@ -0,0 +1,815 @@ +# 1. 실행 계획 + +## MySQL 의 실행 계획을 확인하는 방법 - `Explain` + +MySQL의 옵티마이저는 SQL 쿼리를 실행하기 전에 최적화된 실행 계획을 수립한다. + +그 실행 계획을 조회하는 명령어가 아래 명령어다. + +```jsx +EXPLAIN sql문; +DESCRIBE sql문; +DESC sql문; +``` + +**출력 결과** + +![image](https://github.com/user-attachments/assets/49708ffe-b508-41dd-a67b-dfdb2ced5c5b) + + +해당 결과를 보면 확인할 수 있는 속성은 + +- id +- select_type +- table +- type +- key + +등의 정보가 출력된다. + +이러한 정보는 이후 SQL 쿼리의 적합성을 판단하는데 도움이 되므로 하나하나 확인해보자. + +### `id` 속성 + +**SQL 문이 수행되는 차례를 ID 로 표기한 것.** 조인할때에는 똑같은 id 가 출력된다. + +여기서 알 수 있는 정보는 두가지다. + +- ID의 숫자가 **작을 수록 먼저 수행**된 것이다. +- ID가 같은 값이라면 **두개의 테이블의 조인**이 이루어졌다는 것이다. + +```sql +EXPLAIN +SELECT + 사원.사원번호, + 사원.이름, + 사원.성, + 급여.연봉, + (SELECT MAX(부서번호) + FROM 부서사원_매핑 as 매핑 + WHERE 매핑.사원번호 = 사원.사원번호) as 카운트 +FROM + 사원, + 급여 +WHERE + 사원.사원번호 = 10001 + AND 사원.사원번호 = 급여.사원번호; +``` + +![image](https://github.com/user-attachments/assets/f3728b07-ffd8-4b70-a276-082604649014) + + +### `select_type` 속성 + +SQL문을 구성하는 SELECT문의 유형을 출력하는 것. + +유형을 출력한다는게 무슨 뜻이냐? 그것은 해당 SELECT 문이 + +단순히 FROM 절에 위치한 것인지 → `SIMPLE` + +서브쿼리인지 → `PRIMARY` & `SUBQUERY` + +UNION 절로 묶인 SELECT 문인지 → `UNION` + +등의 정보를 제공한다. + +- **`SIMPLE`**: 서브쿼리나 UNION 없이 단순한 SELECT 문 + + ```jsx + mysql> EXPLAIN SELECT * FROM 사원 WHERE 사원번호 = 100000; + + +----+-------------+--------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ + | 1 | SIMPLE | 사원 | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL | + +----+-------------+--------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ + 1 row in set, 1 warning (0.00 sec) + ``` + + ```sql + EXPLAIN + SELECT 사원.사원번호, 사원.이름, 사원.성, 급여.연봉 + FROM 사원, + (SELECT 사원번호, 연봉 + FROM 급여 + WHERE 연봉 > 80000) AS 급여 + WHERE 사원.사원번호 = 급여.사원번호 + AND 사원.사원번호 BETWEEN 10001 AND 10010; + +----+-------------+--------+------------+-------+---------------+---------+---------+----------------------------+------+----------+-------------+ + | 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 | + | 1 | SIMPLE | 급여 | NULL | ref | PRIMARY | PRIMARY | 4 | tuning.사원.사원번호 | 9 | 33.33 | Using where | + +----+-------------+--------+------------+-------+---------------+---------+---------+----------------------------+------+----------+-------------+ + 2 rows in set, 1 warning (0.01 sec) + ``` + +- **`PRIMARY`**: 가장 바깥쪽의 SELECT 문을 의미하며, 서브쿼리를 포함할 수 있음 + + ```sql + EXPLAIN + SELECT 사원.사원번호, 사원.이름, 사원.성, + (SELECT MAX(부서번호) + FROM 부서사원_매핑 as 매핑 WHERE 매핑.사원번호 = 사원.사원번호) 카운트 + FROM 사원 + WHERE 사원.사원번호 = 100001; + +----+--------------------+--------+------------+-------+---------------+---------+---------+-------+------+----------+------------------------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+--------------------+--------+------------+-------+---------------+---------+---------+-------+------+----------+------------------------------+ + | 1 | PRIMARY | 사원 | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL | + | 2 | DEPENDENT SUBQUERY | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | Select tables optimized away | + +----+--------------------+--------+------------+-------+---------------+---------+---------+-------+------+----------+------------------------------+ + 2 rows in set, 2 warnings (0.00 sec) + ``` + + 이렇게 UNION 절로 묶인 상태에서는 첫번째로 온 쿼리가 PRIMARY 가 됨. + + ```sql + EXPLAIN + SELECT 사원1.사원번호, 사원1.이름, 사원1.성 + FROM 사원 as 사원1 + WHERE 사원1.사원번호 = 100001 + UNION ALL + SELECT 사원2.사원번호, 사원2.이름, 사원2.성 + FROM 사원 as 사원2 + WHERE 사원2.사원번호 = 100002; + +----+-------------+---------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+---------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ + | 1 | PRIMARY | 사원1 | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL | + | 2 | UNION | 사원2 | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL | + +----+-------------+---------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ + 2 rows in set, 1 warning (0.00 sec) + ``` + +- **`SUBQUERY`**: SELECT 문 안에 포함된 서브쿼리를 의미합니다. + + ```sql + EXPLAIN + SELECT + (SELECT COUNT(*) + FROM 부서사원_매핑 as 매핑 + ) as 카운트, + (SELECT MAX(연봉) + FROM 급여 + ) as 급여; + +----+-------------+--------+------------+-------+---------------+----------------+---------+------+---------+----------+----------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+-------+---------------+----------------+---------+------+---------+----------+----------------+ + | 1 | PRIMARY | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | No tables used | + | 3 | SUBQUERY | 급여 | NULL | ALL | NULL | NULL | NULL | NULL | 2838398 | 100.00 | NULL | + | 2 | SUBQUERY | 매핑 | NULL | index | NULL | I_부서번호 | 12 | NULL | 331143 | 100.00 | Using index | + +----+-------------+--------+------------+-------+---------------+----------------+---------+------+---------+----------+----------------+ + 3 rows in set, 1 warning (0.00 sec) + ``` + +- **`DERIVED`**: FROM 절에 포함된 서브쿼리로부터 파생된 SELECT 문 ( = **인라인 뷰)** + + ```sql + EXPLAIN + SELECT 사원.사원번호, 급여.연봉 + FROM 사원, + (SELECT 사원번호, MAX(연봉) as 연봉 + FROM 급여 + WHERE 사원번호 BETWEEN 10001 AND 20000 GROUP BY 사원번호) as 급여 + WHERE 사원.사원번호 = 급여.사원번호; + +----+-------------+------------+------------+--------+------------------------+---------+---------+---------------------+--------+----------+-------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+------------+------------+--------+------------------------+---------+---------+---------------------+--------+----------+-------------+ + | 1 | PRIMARY | | NULL | ALL | NULL | NULL | NULL | NULL | 184756 | 100.00 | NULL | + | 1 | PRIMARY | 사원 | NULL | eq_ref | PRIMARY | PRIMARY | 4 | 급여.사원번호 | 1 | 100.00 | Using index | + | 2 | DERIVED | 급여 | NULL | range | PRIMARY,I_사용여부 | PRIMARY | 4 | NULL | 184756 | 100.00 | Using where | + +----+-------------+------------+------------+--------+------------------------+---------+---------+---------------------+--------+----------+-------------+ + 3 rows in set, 1 warning (0.01 sec) + ``` + +- **`UNION`**: UNION 및 UNION ALL 구문으로 합쳐진 SELECT문에서 첫번째 `PRIMARY`을 제외한 나머지 + + ```sql + EXPLAIN + SELECT 'M' as 성별, MAX(입사일자) as 입사일자 + FROM 사원 as 사원1 + WHERE 성별 = 'M' + + UNION ALL + + SELECT 'F' as 성별, MIN(입사일자) as 입사일자 + FROM 사원 as 사원2 + WHERE 성별 = 'F'; + +----+-------------+---------+------------+------+---------------+--------------+---------+-------+--------+----------+-------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+---------+------------+------+---------------+--------------+---------+-------+--------+----------+-------+ + | 1 | PRIMARY | 사원1 | NULL | ref | I_성별_성 | I_성별_성 | 1 | const | 149534 | 100.00 | NULL | + | 2 | UNION | 사원2 | NULL | ref | I_성별_성 | I_성별_성 | 1 | const | 149534 | 100.00 | NULL | + +----+-------------+---------+------------+------+---------------+--------------+---------+-------+--------+----------+-------+ + 2 rows in set, 1 warning (0.00 sec) + ``` + +- **`UNION RESULT`**: UNION ALL 이 아닌 UNION 으로 SELECT 절을 결합했을 때 출력. + - UNION은 출력 결과에 중복이 없는 유일한 속성ㅇ르 가지므로 SELECT 문에서 중복을 체크하는 과정을 과정을 거침 + - UNION 이 아니라 UNION RESULT 가 나왔다는 건 디스크에 임시 테이블을 만들어 중복을 제거하겠다는 의미 + + ```sql + EXPLAIN + SELECT 사원_통합.* + FROM ( + SELECT MAX(입사일자) as 입사일자 + FROM 사원 as 사원1 + WHERE 성별 = 'M' + UNION + SELECT MIN(입사일자) as 입사일자 + FROM 사원 as 사원2 + WHERE 성별 = 'M' + ) as 사원_통합; + +----+--------------+------------+------------+------+---------------+--------------+---------+-------+--------+----------+-----------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+--------------+------------+------------+------+---------------+--------------+---------+-------+--------+----------+-----------------+ + | 1 | PRIMARY | | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100.00 | NULL | + | 2 | DERIVED | 사원1 | NULL | ref | I_성별_성 | I_성별_성 | 1 | const | 149534 | 100.00 | NULL | + | 3 | UNION | 사원2 | NULL | ref | I_성별_성 | I_성별_성 | 1 | const | 149534 | 100.00 | NULL | + | 4 | UNION RESULT | | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary | + +----+--------------+------------+------------+------+---------------+--------------+---------+-------+--------+----------+-----------------+ + 4 rows in set, 1 warning (0.00 sec) + ``` + +- **`DEPENDENT` UNION 혹은 UNION ALL 을 사용하는 서브쿼리가 메인 테이블의 영향을 받는 경우에 UNION으로 연결된 쿼리들에 붙는다.** + + ```sql + EXPLAIN + SELECT 관리자.부서번호, + ( + SELECT 사원1.이름 + FROM 사원 AS 사원1 + WHERE 성별 = 'F' + AND 사원1.사원번호 = 관리자.사원번호 + UNION ALL + SELECT 사원2.이름 + FROM 사원 AS 사원2 + WHERE 성별 = 'M' + AND 사원2.사원번호 = 관리자.사원번호 + ) AS 이름 + FROM 부서관리자 AS 관리자; + +----+--------------------+-----------+------------+--------+----------------------+----------------+---------+-------------------------------+------+----------+-------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+--------------------+-----------+------------+--------+----------------------+----------------+---------+-------------------------------+------+----------+-------------+ + | 1 | PRIMARY | 관리자 | NULL | index | NULL | I_부서번호 | 12 | NULL | 24 | 100.00 | Using index | + | 2 | DEPENDENT SUBQUERY | 사원1 | NULL | eq_ref | PRIMARY,I_성별_성 | PRIMARY | 4 | tuning.관리자.사원번호 | 1 | 50.00 | Using where | + | 3 | DEPENDENT UNION | 사원2 | NULL | eq_ref | PRIMARY,I_성별_성 | PRIMARY | 4 | tuning.관리자.사원번호 | 1 | 50.00 | Using where | + +----+--------------------+-----------+------------+--------+----------------------+----------------+---------+-------------------------------+------+----------+-------------+ + 3 rows in set, 3 warnings (0.00 sec) + ``` + + - **`DEPENDENT SUBQUERY`**: **UNION 또는 UNION ALL 로 연결된 서브쿼리의 첫번째 쿼리** + - **`DEPENDENT UNION` UNION 또는 UNION ALL 로 연결된 서브쿼리의 두번째 쿼리** +- **`UNCACHEABLE SUBQUERY**:` 메모리에 상주하여 재활용되어야 할 서브쿼리가 재사용되지 못할 때 출력되는 유형 + + 해당 유형이 뜨는 이유 + + 1. 해당 서브쿼리 안에 **사용자 정의 함수나 사용자 변수**가 포함됨. + 2. `RAND()`, `UUID()` 함수 등을 사용하여 **매번 조회마다 결과가 달라지는 경우**. + + > **어떻게 튜닝할까?** + > + > + > 만약 해당 쿼리가 자주 호출된다면 메모리에 서브쿼리 결과가 상주할 수 있도록 변경하는 방향으로 튜닝 검토 가능. + > + + ```sql + EXPLAIN + SELECT * + FROM 사원 + WHERE 사원번호 = (SELECT ROUND(RAND() * 1000000)); + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ + | 1 | SIMPLE | 사원 | NULL | ALL | NULL | NULL | NULL | NULL | 299069 | 10.00 | Using where | + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ + 1 row in set, 2 warnings (0.00 sec) + ``` + +- **`MATERIALIZED`**: IN 절 구문에 연결된 서브쿼리가 임시 테이블을 생성한 뒤 조인이나 가공 작업을 진행할 때 출력됨. + + ```sql + EXPLAIN + SELECT * + FROM 사원 + WHERE 사원번호 IN (SELECT 사원번호 FROM 급여 WHERE 시작일자 > '2020-01-01'); + +----+-------------+--------+------------+--------+---------------+---------+---------+----------------------------+---------+----------+-------------------------------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+--------+---------------+---------+---------+----------------------------+---------+----------+-------------------------------------+ + | 1 | SIMPLE | 급여 | NULL | index | PRIMARY | PRIMARY | 7 | NULL | 2838398 | 3.56 | Using where; Using index; LooseScan | + | 1 | SIMPLE | 사원 | NULL | eq_ref | PRIMARY | PRIMARY | 4 | tuning.급여.사원번호 | 1 | 100.00 | NULL | + +----+-------------+--------+------------+--------+---------------+---------+---------+----------------------------+---------+----------+-------------------------------------+ + 2 rows in set, 1 warning (0.00 sec) + ``` + + +### `table` 속성 + +- 테이블을 표시하는 항목 +- 테이블명이나 테이블 별칭을 출력 +- 서브쿼리나 임시 테이블을 작업할 때는 `` , `` 로 출력됨. (`#`은 ID) + +```sql +EXPLAIN +SELECT 사원.사원번호, 급여.연봉 +FROM 사원, + (SELECT 사원번호, MAX(연봉) as 연봉 + FROM 급여 + WHERE 사원번호 BETWEEN 10001 AND 20000 GROUP BY 사원번호) as 급여 +WHERE 사원.사원번호 = 급여.사원번호; ++----+-------------+------------+------------+--------+------------------------+---------+---------+---------------------+--------+----------+-------------+ +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | ++----+-------------+------------+------------+--------+------------------------+---------+---------+---------------------+--------+----------+-------------+ +| 1 | PRIMARY | | NULL | ALL | NULL | NULL | NULL | NULL | 184756 | 100.00 | NULL | +| 1 | PRIMARY | 사원 | NULL | eq_ref | PRIMARY | PRIMARY | 4 | 급여.사원번호 | 1 | 100.00 | Using index | +| 2 | DERIVED | 급여 | NULL | range | PRIMARY,I_사용여부 | PRIMARY | 4 | NULL | 184756 | 100.00 | Using where | ++----+-------------+------------+------------+--------+------------------------+---------+---------+---------------------+--------+----------+-------------+ +3 rows in set, 1 warning (0.00 sec) +``` + +- 해석 : ID 가 동일한 derived2 와 사원 테이블이 JOIN 했음. 여기서 는 ID 가 2인ㅇ 급여 테이블이라는 것을 알 수 있다. + +### `partitions` 속성 + +- 데이터가 저장된 논리적인 영역을 표시하는 항목. +- 사전에 정의한 전체 파티션 중 특정 파티션에 선택적으로 접근하는 것이 유리하다. +- 만약 너무 많은 영역의 파티션에 접근하는 것을 출력된다면 파티션 정의를 튜닝해봐야한다. + +### `type` 속성 + +- 테이블의 데이터를 어떻게 찾을지에 관한 정보를 제공하는 항목 +- index를 이용할지, 테이블을 처음부터 끝까지 전부 확인할지 등을 해석 가능. +- **`System` : 테이블에 데이터가 한개만 있거나 없는 경우. (성능상 최상의 type 이긴 하다.)** + + ```sql + mysql> CREATE TABLE myisam_테이블 ( + -> col1 INT(11) NULL DEFAULT NULL + -> ) ENGINE=MYISAM; + Query OK, 0 rows affected, 1 warning (0.02 sec) + + mysql> INSERT INTO myisam_테이블 VALUES(1); + Query OK, 1 row affected (0.00 sec) + + -- 이후 + EXPLAIN + SELECT * FROM myisam_테이블; + +----+-------------+------------------+------------+--------+---------------+------+---------+------+------+----------+-------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+------------------+------------+--------+---------------+------+---------+------+------+----------+-------+ + | 1 | SIMPLE | myisam_테이블 | NULL | system | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL | + +----+-------------+------------------+------------+--------+---------------+------+---------+------+------+----------+-------+ + 1 row in set, 1 warning (0.00 sec) + + ``` + +- **`const` : 조회되는 데이터가 단 1건일 때.** + - System 처럼 성능상 매우 유리하다. + - 인덱스나 기본키로 단 1건의 데이터에 접근하면 되므로 속도나 리소스 사용 측면에서 아주 좋음 + + ```sql + EXPLAIN + SELECT * + FROM 사원 + WHERE 사원번호 = 10001; + +----+-------------+--------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ + | 1 | SIMPLE | 사원 | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL | + +----+-------------+--------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ + 1 row in set, 1 warning (0.00 sec) + ``` + +- **`eq_ref`** : 조인이 수행될 때 드리븐 테이블의 데이터에 접근하며 고유 인덱스 또는 기본키로 1건의 데이터를 조회하는 방식. + - 조인이 수행될 때 성능상 가장 유리한 경우임. + + ```sql + EXPLAIN + SELECT 매핑.사원번호, 부서.부서번호, 부서.부서명 + FROM 부서사원_매핑 as 매핑, 부서 + WHERE 매핑.부서번호 = 부서.부서번호 + AND 매핑.사원번호 BETWEEN 100001 AND 100010; + +----+-------------+--------+------------+--------+------------------------+---------+---------+----------------------------+------+----------+--------------------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+--------+------------------------+---------+---------+----------------------------+------+----------+--------------------------+ + | 1 | SIMPLE | 매핑 | NULL | range | PRIMARY,I_부서번호 | PRIMARY | 4 | NULL | 12 | 100.00 | Using where; Using index | + | 1 | SIMPLE | 부서 | NULL | eq_ref | PRIMARY | PRIMARY | 12 | tuning.매핑.부서번호 | 1 | 100.00 | NULL | + +----+-------------+--------+------------+--------+------------------------+---------+---------+----------------------------+------+----------+--------------------------+ + 2 rows in set, 1 warning (0.00 sec + ``` + +- **`ref`** : eq_ref 와 유사.. 1대 다 관계로 드라이빙 테이블과 드리븐 테이블의 조인이 수행될때 + - 이때 검색에 드라이빙 테이블의 기본키나 고유 인덱스를 활용하여 2개 이상의 데이터를 + + ```sql + EXPLAIN + SELECT 사원.사원번호, 직급.직급명 + FROM 사원, + 직급 + WHERE 사원.사원번호 = 직급.사원번호 + AND 사원.사원번호 BETWEEN 10001 AND 10100; + +----+-------------+--------+------------+--------+---------------+---------+---------+----------------------------+------+----------+--------------------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+--------+---------------+---------+---------+----------------------------+------+----------+--------------------------+ + | 1 | SIMPLE | 직급 | NULL | range | PRIMARY | PRIMARY | 4 | NULL | 151 | 100.00 | Using where; Using index | + | 1 | SIMPLE | 사원 | NULL | eq_ref | PRIMARY | PRIMARY | 4 | tuning.직급.사원번호 | 1 | 100.00 | Using index | + +----+-------------+--------+------------+--------+---------------+---------+---------+----------------------------+------+----------+--------------------------+ + 2 rows in set, 1 warning (0.00 sec) + ``` + + + + ```sql + EXPLAIN + SELECT * + FROM 사원 + WHERE 입사일자 = '1985-11-21'; + +----+-------------+--------+------------+------+----------------+----------------+---------+-------+------+----------+-------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+------+----------------+----------------+---------+-------+------+----------+-------+ + | 1 | SIMPLE | 사원 | NULL | ref | I_입사일자 | I_입사일자 | 3 | const | 119 | 100.00 | NULL | + +----+-------------+--------+------------+------+----------------+----------------+---------+-------+------+----------+-------+ + 1 row in set, 1 warning (0.01 sec) + ``` + +- **`ref_or_null`** + - ref 와 비슷하지만 `ISNULL` 구문에 인덱스를 활용하도록 최적화한 방식. + - MySQL, MariaDB는 null도 인덱스를 활용하여 검색 가능하며 제일 앞쪽에 정렬됨. + - 이때 테이블에서 검색할 null 의 양이 많다면 `ref_or_null` 방식으로 튜닝시 효율적인 SQL문이 될 것. + + ```sql + EXPLAIN + SELECT * + FROM 사원출입기록 + WHERE 출입문 IS NULL + OR 출입문 = 'A'; + +----+-------------+--------------------+------------+-------------+---------------+-------------+---------+-------+--------+----------+-----------------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------------------+------------+-------------+---------------+-------------+---------+-------+--------+----------+-----------------------+ + | 1 | SIMPLE | 사원출입기록 | NULL | ref_or_null | I_출입문 | I_출입문 | 4 | const | 329468 | 100.00 | Using index condition | + +----+-------------+--------------------+------------+-------------+---------------+-------------+---------+-------+--------+----------+-----------------------+ + 1 row in set, 1 warning (0.00 sec) + ``` + +- **`range`** + - 테이블 내의 연속된 값을 조회하는 유형 + - =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, IN 연산들을 범위 스캔을 수행하는 방식 + + ```sql + EXPLAIN + SELECT * + FROM 사원 + WHERE 사원번호 BETWEEN 10001 AND 100000; + +----+-------------+--------+------------+-------+---------------+---------+---------+------+--------+----------+-------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+-------+---------------+---------+---------+------+--------+----------+-------------+ + | 1 | SIMPLE | 사원 | NULL | range | PRIMARY | PRIMARY | 4 | NULL | 149534 | 100.00 | Using where | + +----+-------------+--------+------------+-------+---------------+---------+---------+------+--------+----------+-------------+ + 1 row in set, 1 warning (0.01 sec) + ``` + +- **`fulltext`** + - 텍스트 검색을 빠르게 하기 위해 전문 인덱스를 사용하여 데이터에 접근하는 방식 +- **`index_merge`** + - 결합된 인덱스들이 동시에 사용되는 유형 + + ```sql + EXPLAIN + SELECT * + FROM 사원 + WHERE 사원번호 BETWEEN 10001 AND 100000 + AND 입사일자 = '1985-11-21'; + +----+-------------+--------+------------+-------------+------------------------+------------------------+---------+------+------+----------+------------------------------------------------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+-------------+------------------------+------------------------+---------+------+------+----------+------------------------------------------------------+ + | 1 | SIMPLE | 사원 | NULL | index_merge | PRIMARY,I_입사일자 | I_입사일자,PRIMARY | 7,4 | NULL | 15 | 93.75 | Using intersect(I_입사일자,PRIMARY); Using where | + +----+-------------+--------+------------+-------------+------------------------+------------------------+---------+------+------+----------+------------------------------------------------------+ + 1 row in set, 1 warning (0.01 sec) + ``` + +- **`index`** + - 인덱스 풀 스캔 즉, 물리적인 인덱스 블록을 처음부터 끝까지 훑는 방식 + - 인덱스는 보통 테이블보다 크기가 작으므로 풀 스캔보다는 빠름 + + ```sql + EXPLAIN + SELECT 사원번호 + FROM 직급 + WHERE 직급명 = 'Manager'; + +----+-------------+--------+------------+-------+---------------+---------+---------+------+--------+----------+--------------------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+-------+---------------+---------+---------+------+--------+----------+--------------------------+ + | 1 | SIMPLE | 직급 | NULL | index | PRIMARY | PRIMARY | 159 | NULL | 442486 | 10.00 | Using where; Using index | + +----+-------------+--------+------------+-------+---------------+---------+---------+------+--------+----------+--------------------------+ + 1 row in set, 1 warning (0.00 sec) + ``` + +- **`All`** + - 테이블을 처음부터 끝까지 읽는 풀스캔 유형 + - 전체 테이블 중 10~20% 이상 분량의 데이터를 조회할때는 ALL 유형 성능이 유리할 수 있음. + + ```sql + EXPLAIN + SELECT * FROM 사원; + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------+ + | 1 | SIMPLE | 사원 | NULL | ALL | NULL | NULL | NULL | NULL | 299069 | 100.00 | NULL | + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------+ + 1 row in set, 1 warning (0.00 sec) + ``` + + +### `possible_keys` 속성 + +- 옵티마이저가 SQL문을 최적화할때 사용할 수 있는 인덱스 목록 출력. + +### `key` 속성 + +- SQL문을 최적화하고자 사용한 기본 키 또는 인덱스 명을 의미. + +```sql +EXPLAIN +SELECT 사원번호 +FROM 직급 +WHERE 직급명 = 'Manager'; ++----+-------------+--------+------------+-------+---------------+---------+---------+------+--------+----------+--------------------------+ +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | ++----+-------------+--------+------------+-------+---------------+---------+---------+------+--------+----------+--------------------------+ +| 1 | SIMPLE | 직급 | NULL | index | PRIMARY | PRIMARY | 159 | NULL | 442486 | 10.00 | Using where; Using index | ++----+-------------+--------+------------+-------+---------------+---------+---------+------+--------+----------+--------------------------+ +1 row in set, 1 warning (0.00 sec) +``` + +### `key_len` 속성 + +- 사용한 인덱스의 Byte 수를 의미 +- key_len 의 계산 + - INT - 4byte + - VARCHAR - 단위당 UTF-8 의 경우 3byte + **`가변 길이일 경우 2byte 추가`** + - Date - 3byte + +```sql +EXPLAIN +SELECT 사원번호 +FROM 직급 +WHERE 직급명 = 'Manager'; ++----+-------------+--------+------------+-------+---------------+---------+---------+------+--------+----------+--------------------------+ +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | ++----+-------------+--------+------------+-------+---------------+---------+---------+------+--------+----------+--------------------------+ +| 1 | SIMPLE | 직급 | NULL | index | PRIMARY | PRIMARY | 159 | NULL | 442486 | 10.00 | Using where; Using index | ++----+-------------+--------+------------+-------+---------------+---------+---------+------+--------+----------+--------------------------+ +1 row in set, 1 warning (0.00 sec) + +mysql> desc 직급; ++--------------+-------------+------+-----+---------+-------+ +| Field | Type | Null | Key | Default | Extra | ++--------------+-------------+------+-----+---------+-------+ +| 사원번호 | int | NO | PRI | NULL | | +| 직급명 | varchar(50) | NO | PRI | NULL | | +| 시작일자 | date | NO | PRI | NULL | | +| 종료일자 | date | YES | | NULL | | ++--------------+-------------+------+-----+---------+-------+ +4 rows in set (0.01 sec) +``` + +해당 테이블을 보면 + +`varchar(50) * 3 + 2 = 152byte` + +`date = 3byte` + +`int = 4byte` 로 + +→ 152 + 3 + 2 = 159 byte 이다. + +### `ref` 속성 + +- 테이블 조인을 수행할 때 어떤 조건으로 해당 테이블에 엑세스되는지 알려주는 속성 + + ```sql + EXPLAIN + SELECT 사원.사원번호, 직급.직급명 + FROM 사원, 직급 + WHERE 사원.사원번호 = 직급.사원번호 + AND 사원.사원번호 BETWEEN 10001 AND 10180; + +----+-------------+--------+------------+--------+---------------+---------+---------+----------------------------+------+----------+--------------------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+--------+---------------+---------+---------+----------------------------+------+----------+--------------------------+ + | 1 | SIMPLE | 직급 | NULL | range | PRIMARY | PRIMARY | 4 | NULL | 266 | 100.00 | Using where; Using index | + | 1 | SIMPLE | 사원 | NULL | eq_ref | PRIMARY | PRIMARY | 4 | tuning.직급.사원번호 | 1 | 100.00 | Using index | + +----+-------------+--------+------------+--------+---------------+---------+---------+----------------------------+------+----------+--------------------------+ + 2 rows in set, 1 warning (0.00 sec) + ``` + + +해당 테이블을 보면 id가 동일함으로 join 이 일어났고 드리븐 테이블인 직급 테이블의 데이터에 접근할 때 `직급.사원번호` 로 접근한다는 것을 알 수 있음. + +### `rows` 속성 + +- SQL 문을 수행하고자 접근하는 데이터의 모든 행(row)의 수를 나타내는 예측 항목. +- SQL 최종 결과 건 수와 비교해서 rows 의 수가 크게 차이날때는 불필요하게 MySQL 엔진까지 데이터를 많이 가져왓다는 뜻. → SQL 튜닝 필요. + +### `filtered` 속성 + +- SQL 문을 통해 DB 엔진으로 가져온 데이터 대상으로 필터 조건에 따라 어느 정도의 비율로 데이터를 제거했는지 의미. +- 단위는 % + +### `extra` 속성 + +- SQL 수행에 따른 추가 정보를 보여주는 항목 +- 30여가지 항목이 있으나 자주 만나는 정보만 설명. +- `Distinct`: 중복이 제거되어 유일한 값을 찾을 때 출력되는 정보이다. 중복 제거가 포함되는 distinct 키워드나 union 구문이 포함된 경우 출력된다. +- `Using where`: 실행 계획에서 자주 볼 수 있는 extra 정보이다. WHERE 절의 필터 조건을 사용해 MySQL 엔진으로 가져온 데이터를 추출한다는 의미로 이해할 수 있다. +- `Using temporary`: 데이터의 중간 결과를 저장하고자 임시 테이블을 생성한다는 의미이다. 데이터를 가져와 저장한 뒤에 정렬 작업을 수행하거나 중복을 제거하는 작업 등을 수행한다. 보통 DISTINCT, GROUP BY, ORDER BY 구문이 포함된 경우 Using temporary 정보가 출력된다. +- `Using index`: 물리적인 데이터 파일을 읽지 않고 인덱스만을 읽어서 SQL 문의 요청사항을 처리할 수 있는 경우를 의미한다. 커버링 인덱스라고 부르며, 인덱스로 구성된 열만 SQL 문에서 사용할 경우 이 방식을 활용한다. + + ```sql + EXPLAIN + SELECT 직급명 + FROM 직급 + WHERE 사원번호 = 100000; + +----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+ + | 1 | SIMPLE | 직급 | NULL | ref | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | Using index | + +----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+ + 1 row in set, 1 warning (0.01 sec) + + ``` + +- `Using filesort`: 정렬이 필요한 데이터를 메모리에 올리고 정렬 작업을 수행한다는 의미이다. 인덱스를 사용하지 못할 때는 정렬을 위해 메모리 영역에 데이터를 올리게 된다. +- `Using join buffer`: 조인을 수행하기 위해 중간 데이터 결과를 저장하는 조인 버퍼를 사용한다는 의미이다. +- `Using union/Using intersect/Using sort_union`: 인덱스를 병합하여 데이터를 접근하는 방식이다. Using union은 OR 구문, Using intersect는 AND 구문, Using sort_union은 OR 구문이 동등 조건이 아닐 때 사용된다. +- `Using index condition`: 인덱스 조건을 스토리지 엔진에서 필터링하여 MySQL 엔진의 부하를 줄이는 방식이다. +- `Using index condition(BKA)`: 배치 키 액세스를 사용하는 방식이다. +- `Using index for group-by`: Group by 구문이나 Distinct 구문이 포함될 때 인덱스로 정렬 작업을 수행하는 인덱스 루스 스캔일 때 출력된다. +- `Not exists`: 하나의 일치하는 행을 찾으면 추가로 행을 더 검색하지 않아도 될 때 출력되는 유형이다. + +## 확장된 EXPLAIN + +- 실행계획에 대한 추가정보를 확인하고자 한다면 확장된 실행 계획 명령어로 확인한다. + +### MySQL + +- 예측된 실행 계획 정보 + - `EXPLAIN FORMAT = TRADITIONAL` + + Default 실행계획 정보. + + - `EXPLAIN FORMAT = TREE` + + 트리형태로 추가된 실행 계획 황목 확인 + + - `EXPLAIN FORMAT = JSON` + + JSON 형태로 추가된 실행계획 항목 확인 + +- 실행된 실행 계획 정보 + - **`EXPLAIN ANALYZE`** + + 실제 수행된 소요 시간과 비용을 출력 + + ```sql + mysql> EXPLAIN ANALYZE + -> SELECT * + -> FROM 사원 + -> WHERE 사원번호 BETWEEN 100001 AND 200000; + +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | EXPLAIN | + +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | -> Filter: (`사원`.`사원번호` between 100001 and 200000) (cost=4021 rows=20080) (actual time=0.0407..8.52 rows=10025 loops=1) + -> Index range scan on 사원 using PRIMARY over (100001 <= 사원번호 <= 200000) (cost=4021 rows=20080) (actual time=0.0388..7.49 rows=10025 loops=1) + | + +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + 1 row in set (0.02 sec) + ``` + + + - **`EXPLAIN PARTITIONS`** + + 파티션으로 설정된 테이블에 대해 접근 대상인 파티션 정보를 출력. + + +# 2. 좋고 나쁨을 판단하는 기준 + +select_type, type, extra 항목을 기준을 참조하여 튜닝할 쿼리를 찾을 수 있따. + +![image](https://github.com/user-attachments/assets/ebd517c3-b70f-4ffd-9a8e-8f1b86098468) + + +## 프로파일링 + +접속한 세션에 한해서만 적용되는 프로파일링 옵션 on + +```sql +-- 켜졌는지 확인 +show variables like 'profiling%'; +-- 프로파일링 활성화 +set profiling = 'ON'; +``` + +쿼리문 실행 + +```sql +SELECT 사원번호 +FROM 사원 +WHERE 사원번호 = 100000; ++--------------+ +| 사원번호 | ++--------------+ +| 100000 | ++--------------+ +1 row in set (0.01 sec) +``` + +**프로파일링 확인** + +```sql +show profiles; ++----------+------------+-------------------------------------------------------------+ +| Query_ID | Duration | Query | ++----------+------------+-------------------------------------------------------------+ +| 1 | 0.00068300 | SELECT 사원번호 +FROM 사원 +WHERE 사원번호 = 100000 | +| 2 | 0.00043300 | how profiles | ++----------+------------+-------------------------------------------------------------+ +2 rows in set, 1 warning (0.00 sec) +``` + +프로파일링 상세 번호 확인 + +```sql +show profile for query 2 ++---------------+----------+ +| Status | Duration | ++---------------+----------+ +| starting | 0.000350 | +| freeing items | 0.000050 | +| cleaning up | 0.000033 | ++---------------+----------+ +3 rows in set, 1 warning (0.00 sec) +``` + +만약 특정 Status에 Duration 값이 높게 나타난다면 문제의 소지가 될 수 있음. + +### 프로파일링 결과 해석하기 + +![image](https://github.com/user-attachments/assets/3401dba9-7bf0-4169-b448-db9563121e9b) + +- 선택 가능한 출력 정보 +- **`ALL`** 모든 정보를 표시 +- **`BLOCK IO`** 블록 입력 출력 작업의 횟수를 표시 +- **`CONTEXT SWITCHES`** 자발적 및 비자발적인 컨텍스트 스위치 수를 표시 + + > **컨텍스트 스위치란?** + > + > + > 컨텍스트 스위치는 CPU가 현재 실행 중인 프로세스의 상태를 저장하고, 다른 프로세스의 상태를 불러와 실행하는 작업입니다. 이는 멀티태스킹을 지원하기 위해 필수적인 과정입니다. + > +- **`CPU`** 사용자 및 시스템 CPU 사용 기간 표시 +- **`IPC`** : 보내고 받은 메시지의 수를 표시 +- **`PAGE FAULTS`** 주 페이지 오류 및 부 페이지 오류 수 +- **`SOURCE`** 함수가 발생하는 파일 이름과 행 번호와 함께 소스코드의 함수 이름 표시 +- **`SWAPS`** 스왑 카운트 표시 + + > **스왑 카운트란?** + > + > + > 스왑 카운트는 운영 체제에서 메모리가 부족할 때, 사용 중인 메모리 페이지를 디스크에 저장하고 다시 불러오는 횟수 + > + +추가 정보 + +- MySQL 8.0 버전 기준: https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/explain-output.html +- MariaDB: https://mariadb.com/kb/en/explain/ diff --git "a/\354\227\205\353\254\264\354\227\220 \353\260\224\353\241\234 \354\223\260\353\212\224 SQL \355\212\234\353\213\235/\354\244\200\353\240\254/4\354\236\245.md" "b/\354\227\205\353\254\264\354\227\220 \353\260\224\353\241\234 \354\223\260\353\212\224 SQL \355\212\234\353\213\235/\354\244\200\353\240\254/4\354\236\245.md" new file mode 100644 index 0000000..c3c8ca4 --- /dev/null +++ "b/\354\227\205\353\254\264\354\227\220 \353\260\224\353\241\234 \354\223\260\353\212\224 SQL \355\212\234\353\213\235/\354\244\200\353\240\254/4\354\236\245.md" @@ -0,0 +1,636 @@ +# 4장 악성 SQL 튜닝으로 초보자 탈출하기 + +## 4.1.1 각 테이블의 건수와 INDEX 체크. + +### 각 테이블의 수 확인. + +```sql +SELECT COUNT(1) +FROM 테이블명; +``` + + +**결과 예시:** + +``` +COUNT(1) +--------- +50000 + +``` + +### 테이블의 인덱스 체크. + +```sql +SHOW INDEX FROM 테이블명; +``` + +**결과 예시 (가독성을 위해 `Column_name` 이후 결과 생략) :** + +``` +Table Non_unique Key_name Seq_in_index Column_name +---------- ----------- ----------- ------------- ------------ +테이블명 0 PRIMARY 1 id +테이블명 1 idx_name 1 name +``` + +## 4.1.2 튜닝 방향 잡기. + +각 튜닝 방향을 잡는 절차는 다음과 같다. + +### 1. SQL 문 실행 결과 & 현황 파악 + +- `결과 및 소요 시간` 확인 +- `조인/서브쿼리 구조` 확인 +- `동등/범위 조건` 확인 + +### 2. SQL 쿼리의 구성 요소 체크 + +- 가시적 구성요소 + - 테이블의 데이터 건수 + - **실무 케이스**: 테이블에 1억 건의 데이터가 있을 때, 데이터를 효율적으로 처리하기 위해 데이터 파티셔닝을 적용하면 조회 성능이 크게 향상될 수 있다. + - SELECT 절 컬럼 분석 + - **실무 케이스**: 필요한 컬럼만 SELECT 절에 명시하여 네트워크 트래픽을 줄이고, 처리 속도를 높일 수 있다. + - 조건절 컬럼 분석 + - **실무 케이스**: WHERE 절에 사용되는 조건절 컬럼에 적절한 인덱스를 추가하여, 조회 성능을 개선할 수 있다. + - GROUP, ORDER 컬럼 분석 (그룹, 정렬 요소 체크) + - **실무 케이스**: GROUP BY 및 ORDER BY에 사용되는 컬럼에 대해 인덱스를 생성하면, 그룹핑 및 정렬 작업의 성능을 향상시킬 수 있다. +- 비가시적 구성요소 + - 실행 계획 + - **실무 케이스**: 실행 계획을 분석하여 불필요한 테이블 스캔이 발생하는 쿼리를 인덱스 스캔으로 변경하면, 성능이 크게 개선될 수 있다. + - 인덱스 현황 체크 + - **실무 케이스**: 사용되지 않는 인덱스를 제거하고, 자주 사용되는 컬럼에 새로운 인덱스를 추가함으로써, 데이터베이스 성능을 최적화할 수 있다. + - 데이터 변경 추이 + - **실무 케이스**: 데이터의 삽입, 갱신, 삭제 빈도를 분석하여, 빈번한 데이터 변경이 발생하는 테이블에 적절한 인덱스 전략을 적용할 수 있다. + - 업무적 특징 + - **실무 케이스**: 특정 시간대에 조회가 집중되는 경우, 그 시간대에 맞춘 캐싱 전략을 도입하여 성능을 개선할 수 있다. + +### 3. 튜닝 방향 판단 & 개선 적용 + +## 4.2 SQL 단순 수정으로 좋은 쿼리문을 만드는 예시. + +- 사용하지 않는 구문이나 불필요한 구문이 있는지 확인하고 SQL 튜닝을 수행할 것. + +### 예시 1. SubString 은 인덱싱을 사용할 수 없다. + +사원 번호가 1100 으로 시작하면서 사원 번호가 5자리인 사원사원의 정보 모두 출력하는 쿼리. + +```sql +SELECT * +FROM 사원 +WHERE + SUBSTRING(사원번호, 1, 4) = 1100 + AND + LENGTH(사원번호) = 5; +``` + +1. SQL 문 실행 결과 & 현황 파악 + + 해당 쿼리의 실행결과 + + ```sql + mysql> SELECT * FROM 사원 WHERE SUBSTRING(사원번호,1,4) = 1100 AND LENGTH(사원 + 번호) = 5; + +--------------+--------------+-------------+-------------+--------+--------------+ + | 사원번호 | 생년월일 | 이름 | 성 | 성별 | 입사일자 | + +--------------+--------------+-------------+-------------+--------+--------------+ + | 11000 | 1960-09-12 | Alain | Bonifati | M | 1988-08-20 | + | 11001 | 1956-04-16 | Baziley | Buchter | F | 1987-02-23 | + | 11002 | 1952-02-26 | Bluma | Ulupinar | M | 1996-12-23 | + | 11003 | 1960-11-13 | Mariangiola | Gulla | M | 1987-05-24 | + | 11004 | 1954-08-05 | JoAnna | Decleir | F | 1992-01-19 | + | 11005 | 1958-03-12 | Byong | Douceur | F | 1986-07-27 | + | 11006 | 1962-12-26 | Christoper | Butterworth | F | 1989-08-02 | + | 11007 | 1962-03-16 | Olivera | Maccarone | M | 1991-04-11 | + | 11008 | 1962-07-11 | Gennady | Menhoudj | M | 1988-09-18 | + | 11009 | 1954-08-30 | Alper | Axelband | F | 1986-09-09 | + +--------------+--------------+-------------+-------------+--------+--------------+ + 10 rows in set (0.16 sec) + ``` + + 10건의 데이터를 확인할 수 있고 0.16초의 시간이 걸림을 확인할 수 있다. + + 이제 실행 계획을 체크해보자 + + ```sql + mysql> EXPLAIN SELECT * FROM 사원 WHERE SUBSTRING(사원번호,1,4) = 1100 AND LENG + TH(사원번호) = 5; + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ + | 1 | SIMPLE | 사원 | NULL | ALL | NULL | NULL | NULL | NULL | 299069 | 100.00 | Using where | + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ + 1 row in set, 1 warning (0.00 sec) + ``` + + `Type` 속성이 `ALL` 이므로 풀스캔 방식임. ← 인덱스를 사용하지 않고 바로 접근한다는 뜻이다. + +2. SQL 쿼리의 구성 요소 체크 + + 사원 테이블에 총 몇건의 데이터가 있는지 체크한다. + + ```sql + SELECT COUNT(1) FROM 사원; + +----------+ + | COUNT(1) | + +----------+ + | 300024 | + +----------+ + 1 row in set (0.02 sec) + ``` + + 약 30만건의 데이터가 있음을 알 수 있다. + + 다음으로 해당 테이블의 인덱스가 어떻게 구성되어있는지 체크한다. + + ```sql + SHOW INDEX FROM 사원; + +--------+------------+----------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ + | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression | + +--------+------------+----------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ + | 사원 | 0 | PRIMARY | 1 | 사원번호 | A | 299069 | NULL | NULL | | BTREE | | | YES | NULL | + | 사원 | 1 | I_입사일자 | 1 | 입사일자 | A | 4365 | NULL | NULL | | BTREE | | | YES | NULL | + | 사원 | 1 | I_성별_성 | 1 | 성별 | A | 1 | NULL | NULL | | BTREE | | | YES | NULL | + | 사원 | 1 | I_성별_성 | 2 | 성 | A | 3052 | NULL | NULL | | BTREE | | | YES | NULL | + +--------+------------+----------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ + 4 rows in set (0.01 sec) + ``` + + 각 테이블의 인덱스를 보면 사원번호가 `UI`(*Unique Index*)임을 확인할 수 있다. + + 그런데 왜 기본키로 접근하지 못하고 풀스캔을 했을까? + + 이유인 즉슨, `SUBSTRING()` 와 `LENGTH()` 를 사용하여 사원번호 값들을 가공했기에 기본키 검색이 아니라 풀스캔을 했던 것이다. + +3. 튜닝 방향 판단 & 개선 적용 + + 그렇다면 기본키 검색이 가능하도록 하려면 어떡할까? + + 5자리면서 1100으로 시작하는 사원 번호를 찾는 것이므로 `BETWEEN` 구문 사용 혹은 비교 연산자를 사용하면 범위 검색이 되어 시간을 줄일 수 있을 것이다. + + **튜닝 후 결과** + + ```sql + SELECT * + FROM 사원 + WHERE 사원번호 BETWEEN 11000 AND 11009; + +--------------+--------------+-------------+-------------+--------+--------------+ + | 사원번호 | 생년월일 | 이름 | 성 | 성별 | 입사일자 | + +--------------+--------------+-------------+-------------+--------+--------------+ + | 11000 | 1960-09-12 | Alain | Bonifati | M | 1988-08-20 | + | 11001 | 1956-04-16 | Baziley | Buchter | F | 1987-02-23 | + | 11002 | 1952-02-26 | Bluma | Ulupinar | M | 1996-12-23 | + | 11003 | 1960-11-13 | Mariangiola | Gulla | M | 1987-05-24 | + | 11004 | 1954-08-05 | JoAnna | Decleir | F | 1992-01-19 | + | 11005 | 1958-03-12 | Byong | Douceur | F | 1986-07-27 | + | 11006 | 1962-12-26 | Christoper | Butterworth | F | 1989-08-02 | + | 11007 | 1962-03-16 | Olivera | Maccarone | M | 1991-04-11 | + | 11008 | 1962-07-11 | Gennady | Menhoudj | M | 1988-09-18 | + | 11009 | 1954-08-30 | Alper | Axelband | F | 1986-09-09 | + +--------------+--------------+-------------+-------------+--------+--------------+ + 10 rows in set (0.00 sec) + ``` + + 기존 0.16 에서 0.00sec으로 수행 시간이 줄었음을 알 수 있다. + + **튜닝 후 실행 계획** + + ```sql + EXPLAIN + SELECT * + FROM 사원 + WHERE 사원번호 BETWEEN 11000 AND 11009; + +----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+ + | 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 | + +----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+ + 1 row in set, 1 warning (0.00 sec) + + -- 기존 실행계획과 비교 + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ + | 1 | SIMPLE | 사원 | NULL | ALL | NULL | NULL | NULL | NULL | 299069 | 100.00 | Using where | + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ + 1 row in set, 1 warning (0.00 sec) + ``` + + `type` 이 `range` 범위검색으로 변경됐다. + + `rows` 를 보면 299069 개를 탐색하는 기존과는 다르게 10개만 탐색한 것도 확인할 수 있다. + + +### 예시 2. IFNULL 사용시에 주의할 것. 임시테이블은 비용이 든다. + +사원 테이블에서 선별 기준으로 몇 명의 사원이 있는지 출력하는 쿼리. + +```sql +SELECT IFNULL(성별,'NO DATA') AS 성별, COUNT(1) 건수 +FROM 사원 +GROUP BY IFNULL(성별, 'NO DATA'); +``` + +1. SQL 문 실행 결과 & 현황 파악 + + ```sql + +--------+--------+ + | 성별 | 건수 | + +--------+--------+ + | M | 179973 | + | F | 120051 | + +--------+--------+ + 2 rows in set (0.39 sec) + ``` + + 2개의 행이 출력되었고 총 집계 건수는 30만건, 실행 속도는 0.39sec 임을 알 수 있음. + + ```sql + EXPLAIN + SELECT IFNULL(성별,'NO DATA') AS 성별, COUNT(1) 건수 + FROM 사원 + GROUP BY IFNULL(성별, 'NO DATA'); + + +----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+------------------------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+------------------------------+ + | 1 | SIMPLE | 사원 | NULL | index | I_성별_성 | I_성별_성 | 51 | NULL | 299069 | 100.00 | Using index; Using temporary | + +----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+------------------------------+ + 1 row in set, 1 warning (0.00 sec) + + ``` + + `type` 이 `index` 이므로 인덱스 풀 스캔 방식이며 `Extra` 항목에 `Using temporary` 가 있으므로 임시 테이블을 생성한다는 걸 알 수 있음. + + > 왜 임시 테이블이 생성될까? + > + > + > IFNULL() 값의 여부를 검사하기 위해서 임시테이블이 생성된다. + > +2. SQL 쿼리의 구성 요소 체크 + + ```sql + desc 사원; + + +--------------+---------------+------+-----+---------+-------+ + | Field | Type | Null | Key | Default | Extra | + +--------------+---------------+------+-----+---------+-------+ + | 사원번호 | int | NO | PRI | NULL | | + | 생년월일 | date | NO | | NULL | | + | 이름 | varchar(14) | NO | | NULL | | + | 성 | varchar(16) | NO | | NULL | | + | 성별 | enum('M','F') | NO | MUL | NULL | | + | 입사일자 | date | NO | MUL | NULL | | + +--------------+---------------+------+-----+---------+-------+ + 6 rows in set (0.00 sec) + ``` + + 성별의 경우 NOT NULL 속성이 적용됨을 알 수 있음. 그러므로 IFNULL 사용할 필요가 없음. + +3. 튜닝 방향 판단 & 개선 적용 + + ```sql + EXPLAIN + SELECT 성별, COUNT(1) 건수 + FROM 사원 + GROUP BY 성별; + + +--------+--------+ + | 성별 | 건수 | + +--------+--------+ + | M | 179973 | + | F | 120051 | + +--------+--------+ + 2 rows in set (0.08 sec) + ``` + + 0.39sec → 0.08 sec + + ```sql + mysql> EXPLAIN + -> SELECT 성별, COUNT(1) 건수 + -> FROM 사원 + -> GROUP BY 성별; + + +----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+-------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+-------------+ + | 1 | SIMPLE | 사원 | NULL | index | I_성별_성 | I_성별_성 | 51 | NULL | 299069 | 100.00 | Using index | + +----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+-------------+ + 1 row in set, 1 warning (0.00 sec) + + -- 개선 전. + +----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+------------------------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+------------------------------+ + | 1 | SIMPLE | 사원 | NULL | index | I_성별_성 | I_성별_성 | 51 | NULL | 299069 | 100.00 | Using index; Using temporary | + +----+-------------+--------+------------+-------+---------------+--------------+---------+------+--------+----------+------------------------------+ + 1 row in set, 1 warning (0.00 sec) + ``` + + 임시 테이블을 사용하지 않는 것으로도 실행 속도를 줄 일 수 있다. + + +### 예시 3. 형변환할 시에는 인덱스를 활용할 수 없다. + +급여 테이블에서 현재 유효한 급여 정보만 조회하고자 하는 경우. + +1. SQL 문 실행 결과 & 현황 파악 + + ```sql + SELECT COUNT(1) + FROM 급여 + WHERE 사용여부 = 1; + + +----------+ + | COUNT(1) | + +----------+ + | 42842 | + +----------+ + 1 row in set (0.42 sec) + ``` + + 약 4만건의 데이터, 0.42 sec 실행속도. + + ```sql + -- 실행 계획 + EXPLAIN + SELECT COUNT(1) + FROM 급여 + WHERE 사용여부 = 1; + + +----+-------------+--------+------------+-------+----------------+----------------+---------+------+---------+----------+--------------------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+-------+----------------+----------------+---------+------+---------+----------+--------------------------+ + | 1 | SIMPLE | 급여 | NULL | index | I_사용여부 | I_사용여부 | 4 | NULL | 2838398 | 10.00 | Using where; Using index | + +----+-------------+--------+------------+-------+----------------+----------------+---------+------+---------+----------+--------------------------+ + 1 row in set, 3 warnings (0.01 sec) + ``` + + `type`을 보면 `index` 스캔을 사용했음을 알 수 있음. + + `rows` 와 `filtered` 를 보면 약 28만건의 데이터를 탐색하여 10% 의 데이터를 필터링하여 최종 데이터값을 산출했음을 알 수 있다 + +2. SQL 쿼리의 구성 요소 체크 + + 먼저 다음과 같은 SQL문을 실행하여 조건절로 작성된 사용 여부 열의 데이터 건수 확인. + + ```sql + SELECT 사용여부, COUNT(1) + FROM 급여 + GROUP BY 사용여부; + + +--------------+----------+ + | 사용여부 | COUNT(1) | + +--------------+----------+ + | 0 | 2801205 | + | 1 | 42842 | + +--------------+----------+ + 2 rows in set (0.38 sec) + ``` + + → 조회하고자 하는 쿼리에 사용된 사용 여부 열의 값이 1 인 데이터 건수는 10% 이하임을 알 수 있음. + + 급여 테이블에 인덱스 현황 확인. + + ```sql + SHOW INDEX FROM 급여; + + +--------+------------+----------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ + | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression | + +--------+------------+----------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ + | 급여 | 0 | PRIMARY | 1 | 사원번호 | A | 302984 | NULL | NULL | | BTREE | | | YES | NULL | + | 급여 | 0 | PRIMARY | 2 | 시작일자 | A | 2838398 | NULL | NULL | | BTREE | | | YES | NULL | + | 급여 | 1 | I_사용여부 | 1 | 사용여부 | A | 1 | NULL | NULL | YES | BTREE | | | YES | NULL | + +--------+------------+----------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ + 3 rows in set (0.01 sec) + ``` + + I_사용여부 인덱스가 있어 해당 인덱스 스캔만 사용하면 되나 인덱스 풀 스캔을 사용하고 있다는 것을 알 수 있음. + + 왜 인덱스 풀 스캔이 발생하는지 해당 명령어로 체크 + + ```sql + desc 급여; + + +--------------+---------+------+-----+---------+-------+ + | Field | Type | Null | Key | Default | Extra | + +--------------+---------+------+-----+---------+-------+ + | 사원번호 | int | NO | PRI | NULL | | + | 연봉 | int | NO | | NULL | | + | 시작일자 | date | NO | PRI | NULL | | + | 종료일자 | date | NO | | NULL | | + | 사용여부 | char(1) | YES | MUL | | | + +--------------+---------+------+-----+---------+-------+ + 5 rows in set (0.00 sec) + ``` + + 사용 여부가 `char(1)` 으로 문자형 데이터 유형이지만 해당 쿼리는 `WHERE 사원번호 = 1` 로 조회하기에 DBMS 내부에 **`묵시적 형 변환`**이 발생하였던 것이었음. + +3. 튜닝 방향 판단 & 개선 적용 + + ```sql + SELECT COUNT(1) + FROM 급여 + WHERE 사용여부 = '1'; + + +----------+ + | COUNT(1) | + +----------+ + | 42842 | + +----------+ + 1 row in set (0.02 sec) + ``` + + 묵시적 형 변환을 제거하여 I_사용여부 인덱스를 사용하게만듦. 실행 시간 0.42 sec → 0.02 sec + + **실행 계획 확인** + + ```sql + EXPLAIN + SELECT COUNT(1) + FROM 급여 + WHERE 사용여부 = '1'; + + -- 개선 후 + +----+-------------+--------+------------+------+----------------+----------------+---------+-------+-------+----------+-------------+ + | 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 row in set, 1 warning (0.00 sec) + + -- 개선 전. + +----+-------------+--------+------------+-------+----------------+----------------+---------+------+---------+----------+--------------------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+-------+----------------+----------------+---------+------+---------+----------+--------------------------+ + | 1 | SIMPLE | 급여 | NULL | index | I_사용여부 | I_사용여부 | 4 | NULL | 2838398 | 10.00 | Using where; Using index | + +----+-------------+--------+------------+-------+----------------+----------------+---------+------+---------+----------+--------------------------+ + 1 row in set, 3 warnings (0.01 sec) + ``` + + 개선 후 실행 계획을 보면 `type` 이 `index`(인덱스 풀 스캔) → `ref` (하나의 인덱스를 활용하여 1대 다 검색) 으로 바뀐 것을 확인할 수 있다. + + > **데이터 유형의 중요성** + > + > + > 데이터 유형에 맞게 열을 활용해야 내부적 형 변환이 발생하지 않는다! + > + > 내부적 형 변환이 발생할 경우 기껏 만들어놓은 인덱스를 활용하지 못하는 경우가 있으므로 주의! + > + +### 예시 4. 열을 결합할 경우 + +사원 테이블에서 성별의 값과 1칸의 공백. 성의 값을 모두 결합한 결과가 ‘***M Radwan***’ 인경우를 검색하는 쿼리 + +```sql +SELECT * +FROM 사원 +WHERE CONCAT(성별, ' ', 성) = 'M Radwan'; +``` + +1. SQL 문 실행 결과 & 현황 파악 + + ```sql + +--------------+--------------+-------------+--------+--------+--------------+ + | 사원번호 | 생년월일 | 이름 | 성 | 성별 | 입사일자 | + +--------------+--------------+-------------+--------+--------+--------------+ + | 10346 | 1963-01-29 | Aamod | Radwan | M | 1987-01-27 | + | 16491 | 1952-12-03 | Emdad | Radwan | M | 1988-05-28 | + | 18169 | 1954-09-21 | Nathalie | Radwan | M | 1998-03-04 | + ... 이하 생략. + | 491504 | 1954-12-27 | Zeydy | Radwan | M | 1998-03-08 | + | 498822 | 1955-06-15 | Boutros | Radwan | M | 1989-06-13 | + +--------------+--------------+-------------+--------+--------+--------------+ + 102 rows in set (0.13 sec) + ``` + + 102 건의 행이 검색되었고 수행 시간은 0.13 sec + + **실행 계획 확인** + + ```sql + EXPLAIN + SELECT * + FROM 사원 + WHERE CONCAT(성별, ' ', 성) = 'M Radwan'; + + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ + | 1 | SIMPLE | 사원 | NULL | ALL | NULL | NULL | NULL | NULL | 299069 | 100.00 | Using where | + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ + 1 row in set, 1 warning (0.00 sec) + ``` + + `type` 이 `ALL` 임, `Extra` 가 `Using where`→ WHERE 문에 따른 풀스캔이 실행되었음. + +2. SQL 쿼리의 구성 요소 체크 + + **조회하려는 전체 데이터 건수 체크** + + ```sql + SELECT '성별_성', COUNT(1) + FROM 사원 + WHERE CONCAT(성별, ' ', 성) = 'M Radwan' + + UNION ALL + + SELECT '전체 데이터', COUNT(1) + FROM 사원; + + +------------------+----------+ + | 성별_성 | COUNT(1) | + +------------------+----------+ + | 성별_성 | 102 | + | 전체 데이터 | 300024 | + +------------------+----------+ + 2 rows in set (0.10 sec) + ``` + + 30만건의 데이터 중 102 건의 데이터를 조회하려고 함을 알 수 있음. + + ```sql + desc 사원; + + +--------------+---------------+------+-----+---------+-------+ + | Field | Type | Null | Key | Default | Extra | + +--------------+---------------+------+-----+---------+-------+ + | 사원번호 | int | NO | PRI | NULL | | + | 생년월일 | date | NO | | NULL | | + | 이름 | varchar(14) | NO | | NULL | | + | 성 | varchar(16) | NO | | NULL | | + | 성별 | enum('M','F') | NO | MUL | NULL | | + | 입사일자 | date | NO | MUL | NULL | | + +--------------+---------------+------+-----+---------+-------+ + 6 rows in set (0.00 sec) + + SHOW INDEX FROM 사원; + + +--------+------------+----------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ + | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression | + +--------+------------+----------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ + | 사원 | 0 | PRIMARY | 1 | 사원번호 | A | 299069 | NULL | NULL | | BTREE | | | YES | NULL | + | 사원 | 1 | I_입사일자 | 1 | 입사일자 | A | 4365 | NULL | NULL | | BTREE | | | YES | NULL | + | 사원 | 1 | I_성별_성 | 1 | 성별 | A | 1 | NULL | NULL | | BTREE | | | YES | NULL | + | 사원 | 1 | I_성별_성 | 2 | 성 | A | 3052 | NULL | NULL | | BTREE | | | YES | NULL | + +--------+------------+----------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ + 4 rows in set (0.00 sec) + ``` + + 사원 테이블의 인덱스가 `I_성별_성` 인덱스가 존재함을 알 수 있음 + + 따라서 WHERE 절의 조건과 부합함을 알 수 있고 조건문도 동등조건이므로 인덱스를 활용하여 데이터를 빠르게 조회 가능 + +3. 튜닝 방향 판단 & 개선 적용 + + **개선 쿼리 실행 결과** + + ```sql + SELECT * + FROM 사원 + WHERE 성별 = 'M' + AND + 성 = 'Radwan'; + + +--------------+--------------+-------------+--------+--------+--------------+ + | 사원번호 | 생년월일 | 이름 | 성 | 성별 | 입사일자 | + +--------------+--------------+-------------+--------+--------+--------------+ + | 10346 | 1963-01-29 | Aamod | Radwan | M | 1987-01-27 | + | 16491 | 1952-12-03 | Emdad | Radwan | M | 1988-05-28 | + | 18169 | 1954-09-21 | Nathalie | Radwan | M | 1998-03-04 | + ... 이하 생략. + | 491504 | 1954-12-27 | Zeydy | Radwan | M | 1998-03-08 | + | 498822 | 1955-06-15 | Boutros | Radwan | M | 1989-06-13 | + +--------------+--------------+-------------+--------+--------+--------------+ + 102 rows in set (0.00 sec) + ``` + + 실행 시간이 0.13 sec → 0.00sec 으로 줄어듦 + + ```sql + EXPLAIN + SELECT * + FROM 사원 + WHERE 성별 = 'M' + AND + 성 = 'Radwan'; + + +----+-------------+--------+------------+------+---------------+--------------+---------+-------------+------+----------+-------+ + | 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 | + +----+-------------+--------+------------+------+---------------+--------------+---------+-------------+------+----------+-------+ + 1 row in set, 1 warning (0.00 sec) + -- 개선 전 + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ + | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ + | 1 | SIMPLE | 사원 | NULL | ALL | NULL | NULL | NULL | NULL | 299069 | 100.00 | Using where | + +----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+ + 1 row in set, 1 warning (0.00 sec) + ``` + + `type` 이 `ref` 로 I_성별_성 인덱스를 사용해서 사원 테이블에 접근함을 알 수 있음. + + 그 결과 30만건의 데이터에 접근해야 했던 쿼리가 102건의 데이터만 조회하면 되도록 개선됨. \ No newline at end of file