Sort Order By 생략

-- 인덱스 선두 컬럼을 (종목코드 + 거래일시) 순으로 구성하지 않으면, 소트 연산 생략 불가
-- 종목코드 조건을 만족하는 레코드를 인덱스에서 모두 읽어야 함
select 거래일시, 체결건수, 체결수량, 거래대금
from   종목거래
where  종목코드 = 'KR123456'
order by 거래일시

--------------------------------------------------------------------------------------------
| Id  | Operation                     | Name        | Rows  | Bytes | Cost (%CPU)|
--------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |             | 40000 | 3515K |  2041   (1)|
|   1 |  SORT ORDER BY                |             | 40000 | 3515K |  2041   (1)|
|   2 |   TABLE ACCESS BY INDEX ROWID | 종목        | 40000 | 3515K |  1210   (1)|
|* 3  |    INDEX RANGE SCAN           | 종목거래_N1  | 40000 |       |    96   (2)|
--------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("종목코드"='KR123456')

-- 인덱스 선두 컬럼을 (종목코드 + 거래일시) 순으로 구성하면 소트 연산 생략 가능
-- 종목코드 조건을 만족하는 전체 레코드를 읽지 않고도, 부분범위 처리를 통해 결과집합 출력
--------------------------------------------------------------------------------------------
| Id  | Operation                     | Name        | Rows  | Bytes | Cost (%CPU)|
--------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |             | 40000 | 3515K |  1372   (1)|
|   1 |  TABLE ACCESS BY INDEX ROWID | 종목          | 40000 | 3515K |  1372   (1)|
|* 2  |   INDEX RANGE SCAN           | 종목거래_PK    | 40000 |       |   258   (1)|
--------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("종목코드"='KR123456')

부분범위 처리를 활용한 튜닝 기법은 유효한가

Top N 쿼리

select * from (
  select 거래일시, 체결건수, 체결수량, 거래대금
  from   종목거래
  where  종목코드 = 'KR123456'
  and    거래일시 >= '20180304'
  order by 거래일시
)
where rownum <= 10
img
Execution Plan
-----------------------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS
1    0   COUNT (STOPKEY)
2    1     VIEW
3    2       TABLE ACCESS (BY INDEX ROWID) OF '종목거래' (TABLE)
4    3         INDEX (RANGE SCAN) OF '종목거래_PK' (INDEX (UNIQUE))

페이징 처리

-- 페이징 처리 표준 패턴
select *
from (
    select rownum no, a.*
    from
    (
        /* SQL Body */
    ) a
    where rownum <= (:page * 10)
)
where no >= (:page-1) * 10 + 1
select *
from (
    select rownum no, a.*
    from
    (
        select 거래일시, 체결건수, 체결수량, 거래대금
        from   종목거래
        where  종목코드 = 'KR123456'
        and    거래일시 >= '20180304'
        order by 거래일시
    ) a
    where rownum <= (:page * 10)
)
where no >= (:page-1)*10 + 1

Execution Plan
--------------------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=16 Card=756 Bytes=126K)
1    0   VIEW (Cost=16 Card=756 Bytes=126K)
2    1     COUNT (STOPKEY)      → NO SORT + STOPKEY
3    2       VIEW (Cost=16 Card=756 Bytes=117K)
4    3         TABLE ACCESS (BY INDEX ROWID) OF '종목거래' (TABLE) (Cost=16 ... )
5    4           INDEX (RANGE SCAN) OF '종목거래_PK' (INDEX) (Cost=4 Card=303)

페이징 처리 ANTI패턴

select *
from (
    select rownum no, a.*
    from
    (
        select 거래일시, 체결건수, 체결수량, 거래대금
        from   종목거래
        where  종목코드 = 'KR123456'
        and    거래일시 >= '20180304'
        order by 거래일시
    ) a
)
where no between (:page-1)*10 + 1 and (:page * 10)

Execution Plan
--------------------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=16 Card=756 Bytes=126K)
1    0   FILTER
2    1     VIEW (Cost=16 Card=756 Bytes=126K)
3    2       COUNT      → NO SORT + NO STOP
4    3         VIEW (Cost=16 Card=756 Bytes=117K)
5    4           TABLE ACCESS (BY INDEX ROWID) OF '종목거래' (TABLE) (Cost=16 ... )
6    5             INDEX (RANGE SCAN) OF '종목거래_PK' (INDEX) (Cost=4 Card=303)
img

부분범위 처리 가능하도록 SQL 작성하기

거래_PK  : 거래일자 + 계좌번호 + 거래순번
거래_X01 : 계좌번호 + 거래순번 + 결제구분코드

-- 인덱스로 소트 연산 생략 불가
select *
from (
    select 계좌번호, 거래순번, 주문금액, 주문수량, 결제구분코드, 주문매체구분코드
    from   거래
    where  거래일자 = :ord_dt
    order by 계좌번호, 거래순번, 결제구분코드
)
where rownum <= 50

Execution Plan
------------------------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=433K Card=10 Bytes=1K)
1    0   COUNT (STOPKEY)
2    1     VIEW (Cost=433K Card=421K Bytes=57M)
3    2       SORT (ORDER BY STOPKEY) (Cost=433K Card=421K Bytes=40M)
4    3         TABLE ACCESS (BY INDEX ROWID) OF '거래' (TABLE) (Cost=4

최소값/최대값 구하기

SELECT MAX(SAL) FROM EMP;

Execution Plan
-----------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=3 Card=1 Bytes=4)
1    0   SORT (AGGREGATE) (Card=1 Bytes=4)
2    1     TABLE ACCESS (FULL) OF 'EMP' (TABLE) (Cost=3 Card=14 Bytes=56)
CREATE INDEX EMP_X1 ON EMP(SAL);

SELECT MAX(SAL) FROM EMP;

Execution Plan
-----------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=1 Card=1 Bytes=3)
1    0   SORT (AGGREGATE) (Card=1 Bytes=3)
2    1     INDEX (FULL SCAN (MIN/MAX)) OF 'EMP_X1' (INDEX) (Cost=1 Card=1 ... )

인덱스를 이용해 최소/최대값 구하기 위한 조건

CREATE INDEX EMP_X1 ON EMP(DEPTNO, MGR, SAL);

SELECT MAX(SAL) FROM EMP WHERE DEPTNO = 30 AND MGR = 7698;

Execution Plan
-----------------------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=1 Card=1 Bytes=8)
1    0   SORT (AGGREGATE) (Card=1 Bytes=8)
2    1     FIRST ROW (Cost=1 Card=1 Bytes=8)
3    2       INDEX (RANGE SCAN (MIN/MAX)) OF 'EMP_X1' (INDEX) (Cost=1 Card=1 ... )
img
CREATE INDEX EMP_X1 ON EMP(DEPTNO, SAL, MGR);

SELECT MAX(SAL) FROM EMP WHERE DEPTNO = 30 AND MGR = 7698;

Execution Plan
-----------------------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=1 Card=1 Bytes=8)
1    0   SORT (AGGREGATE) (Card=1 Bytes=8)
2    1     FIRST ROW (Cost=1 Card=1 Bytes=8)
3    2       INDEX (RANGE SCAN (MIN/MAX)) OF 'EMP_X1' (INDEX) (Cost=1 Card=1 ... )
img
CREATE INDEX EMP_X1 ON EMP(SAL, DEPTNO, MGR);

SELECT MAX(SAL) FROM EMP WHERE DEPTNO = 30 AND MGR = 7698;

Execution Plan
-----------------------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=1 Card=1 Bytes=8)
1    0   SORT (AGGREGATE) (Card=1 Bytes=8)
2    1     FIRST ROW (Cost=1 Card=1 Bytes=8)
3    2       INDEX (FULL SCAN (MIN/MAX)) OF 'EMP_X1' (INDEX) (Cost=1 Card=1 ... )
img
CREATE INDEX EMP_X1 ON EMP(DEPTNO, SAL);

SELECT MAX(SAL) FROM EMP WHERE DEPTNO = 30 AND MGR = 7698;

Execution Plan
--------------------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=2 Card=1 Bytes=8)
1    0   SORT (AGGREGATE) (Card=1 Bytes=8)
2    1     TABLE ACCESS (BY INDEX ROWID) OF 'EMP' (TABLE) (Cost=2 Card=1 Bytes=8)
3    2       INDEX (RANGE SCAN) OF 'EMP_X1' (INDEX) (Cost=1 Card=5)
img

Top N 쿼리 이용해 최소/최대값 구하기

CREATE INDEX EMP_X1 ON EMP(DEPTNO, SAL);

SELECT *
FROM (
    SELECT SAL
    FROM   EMP
    WHERE  DEPTNO = 30
    AND    MGR = 7698
    ORDER BY SAL DESC
)
WHERE ROWNUM <= 1;

Execution Plan
------------------------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=2 Card=1 Bytes=13)
1    0   COUNT (STOPKEY)
2    1     VIEW (Cost=2 Card=1 Bytes=13)
3    2       TABLE ACCESS (BY INDEX ROWID) OF 'EMP' (TABLE) (Cost=2 Card=1 ... )
4    3         INDEX (RANGE SCAN DESCENDING) OF 'EMP_X1' (INDEX) (Cost=1 Card=5)
img

이력 조회

img

단순한 이력 조회

SELECT 장비번호, 장비명, 상태코드
     , (SELECT MAX(변경일자)
        FROM   상태변경이력
        WHERE  장비번호 = P.장비번호) 최종변경일자
FROM   장비 P
WHERE  장비구분코드 = 'A001'

---------------------------------------------------------------------------------------
| Id | Operation                     | Name           | Starts | A-Rows | Buffers |
---------------------------------------------------------------------------------------
|  0 | SELECT STATEMENT              |                |      1 |     10 |       4 |
|  1 |  SORT AGGREGATE               |                |     10 |     10 |      22 |
|  2 |   FIRST ROW                   |                |     10 |     10 |      22 |
|  3 |    INDEX RANGE SCAN (MIN/MAX) | 상태변경이력_PK |     10 |     10 |      22 |
|  4 |  TABLE ACCESS BY INDEX ROWID  | 장비           |      1 |     10 |       4 |
|  5 |   INDEX RANGE SCAN            | 장비_N1        |      1 |     10 |       2 |
---------------------------------------------------------------------------------------

복잡한 이력 조회

SELECT 장비번호, 장비명, 상태코드
     , SUBSTR(최종이력, 1, 8) 최종변경일자
     , TO_NUMBER(SUBSTR(최종이력, 9, 4)) 최종변경순번
FROM (
    SELECT 장비번호, 장비명, 상태코드
         , (SELECT MAX(H.변경일자 || LPAD(H.변경순번, 4))
            FROM   상태변경이력 H
            WHERE  장비번호 = P.장비번호) 최종이력
    FROM   장비 P
    WHERE  장비구분코드 = 'A001'
)

---------------------------------------------------------------------------------------
| Id | Operation                     | Name           | Starts | A-Rows | Buffers |
---------------------------------------------------------------------------------------
|  0 | SELECT STATEMENT              |                |      1 |     10 |       4 |
|  1 |  SORT AGGREGATE               |                |     10 |     10 |    6380 |
|  2 |   INDEX RANGE SCAN            | 상태변경이력_PK |     10 |  1825K |    6380 |
|  3 |  TABLE ACCESS BY INDEX ROWID  | 장비           |      1 |     10 |       4 |
|  4 |   INDEX RANGE SCAN            | 장비_N1        |      1 |     10 |       2 |
---------------------------------------------------------------------------------------
SELECT 장비번호, 장비명, 상태코드
     , (SELECT MAX(H.변경일자)
        FROM   상태변경이력 H
        WHERE  장비번호 = P.장비번호) 최종변경일자
     , (SELECT MAX(H.변경순번)
        FROM   상태변경이력 H
        WHERE  장비번호 = P.장비번호
        AND    변경일자 = (SELECT MAX(H.변경일자)
                           FROM   상태변경이력 H
                           WHERE  장비번호 = P.장비번호)) 최종변경순번
FROM   장비 P
WHERE  장비구분코드 = 'A001'

-------------------------------------------------------------------------------------------
| Id  | Operation                      | Name            | Starts | A-Rows | Buffers |
-------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |                 |      1 |     10 |       4 |
|   1 |  SORT AGGREGATE                |                 |     10 |     10 |      22 |
|   2 |   FIRST ROW                    |                 |     10 |     10 |      22 |
|   3 |    INDEX RANGE SCAN (MIN/MAX)  | 상태변경이력_PK  |     10 |     10 |      22 |
|   4 |  SORT AGGREGATE                |                 |     10 |     10 |      47 |
|   5 |   INDEX RANGE SCAN             | 상태변경이력_PK  |     10 |   1000 |      47 |
|   6 |    SORT AGGREGATE              |                 |     10 |     10 |      22 |
|   7 |     FIRST ROW                  |                 |     10 |     10 |      22 |
|   8 |      INDEX RANGE SCAN (MIN/MAX)| 상태변경이력_PK  |     10 |     10 |      22 |
|   9 |  TABLE ACCESS BY INDEX ROWID   | 장비            |      1 |     10 |       4 |
|  10 |   INDEX RANGE SCAN             | 장비_N1         |      1 |     10 |       2 |
-------------------------------------------------------------------------------------------
SELECT 장비번호, 장비명
     , (SELECT MAX(H.변경일자)
        FROM   상태변경이력 H
        WHERE  장비번호 = P.장비번호) 최종변경일자
     , (SELECT MAX(H1.변경순번)
        FROM   상태변경이력 H1
        WHERE  장비번호 = P.장비번호
        AND    변경일자 = (SELECT MAX(H2.변경일자)
                           FROM   상태변경이력 H2
                           WHERE  장비번호 = P.장비번호)) 최종변경순번
     , (SELECT H1.상태코드
        FROM   상태변경이력 H1
        WHERE  장비번호 = P.장비번호
        AND    변경일자 = (SELECT MAX(H2.변경일자)
                           FROM   상태변경이력 H2
                           WHERE  장비번호 = P.장비번호)
        AND    변경순번 = (SELECT MAX(H3.변경순번)
                           FROM   상태변경이력 H3
                           WHERE  장비번호 = P.장비번호
                           AND    변경일자 = (SELECT MAX(H4.변경일자)
                                              FROM   상태변경이력 H4
                                              WHERE  장비번호 = P.장비번호))) 최종상태코드
FROM   장비 P
WHERE  장비구분코드 = 'A001'

INDEX_DESC 힌트 활용

SELECT 장비번호, 장비명
     , SUBSTR(최종이력, 1, 8) 최종변경일자
     , TO_NUMBER(SUBSTR(최종이력, 9, 4)) 최종변경순번
     , SUBSTR(최종이력, 13) 최종상태코드
FROM (
    SELECT 장비번호, 장비명
         , (SELECT /*+ INDEX_DESC(X 상태변경이력_PK) */
                   변경일자 || LPAD(변경순번, 4) || 상태코드
            FROM   상태변경이력 X
            WHERE  장비번호 = P.장비번호
            AND    ROWNUM <= 1) 최종이력
    FROM   장비 P
    WHERE  장비구분코드 = 'A001'
)

-------------------------------------------------------------------------------------------
| Id  | Operation                       | Name            | Starts | A-Rows | Buffers |
-------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                |                 |      1 |     10 |       4 |
|   1 |  COUNT STOPKEY                  |                 |     10 |     10 |      41 |
|   2 |   TABLE ACCESS BY INDEX ROWID   | 상태변경이력    |     10 |     10 |      41 |
|   3 |    INDEX RANGE SCAN DESCENDING  | 상태변경이력_PK  |     10 |     10 |      30 |
|   4 |  TABLE ACCESS BY INDEX ROWID    | 장비            |      1 |     10 |       4 |
|   5 |   INDEX RANGE SCAN              | 장비_N1         |      1 |     10 |       2 |
-------------------------------------------------------------------------------------------

11g/12c 신기능 활용

-- 11g부터
SELECT 장비번호, 장비명
     , SUBSTR(최종이력, 1, 8) 최종변경일자
     , TO_NUMBER(SUBSTR(최종이력, 9, 4)) 최종변경순번
     , SUBSTR(최종이력, 13) 최종상태코드
FROM (
    SELECT 장비번호, 장비명
         , (SELECT 변경일자 || LPAD(변경순번, 4) || 상태코드
            FROM   (SELECT 장비번호, 변경일자, 변경순번, 상태코드
                    FROM   상태변경이력
                    ORDER BY 변경일자 DESC, 변경순번 DESC)
            WHERE  장비번호 = P.장비번호
            AND    ROWNUM <= 1) 최종이력
    FROM   장비 P
    WHERE  장비구분코드 = 'A001'
)

-------------------------------------------------------------------------------------------
| Id  | Operation                       | Name            | Starts | A-Rows | Buffers |
-------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                |                 |      1 |     10 |       4 |
|   1 |  COUNT STOPKEY                  |                 |     10 |     10 |      40 |
|   2 |   VIEW                          |                 |     10 |     10 |      40 |
|   3 |    TABLE ACCESS BY INDEX ROWID   | 상태변경이력    |     10 |     10 |      40 |
|   4 |     INDEX RANGE SCAN DESCENDING  | 상태변경이력_PK  |     10 |     10 |      30 |
|   5 |  TABLE ACCESS BY INDEX ROWID    | 장비            |      1 |     10 |       4 |
|   6 |   INDEX RANGE SCAN              | 장비_N1         |      1 |     10 |       2 |
-------------------------------------------------------------------------------------------
-- 12c보다 정상 Top N Stopkey가 잘 작동하는 쿼리
SELECT 장비번호, 장비명
     , SUBSTR(최종이력, 1, 8) 최종변경일자
     , TO_NUMBER(SUBSTR(최종이력, 9, 4)) 최종변경순번
     , SUBSTR(최종이력, 13) 최종상태코드
FROM (
    SELECT 장비번호, 장비명
         , (SELECT 변경일자 || LPAD(변경순번, 4) || 상태코드
            FROM   (SELECT 변경일자, 변경순번, 상태코드
                    FROM   상태변경이력
                    WHERE  장비번호 = P.장비번호
                    ORDER BY 변경일자 DESC, 변경순번 DESC)
            WHERE  ROWNUM <= 1) 최종이력
    FROM   장비 P
    WHERE  장비구분코드 = 'A001'
)

윈도우 함수와 Row Limiting 절

상황에 따라 달라져야 하는 이력 조회 패턴

-- 전체 장비 이력에 윈도우 함수를 이용하면 효과적
SELECT P.장비번호, P.장비명
     , H.변경일자 AS 최종변경일자
     , H.변경순번 AS 최종변경순번
     , H.상태코드 AS 최종상태코드
FROM   장비 P
     , (SELECT 장비번호, 변경일자, 변경순번, 상태코드
             , ROW_NUMBER() OVER (PARTITION BY 장비번호
                                 ORDER BY 변경일자 DESC, 변경순번 DESC) RNUM
        FROM   상태변경이력) H
WHERE  H.장비번호 = P.장비번호
AND    H.RNUM = 1;

-------------------------------------------------------------------------------------------------
| Id  | Operation                | Name         | A-Rows | Buffers | Reads  |
-------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |              |   1000 |   2881K |  36812 |
|   1 |  HASH JOIN               |              |   1000 |   2881K |  36812 |
|   2 |   TABLE ACCESS FULL      | 장비         |   1000 |      23 |      6 |
|   3 |   VIEW                   |              |   1000 |   2881K |  36806 |
|   4 |    WINDOW SORT PUSHED RANK|              |   8700 |   2881K |  36806 |
|   5 |     TABLE ACCESS FULL    | 상태변경이력 | 18250K |   2880K |  36803 |
-------------------------------------------------------------------------------------------------
-- KEEP절 활용도 가능
SELECT P.장비번호, P.장비명
     , H.변경일자 AS 최종변경일자
     , H.변경순번 AS 최종변경순번
     , H.상태코드 AS 최종상태코드
FROM   장비 P
     , (SELECT 장비번호
             , MAX(변경일자) 변경일자
             , MAX(변경순번) KEEP (DENSE_RANK LAST ORDER BY 변경일자, 변경순번) 변경순번
             , MAX(상태코드) KEEP (DENSE_RANK LAST ORDER BY 변경일자, 변경순번) 상태코드
        FROM   상태변경이력
        GROUP BY 장비번호) H
WHERE  H.장비번호 = P.장비번호

-------------------------------------------------------------------------------------------------
| Id  | Operation                | Name         | A-Rows | Buffers | Reads  |
-------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |              |   1000 |   2881K |  17809 |
|   1 |  HASH JOIN               |              |   1000 |   2881K |  17809 |
|   2 |   TABLE ACCESS FULL      | 장비         |   1000 |      23 |      3 |
|   3 |   VIEW                   |              |   1000 |   2881K |  17806 |
|   4 |    SORT GROUP BY         |              |   1000 |   2881K |  17806 |
|   5 |     TABLE ACCESS FULL    | 상태변경이력 | 18250K |   2880K |  17803 |
-------------------------------------------------------------------------------------------------

선분이력 맛보기

img
-- 1. 현재 시점의 유효한 이력 조회 (종료일자 상수를 활용)
SELECT P.장비번호, P.장비명
     , H.상태코드, H.유효시작일자, H.유효종료일자, H.변경순번
FROM   장비 P, 상태변경이력 H
WHERE  P.장비구분코드 = 'A001'
AND    H.장비번호 = P.장비번호
AND    H.유효종료일자 = '99991231'

-- 또는

-- 2. 특정 기준일자(:BASE_DT) 시점의 유효한 이력 조회
SELECT P.장비번호, P.장비명
     , H.상태코드, H.유효시작일자, H.유효종료일자, H.변경순번
FROM   장비 P, 상태변경이력 H
WHERE  P.장비구분코드 = 'A001'
AND    H.장비번호 = P.장비번호
AND    :BASE_DT BETWEEN H.유효시작일자 AND H.유효종료일자

Sort Group By 생략

select region, avg(age), count(*)
from   customer
group by region

-------------------------------------------------------------------------------------------------
| Id | Operation                   | Name         | Rows  | Bytes | Cost (%CPU)|
-------------------------------------------------------------------------------------------------
|  0 | SELECT STATEMENT            |              |    25 |   725 | 30142  (1) |
|  1 |  SORT GROUP BY NOSORT       |              |    25 |   725 | 30142  (1) |
|  2 |   TABLE ACCESS BY INDEX ROWID| CUSTOMER     | 1000K |   27M | 30142  (1) |
|  3 |    INDEX FULL SCAN          | CUSTOMER_X01 | 1000K |       |  2337  (2) |
-------------------------------------------------------------------------------------------------
img