diff --git a/.gitignore b/.gitignore index 25da6e6..f6b6248 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -.obsidian \ No newline at end of file +.obsidian/ diff --git "a/JongminJeon/10. \354\213\244\355\226\211 \352\263\204\355\232\215.md" "b/JongminJeon/10. \354\213\244\355\226\211 \352\263\204\355\232\215.md" new file mode 100644 index 0000000..13c22b8 --- /dev/null +++ "b/JongminJeon/10. \354\213\244\355\226\211 \352\263\204\355\232\215.md" @@ -0,0 +1,1433 @@ + +- 대부분의 DBMS는 많은 데이터를 안전하게 저장 및 관리하고 사용자가 원하는 데이터를 빠르게 조회할 수 있게 해주는 것이 주목적 +- 이러한 목적을 달성하려면 옵티마이저가 사용자의 쿼리를 최적으로 처리될 수 있게 하는 쿼리의 실행 계획을 수립할 수 있어야 함 +- 하지만 옵티마이저가 관리자나 사용자의 개입 없이 항상 좋은 실행 계획을 만들어낼 수 있는 것은 아님 +- DBMS 서버는 이러한 문제점을 사용자가 보완할 수 있도록 explain 명령으로 옵티마이저가 수립한 실행 계획을 확인할 수 있게 해줌 + +요약: +- 데이터를 빠르게 조회하기 위해 옵티마이저가 실행 계획을 최적화 하지만 항상 완벽하지는 않다. +- 따라서 Explain 명령으로 사용자에게 실행 계획을 보여주고 보완할 수 있게 해준다. + + +## 10. 1. 통계 정보 + +- MySQL 서버는 5.7 버전까지 테이블과 인덱스에 대한 개괄적인 정보를 가지고 실행 계획을 수립 +- MySQL 8.0 버전부터는 인덱스 되지 않은 컬럼들에 대해서도 데이터 분포도를 수집해서 저장하는 히스토그램 정보가 도입 + + +### 10.1.1. 테이블 및 인덱스 통계 정보 + +- 비용 기반 최적화에서 가장 중요한 것은 통계 정보 +- 통계 정보가 정확하지 않다면 엉뚱향 방향으로 쿼리를 실행할 수 있음 + - 예시: 1억 건의 레코드가 저장된 테이블의 통계정보가 갱신되지 않아서 레코드가 10건 미만으로 되어있다면 옵티마이저는 실제 쿼리를 실행할 때 `인덱스 레인지 스캔`이 아니라 `테이블 풀 스캔으로 실행`해 버릴 수 있음. + +#### 10.1.1.1. MySQL 서버의 통계 정보 + +- MySQL 5.6 버전부터 InnoDB 스토리지 엔진을 사용하는 테이블에 대한 통계 정보를 영구적으로 관리할 수 있게 개선 되었음 (5.5 버전까지는 각 테이블의 통계정보가 메모리에서만 관리되어 서버가 재시작 될 경우 모두 사라졌음) + +```sql +select * +from mysql.innodb_table_stats +where database_name = 'employees' +order by n_rows desc; +``` + +![alt](./src/img/10.%20통계정보테이블예시.png) +```text +■ innodb_table_stats.n_rows: 테이블의 전체 레코드 건수 +■ innodb_table_stats.clustered_index_size: 프라이머리 키의 크기(InnoDB 페이지 개수) +■ innodb_table_stats.sum_of_other_index_sizes: 프라이머리 키를 제외한 인덱스의 크기(InnoDB 페이지 개수) +``` + +```sql +mysql> +SELECT * + FROM innodb_index_stats +WHERE database_name='employees' + AND TABLE_NAME='employees'; ++--------------+--------------+------------+-------------+-----------------------------------+ +| index_name | stat_name | stat_value | sample_size | stat_description | ++--------------+--------------+------------+-------------+-----------------------------------+ +| PRIMARY | n_diff_pfx01 | 299202 | 20 | emp_no | +| PRIMARY | n_leaf_pages | 886 | NULL | Number of leaf pages in the index | +| PRIMARY | size | 929 | NULL | Number of pages in the index | +| ix_firstname | n_diff_pfx01 | 1313 | 20 | first_name | +| ix_firstname | n_diff_pfx02 | 294090 | 20 | first_name,emp_no | +| ix_firstname | n_leaf_pages | 309 | NULL | Number of leaf pages in the index | +| ix_firstname | size | 353 | NULL | Number of pages in the index | +| ix_hiredate | n_diff_pfx01 | 5128 | 20 | hire_date | +| ix_hiredate | n_diff_pfx02 | 300069 | 20 | hire_date,emp_no | +| ix_hiredate | n_leaf_pages | 231 | NULL | Number of leaf pages in the index | +| ix_hiredate | size | 289 | NULL | Number of pages in the index | ++--------------+--------------+------------+-------------+-----------------------------------+ +``` + +```text +■ innodb_index_stats.stat_name='n_diff_pfx%': 인덱스가 가진 유니크한 값의 개수 +■ innodb_index_stats.stat_name='n_leaf_pages': 인덱스의 리프 노드 페이지 개수 +■ innodb_index_stats.stat_name='size': 인덱스 트리의 전체 페이지 개수 +``` + + +통계 정보 갱신 이벤트 종류 및 설정 + +``` +MySQL 5.5. 버전까지의 통계 정보 수집 이벤트 종류 +■ 테이블이 새로 오픈되는 경우 +■ 테이블의 레코드가 대량으로 변경되는 경우(테이블의 전체 레코드 중에서 1/16 정도의 UPDATE 또는 INSERT나 DELETE가 실행되는 경우) +■ ANALYZE TABLE 명령이 실행되는 경우 +■ SHOW TABLE STATUS 명령이나 SHOW INDEX FROM 명령이 실행되는 경우 +■ InnoDB 모니터가 활성화되는 경우 +■ innodb_stats_on_metadata 시스템 설정이 ON인 상태에서 SHOW TABLE STATUS 명령이 실행되는 경우 +``` + +테이블 생성시 STAT_AUTO_RECALC 옵션을 이용해 테이블 단위로 통계 정보 갱신 이벤트를 설정할 수 있음 + +``` +■ STATS_AUTO_RECALC=1: 테이블의 통계 정보를 MySQL 5.5 이전의 방식대로 자동 수집한다. +■ STATS_AUTO_RECALC=0: 테이블의 통계 정보는 ANALYZE TABLE 명령을 실행할 때만 수집된다. +■ STATS_AUTO_RECALC=DEFAULT: 테이블을 생성할 때 별도로 STATS_AUTO_RECALC 옵션을 설정하지 않은 것과 동일하며, 테이블의 통계 정보 수집을 innodb_stats_auto_recalc 시스템 설정 변수의 값으로 결정한다. +``` + + +통계 정보 갱신 시 샘플링할 페이지수(MySQL 데이터 저장 단위) 설정 +``` +■ innodb_stats_transient_sample_pages +이 시스템 변수의 기본값은 8인데, 이는 자동으로 통계 정보 수집이 실행될 때 8개 페이지만 임의로 샘플링해서 분석하고 그 결과를 통계 정보로 활용함을 의미한다. +■ innodb_stats_persistent_sample_pages +기본값은 20이며, ANALYZE TABLE 명령이 실행되면 임의로 20개 페이지만 샘플링해서 분석하고 그 결과를 영구적인 통계 정보 테이블에 저장하고 활용함을 의미한다. +``` + +```sql +show variables like '%sample_pages%'; + +innodb_stats_persistent_sample_pages,20 +innodb_stats_transient_sample_pages,8 +``` + + +### 10.1.2. 히스토그램 + +- 5.7 버전까지의 통계 정보는 단순히 인덱스된 컬럼의 유니크한 값의 개수 정도만 가지고 있었고 이는 옵티마이저가 최적의 실행 계획을 수립하기에는 부족했음 +- 그래서 이러한 부족함을 보완하기 위해 옵티마이저는 실행 계획을 수립할 때 실제 인덱스의 일부 페이지를 랜덤으로 가져와 참조하는 방식을 사용 했었음 (실제 데이터 일부 샘플링) +- 8.0 버전으로 업그레이드 되면서 MySQL 서버도 컬럼의 데이터 분포를 참조할 수 있는 히스토그램 도입 + + +#### 10.1.2.1. 히스토그램 정보 수집 및 삭제 + +- 히스토그램 정보는 컬럼단위로 관리되는데 이는 자동으로 수집되지 않고 별도 수집이 필요함 + + +2가지 히스토그램 + +``` + +-- 1. hire_date: 높이 균형 히스토그램으로 생성 +-- 컬럼값의 범위를 균등한 개수로 구분해서 관리하는 히스토그램 + +mysql> ANALYZE TABLE employees.employees + UPDATE HISTOGRAM ON gender, hire_date; + +mysql> SELECT * + FROM COLUMN_STATISTICS + WHERE SCHEMA_NAME='employees' + AND TABLE_NAME='employees' +*************************** 1. row *************************** +SCHEMA_NAME: employees + TABLE_NAME: employees +COLUMN_NAME: gender + HISTOGRAM: {"buckets": [ + [1, 0.5998529796789721], + [2, 1.0] + ], + "data-type": "enum", + "null-values": 0.0, + "collation-id": 45, + "last-updated": "2020-08-03 03:47:45.739242", + "sampling-rate": 0.3477368727939573, + "histogram-type": "singleton", + "number-of-buckets-specified": 100 + } +*************************** 2. row *************************** +SCHEMA_NAME: employees + TABLE_NAME: employees +COLUMN_NAME: hire_date + HISTOGRAM: {"buckets": [ + ["1985-02-01", "1985-02-28", 0.009838277646869273, 28], + ["1985-03-01", "1985-03-28", 0.020159909773830382, 28], + ["1985-03-29", "1985-04-26", 0.030159305580730267, 29], + ["1985-04-27", "1985-05-24", 0.03999758322759954, 28], + ... + ["1997-01-24", "1997-06-25", 0.9700118824643023, 153], + ["1997-06-26", "1997-12-15", 0.980021348156204, 172], + ["1997-12-16", "1998-08-06", 0.9900006041931001, 233], + ["1998-08-07", "2000-01-06", 1.0, 420] + ], + "data-type": "date", + "null-values": 0.0, + "collation-id": 8, + "last-updated": "2020-08-03 03:47:45.742159", + "sampling-rate": 0.3477368727939573, + "histogram-type": "equi-height", + "number-of-buckets-specified": 100 + } + + + +-- 2. gender: 싱글톤 히스토그램으로 생성 +-- 컬럼값 개별로 레코드 건수를 관리하는 히스토그램 + +ANALYZE TABLE employees.employees + UPDATE HISTOGRAM ON gender; + +select * +from information_schema.COLUMN_STATISTICS; +where column_name='gender' + +{ +"buckets": [ +[1, 0.5998620110391168], +[2, 1.0]], +"data-type": "enum", +"null-values": 0.0, +"collation-id": 45, +"last-updated": "2023-07-25 10:20:51.267018", +"sampling-rate": 1.0, +"histogram-type": "singleton", +"number-of-buckets-specified": 100} +``` + +![alt](./src/img/10.2.%20높이%20균형%20히스토그램.png) + +```text +■ sampling-rate: 히스토그램 정보를 수집하기 위해 스캔한 페이지의 비율을 저장한다. 샘플링 비율이 0.35라면 전체 데이터 페이지의 35%를 스캔해서 이 정보가 수집됐다는 것을 의미한다. 물론 샘플링 비율이 높아질수록 더 정확한 히스토그램이 되겠지만, 테이블을 전부 스캔하는 것은 부하가 높으며 시스템의 자원을 많이 소모한다. 그래서 MySQL 서버는 histogram_generation_max_mem_size 시스템 변수에 설정된 메모리 크기에 맞게 적절히 샘플링한다. histogram_generation_max_mem_size 시스템 변수의 메모리 크기는 20MB로 초기화돼 있다. +■ histogram-type: 히스토그램의 종류를 저장한다. +■ number-of-buckets-specified: 히스토그램을 생성할 때 설정했던 버킷의 개수를 저장한다. 히스토그램을 생성할 때 별도로 버킷의 개수를 지정하지 않았다면 기본으로 100개의 버킷이 사용된다. 버킷은 최대 1024개를 설정할 수 있지만, 일반적으로 100개의 버킷이면 충분한 것으로 알려져 있다 +``` + +- 주의사항 + - MySQL 8.0.19 미만의 버전은 히스토그램 생성시 풀 스캔으로 데이터 페이지를 스캔해서 히스토그램을 생성했었음 (서버에 부하를 주고 오래 걸림) + - MySQL 8.0.19 버전 부터는 InnoDB 스토리지 엔진 자체적으로 샘플링 엔진 구현 + + + +#### 10.1.2.2. 히스토그램의 용도 + +히스토그램의 도입으로 데이터의 분포까지 고려해서 실행계획을 수립할 수 있게 되었음 + +히스토그램 정보가 없으면 옵티마이저는 아래와 같이 데이터가 균등하게 분포돼 있을 것으로 예측 +- 테이블의 레코드가 1000건 이고 어떤 컬럼의 유니크한 값의 개수가 100개였다면 아래 동등 비교 검색 결과는 대략 10개 일 것이라고 예측함 +```sql +mysql> SELECT * FROM order WHERE user_id='matt.lee'; +``` + + +``` +-- 1. 히스토그램 생성 전 +EXPLAIN +SELECT * +FROM employees +WHERE first_name='Zita' +AND birth_date BETWEEN '1950-01-01' AND '1960-01-01'; + ++----+-------------+-----------+------+--------------+------+----------+ +| id | select_type | table | type | key | rows | filtered | ++----+-------------+-----------+------+--------------+------+----------+ +| 1 | SIMPLE | employees | ref | ix_firstname | 224 | 11.11 | ++----+-------------+-----------+------+--------------+------+----------+ + +-- 예상 레코드 수: 224 * 0.1111 = 25 개 + + + +-- 2. 히스토그램 생성 후 +ANALYZE TABLE employees +UPDATE histogram ON first_name, birth_date; + +EXPLAIN +SELECT * +FROM employees +WHERE first_name='Zita' +AND birth_date BETWEEN '1950-01-01' AND '1960-01-01'; ++----+-------------+-----------+------+--------------+------+----------+ +| id | select_type | table | type | key | rows | filtered | ++----+-------------+-----------+------+--------------+------+----------+ +| 1 | SIMPLE | employees | ref | ix_firstname | 224 | 60.82 | ++----+-------------+-----------+------+--------------+------+----------+ + +-- 예상 레코드 수: 224 * 0.6082 = 136 개 + + +-- 3. 실제 레코드 수: 143개 +SELECT count(*) +FROM employees +WHERE first_name='Zita' +AND birth_date BETWEEN '1950-01-01' AND '1960-01-01'; + +``` +분포를 알면 유용할 상황 +- orders 테이블의 status 컬럼 + - 1000개 레코드 실제 분포 + - 700개 '완료' + - 150개 '대기' + - 100개 '배송중' + - 50개 '취소' + - 히스토그램이 없다면 모두 250개 일 것이라고 가정 + + +조인을 할 때 옵티마이저는 일반적으로 결과 집합이 적은 테이블을 선두 테이블로 설정 +- 각 컬럼에 대해 히스토그램 정보가 있으면 어느 테이블을 먼저 읽어야 조인의 횟수를 줄일 수 있을지 옵티마이저가 더 정확히 판단할 수 있음 +``` +-- 1. employees, salaries 테이블 순으로 조인 +explain analyze +SELECT /*+ JOIN_ORDER(e, s) */ * + FROM salaries s + INNER JOIN employees e ON e.emp_no=s.emp_no + AND e.birth_date BETWEEN '1950-01-01' AND '1950-02-01' + WHERE s.salary BETWEEN 40000 AND 70000; + +-> Nested loop inner join (cost=30630 rows=1429) (actual time=149..149 rows=0 loops=1) + -> Filter: (e.birth_date between '1950-01-01' and '1950-02-01') (cost=30177 rows=300) (actual time=149..149 rows=0 loops=1) + -> Table scan on e (cost=30177 rows=299612) (actual time=0.0888..93.1 rows=300024 loops=1) + -> Filter: (s.salary between 40000 and 70000) (cost=0.56 rows=4.77) (never executed) + -> Index lookup on s using PRIMARY (emp_no=e.emp_no) (cost=0.56 rows=9.54) (never executed) + +-- 2. salaries, employees 테이블 순으로 조인 +explain analyze +SELECT /*+ JOIN_ORDER(s, e) */ * + FROM salaries s + INNER JOIN employees e ON e.emp_no=s.emp_no + AND e.birth_date BETWEEN '1950-01-01' AND '1950-02-01' + WHERE s.salary BETWEEN 40000 AND 70000; + +-> Nested loop inner join (cost=783953 rows=70961) (actual time=1489..1489 rows=0 loops=1) + -> Filter: (s.salary between 40000 and 70000) (cost=287228 rows=1.42e+6) (actual time=0.124..540 rows=1.92e+6 loops=1) + -> Table scan on s (cost=287228 rows=2.84e+6) (actual time=0.122..434 rows=2.84e+6 loops=1) + -> Filter: (e.birth_date between '1950-01-01' and '1950-02-01') (cost=0.25 rows=0.05) (actual time=437e-6..437e-6 rows=0 loops=1.92e+6) + -> Single-row index lookup on e using PRIMARY (emp_no=s.emp_no) (cost=0.25 rows=1) (actual time=189e-6..208e-6 rows=1 loops=1.92e+6) +``` + + + +#### 10.1.2.3. 히스토그램과 인덱스 + +- 히스토그램과 인덱스는 완전히 다른 객체지만 MySQL 서버에서 인덱스는 부족한 통계 정보를 수집하기 위해 사용된다는 측면에서 어느 정도 공통점을 가진다고 볼 수 있음 +- 실행 계획을 수립할 때 사용 가능한 인덱스들로부터 조건절에 일치하는 레코드 건수를 대략적으로 파악해서 최종적으로 가장 나은 실행 계획 선택 +- 인덱스 다이브(Index Dive): 조건절에 일치하는 레코드 건수를 예측하기 위해 실제 인덱스의 B-Tree를 샘플링해서 살펴보는 것 + +```sql +-- first_name: 인덱스 +-- birth_date: 인덱스 없음 +mysql> SELECT * + FROM employees + WHERE first_name='Tonny' + AND birth_date BETWEEN '1954-01-01' AND '1955-01-01'; +``` + +- 인덱스 된 컬럼을 검색 조건으로 사용하는 경우 그 컬럼의 히스토그램은 사용하지 않고 실제 인덱스 다이브를 통해 직접 수집한 정보를 활용함 + + +### 10.1.3. 코스트 모델 (Cost Model) + + +MySQL 서버가 쿼리를 처리할 때 필요한 작업 들 +```text +■ 디스크로부터 데이터 페이지 읽기 +■ 메모리(InnoDB 버퍼 풀)로부터 데이터 페이지 읽기 +■ 인덱스 키 비교 +■ 레코드 평가 +■ 메모리 임시 테이블 작업 +■ 디스크 임시 테이블 작업 +``` + +- 위와 같이 전체 쿼리 비용을 계산하는데 필요한 단위 작업들의 비용을 코스트 모델이라고 함. +- MySQL 5.7 이전 버전까지는 단위 작업들의 비용이 MySQL 서버 소스 코드에 상수화 되어 있었음 +- 5.7 버전 이후로는 작업의 비용을 사용자가 조정할 수 있음 + + +```sql +-- 1. server_cost: 인덱스를 찾고 레코드를 비교하고 임시 테이블 처리에 대한 비용 관리 +select * from mysql.server_cost; +-- 2. engine_cost: 레코드를 가진 데이터 페이지를 가져오는 데 필요한 비용 관리 +select * from mysql.engine_cost; +``` + +위 테이블의 컬럼들 의미 + +```test +■ cost_name: 코스트 모델의 각 단위 작업 +■ default_value: 각 단위 작업의 비용(기본값이며, 이 값은 MySQL 서버 소스 코드에 설정된 값) +■ cost_value: DBMS 관리자가 설정한 값(이 값이 NULL이면 MySQL 서버는 default_value 칼럼의 비용 사용) +■ last_updated: 단위 작업의 비용이 변경된 시점 +■ comment: 비용에 대한 추가 설명 +engine_cost 테이블은 위의 5개 칼럼에 추가로 다음 2개 칼럼을 더 가지고 있다. +■ engine_name: 비용이 적용된 스토리지 엔진 +■ device_type: 디스크 타입 +``` + +![alt](./src/img/10.%20코스트%20모델%20테이블.png) + +코스트 모델에서 중요한 것은 각 단위 작업에 설정되는 비용 값이 커지면 어떤 실행 계획들이 고비용으로 바뀌고 어떤 실행 계획들이 저비용으로 바뀌는지 파악하는 것 + +<각 단위 작업의 비용이 변경되면 예상할 수 있는 대략적인 결과들> + +``` +■ key_compare_cost 비용을 높이면 MySQL 서버 옵티마이저가 가능하면 정렬을 수행하지 않는 방향의 실행 계획을 선택할 가능성이 높아진다. + +■ row_evaluate_cost 비용을 높이면 풀 스캔을 실행하는 쿼리들의 비용이 높아지고, MySQL 서버 옵티마이저는 가능하면 인덱스 레인지 스캔을 사용하는 실행 계획을 선택할 가능성이 높아진다. + +■ disk_temptable_create_cost와 disk_temptable_row_cost 비용을 높이면 MySQL 옵티마이저는 디스크에 임시 테이블을 만들지 않는 방향의 실행 계획을 선택할 가능성이 높아진다. + +■ memory_temptable_create_cost와 memory_temptable_row_cost 비용을 높이면 MySQL 서버 옵티마이저는 메모리 임시 테이블을 만들지 않는 방향의 실행 계획을 선택할 가능성이 높아진다. + +■ io_block_read_cost 비용이 높아지면 MySQL 서버 옵티마이저는 가능하면 InnoDB 버퍼 풀에 데이터 페이지가 많이 적재돼 있는 인덱스를 사용하는 실행 계획을 선택할 가능성이 높아진다. + +■ memory_block_read_cost 비용이 높아지면 MySQL 서버는 InnoDB 버퍼 풀에 적재된 데이터 페이지가 상대적으로 적다고 하더라도 그 인덱스를 사용할 가능성이 높아진다 +``` + + +## 10.2. 실행 계획 확인 + +- 실행 계획의 출력 포맷 조정 (테이블, Tree, JSON) +- 실제 쿼리의 실행 결과 까지 확인 (explain analyze) + +### 10.2.1. 실행 계획 출력 포맷 + +실행 계획: 테이블 포맷 +```sql +mysql> EXPLAIN + SELECT * + FROM employees e + INNER JOIN salaries s ON s.emp_no=e.emp_no + WHERE first_name='ABC'; ++----+-------------+-------+------------+------+----------------------+--------------+---------+--------------------+------+----------+-------+ +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | ++----+-------------+-------+------------+------+----------------------+--------------+---------+--------------------+------+----------+-------+ +| 1 | SIMPLE | e | NULL | ref | PRIMARY,ix_firstname | ix_firstname | 58 | const | 1 | 100.00 | NULL | +| 1 | SIMPLE | s | NULL | ref | PRIMARY | PRIMARY | 4 | employees.e.emp_no | 10 | 100.00 | NULL | ++----+-------------+-------+------------+------+----------------------+--------------+---------+--------------------+------+----------+-------+ +``` + +실행 계획: 트리 포맷 +```sql +mysql> EXPLAIN FORMAT=TREE + SELECT * + FROM employees e + INNER JOIN salaries s ON s.emp_no=e.emp_no + WHERE first_name='ABC'\G +*************************** 1. row *************************** +EXPLAIN: -> Nested loop inner join (cost=2.40 rows=10) + -> Index lookup on e using ix_firstname (first_name='ABC') (cost=0.35 rows=1) + -> Index lookup on s using PRIMARY (emp_no=e.emp_no) (cost=2.05 rows=10) +``` + + +실행 계획: JSON 포맷 +```sql + SELECT * + FROM employees e + INNER JOIN salaries s ON s.emp_no=e.emp_no + WHERE first_name='ABC'\G +*************************** 1. row *************************** +EXPLAIN: { + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "2.40" + }, + "nested_loop": [ + { + "table": { + "table_name": "e", + "access_type": "ref", + "possible_keys": [ + "PRIMARY", + "ix_firstname" + ], + "key": "ix_firstname", + "used_key_parts": [ + "first_name" + ], +``` + + +#### 10.2.2. 쿼리의 실행 시간 확인 + +```sql +mysql> EXPLAIN ANALYZE + SELECT e.emp_no, avg(s.salary) + FROM employees e + INNER JOIN salaries s ON s.emp_no=e.emp_no + AND s.salary>50000 + AND s.from_date<='1990-01-01' + AND s.to_date>'1990-01-01' + WHERE e.first_name='Matt' + GROUP BY e.hire_date \G +A) -> Table scan on (actual time=0.001..0.004 rows=48 loops=1) +B) -> Aggregate using temporary table (actual time=3.799..3.808 rows=48 loops=1) +C) -> Nested loop inner join (cost=685.24 rows=135) + (actual time=0.367..3.602 rows=48 loops=1) +D) -> Index lookup on e using ix_firstname (first_name='Matt') (cost=215.08 rows=233) + (actual time=0.348..1.046 rows=233 loops=1) +E) -> Filter: ((s.salary > 50000) and (s.from_date <= DATE'1990-01-01') + and (s.to_date > DATE'1990-01-01')) (cost=0.98 rows=1) + (actual time=0.009..0.011 rows=0 loops=233) +F) -> Index lookup on s using PRIMARY (emp_no=e.emp_no) (cost=0.98 rows=10) + (actual time=0.007..0.009 rows=10 loops=233) +``` + +![[10.쿼리 실행시간 확인.png]] + +```text +■ actual time=0.007..0.009: employees 테이블에서 읽은 emp_no 값을 기준으로 salaries 테이블에서 일치하 는 레코드를 검색하는 데 걸린 시간(밀리초)을 의미한다. 이때 숫자 값이 2개가 표시되는데, 첫 번째 숫자 값은 첫 번째 레코드를 가져오는 데 걸린 평균 시간(밀리초)을 의미한다. 두 번째 숫자 값은 마지막 레코드를 가져오는 데 걸린 평균 시간(밀리초)을 의미한다. + +■ rows=10: employees 테이블에서 읽은 emp_no에 일치하는 salaries 테이블의 평균 레코드 건수를 의미한다 + +■ loops=233: employees 테이블에서 읽은 emp_no를 이용해 salaries 테이블의 레코드를 찾는 작업이 반복된 횟수 +를 의미한다. 결국 여기서는 employees 테이블에서 읽은 emp_no의 개수가 233개임을 의미한다. +``` + + +## 10.3. 실행 계획 분석 + +- 실행 계획의 출력 보다는 실행 계호기이 어떤 접근 방법을 사용해서 어떤 최적화를 수행하는지, 그리고 어떤 인덱스를 사용하는지 등을 이해하는 것이 더 중요하다. + +```sql +-- 출력된 실행 계획에서 위쪽에 출력된 결과일수록(id 컬럼의 값이 작을 수록) 쿼리의 바깥 부분이거나 먼저 접근한 테이블 +-- 아래쪽에 출력된 결과일 수록(id 컬럼의 값이 클수록) 쿼리의 안쪽 또는 나중에 접근한 테이블 + +mysql> EXPLAIN + SELECT * + FROM employees e + INNER JOIN salaries s ON s.emp_no=e.emp_no + WHERE first_name='ABC'; ++----+-------------+-------+------------+------+----------------------+--------------+---------+--------------------+------+----------+-------+ +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | ++----+-------------+-------+------------+------+----------------------+--------------+---------+--------------------+------+----------+-------+ +| 1 | SIMPLE | e | NULL | ref | PRIMARY,ix_firstname | ix_firstname | 58 | const | 1 | 100.00 | NULL | +| 1 | SIMPLE | s | NULL | ref | PRIMARY | PRIMARY | 4 | employees.e.emp_no | 10 | 100.00 | NULL | ++----+-------------+-------+------------+------+----------------------+--------------+---------+--------------------+------+----------+-------+ +``` + + +### 10.3.1. id 컬럼 + +- 실행 계획에서 가장 왼쪽에 표시되는 id 컬럼은 Select 쿼리 별로 부여되는 식별자 값 + - 반드시 테이블의 접근 순서를 의미하지는 않음 +- 여러개의 테이블이 조인되는 경우에는 id값이 증가하지 않고 같은 id 값이 부여 +```sql +mysql> EXPLAIN + SELECT e.emp_no, e.first_name, s.from_date, s.salary + FROM employees e, salaries s + WHERE e.emp_no=s.emp_no LIMIT 10; ++----+-------------+-------+-------+--------------+--------------------+--------+-------------+ +| id | select_type | table | type | key | ref | rows | Extra | ++----+-------------+-------+-------+--------------+--------------------+--------+-------------+ +| 1 | SIMPLE | e | index | ix_firstname | NULL | 300252 | Using index | +| 1 | SIMPLE | s | ref | PRIMARY | employees.e.emp_no | 10 | NULL | ++----+-------------+-------+-------+--------------+--------------------+--------+-------------+ +``` + +```sql +mysql> EXPLAIN + SELECT + ( (SELECT COUNT(*) FROM employees) + (SELECT COUNT(*) FROM departments) ) AS total_ +count; ++----+-------------+-------------+-------+-------------+------+--------+----------------+ +| id | select_type | table | type | key | ref | rows | Extra | ++----+-------------+-------------+-------+-------------+------+--------+----------------+ +| 1 | PRIMARY | NULL | NULL | NULL | NULL | NULL | No tables used | +| 3 | SUBQUERY | departments | index | ux_deptname | NULL | 9 | Using index | +| 2 | SUBQUERY | employees | index | ix_hiredate | NULL | 300252 | Using index | ++----+-------------+-------------+-------+-------------+------+--------+----------------+ +``` + +```sql +mysql> EXPLAIN FORMAT=TREE + SELECT * FROM dept_emp de + WHERE de.emp_no= (SELECT e.emp_no + FROM employees e + WHERE e.first_name='Georgi' + AND e.last_name='Facello' LIMIT 1); ++----+-------------+-------+------+-------------------+------+-------------+ +| id | select_type | table | type | key | rows | Extra | ++----+-------------+-------+------+-------------------+------+-------------+ +| 1 | PRIMARY | de | ref | ix_empno_fromdate | 1 | Using where | +| 2 | SUBQUERY | e | ref | ix_firstname | 253 | Using where | ++----+-------------+-------+------+-------------------+------+-------------+ + +-> Filter: (de.emp_no = (select #2)) (cost=0.35 rows=1) (actual time=0.0259..0.0285 rows=1 loops=1) + -> Index lookup on de using ix_empno_fromdate (emp_no=(select #2)) (cost=0.35 rows=1) (actual time=0.0249..0.0273 rows=1 loops=1) + -> Select #2 (subquery in condition; run only once) + -> Limit: 1 row(s) (cost=65.8 rows=1) (actual time=0.39..0.39 rows=1 loops=1) + -> Filter: (e.last_name = 'Facello') (cost=65.8 rows=25.3) (actual time=0.39..0.39 rows=1 loops=1) + -> Index lookup on e using ix_firstname (first_name='Georgi') (cost=65.8 rows=253) (actual time=0.387..0.387 rows=1 loops=1) + +``` + + +### 10.3.2. select_type 컬럼 + +- 각 단위 SELECT 쿼리가 어떤 타입의 쿼리인지 표시하는 컬럼 + - 각 단위 SELECT 쿼리 + - 각 단위 SELECT 쿼리 별로 테이블이 생성 + + +#### 10.3.2.1. SIMPLE + +- UNION이나 서브쿼리를 사용하지 않는 단순한 SELECT 쿼리인 경우 해당 쿼리 문장의 select_type은SIMPLE로 표시 +- 쿼리 문장이 아무리 복잡하더라도 실행 계획에서 select_type이 SIMPLE인 단위 쿼리는 하나만 존재 +- 일반적으로 가장 바깥 Select 쿼리 + + +#### 10.3.2.2. PRIMARY +- UNION이나 서브쿼리를 가지는 SELECT 쿼리의 실행 계획에서 가장 바깥쪽(Outer)에 있는 단위 쿼리는select_type이 PRIMARY로 표시 +- SIMPLE과 마찬가지로 select_type이 PRIMARY인 단위 SELECT 쿼리는 하나만 존재하며, 쿼리의 제일 바깥쪽에 있는 SELECT 단위 쿼리가 PRIMARY + +#### 10.3.2.3. UNION +- UNION으로 결합하는 단위 SELECT 쿼리 가운데 첫 번째를 제외한 두 번째 이후 단위 SELECT 쿼리의 select_type은 UNION으로 표시 + + +```sql +mysql> EXPLAIN + SELECT * FROM ( + (SELECT emp_no FROM employees e1 LIMIT 10) UNION ALL + (SELECT emp_no FROM employees e2 LIMIT 10) UNION ALL + (SELECT emp_no FROM employees e3 LIMIT 10) ) tb; + + ++----+-------------+------------+-------+-------------+------+--------+-------------+ +| id | select_type | table | type | key | ref | rows | Extra | ++----+-------------+------------+-------+-------------+------+--------+-------------+ +| 1 | PRIMARY | | ALL | NULL | NULL | 30 | NULL | +| 2 | DERIVED | e1 | index | ix_hiredate | NULL | 300252 | Using index | +| 3 | UNION | e2 | index | ix_hiredate | NULL | 300252 | Using index | +| 4 | UNION | e3 | index | ix_hiredate | NULL | 300252 | Using index | ++----+-------------+------------+-------+-------------+------+--------+-------------+ +``` + + +#### 10.3.2.4. DEPENDENT UNION + + +```sql +mysql> EXPLAIN + SELECT * + FROM employees e1 WHERE e1.emp_no IN ( + SELECT e2.emp_no FROM employees e2 WHERE e2.first_name='Matt' + UNION + SELECT e3.emp_no FROM employees e3 WHERE e3.last_name='Matt' + ); + +-- UNION에 사용된 SELECT 쿼리의 WHERE 조건에 "e2.emp_no=e1.emp_no"와 +-- "e3.emp_no=e1.emp_no"라는 조건이 자동으로 추가되어 실행된다. +-- 외부에 정의된 employees 테이블의 emp_no 칼럼이 서브쿼리에 사용되기 때문에 DEPENDENT UNION이 +-- select_type에 표시된 것이다. ++----+--------------------+------------+--------+---------+------+--------+-----------------+ +| id | select_type | table | type | key | ref | rows | Extra | ++----+--------------------+------------+--------+---------+------+--------+-----------------+ +| 1 | PRIMARY | e1 | ALL | NULL | NULL | 300252 | Using where | +| 2 | DEPENDENT SUBQUERY | e2 | eq_ref | PRIMARY | func | 1 | Using where | +| 3 | DEPENDENT UNION | e3 | eq_ref | PRIMARY | func | 1 | Using where | +|NULL| UNION RESULT | | ALL | NULL | NULL | NULL | Using temporary | ++----+--------------------+------------+--------+---------+------+--------+-----------------+ +``` + +#### 10.3.2.5. UNION RESULT + +- Union (Union distinct)결과를 담아두는 테이블을 의미함 +- Union all의 경우 임시테이블을 생성되지 않도록 개선됨 (Union all의 경우 임시테이블에 버퍼링하지 않기 때문에 UNION RESUTL 라인이 없어짐) + + +```sql +-- 1. Union +mysql> EXPLAIN + SELECT emp_no FROM salaries WHERE salary>100000 + UNION DISTINCT + SELECT emp_no FROM dept_emp WHERE from_date>'2001-01-01'; ++----+--------------+------------+-------+-------------+--------+--------------------------+ +| id | select_type | table | type | key | rows | Extra | ++----+--------------+------------+-------+-------------+--------+--------------------------+ +| 1 | PRIMARY | salaries | range | ix_salary | 191348 | Using where; Using index | +| 2 | UNION | dept_emp | range | ix_fromdate | 5325 | Using where; Using index | +|NULL| UNION RESULT | | ALL | NULL | NULL | Using temporary | ++----+--------------+------------+-------+-------------+--------+--------------------------+ + +-- 2. Union all +mysql> EXPLAIN + SELECT emp_no FROM salaries WHERE salary>100000 + UNION ALL + SELECT emp_no FROM dept_emp WHERE from_date>'2001-01-01'; ++----+-------------+----------+-------+-------------+--------+--------------------------+ +| id | select_type | table | type | key | rows | Extra | ++----+-------------+----------+-------+-------------+--------+--------------------------+ +| 1 | PRIMARY | salaries | range | ix_salary | 191348 | Using where; Using index | +| 2 | UNION | dept_emp | range | ix_fromdate | 5325 | Using where; + +``` + + +#### 10.3.2.6. SUBQUERY +- select_type의 SUBQUERY는 FROM 절 이외에서 사용되는 서브쿼리만을 의미한다 +- FROM 절에 사용된 서브쿼리는 DERIVED로 표시 + +```sql +mysql> EXPLAIN + SELECT e.first_name, + (SELECT COUNT(*) + FROM dept_emp de, dept_manager dm + WHERE dm.dept_no=de.dept_no) AS cnt + FROM employees e WHERE e.emp_no=10001; ++----+-------------+-------+-------+---------+-------+-------------+ +| id | select_type | table | type | key | rows | Extra | ++----+-------------+-------+-------+---------+-------+-------------+ +| 1 | PRIMARY | e | const | PRIMARY | 1 | NULL | +| 2 | SUBQUERY | dm | index | PRIMARY | 24 | Using index | +| 2 | SUBQUERY | de | ref | PRIMARY | 41392 | Using index | ++----+-------------+-------+-------+---------+-------+-------------+ +``` + + +```text +서브쿼리는 사용하는 위치에 따라 각각 다른 이름을 지니고 있다. +■ 중첩된 쿼리(Nested Query): SELECT되는 칼럼에 사용된 서브쿼리를 네스티드 쿼리라고 한다. +■ 서브쿼리(Subquery): WHERE 절에 사용된 경우에는 일반적으로 그냥 서브쿼리라고 한다. +■ 파생 테이블(Derived Table): FROM 절에 사용된 서브쿼리를 MySQL에서는 파생 테이블이라고 하며, 일반 +적으로 RDBMS에서는 인라인 뷰(Inline View) 또는 서브 셀렉트(Sub Select)라고 부른다. +또한 서브쿼리가 반환하는 값의 특성에 따라 다음과 같이 구분하기도 한다. +■ 스칼라 서브쿼리(Scalar Subquery): 하나의 값만(칼럼이 단 하나인 레코드 1건만) 반환하는 쿼리 +■ 로우 서브쿼리(Row Subquery): 칼럼의 개수와 관계없이 하나의 레코드만 반환하는 쿼리 +``` + + +#### 10.3.2.7. DEPENDENT SUBQUERY + +서브 쿼리가 바깥쪽 SELECT 쿼리에서 정의된 컬럼을 사용하는 경우 + +```sql +mysql> EXPLAIN + SELECT e.first_name, + (SELECT COUNT(*) + FROM dept_emp de, dept_manager dm + WHERE dm.dept_no=de.dept_no AND de.emp_no=e.emp_no) AS cnt + FROM employees e + WHERE e.first_name='Matt'; + ++----+--------------------+-------+------+-------------------+------+-------------+ +| id | select_type | table | type | key | rows | Extra | ++----+--------------------+-------+------+-------------------+------+-------------+ +| 1 | PRIMARY | e | ref | ix_firstname | 233 | Using index | +| 2 | DEPENDENT SUBQUERY | de | ref | ix_empno_fromdate | 1 | Using index | +| 2 | DEPENDENT SUBQUERY | dm | ref | PRIMARY | 2 | Using index | +``` + +안쪽의 서브쿼리과 바깥쪽 Select 쿼리의 컬럼에 의존적이기 때문에 Dependent 키워드가 붙는다. +- 안쪽 select count ... +- 바깥쪽 employees + + + +#### 10.3.2.8 DERIVED +- DERIVED는 단위 SELECT 쿼리의 실행 결과로 메모리나 디스크에 임시 테이블을 생성하는 것을 의미한다. (파생 테이블) + +```sql + EXPLAIN + SELECT * + FROM (SELECT de.emp_no FROM dept_emp de GROUP BY de.emp_no) tb, + employees e + WHERE e.emp_no=tb.emp_no; + +-> Nested loop inner join (cost=213303 rows=305063) (actual time=103..332 rows=300024 loops=1) + -> Table scan on tb (cost=96597..100413 rows=305063) (actual time=103..120 rows=300024 loops=1) + -> Materialize (cost=96597..96597 rows=305063) (actual time=103..103 rows=300024 loops=1) + -> Group (no aggregates) (cost=66091 rows=305063) (actual time=0.0783..88.9 rows=300024 loops=1) + -> Covering index scan on de using ix_empno_fromdate (cost=33138 rows=329534) (actual time=0.0742..53.1 rows=331603 loops=1) + -> Single-row index lookup on e using PRIMARY (emp_no=tb.emp_no) (cost=0.25 rows=1) (actual time=596e-6..614e-6 rows=1 loops=300024) + + +explain analyze +SELECT e.* +FROM dept_emp de +INNER JOIN employees e ON e.emp_no = de.emp_no +GROUP BY de.emp_no; + +-> Table scan on (cost=169829..173877 rows=323646) (actual time=935..993 rows=300024 loops=1) + -> Temporary table with deduplication (cost=169829..169829 rows=323646) (actual time=935..935 rows=300024 loops=1) + -> Nested loop inner join (cost=137465 rows=323646) (actual time=0.126..487 rows=331603 loops=1) + -> Table scan on e (cost=30177 rows=299612) (actual time=0.0977..80.1 rows=300024 loops=1) + -> Covering index lookup on de using ix_empno_fromdate (emp_no=e.emp_no) (cost=0.25 rows=1.08) (actual time=0.00104..0.00125 rows=1.11 loops=300024) +``` + +- MySQL 서버의 버전이 업그레이드 되면서 조인 쿼리에 대한 최적화는 많이 성숙된 상태다. 그래서 파생 테이블에 대한 최적화가 부족한 버전의 MySQL 서버를 사용 중일 경우, 가능하다면 DERIVED 형태의 실행 계획을 조인으로 해결할 수 있게 쿼리를 바꿔주는 것이 좋다. + + +#### 10.3.2.9 DEPENDENT DERIVED + +- 사용해본적 없었습니다. +- `LEFT JOIN LATERAL` +```sql +mysql> + SELECT * + FROM employees e + LEFT JOIN LATERAL + (SELECT * + FROM salaries s + WHERE s.emp_no=e.emp_no + ORDER BY s.from_date DESC LIMIT 2) AS s2 ON s2.emp_no=e.emp_no; + +-- salareis를 최대 2개만 가져와서 보여준다. + ++----+-------------------+------------+------+-------------+----------------------------+ +| id | select_type | table | type | key | Extra | ++----+-------------------+------------+------+-------------+----------------------------+ +| 1 | PRIMARY | e | ALL | NULL | Rematerialize () | +| 1 | PRIMARY | | ref | | NULL | +| 2 | DEPENDENT DERIVED | s | ref | PRIMARY | Using filesort | ++----+-------------------+------------+------+-------------+----------------------------+ +``` + + + +#### 10.3.2.10. UNCACHEABLE SUBQUERY + +캐시된 결과 확인 예시 + +캐시를 사용하지 못하게하는 대표적인 요소 +```text +■ 사용자 변수가 서브쿼리에 사용된 경우 +■ NOT-DETERMINISTIC 속성의 스토어드 루틴이 서브쿼리 내에 사용된 경우 +■ UUID()나 RAND()와 같이 결괏값이 호출할 때마다 달라지는 함수가 서브쿼리에 사용된 경우 +``` + +```sql +mysql> EXPLAIN + SELECT * + FROM employees e WHERE e.emp_no = ( + SELECT @status FROM dept_emp de WHERE de.dept_no='d005'); ++----+----------------------+-------+------+---------+--------+-------------+ +| id | select_type | table | type | key | rows | Extra | ++----+----------------------+-------+------+---------+--------+-------------+ +| 1 | PRIMARY | e | ALL | NULL | 300252 | Using where | +| 2 | UNCACHEABLE SUBQUERY | de | ref | PRIMARY | 165571 | Using index | ++----+----------------------+-------+------+---------+--------+-------------+ +``` + + + +#### 10.3.2.11. UNCACHEABLE UNION +- Union 결과를 다시 사용하지 못하는 것 + + +#### 10.3.2.12. MATERIALIZED +- 5.6 버전 부터 도입 + +아래 처럼 서브쿼리를 구체화 (임시 테이블 생성)) +```sql +mysql> EXPLAIN + SELECT * + FROM employees e + WHERE e.emp_no IN (SELECT emp_no FROM salaries WHERE salary BETWEEN 100 AND 1000); + ++----+--------------+-------------+--------+-----------+------+--------------------------+ +| id | select_type | table | type | key | rows | Extra | ++----+--------------+-------------+--------+-----------+------+--------------------------+ +| 1 | SIMPLE | | ALL | NULL | NULL | NULL | +| 1 | SIMPLE | e | eq_ref | PRIMARY | 1 | NULL | +| 2 | MATERIALIZED | salaries | range | ix_salary | 1 | Using where; Using index | ++----+--------------+-------------+--------+-----------+------+--------------------------+ +``` + + +### 10.3.3. table 컬럼 + +MySQL 서버의 실행 계획은 Slect 쿼리 기준이 아니라 테이블 기준으로 표시된다. +테이블의 이름에 별칭이 부여된 경우에는 별칭이 표시된다. + + +```sql +mysql> EXPLAIN SELECT NOW(); +mysql> EXPLAIN SELECT NOW() FROM DUAL + + ++----+-------------+-------+------+---------+----------------+ +| id | select_type | table | key | key_len | Extra | ++----+-------------+-------+------+---------+----------------+ +| 1 | SIMPLE | NULL | NULL | NULL | No tables used | ++----+-------------+-------+------+---------+----------------+ +``` + + +임시 테이블 <> +```sql ++----+-------------+------------+--------+-------------------+--------+-------------+ +| id | select_type | table | type | key | rows | Extra | ++----+-------------+------------+--------+-------------------+--------+-------------+ +| 1 | PRIMARY | | ALL | NULL | 331143 | NULL | +| 1 | PRIMARY | e | eq_ref | PRIMARY | 1 | NULL | +| 2 | DERIVED | de | index | ix_empno_fromdate | 331143 | Using index | ++----+-------------+------------+--------+-------------------+--------+-------------+ +``` + + +### 10.3.4. partitions 컬럼 + +그런데 파티션 생성 시 제약 사항(파티션 키로 사용되는 컬럼은 프라이머리 키를 포함한 모든 유니크 인덱스의 일부여야 함)으로 인해 프라이머리 키를 (emp_no, hire_date)로 생성했다. + +```sql +mysql> CREATE TABLE employees_2 ( + emp_no int NOT NULL, + 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, + PRIMARY KEY (emp_no, hire_date) + ) PARTITION BY RANGE COLUMNS(hire_date) + (PARTITION p1986_1990 VALUES LESS THAN ('1990-01-01'), + PARTITION p1991_1995 VALUES LESS THAN ('1996-01-01'), + PARTITION p1996_2000 VALUES LESS THAN ('2000-01-01'), + PARTITION p2001_2005 VALUES LESS THAN ('2006-01-01')); +mysql> INSERT INTO employees_2 SELECT * FROM employees; + + +-- 파티션에 대한 풀 스캔 진행 + EXPLAIN analyze SELECT * + FROM employees_2 + WHERE hire_date BETWEEN '1999-11-15' AND '2000-01-15'; + ++----+-------------+-------------+-----------------------+------+-------+ +| id | select_type | table | partitions | type | rows | ++----+-------------+-------------+-----------------------+------+-------+ +| 1 | SIMPLE | employees_2 | p1996_2000,p2001_2005 | ALL | 21743 | ++----+-------------+-------------+-----------------------+------+-------+ + +-> Filter: (employees_2.hire_date between '1999-11-15' and '2000-01-15') (cost=2201 rows=2418) (actual time=0.334..30.9 rows=68 loops=1) + -> Table scan on employees_2 (cost=2201 rows=21763) (actual time=0.0804..17.6 rows=21925 loops=1) +``` + + +### 10.3.5. type 컬럼 + +- 쿼리의 실행 계획에서 type 이후의 컬럼은 MySQL 서버가 각 테이블의 레코드를 어떤 방식으로 읽었는지를 나타냄 +- 여기서 접근 방식이라 함은 인덱스를 사용해 레코드를 읽었는지 아니면 테이블을 처음부터 끝까지 읽는 풀 테이블 스캔으로 레코드를 읽었는지 등을 의미함 +- 쿼리 튜닝 시 인덱스를 효율적으로 사용하는지 확인하는 것이 중요하므로 실행 계획에서 type 컬럼은 반드시 체크해야함 + +각 테이블에 대한 접근 방법 12가지 + +```text +■ 1.system +■ 2.const +■ 3.eq_ref +■ 4.ref +■ 5.fulltext +■ 6.ref_or_null +■ 7.unique_subquery +■ 8.index_subquery +■ 9.range +■ 10.index_merge +■ 11.index +■ 12.ALL +``` +1. 위의 12가지 접근 방법은 성능이 빠른 순서대로 나열 +2. ALL을 제외한 나머지는 모두 인덱스를 사용하는 접근 +3. 하나의 단위 Select 쿼리는 위의 접근 방법중 단 1가지만 사용할 수 있음 +4. index_merge를 제외한 접근은 오직 1개의 인덱스만 사용함 + +#### 10.3.5.1. system +#### 10.3.5.2. const +#### 10.3.5.3. eq_ref +#### 10.3.5.4. ref + +인덱스의 종류와 관계없이 동등 조건으로 검색할 때는 ref 접근 방법이 사용된다 + +```sql +mysql> EXPLAIN + SELECT * FROM dept_emp WHERE dept_no='d005'; ++----+-------------+----------+------+---------+---------+-------+ +| id | select_type | table | type | key | key_len | ref | ++----+-------------+----------+------+---------+---------+-------+ +| 1 | SIMPLE | dept_emp | ref | PRIMARY | 16 | const | ++----+-------------+----------+------+---------+---------+-------+ +``` + +- ref 컬럼의 cont는 접근 방법이 아니라 ref 접근 방법에서 값 비교에 사용된 입력값이 상수('d005')였음을 의미함 + + +#### 10.3.5.5. fulltext +#### 10.3.5.6. ref_or_null +#### 10.3.5.7. unique_subquery +#### 10.3.5.8. index_subquery +#### 10.3.5.9. range + +```text +이 책에서 인덱스 레인지 스캔이라고 하면 const, ref, range라는 세 가지 접근 방법을 모두 묶어서 지칭하는 것임을 기억하자. 또한 “인덱스를 효율적으로 사용한다” 또는 “작업 범위 결정 조건으로 인덱스를 사용한다”라는 표현 모두 이 세 가지 접근 방법을 의미한다. 업무상 개발자나 DBA와 소통할 때도 const나 ref, range 접근 방법을 구분해서 언급하는 경우는 거의 없으며, 일반적으로 “인덱스 레인지 스캔” 또는 “레인지 스캔”으로 언급할 때가 많다. +``` +#### 10.3.5.10. index_merge +#### 10.3.5.11. index +#### 10.3.5.12. ALL + +### 10.3.6. possible_keys 컬럼 + +### 10.3.7. key 컬럼 +- key 컬럼에 표시되는 인덱스는 최종 선택된 실행 계획에서 사용하는 인덱스를 의미 +- 그러므로 쿼리를 튜닝할 때는 key 칼럼에 의도했던 인덱스가 표시되는지 확인하는 것이 중요 +- key 칼럼에 표시되는 값이 PRIMARY인 경우에는 프라이머리 키를 사용한다는 의미이며, 그 이외의 값은 모두 테이블이나 인덱스를 생성할 때 부여했던 고유 이름 + +### 10.3.8. key_len 컬럼 + + +### 10.3.9 ref 컬럼 +- 테이블 접근 방법이 ref 일 때(동등 비교조건 일 때) 어떤 값이 제공됐는지 보여줌 +- 상숫값을 지정했다면 ref 컬럼의 값은 const로 표시되고, 다른 테이블의 컬럼 값이면 그 테이블명과 컬럼명이 표시됨 + +Const +```sql +mysql> EXPLAIN + SELECT * FROM dept_emp WHERE dept_no='d005'; ++----+-------------+----------+------+---------+---------+-------+ +| id | select_type | table | type | key | key_len | ref | ++----+-------------+----------+------+---------+---------+-------+ +| 1 | SIMPLE | dept_emp | ref | PRIMARY | 16 | const | ++----+-------------+----------+------+---------+---------+-------+ + +``` + +다른 테이블의 컬럼 참조 값 +```sql +mysql> EXPLAIN + SELECT * + FROM employees e, dept_emp de + WHERE e.emp_no=de.emp_no; ++----+-------------+-------+--------+---------+---------------------+ +| id | select_type | table | type | key | ref | ++----+-------------+-------+--------+---------+---------------------+ +| 1 | SIMPLE | de | ALL | NULL | NULL | +| 1 | SIMPLE | e | eq_ref | PRIMARY | employees.de.emp_no | ++----+-------------+-------+--------+---------+---------------------+ +``` + + +### 10.3.10. rows 컬럼 + +- 실행 계획의 효율성 판단을 위해 예측했던 레코드 건수를 보여줌 + - 반환하는 레코드 개수의 예측치가 아니라 쿼리를 처리하기 위해 얼마나 많은 레코드를 읽고 처리해야하는지를 의미 +```sql +mysql> EXPLAIN + SELECT * FROM dept_emp WHERE from_date>='1985-01-01'; + +----+-------------+----------+------+------+---------+--------+ +| id | select_type | table | type | key | key_len | rows | ++----+-------------+----------+------+------+---------+--------+ +| 1 | SIMPLE | dept_emp | ALL | NULL | NULL | 331143 | ++----+-------------+----------+------+------+---------+--------+ +``` + +### 10.3.11. filtered 컬럼 + +- 각 테이블에서 일치하는 레코드 개수를 예측하면 좀 더 효율적인 실행 계획을 수립할 수 있음 + - ex. 최종적으로 일치하는 레코드 건수가 적은 테이블을 드라이빙 테이블로 결정 +```sql +mysql> EXPLAIN + SELECT * + FROM employees e, + salaries s + WHERE e.first_name='Matt' + AND e.hire_date BETWEEN '1990-01-01' AND '1991-01-01' + AND s.emp_no=e.emp_no + AND s.from_date BETWEEN '1990-01-01' AND '1991-01-01' + AND s.salary BETWEEN 50000 AND 60000; + ++----+-------------+-------+------+--------------+------+----------+ +| id | select_type | table | type | key | rows | filtered | ++----+-------------+-------+------+--------------+------+----------+ +| 1 | SIMPLE | e | ref | ix_firstname | 233 | 16.03 | +| 1 | SIMPLE | s | ref | PRIMARY | 10 | 0.48 | ++----+-------------+-------+------+--------------+------+----------+ + +-- 233 * 0.1603 = 약 37 +``` + +```sql +mysql> EXPLAIN + SELECT /*+ JOIN_ORDER(s, e) */ * + FROM employees e, + salaries s + WHERE e.first_name='Matt' + AND e.hire_date BETWEEN '1990-01-01' AND '1991-01-01' + AND s.emp_no=e.emp_no + AND s.from_date BETWEEN '1990-01-01' AND '1991-01-01' + AND s.salary BETWEEN 50000 AND 60000; + ++----+-------------+-------+--------+-----------+------+----------+ +| id | select_type | table | type | key | rows | filtered | ++----+-------------+-------+--------+-----------+------+----------+ +| 1 | SIMPLE | s | range | ix_salary | 3314 | 11.11 | +| 1 | SIMPLE | e | eq_ref | PRIMARY | 1 | 5.00 | ++----+-------------+-------+--------+-----------+------+----------+ + +-- 3314 * 0.1111 = 약 368 +``` + +### 10.3.12. extra 컬럼 + +- 실행 계획에서 성능에 관련한 중요한 내용을 extra 컬럼에 표시 +- 일반적으로 2~3개씩 함께 표시되고 주로 내부적인 처리 알고리즘에 대해 조금 더 깊이 있는 내용을 보여주는 경우가 많음 +```sql +EXPLAIN +SELECT DISTINCT d.dept_no +FROM departments d, dept_emp de WHERE de.dept_no=d.dept_no; +``` + +![[10. Extra 컬럼 예시.png]] +- MySQL 서버의 버전이 업그레이드 되고 최적화 기능이 도입될수록 새로운 내용이 더 추가될 것으로 보임 + - 옵티마이저 스위치 옵션이 추가되면 더 추가될 것 같아요. + +#### 10.3.12.1. const row not found +- 쿼리의 실행 계획에서 const 접근 방식으로 테이블을 읽었지만 일치하는 레코드가 존재하지 않음 + +#### 10.3.12.2. Deleting all rows +- 테이블의 모든 레코드를 삭제하는 기능을 제공하는 스토리지 엔진 테이블인 경우 Extra 칼럼에 “Deleting all rows” 문구가 표시됨 (MyISAM) + +#### 10.3.12.3. Distinct + +- 꼭 필요한 데이터만 스킵하며 읽어온 경우 + +```sql +mysql> EXPLAIN + SELECT DISTINCT d.dept_no + FROM departments d, dept_emp de WHERE de.dept_no=d.dept_no; ++----+-------------+-------+-------+-------------+------------------------------+ +| id | select_type | table | type | key | Extra | ++----+-------------+-------+-------+-------------+------------------------------+ +| 1 | SIMPLE | d | index | ux_deptname | Using index; Using temporary | +| 1 | SIMPLE | de | ref | PRIMARY | Using index; Distinct | ++----+-------------+-------+-------+-------------+------------------------------+ +``` + +![[10.6 Distinct의 처리 방식.png]] + +#### 10.3.12.4. FirstMatch + +```sql +mysql> EXPLAIN SELECT * + FROM employees e + WHERE e.first_name='Matt' + AND e.emp_no IN ( + SELECT t.emp_no FROM titles t + WHERE t.from_date BETWEEN '1995-01-01' AND '1995-01-30' + ); ++----+-------+------+--------------+------+-----------------------------------------+ +| id | table | type | key | rows | Extra | ++----+-------+------+--------------+------+-----------------------------------------+ +| 1 | e | ref | ix_firstname | 233 | NULL | +| 1 | t | ref | PRIMARY | 1 | Using where; Using index; FirstMatch(e) | ++----+-------+------+--------------+------+---------------------------------------- + +EXPLAIN analyze + SELECT * + FROM employees e + WHERE e.first_name='Matt' + AND e.emp_no IN ( + SELECT t.emp_no FROM titles t +# WHERE t.from_date BETWEEN '1995-01-01' AND '1995-01-30' + ); + +-> Nested loop semijoin (cost=339 rows=341) (actual time=1.35..15.4 rows=233 loops=1) + -> Index lookup on e using ix_firstname (first_name='Matt') (cost=81.5 rows=233) (actual time=1.3..1.71 rows=233 loops=1) + -> Covering index lookup on t using PRIMARY (emp_no=e.emp_no) (cost=1.4 rows=1.46) (actual time=0.0584..0.0584 rows=1 loops=233) + + +explain analyze +select * from employees as e +left join titles as t +on e.emp_no = t.emp_no +where e.first_name='Matt'; + +-> Nested loop left join (cost=334 rows=341) (actual time=0.385..3.2 rows=337 loops=1) + -> Index lookup on e using ix_firstname (first_name='Matt') (cost=81.5 rows=233) (actual time=0.356..0.863 rows=233 loops=1) + -> Index lookup on t using PRIMARY (emp_no=e.emp_no) (cost=0.936 rows=1.46) (actual time=0.00891..0.00975 rows=1.45 loops=233) +``` + +#### 10.3.12.5. Full scan on NULL key + +#### 10.3.12.6. Impossible HAVING + +#### 10.3.12.7. Impossible WHERE + +#### 10.3.12.8. LooseScan + +#### 10.3.12.9. No matching min/max row + +#### 10.3.12.10. no matching row in const table + +#### 10.3.12.11. No matching rows after partition pruning + +##### 10.3.12.12. No tables used + +```sql +mysql> EXPLAIN SELECT 1; +mysql> EXPLAIN SELECT 1 FROM dual; ++----+-------------+-------+------+------+----------------+ +| id | select_type | table | type | key | Extra | ++----+-------------+-------+------+------+----------------+ +| 1 | SIMPLE | NULL | NULL | NULL | No tables used | ++----+-------------+-------+------+------+----------------+ +``` + +#### 10.3.12.13. Not Exist + +#### 10.3.12.14. Plan isn’t ready yet + +#### 10.3.12.15. Range checked for each record(index map: N) + +#### 10.3.12.16. Recursive + +#### 10.3.12.17. Rematerialize + +#### 10.3.12.18. Select tables optimized away + +#### 10.3.12.19. Start temporary, End temporary + +#### 10.3.12.20. unique row not found + +#### 10.3.12.21. Using filesort + +- 실행 계획의 Extra 칼럼에 “Using filesort”가 출력되는 쿼리는 많은 부하를 일으키므로 가능하다면 쿼리를 튜닝하거나 인덱스를 생성하는 것이 좋음 +- “Using filesort”는 중요한 부분이므로 11.4.9절 ‘ORDER BY’에서 다시 자세히 다룸 + +#### 10.3.12.22. Using index(커버링 인덱스) + + +--- +#### 10.3.12.23. Using index condition +MySQL 옵티마이저가 인덱스 컨디션 푸시 다운(Index condition pushdown) 최적화를 사용하면 다 +음 예제와 같이 Extra 칼럼에 “Using index condition” 메시지가 표시된다 +```sql +mysql> SELECT * FROM employees WHERE last_name='Acton' AND first_name LIKE '%sal'; ++----+-------------+-----------+------+-----------------------+---------+-----------------------+ +| id | select_type | table | type | key | key_len | Extra | ++----+-------------+-----------+------+-----------------------+---------+-----------------------+ +| 1 | SIMPLE | employees | ref | ix_lastname_firstname | 66 | Using index condition | ++----+-------------+-----------+------+-----------------------+---------+-----------------------+ +``` + + +#### 10.3.12.24. Using index for group-by +- Group By 처리는 고부하 작업 + - 1. 그루핑 기준 컬럼을 이용한 정렬 작업 + - 2. 정렬된 결과를 그루핑 +- 그루핑 기준 컬럼이 인덱싱 되어있다면 정렬을 생략할 수 있음 + +##### 10.3.12.24.1. 타이트 인덱스 스캔(인덱스 스캔)을 통한 GROUP BY 처리 +- 듬성 듬성 읽기 불가능한 경우, ex. Count(), Sum(), Avg() +- 이러한 쿼리의 실행 계획에는 “Using index for group-by” 메시지가 출력되지 않음 +```sql +mysql> EXPLAIN + SELECT first_name, COUNT(*) AS counter + FROM employees GROUP BY first_name; ++----+-------------+-----------+-------+--------------+---------+-------------+ +| id | select_type | table | type | key | key_len | Extra | ++----+-------------+-----------+-------+--------------+---------+-------------+ +| 1 | SIMPLE | employees | index | ix_firstname | 58 | Using index | ++----+-------------+-----------+-------+--------------+---------+-------------+ +``` + +##### 10.3.24.2. 루스 인덱스 스캔을 통한 GROUP BY 처리 +- 듬성 듬성 읽을 수 있는 경우, ex. Min(), Max() +``` +mysql> EXPLAIN + SELECT emp_no, MIN(from_date) AS first_changed_date, MAX(from_date) AS last_changed_date + FROM salaries + GROUP BY emp_no; + ++----+-------------+----------+-------+---------+---------+--------------------------+ +| id | select_type | table | type | key | key_len | Extra | ++----+-------------+----------+-------+---------+---------+--------------------------+ +| 1 | SIMPLE | salaries | range | PRIMARY | 4 | Using index for group-by | ++----+-------------+----------+-------+---------+---------+--------------------------+ +``` +- Where 조건이 있는 경우에는 루스 스캔을 하지 못할 수 있음 +- 그루핑과 Where 조건이 같은 인덱스를 사용할 때 루스 인덱스 스캔이 가능함 +- 하지만 Where 조건으로 소량의 데이터만 뽑아낼 수 있다면 루스 인덱스 스캔이 아닌 인덱스 스캔으로 처리됨 +```sql +mysql> EXPLAIN + SELECT emp_no, + MIN(from_date) AS first_changed_date, + MAX(from_date) AS last_changed_date + FROM salaries + WHERE emp_no BETWEEN 10001 AND 10099 + GROUP BY emp_no; ++----+-------------+----------+-------+---------+---------+--------------------------+ +| id | select_type | table | type | key | key_len | Extra | ++----+-------------+----------+-------+---------+---------+--------------------------+ +| 1 | SIMPLE | salaries | range | PRIMARY | 4 | Using where; Using index | ++----+-------------+----------+-------+---------+---------+--------------------------+ +``` + + + +#### 10.3.12.25. Using Index for skip scan + +- MySQL 8.0 이전에는 (gender, birth_date) 인덱스의 경우 gender 컬럼이 Where절에 있어야지만 사용가능했으나 8.0 버전 부터 인덱스 스킵 스캔이 도입되어 경우에 따라 인덱스 선두컬럼인 gender 컬럼이 Where절에 없어도 인덱스를 사용한 레인지 스캔 가능 +```sql +mysql> EXPLAIN + SELECT gender, birth_date + FROM employees + WHERE birth_date>='1965-02-01'; ++----+-----------+-------+---------------------+----------------------------------------+ +| id | table | type | key | Extra | ++----+-----------+-------+---------------------+----------------------------------------+ +| 1 | employees | range | ix_gender_birthdate | Using where; Using index for skip scan | +``` + + + +#### 10.3.12.26. Using join buffer(Block Nested Loop), Using join buffer(Batched Key Access), Using join buffer(hash join) + +- 드리븐 테이블의 조인 컬럼에 적절한 인덱스가 없다면 블록 네스티드 루프 조인이나 해시 조인이 발생하고 Extra컬럼에는 `Using join buffer` 가 나타남 + + +```sql +mysql> EXPLAIN + SELECT * + FROM dept_emp de, employees e + WHERE de.from_date>'2005-01-01' AND e.emp_no<10904; +위 예제 쿼리의 실행 계획은 다음과 같다. ++----+-------------+-------+-------+-------------+--------------------------------------------+ +| id | select_type | table | type | key | Extra | ++----+-------------+-------+-------+-------------+--------------------------------------------+ +| 1 | SIMPLE | de | range | ix_fromdate | Using index condition | +| 1 | SIMPLE | e | range | PRIMARY | Using where; Using join buffer (hash join) | +``` + +#### 10.3.12.27. Using MRR + +- MRR(Multi Range Read) 최적화 +- MySQL 엔진이 스토리지 엔진에 키 값을 기준으로 레코드를 1건, 1건 씩 읽어서 가져옴 +- 레코드가 동일 페이지에 있다고 하더라도 레코드 단위로 스토리지 엔진 API 호출이 필요함 + +- 위와 같이 스토리지 엔진 API 호출이 과도하게 발생하는 단점을 보완하기 위해 + 1. MySQL엔진에서 여러개의 키값을 모았다가 스토리지 엔진에 전달하고 + 2. 스토리지 엔진은 전달받은 키값을 정렬해서 최소한의 페이지 접근만으로 필요한 레코드를 가져올 수 있도록 최적화함 + +```sql +mysql> EXPLAIN + SELECT /*+ JOIN_ORDER(s, e) */ * + FROM employees e, + salaries s + WHERE e.first_name='Matt' + AND e.hire_date BETWEEN '1990-01-01' AND '1991-01-01' + AND s.emp_no=e.emp_no + AND s.from_date BETWEEN '1990-01-01' AND '1991-01-01' + AND s.salary BETWEEN 50000 AND 60000; ++----+-------+--------+-----------+---------+------+----------------------------------+ +| id | table | type | key | key_len | rows | Extra | ++----+-------+--------+-----------+---------+------+----------------------------------+ +| 1 | s | range | ix_salary | 4 | 3314 | Using index condition; Using MRR | +| 1 | e | eq_ref | PRIMARY | 4 | 1 | Using where | ++----+-------+--------+-----------+---------+------+----------------------------------+ +``` + +#### 10.3.12.28 Using sort_union(...), Using union(...), Using intersect(...) + +- 실행계획의 Type 컬럼의 값이 index_merge인 경우에는 2개 이상의 인덱스가 사용되어 데이터를 가져옴 +- 이 때 실행계획의 Extra 컬럼에 인덱스로 읽어온 결과를 어떻게 병합했는지 상세 정보를 보여줌 + +```text +■ Using intersect(...): 각각의 인덱스를 사용할 수 있는 조건이 AND로 연결된 경우 각 처리 결과에서 교집합을 추출해 내는 작업을 수행했다는 의미다. +■ Using union(...): 각 인덱스를 사용할 수 있는 조건이 OR로 연결된 경우 각 처리 결과에서 합집합을 추출해내는 작업 을 수행했다는 의미다. +■ Using sort_union(...): Using union과 같은 작업을 수행하지만 Using union으로 처리될 수 없는 경우(OR로 연결된 상대적으로 대량의 range 조건들) 이 방식으로 처리된다. Using sort_union과 Using union의 차이점은 Using sort_union은 프라이머리 키만 먼저 읽어서 정렬하고 병합한 이후 비로소 레코드를 읽어서 반환할 수 있다는 것 이다 +``` + +#### 10.3.12.29. Using temporary + +- 쿼리의 실행 계획에서 Extra 칼럼에 `Using temporary` 키워드가 표시되면 임시 테이블을 사용한 것인데, 이때 사용된 임시 테이블이 메모리에 생성됐는지 디스크에 생성됐는지는 실행 계획만으로 판단할 수 없음 +```sql +EXPLAIN + SELECT gender, min(emp_no), max(emp_no), count(*) + FROM employees + GROUP BY gender + ORDER BY min(emp_no); + ++----+-------------+-----------+-------+---------------------------------+ +| id | select_type | table | type | Extra | ++----+-------------+-----------+-------+---------------------------------+ +| 1 | SIMPLE | employees | index | Using temporary; Using filesort | ++----+-------------+-----------+-------+---------------------------------+ + +explain analyze +select birth_date, count(*) from employees +group by birth_date; + +-> Table scan on (actual time=107..107 rows=4751 loops=1) + -> Aggregate using temporary table (actual time=107..107 rows=4751 loops=1) + -> Covering index scan on employees using ix_gender_birthdate (cost=30774 rows=299809) (actual time=0.0729..47.1 rows=300024 loops=1) + +explain +select distinct birth_date from employees; + +-> Table scan on (cost=60755..64505 rows=299809) (actual time=81.9..82.2 rows=4751 loops=1) + -> Temporary table with deduplication (cost=60755..60755 rows=299809) (actual time=81.9..81.9 rows=4751 loops=1) + -> Covering index scan on employees using ix_gender_birthdate (cost=30774 rows=299809) (actual time=0.0532..46.3 rows=300024 loops=1) +``` + +주의사항: 임시테이블을 사용하지만 Using temporary가 표시되지 않는 경우도 있음 +임시테이블을 생성하는 경우들 +- From절에 사용한 서브쿼리는 무조건 임시테이블을 생성 (인라인 뷰들) +- Count(distinct column_name) +- Union (Union Distinct), Union All은 X +- 인덱스를 사용하지 못하는 정렬 작업들 (Using filesort) + +#### 10.3.12.30. Using where + +MySQL 서버 내부의 2개 레이어 +- 1. 스토리지 엔진: 디스크나 메모리상에서 필요한 레코드를 읽거나 저장 +- 2. MySQL 엔진: 스토리지 엔진으로부터 받은 레코드를 가공 또는 연산하는 작업 + +MySQL 엔진 레이어에서 별도의 가공을해서 데이터 필터링 작업이 있었을 경우 Extra 컬럼에 `Using Where` 가 표시됨 +![[10.12. MySQL 엔진과 각 스토리지 엔진의 처리 차이.png]] + +8.3.7.1절 비교 조건의 종류와 효율성 에서 `작업 범위 결정 조건`과 `체크 조건(필터링)`의 구분을 언급했는데 +- 실제로 작업 범위 결정 조건은 각 스토리지 엔진 레벨에서 처리되지만 +- 체크 조건은 MySQL 엔진 레이어 에서 처리됨 + +```sql + EXPLAIN analyze + SELECT * + FROM employees + WHERE emp_no BETWEEN 10001 AND 10100 + AND gender='F'; + +-> Filter: ((employees.emp_no between 10001 and 10100) and (employees.gender = 'F')) (cost=20.9 rows=50) (actual time=0.0754..0.319 rows=37 loops=1) + -> Index range scan on employees using PRIMARY over (10001 <= emp_no <= 10100) (cost=20.9 rows=100) (actual time=0.0659..0.282 rows=100 loops=1) + + + EXPLAIN analyze + SELECT * + FROM employees + WHERE emp_no BETWEEN 10001 AND 10100; + +-> Filter: (employees.emp_no between 10001 and 10100) (cost=20.9 rows=100) (actual time=0.0258..0.109 rows=100 loops=1) + -> Index range scan on employees using PRIMARY over (10001 <= emp_no <= 10100) (cost=20.9 rows=100) (actual time=0.0242..0.0976 rows=100 loops=1) +``` + + +``` +위의 쿼리 예제를 통해 인덱스 최적화를 조금 더 살펴보자. 위 처리 과정에서 최종적으로 쿼리에 일치하는 레코 +드는 37건밖에 안 되지만 스토리지 엔진은 100건의 레코드를 읽은 것이다. 상당히 비효율적인 과정이라고 볼 수 있다. +그런데 employees 테이블에 (emp_no, gender)로 인덱스가 준비돼 있었다면 어떻게 될까? 이때는 두 조건 모두 작업 범위의 제한 조건으로 사용되어 필요한 37개의 레코드만 정확하게 읽을 수 있다. + +``` + +#### 10.3.12.31. Zero limit + +- 실제 쿼리 결과가아닌 컬럼 개수, 이름, 타입 등 메타 데이터 정보만 필요할 경우 쿼리의 마지막에 limit 0;를 사용 +- 이 때 Zero limit이 Extra 컬럼에 나오고 실제 데이터를 읽지 않게 됨 + +```sql +mysql> EXPLAIN SELECT * FROM employees LIMIT 0; ++----+-------------+-------+------+------+------------+ +| id | select_type | table | type | key | Extra | ++----+-------------+-------+------+------+------------+ +| 1 | SIMPLE | NULL | NULL | NULL | Zero limit | ++----+-------------+-------+------+------+------------+ +``` \ No newline at end of file diff --git "a/JongminJeon/9. \354\230\265\355\213\260\353\247\210\354\235\264\354\240\200\354\231\200 \355\236\214\355\212\270.md" "b/JongminJeon/9. \354\230\265\355\213\260\353\247\210\354\235\264\354\240\200\354\231\200 \355\236\214\355\212\270.md" index 8acb936..f6f7189 100644 --- "a/JongminJeon/9. \354\230\265\355\213\260\353\247\210\354\235\264\354\240\200\354\231\200 \355\236\214\355\212\270.md" +++ "b/JongminJeon/9. \354\230\265\355\213\260\353\247\210\354\235\264\354\240\200\354\231\200 \355\236\214\355\212\270.md" @@ -655,7 +655,7 @@ mysql> EXPLAIN SELECT * FROM employees e WHERE e.first_name='Matt' AND e.emp_no IN ( SELECT t.emp_no FROM titles t - WHERE t.from_ + WHERE t.from_date BETWEEN '1995-01-01' AND '1995-01-30' ``` 실행 계획 diff --git "a/JongminJeon/src/img/10. Extra \354\273\254\353\237\274 \354\230\210\354\213\234.png" "b/JongminJeon/src/img/10. Extra \354\273\254\353\237\274 \354\230\210\354\213\234.png" new file mode 100644 index 0000000..0cb4e06 Binary files /dev/null and "b/JongminJeon/src/img/10. Extra \354\273\254\353\237\274 \354\230\210\354\213\234.png" differ diff --git "a/JongminJeon/src/img/10.12. MySQL \354\227\224\354\247\204\352\263\274 \352\260\201 \354\212\244\355\206\240\353\246\254\354\247\200 \354\227\224\354\247\204\354\235\230 \354\262\230\353\246\254 \354\260\250\354\235\264.png" "b/JongminJeon/src/img/10.12. MySQL \354\227\224\354\247\204\352\263\274 \352\260\201 \354\212\244\355\206\240\353\246\254\354\247\200 \354\227\224\354\247\204\354\235\230 \354\262\230\353\246\254 \354\260\250\354\235\264.png" new file mode 100644 index 0000000..9abd21f Binary files /dev/null and "b/JongminJeon/src/img/10.12. MySQL \354\227\224\354\247\204\352\263\274 \352\260\201 \354\212\244\355\206\240\353\246\254\354\247\200 \354\227\224\354\247\204\354\235\230 \354\262\230\353\246\254 \354\260\250\354\235\264.png" differ diff --git "a/JongminJeon/src/img/10.4. Subquery \352\262\260\352\263\274 \354\272\220\354\213\234.png" "b/JongminJeon/src/img/10.4. Subquery \352\262\260\352\263\274 \354\272\220\354\213\234.png" new file mode 100644 index 0000000..000ead3 Binary files /dev/null and "b/JongminJeon/src/img/10.4. Subquery \352\262\260\352\263\274 \354\272\220\354\213\234.png" differ diff --git "a/JongminJeon/src/img/10.6 Distinct\354\235\230 \354\262\230\353\246\254 \353\260\251\354\213\235.png" "b/JongminJeon/src/img/10.6 Distinct\354\235\230 \354\262\230\353\246\254 \353\260\251\354\213\235.png" new file mode 100644 index 0000000..86ca908 Binary files /dev/null and "b/JongminJeon/src/img/10.6 Distinct\354\235\230 \354\262\230\353\246\254 \353\260\251\354\213\235.png" differ diff --git "a/JongminJeon/src/img/10.\354\277\274\353\246\254 \354\213\244\355\226\211\354\213\234\352\260\204 \355\231\225\354\235\270.png" "b/JongminJeon/src/img/10.\354\277\274\353\246\254 \354\213\244\355\226\211\354\213\234\352\260\204 \355\231\225\354\235\270.png" new file mode 100644 index 0000000..3baa860 Binary files /dev/null and "b/JongminJeon/src/img/10.\354\277\274\353\246\254 \354\213\244\355\226\211\354\213\234\352\260\204 \355\231\225\354\235\270.png" differ