posted by 구름너머 2006. 8. 9. 16:40
자동화 툴로 채울 수 없는 DB 성능관리 2% 2 :테이블 파티셔닝의 재발견 | 낙서장2006/08/09 14:17
http://blog.naver.com/k2sunny/70007105618

자동화 툴로 채울 수 없는 DB 성능관리 2% 2

테이블 파티셔닝의 재발견

남준현 |데이터베이스 모델링, 튜닝 전문 컨설턴트

기업들의 데이터베이스가 대용량화 되면서 이를 효과적으로 관리할 수 있는 방안을 찾는 것이 관리자들의 주요 업무가 됐다. 이를 위한 매우 효과적인 방안 가운데 하나가 파티셔닝이다. 일반적으로 단순한 명령어 위주로만 알려져 있지만 실제 현장에서 접하는 파티셔닝의 효용은 그 이상이다. 익숙한 개념이지만 그동안 제대로 알지 못했던 파티셔닝의 의미와 대표적인 활용 사례를 살펴보자.

필자는 많은 현장 사이트에서 대용량의 가치있는 데이터들이 놀라운 능력을 보유하고 있는 데이터베이스 안에서 사용자의 무지로 인해 방치돼 있거나 잘못 사용되고 있어 역효과를 일으키는 모습을 많이 보아 왔다. 예를 들어 총 테이블 건수 1억 건이 넘는 상황에서 우리가 어떤 형태로든 건드려야 할 부분이 약 10% 정도라고 할 때 그 테이블 전체를 읽지 않고 1000만 건만 읽을 수 있게 해야 하는 것이 당연하지만 실제로는 그렇지 못한 경우를 많이 봐 왔다. 어떻게 처리해야겠다는 생각도 없이 무조건 명령어(command)부터 날리는 것이다. 그렇다면 필요한 테이블 만을 다루려면 어떻게 해야 할까. 이를 위해 필요한 개념이 바로 테이블 파티셔닝(Table Parti tioning)이다.

파티셔닝은 지난 강좌에서 살펴본 사항들과 함께 어떤 자동화된 툴로도 해결할 수 없는 부분으로 실제로 어떤 상황에서 파티셔닝이 필요하다고 정형화된 법칙은 없다. 중소 용량의 데이터베이스에서도 상황에 따라 꼭 사용해야 하는 경우가 있고, 초대용량의 경우 파티셔닝을 쓰지 않으면 시스템 자체가 관리되지 않을 수도 있다(필자 역시 컨설팅을 하면서 파티셔닝을 이용해 많은 시스템을 효율적으로 운영할 수 있다는 것을 직간접적으로 체험한 바 있다).

그러나 대부분의 파티셔닝 관련 자료들은 형식적으로 파티셔닝의 종류를 나열하고 스크립트 정도를 언급하는 수준이다. 이런 식의 접근은 한계가 명확하다. 오히려 파티셔닝을 올바르게 이용하기 위해서는 먼저 데이터베이스 액세스 방식의 정확한 차이와 장단점 그리고 파티션을 이용한 풀 스캔(full scan)에 대해 정확하게 이해할 필요가 있다. 파티셔닝은 일종의 기능일 뿐이어서 스캔에 대한 정확한 이해없이는 이를 사용할 이유도, 어떻게 사용해야 할지도 전혀 알 수가 없다. 각 스캔 방식의 장단점을 알고 어떤 상황에서 어떤 스캔 방법이 유리한 지를 명확하게 이해해야 그에 대한 보완책으로서 파티셔닝의 가치가 보이기 시작한다.

파티셔닝 세계 입문

대용량 테이블이나 인덱스를 파티셔닝한다는 것은 하나의 Object를 여러 개의 세그먼트로 나눈다는 의미이다. 즉 하나의 테이블이나 인덱스가 동일한 논리적 속성을 가진 여러 개의 단위(partition)로 나누어져 각각이 PCTFREE, PCTUSED, INITRANS, MAXTRANS, TABLESPACE, STORAGE PARAMETER 등 별도의 물리적 속성을 갖는 것이다. 특히 관리해야 할 데이터가 늘어나면 성능과 스토리지 관점에서 문제가 생길 수 있는데, 이를 해결할 수 있는 효율적인 방법 가운데 하나가 곧 파티셔닝이다. 파티셔닝은 보통 다음과 같은 장점을 갖고 있다.

데이터 액세스시(특히 풀 스캔시) 액세스의 범위를 줄여 성능을 향상시킨다.
물리적으로 여러 영역으로 파티셔닝해 전체 데이터의 훼손 가능성이 줄어들고 데이터 가용성이 향상된다.
각 파티션별로 백업, 복구 작업을 할 수 있다.
테이블의 파티션 단위로 디스크 I/O를 분산해 부하를 줄일 수 있다.

오라클 DBMS에서 제공하는 파티셔닝 방식에는 레인지(range) 파티셔닝, 해시(hash) 파티셔닝, 리스트(list) 파티셔닝, 컴포지트(composite) 파티셔닝(레인지-해시, 레인지-리스트) 등이 있다.

특정 컬럼 값을 기준으로 분할하는 레인지 파티셔닝

레인지 파티셔닝은 어떤 특정 컬럼의 정렬 값을 기준으로 분할하는 것이다. 주로 순차적인(historical) 데이터를 관리하는 테이블에 많이 사용된다. 예를 들면 ‘가입계약’이라는 테이블이 있고 여기에 몇 년 동안의 데이터가 쌓여 있다면, 보통 5년치 데이터만 관리하고 이 가운데 자주 액세스하는 하는 것은 최근 1~2년 정도가 일반적이다. 따라서 이를 년별, 월별로 파티셔닝하고 애플리케이션의 SQL을 조정해 전체 데이터가 아닌 최근 정보를 가지고 있는 파티션만 액세스하도록 하면 전체 데이터베이스의 성능을 향상시킬 수 있다. 일부 기업의 경우 가입계약_1999, 가입계약_2000처럼 월별 또는 년별로 테이블을 따로 만들어 사용하기도 하지만 실제로 쓰는데 불편한 점이 많고 액세스하는 SQL이 복잡해지는 단점이 있다. 다음은 레인지 파티션을 만드는 DDL(Data Definition Language) 스크립트다.

CREATE TABLE CONTRACT    (I_YYYYMMDD VARCHAR2(8), I_CUSTOMER VARCHAR2(9), …… )TABLESPACE TBS1STORAGE (INITIAL 2M NEXT 2M PCTINCREASE 0)    PARTITION BY RANGE (I_YYYYMMDD)    (PARTITION PAR_200307 VALUES LESS THAN (‘20030801’),    PARTITION PAR_200308 VALUES LESS THAN (‘20030901’), …… )

PARTITION BY RANGE (COLUMN_LIST)는 특정 컬럼을 기준으로 파티셔닝할 것인지를 결정하는 것이고, VALUES LESS THAN (VALUE_LIST)는 해당 파티션이 어느 범위에 포함될 것인지 상한을 정하는 것이다. PARTITION BY RANGE에 나타나는 COLUMN_LIST를 파티셔닝 컬럼이라고 하며 이 값이 파티셔닝 키를 형성한다. 파티셔닝 컬럼은 결합 인덱스처럼 최대 16개까지 지정할 수 있다. VALUESS LESS THAN에 나타나는 VALUE_LIST는 파티셔닝 컬럼들의 상한 값으로, 여기 지정된 값보다 작은 값만을 저장하겠다는 의미이다. 이런 스크립트에서 지정한 물리적 속성들은 각 파티션들이 생성될 때 개별적으로 물리적 속성을 지정하지 않으면 각 파티션들은 이러한 속성 값을 적용받게 된다.

오직 성능 향상, 해시 파티셔닝

해시 파티셔닝은 특정 컬럼 값에 해시 함수를 적용해 분할하는 방식으로, 데이터의 관리보다는 성능 향상에 초점을 맞춘 개념이다. 레인지 파티셔닝은 각 범위에 따라 데이터 양이 달라 분포도가 일정치 않은 단점이 있는데, 해시 파티셔닝은 이런 단점을 보완해 일정한 분포를 가진 파티션으로 나누고, 균등한 분포도를 가질 수 있도록 조율해 병렬 프로세싱으로 성능을 높인다. 실제로 분포도를 정의하기 어려운 테이블을 파티셔닝을 할 때 많이 이용하고 2의 배수 개수로 파티셔닝하는 것이 일반적이다. 해시 파티셔닝으로 구분된 파티션들은 동일한 논리, 물리적 속성을 가진다(단 테이블스페이스(tablespace)는 유일하게 파티션별로 지정할 수 있다). 또한 레인지 파티션과 달리 각 파티션에 지정된 값들을 DBMS가 결정하므로 각 파티션에 어떤 값들이 들어 있는지 알 수 없다. 그러나 대용량의 분포도가 일정치 않은 테이블을 마이그레이션할 때는 프로그램 병렬 방식과 함께 유용하게 사용할 수 있다. 다음은 해시 파티션을 만드는 DDL 스크립트이다.

CREATE TABLE CONTRACT    ( SERIAL NUMBER, CODE VARCHAR2(4), ……)TABLESPACE TBS1STORAGE (INITIAL 2M NEXT 2M PCTINCREASE 0)PARTITION BY HASH(SERIAL)    (PARTITION PAR_HASH_1 TABLESPACE TBS2,    PARTITION PAR_HASH_2 TABLESPACE TBS3, ……)

함께 쓰일 때 더욱 강력한 리스트 파티셔닝

리스트 파티셔닝은 특정 컬럼의 특정 값을 기준으로 파티셔닝을 하는 방식이다. 주로 이질적인(distinct) 값이 많지 않고 분포도가 비슷하며 다양한 SQL의 액세스 패스에서 해당 컬럼의 조건이 많이 들어오는 경우 유용하게 사용된다. 예를 들어 ‘서비스 계약’이라는 테이블이 있고 서비스를 최초 가입한 대리점을 ‘가입 대리점’, 변경사항을 처리한 대리점을 ‘처리 대리점’이라고 한다면 모든 서비스의 가입, 해지, 전환 등의 처리 데이터에는 이 두 대리점이 존재한다. 테이블 구조를 보면 다음과 같다.

CREATE TABLE SERVICE_CONTRACT    (I_YYYYMMDD VARCHAR2(8), I_CUSTOMER VARCHAR2(6),    I_DLR_IND VARCHAR2(2), I_DEALER VARCHAR2(6), ……)

즉 I_DLR_IND(대리점 구분)라는 컬럼이 존재하고 ‘A’일 때는 ‘가입 대리점’, ‘S’일 때는 ‘처리 대리점’이라고 할 때 대부분의 조회 패턴에는 가입 대리점 또는 처리 대리점에 해당하는 값이 들어오기 마련이다. 이럴 때 I_DLR_IND로 리스트 파티셔닝을 한다면 어떨까. 즉 집합의 서브 타입을 분류할 때 리스트 파티션은 매우 유용하다. 지금 예로 든 것은 단편적인 것에 불과하지만 리스트 파티셔닝의 위력은 강력하다. 특히 컴포지트 파티션에서 레인지 파티션과 함께 사용하면 전체 데이터베이스의 성능을 크게 향상시킬 수 있다. 다음은 리스트 파티션을 만드는 DDL 스크립트이다.

CREATE TABLE SERVICE_CONTRACT    (I_YYYYMMDD VARCHAR2(8), I_CUSTOMER VARCHAR2(6),    I_DLR_IND VARCHAR2(2), I_DEALER VARCHAR2(6), …….)TABLESPACE TBS1STORAGE (INITIAL 2M NEXT 2M PCTINCREASE 0)PARTITION BY LIST (I_DLR_IND)    (PARTITION PAR_A VALUES (‘A’), PARTITION PAR_S VALUES (‘S’))

PARTITION BY LIST에 나타나는 COLUMN_LIST는 파티셔닝 컬럼으로 파티션 키에 해당하고(단 단일 컬럼만 지정할 수 있다), VALUESS LESS THAN에 나타나는 VALUE_LIST는 파티셔닝 컬럼들의 값이다. 여기에 나타낸 값에 해당하는 행들을 저장하겠다는 의미가 된다.

레인지의 장점을 그대로, 레인지-해시 컴포지트 파티셔닝

레인지-해시 컴포지트 파티셔닝은 레인지 방식을 사용해 데이터를 파티셔닝하고 각각의 파티션 내에서 해시 방식으로 서브 파티셔닝하는 방식이다. 서브 파티션이 독립된 세그먼트가 되는 것이 특징으로, 다음과 같은 장점이 있다.

관리와 성능 등 레인지 파티션의 장점을 그대로 수용한다.
해시 파티션의 이점인 데이터 균등 배치와 병렬화
서브 파티션에 특정 테이블스페이스를 지정할 수 있다.
서브 파티션별로 풀 스캔을 할 수 있어 스캔 범위를 줄여 성능을 향상시킨다.

레인지 파티션에서 해당 테이블이 단지 논리적인 구조이고 실제 데이터는 파티셔닝된 세그먼트에 저장됐던 것처럼 컴포지트 파티션에서도 해당 테이블과 파티셔닝된 테이블은 단지 파티셔닝을 위한 논리적인 구조일 뿐이다. 데이터는 가장 하위에 위치한 서브 파티션 영역에 저장된다. 다음은 레인지-해시 컴포지트 파티션을 생성하는 DDL 스크립트이다. PARTITION BY RANGE (I_YYYYMMDD)에 의해 레인지로 파티션을 한 후 SUBPARTITION BY HASH에 의해 서브 파티셔닝을 수행했음을 알 수 있다.

CREATE TABE TB_RANGE_HASH    (I_YYYYMMDD VARCHAR2(8), I_SERIAL NUMBER, SALE_PRICE NUMBER, ……)TABLESPACE TBS1STORAGE (INITIAL 2M NEXT 2M PCTINCREASE 0)PARTITION BY RANGE (I_YYYYMMDD)SUBPARTITION BY HASH (I_SERIAL)    (PARTITION SALES_1997 VALUES LESS THAN (‘19980101’)    (SUBPARTITION SALES_1997_Q1 TABLESPACE TBS2,    SUBPARTITION SALES_1997_Q2 TABLESPACE TBS3), ……)

레인지-리스트 컴포지트 파티셔닝

레인지-리스트 컴포지트 파티셔닝은 레인지 방식을 사용해 데이터를 파티셔닝하고 각 파티션 안에서 리스트 방식을 이용해 서브 파티셔닝하는 방식이다(이때 서브 파티션은 독립된 세그먼트가 된다). 레인지-리스트 컴포지트 파티션은 레인지-해시 컴포지트 파티션과 비슷하지만 서브 파티션이 리스트 파티션이라는 점이 다르다. 실제 업무에서는 레인지-해시보다 유용한 면이 많다. 다음은 레인지-리스트 컴포지트 파티션을 생성하는 DDL 스크립트이다.

CREATE TABLE TB_RANGE_LIST (    I_YYYYMMDD VARCHAR2(8), I_AGR_IND VARCHAR2(2), I_DELAER VARCHAR2(6), …….)TABLESPACE TBS1STORAGE (INITIAL 2M NEXT 2M PCTINCREASE 0 MAXEXTENTS UNLIMITED)PARTITION BY RANGE (I_YYYYMMDD)SUBPARTITION BY LIST (I_AGR_IND)     (PARTITION PAR_1997 VALUES LESS THAN (‘19980101’)    (SUBPARTITION PAR_1997_A VALUES (‘A’), SUBPARTITION PAR_1997_A VALUES (‘S’)),    ……)

파티션된 인덱스의 참뜻

‘파티션된 인덱스(partitioned index)’라고 하면 대부분의 개발자들은 로컬 인덱스를 떠올린다. 또한 파티션된 테이블에서만 쓰이는 것으로 생각한다. 그러나 이것은 명백한 오산이다. 파티션된 인덱스는 파티션된 테이블과 별개의 것으로, 단지 많은 상호 연관을 갖고 있을 뿐이다. 파티션된 인덱스는 문자 그대로 인덱스를 파티셔닝한 것으로, 해당 테이블이 파티션된 테이블이든 파티션되지 않은(non-parti tioned) 테이블이든 상관없이 만들 수 있다. 예를 들면 ‘EMP’ 테이블의 크기가 상당히 크고 파티션되지 않은 일반 테이블일 경우 다음과 같은 과정을 통해 파티션된 인덱스를 만들 수 있다. 이를 ‘Global Prefixed Partitioned Index’라고 부르는데, 파티션 인덱스와 마찬가지로 대용량 데이터 환경에서 성능을 높이고 관리를 편리하게 하기 위해서다.

<그림 1> 파티션된 인덱스와 파티션되지 않은 인덱스의 차이

CREATE INDEX EMP_IDX1 ON EMP (DEPTNO)GLOBALPARTITION BY RANGE (DEPTNO)    (PARTITION PAR_10 VALUES LESS THAN (‘20’) TABLESPACE TBS1,    PARTITION PAR_20 VALUES LESS THAN (‘30’) TABLESPACE TBS2,    PARTITION PAR_30 VALUES LESS THAN (‘40’) TABLESPACE TBS3,    PARTITION PAR_40 VALUES LESS THAN (‘50’) TABLESPACE TBS4,    PARTITION PAR_MAX VALUES LESS THAN (MAXVALUE) TABLESPACE TBS5)

파티션된 인덱스가 유용한 이유는, 앞서 파티션의 개념에서 설명한 것처럼 하나의 인덱스를 여러 개의 독립적인 물리 속성을 가진 세그먼트로 나누어 생성, 관리할 수 있기 때문이다. 오라클 DBMS에서 제공하는 인덱스는 글로벌/로컬 인덱스와 Prefixed/Non-Prefixed 인덱스로 분류된다.

파티션된 인덱스와 일반 인덱스 사이의 차이점은 파티션 테이블과 일반 테이블의 그것과 동일하다. 인덱스는 인덱스 컬럼과 Rowid 순으로 값이 정렬되는데, 이런 특성은 파티션 인덱스에서도 동일하다. 많은 개발자들이 파티션된 인덱스는 전체 테이블 값이 정렬되지 않는다고 생각하지만 이것은 사실과 다르다. 글로벌 파티션된 인덱스의 경우 테이블에 대해 값 정렬이 보장돼 있으며, 인덱스도 파티션별로 독립적으로 관리할 수 있다. 두 가지 방식의 차이는 <그림 1>과 같다.

파티션되지 않은 인덱스는 하나의 루트(root) 노드에서 리프(leaf) 노드까지 전체적인 밸런스를 유지하는 구조이고, 파티션 인덱스는 파티션 별로 독립적인 루트 노드와 리프 노드를 갖고 있음을 알 수 있다. 따라서 파티션되지 않으면 대용량 테이블에서는 글로벌 인덱스의 깊이(depth)가 매우 깊어질 수 있는 단점이 있다. 반면 파티션된 인덱스는 각 파티션별 깊이가 일반 인덱스의 깊이보다 얕고 인덱스도 파티션별로 할 수 있어 병렬 프로세싱을 이용한 인덱스 관리에 매우 효과적이다.

그렇다면 글로벌 인덱스와 로컬 인덱스는 어떤 차이가 있는 것일까? 많은 개발자들이 파티션됐는지 여부로 판단하지만 이것은 잘못된 생각이다. 앞서 설명한 것처럼 글로벌 인덱스도 파티셔닝할 수 있으며, 이를 파티션별로 관리할 수 있다. 글로벌 인덱스와 로컬 인덱스의 가장 큰 차이는 ‘정렬’이다. 글로벌 인덱스는 테이블 전체에 대해 인덱스된 컬럼과 Rowid 순으로 정렬되고, 로컬 인덱스는 해당 파티션 내에서만 인덱스된 컬럼과 Rowid 순으로 정렬된다.

또한 로컬 인덱스는 ‘Local’이라는 말에서 알 수 있듯이 지역적인 인덱스로, 해당 테이블(base table)의 파티션 키로 파티셔닝된 인덱스다. 일반적으로 로컬 인덱스의 구성 컬럼에 반드시 파티션 키가 포함돼야 가능한 것으로 알려져 있지만 로컬 인덱스에는 파티션 키가 포함되어 있지 않아도 사용할 수 있다.

다음 예제를 보자. PACKAGE_DLR_IDX1 인덱스의 구성 컬럼에 테이블 파티션 키인 I_DLR_IND가 포함되지 않아도 검색조건에 I_ DLR_IND = ‘C’라는 검색 조건이 있기 때문에 해당 파티션의 로컬 인덱스를 이용하는 것을 알 수 있다.

select*from PACKAGE_DLRwhere i_package = ‘AAA’  and i_dlr_ind = ‘C’Operation   Object Name  PStart PStopSELECT STATEMENT Hint=CHOOSE    TABLE ACCESS BY LOCAL INDEX ROWIDPACKAGE_DLR 3 3    INDEX RANGE SCAN  PACKAGE_DLR_IDX1 3 3

글로벌 인덱스는 전역적인 인덱스로, 기본적으로는 파티션되지 않은 인덱스이다. 대부분의 개발자들은 글로벌 인덱스를 파티셔닝해 사용할 생각을 하지 못하는데, 대용량 테이블에서 인덱스 관리의 효율성을 높이고 인덱스 검색 성능을 높이기 위해서는 이를 파티셔닝하는 것이 좋다.

글로벌 인덱스는 기본 테이블의 파티션 키와 무관하게 파티셔닝하는 것으로 설사 기본 테이블의 파티션 키로 글로벌 인덱스를 파티셔닝했다고 해도 로컬 인덱스처럼 동일파티셔닝(equipartitioning) 된 개념이 아니므로 테이블 DDL시 전체 인덱스를 다시 생성해야 한다.

그렇다면 글로벌 파티션 인덱스의 인덱스 컬럼 값은 어떻게 전체 테이블에 대해 정렬을 보장하는 것일까. 예를 들어 5000만 건의 파티션되지 않은 EMP 테이블을 부서번호에 따라 파티셔닝했다고 가정하면 다음과 같다.

CREATE INDEX EMP_IDX1 ON EMP (DEPTNO)GLOBALPARTITION BY RANGE (DEPTNO)(PARTITION PAR_10 VALUES LESS THAN (‘20’) TABLESPACE TBS1,PARTITION PAR_20 VALUES LESS THAN (‘30’) TABLESPACE TBS2,PARTITION PAR_30 VALUES LESS THAN (‘40’) TABLESPACE TBS3,PARTITION PAR_40 VALUES LESS THAN (‘50’) TABLESPACE TBS4,PARTITION PAR_MAX VALUES LESS THAN (‘MAXVALE’) TABLESPACE TBS2,

<그림 2>는 Global Prefixed Partitioned 인덱스의 구조다. Pre fixed와 Non-Prefixed는 인덱스 파티셔닝 키가 인덱스의 선두 컬럼으로 오는가 그렇지 않은가의 차이가 있다. <그림 2>에서도 ‘Pre fixed’란 인덱스의 파티션 키(DEPTNO)가 인덱스 선두 컬럼(DEPTNO)이 되는 것을 알 수 있다. 글로벌 인덱스의 경우 모든 인덱스 컬럼 값이 정렬돼 있다. 각 인덱스 파티션의 루트 블럭(root block)에 들어가는 값들이 인덱스 파티션에 따라 정렬되기 때문에 자연적으로 리프 블럭(leaf block)에 들어가는 모든 값들도 정렬되는 것이다. 반면 Global Non-Prefixed 인덱스를 파티셔닝하면 레인지 파티셔닝 방식으로만 가능하다. 이것은 정렬 때문인데, 레인지 파티션은 정렬 기능을 이용해 파티셔닝 키 자체를 생성하는데 반해 다른 파티셔닝 방식은 정렬과 상관없이 수행하기 때문이다.

로컬 인덱스는 Prefixed 인덱스와 Non-Prefixed 인덱스를 모두 지원한다. 로컬 인덱스는 기본적으로 현재 테이블의 파티션 키가 곧 인덱스 파티션 키가 되기 때문에 인덱스 컬럼에 현재 테이블의 파티션 키가 포함되지 않아도 인덱스를 생성할 수 있다. 또한 인덱스 컬럼 값의 정렬이 전체 테이블에 대해 보장된 것도 아니기 때문에 인덱스 파티션 키가 인덱스의 선두 컬럼이 될 필요가 없다. 또한 Non-Parti tioned 인덱스이든 파티션 인덱스든 상관없이 인덱스를 이용하고자 할 때는 무조건 인덱스 파티션 키를 조회해야 하는 글로벌 인덱스와 달리 로컬 인덱스는 조회 검색조건에 파티션 키가 들어올 수도 있고 들어오지 않을 수도 있다.

<그림 2> Global Prefixed Partitioned 인덱스

대용량 DB 테이블과 인덱스 전략

파티션 인덱스 전략은 파티션 테이블과 밀접하게 연관돼 수립해야 하지만 여기서는 파티션 인덱스를 위주로 이야기를 풀어본다. 먼저 인덱스 크기에 대한 논의는 기본적으로 테이블보다는 훨씬 작게 생성, 관리하는 것이 원칙이다. 따라서 중소 용량의 데이터베이스 환경에서는 파티션 인덱스의 유용성을 따질 필요가 없다. 단 중소 용량의 데이터 환경에서도 테이블이 파티셔닝돼 있다면 파티션 인덱스를 고려해야 한다. 또한 파티션되지 않은 인덱스(일반 인덱스) 전략을 기본으로 해 테이블이 파티셔닝된 경우와 인덱스를 파티셔닝했을 때의 장점을 비교해 봐야 한다.

먼저 테이블 파티션 키가 항상 ‘=’로 들어오는 경우 또는 파티션 범위가 크지 않은 경우에는 로컬 인덱스가 최상이다. 인덱스 컬럼의 순서와 구성은 액세스 패스에 따라 생성하면 되지만 최대한 가볍게 생성하는 것이 좋다. 기본 테이블의 파티션 키는 반드시 포함될 필요가 없으나, 테이블이 레인지 파티션이고 한 파티션 범위 안에서 파티션 키의 분포도가 좋을 경우 이를 포함하는 것을 고려해 볼만하다. 이렇게 하면 각 파티션당 인덱스가 파티션되지 않았을 때보다 가벼워지고 데이터 마이그레이션을 할 때도 테이블 파티션과 인덱스 파티션이 동일하므로 exchange, add, drop, split 등 파티션별 관리도 용이하다.

또한 빠른 응답 시간을 요구하는 환경에서 대용량 파티션 테이블의 조회 조건에 파티션 키가 들어오지 않을 가능성이 있다면 파티션 글로벌 인덱스를 고려해 볼만하다. 이렇게 하면 파티션되지 않은 글로벌 인덱스와 달리 레인지 파티션 별로 인덱스가 가벼워지는 장점이 있고, 레인지 파티션 별로 인덱스 split와 rebuild 명령을 독립적으로 수행할 수 있다. 컬럼 분포도에 따른 파티셔닝이나 민감한(critical)한 상수 레인지에 대해서는 파티션을 독립적으로 생성해 인덱스 크기를 줄임으로써 인덱스 검색 시간을 줄일 수 있는 이점도 있다.

exchange는 파티션된 테이블의 특정 파티션과 파티션되지 않은 일반 테이블 간의 구조를 서로 바꾸는 것으로, 대용량의 파티션된 테이블을 관리하는데 상당한 효과가 있다. <그림 2>와 같이 데이터가 없는 새로운 데이터 테이블과 데이터가 들어 있는 파티션 2를 exch ange하면 파티션 2에 해당하는 디렉토리 정보가 새로운 데이터로 바뀌고 새 테이블에 데이터가 들어간다. 이것은 실제 데이터가 이동하는 것이 아니라 데이터를 저장하는 테이블 정보만을 업데이트하는 것이다. 한 가지 주의할 점은 exchange하고자 하는 파티션과 테이블의 구조가 같아야 하고 속성들의 특성도 같아야 한다는 사실이다. exchange의 기본 문법은 다음과 같다.

<그림 3> 대용량 DB에서 exchange 작업

<그림 4> 대용량 DB에서 split 작업

Alter table Tb_PartitionExchange partition par_200306With table Tb_Exchange(Without validation Including indexes)

한편 파티션된 대용량 테이블에 split 함수를 실행하면 많은 시간이 걸린다. 이럴 때 exchange 기능을 이용하면 빠르고 안전하게 작업할 수 있다. <그림 4>에서 보는 것처럼 split를 해야 하는 파티션을 exchange에 의해 빈 공간으로 만든 다음 split을 하고 다시 데이터를 채우기 위해 split하는 것이다. 이렇게 하면 대용량의 데이터라도 매우 빠른 시간 내에 split 작업을 마칠 수 있다. 한편 대부분의 DBA들과 개발자들은 동일한 테이블을 생성할 때 create table ~ as select 구문을 이용한다. 대용량의 데이터일 경우 parallel 옵션을 주고 생성하기도 한다. 만약 1억 건의 테이블을 그대로 생성한다고 할 때 어떤 방법이 효과적일까. 이렇게 파티션된 대용량 테이블을 생성할 때는 exchange, program parallel 방법을 사용하는 것이 바람직하다.

<그림 5>는 이 과정을 도식화한 것이다. 먼저 생성할 TB_PART_1 테이블의 빈 껍데기를 만든다. 대용량의 파티션된 테이블의 파티션 각각을 create table ~ as select 구문의 parallel 옵션을 이용해 각 테이블로 생성한다. 미리 생성해 놓은 TB_PART_1 테이블의 파티션과 만들어 놓은 테이블들을 exchange하는 것이다. 이 때 파티션별로 200105.sql, 200106.sql, 200107.sql…… 형식으로 만들어 놓고 이 프로그램들을 동시에 실행하면(program parallel) 극적인 효과를 볼 수 있다.

이번엔 데이터 마이그레이션에 대해 살펴 보자. 원격으로 데이터를 옮겨야 할 때 보통 database link를 이용한다. 네트워크를 통해 데이터를 옮기면 직렬(serial)로 데이터가 이동되므로 속도가 현저하게 떨어지기 때문이다. 따라서 소스 테이블을 파티셔닝하고 해당 파티션을 액세스하는 프로그램을 각각 띄워 병렬 프로세싱을 하면 매우 빠른 속도로 데이터를 옮길 수 있다. 소스 테이블을 파티셔닝할 수 있는 상황이라면 테이블의 분포를 보고 레인지나 리스트 방식으로 파티셔닝할 수 있고, 일정한 분포가 존재하지 않는 테이블이라면 해시 파티셔닝으로 분포도를 고르게 나눈 다음 해당 파티션을 읽는 뷰를 액세스해 데이터를 옮기는 것이 좋다.

예를 들어 다음은 중대형 정도 크기인 약 2700만 건의 회원 테이블을 옮기는 DDL 스크립트다. 앞서 언급한 대로 database link를 이용해 처리하면 네트워크의 속도가 떨어져 엄청난 시간이 소요된다. 그러나 이것을 일반 테이블을 여러 개로 파티션을 나누어서 파티션과 병렬 처리하면 성능이 크게 향상된다. 작업 순서는 다음과 같다.

create table t_cust_hashstorage (initial 5M next 5M pctincrease 0)partition by hash(mem_no)    (    partition par_hash_1 TABLESPACE TS_DATA,    partition par_hash_2 TABLESPACE TS_DATA,    partition par_hash_3 TABLESPACE TS_DATA,    partition par_hash_4 TABLESPACE TS_DATA,    partition par_hash_6 TABLESPACE TS_DATA,    partition par_hash_7 TABLESPACE TS_DATA,    partition par_hash_8 TABLESPACE TS_DATA,    partition par_hash_9 TABLESPACE TS_DATA,    partition par_hash_10 TABLESPACE TS_DATA,    )nologgingasselect /*+ parallel(x 10) */ * from t_cust x이제 다음과 같이 소스 테이블 뷰를 생성한 후create or replace view t_cust_1as select * from t_cust_hash partition (par_hash_1);create or replace view t_cust_2as select * from t_cust_hash partition (par_hash_2);create or replace view t_cust_3as select * from t_cust_hash partition (par_hash_3)……다음과 같이 프로그램 패러럴(program parallel) 작업을 동시에 실행한다.T_cust_1.sqlcreate table t_cust_1storage (initial 5M next 5M pctincrease 0)nologgingtablespace njhasselect /*+ parallel(x 4) */ * from t_cust_1@remote x;T_cust_2.sqlcreate table t_cust_2storage (initial 5M next 5M pctincrease 0)nologgingtablespace njhasselect /*+ parallel(x 4) */ * from t_cust_2@remote x

이것은 단적인 예에 지나지 않는다. 활용할 수 있는 사례는 얼마든지 있다. 한편 인덱스는 전체 데이터에 대해 해당 컬럼의 값으로 정렬하기 때문에 대용량 테이블의 경우 create, rebuild 명령을 실행할 때 많은 시간이 필요하다. 이 때 파티션된 인덱스를 만들면 인덱스의 생성과 관리를 더 효과적으로 할 수 있다. 다음은 파티션된 인덱스를 Unusable로 생성한 사례다(로컬/글로벌 파티션된 인덱스).

먼저 파티션 인덱스를 ‘unusable’ 옵션을 이용해 생성한다. 실제 데이터를 정렬해 만드는 것이 아니라 일종의 껍데기를 만드는 과정이다. 이제 앞서 살펴본 병렬 처리를 이용해 여러 파티션을 동시에 rebuild를 하면 대용량 데이터라도 빠른 시간에 인덱스를 생성할 수 있다.

<그림 5> 동일 테이블을 만들 때

CREATE INDEX EMP_IDX1 ON EMP (DEPTNO)GLOBALPARTITION BY RANGE (DEPTNO)    (PARTITION PAR_10 VALUES LESS THAN (‘20’) TABLESPACE TBS1,    PARTITION PAR_20 VALUES LESS THAN (‘30’) TABLESPACE TBS2,    PARTITION PAR_30 VALUES LESS THAN (‘40’) TABLESPACE TBS3,    PARTITION PAR_40 VALUES LESS THAN (‘50’) TABLESPACE TBS4,    PARTITION PAR_MAX VALUES LESS THAN (MAXVALUE) TABLESPACE TBS5)UNUSABLE;이제 파티션별로 index1.sql, index2.sql 등을 독립적으로 병렬 실행한다.ALTER INDEX EMP_IDX1 REBUILD PARTITION PAR_10 PARALLEL 4;   ---‘ index1.sqlALTER INDEX EMP_IDX1 REBUILD PARTITION PAR_20 PARALLEL 4;   ---‘ index2.sqlALTER INDEX EMP_IDX1 REBUILD PARTITION PAR_30 PARALLEL 4;   ---‘ index3.sqlALTER INDEX EMP_IDX1 REBUILD PARTITION PAR_40 PARALLEL 4;   ---‘ index4.sqlALTER INDEX EMP_IDX1 REBUILD PARTITION PAR_MAX PARALLEL 4; ---‘ index5.sql

지금까지 테이블 파티셔닝에 대해 다뤄봤다. 자동화된 성능관리 툴로 커버할 수 없는 영역을 살펴보고 있으나 가장 중요한 것은 데이터베이스 액세스 개념에 대해 정확하게 이해하는 것이다. 많은 사람들이 파티셔닝을 알고 있지만 정확하게 사용하고 있지 못하는 현실이 안타까울 때가 많다. 그러나 이 점은 역설적으로 파티셔닝의 매력이기도 하다. 노력하는 데이터베이스 관리자 만이 도전해 볼 수 있는 영역이 바로 ‘파티셔닝’ 분야이기 때문이다.

제공 : DB포탈사이트 DBguide.net

'ORACLE' 카테고리의 다른 글

sqlplus 2  (0) 2006.08.30
sqlplus 정리  (0) 2006.08.30
개발자가 범하기 쉬운 튜닝 이슈 10가지  (0) 2006.08.09
오라클 데이타베이스 입문  (0) 2006.07.21
제6회 정보시스템감리사 시험공고.  (0) 2006.05.24
posted by 구름너머 2006. 8. 9. 16:37
[펌]개발자가 범하기 쉬운 튜닝 이슈 10가지 유형 분석 | 낙서장2006/08/09 14:22
http://blog.naver.com/k2sunny/70007105780

개발자가 범하기 쉬운 튜닝 이슈 10가지 유형 분석

박 성 우
dont4get@chol.com


현영씨스템즈와 LG-CNS를 거쳐 현재는 수로텍 연구소 팀장으로 일하고 있다. 태평양 판매 물류 개발, KT-ICIS 요금관리시스템 프로젝트 DBA, KTF-차세대 빌링 프로젝트 DBA, 통합 하수관거 유지관리 SW 개발 팀장으로 활동했다.


지난 2회에 걸쳐 기본을 튼튼히 하자는 의미에서 쉬운 예제를 중심으로 주로 플랜(PLAN)을 작성하고 해독하는 데 주력했다. 지난 강좌만 자기 것으로 만들었다면 적어도 튜닝 부문에서는 웬만한 평범한 DBA는 능가할 수 있다. 이번에는 지금까지 개발자들이 자신의 프로그램이 느리다고 찾아오는 가장 많은 유형을 10가지 정도로 요약해 설명하고자 한다. 필자가 10년 동안 DBA 업무를 하면서 이 10가지 유형을 제외하고 느린 경우는 10% 미만이었다. 10% 미만은 프로그램 구조 조정 및 프로세스의 조정을 통한 대대적인 튜닝 작업이 이뤄져야 한다. 이런 경우는 진짜 튜닝 전문가에게 맡겨라. 90%만 개발자들이 신경써서 자신의 프로그램을 튜닝한다면 프로젝트에서 엄청남 효과가 나타난다.


2회분 줄거리

지난 회를 간단하게 꼭 알아야 할 부분만 요약해 본다. 몇 번을 강조해도 지나침이 없는 부분이다.

1회
옵티마이저에는 룰 기반과 비용 기반의 옵티마이저가 있으며 플랜 작성법에는 다음의 네 가지가 있다.
1. SQLPLUS의 Autotrace 기능 활용
2. EXPLAIN 명령과 플랜 테이블 조회
3. tkprof 유틸리티 활용 방법 : 가장 자세히 볼 수 있는 방법
4. 튜닝 툴을 통해 조회하는 방법

2회
튜닝은 프로그램을 빠르게 하기 위한 튜닝 전문가의 노력이 아닌 제한된 자원으로 설정된 목표 시간 내에 정확한 결과를 지속적, 안정적으로 내기 위한 프로그래머의 모든 노력이다. 따라서 정확성, 신속성, 운영의 편이성, 장애 대처성, 자원 활용성, 가독성, 이식성의 모든 측면이 고려돼야 한다. 그리고 룰 기반 옵티마이저의 우선 순위에 따라 접근 경로를 플랜 작성을 위주로 추적하여 보았다.



1. 인덱스를 왜 안 타나요

튜닝에서 역시 가장 많은 문제를 일으키는 것은 인덱스의 잘못된 활용이다. 또한 개발자들 대부분 느리다고 생각되면 인덱스를 잘 사용하고 있는지를 검토한다. 인덱스를 사용한다고 무조건 빨라지는 것은 아니지만 인덱스의 올바른 활용은 SQL 튜닝의 첫 시작임은 분명하다. 특히 OLTP성 업무에서는 인덱스 활용이 중요하다. 하나씩 경우의 수를 나열해 보자.


인덱스가 존재하지 않는 경우(인덱스 구조를 볼 줄 모르는 사람을 위해)

우문현답이라고 할까? 인덱스가 존재하지 않으면 당연히 인덱스를 타지 않는다. 하지만 많은 개발자는 해당 테이블에 인덱스가 존재하는지, 어떤 컬럼으로 구성되어 있는지 알지 못한다. 심지어 OCP 자격증을 가진 개발자도 이런 경우가 있다. 지난 회에 간략하게 설명하였지만 USER_INDEXES, USER_IND_COLUMNS라는 딕셔너리에 해당 계정에서 만든 인덱스 및 인덱스 컬럼을 조회할 수 있다. 여기까지는 개발자들이 대부분 알 것이라 생각한다.


[화면 1]자신의 계정에 생성된 테이블의 인덱스를 조회하는 화면


하지만 대부분 프로젝트에서 개발자 계정에 테이블을 만들어 주는 경우는 없다. DBA 계정에 테이블을 만들고 시노님(synonym)이나 뷰(view)를 통해 개발자에게 권한을 주게 된다. SCOTT 말고 SYSTEM 계정으로 로긴한 다음 다음과 같이 실행해 보자.

[화면 2] 다른 계정에서 시노님으로 연결되는 예제 화면

이 다음 SELECT * FROM EMP;를 실행하면 이전과 결과는 같을 것이다. 하지만 SELECT * FROM USER_INDEXES WHERE TABLE_NAME = ‘EMP’하면 아무 인덱스도 없다고 나올 것이다. 물론 SELECT * FROM USER_TABLES WHERE TABLE_NAME = ‘EMP’해도 테이블은 없을 것이다. [화면 2]의 작업을 본인이 하지 않았을 경우에 어떤 계정의 테이블로 시노님이 생성되어 있는지 모르는 개발자가 의외로 많다.
일단 SELECT OBJECT_TYPE FROM ALL_OBJECTS WHERE OBJECT_NAME = ‘EMP’하여 EMP가 테이블인지 뷰인지 시노님인지를 확인한 후 이에 해당하는 테이블을 찾아가면 된다.
시노님일 경우에는 SELECT TABLE_OWNER, TABLE_NAME FROM USER_SYNONYMS WHERE SYNONYM_NAME = ‘EMP’라고 해서 해당 테이블의 오너와 테이블명을 찾으면 된다. 만약에 시노님이 PUBLIC으로 되어 있으면 ALL_SYNONYMS를 찾아 오너가 ‘PUBLIC’인 것을 찾으면 된다.

인덱스가 존재하는데도 인덱스를 활용 못하는 경우

  • 첫 번째 컬럼에 조건을 주지 않은 경우
    지난 호에도 설명했듯 첫 번째 컬럼에 = 또는 LILE, > 조건 등을 기술해야 한다. 10개 컬럼으로 구성된 결합 인덱스가 있어 9개 컬럼에 = 조건을 주더라도 첫 번째 컬럼 조건이 빠진다면 이 인덱스는 있으나마나 한 것이다. 인덱스란 B-트리 형태로 정렬된 순서를 가지고 있다. 이 순서를 가지고 액세스를 하여 해당 범위가 초과되면 검색을 중단한다. 그러므로 인덱스를 구성하는 컬럼 순서대로 조건이 많이 주어질수록 좋은 결과를 낸다. 중간에 하나라도 빠지게 되면 뒤 순서의 조건은 인덱스와는 무관하게 체크 조건이 될 뿐이다.
    인덱스를 통해 검색 조건을 줄여주는 컬럼을 ‘인덱스 활용 컬럼’이라 하고, 검색 조건을 줄여주지 못하고 데이터를 추출하는 체크 조건만 될 때 이를 ‘데이터 체크 컬럼’이라 정의한다([표 1]).


구성 컬럼조 건비 교
인덱스 컬럼
① 매장 코드
② 판매 일자
③ 제품 코드
첫 번째 컬럼 조건이 누락된 경우
WHERE 판매 일자 = ‘20030618''
AND 제품 코드 = ‘50001’
인덱스 활용 못함
판매 일자, 제품 코드는 단지 데이터 체크 컬럼
=> 전체 데이터 다 읽음
* 중간 컬럼 조건이 누락된 경우
WHERE 매장코드 = ‘R2001’
?AND 제품 코드 = ‘50001’
인덱스 활용 가능
=> 매장 코드 : 인덱스 활용 컬럼
=> 제품 코드 : 데이터 체크 컬럼
판매 일자의 조건이 누락됨에 따라 매장 코드 조건에 의해 모두 데이터가 읽어지면서 제품 코드는 데이터를 가지고 올 것인가를 체크하는 조건만 된다.
* 첫 번?? 컬럼 조건이 ‘=’이 아닌 경우
WHERE 매장 코드 = ‘R2%’
 ?AND 판매 일자 = ‘20030618’
AND 제품 코드 = ‘50001’
인덱스 활용 가능
=> 매장코드 : 인덱스 활용 컬럼
=> 판매일자 : 데이터 체크 컬럼
=> 제품코드 : 데이터 체크 컬럼
하지만 첫 번째 컬럼이 LIKE 조건이므로 판매 일자와 제품 코드는 단지 데이터를 가지고 오기 위한 체크 컬럼만 된다. 즉, 우선 순서를 가진 컬럼이 = 조건이 아닌 경우는 뒤 컬럼은 모두 데이터 체크 컬럼이다.

결합 인덱스를 사용할 때에는 인덱스 활용 컬럼이 데이터 범위를 충분히 좁혀질 때 유용한 것이다. [그림 1]과 같이 데이터가 인덱스 활용 컬럼에 의해 점점 좁혀지는 구조가 돼야 한다. 데이터 체크 컬럼은 데이터를 좁혀주는 것이 아니라 읽은 후 버려지는 경우가 많기 때문이다.

[그림 1]
                  

인덱스를 활용하지 못하도록 SQL을 작성한 경우

SQL문의 WHERE 조건절 기술시 해당하는 인덱스가 있어도 힌트를 강제적으로 사용하지 않으면 인덱스를 활용 못하는 경우는 [표 2]와 같다.



유형예제 및 해결 방법
인덱스 컬럼 절을 변형한 경우수식, 함수 등으로 인덱스 컬럼 절을 변형했을 경우이다.
반드시 함수나 수식을 사용하는 경우에는 인덱스 컬럼 부분에 적용하지 말고 여기에 대입되는 컬럼 또는 상수 부분에 적용해야 한다.
WHERE TO_CHAR(등록일, ’YYYYMMDD) = ‘20030618’
=> WHERE 등록일 = TO_DATE(‘20030618’, ’YYYYMMDD’)
WHERE SAL * 30 > 30000
=> WHERE SAL > 1000
내부적으로 데이터 형 변환이 일어난 경우서로 대입되는 항목끼리 데이터 타입이 다르면 내부적인 형 변환에 의해 컬럼이 함수를 사용한 효과를 나타낼 수 있다.
WHERE 등록일 = ‘20030618’
등록일이 DATE 타입인데 문자형을 직접 대입하게 되면 WHERE TO_CHAR(등록일, ’YYYYMMDD’) = ‘20030618’로 기술된 것과 똑 같은 효과, 즉 인덱스 컬럼을 변형한 것과 같다. 따라서 상수 절을 변형시켜 주어야 한다.
=> WHERE 등록일 = TO_DATE(‘20030618’, ’YYYYMMDD’)
조건절에 NULL 또는 NOT NULL을 사용한 경우WHERE 연체금액 IS NULL
기본적으로 인덱스를 구성한 컬럼 값이 전부 NULL이라면 인덱스는 이런 값을 저장하지 않는다. 따라서 NULL인 값이 많지 않아 인덱스를 통해 액세스를 하고자 한다면 데이터 생성시 디폴트로 0과 같이 데이터를 만들어 주는 것이 낫다. 반대로, 만약 연체금액이 NULL인 사람이 많고 연체금액이 없는 사람은 별로 분석 대상이 아니고 연체금액이 NOT NULL인 사람이 분석 대상이라면 연체금액 컬럼을 NULL 허용 컬럼으로 두는 것이 좋다.
WHERE 연체금액 IS NOT NULL
=> WHERE 연체금액 > 0
앞서 말한 것처럼 인덱스에는 연체금액 NULL인 사람은 존재하지 않기 때문에, 연체금액 있는 사람만을 추출하고자 할 때에는 해당 인덱스를 활용하는 것이 훨씬 유리하다.
부정형으로 조건을 사용한 경우WHERE 연체코드 != ‘부분납’
부정문은 인덱스를 활용하지 못한다.
WHERE 연체코드 < ‘부분납’ OR 연체코드 > ‘부분납’
또는 테이블을 한번 더 읽어 NOT EXISTS를 사용하라
WHERE NOT EXISTS
(SELECT ‘X’ FROM 체납 WHERE 연체코드=’부분납’)
LIKE 연산자를 맨 앞에 사용하는 경우WHERE 주소 LIKE ‘%신림’
이 경우에는 인덱스를 사용할 수 없다. 이런 이유로 컬럼을 시도, 군구, 동읍으로 주소 컬럼을 나누어 생성하는 것이 좋다.

[표 2] 인덱스 활용을 못하는 유형



역으로 인덱스를 사용하는 것을 방해하는 원리를 이용하여 인덱스가 불리하다고 생각되는 경우 일부러 앞의 예에 기술된 것처럼 써 주는 기법도 많이 사용된다. 하지만 필자 의견으로는 적절하게 힌트를 구사하여 제어를 하는 것이 가독성 측면에서 더 나은 방법이라 생각한다.


옵티마이저의 선택(힌트 이용)

룰 기반 옵티마이저가 아닌 경우 인덱스를 활용하고자 하나 옵티마이저가 판단하여 자신이 생각하는 인덱스를 활용하지 않고 풀(FULL) 스캔이나 다른 인덱스를 사용하는 경우가 있다. 이는 옵티마이저가 자체적으로 판단하여 SQL 실행계획을 작성하기 때문이다. 하지만 아직 최적화된 SQL 실행 계획을 제시해 주지 못하고 있다. 따라서 이런 경우 힌트를 사용한다면 자신이 생각하는 인덱스를 사용하라고 지시를 내릴 수 있다.

힌트 사용 문법은 /*+ INDEX(테이블명 인덱스명) */이다. 힌트는 /* */이라는 주석 문구에 해당하므로 다르더라도 컴파일이나 실행시 오류는 내지 않는다. 따라서 문제가 있어 수정했는데 잘 못 사용하여 힌트 문장이 제대로 수행되지 않을 수 있으니 반드시 플랜을 작성해 제대로 활용되었는지를 확인해야 한다.

2. 인덱스를 타는데 왜 느리죠
(데이터 분포도와 SQL문 호출 횟수를 고려)

데이터 분포도가 낮은 경우

과도한 소트를 방지하기 위해 인덱스를 활용하는 경우도 있지만 인덱스 활용의 주 목적은 수많은 데이터 중 자신이 읽고자 하는 부분이 적은 경우 해당 부분만을 읽고자 할 때 사용되는 것이다. 따라서 인덱스로 지정한 컬럼에 주어진 조건이 전체 데이터의 10% 미만이지 않을 때에는 되도록 풀 스캔을 하는 것이 일반 룰로 되어 있다. 이는 인덱스와 데이터 파일을 읽어야 하기 때문에 2배의 노력이 드는 것이고 멀티 블럭 Read를 하지 않기 때문에 5개 이상의 블럭을 한 번에 읽어오는 풀 스캔에 비해 5배, 즉 최소 10배 이상의 노력이 수반된다고 보기 때문이다. 따라서 조건이 인덱스로 읽혀진다면 반드시 데이터 분포도를 생각해야 한다.
예를 들어 전국 국민을 조회하는데 이중 서울에 사는 사람만 조회를 한다면 서울에 사는 인구 분포는 반 정도이다. 이를 인덱스를 활용한다면 분명 효과적인 인덱스 활용이 아니다.

SQL문이 과도하게 호출되는 경우

만약 대용량 데이터를 활용하는 사람은 반드시 숙독하기 바란다. 전형적인 배치 프로그램을 예로 든다면 [리스트 1]과 같이 프로그램이 수행된다.


구 조예 문

DECLARE MAIN-CURSOR;
DECLARE SUB-CURSOR;
OPEN MAIN-CURSOR ;
LOOP
FETCH MAIN-CURSR;
    OPEN SUB-CURSOR ;
FETCH SUB-CURSOR;
CLOSE SUB-CURSOR;
END-LOOP;
CLOSE MAIN-CURSOR;

DECLARE MAIN_CURSOR
SELECT 고객, 고객명, 우편번호, 나머지 주소
FROM 고객;
DECLARE SUB_CURSOR
SELECT 우편번호주소 FROM 우편번호
WHERE 우편번호 = :변수;
OPEN MAIN-CURSOR
LOOP
  FETCH MAIN-CURSOR;
  :변수 = 고객.우편번호;
SELECT 우편번호 주소 FROM 우편번호
WHERE 우편번호 = :변수;
END-LOOP
CLOSE MAIN-CURSOR;

요즈음은 SQL문으로 한 번에 데이터를 가지고 오는 걸 프로그래머는 선호하기 때문에 [리스트 1]처럼 프로그램을 작성하는 예는 드물다. 아마 MAIN CURSOR를 다음과 같이 구성할 것이다.

SELECT A.고객번호, A.고객명, A.우편번호, B.우편번호 주소, A.나머지 주소
FROM 고객 A, 우편번호 B
WHERE A.우편번호 = B.우편번호;

프로그램의 편이성 때문에 이렇게 하는 사람들이 많지만, 왜 SQL문을 합쳐야 하는지를 물어보면 원리를 이해하는 사람은 많지 않다. [리스트 1]의 경우 LOOP문 안에 정말 간단한 SQL문이 인덱스를 활용하여 제대로 읽혀진다 해도 고객 수가 몇 천만 건인 경우 어떠한가? 이 SQL문은 여지없이 몇 천만번 DB 엔진에게 자료를 요청한다. SQL문이 던져지면 [그림 2]와 같이 구문해석 -> 실행계획 -> 바인드 -> 실행 -> 인출이라는 과정을 거치게 된다.


[그림 2] SQL문 해석 과정


반복 수행됨은 물론 공유 풀이라는 곳에 이미 실행된 SQL문이 존재하여 실행계획을 다시 생성하지 않더라도 계속적인 수행은 데이터베이스의 상당한 부담이 된다. 따라서 LOOP문 안에 호출되는 SQL문이 단 한 건을 요청하더라도 반드시 MAIN CURSOR에 병합하는 연습을 해야 한다. 지금은 두 개의 테이블로 간단하겠지만 여러 테이블이 되고 도중에 업무가 바뀌어 프로그램을 수정하면서 여기저기 SQL문을 추가하기 때문에 이런 형태의 프로그램이 많이 만들어지게 된다.

하지만 이 SQL문도 맹점은 있다. 왜냐하면 우편번호 인덱스와 데이터 파일을 계속적으로 몇 천만번 데이터를 액세스하기 때문이다. 이는 [리스트 1]에서 나타난 프로그램의 구조와 별 차이가 없게 된다. 따라서 이를 개선하기 위한 방법이 해시 조인이다.

해시 조인은 중첩 루프(NESTED LOOP) 방식 조인의 단점을 획기적으로 개선해준다. 통상적으로 작은 테이블을 기준으로 해시 테이블을 만들어 정렬한 후 해시 함수를 이용하여 조인하는 방식이다. 해시 조인이 내부적으로 어떻게 일어나는지는 이해하지 못하더라도, 과도한 중첩 루프 방식을 피하고자 할 때 특히 건수가 큰 테이블을 추출하여 건수가 작은 테이블과 조인시에는 훨씬 더 좋은 성능을 낸다. 단 해시 조인은 ‘=’ 조건일 때 가능하다.

SELECT /*+ USE_HASH(B A) FULL(B) FULL(A) */
A.고객번호, A.고객명, A.우편번호, B.우편번호 주소, A.나머지 주소
FROM 고객 A, 우편번호 B
WHERE A.우편번호 = B.우편번호;

또 한 가지 방법으로는 데이터베이스를 사용하지 말고 메모리에 사용정보를 올려서 이를 참조하는 방식이다. 조인되는 테이블이 많아 해시 조인으로도 목표 시간을 획득하기 힘들다면 자주 변하지 않는 정보에 한해 데이터베이스상의 정보를 프로세스 시작시에 메모리로 로드한 뒤 메모리를 참조하라는 뜻이다. 앞의 예와 같이 우편번호 테이블은 거의 변동이 없는 정보이다. 이 정보를 메모리에 올려 사용하더라도 데이터 무결성이 깨질 위험은 적다. 상품 정보, 요율 정보, 각종 코드 정보 등이 이에 해당한다.
하지만 이 방법은 무결성을 깨트릴 위험이 어쨌든 존재하므로 반드시 다른 업무팀과 협의하여 결정해야 하며, 대상 프로세스가 수행시에는 관련 정보를 수정하는 일이 없도록 해야 한다.
마지막 방법으로는 LOOP문 안에 SQL문 실행 횟수를 줄이는 좋은 아이디어를 짜내어야 한다. 예를 들어 지역별로 서버가 나눠진 경우라면 우편번호가 같은 고객이 연달아 읽혀질 가능성도 있다. 그렇다면 이전 고객에서 읽혀진 고객 우편번호와 지금 읽혀진 고객 우편번호가 같다면 굳이 우편번호 테이블을 또 읽으러 갈 이유가 없다. 이전 데이터를 메모리에 저장했다가 이 값을 적용하면 된다. 이는 예상 밖의 소득을 올릴 수 있다.

3. 몇천 만건은 속도가 빨리 나오는데 왜 겨우 100건 조회 시에는 느린가요

(전체 범위 처리와 부분 범위 처리의 고려)
“SQLPLUS에서 몇천 만 건 이상인 테이블을 조회하면 금방 결과가 화면에 나오는데 몇건 되지 않는 데이터는 왜 오래 걸리나요?”라고 질문을 하는 개발자가 많이 있다.

SELECT * FROM 통화내역 WHERE 발신지역 = ‘서울’;
SELECT 년월, SUM(금액) FROM 체납정보 GROUP BY 년월;

이것이 일반적으로 튜닝 책에서 말하는 부분 범위와 전체 범위를 뜻하는 것이다. 첫 번째 SQL문에서 통화내역은 몇억 건이 되지만 DB에서 읽은 결과를 그대로 화면에 나타내면 되기 때문에 일정 버퍼에 차면 출력한다. 두 번째 SQL문에서는 DB에서 읽은 결과를 그대로 화면에 출력하는 것이 아니라 GROUP BY와 SUM 작업 후 그 결과를 출력한다.

따라서 첫 번째 SQL문은 엔터 키를 치면 주르륵 화면에 디스플레이되고 두 번째 문장은 일정시간을 기다린 후 출력되는 것이다. 이는 튜닝에서 중요한 의미를 가진다. 특히 OLTP성 업무(온라인, 즉 화면 처리)에서 더욱 의미가 크다. 첫 번째 SQL문은 빨리 빨리 결과 값이 차례로 리턴되지만 완전히 전 데이터를 화면에 출력한다면 두 번째 SQL문보다 느리다. 첫 번째 같은 처리를 부분 범위 처리라 하고 두 번째 같은 처리를 전체 범위 처리라 부른다.

OLTP 업무는 한정된 화면, 많아야 만건 정도를 사용자가 온라인에서 처리를 하게 된다. 따라서 얼마나 빨리 사용자에게 결과를 처음 내보내 주어야 하는가가 튜닝 포인트다. 따라서 건수가 많은 경우에는 ‘NEXT’ 버튼을 이용하여 다음 결과부터 100건 정도씩 보여주면 된다. 우리나라 사용자는 무척 속도에 민감하다. 5초 10초가 넘어가면 엄청 짜증을 낸다. 30초 지나서 화면에 디스플레이된다면 사용자는 당장 개발자에게 시정을 명할 것이다. 많은 출력 데이터를 가지고 있더라도 화면 단위로 빠르게 처리를 해 나가야 한다.

반대로 배치성 업무는 주로 전산 작업자가 일정한 작업 시나리오로 작업을 한다. 화면에 나타나는 중간 값보다는 최종 작업 결과가 중요하다. 화면에 빨리 디스플레이된다고 빨리 끝나는 것은 절대 아니다. 이런 이유로 OLTP에서는 주로 중첩 루프 방식을 선호하고 배치 업무에서는 해시 조인 방식을 선호하게 된다. 해시 조인은 해시 테이블을 생성하기 때문에 처음 리턴하는 시간이 지연된다. 하지만 중첩 루프 방식은 해당 조건에 맞는 데이터를 차례차례 검색하기 때문에 일정 버퍼에 채우기까지 얼마의 시간이 소요되지 않는다. 예를 들어 고객 테이블과 사용요금 테이블이 있다면 온라인 업무에서는 다음과 같이 SQL문이 구사될 것이다.

SELECT /*+ USE_NL(A B) */ A.고객명, B.사용요금 FROM 고객 A, 요금 B
WHERE A.고객ID = B.고객 ID

이런 경우 B 테이블에 고객 ID라는 인덱스가 구성되어 고객 ID를 차례로 조인해 가면서 읽게 되며 일정 화면 버퍼에만 차면 결과를 리턴하는 것이다. 만약에 배치 프로그램이라면 앞서 말했듯이 중첩 루프 방식이 내부적인 DB작업이 부담이 더 많기 때문에 해시 조인으로 바꾸어 실행할 것이다.

SELECT /*+ USE_HASH(A B) */ A.고객명, B.사용요금 FROM 고객 A, 요금 B
WHERE A.고객ID = B.고객ID

이렇다면 고객 테이블을 해시 테이블로 만드는 동안은 화면에 아무것도 출력되지 않는다. 하지만 최종 결과는 해시 조인 방식이 훨씬 빠르다. 따라서 온라인 프로그램 튜닝시에는 화면 버퍼에 나타낼 부분을 어떻게 빠르게 할 것인가를 고민하여 되도록 부분 범위로 만들어 주는 연습을 많이 해야 하며, 배치 프로그램인 경우는 전체 완료시간 단축을 목표로 튜닝해야 한다. GROUP BY, ORDER BY로 되어 있는 문장은 그에 맞는 인덱스를 생성하여 별도의 정렬 작업으로 인해 전체 범위로 실행되는 것을 막아 줄 수 있다.

온라인 화면 설계시에도 PC 사양이 많이 좋아진 탓에 몇만 라인이 넘도록 화면 버퍼를 잡는 사람들도 많다. 이는 1000건 단위로 화면을 만들었을 때보다 속도가 떨어질 수밖에 없다. 필자는 어떤 경우 온라인 프로그램이 마치 출력 프로그램을 보는 듯한 느낌을 받을 때가 많다. 예를 들어 한 달 동안의 체납자를 조회하는 화면이 있었다. 한 달에 200만 건 정도의 체납자가 있는데 화면 조건에는 년 월 조건만 있었으며 한 화면에서 1만 건 정도씩 보여 주었던 것으로 생각난다. 그럼 이 사용자는 200번을 ‘NEXT’ 버튼을 보며 전 체납자를 조회하는 것인가?

이런 화면은 리포트로 출력하여 보관의 의미로만 하며 온라인 프로그램으로서의 존재 의미가 별로 없다. 특정 체납자를 찾기 위해 이 사람은 최대 200번의 ‘NEXT’ 버튼을 눌러야 한다. 이는 이 사람에게 필요 없는 정보를 제공하면서 귀중한 데이터베이스 자원을 낭비하고 있는 것이다. 주민등록번호를 입력하면 특정 체납자를 찾도록 해 주든지, 체납 금액이 얼마 이상인 고객을 찾는다든지 이런 주요 조건을 가지고 찾을 수 있어야 한다.

그렇다면 “이런 주민등록번호라든지, 체납금액, 체납 횟수 등에 대해 조회를 하기 위해 모든 컬럼에 인덱스를 만들어야 하는가?”라는 질문이 나올 수 있다. 원칙적으로 이 프로그램만을 위해서는 ‘YES’가 정답이다. 하지만 인덱스 하나를 만들면 이 프로그램의 속도를 개선할지 몰라도 이 체납 테이블을 생성하는 프로세스는 그 이상 느려진다.


이 문제가 튜닝시 자주 부딪히게 된다. 조회를 빠르게 하자니 생성 프로세스가 느려지고 생성 프로세스를 빠르게 하자니 인덱스를 포기하게 되고. 아마 튜닝 담당자들이 가장 애를 먹는 부분이 아닐까 싶다. 하지만 왕도는 없다. ‘둘 중 하나는 포기하라’가 답이다. 포기의 조건은 업무의 중요성과 사용 빈도가 기준이 된다. 하나를 포기한 다음 최소의 피해가 가는 방향을 선택해야 한다. 매우 어려운 선택이며 오랜 경험이 필요한 부분이다.

4. 데이터 건수가 많아서 도저히 속도 개선이 힘들다?

진짜 무지막지하게 큰 데이터를 가진 테이블끼리의 조인은 풀 스캔에 해시 조인 함수를 쓰더라도 힘든 경우가 많다. 이런 경우에는 패러럴(PARALLEL) 옵션을 힌트 절에 추가해 속도를 개선하는 것이 가장 손쉬운 튜닝 법이다. 이래도 안 된다면 프로세스를 잘게 나누어 병렬 처리를 해야 하는데 이는 프로그램을 수정해야 하므로 부담이 많이 되는 작업이다. 일단 패러럴 옵션을 사용하기 위해서는 힌트에 다음과 같이 써주면 된다.

/*+ PARALLEL(테이블 명 패러럴 서버 개수) */

예를 들어 TAB1, TAB2, TAB3라는 테이블을 서로 조인하여 PARALLEL OPTION을 주게 되면 다음과 같다.

SELECT /*+ PARALLEL(A 5) PARALLEL(B 5) PARALLEL(C 5) */ * FROM TAB1 A, TAB2 B, TAB3 C
WHERE A.COL1 = B.COL1 AND
B.COL1 = C.COL1;

힌트 절에 되도록 패러럴 옵션 수를 같이 맞추어 주면 성능을 향상시킬 수 있다. 여러분이 패러럴 옵션을 사용하는 경우 플랜 결과를 보면 PARALLEL-TO-PARALLEL, SERIAL-TO-PARALLEL, PARALLEL-TO-PARALLEL이라는 것을 발견한다. PARALLEL-TO-PARALLEL이라는 것이 나타날 때 가장 좋은 성능을 발휘한다. 우선순위에서 읽은 결과를 다음 병렬 프로세서로 계속적으로 넘기기 때문에 좋은 결과를 넘기는 것이다. 만약 두 개 이상의 테이블을 조인시에 최적의 성능을 내기 위해서는 되도록 같은 수를 지정해 주어라.

그러나 패러럴 옵션은 과도한 CPU 부하를 가지고 올 수 있다. CPU 개수를 초과하는 패러럴 옵션은 별로 도움이 안 된다. 그러므로 프로세스를 빠르게 수행하고자 하는 욕심에 병렬 프로세서를 너무 많이 띄운다면 다른 프로세서에 방해가 되며, 자신의 프로세서에도 결코 도움이 되지 않는다.

그리고 DDL 문장에 패러럴 옵션을 사용한다면 해당 테이블 스페이스의 디폴트 INITIAL_EXTENT, NEXT_EXTENT 의 스토리지 절에 유의해야 한다. 병렬 프로세서가 초기에 이 스토리지 절의 옵션을 그대로 적용하기 때문이다.

5. DB가 문제인가요? 내 SQL문이 문제인가요?

개발자들은 프로세스가 수행이 느려지면 먼저 서버나 데이터베이스의 성능을 의심하게 된다. 그러나 서버가 계획 하에 도입되었다면 그럴 가능성은 희박하다. 가장 확실한 방법은 오라클의 성능 뷰를 조회를 통해 알 수 있지만 다음 회에 다루기로 하고 SQL이 문제인지 데이터베이스내의 문제인지를 쉽게 판별해 보기로 하자. 간단하다.

데이터베이스에 문제가 발생한 것이라면 SQLPLUS가 제대로 접속되지 않거나 엄청 느리게 접속된다. 또한 ps ?ef|grep 프로그램명을 치면 숫자가 두 개 나오는 데 서버에서 SQL*NET을 통하지 않은 경우 뒷 숫자가 해당 프로그램과 연결된 오라클 서버 프로세스 번호이다.

Ps ?ef|grep 프로세스 ID를 하면 데이터베이스와 연관된 작업을 하고 있을 경우 분명 CPU 점유율 또는 사용량이 변하기 마련이다. 이 숫자가 변하고 있는데 느리다면 분명 자신의 SQL문이 뭔가 문제가 있다고 보고 플랜을 작성해 분석해 봐야 한다.

현재 프로그램을 수행 중이어서 어떤 SQL문이 수행되는지 모른다면 다음의 SQL문을 실행해 보기 바란다. 먼저 v$session이라는 성능 뷰에서 자신의 프로세스에 해당하는 SID를 찾은 다음(권한이 없다거나 잘 모른다면 다음에 자세히 설명할 테이니 DBA의 도움을 받을 것) 해당 SQL을 실행하고 SQL문을 수행해 보아라. 자신이 마지막으로 수행한 SQL문이 보일 것이다. 이를 반복하면 수행이 오래 걸리는 SQL문이 쉽게 보일 것이다. 튜닝 툴을 이용하면 더욱 쉽게 알 수 있다.

col piece for 999
col sql_text for a85
select a.piece,b.osuser,b.process,a.sql_text
from v$sqltext_with_newlines a, v$session b
where a.address=b.sql_address and b.sid=&SID
order by 1 asc
/

만약 변하고 있지 않다면 LOCK을 의심해 보고 다음의 SQL문을 실행시켜 보길 바란다.

[리스트 2] 제목
column username format a10
column sid format 999
column lock_type format a25
column MODE_HELD format a11
column MODE_REQUESTED format a10
column LOCK_ID1 format a8
column LOCK_ID2 format a8

select
a.sid,
decode(a.type,
''MR'', ''Media Recovery'',
''RT'', ''Redo Thread'',
''UN'', ''User Name'',
''TX'', ''Transaction'',
''TM'', ''DML'',
''UL'', ''PL/SQL User Lock'',
''DX'', ''Distributed Xaction'',
''CF'', ''Control File'',
''IS'', ''Instance State'',
''FS'', ''File Set'',
''IR'', ''Instance Recovery'',
''ST'', ''Disk Space Transaction'',
''IR'', ''Instance Recovery'',
''ST'', ''Disk Space Transaction'',
''TS'', ''Temp Segment'',
''IV'', ''Library Cache Invalidation'',
''LS'', ''Log Start or Switch'',
''RW'', ''Row Wait'',
''SQ'', ''Sequence Number'',
''TE'', ''Extend Table'',
''TT'', ''Temp Table'',
a.type) lock_type,
decode(a.lmode,
0, ''None'', /* Mon Lock equivalent */
1, ''Null'', /* N */
2, ''Row-S (SS)'', /* L */
3, ''Row-X (SX)'', /* R */
4, ''Share'', /* S */
5, ''S/Row-X (SSX)'', /* C */
6, ''Exclusive'', /* X */
to_char(a.lmode)) mode_held,
decode(a.request,
0, ''None'', /* Mon Lock equivalent */
1, ''Null'', /* N */
2, ''Row-S (SS)'', /* L */
3, ''Row-X (SX)'', /* R */
4, ''Share'', /* S */
5, ''S/Row-X (SSX)'', /* C */
6, ''Exclusive'', /* X */
to_char(a.request)) mode_requested,
to_char(a.id1) lock_id1, to_char(a.id2) lock_id2
from v$lock a
where a.type not in (''MR'', ''DM'', ''RT'')
and (id1,id2) in
(select b.id1, b.id2 from v$lock b where b.id1=a.id1 and
b.id2=a.id2 and b.request>0)
order by 5,6
/

이 SQL문을 수행한다면 다른 프로세스의 영향으로 LOCK이 걸린 것을 발견할 수 있다. 만약 데이터베이스에 문제가 있다면 [리스트 2]의 두 SQL문은 조회조차 되지 않는다. 이제 느리다고 다른 사람의 프로세스 탓이라든지, 데이터베이스가 느리다든지 하는 오류는 범하지 않길 바란다.

6. 온라인 프로그램에서 목표 시간내 프로그램이 수행되지 않아요

정신없이 설명하다 보니 잠시 머리를 식히라는 의미에서 적었다. 온라인 프로그램인 경우 조회 조건이 까다롭고 건수가 많은 테이블은 속도를 줄이는데 한계가 있다. 심지어 일부 프로젝트에서는 ‘온라인 프로그램 수행 목표시간을 3초 이내’라고 절대적인 수치를 정해 놓고 무조건 시간 초과 시에는 불합격을 선언하기도 한다. 하지만 필자는 이런 경우도 무난히 통과했다.

대개 튜닝을 해도 안 되는 조회 화면은 대개 건수가 많은 테이블을 다양한 검색 조건을 통해 분석용 프로그램이 많다. 이는 특정 사용자가 일 몇회 미만으로 사용하는 것이 대부분이다. 이를 억지로 튜닝하느라 다른 프로세스를 추가하거나 인덱스를 추가한다면 이는 득보다 실이 많다.

온라인 프로그램은 사용자와의 대화이다. 이들이 갑갑하게 여기는 것은 엔터를 친 다음 PC가 다운된 것처럼 아무런 메시지 없이 마우스의 모래시계만 돌다가 수십 초 경과 후 조회되는 것이다. 그리고 건수가 많은 테이블을 분석용으로 사용하는 것은 월 1~2회의 한정된 사용자만이 사용한다. 이런 프로그램은 조회 버튼을 누르자마자 ‘잠시만 기다리세요’라는 메시지 박스를 띄워라. 한 메시지 박스를 띄우는 것만으로는 부족하다. 왜냐하면 또 화면이 가만히 있을 테니까. ‘데이터를 처리 중입니다’ 계속 3초 간격으로 번갈아 메시지를 보여주라. 남은 시간을 보여주거나 남은 처리 건수를 보여준다면 더욱 효과적이다. 물론 증권회사의 주문 등록 같은 온라인 프로그램을 이렇게 해서는 안 된다. 하지만 입찰 안내서나 요구사항이 명시되어 절대적인 기준을 삼거나 사용자가 프로그램이 느리다고 불평한다면 이렇게 해보라. 신기하게도 사용자는 아무 말 안한다.

7. 패러럴 옵션을 주었는데 속도가 나지 않아요

이론상으로는 알고 있었지만 언뜻 상황이 되면 떠오르지 않는 때가 있다. 데이터 이행을 하였을 때 경험한 것이다. 백업 서버에서 몇일 간을 테스트하면서 하루 이상 걸리던 프로세스를 2시간 이내로 줄이는데 성공을 했다. 실행서버에서 새로 유저를 만들어 프로젝트 오픈 몇 일전 최종 리허설을 하게 되었다. 그런데 이게 웬일인가. 2시간이면 끝났던 프로세스가 5시간을 넘어도 끝나지 않는 것이었다. 모니터링 해보니 패러럴 옵션이 먹히지 않은 것이었다. “실행 서버는 CPU도 백업 서버보다 많은데…” 문제는 MAX_PARALLEL_SERVER 개수가 백업 서버보다 작게 잡혀져 있었다. 공교롭게도 실행 서버에서 다른 프로세서는 다 중단시켰는데 데이터 백업 프로세서가 수행되면서 10개 정도를 사용하고 있었던 것이다. 패러럴 옵션은 앞에서 말한대로 CPU 개수를 초과하면 그리 효력이 없다. 또한 총 수행되는 패러럴 서버 개수가 MAX_PARALLEL_SERVER 개수를 초과한다면 병렬 서버는 수행되지 않는다. 만약 이것이 실제 이행시 일어났다면 분명 필자는 해고되었을 것이다. 문제는 예기치 않은 곳에서 특히 자신하고 체크하지 않는 부분에서 일어난다는 사실을 기억하기 바란다.

8. 예전에는 빨리 수행되었는데 지금은 속도가 안나요

인덱스의 추가나 변경

프로그램이 작성된 이후 신규로 추가되거나 컬럼이 변경된 인덱스가 영향을 미치는 것이다. 예를 들어 WHERE 고객ID LIKE ‘200310% AND 고객유형 = ‘기업’이란 조건 절이 있다면 이 SQL문이 작성되기 전에 없었던 고객 유형이 첫 컬럼으로 시작하는 인덱스가 신규로 추가되었다고 가정하자. 이런 경우 ‘=’ 조건이 우선할 수 있으므로 고객 ID를 컬럼으로 한 인덱스를 사용하다가 고객 유형으로 시작하는 인덱스를 사용하도록 바뀔 수 있다.

그러므로 전체 프로젝트의 관점에서 볼 때 자기 혼자서 잘한다고 되는 것이 아니다.
다른 팀과 공유해서 사용하는 테이블에 대해서는 스키마 변동 내역을 반드시 공유해야 한다.

데이터 건수 증가, 분포도 변경

이는 개발환경에서는 몇 건 안 되는 시험용 데이터를 사용하며, 프로젝트 초기에는 데이터가 많이 누적되지 않아 시간이 지나면 지날수록 속도가 점점 저하되어 나중엔 하드웨어 증설이라든지 대대적인 애플리케이션 튜닝을 하는 경우가 많다. 이는 정말로 심각한 문제다.

이는 각각의 테이블에 대해 디스크 보관 주기라는 개념이 없이 계속 데이터를 누적해 나가는 현실과도 무관하지 않다. 업무 설계시 시간 개념과 마감 개념이 도입되지 않는다면 이 문제는 쉽사리 해결되지 않는다. 즉, 장기간 보관해서 사용할 데이터를 따로 어떻게 관리할 것인지 전략을 수립해야 한다. 통계 테이블을 만들고 일정 기간이 지난 상세 테이블은 따로 백업 테이블로 옮긴다든지 하여 현재 업무를 진행하고 있는 데이터가 과거 데이터와 같이 섞여 있어 현재 업무가 지연되는 것을 막아야 한다.

예를 들어 매출 테이블을 들자면 최근 3년치만 보관하고 3년이 지난 데이터는 백업 매출 데이터로 옮겨라. 그리고 장기 분석하는 데이터를 분석하는 프로그램은 실제 매출 테이블과 백업 매출 테이블을 묶어 조회하도록 해라. 분석은 어느 정도 시간이 걸려도 크게 문제가 안 되지만 실제 이번 달 매출을 처리하기 위한 테이블은 훨씬 가벼워지기 때문에 상당한 성능 개선효과가 나타난다.

9. 파티션 테이블의 사용

건수가 많은 테이블은 파티션 테이블을 고려해 주어야 한다. 파티션 테이블은 오라클 8 버전부터 지원된다. 필자는 오라클이 파티션 테이블 기능으로 인하여 명실상부하게 대용량 데이터베이스의 발판을 이룩하였다고 본다. 파티션 테이블은 테이블을 파티션이란 개념으로 쪼개어 놓아 WHERE 조건절을 해석하여 해당 조건에 맞는 파티션만 액세스할 수 있도록 한 것이다. 따라서 8번에서 기술한 것처럼 굳이 테이블을 나누지 않아도 파티션 기능을 이용한다면 효과적으로 최신 정보에 대한 관리가 가능하다.

[그림 3] 파티션테이블의 사용

파티션 테이블을 사용함으로써 얻는 큰 이점 중 하나는 작업을 병렬처리 할 수 있다는 것이다. 특히 이행이나 큰 배치 작업시 파티션 별로 프로세스를 동시에 수행하여 효과를 볼 수 있다는 것이다.
파티션 테이블을 만드는 방법은 ADMINISTRATOR’S GUIDE를 참고하기 바란다. 특히 EXCHANGE PARATITION 이란 기능은 큰 데이터를 지닌 테이블을 관리하는 DBA라면 눈여겨 보길 바란다.

10. 이론은 알겠는데 막상 닥치면 어떻게 해결해야 할까요

튜닝에 관심이 있었던 독자들은 아마 필자가 기술한 내용을 다른 책에서도 많이 봐왔을 것이다. 하지만 실제적으로 OCP에 튜닝 과목이 있지만 제대로 튜닝을 할 줄 모르는 DBA가 많다는 것은 실제 닥쳐서 문제를 해결할 수 있는 능력을 키우는 것이 쉽지 않다는 것을 의미한다.
아직 실전을 통해 튜닝 마인드를 쌓아야 한다. 앞에서 필자가 나열한 내용을 자세히 살펴보라. 그럼 공통적인 사항이 도출될 것이다. 필자는 튜닝 비법이라면 두 가지의 측면에서 계속적인 시뮬레이션을 해 본다는 것이다.

(1) 적게 읽거나 적게 실행되도록 노력
=> 안 읽어도 될 부분이 읽혀지고 있지는 않은가?
=> 결과와 상관없는 데이터가 조인되고 있지는 않은가?
=> 한번 읽은 것을 또 읽은 것이 없는가?
=> LOOP문 안에서의 SQL문 실행 횟수 제거
=> 멀티 블록 액세스(풀 스캔)과 인덱스의 부분 액세스의 대비 분석
=> 중첩 루프 방식과 해시 조인 중 어느 것이 나은가?

(2) 패러럴(병렬) 처리 기법을 반드시 활용
=> 패러럴 옵션의 적절한 활용
=> 프로세스 수행 단위를 최소화하여 병렬 처리가 가능토록 조정
=> THREAD 기법을 활용한 병렬 프로그래밍 기법 향상
=> 작업 스케쥴 수립을 통하여 누수 시간을 방지

즉, 이론을 고민하지 말고 두 가지 원칙에 의거하여 데이터의 분포도를 파악하고 플랜을 작성하면서 어떤 경우의 수가 데이터를 적게 읽히는가? 어떤 경우의 수가 데이터를 한꺼번에 처리할 수 있는가? 어떤 경우의 수가 내부적인 실행 횟수를 줄이는가를 고민해 보길 바란다.
그리고 많은 데이터가 있을 경우에는 되도록 병렬 프로세서(PARALLEL SERVER)를 활용하든 프로그램을 병렬로 수행하든 병렬 처리 기법을 익혀야 한다.

성능 저하 요인을 찾아 보자

대략의 10가지 유형을 가지고 개발자가 범하기 쉬운 오류나 자주 들어왔던 질문을 중심으로 여러분과 같이 고민해 보았다. 결론적으로 SQL 튜닝은 온라인 프로그램에서는 되도록 적게 읽혀지고 조인되는 횟수를 줄이도록 노력하며, 배치에서는 되도록 멀티블럭으로 처리하고 병렬로 처리해 주는 노력을 해주는 것이라 보면 된다. “적게 읽자, 적게 조인하자, 쓸데없는 부분 읽지 말자, 멀티블럭 활용하자, 병렬처리하자” 머리에 새겨야 한다.
처음에는 이런 방법으로 액세스 방식을 떠올리고 하나하나 자신이 DB 엔진이 되어 어떤 것이 효율적인가를 플랜을 작성하면서 연습해야 한다. 마지막 회(5회)에 종합 편으로 오늘 기술한 내용에 대해 실제로 많은 데이터를 발생시켜 실습해 볼 것이다. 다음 시간에는 성능 뷰를 통하여 성능저하 요인을 찾는 연습을 해보기로 하자.



옵티마이저 활용 예제

이제부터 여러분이 옵티마이저라고 생각하고 다음의 활용 예제를 판단해 보자. 모든 문제를 작게 읽고 조인 횟수를 줄이며, 멀티블럭 READ, 병렬 처리의 관점에서 어느 것이 유리한지를 판단하고 접근해야 한다.

활용 1 : 읽혀지는 횟수, 반복적인 READ 없애기
예를 들어 같은 건수의 부서와 사원 테이블이 있다. 부서 테이블은 인덱스가 있고 사원 테이블은 인덱스가 없다. 이를 중첩 루프 방식으로 조인할 때 어느 테이블부터 읽는 것이 빠르겠는가?

   SELECT 사원명, 부서명 FROM 사원, 부서 WHERE 사원.부서 코드 = 부서.부서 코드

[표 1]의 박스 안에 있는 것이 매번 반복 실행된다. 어느 것이 빠르겠는가? 한 부서 당 1000건 되는 데이터를 전부 다 읽어 조인하는 것보다는 당연히 한건씩 정확하게 찾아올 수 있는 1안이 빠를 것이다(온라인). 1안을 개선한다면 전체-전체 테이블이 조인되므로 멀티블럭 액세스를 하는 풀 스캔을 활용한 해시 조인이 더 효율적일 것이다(배치).


[표1] 인텍스 사용의 비교 예

활용 2 : 조인 횟수 줄이기

SELECT A.EMPNAME, SUM(B.SALARY) FROM EMP A, SALARY B
WHERE A.empno = B.empno AND b.date between ‘20010101’ AND ‘20011231’ GROUP BY A.EMPNAME

이 SQL문에는 무슨 문제가 있는가? 이는 조인 횟수를 줄일 수 있는 요소가 있는데 간과한 경우이다.

SELECT /*+ ORDERED */ A.EMP_NAME, B.SALARY
FROM (SELECT EMPNO, SUM(SALARY) FROM SALARY WHERE DATE BETWEEN ‘20010101’ AND ‘20011131’
GROUP BY ENPNO) B,
EMP A
WHERE A.empno = B.empno;

먼저 SUM을 한 다음 그 결과를 가지고 조인이 일어나기 때문에 조인 횟수가 많이 줄어든다.

활용 3 : UPDATE문에서의 조인
UPDATE문은 데이터를 변경하기 때문에 락(Lock)을 걸고 작업을 하게 된다. 그렇기 때문에 UPDATE SQL문을 잘 못 사용하면 완료가 될 때까지 다른 프로세스가 수행되지 못하는 결과를 초래한다.

UPDATE TAB1 A SET COL2 = (SELECT COL2 FROM TAB2 WHERE COL1 = A.COL1)
WHERE COL1 IN (SELECT COL1 FROM TAB2)

이와 같이 조인을 이용한 UPDATE문은 두 가지의 인라인 뷰(InLine-View)를 가지는 경우가 많다. 이런 경우 WHERE절은 데이터의 분포도에 따라 인덱스를 활용한 중첩 루프 방식으로 가든 아니면 해시 조인 방식으로 경우에 따라 조절해 주어야 하고, SET절의 TAB2 테이블을 읽을 시에는 반드시 인덱스를 활용한 중첩 루프 방식의 조인 방법을 택해 주어야 한다.
UPDATE문은 WHERE절에서 해당하는 로우(ROW)를 가지고 와서 한 건마다 SET절 안의 SQL문을 수행하기 때문이다. 따라서 앞의 경우 반드시 TAB2에는 COL1을 제1 컬럼으로 하는 인덱스가 있어야 한다.

그 외 프로그램을 느리게 하는 요인
  • 과도한 DECODE 등의 시스템 함수나 유저가 정의한 함수를 사용
    DECODE(DECODE(DECODE………………) 등의 함수를 많은 데이터를 처리하는 SQL문에 사용하거나 유저가 FUNCTION을 작성하여 이를 SQL문에서 호출한 경우 함수에 있는 SQL문은 반드시 튜닝되어 있어야 한다.

  • 데이터 존재 유무 확인을 위해 조건에 해당하는 건수를 모두 세는 행위
    SELECT COUNT(*) FROM TAB1 WHERE COL1 = ‘조건’
    => SELECT ‘X’ FROM TAB1 WHERE COL1 = ‘조건’ AND ROWNUM = 1 (한건만 세는 것으로 바꿈, EXISTS 문장을 써도 무방)

  • 불필요한 I/O를 줄이기
    테스트 단계의 디버그나 로그는 실행시에는 필수적인 것만 남길 것. 특히 printf문

  • 사소한 오류나 임의의 오류는 일단 건너뛰고 다시 처리할 것
    무슨 조건만 안 맞으면 무조건 프로그램을 중단시켜 롤백시키는 경우가 있다. 해당 부모 테이블에서 데이터를 못 찾은 경우, INSERT시 데이터가 중복된 경우 등 어떤 특이한 경우로 발생되는 오류는 따로 로깅 파일에 쌓아 분석하게 하고 연속적으로 이런 오류가 나오지 않는다면 프로그램은 일단 정상 완료시켜야 좋다.
    1000만건 이상을 처리하는 프로그램에서 막판에 약간 잘못된 데이터가 있어 3건 정도가 중복돼서 전체 데이터를 롤백 처리한다면 엄청난 시간적인 손실이다. 일단 3건을 로깅하고 프로그램을 정상 종료시킨 다음 로깅 파일을 보고 별도처리를 하면 된다. 하지만 중복이 계속적으로, 예를 들어 연속적으로 1000건 이상 중복이 일어난다면 무엇인가 잘못 처리되고 있는 경우이므로 이런 경우는 프로그램을 중단시키면 된다.

  • PRO*C를 사용하는 경우 ARRAY 프로세싱 프로그래밍을 해야 한다. I/O 횟수를 줄여주기 때문에 한건씩 처리하는 것 보다 몇 배의 시간이 단축된다. 2시간의 작업시간이 10분 이내로 줄어든다. 반드시 책을 보고 이 부분은 따로 익히기를 바란다.
참고 : 힌트에 대한 활용
자신이 뜻하는 대로 옵티마이저는 움직여주지 않는다. 이런 때에는 적절한 힌트를 구사한 후 플랜을 작성하여 자신이 원하는 대로 SQL 구문이 해석되었는지를 검증해야 한다. 다음에 자주 쓰이는 힌트를 모아 놓았다. 꼭 알아야 할 힌트는 굵게 표시하였으니 꼭 알아두길 바란다.
  • Optimizer 모드에 대한 힌트
    -. RULE
    룰 기반 옵티마이저
    -. CHOOSE
    주로 비용 기반을 유도하고자 할 때 사용
    -. ALL_ROWS
    전체 범위로 처리
    -. FIRST_ROWS
    부분 범위로 처리

  • ACCESS PATH에 대한 힌트
    -. FULL : /*+ FULL (테이블명) */
    -. ROWID : /*+ ROWID (테이블명) */
    -. CLUSTER : /*+ CLUSTER (테이블명) */ CLUSTER에 사용
    -. HASH : /*+ HASH (테이블명) */ CLUSTER에 사용
    -. HASH_AJ : /*+ HASH_AJ */ Not IN시 사용
    -. HASH_SJ : /*+ HASH_SJ (테이블명) */ EXISTS
    -. INDEX : /*INDEX (테이블명, 인덱스명1, 인덱스명2…) */
    -. INDEX_ASC : /*+INDEX_ASC (테이블명, 인덱스명) */
    -. INDEX_DESC : /*+INDEX_DESC (테이블명, 인덱스명) */
    -. INDEX_FFS : /*+INDEX_FFS (테이블명, 인덱스명) */
    -. MERGE_AJ : /*+MERGE_AJ */ NOT IN시 사용
    -. MERGE_SJ : /*+MERGE_SJ */ EXISTS시 사용
    -. AND_EQUAL : /*+AND_EQUAL (테이블명, 인덱스명, 인덱스명….) */
    -. USE_CONCAT : /*+USE_CONCAT */ OR조건을 UNION ALL로

  • JOIN 순서에 대한 힌트
    -. ORDERED : /*+ORDERED */ FROM 절의 순서대로 테이블을 읽음. 반드시 WHERE절을 맞춰주어야 함

  • JOIN 연산에 대한 힌트
    -. USE_NL : /*+USE_NL(테이블명,테이블명,..) */
    중첩 루프 조인 방식
    -. USE_MERGE : /*+USE_MERGE(테이블명,테이블명,..) */
    정렬 병합 조인 방식
    -. USE_HASH : /*+USE_HASH(테이블명,테이블명,..) */
    해시 조인 방식
    -. DRIVING_SITE : /*+DRIVING_SITE (테이블명,테이블명,..) */
    원격지 DB에 있는 테이블과 조인시

  • PARALLEL에 대한 힌트
    -. PARALLEL : /*+PARALLEL (테이블명,숫자1,숫자2) */
    -. NOPARALLEL : /*+NOPARALLEL (테이블명) */
    -. APPEND : INSERT /*+APPEND [PARALLEL..] */
    빠른 INSERT시 사용, 중단시 인덱스 깨짐
    -. NOAPPEND : INSERT /*+NOAPPEND */
    -. PARALLEL_INDEX : /*+PARALLEL_INDEX (테이블,
    -. NOPARALLEL_INDEX : /*+NOPARALLEL_INDEX (테이블, 인덱스)*/

  • 기타
    -. CACHE : /*+CACHE (테이블명) */
    -. NOCACHE : /*+NOCACHE (테이블명) */
    -. MERGE : /*+MERGE (테이블명) */
    -. NO_MERGE : /*+NO_MERGE (테이블명) */
    -. PUSH_JOIN_PRED : /*+PUSH_JOIN_PRED (테이블명) */
    (PUSH_JOIN_PREDICATE 파라미터 FALSE 설정)
    -. NO_PUSH_JOIN_PRED : /*+NO_PUSH_JOIN_PRED (테이블명) */
    (PUSH_JOIN_PREDICATE 파라미터 TRUE 설정)
    -. PUSH_SUBQ : /*+PUSH_SUBQ */




posted by 구름너머 2006. 7. 21. 09:31
posted by 구름너머 2006. 5. 24. 14:25
한국전산원(원장 김창곤 www.nca.or.kr)은 제6회 정보시스템감리사 검정을 위한 필기시험을 오는 4월 1일 실시한다.

원서접수는 14일까지이며, 감리사 홈페이지(http://auditor.nca.or.kr)를 통해서 가능하다.

정보시스템 감리사란 정보시스템의 효율성과 안전성 향상을 위해 시스템 구축과 운영에 관한 사항을 종합적으로 점검, 평가해 개선방향을 제시하는 전문가를 말한다.

필기전형 시험은

1.프로젝트 관리·

2.소프트웨어공학·

3.데이터베이스·

4.시스템 아키텍처 및 보안의

4개 과목에 대해 각 과목별 25문제씩 총 100문제가 출제된다.

상대 평가이기는 하지만,

과목별 40점 미만의 점수 획득시 과락으로 분류돼 다른 과목에서 높은 점수를 획득해도 불합격 처리된다.

'ORACLE' 카테고리의 다른 글

개발자가 범하기 쉬운 튜닝 이슈 10가지  (0) 2006.08.09
오라클 데이타베이스 입문  (0) 2006.07.21
테이블 조회(PK,comments포함)  (0) 2006.04.21
오라클 팁  (0) 2006.04.21
sysdate 활용방법  (0) 2006.04.21
posted by 구름너머 2006. 4. 21. 11:01

--1. PK 포함하여 테이블 조회
SELECT /*+ ORDERED (A B C D) */ a.table_name, D.COMMENTS, A.COLUMN_ID, A.COLUMN_NAME,
B.COMMENTS,
--C.constraint_name||' '||c.position PO,
DATA_TYPE||'('||DATA_LENGTH||')' DATA_TYPE,
A.NULLABLE,c.position PK
FROM COLS a, DBA_COL_COMMENTS B,
(select *
from all_cons_columns
where constraint_name NOT LIKE 'SYS_C%'
and owner= upper(:table_owner)
and TABLE_NAME = upper(:table_name)
) C, ALL_TAB_COMMENTS d
WHERE A.TABLE_NAME = upper(:table_name)
AND A.TABLE_NAME = B.TABLE_NAME
AND A.TABLE_NAME = C.TABLE_NAME(+)
AND A.COLUMN_NAME = B.COLUMN_NAME
AND A.COLUMN_NAME = C.COLUMN_NAME(+)
and b.owner= upper(:table_owner)
AND A.TABLE_NAME = d.TABLE_NAME
ORDER BY a.table_name, A.COLUMN_ID, a.nullable

'ORACLE' 카테고리의 다른 글

오라클 데이타베이스 입문  (0) 2006.07.21
제6회 정보시스템감리사 시험공고.  (0) 2006.05.24
오라클 팁  (0) 2006.04.21
sysdate 활용방법  (0) 2006.04.21
테이블의 필드명 변경하기  (0) 2006.04.20
posted by 구름너머 2006. 4. 21. 09:27
* 이 팁들은 oracleclub.com의 질문과 답변에 올라온 글을 제가 정리했습니다.

1. 10%를 랜덤하게 가져옵니다.(오라클 8.1.7 이상부터 지원이 됩니다.) [하얀그림자님 답변글]

SQL> SELECT *
FROM table_name
SAMPLE (10);



2. 오라클에서 CASE문 사용 예제 [하얀그림자님 답변글]

SQL>SELECT col,
CASE
WHEN col >= 0 AND col < 6 THEN 'A'
WHEN col >= 6 AND col < 14 THEN 'B'
WHEN col >= 14 AND col < 22 THEN 'C'
WHEN col >= 22 THEN 'D'
END
FROM table_name;




3. 해당년도의 선택된 주의 첫번째 날자를 구하고 싶습니다.[석철희님 답변글]

[질문]
해당년도의 선택된 주의 첫번째 날자를 구하고 싶습니다.

예로 2002년 36번째 주의 첫번째 날자는 2002/09/02입니다.
2002년 36번째주를 가지고 '2002/09/02'이란 해당주의 첫번째 날자를 구하고 싶습니다.


[답변][석철희님 답변글]

SQL> SELECT NEXT_DAY(TO_DATE(해당년도||'0101','RRRRMMDD') + (선택한주 - 2) * 7, 2)
FROM DUAL;


SQL> SELECT NEXT_DAY(TO_DATE(2003||'0101','RRRRMMDD') + (2 - 2) * 7, 2)
FROM DUAL;
NEXT_DAY
--------
03/01/06


==> FROM 앞에 2 는 월요일을 그 주에 첫번째로 선택한 거니깐 일요일을
그 주의 첫번째 날로 바꾸시려면 1로 바꾸시면 됩니다.



4. DB 테이블 내용 복사 대해서[하얀그림자님 답변글]

1). Table A와 Table B 가 있는데. A의 내용을 B로 복사하는 방법

SQL>INSERT INTO b SELECT * FROM a;


2). Table A가 있고 Table B를 생성하면서 복사하는 방법

SQL>CREATE TABLE b AS SELECT * FROM a;


3). Table A가 있고 Table A와 구조가 같고 내용은 복사하지 않는 Table B 생성 방법

SQL>CREATE TABLE b AS SELECT * FROM a WHERE 1 = 2;



5.점수별로 몇명씩 있는지 알고 싶습니다 .[배경열님 답변글]

[질문]
이름 점수
===============
홍길동 90
김길동 98
이길동 75
성춘향 60
이동국 30
최성국 100
김남일 85

일때 .....
점수별로 몇명씩 있는지 알고 싶습니다 ..

원하는 결과값
=============
점수대 명수
===============
90-100 3
80-89 1
0-79 3


[답변 ]
Sign Function을 쓰세요.
SELECT
DECODE(SIGN(점수-89),1,'90-100',
DECODE(SIGN(점수-79),1,'80-89','0-79')),
COUNT(*)
FROM
성적
GROUP BY
DECODE(SIGN(점수-89),1,'90-100',
DECODE(SIGN(점수-79),1,'80-89','0-79'))
ORDER BY 1 DESC




6. 컬럼에 해당하는 테이블명 알아내기..

SQL>SELECT TABLE_NAME
FROM USER_TAB_COLS
WHERE COLUMN_NAME ='BOARD_ID'

이렇게 하시면 됩니다..
컬럼관련 정보는 USER_TAB_COLS 데이터사전을 통해 확인할 수 있습니다.
COLS라는 시노님을 이용해도 됩니다.


SQL>SELECT COUNT(COLUMN_NAME) "Column Count"
FROM COLS
WHERE TABLE_NAME ='STORM_BOARD'



시간날때마다 틈틈히 추가하겠습니다.

================================================
* Oracle Community OracleClub.com
* http://www.oracleclub.com
* http://www.oramaster.net
* 운영자 : 김정식 (oramaster _at_ empal.com)
================================================
※ oracleclub 강좌를 다른 홈페이지에 퍼가실 경우에는 출처를 꼭 밝혀 주시면 고맙겠습니다.~^^

'ORACLE' 카테고리의 다른 글

제6회 정보시스템감리사 시험공고.  (0) 2006.05.24
테이블 조회(PK,comments포함)  (0) 2006.04.21
sysdate 활용방법  (0) 2006.04.21
테이블의 필드명 변경하기  (0) 2006.04.20
ORA-01027  (0) 2006.04.20
posted by 구름너머 2006. 4. 21. 09:18

sysdate를 활용하는 방법

select sysdate, --2006-04-21 오전 9:16:23
trunc(sysdate), --2006-04-21
trunc(sysdate,'YYYY'), --2006-01-01
trunc(sysdate,'MM'), --2006-04-01
trunc(sysdate,'DD'), --2006-04-21
trunc(sysdate,'HH24'), --2006-04-21 오전 9:00:00
trunc(sysdate,'MI')--2006-04-21 오전 9:16:00
from dual

'ORACLE' 카테고리의 다른 글

테이블 조회(PK,comments포함)  (0) 2006.04.21
오라클 팁  (0) 2006.04.21
테이블의 필드명 변경하기  (0) 2006.04.20
ORA-01027  (0) 2006.04.20
무한로우 생성 쿼리  (0) 2006.04.20
posted by 구름너머 2006. 4. 20. 18:03

ALTER TABLE BILL.TB_NET_HIST
RENAME COLUMN USE_PURPOSE_FLAG to USE_PURPOSE_CD

'ORACLE' 카테고리의 다른 글

오라클 팁  (0) 2006.04.21
sysdate 활용방법  (0) 2006.04.21
ORA-01027  (0) 2006.04.20
무한로우 생성 쿼리  (0) 2006.04.20
TO_CHAR 함수 사용시 주의점!!!  (0) 2006.04.11