본문 바로가기

Data Engineering/OnPrem_DataBase

[DataBase] 옵티마이저

** 데이터 진흥원 - SQL 전문가 가이드의 [과목5 : SQL 옵티마이저] 내용


1) SQL 옵티마이징 원리

◼︎ 옵티마이저 : 규칙과 비용을 고려하여 실행계획을 수립하는 엔진 (최근에는 Self-Learning 기능들이 추가되고 있다.)

◼︎ 고려 요인

    (1) 연산자 형태

    (2) 옵티마이징 팩터 (인덱스, IOT, 클러스터링, 파티셔닝, MV 등)

          -> 사용자가 최적의 옵티마이징 팩터를 제공하지 않으면 성능은 나빠질 수밖에 없다.

    (3) DBMS 제약 설정 (PK, FK, Check, Not Null 등)

    (4) 옵티마이저 힌트

    (5) 통계정보 (비용 기반 계획 수립의 토대)

           -> 통계정보를 수집/관리하는데 막대한 리소스가 필요하여 관리 복잡도가 높아질수록 잘못된 근거가 될 수 있다.

    (6) 옵티마이저 관련 파라미터 (DBMS 버전에 따라 변경됨)

    (7) DBMS 버전과 종류 

    (8) 바인딩 변수의 사용 여부

           -> 프로시저와 같이 변수를 사용하는 경우, 균등 분포를 가정하고 보편적인 실행계획을 세워 저장한 뒤,

                변수만 바꿔가면서 동일한 실행 순서를 진행한다. (분포가 불균일한 데이터의 경우 비효율이 발생할 수 있다)

 

◼︎ 비용 계산 원리 (사용 통계정보)

통계유형  세부 통계 항목
테이블 통계 전체 레코드 수, 총 블록 수, 빈 블록 수, 한 행당 평균 크기 등
인덱스 통계 인덱스 높이, 리프 블록 수, 클러스터링 팩터, 인덱스 레코드 수 등
칼럼 통계 값의 수, Min/Max 값, 밀도, null 개수, 컬럼 히스토그램 등
시스템 통계 CPU속도, 평균 I/O 속도, 초당 I/O 처리량 등

 

선택도 산정 -> 카디널리티 측정 -> 비용 산출 -> 엑세스 방식 / 조인 순서 / 조인 방법 결정

 

** 선택도 : 전체 중 특정 조건에 의해 선택될 것으로 예상되는 레코드 비율 (기본적으로는 균등분포를 가정하나, 히스토그램을 참조)

     

    + 히스토그램의 x축은 특정 구간일 수도(도수분포), 동일한 갯수의 행을 갖는 범위(높이균형)일 수도 있다.

    + 조건이 다중 컬럼을 참조하면 계산의 복잡도가 더 높아진다.

 

     균등 분포일 때 선택도는 "1 / distinct value의 개수"가 된다

     

** 카디널리티 : "전체 행수 * 선택도" : 특정 조건을 취했을 때 결과로 받을 행의 갯수를 의미

 

** 비용(I/O Call 비용)

산식 항 별 설명
blevel                                 -- 인덱스 수직적 탐색 비용

+ (리프 블록 수                    
     * 유효 인덱스 선택도)      -- 인덱스 수평적 탐색 비용


+ (클러스터링 팩터
    * 유효 테이블 선택도)       -- 테이블 랜덤 엑세스 비용
blevel : 브랜치 레벨 (리프 블록까지 읽게 될 블록의 개수)


유효 인덱스 선택도 : 전체 인덱스 중 조건을 만족하는 스캔 대상 비율 (방문 비율)

클러스터링 팩터 : 특정 컬럼에서 같은 값들이 모여있는 정도
유효 테이블 선택도 : 필터 완료 후, 방문 예상 되는 블록수 / 테이블 전체 블록 수

 


2) SQL 공유 및 재사용

◼︎ 실행계획의 최적화 과정은 상당한 리소스를 소모하는 과정이다. 할 수 있다면 최대한 재활용해야 한다.

 

[SQL문 수행 프로세스] 

 

(1) 문법/권한 검사

(2) 해시 함수로 반환된 해시 값으로 **(라이브러리 캐시 or 프로시저 캐시) 내 해시버킷 탐색 (SQL문 자체가 Key값인 해시 값)

(3) 해시버킷에 체인으로 연결된 엔트리를 차례로 스캔하면서 같은 SQL문 탐색

(4-1) SQL 문장을 찾으면 실행계획을 공유 받음 : soft 파싱

(4-2) 못 찾으면 최적화 수행 : hard 파싱

(5) 공유 받거나, 최적화한 실행계획을 탐색했던 해시버킷 체인에 연결

(6) 실행계획의 실행

 

** SQL과 실행계획이 저장되는 영역을 Orcale은 라이브러리 캐시, SQL Server는 프로시저 캐시라고 한다.

 

[SQL 해시 값이 달라져 실행계획이 공유되지 못하는 경우 6가지]

공유 불가 상황 SQL 문 예시
(1) 공백 문자 또는 줄바꿈이 추가된 경우 select * from table
select *   from table  (공백이 추가된 경우)
(2) 대소문자의 구분 select * from table
select * from TABLE  (대소문자 변경된 경우)
(3) 주석(Comment) 추가된 경우 select * from table 
select /* 주석문 */ * from table (주석문 추가)
(4) 테이블 Owner 명시 select * from table;
select * from dbo.table; (테이블 소유자 명시)
(5) 옵티마이져 힌트 사용 select * from table 
select /*+ all_rows */ * from table (옵티마이져 힌트 추가)
(6) 조건절 비교 값 select * from table where col = 'A' 
select * from table where col = 'B' (조건절이 변경된 경우)
다른 경우는 Ad-Hoc query의 가능성이 높음으로 크게 문제가 되지 않겠지만, 6번이 자주 사용되는 경우 문제가 심각할 수 있다.

오히려,
DW, OLAP 등 정보계 시스템에 사용되는 Long Running 쿼리들은 오히려 새로 계획을 수립하는 것이 도움이 되는 경우,
히스토그램을 새롭게 반영하고 싶은 경우에는 재수립을 일부러 유도한다.

 

◼︎ 바인드 변수 활용 : (6) 조건절 비교 값의 해결책

-> 프로시저에 변수를 넘겨 실행시키는 것을 생각한다. 해당 프로시저의 실행 계획은 고정되고, 어떤 변수가 들어오든 동일하게 실행된다.

-> 최근에는 상수 값을 자동으로 변수로 변경하여 수행하는 기능이나,

     (SQL Server : 단순 매개 변수화, 자동 매개 변수화 옵션 on/off), (Oracle : cursor_sharing 옵션을 FORCE나 SIMILAR로 설정)

     해당 시점의 데이터 분포를 참고하여 실행계획을 수립하는 기능이 제공되고 있으나

     (SQL Server : Parameter Sniffing), (Oracle : 바인드 변수 peeking)

     해당 기능들은 데이터의 분포가 변하면 성능을 보장하지 못한다는 문제점들이 있어 결국 아직은 관리자의 노력이 필요하다.

-> 변수의 특성에 따라 쿼리나 프로세스를 분리하여 다른 실행계획을 사용하는 방식도 고려해볼 수 있다.

 

◼︎ 어플리케이션 커서 캐싱 : 어플리케이션 단에서 반복 실행시킬 때, 파싱을 1회만 수행하는 방법

-> PL/SQL은 자동으로 커서를 캐싱하며, 각 언어마다 방법이 다름으로 필요 시, 추가 학습할 것


3) 쿼리 변환

결과만 같다면 더 효율적인 쿼리로 변경을 시도하는 기법 (버전이 업데이트 될수록 더 과감한 접근들이 이뤄지고 있다)

휴리스틱 : 규칙 기반으로 무조건 수행하는 변환 / 비용기반 : 비용 우위 시 쿼리 변환 2가지로 구분된다.

 

[변환이 이뤄지는 경우 분류]

 

◼︎ 서브쿼리의 Unnesting

> 서브 쿼리 구문을 Join으로 변경했을 때, 득실을 따져 변경하는 것을 의미한다.

  sql 쿼리 실행계획
변경 전 select * from dept
where deptno in (select deptno from emp)
id  | OPERATION                  | NAME | ROWS | BYTES | COST (%)
-----------------------------------------------------------------
 0  | SELECT STATMENT       |            |          3 |        99|          3 (0)
*1  |  FILTER                            |            |              |             |          
  2 |   TABLE ACCESS FULL  | EMP    |        10 |      330|          3 (0)
*3 |   INDEX UNIQUE SCAN | DEPT_PK |     1 |           2|          0 (0)
-----------------------------------------------------------------

predicate information
-----------------------------------------------------------------
1-filter(exists (select 0 from "dept" "dept" where "deptno"=:B1))
3-access("deptno"=:B1)
변경 후 select *
  from (select deptno from emp) a ,dept b
where b.deptno = a.deptno

->

select emp.*
  from dept, emp
where emp.deptno = dept.deptno
id  | OPERATION                  | NAME | ROWS | BYTES | COST (%)
-----------------------------------------------------------------
0 | SELECT STATMENT                           |           | 10 | 350  |  2 (0)
1  |  TABLE ACCESS BY INDEX ROWID  |  EMP | 3   |  99   |  1 (0) 
2 |    NESTED LOOPS                               |           | 10 |  350|  2 (0)
3 |     INDEX FULL SCAN                | DEPT_PK   | 4  |     8   |  1 (0)
4 |     INDEX RANGE SCAN  | EMP_DEPONO_IDX | 3|        |  0 (0)
-----------------------------------------------------------------

predicate information
-----------------------------------------------------------------
4-access("DEPTNO"="DEPTNO")
변경 전에는 dept에 해당하는 행들을 emp에서 풀 스캔했다면, 변경 후에는 NL JOIN을 사용하는 것을 볼 수 있다.

위 경우 1:M 의 관계를 가정하고 진행했지만 그렇지 않을 경우에는 (1) 미진행 (2) sort unique (3) semi 조인 등을 활용한다.

 

◼︎ 뷰 Merging

> 인라인 뷰를 일반적인 where 조건으로 변경하였을 때, 득실을 따져 변경하는 방법이다.

변경 전 sql 쿼리 변경 후 SQL 쿼리
select *
  from (select * from emp where job = 'A') a
          ,(select * from dept where loc = 'B') b
where a.deptno = b.deptno
select * from emp a, dept b
where a.deptno = b.deptno
    and a.job = 'A'
    and b.loc = 'B'
사용자가 명시한 순서를 옵티마이저가 풀어서 기술한 후, 더 다양한 최적화를 진행해볼 수 있게 된다.

** group by, distinct 등이 포함된 쿼리를 해체하면 오히려 성능이 더 나빠지는 경우들이 있다.
** 집합 연산자(UNION ...),connect by, ROWNUM pseudo 컬럼, select-list에 집계 함수, 분석함수 등에는 불가능하다.

 

◼︎ 조건절 Pushing

여러 테이블에 기재된 조건 절들을 다른 테이블로 push,pull하는 방식을 의미한다.

예시 SQL 설명
조건절 predicate pushdown
: 쿼리 밖의 조건 절을 밀어 넣는 것
select deptno, avg_sal
  from (select deptno, avg(sal) avg_sal
                from emp group by deptno) a
where deptno = 30
쿼리의 where을 인라인 뷰에
먼저 적용할 수 있다면 group by
대상을 줄일 수 있다.
(필터의 순서를 앞으로 당긴다)
조건절 predicate pullup
: 쿼리 내의 조건을 밖으로 빼고 다시 활용
select * from
(select deptno, avg(sal)
    from emp
 where deptno = 10
  group by deptno) e1
(select deptno, min(sal), max(sal)
    from emp
  group by deptno) e2
whre e1.deptno = e2.deptno
e1 인라인뷰의 deptno 조건을
e2에도 적용해줄 수 있다면
읽을 량을 줄일 수 있다.
조건절 join predicate pushdown
: NL 중 읽은 조건을 Inner 쪽에 대입 활용
select d.deptno, d.dname, e.avg_sal
   from dept d,
           (select /*+ no_merge push_pred*/
                       deptno, avg(sal) avg_sal
               from emp
            group by deptno) e
where e.deptno(+) = d.deptno
조회해야할 행들에 대해서만
인라인 뷰에서 group by를 수행한다.

 

◼︎ 조건절 이행

조건절들의 논리적 관계를 고려해 통합시키는 과정이다.

변경 전 sql 쿼리 변경 후 SQL 쿼리
select * from dept d, emp e
where e.job = 'A'
    and e.deptno = 10
    and d.deptno = e.deptno
select * from dept d, emp e
where e.job = 'A'
    and e.deptno = 10
    and d.deptno = 10

 

◼︎ 불필요한 조인절 제거

-> PK/FK가 설정되어 있는 테이블에 한하여, 만약 조인절에는 기재되었지만 사용되지 않는 테이블이 있다면 계획에서 읽지 않는다.

 

◼︎ or 조건을 union all로 변환

조건절을 활용한 인덱스 사용이 가능하도록 or을 union all로 전개한다. (or-expansion이라고 지칭한다)

변경 전 sql 쿼리 변경 후 SQL 쿼리
select * from emp 
where job = 'A' and deptnop = 20
select * from emp
where job = 'A'

union all

select * from emp
where deptno = 20

 

◼︎ 기타 쿼리 변환

(1) 집합(minus)에서 발생하는 sort를 제거하기 위해 join으로 변경하는 경우

(2) 조인 컬럼에 "is not null"을 추가하여 null인 값이 조인 대상이 되지 않도록 변환하는 경우

(3) between에서 " between Min and Max" 가 아니라 "between Max and Min"을 입력해버린 경우, 순서를 바꿔준다.

(4) where 조건에 기술된 내용들의 순서를 바꿔 필터가 더 많이 되는 것부터 실행한다.