개발/TIL

[sql 튜닝] Chapter 5. 소트 튜닝

ebang 2025. 5. 14. 22:00
  • 소트하거나 추가 저장공간이 필요할 때 Oracle 은 PGA(인메모리), temp 테이블 스페이스(디스크.)를 활용한다.
    • 소트 머지 조인(양쪽 집합을 미리 소트하고 저장.) 해시 조인 (해시 테이블 저장).
    • 추가로 필요하다면 temp 테이블 스페이스 활용.
    • 소트, 데이터 그루핑을 할 때도 마찬가지.
  • 5.1.1. 소트 과정
    • 소트를 하는 과정에서 Sort Area 만 사용해서 완료할 수 있는지 여부에 따라 2가지 종류로 나뉜다.
      • 메모리 소트, Internal Sort : PGA 의 Sort Area 만 사용해서 소트하는 경우.
      • 디스크 소트, External Sort : 용량이 부족해서 디스크까지 사용해서 소트하는 경우.
        • 디스크 소트는 디스크 I/O 가 발생해서 성능저하가 발생하기도 하지만, 부분범위 처리가 불가능하게 함으로써 성능저하가 일어나는 부분도 매우 크다.

5.1.2. 소트 오퍼레이션.
소트 연산이 사용되는 경우에 대해 알아보자.

  • Sort Aggregate
    • 전체 행을 대상으로 집계를 수행. Sort Area 를 사용함.
  • Sort Order by
    • 데이터를 정렬할 때.
  • Sort Group by
    • group by 후 집계 시에 sort 알고리즘 사용 (메모지). 결과 보장 x
  • Sort Unique
    • 서브 쿼리에 대해 unnest 되는 경우 && unique 조건이 없는 경우 (4.4절 참고)
    • Union 과 같은 집합 연산을 수행하는 경우
    • Distinct 연산자를 사용하는 경우 (+order by. ) (특정 시점 이후로는 order by가 없다면 hash unique 사용.)
  • Sort Join
    • 소트 머지 조인을 사용하는 경우.
  • Window sort
    • 전체 행에 대해 분석함수를 수행하는 경우.

5.2. 소트가 발생하지 않도록 SQL 작성
소트는 메모리 집약적, cpu 집약적인 연산이기 때문에 disk IO 가 발생하면 더욱 큰 성능 저하가 일어난다.

5.2.1. Union VS Union All

  • Union 은 소팅 후에 중복이 발생하는지 여부를 모두 확인해야하지만, union all 은 모든 레코드를 다 합친다. (중복 확인이 없기 때문에 성능이 더 좋다. )
  • 조회하는 데이터에 대해 높은 이해도를 가지고, 굳이 중복되지 않는 두 집합을 union 하는 일이 없도록 하는 것이 좋고, union all 을 쓰면서도 데이터 중복이 일어나지 않도록 조회하는 쿼리를 사용하면 좋다.
    • 예시
      원래 sql
select 결제번호, 결제수단코드, 주문 번호, 결제금액,  결제일자, 주문일자 ...
    from 결제
    where 결제일자 ='20180316'

    UNION

    select 결제번호, 결제수단코드, 주문 번호, 결제금액,  결제일자, 주문일자 ...
    from 결제
    where 결제일자 <> '20180316'

중복 없이 union

    select 결제번호, 결제수단코드, 주문 번호, 결제금액,  결제일자, 주문일자 ...
    from 결제
    where 결제일자 ='20180316'

    UNION ALL

    select 결제번호, 결제수단코드, 주문 번호, 결제금액,  결제일자, 주문일자 ...
    from 결제
    where 주문일자 = '20180316' and 결제일자 <> '20180316'

주문일자에 null 이 가능하다면, 다음 조건으로

    and (결제일자 <> '20180316' and 결제일자 is null)

또는

    and LNNVL(결제일자 ='20180316')

5.2.2. Exists 사용

  • Distinct 연산자 대신에 사용.
    • Distinct 연산자가 있으면 소팅 후 중복이 되지 않도록 연산이 수행됨. (Sort Unique: order by 절이 있는 경우.)
  • distinct 대신 exists 문을 서브쿼리로 해서 넣으면, 부분범위처리도 되고 성능 상 이점이 생긴다.
  • 예시
  • 계약 X2 인덱스 구성이 「상품변호 + 계약일자」 -> 이때 execution plan 확인
select DISTINCT p.상품번호, p.상품명, p.상품가격
from 상품 p, 계약 c
where p.상품유형코드 = :pclscd
and c 상품번호 = p.상품번호
and c.계약일자 between :dt1 and :dt2
and c.계약구분코드 = :ctpcd

특정 계약 구분에 대해 특정 계약 기간 내에 거래가 이루어진 특정 유형 상품의 정보.
해당 상품에 대한 중복 없는 데이터.

  • 실행계획

만약 이렇게 바꾼다면?

select p.상품번호, p.상품명, p.상품가격
from 상품 p
where p.상품유형코드 = :pclscd
and EXISTS (select 'x' from 계약 c
    where c.상품번호 = p.상품번호
    and c.계약일자 between :dt1 and :dt2
    and c.계약구분코드 = :ctpcd)

-> Exists 쿼리는 서브쿼리에서 데이터의 존재여부만을 따질 뿐, 조건에 해당하는 데이터를 모두 읽을 필요가 없다.
상품에 여러 계약이 묶여있을 수 있지만, exists 조건에 의해 하나라도 성립하는 데이터가 있다면 그 데이터만 확인하고 넘어감.

-> 상품에 대한 데이터만 조회한다면 굳이 distinct 조건이 필요한게 아니라 exists 조건에 의해 존재여부만 해당하면 그 데이터를 조회하는 것만으로도 중복을 피할 수 있다.

특정 계약 구분에 대해 특정 계약 기간 내에 원하는 상품 유형의 거래가 있다면 -> 그 상품.

  • 데이터의 존재 유무만 따지게 됨 -> (4.4.2절 참고)
  • 계약 일자에 대해 부분범위 처리 가능.

5.3. 인덱스를 이용한 소트 연산 생략

  • 인덱스가 키, 칼럼순으로 정렬된 상태를 유지한다는 점을 이용하면,
    • order by, group by 절이 있을 때 소트 생략이 가능하다.
    • Top N 쿼리 특성을 결합하면, 온라인 트랜잭션 시스템에서 대량 데이터를 조회할 때 매우 빠른 응답속도를 낼 수 있다.

5.3.1. Sort Order By 생략

select 거래일시, 체결건수, 체결수량, 거래대금
from 종목거래
where 종목코드 = 'KR123456'
order by 거래일시

인덱스가 종목코드 + 거래일시로 구성되지 않은 경우. 소트 연산을 수행할 수 밖에 없고,

  • 매번 테이블 엑세스로 데이터 레코드를 조회해 온 뒤, 정렬을 수행하게 된다.

인덱스가 종목코드 + 거래일시로 구성된 경우, 소트 연산을 생략 가능하다.

  • 결과를 바로 반환 가능.
  • 부분 범위 처리도 가능.

**부분범위처리를 이용한 튜닝 기법, 아직도 유효한가?

특정 개수만 먼저 반환하고, 다음 클라이언트 요청이 왔을 때 남은 데이터를 반환하는 식의 부분범위 처리는,
클라이언트와 바로 연결된 2Tier 구조에서만 가능해보인다.

3 Tier 아키텍처란, WAS, AP 서버가 중간에 더 존재하는 아키텍처인데,
서버 리소스를 수많은 클라이언트가 공유하는 구조이므로, 클라이언트가 커넥션을 독점할 수 없고 단위 작업을 마치면 커넥션을 반환해야한다. 따라서 어디까지 반환했는지 등의 정보를 유지할 수가 없기 때문에, 부분범위처리가 불가능해보인다.
하지만 실제로는 이 기능이 가능한데, Top N 쿼리 덕분에 가능하다.

5.3.2. Top N 쿼리

  • 전체 레코드 중 상위 N개만 선택해서 반환하는 쿼리

  • 언어 별 형태.

    • SQL Server
      select TOP 10 거래일시, 체결건수, 체결수량, 거래대금
      from 종목거래
      where 종목코드 = 'KR123456'
      and 거래일시 >= '20180304'
      order by 거래일시
    • Oracle
      select * from (
        select 거래일시, 체결건수, 체결수량, 거래대금
        from 종목거래
        where 종목코드 = 'KR123456',
        and 거래일시 >= '20180304'
        order by 거래일시
      )        
      where rownum <= 10
  • 형태만 보면, 전체 레코드를 조회한 다음, 정렬을 수행해서 상위의 N 개 쿼리를 취하는 형태로 보인다.

    • 전부 조회 한 다음, 정렬 후 rownum 부여.
  • 그런데 굳이 전체 정렬하지 않아도 된다는 걸 눈치채고 동작하는 알고리즘!

  • 인덱스를 이용해서 소트가 생략된 구조에서는, 읽다가 N개를 읽고나면 멈추는 구조가 되기 때문에 성능상 매우 빠른 구조가 된다.

    • 인덱스 존재 시, 인덱스를 타고 정렬 순서대로 스캔
    • 상위 N개만 도달하면, STOP!
    • 정렬은 필요 최소한만 진행됨

  • 실행계획
  • Sort Order by 절 대신 COUNT(STOP KEY) 라는 절이 보인다.
  • rownum 으로 지정한 건 수만큼 읽으면 데이터를 더 이상 읽지 않겠다는 의미가 된다.
  • 이를 이하 Top N StopKey 알고리즘이라고 부르자.
  • 페이징 처리
  • 3 Tier 환경에서 부분범위처리를 적용할만한 부분.
    • 예시
    • 페이지당 데이터는 10개씩 보여주는 예시.
    • page = 1 : 110, 2 : 1120
    • page = 3이면, 21번째~30번째 데이터를 보여줘야함.
      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

이 때의 실행 계획

만약, rownum 조건을 다음과 같이 수정한다면?

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)

바꾸면 이렇게 실행되어버린다.
소트 생략이 가능한 인덱스이므로 sort 는 여전히 실행되지 않아도 되지만, 도중에 멈추지 않는 것이다.
(전체 데이터에 대해서 조회하고 rownum no 로 부여한 후에, 필터링을 수행하게 됨. )

-> 페이징 처리 안티패턴임.

  • (실제로 이렇게 paging 쿼리를 작성해주는 프레임워크도 존재한다. )
  • rownum 이 사용된 조건 절이 바로 Top N StopKey 알고리즘이 실행되기 위한 조건!
  • 트레이스 확인 대신 더 쉽게 확인할 수 있는 방법
    • 조회 조건에 해당하는 데이터를 아주 많이 입력해둔다.
    • 조회하는 SQL 을 수행한다.
    • 첫번째 페이지는 출력되었지만, 컴퓨터에 귀를 가져다대면 하드웨어가 돌아가는 소리가 계속난다.
      • 다 처리했음에도 불구하고, 해당 between 조건에 해당하는 다른 데이터가 존재하는지 확인하려고 계속 조회하기 때문에 나는 소리.

부분범위 처리가 가능하도록 SQL 구성하기 - 튜닝 사례

  • 부분범위 처리가 가능하게 SQL 을 구성한다는 의미

      1. 인덱스를 활용가능하게 조건절을 구사.
      1. 조인을 사용한다면 NL 조인으로 (작은 테이블의 경우 Hash Join 의 Build Input 으로 가도 가능. )
      1. Order By 절이 있어도 소트 연산 생략이 가능하게끔.
  • 예시

    • 거래 테이블
    • 인덱스 구성
      • 거래_PK: 거래일자 + 계좌번호 + 거래순번
      • 거래_XOR: 계좌번호 + 거래순번 + 결제구분코드
    • select *
      from (
      select 계좌번호, 거래순번, 주문금액, 주문수량, 결제구분코드 , 주문매체구분코드
      from 거래
      where 거래일자 = :ord_dt
      order by 계좌번호, 거래순번, 결제구분코드
      )
      where rownum <= 50

인덱스를 이용한 소트연산 생략이 일어날 수 없다. 50개만 조회하면되는데도 전부 조회하고, 정렬하고, 상단 50개만 가져오게 되는 것.

  • 결제 구분 코드가 없었으면 가능했겠지만...(거래_PK )
  • 거래_XOR 의 선두 칼럼이 거래일자였다면 가능했겠지만...

데이터 모델에 대한 이해와 집합적 사고를 해보면, 거래_PK 를 사용한 경우
특정 거래 일자에 대해 계좌번호 + 거래 순번으로 정렬한다면 중복이 없고 알아서 잘 정리된 상태일 것.
따라서 굳이 결제 구분코드를 뒤에 두고 정렬할 필요가 없다.

결제구분코드를 order by 조건절에서 제거하면, sort order by 실행계획도 사라지고 부분범위처리도 잘 일어난다.

5.3.3. 최대값/최솟값 구하기

  • MIN / MAX 값을 구하는 경우, Sort Aggregation 이 일어남
    • 마지막에 데이터 정렬을 수행하는 건 아니지만, 전체 데이터를 읽으면서 값을 비교한다.

  • 특히 인덱스를 사용한다면, 왼쪽 혹은 오른쪽만 타고가서 값을 구할 수 있다.
  • 인덱스를 사용하는 경우
    • 왼쪽만 타고 가거나(MIN) 오른쪽만 타고가면(MAX) 구할 수 있다.
    • Oracle 기준으로 8i 버전 이후로 다음과 같은 실행계획이 가능해졌다. (이전에는 전체 행을 조회했었음. )
  • 원래 전체행을 조회하는 경우
  • 인덱스만 타고 가는 경우
  • 인덱스를 이용해서 MIN/MAX 를 구하는 조건
    • 예시
    • DEPTNO + MGR 十 SAL 순으로구성
      CREATE INDEX EMP_X1 ON EMP (DEPTNO , MGR, SAL);
      SELECT MAX (SAL) FROM EMP WHERE DEPTNO = 30 AND MGR = 7698;

-> FIRST ROW : 조건에 맞는 레코드 하나를 찾았을 때 탐색을 멈춘다는 의미.
-> 이를 'FIRST ROW StopKey 알고리즘'으로 부르자.

  • 스캔 과정

  • DEPTNO=30 인 경우인 데이터의 가장 오른쪽부터 조회하면, MGR+SAL 값이 최대인 경우에 해당한다. 거기서 MGR 이 '7698'인 첫번째 레코드를 만나면 바로 그게 찾던 레코드가 된다.

    • DEPTNO는 엑세스 조건, MGR 는 필터 조건이 된 상황.

인덱스가 SAL + DEPTNO + MGR로 구성된 경우라면?
Index Range Scan 은 안되지만 Index Full Scan.
가장 오른쪽에서부터 탐색을 시작해서, DEPTNO=30, MGR=7698인 첫번째 레코드를 찾으면 탐색을 종료하면 됨.

-> 조건절 칼럼과 MAX 칼럼이 모두 인덱스에 구성된 경우.

그렇지 않은 경우라면?
인덱스가 DEPTNO + SAL 로만 구성된 경우

CREATE INDEX EMP_X1 ON EMP (DEPTNO, SAL );

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

-> DEP_NO = 30 인 전체 레코드를 읽어서 테이블에서 MGR이 7698인 조건을 필터링 한 후, MAX(SAL)을 구한다.
즉, FIRST ROW StopKey 알고리즘이 실행되지 않았다.

**TOP N 쿼리 이용해서 최소/최대값 구하기

같은 조건에서도 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;

-> 위의 MIN/MAX 쿼리보다는 더 좋은 성능을 보인다.
DEPT_NO 에 해당하는 모든 테이블을 읽는 것이 아니라 DEPT_NO=30 에 해당하는 레코드 사이에서 가장 오른쪽부터 탐색하면서 MGR=7698인 레코드를 찾으면 바로 멈춘다.

5.3.4. 이력 조회

  • 이력을 관리하는 테이블의 경우.

  • 이력을 조회할 때, First Row Stopkey 알고리즘 혹은 Top N Stopkey 알고리즘이 수행될 수 있게 인덱스를 설계하고 sql 을 작성하는 것이 고급 sql 튜너가 되기 위해 반드시 정복해야할 산이다.

  • 같이 산을 정복해봅시다 ㅎㅎ

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

장비구분코드가 'A001' 인 데이터 중 상태코드가 변경된 가장 최신날짜를 상태변경이력 테이블에서 MAX(변경일자)로 조회한뒤 '최종변경일자'로 매핑하고 있다.

  • FIRST ROW Stopkey 알고리즘이 작동하는 것을 볼 수 있다.

  • 상태변경이력_PK 의 구성이 장비번호, 변경일자, 변경순번 으로 구성.

  • 만약 최종 변경 순번까지 알고 싶다면?

  • 많은 개발자들이 작성하는 쿼리는 다음과 같다.

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'

이때 실행계획

  • 인덱스 칼럼을 가공 -> First Row Stopkey 알고리즘이 사용되지 않았다.
  • 장비별 상태변경이력이 많다면, 문제가 될 수 있다.
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'
  • 상태 변경 이력 테이블을 2번 순회하지만 First Row Stopkey 알고리즘이 작동하므로, 성능은 비교적 좋다.
  • 하지만 조회하고 싶은 칼럼의 수가 많아진다면 쿼리가 매우 복잡해질 것.
  • 단순하게 쿼리를 사용하면서도 성능을 올리기 위해서 튜너들이 사용해온 전통적인 방식

  • index_desc 힌트, rownum <= 1 조건

    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'
    )
  • 아까는 최종변경이력을 변경일자+변경순번만 조회했었는데

  • 지금은 변경일자+변경순번+상태코드까지 합침 , ROWNUM <=1 조건

    • 인덱스 구성이 잘 되어있어야 함. (장비번호 + 변경일자 + 변경순번)
    • index 를 거꾸로 읽다가 조건에 부합하면 바로 조회하는 방식.
  • 쿼리를 이렇게 사용했던 이유.

  • 이런 느낌의 쿼리가 파싱 오류가 발생하기 때문에.

SELECT 장비번호, 장비명,
, SUBSTR(최종이력, 1, 8) 최종변경일자
, TO_NUMBER(SUBSTR(최종이력, 9, 4)) 최종변경순번
, SUBSTR(최종이력, 13) 최종상태코드
FROM (
SELECT 장비번호, 장비명
, (SELECT 변경일자 || LPAD (변경순번, 4) || 상태코드
FROM (SELECT 변경일자 , 변경순번 , 상태코드
FROM 상태변경이력
WHERE 장비번호 = P.장비번호 -- ORA-00904 (부적합한 식별자) 오류
ORDER BY 변경일자 DESC , 변경순번 DESC)
WHERE ROWNUM <= 1) 최종이력
FROM 장비 P
WHERE 장비구분코드 = 'A001'

- 변경일자, 변경 순번을 기준으로 desc order 한 후에 rownum <= 1을 수행하고 싶은데, 
- 파싱 오류 발생
    - 메인 쿼리 컬럼을 서브 쿼리 내 인라인 뷰에서 참조했기 때문에 발생
- 12c 버전부터는 오류 없이 실행된다. 

11g 버전에서는 이렇게 실행했었음. 
```sql
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'
  • 변경일자, 변경순번을 기준으로 정렬을 수행한 후에, 장비번호가 맞는 지 필터링하고 rownum <=1 조건을 처리할 것 같지만 실제로는 그렇지 않다.
  • Predicate Pushing’이라고 불리는 쿼리 변환이 작동한 결과로, WHERE 장비번호 P.장비번호 조건이 인라인 뷰 안쪽으로 들어간다.
  • 12c 버전
    SELECT 장비번호, 장비명,
      SUBSTR(최종이력, 1, 8) 최종변경일자
      ,TO_NUMBER (S UBSTR ( 최종이력 , 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'
    )
    




![](https://i.imgur.com/c6y9H6E.png)


**상황에 따라 달라져야하는 이력 조회 패턴**
본 절의 주제가 ‘인덱스를 이용한 소트 연산 생략’이므로 ‘First Row Stopkey’ 또는 ‘Top N Stopkey’ 알고리즘이 작동할 수 있게 SQL 을 작성하는 내용 중심으로 살펴봤지만, 상황에 따라 이력 조회 패턴도 달라야 한다.

- 특히,전체(또는 상당히 많은) 장비의 이력을 조회할 때는 인덱스를 이용한 Stopkey 기능 작동여부가 튜닝의 핵심요소가 아니다. 인덱스 활용 패턴은 랜덤 I/O 발생량만큼 성능도 비례해서 느려지므로 대량 데이터 조회할 때 결코 좋은 솔루션이 되지 못한다.

```sql
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;
  • ROW_NUMBER()
    • 결과 집합 내에서 지정한 정렬 순서에 따라 각 행에 고유한 순번을 부여하는 함수
    • 윈도우 함수이므로, GROUP BY처럼 집계해서 줄이는 게 아니라 원본 행 개수를 유지하면서 각 행에 순번을 붙입니다.

선분이력 맛보기

  • 이런 식으로 상태변경이력을 관리하는 것도 가능하다.

  • 다양한 이력모델과 조회 패턴의 장단점을 연구함으로써 각 상황에 맞는 최적 패턴을 선택할 수 있기를...

5.3.5. Sort Group By 생략
인덱스를 이용해 소트 연산을 생략할 수 있다는 사실은 많이 알려졌고 이해하기도 비교적 쉽다. 반면, 그룹핑 연산에도 인덱스를 활용할 수 있다는 사실은 모르는 분이 많다.
아래 SQL에 region 이 선두 컬럼인 인덱스를 이용하면 ,Sort Group By 연산을 생략할 수 있다.

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

region 이 선두칼럼인 인덱스를 사용한다면, sort 도 생략 가능하고 부분범위처리도 가능해진다.

예시: ArraySize = 3일 때.

  1. 인덱스에서 ‘A’ 구간을 스캔하면서 테이블을 액세스하다가 ‘ B’를 만나는 순간, 그때까지 집계한 값을 운반단위에 저장한다.
  2. 계속해서 ‘B’ 구간을 스캔하다가 ‘ C’를 만나는 순간, 그때까지 집계한 값을 운반단위에 저장한다.
  3. 계속해서 ‘C’ 구간을 스캔하다가 ‘ D’를 만나는 순간, 그때까지 집계한 값을 운반단위에 저장한다, Array Size 가 3 이므로 지금까지 읽은 A, B, C에 대한 집계결과를 클라이언트에게 전송하고 다음 Fetch Call 이 올 때 까지 기다린다, (추가 Fetch Call 이 오지 않을 수도 있다. 그럴 경우,작업은 여기서 끝난다.)
  4. 클라이언트로부터 디음 Fetch Call 이 오면, 1 ~3 과정을 반복한다. 물론, 두 번째 Fetch Call 에서는 ‘D’ 구간부터 읽기 시작한다.