상세 컨텐츠

본문 제목

기본적인 SQL 튜닝 학습하기

기록 - 프로그래밍/Data

by wjjun 2022. 2. 23. 23:10

본문

쿼리 튜닝 학습 목적으로 전체 내용은

링크된 업무에 바로 쓰는 SQL 튜닝 - 한빛미디어 도서를 참고하여

글을 작성하였습니다



SQL 실행 과정

(1) 사용자가 요청합니다 (SQL을 실행합니다)

(2) 파서(parsor)가 구문을 해석합니다 (SQL을 최소 단위로 분리, 구성 요소를 트리로 작성합니다)

(3) 전처리기(preprocessor)가 실행됩니다 (트리의 구성요소로 권한/존재 여부 확인)

(4) 옵티마이저 (optimizer) (엔진 실행기에 실행계획을 전달)

(5) 엔진 실행기 (engine executor) (스토리지 엔진을 이용해 data file을 가져옵니다)

(6) 가져온 데이터의 최종결과를 사용자에게 전달합니다

용어 정리

서브쿼리

쿼리 안의 보조쿼리입니다

메인쿼리

가장 바깥쪽 SELECT 문의 쿼리입니다

SELECT ( SELECT ... FROM ...) # SELECT 절 : 스칼라 서브쿼리 
    FROM ( SELECT ... FROM ...) # FROM 절 : 인라인 뷰
WHERE 컬럼명 IN (SELECT... FROM ...) # WHERE 절 : 중첩 서브쿼리

스칼라 서브쿼리

출력되는 데이터 건수가 1건이어야 하므로 집계함수가 자주 사용됩니다

SELECT 이름, 
          (SELECT COUNT(*)
              FROM 학생 AS 학생2
           WHERE 학생2.이름 = 학생1.이름) 카운트
FROM 학생 AS 학생1;

인라인 뷰

메인쿼리의 FROM 절에 있는 또 다른 SELECT 절이 인라인 뷰입니다.

FROM 절 내부에서 일시적으로 뷰를 생성하는 방식이므로 인라인 뷰라고 부릅니다

인라인 뷰의 결과는 내부적으로 메모리 또는 디스크에 임시 테이블을 생성하여 활용합니다

SELECT 학생2.학번, 학생2.이름
        FROM (SELECT *
                FROM 학생
              WHERE 성별 = '남') 학생2;

중첩 서브쿼리

메인 쿼리의 WHERE 절에 있는 또 다른 SELECT 절을 중첩 서브쿼리입니다

WHERE 절에서 단순히 값을 비교 연산하는 대신, 서브쿼리를 추가하여 비교 연산하기 위해 중첩 서브쿼리를 사용합니다

WHERE 절에서 중첩 서브쿼리와 비교할 때는 보통 (=, <, >, ≤, ≥, <>,≠를 비롯해 IN, EXISTS, NOT INm NOT EXISTS문을 많이 사용합니다

SELECT 학생2.학번, 학생2.이름
        FROM (SELECT *
                FROM 학생
              WHERE 성별 = '남') 학생2

상관 서브쿼리

메인쿼리와 서브쿼리 간에 관계성이 있는 것을 의미합니다

서브쿼리가 수행되려면 메인쿼리 값을 받아야 합니다. 서브와 메인쿼리는 끈끈한 관계를 유지합니다

SELECT ...
    FROM 학생
WHERE ... IN (SELECT ...
                  FROM 지도교수
              WHERE 학생.학번 = ...)

메인 쿼리에서 학생 테이블의 학번 결과를 서브쿼리로 전달한 뒤 지도교수 테이블의 학번과 비교 (지도교수.학번 = 학생.학번)합니다. 이렇게 도출된 서브쿼리의 학번 결과를 메인쿼리의 학번과 비교해 최종 결과를 출력합니다

수행순서

1) 메인쿼리 실행(학생.학번 데이터 가져오기)

2) 서브쿼리 실행(지도교수.학번 = 학생. 학번)

3) 다시 메인쿼리 실행한 뒤 결과 출력 (SELECT * FROM 학생~)

비상관 서브쿼리 : 메인쿼리와 서브쿼리 간 관계성이 없는 것입니다

SELECT ...
    FROM 학생
WHERE ... IN (SELECT ... # 메인쿼리 = 외부쿼리와 서브쿼리 = 내부쿼리 연관성이 없습니다
                FROM 지도교수 )

메인쿼리와 서브쿼리의 독립적인 관계

비상관 서브쿼리가 먼저 실행되고 그 결과를 메인쿼리가 활용합니다. 즉, 서브쿼리 실행 → 메인쿼리 실행 순서입니다

단일행 서브쿼리

SELECT ...
    FROM ...
 WHERE 학번 = 서브쿼리 (SELECT MAX(학번) FROM 학생) #출력값이 하나인 서브쿼리

서브쿼리 결과가 항상 하나의 값만 반환됩니다

다중행 서브쿼리

SELECT ...
    FROM ...
 WHERE 학번 IN 서브쿼리 (SELECT MAX(학번) FROM 학생 GROUP BY 전공코드) # 출력값이 두개 이상인 서브쿼리

서브쿼리 결과가 여러 건의 행으로 반환되는 쿼리입니다

학생 테이블에서 전공코드별 학번 최댓값을 반환하므로 전공 코드의 종류만큼 학번의 최댓값들이 반환됩니다. 이후 메인 쿼리가 수행되고 최종 결과가 나옵니다

다중행 서브쿼리

서브쿼리 결과가 여러개의 행으로 나오는 것을 의미합니다

출력값이 2개 이상인 서브쿼리

이름 # 컬럼
홍ㅇㅇ # 결과 행1
김ㅇㅇ # 결과 행2

다중열 서브쿼리

서브쿼리 결과가 여러 개의 열과 행으로 반환됩니다

출력값으로 2개 이상의 컬럼을 출력하는 서브쿼리

이름 | 나이 | 성별 # 컬럼
홍ㅇㅇ | 20 | 남 # 결과 행과 열1
김ㅇㅇ | 25 | 여 # 결과 행과 열2

내부조인

양쪽 테이블에 같은 데이터가 있는 데이터만 해당됩니다

외부조인

왼쪽 외부조인(LEFT OUTER JOIN table ON)은 왼쪽 테이블에만 있는

오른쪽 외부조인(RIGHT OUTER JOIN table ON)은 오른쪽 테이블에만 있는 데이터만 해당됩니다

중첩 루프 조인

드라이빙 테이블의 데이터 1건당 드리븐 테이블을 반복해서 검색하며 최종적으로 양쪽 테이블에 공통된 데이터를 출력합니다

SELECT 학생.학번, 학생.이름,
             비상연락망.관계, 비상연락망.연락처
        FROM 학생
        JOIN 비상연락망
            ON 학생.학번 = 비상연락망.학번
    WHERE 학생.학번 IN (1, 100)

극단적인 중첩루프 예시입니다

학번 1과 100에 해당하는 학생들의 ‘학생’ 테이블 정보를 가져옵니다

가져온 학생 테이블 정보 1건 당 ‘비상연락망’ 테이블의 데이터 10,000건에 모두 접근합니다

이 경우,

학번 1 데이터를 찾기 위해 100 + 10,000건

학번 100 데이터를 찾기 위해 100 + 10,000건

총 22,000 건의 데이터를 접근해야 합니다

인덱스를 이용해 테이블의 데이터를 찾아가는 과정에서 임의 접근 방식인 랜덤 액세스가 발생합니다

따라서, 랜덤 액세스를 줄일 수 있도록 데이터 액세스 범위를 좁혀지도록 인덱스를 설계하고 조건절을 작성해야 합니다

기본 키는 인덱스가 순서대로 되어 있어 조회 효율이 높습니다

블록 중첩 루프 조인

중첩 루프 조인의 효율성을 높이기 위해 등장했습니다

드라이빙 테이블에 조인 버퍼(join buffer) 개념을 도입하여 성능을 향상시킵니다

학생 인덱스 1, 100 → 조인버퍼(메모리) 0 → 2, 3, 4, 5

[학생테이블]                          [비상연락망 테이블]
학번 | 이름                          학번 | 관계 | 연락처
1   | 정ㅇㅇ ----> 조인버퍼 : 0 <--- 2   1 | 어머니 | ...
2   | 김ㅇㅇ                0 <--- 3   1 | 아버지 | ...
                          0 <--- 4   2 | 어머니 | ...

해시 조인

조각 테이블의 데이터를 내부적으로 해시값으로 만들어 내부 조인을 수행합니다

해시값으로 내부 조인을 수행한 결과는 조인 버퍼에 저장되므로 조인 열의 인덱스를 필수로 요구하지 않습니다

[학생테이블]                        [비상연락망테이블]
학생 | 이름 -----> 조인 버퍼 <------ 학번 | 관계 | 연락처
1 | 김ㅇㅇ    data1 | Hash#1       1  | 어머니 | ...
            data1 | Hash#2       1  | 아버지 | ...   
2 | 홍ㅇㅇ    data2 | Hash#3       2  | 어머니 | ...
3 | 이ㅇㅇ    data3 | Hash#4       3  | 아버지 | ...    
100 | 금ㅇㅇ  data100 | Hash#n     100  | 어머니 | ...

오브젝트 스캔

테이블 풀스캔

인덱스를 거치지 않고 테이블로 바로 접근하여 처음부터 끝까지 훑는 방식입니다

인덱스 범위 스캔

인덱스를 범위 기준으로 테이블의 데이터를 찾아가는 방식입니다z

SQL 문에서 BETWEEN ~ AND 구문이나 <, >, LIKE 구문 등 비교 연산 및 구문이 포함된 경우 인덱스 범위 스캔으로 수행합니다

좁은 범위 조회시 효율적이나 넓은 범위 스캔시에는 비효율적입니다

인덱스 풀 스캔

인덱스를 기준으로 처음부터 끝가지 수행하는 방법입니다

인덱스 고유 스캔

기본 키나 고유 인덱스로 테이블에 접근하는 방식입니다

인덱스를 사용하는 스캔 방식 중 가장 효율적인 스캔 방식입니다

인덱스 루스 스캔

인덱스의 필요한 부분만 골라 스캔하는 방식입니다

WHERE 절 조건문 기준으로 필요한 데이터와 필요하지 않은 데이터를 구분한 뒤 불필요한 인덱스 키는 무시합니다

GROUP BY 구문이나 MAX(), MIN() 함수가 포함되면 작동합니다

인덱스 병합 스캔

테이블 내 생성된 인덱스들을 통합해서 스캔하는 방식입니다

WHERE문 조건절의 열들이 서로 다른 인덱스로 존재하며 옵티마이저가 해당하는 인덱스를 가져와 모두 활용하는 방식입니다. 통합 방법으로는 결합과 교차 방식이 있으며 실행 계획으로 출력됩니다

튜닝방법 : 별개로 생성된 인덱스들은 보통 하나의 인덱스로 통합하여 SQL을 튜닝하거나 SQL 문 자체를 하나의 인덱스만 수행하도록 변경할 수 있습니다

엑세스 조건

WHERE 절의 특정 조건문을 이용해 소량의 데이터를 가져오고

인덱스를 통해 조건절을 선택합니다

SELECT *
        FROM TAB
    WHERE ID = 1
            AND CODE = 'A'; 

1번 테이블의 ID 인덱스를 이용해서 2번 테이블의 일부 데이터에만 접근하는 것을 볼 수 있습니다

ID = 1 로 일부 데이터에만 접근합니다

쿼리튜닝 (튜닝절차)

(1) SQL문 실행결과와 현황파악

  • 결과 및 소요시간 확인
  • 조인/서브쿼리 구조
  • 동등/범위 조건

(2-1) 가시적

  • 테이블 데이터 건수
  • SELECT절 컬럼 분석
  • 조건절 컬럼 분석
  • 그루핑/정렬 검색

(2-2) 비가시적

  • 실행계획
  • 인덱스 현황
  • 데이터 변경 추이
  • 업무적 특징

(3) 튜닝 방향 판단, 개선/적용


SQL 문 단순 수정으로 착한 쿼리 만들기

기본 키를 변형하는 나쁜 SQL문

[튜닝전]

SELECT *
    FROM 사원
WHERE SUBSTRING (사원번호, 1, 4) = 1100
AND LENGTH(사원번호) = 5;

사원 번호는 인덱스키 입니다

사원번호를 그래로 쓰는 대신 SUBSTRING, LENGTH와 같이 가공하여 작성했기 때문에

기본 키를 사용하지 않고 테이블 풀 스캔을 수행합니다

[튜닝후]

SELECT *
    FROM 사원
WHERE 사원번호 BETWEEN 11000 AND 11009

범위 검색을 수행하고 비교연산자를 사용하였습니다

그 결과, 사원번호가 변형되지 않아 기본키나 인덱스를 활용할 수 있게 되었습니다

사용하지 않는 함수를 포함하는 나쁜 SQL문

[튜닝전]

SELECT IFNULL(성별, 'NO DATA') AS 성별, COUNT(1) 건수
    FROM 사원
GROUP BY IFNULL(성별, 'NO DATA')

Key 항목이 1_성별_성 인덱스로 나타내어 인덱스 풀 스캔 방식으로 수행됩니다

Extra 항목이 ‘Using temporary(임시 테이블)’ 이므로 임시 테이블을 생성합니다

(설명추가) 임시테이블(Using Temporary)이 필요한 쿼리
  • ORDER BY와 GROUP BY에 명시된 칼럼이 다른 쿼리
  • ORDER BY와 GROUP BY에 명시된 칼럼이 조인의 순서상 첫 번째 테이블이 아닌 쿼리
  • DISTINCT 와 ORDER BY 가 동시에 쿼리에 존재하는 경우 또는 DISTINCT 가 인덱스로 처리되지 못하는 쿼리
  • UNION 이나 UNION DISTINCT 가 사용된 쿼리(select_type 칼럼이 UNION RESULT 인 경우)
  • UNION ALL이 사용된 쿼리 (select_type 칼럼이 UNION RESULT 인 경우)
  • 쿼리 실행 계획에서 select_type 이 DERIVED인 쿼리 어떤 쿼리의 실행 계획에서 임시 테이블이 사요하는지 Extra 칼럼에 "Using temporary" 라는 키워드가 표시되는지 확인하면 된다. 단 마지막 3개패턴은 키워드가 표시되지 않는다.

[튜닝후]

SELECT 성별, COUNT(1) 건수
    FROM 사원
GROUP BY 성별

Extra 항목이 ‘Using index’ 이므로 임시 테이블 없이 인덱스만 사용하여 데이터를 추출합니다

IFNULL() 함수를 처리하려고 DB 내부적으로 별도 임시 테이블을 만들어 NULL 여부를 검사할 필요가 없습니다. 즉, 성별 열에 NULL 값이 존재할 수 없어 불필요한 로직은 튜닝 대상입니다

형변환으로 인덱스를 활용하지 못하는 나쁜 SQL문

[튜닝전]

SELECT 사용여부, COUNT(1)
    FROM 급여
    GROUP BY 사용여부 = 1

문자 유형인 사용여부를 숫자 유형 1로 써서 DBMS 내부에서 묵시적 형변환이 발생하였습니다

이 경우, 인덱스를 활용하지 못하고 전체 데이터를 스캔하게 됩니다

[튜닝후]

SELECT 사용여부, COUNT(1)
    FROM 급여
    WHERE 사용여부 = '1'

열을 결합하여 사용하는 나쁜 SQL문

[튜닝전]

SELECT * 
    FROM 사원
    WHERE CONCAT(성별, ' ' , 성) = 'M Radwan'

결합한 결과로 데이터로 조회시 인덱스 풀스캔을 합니다

[튜닝후]

SELECT * 
    FROM 사원
    WHERE 성별 = 'M'
        AND 성 = 'Radwan'

습관적으로 중복을 제거하는 나쁜 SQL문

[튜닝전]

SELECT DISTINCT 사원.사원번호, 사원.이름, 사원.성
    FROM 사원
    JOIN 부서관리자
    ON (사원.사원번호 = 부서관리자.사원번호)

DISTINCT 구문을 사용하여 중복 제거하여 조회하는 쿼리입니다

DISTINCT 사용시 실행계획에서 임시 테이블(Using temporary) 생성되는 것을 확인할 수 있습니다

[튜닝후]

SELECT 사원.사원번호, 사원.이름, 사원.성
    FROM 사원
    JOIN 부서관리자
    ON (사원.사원번호 = 부서관리자.사원번호)

중복된 데이터가 없다면 DISTINCT를 제거해 줍니다

다수 쿼리를 UNION 연산자로만 합치는 나쁜 SQL문

[튜닝전]

SELECT 'M' AS 성별, 사원번호
        FROM 사원
    WHERE 성별 = 'M' 
        AND 성 = 'Babe'

UNION

SELECT 'F', 사원번호
        FROM 사원
    WHERE 성별 = 'F'
        AND 성 = 'Babe'

SELECT 1, SELECT 2 각 결과 id(Using Index)를 통합하여 중복을 제거합니다

이때, 임시 테이블을 생성하고 내부에서 각 결과의 UNION 작업이 수행됩니다

id 1과 id 2 행의 결과량이 많으면 메모리가 아닌 디스크에 임시 파일을 생성하여 UNION 작업을 수행합니다

실행계획

Extra Using index

Extra Using index

Extra Using temporary

[튜닝후]

SELECT 'M' AS 성별, 사원번호
        FROM 사원
    WHERE 성별 = 'M' 
        AND 성 = 'Babe'

UNION ALL

SELECT 'F' AS 성별, 사원번호
        FROM 사원
    WHERE 성별 = 'F'
        AND 성 = 'Babe'

튜닝전 SQL문에서 첫번째 SELECT 문과 두번째 SELECT 문의 결과 데이터가 중복되지 않으므로 중복을 제거하는 작업이 필요하지 않습니;다

따라서 임시 테이블을 만들고 정렬한 뒤, 중복을 제거하는 UNION 연산자 대신 결괏값을 단순히 합치는 UNION ALL 연산자로 변경해주어야 합니다

인덱스 고려 없이 열을 사용하는 나쁜 SQL문

[튜닝전]

SELECT 성, 성별, COUNT(1) as 카운트
    FROM 사원
GROUP BY 성, 성별

성과 성별 순서로 그루핑하여 데이터 건 수를 구하는 쿼리입니다

기존 성별_성 순서로 구성된 인덱스를

성_성별 순서로 재구성 하기 위해 임시테이블을 생성하게 됩니다

[튜닝후]

SELECT 성, 성별, COUNT(1) as 카운트
    FROM 사원
GROUP BY 성별, 성

이미 존재하는 성별_성 인덱스를 최대한 활용하기 위해

성별, 성 순서로 그루핑하였습니다. 그럼 별도 임시테이블을 생성하지 않고도 그루핑과 카운트 연산 수행이 가능합니다

엉뚱한 인덱스를 사용하는 나쁜 SQL문

[튜닝전]

SELECT 사원번호
    FROM 사원 USE INDEX(I_입사일자)
    WHERE 입사일자 LIKE '1989%'
    AND 사원번호 > 100000;

USE INDEX 힌트를 설정한 경우 인덱스로 테이블을 스캔하지만

인덱스 루스 스캔 (Using index for skip scan) 방식에 의해 인덱스를 스킵(skip)하는 오버헤드가 발생할 수 있습니다

[튜닝후]

SELECT 사원번호
    FROM 사원
    WHERE 입사일자 >= '1989-01-01' AND 입사일자 < '1990-01-01'
    AND 사원번호 > 100000;

기존 입사일자 LIKE ‘1989%’ 조건절을

입사일자 ≥ ‘1989-01-01’ AND 입사일자 < ‘1990-01-01’ 같이 변경했습니다

LIKE 절보다 부등호 조건절이 우선하여 인덱스를 사용하므로 데이터 접근 범위가 줄어듭니다

동등 조건으로 인덱스를 사용하는 나쁜 SQL문

[튜닝전]

SELECT *
        FROM 사원출입기록
    WHERE 출입문 = 'B'; 

출입문 B는 총 66만 건의 전체 데이터를 조회하고 중 30만 건을 차지하고 있습니다

이때 실행계획은 출입문 인덱스로 인덱스를 스캔합니다

여기서 전체의 50% 데이터를 조회하기 위해 인덱스를 활용하는게 맞는지 고민해야 합니다

[튜닝후]

SELECT * 
    FROM 사원출입기록 IGNORE INDEX(I_출입문)
WHERE 출입문 = 'B'

옵티마이저 로직에 따라 인덱스를 활용하는 방식으로 수행합니다. 사실 내부 옵티마이저 내부 알고리즘은 완벽하지 않습니다

내가 의도한 대로 SQL문이 수행이 되지 않는다면 IGNORE INDEX를 힌트로

인덱스를 무시하고 조회하도록 괄호 안에 직접 작성할 수 있습니다

튜닝후 수행결과는 총 30만 건만 조회합니다

범위 조건으로 인덱스를 사용하는 나쁜 SQL문

[튜닝전]

입사일자 인덱스로 범위 스캔을 수행합니다

Using index condition로 스토리지 엔진에서 입사일자 조건절로 인덱스 스캔을 수행합니다

SELECT 이름, 성
    FROM 사원
    WHERE 입사일자 BETWEEN STR_TO_DATE('1994-001-01', '%Y-%m-%d')
                                     AND STR_TO_DATE('2000-12-31', '%Y-%m-d') 

[튜닝후]

SELECT 이름, 성
    FROM 사원
    WHERE YEAR(입사일자) BETWEEN '1994' AND '2000'

입사일자 열로 생성한 인덱스를 사용하지 않게 의도적으로 인덱스 열을 변형한 쿼리입니다.

WHERE 조건절을 YEAR로 가공하여 입사일자 인덱스를 사용하지 못하도록 했습니다

사원 테이블 풀스캔 방식으로 데이터에 접근합니다

인덱스 없이 테이블에 직접 접근하며 한 번에 다수의 페이지에 접근하여 더 효율적입니다

테이블 조인 설정 변경으로 착한 쿼리 만들기

작은 테이블이 먼저 조인에 참여하는 나쁜 SQL문

[튜닝전]

SELECT 매핑.사원번호,
         부서.부서번호
    FROM 부서사원_매핑 매핑,
             부서
    WHERE 매핑.부서번호 = 부서.부서번호
        AND 매핑.시작일자 >= '2002-03-01

부서사원_매핑 테이블은 중첩 루프 조인을 수행합니다

작은 크기의 부서 테이블에서 부서.부서번호 열만 SELECT, WHERE 절에 필요하므로 부서명 인덱스를 활용해 인덱스 풀 스캔을 합니다

상대적으로 큰 부서사원_매핑 테이블은 부서번호 인덱스로 인덱스 스캔을 수행합니다. 인덱스 스캔을 하고 랜덤 액세스로 테이블에 접근하게 됩니다

대량의 데이터에 대해 랜덤 액세스하면 비효율 적입니다

[튜닝후]

SELECT STRAIGHT JOIN
                매핑.사원번호,
                부서.부서번호
    FROM 부서사원_매핑 매핑, 부서
WHERE 매핑.부서번호 = 부서.부서번호
    AND 매핑.시작일자 >= '2002-03-01'

부서사원_매핑 테이블은 랜덤 액세스 없이 테이블 풀 스캔으로 한 번에 다수의 페이지에 접근합니다.

그리고 테이블에서 추출된 데이터만큼 반복하여 부서 테이블에 접근하게 됩니다

정리하면, 상대적으로 대용량인 부서사원_매핑 테이블을 테이블 풀 스캔으로 처리하고, 부서 테이블에는 기본 키로 반복 접근하여 1개의 데이터에만 접근하는 식으로 수행합니다

메인 테이블에 계속 의존하는 나쁜 SQL문

[튜닝전]

SELECT 사원.사원번호, 사원.이름, 사원.성
    FROM 사원
    WHERE 사원번호 > 450000
    AND ( SELECT MAX(연봉)
                        FROM 급여
                        WHERE 사워번호 = 사원. 사원번호
            ) > 100000    

사원번호 추출 대상을 정의하고, 중첩 서브쿼리의 급여 테이블에서 메인 테이블의 사원번호를 받아와 사원의 최대 연봉을 확인합니다

사원 테이블에는 30만 건의 데이터가 있습니다. 급여 테이블은 50만 건의 데이터가 있습니다.

사원 번호가 450000를 초과하는 데이터는 5만 건으로 전체 데이터의 약 15%입니다

튜닝 대상 SQL문에 사용하는 인덱스는 모두 기본키입니다

사원 테이블은 사원번호 열로 구성된 기본 키를 사용하고, 급여 테이블은 사원번호와 시작일자로 구성된 기본 키를 사용합니다

[튜닝후]

SELECT 사원.사원번호,
             사원.이름,
             사원.성,
    FROM 사원,
             급여
 WHERE 사원.사원번호 > 450000
     AND 사원.사원번호 = 급여.사원번호
    GROUP BY 사원.사원번호
HAVING MAX(급여.연봉) > 100000

그룹별 최댓값을 계산하도록 개선합니다.

GROUP BY 절은 사원번호별 그루핑을 수행하고

HAVING 절에서는 연봉의 최댓값으로 조건을 설정하여 원하는 데이터를 조회합니다

테이블에 접근하기 위한 범위 축소 조건은 사원.사원번호 > 450000 절을 통한

급여.사원번호 > 450000 조건절로 변형되어 적용됩니다. 이는 옵티마이저에 의해 SQL문이 재작성된 부분으로 수행됩니다

튜닝전 서브쿼리 방식은 제거되고 사원, 급여 테이블을 단순히 조인하는 방식으로 변경되었습니다

불필요한 조인을 수행하는 나쁜 SQL문

[튜닝전]

SELECT COUNT(DISTINCT 사원.사원번호) as 데이터건수
    FROM 사원,
        (SELECT 사원번호
            FROM 사원출입기록 기록
                WHERE 출입문 = 'A'
         ) 기록
    WHERE 사원.사원번호 = 기록.사원번호

사원출입기록 테이블은 인덱스를 활용하여 A 출입문 기록이 있는 사원번호를 구합니다

‘A’인 상수와 직접 비교하므로 ref 항목이 const로 출력됩니다

인덱스를 사용한 동등(=) 비교를 수행하므로 type 항목이 ref로 표시됩니다

사원 테이블은 기본키를 사용해서 조인 조건절인 사원번호 열로 데이터를 비교합니다.

[튜닝후]

SELECT COUNT(1) as 데이터건수
    FROM 사원
    WHERE EXISTS (SELECT 1
                                    FROM 사원출입기록 기록
                                        WHERE 출입문 = 'A'
                                        AND 기록.사원번호 = 사원.사원번호)

사원출입기록 테이블의 데이터는 최종 결과에 사용하지 않고 단지 존재 여부만 파악하면 되므로 EXIST 구문으로 변경합니다

먼저, 15만 건의 사원 테이블 데이터 집계 결과가 출력됩니다

서브쿼리는 사원출력기록 테이블에서 EXIST 연산자로 데이터 존재 여부 파악을 위해 임시 테이블을 생성하는 MATERIALIZED로 표기됩니다

4장에서는 기본적인 SQL 튜닝 방법과 예시를 통해 학습할 수 있었습니다
5장에서는 실무에 활용하면 조금 더 도움이 될 수 있는 내용이 포함되어 있습니다

처음부터 모든 데이터를 가져오는 나쁜 SQL문
비효율적인 페이징을 수행하는 나쁜 SQL문
필요 이상으로 많은 정보를 가져오는 나쁜 SQL문
대량의 데이터를 가져와 조인하는 나쁜 SQL문
인덱스를 하나만 사용하는 나쁜 SQL문
큰 규모의 데이터 변경으로 인덱스에 영향을 주는 나쁜 SQL문
비효율적인 인덱스를 사용하는 나쁜 SQL문
잘못된 열 속성으로 비효율적으로 작성한 나쁜 SQL문
대소문자가 섞인 데이터와 비교하는 나쁜 SQL문
분산 없이 큰 규모의 데이터를 사용하는 나쁜 SQL문

관련글 더보기

댓글 영역