rows
옵티마이저가 예측한 값이 틀릴 가능성이 높다.
가끔 인덱스되지 않은 컬럼이나 컬럼의 값이 균등하게 분포되지 않은 경우에도 제대로 된 예측을 못할 수 있음
이런 경우 '히스토그램'을 사용
filtered
SELECT ..
WHERE e.first_name = 'Matt' (1)
AND s.hire_date BETWEEN `1991-01-01` AND `1991-01-01` (2)
AND s.emp_no = e.emp_no
AND s.from_date BETWEEN '1990-01-01' AND '1991-01-01' (3)
AND s.salary BETWEEN 50000 AND 60000; (4)
위의 쿼리에서 index를 사용가능한 조건은 e.first_name = 'Matt'
, AND s.salary BETWEEN 50000 AND 60000
(1),(2) 2가지 입니다.
이제 index를 사용할 수 없는 조건까지 합쳐 결과적으로 조회한는 레코드가 적은 테이블을 드라이빙 테이블로 선정
만약 e table의 실행 계획 rows 233, filtered 16.03이라면
e table의 조회 레코드에서 index가 사용된 컬럼 233개, 사용되지 않은 컬럼 37개(233 x 0.1603)
정리
1차(Index를 사용한) 값 Matt
라는 이름의 컬럼 233건에서
2차() 값 1991-01-01
AND 1991-01-01
컬럼은 37개(233 x 0.16)개가 존재한다.
해당 37개의 레코드는 s table과 join
filterd 수치가 높은 것은 index가 걸리지 않은 조건이 많다는 뜻 = 임시 테이블을 만들어야 함
드라이빙 테이블 선택 조건은 임시 테이블이 생성하지 않는 방향으로 할 가능성이 있음
AND 연산이기 때문에 rows * filtered 공식인 것 같다.
OR 연산이면 결과의 크기는 최소 rows보다 더 큰 것이 아닐까?
Extrea
const row not found
const 접근 방법으로 테이블을 읽었지만 실제로 해당 테이블에 레코드가 1건도 존재하지 않음
Deleting All rows
8.0 부터는 where절 없는 delete보다 truncate table을 권장
Distinct
SELECT DISTINCT d.dept_no가 있다면 테이블 조인 시에 on
절에 부합하는 레코드 1개만 발견하고 넘어감
First Match
First Match 전략이 사용되면 노출, 테이블에서 첫 번째로 일치하는 한 건만 검색한다는 것을 의미
FristMatch 전략 : 세미 조인 최적화 전략, Exists형태로 튜닝하는 것
Full scan on NULL Key
차선책으로 Full Scan을 할 것이라는 알림
col1 IN (SELECT col2 FROM ..)과 같은 쿼리에서 col1이 NULL 이라면 NULL IN (SELECT col2 FROM..) 과 같음
비교 과정에서 col1이 Null이면 서브쿼리에 사용된 테이블에 대해서 풀 테이블 스캔 필요
col1이 Not Null이라면 Full scan on Null Key
가 출력되지 않음
Full scan on Null Key
가 출력되어도 실제 값에 Null이 없다면
풀 테이블 스캔이 일어나지 않음
하지만 Null이 존재하고 서브쿼리에 개별적으로 WHERE 조건이 있다면 상당한 성능 문제가 발생한 가능성 존재
- 인덱스 컬럼은
Null
도 정렬이 될텐데.. 왜?Full Scan
이 차선책인가?
impossible WHERE
HAVING 절의 조건을 만족하는 레코드가 없을 때 노출, 1가지 예로 NOT NULL 컬럼에 IS NULL 조건을 거는 경우
(쿼리 다시 점검 필요)
LooseScan
세미 조인 최적화 LooseScan 최적화 전략이 사용
loose scan 참고
https://velog.io/@kma95278/Index-Scan
No matching min/max row
min(), max()와 같은 집합 함수 쿼리의 조건절에 일치하는 레코드가 1건도 없는 경우 노출, 결과로 NULL 반환
No matching row in const table
조인에 사용된 테이블에서 const방법으로 접근할 때 일치하는 레코드가 없다면 출력
No matching rows after partition pruning
파티션된 테이블에 대하여 UPDATE or DELETE 명령에서 표시
만약 날짜 기준으로 1991, 1996, 2001, 2006 5년 단위로 partition이 되었을 때 2020년 이상 레코드에 대하여 UPDATE or DELETE 쿼리 실행 시 발생
삭제할 레코드가 없는 것이 아니라 삭제할 대상 파티션이 없다는 것을 의미
삭제 할 레코드는 없지만 대상 파티션이 존재하는 경우에는 출력 X
No tables used
FROM절이 없는 쿼리 문장이나 FROM DUAL형태의 쿼리 실행 시 출력
DUAL은 상수 테이블을 의미한다.
- 컬럼과 레코드를 각각 1개씩만 가지는 가상의 테이블
- dual 은 펑션이나 계산식을 테이블 생성없이 수행해 보기 위한 용도
Not Exists
A 테이블에는 존재하고 B 테이블에는 존재하지 않는 것을 조회하는 쿼리(outer join)에서 사용, 일반적으로 NOT IN, NOT EXISTS를 사용하지만 레코드가 많으면 OUTER JOIN이 성능에 유리
아래와 같은 느낌, B 레코드가 NULL인 A 레코드를 찾는다.
FROM A
LEFT JOIN B
ON A.id = B.a_id
WHERE B.a_id IS NULL
Not Exists가 사용되면 옵티마이저가 A에만 존재하는지만 검사
A와 매칭되는 B가 2건 이상 있더라도 1건만 보고 처리를 종료
이게 inner join 아닌가?
Plan isn't ready yet
아직 옵티마이저가 실행 계획을 수립하지 못한 경우 노출
1번에서 ProcessId를 획득한 후 2번 [PROCESSID]에 넣고 실행
1) SHOW PROCESSLIST;
2) EXPLAIN FOR CONNECTION [RPOCESSID];
이 때 Extra 값을 확인하며 Plain isn't pready yet을 확인이 가능
Range checked for each record(index map:N)
조인을 할 때 2개의 조인 조건이 상수가 아니라 변수일 때 옵티마이저는
인덱스 레인지 스캔, 풀 테이블 스캔 어떤 것이 효율적인지 판단할 수 없음
FROM employee e1, employee e2
WHERE e2.emp_no >= e1.emp.no;
사번이 1억 번까지 있고 e1의 emp_no이 1인 경우는 1억 번을 전부 읽지만
e1의 emp_no이 1억이라면 1번만 테이블을 읽어도 됩니다.
"레코드마다 인덱스 레인지 스캔을 체크한다." 이러한 경우 "Range checked for each record" 노출
?: 레코드마다 다른 인덱스를 사용하는 것인가?
많이 읽어야 한다면 풀 테이블 스캔이 유리, 조금 읽어야 한다면 인덱스 테이블 스캔이 유리
index map: 0x1
은 어떤 인덱스를 후보로 두었는지에 대한 값
2진수로 변환했을 때 비트맵의 각 자릿수는 CREATE TABLE table_name
에서 나열된 인덱스 그대로 반영함
'1'이라면 후보, '0'이라면 후보 탈락을 의미
실행 계획에 type All로 노출되지만 무조건 풀 테이블 스캔을 사용하는 것이 아니라 인덱스 사용 여부 검토 후 도움이 되지 않는다면 사용한다는 의미로 ALL이 노출
Recursive
8.0부터 CTE(Common Table Expression)을 이용해 재귀 쿼리를 작성할 수 있다.
WITH RECURSIVE cte (n) AS
(
SELECT 1
UNION ALL
SELECT n + 1 FROM cte WHERE n < 5
)
SELECT * FROM cte;
"( )"안에서 만든 결과로 내부 임시 테이블을 생성
그리고 그다음 SELECT 쿼리에서 WITH RECURSIVE절에서 생성된 내부 임시 테이블을 풀 스캔해서 결과 반환
CTE를 사용한 재귀 쿼리의 실행 계획은 "RECURSIVE"로 노출
Rematerialize
8.0부터 래터럴 조인(LATERAL JOIN)이 추가되었으며, 이 경우 래터럴로 조인되는 경우 선행 테이블의 레코드 별로 서브 쿼리가 실행해서 결과를 임시 테이블
에 저장
SELECT
...
employee e LEFT JOIN LATERAL ( SELECT *
FROM salaries s
WHERE s.emp_no = e.emp_no
ORDER BY s.from_date DESC LIMIT 2 ) s2 ON s2.emp_no = e.emp_no
employee
테이블의 레코드 1개 마다
(1) s.emp_no = e.emp_no [emp_no이 일치하는 레코드]
(2) ORDER BY s.from_date DESC [form_date 역순]
(3) LIMIT 2 [2건만]
를 처리하여 임시 테이블을 만든다.
결론 : 테이블의 레코드마다 내부 임시 테이블이 생성
Select tables optimized away
emp_no에 index가 있다고 가정 'optimized way' 최적화가 가능
SELECT MAX(emp_no), MIN(emp_no) FROM emplyoees;
emp_no, from_date 순으로 index가 있다고 가정, 역시 'optimized way'최적화가 가능
SELECT MAX(from_date), MIN(from_date) FROM salaries WHERE emp_no = 10002;
Select temporary, End temporary
Duplicate Weed-out 최적화를 사용하면 'Start temporary','End temporary'가 표시
임시 테이블에 들어가는 테이블의 시작과 끝에 'Start ..' ~ 'End ..'가 표시됩니다.
Duplicate Weed-out
Duplicate Weed-out : Join 중복 제거를 위해 임시 테이블 사용
https://mariadb.com/kb/en/duplicateweedout-strategy/
Unique row not found
두 테이블이 유니크(프라이머리 포함)컬럼으로 조인할 때 일치하는 레코드가 없을 때 표시
Using filesort
Order By를 사용하는 정렬에서 적절한 index를 사용하지 못한 경우 표시,
정렬용 메모리 버퍼에 레코드를 복사해 퀵/힙 소트로 정렬을 수행
Using filesort가 출력된 쿼리가 많이 느리다면 쿼리 튜닝이나 인덱스를 생성하는 것이 좋음
Using index(커버링 인덱스)
데이터 파일을 전혀 읽지 않고 인덱스만 읽어서 쿼리를 모두 처리할 수 있다면 표시
! 가장 큰 부하를 차지하는 부분은 일치하는 키 값들의 레코드를 읽기 위해 데이터 파일을 검색하는 작업
최악의 경우 한 건 한 건마다 IO요청이 생길 수 있다
!
이미지에서는 BookID
로 정렬된 INDEX
가 존재하며 만약 쿼리가 SELECT book_id, name ..
이라면 INDEX Table에 존재하지 않는 name
컬럼 조회를 위해 IO가 생김
INDEX Table의 값만 이용해서 조회시 훨씬 빠르게 조회가 가능함
타겟 index나 clustering table의 PK가 secondary index table에 포함
https://blog.quest.com/sql-server-indexes-key-requirements-performance-impacts-and-considerations/s
Using index condition
Index condition pushdown을 사용 시 표시
Using index for group-by
group by에서 Index가 사용되면 표시
기본적으로 MySQL은 group by 처리를 위해 (1)그루핑 기준 컬럼을 이용해 정렬 작업 수행
그리고 (2)다시 그루핑 하는 형태
2가지 케이스
타이트 인덱스 스캔(인덱스 스캔)을 통한 group by 처리
인덱스를 이용해 GROUP BY를 처리할 수 있더라도 AVG(), COUNT() 같이 모든 값이 필요한 경우 루스 인덱스 스캔을 사용할 수 없다.
SELECT first_name, COUNT(*) AS counter
FROM employees GROUP BY first_name;
루스 인덱스 스캔을 통한 GROUP BY 처리
Grouy By + 루스 인덱스 스캔 조건
단일 컬럼 : 그루핑 컬럼말고는 아무것도 조회하지 않는 경우
다중 컬럼 : group by절이 인덱스를 사용할 수 있어야 하고 MIN(), MAX()같이 조회하는 값이 첫 번째 혹은 마지막 레코드만 읽어도 되는 경우
index(emp_no, from_date) 설정
SELECT emp_no, MIN(from_date), MAX(from_date)
FROM salaries
GROUP BY emp_no;
emp_no 그룹별로 처음 값, 마지막 값만 듬성 듬성 읽으면서 진행이 가능하다.
where절이 있다면 조건이 또 추가된다!
where, group by절 동일한 인덱스를 사용해도 루스 인덱스 스캔을 사용하지 않는 경우가 존재
where 조건에 의해 검색되는 레코드가 적으면 루스 인덱스 스캔을 사용하지 않아도 매우 빠름
루스 인덱스 스캔은 대량의 레코드를 group by할 때 효과가 좋다!
같은 인덱스를 사용하지만 where 조건에 의해 검색되는 레코가 매우 작으면
Using index for group-by가 extra에서 사라짐
WHERE emp_no BETWEEN 10001 AND 10009
GROUP BY emp_no;
Using index for skip scan
MySQL 옵티마이저가 인덱스 스킵 스캔 최적화를 사용하면 표시
8.0부터
루스 인덱스 스캔
을 확장한 인덱스 스킵 스캔이 도입
Using join buffer(Block Nested Loop), Using join buffer(batched key access), using join buffer(hash join)
카테시안 조인은 항상 join buffer를 사용
Using MRR(Multi Range Read)
MySQL 엔진은 실행 계획을 수립하고 스토리지 엔진 API를 호출
스토리지 엔진 레벨에서는 쿼리 실행의 전체적인 문맥을 모름
문제 : 여러 건의 레코드라도 스토리지 엔진은 한 건 한 건 읽어서 반환
MRR : MySQL엔진이 여러 개의 키 값을 한 번에 스토리지 엔진에 전달하고 넘겨받은 키 값들을 정렬해서 최소한의 접근만으로 최적화
Extra에 Using MRR 표시
정확한 내용은 MRR & Batched key access 참조
Using sort_union, Using union, Using intersect
쿼리가 index_merge 접근 방법으로 실행되는 경우에는 2개 이상의 인덱스가 동시에 사용될 수 있음
intersect : 인덱스를 사용할 수 있는 조건이 AND로 연결된 경우 처리 결과에서 교집합으로 추출해내는 작업 수행
union : OR로 연결된 경우 합집합을 추출해내는 작업 수행
sort_union : ??? 모르겠다.
Using temporary
임시 테이블을 사용했다는 의미 하지만 메모리, 디스크 어디에 생성됐는지 알 수 없다.
실행 계획에서는 표시되지 않지만 내부적으로 사용할 때가 많음
대표적으로 메모리, 디스크에 임시테이블을 생성하는 쿼리
- FROM절에 서브쿼리가 포함되는 쿼리의 경우
- COUNT(DISTINCT col)를 포함하는 쿼리가 인덱스를 사용할 수 없는 경우
- UNION, UNION DISTINCT를 사용하는 경우 (UNION ALL 제외)
- 인덱스를 사용하지 못하는 정렬 작업의 경우는 임시 버퍼를 사용하짐나 정렬해야 할 레코드가 많다면 디스크를 사용(Using filesort 출력)
Using where
MySQL 엔진 레이어에서 별도의 가공을 해서 필터링 작업을 처리한 경우 표시
예로 들어
WHERE emp_no BETWEEN 10001 AND 10100
AND gender='F'
Using where
은 BETWEEN 조건 100건의 컬럼을 넘겨서
MySQL 엔진이 gender = F
조건을 필터링해서 부합하지 않는 조건에 대하여 버리는 것을 의미
하지만 실제로 왜 'Using where'이 왜 생겼는지 이해할 수 없을 때도 많음.(당근마켓 DBA도 모른다. 알려하지 말자)
emp_no, gender(where절의 index)로 인덱스가 준비돼 있다면 버리는 레코코드(filtered)없이 필요한 레코드만 읽을 수 있음
Zero limit
때로는 데이터가 아닌 메타 데이터가 필요함
쿼리의 결과가 몇개의 데이터를 가지는지, 컬럼의 타입은 무엇인지 정보만 필요한 경우 "LIMIT 0 "을 사용하면 옵티마이저가 의도를 알고 메타 정보만 반환 이러한 경우 표시