개요
쿼리 실행 절차
- SQL 문장을 쪼개서 MySQL 서버가 이해할 수 있는 수준으로 분리한다.
- SQL 파스 트리가 만들어진다.
- SQL의 파싱 정보를 확인하면서 어떤 테이블부터 읽고 어떤 인덱스를 이용해 테이블을 읽을지 선택한다.
- 불필요한 조건 제거 및 복잡한 연산 단순화
- 여러 테이블의 조인이 있는 경우 순서 결정
- 각 테이블에 사용된 조건과 인덱스 통계 정보를 이용해 사용할 인덱스 결정
- 가져온 레코드들을 임시 테이블에 넣고 다시 한번 가공해야 하는지 결정
- 결정된 테이블 순서나 인덱스를 이용해 스토리지 엔진으로부터 데이터를 가져온다.
옵티마이저의 종류
비용 기반 최적화(Cost-based optimizer, CBO)
- 쿼리를 처리하기 위한 여러가지 방법을 만들고, 각작업의 비용과 예측된 통계 정보를 이용해 실행 계획 별 비용을 산출하고, 최소로 소요되는 처리 방식을 선택한다. 규칙 기반 최적화(Rule-based optimizer, RBO)
- 테이블의 레코드 수나 선택도 등을 고려하지 않고 옵티마이저에 내장된 우선순위에 따라 실행 계획을 수립하는 방식
기본 데이터 처리
풀 테이블 스캔과 풀 인덱스 스캔
풀 테이블 스캔을 하는 경우
- 테이블의 레코드 건수가 너무 작아서 인덱스를 통해 읽는 것보다 풀 테이블 스캔을 하는 편이 더 빠른 경우
- WHERE절이나 ON절에 인덱스를 이용할 수 있는 적절한 조건이 없는 경우
- 인덱스 레인지 스캔을 사용할 수 있는 쿼리지만, 옵티마이저가 판단한 조건 일치 레코드 건수가 너무 많은 경우
Read ahead
- 데이터가 앞으로 필요해지리라는 것을 예측해서 요청이 오기 전에 미리 디스크에서 읽어 InnoDB 버퍼 풀에 가져다 두는 것
- InnoDB 스토리지 엔진은 특정 테이블의 연속된 데이터 페이지가 읽히면 백그라운드 스레드에 의해 Read ahead 작업이 이루어진다.
- 데이터 웨어하우스용으로 MySQL을 사용한다면
innodb_read_ahead_threshold값을 낮은 값으로 설정해서 더 빨리 Read ahead가 시작되게 유도하는 것도 좋은 방법이다.
병렬 처리
innodb_parallel_read_threads시스템 변수를 이용해 하나의 쿼리를 최대 몇 개의 스레드를 이용해서 처리할지를 변경할 수 있다.- 병렬 처리용 스레드 개수가 CPU 코어 개수를 넘어서는 경우에는 오히려 성능이 떨어질 수 있다.
ORDER BY

다음과 같은 이유로 모든 정렬을 인덱스를 이용하도록 튜닝하기란 불가능하므로 Filesort 정렬을 사용한다.
- 정렬 기준이 많아서 요건 별로 모두 인덱스를 생성하는 것이 불가능한 경우
- GROUP BY의 결과 또는 DISTINCT 같은 처리의 결과를 정렬해야 하는 경우
- UNION의 결과와 같이 임시 테이블의 결과를 다시 정렬해야 하는 경우
- 랜덤하게 결과 레코드를 가져와야 하는 경우
Sort buffer
- 정렬을 수행하기 위한 별도의 메모리 공간
- 정렬해야 할 레코드의 건수가 Sort buffer 공간 보다 크다면 레코드를 여러 조각으로 나누고 임시 저장을 위해 디스크를 사용한다.
- 정렬 결과를 디스크에 임시 기록하고 병합한다. (Multi-merge)
- Sort buffer 크기를 크게 설정한다고 성능상의 큰 차이는 없고 오히려 메모리가 부족할 수 있다.
- 디스크의 읽기와 쓰기 사용량은 줄일 수 있다.
정렬 알고리즘
- single pass
- sort buffer에 정렬 기준 컬럼을 포함해 SELECT 대상 모든 컬럼을 담아서 정렬을 수행하는 방식
- two pass
- 정렬 대상 컬럼과 PK 값만 sort buffer에 담아서 정렬을 수행하고, 정렬된 순서대로 다시 PK로 테이블을 읽는 방식
- two pass 방식은 테이블을 두 번 읽어야 하기 때문에 일반적으로 single pass 정렬 방식을 주로 사용한다.
- two pass 방식을 사용하는 경우
- 레코드의 크기가
max_length_for_sort_data시스템 변수에 설정된 값보다 큰 경우 - BLOB이나 TEXT 컬럼이 SELECT 대상에 포함되는 경우
- 레코드의 크기가
정렬 처리 방법

인덱스를 이용한 정렬
- ORDER BY에 사용한 컬럼은 가장 먼저 조회되는 테이블(드라이빙 테이블)의 컬럼이어야 한다.
- ORDER BY에 적힌 컬럼 순서 그대로 인덱스가 만들어져 있어야 정렬 없이 인덱스를 사용할 수 있다.
- WHERE 절이 있다면, WHERE 조건과 ORDER BY가 같은 인덱스를 함께 사용할 수 있어야 한다.
- B-Tree 인덱스만 정렬에 사용 가능하며, 해시 인덱스, 전문 검색 인덱스, R-Tree 인덱스는 사용할 수 없다.
조인의 드라이빙 테이블만 정렬
- 조인에서 드라이빙 테이블의 컬럼만으로 ORDER BY를 작성해야 한다.
임시 테이블을 이용한 정렬
- 하나의 테이블을 정렬하거나 드라이빙 테이블만 정렬하는 것이 아니라면 2개 이상의 테이블이 조인되면서 정렬할 경우 항상 조인의 결과를 임시 테이블에 저장하고, 결과를 다시 정렬한다.
정렬 처리 방법의 성능 비교
- ORDER BY나 GROUP BY 같은 작업은 조건을 만족하는 레코드를 모두 가져와서 정렬을 수행하거나 그루핑 작업을 해야만 LIMIT으로 건수를 제한할 수 있기 때문에 쿼리가 느려지는 경우가 많다.
- 스트리밍 방식
- 서버 쪽에서 조건에 일치하는 레코드가 검색될 때마다 클라이언트로 전송해주는 방식
- LIMIT처럼 결과 건수를 제한하는 조건들은 실행 시간을 줄여줄 수 있다.
- 버퍼링 방식
- 결과를 모아서 서버에서 일고라 가고애야 하므로 모든 결과를 스토리지 엔진으로부터 가져올 때까지 기다려야 한다.
- LIMIT처럼 결과 건수를 제한해도 성능 향상에 도움이 되지 않는다.
- 인덱스를 사용한 정렬 방식만 스트리밍 방식이고 나머지는 모두 버퍼링 후에 정렬된다.
GROUP BY
인덱스 스캔을 이용하는 GROUP BY(타이트 인덱스 스캔)
- 조인의 드라이빙 테이블에 속한 컬럼만 이용해 그루핑할 때 GROUP BY 컬럼으로 인덱스가 있다면 인덱스를 차례대로 읽으면서 그루핑하고, 결과로 조인을 처리한다.
Loose 인덱스 스캔을 이용하는 GROUP BY
- Loose 인덱스 스캔 방식은 인덱스의 레코드를 건너뛰면서 필요한 부분만 읽어서 가져오는 것을 의미한다.
DISTINCT
내부 임시 테이블 활용
임시 테이블이 필요한 쿼리
- ORDER BY와 GROUP BY에 명시된 칼럼이 다른 쿼리
- ORDER BY나 GROUP BY에 명시된 칼럼이 조인의 순서상 첫 번째 테이블이 아닌 쿼리
- DISTINCT와 ORODER BY가 동시에 쿼리에 존재하는 경우 또는 DISTINCT가 인덱스로 처리되지 못하는 쿼리
- UNION이나 UNION DISTINCT가 사용된 쿼리(select_type 칼럼이 UNION RESULT인 경우)
- 쿼리의 실행 계획에서 select_type이 DERIVED인 쿼리
고급 최적화
옵티마이저가 실행 계획을 수립할 때 통계 정보와 옵티마이저 옵션(스위치)을 결합하여 최적의 계획을 수립한다.
옵티마이저 스위치 옵션
optimizer_switch시스템 변수를 이용해 제어한다.- MRR, Batched Key Access, Index Merge, Condition Pushdown 등 다양한 최적화 기능의 활성화 여부를 설정할 수 있다.
MRR과 배치 키 액세스 (MRR & Batched Key Access)
- MRR (Multi-Range Read)
- 드라이빙 테이블의 레코드를 읽어 조인 대상을 버퍼링(조인 버퍼)한 후, 스토리지 엔진에 한 번에 요청하는 방식
- 스토리지 엔진은 읽어야 할 레코드들을 데이터 페이지에 정렬된 순서로 접근하여 디스크 읽기를 최소화한다.
- 읽은 인덱스 값들을 Join Buffer에 모아서 PK 순서대로 정렬한 뒤 레코드에 순차적으로 읽어서 속도를 높이는 기법이다.
- BKA (Batched Key Access) 조인
- MRR을 응용해서 실행되는 조인 방식
- 부가적인 정렬 작업이 필요해 성능에 악영향을 줄 수도 있어 기본 비활성화되어 있다.
Block Nested Loop Join
- 조인 조건에 인덱스를 사용할 수 없는 경우, 드라이빙 테이블의 레코드를 메모리(조인 버퍼)에 캐시한 후 드리븐 테이블과 조인하는 방식
- MySQL 8.0.20 버전부터는 Block Nested Loop Join이 더 이상 사용되지 않고 해시 조인(Hash Join)으로 대체되었다.
ICP(Index Condition Pushdown)
- 인덱스 범위 제한 조건으로 사용하지 못하는 조건이라도 인덱스에 포함된 칼럼이라면 스토리지 엔진으로 전달하여 최대한 필터링한 후 테이블을 읽도록 최적화한다.
- 불필요한 테이블 데이터 읽기(랜덤 I/O)를 줄여 성능을 향상시킨다.
인덱스 확장 (Use Index Extensions)
- InnoDB 스토리지 엔진을 사용하는 테이블에서 세컨더리 인덱스에 자동으로 추가된 프라이머리 키를 활용할 수 있게 할지를 결정하는 옵션
- InnoDB의 세컨더리 인덱스 리프 노드에는 프라이머리 키가 포함되어 있다는 점을 활용한다.
- 옵티마이저가 세컨더리 인덱스의 마지막에 자동으로 추가된 프라이머리 키 칼럼까지 인지하고 실행 계획(Key_len, 정렬 등)에 활용한다.
인덱스 머지 (Index Merge)
- 하나의 테이블에 대해 2개 이상의 인덱스를 이용하여 쿼리를 처리하는 방식이다.
- 교집합 (Intersection)
- 여러 인덱스 검색 결과의 교집합을 반환(AND)
- 합집합 (Union)
- 여러 인덱스 검색 결과의 합집합을 반환(OR)
- 각 인덱스의 결과가 이미 정렬되어 있으므로 별도의 정렬 없이 중복 제거가 가능하다.
- Priority Queue
- 정렬 후 합집합 (Sort Union)
- 각 인덱스의 검색 결과가 정렬되어 있지 않은 경우, 결과를 정렬한 후 중복을 제거하고 합집합을 반환한다.
Semi Join
- 다른 테이블과 실제 조인을 수행하지 않고, 조건 일치 여부만 체크하는 형태의 쿼리를 최적화한다.
- 주로 IN 서브 쿼리
Table Pull-out
- semi join의 서브쿼리에 사용된 테이블을 아우터 쿼리로 끄집어낸 후에 쿼리를 조인 쿼리로 재작성하는 형태의 최적화
First Match
IN형태를EXISTS처럼 처리한다.- 조인 수행 중 일치하는 첫 번째 레코드를 찾으면 검색을 중단하고 반환한다.
Loose Scan
- 인덱스를 사용하는
GROUP BY처럼 서브쿼리 테이블의 인덱스를 루스 인덱스 스캔으로 읽어 중복을 제거하며 조인한다.
구체화(Materialization)
- 서브쿼리를 통째로 실행하여 내부 임시 테이블로 만든 후 조인하는 방식
중복 제거 (Duplicate Weed-out)
- 세미 조인을
INNER JOIN으로 실행하고, 마지막에 중복된 레코드를 제거하는 방식
Condition Fanout Filter
- 조인 시 어떤 테이블을 먼저 처리하느냐에 따라 전체 성능이 크게 달라지기 때문에, 옵티마이저는 단순히 인덱스 사용 여부만 보지 않는다.
- 인덱스를 적용할 수 없는 조건들에 대해서도 레코드가 얼마나 걸러질지를 filtered 값으로 추정하고, 정보를 종합해 가장 적은 데이터가 흐르도록 조인 순서를 결정한다.
파생 테이블 머지 (Derived Merge)
FROM절에 사용된 서브쿼리(파생 테이블)를 외부 쿼리와 병합하여 서브쿼리(임시 테이블 생성)를 제거하는 최적화다.- 집계 함수,
DISTINCT,LIMIT등이 사용된 경우 자동 병합이 불가능할 수 있다.
Invisible Index
- 인덱스를 삭제하지 않고 옵티마이저가 사용하지 못하게 숨기는 기능
- 인덱스 삭제 전 영향도를 테스트할 때 유용하다.
Skip Scan
- 복합 인덱스에서 선행 칼럼에 대한 조건이 없어도 인덱스를 사용할 수 있게 하는 최적화
- 선행 칼럼의 유니크한 값의 개수가 적을 때 효율적이다.
Hash Join
- Nested Loop Joi호n을 사용할 수 없는 경우(인덱스 부재 등) 사용되는 차선책이다.
- 조인 조건의 인덱스가 없거나 레코드 건수가 매우 적은 경우에만 사용된다.
- Hash Join은 첫 번째 레코드를 찾는 데는 시간이 많이 걸리지만 최종 레코들르 찾는 데까지는 시간이 많이 걸리지 않는다.
- Best Throughput 전략에 적합
- Nested Loop Join은 마지막 레코드를 찾는 데까지는 시간이 많이 걸리지만 첫 번째 레코드를 찾는 것은 상대적으로 훨씬 빠르다.
- Best Response-time 전략에 적합ㄴ
- 빌드 단계: 레코드 건수가 적은 테이블을 읽어 메모리에 해시 테이블을 생성한다.
- 프로브 단계: 나머지 테이블을 읽으며 해시 테이블을 검색해 조인한다.
- MySQL 8.0.20부터 Block Nested Loop Join을 대신하여 사용된다.
인덱스 정렬 선호
- ORDER BY를 위한 인덱스에 너무 가중치를 부여하지 않도록 하는 옵션
조인 최적화 알고리즘
- Exhaustive 검색
- 모든 테이블 조인 조합에 대해 비용을 계산 후 비용이 가장 낮은 것을 선택하는 알고리즘
- 항상 최적의 실행 계획을 찾아내지만 테이블이 많으면 시간이 오래 걸린다.
- 5.0 이전에 사용되던 알고리즘
- Greedy 검색
- 모든 경우를 계산하지 않고 당장 최적의 조인 순서를 결정한다.
optimizer_search_depth만큼의 깊이만 탐색- default : 62
- Exhaustive 검색의 문제점을 해결하기 위해 5.0부터 도입
- 모든 경우를 계산하지 않고 당장 최적의 조인 순서를 결정한다.
쿼리 힌트
MySQL 옵티마이저가 비즈니스를 완벽히 이해하지 못해 부족한 실행 계획을 수립할 때 힌트를 사용한다.
인덱스 힌트
- ANSI-SQL 표준 문법은 아니며
SELECT,UPDATE명령에서만 사용 가능하다. - STRAIGHT_JOIN:
SELECT바로 뒤에 사용하여 조인 순서를 고정한다 (FROM 절 명시 순서대로 조인). - USE INDEX: 특정 인덱스를 사용하도록 권장한다.
- FORCE INDEX:
USE INDEX보다 강력하게 사용을 유도한다. - IGNORE INDEX: 특정 인덱스를 사용하지 못하게 한다.
- SQL_CALC_FOUND_ROWS:
LIMIT을 사용한 경우에도 전체 조건을 만족하는 레코드 수를 계산하도록 한다. (성능상 비효율적이므로 사용을 지양하고COUNT(*)쿼리 분리를 권장)
옵티마이저 힌트
- MySQL 5.6부터 도입되었으며 주석(
/*+ ... */) 형태로 사용하여 ANSI-SQL 표준을 준수한다. - 영향 범위에 따라 글로벌, 쿼리 Block, 테이블, 인덱스 수준으로 나뉜다.
- 주요 힌트
MAX_EXECUTION_TIME: 쿼리 실행 시간 제한SET_VAR: 쿼리 실행 동안 특정 시스템 변수 설정MERGE/NO_MERGE: 파생 테이블 병합 여부 제어JOIN_PREFIX/JOIN_SUFFIX: 조인 순서 제어 (드라이빙/드리븐)INDEX_MERGE/NO_INDEX_MERGE: 인덱스 머지 사용 제어SKIP_SCAN/NO_SKIP_SCAN: 인덱스 스킵 스캔 제어