Data Processing(데이터 처리)
쿼리 실행 절차
MySQL 서버에서 쿼리가 실행되는 과정은 크게 세 단계로 나눌 수 있다.
SQL Parsing: 사용자로부터 요청된 SQL 문장을 잘게 쪼개서 MySQL 서버가 이해할 수 있는 수준으로 분리(Parse Tree)
최적화 및 실행 계획 수립: SQL 파싱 정보(Parse Tree)를 확인하면서 어떤 테이블로부터 읽고 어떤 인덱스를 이용해 테이블을 읽을지 결정(Execution Plan)
데이터 읽음: 위에서 결정된 테이블의 읽기 순서나 선택된 인덱스를 이용해 스토리지 엔진으로부터 데이터를 읽어옴
자세한 내용은 쿼리 처리(Query Processing) 문서 참고
쿼리 처리 방법
MySQL에서 쿼리 처리 방법은 스트리밍 처리와 버퍼링 처리 두 가지로 나눌 수 있다.
스트리밍 방식: 조건에 일치하는 레코드가 검색될 때마다 바로 클라이언트로 전송하는 방식
서버 쪽에서 처리할 데이터가 얼마인지 관계 없이 바로 전송
첫 번째 레코드를 처리하자마자 전달할 수 있으므로 데이터 양과 상관 없이 빠른 응답을 받을 수 있음
단순
SELECT
쿼리는 대부분 스트리밍 방식으로 처리
버퍼링 방식: 모든 결과를 스토리지 엔진으로부터 가져와서 서버 쪽에 저장한 뒤 클라이언트로 전송하는 방식
ORDER BY
,GROUP BY
,DISTINCT
와 같이 전체 결과 집합을 대상으로 해야 하는 작업은 반드시 버퍼링이 필요
리드 어헤드(Read ahead)
어떤 영역의 데이터가 필요할 것을 예측해 요청이 오기 전에 미리 디스크에서 읽어 InnoDB 버퍼 풀에 가져다 두는 처리 방식이다.
포그라운드 스레드의 요청이 오기 전에 백그라운드 스레드가 미리 디스크에서 페이지를 읽어 버퍼 풀에 적재하는 방식
풀 테이블 스캔이나 풀 인덱스 스캔과 같이 대량의 데이터를 순차적으로 읽어야 할 때 사용
I/O 대기 시간을 줄여 대용량 순차 읽기 작업의 성능을 향상
테이블 풀 스캔 시 리드 어헤드는 다음과 같은 순서로 진행하게 된다.
풀 스캔이 실행되면 포그라운드 스레드(Foreground Thread)에 의해 처음 몇 개의 데이터 페이지가 읽힘
특정 시점부터 백그라운드 스레드가 읽기 작업을 시작
백그라운드 스레드는 한 번에 읽는 페이지 수를 늘려가며 읽기 작업을 진행
읽은 페이지를 버퍼 풀에 저장
포그라운드 스레드는 백그라운드 스레드가 읽은 페이지(버퍼 풀)에서 가져다가 사용
병렬 처리(Parallel Processing)
하나의 쿼리를 여러 스레드가 작업을 나누어 처리하는 방식이다.
MySQL 8.0에서 병렬 처리 기능이 추가
WHERE 조건 없이 단순이 테이블 전체 건수를 가져오는 경우에만 제한적으로 사용
SET SESSION innodb_parallel_read_threads = 4;
SELECT COUNT(*)
FROM TABLE_NAME;
innodb_parallel_read_threads
시스템 변수로 병렬 처리 스레드 수를 조절할 수 있으며, 과도하게 늘리면 오히려 성능이 떨어질 수 있다.
ORDER BY 처리
정렬을 처리할 때 인덱스를 이용하는 방법과 쿼리가 실행 될 때 Filesort
라는 별도의 처리를 이용하는 방법으로 나뉜다.
인덱스 사용 정렬
인덱스를 이용해 정렬하는 방법은 가장 효율적인 방법으로, 인덱스가 이미 정렬된 상태이기 때문에 인덱스를 순서대로 읽기만 하면 되기 때문에 별도의 정렬 작업이 필요 없다.
Filesort
Filesort
는 인덱스를 이용하지 못하는 경우에 사용하는 정렬 방법으로, 실행 계획의 Extra 컬럼에 Using filesort
로 표시된다.
소트 버퍼(Sort Buffer)
Filesort
를 이용해 정렬을 처리할 때, 정렬을 수행하기 위해 별도의 메모리 공간을 할당 받는 공간을 소트 버퍼(Sort Buffer)라고 하며 아래와 같은 특징을 가진다.
정렬이 필요한 경우에만 메모리 공간을 할당
버퍼의 크기는 정렬해야 할 레코드의 크기에 따라 가변적으로 증가
최대 사용 가능한 소트 버퍼 공간은
sort_buffer_size
라는 시스템 변수로 설정 가능
쿼리 실행이 완료되면 바로 시스템으로 반납
세션 메모리 영역에 해당(클라이언트가 공유해서 사용되는 영역이 아님)
여기서 정렬해야 할 레코드가 메모리에 할당된 소트 버퍼만으로 처리되면 인메모리 정렬로 수행되어 빠르게 처리할 수 있다.
멀티 머지(Multi Merge)
하지만 소트 버퍼를 초과하는 경우엔 레코드를 여러 조각으로 나눠서 처리하며 임시 저장을 위해 디스크를 사용하게 되는데 과정은 아래와 같다.
메모리의 소트 버퍼에서 정렬 수행
그 결과를 임시로 디스크에 기록
다음 레코드를 가져와서 다시 정렬하여 반복적으로 디스크에 임시 저장
버퍼 크기만큼 정렬된 레코드를 다시 병합하면서 정렬
디스크 I/O가 발생하기 때문에 레코드 건수가 많을 수록 위 작업 횟수도 많아지므로 성능이 크게 저하된다.
정렬 처리 방법
ORDER BY가 사용되면 아래 세 가지 방법 중 하나를 선택해서 처리하며, 아래쪽으로 내려갈수록 성능이 떨어진다.
인덱스 사용 정렬
별도 표기 없음
조인에서 드라이빙 테이블만 정렬
Using filesort
조인에서 조인 결과를 임시 테이블로 저장 후 정렬
Using temporary; Using filesort
여기서 드라이빙 테이블은 조인에서 먼저 읽는 테이블을 의미하며 옵티마이저에서 아래의 조건을 만족하는 테이블을 드라이빙 테이블로 선정한다.
크기: 레코드 건 수가 가장 많은 테이블
인덱스: WHERE 절의 검색 조건이나 조인 조건에 사용된 컬럼에 인덱스 존재
1. 인덱스 사용 정렬
인덱스를 이용한 정렬을 사용하기 위해선 아래의 조건을 만족해야 한다.
ORDER BY 절에 사용된 컬럼이 제일 먼저 읽는 테이블에 속함
ORDER BY 순서대로 생성된 인덱스 사용
WHERE절에 첫 번째로 읽는 테이블의 컬럼에 대한 조건(존재 한다면)과 ORDER BY는 같은 인덱스를 사용해야 함
인덱스를 이용해 정렬 처리되는 경우 실제 인덱스 값으로 정렬돼 있기 때문에 그대로 순서대로 읽기만 하면 되기 때문에, 정렬 처리를 위한 별도의 작업 없이 스트리밍 형태로 처리할 수 있다.
2. 조인에서 드라이빙 테이블만 정렬
조인을 하게 되면 레코드 건수가 증가하게 되고, 하나하나의 레코드 크기도 늘어나기 때문에 레코드를 먼저 정렬한 뒤 조인을 수행하여 처리하게 된다.
-- 조건: 조인에서 첫 번째로 읽히는 테이블(드라이빙 테이블)의 컬럼만으로 ORDER BY 가능
SELECT *
FROM employees e,
salaries s
WHERE e.emp_no = s.emp_no
AND e.emp_no BETWEEN 10001 AND 10010
ORDER BY e.last_name;
인덱스를 이용해
emp_no BETWEEN 10001 AND 10010
조건을 만족하는 레코드 검색employees
테이블의last_name
컬럼 정렬 수행정렬된 결과를 순서대로 읽으면서
salaries
테이블과 조인
3. 조인에서 조인 결과를 임시 테이블로 저장 후 정렬
위의 경우를 제외하고 2개 이상의 테이블이 조인되는 경우 임시 테이블에 저장하고 그 결과를 다시 정렬하는 방식을 사용한다.
-- 정렬이 수행 되기 위해선 `salaries` 테이블의 salary 컬럼 필요
SELECT *
FROM employees e,
salaries s
WHERE e.emp_no = s.emp_no
AND e.emp_no BETWEEN 10001 AND 10010
ORDER BY s.salary;
GROUP BY 처리
그룹핑 역시 인덱스를 사용하거나, 인덱스를 사용하지 못할 경우 임시 테이블을 사용한다.
타이트 인덱스 스캔(Tight Index Scan)
인덱스를 차례대로 읽으면서 그루핑 작업을 수행하고 그 결과로 조인을 처리하며, 추가적인 정렬 작업이나 내부 임시 테이블을 사용하지 않는다.
루스 인덱스 스캔(Loose Index Scan)
GROUP BY
컬럼이 인덱스의 선행 컬럼일 때, 각 그룹의 첫 번째 레코드만 읽고 다음 그룹으로 바로 건너뛰어 불필요한 스캔을 최소화한다.
CREATE INDEX idx_empNo_fromDate ON salaries (emp_no, from_date);
EXPLAIN
SELECT emp_no
FROM salaries
WHERE from_date = '2000-01-01'
GROUP BY emp_no;
인덱스가 emp_no
, from_date
으로 생성되어 있으므로 WHERE 조건은 인덱스 레인지 스캔 방식으로 불가능하지만, 실행 계획과 실행 과정은 아래와 같이 진행된다.
실행 계획
1
salaries
range
PRIMARY
Using where; Using index for group-by
실제 쿼리 실행 과정
(emp_no, from_date)
인덱스를 차례대로 스캔하면서emp_no
의 첫 번째 유일한 값(그룹 키) 탐색찾은 emp_no 중에서
from_date
가 '2000-01-01'인 레코드가 있는지 확인해당 단계는 WHERE 조건으로 emp_no AND from_date 인덱스 탐색을 하는 것과 유사하게 동작
(emp_no, from_date)
인덱스에서 emp_no의 다음 유일한 값으로 이동2 - 3단계를 반복하며 emp_no의 유일한 값이 없을 때까지 반복
임시 테이블 사용
인덱스를 사용할 수 없는 경우, 내부적으로 임시 테이블을 생성하여 그룹핑을 처리한다
내부적으로 임시테이블을 생성(실행 계획에서
Using temporary
로 표시)그루핑할 컬럼으로 유니크 인덱스를 생성해, 모든 레코드를 삽입하면서 중복 제거 및 집계 함수 계산
DISTINCT 처리
DISTINCT 처리를 실행 계획에서 인덱스를 사용하지 못하는 경우 항상 임시 테이블이 사용된다.
집합 함수 없는 경우(SELECT DISTINCT)
단순히 조회된 레코드 중에서 유니크한 레코드만 가져올 땐 SELECT DISTINCT
를 사용할 수 있다.
GROUP BY와 유사한 방식으로 처리
특정 컬럼에 대해 유니크한 값을 조회하는 것이 아닌 조회된 레코드 전체에 대해 유니크한 값을 조회
-- fist_name, last_name 조합이 유니크한 레코드만 조회
SELECT DISTINCT fist_name, last_name
FROM employees;
-- first_name만 유니크한 레코드만 조회하는 것이 아닌, 위 쿼리와 동일하게 조회
SELECT DISTINCT(first_name), last_name
FROM employees;
집합 함수와 함께 사용된 DISTINCT
집합 함수 내에서 사용된 DISTINCT는 집합 함수의 인자로 전달된 컬럼값에 유니크한 값만을 대상으로 집계 함수를 수행한다.
EXPLAIN
SELECT COUNT(DISTINCT s.salary) -- s.salary 컬럼의 유니크한 값만 조회
FROM employees e,
salaries s
WHERE e.emp_no = s.emp_no
AND e.emp_no BETWEEN 100000 AND 100100;
내부적으로
COUNT(DISTINCT)
를 처리하기 위해 salary 컬럼의 값만 저장하기 위한 임시 테이블 생성(실행 계획엔 표시되지 않음)생성된 임시 테이블의 salary 컬럼에는 유니크 인덱스가 생성되기 때문에 레코드 건수가 많아질 수록 성능 저하
인덱스를 사용하는 경우
만약 인덱스를 사용할수 있는 컬럼에 대해 DISTINCT를 사용하는 경우 임시 테이블 없이 레인지 스캔 혹은 인덱스 풀 스캔을 수행하여 최적화하여 처리할 수 있다.
SELECT COUNT(DISTINCT emp_no)
FROM employees;
SELECT COUNT(DISTINCT emp_no)
FROM employees
GROUP BY emp_no
참고자료
Last updated
Was this helpful?