2015년 8월 5일 수요일

오라클 AWR의 기본과 export/import

AWR export / import
최근에 AWR을 이용한 DBMS 점검을 수행해야할 일이 있었다.
하지만 며칠을 해당 사이트에 방문하여 작업을 할 수 없는 관계로 AWR을 export하여 import하여 분석하기로 했다.
AWR(Auto Workload Repository) DBMS의 성능 상태 모니터링을 위해서 기본 1시간에 한번씩 7일간 DBMS에 발생한 이벤트 및 통계 정보 등을 DISK로 저장을 한다.
Oracle Dictionary에서 "WRH$"로 시작하는 테이블에 저장되며 "DBA_HIST_" 로 시작하는 View의 조회를 통해서 확인할 수 있다.
그리고 AWR은 dbms_workload_repository 패키지를 이용해서 조작할 수 있다.
아래는 DISK로 저장하는(snapshot) 주기 및 보관 기간을 10분 주기로 10일간 보관하는 예제 이다.
 begin
   dbms_workload_repository.modify_snapshot_settings(retention => 10*24*60, interval => 10);
 end;
 /

또한 create_snapshot 함수를 이용해서 snapshot을 생성할 수도 있다.
 begin
   dbms_workload_repository.create_snapshot([&flush_level=TYPICAL|ALL|BASIC]);
 end;
 /
그리고 아래 예제를 통해서 snapshot을 삭제할 수도 있다.
 begin
   dbms_workload_repository.drop_snapshot_range(&low_snap_id, &high_snap_id, [&dbid])
 end;
 /

이렇게 DISK로 저장된 snapshot을 보고서 형태로 조회하기 위해서는 dbms_workload_repository 패키지를 이용하는 방법과
 - select * from table(dbms_workload_repository.awr_report_html( &db_id, &inst_num, &begin_snap, &end_snap));  <-- html 형태로 보기
 - select * from table(dbms_workload_repository.awr_report_text( &db_id, &inst_num, &begin_snap, &end_snap));  <-- text 형태로 보기
오라클에서 제공하는 awrrpt.sql을 이용하는 방법이 있다.
 - @?/rdbms/admin/awrrpt
때로는 엑셀 등을 이용해서 chart를 그리고 별도의 보고서를 작성하기 위해서 "DBA_HIST_"로 시작하는 View를 직접 조회할 수도 있다.
위의 dbms_workload_repository를 이용하여 AWR 보고서를 조회하기위해서는
아래와 같은 SQL을 이용해서 database id와 instance number 그리고 begin snapshot id, end snapshot id를 확인할 수 있다.
 - select dbid, snap_id, startup_time, begin_interval_time, end_interval_time
   from   dba_hist_snapshot
   where  begin_interval_time >= to_char('2015.07.10 12:00', 'yyyy.mm.dd hh24:mi')
   and    end_interval_time <= to_char('2015.07.10 15:00', 'yyyy.mm.dd hh24:mi')
   order by snap_id;
  
만들어진 보고서는 begin snapshot과 end snapshot 사이의 delta 값으로 만들어지게 된다.
즉, begin_interval_time과 end_interval_time 사이의 delta 값이 되겠다.  
  
DBMS가 정상적인 경우(아무 문제가 없는)와 문제가 발생한 경우 또는 튜닝 전후를 비교하기 위해 Baseline를 생성해둘 수도 있다.
 begin
   dbms_workload_repository.create_baseline(&start_snap_id, &end_snap_id, &baseline_name, &dbid);
 end;
 /

이렇게 생성한 Baseline은 modify_snapshot_settings 함수로 설정한 보관기간이 지나도 삭제되지 않으며,
Baseline을 삭제할 때 Baseline에 해당하는 snapshot을 함께 삭제할 것인지 cascade 옵션으로 결정할 수 있다.
아래는 생성한 Baseline을 삭제한다.
 begin
   dbms_workload_repository.drop_baseline(&baseline_name, [&cascade], [&dbid]);
 end;
 /

Baseline과 특정 snapshot과의 비교를 위해서는 dbms_workload_repository 패키지를 이용하는 방법과
 - select * from table(dbms_workload_repository.awr_diff_report_html( &db_id, &inst_num, &begin_snap, &end_snap));  <-- html 형태로 보기
 - select * from table(dbms_workload_repository.awr_diff_report_text( &db_id, &inst_num, &begin_snap, &end_snap));  <-- text형태로 보기
오라클에서 제공하는 awrddrpt.sql 이용하는 방법이 있다.
 - @?/rdbms/admin/awrddrpt.sql

간단하게 AWR에 대해서 알아보았으며 각 옵션 등 더 상세한 내용은 Oracle 매뉴얼 또는 다른 문서를 참조하고
이제 AWR을 export하고 import하는 방법에 대해서 알아보자.
AWR export/import는 Oracle의 Data Pump를 이용하게 되며, Data Pump에서 사용할 Oracle Directory 객체를 미리 생성해 두어야 한다.
 - create or replace directory <directory_name> as '<os directory path>';
이미 Data Pump를 위한 directory가 있다면 별도로 생성할 필요는 없다.
AWR export는 오라클에서 제공하는 awrextr.sql 을 실행하면 된다.
 - @?/rdbms/admin/awrextr.sql
awrextr.sql을 실행하면 아래와 같은 절차로 진행하게되며, 완료되면 지정한 directory에 export된 파일이 생성된다.
 1. database id를 입력
 2. display될 snapshot id의 기간을 일단위로 입력
    ex) 최근 2일치의 snapshot id를 보고싶다면 2를 입력
 3. export받을 begin snapshot id와 end snapshot id 입력
 4. export받는 데이터 파일이 생성될 oracle directory명 입력
 5. export받을 file명(기본으로 dmp 확장자가 붙으니 file명만 입력)

그 다음 AWR을 import 해보자. AWR import도 오라클에서 제공하는 awrload.sql을 실행하면 된다.
주의사항은 export받은 DBID와 동일한 DBID로 운영중인 DBMS에는 import할 수가 없다.
즉 export 받은 DBMS에는 import할 수가 없다는 것이다. 결국 다른 DBMS가 필요하다.
먼저 export 받은 파일을 Oracle Directory로 지정된 OS 디렉토리에 복사한후 awrload.sql을 실행한다.
 - @?/rdbms/admin/awrload.sql
awrload.sql을 실행면 아래와 같은 절차로 진행하게되며, 완료되면 "DBA_HIST_" View를 통해 조회할 수 있다.
 - export 파일을 복사한 Oracle directory명 입력
 - export file명 입력(입력받은 file명에 기본으로 dmp 확장자가 붙으니 확장자를 제외한 file명만 입력)
 - import할 동안 사용될 임시 user명을 입력(이 user는 import가 완료되면 자동으로 삭제되니 기본값을 사용)
 - 임시 user가 사용할 default tablespace 입력(변경 가능하나 기본값을 사용)
 - 임시 user가 사용할 임시 tablespace 입력(변경 가능하나 기본값을 사용)
여기까지 진행하면 import가 시작되며, import가 모두 진행된 이후에는 import를 수행한 DBMS의 AWR 테이블인 WRH$ 테이블에 import 데이터를 migration 하게된다.
이후 import시 사용한 임시 user는 cascade 옵션으로 자동 삭제된다.
import를 진행한 후에 WRH$ 테이블에 import 데이터를 밀어넣을 때 Oracle의 version이 다르면 문제가 발생하게 된다.
내가 AWR을 export한 Oracle version은 11.2.0.4 인데 import한 Oracle version은 11.2.0.1 이었다.
마이너 버전에서 문제가 발생한 것이다. 확인해 보니 11.2.0.4 버전의 AWR과 11.2.0.1 버전의 AWR에는 다음과 같은 차이가 있었다.



11.2.0.1
컬럼개수
11.2.0.4
컬럼개수
차이개수
WRH$_ACTIVE_SESSION_HISTORY67WRH$_ACTIVE_SESSION_HISTORY70-3
WRH$_ACTIVE_SESSION_HISTORY_BL67WRH$_ACTIVE_SESSION_HISTORY_BL70-3
WRH$_BUFFERED_QUEUES11WRH$_BUFFERED_QUEUES23-12
WRH$_BUFFERED_SUBSCRIBERS13WRH$_BUFFERED_SUBSCRIBERS26-13
WRH$_PERSISTENT_QUEUES21WRH$_PERSISTENT_QUEUES29-8
WRH$_PERSISTENT_SUBSCRIBERS16WRH$_PERSISTENT_SUBSCRIBERS22-6
WRH$_RSRC_CONSUMER_GROUP35WRH$_RSRC_CONSUMER_GROUP41-6
WRH$_RSRC_PLAN9WRH$_RSRC_PLAN10-1
WRM$_SNAPSHOT12WRM$_SNAPSHOT13-1
없음 WRH$_MVPARAMETER8-8
없음 WRH$_MVPARAMETER_BL8-8
없음 WRH$_PERSISTENT_QMN_CACHE27-27
없음 WRH$_TABLESPACE7-7
없음 WRM$_SNAPSHOT_DETAILS6-6

11.2.0.4 버전에서는 11.2.0.1 버전보다 더 많은 성능 정보를 저장하는 것으로 업그레이드가 되어서 정상적으로 migration을 할 수 없었던 것이다.
결국 11.2.0.4 버전으로 업그레이드를 수행하고서야 정상적으로 데이터를 migration을 할 수 있었다.
12c 버전에서 11g 버전의 AWR 데이터를 정상적으로 migration이 되는지를 확인해 볼 필요가 있겠다.

다음에는 AWR 을 이용해서 Excel 등으로 별도의 보고서를 만드는 방법을 게시하도록 하겠다.

2015년 5월 17일 일요일

컬럼값을 레코드로 변환하기


거의 2년만에 새로운 글을 올리게 되어 기분이 남다르다. ^^;

나는 지난달 MySQL에 대한 튜닝 요청이 있어서 약 3주간 지원을 다녀온 적이 있다.

오라클이 아니라서 살짝 MySQL 책을 사서 맛만 좀 보고 지원을 하게되었다.

사실 직접 가서 확인해보니, MySQL2TB에 달하는 대용량 데이터를 운영하고 있는 것이 놀라울 정도였다. 전체 데이터는 약 2TB, 가장 큰 테이블 하나가 약 200GB 였다. MySQL의 다른 면을 보는 듯 했다.

튜닝이 필요한 SQL을 전달 받고서 점검을 수행하던 중에 매우 특이한 SQL을 발견했다.

SQL문장 하나가 4만라인... ~~

도저히 이해가 되지 않아 가만히 들어다 보니, 응용프로그램에서 SQL을 동적으로 생성하여 실행한 SQL처럼 보였다.

대략 아래와 같은 패턴이었다.

SELECT ...
FROM   (
            SELECT 1 AS CNT FROM DUAL
            UNION ALL
            SELECT 1 AS CNT FROM DUAL
            UNION ALL
            SELECT 1 AS CNT FROM DUAL
            UNION ALL
            SELECT 1 AS CNT FROM DUAL
            .....
            UNION ALL
            SELECT 1 AS CNT FROM DUAL
          )

위와 같은 SQL이 생성 되어진 이유는 다음과 같았다.

1. 다음과 같이 테이블A 가 존재하는데
2. 테이블에 데이터가 입력되는 형태는 아래와 같이 생성시간과 생성유형 컬럼에 버티컬바(‘|’)를 구분 값으로 하여 쌍으로 입력되며, 생성개수 컬럼에는 쌍의 개수가 입력되어 진다.


일련번호


생성시간


생성유형


생성개수


1


2015010100|2015010101|2015010102|


080101|080101|090102|


40


2


2015010200|2015010201|2015010202|


090101|080101|090102|


45


3


2015010300|2015010301|2015010302|


080101|090101|090102|


80


4


2015010400|2015010401|2015010402|


080101|080101|080102|


30


5


2015010500|2015010501|2015010502|


070101|080101|090102|


109


3. 이렇게 입력된 데이터를 생성시간의 일자, 생성유형별로 그룹핑하여 개수를 구해야 하는데 이를 구하기 위해서 프로그램을 작성하여 한 건씩 패치하여 SQL문장을 생성 및 실행하게 된다.

String SQL = “”;
while(rs.next())
{
   String[] createTime = rs.getString(1).split("|");
   String[] createType = rs.getString(2).split("|");
   int createCnt = rs.getInt(3);
   for(int i = 0; i < createCnt; i++)
   {
      SQL += “select ‘“ + createDate[I] + “_” + createType[I] + “’ as d, ‘1’ as cnt from dual union all”;
}
}
rs.close();
/* to-do */
SQL 문장 실행


위와 같이 SQL 문장을 만들게 되니 4만 라인의 SQL이 생성된 것이다.

이러한 로직에는 어떤 문제점이 있을까?

우선 첫 번째로 불필요한 fetch를 수행하게 되고,

두 번째 불필요한 network 부하를 발생하게 된다. 그러므로 해당 업무의 성능이 저하될 수 밖에 없다.

그럼 이런 문제점들을 어떻게 해결할 수 있을까? 답은 간단하다. 그냥 SQL로 처리하는 것이다.

위의 데이터에서 우리는 각 레코드를 응용프로그램에서 fetch하지 않고 생성일자, 생성유형별 개수를 한방의 SQL로 추출할 수 있는 중요한 두 가지를 발견할 수 있다.

첫 번째 ‘|’로 구분되는 생성시간과 생성유형의 각 값의 길이가 고정되어 있다는 것과

두 번째 생성개수 컬럼에 의해 몇 개가 생성되어 있는지 알 수 있다는 것이다.
이렇게 두 가지만 알면 ‘|’로 구분된 각 컬럼의 데이터를 레코드로 변환할 수 있다.

실제로 테스트를 수행해 보자

1. 먼저 테스트를 위한 테이블을 만들자.
   CREATE TABLE 테스트A
   (
  일련번호 NUMBER(10) PRIMARY KEY,
     생성시간 VARCHAR2(4000) NOT NULL,
     생성유형 VARCHAR2(4000) NOT NULL,
     생성개수 NUMBER(5) NOT NULL
   );

2. 데이터를 편하게 넣기 위해 SEQUENCE를 하나 생성하자.    CREATE SEQUENCE SEQ1 START WITH 1 INCREMENT BY 1 MINVALUE 1 NOCYCLE;
3. 테스트 데이터를 입력하자
INSERT INTO 테스트A VALUES (SEQ1.NEXTVAL, '2015010100|2015010101|2015010102|2015010115', '080101|080101|090102|090102', 4);
INSERT INTO 테스트A VALUES (SEQ1.NEXTVAL, '2015010200|2015010201|2015010202|2015010202|2015010203', '090101|080101|090102|080101|080101', 5);
INSERT INTO 테스트A VALUES (SEQ1.NEXTVAL, '2015010300|2015010301|2015010302|2015010302', '080101|090101|090102|080101', 4);
INSERT INTO 테스트A VALUES (SEQ1.NEXTVAL, '2015010400|2015010401|2015010402', '080101|080101|080102', 3);
INSERT INTO 테스트A VALUES (SEQ1.NEXTVAL, '2015010500|2015010501|2015010502|2015010510|2015010510|2015010510', '070101|080101|090102|090102|070101|080101', 6);
COMMIT;
4. ‘|’로 구분된 값을 레코드로 변환하기 위해서는 데이터 복제방법을 사용해야 한다. 오라클 이라면 CONNECT BY 절을 사용하여 쉽게 데이터 복제를 위한 데이터를 생성할 수 있겠지만, 다른 DBMS에서도 활용할 수 있게 하려면 고전적인 COPY_T 테이블을 생성해야 한다.
CREATE TABLE COPY_T
(
  NO NUMBER(10) NOT NULL PRIMARY KEY
);
INSERT INTO COPY_T
SELECT LEVEL FROM DUAL CONNECT BY LEVEL <= 100;

MySQL 이라면 아래와 같이 COPY_T 테이블에 데이터를 입력할 수 있다.
CREATE TABLE COPY_T
(
  NO INT PRIMARY KEY
);
INSERT INTO COPY_T
SELECT @RN:=@RN+1 AS ROWNUM
FROM (SELECT 1 FROM <AnyTable>) AS T, (SELECT @RN:=0) AS R
LIMIT 500000;

5. 이제 준비가 완료 되었다.
SQL을 작성하여 컬럼을 레코드가 아닌 데이터 값을 레코드로 변환해보자. 원리는 간단하다. 먼저 이야기 했듯이 우리는 각 레코드의 값에 몇 개씩 생성되었는지 이미 개수를 알고 있으므로 COPY_T 테이블을 이용하여 그 개수만큼 레코드를 복제하면 된다.

SELECT  A.생성시간, A.생성유형, T.NO
FROM   테스트A A, COPY_T T
WHERE  T.NO <= A.생성개수
ORDER BY 생성시간, 생성시간, NO;

6. 레코드가 생성개수만큼 복제 되었다. 다음으로 우리가 알고 있는 정보를 사용해야 한다. 생성시간은 10자리마다 ‘|’로 구분되어 있으며, 생성유형은 6자리마다 ‘|’로 구분되어 있다. 이정보를 사용하여 각 레코드별로 해당되는 위치의 값을 잘라서 취해보자.

SELECT  SUBSTR(A.생성시간, (CASE WHEN T.NO = 1 THEN 1 ELSE (T.NO * 10 + T.NO - 11 + 1) END), 10) AS 생성시간
       ,SUBSTR(A.생성유형, (CASE WHEN T.NO = 1 THEN 1 ELSE (T.NO * 6 + T.NO - 7 + 1) END), 6) AS 생성유형, T.NO
FROM   테스트A A, COPY_T T
WHERE  T.NO <= A.생성개수
ORDER BY 생성시간, 생성시간, NO;

이렇게 하면 구분자로 분리된 값을 레코드로 변환하여 다른 테이블과 조인도 수행할 수 있다.
 
다음을 기약하며 이번 포스팅은 여기까지다.

2013년 7월 19일 금요일

PostgreSQL vs Oracle

최근에 PostgreSQL에 대해서 연구를 해야할 일이 있었다.
아직까지 PostgreSQL의 정확한 아키텍처가 눈에 들어오지는 않았지만, 오라클과 비교를 해보았다.  나름 나의 생각을 정리한 내역이며, 정확한 정보가 아님을 밝힌다.


1. 데이터베이스 파일 비교

2. 메모리 구조 비교

3. 프로세스 비교

오라클 소트 작업

일반적으로 sort 작업은 메모리 sort와 디스트 sort로 구분할 수 있다. SQL문에서 sort는 성능상에 아주 나쁜 영향을 미친다.

SQL을 수행하는 도중 소트 오퍼레이션이 필요해 질때 마다 DBMS는 정해진 메모리 공간에
소트 영역(SORT AREA)을 할당하고 정렬을 수행하게 된다.
Oracle은 sort 영역을 PGA에 할당하게 되고 가급적 소트 영역 내에서 데이터 정렬 작업을 완료하는 것이 최적이지만, 대량의 데이터를 정렬할 땐 디스크 소트가 불가피 하다.
소트 영역의 메모리 공간이 부족할 경우에 Oracle은 Temp Tablespace를 이용하여 정렬을 수행한다. 간단하게 메모리 sort와 디스크 sort에 대해서 정리하면 아래와 같다.

  • 메모리 소트 : 전체 데이터의 정렬 작업을 할당받은 소트 영역 내에서 완료하는 것을 말하며, ‘Internal Sort’ 또는 ‘Optimal Sort’라고도 한다.
  • 디스크 소트 : 할당 받은 소트 영역 내에서 정렬을 완료하지 못해 디스크 공간까지 사용하는 경우를 말하며, ‘External Sort’ 라고도 한다.
    디스크에 임시 저장 했다가 다시 읽는 작업을 반복한 횟수에 따라 디스크 소트를 두 가지로 구분하기도 한다.
     - Onepass Sort : 정렬 대상 집합을 디스크에 한 번만 기록
     - Multipass Sort : 정렬 대상 집합을 디스크에 여러 번 기록
다음은 소트를 발생시키는 오퍼레이션 들이다.
  • Sort Aggregate : 전체 로우를 대상으로 집계를 수행할 때 나타나며, 실제 소트가 발생하지는 않는다.
----------------------------------------------------------------------------
| Id  | Operation          | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |       |     1 |    13 |     3   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |       |     1 |    13 |            |          |
|   2 |   TABLE ACCESS FULL| EMP_T |    14 |   182 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------
  • Sort Order By : 정렬된 결과집합을 얻고자 할 때 나타난다.
----------------------------------------------------------------------------
| Id  | Operation          | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |       |    14 |  1218 |     4  (25)| 00:00:01 |
|   1 |  SORT ORDER BY     |       |    14 |  1218 |     4  (25)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| EMP_T |    14 |  1218 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------
  • Sort Group By : Sorting 알고리즘을 사용해 그룹별 집계를 수행할 때 나타난다. Oracle의 경우 Hashing 알고리즘으로 그룹별 집계를 수행하기도 하는데(10g 이상) 그때는 실행계획에 ‘HASH (GROUP BY) ‘ 가 나타 난다
----------------------------------------------------------------------------
| Id  | Operation          | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |       |    14 |   448 |     4  (25)| 00:00:01 |
|   1 |  SORT GROUP BY     |       |    14 |   448 |     4  (25)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| EMP |    14 |   448 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------

----------------------------------------------------------------------------
| Id  | Operation          | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |       |    14 |   448 |     4  (25)| 00:00:01 |
|   1 |  HASH GROUP BY     |       |    14 |   448 |     4  (25)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| EMP |    14 |   448 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------
  • Sort Unique : 선택된 결과집합에서 중복 레코드를 제거하고자 할 때 나타난다. Union 연산자나 Distinct 연산자를 사용할 때가 대표적이다.
----------------------------------------------------------------------------
| Id  | Operation          | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |       |    14 |   182 |     5  (40)| 00:00:01 |
|   1 |  SORT UNIQUE       |       |    14 |   182 |     4  (25)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| EMP |    14 |   182 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------
----------------------------------------------------------------------------
| Id  | Operation          | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |       |    14 |   182 |     5  (40)| 00:00:01 |
|   1 |  HASH UNIQUE       |       |    14 |   182 |     4  (25)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| EMP |    14 |   182 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------
  • Sort Join : Sort Merge Join을 수행할 때 나타 난다
------------------------------------------------------------------------------
| Id  | Operation           | Name   | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |        |    14 |  1638 |     8  (25)| 00:00:01 |
|   1 |  MERGE JOIN         |        |    14 |  1638 |     8  (25)| 00:00:01 |
|   2 |   SORT JOIN         |        |    14 |  1218 |     4  (25)| 00:00:01 |
|   3 |    TABLE ACCESS FULL| EMP  |    14 |  1218 |     3   (0)| 00:00:01 |
|*  4 |   SORT JOIN         |        |     4 |   120 |     4  (25)| 00:00:01 |
|   5 |    TABLE ACCESS FULL| DEPT |     4 |   120 |     3   (0)| 00:00:01 |
------------------------------------------------------------------------------
  • Window Sort : 윈도우 함수를 수행할 때 나타난다.
----------------------------------------------------------------------------
| Id  | Operation          | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |       |    14 |   546 |     4  (25)| 00:00:01 |
|   1 |  WINDOW SORT       |       |    14 |   546 |     4  (25)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| EMP |    14 |   546 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------

소트 오퍼레이션은 메모리 집약적(Memory-intensive)일뿐만 아니라 CPU 집약적(CPU-intensive)이다.
데이터량이 많을 때는 디스크 I/O까지 발생시키므로 쿼리 성능을 크게 떨어뜨린다.
특히, 부분범위처리를 할 수 없게 만들어 OLTP 환경에서 성능을 떨어뜨리는 주요인이 되곤 한다.
가능하면 소트가 발생하지 않도록 SQL을 작성해야 하고, 소트가 불가피하다면 메모리 내에서 수행을 완료할 수 있도록 해야 한다.