※ 본 글은 "업무에 바로 쓰는 SQL 튜닝" 도서를 학습하며 정리한 글입니다.
🙋🏻♀️ 튜닝 여부를 판단할 때는 짧은 소요 시간만을 기준으로 삼지 않는다. 더 나은 쿼리로 변경할 수 있다면 모두 튜닝 대상이 된다.
show variables like 'profiling%';
set profiling = 'ON'
// 프로파일링된 쿼리 목록 확인 (query id로 구분됨)
show profiles
// 특정 쿼리 ID에 대한 프로파일링된 상세 내용 확인
show profile for query 2
show profile all for query 2
show profile cpu for query 2
show profile block io for query 2
나쁜 예 0.25초
SELECT * FROM 사원 WHERE SUBSTRING(사원번호,1,4) = 1100
좋은 예 0.00초
SELECT * FROM 사원 WHERE 사원번호 BETWEEN 1100 AND 1109
나쁜 예 0.15초
SELECT COUNT(1) FROM 급여 WHERE 사용여부 = 1
좋은 예 0.0초
SELECT COUNT(1) FROM 급여 WHERE 사용여부 = '1'
(이하 생략)
EXPLAIN
select 성, 성별, count(1) as 카운트 from 사원 group by 성, 성별
key : I_성별_성
(해당 인덱스는 성별 열과 성 열 순으로 정렬됨)
⇒ 위 SQL문은 I_성별_성
인덱스의 결과를 다시 ‘성’ 열과 ‘성별’ 열 순으로 재정렬한다 ⇒ 비효율적
개선 코드
select 성, 성별, count(1) as 카운트 from 사원 group by 성별, 성
※ 먼저, count와 group by를 이용해서 데이터가 차지하는 비율을 알아보자
나쁜 예 3.7초
select * from 사원출입기록 where 출입문 = 'B'
I_출입문
인덱스로 인덱스 스캔을 수행하는데(Explain 참고), 이는 인덱스에 접근한 뒤 테이블에 랜덤 엑세스하는 방식이다.좋은 예 0.85초
select * from 사원출입기록 ignore index(I_출입문) where 출입문 = 'B'
나쁜 예 1.21초
select 이름, 성 from 사원
where 입사일자 between STR_TO_DATE('1994-01-01', '%Y-%m-%d')
and STR_TO_DATE('2000-01-01', '%Y-%m-%d')
좋은 예 0.2초
select 이름, 성 from 사원 where YEAR(입사일자) between '1994' and '2000'
I_입사일자
인덱스를 사용하지 못하도록 했다.나쁜 예 13.2초
select 매핑.사원번호, 부서.부서번호 from 부서사원_매핑 매핑, 부서
where 매핑.부서번호 = 부서.부서번호 and 매핑.시작일자 >= '2022-03-01'
좋은 예 0.17초
select STRAIGHT_JOIN 매핑.사원번호, 부서.부서번호 from 부서사원_매핑 매핑, 부서
where 매핑.부서번호 = 부서.부서번호 and 매핑.시작일자 >= '2022-03-01'
나쁜 예
select ~ from ~ where ~ and (select max(연봉) from 급여 where 사원번호 ...)
좋은 예
select ~ from ~ where ~
group by ~
having max(급여.연봉) > 1000000
나쁜 예
select count(distinct 기록.사원번호) as 데이터건수
from 사원, (select 사원번호 from 기록 where 출입문 = 'A') 기록
where 사원.사원번호 = 기록.사원번호
좋은 예
select count(1) as 데이터건수
from 사원 where EXISTS (select 1 from 기록
where 출입문 = 'A' and 기록.사원번호 = 사원.사원번호)
나쁜 예
select 사원.사원번호, 급여.평균연봉, 급여.최고연봉, 급여.최저연봉
from 사원, (select 사원번호, ROUND(AVG(연봉),0) 평균연봉
ROUND(MAX(연봉),0) 최고연봉
ROUND(MIN(연봉),0) 최저연봉
from 급여 group by 사원번호) 급여
where 사원.사원번호 = 급여.사원번호 and 사원.사원번호 between 10001 and 10100
좋은 예
select 사원, 사원번호,
(select ROUND(AVG(연봉),0) from 급여 as 급여1 where 사원번호 = 사원.사원번호) as 평균연봉,
(select ROUND(MAX(연봉),0) from 급여 as 급여2 where 사원번호 = 사원.사원번호) as 최고연봉,
(select ROUND(MIN연봉),0) from 급여 as 급여3 where 사원번호 = 사원.사원번호) as 최저연봉
from 사원
where 사원.사원번호 between 10001 and 10100
나쁜 예
select 사원.사원번호, 사원.이름, 사원.성, 사원.입사일자
from 사원, 급여
where 사원.사원번호 = 급여.사원번호 and 사원.사원번호 between 10001 and 50000
group by 사원.사원번호
order by SUM(급여.연봉) desc
LIMIT 150,10 // 150번째부터 10건
좋은 예
select 사원.사원번호, 사원.이름, 사원.성, 사원.입사일자
from (select 사원번호 from 급여
where 사원번호 between 10001 and 50000
group by 사원번호
order by SUM(급여, 연봉) desc
LIMIT 150,10) 급여, 사원
where 사원.사원번호 = 급여.사원번호
나쁜 예
select distinct 매핑.부서번호 from 관리자, 매핑
where 관리자.부서번호 = 매핑.부서번호 order by 매핑.부서번호
⇒ 두 테이블 모두 데이터에 접근한 뒤 부서번호가 같은지 일일이 확인하는 작업이 과연 필요할까?
좋은 예
select 매핑.부서번호 from (select distinct 부서번호 from 매핑) 매핑
where EXISTS (select 1 from 관리자 where 부서번호 = 매핑.부서번호)
order by 매핑.부서번호
⇒ 중복 제거를 미리 수행하고, select 절에서 활용하지 않는 관리자 데이터는 존재여부만 판단하기
예시 쿼리
select * from 사원 where 이름 = 'George' and 성 = 'Wielonsky'
※ 인덱스 생성 전, 위 쿼리의 인덱스(key)는 null로 확인
인덱스 생성
alter table 사원 add index I_사원_성_이름 (성,이름)
인덱스 생성 확인
show index from 사원
예시 쿼리
select * from 사원 where 이름 = 'Matt' or 입사일자 = '1997-03-31
⇒ 전체 30만건 중, 330건 출력
select count(*) from 사원; // 300024
select count(*) from 사원 where 이름 = 'Matt'; // 233
select count(*) from 사원 where 입사일자 = '1997-03-31'; // 111
show index
를 통해 조건절 열이 포함된 인덱스가 존재하는지 확인 ⇒ I_입사일자 인덱스는 있지만, ‘이름’ 열이 포함된 인덱스는 보이지 않음 ⇒ ‘이름’ 열에 대한 인덱스 추가 필요ALTER TABLE 사원 ADD INDEX I_이름(이름);
큰 규모의 데이터 업데이트에 한해서.
Update 문은 수정할 데이터에 접근한 뒤에 set 절의 수정 값으로 변경하므로, 인덱스로 데이터에 접근한다는 측면에서 인덱스의 존재 여부는 중요하다.
조회한 데이터를 변경하는 범위에는 테이블 뿐만 아니라 인덱스도 포함되므로, 인덱스가 많은 테이블의 데이터를 변경할 때는 성능적으로 불리하다
※ 총 업데이트된 데이터 개수를 알고 싶다면,
select @@autocommit;
: 1 이면 자동 커밋, 0이면 자동 커밋X
set autocommit = 0;
)예시 쿼리
update 출입기록 set 출입문 = 'X' where 출입문 = 'B';
set 출입문 = ‘X’
이므로 “출입문” 열을 포함하는 I_출입문
인덱스의 튜닝 여부를 고민해보자ALTER TABLE 출입기록 DROP INDEX I_출입문;
결과
30초 → 2초 (30만건 데이터가 업뎃 되었을 때)
예시 쿼리
select 사원번호, 이름, 성 from 사원 where 성별 = 'M' and 성 = 'Baba';
I_성별_성
인덱스를 활용하여 데이터에 접근하고 있다.성별 열보다 성 열이 더 다양한 종류의 값을 가지므로 “성” 열을 먼저 활용하면 데이터 접근 범위를 줄일 수 있다.
I_성_성별
인덱스로 변경한다.ALTER TABLE 사원 DROP INDEX I_성별_성, ADD INDEX I_성_성별(성, 성별);