활용이 가능한 블록들은 사용목록(Free List)에 기록 후 데이터 저장시 사용할 블록으로 제공됨
[그림1-1-1] 의Insert데이터 저장된 모습을 보면 데이터에 들어있는 값과는 전혀 무관하게 저장됨을 알 수 있음.
저장할 블록이 재활용 블록(11번)이었다면,비어있는 공간에 채웠을 것임.
이어진 공간이 없다면 전체블록의 로우 위치를 재배치(Condensing)하는 작업이 발생.
블록 하단의FREE SPACE는 새로운 로우가 들어올 때 사용하는 공간이 아니라,이미 들어가 있는 로우의 길이에 변화가 생겼을 때 사용하는 공간으로 테이블 정의시 파라메터(PCTFREE)로 지정 가능함.
테이블 스페이스**(Table Space)**란
논리적인 저장공간을 말함(ex.내 소유의 토지or부지) 테이블 스페이스는 물리적인 데이터파일(Datafile)로 구성되고 용도별로 나눌 수 있음
세그먼트**(Segment)란**
테이블 스페이스를 용도별로 나눌 수 있는데 이것을 세그먼트라고 함. 이 안에는 Object가 들어올 수 있음.
ROWID**의 구성**
Object No. + Datafile No.(Table space**당**SeqNo.) + BlockNo. + SlotNo. AAARJG**AAeAAACBmAAA** 6자리:데이터오브젝트번호= DB Segment식별정보(해당 로우가 속해있는 오브젝트번호) 3자리: Relative file = Tablespace의 상대적Datafile번호(해당 로우가 속해있는 데이터파일번호) 6자리: Block number = Row를 포함하는DataBlock번호(해당 로우가 속해있는 데이터파일의 데이터 블록 주소값) 3자리: Row number = Block에서의Row의Slot :데이터 블록 내에서 해당 로우의 주소값
ROWID는 테이블에 존재하는것이 아니라 인덱스에 존재함. ROWID에는 로우의 구체적인 위치정보가 들어있는 것이 아니라 방(슬록)번호가 존재
이 슬롯 번호속에 로우의 위치 정보가 들어 있다.이는 블록내의 로우의 위치가 이동 하더라도ROWID는 결코 변하지 않음.
응축**(Condensing)** 블럭 내에 저장될 로우가 한조각(one piece)이 되도록 하기 위한 공간은 있지만 이어진 조각이 없어 저장이 불가능하게 될 때 자동으로 블록의 로우들을 재구성하는 작업이 일어나는데 이를 응축이라 함.
이주**(RowMigration)** 발생한 로우들의 길이가 너무 많이 커져 응축을 하더라도 로우의 길이가 늘어날 때 사용할 여유공간이 부족하게 되면,이는 결국 다른 블록을 사용할 수 밖에 없게 됨. 로우가 다른 블록으로 이동하면ROWID의 변경을 의미하여 이를 변경시키지 않고 여러 블록을Access하는 오버헤드를 감수하는 상태를 로우의 이주(Migration)이라 함. 이는 로우나 테이블을 삭제하고 다시 생성해야만 치유할 수 있음
체인**(Chain)** 여러 블록에 걸쳐 데이터가 존재하는 점에서는 이주와 유사하나 어떤로우의 길이가 한 블록을 넘는 다면 어떤 방법을 사용하더라도 하나의 블록 내에는 존재할 수 없기 때문, 필요한 공간만큼 블록을 연결해서 저장해야 하는데 이것을 체인이 발생했다고 함. 이는 본질적으로 이주와는 다름을 유념해야 함.
1.1.2**클러스터링 팩터**(clustering factor)
분리형 테이블의 특징인'임의의 위치'에 저장으로 인해 원하는 값을 찾기위해 필연적으로 여러 블럭을 찾아 보아야 함. 이미 액세스해 두었던 블록에서 원하는 로우를 찾을 확률이 높다면 물리적으로 액세스할 블럭의 량은 분명히 줄어듬. 임의의 위치에 흩어져 있더라도 얼마나 주변에 모여 있느냐에 따라 액세스 효율은 커다란 영향을 받게 됨
인덱스의 컬럼값으로 정렬되어 있는 인덱스 로우의 순서와 테이블에 저장되어 있는 데이터 로우의 위치가 얼마나 비슷한 순서로 저장되어 있는지의 정도를 나타낸 것을'클러스터링 팩터'라고 표현.
분리형 테이블 구조가 가지는 최대 특징
임의의 위치에 저장=원하는 값을 찾기 위해 여러 곳을Access 아무 곳에나 저장됨=다양한 블록에 흩어져 있을 수 있음
관계형 데이터 베이스는 최소한 하나의 블록은Access되어야 함
1 Row Access = n Block Access
클러스터링 팩터
인덱스의 컬럼값으로 정렬되어 있는 인덱스 로우의 순서와 테이블에 저장된 데이터 로우의 위치가 얼마나 비슷한 순서로 저장되어 있는 정도
클러스터링 팩터를 향상시키는 것은 엑세스 효율에 직접적인 영향을 줌
임의의 위치에 저장이 되는 분리형 테이블은 클러스터링 팩터의 문제가 매우 중요함.
클러스터링 팩터가 좋은 인덱스로 엑세스를 하면 많은 로우를 엑세스 하더라도 적은 블록을 엑세스 하게 되어 효율적일 수 있음
1.1.3**분리형 테이블의 액세스 영향요소**
가)**넓은 범위의 액세스 처리에 대한 대처방안**
대부분의RDBMS에서는 사용자의 요청 데이터를 메모리 내에서만 처리하고 디스크에서는 여유가 있을 때 옮겨다 두는 지연기록(Differed write)방식을 취함 DISK I/O**최적화를 위하여 디스크로의 저장(Write)시간을 실제 작업의 시간과 다르게 유지(Differed Write)하는 기법을 사용한다**
1)**소형테이블** 소량의 데이터라면 그리 많지 않은 블록에 흩어져 있을 것이고,앞서 발생한 엑세스에 의해 이미 메모리 내에 존재할 가능성이 높음
메모리 내의 엑세스라면 그것이 비록 랜덤 엑세스라 하더라도 부하의 부담이 그리 크지 않음으로 충분이 수용 가능
중대한 엑세스(Critical Access Path)라면 더 적극적인 방법을 적용 특별한 경우가 아니라면 이러한 조치를 하지 않아도 무방함.
2)**중형 테이블** 등록시에는 부담이 되더라도 좋은 엑세스를 위해서는 찾기 좋은 형태로 저장해 두는 것이 바람직함. 가장 중요한 엑세스 형태를 선정 특정한 저장위치를 어느 컬럼(들)에 맞추어 줄 것인지 결정 데이터 등록시 부하 부담을 무시할 수 없다면 특정 위치에 저장을 하는 것을 재검토
3)**대형테이블** 첫째**:**단순 저장 형 개념으로 사용시 로그 정보를 관리하는 테이블을 예로 들 수 있음 신속한 저장을 요구함. 분리형이 가장 적적한 형태 파티션과 같은 조치가 필요
둘째**:대량의 데이터 수집,**랜덤 엑세스 주로 발생 고객 테이블을 예로 들 수 있음 대량의 데이터가 급속히 들어오는 경우는 적음. 범위 처리를 자주 하지 않는 편임 분리형 구조가 가장 적적한 형태 파티션이나 클러스터링을 하더라도 큰 혜택이 없는 경우임
셋째**:대량의 데이터 지속적 증가,**매우 다양한 엑세스 매출 테이블을 예로 들 수 있음 시스템에 지대한 영향을 미침 데이터 관리 및 엑세스에 대한 부담이 큼 증가하는 데이터에 대한 관리적인 부담이 크면,파티션을 적용. 최소한의 필요부분 엑세스를 위해 인덱스를 전략적으로 구성 SQL의 실행계획을 최적화 하는 방법이 가장 중요
나) 클러스터링 팩터 향상 전략
자주 엑세스 하는 것들이 유사한 위치에 모여 있도록 하는 것을 응집이라고 함. 즉 클러스터링 팩터를 좋게 하는 것을 말함.
분리형 구조는 저장 시에 우리가 강제로 제어할 수 없음.
이는 주기적인 테이블 재생성으로 응집도를 높여주어야 함.
현실적으로는체인(Chain)을 감소시키고 블록 내 데이터의 저장율을 높여 불필요한I/O를 줄이기 위해 사용함.
이로 인한 효율이 높아지는 이유는 테이블을 재생성 하면 당연히 인덱스도 다시 생성 되야 함.이는 인덱스의 가지 깊이(Branch depth)가 정리되는 효과를 줌
원하는 정렬로 생성된 인덱스가 있다면 약간의 부하가 증가하지만 힌트를 적용해서라도 인덱스를 경유하여 저장하는 것이 좋음.
인덱스와 테이블이 나누어져 있는 분리형 테이블과 달리 인덱스와 테이블이 데이터를 모두 가지고 있음. 각 인덱스 로우마다 임의의 위치에 흩어져 있는 테이블에 랜덤 액세스하는 부담이 거의 없음. 인덱스에 데이블 데이터도 포함되어 인덱스만 스캔하는 경우 더 많은 블럭을 스캔해야 함.
기본적으로 기본키로 접근 추가인덱스(Secondary Index)를 사용해도 기본기를 이용해야 함. 기본키로 이용해 만든 인덱스를 통해 다른 액세스형태를 가능하게 함
추가적 인덱스가 적용 가능한 경우 Secondary Index사용이 빈번하지 않거나 처리범위가 넓지 않은 경우처럼 부담이 없는 경우 충분히 적용가능.
로우길이에 따라 발생할 수 있는 오버플로우 일체형 구조는 일반 컬럼은 모두 보유하고 있기 때문에 길이가 증가할 확률이 매우 큼. 테이블 생성 시 적절한 파라미터를 지정하여 단점을 어느 정도 해소할 수 있음.
장점 인덱스와 칼럼들이 같이 저장되어 있기 때문에 저장 및 엑세스 효율이 증가(단 효과는 없음)
단점 유연성이 부족 인덱스와 컬럼들이 같이 있기 대문에 데이터 갱신 시 로우들의 길이가 변하므로 발생할 수 있는 오버플로우 발생 즉 저장형태는b-tree와 같은 구조이나 리프트 노드에 데이터를 같이 저장하기 대문에 길이의 변화가 생기면 새로운 노드에 이관이 되기 때문에 영구적인 주소가 될 수 없음.
1.2.3**논리적ROWID와 물리적 주소**(Physical Guess)
논리적**ROWID**의 개념 분리형 테이블에서는 인덱스의 분할이 발생하더라도 테이블의rowid는 변하지 않음. 일체형은 인덱스자신이 테이블이므로 인덱스의 분할에 따라rowid가 변할 수 있으므로 어떤 키값이 동일한 노드에 지속적으로 저장될 수 없음.
인덱스로우가 생성될 때 만들어져서 키분할에 의해 데이터블록의 위치가 달라져도 변경되지 않음.
키분할에 의해 해당 로우의 위치가 변경되었다면 논리적ROWID로 찾았을 경우 다른 로우가 존재할 수 있음.
그 로우가 존재할 가능성이 높은 주소를 나타내기 때문에 이것을Physical Guess혹은Guess라고 함.
데이터를 액세스할 때 값(ROWID)이 부정확한 값이라고 판명되면 그 때는 기본키를 이용한 액세스를 하게 됨. (부정확한 값이 높다면 아예 사용되지 않는다.)
논리적**ROWID**를 사용하는 이유
물리적 위치정보와 기본키가 상호보완작용을 하여 논리적으로 완벽한ROWID역할을 수행 Physical Guess가 완벽한ROWID는 아니지만 극히 일부는 제외하고는 유효하다면 언제나 기본키로 액세스하는 것보다는 훨씬 유리 ROWID를 이용한 적중률이 지나치게 저하되는 경우는 이미 일체형구조를 사용하지 않을 것
일체형 구조의 적용가능
전자 카탈로그나 키워드 검색용 테이블
코드성 테이블
색인 테이블
공간 정보 관리용 테이블
대부분 기본키로 검색되는 테이블
OLAP의 디멘젼 테이블
로우의 길이가 비교적 짧고,트랜젝션이 빈번하게 발생되지 않는 테이블
일체형 구조에서**추가적인Index사용 시** 물리적 위치정보가 정확하다면 일반적인 인덱스 스캔과 동일하지만,적중률이 낮다면 오히려 기본키를 이용하여 액세스하는 것이 더 효율적
물리적 위치정보를 사용하지 않는 경우 1. Secondery Index를 액세스하여 기본키정보를 얻는다 2.기본키로 데이터블록을 액세스
물리적 위치정보를 사용하는 경우 1.추가적인 인덱스를 액세스하여 물리적 위치정보를 참조한다 2.참조한 물리적 위치정보를 이용하여 데이터블록을 액세스한 후에 비교한다.이때 기본키 값이 같으면 물리적 위치정보가 유효한 것으로 간주하여 액세스를 종료한다. 3.만약 유효하지 않다면 다시 기본키로 액세스하여 데이터 블록을 가져온다
1.2.4**오버플로우 영역**(Overflow Area)
일체형 테이블의 부담요소 인덱스와 모든 컬럼이 같은 장소에 저장된다는 것은 저장공간의 분할이 발생한다든지 저장 밀도가 나빠지는 등의 부담이 증가한다
부담을 줄일 수 있는 방법
가장 쉽고 확실한 방법은 같이 적재할 컬럼을 줄이는 것
즉 오버플로우 영역에 상대적으로 빈번하게 액세스되지 않는 것들을 옮겨두는 것
따라서 테이블 생성 시 컬럼을 지정할 때 순서를 잘 결정할 필요가 있다.
일체형의 체인 IOT테이블 생성시INCLUDING과 임계치(PCTTHRESHOLD)를 이용하여 결정되며 일체형 체인은 전략적으로 사용할 수 있다. 일체형의 체인은 단순히 체인되었다는 의미보다는 인덱스영역의 저장밀도를 향상시킬 수 있는 전략이라고 볼 수 있다.
③**(c)**PCTTHRESHOLD : 인덱스블록 내의 예약된 공간의 비율을 백분율로 지정, 단일 로우 크기가 (PCTTHRESHOLD/100)/*DB_BLOCK_SIZE보다 크게 되면 이 범위 이내의 크기 까지 인덱스 영역에 남겨두고 나머지 컬럼들은 모두 OVERFLOW영역으로 이동, OVERFLOW를 지정하지 않았을 경우 데이터 입력이 실패 PCTTHRESHOLD의 기본값은 50 (이 값은 0에서 50사이에 들어야 함)
④**(d)**INCLUDING : INCLUDING이후에 선언된 컬럼들은 오버플로우 영역에 저장 INCLUDING이 지정되지 않고, 로우의 크기가 PCTTHRESHOLD 초과하면 기본키 컬럼을 제외한 모든 컬럼이 오버플로우 영역으로 이동 INCLUDING절이 적용되는 시점은 처음 로우가 저장될 때 로우인서트 시 인클루드에 선언된 컬럼을 NULL로 넣고 이후 UPDATE시 선언된 컬럼에 값을 입력하면 PCTTHRESHOLD를 초과하지 않는 한도 내에서인덱스영역에 저장된다.
⑤**(e)**OVERFLOW TABLESPACE : 지정한 PCTTHRESHOLD값을 초과한 로우의 일부가 저장될 테이블스페이스 지정 이 절을 지정하지 않으면 임계값을 초과한 로우들은 입력이 거부됨.
로우의 체인이 발생하면 데이터 액세스 효율은 떨어짐
일반테이블 : 사용자가 체인을 결정할 수 없음
일체형 : 사용자가 체인을 결정 할 수 있음, 결과에 따라 액세스 성능이 달라짐.
액세스 패턴을 분석하여 미래에 대한 예측이 가능하면 일체형을 선택하고 그렇지 않다면 사용하지 않는 것이 좋음.
일체형의 단점 : 특정 액세스에서는 랜덤 액세스의 부하가 감소하나, 다양한 액세스를 모두 수용하지 못하면서 오히려 더 많은 부담이 발생
DBMS에서의 클러스터란 테이블이나 인덱스처럼 저장공간을 가지고 있는 하나의 오브젝트
클러스터는 테이블의 상위 개념(EX : 테이블은 인덱스의 상위 개념임)
클러스터로 생성된 오브젝트 내에 테이블이 생성됨
테이블이 없는 클러스터는 어떤 데이터도 입력할 수 없음
다중 클러스터와 단일 클러스터로 구분됨
다중 클러스터 : JOIN에 필요한 컬럼을 클러스터 키로 생성, 같은 클러스터내의 컬럼은 미리 조인됨
단일 클러스터 : 하나의 테이블에서 특정 컬럼을 클러스터 키로 생성하여 액세스의 효율성을 높임(동일한 값을 같은 장소에 저장)
클러스터의 종류
인덱스 클러스터 : 인덱스를 경유하여 클러스터를 찾아감
해쉬 클러스터 : 해쉬 함수를 이용하여 클러스터를 찾아감
클러스터링 팩터란 : 액세스하고자 하는 데이터가 모여진 정도를 의미
1.3.2 단일 테이블 클러스터링
클러스터에 하나의 테이블만을 생성시킴
같은 클러스터 컬럼 값을 가진 로우는 같은 장소에 저장되므로 넓은 범위의 데이터를 동시에 액세스하고자 할 때 주로 활용
일반 테이블과 Cluster 비교
일반 테이블과 달리 클러스터 인덱스는 클러스터 키 컬럼 값마다 단 하나씩만 존재한다.
클러스터 스캔 방식
클러스터 인덱스에 있는 클러스터ID 정보를 통해 해당 클러스터를 찾는 작업
클러스터 컬럼의 조건을 '='이 아닌 'Between' , 'Like' 등을 사용할 경우 인덱스의 처리범위가 끝날 때까지 클러스터 스캔을 반복함
클러스터 특징
클러스터에 동일한 클러스터 키 값이 모두 표시되지만 물리적으로는 단 한번만 저장됨
각각의 로우는 클러스터 ID를 가지고 있어 블록헤더에는 클러스터 키 ID와 클러스터 값을 가지고 있음
클러스터 값이 수정될 경우 정해진 위치에 저장되어야 하는 클러스터의 특성상 다른 위치로 이동할 수 밖에 없음
ROWID의 변경이 발생하므로 일반 인덱스에 문제를 일으킴
ROWID는 그대로 있고 ROW Migration이 발생함
1.3.3 다중 클러스터링
다중 클러스터링 이란 ?
단위 클러스터에 두 개 이상의 테이블을 함께 저장하는 것
같은 클러스터 키 컬럼 값을 가진 각 테이블의 로우는 정해진 장소에 같이 저장되므로 테이블 조인 속도를 향상 시키고자 할 때 주로 사용
각 로우에는 클러스터 키 ID를 가지고 있으며, 블록헤더에 클러스터 정보를 가지고 있으므로, 단위 클러스터에는 동일한 값인 클러스터 키 값을 가질 필요가 없으며 클러스터 키 값은 실제로 저장되지 않음
SELECT * FROM EMP E, DEPT D WHERE E.DEPTONO = D.DEPTNO AND D.DEPTNO = '111'; --------------------------------------------------------- Execution Plan SELECT STATEMENT NESTED LOOPS TABLE ACCESS (CLUSTER) OF DEPT INDEX (UNIQUE SCAN) OF 'DEPT_CLUSTER_IDX' (CLUSTER) TABLE ACCESS (CLUSTER) OF EMP
클러스터로 묶인 테이블끼리 조인을 수행하는 경우 성능 향상
클러스터 컬럼이 수정되면 데이터 값이 수정됨으로 추가적인 클러스터 체인 블록이 발생하므로 클러스터링 효율이 감소
1.3.4 클러스터링 테이블의 비용
클러스터링의 역할
클러스터링의 부하를 감수할 수 있는 상황이거나 클러스터링의 도입만이 유일한 대안일 경우만 사용
클러스터링으로 인한 부하
클러스터링은 단지 조회의 효율만 높여줄 뿐이며 DML시 부하 발생
(가) 입력 시 부하
정해진 위치를 찾아서 저장하므로 일반 테이블에 비해 추가적인 부하가 발생
값에 따라 저장 위치가 다르므로 프리리스트를 요구하는 횟수가 증가
(나) 수정 시 부하
클러스터 키를 수정하는 부하
ROW MIGRATION 발생으로 인한 부하 발생
클러스터 체인으로 인한 클러스터링 팩터가 나빠짐
클러스터 키 이외의 컬럼을 수정하는 부하
일반 테이블과 동일
(다) 삭제 시 부하
추가적인 부하 없음
1.3.5 해쉬 클러스터링
해쉬 클러스터링의 특징
SIZE, HASHKEYS, HASH IS 파라미터를 변경할 수 없음
= 로만 액세스 가능
클러스터가 생성되면서 저장공간이 미리 할당
지정된 클러스터보다 많은 로우가 들어오면 오버플로우 영역에 저장
컬럼 값의 분포가 고르지 않으면 해쉬 Collision 발생
인덱스를 경유하지 않고 액세스하므로 인덱스 보다 효율적인 액세스를 할 수 있음
해쉬 클러스터링의 활용 범위
각종 코드를 관리하는 테이블, 우편 번호, 시스템 사용자 정보를 관리하는데 활용
해쉬 클러스터에 저장시키는 개념의 좀 더 발전한 형태가 해쉬 파티션이므로, 해쉬 파티션을 적용하는 것이 옳음
해쉬 클러스터링의 정의 -> 해쉬 클러스터의 적용을 결정한 경우
클러스터 명칭과 클러스터 키를 정의
Hashkeys : 해쉬 키 값의 개수 -> 지정한 값 보다 큰 소수로 할당
HashIs : 해쉬 함수를 정의할 수 있음 -> 키 분포를 고려해야 함
Size : 같은 해쉬 값을 가지는 로우를 위해 확보할 클러스터의 크기를 의미 -> 계산 예제는 57Page 참조
CREATE INDEX deptno_idx ON emp (TO_NUMBER(deptno)); <-이렇게 사용한다. 그냥 query날릴때 형변환해서 쓰는게 낮을꺼같다.
조인 컬럼이 경우에 따라 달라지는 경우의 조인
deparment테이블이 조인하는 컬럼과 달라질때에 sales테이블 과 departments 에 deparments가 먼저 수행될때.
...... from sales s, deparments d where d.deptno = CASE when sal_type =1 THEN sal_dept ELSE agent_no END) and d.location ='PUSAN'... 일때
CREATE INDEX deptno_idx ON sales (CASE WHEN sal_type =1 THEN sal_dept DLSE agent_no END);
부모테이블의 컬럼과 결합한 인덱스 생성
- from movement x, movement_trans y where x.mov_order =y.mov_order and x.deptno = '12345'(<- 처리주관 조건) and y.mov_date like '20110%' (<- 체크기능 역할)
- 이들 두 조건이 처리주관조건으로 된다면 수행속도를 얻을수있다.
create or replace function get_deptno(v_mov_order in number)
return varchar2 DETERMINISTIC is
ret_val varchar2(5);
begin
select deptno into ret_val
from movement
where mov_order = v_mov_order;
return ret_val;
end get_deptno;
create index dept_date_idx on movement_trans(get_deptno(mov_order),mov_date);
- x.deptno => get_deptno(mov_order)데체하면 둘다 처리주관조건이 될수있어 수행속도가 향상된다.
나. 오류데이터의 검색 문제를 해결
대.소문자나 공백이 혼재된 컬럼의 검색
enmae컬럼에 성 과 명사이에 공백이 있거나 없을경우.
=>CREATE INDEX ename_upper_ix ON employees (UPPER(ename));
불필요한 공백을 제거하고 검색하려고 할경우.
=> CREATE INDEX ename_upper_ix ON employees (UPPER(REPLACE(ename, ' ')));NULL값을 치환하여 검색
NULL값 을 다른 값으로 치환하여 검색
CREATE INDEX end_date_idx ON account_history (NVL(end_date, '99991231'));
접두사를 채워서 검색
식별번호가 다른 전화번호가 발생할때 과거데이터를 수정하기 어려울경우.
CREATE INDEX call_number_idx ON call_data (DECODE(SUBSTR(call_number,1,3),'011','','010')||call_number);
다. 가공처리 결과의 검색
복잡한 계산 결과의 검색
- create index order_amount_idx on order_items(item_cd, (order_price-nvl(order_discount,0),0) * order_count));
select /*+ index_desc(x order_amount_idx) */ *
from order_items x
where item_cd = :b1
and rownum <= 100
Execution Plan
----------------------------------------
SELECT STATEMENT Optimizer = FIRST_ROWS
COUNT (STOPKEY)
TABLE ACCESS (BY INDEX ROWID) OF 'ORDER_ITEMS'
INDEX (RANGE SCAN {*DESCENDING*}) OF 'ORDER_AMOUNT_IDX'
말일, 단가,율 의 검색
sal_date달에 가장높은 sal_amount을 검색할경우.
CREATE INDEX sal_amount_idx ON sales (LAST_DAY(sal_date), sal_amount);
판매단가를 검색하는 경우 증가율, 할당도 검색 할경우.
CREATE INDEX price_idx ON sales (ROUND(sal_amount/sal_quantity));
기간, 컬럼 길이 검색
DATE 타입의 업무처리 기간검색
CREATE INDEX term_idx ON activities (expire_date - start_date);
업무처리 기간검색 (길이)
- CREATE INDEX source_length_idx ON print_media(text_length(source_text));
라. 오브젝트 타입의 인덱스 검색
CUBE 형(TYPE)으로된 테이블생성후 VOLUMN()메소드로 인덱스 생성.
CREATE TALBLE cube_tab OF CUBE;
CREATE INDEX volume_idx ON cube_tab x (x.volume());
- 인덱스를 경유해서 액세스 수행
SELECT * FROM cube_tab x WHERE x.volume() > 100;
마. 배타적 관계의 인데스 검색
배타적 관계의 유일성 보자
특정범위의 데이터 ins_type = 'A01'인 경우만 customer_id와 ins_type 두컬럼에 유일성체크하는 경우. CREATE UNIQUE INDEX contract_idx ON insurance ( CASE WHEN THAN ins_type = 'A01' THEN customer_id ELSE null END, CASE WHEN THAN ins_type = 'A01' THEN ins_type ELSE null END );
배타적 관계의 결합 인덱스
적용일기준일이 다른 컬럼들과 결합할 경우. CREATE INDEX order_deliver_idx1 ON order_delivery (order_dept, -> 고정된 컬럼 CASE WHEN ord_type=1 THEN delivery_date ELSE shipping_date END), Item_type)
논리적으로 처리 가능한 경로는 (인덱스, 클러스터, 옵티마이져 모드, 수립된 통게정보, SQL 문장과 형태 , 시스템 및 네트워크 상태 등) 여러가지에 의해 종합적으로 감안하여 옵티마이져가 실행계획을 수립한다.
옵티마이져 버전에 따라 실행게획에 반영되는 통계정보의 차이는 있겠지만 이 중에서는 선택성(Selectivity)에 가장 중요한 미치는 정보는 컬럼값들에 대한 차별적인 분포도(밀도)와 엑세스의 분류 단가 즉 클러스터 팩터 부분이다.
Clustering Factor 란 Index 의 Table에 대한 정렬 정도(orderedness)이다.
이것은 index를 scan하는 동안 방문(access) 하게되는 Table의 Data Block 의 개수이다.
즉 넓은 범위의 Data를 Index를 경유해서 읽을 경우 Clustering Factor 가 Physical Reads 의 발생 빈도에 큰 영향을 미친다.
옵티마이저가 가장 실행계획을 수립하는데 절대적으로 영향은 미치는 것은 가장 최소량을 처리할 수 있도록 하는 것 과 가장 싼값으로 엑세스 할 수 있는냐가 가장 큰 요소이다
3.1.1. 옵티마이져와 우리의 역할
SQL 이라는 언어로 요구를 하면 DBMS는 최적의 경로(자원)를 사용하여 결과를 보여준다. 여기서 최적의 경로를 계산하는 부분이 옵티마이져라고 역활이다.
RDBMS 의 여러가지 특징 중 하나가 물리적 연결고리가 없어도 논리적 연결고리만 있으면 원하는 데이터를 찾을 수 있다는 점이 중요하다. 이것은 여러가지 논리적 연결고리가 생성 될 수 가 있다는 점이며 여기서 가장 효율적인 연결고리를 찾아 데이터를 엑세스 하는 점이 포인트 이다
DBMS 을 사용하는데 있어서 옵티마이져를 몰라도 SQL을 질의 한 뒤 원하는 결과를 얻는데는 문제는 없다. 단 원하는 결과가 나오는데 까지 있어서 어떠한 비용(시간, 자원)을 지불 하였는지 알지 못 한다면 이 옵티마이져가 효율적으로 일하는지 판단하기 어렵다. 특히 소량의 결과를 처리할 때 더욱 그렇다.
사용자가 준비해야 두어야 할 기본적인 옵티마이징 팩터는 인덱스 구성에 대한 전략과 적절한 SQL 을 작성하는 것이다. 그러나 적절한을 판단하기 위해서는 실행계획을 이해하고, 제어하기 위해서는 옵티마이져를 알아야한다.
SQL 는 비절차형 언어이기 때문에 적절한 SQL를 작성하기 위해서는 집합적 사고가 필요.
3.1.2 옵티마이져의 형태
규칙기준 옵티마이져 (RBO,Rule Based Optimizer): 엑세스를 위해 사용할 도구 인덱스 , 클러스터링 등 상태와 사용된 연산자의 형태에 따라 순위를 부유하는 방식
비용기준 옵티마이져 (CBO,Cost-Based Optimizer): 통계 정보를 바탕으로 실제의 Cost 비용을 산출하여 비교하여 최소비용을 드는 방식 선택.
Optimizer 를 어느정도 판단할 수 있다면 SQL 를 작성할 때 어떤 실행계획이 가장 유리한지 알 수 있으며 또한 예기치 않은 부하가 발생했다면 옵티마이져가 어떤 실수를 했는지 쉽게 찾아낸다
3.1.2.1 규칙기준 옵티마이져 (RBO,Rule Based Optimizer)
규칙기반 옵티마이져는 인덱스 구조나 비교연산자에 따라 순위를 부여하여 이것을 기준으로 최적의 결정을 결정한다.
01 > ROWID 로 1 로우 액세스
02 > 클러스터 조인에 의한 1 로우 액세스
03 > Unique HASH Cluster 에 의한 1 로우 액세스
04 > Unique Index 에 의한 1 로우 엑세스
05 > Cluster 조인
06 > Non Unique Hash cluster key
07 > Non Unique cluster key
08 > Non Unique 결합 인덱스
09 > Non unique 한 컬럼 인덱스
10 > index 에 의한 범위처리
11 > index 에 의한 전체범위처리
12 > Sort Merge 조인
13 > 인덱스 컬럼의 Min, MAX 처리
14 > 인덱스 컬럼의 order by
15 > 전체테이블 스켄
어떤 유형의 인덱스를 어떤 연산자를 사용하였는지 순위차이는 있다. 질의 최적화(query optimization)에서 RBO(Rule Base Optimizer)는 정해진 랭킹(ranking)에 의해 플랜을 결정한다. 같은 랭킹이라면 Where 절의 뒤부터, From절 뒤의 객체가 우선 순위를 갖는다. 한 객체(예 : 테이블)에서 같은 랭킹의 인덱스가 있다면 가장 최근에 만들어진 인덱스를 사용한다. 이는 CBO(Cost Base Optimizer)에서도 같이 적용되는 사항이다.
가) 규칙기준 옵티마이져 장점
사용자가 문제점을 미리 예측하고 자신이 원하는 방법으로 실행계획을 제어하기가 쉽다 즉 전력적인 인덱스를 구성하여 SQL 를 구성 하였을 경우 다른 요소에 실행계획이 변동되는 변수가 적다.
나) 규칙기준 옵티마이져 단점
현실적인 요소를 무시하여 개산하므로 판단 오차가 크게 나타날 수 있음.
SQL> create index emp_a on emp(deptno);
Index created.
SQL> create index emp_b on emp(deptno, empno);
Index created.
SQL> select /*+rule */* from emp where deptno =10 and empno between 7888 and 8888;
---------------------------------------------
| Id | Operation | Name |
---------------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | TABLE ACCESS BY INDEX ROWID| EMP |
|* 2 | INDEX RANGE SCAN | EMP_A |
---------------------------------------------
09 > Non unique 한 컬럼 인덱스 EMP_A index
10 > index 에 의한 범위처리 EMP_B index
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("EMPNO"<=8888 AND "EMPNO">=7888)
2 - access("DEPTNO"=10)
위 경우 이상적인 index를 타는 경우는 EMP_B index를 타는 것이다.
access("DEPTNO"=10 AND "EMPNO">=7888 AND "EMPNO"<=8888)
3.1.2.2 비용기준 옵티마이져 (CBO,Cost-Based Optimizer)
통계 정보를 바탕으로 실제의 Cost 비용을 산출하여 비교하여 최소비용을 드는 방식 선택
통계 정보 형태, 종류 DBMS, Version 에 따라 차이는 있지만 보통 테이블의 로우 수 , 블록 수 , 블록당 평균 로우수, 로우의 평균길이, 컬럼별 상수값의 종류, 분포도, 컬럼 내 NULL 값의 수, 클러스터링 팩터, 인덱스의 깊이, 최소 최대 값, 리프 블럭 수, 가동시스템의 I/O, CPU 사용정보 등 가지고 있다.
통계정보 중 가장 중요한 정보는 분포도 라고 할 수 있으며, 여기서 분포도가 좋다라는 것은 컬럼값의 종류가 많다는 것이며
이것은 곧 조건을 만족하는 처리 범위가 좁다는 의미하므로 일의 양을 결정하는 중요한 요소 이다.
여기서 문제는 분포도 라는 정보가 결합인덱스 또는 다양한 연산자를 사용하는 경우 불완전한 분포드 정보가 나올 가능성이 높다는 점이다.
모든 컬럼 값에 대해 분포도를 가지기에는 어려움이 있으므로 값을 저장하는 방식에는 두가지 있다.
넓이 균형 히스토그램(Width_balanced Histogram) : 값(Value)의 Distinct Count와 Bucket 수가 일치하는 경우 정확한 컬럼 개수 값을 가지고 있다.
높이 균형 히스토그램(Height_balanced Histogram) : 값(Value)의 Distinct Count가 Bucket 수보다 많은 경우 값마다 Bucket을 하나씩 할당
할 수 없으므로 값들을 적당한 Bucket으로 분배
-- 참조 : 2011년 상반기 대용량데이터베이스 스터디 Frequency vs. Height-Balanced (작성자 : 노준기)
통계정보의 관리를 통해 최적화를 제어 할 수 잇다.
통계정보를 관리 함으로서 테이블을 모니터링 할 수가 있다. Insert, update, delete 에 대한 발생 정보를 개략적으로 모니터링 가능.
통계정보관리는 제공된 프로시져를 이용하여 관리하는 것이 좋다.
옵티마이져를 깊이 이해하지 있지 않더라도 최소한의 성능이 보장된다.
나) 비용기준 옵티마이져의 단점
실행게획을 미리 예측하기가 어렵다.
버젼에 따라 변화가 심하다.
실행계획의 제어가 어렵다.
다) 옵티마이져의 발전 방향
옵티마이져가 발전해 가는 방향은 비용기반으로 발전 해 가고 있다. 1992년 Oracle 7에서 CBO가 지원되면서 CBO는 계속적인신기능의 적용으로 발전해 온 반면, RBO는 더 이상의 기능 향상은 없으며,향후는CBO만 지원될 계획이다.
라) 통계정보 관리를 위한 제언
자세한 TEST 내역은 2011 상반기 Optimizing Oracle Optimizer 에 나와 있습니다.
통계정보를 관리하기 위해서 DBMS 제공하는 많은 편리한 패키지들이 업데이트 되고 있다.
시스템 통계정보 수집.
시스템의 CPU, I/O 성능, 메모리성능, 등 성능에 따른 변수가 있을 수 있으므로 시스템 통계를 측정하여 좀 더 정확한
정보를 옵티마이져에게 제공함으로써 좀 더 정확한 통계정보를 가질 수 있다.
시스템 통계정보는 지정한 기간동안 시스템을 모니터링 함으로써 측정하는 부분이다.
(일반적으로 업무시간을 위주로 어느정도 시간의 Delta 값을 측정하는지..)
SYSTEM 통계 정보_info
select * from aux_stats$;
수집
SQL> exec dbms_stats.gather_system_stats(gathering_mode => 'START');
~~~~ 이 시기 동한 발생된 워크로드를 분석해서 aux_stats$ 반영
SQL> exec dbms_stats.gather_system_stats(gathering_mode => 'STOP');
삭제
SQL> exec dbms_stats.delete_system_stats();
통계정보 수집.
- 통계 정보 수집하는 경우 주의점은 Sample Data 의 양이다.
일반적으로 5% 이하 선정하는데 바람직하다고 한다. 하지만 데이터 양의 따라 제약조건이 있으므로 검토가 필요.
Analyze & DBMS_STAT 차이점
Analyze | Serial statistics gathering 기능
전체 클러스터 테이블에 대한 통계정보 수집 가능
각 테이블 파티션, 인덱스에 대해서 수집하고 , Global statistics 는 파티션 정보를 가지고 계산( 비정확 )
Analyze 에서만 가능한 기능.
Structural integrity check ( analyze (index/table/cluster) (schema.)(index/table/cluster) validate structure (cascade)(into schema table)
Chained rows 수집 ( analyze table order_hist list chained rows into <user_tabs> )
DBMS_STAT | Parallel statistics gathering 기능
Partition , sub partition 이 있는 경우에는 DBMS_STAT 이용
전체 클러스터 테이블에 대한 통계정보 수집 불가
DBMS_STAT 는 CBO 관련 통계만 수집. (즉 EMPTY_BLOCKS ,AVG_SPACE, CHAIN_CNT 수집 않함)
통계정보를 저장가능, 각 컬럼, 테이블, 인덱스, 스키마 반영가능
DBMS_STAT import /export 가능
통계정보 비교.
Oracle 10.2.0.4 통계정보에 DBMS_STATS Package 으로 제공.
* 통계정보를 비교시 일반적으로 현재,과거 를 비교하므로 과거의 통계정보의 백업(export)가 필요하며
자동적으로 백업되는 부분은 DBA_TAB_STATS_HISTORY View 조회된다 (10g 부터)
dbms_stats.diff_table_stats_in_stattab(user,'table1','table1_old');
통계정보 백업.
exec dbms_stats.export_table_stats(user,'t1',null,'t1_stat');
exec dbms_stats.import_table_stats(user,'t1',null,'t1_stat');
SQL> exec dbms_stats.gather_table_stats('JIN','EMP');
PL/SQL procedure successfully completed.
SQL> select table_name, stats_update_time from dba_tab_stats_history where table_name = 'EMP'
TABLE_NAME STATS_UPDATE_TIME
------------------------------ ---------------------------------------------------------------------------
EMP 28-JUL-11 10.00.17.072692 PM +09:00
EMP 25-AUG-11 11.35.21.381488 AM +09:00
SQL> select OWNER,TABLE_NAME,LAST_ANALYZED from dba_tables where OWNER='JIN' and TABLE_NAME='EMP';
OWNER TABLE_NAME LAST_ANALYZE
------------------------------ ------------------------------ ------------
JIN EMP 25-AUG-11
SQL> exec dbms_stats.restore_table_stats('JIN','EMP','28-JUL-11 10.00.17.072692 PM +09:00');
PL/SQL procedure successfully completed.
SQL> select OWNER,TABLE_NAME,LAST_ANALYZED from dba_tables where OWNER='JIN' and TABLE_NAME='EMP';
OWNER TABLE_NAME LAST_ANALYZE
------------------------------ ------------------------------ ------------
JIN EMP 28-JUL-11
3.1.2.3 옵티마이져 목표(Goal)의 선택
가) 옵티마이져 모드의 종류
First_rows
First_rows_n
All_rows
CHOOSE
저자는 default mode 인 All_rows 보다는 First_rows 를 기본 모드를 권장하고 있다.
이유는 대부분의 시스템은 OLTP 업무가 보다 많은 비중을 차지하고 있으며, 온라인 화 면을 통한 처리는 화면의 크기에 제한이 있고, 사람의 인지능력에도 한계가 있으므로 대부분 일단 일정량이 먼저 나타나는 형태이다. first_row 로 지정했다고 해서 전체범위를 처리하지 않은 것은 아니며, 모드에 따라서 항상 다른 실행계획이 나타나는것은 더욱 아니다.
논리적으로 전체 범위를 엑세스해야만 결과를 제공 할 수 있다면 옵티마이져 모드가 어떤 것이든 거의 동일한 실행계획을 제공한다. 그러나 부분 범위로 처리되는 SQL 이 만약 ALL_ROWS 모드로 최적화 하게 되면 전체결과를 최적화 하는 방식의 실행계획이 나타나는 경우가 많이 있다.
옵티마이져 모드가 CHOOSE 인 경우 통계정보가 있는 경우 CBO 로 풀리며, 없는 경우 RBO 로 풀리는 모드이다.
하지만 아래 같은 경우에는 통계정보가 없더라도 무조건 CBO 로 풀린다.
Partitioned tables
Index-organized tables
Reverse key indexes
Function-based indexes
SAMPLE clauses in a SELECT statement
Parallel execution and parallel DML
Star transformations
Star joins
Extensible optimizer
Query rewrite (materialized views)
Progress meter
Hash joins
Bitmap indexes
Partition views (release 7.3)
Hint
Parallel DEGREE & INSTANCES - 'DEFAULT'도 해당
나) 옵티마이져 모드의 결정 기준
OLTP 환경 (CHOOSE :9i) > First_rows 신규 OLTP 환경 > First_rows_n OLAP 환경 > ALL_ROWS
물론 절대적인 기준은 아니다. 다만 저자는 RDBMS 사용하는 목적에 따라서 위와 같은 옵티마이져가 적절하다고말하고 있다.
문제는 동작 방식이 First_rows_n 동작 방식이 버젼에 따라 (10gR2) Rule Based logic 이 추가 되었다는 점이다.
옵티마이져모드를 운영중에 바꾸는것은 매우 위험하다.
모드를 변경하거나 업그레이드로 인한 변경된 옵티마이져를 적용 할 시 충격을 줄이는 대책
옵티마이져 버젼을 과거에 사용하던 버젼으로 지정하는 방법
Parameter OPTIMIZER\_FEATURES\_ENABLE
Modifiable ALTER SESSION, ALTER SYSTEM
해당 값을 변경되면 옵티마이져 관련된 Hidden Parameter 도 적용된다. 다만 상위 버젼의 개선된 값이 False 되는 것과 대표적인 값만 바뀐다. 오히러 사라진 Parameter 도 있기 때문에 완벽한 호환성은 아니다.
기존 구 버젼의 Parameter 도 최적화로 instance 튜닝이 되어 있다면 거기에 대한 값도 사용자가 고려해서 반영해야 된다.
아래는 같은 11.2.0.1 > 11.1.0.7 옵티마이져 기능을 바꾸는 경우 추가적으로 변경이 Parameter 도 있다.
PARAMETERS TO CHANGE 11.2.0.1 TO 11.1.0.7 [ID 1096377.1]
These are the parameters that are changed when setting optimizer_features_enable=11.1.0.7 in 11.2.0.1 database.
These values represent OFE=11.1.0.7
alter session set "_optimizer_coalesce_subqueries" = false ;
alter session set "_optimizer_distinct_placement" = false ;
alter session set "_optimizer_unnest_disjunctive_subq" = false ;
alter session set "_optimizer_unnest_corr_set_subq" = false;
alter session set "_optimizer_distinct_agg_transform" = false;
alter session set "_optimizer_fast_pred_transitivity" = false ;
alter session set "_optimizer_fast_access_pred_analysis" = false;
alter session set "_aggregation_optimization_settings" = 32 ;
alter session set "_optimizer_eliminate_filtering_join" = false;
alter session set "_optimizer_join_factorization" = false ;
alter session set "_optimizer_use_cbqt_star_transformation" = false ;
alter session set "_optimizer_connect_by_elim_dups" = false ;
alter session set "_connect_by_use_union_all" = old_plan_mode;
alter session set "_optimizer_table_expansion" = false;
alter session set "_and_pruning_enabled" = false ;
alter session set "_optimizer_use_feedback" = false ;
alter session set "_optimizer_try_st_before_jppd" = false ;
alter session set "_optimizer_undo_cost_change' = '11.1.0.7' ;
버젼에 따른 옵티마이져 기능
실행계획 요약본을 생성하여 이를 참조하게 하는 아우트라인 활용 (Outline)
다) 옵티마이져 모드와 관련된 파라메터 지정
주의 할 점은 Parameter 들이 통계정보에 미치는 영향이 크기 때문에 신중한 검토가 필요하다.
CURSOR_SHARING cursor_sharing = (EXACT(default)| FORCE | SIMILAR ) 기본값인 EXACT 는 대소문자, 공백 , 비교되는 상수값이 다르면 공유하지 못한다. 그러므로 상수값만 다른 Dynamic SQL 이 루프내에서 돈다면 파싱 부하가 발생. (sql공유에 문제가 있는 경우Session level 에 적용을 권장.)
DB_FILE_MULTIBLOCK_READ_COUNT db_file_multiblock_read_count Full_TABLE_SCAN, INDEX_FAST_FULL_SCAN 을 할때 한번에 읽는 블럭의 수 대용량의 배치위주로 작업한다면 늘려주는것이 좋다. 이 파라미터의 수치가 클수록 인덱스 스캔보다는 풀 테이블 스캔의 비중이 높아진다. 옵티마이저의 플랜 결정에 민감하게 영향을 주는 값이며 값이 커지면 풀 테이블 스캔과 병행해서 Sort Merge Join 또는Hash Join의 경향이 커진다.
OPTIMIZER_INDEX_CACHING CBO가 Nested Loop Join ,IN-LIST 탐침을 선호하도록 조절하는 파라미터, Nested Loop Join시 버퍼 캐쉬 내에 이너 테이블의 인덱스를 캐쉬화하는 비율(%)을 지정하므로 Nested Loop Join시 성능이 향상되며, 옵티마이저는 비용 계산시 이 비율을 반영하여 Nested Loop Join을 선호하도록 플랜이 선택된다(0~100). 100에 근접할수록 인덱스 액세스 경로가 결정될 가능성이 높다. 기존의 RBO 를CBO로 전환시 옵티마이저를RBO 성향으로 보정하는 데 효과적이다.
OPTIMIZER_INDEX_COST_ADJ 옵티마이저가 인덱스를 사용하는 위주의 플랜으로 풀릴 것인지 또는 가능한 사용하지 않을 쪽으로 풀릴 것인지의 비중을 지정한다. CBO는 RBO처럼 인덱스를 사용하도록 플랜이 주로 만들어지게 되나, 인덱스가 있다고 해서 RBO처럼 인덱스를 이용한 플랜으로 처리되는 것은 아니다. 인덱스를 이용하는 플랜 위주로 하고자 한다면 100(%) 이하를, 가능한 인덱스를 사용하지 않고자 한다면 100 이상을 지정한다(1 ~ 10000). 이 파라미터는 기존의 RBO를 CBO로 전 환시 옵티마이저를 RBO의 인덱스 위주 성향으로 보정하는 데 효과적이다. 즉 Index Scan Cost 를 낮춤으로써 Single Block I/O 와 Multi Block I/O Cost 간의 불균형을 해소 하는 역활.
optimizer_dynamic_sampling OPTIMIZER_DYNAMIC_SAMPLING (1~10) (Default = 1(Oracle9i Database),2(Oracle Database 10g)) 통계정보가 없는 겨우 소량의 표본을 동적으로 추출하여 통계정보로 활용함을 말함. 더 나은 플랜을 결정하기 위한 목적으로 더 정확한 Selectivity & Cardinality 를 구하기 위한 방법으로 0 ~ 10 레벨이 있으며, 레벨이 높을수록 SQL 문장의실행 시점에 통계정보를 만들기 위해 테이블의 데이타를 샘플링하기 위한 추가적인 Recursive SQL이 발생된다. DYNAMIC_SAMPLING(0 ~ 10) 힌트를 통해서도 같은 기능을 할 수 있다. 그러나 내부적으로 추가적인 테이블 액세스의 비용이 발생하므로OLTP에서는 주로 사용하지 않는다. 특히 OLTP 환경에서 레벨을 디폴트 값 이상 높여 놓지 않도록 한다. Oracle Database 10g의 경우 통계정보가 없다면'다이나믹 샘플링'이 적용된다.
WORKAREA_SIZE_POLICY (AUTO | MANUAL) 옵티마이져 가 (HASH |SORT|BITMAP_MERGE|CREATE_ BITMAP) *_AREA_SIZE를 자동으로 결정하는 PGA 자동 관리 방식으로, 인스턴스에속한 모든PGA의 메모리의 합이PGA_AGGREGATE_TARGET에서 설정된메모리를 가능한 넘지 않는 범위 내에서 Workarea(Sort, Hash, Bitmap 등)를 충분히 사용하고자 하는 방식이다. 플랜은 할당된 Workarea를 가지고 플랜을 결정하게 되므로 풍부한 메모리에 의해 Hash Join, Sort Merge Join등을 선호하는 경향이 높다. 내부적으로 히든 파라미터로*_AREA_SIZE의값을가지 고 플랜을 결정할 수도 있으나 인위적인 설정 없이는 자동 할당된 메모리로 플랜이 결정된다.
HASH_AREA_SIZE, HASH_JOIN_ENABLED (Oracle Database 10g : _hash_join_enabled=true) 위의 파라미터 값에 따라서 Hash Join으로유도할수있다. Hash Join이 가능하고 해쉬 메모리가 충분하다면, 플랜에 Hash Join의 경향이 커진다
SORT_AREA_SIZE , SORT_MULTIBLOCK_READ_COUNT 위의 파라미터의 값에 따라서 Sort Merge Join으로 유도할 수 있다. 소트 메모리가 충분하다면, 플랜에 Sort Merge Join의 경향이 커진다. HASH JOIN 으로 유도 할 수 있음. (size 가 충분하면 플랜의 hash join 의 경향이 커짐)
OPTIMIZER_SEARCH_LIMIT (Default = 5) 옵티마이저에게 조인 비용을 계산할 경우, From절에 나오는 테이블의 개수에 따라서 조인의 경우의 수가 있을 수 있으며, 옵티마이저는 이들 각각의 경우의 수에 대한 조인 비용을 계산하게 된다. 물론 일부 예외사항은 있다. 예를 들어,Cartesian Production Join 등은 우선 순위가 낮으므로 뒤로 미뤄질 것이다. 이 파라미터의 값이 5일 경우 From절에 5개의 테이블에 대해서 모든 조인의경우의 수를 가지고 비용을 계산하게 되며, 그 개수는 5!=120개의 경우의 수에 대한 조인 비용을 계산하게 되므로 옵티마이저가 많은 시간을 소모하게 되므로 성능에 영향을 미칠 수도 있다.
OPTIMIZER_PERCENT_PARALLEL (Default = 0)* Optimizer_Percent_Parallel의 Parameter는 CBO가 비용을 계산하는 데 영향을 주는 파라미터이다. 즉 수치가 높을수록 병렬성을 이용하여 풀 테이블 스캔으로 테이블을 액세스하려고 한다. 이 값이 0인 경우는 최적의 시리얼 플랜이나 패러렐 플랜을 사용하며, 1~100일 경우는 비용 계산에서 객체의 등급 을 사용한다.
출처: Oracle Database Reference
3.1.3. 옵티마이져의 최적화 절차
옵티마이져의 목표
사용자가 요구한 결과를 가장 최소의 자원으로 처리할 수 있는 방법을 찾아내는 것
옵티마이저 처리 과정
1
최초 실행한 SQL은 데이터 딕셔너리를 참조하여 파싱을 수행 옵티마이저는 파싱된 결과를 이용해 논리적으로 적용 가능한 실행계획 형태를 골라내고, 힌트를 감안하여 일차적으로 잠정적인 실행계획들을 생성
2
통계정보를 기반으로 데이터의 분포도와 테이블의 저장 구조의 특성, 인덱스 구조, 파티션 형태, 비교연산자 등을 감안하여 각 실행계획의 비용을 계산 비용의 계산에는 컴퓨터의 자원(I/O, CPU, Memory)도 함께 감안됨
3
비용이 산출된 실행계획들을 비교하여 가장 최소의 비용을 가진 실행계획을 선택
가) 질의 변환기
양호한 실행계획을 얻을 수 있도록 적절한 형태로 SQL의 모양을 변환
질의 변환에 사용되는 기술들 - 뷰병합 - 조건절 진입 - 서브쿼리 비내포화 - 실체 뷰의 쿼리 재생성 - OR 조건의 전개 - 사용자 정의 바인드 변수의 엿보기
나) 비용 산정기
옵티마이져는 비용산정을 위해 선택도(Selectivity), 카디널러티(Cardinality), 비용(Cost)을 측정
선택도(Selectivity) 처리할 대상 집합에서 해당 조건을 만족하는 로우가 차지하는 비율 히스토그램 정보 유무에 따라 선택도 계산 방식은 달라짐.
히스토그램 정보가 없는 경우
통계정보만을 이용해서 선택도를 계산
히스토그램 정보가 있는 경우
미리 계산된 분포값을 직접 활용
카디널러티(Cardinality) 판정 대상이 가진 결과건수 혹은 다음 단계로 들어가는 중간결과건수를 의미 위에서 계산한 선택도(selectivity)와 전체 로우 수(Num_rows)를 곱해서 계산
비용(Cost) 각 연산들을 수행할 때 소요되는 시간비용을 상대적으로 계산한 예측치 스키마 객체에 대한 통계정보에 추가적으로 CPU와 메모리 상황, Disk I/O 비용도 고려되어 계산
다) 실행 계획 생성기
적용 가능한 실행 계획을 선별하고 비교 검토를 거쳐 가장 최소의 비용을 가진 것을 선택 다양한 적용 가능한 실행계획 형태에 대해 비교평가를 하지만 그렇다고 해서 논리적으로 존재하는 모든 것에 대해 시도를 하지는 않는다.
실행 계획 생성기는 아래의 두가지 전략을 사용 1. 쿼리 수행에 예상되는 총 수행시간에 비해 최적화에 소요되는 시간이 일정비율을 넘지 않도록 함 2. 탐색 도중 최적이라고 발생하면 실행계획을 더 이상 진행하지 않고 멈춤
3.1.4. 질의의 변환(Query Transforming)
질의 변환 Case
상수 변환 옵티마이져는 가능한 모든 수식의 값을 미리 구함 상수의 연산을 실행될 때마다 이루어지는 것이 아니라 질의 변환단계에서 한 번만 수행
예)
① sales_qty > 1200/12
② sales_qty > 100
③ sales_qty*12 > 1200
①,② 는 동일하게 취급 - 사전에 최대한의 연산을 하여 두 번째 조건식이 됨
③은 연산을 하여 ② 조건식을 만들지 않음 - 비교연산자의 추가적 연산은 연산자를 좌우로 이동시켜 단순화를 하지는 않기 때문
Like 단순화 LIKE를 사용한 조건절에서 '%' 나 '_'를 사용하지 않는 경우에는 '=' 연산으로 조건절 단순화 가변길이 데이터타입일 때만 이러한 수식의 단순화가 가능
조건식에 IN, OR을 이용한 Case IN 비교 연산자를 사용한 문장에서는 OR 논리 연산자를 이용해 여러개의 '='로 된 같은기능의 조건절로 확장
예)
① job IN ('CLERK','MANAGER')
② job = 'CLERK' OR job = 'MANAGER'
조건식에 ANY, SOME 을 이용한 Case '=' 비교연산자와 OR논리 연산자를 이용해 같은 기능의 조건절로 확장
예)
① sales_qty > ANY (:in_qty1, :in_qty2)
② sales_qty > :in_qty1 OR sales_qty > :in_qty2
ANY, SOME 뒤에 서브쿼리를 사용한 Case EXISTS 연산자를 이용한 서브쿼리로 변환
예)
① where 100000 > ANY (select sal from emp where job = 'CLERK')
② where EXISTS (select sal from emp where job = 'CLERK' and 100000 > sal)
조건식에 ALL 을 이용한 Case 조건절은 '=' 연산자와 AND 논리 연산자를 사용해 같은 기능의 조건절로 변환
예)
① sales_qty > ALL (:in_qty1, :in_qty2)
② sales_qty > :in_qty1 AND sales_qty > :in_qty2
ALL 뒤에 서브쿼리를 사용한 Case NOT ANY로 변환한 후에 다시 EXISTS 연산자를 이용하여 서브쿼리로 변환
예)
① WHERE 100000 > ALL (SELECT sal FROM emp WHERE job = 'CLERK')
② WHERE NOT (100000 <= ALL (SELECT sal FROM emp WHERE job = 'CLERK'))
③ WHERE NOT EXISTS (SELECT sal FROM emp WHERE job = 'CLERK' AND 100000 <= sal)
조건식에 BETWEEN 를 이용한 Case '>=', '<=' 비교 연산자를 사용하여 변환
예)
① sales_qty BETWEEN 100 and 200
② sales_qty >= 100 AND sales_qty <= 200
조건식에 NOT를 이용한 Case 논리 연산자를 제거할 수 있는 반대 비교연산을 찾아 대체시키는 변환
예)
① NOT (sal < 30000 OR comm IS NULL)
② NOT sal < 30000 AND comm IS NOT NULL)
③ sal >= 30000 AND comm IS NOT NULL
① → ② → ③ 순서로 변환
서브쿼리에 사용된 NOT을 변환하는 Case NOT를 없애기 위해 반대 비교연산을 찾아 대체시키는 변환
예)
① NOT deptno = ( SELECT deptno FROM emp WHERE empno = 7689)
② deptno <> (SELECT deptno FROM emp WHERE empno = 7689)
3.1.4.1 이행성규칙(Transitivity principle)
조건절에 같은 컬럼을 사용한 두 개의 조건식이 있다면 옵티마이져는 새로운 조건식을 이행성 규칙에 따라 생성하고 이 조건식을 이용하여 최적화를 수행
WHERE column1 comparison_operators constant AND column1 = column2
comparison_operators : =, !=, ^=, <, <>, >, <=, >= 중의 하나
OR를 사용한 조건절을 분기시켰을 때 각각이 인덱스 접근 경로로 이용할 수 있다면 변환 : UNION ALL에 의해 각각의 인덱스를 경유하는 실행계획을 선택하고 나중에 결합 : 실행계획에서는 'IN-LIST ITERATOR' 나 'CONCATENATION'이라는 표시가 나타남
예)
select * from emp where job = 'CLERK' OR deptno = 10 ;
select * from emp where job = 'CLERK'
UNION ALL
SELECT * from emp where deptno = 10 and job <> 'CLERK';
조건절에 인덱스를 사용할수 없고 전체 테이블을 스캔한다고하면 OR를 사용한 조건절은 그 문장에 대한 변환을 하지 않음이행성규칙은 효율적이라고 판된될 때만 변환이 일어난다.
3.1.4.2 뷰병합(View Merging)
뷰를 사용하는 쿼리
뷰쿼리 : 우리가 뷰를 생성할 때 사용한 SELECT 문
액세스 쿼리 : 뷰를 수행하는 SQL
뷰쿼리와 엑세스쿼리를 병합하는 방법
뷰병합(View Merging)법: 뷰쿼리를 액세스쿼리에 병합해 넣는방식
조건절 진입(Pushing predicate)법 : 뷰병합을 할 수 없는 경우를 대상으로 뷰쿼리 내부에 액세스쿼리의 조건절 진입시키는방식
조건절 진입의 경우(뷰병합이 불가능한 경우) ■ 집합연산(UNION, UNION ALL, INTERSECT, MINUS) ■ CONNECT BY ■ ROWNUM 을 사용한경우 ■ SELECT-List의 그룹함수(AVG,COUNT,MAX,MIN,SUM) ■ GROUP BY(단, Merge 힌트를 사용했거나 관련 파라메터가 Enable이면 뷰병합 가능) ■ SELECT-List 의 DISTINCT (단, Merge 힌트를 사용했거나 관련 파라메터가 Enable이면 뷰병합 가능)
뷰쿼리를 액세스 쿼리로 병합시키기
액세스 쿼리에 있는 뷰의 이름을 뷰쿼리의 원래의 테이블로 이름을 바꾸고, 뷰의 WHERE절에 있는 조건절을 액세스 쿼리 WHERE절에 추가
예)
CREATE VIEW emp_10 (e_no, e_name, job, manager, hire_date, salary, commission, deptno) AS
SELECT empno, ename, job, mgr, hiredate, sal, comm, deptno
FROM emp
WHERE deptno= 10;
- 액세스 쿼리
SELECT em_no, e_name, salary, hire_date
FROM emp_10
WHERE salary >10000;
- SQL 병합된 쿼리
SELECT empno, ename, sal, hiredate
FROM emp
WHERE deptnl = 10
AND sal > 10000
조건절 진입의 경우
집합연산의 경우
예)
CREATE VIEW emp_union_view(e_no,e_name, job, mgr, hiredate,sal, comm,deptno) AS
SELECT empno, ename, job, mgr, hiredate, sal, comm, deptno FROM regular_emp
UNION ALL
SELECT empno, ename, job, magager, hiredate, salary, comm, 90 FROM temporary_emp;
- 액세스 쿼리
SELECT e_no, e_name, mgr,sal
FROM emp_union_view
WHERE deptnl = 20;
- 변환된 쿼리
SELECT empno, ename, mgr, sal
FROM (SELECT empno, ename, ggr, sal FROM regular_emp WHERE deptno = 20
UNION ALL
SELECT empno, ename, magager, salary FROM temporary_emp WHERE 90 = 20);
액세스쿼리에 있는 조건들이 각 select 문의 조건절속으로 파고 들어갔음을 알 수 있음
GROUP BY 를 사용한 뷰
예)
CREATE VIEW emp_group_by_deptno AS
SELECT deptno, AVG(sal) avg_sal, MIN(sal) min_sal, MAX(sal) max_sal
FROM emp
GROUP BY deptno;
- 액세스 쿼리
SELECT *
FROM emp_group_by_deptno
WHERE deptno = 10;
- 변환된 쿼리
SELECT deptno, AVG(sal) avg_sal, MIN(sal) min_sal, MAX(sal) max_sal
FROM emp
WHERE deptno = 10
GROUP BY deptno;
복잡한 뷰 내부에 GROUP BY나 DISTINCT 를 사용한 경우
뷰병합과 관련된 파라메터(complex\_view\_merging, optimizer\_secure\_view\_merging)가 작동상태되어 있으면 복잡한 뷰 내부에 GROUP BY 나 DISTINCT 를 사용했다라도 액세스쿼리의 조건들이 뷰쿼리에 파고들어 갈 수 있다.
예)
- 액세스 쿼리
SELECT emp.ename, emp.sal
FROM emp, dept
WHERE (emp.deptno, emp.sal) IN (SELECT deptno, avg_sal FROM emp_group_by_deptno)
AND emp.deptno = dept.deptno
AND dept.oc = 'London' ;
- 변환된 쿼리
SELECT e1.ename, e1.sal
FROM emp e1, dept d, emp e2
WHERE e1.deptno = d.deptno
AND d.loc = 'London'
AND e1.deptno = e2.deptno
GROUP BY e1.rowid, d.rowid, e1.ename, e1.dalary
HAVING e1.sal = AVG(e2.sal) ;
3.1.4.3 사용자 정의 바인드 변수의 엿보기(Peeking)
바인드 변수를 사용한 커리가 처음 실행될 때 옵티마이져는 사용자가 지정한 바인드 변의 값을 '살짝 커닝'함으로써 조건절의 컬럼값이 상수값으로 제공될때와 마찬가지로 선택도를 확인하여 최적화를 수행하도록 한다.
최초에 실질적인 파싱이 일어날때만 단 한번 변수의 값을 PEEKING 함
PEEKING의 적용 여부는 _OPTIM_PEEK_USER_BINDS 파라메터로 결정
현실적으로 최초의 단 한 가지 경우의 선택도를 사용하는 것이나 전체를 평균적인 분포로 보는 것이나 논리적으로 오류일 확률은 다르지 않음 : 11g에서는 Adaptive Cursor Sharing 기능으로 이러한 문제를 많이 해결함
3.1.5 개발자의 역할
집합적 사고로의 전환 과거 절차형 프로그래밍의 개념에서 SQL의 근간이 되는 집합적, 비절차형 처리에 적응해야 함
수행 결과 뿐만 아니라 성능을 고려한 SQL을 활용 SQL에 대한 활용도가 높아지면서 점차 모든 처리를 SQL로 해결하려는 개발자들이 생겨남 하지만 그 과정에서 요구되는 컬럼의 값이 늘어갈 때마다 조인으로 붙이고 집합의 결과 단위가 달라지면 Group by. 추가 정보가 필요하면 Union all 로 붙이고.. 무한 반복. 옵티마이저에 무지한 상태에서 어설픈 방법으로 복잡한 SQL을 작성하면 안됨
자신이 작성한 SQL이 개략적으로라도 어떤 형식으로 수행하며, 어느정도 처리량으로 수행될 것인지 파악할 수 있는 힘이 바탕이 되어야 함
어떤 하나의 스캔이 가장 최적이라 말할 수 없다. 우리의 실생활에는 다양한 운송수단이 있다. 이는 데이터를 액세스 하는 방법에서도 마찬가지 이다.
이제 그 스캔의 유형의 내용을 살펴봅시다.
3.2.1.1 전체테이블 스캔(Full Tables Scans)
전체테이블 스캔(Full Tables Scans) 테이블에 있는 모든 로우들을 읽어내는 방법. 다중 블록단위로 메모리에 옮겨지며, 이 블록들은 순차적으로 읽혀진다. 일반적으로 블록들은 서로 인접되어 있기 때문에 한번의 I/O 에서 처리되며 , 이것은 로우당 소요되는 운반단가를 저렴학게 만든다. 한번의 액세스 하는 블록의 양을 정의하려면 DB_FILE_MULTIBLOCK_READ_COUNT 파라메터에서 지정한다. 그럼 이제 옵티마이져가 Full Tables Scan 을 선택하게 되는 경우를 살펴 봅시다.
적용가능 인덱스의 부재 존재하고 있는 인덱스를 전혀 사용할 수 없는 경우. ex) 1.결합인덱스의 선두컬럼이 존재하지 않을때. 2.인덱스를 가졌지만 가공을 해버려 그 인덱스를 사용할 수 없을때 단, 예외적으로 Function Base Index 나 Index Skip Scan이 적용되면 인덱스 사용이 가능하다.
넓은 범위의 데이터 액세스 적용가능한 인덱스가 존재하더라도 처리범위가 넓어서 전체테이블 스캔이 보다 적은 비용이 든다면 Full Tables Scan을 적용할 수 있다.
소량의 테이블 액세스 최고수위 표시 내에 있는 블록이 DB_FILE_MULTIBLOCK_READ_COUNT 이내에 있다면 Full Tables Scan이 일어날 수 있다. (항상 그런것은 아님)
병렬처리 액세스 병렬처리는 Full Tables Scan을 더욱 효과적으로 수행하게 되므로 병렬처리로 수행되는 실행계획을 수립할때는 항상 Full Tables Scan을 선택한다.
'FULL' 힌트를 적용 했을때 Full 힌트를 사용했을때...단, FULL 힌트가 적절하지 않다면 옵티마이져는 이를 무시할 수 있다.
※ Full Table Scan 을 했을때의 실행계획 ※
Full Tables Scan의 가장 단순한 형태
SELECT * FROM TF002NGT WHERE ITEM_NAME LIKE '%BF%'
'BT_REG_BASE' 테이블을 Full Tables Scan 하여 150842 건을 액세스 하여 조건절을 체크하였더니 2658건이 되었고 이를 다른 테이블에 입력하였다는 것을 의미. 이때 Select 한 테이블은 실행계획에 나타나지만 입력된 테이블은 해당 SQL 에서만 확인할 수 있다. 또한 'Execute' 란에만 숫자가 나타난다. 액세스를 한 테이블에 대해서는 'Fetch'가 발생하였지만 실행통계에는 액세스와 입력 작업에서 발생한 것을 모두 합해서 두 번째 라인에 기록한다.
조인에서 발생하는 Full Tables Scan. <NestedLoops 조인시 선행 처리에서 Full Tables Scan이 발생하는경우> Rows Execution Plan --------------- ------------------------------------------------------------------------------ ④ 7701 NESTED LOOPS ①148046 TABLE ACCESS (FULL) OF 'ITEM_BASE' ③ 7719 TABLE ACCESS (BY INDEX ROWID) OF 'CS_SPEC' ② 7724 INDEX (UNIQUE SCAN) OF 'PK_CS_SPEC'
① 'ITEM_BASE'를 Full Tables Scan으로 액세스한 로우수는 148046. 이중 조건절을 통과한 로우수는 7724. 이것은 'PK_CS_SPEC' 에 연결을 시도한 회수를 보고 알아낼 수 있음
② 선행테이블의 조건을 통과한 7724건이 'CS_SPEC' 테이블의 기본키를 이용하여 연결을 시도. 이중 5건은 실패. 이는 7724 - 7719 를 해보면 알 수 있음.
③ 기본키의 ROWID 로 액세스 하였다. 근데 'CS_SPEC' 에도 체크조건이 있었다는걸 알 수 있음. 이는 7719 - 7701 를 해보면 알 수 있음.
④ 최종결과가 7701 임을 알 수 있음.
조인에서 발생하는 Full Tables Scan. <NestedLoops 조인시 후행 처리에서 Full Tables Scan이 발생하는경우> Rows Execution Plan --------------- ------------------------------------------------------------------------------ ④ 280 NESTED LOOPS (OUTER) ① 74861 TABLE ACCESS (BY INDEX ROWID) OF 'BAL_ITEM' ③210991 INDEX (RANGE SCAN) OF 'PK_BAL_ITEM' (UNIQUE) ② 53200 TABLE ACCESS (FULL) OF 'TPF_INFO'
① 'BAL_ITEM' 테이블의 기본키를 범위 스캔하면서 Rowid 로 테이블을 액세스한 건이 210991 이지만. 실제 테이블을 액세스 한것은 74861건. 이러한 현상이 나타나는 이유는 최소 두개 이상의 컬럼에 조건이 부여되었지만 이들이 결합인덱스로 구성된 기본키에서 연속된 순서를 가지고 있지 않다는것을 나타낸다. 즉, 이 컬럼들 사이에 조건으로 부여하지 않은 하나이상의 컬럼이 존재 한다는것.
② 테이블을 액세스한 78461 중에서 체크조건에 의해서 다시 걸러지고 남은것은 280건. 그 이유는 이 조인은 아우터 조인이기 때문에 연결에 실패 하였더라도 조인은 언제나 성공이므로 그 조인결과 집합인 280과 동일하다. 또한. 선행테이블(BAL_ITEM)이 이미 범위 처리를 했기때문에 나중에 연결되는 집합은 그 테이블의 기본키로 유일하게 액세스 되어야 한다. 그러나 연결고리에 인덱스가 없기때문에 연결 대상마다 매번 Full Tables Scan를 하였고.
③ 이 테이블(TPF_INFO)을 스캔한 로우스는 53200이지만 연결을 시도한 횟수는 280 이다. 그렇다면 TPF_INFO 의 총 로우 수는 190(=53200/280) 이다.
④ 연결을 시도한 280건이 아우터 조인에 의해 모두 성공하게 되므로 최종결과는 280건.
3.2.1.2 로우식별자 스캔(Rowid Scans)
ROWID는 그 로우를 포함하고 있는 데이터파일과 데이터 블록, 그리고 블럭 내에서의 위치를 가지고 있다. 그러므로 하나의 로우를 찾는 가장 빠른 방법. 대부분의 ROWID 스캔은 인덱스를 경유하여 테이블을 액세스 하는 과정에서 발생.
3.2.1.3 인덱스 스캔(Indesx Scans)
실제적으로 가장 많이 발생하는 방식. 로우를 추출할때 결과를 보면 로우를 찾는것이지만 실제 내부적인 I/O는 언제나 블록을 액세스 한다. 따라서 옵티마이져가 비용을 산정할때도 블록을 기준으로 계산한다. 이는 클러스터링 팩터가 얼마나 좋으냐에 따라 액세스 효율에 커다란 영향을 미치게 된다.
인덱스 스캔을 좀더 세부적으로 분류하면 다음과 같다.
가) 인덱스 유일 스캔 (Index unique Scan) 나) 인덱스 범위 스캔 (Index Range Scan) 다) 인덱스 역순 범위 스캔 (Index Range Scans Descending) 라) 인덱스 스킵 스캔 (Index Skip Scan) 마) 인덱스 전체 스캔 (Ful Scan) 바) 인덱스 고속 전체 스캔 (Fast Full Index Scan) 사) 인덱스 조인 (Index Join) 아) 비트맵 인덱스 (Bitmap Index)
가) 인덱스 유일 스캔 (Index unique Scan) 단 하나의 Rowid를 추출한다. 인덱스가 기본키나 유일인덱스 (Unique index)로 생성되어 있어야 하며 인덱스를 구성하는 모든 컬럼들이 모두 조건절에서 '=' 비교 되어야 한다. 인덱스 유일 스캔 (Index unique Scan)로 유도해야 하는 경우 특정 인덱스 사용을 권고하는 'INDEX(Table_Alias Index_Name)' 힌트를 준다.
나) 인덱스 범위 스캔 (Index Range Scan) 가장 보편정인 액세스 형태. 시작과 종료를 가진 경우와 하나 이상이 끝을 가지지 않은 경우가 있다. 이 스캔을 경유 하여 추출되는 로우는 인덱스 구성 컬럼의 정렬 순서와 동일하게 나타난다. 최초의 시작점을 찾을 때만 랜덤 액세스를 사용하고 그 후로 종료시 까지는 스캔을 한다. 즉, Branch Block 를 경유 하여 Leaf Block 을 찾은후 연결된 다음 Leaf Block 를 스캔 하다 종료점을 만나면 멈춘다. 스캔 범위가 넓어질때 부하가 증가하는것은 인덱스 탓이 아니라 인덱스의 ROWID로 테이블을 랜덤 액세스 해야 하는 부분이다 따라서 인덱스 범위 스캔 (Index Range Scan) 클러스터링 팩터에 직접적인 영향을 받는다. 인덱스 범위 스캔 (Index Range Scan)로 유도하는 힌트는 'INDEX(Table_Alias Index_Name)' 이다.
다) 인덱스 역순 범위 스캔 (Index Range Scans Descending) 역순으로 데이터를 액세스 한다는 것을 제외 하면 인덱스 범위 스캔 (Index Range Scan)와 동일. 인덱스는 순차적으로 정렬되어 저장된다. 따라서 이 스캔은 가장 최근의 값을 가장 처음 스캔할 수 있다. 이는 실무적으로 많은 도움이 된다. 인덱스 역순 범위 스캔 (Index Range Scans Descending)을 유도하는 힌트는 'INDEX_DESC(Table_Alias Index_Name)' 이다
라) 인덱스 스킵 스캔 (Index Skip Scan) sal_tp(매출유형) + item_cd(상품코드) + sal_dt(매출일자) 로 구성된 인덱스가 있고 쿼리의 조건에는 item_cd , sal_dt만 사용되었다. sal_tp는 D,E,L 세종류만 있다고 가정한다. 인덱스 스킵 스캔이 적용 되었다면 마치 조건절에 sal_tp IN ('D','E','L') 을 추가한 것과 동일한 효과를 얻을 수 있다. 여기서 D,E,L 을 논리적 서브 인덱스 라고 부른다. 인덱스 스킵 스캔은 서브인덱스의 종류가 많지 않고 뒤에 오는 컬럼은 종류가 많을때 가장 좋은 결과를 얻을수 있다. 이말은 이런 경우가 아니라면 큰 효과를 얻을 수 없다는 뜻이다. 인덱스 스킵 스캔으로 유도 하는 방법은 'INDEX_SS','INDEX_SS_ASC','INDEX_SS_DESC' 등이 있고 인덱스 스킵 스캔을 하지 않고 싶다면 'NO_INDEX_SS'를 사용한다.
마) 인덱스 전체 스캔 (Ful Scan) 조건절에서 그 인덱스의 컬럼이 적어도 하나이상 사용 되었을 때 적용이 가능하다. 즉, 반드시 선행컬럼이 사용되어야 할 필요는 없다는 것. 근데 만약 아래 두 조건을 모두 만족한다면 조건절에 전혀 사용된 컬럼이 없어도 적용 가능하다. *쿼리 내에 사용된 어떤 테이블들의 모든 컬럼들이 그 인덱스에 모두 존재하고 *인덱스 컬럼 중에서 최소한 NOT NULL 인 컬럼이 하나는 존재할 때
바) 인덱스 고속 전체 스캔 (Fast Full Index Scan) 만약 쿼리를 위해 사용된 어떤 테이블의 컬럼이 모두 그 인덱스에 포함되어 있다면 인덱스 고속 전체 스캔은 전체테이블 스캔의 대안으로 사용될 수 있다. 단, 인덱스 전체 스캔 (Ful Scan) 과 마찬가지로 NOT NULL 제약조건의 컬럼이 반드시 하나 이상 존재해야 한다. 이 스캔으로 유도 하고 싶다면 'INDEX_FFS' 그렇지 않다면 'NO_INDEX_FFS' 로 해제 한다.
사) 인덱스 조인 (Index Join) 별도의 장에서 상세히 설명
아) 비트맵 인덱스 (Bitmap Index) 별도의 장에서 상세히 설명
3.2.1.4 B-Tree클러스터 액세스(Cluster Access)
1:M 관계를 가진 두 테이블을 클러스터링 하면 동일한 클러스터 키값을 가진 두 테이블의 모든 로우는 같은 클러스터 내에 저장된다. 이때 1쪽의 데이블을 클러스터키로 액세스 하면 하나의 로우가... M 쪽을 액세스 하면 여러개가 나타날 것이다.
3.2.1.5 해쉬 액세스(Hash Access)
인덱스를 이용하여 데이터를 액세스 하는 방법은 반드시 인덱스 I/O 와 테이블 I/O를 거쳐야 하지만 해쉬클러스터의 데이터 접근경로는 해쉬함수를 생성하는 것과 테이블 I/O로만 구성되므로 그만큼 I/O를 줄일 수 있다. 따라서 넓게 산포된 테이블의 액세스에서 디스크 I/O를 줄임으로써 시스템 성승향상을 기대할 수 있다. 이 스캔은 해쉬함수를 통해서 데이터를 액세스 해야하므로 액세스 형태가 다양하지 않고 주로 '=' , 'BETWEEN', 'IN'으로 적용할 수 있는 테이블에 적용해야 한다. 또한 해쉬키로 지정된 컬럼이 자주 손상되지 않는것이 좋고 대량으로 데이터가 증가하지 않는것이 바람직 하다.
3.2.1.6 표본 테이블 스캔(Scans)
테이블의 데이터 중에서 사용자가 부여한 비율 만큼의 데이터를 읽고 그중에서 조건을 만족하는 로우들을 리턴 한다. 테이블의 표본 데이터를 스캔하는 방식은 아래와 같다. SELECT ..... FROM tabel_name SAMPLEBLOCK option(Sample Percent) WHERE ... GROUP BY .... HAVING .... ORDER BY .....
SAMPLE BLOCK(sample percent)를 사용하면 전체 액세스 대상 블록에서 지정한 비율(sample percent) 만큼의 블록을 읽은 후 조건을 만족하는지 확인한다. 여기서 숫자는 확률값을 의미 하므로 수행할 때마다 다른 블록이 나타날수 있다.
SAMPLE BLOCK(sample percent)를 지정하면 모든 블록이 액세스되지만 각각의 블록에서 지정한 비율만큼의 로우들을 임의로 선택한 후 이를 대상으로 조건을 체크하여 결과를 리턴한다.
비율(Sample Percent)는 0.000001 과 99.999999 값을 지원하며 , 0 또는 100을 지원하지 않는다.
주의사항은 로우 수가 작은 테이블에서 견본 데이터를 액세스하면 일정 비율의 데이터가 리턴되지 않을 수도있다.
이 기능은과거에는 하나의 테이블에서 쿼리를 할 때만 사용가능 하였으나 10g 부터는 이런 제한이 없어졌다. 또한 비용기준 옵티마이져를 사용하게 되며 'RULE' 힌트를 사용하더라도 비용기준으로 수행된다.
테이블 , 뷰 혹은 인라인뷰로 가공된 중간집합들은 조인을 통하여 연결된다. 조인은 조건절에 연결을 위한 논리적인 연결고리를 가지고 있다. 조인의 종류는 다음과 같다.
내포 조인(Nested loops Join)
정렬 병합 조인(Sort Merge Join)
해쉬 조인(Hash Join)
세미조인(Semi Join)
카티젼 조인(Cartesian Join)
아우터 조인(Outer Join)
인덱스 조인(Index Join)
3.2.2.1. 내포 조인(Nested loops Join).
가장 고전적인 형태의 조인방식. 현실적으로 가장 많이 적용 되고 그럴수 밖에 없는 가장 기본적인 조인. 이 조인의 핵심은 먼저수행되는 칩합의 처리범위가 전체의 일량을 좌우한다는 것과 나중에 반복 수행되는 연결작업이 랜덤 액세스로 발생한다는 점이다. 이 조인의 단계는 다음과 같다.
. 옵티마이져는 외측집합(Outer Lopo)을 결정한다. 이를 선행(Driving) 집합 이라 한다. 선행집합의 처리범위에 있는 각 로우에 대해 내측집합을 연결한다.
. 선행 집합이 액세스되면 그들의 모든 컬럼은 상수값을 가지게 되며 이미 존재하던 상수값 까지 감안해서 나머지 집합들 중에서 다음 수행할 내측 집합을 선택한다.
. 만약 조인될 집합이 더 있다면 위의 방법으로 나머지 순서도 결정한다.
. 외측 집합 각각의 로우에 대해 내측 집합의 대응되는 모든 로우가 액세스 된다.
큰 흐름은 이렇다
NESTED LOOPS outer_loop inner_loop
Rows Execution Plan -------- ------------------------------------------------------------------------------ 0 SELECT STATEMENT 240 NESTED LOOPS 540 TABLE ACCESS (CLUSTER) OF 'ORD_MST' 63 INDEX (RANGE SCAN) OF 'ORD_MST_CLX' (CLUSTER) 806 TABLE ACCESS ( BY ROWID) OF 'ORD_DETAIL' 1346 INDEX (RANGGE SCAN) OF 'ORD_DETAIL_IDX2' (NON-UNIQUE)
위 실행계획의 외측 루프는 클러스터 키를 범위 스캔 하면면서 테이블을 클러스터 스캔을 하고 있다. 이렇게 액세스된 각각의 로우에 대해 내측루프가 수행된다. 외측 루프에서 액세스한 로우수보다 내측루프의 인덱스 스캔량이 많고 'RANGE SCAN'을 한것을 보면 내측 루프 액세스는 'M'쪽 집합이다. 이 인덱스를 경유하여 테이블을 액세스 한것이 806건 이니 인덱스 스캔에서 540건이 걸러졌다는 것도 알 수 있다. 그후 내측조인에서 테이블 액세스까지 완료한 후 다시 조건에 의해 걸러졌기 때문에 최종 완료 건수는 240건 이다. 이 조인 방식으로 유도하기 위하여 'USE_NL(table1, table2)'를 사용할 수 있다.
◈ 진보된 내포 조인(Advanced Nested Loops Join) 진보된 Nested Loops 조인은 클러스터링 팩터가 양호하다면 보다 많은 부분을 한번의 블록 액세스에서 연결할 수 있기 때문에 물리적, 논리적 블록 액세스 량이 크게 감소함으로써 효율성이 크게 높아지게 되었다.
SELECT e.* , d.DNAME FROM EMP e , DEPT d WHERE e.DEPTNO = d.DEPTNO AND d.LOC = 'SEOUL' AND e.JOB = 'CLERK';
Execution Plan -------------------------------------------------------------------------------- 1.TABLE ACCESS (BY INDEX ROWID) OF 'EMP' 2. NESTED LOOPS 3. TABLE ACCESS (BY INDEX ROWID) OF 'DEPT' 4. INDEX (RANGE SCAN) OF 'DEPT_LOC_IDX'(NON-UNIQUE) 5. TABLE ACCESS (BY INDEX ROWID) OF 'EMP' 6. INDEX (RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE)
내측 루프의 테이블 액세스 부분이 제일위로 이동하고, 인덱스를 액세스 하는 부분이 차지 하고 있음. 그 절차는. 다음과 같다.
1. 'DEPT_LOC_IDX' 인덱스를 이용하여 LOC = 'SEOUL'인 DEPT 테이블의 첫번째 로우를 액세스. 2. 액세스한 DEPT 테이블의 DEPTNNO를 이용하여 EMP 테이블의 인덱스를 범위 스캔하면 DEPT 테이블의 DNAME과 결합한 결과의 집합이 만들어 진다. 3. 이제 ROWID를 이용하여 EMP 테이블의 블록을 액세스 하여 PGA 버퍼에 저장 4. 앞서 2. 에서 만들어진 집합에 있는 해당 블록을 가진 로우와 3. 에서 액세스해 둔 PGA버퍼를 이용하여 대응되는 로우를 찾아서 JOB='CLERK' 조건을 체크하고, 성공하면 운반단위로 보냄. 이부분이 핵심. 각각의 ROWID 마다 테이블 액세스를 시도하는것이 아니라 한 번 액세스한 블록에서 계속 연결을 시도 한다는 것. 5.PGA 버퍼와의 연결이 완료되면 다시 2.에서 만들어진 집합으ㅣ ROWID 에서 다음 블록을 찾아 EMP 테이블의 블록을 액세스하는 3. 작업을 수행한다. 6.PGA 버퍼에 새로운 블록이 등장했으므로 다시 4.의 작업을 수행. 7. 앞서 1.에서 수행한 'DEPT_LOC_IDX'인덱스에서 두 번째 로우를 액세스 하여 위의 2~6까지으ㅣ 작업을 반복 8. 'DEPT_LOC_IDX'인덱스의 처리 범위가 끝나면 쿼리 종료.
3.2.2.2. 정렬 병합 조인(Sort Merge Join)
고전적인 형태 이며 NL 조인의 문제를 해결하기 위한 일종의 대안으로 나타 났다. 가장 큰 특징은 연결을 위해 랜덤 액세스를 하지 않고 스캔을 하면서 이를 수행 한다는것. 연결작업은 효과적으로 수행하게 디었지만 먼저 정렬을 해야한다는 부담이 있음. 정렬은 메모리에서 수행되기 때문에 정렬을 위한 영역(Sort Area Size)에 따라 효율은 큰 차이가 난다. 어느 한계를 넘으면 NL 보다 더 나쁜 수행속도를 가져오 수 있다. 또한 연결고리의 비교연산자가 '=' 이 아닌경우 (LIKE,BETWEEN,>,>=,<,<=) 일때 NL 조인보다 유리한 경우가 많다. NL과 달리 선행 집합의 개념이 없이 독립적인 처리를 한다. 이 형태로 조인의 방식을 유도 하려면 'USE_MERGE(table1,table2)' 힌트를 사용 한다.
3.2.2.3. 해쉬 조인(Hash Join)
해슁함수 기법을 홀용하여 조인을 수행하는 방식 해쉬함수란 컬럼의 값을 받아서 이함수를 경유하면 로우의 저장위치를 리턴 하는 것. (이것을 활용하는 것이 해쉬클러스터라는 것을 우린 알고 있다.?) NL의 랜덤액세스로 연결하는 큰 부하 때문에 그 대안으로 Sort Merge 조인을 선택하였으나 정렬에 대한 부담은 데이터 양이 늘어날수록 크게 증가 하기 때문에 대용량에선 해결책이 될 수 없다. 해쉬 조인은 이러한 경우의 해결대안 으로 등장. 정렬을 하지않으면서 연결할 대상을 주변에 위치시킬 수 있는 방법은 바로 해쉬 함수를 활용하는 것이다. 그러나 해쉬 함수는 직접적인 연결을 담당하는 것이 아니라 연결될 대상을 특정 지역에 모아 두는 역할만을 담당한다. 이렇게 동일한 해쉬값을 가진 데이터들을 모아둔 공간을파티션(Partition)이라고 하고 이들중 서로 조인해야 할 것들을 연결하게 되는데 이것을파티션 짝(Pair)라고 한다. 실제 연결작업은 이 짝들을 대상으로 일어난다. 이 파티션중 작은 파티션을 메모리 내에 임시적인 해쉬 테이블로 만든다. 큰 파티션의 로우들이 외측 루프가 되고 해쉬 테이블 액세스는 내측 루프가 되어 조인을 수행한다.
해쉬함수는 실제 연결이 수행될 때는 마치 랜덤 액세스가 발생하는 것처럼 보이지만 이미 연결대상 짝들만 모아둔 상태이고 메모리 내에서 해쉬 액세스를 하기 때문에 일반적인 정렬에 비해 훨씬 유리하다. 또한 한건의 연결을 위해 하나의 블록이 액세스 될수도 있는 NL 과는 근본적으로 다르다.
만약 조인의 한쪽이 해쉬영역 보다 작아서 In-Memory 해쉬조인이 가능 하게 되면 수행속도는 매우 빨라진다. 그리하여 최근의 옵티마이져는 많은 조인을 해쉬 조인으로 선택하려는 경향이 있다.
해쉬 조인은 연산자에 대한 제약이 있다. 해쉬 조인은 동치조인(Equijoin) 일때만 가능하다. 대량 범위에 해단 조인이나 테이블이 너무 많은 조각으로 산재 되어 있을때 특히 유리하다. 또한 인덱스를 가질 수 없는 가공된 집합과의 조인 에서도 매우 효과적이다.
해쉬 조인으로 유도하기 위해서는 'USE_HASH(table1,table2)' 힌트를 사용 한다.
3.2.2.4. 세미조인(Semi Join)
- 이 책에서 말하는 세미조인은 다양한 비교연산자에 의해 사용된 서브쿼리가 메인쿼리와 연결되는 모든 경우를 뜻하는 넓은 의미의 세미조인을 의미한다. - 우리가 여러형태의 서브쿼리를 사용했더라도, 결국은 메인쿼리와 서브쿼리의 집합을 연결해야 하는 일종의 조인인 것이다. - 조인은 조인되는 집합간에는 어느 것이 먼저 수행되느냐 상관없이 논리적으로는 수평적 관계이다. 하지만, 서브쿼리는 메인쿼리와 종속적 관계이다. 이는 서브쿼리의 집합이 메인쿼리의 집합을 변형시켜서는 안된다는 것을 의미한다. - M*1이 M이듯이, 메인쿼리가 변형없이 조인하기 위해서는 서브쿼리의 집합은 항상 '1'집합이 되도록 하는 것이다. - 그러므로 서브쿼리가 '1'이라는 것이 증명되면 옵티마이저는 조인과 완전히 동일한 실행계획을 수립한다. - 서브쿼리가 'M'집합인 경우, 옵티마이저는 'M'집합인 채로 조인을 시도하지 않도록 하기위해. 중간처리를 하게된다. 이때 수행되는 순서나 조인방식에 따라 처리하는 방법이 다르다.
- 서브쿼리가 먼저 수행되어 메인쿼리에 결과를 제공하는 Nested Loops 방식이라면 서브쿼리는 먼저 수행되머 'SORT(UNIQUE)'처리를 하여 메인쿼리에 '1'집합을 조인하게 한다. 만약 Sort Merge 조인이난 해쉬 조인으로 수행된다면 서브쿼리는 언제나 이런처리를 해야한다. - 'M'집합을 가진 서브쿼리가 나중에 수행되는 Nested Loops로 수행된다면 메인쿼리가 외측 루프가 되고, 서브쿼리가 내측 루프가 되어서, 첫번째 연결에 성공하면 해당 내측 루프를 즉시 종료하는 방법이다. 이러한 처리를 실행계획에서는 '필터(Filter)형 처리'라고 부른다.
- IN을 사용한 서브쿼리라고 해서 항상 먼저 수행되는 것은 아니지만, EXISTS를 사용한 경우가 Nested Loops형으로 수행된다면 언제나 나중에 수행된다.
- 특별히 Sort Merge나 해쉬조인형식으로 유도하기 위해서, 10g부터는 일반적 조인과 같이 서브쿼리에 'USE_MERGE'나 'USE_HASH'힌트를 사용하도록 변경되었다.
[정리] - 비교연산자에 의해 사용된 서브쿼리도 넓은 의미로는 조인이나 마찬가지다. - 단, 조인은 조인되는 집합간에 어느집합이 먼저 수행되느냐가 상관없지만, 서브쿼리는 메인쿼리에 종속관계이므로, 조인형태를 취하더라도, 메인집합에 변형을 가해서는 안된다. M*1은 M이므로, 서브쿼리는 항상 '1'집합 모양이 되어야, 변형이 안되는것이다. => '1'집합이라는게 증명되면, 옵티마이저는 조인과 동일한 실행계획을 수립한다. - 서브쿼리가 'M'집합이면, 옵티마이저는 메인집합에 변형을 가하지 않게 하기 위해 중간처리를 한다. - 서브쿼리가 먼저 수행된다면, 'SORT(UNIQUE)'처리를 하여 메인쿼리에 '1'집합과 조인하게 해주고, 서브쿼리가 나중에 수행된다면, 메인쿼리가 바깥쪽의 Loop가 되어서, 안쪽 Loop인 서브쿼리와 첫번째 연결에 성공하면 바로 안쪽 Loop를 빠져나오는 방식으로 처리하는 것이다. => 이를 'Filter 처리'라 한다. - 특별히, Sort Merge나 해쉬조인으로 유도하기 위해서, 10g부터는 서브쿼리에 일반적인 조인과 같이 'USE_MERGE'나 'USE_HASH'힌트를 사용하면된다.
3.2.2.5. 카티젼 조인(Cartesian Join)
- 카티젼조인은 조인되는 두 개의 집합간에 연결고리가 되는 조건이 전혀 없는 경우를 말한다. 넓은 의미에서 보면 M:M 조인을 말하기도 한다. - 실제로는 실행계획에서 'CARTESIAN'으로 나오는 조인은 Sort Merge조인뿐이며, 다른조인은 정상적인 조인과 동일하게 실행계획이 나타난다. - 카티젼 조인이 발생하는 경우는 특별한 목적으로 고의적으로 만드는 경우와, 3개 이상의 집합을 조인할 때 조인순서의 잘못으로 연결고리가 빠져서 발생하는 경우가 있다.
3.2.2.6. 아우터 조인(Outer Join)
- 아우터 조인은 어떤 대상집합을 기준으로 거기에 아우터 조인되어 있는 집합에 대응되는 Row가 없더라도 기준집합의 모든 Row를 리턴하는 조인이다,
Nested Loops 아우터 조인 - 이 조인은 반드시 기준이 되는 집합이 바깥쪽 루프로써 먼저 수행되어야 하기때문에, 목적없이 함부로 아우터 조인을 시키면 조인의 방향이 고정되므로 주의한다. - 이 방식으로 유도하는 힌트는 기존 'USE_NL(table1, table2)'을 사용하며 실행계획은 동일하나 'NESTED LOOPS(OUTER)'로 표현되는 것이 다르다.
Hash 아우터 조인 - Nested Loops조인으로는 부담이 되는 대량의 데이터이거나 인덱스 등의 문제로 NL조인으로 수행에 문제가 있을때 선택될 수있다. - 이 방식에서도 기준집합은 무조건 빌드입력(Build Input)을 담당하고, 내측 조인집합이 해쉬테이블로 생성되어 연결작업이 수행된다. - 해쉬조인에서는 역할이 고정되드라도 NL조인에서와 같은 부담은 크게 발생하지 않는다.(2.3.4 해쉬조인참고) - 조인뷰나 조인 인라인뷰와 아우터 조인을 수행한다면 일반적인 조인에서와 같이 뷰의 병합이나 조건절 진입이 허용되지 않고, 뷰의 전체집합이 독립적으로 수행된 결과와 아우터 조인을 수행한다. 그러므로, 경우에 따라서는 뷰내로 조건들이 파고 들어갈 수 없으므로 심각한 부하를 발생시킬 수 있다. - 내측테이블의 특정조건을 만족하는 집합과 아우터 조인을 시도해야 한다면, 조건을 담은 집합을 인라인뷰로 만든 후 아우터 조인을 하면 해당 조건을 만족 할수 있다. - 인라인뷰가 단일테이블로 구성된 경우에는 조건절 진입(Pushing Predicates)가 가능하므로, 처리범위를 줄일 수 있다. - 하지만, 조인된 인라인 뷰는 조건절 진입이 불가능하게 되므로, 인라인뷰 내에서 충분히 처리범위를 줄일 수 없다면, 아우터 조인으로 인해 심각한 수행속도 저하를 가져올수 있다.
Sort Merge 아우터 조인 - Nested Loops조인으로는 부담이 되는 대량의 데이터이거나 인덱스 등의 문제로 NL조인으로 수행에 문제가 있을때 선택될 수있다. - 또한, 조건 연산자로 인해 해쉬조인이 불가능 할때이거나, 이미 다른 처리에 의해 조인을 위한 정렬이 선행되어 있어서 더 유지해질때 적용된다.
전체 아우터 조인 - 양쪽 집합이 모두 기준집합이면서 대응집합이 되는 아우터 조인이다. - 먼저 한 쪽을 기준으로 아우터조인을 수행한 결과와 다른 쪽을 기준으로 부정형 조인을 한 결과를 결합해서 리턴하는 방법이다. - 이러한 경우는 설계상의 문제나, 데이터의 일관성에 대한 문제가 원인이 되는 경우가 많다. - 이러한 경우 다양한 대안들이 있으며, 예를 들어 UNION ALL로 합집합을 만들고 GROUP BY를 이용하여 소소공배수 집합을 만드는 방법 또는 인라인뷰, 사용자 지정 저항형 함수 등을 활용하는 방법이 있다.
3.2.2.7. 인덱스 조인
- 인덱스 조인이란 어떤 쿼리에서 특정 테이블에서 사용된 모든 컬럼이 하나 이상의 인덱스들에 존재할 때, 인덱스들을 결합하여 테이블을 엑세스하지 않고 인덱스들로만 처리하는 방법이다. - 인덱스를 경유해서 다른 인덱스를 액세스하는 것은 불가능 하므로, 인덱스 병합은 머지나 해쉬 조인으로만 가능하다. - 인덱스를 조인한다고 해서 항상 유리한 것은 아니므로, 함부로 유도하지 말하야 한다. - 자주 같이 액세스되며, 특정 컬럼 때문에 테이블을 액세스해야 하는 경우가 많이 발생한다면 인덱스 조인을 활용할수있다.
단, 조인은 조인되는 집합간에 어느집합이 먼저 수행되느냐가 상관없지만, 서브쿼리는 메인쿼리에 종속관계이므로, 조인형태를 취하더라도, 메인집합에 변형을 가해서는 안된다.
M*1은 M이므로, 서브쿼리는 항상 '1'집합 모양이 되어야, 변형이 안되는것이다. => '1'집합이라는게 증명되면, 옵티마이저는 조인과 동일한 실행계획을 수립한다.
서브쿼리가 'M'집합이면, 옵티마이저는 메인집합에 변형을 가하지 않게 하기 위해 중간처리를 한다.
서브쿼리가 먼저 수행된다면, 'SORT(UNIQUE)'처리를 하여 메인쿼리에 '1'집합과 조인하게 해주고, 서브쿼리가 나중에 수행된다면, 메인쿼리가 바깥쪽의 Loop가 되어서, 안쪽 Loop인 서브쿼리와 첫번째 연결에 성공하면 바로 안쪽 Loop를 빠져나오는 방식으로 처리하는 것이다.
/* Nested Loops Semi Join */
SELECT *
FROM emp
WHERE deptno
IN (SELECT deptno
FROM t_emp
WHERE sal > 2000
);
- 'IN'을 사용한 경우는 실행계획에 큰 영향을 미칠 수 있다. - 'BETWEEN'은 선분을 의미하지만, 'IN'은 여러개의 점을 의미한다. - 선분 개념은 Range Scan을 하게 되지만, 점의 개념은 '='을 사용할 수 있다. - (예) 실행계획 'INLIST ITERATOR'아래에 있는 처리를 IN조건에 나열된 각각의 비교값만큼 반복수행한다. IN대신 OR을 사용해도 동일한데, 그 이유는 옵티마이저가 'IN'연산을 'OR'형태로 변경시킨 후 실행계획을 수립하기 때문이다. - 'INDEX(table_alias index_name)힌트를 적용하여 IN조건을 사용한 컬럼이 속한 인덱스를 사용하도록 할수 있으나, 적용여부는 옵티마이저가 결정한다.
3.2.3.2. 연쇄(Concatenation) 실행계획
-'OR'로 연결된 서로 다른 컬럼을 사용한 조건을 별도의 실행단위로 분리하여 가각의 최적의 액세스 경로를 수립하여 이를 연결하는 실행계획 - 항상 이런 실행계획이 나타나는 것은 아니며, 'OR'조건이 처리주관 조건의 역할을 하는 경우에만 그렇게 실행되며, 아닌경우 단순히 체크조건으로만 사용된다. - 'USE_CONCAT'으로 유도가능, 'NO_EXPAND'적용 불가능하게 할 수 있음. - 'OR'조건은 주어진 상황에 따라사 연결 실행계획이 유리할 수도, 불리할 수도 있으므로 가능하다면 실행계획을 확인해보고, 함부로 힌트를 적용하지 않는것이 바람직 하다. * 이런경우는 적용하지 않는 것이 바람직* - 조인의 연결고리가 'OR"조건을 가질 때 조인의 상대방이 넓은 처리범위를 가질 때 - 동일 컬럼의 'OR'조건: 이때는 IN-LIst탐침이 유리하다. - 보다 효율적으로 처리범위를 줄일 수 있는 다른 얙세스 경로가 있을때 - 'OR'조건들 중에서 너무 넓은 처리범위를 가진 것들이 존재할 때
3.2.3.3. 원격(Remote) 실행계획
- 다른 데이터베이서 테이블을 DC Link로 엑세스하는 형태. - 원격 테이블의 상세한 실행계획은 나타나지 않으므로, 어떤 방법으로 액세스했는지 알 수 없다. - 원격테이블이 실행계획에 부담이 많은 내측루프에서 수행되면 큰 부담이 된다. 원격테이블 액세스는 랜덤이 아닌 범위처리에는 부담이 적으므로 이와 같은 경우에는 Sort Merge나 해쉬조인으로 나타나는 경우가 많음. - 논리적으로 가장 이상적인 방법은, 먼저 원격에서 두 테이블을 조인하고 그 결과를 로컬에서 받는 방법이다. 이렇게 처리되도록 하려면, 원격에 미리 조인된 뷰를 만드는 방법이 있다. - 항상 실행계획을 확인하는 습관을 가질것.
3.2.3.4. 정렬처리(Sort Operation)실행계획
- 엑세스된 데이터는 사용자의 요구를 충족시키기 위해 다양한 가공을 하게 되며, 이러한 가공에는 상당부분 정렬이 필요한 경우가 많다. - 정렬형태별로 처리방법에 차이가 있으며, 발생하는 부하의 정도도 다르다.
SORT(UNIQUE) - DISTINCT함수를 사용했을 때 - 서브쿼리에서 제공자 역할을 할때 SORT(AGGREGATE) GROUP BY를 하지 않은 상태에서 전체 대상에 대해 그룹함수로 계산할 때 SORT(GROUP BY) GROUP BY를 사용하여 여러개의 다른 그룹으로 집결을 수행할 때 발생되며, 그룹의 개수가 많을수록 부담이 커진다. 이러한 문제를 해결하기위해, GROUP BY를 해쉬를 이용해서 처리할 수도 있다. SORT(GROUP BY NOSORT)
3.2.3.5. 집합처리(Set Operation)실행계획
합집합(Union, Union All) 실행계획 UNION은 최소 공배수 집합을 구해야 하므로, 추가로 SORT(UNIQUE)를 수행해야 하고, UNION ALL은 그런 처리가 필요하지 않다.
교집합(Intersection) 실행계획 교집합은 양쪽집합 모두에 속하는 공통집합의 의미한다. 이를 구하기 위해서는 먼저 각각의 집합에서 유일한 집합을 구해야 하낟. 마치 Sort Merge조인과 유사한 방법을 사용하여 양쪽집합을 유일하게 정렬한 다음 머지한다.
차집합(Minus) 실행계획 이 작업은 머지작업과 유사한 형태로 처리되며, 어느 한쪽을 기준으로 다른 쪽에 존재하면 제거하는 형식으로 선별하는 처리가 될 것이다. 집합연산에서 만약 SELECT-List의 컬럼이 그 테이블의 기본키였다면 논리적으로 유일성이 보장되지만 그래도 SORT(UNIQUE)은 발생된다. 물론 WHERE절에 부여된 조건이 기본키를 =로 엑세스한 경우는 SORT(UNIQUE)이 발생되지 않는다.
3.2.3.6. COUNT(STOPKEY)실행계획
- 쿼리의 조건절에 ROWNUM을 사용했을 때 나타난다. - 이 컬럼은 가상컬럼으로 ROWNUM을 잘 활용하면 내부적인 진행단계를 제어할 수도 있다.
적용하지 않는 것이 바람직한 경우 - 조인의 연결고리가 'OR"조건을 가질 때 조인의 상대방이 넓은 처리범위를 가질 때 - 동일 컬럼의 'OR'조건 : INLIST ITERATOR가 유리 - 보다 효율적으로 처리범위를 줄일 수 있는 다른 액세스 경로가 있을때 - 'OR'조건들 중에서 너무 넓은 처리범위를 가진 것들이 존재할 때
3.2.3.3. 원격 실행계획 - REMOTE
다른 데이터베이서 테이블을 DB Link로 엑세스하는 형태.
원격 테이블의 상세한 실행계획은 나타나지 않으므로, 어떤 방법으로 액세스했는지 알 수 없다.
원격테이블이 실행계획에 부담이 많은 내측루프에서 수행되면 큰 부담이 된다.
원격테이블 액세스는 랜덤이 아닌 범위처리에는 부담이 적으므로 이와 같은 경우에는 Sort Merge나 해쉬조인으로 나타나는 경우가 많음.
논리적으로 가장 이상적인 방법은, 먼저 원격에서 두 테이블을 조인하고 그 결과를 로컬에서 받는 방법이다. 이렇게 처리되도록 하려면, 원격에 미리 조인된 뷰를 만든다.
차집합(Minus) 실행계획 - 머지작업과 유사한 형태로 처리되며, 어느 한쪽을 기준으로 다른 쪽에 존재하면 제거하는 형식으로 처리된다. - SELECT절에 나열된 컬럼이 테이블의 기본키였다면 논리적으로 유일성이 보장되나, SORT(UNIQUE)은 발생된다. - WHERE절에 부여된 조건이 기본키를 =로 엑세스한 경우는 SORT(UNIQUE)이 발생되지 않는다.
SELECT empno, ename
FROM emp WHERE mgr = 7698
MINUS
SELECT empno, ename
FROM emp WHERE deptno = 10;
* 옵티마이저에 대한 충분한 이해 → SQL 에 대한 최적의 실행계획 판단 → 최적의 실행계획 유도
* 옵티마이저를 적절한 방법으로 제어 (힌트 사용 / SQL 수정)
-- http://download.oracle.com/docs/cd/E11882_01/server.112/e16638/hintsref.htm#PFGRF94937
-- V$SQL_HINT : 263 개 힌트가 정의되어 있음 (11.2.0.1.0 기준)
* 힌트 목적
* 과거 : 옵티마이저의 실수를 보완
* 현재 : 옵티마이저에 조언 (옵티마이저가 모르는 정보의 보완, 사용자의 목적)
* 힌트 특성
* 힌트는 "훈수" 다 - 옵티마이저 마음대로 에러 없이 무시
* 자습적(Heuristic) 기법에 의한 초기치 선택(Cutoff)을 하는 전략
* 버전 증가에 따른 힌트 추가/삭제 숙지 필요 - 힌트 변경 유형에 따른 액세스가 있다는 의미
* 힌트 이슈
* 10% 이상의 쿼리에 힌트가 적용 되었다면 원인 파악 필요
* 불필요한 힌트는 인덱스 구성 변경이 어려워지고, 옵티마이저에 의한 더 좋은 실행계획 선택을 막는다
데모 준비
데모 준비
T1, T2, T3... 초기화
-- T1 / LOCAL DB
DROP TABLE T1;
CREATE TABLE T1 AS
SELECT LEVEL-1 AS N1, TRUNC((LEVEL-1)/10) AS N2, TRUNC((LEVEL-1)/100) AS N3, TRUNC((LEVEL-1)/1000) AS N4 FROM DUAL CONNECT BY LEVEL <= 10000;
ALTER TABLE T1 MODIFY N2 NUMBER NOT NULL;
ALTER TABLE T1 MODIFY N3 NUMBER NOT NULL;
ALTER TABLE T1 MODIFY N4 NUMBER NOT NULL;
ALTER TABLE T1 ADD CONSTRAINT T1PK PRIMARY KEY (N1);
CREATE INDEX T1N2 ON T1 (N2);
CREATE INDEX T1N3 ON T1 (N3);
CREATE INDEX T1N4 ON T1 (N4);
CREATE INDEX T1N4N3 ON T1 (N4, N3);
EXEC DBMS_STATS.GATHER_TABLE_STATS (OWNNAME => 'UADMIN', TABNAME => 'T1' , DEGREE => 2);
EXEC DBMS_STATS.GATHER_INDEX_STATS (OWNNAME => 'UADMIN', INDNAME => 'T1PK', DEGREE => 2);
EXEC DBMS_STATS.GATHER_INDEX_STATS (OWNNAME => 'UADMIN', INDNAME => 'T1N2', DEGREE => 2);
EXEC DBMS_STATS.GATHER_INDEX_STATS (OWNNAME => 'UADMIN', INDNAME => 'T1N3', DEGREE => 2);
EXEC DBMS_STATS.GATHER_INDEX_STATS (OWNNAME => 'UADMIN', INDNAME => 'T1N4', DEGREE => 2);
EXEC DBMS_STATS.GATHER_INDEX_STATS (OWNNAME => 'UADMIN', INDNAME => 'T1N4N3', DEGREE => 2);
-- T2 / LOCAL DB
DROP TABLE T2;
CREATE TABLE T2 AS SELECT * FROM T1;
ALTER TABLE T2 MODIFY N1 NUMBER NOT NULL;
ALTER TABLE T2 MODIFY N2 NUMBER NOT NULL;
ALTER TABLE T2 MODIFY N3 NUMBER NOT NULL;
ALTER TABLE T2 MODIFY N4 NUMBER NOT NULL;
ALTER TABLE T2 ADD CONSTRAINT T2PK PRIMARY KEY (N1);
CREATE INDEX T2N2 ON T2 (N2);
CREATE INDEX T2N3 ON T2 (N3);
CREATE INDEX T2N4 ON T2 (N4);
CREATE INDEX T2N4N3 ON T2 (N4, N3);
EXEC DBMS_STATS.GATHER_TABLE_STATS (OWNNAME => 'UADMIN', TABNAME => 'T2' , DEGREE => 2);
EXEC DBMS_STATS.GATHER_INDEX_STATS (OWNNAME => 'UADMIN', INDNAME => 'T2PK', DEGREE => 2);
EXEC DBMS_STATS.GATHER_INDEX_STATS (OWNNAME => 'UADMIN', INDNAME => 'T2N2', DEGREE => 2);
EXEC DBMS_STATS.GATHER_INDEX_STATS (OWNNAME => 'UADMIN', INDNAME => 'T2N3', DEGREE => 2);
EXEC DBMS_STATS.GATHER_INDEX_STATS (OWNNAME => 'UADMIN', INDNAME => 'T2N4', DEGREE => 2);
EXEC DBMS_STATS.GATHER_INDEX_STATS (OWNNAME => 'UADMIN', INDNAME => 'T2N4N3', DEGREE => 2);
-- T3 / REMOTE DB
DROP TABLE T3;
CREATE TABLE T3 AS SELECT * FROM T1@TESTDB;
-- T4 / LOCAL DB
DROP CLUSTER H1 INCLUDING TABLES;
CREATE CLUSTER H1 (N4 NUMBER) HASHKEYS 10;
CREATE TABLE T4 CLUSTER H1 (N4) AS SELECT * FROM T1;
-- T5 / LOCAL DB
CREATE CLUSTER H2 (N4 NUMBER);
CREATE INDEX H2N4 ON CLUSTER H2;
CREATE TABLE T5 CLUSTER H2 (N4) AS SELECT * FROM T1;
CREATE TABLE T6 CLUSTER H2 (N4) AS SELECT * FROM T1;
-- M1 / LOCAL DB
CREATE MATERIALIZED VIEW M1 ENABLE QUERY REWRITE AS
SELECT N4, COUNT(*) AS CNT FROM T1 GROUP BY N4;
-- PT1, PT2 / LOCAL DB
CREATE TABLE PT1 PARTITION BY LIST (N4)
(
PARTITION PT1_0 VALUES (0),
PARTITION PT1_1 VALUES (1),
PARTITION PT1_2 VALUES (2),
PARTITION PT1_3 VALUES (3),
PARTITION PT1_4 VALUES (4),
PARTITION PT1_5 VALUES (5),
PARTITION PT1_6 VALUES (6),
PARTITION PT1_7 VALUES (7),
PARTITION PT1_8 VALUES (8),
PARTITION PT1_9 VALUES (9)
)
AS SELECT * FROM T1;
CREATE TABLE PT2 PARTITION BY LIST (N4)
(
PARTITION PT2_0 VALUES (0),
PARTITION PT2_1 VALUES (1),
PARTITION PT2_2 VALUES (2),
PARTITION PT2_3 VALUES (3),
PARTITION PT2_4 VALUES (4),
PARTITION PT2_5 VALUES (5),
PARTITION PT2_6 VALUES (6),
PARTITION PT2_7 VALUES (7),
PARTITION PT2_8 VALUES (8),
PARTITION PT2_9 VALUES (9)
)
AS SELECT * FROM T1;
ALTER SYSTEM FLUSH SHARED_POOL;
ALTER SESSION SET CURSOR_SHARING = SIMILAR;
SELECT SQL_ID, EXECUTIONS, SQL_TEXT FROM V$SQL WHERE SQL_TEXT LIKE 'SELECT% FROM T1 %' AND SQL_TEXT NOT LIKE '%V$SQL%';
SQL_ID EXECUTIONS SQL_TEXT
------------- ---------- -------------------------------------------------------
2u436jdqsrw4d 2 SELECT COUNT(*) FROM T1 WHERE N1 = :"SYS_B_0"
SELECT SQL_ID, EXECUTIONS, SQL_TEXT FROM V$SQL WHERE SQL_TEXT LIKE 'SELECT% FROM T1 %' AND SQL_TEXT NOT LIKE '%V$SQL%';
SQL_ID EXECUTIONS SQL_TEXT
------------- ---------- -------------------------------------------------------
2zdn2wy2dnad6 1 SELECT /*+ CURSOR_SHARING_EXACT */ COUNT(*) FROM T1 WHERE N1 = 1
gtxtp70k5yrrt 1 SELECT /*+ CURSOR_SHARING_EXACT */ COUNT(*) FROM T1 WHERE N1 = 2
2u436jdqsrw4d 2 SELECT COUNT(*) FROM T1 WHERE N1 = :"SYS_B_0"
SELECT /*+ DYNAMIC_SAMPLING(T1 10) */ COUNT(*) FROM T1;
-- 통계 정보를 사용할 수 없을때 사용
-- http://ukja.tistory.com/112
RESULT_CACHE
NO_RESULT_CACHE
ALTER SYSTEM FLUSH SHARED_POOL;
SELECT /*+ RESULT_CACHE */ T1.N4, COUNT(*)
FROM T1, T2
WHERE T1.N4 = T2.N4
GROUP BY T1.N4;
Statistics
----------------------------------------------------------
1167 recursive calls
0 db block gets
266 consistent gets
0 physical reads
0 redo size
14 sorts (memory)
0 sorts (disk)
10 rows processed
SELECT /*+ RESULT_CACHE */ T1.N4, COUNT(*)
FROM T1, T2
WHERE T1.N4 = T2.N4
GROUP BY T1.N4;
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
0 consistent gets
0 physical reads
0 redo size
0 sorts (memory)
0 sorts (disk)
10 rows processed
ALTER SYSTEM FLUSH SHARED_POOL;
SELECT /*+ NO_RESULT_CACHE */ T1.N4, COUNT(*)
FROM T1, T2
WHERE T1.N4 = T2.N4
GROUP BY T1.N4;
Statistics
----------------------------------------------------------
1167 recursive calls
0 db block gets
358 consistent gets
0 physical reads
0 redo size
14 sorts (memory)
0 sorts (disk)
10 rows processed
SELECT /*+ NO_RESULT_CACHE */ T1.N4, COUNT(*)
FROM T1, T2
WHERE T1.N4 = T2.N4
GROUP BY T1.N4;
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
92 consistent gets
0 physical reads
0 redo size
0 sorts (memory)
0 sorts (disk)
10 rows processed
STATEMENT_QUEUING
NO_STATEMENT_QUEUING
SELECT /*+ PARALLEL STATEMENT_QUEUING */ T1.N4, COUNT(*)
FROM T1 FULL OUTER JOIN T2
ON T1.N4 = T2.N4
GROUP BY T1.N4;
Note
-----
- automatic DOP: Computed Degree of Parallelism is 2
-- 가용한 PARALLEL 프로세스가 존재할 때 까지 대기
-- FIFO
SELECT /*+ PARALLEL NO_STATEMENT_QUEUING */ T1.N4, COUNT(*)
FROM T1 FULL OUTER JOIN T2
ON T1.N4 = T2.N4
GROUP BY T1.N4;
Note
-----
- automatic DOP: Computed Degree of Parallelism is 2
- statement not queuable: no-queuing hint specified
INSERT INTO T1 (N1, N2, N3, N4)
SELECT 0, 0, 0, 0 FROM DUAL UNION ALL
SELECT -1, -1, -1, -1 FROM DUAL;
ERROR at line 1:
ORA-00001: unique constraint (UADMIN.T1PK) violated
INSERT /*+ CHANGE_DUPKEY_ERROR_INDEX(T1, T1PK) */ INTO T1 (N1, N2, N3, N4)
SELECT 0, 0, 0, 0 FROM DUAL UNION ALL
SELECT -1, -1, -1, -1 FROM DUAL;
ERROR at line 1:
ORA-38911: unique constraint (UADMIN.T1PK) violated
UPDATE T1 SET N1 = 0 WHERE N1 = 1;
ERROR at line 1:
ORA-00001: unique constraint (UADMIN.T1PK) violated
UPDATE /*+ CHANGE_DUPKEY_ERROR_INDEX(T1, T1PK) */ T1 SET N1 = 0 WHERE N1 = 1;
ERROR at line 1:
ORA-00001: unique constraint (UADMIN.T1PK) violated
-- PL/SQL 에서 DUP_VAL_ON_INDEX 와 EXCEPTION 을 분리 하고자 할때 사용?
IGNORE_ROW_ON_DUPKEY_INDEX
INSERT INTO T1 (N1, N2, N3, N4)
SELECT 0, 0, 0, 0 FROM DUAL UNION ALL
SELECT -1, -1, -1, -1 FROM DUAL;
ERROR at line 1:
ORA-00001: unique constraint (UADMIN.T1PK) violated
INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(T1, T1PK) */ INTO T1 (N1, N2, N3, N4)
SELECT 0, 0, 0, 0 FROM DUAL UNION ALL
SELECT -1, -1, -1, -1 FROM DUAL;
1 row created.
-- INSERT
SELECT /*+ ELIMINATE_JOIN(T2) */ T1.N1
FROM T1, T2
WHERE T1.N1 = T2.N1(+)
AND T1.N1 = 0;
--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 4 | 1 (0)| 00:00:01 |
|* 1 | INDEX UNIQUE SCAN| T1PK | 1 | 4 | 1 (0)| 00:00:01 |
--------------------------------------------------------------------------
ELIMINATE_OBY
NO_ELIMINATE_OBY
SELECT /*+ ELIMINATE_OBY(@QB) */ A.N4, SUM(A.N1) FROM (SELECT /*+ QB_NAME(QB) */ N4, N1 FROM T1 WHERE N2 = 0 ORDER BY N3) A GROUP BY A.N4;
-------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 7 | 77 | 2 (50)| 00:00:01 |
| 1 | HASH GROUP BY | | 7 | 77 | 2 (50)| 00:00:01 |
| 2 | TABLE ACCESS BY INDEX ROWID| T1 | 10 | 110 | 1 (0)| 00:00:01 |
|* 3 | INDEX RANGE SCAN | T1N2 | 10 | | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------
SELECT /*+ NO_ELIMINATE_OBY(@QB) */ A.N4, SUM(A.N1) FROM (SELECT /*+ QB_NAME(QB) */ N4, N1 FROM T1 WHERE N2 = 0 ORDER BY N3) A GROUP BY A.N4;
---------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 160 | 3 (67)| 00:00:01 |
| 1 | HASH GROUP BY | | 10 | 160 | 3 (67)| 00:00:01 |
| 2 | VIEW | | 10 | 160 | 2 (50)| 00:00:01 |
| 3 | SORT ORDER BY | | 10 | 140 | 2 (50)| 00:00:01 |
| 4 | TABLE ACCESS BY INDEX ROWID| T1 | 10 | 140 | 1 (0)| 00:00:01 |
|* 5 | INDEX RANGE SCAN | T1N2 | 10 | | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------------------
제4장. 인덱스 수립 전략
4.1 인덱스의 선정 기준
인덱스가 선정되면 득이 되는 경우와 반대인 경우가 있다.
역활이 중복될 경우 안 좋은 영향이 발생할 수 있다.
모든 엑세스 형태와 분석을 토대로 이상적인 컬럼 구성과 순서 결정을 통해 단위 인덱스의 역량을 강화시키고, 최소의 인덱스로 모든 엑세스 형태를 만족할 수 있도록 해야한다.
가능한 실측자료(엑세스 형태 수집, 분석, 엑세스의 빈도, 처리범위의 크기, 분포도, 테이블의 크기, 엑세스 유형등)를 활용하여 종합적으로 전략적인 결정을 해야한다.
4.1.1. 테이블 형태별 적용기준
적은 데이터를 가진 소형 테이블
주로 참조되는 역활을 하는 중대형 테이블
업무의 구체적인 행위를 관리하는 중대형 테이블
저장용 대형 테이블
가) 적은 데이터를 가진 소형 테이블
적은 데이터의 의미: 한번의 다중 블록 I/O Call로 모든 데이터를 읽을 수 있는 경우((DB_FILE_MULTIBLOCK_READ_COUNT의 값보다 적은 테이블)
한번의 다중 블록 I/O Call로 모든 내용을 읽을 수 있으므로, 인덱스를 통한 스캔과 거의 차이가 없음. 그럼에도 불구하고 생성해야 하는가?
인덱스 유무는 옵티마이저의 판단에 많은 영향을 미치는데 특히 조인과 인덱스는 옵티마이저의 결정에 큰 영향을 미침
인덱스는 가장 큰 영향요소 이므로 존재 여부에 따라 실행 계획에 큰 영향을 미칠 수 있음.
해당 유형의 인덱스는 Nested Loop Join시 내측루프를 주로 수행하므로 많은 횟수가 수행될 수 있음. 약간의 차이가 전체적으로 큰 영향을 미칠 수 있음.
권고 : 작은 테이블이라도 PK는 반드시 생성을 권고하며, PK에 의해 참조가 많이 되는 테이블의 경우는 조인 시 효율을 위해 IOT 생성 검토도 필요
나) 주로 참조되는 역활을 하는 중대형 테이블
의미: 트랜잭션 데이터들의 행위의 주체나 목적이 되는 개체들로 구성된 테이블들. 모델링에서 키 엔티티로 분류되는 집합임.
특징: 좁은 범위의 스캔 또는 조인등에 의해 내측 루프에서 기본키에 의해 연결됨 검색 조건의 형태가 뚜렷하고, 데이터 증감이 별로 없으며, 검색 위주의 엑세스 발생
권고
기존의 사용중인 인덱스 블록에 사용하는 비율이 낮으므로, 대량 입력 작업이나 오랜 시간이 지났다면 인덱스 Rebuild 권고
다) 업무의 구체적인 행위를 관리하는 중대형 테이블
의미: 업무의 구체적인 내용을 담고 있는 테이블.
특징
조인에서 주로 외측 루프를 담당하고 있음.
테이블이 크고, 지속적으로 증가하고 있으므로 많은 인덱스는 입력 시 부하가 가게 하며, 인덱스 구성이 약간만 잘못 되어도 처리 범위가 증가할 수 있음.
다양한 엑세스 조건으로 인덱스의 수가 많고 복합 인덱스가 많으며, 다양한 연산자가 존재하여 결합 인덱스의 순서 또한 중요
권고
인덱스 구성 전략에 따라서 시스템이 미치는 영향이 크므로 절차를 충실히 따라야 함. 이를 위해 테이블에 엑세스 하고 있는 모든 유형을 수집하고, 앞으로 예상되는 형태까지 감안하는 것이 필요
라) 저장용 대형 테이블
의미: 로그성 데이터를 관리할 목적으로 생성된 테이블
특징: 대량의 데이터를 가지고 있으며, 지속적으로 대량의 데이터가 입력됨.
권고
갱신이 발생하지 않기 때문에 낮은 PCTFREE 값 지정하는 것이 공간 사용에 유리
기본키를 가지는 것은 입력 시 부담이 될 수 있으므로 사용하지 않는 것도 고려. 대신 Unique Index 생성 고려
테이블에 파티션을 만들고 파티션 마다 필요한 인덱스(Local Index)를 생성하는 것이 좋음. 오래된 데이터의 경우 파티션 단위로 떼어내기 쉽고 입력이나 검색 시 부담이 적어짐
파티션 인덱스 사용 시, 사용 파티션의 인덱스만 사용하는 것도 고려(전체 Local Index를 UNUSABLE 상태로 만든 후, 이후 필요한 파티션의 인덱스만 생성하는 방식)
4.1.2. 분포도와 손익 분기점
인덱스 생성의 목적: 전체 집합에서 범위를 줄여 선별된 부분만 엑세스 하고자 함
분포도: 전체 집합에서 해당 컬럼으로 조회 시 해당 조건이 차지하는 비율을 의미.
손익 분기점: 인덱스는 단일 블록 I/O를 유발하므로 분포도가 높은 경우에는 인덱스가 존재한다 할지라도 사용하지 않을 수 있다. 손익 분기점은 이렇게 인덱스 사용 여부를 결정하는 분포도 값이다. 옵티마이저는 손익분기점 이상이면 인덱스를 사용하지 않고 이하면 사용한다.
인덱스의 분포도가 높으면 무조건 인덱스를 생성하지 않는 것이 유리한가?
반드시 그렇지 않다. 부분 범위 처리를 할 경우 실제 분포도는 아주 낮아질 수 있다.
4.1.3. 인덱스 머지와 결합 인덱스 비교
인덱스 머지(Index Merge): 여러 인덱스가 협력하여 같이 엑세스를 주관
장점: 처리 범위가 넓은 경우, 테이블에 엑세스 하여 Filter 하는 부분이 생략됨. 비교하는 조건으로 인덱스만 사용.
결합 인덱스: 여러 컬럼을 모아 하나의 인덱스로 만드는 방식.
장점: 모든 조건이 인덱스 컬럼에 사용되는 경우 인덱스 머지에 비해 아주 효율적으로 수행될 수 있다.(단, 선두 컬럼의 Equal 연산에서만 적용)
Index Merge 방식과 결합 인덱스 방식의 연산 차이
4.1.4. 결합 인덱스의 특징
인덱스의 첫 번째 컬럼이 조건절에 없다면 일반적으로 인덱스응 사용되지 않음(예외. Oracle 9i 이후 지원되는 Index Skip Scan의 경우는 가능)
Equal 연산이 아닌 검색 조건이 들어오는 경우(범위 연산), 처리 범위가 크게 증가하여 효율이 크게 저하될 수 있음
가) 분포도와 결합 순서의 상관 관계
ex) 구성
테이블: TAB1, 컬럼: COL1, COL2
컬럼 COL1: 분포도가 좋지 않음
컬럼 COL2: 분포도가 좋음
사용된 SQLSELECT * FROM TAB1WHERE col1='A'AND col2 between '113' and '115'
엑세스 방식에 따른 구분
분포도가 좋지 않은 COL1 컬럼을 복합인덱스의 선행 컬럼으로 설정한 경우 처리 과정(왼쪽 그림) 1. col1='A' AND col2=112 인 데이터를 B-Tree 방식으로 바로 찾음 2. ROWID를 이용하여 테이블 로우 엑세스 3. 다음 로우를 탐색하여 조건에 만족하면 다시 테이블의 로우를 엑세스하고 그렇지 않으면 스캔 종료(이 경우, 한 번의 랜덤, 2개의 인덱스 로우 스캔)
분포도가 좋은 COL2 컬럼을 복합인덱스의 선행 컬럼으로 설정한 경우 처리 과정(오른쪽 그림) 1. col2=112 AND col1='A' 인 데이터를 B-Tree 방식으로 바로 찾음 2. ROWID를 이용하여 테이블 로우 엑세스 3. 다음 로우를 탐색하여 조건에 만족하면 다시 테이블의 로우를 엑세스하고 그렇지 않으면 스캔 종료(이 경우, 한 번의 랜덤, 2개의 인덱스 로우 스캔)
결론: 인덱스의 모든 컬럼을 Equal 연산으로 처리한 경우 처리량에는 차이가 없음. 단지 Equal 연산만 사용했으므로 필요한 데이터만 읽을 수 있게 되었기 때문.
나) 이퀄(=)이 결합순서에 미치는 영향
실제 실무에서는 Equal 연산만으로 처리되지 않고, 다양한 범위 처리 연산(LIKE, BETWEEN, < > 등)이 사용됨 인덱스의 선행 컬럼에 대해 Equal 연산이냐 아니냐는 처리 범위에 크게 영향을 미침
ex) 구성
테이블: TAB1, 컬럼: COL1, COL2
컬럼 COL1: 분포도가 좋지 않음
컬럼 COL2: 분포도가 아주 좋음
사용된 SQLSELECT * FROM TAB1WHERE col1='A'AND col2 between '113' and '115'
엑세스 방식에 따른 구분
분포도가 좋지 않은 COL1 컬럼을 복합인덱스의 선행 컬럼으로 설정한 경우 처리 과정(왼쪽 그림) 1. col1='A' AND col2=113 인 첫번째 로우를 찾는다. 2. ROWID를 이용하여 테이블의 로우를 엑세스 한다. 3. 다음 로우를 차례로 스캔하면서 col1 !='A' OR col2 > 115 일 때 까지 테이블의 로우를 엑세스 하고 그렇지 않으면 처리를 종료한다. ◎ 이 경우, 첫 번째 컬럼이 Equal 연산이고 두 번째 컬럼에는 정렬이 되어 있으므로 between이 들어갔다 하더라도 필요한 엑세스만 하게 된다.
분포도가 좋은 COL2 컬럼을 복합인덱스의 선행 컬럼으로 설정한 경우 처리 과정(오른쪽 그림) 1. col2=113 AND col1='A' 인 첫번째 로우를 찾는다. 2. ROWID를 이용하여 테이블의 로우를 엑세스 한다. 3. col2 > 115 일 때 까지 계속해서 스캔한다. 스캔한 로우는 col1='A' 인지 체크하여 성공하면 ROWID를 이용하여 테이블을 엑세스한다. 4. col2 > 115 이면 처리를 종료한다. ◎ 이 경우, col2 에 대해서는 체크만 하는 이유는 이 컬럼에 대해서는 정렬이 되어있지 않기 때문. 체크로 인해 ROWID를 이용한 테이블 엑세스만 줄어들 뿐이다. 아래의 경우에서 col1 컬럼에 대해서만 인덱스가 있는 경우에서 차이를 볼 수 있다. 11번의 ROWID 스캔에서 3번의 ROWID 스캔으로만 줄고, 실제 인덱스 내부 처리 범위내에서는 효율을 보지 못했다.
결론: 인덱스의 처리 범위를 고려해야 하며 컬럼의 분포도를 고려한 인덱스 컬럼 선정 방식은 맞지 않다. 조회 시 사용되는 컬럼에 Equal를 많이 쓰는 경우 처리 범위를 크게 줄여주므로 해당 컬럼을 인덱스 선행 컬럼으로 두는 것이 올바르다.
다) IN 연산자를 이용한 징검다리 효과
적용 예 1. 비효율적인 인덱스가 이미 큰 테이블에 대해 사용하고 있는 경우 ( 위 예에서 col2, col1 순으로 생성된 결합 인덱스) 2. 올바르게 컬럼 순서로 생성된 인덱스의 쿼리 유형에 상반되는 컬럼 순서의 쿼리가 가끔 사용되는 경우 ( col1(=), col2(between) 순서로 들어오는 쿼리가 대부분이고 인덱스고 그 순서로 생성되어있는데, 가끔 col2(=), col1(between) 과 같은 쿼리도 수행되는 경우) ◎ 이 경우, 새롭게 인덱스를 생성하는 것은 인덱스의 중복 투자이므로 올바르지 않음. IN 연산자를 통해 기존 인덱스 만으로도 효율적인 결과를 가져올 수 있음
사용 예제
기존의 between 연산을 이용한 경우(왼쪽 그림) col2 컬럼 기준으로 넒은 범위 스캔을 한다. col1 컬럼의 필요한 컬럼인 'A'만을 뽑아내지 못한다. 이 경우, 범위에 해당되는 모든 인덱스를 읽으므로 선을 그은것과 같다 하여 '선(line)연산' 이라고도 한다.
IN 연산을 이용한 경우(오른쪽 그림) INLIST ITERATOR(111,112) 1. col2=:value AND col1='A' 인 첫번째 로우를 찾는다. 이 때, 인덱스 트리를 탐색하는 과정이 포함된다. 2. ROWID 탐색을 하고 테이블의 데이터를 뽑아낸다. 3. col2!=:value OR col1!='A' 일 때 까지 테이블의 로우를 엑세스하고 만족하지 않으면 다음의 Iterator 값을 뽑아내 1번 과정을 반복한다. 만약 값이 없으면 전체적인 탐색을 종료한다. ◎ 위 연산은 IN이 가지는 연산의 특징으로 col2 , col1 컬럼의 equal 연산을 여러번 수행하여 불필요한 인덱스 값을 읽는 것을 방지한다. IN 리스트 내부에 있는 데이터들로 인해 필요한 인덱스 값만 읽게 되는데, 해당 인덱스에 점을 찍은것과 같다하여 '점(point)연산' 이라고도 한다.
위 IN 리스트를 사용한 쿼리는 다음의 OR를 사용한 쿼리와 거의 동일한 수행방식을 가진다.
SELECT * FROM TAB1 WHERE (COL2=111 AND COL1='A') OR (COL2=112 AND COL1='A')
라) 처리 범위에 직접적인 영향을 주지 못하는 컬럼의 추가 기준
결합 인덱스를 사용하는 쿼리 내부에 equal 연산을 사용하지 않는 컬럼 이후로는 범위를 줄여주지 못함. (예로써, (A,B,C,D)로 구성된 인덱스에 A(=),B(between),C(=),D(=) 와 같이 사용했을지라도 B 연산에서 범위 연산자를 쓰게 됨에 따라 C,D 컬럼은 인덱스 스캔 범위를 줄여주지 못함) ex) 테이블 구성
col1: equal(=) 연산이 사용됨.
col2: 사용되지 않거나, 범위 연산자(like, between, < , > 등)이 사용됨.
col3: 다양한 처리 연산이 사용됨
사용 쿼리
SELECT * FROM TAB1WHERE A='2' AND C='61'
사용 예제
(A+B) 컬럼에 복합인덱스를 만든 경우(왼쪽 그림) 1. A 컬럼에 대해서는 인덱스가 존재하므로 A='2' 인 모든 노드를 탐색한다. 2. 그러나 C 컬럼에는 인덱스가 없으므로 ROWID를 통한 테이블 억세스으로 테이블 데이터에서 C 컬럼을 뽑아내 C='61' 것을 필터링 한다. 3. 테이블 내부에서 필터링해서 조건에 맞는 것만 출력한다. 이 과정은 인덱스 스캔이 종료될때 까지 계속 수행된다. ◎ 인덱스 탐색 결과 나온 5건에 대해 모두 ROWID를 통한 테이블 엑세스를 한다. 위 경우 총 5번의 테이블 스캔을 한다.
(A+B+C) 컬럼에 복합인덱스를 만든 경우(오른쪽 그림) 1. A 컬럼에 대해서는 인덱스가 존재하므로 A='2' 인 모든 노드를 탐색한다. 2. 앞선 B 컬럼의 검색 조건 부재로 C 컬럼이 인덱스 검색 범위를 줄여주진 못하나, 값은 존재하므로 C='61' 인 것을 필터하는 부분을 인덱스 내부에서 해결한다. 3. 인덱스 내부에서 필터링해서 조건에 맞는 것만 ROWID를 통한 테이블 억세스로 값을 뿌려준다. ◎ 인덱스 탐색 결과 나온 5건에 대해 필터를 통해 걸리진 1건에 대해서만 ROWID를 통한 테이블 스캔을 한다. 위 경우 총 1번의 테이블 스캔을 한다.
4.1.5. 결합 인덱스의 컬럼 순서 결정 기준
< 컬럼 순서를 결정하는 우선 순위 >
1단계: 항상 사용하는가?
2단계: 항상 '='로 사용하는가?
3단계: 어느 것이 더 좋은 분포도를 가지는가?
4단계: 자주 정렬되는 순서는 무엇인가?
5단계: 부가적으로 추가시킬 컬럼은 어떤 것으로 할 것인가?
[ 상황 가정 ] : C1, C2, C3, C4 컬럼들에 대해 복합인덱스 컬럼 순서를 어떻게 결정할 것인가? - 아래는 쿼리가 사용하고 있는 쿼리 조건의 목록임 1. C1(=), C2(BETWEEN), C3(=) 2. C2(=), C3(BETWEEN), C4(>) 3. C1(=), C2(=), C3(LIKE) 4. C1(=), C2(=), C4(>)
▶ 1단계: 항상 사용하는가? * 컬럼의 사용 횟수: C2(4회), C1(3회), C3(2회), C4(2회) - 추가 가정: C1 컬럼의 카디널러티가 낮아서 해당 컬럼이 없는 경우에 대해서는 Index Skip Scan 사용하도록 가정 두 번째 경우에서 C2의 경우에서 Equal 연산이 사용되므로 C1 케이스에서 +1 해서 총 4회 사용으로 가정 - 최종 후보: C1, C2 ▶ 2단계: 항상 '='로 사용하는가? * Equal 연산 사용 횟수: C1(3회), C2(3회) - C2 컬럼의 경우 첫 번째 경우에서 Between 연산을 사용하므로 - 결과: C1+C2
▶ 3단계: 어느 것이 더 좋은 분포도를 가지는가? * 분포도를 고려 및 사용 가능한 추가 연산 형태(예로써 Between을 IN으로 변환 가능한 경우) C1, C2 컬럼의 순서를 추가 검토 * 이후에 발생할 상황 추이 후 검토 - 엑세스 형태를 수집할 때 현재 사용하고 있는 인덱스 뿐만 아니라 앞으로 등장할 것으로 예측되는 엑세스 형태도 같이 추출 - 앞으로 어떤 컬럼이 Equal 연산 외에 다른 연산을 사용할 지 검토
▶ 4단계: 자주 정렬되는 순서는 무엇인가? * 정렬을 요구하는 구문의 사용 시, 정렬 순서 조사 후 검토. 정렬 시 앞단에 나오는 컬럼을 우선적으로 두는 것이 맞다.
▶ 5단계: 부가적으로 추가시킬 컬럼은 어떤 것으로 할 것인가? * C3, C4 컬럼의 경우 모든 조건들이 처리 범위를 줄여주는 형태는 아님. C3, C4간의 우선 순위 선정 필요 1. C1(=), C2(BETWEEN),C3(=) : C3이 C4 앞에 오는 것이 유리 2. C2(=),C3(BETWEEN), C4(>): 순서 관계 없음 3. C1(=), C2(=),C3(LIKE): C3이 C4앞에 오는 것이 유리 4. C1(=), C2(=),C4(>): C4가 C3 앞에 오는 것이 유리 - 위의 단순한 결과대로라면 (C1+C2+C3+C4)의 순으로 인덱스 생성하는 것이 좋음 - 그러나, 4 의 경우가 빈번하게 엑세스 되는 경우라면 (C1+C2+C4) 인덱스를 추가 생성해야할 필요도 있을 것임 - 이 이외에 C3, C4 컬럼의 분포도 또한 고려해서 전체적인 상황을 살펴 결정하는 것이 필요
4.1.6. 인덱스 선정 절차
가) 테이블의 엑세스 형태를 최대한 수집
▶ 개발 단계에서의 엑세스 형태 수집 1) 반복 수행되는 엑세스 형태를 찾는다. 반복 수행되는 엑세스는 자신의 수행속도에 반복횟수를 곱한 만큼의 부하를 가져오므로 수행속도에 미치는 영향이 아주 크다. - Nested Loop Join, Sub-Query, Fetch후 루프 내에서 반복되는 쿼리 2) 분포도가 아주 양호한 컬럼들을 발췌하여 엑세스 유형을 조사한다. 테이블에는 아주 양호한 분포도를 가진 컬럼이 있기 마련이다. - 인덱스 컬럼 순서를 정할 때등 여러 경우에서 크게 활용될수 있음 3) 자주 넒은 조건이 부여되는 경우를 찾는다. 인덱스는 넓은 범위를 처리하게 될 때 많은 부담을 주게 된다. - 전체 범위 처리 시, 넓은 범위 처리는 인덱스 사용 때 수행속도가 좋지 않을 수 있음. - 처리 범위의 최대 크기와 평균 예상 범위, 자주 사용되는 정렬의 순서, 처리 유형을 수집하면 유용한 자료가 될 것임 4) 조건에 자주 사용되는 주요 컬럼들을 추출하여 엑세스 유형을 조사한다. 빈번하게 사용된다는 것은 시스템 전반에 미치는 영향이 그 만큼 크기 때문에 아주 중요하다. 5) 자주 결합되어 사용되는 컬럼들의 조합 및 정렬되는 순서를 조사한다. 업무적인 측면에서 각 컬럼들간의 상호관계를 잘 파악해 보면 특정 컬럼들끼리 자주 조합하여 사용되는 경합형태를 찾을 수 있다. - 중심이 되는 컬럼을 찾아 이와 연관되는 컬럼들을 검토 6) 역순으로 정렬되어 있는 경우를 찾는다. End User는 조건의 범위는 넓게 부여하면서 최근의 데이터만 보려고 하는 경우가 많음. 빠른 응답까지 요구함 7) 통계 자료 추출을 위한 엑세스 유형을 조사한다. 통계자료를 추출하는 경우는 대개 범위가 넓다. - 넓은 범위 처리가 대부분 임. 클러스터나 잘 조합된 결합 인덱스 이용하는 것이 좋음.
▶ 운영 단계에서의 엑세스 형태 수집 * 시점: Test 단계 또는 정상 가동 중인 시스템의 인덱스 교정 시점 1) 어플리케이션 소스 코드에서 SQL을 추출하여 분석용 테이블에 보관한다. - 기능: 어플리케이션을 분석하여 SQL 문장을 Repository 에 저장. 손쉽게 SQL 문장을 가져올 수 있음. - 한계: 동적 SQL 과 같은 쿼리는 가져오는데 한계가 있으며, 수행 횟수와 같은 자세한 사항은 알 수 없음 2) SQL-Trace 파일을 파싱하여 SQL 문장 뿐만 아니라 실행계획, 현재 적용되고 있는 인덱스 , 실행 횟수, 처리 범위등의 매우 상세한 정보 획득할 수 있다. - 기능: 전체 커다란 trace 결과에 대해 하나의 상세한 레포트도 얻을 수 있으며 상용 SW에 따라 Advice 역할 까지 해주기도 함. - 한계: 시스템에 큰 부하를 줌. 그러므로 trace 기간은 제한 되어야 하며 그 기간내의 분석 밖에 행할수 없음 3) 공유 SQL 영역에서 직접 SQL을 찾아오는 방법은 보다 SQL 수집을 간편하게 한다. - 기능: 서버에 부담을 덜 주므로 장기간 수집하는 것이 가능하다. - 단점: trace에 비해 정보가 빈약하다. 어떤 어플리케이션에서 수행되었는지와 같은 장기간 수집을 하더라도 여전히 모든 SQL 구문 수집하는 것이 어렵다.
▶ 수집된 SQL을 테이블 별로 출력하여 엑세스 형태 기록 - 유사한 형태의 SQL들이 정렬되도록 출력 - SQL문장 별로 정렬하는 것 보다 조건절에 사용된 컬럼별로 정렬하는 것이 보다 유리 -
나) 인덱스 대상 컬럼의 선정 및 분포도 조사
인덱스 대상 컬럼 기준
엑세스 유형에 자주 등장하는 컬럼
인덱스의 앞부분에 지정해야 할 컬럼
기타 수행속도에 영향을 미칠 것으로 예상되는 컬럼
인덱스 대상 컬럼을 선정하고 분포도를 분석한 사례
<그림 1-4-9>
매출번호(SALENO): 3년동안 몇건이 발생할 것인지 예상한다.과거 발생자료를 이용한 통계자료 추출 혹은 현업담당자와 협의.
판매일자(SALEDATE): 앞서 예상한 월평균 발생건수를 토대로 함.혹은 3년중 휴일을 제외한 날짜를 계산 한 후 총로우수로 나눔. 최대치를 찾는다.
판매부서(SALEDEPT)+판매일자: 3년간의 데이터를 부서로 나누어 평균을 산출. 최대, 최소치를 구한다.
진행상태(STATUS): 몇가지 종류인지, 어떤 상태에 데이터가 몰려있는지 조사한다.
위와같이 하여 최대치와 평균치의 차이가 심하다면 '특기사항'란에 기입한다.
<그림 1-4-10>
가장 우선적인 액세스 형태부터 결정한다.
다) 특수한 액세스 형태에 대한 인덱스 선정
반복되는 액세스형태를 찾아낸다.
'항상사용되느냐','항상=로 사용되느냐'
동률이라면 분포도가 좋은 것이 앞에 오는 것이 유리.
1번 액세스 형태인 SALENO+SALEDATE 를 검토해 보면 둘 다 =로 사용되기 때문에 분포도나 정렬 순서를 고려해서 결정하면 된다. 7번 및 11번 액세스 형태를 감안한다면 항상 사용되는 것은 ITEM이다.
SALENO는 1번 액세스에서만 사용되는 것으로 조사되었지만, 혼자서만 사용될 가능성이 높음. 따라서 선두 컬럼에서 배제하기는 어렵다.
ITEM은 좋은 분포도를 가지고 있고, 업무적으로도 중요하며, 앞으로도 자주 사용될 것으로도 보인다.(??) 또한 SALENO 없이도 사용되는 경우가 많으므로 독립된 인덱스에서 선행컬럼이 될 자격을 갖추고 있다.
11번 액세스에서 ITEM은 주도적인 역할을 하는 것으로 보이진 않으므로 특별한 역할을 부여할 필요는 없어보인다.
CUSTNO는 평균이 63에 불과하고, 최대가 300 정도이며, 범위처리를 하는 경우가 거의 없이 =로 사용되고 있으므로 CUSTNO 혼자 만으로도 확실한 수행속도를 보장 받을 수 있을 것으로 보임.
현재보다 현격하게 데이터량이 증가할 것으로 보인다면 SALEDATE를 인덱스에 추가시키는 것을 검토한다.
라) 클러스터링 검토
넓은 범위의 데이터가 자주 액세스 되어 별도의 집계 테이블을 만들지 않고서는 다른 방법이 없는 경우에는 주로 클러스터링을 사용한다.
클러스터링은 인덱스와는 달리 분포도가 넓은 것이 유리하다.
검색의 속도를 향상시켜주며, 입력,수정,삭제시에는 부하를 준다.
마) 결합인덱스 구성 및 순서의 결정
그룹1
5 SALEDATE(like),STATUS(=60),CUSTNO(like)
8 SALEDATE(like),STATUS(=),group by CUSTNO
6 STATUS(in),[AGENTNO(like)]
전체범위처리 ( 주어진 조건의 범위가 좁은 경우는 문제가 없으나 넓은 경우는 빠른 수행속도를 기대하기 어려움) 1. 드라이빙 조건을 만족하는 범위를 모두 스캔 2. 체크조건 검증한 후 성공한 건에 대해 임시 저장공간에 저장 3. 저장이 완료되면 필요한 2차 가공을 한 후 운반단위만큼 추출시키고 다음 요구가 있을 때까지 일단 멈춤
부분범위처리 ( 처리할 범위가 아무리 넓다고 하더라도 그 범위 중의 일부만 처리) 1. 드라이빙 조건을 만족하는 범위를 차례로 스캔함 2. 체크조건을 검증하여 성공한 건을 바로 운반단위로 보냄 3. 운반단위가 채원지면 수행을 멈추고 결과를 추출
공통점 : 항상 운반단위만 채워지면 일단은 멈춘다. -운반단위만큼만 추출되었다고 그것이 부분범위 처리를 한 것이라고 단정지어서는 안된다.
HASH JOIN(인-메모리 해쉬조인) SQL_TRACE에서 Execute나 Fetch라인에 있는 'Query'나 'Disk', 'Current'를 확인하여 전체 테이블의 블록 수보다 훨씬 적을때
1.2 부분범위처리의 적용원칙
1.2.1 부분범위 처리의 자격
-논리적으로도 어쩔 수 없는 경우만 아니라면 언제나 부분범위 처리를 할 수 있다
SELECT SUM(ORDQTY)
FROM ORDER
WHERE ORD_DATE LIKE '200512%';
SELECT ORD_DEPT, COUNT(*)
FROM ORDER
WHERE ORD_DATE LIKE '200512%'
GROUP BY ORD_DEPT;
분석 : SELECT-LIST나 HAVING절에 그룹함수를 사용하였다면 부분범위 처리를 할 수 없다.
SELECT ORD_DATE, ORDQTY * 1000
FROM ORDER
WHERE ORD_DATE LIKE '200512%'
ORDER BY ORD_DATE;
분석 : ORDER BY가 사용되었다면 마찬가지로 전체범위를 처리할 수 밖에 없다. Driving역할을 하는 인덱스와 ORDER BY에 사용된 컬럼이 동일하다면 비록 ORDER BY를 사용하였더라도 부분범위로 처리 가능
(인덱스로 처리하여 바로 리턴하는 부분범위 처리방식으로 실행계획을 수립하게 됨)
SELECT DEPTNO, EMPNO
FROM EMP1
WHERE SAL > 1000000
UNION
SELECT DEPTNO, EMPNO
FROM EMP2
WHERE HIREDATE BETWEEN '01-JAN-2005' AND SYSDATE;
분석 :
UNION, MINUS, INTERSECT를 사용한 SQL은 부분범위로 처리할 수 없다.
(집합연산은 그 결과가 반드시 유일해야하기 때문)
UNION ALL 부분범위처리를 한다.
(중복을 확인할 필요가 없다)
1.2.2 옵티마이져 모드에 따른 부분범위처리
옵티마이져 모드에 따라 차이가 날 수 있음
규칙기준이나, 비용기준 :'FIREST_ROWS'로 지정되어 있는 경우에는 대개의 경우 부분범위 처리
비용기준 :'ALL_ROWS'로 지정되어 있다면 전체범위 처리를 하는 경우가 자주 발생
1.3 부분범위처리의 수행속도 향상원리
SELECT * FROM ORDER;
-무조건으로 데이블의 첫 번째 블록을 스캔한 모든 로우들을 그대로 운반단위로 보내서 바로 추출
SELECT * FROM ORDER
ORDER BY ITEM;
-오랜 시간이 지나서야 첫 번째 운반단위가 추출됨(전체 테이블을 모두 액세스하여 임시 저장공간에 저장하고 정렬시킨 후 하나의 운반단위를 추출)
SELECT * FROM ORDER
WHERE ITEM > ' ';
-옵티마이져 목표가 'ALL_ROWS'가 아니라면 ITEM인덱스를 사용하는 실행계획을 수립하며 부분범위 처리
-옵티마이져 목표가 'ALL_ROWS'였다면 전체 테이블을 스캔하는 실행계획(정렬순으로 추출되지 않음)
ALL_ROWS일때
SELECT /*+ index( order item_index ) */ *
FROM ORDER
WHERE ITEM > ' '
-힌트를 사용하여 인덱스를 사용하게 유도
'^^'
가정: ORDER테이블 ORDNO조건을 만족하는 로우가 1000건이 있고, CUSTNO를 만족하는 로우가 10건 각각의 컬럼에는 별도의 인덱스가 생성되어 있음
SELECT * FROM ORDER WHERE ordno between 1 and 1000 AND custno like 'DN%';
옵티마이져가 현재 어떤 실행계획을 수립했는가에 상관없이 액세스 형태별로 발생될 수 있는 일량
1) ORDNO인덱스를 사용한 경우 1-1. ORDNO인덱스에서 조건을 만족하는 첫번째 로우를 랜덤으로 찾는다. 1-2. 인덱스에 있는 ROWID를 이용하여 ORDER테이블의 해당 로우를 액세스한다. 1-3. 액세스된 테이블의 로우에 있는 CUSTNO가 'DN'으로 시작하는지를 검증하여 만족하는 로우만 운반단위로 보낸다. 즉, CUSTNO는 인덱스를 사용하지 않고 검증 기능으로만 사용 1-4. ORDNO인덱스의 다음 로우를 액세스하여 1-2~1-3을 반복하여 운반단위가 채워지거나 인덱스의 처리범위가 끝나면 수행을 멈춘다.
결론 : 1000회의 처리를 해야 1000건의 로우들은 거의다 CUSTNO LIKE 'DN%' 조건을 만족하지 못할 것이다. 운반단위를 채울 수가 없어 계속해서 다음건을 수행해야 하며, 최악의 경우 ORDNO의 모든 범위(1000건)를 완료해야만 멈출 수 있다.
해결방법 : 1. ORDNO의 처리범위를 줄인다 2. CUSTNO의 조건이 오히려 넓어진다면 ORDNO인덱스를 경우한 로우들이 훨씬 쉽게 운반단위를 채울 수 있다.
'^^'
가정: ORDER테이블 ORDNO조건을 만족하는 로우가 1000건이 있고, CUSTNO를 만족하는 로우가 10건 각각의 컬럼에는 별도의 인덱스가 생성되어 있음
옵티마이져가 현재 어떤 실행계획을 수립했는가에 상관없이 액세스 형태별로 발생될 수 있는 일량
SELECT * FROM ORDER WHERE ordno between 1 and 1000 AND custno like 'DN%';
2) CUSTNO 인덱스를 사용한 경우 2-1. CUSTNO인덱스에서 조건을 만족하는 첫번째 로우를 랜덤으로 찾는다. 2-2. 인덱스에 있는 ROWID를 이용하여 ORDER테이블의 해당 로우를 액세스한다. 2-3. 액세스된 테이블의 로우에 있는 ORDNO가 1과 1000사이에 있는지를 검증하여 만족하는 로우만 운반단위로 보낸다. 즉,ORDNO의 범위는 넓으므로 쉽게 조건을 만족할 수가 있어 운반단위로 보낼 수 있는 확률이 높아지는 것에 유의 하기 바란다. 2-4. CUSTNO인덱스의 다음 로우를 액세스하여 2-1~2-3을 반복하여 운반단위가 채워지거나 인덱스의 처리범위가 끝나면 수행을 멈춘다.
액세스 주관조건의 범위
검증조건의 범위
수행속도
조치사항
좁다
좁다
양호
좁다
넓다
양호
넓다
좁다
불량
주관조건과 검증조건 역할 교체
넓다
넓다
양호
판단기준 WHERE절에 조건을 만족하는 ROW수가 좁다, 넓다에 따라 운반단위가 채워지는 시간으로 판단
양쪽 모두 ITEM_CD 인덱스르르 사용하는 실행계획이 수립되었더라도 좌측 그림은 주어진 ITEM 조건에 해당하는 전체범위를 액세스하여 역순으로 정렬(Descending sort)하여 운반단위만큼 추출. 그러나 우측의 그림은 ITEM_CD 인덱스를 역순으로 처리하여 테이블을 액세스한 후 다른 조건과 비교하여 만족하는 로우들만 운반단위로 보내며 운반단위가 채워지면 추출시킴.
규칙기준 옵티마이저인 경우는 좌측의 경우도 인덱스를 역순으로 액세스하는 부분범위 처리로 실행계획이 수립되지만 비용기준일 때는 반드시 그렇게 된다는 보장이 없기 때문에 우측과 같이 힌트를 사용하여 ORDER BY 대신에 인덱스를 이용한 부분범위 처리로 유도하는 것이 바람직함.
원하는 정렬이 'ITEM_CD_DESC'가 아니라 'ITEM_CD DESC, CATEGORY DESC' 일경우 인덱스가 'ITEM_CD + CATEGORY'로 결합되어 있다면 가능함.
액세스의 조건으로 사용되지 않더라도 'ORDER BY'를 없애기 위한 목적만으로 인덱스에 필요한 컬럼을 추가시키는 것도 하나의 좋은 방법이 될수 있음.
SQL 의 액세스를 주관하는 컬럼과 'ORDER BY'의 컬럼이 다른경우.
ord_date like '2005%' 인 범위가 아주 넓다고 가정.
SELECT ord_dept, ordqty * 1000
FROM order
WHERE ord_date like '2005%'
ORDER BY ord_dept desc
;
-- ORD_DATE 의 처리범위가 넓지 않다면 가장 양호한 액세스 경로가 되겠지만, 처리범위가 넓다면 부담이됨.
# 예측 실행계획
------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)|
------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 96929 | 5774K| | 2120 (1)|
| 1 | SORT ORDER BY | | 96929 | 5774K| 13M| 2120 (1)|
| 2 | TABLE ACCESS BY INDEX ROWID| ORDER_T | 96929 | 5774K| | 675 (1)|
|* 3 | INDEX RANGE SCAN | IDX_ORDER_ORD_DATE | 96929 | | | 272 (2)|
------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access("ORD_DATE" LIKE '2005%')
filter("ORD_DATE" LIKE '2005%')
SELECT /*+ INDEX_DESC(a ord_dept_index) */
ord_dept, ordqty * 1000
FROM order a
WHERE ord_date like '2005%'
AND ord_dept > ' '
;
-- AND ord_dept > ' ' 을 추가함으로써 이 SQL 은 액세스를 주관하는 컬럼과 'ORDER BY'할 컬럼이 같아짐.
-- 속도향상 원리 '액세스 주관 컬럼의 처리범위가 넓어도 다른 조건의 처리범위도 같이 넓으면 빠르다'는
원리에 의해 아주 빠른 수행속도를 얻을수 있음.
-- 힌트를 통해 액세스 주관 컬럼을 ord_dept_index 로 지정, 원래의 조건은 검증 조건으로 바꾸어 부분범위 처리로 유도함.
# 예측 실행계획
------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 96929 | 5774K| 213K (1)|
|* 1 | TABLE ACCESS BY INDEX ROWID | ORDER_T | 96929 | 5774K| 213K (1)|
|* 2 | INDEX RANGE SCAN DESCENDING| IDX_ORDER_T_ORD_DEPT | 1034K| | 2005 (2)|
------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("ORD_DATE" LIKE '2005%')
2 - access("ORD_DEPT">' ')
filter("ORD_DEPT">' ')
만약 ord_date like '2005%'로 사용하지 않고 ord_date like :date 와 같이 변수값을 사용했다면 좀더 많은 고려 사항이 필요.
실행 시 변수에 주어진 상수값은 경우에 따라 넓은 처리범위를 가질 수도, 그렇지 않을 수도 있기 때문.
만약 주어진 상수값이 좁은 범위를 가진다면 위의 예에서처럼 부분범위로 유도한 방법은 오히려 수행속도를 매우 나쁘게 만듬.
만약 1개월 분량의 데이터가 이러한 방법에 대한 손익분기점이라고 한다면, 동적(Dynamic) SQL 을 사용하거나 SQL 을 별도로 적용시켜야 함.
1.4.2 인덱스만 액세스하는 부분범위처리
옵티마이저는 사용자의 의도와는 상관없이 인덱스만으로 처리할 수 잇다고 판단되면 테이블을 액세스하지 않고 인덱스만 액세스하는 실행계획을 수립함.
인덱스는 첫 번재 로우를 찾을 때만 랜덤 액세스를 하고 그 다음부터는 스캔을 하지만 테이블을 액세스하는 행위는 항상 랜덤 액세스를 해야 함. 수행속도에 가장 많은 영향을 주는 테이블 랜덤 액세스를 하지 않고, 인덱스로만 처리할 수 있다면 비록 처리범위가 넓더라도 매우 효율적인 처리를 할 수 있음. 이러한 방식으로 유도하기 위한 최대의 관건은 인덱스가 어떻게 구성되어 있느냐에 달려있음.
인덱스만을 사용하는 실행계획이 수립되기 위해서는 다음과 같은 경우에 해당 되어야 함.
쿼리에 사용된 모든 컬럼들이 하나의 인덱스에 결합되어 있거나,
인덱스 머지가 되는 실행계획이 수립되려면 머지되는 두 개의 인덱스 내에 모든 컬럼들이 모두 사용되어야 함.
쿼리에 사용된 전체 컬럼들이 몇 개의 인덱스에 모두 포함될 수 있다면 인덱스 조인이 성립하게 된다. 이런 경우는 인덱스들만 이용해서 조인을 하는 실행계획이 수립됨. (인덱스 조인:235~238 참조)
그림에서와 같이 ORD_DATE 인덱스에 QTY를 합친 결합 인덱스를 생성하면 옵티마이저는 인덱스만 사용하는 실행계획을 수립. 처리범위 및 클러스터링 팩터에 따라 차이가 있겠지만 좌측에 비해 거의 200~500% 정도의 수행속도를 향상시킴.
두 가지 경우의 차이가 손익분기점만큼 차이가 나지 않는 것은 'GROUP BY' 처리가 양쪽에 모두 같이 있기 때문. 이와 같이 WHERE 절의 조건으로 사용하지 않은 컬럼도 인덱스만 사용하도록 유도할 목적으로 결합 인덱스에 추가할 수 있음.
SELECT ord_date, SUM(ordqty)
FROM order
WHERE ord_date LIKE '2005%'
GROUP BY ord_date
;
# 예측 실행계획
----------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
----------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 96929 | 3691K| 675 (1)|
| 1 | SORT GROUP BY NOSORT | | 96929 | 3691K| 675 (1)|
| 2 | TABLE ACCESS BY INDEX ROWID| ORDER_T | 96929 | 3691K| 675 (1)|
|* 3 | INDEX RANGE SCAN | IDX_ORDER_ORD_DATE | 96929 | | 272 (2)|
----------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access("ORD_DATE" LIKE '2005%')
filter("ORD_DATE" LIKE '2005%')
# 예측 실행계획 : 결합인덱스 생성후 (ORD_DATE + QTY)
------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 96929 | 3691K| 326 (1)|
| 1 | SORT GROUP BY NOSORT| | 96929 | 3691K| 326 (1)|
|* 2 | INDEX RANGE SCAN | IDX_ORDER_ORD_DATE_QTY | 96929 | 3691K| 326 (1)|
------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("ORD_DATE" LIKE '2005%')
filter("ORD_DATE" LIKE '2005%')
ORDER 테이블에 'ORD_DATE + AGENT_CD'로 결합된 인덱스가 있다고 가정하고 다음 SQL을 수행.
SELECT ord_dept, COUNT(*)
FROM order
WHERE ord_date LIKE '200510%'
GROUP BY ord_dept
;
# 예측 실행계획
-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 48 | 2304 | 17 (6)|
| 1 | HASH GROUP BY | | 48 | 2304 | 17 (6)|
| 2 | TABLE ACCESS BY INDEX ROWID| ORDER_T | 48 | 2304 | 16 (0)|
|* 3 | INDEX RANGE SCAN | IDX_ORDER_ORD_DATE_AGENT_CD | 48 | | 3 (0)|
-------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access("ORD_DATE" LIKE '200510%')
filter("ORD_DATE" LIKE '200510%')
-- 'ORD_DATE + AGENT_CD' 인덱스로 전체범위를 스캔하여 랜덤으로 테이블을 액세스하고, GROUP BY 를 한 후 결과를 추출.
AGENT_CD 로 변경후 SQL 실행
SELECT agent_cd, COUNT(*)
FROM order
WHERE ord_date LIKE '200510%'
GROUP BY agent_cd
;
# 예측 실행계획
--------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
--------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 48 | 1536 | 4 (25)|
| 1 | HASH GROUP BY | | 48 | 1536 | 4 (25)|
|* 2 | INDEX RANGE SCAN| IDX_ORDER_ORD_DATE_AGENT_CD | 48 | 1536 | 3 (0)|
--------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("ORD_DATE" LIKE '200510%')
filter("ORD_DATE" LIKE '200510%')
-- 이 SQL 은 'ORD_DATE + AGENT_CD' 인덱스만 사용하는 실행계획을 수립하므로 훨씬 빠른 수행속도를 얻을 수 있음.
1.4.3. MIN, MAX의 처리
대량의 데이터를 가지고 있는 테이블에서 최대값(Max)이나 최소값(Min)을 찾거나, 기본키(Primary key)에 있는 일련번호를 생성하기 위해 최대값을 찾는 형태 해결방안
각 분류단위마다 하나의 로우를 가진 별도의 테이블을 생성해 두고, 트랜잭션마다 읽어서 사용하고 +1을 하여 저장하는 방법. 현재 사용자들이 가장 애용하고 있는 이 방법은 결코 권장할 방법이 못된다. 대개의 경우 트랜잭션의 병목현상 및 잠금(Lock)을 발생시키는 원인이 되기 때문.
시퀀스 생성기(Sequence Generator)을 사용하는 방법. 시퀀스는 테이블이나 뷰처럼 일종의 데이터베이스 객체(Object)이며, 사용권한만 있으면 어떤 SQL 내에서도 마음대로 사용할 수가 있음.
CREATE SEQUENCE empno_seq ------------------- 시퀀스 명
INCREMENT BY 1 ------------------- 증가 단위
START WITH 1 ------------------- 시작 숫자
MAXVALUE 100000000 ------------------- 최대값 제한
NOCYCLE ------------------- 순환 여부
CACHE 20 ------------------- 메모리 확보 단위
생성된 시퀀스는 SQL 내에서 CURRVAL(현재값), NEXTVAL(다음값)을 이용하여 원하는 값을 제공받음
시퀀스는 사용자가 생성한 테이블과는 근본적으로 다름.
메모리 내에서 DBMS가 자동으로 관리해 주며, LOCK을 발생하지 않기 때문에 양호한 수행속도를 보장.
다음과 같은 경우에서 사용될 수 있음.
INSERT 문의 VALUE 절
SELECT 문의 SELECT-List
UPDATE 문의 SET 절
사용 예
SELECT empno_seq.CURRVAL FROM DUAL ;
INSERT INTO EMP (empno, ename, job, hiredate, sal)
VALUES (empno_seq.NEXTVAL, 'James Dean', 'MANAGER', '2011-08-14', 30000000) ;
UPDATE EMP
SET empno = empno_seq.NEXTVAL
WHERE empno = 10001 ;
시퀀스의 특징
시퀀스는 세션(session)단위로 관리된다.
서로 다른 세션에서 NEXTVAL 을 요구하든, 자신의 세션에서 계속해서 요구를 하든 무조건 하나씩 증가된 값이 나타남.
A 세션에서 NEXTVAL 을 실행하여 CURRVAL 의 값이 10이 나오고, B 세션에서 NEXTVAL 을 실행하여 CURRVAL의 값이 11이 나왔을 경우.
A 세션에서 CURRVAL 을 다시 실행해도 값은 10 으로 나옴.
A 세션에서 NEXTVAL 을 다시 실행하면 CURRVAL 의 값은 12가 나옴. (결과적으로 해당 세션에서 NEXTVAL 을 실행하고 난후 다른 세션의 영향없이 해당 세션의 CURRVAL 값은 동일)
어떤 세션에서 NEXTVAL 을 하지 않은 채 CURRVAL 을 요구하게 되면 에러가 발생. CURRVAL 은 바로 그 세션이 지그 현재 가지고 있는 최종 시퀀스 값을 의미하므로 한번도 NEXTVAL 을 요구한적이 없다면 보유하고 있는 CURRVAL 값이 없기 때문.
데이터베이스의 데이터 딕셔너리에 관련정보가 저장(시퀀스의 최종값).
사용자가 NEXTVAL 을 했을 때마다 디스크에 저장하지 않고, 'CACHE'에 있는 값만큼 선행 증가를 시킴. 최초에 생성되었을때 0을 저장하는 것이 아니라 20(cache 값을 20 으로 지정했다고 가정)을 저장 이후 사용자의 요구에 의해 증가하는 것은 메모리 내에서 관리되다가 20(START WITH 20)이 되는 순간 다시 40(START WITH 40)을 저장. 만약 36 인 상태에서 시스템에 이상이 발생하여 비정상 종료(Abnormal shutdown)가 되었다면 시스템이 다시 가동(startup)되었을 때 41 부터 제공하게 됨. 이로 인해 일부의 값에 누락이 발생(이것을 방지하려면 CACHE 값을 2로 주면되지만 시퀀스를 사용하는 장점이 없어지게 되므로 바람직하지 않음) CACHE 값은 괜히 불필요한 저장이 빈번하게 발생하지 않도록 충분히 크게 주는 것이 좋음.
특정 분류단위로 일련번호가 증가하는 경우에는 처리할 수 없음.('DEPTNO + SEQ'로 식별자가 구성되어 있다면 부서별로 일련번호가 발생해야 하므로 시퀀스 적용이 곤란)
1.4.4. FILTER 형 부분범위 처리
이 SQL 은 그룹함수를 사용하였기 때문에 논리적으로 이미 일부분만 액세스하여 처리하는 것이 불가능함.
먼저 드라이빙 조건인 DEPT 를 액세스하여 검증조건인 SEQ 를 체크.
통과한 결과에 대해 COUNT 를 하기 위한 정렬작업을 하여 그 결과를 변수에 저장한 후, 'IF CNT > 0'의 조건으로 다음 처리를 진행
그림에서도 확인할 수 있듯이 이 SQL 은 그 결과가 몇 건이건 간에 전체범위를 모두 처리하여 그룹함수로 가공하여 하나의 결과를 추출. 이와 같이 단지 존재 여부를 확인하기 위해 전체범위를 모두 처리했다는 것은 엄청나게 비효율적인 처리를 한 것임.
우측의 그림과 같이 EXISTS 를 사용하여 수행결과의 존재여부를 체크하여 성공과 실패만을 확인하는 불린(Boolean) 함수를 이용하여 단 하나라도 성공한 결과가 나타나면 즉시 수행을 멈추고 결과를 리턴.
서브쿼리를 부분범위 처리로 수행하다가 조건을 만족하는 첫 번째 로우를 만나는 순간 서브쿼리의 수행을 멈추고 메인쿼리를 통해 결과를 리턴.
SELECT ord_dept, ord_date, custno
FROM order
WHERE ord_date like '2005%'
MINUS
SELECT ord_dept, ord_date, '12541'
FROM sales
WHERE custno = '12541';
위 SQL 은 대량의 처리범위를 각각의 로우마다 랜덤 액세스로 대응되는 로우를 찾아 확인하는 것을 피하기 위해 '전체처리가 최적화(ALL ROWS)'되도록 머지 방법으로 유도한 경우. 배치처리 애플리케이션인 경우에는 좋은 방법이 될 수도 있겠지만 전체 범위로 처리되므로 온라인에서는 첫 번째 운반단위가 추출되는 시간이 많이 소요된다.
SELECT ord_dept, ord_date, custno
FROM order x
WHERE ord_date like '2005%'
AND NOT EXISTS (SELECT * FROM sales y
WHERE y.ord_dept = x.ord_dept
AND y.ord_date = x.ord_date
AND y.custno = '12541'
) ;
위 SQL 은 부분범위로 처리하도록 실행계획이 변경되었음.
여기에서 사용된 서브쿼리는 'NOT EXISTS'를 요구했으므로 서브쿼리의 집합이 적을수록 조건을 만족하는 범위는 오히려 넓어지게 된다. 그러므로 CUSTNO = '12541'을 만족하는 로우가 적다면 체크 조건은 넓어져 아주 빠른 수행속도를 보장받을수가 있으며, 그렇지 않다면 앞서 제시한 'MINUS'를 사용한 SQL 이 더 유리해질 것이다.
1.4.5. ROWNUM의 활용
ROWNUM 은 모든 SQL 에 그냥 삽입해서 사용할 수 있는 일종의 가상(Pseudo)의 컬럼.
이 값은 SQL 이 실행되는 과정에서 발생하는 일련번호이므로 각 SQL 수행 시마다 같은 로우라 하더라도 서로 다른 ROWNUM 을 가질 수 있다.
이 또한 컬럼이 분명하기 때문에 조건절에 사용하여 원하는 만큼만 처리가 수행되도록 할 수 있다. 이 방법 역시 전체를 처리하지 않고 일부만 처리하도록 유도하는 방법이므로 일종의 부분범위 처리라고 말할 수 있다. 그러나 SQL 이 실행하는 어떤 과정의 어느 특정 부분에서 ROWNUM 이 결정되는지를 정확히 알지 못하고 사용한다면 원하지 않는 결과가 추출될 수도 있음.
위의 그림에서 인덱스를 통해 액세스한 테이블의 로우들 중에 체크 조건을 확인하여 만족하는 로우들만 ROWNUM 이 부여되어 운반단위로 보내진다는 것을 알수 있다.
운반단위가 채워지거나 ROWNUM 이 10보다 커지는 순간 수행을 멈춤.
만약 액세스는 했으나 조건을 만족하는 로우가 없다면 끝까지 ROWNUM <= 10 을 만족하지 못했으므로 부분범위 처리 방식임에도 불구하고 전체범위가 끝날 때까지 계속 처리.
ROWNUM 은 액세스되는 로우의 번호가 아니라 조건을 만족한 '결과에 대한 일련번호'이므로 우리가 단지 10건만 요구하였더라도 내부적으로는 그것보다 훨씬 많은 로우가 액세스될 수도 있다.
추출되는 로우중에 10번째 로우를 찾기 위해 WHERE 절에서 'ROWNUM = 10' 을 요구했다면 이 조건을 만족하는 로우는 결코 추출될 수가 없음.
Execution Plan
------------------------------------------------------------
SELECT STATEMENT
COUNT (STOPKEY)
TABLE ACCESS (FULL) OF 'PRODUCT'
이 실행계획은 테이블을 부분범위 처리로 액세스하여 ROWNUM 을 COUNT 하다가 주어진 ROWNUM 조건에 도달하면 멈춤(Stopkey)을 하겠다는 것을 나타내고 있다. 이로써 확실히 일정 범위만 스캔했다는 것을 확인할 수 있다.
SELECT ROWNUM, item_cd, category_cd, ......
FROM product
WHERE deptno like '120%'
AND qty > 0
AND ROWNUM <= 10
ORDER BY item_cd
;
이 결과는 조건을 만족하는 10개의 로우를 액세스한 다음 정렬하여 결과를 추출한다. 비록 ROWNUM 이 SQL 단위로 생성되는 값이기는 하지만 'ORDER BY'가 수행되기 전에 이미 WHERE 절에 있는 조건을 만족한느 로우마다 부여되어 임시공간에 저장됨. 그러므로 'ORDER BY'를 수행하기 전에 이미 'ROWNUM <= 10'이 적용되어 단 10건만 정렬하여 운반단위로 보냄.
위 그림은 추출된 로우들에 있는 ROWNUM 은 순차적 정렬이 되어 있지 않는다는 것을 확인할 수 있다.
이 SQL 은 전체범위 처리를 하여야 하므로 일단 조건에 맞는 데이터를 액세스하여 내부적으로 저장한 다음 이를 정렬하게 된다.
내부적인 저장이 일어날 때 ROWNUM 이 생성되어 같이 저장된다는 것을 알수 있음.
전체범위 처리를 하는 경우는 중간에 한번 더 가공 단계를 거치게 되므로 최종적으로 추출되는 로우의 순서와 ROWNUM 이 일치하지 않으며, 잘못된 결과를 추출할 수도 있다.
ORDER BY 를 인라인뷰에 넣은 다음 ROWNUM 을 체크하는 방법
SELECT ROWNUM, item_cd, category_cd, ......
FROM (SELECT *
FROM product
WHERE deptno like '120%'
AND qty > 0
ORDER BY item_cd
)
WHERE ROWNUM <= 10
;
-- 이 방법은 인라인뷰 내에 있는 처리는 전체범위로 수행되므로
-- 불필요한 액세스가 많이 발생할 수 있으므로 주의하여야 함.
GROUP BY 한 결과를 원하는 로우만큼 추출하고자 할때.
CREATE VIEW PROD_VIEW (deptno, totqty)
AS SELECT deptno, sum(qty)
FROM product
GROUP BY deptno
;
-- 뷰는 자료사전(Data Dictionary)에 SQL 문장이 저장되어 있다가 SQL 을 수행하는 순간,
-- 뷰의 SQL(뷰 쿼리)과 수행시킨 SQL(액세스 쿼리)을 합성(Merge)하여 실행계획을 수립.
-- 아래 SQL 에서는 뷰를 이용해 ROWNUM 을 요구
SELECT ROWNUM, deptno, totqty
FROM PROD_VIEW
WHERE deptno like '120%'
AND ROWNUM <= 10
;
-- 뷰를 생성한 SQL 에는 조건절이 전혀 없었지만 SQL 을 실행하면
-- 액세스 쿼리가 뷰 쿼리로 합성되어 다음과 같은 SQL 이 내부적으로 만들어져서 수행됨.
SELECT ROWNUM, dept, totqty
FROM (SELECT deptno, sum(qty) totqty
FROM product
WHERE deptno like '120%'
GROUP BY deptno
)
WHERE ROWNUM <= 10
;
-- GROUP BY 를 수행한 결과는 내부적으로 저장이 되며 이때 ROWNUM 도 같이 생성됨.
위와 같은 처리는 마치 두 개의 SQL 이 수행된 것과 유사하며 내부적으로 저장을 하는 단계 또한 두번에 걸쳐 일어난다.
위 그림에서 보는 바와 같이 조건에 맞는 데이터를 액세스할 때 발생되는 ROWNUM(1차)과 GROUP BY 된 결과에 대한 ROWNUM(2차)은 분명한 차이가 있다.
논리적으로 보면 단위 SELECT 마다 하나씩의 ROWNUM 을 가질 수 있다고 생각하면 보다 이해가 쉬울 수도 있다. 논리적으로 구성되는 단위 집합마다 ROWNUM 이 존재한다. 만약 선행 집합에서 생성된 ROWNUM 을 활용하고 싶다면 다른 이름으로 치환을 해두면 계속 유효하다.
1.4.6. 인라인뷰를 이용한 부분범위처리
인라인뷰를 활용하여 부분범위 처리를 유도하는 원리는 전체범위 처리가 되는 부분을 인라인뷰로 묶어서 다른 부분들은 부분범위 처리가 되도록 하겠다는 것이다. 그대로 두면 전체 쿼리를 전체범위 처리로 만들어 버리는 요인을 인라인뷰로 묶어내어 격리함으로써 다른 처리들을 부분범위 처리가 되도록 하겠다는 것.
SELECT a.dept_name, b.empno, b.emp_name, c.sal_ym, c.sal_tot
FROM department a, employee b, salary c
WHERE b.deptno = a.deptno
AND c.empno = b.empno
AND a.location = 'SEOUL'
AND b.job = 'MANAGER'
AND c.sal_ym = '200512'
ORDER BY a.dept_name, b.hire_date, c.sal_ym
;
위 SQL 은 서울에 근무하는 매니저들의 입사일 순으로 급여액을 조회하고자 한다. 이러한 결과를 얻기 위해서 작성한 이 SQL 은 일견 너무나 당연해 보인다. 부분범위 처리를 방해하는 정렬처리 컬럼은 세 테이블에 각각 분산되어 있으며, 추출하려는 로우의 단위는 최하위 집합인 'SALARY' 테이블이므로 어쩔 수 없이 모든 집합을 조인한 후에 정렬을 해야만 한다.
그러나 가만히 따져보면 분명히 무엇인가 불필요한 작업이 들어가 있다. 서울에 근무하는 매니저들은 크지 않은 집합이 분명하다. 그렇지만 'SALARY' 테이블과 조인하는 순간 로우는 크게 증가한다. 이렇게 증가된 집합을 정렬하여 결과를 추출한다면 좋은 수행속도를 얻을 수 없다.
적은 집합인 'DEPT' 와 'EMPLOYEE' 테이블만 먼저 조인시키고 그 결과만 먼저 정렬시킨 다음, 대량의 집합인 'SALARY' 테이블은 기본키인 'empno + sal_ym' 인덱스를 경유하게 하는 벙법으로 동일한 결과를 얻을 수 있다면 가장 이상적인 실행방법이 될 것이다.
SELECT /*+ ORDERD, USE_NL(x y) */
a.dept_name, b.empno, b.emp_name, c.sal_ym, c.sal_tot
FROM (SELECT a.dept_name, b.hire_date, b.empno, b.emp_name
FROM DEPT a, EMPLOYEE b
WHERE b.deptno = a.deptno
AND a.location = 'SEOUL'
AND b.job = 'MANAGER'
ORDER BY a.dept_name, b.hire_date ) x, SALARY y
WHERE y.empno = x.empno
AND y.sal_ym = '200512'
;
위에서 힌트를 사용한 것은 반드시 인라인뷰를 먼저 수행한 다음, 그 결과와 'SALARY' 테이블이 Nasted Loop 조인이 되도록 하기 위해서 이다.
이 SQL 에서 알 수 있듯이 처리 대상이 적은 테이블을 먼저 처리하여 전체범위 처리가 되도록 함으로써 대량의 데이터를 가진 집합은 부분범위로 처리되도록 했다는 것을 알수 있다.
다음은 GROUP BY 를 통해 가공된 중간 집합을 생성하고 나머지 참조되는 일종의 디멘전 테이블들을 부분범위 처리로 유도하는 경우.
SELECT a.product_cd, product_name, avg_stock
FROM PRODUCT a,
(SELECT product_cd, SUM(stock_gty) / (:b2 - :b1) avg_stock
FROM PROD_STOCK
WHERE stock_date between :b1 and :b2
GROUP BY product_cd ) b
WHERE b.product_cd = a.product_cd
AND a.category_cd = '20'
;
최적의 처리가 될 수 있는 수행절차의 시나리오
대량의 처리 대상 범위를 가진 CATEGORY_CD 가 20인 PRODUCT_CD 를 부분범위 처리 방식으로 첫 번째 로우를 읽는다.
읽혀서 상수값을 가지게 된 PRODUCT_CD 와 이미 상수값을 가진 STOCK_DATE 로 인덱스 (product_cd + stock_date 로 구성되었다고 가정)를 이용해 PROD_STOCK 테이블을 액세스하여 평균 재고수량을 구한다.
두 번째 PRODUCT_CD 에 대해 위의 작업을 반복하다가 운반단위가 채워지면 일단 멈춘다.
1.4.7. 저장형 함수를 이용한 부분범위처리
저장형 함수가 가지고 있는 가장 큰 특징은 SQL 내로 절차형 처리를 끌어들였다는 점이다.
저장형 함수는 데이터베이스가 제공하는 절차형 SQL 을 이용해 생성한다.
저장형 함수 내에는 하나 이상의 SQL 이 존재할 수 있음은 물론이고 다양한 연산이나 조건처리, 반복(Loop) 처리도 할 수 있음.
외부에서 받은 값이나 고정된 상수값을 이용하여 필요한 처리를 하여 단 하나의 결과값만을 리턴.
저장형 함수는 처리할 각 집합의 각각의 대상 로우에 대해 한 번씩 수행.
저장형 함수는 장점뿐만 아니라 많은 단점을 가지고 있으므로 정확한 개념을 알지 못한 채 함부로 사용해서는 안된다.
단일값만 리턴하기 때문에 만약 여러 개의 결과를 얻고자 한다면 반복해서 사용해야 하므로 매우 주의해서 사용해야함.
해당 집합의 로우마다 실행된다는 것은 처리 속도에 문제가 될 소지가 많기 때문에 주의를 할 필요가 있음.
배치 처리 형태의 애플리케이션이라면 더욱 더 저장형 함수를 사용하지 않는 것이 좋다. 배치 처리는 일반적으로 처리할 데이터가 훨씬 더 많으므로 부하에 대한 부담은 더욱 늘어날 수 있기 때문.
CREATE or REPLACE FUNCTION GET_AVG_STOCK
( v_start_date in date,
v_end_date in date,
v_product_cd in varchar2
)
RETURN number IS
RET_VAL number(14) ;
BEGIN
SELECT SUM(stock_qty) / (v_start_date - v_end_date) ) into RET_VAL
FROM PROD_STOCK
WHERE product_cd = v_product_cd
AND stock_date between v_start_date and v_end_date ;
RETURN RET_VAL ;
END GET_AVG_STOCK ;
SELECT product_cd, product_name,
GET_AVG_STICJ (product_cd, :b1, :b2) avg_stock
FROM PRODUCT
WHERE category_cd = '20'
;
위의 SQL 을 보면 어디에도 전체범위 처리를 해야 할 요인들을 찾을 수 없기 때문에 부분범위 처리가 되는 것이 확실하다. 설사 저장형 함수에 훨씬 더 복잡한 처리가 들어있더라도 부분범위 처리에는 아무런 영향을 미치지 못한다.
가) 확인자 역할의 M 집합 처리를 위한 부분범위처리
SELECT y.cust_no, y.cust_name, x.bill_tot, ............
FROM ( SELECT a.cust_no, sum(bill_amt) bill_tot
FROM account a, charge b
WHERE a.acct_no = b.acct_no
AND b.bill_cd = 'FEE'
AND b.bill_ym = between :b1 and :b2
GROUP BY a.cust_no
HAVING sum(b.bill_amt) >= 1000000 ) x, customer y
WHERE y.cust_no = x.cust_no
AND y.cust_status = 'ARR'
AND ROWNUM <= 30
;
위 SQL 은 고객의 상태가 '체납(ARR)'인 사람이 보유하고 있는 계정(Account)들의 청구(Charge) 정보에서 주어진 기간 내에 요금(Fee)으로 청구한 총금액이 백만 원이 넘는 30명만 선별하고자 하는 SQL 이다.
추출하고자 하는 데이터는 CUSTOMER 레벨이지만 이를 위해 액세스해야 할 데이터는 그 보다 하위인 ACCOUNT 와 CHARGE 테이블이다.
만약 체납 고객이 5% 미만이고 그 중에서 30명은 1/100 이라면 우리가 불필요하게 처리한 일량은 실로 막대하다.
이러한 비효율을 제거하기 위해서 우선 생각해볼것은 EXISTS 서브쿼리를 활요하는 방법일 것이다.
그러나 이 서브쿼리는 불린 값만을 리턴할 수 있기 때문에 위의 예에서처럼 고객의 요금청구총액(bill_amt)을 얻을 수 없어 적용이 불가능하다.
결과를 리턴 받으면서 부분범위를 가능하게 하려면 저장형 함수를 사용함으로써 간단하게 해결할 수 있다.
CREATE or REPLACE FUNCTION CUST_ARR_FEE_FUNC
( v_costno in varchar2,
v_start_ym in varchar2,
v_end_ym in varchar2
)
RETURN number IS
RET_VAL number(14) ;
BEGIN
SELECT sum(bill_amt) into RET_VAL
FROM account a, charge b
WHERE a.acct_no = b.acct_no
AND a.cust_no = v_cust_no
AND b.bill_cd = 'FEE'
AND b.bill_ym between v_start_ym and v_end_ym ;
RETURN RET_VAL ;
END CUST_ARR_FEE_FUNC ;
SELECT CUST_NO, CUST_NAME, CUST_ARR_FEE_FUNC(cust_no, :b1, :b2), ................
FROM CUSTOMER
WHERE CUST_STATUS = 'ARR'
AND CUST_ARR_FEE_FUNC(cust_no, :b1, :b2) >= 1000000
AND ROWNUM <= 30
;
인라인뷰를 활용하는 방법
SELECT cust_no, cust_name, bill_tot, .........
FROM ( SELECT ROWNUM, cust_no, cust_name,
CUST_ARR_FEE_FUNC(cust_no, :b1, :b2) bill_tot, ...............
FROM customer
WHERE cust_status = 'ARR' )
WHERE bill_tot >= 1000000
AND ROWNUM <= 30
;
-- 이 SQL 의 실행계획을 보면 몇 가지 특이한 점을 발견할 수 있다.
Execution Plan
----------------------------------------------------------------------
SELECT STATEMENT
COUNT (STOPKEY)
VIEW --------------------------------------- (b)
COUNT --------------------------------------- (a)
TABLE ACCESS (BY ROWID) OF 'CUSTOMER'
INDEX (RANGE SCAN) OF 'CUST_STATUS_IDX'
(a) 에 있는 COUNT 는 서브쿼리에 추가한 ROWNUM 때문에 나타난 것.
(b) 의 'VIEW' 는 내부적으로 이 집합이 저장되었음을 의미함.
1.4.8. 쿼리의 분리를 이용한 부분범위처리
SQL은 분리보다는 통합을 하는 것이 유리하지만 드물게 분리함으로써 유리해지는 경우가 있다.
일반적으로 1:M의 관계를 가지는 집합에서 1에 해당하는 집합에 대해 검색하고자 할 경우이다.
온라인 검색화면과 같은 곳은 한번에 모든 결과를 보여주지 않기 때문에 1집합의 일부를 추출한 후에 거기에 해당해는 M집합만 전체 범위로 처리하는 방식이다.
예제
(인덱스는 deptno+sale_date로 구성)
select deptno, y.empno, y.empno, y.job, sal_tot, comm_tot
from (select empno,
sum(sal_amt) sal_tot,
sum(comm) comm_tot
from salary s
where s.deptno like '12%'
and s.sal_date between '20050101' and '20051231'
group by empno ) x, employee y
where y.empno = x.empno;
전체 범위로 처리되며 결합인덱스의 선행컬럼이 범위로 비교되어 액세스효율이 떨어진다.
select deptno into :v_deptno
from dept
where deptno like '12%';
select deptno, y.empno, y.empno, y.job, sal_tot, comm_tot
from (select empno,
sum(sal_amt) sal_tot,
sum(comm) comm_tot
from salary s
where s.deptno = :v_deptno
and s.sal_date between '20050101' and '20051231'
group by empno ) x, employee y
where y.empno = x.empno;
앞의 SQL에서 조건을 만족하는 모든 DEPTNO를 페치한다.
각 DEPTNO에 대해 뒤의 SQL을 실행한다.
추출된 데이터를 사용자에게 제공한다.
어플리케이션 버퍼를 만든후 버퍼가 찰만큼 페치한다.
한 DEPTNO에 해당하는 데이터만을 보여주고 'NEXT'키를 치면 다음 DEPTNO를 검색하도록 한다.
=> 장점: 부분범위 처리가 가능해지며 인덱스 선행컬럼이 =으로 변경되면서 효율적인 인덱스 액세스가 가능해진다. 사용자의 조건범위에 무관하게 좋은 수행속도를 얻을 수 있다. 단점: 어플리케이션의 부가적인 처리가 필요하다.
1.4.9. 웹 게시판에서의 부분범위처리
웹 어플리케이션의 세션 유지는 페이지 단위이다.(앞서 페치한 것을 재사용할 수 없다.)
컴포넌트가 한번의 전체 범위의 데이터를 모두 액세스한후 페이지를 분할하여 관리하는 방식을 사용하지만 처리범위가 매우 넓거나 다수의 사용자가 동시에 사용하는 경우라면 심각한 문제를 일으킬 수 있다.
현재 웹 환경은 다수의 사용자가 넓은 범위의 데이터를 사용하므로 한 페이지에 추출할 만큼의 엑세스가 가장 바람직하다.
웹게시판의 부분범위처리를 위한 기본원리
인덱스를 활용한 정렬을 통해 순서대로 원하는 만큼만 액세스하고 그 다음은 앞서 패치했던 마지막 건의 정보를 이용해 그 후 원하는 만큼만 액세스하는 방식 다음 페이지를 액세스 하였을 때 새로운 세션으로 시작되었음에도 불구하고 마치 기존의 세션이 계속 유지된 것과 같은 효과
rownum을 이용하여 원하는 만큼만 잘라오는 방식
연쇄(concatenation)실행계획을 활용하는 방식
처리주관조건이 OR나 IN일 때 각각의 대상만을 액세스하도록 분리하여 추출하여 불필요한 액세스가 발생하지 않도록 한다.
>= 연산의 경우에도 처리량이 크기 때문에 OR를 이용하여 몇개의 독립적인 액세스 단위로 실행계획을 나누어 각각 부분범위로 처리한다.
인덱스: SAL_DATE + SEQ
1) SELECT *
FROM SALES
WHERE SAL_DEPT = '1230'
AND SEQ = 111;
2) SELECT *
FROM SALES
WHERE SAL_DEPT >= '1230'
AND SEQ >= 111;
1)의 경우 시작점이 있지만 2)의 경우 SEQ의 경우 불필요한 액세스 양이 증가한다. 원하는 방식으로의 처리를 위해서는 SAL_DEPT||SEQ로 조건절을 변경후 함수기반인덱스를 생성하거나 아래와 같이 연쇄 실행계획을 이용한다.
=>
1) SELECT *
FROM SALES
WHERE (SAL_DEPT > '1230') ---------------------(2)
OR (SAL_DEPT = '1230' AND SEQ = 111); ---------------------(1)
연쇄 실행 계획은 WHERE 절에서 가장 나중에 기술한 조건부터 처리하여 완료되면 다음 실행단위로 넘어간다. 시작점을 찾고 스캔하는 모습을 띄고 있으며 각 단위가 부분범위 처리를 하여 효율적이다.
웹게시판 부분범위처리 사례1(NON-UNIQUE INDEX)
이전/다음 페이징(기본형)
고객명(cust_name) non unique index일때
union all 역시 연쇄 실행 계획의 일종
옵티마이저는 컬럼보다 변수/상수 비교(:V2)를 먼저 수행하므로 아래 세개의 SELECT 문중 단 하나만 성공
USE_CONCAT: OR 조건절에 대해서 연쇄 실행계획을 수립
어떤 페이지를 요구하더라도 항상 추출할 대상만 액세스하므로 효율적이다.
select /*+ index(w cust_name_idx */
rowidtochar(rowid) rid, cust_name, ...
from cust_table w
where :v2 = 'FIRST' and cust_name like :v1 || '%'
and rownum <= 25
union all
select /*+ use_concat index(x cust_name_idx */
rowidtochar(rowid) rid, cust_name, ...
from cust_table x
where :v2 = 'NEXT'
and (cust_name > :v3 or
(cust_name = :v3 and rowid > chartorowid(:v4) ) )
and cust_name like :v1 || '%'
and rownum <= 25
union all
select /*+ use_concat index_desc(x cust_name_idx */
rowidtochar(rowid) rid, cust_name, ...
from cust_table x
where :v2 = 'PREV'
and (cust_name < :v3 or
(cust_name = :v3 and rowid < chartorowid(:v4) ) )
and cust_name like :v1 || '%'
and rownum <= 25
order by cust_name, rid
웹게시판 부분범위처리 사례2(UNIQUE INDEX)
이전/다음 페이징
기본키(billboard_uk)는 '게시판id+작성일자+글번호'로 구성
작성된 글의 생성 역순으로 추출
:INIT_DT, :V_NUM은 처음/다음일때 각각 다른값을 주어야 한다는 것 주의(처음: SYSDATE/최대값)
(26-rownum) rnum: 이전 페이지의 경우 인덱스 ASC 순서로 뽑았기 때문에 이와 같이 보수로 표현하여 최신순으로 정렬될 수 있도록 처리
select bbs_id, 작성일자, 글번호, rnum
from (select /*+ use_concat index_desc(a billboard_uk) */
rownum rnum, bbs_id, 작성일자, 글번호, 글내용
from billboard a
where :sw = 'NEXT'
and bbs_id = :bid
and (작성일자 < :init_dt or
(작성일자 = :init_dt and 글번호 < :v_num) )
and rownum <= 25
union all
select /*+ use_concat index_asc(a billboard_uk) */
(26-rownum) rnum, bbs_id, 작성일자, 글번호, 글내용
from billboard a
where :sw = 'PREV'
and bbs_id = :bid
and (작성일자 > :init_dt or
(작성일자 = :init_dt and 글번호 > :v_num) )
and rownum <= 25
)
order by rnum;
웹게시판 부분범위처리 사례3(처음,이전,다음,끝)
인덱스: cday + cid + cseq (정렬 처리를 위한 인덱스)
SELECT CDAY, CUST_NM, CID, CSEQ, CTEXT
FROM (SELECT /*+ INDEX_ASC(a IDX01) */
CDAY, CUST_NM, CID, CSEQ, CTEXT
FROM CSTAB a
WHERE :SW = 'FIRST'
AND CDAY BETWEEN :B11 AND :B12
AND ROWNUM <=25
UNION ALL
SELECT /*+ INDEX_DESC(a IDX01) */
CDAY, CUST_NM, CID, CSEQ, CTEXT
FROM CSTAB a
WHERE :SW = 'LAST'
AND CDAY BETWEEN :B11 AND :B12
AND ROWNUM <=25
UNION ALL
SELECT /*+ USE_CONCAT INDEX_ASC(a IDX01) */
CDAY, CUST_NM, CID, CSEQ, CTEXT
FROM CSTAB a
WHERE :SW = 'NEXT'
AND ((CDAY > :B100) OR (CDAY = :B100 AND CID > :B20
OR (CDAY = :B100 AND CID = :B20 AND CSEQ > :B27) )
AND CDAY BETWEEN :B11 AND :B12
AND ROWNUM <=25
UNION ALL
SELECT /*+ USE_CONCAT INDEX_DESC(a IDX01) */
CDAY, CUST_NM, CID, CSEQ, CTEXT
FROM CSTAB a
WHERE :SW = 'PREV'
AND ( (CDAY < :B100) OR (CDAY = :B100 AND CID < :B20
OR (CDAY = :B100 AND CID = :B20 AND CSEQ < :B27) )
AND CDAY BETWEEN :B11 AND :B12
AND ROWNUM <=25 )
ORDER BY CDAY, CID, CSEQ
웹게시판 부분범위처리 사례4(SET단위처리)
넘버 페이징
세트간의 이동은 사례 이전/다음 페이징과 동일(세트가 시작될 시작점만 알면 된다.)
인덱스: bbs_id + cre_dt + num
rownum <= 201:한 세트의 총 글수는 200개이나 다음 세트의 시작점을 확보하기 위해 1개의 로우를 추가
예: 2세트에서 1세트로 이동할때
SELECT RNUM, BBS_ID, CRE_DT, NUM
FROM (SELECT /*+ USE_CONCAT INDEX_DESC(a bbs_idx1) */
ROWNUM RNUM, BBS_ID, CRE_DT, NUM
FROM BILLBOARD a
WHERE :SW = 'NEXT' AND BBS_ID = :V_BBS
AND ( CRE_DT < :V_INIT_DT
OR ( CRE_DT = :INIT_DT AND NUM <= :V_NUM) )
AND ROWNUM <= 201
UNION ALL
특정 페ㅣ지를 액세스하는 SELECT /*+ USE_CONCAT INDEX_ASC(a bbs_idx1) */
((20*10)+2 - ROWNUM) RNUM, BBS_ID, CRE_DT, NUM
FROM BILLBOARD a
WHERE :SW = 'PREV' AND BBS_ID = :V_BBS
AND ( CRE_DT > :V_INIT_DT
OR ( CRE_DT = :INIT_DT AND NUM >= :V_NUM) )
AND ROWNUM <= 201
)
WHERE RNUM IN (1,21,41,61,81,101,121,141,161,181,201)
ORDER BY RNUM
특정 페이지를 액세스하는 SQL
다음 세트를 처리한 SELECT 문과 유사
각 변수 값은 앞서 추출한 각 페이지의 첫번째 로우의 값
다른 테이블과 조인하여 정보를 추가할 수는 있지만 정렬 순서에 문제가 발생하지 않도록 주의해야 한다.
SELECT /*+ USE_CONCAT INDEX_DESC(a bbs_idx1) */ bbs_id, cre_dt, num, c_text
FROM BILLBOARD a
WHERE bbs_id = :bbs
AND ( cre_dt < :v_init_dt
OR ( cre_dt = :v_init_dt AND num <= :v_num ) )
AND ROWNUM <= 20;
웹게시판 부분범위처리 사례5(계층구조의 처리)
답글에 다시 답글이 달릴 수 있는 계층 구조의 게시판 처리
하위 계층의 중간에서 페이지가 끝났을때 기존과 같이 현재 페이지의 마지막이나 처음 로우 정보만으로는 부족하다. 페이지가 어느 단계에서 잘렸든 최상위의 원본글 정보를 가지고 있어야 한다.
계층 구조의 전개를 부분범위로 처리하는 방법 1. 처음 페이지를 처리하는 방법 원본글 들만 이용해 원하는 순서로 계층구조를 전개해 가다가 페이지가 채워지면 처리를 멈춘다. 이전/다음 처리를 위해서 최상위 레벨의 식별자 정보를 보유한다. 2. 다음 페이지를 처리하는 방법 마지막 글이 원본글로 부터 몇번째 순번의 글인지 알아야 한다. 해당 순번의 다음번 부터 잘라온다. => 추출 대상 로우는 한테이지에 나타나는 데이터의 수보다 많아진다. 3. 이전페이지를 처리하는 방법 => SQL을 분리하여 절차형 처리를 가미해야한다.
SELECT ID, PID, C_TEXT,SUBSTR(PATH,3,3) * 1 PARENT_ID -------(e)
FROM (SELECT ROWNUM RNUM, ID, PID, WRITER,
LPAD(' ', 2*LEVEL-1)||C_TEXT C_TEXT,
SYS_CONNECT_BY_PATH(TO_CHAR(ID,'999'),'/') PATH -------(d)
FROM BILLBOARD
CONNECT BY PID = PRIOR IS
AND ROWNUM <=10 + :CNT -------(b)
START WITH PARENT_SW = 1 -------(a)
AND ID >= :START_ID
)
WHERE RNUM BETWEEN :CNT AND 10+ :CNT -------(c)
AND rownum <= 10;
pid + id, paraent_sw + id 의 두개의 인덱스 필요
처음 페이지 처리와 다음 페이지 처리를 할 수 있는 쿼리
:cnt:원본글에서 몇번째의 순번인지 :start_id: 페이지 시작 전의 원본글 번호(d, e를 통해 미리 찾아두어야 하는 부분)
SQL 처리 경로:(a) 행하여 원본글로부터 계층구조를 시작 -> (b) 를 통해 계층 구조를 전개(rownum 조건을 여기에 둔 이유는 connect by 절에 조건을 기술하면 계층구조를 진행해가면서 체크가 일어나기 때문, where 조건에 기술하면 계층구조에 전개 이후에 조건을 체크) -> (c)에서는 rnum을 이용하여 원하는 만큼 잘라냄
문제: 결국엔 start with에서 준 전체 원본글에 대해 모두 전개가 수행되어 부분 범위 처리를 할 수 없다.(계층 구조의 처리는 각 레벨을 전개할 때 액세스하는 즉시 추출할 수가 없기 때문, 옵티마이저의 처리방법)
아래와 같이 쿼리를 분리하여 부분범위 처리를 해야 한다.
p490
원본글을 액세스 하는 부분과 답글을 전개하는 부분으로 쿼리를 분기(각 원본글에 대해 순환전개를 수행)
10개의 글을 게시하며 다음 페이지 처리를 위해 11개의 글을 추출
p492
근본적으로는 다음 페이지를 처리하는 것과 유사
(a)에서 원본글을 액세스할때 인덱스를 역순으로 액세스한다.
배열에 저장하는 부분이 많은 차이점이 있다.(나중 글을 먼저 옮기면서 전개 순서를 원래대로 해야 하기 때문에 loop를 역으로 수행시키고 (112-CNT)를 함
Declare Cursor 를 통해 기준 테이블을 하나씩 Fetch 한 다음 거기서 얻은 상수값을 For Loop 에서 다시 연결하는 SQL을 수행하는 방법.
2.1.1. 전체범위 처리방식에서의 비교
조인(Nested Loops)과 반복연결(Loop Query) 방식의 비교
공통점 : TAB1의 자료 1000건을 읽어 1000번의 인덱스 스캔에 의한 TAB2 테이블 랜덤엑세스
차이점 : 조인은 단 하나의 SQL로 위 과정을 수행하지만 반복연결(Loop Query) 에서는 1001개의 SQL이 수행됨
조인은 SQL 내에서 조인된 테이블의 모든 컬럼을 마음대로 가공하여 사용할 수 있다.
반복연결(Loop Query)은 별도의 언어를 통해 어플리케이션 내에서 추가적인 가공을 해야 한다.
결국 조인이 유리하다.
항상 One SQL 이 유리할까?
반복연결이 유리할 수도 있는 경우 1
조인처리
반복연결
인라인뷰
SELECT a.fld1, ....., a.fldn
, b.col1, ....., b.coln
FROM tab2 b, tab1 a
WHERE a.key1 = b.key2
AND a.fld1 = '10'
ORDER BY a.fld2
;
DECLARE
CURSOR cur1 IS
SELECT a.fld1, ....., a.fldn
FROM tab1 a
WHERE a.fld1 = '10'
ORDER BY a.fld2
;
BEGIN
FOR a IN cur1 LOOP
SELECT b.col1, ....., b.coln
FROM tab2 b
WHERE b.key2 = a.key1
;
END LOOP;
END;
/
SELECT a.fld1, ....., a.fldn
, b.col1, ....., b.coln
FROM tab2 b
, (SELECT a.fld1, ....., a.fldn
FROM tab1 a
WHERE a.fld1 = '10'
ORDER BY a.fld2
) a
WHERE a.key1 = b.key2
;
1) 조인 후 정렬하는 쿼리 2) 우선 a 를 정렬 한 후 b를 반복적으로 읽어오도록 변경한 쿼리
SELECT b.부서명
, SUM(a.매출액) 매출액
FROM tab1 a, tab2 b
WHERE a.부서코드 = b.부서코드
AND a.매출일 LIKE '200503%'
GROUP BY b.부서명
;
DECLARE
CURSOR cur1 IS
SELECT a.부서코드
, SUM(a.매출액) 매출액
FROM tab1 a
WHERE a.매출일 LIKE '200503%'
GROUP BY a.부서코드
;
BEGIN
FOR a IN cur1 LOOP
SELECT b.부서명
FROM tab2 b
WHERE b.부서코드 = a.부서코드
;
END LOOP;
END;
/
SELECT b.부서명
, a.매출액
FROM (SELECT a.부서코드
, SUM(a.매출액) 매출액
FROM tab1 a
WHERE a.매출일 LIKE '200503%'
GROUP BY a.부서코드
) a
, tab2 b
WHERE WHERE b.부서코드 = a.부서코드
;
1) 조인 후 집계(Group By)하는 쿼리 2) 우선 a 를 Group By 한 후 b를 반복적으로 읽어오도록 변경한 쿼리
a를 Group By 하여 b를 읽어 올 횟수가 줄어든다.
3) 인라인뷰를 이용한 조인쿼리
인라인뷰를 이용한다면 반복쿼리의 효과를 낼 수 있다.
반복쿼리처럼 조인쿼리가 가능하므로 굳이 반복쿼리를 사용해야 할 이유가 없다.
반복연결이 유리할 수도 있는 경우 3
조인처리
반복연결
SELECT a.*
, DECODE(a.입출고구분, '1', b.거래처명
, '2', c.공정명
, '3', d.창고명 ) 입출고처명
FROM 입출정보 a
, 거래처 b
, 공정 c
, 창고 d
WHERE a.입출일자 LIKE '200503%'
AND b.거래처코드(+) = DECODE(a.입출고구분, '1', a.입출고처코드)
AND c.공정코드 (+) = DECODE(a.입출고구분, '2', a.입출고처코드)
AND d.창고코드 (+) = DECODE(a.입출고구분, '3', a.입출고처코드)
;
DECLARE
CURSOR cur1 IS
SELECT a.*
FROM 입출정보 a
WHERE a.입출일자 LIKE '200503%'
;
BEGIN
FOR a IN cur1 LOOP
IF a.입출고구분 = '1' THEN
SELECT b.거래처명
FROM 거래처 b
WHERE b.거래처코드 = a.입출고처코드
;
ELSIF a.입출고구분 = '2' THEN
SELECT c.공정명
FROM 공정 c
WHERE c.공정코드 = a.입출고처코드
;
ELSIF a.입출고구분 = '3' THEN
SELECT d.창고명
FROM 창고 d
WHERE d.창고코드 = a.입출고처코드
;
END IF;
END LOOP;
END;
/
스칼라서브쿼리
SELECT a.*
, DECODE(a.입출고구분
, '1', (SELECT b.거래처명 FROM 거래처 b WHERE b.거래처코드 = a.입출고처코드)
, '2', (SELECT c.공정명 FROM 공정 c WHERE c.공정코드 = a.입출고처코드)
, '3', (SELECT d.창고명 FROM 창고 d WHERE d.창고코드 = a.입출고처코드)
) 입출고처명
FROM 입출정보 a
WHERE a.입출일자 LIKE '200503%'
;
1) 연관된 모든 테이블을 아우터 조인
무조건 조인해야 하는 불리함 2) a의 구분값에 따라 b,c,d 를 선택하여 읽어오도록 변경한 쿼리
무조건 조인할 필요 없이 필요한 테이블만 조인하게 됨
3) 스칼라서브쿼리를 이용한 조인쿼리
스칼라서브쿼리를 이용한다면 반복쿼리의 효과를 낼 수 있다.
반복쿼리처럼 조인쿼리가 가능하므로 굳이 반복쿼리를 사용해야 할 이유가 없다.
2.1.2. 부분범위 처리방식에서의 비교
부분범위로 수행되는 SQL은 주어진 처리범위가 아무리 크더라도 매우 빠른 수행속도를 얻을 수 있다.
조인(Nested Loops)과 반복연결(Loop Query) 방식의 비교
공통점 : 부분범위에 필요한 운반단위만큼만 패치하고 멈출 수 있다.
차이점 : 조인은 단 하나의 SQL로 위 과정을 수행하지만 반복연결(Loop Query) 에서는 여러개의 SQL이 수행됨
비교
조인(One SQL)
반복연결(Loop Query)
컬럼가공
SQL 내에서 조인된 테이블의 모든 컬럼을 마음대로 가공
별도의 언어를 통해 어플리케이션 내에서 추가적인 가공
병렬처리
SQL 단위로 병렬처리 가능
Loop 내에서는 병렬처리 포기
튜닝
엑세스 경로를 변경해 준다거나 조인방법을 바꾸는 등 간단하게 수행속도를 향상시킬 수 있다.
조인이란 결국 집합을 연결고리(조인 조건)를 통해 서로 연결하는 것이기 때문에 연결고리 상태가 조인에 커다란 영향을 미치게 된다.
연결고리 상태란 주어진 조인조건의 연산자의 형태와 그들이 가지고 있는 인덱스의 상태를 말한다.
2.2.1. 연결고리 정상
조인의 연결조건 양쪽 테이블에 모두 인덱스가 존재하는 경우
어느 방향으로 조인을 해도 큰 문제는 없어 보인다.
연결고리 정상인 경우 1:M 의 관계에 있다 하더라도 발생되는 연결작업의 논리적인 양은 동일하다.
즉, 연결고리 정상 상태에서 1:M 관계나 연결방향은 수행속도를 좌우하는 큰 요소가 아니다.
수행속도를 좌우하는것은 조인 조건보다는 필터조건에 따른 열결 방향이다.
처리범위를 최대한 줄인 후 연결하느냐 연결한 후에 필요없는 자료를 버리느냐?
조인처리
SELECT a.fld1, ....., a.fldn
, b.col1, ....., b.coln
FROM tab2 b, tab1 a
WHERE a.key1 = b.key2
AND b.col2 LIKE 'AB%'
AND a.fld1 = '10'
ORDER BY a.fld2
;
개념 이해를 위해 극단적인 가정을 해보자.
Tab1의 Fld1 = '10' 을 만족하는 범위 5000 건
Tab2의 col2 LIKE 'AB%' 를 만족하는 범위 100 건
Tab1 : Tab2 = 2 : 1 이며 연결고리로 연결했을때 모두 연결 성공한다.
외부적으론 동일한 결과를 내지만 연결방향에 따라 내부적인 처리량의 차이가 엄청난것을 알 수 잇다.
조인의 양을 먼저 많이 줄여줄 수 있는 테이블을 먼저 엑세스 하면 처리량이 줄어든다.
RBO 모드에서는 이퀄조건을 우선하여 잘못된 엑세스 경로를 선택할 수 있다.
CBO 모드에서는 데이터 분포도에 따라 Tab2가 먼저 엑세스 되도록 실행계획을 수립할 것이다.
하지만 실제 데이터에 따라 달라지는 것이므로 어느 방향이라 단언 할 수 없다.
사용자가 어느방향의 연결이 유리한지를 정확하게 알고 있다면 ORDERED 나 LEADING 힌트로 제어해 주는 것도 좋다.
2.2.2. 한쪽 연결고리 이상
연결고리 이상이 있는 테이블을 먼저 엑세스 하거나 인덱스와 무관하게 소트머지나 해시조인을 수행하도록 해야 한다.
이런 이유로 RBO 에서는 한쪽에 인덱스가 없으면 무조건 없는쪽을 먼저 처리하도록 실행계획을 수립한다.
이를 역이용하여 연결고리를 강제로 끊어(조인조건 가공) 원하는 실행계획을 유도하기도 한다.
조인되는 컬럼이 1:1 로 대응되지 않는 경우
원래 테이블의 키는 a,b,c 이지만 다른 테이블에서는 통함된 d라는 하나의 통합된 외뢰키를 만들어 사용.
애초에 설계 잘못으로 다음 두가지 유형중 하나를 선택하여 연결방향을 고정시켜야 한다.
상황변화에 유연하게 대처하지 못하므로 이런식의 설계는 지양해야 한다.
Tab1==>Tab2
Tab2==>Tab1
SELECT *
FROM tab1, tab2
WHERE a||b||c = d
;
SELECT *
FROM tab1, tab2
WHERE a = SUBSTR(d, 1, 2)
AND b = SUBSTR(d, 3, 1)
AND c = SUBSTR(d, 4, 3)
;
데이터 타입의 차이에 의해 발생되는 경우
연결고리 조인 컬럼의 데이터 타입이 서로 다른 경우
CHAR 와 NUMBER 연결시 CHAR 가 NUMBER 로 자동 형변환되어 한쪽 연결고리 이상 발생
CHAR 와 DATE 연결시 CHAR 가 DATE 로 자동 형변환되어 한쪽 연결고리 이상 발생
마찬가지로 설계의 문제
2.2.3. 양쪽 연결고리 이상
양쪽 모두 연결고리에 인덱스가 없다면 억지 NL조인은 엄청난 횟수의 풀테이블스캔이 발생된다.
따라서 연결고리 상태에 영향을 받지 않는 소트머지나 해시조인 방식으로 실행계획을 수립하게 된다.
물론 대량의 경우 이방식이 더 유리할수도 있으나 어느 한가지 방식만을 고집할수 없으므로
여러 상환에 유연하게 대처가 가능하도록 연결고리 정상상태를 만들어 주는것이 좋겠다.
연결고리 이상 실전 예제 1
BETWEEN
SELECT *
FROM tab1 x, tab2 y
WHERE y.key1 = x.key1
AND y.key2 BETWEEN '200501' AND '200503'
;
y의 인덱스가 key1 + key2 이라면 연결고리 정상
y의 인덱스가 key2 + key1 이라면 연결고리 이상 : 선두컬럼이 = 조건이 아닌 범위 조건임
Between 을 In으로 바꾸어 = 조건을 만들어 줌
IN
SELECT *
FROM tab1 x, tab2 y
WHERE y.key1 = x.key1
AND y.key2 IN ('200501', '200502', '200503')
;
범위조건이 항상 IN 으로 대체 가능한것은 아니다.
이경우 In 서브쿼리를 이용할 수 있다.
IN 서브쿼리
SELECT *
FROM tab1 x, tab2 y
WHERE y.key1 = x.key1
AND y.key2 IN (SELECT ymd
FROM date_dual
WHERE ymd BETWEEN :start_Dt AND :end_Dt
)
;
이론적으로는 연결고리 이상을 만들지 않을 수 있을것 같다.
하지만 서브쿼리가 나타나는 순간 문제는 간단하지가 않다.
tab2가 key2의 조건값을 상수값으로 받기 위해선 서브쿼리가 먼저 수행되어야 하는데
이러한 제약은 이미 tab1과 tab2 사이의 평등관계에 영향을 미치기 때문이다.
연결고리 이상 실전 예제 2
Tab1 > (Tab2 , Tab3) 로 연결된다면 연결고리 정상
Tab2 > Tab1 > Tab3 또는 Tab3 > Tab1 > Tab2 로 연결된다면 연결고리 이상
(Tab2 , Tab3) > tab1 로 연결될수 있다면 연결고리 정상 가능하다.
Tab2 와 Tab3 의 자료가 극히 적다고 가정하고 카티젼곱을 통해 이를 구현한 것이 스타조인이다.
연결고리 이상 실전 예제 3
key1 은 tab1에서 상속받아 tab2, tab3 까지 이어졌다.
이때 (b)와 같이 x와 y를 비교하게 된다면?
z.key1이 상수값으로 주어진다고 해도 x에서는 이를 사용못하며, 반드시 y를 거쳐야만 x를 읽을 수 있다.
z.key 에 상수 조건이 주어진다면 쿼리변환에 의해 x나 y도 바로 상수조건이 적용되지만
z.key 에 상수 조건이 주어진것이 아니므로 실행계획이 한쪽 방향으로만 고정된다.(z > y > x)
이렇듯 조인절을 어떤 테이블끼리 비교하느냐에 따라 연결고리 이상이 발생할 수 있으며
조건절의 테이블 알리아스만 살짝 변경해주는것만으로도 엄청난 성능향상 효과가 발생되기도 한다.
조인이란 데이터와 데이터를 연결하는 작업이다. 우리가 원하는 데이터를 위하여 정규화 되어 있는 정보들을 필요에 의해서 다양한 형태로 결합하여 정보를 얻는다. 조인은, 여러가지 형태가 존재하지만 종류마다 가지고 독특한 장ㆍ단점이 존재한다. 이러하여 조인의 특성을 정확히 알고 상황에 맞도록 적절한 방법을 선택해야한다. 물론, 옵티마이져의 성능이 나날이 좋아지고 있지만, 결국 옵티마이져가 처리 할수 있는 한계는 분명 존재하기 때문이다. 이러한 문제로 인하여 우리가 정확한 조인방법을 이해하여 옵티마이져가 최적의 성능을 발휘 할수 있도록 조정해줄수 있어야한다.
<명확하지않은경우>
<명확한경우>
위 두개의 차이는 A.X_PANY_OCC='Y' 부분 인덱스를 강제로 태우도록 힌트를 주었다.
위의 차이에서 유추 해서 볼수있는 것은 통계정보를 수집하지 않은 상태에서 옵티마이져의 선택은 불불명할수있다 라는걸 알수있다. 인덱스가 존재존재하는경우 이런식으로 강제적으로 인덱스를 태울수 있도록 처리하는것도 하나의 방법일 수 있다.
조인의 형태로 크게 두 가지로 존재하는데, NESTED LOOP JOIN과 SORT MERGE JOIN 이존재한다. 두가지 조인에 대해서 알아보도록 하겠다.
조인 종류별 특징 및 활용 방안
Nested Loop 조인은 가장 전통적인 방법이면서, 보편적인 조인방식이다. 이 조인 방식이 그러한 역할을 하게 되는 가장 근복적인 이유는 먼저 엑서스한 결과를 다음의 엑세스에 상수값으로 제공해 줄 수 있다는데 있다. 하지만, 선행처리결과에서 범위를 적게 줄여 주었는데, 다음 처리할 집합에서 이것을 전혀 활용하지못한다면 이것은 전혀 효율적이지 않다.
Nested Loop 조인은 그림과 같은 절차로 수행하게 된다. SELECT * FROM TAB 1 A, TAB B WHERE A.KEY1 = B.KEY2 AND A.FLD1 ='AB' AND B.FLD2 ='10'
조인되는 어느 한족이 상대방 테이블에서 제공한 결과를 받아야만 처리범위를 줄일수 있는 상황이라면 Nested Loop 조인방식을 선택해야한다.(순차적, 선행적, 종속적)
주로 처리량이 적은 경우 라면 대개 Nested Loop 방식이 가장 무난하다.(랜덤 액세스)
Nested Loop 방식은 연결고리의 인덱스를 이용하기 때문에 연결고리의 상태에 따라 매우 큰 차이가 발생한다(선택적, 연결고리 상태, 방향성)
먼저 수행한 집합의 처리범위의 크기와 얼마나 많은 처리 범위를 미리 줄여 줄 수 있느냐가 수행속도에 많은 영향을 미친다.(순차적,선행적)
부분범위처리를 하는 경우에는 운반단위의 크기가 수행속도에 상당항 영향을 미칠 수 있다.(부분범위처리)
선행 테이블의 처리 범위가 많거나 연결 테이블의 랜덤 엑세스의 양이 아주 많다면 Sort Merge 조인이나 해쉬조인을 검토해야 한다.(체크조건의 영향력)
Nested Loops 조인의 조인의 순서결정
Nested Loop 은 먼저 수행되는 결과가 연결할 범위에 절대적인 영향을 미치기 때문에 어떤 순서로 선행조건이 수행되었는가가 중요하다.
SELECT ........
FROM TAB1 X, TAB2 Y, TAB3 Z
WHERE X.A1 = Y.B1
AND Z.C1 = Y.B2
AND Z.A2 = '10'
AND Y.B2 LIKE 'AB%';
NO
순서
ACCESS PATH
1
TAB1 TAB2 TAB3
A2 ='10' B1 = A1 AND B2 LIKE 'AB%' C1 = B2
2
TAB2 TAB3 TAB1
B2 LIKE 'AB%' C1 = B2 A1 = B1 AND A2 ='10'
3
TAB3 TAB2 TAB1
FULL TABLE SCAN B2 = C1 AND B2 LIE 'AB%' A1 = B1 AND A2 ='10'
데이터 모델의 릴레이션쉽에 따라 조인의 성공률은 달라진다. 여기서 성공률이란 조인되는 집합이 자식 쪽이어서 1'쪽의 집합을 'M' 집합으로 증가시킨 다는 것을 표현한 말이다. 집합이 증가할수록 다음에 조인할 대상이 증가하는 것은 당연하므로 조인의 순서를 결정할 때 가능하다면 'M' 집합은 나중에 처리될수록 좋다. 물론 항상 그렇다는 것은아니다. 여기서 '가능하다면'이라는 말의 진정한 의미는 '자신의 처리범위를 줄여줄 수 있는 다른 집합이 먼저 엑세스 된후' 라는 의미와 자신이 먼저 처리범위를 줄여줄 수 있다면 그들보다 먼저 라는 의미가 함께 포함된것이다.
SELECT .....
FROM ITEM_MST M , ITEM_MOV V , VENDOR A, DEPT D
WHERE M.ITEM_CD = V.ITEM_CD
AND A.VENTOR = V.MANUFACTURE
AND D.DEPTNO = V.ACT_DEPT
AND M.CATEGORY = :B1
AND M.PMA LIKE :B2
AND V.PATERN = :B3
AND V.MOV_DATE BETWEEN :B4 AND :B5
AND D.LOCATION = :B6
위의 SQL은 ITEM_MST 와 ITEM_MOV가 1:M의 관계를 가지며 , ITEM_MOV에 VENDOR 와 DEPT가 '1'쪽 집합으로 연결된걸 알수있다.
이 조인은 ITEM_MST -> ITEM_MOV -> VENDOR -> DEPT 순으로 수행되고있다.
(1) 인덱스를 스캔한 로우 수가 9621 인데 테이블을 액세스한 숫자는 4253이다. 인덱스를 스캔한 개수보다 테이블을 액세스한 숫자가 적다는 것은 인덱스 내부에서 체크하여 걸러졌다는걸 의미한다. ITEM_M_IDX1 인덱스에는 주어진 조건인 CATEGORY와 PMA 사이에 또 다른 컬럼이 존재하고 있었기 때문에 CATEGORY의 처리 범위인 9620개를 스캔하여 PMA 조건을 체크하니 4253개가 되었다는 것이다.
(2) 선행처리되는 ITEM_MST의 4253 개에 대해 M쪽 집합인 ITEM_MOV가 기본키를 이용해 연결을 하고있다. 약 1:57 비율로 연결이 되어 인덱스를 스캔한 숫자는 243209가 되었다. 그러나 테이블을 액세스 한 것이 그 보다 적은 83212이었다는 것은 조건을 받은 PATTERN이나 MOV_DATE중 하나의 기본키에 포함되어 있기는 하지만 그 사이에 다른 컬럼이 존재하고 있어 인덱스 내에 체크 되었음을 알 수 있다.
이렇게 조인한 결과는 (A)에 나타나는데 4218로 크게 감소한 것을 발견할 수 있다.
(3) 이제 상수값이 된 ITEM_MOV의 MANUFACTURE를 이용해 VENDOR 테이블을 연결하고 있다. 물론 기본키로 연결하기 때문에 나타난 숫자는 인덱스, 테이블 스캔 모두 4218이다. 이것은 곧 이 연결을 통해서 아무것도 줄여 주지 못했음을 의미한다.
(4) 마찬가지 방법으로 DEPT를 연결하였다. 그러나 (B)를 보면 조인에 최종적으로 성공한 것은 124개에 불과하다는 것을 알 수 있다. 이것은 바로 DEPT에 부여한 조건인 LOCATION으로 인한 것임을 쉽게 알 수 있다. 대부분 처리가 완료된 후에 버리게 되었다는것은 비효율을 발생한 것은 틀림없다.
위 실행계획을 분석한 결과 문제점이 발견된걸 확인할수있다. ITEM_MOV의 MOV_DATE와 DEPT의 LOCATION 이었다. VENDOR는 전혀 범위를 줄여주지 못한것도 알수있다.
그렇다면 제일많은 데이터를 가진 ITEM_MOV 테이블을 가장 먼저 최소 범위를 만드는게 중요하다. MOV_DATE와 LOCATION이 :B6이 가진 DEPTNO 가 협력해서 처리할 범위를 최소화 시켜주는것이다. VENDOR는 처리범위를 줄여주지못하기 때문에 제일 나중에 조인되어야한다.
가장유리한 처리순서는 DEPT->ITEM_MOV->ITEM_MST->VENDOR 순이다. 많은범위 처리를 줄여주었던 DEPT 의 LOCATION이 젤먼저 범위를 줄여줘야지만 가장 먼저 엑세스 하여 해당 DEPTNO만 ITEM_MOV만 제공하여야 한다.
이때 가장 이상적인 인덱스 구조는 'ACT_DEPT+PATTERN+MOV_DATE' 이다.
ITEM_MST가 세번째로 연결되어야 하는 이유는 여기서도 일부분 범위가 줄어들기 때문이다. 이 조인은 OTEM_CD 로만 생성된 기본키로 'UNIQUE_SCAN'을 하게 되므로 연결이 된 후에 나머지 조건인 CATEGORY와 PMA를 체크할수 밖에없다. 만약에 ITEM_MST의 조건인 CATEGORY, PMA 조인인덱스로 구성되어있다면 최적의 조인이 수행될것이다.
가령, 'ACT_DEPT+PATTERN+MOV_DATE'로 인덱스를 구성하지못하고 PATTERN+MOV_DATE로 된 인덱스를 사용할 수 밖에없다면, ITEM_MOV->DEPT->ITEM_MST->VENDOR 순으로 조인되는게 가장 유리하다.
조인의 순서를 결정하기 위한 구체적인 방법에 대해서 알아보도록 하겠다.
대부분의 조인쿼리는 몇 개의 트랜잭션 데이터가 들어가 있는 일종의 팩트 테이블이 존재한다,. 개념적으로는 하나의 집합이지만 정규화로 인해 이러한 모양이 될 수 있다. 그들의 주변에는 여러 개의 참조 모델들이 관계를 맺게 된다.
이러한 참조 모델들은 필수적(MANDATORY)관게를 가지거나, 선택적(OPTIONAL) 관계를 가지게 되며, 그 중에서는 아우터 조인으로 연결되기도 한다. 경우에 따라 D0 처럼 몇단계 걸친 관계의 조인도 발생한다.
조인을 하는경우 가장먼저 해야 할일은 조인하는 집합중에서 제일 하위 집합을 찾아내는일이다. 그림에서는 FACT2 테이블인걸 확인할수있다. 이테이블 기준으로 또다른 팩트 테이블이 FACT1을 찾을수가 있다.
FACT1 테이블의 자기부여 상수조건은 D1, D2, D0 FACT2 테이블의 자기부여 상수조건은 D3, D4, D5 인데 D5는 OUTER 조인이므로 제외한다. 이유는 아우터 조인은 제공자 역할을 할 수 없기 때문이다.
이런식으로 확보된 조건을 확인하여 처리범위를 많이 줄일수 있는지 비교해본뒤 애매한 경우 SROT MERGE 조인이나 해쉬조인으로 수행하는 경우가 많다.
우열을 가려졌으면 각 테이블이 보유한 인덱스 구조를 참조하여 다시한번 평가한다. 아무리 많은 조건이 걸려있어도 인덱스에 결합 되지 않았다면 인덱스 머지를 하지 않고서는 그중 하나만 선택되고 나머지는 체크 조건만 역할 한다.
WHERE 절에 사용된 조건들은 어떤 수단으로 액세스를 했든지 소량의 집합이 된다면 선행되어야 할 이유는 충분히 존재한다. 그것은 다음 선행되는 처리범위를 줄여주기 때문이다.
또한, 자신은 비록 처리범위를 줄이지 못했지만 그 결과가 다른 조인에 좋은 영향을 미칠 수 있다면 선행처리 될수 있도록 한다.
집합을 증가 시키는 'M'쪽 집합과 조인이 남아 있다면 일단 그 집합을 조인하기 전에 남아 있는 연결 가능한 디멘전 테이블과 조인을 우선적으로 검토 하는 것이 좋다. 그 중 자신의 범위를 줄여줄수 있는 조건이 있다면 보다 많은 양을 줄일수 있는 것부터 조인을 하고, 아우터 조인은 마지막으로 보낸다.
여기까지 순서가 결정되었다면 'M'쪽 집합과 조인을 한다. 가능한 체크조건을 활용하여 조금이라도 줄여 줄 수 있는 것들부터 조인을 실시한다.
지금까지 처리된 집합의 결과를 조인하고자 하는 집합에 결과를 제공했을 때 그것을 받는것이 훨씬 유리하다면 NESTED LOOP 아니면, SORT MERGE나 해쉬 조인으로 유도해야한다.
최적의 처리경로를 알고 있어도 실행계획을 그렇게 나타나지 않는 경우가 존재한다. 그런경우에는 적절한 힌트나 인라인류를 잘활용 하는 방법이있다. 그래도 안되면 특정 컬럼의 인덱스 사용 억제를 활용하라! 그것도 또 안되면 ROWNUM을 삽입하여 특정 인라인뷰가 먼저 수행되도록 하는 등 갖가지 수단과 방법을 동원하라.
Sort Merge 조인
Nested Loop 조인처럼 각각의 연결할 대상에 대해 일일이 랜덤 액세스를 하지 않으려면 어떻게 하는 방법이있을까? 조인할 집합의 대응되는 대상은 서로 알 수 없는 임의의 장소에 위차한다. 이것을 연결 가능하게 하는 방법은 서로 사전 작업을 통해 연결할 수 있는 모습으로 다시 배치를 하게 해야 한다. 여기서 연결을 할 수 있도록 사전에 하는 재배치를 정렬(Sort) 방식으로 하여, 이들을 서로 연결(Merge)하는 방식이 Sort Merge 조인이다
Sort Merge 조인은 그림과 같이 절차로 수행하게 된다.
Sort Merge 조인의 특징 및 적용기준
전체범위 처리를 할 수 밖에 없는 프로세싱에서 검토된다(전체범위처리)
상대방 테이블에서 어떤 상수값을 받지 않고서도 충분히 처리범위를 줄일 수 있다면 상당한 효과를 기대할 수 있다.(독립적)
주로 처리량이 많으면서 항상 전체범위 처리를 해야 하는 경우에 유리해진다.(전체범위처리)
연결고리 이상 상태에 영향을 받지 않으므로 연결고리르 ㄹ위한 인덱스를 생성할 필요가 없을때 매우 유용하게 사용할수 있다.(스캔방식)
스스로 자신의 처리범위를 어떻게 줄일 수 있느냐가 수행속도에 많은 영향을 미치므로 보다 효율적으로 액세스할 수 있는 인덱스 구성이 중요하다.(선택적)
전체범위처리를 하므로 운반단위의 크기가 수행속도에 영향을 미치지 않는다.(무방향성)
처리할 데이터 량이 적은 온라인 애플리케이션에서는 Nested Loops 조인이 유리한 경우가많다.(Nested Loop조인비교)
옵티마이져 목표(Goal)가 'ALL_ROWS'인 경우는 자주 Sort Merge 조인이나 해쉬조인으로 실행계획이 수립되므로 부분범위 처리를 하고자 한다면 이 옵티마이져 목표가 어떻게 지정되어있는지에 주의하여야한다.(Nested Loop조인비교)
충분한 메모리 활용이 가능하고, 병렬처리를 통해 빠르게 정렬작업을 할 수 있다면 대량의 데이터 조인에 매우 유용하게 적용할 수 있다.(Nested Loop조인비교)
Nested Loops 조인& Sort Merge 조인 비교
해당SQL를 실행하였을때 두조인에 대해서 비교해보자. SELECT * FROM TAB1 A, TAB2 B WHERE A.KEY1 = B.KEY2 AND A.FLD1 = '111' AND A.FLD2 LIKE 'AB%'<== 삭제
Nested Loops 조인으로 수행했을경우 처리철차를 확인해보자
이 조건은 수행했던 일의 모든조인이 완료된 다음 단지 최종적으로 체크하는 역할을 했다. 이 체크 기능이 없어짐으로써 달라진 일은 성공 결과값이 늘어난것이다. 이와 같이 Nested Loops 조인에서는 어느 한쪽의 조건이 없어지더라도 영향을 크게 받지 않을 수도 있으며, 경우에 따라서 오히려 유리해질 수도 있다.
Sort Merge 조인으로 수행을 하였을경우 처리절차를 확인해보자.
위와같이 TAB2의 COL1 ='10'인 조건이 빠짐으로써 발생하는 차이를 살펴보자면, Nested Loops 는 TAB2의 처리범위를 줄여 주는 중요한 역할을 했지만, 이 조건이 없어짐으로써 이제 TAB2는 전체 테이블을 모두 스캔하여 부담되는 정렬의 범위가 크게늘어나고, 머지할 양도 크게증가하였다. 이와 같이 한쪽에 조건을 삭제해 보았더니 nested Loops 조인은 거의 영향은 받지않았으나 Sort Merge 조인은 엄청난 일의 증가를 가져왔다. 이것으로 두개의 조인의 차이점은 확인할수있다.
아래 SQL은 ORDER BY 를 하였으므로 전체범위를 처리하게 될것이다. SELECT * FROM TAB1 A, TAB2 B WHERE A.KEY1 = B.KEY2 ORDER BY A.FLD5, B.COL5 SQL이 전체범위처리를 Nested Loops 조인으로 수행하였을 때 처리되는 절차를 확인해보자
조인되는 양쪽에 모주 조건이 없으므로 어느 한쪽이 먼저 전체 테이블을 스캔한다. 여기서는 TAB1 읽혀진 KEY1의 상수값에 대응되는 로우를 KEY2 인덱스에서 찾아 ROWID로 TAB2의 해당 로우를 액서스한다. 이 처리방식은 선행테이블이 전체 테이블을 스캔하므로 전체 테이블을 대상으로 랜덤 액세스가 발생한다. 이번에는 동일한 SQL로 Sort Merge 조인으로 수행시켰을 때의 어떤 차이를 내는지 확인해보자.
조인되는 양쪽에 모두 조건이 없으므로 각각의 테이블마다 전체 테이블을 스캔하여 연결고리가 되는 컬럼으로 정렬한 후 머지한다. 어차피 전체 테이블을 조인해야 한다면 랜덤으로 전체 테이블을 액세스하는 것보다 스캔방식으로 전체 테이블을 액세스하는 것이 당연 유리하다. 이와 같이 전체 테이블에 대한 Sort Merge 조인에서는 대량의 랜덤 액세스를 피할 수 있으므로 Nested Loops 조인에 비해 훨씬 더 유리해진다.
해쉬(Hash)조인
전통적인 조인방식인 Nested Loops 방식과 Sort Merge 방식은 장-단점이 매우 대조적이어서 서로의 단점을 보완해 주는 대체 수단으로서 활용되어 왔다. 데이터 처리범위가 날이 갈수록 초대형으로 증가하면서 이제 많은 부분에서 Sort Merge 가 더 이상의 대안이 될 수 없는 지경에 이르렀다. 이에 대한 해결책이 필연적으로 요구되었고ㅡ 거기에 부응해서 나타난 솔류션이 바로 해쉬조인이라고 할 수 있다. 해쉬 조인은 대용량 처리의 선결조건이 랜덤과 정렬에 대한 부담을 해결할 수 있는 대안으로서 등장하게 되었다. 물론, 이런 문제는 초대용량 데이터가 아니라면 클러스터링 팩터를 향상시킨다든지, 정렬영역을 늘여 주는 방법으로 상당한 효과를 얻을 수도 있다. 그러나 초대량 데이터라면 이미 그러한 방법으로서는 도저히 버틸 수 없게 된다는 데 문제의 심각성이 있다. 해쉬 조인이 이러한 면에서 강점을 가지는 이유는 연결행위마다 인덱스를 경유하여 랜덤을 하지 않고 해쉬함수를 이용한 연결을 한다는 점과 파티션단위로 처리하기 때문에 대량의 처리에도 수행속도가 급격히 상승하지 않는다는 것에 있다. 해쉬 조인은 가장 기본적인 원리는 해쉬함수를 활용하는데 있다, 원래 수학에서 말하는 함수란 어떤 값을 대입하면 어떤 연산을 처리하여 결과값을 리턴하는 것이다. 이와 마찬가지로 데이터의 컬럼에 있는 상수값을 입력으로 받아 '위치값'을 리턴하는 것을 해쉬함수라고 이해하면된다.
해쉬영역(Hash Area)
해쉬 영역이란 해쉬 조인을 수행하기 위해 메모리 내에 만들어진 영역을 말한다. 이 영역은 비트맵 벡터와 해쉬 테이블, 그 리고 파티션 테이블 영역으로 구성되어 있다. 해쉬 테이블에는 파티션들의 위치정보를 가지고 있으며 조인의 연결 작업을 수행할 때나 디스크에 내려가 있는 파티션 짝들을 찾는데 사용된다. 파티션 테이블에는 여러 개의 파티션이 존재하며, 조인할 집합의 실제 로우들을 가지고 있는 영역이다. 해쉬영역이 부족하면 디스크를 사용할 수 밖에 없어 수행속도에 지대한 영향을 미치게 되므로 해쉬영역크기를 적절하게 지정하는 것은 매우 중요하다.
파티션(Partition)
파티션이란 파티션을 결정하기 위해 수행하는 첫 번째 해쉬함수가 리턴한 동일한 해쉬값을 갖는 묶음을 말한다. 즉, 동일한 해쉬값을 가진 로우들의 버켓을 말한다. 특히 빌드입력이 인_메모리에 작업이 불가능하면 반드시 파티션으로 나뉘어져야 한다. 이렇게 만들어진 파티션 수를 팬아웃이라고 부른다. 하나의 파티션은 여러 개의 클러스트로 분리된다, 2차 해슁을 하면 저장할 클러스터의 위치가 결정되며, 이 단위는 I/O의 단위가 될 뿐만 아니라 검색의 단위로도 활용된다. 파티션 수를 많게 하면 - 즉, 팬아웃을 크게하면 적는 크기의 많은 파티션이 생성되기 때문에 비효율적인 I/O가 발생할 수 있다. 반대로 너무 적게하면, 지나치게 큰 파티션이 생성되어 해쉬영역과 맞지 않을 수도 있으므로 이에 대한 결정은 해쉬 조인의 효율에 큰영향을 미친다.
클러스터(Cluster)
파티션은 작지 않은 크기로 되어 있기 때문에 이를 다시 클러스터 단위로 분할한다. 이 클러스터는 연속된 블록으로 되어 있으며 디스크와 I/O를 하는 단위가 된다. 물론 주어진 파라메터에 의해 동시에 I/O 하는 양이 결정된다. 특히 해쉬 ㅋ클러스터링을 했을 때와 매우 유사한 형태로 이해하는 것이 좋다. 해쉬 클러스터링을 했을 ?는 해쉬함수에서 생성된 값이 같으면 동일한 클러스터에 저장되고, 이를 검색할 때는 해쉬값으로 해당 클러스터를 찾아 클러스터를 스캔하면서 원하는 로우를 찾는다. 캐비닛을 파티션이라고 한다면 슬롯은 서랍이라고 생각하면 이해가 빠를 것이다. 우리가 물건을 찾을 때도 먼저 캐비닛을 결정한 다음 해당 서랍을 열어서 물건을 꺼내는 것과 매우 유사하다고 하겠다.
빌드입력(Build Input)과 검색입력(Probe Input)
조인을 위해 먼저 액세스하여 필요한 준비를 해두는 처리를 빌드입력이라 하며, 나중에 액세스하면서 조인을 수행하는 처리를 검증 혹은 검색입력이라고 한다.
인_메모리(In-memory)해쉬조인과 유예 해쉬조인
해쉬 조인에는 전체 빌드입력이 해쉬영역보다 적은 경우와 그렇지 않은 경우에 따라 처리 방식에 큰 차이가 있다. 그러므로 무조건 적은 집합을 가진 것이 빌드입력이 되도록 해야 함은 당연하다. 빌드입력이 해쉬영역에 모두 위치할 수 있는 경우는 인_메모리 해쉬조인을 수행하게 되고, 그렇지 못한 경우에는 유예 해쉬조인을 수행하게 된다. 유예 해쉬 조인은 먼저 전체를 빌드입력과 검색입력을 수행하여 여러 개의 파티션에 분할하고, 해쉬영역을 초과할 때마다 임시 세그먼트에 파티션을 저장한다.
비트맵 벡터(Bitmap Vector)
빌드입력에 대해서 파티션을 구하는 작업 중에 생성되며, 빌드입력값들에 대한 유일 값을 메모리 내의 해쉬영역에 정의하는 것을 말한다. 해쉬영역에 만들어지는 비트맵 벡터는 빌드입력의 전체 집합에 대해서 생성된다. 즉, 처리대상이 커서 해쉬영역을 초과하여 임시 세그먼트에 저장하게 되더라도 처리할 빌드 입력의 모든 집합에 대해 조인종료 시 까지 유지된다. 검색입력에 액세스한 것이 어차피 빌드입력에 존재하지 않는다면 굳이 파티션에 위치시킬 필요조차 없기 때문에 이를 필터링 하는 중요한 역할을 하게 된다. 이 작업은 파티션을 결정하기 위해 해쉬함수를 적용하기 전에 수행하여 불필요한 대상들을 걸러내므로 실제 조인에 참여할 대상을 경우에 따라 크게 감소시킬 수 있다, 일반적으로 이 영역은 메모리에 정의하는 해쉬영역의 5%로 생성된다.
해쉬 테이블(Hash Table)
최종적으로 조인의 연결작업에 대응되는 로우를 찾기 위한 해쉬 인덱스로 사용된다. 마치 Nested Loops 조인에서 나중에 대응되는 로우를 인덱스에서 찾는 것과 유사하지만 물리적인 인덱스가 미리 생성되는 것이 아니라 조인을 수행할 때 임시적으로 생성된다는 것이 다른 점이다. 물론 그 인덱스 구조는 해슁을 활용하는 방법이다. 일반 인덱스에서 인덱스 컬럼값과 ROWID를 가지고 있는 것과 유사하게 해쉬 테이블은 해쉬키값과 해쉬 클러스터의 주소를 가지고 있다. 일반적인 인덱스는 테이블의 로우만큼의인덱스 로우를 가지고 있지만 해쉬 테이블에는 실제 테이블의 로우 별로 인덱스를 가지고 있지는 않는다. 이는 마치 해쉬 클러스터링 테이블에서 해쉬 클러스터로 액세스 하는 것과 유사한 개념이다. 일반적인 인덱스 연결은 최악의 경우 하나의 연결을 위해 한 블록이 액세스 될 수 있다. 그러나 해쉬 조인의 연결에서는 파티션 짝(Pair)들간에는 연결에 필요한 데이터가 100%존재하는 것이 보장되기 때문에 메모리 내에 해쉬를 적용하여 수행하는 연결은 일반적인 랜덤과 비교할 수 없이 빠르다.
파티션 테이블(Partition Table)
만약 빌드입력이 메모리 크기를 초과하여 파티션을 생성하게 되면 '파티션 테이블'에 관련정보가 저장된다. 또한 디스크의 임시 세그먼트로 이동하면 그 위치정보를 갖는다. 이 위치 정보는 나중에 다시 메모리로 올려서 처리할 대상을 선정할 때 활용된다. 또한 빌드입력과 검색입력에서 생성된 파티션 간에 서로 짝을 찾는데도 활용된다. 처리되는 단위마다 동적으로 역할이 변할 수 있으며, 이것을 일컬어 동적 역할 반전이라고 부른다. 이러한 개념을 적용하는 이유는 인_메모리 해쉬 조인의 가능성을 높이기 위한 전략 때문이다. 해쉬 테이블은 해당 처리 단위의 연결작업을 위한 인덱스 개념으로 사용되지만 다른 구 가지는 전체 작업에 필요한 정보를 가지고 있기 때문이다. 이들의 합이 전체 해쉬영역의 15%~20% 넘으면 오버헤드가 발생하므로 이런 경우에는 해쉬영역을 증가시키는 것이 바람직하다. 통계정보에 있는 히스토그램을 이용하여 파티션의 크기나 파티션 내의 데이터 분포를 동적으로 관리하여, 이 결과를 다음 번 수행 시에 활용하여 파티션의 크기를 결정하기도 한다. 물론 이러한 기능은 DBMS나 버전에 따라 차이가 있다.
인_메모리 해쉬조인
빌드입력을 모두 메모리에 저장하고 해쉬 테이블을 만들어 검색입력을 스캔하면서 조인을 수행한다. 비록 모양상으로 랜덤이지만 거의 부담이 없는 랜덤을 수행하기 때문에 Sort Merge 조인처럼 연결을 위해서 정렬을 해야 하거나 Nested Loops 조인처럼 수많은 블록을 액세스하지 않아도 되므로 대용량 데이터의 조인에 매우 효과적이다. 또한, Nested Loops 조인만 가질 수 있는 부분범위 처리도 가능하게 되므로 만약 어느 한쪽의 처리범위가 크지 않는 경우라면 매우 효율적인 조인방법이다.
1) 통계정보를 참조하여 보다 효과적인 카디널러티를 갖는 집합을 빌드입력으로 선택한다. 일반적으로 조인은 1:1이나 1:M 관계에서 발생하므로 대부분의 경우 '1' 족 집합이 빌드입력이된다. 2) 팬아웃, 즉 파티션 수를 결정한다. 파ㅣ션의 수와 크기는 성능에 큰 영향을 미치게 되므로 히스토그램 정보를 입각하여 이를 동적으로 최적화하여 결정하게 된다. 3) 빌드입력의 조인키에 1차 해슁함수를 적용하여 저장할 파티션을 결정한다. 4) 2차 해슁함수를 적용하여 해쉬값을 생성한다. 5) 이 값을 이용하여 해쉬 테이블을 만들고, 해당파티션의 슬롯에 저장한다. 이때 저장되는 컬럼은 SQL의 SELECT-List 에 있는 컬럼들도 같이 저장된다. 6) 검색입력의 필터링을 위해 사용할 비트맨 백터를 생성한다. 이 값은 유일한 값으로 만들어지므로 처리할 값을 찾아본 후 없으면 생성하고 있으면 그대로 통과하는 방식으로 생성된다. 7) 이러한 방식으로 빌드입력의 처리범위를 모두 처리할 때까지 반복해서 수행한다. 8) 이번에는 검색입력의 처리범위를 액세스하기 시작하여 조건을 만족하지 않으면 버리고 그렇지 않으면 다음을 실행한다. 9) 첫 번째 해슁함수를 적용하여 비트맨 백터를 필터링 한다. 물론 여기에서 찾을 수 없다면 해당 처리는 종료되고 다음 검색입력 대상으로 넘어간다. 10) 필터링을 통과한 것은 2차 해슁함수를 적용하여 해쉬 테이블을 읽고 해당 파티션을 찾아 슬롯에서 대응 로우를 찾는다. 11) 조인이 되면 SELECT-List 에 기술된 로우를 완성하여 운반단위에 태운다. 12) 이러한 작업을 반복해서 수행하여 계속 운반단위로 보낸다. 13) 정해진 운반단위가 채워지면 리턴한다. 빌드입력은 전체범위를 모두 처리했지만 검색입력은 운반단위가 채워지면 먼저 리턴을 할 수 있으므로 부분적으로 나마 부분범위 처리가 가능해진다. 그러나 실제로는 일반적으로 빌드입력은 크지 않으므로 거의 Nested Loops 조인과 유사한 부분범위 처리가 가능하다고 할 수 있다. 14) 이러한 방식으로 검색입력의 처리범위가 끝날 때까지 반복해서 수행한다.
유예 해쉬조인
빌드입력이 해쉬영역을 초과하면 해쉬조인은 좀더 복잡한 과정을 거치게 된다. 그 이유는 빌드입력이 해쉬영역을 초과하게 되면 어쩔 수 없이 디스크에 저장을 할 수 밖에 없기 때문이다. 빌드입력의 일부라도 디스크에 저장을 할 수 밖에 없게 된다면 마치 정렬을 통해 연결을 하는 Sort Merge 조인처럼 무엇인가 정렬을 유사한 효과를 얻을 수 있는 방법이 있어야 한다. 정렬을 하지 않고서도 연결이 가능하도록 데이터를 위치시키는 방법은 따로 해슁함수를 적용하는 것이다. Sort Merge 조인은 각각의 집합을 먼저 정렬을 한 후 그것은 머지하는 방식으로 연결을 수행하고, 해쉬조인은 각각의 집합에 대해서 먼저 해슁함수를 적용하여 같은 해쉬값을 갖는 파티션에 저장을 한 후 그들을 짝을 찾아 연결을 수행한다.
1) 통계정보를 참조하여 보다 효과적인 카디널러티를 갖는 집합을 빌드입력으로 선택한다. 2) 파티션 수를 결정한다. 3) 빌드입력의 조인키에 대하여 1차 해슁함수를 적용하여 저장할 파티션을 결정한다. 4) 2차 해슁함수를 적용하여 해쉬값을 생성한다. 5) 이 값을 이용하여 해쉬 테이블을 만들고, 해당 파티션의 슬롯에 저장한다. 이때 저장되는 컬럼은 SQL 의 SELECT-List 에 있는 컬럼들도 같이 저장된다. 6) 검색입력의 필터링을 위해 사용할 비트맵 백터를 생성한다. 여기서 생성된 값을 이용해 다음에 수행될 검색입력의 필터링을 하게 되므로 유예 해쉬조인에서도 크기가 작은 집합이 빌드입력이 대상을 크게 줄이는 효과를 볼수있다. 여기까지는 인_메모리 해쉬조인과 동일하다. 7) 이러한 방식으로 빌드입력의 처리범위를 처리하다가 해쉬영역을 초과하면 파티션 테이블에 위치정보를 남기고 디스크로 이동하게 된다. 파티션 테이블에 있는 정보는 나중에 파티션 짝을 찾아 연결작업을 수행할 때 사용된다. 8) 빌드입력의 모든 처리범위를 위의 방법으로 끝까지 수행한다. 9) 이번에는 검색입력의 처리범위를 액세스하기 시작하여 조건을 만족하지 않으면 버리고, 만족한 것들은 1차 해슁함수를 저?하여 비트맵 백터를 필터링 한다 비트맵 백터에서 찾을수 없다면 해당 건의 처리는 종료되고 다음 검색입력 대상으로 넘어간다. 10) 필터링을 통과한 것은 2차 해슁함수를 적용한다. 만약 이때 검색입력에 대응되는 빌드입력이 메모리 내에 존재하면 해쉬 테이블을 읽어 연결을 수행하고, 그렇지 않으면 해당 파티션에 저장한다. 11) 연결을 수행할 수 없는 파티션들을 디스크에 저장한다. 12) 이러한 작업을 검색입력의 모든 처리범위에 대해 반복해서 수행한다. 13) 처리되지 않은 파티션들을 처리하기 위해 파티션 테이블의 정보를 이용하여 파티션 짝들을 디스크에서 메모리로 이동시킨다. 14) 새로 메모리로 이동한 집합 중에서 크기가 작은 집합으로 해쉬 테이블을 생성한다. 즉, 처리할 파티션 짝들을 모았을 때, 그 크기에 따라서 빌드입력과 검색입력이 다시 결정된다. 그러므로 경우에 따라서 최초에 결정되었던 빌드입력과 검색입력의 역할이 바뀔 수가 있다. 15) 검색입력으로 결정된 집합을 스캔하면서 해쉬 테이블을 이용하여 연결을 수행한다. 물론 운반단위에 모았다가 채워지면 리턴하는 것은 당연하다. 이러한 작업을 남아 있는 모든 대상에 대해서 실시한다.
유예 해쉬조인의 특징과 적용기준
-조인의 연결을 위해서는 기존에 미리 생성되어 있는 인덱스를 전혀 사용하지 않음 (조인 연결고리에 인덱스가 없어도 영향을 받지 않는다는 것과 그러면서도 오히려 기존의 B-Tree 인덱스 보다 더 유리한 해쉬를 이용한 조인을 할수 있기 때문이다. 이러한 집합의 연산과정은 이미 가공된 집합일 수 있기 때문에 인덱스가 존재하지 않을 수도 있음은 물론이고, 대개의 경우 대용량의 처리이기 때문에 데이터 량의 증가에 따른 부담이 최소화 될 필요가있다.) -부분범위 처리가 불가능하다.(해쉬조인은 비록 해쉬영역을 초과하는 대용량의 데이터라 하더라도 해쉬함수를 이용하여 적절한 위치에 옮겨 두었다가 조인대상들을 다시 불러 들여 해쉬 테이블을 통해 조인을 수행하므로 Sort Merge 조인이 갖는 최대의 약점인 대용량 데이터의 정렬에 대한 오버헤드를 해결하는 최적의 수단이다. 사실 이 경우 따라 조인 방식을 결정하는 매우 중요한 기준이 된다.)
세미조인이란? 말 그대로 조인과 유사한 데이타 연결( 서브쿼리를 사용했을 때 메인 쿼리와 연결하는 처리를 의미 )
2.3.5.1 세미 조인의 개념 및 특징
세미 조인의 개념.?
세미 조인이란 말은 본래 분산질의를 효율적으로 수행하기 위하여 도입된 개념이다.
분산질의 : 두 테이블 간에 조인을 할 때 한 테이블을 다른 사이트에 전송하기 전에 먼저 조인에 필요한 속성만을 추출(프로젝션)하여 전송한 후 조인에 성공한 로우의 집합만을 다시 전송함으로써 네트워크를 통해 전송되는 데이터의 양을 줄이고자 하는 개념으로 도입
분산질의
-- Mgr : M 사이트
-- Emp : E 사이트
SELECT mgr.name, emp.name
FROM mgr, emp
WHERE mgr.sal < emp.sal;
|
1 : M 사이트에서 SELECT sal FROM mgr 질의문을 수행한다 2 : 조인 속성만 추출( Projection) 하여 E 사이트로 전송하고 이를 mgrP라고 하자 3 : E 사이트에서 SELECT name, sal FROM emp WHERE sal > mgrP.sal를 수행한다. 4 : 3의 결과는 상대적으로 작은 양일 것이고 이를 M 사이트로 전송한다.
여기서 설명하는 세미조인?
분산질의의 효율적인 수행을 위해 수행하는 협의의 조인만을 말하는 것이 X
주로 서브쿼리르 사용했을 때 메인 쿼리와의 연결을 하기 위해 적용되는 광범위한 유사 조인을 의미하고 있다.
조인된 결과 집합의 차이
조인 : 수학적 곱셈 연산관 유사함
세미조인 : 언제나 메인쿼리의 집합과 동일함.
2.3.5.2 세미조인의 실행 계획
원리적인 측면에서 볼 때는 결코 일반적인 조인의 범주를 벗어 나지 않는다고 하였다. 그렇다면 결국 가장 보편적인 조인이 Nested Loops 조인이었듯이 세미 조인에서도 대부분의 경우는 Nested Loops조인과 매우 유사하게 처리된다는 것을 뜻한다.
가) Nested Loop형 세미조인
제공자 : 서브쿼리가 먼저 수행되어 SELECT-List의 연결고리 값을 상수값으로 만들고, 이것을 메인쿼리 연결고리에 대응시키는 방법
확인자 : 메인쿼리가 먼저 수행되어 상수값이 된 연결고리 값을 서브쿼리의 연결고리( 서브쿼리 SELECT-List에 있는 컬럼 )에 제공하는 방법
그림 2-2-24 준비스크립트
11:45:32 SQL> SELECT * FROM V$VERSION WHERE ROWNUM <= 1;
BANNER
----------------------------------------------------------------
Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 - 64bi
DROP TABLE TAB1 PURGE;
DROP TABLE TAB2 PURGE;
CREATE TABLE TAB1 AS
SELECT LEVEL KEY1
, '상품'||LEVEL COL1
, TRUNC( dbms_random.value( 1,7 ) ) * 500 + TRUNC( dbms_random.value( 1,3 ) ) * 750 COL2
FROM DUAL
CONNECT BY LEVEL <= 10000;
CREATE TABLE TAB2 AS
SELECT LEVEL KEY1
, TRUNC(dbms_random.value( 1, 10000 ) ) KEY2
, TRUNC( SYSDATE ) - 100 + CEIL( LEVEL/ 1000 ) COL1
, 'A' COL2
FROM DUAL
CONNECT BY LEVEL <= 100000;
CREATE INDEX KEY1_IDX ON TAB1( KEY1 );
CREATE INDEX COL1_IDX01 ON TAB2( COL1, COL2 );
EXEC DBMS_STATS.GATHER_TABLE_STATS( USER, 'TAB1' );
EXEC DBMS_STATS.GATHER_TABLE_STATS( USER, 'TAB2' );
SQL> DESC TAB2;
이름 널? 유형
----------------------------------------------------------------------------------------------------------------- -------- -
KEY1 NUMBER
KEY2 NUMBER
COL1 DATE
COL2 CHAR(1)
SQL> SELECT /*+ gather_plan_statistics */ COUNT( DISTINCT KEY2 )
2 FROM TAB2 A
3 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
4 AND COL2 = 'A';
COUNT(DISTINCTKEY2)
-------------------
1810
경 과: 00:00:00.02
SQL> @xplan
---------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
---------------------------------------------------------------------------------------------------------------------------------
| 1 | SORT GROUP BY | | 1 | 1 | 1 |00:00:00.01 | 15 | 83968 | 83968 |73728 (0)|
|* 2 | FILTER | | 1 | | 2000 |00:00:00.01 | 15 | | | |
| 3 | TABLE ACCESS BY INDEX ROWID| TAB2 | 1 | 2000 | 2000 |00:00:00.01 | 15 | | | |
|* 4 | INDEX SKIP SCAN | COL1_IDX01 | 1 | 40 | 2000 |00:00:00.01 | 8 | | | |
---------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
4 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL2"='A' AND "COL1"<=TRUNC(SYSDATE@!))
filter("COL2"='A')
|
NO_HINT 옵티마이져가 서브쿼리를 조인으로 변형시킴 ( 그림 2-2-24 )
SQL> SELECT /*+ gather_plan_statistics */ COUNT( * )
2 FROM TAB1 A
3 WHERE KEY1 IN ( SELECT KEY2
4 FROM TAB2 B
5 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
6 AND COL2 = 'A' ) ;
COUNT(*)
----------
1810
경 과: 00:00:00.02
SQL> @xplan
----------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
----------------------------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 41 | | | |
|* 2 | FILTER | | 1 | | 1810 |00:00:00.01 | 41 | | | |
|* 3 | HASH JOIN RIGHT SEMI | | 1 | 1830 | 1810 |00:00:00.01 | 41 | 1517K| 1517K| 1277K (0)|
| 4 | TABLE ACCESS BY INDEX ROWID| TAB2 | 1 | 2000 | 2000 |00:00:00.01 | 15 | | | |
|* 5 | INDEX SKIP SCAN | COL1_IDX01 | 1 | 40 | 2000 |00:00:00.01 | 8 | | | |
| 6 | INDEX FAST FULL SCAN | KEY1_IDX | 1 | 10000 | 10000 |00:00:00.01 | 26 | | | |
----------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
3 - access("KEY1"="KEY2")
5 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL2"='A' AND "COL1"<=TRUNC(SYSDATE@!))
filter("COL2"='A')
|
HASH JOIN SEMI ( Semi/Anti/Outer ) : Oracle 10g
Semi/Anti Join의 단점을 보안
Semi/Anti Join의 단점.? Semi/Anti Join의 경우 서브쿼리는 항상 후행집합이 될수 밖에 없다. Hash Outer Join의 경우도 마찬가지로표시가 붙는 쪽의 집합은 항상 후행집합이 될 수 밖에 없다. 하지만 10g 부터 Hash join Right( Semi/Anti/Outer ) 기능이 나오게 되면서 서브쿼리 혹은 아우터 Join 되는쪽의 집합이 선행집합이 될 수 있다.
Right.? left 집합 대신에 right( 후행집합 )을 선행집합으로 하겠다는 뜻이다.
HASH JOIN RIGHT SEMI을 힌트로 유도하는 법( 그림 2-2-24 )
SQL> SELECT /*+ gather_plan_statistics */ COUNT( * )
2 FROM TAB1 A
3 WHERE KEY1 IN ( SELECT /*+ HASH_SJ */ KEY2
4 FROM TAB2 B
5 WHERE B.COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
6 AND B.COL2 = 'A'
7 ) ;
COUNT(*)
----------
1810
경 과: 00:00:00.01
SQL> @XPLAN
----------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
----------------------------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 40 | | | |
|* 2 | FILTER | | 1 | | 1810 |00:00:00.01 | 40 | | | |
|* 3 | HASH JOIN RIGHT SEMI | | 1 | 1830 | 1810 |00:00:00.01 | 40 | 1517K| 1517K| 1274K (0)|
| 4 | TABLE ACCESS BY INDEX ROWID| TAB2 | 1 | 2000 | 2000 |00:00:00.01 | 15 | | | |
|* 5 | INDEX SKIP SCAN | COL1_IDX01 | 1 | 40 | 2000 |00:00:00.01 | 8 | | | |
| 6 | INDEX FAST FULL SCAN | KEY1_IDX | 1 | 10000 | 10000 |00:00:00.01 | 25 | | | |
----------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
3 - access("KEY1"="KEY2")
5 - access("B"."COL1">=TRUNC(SYSDATE@!-1) AND "B"."COL2"='A' AND "B"."COL1"<=TRUNC(SYSDATE@!))
filter("B"."COL2"='A')
|
SQL> UPDATE /*+ gather_plan_statistics SWAP_JOIN_INPUTS(@SUB Y) QB_NAME( MAIN ) */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ QB_NAME( SUB ) */ 고객번호
5 FROM 고객 Y
6 WHERE 납입자 = 1000);
1 행이 갱신되었습니다.
SQL> @XPLAN
-------------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem |
-------------------------------------------------------------------------------------------------------------------------------------------
| 1 | UPDATE | 청구 | 1 | | 0 |00:00:00.14 | 10 | 3 | | | |
| 2 | NESTED LOOPS | | 1 | 1 | 1 |00:00:00.12 | 7 | 2 | | | |
| 3 | SORT UNIQUE | | 1 | 1 | 1 |00:00:00.01 | 4 | 0 | 9216 | 9216 | 8192 (0)|
| 4 | TABLE ACCESS BY INDEX ROWID| 고객 | 1 | 1 | 1 |00:00:00.01 | 4 | 0 | | | |
|* 5 | INDEX RANGE SCAN | 고객_INDEX0| 1 | 1 | 1 |00:00:00.01 | 3 | 0 | | | |
|* 6 | INDEX RANGE SCAN | 청구_INDEX0| 1 | 1 | 1 |00:00:00.12 | 3 | 2 | | | |
-------------------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
5 - access("납입자"=1000)
6 - access("고객번호"="고객번호" AND "청구년월"='200503')
--음.. 좀더 공격적인 힌트
SQL> UPDATE /*+ gather_plan_statistics SWAP_JOIN_INPUTS( @SUB Y ) LEADING( @SUB Y ) HASH_SJ( @SUB ) INDEX( X 청구_INDEX02 ) */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ QB_NAME( SUB ) */ ( 고객번호 )
5 FROM 고객 Y
6 WHERE 납입자 = 1000);
1 행이 갱신되었습니다.
SQL> @XPLAN
---------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
---------------------------------------------------------------------------------------------------------------------------------
| 1 | UPDATE | 청구 | 1 | | 0 |00:00:01.24 | 36800 | | | |
|* 2 | HASH JOIN RIGHT SEMI | | 1 | 1 | 1 |00:00:01.24 | 36799 | 1035K| 1035K| 340K (0)|
| 3 | TABLE ACCESS BY INDEX ROWID| 고객 | 1 | 1 | 1 |00:00:00.01 | 4 | | | |
|* 4 | INDEX RANGE SCAN | 고객_INDEX0| 1 | 1 | 1 |00:00:00.01 | 3 | | | |
|* 5 | INDEX FULL SCAN | 청구_INDEX0| 1 | 575K| 1000K|00:00:01.00 | 36795 | | | |
---------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("고객번호"="고객번호")
4 - access("납입자"=1000)
5 - access("청구년월"='200503')
filter("청구년월"='200503')
|
Filter 서브쿼리
SQL> UPDATE /*+ gather_plan_statistics LEADING( @SUB Y ) NO_UNNEST( @SUB ) INDEX( X 청구_INDEX01 ) */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ QB_NAME( SUB ) */ 고객번호
5 FROM 고객 Y
6 WHERE 납입자 = 1000);
1 행이 갱신되었습니다.
SQL> @XPLAN
---------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
---------------------------------------------------------------------------------------------------------------
| 1 | UPDATE | 청구 | 1 | | 0 |00:00:24.74 | 4038K| 38444 |
|* 2 | INDEX FULL SCAN | 청구_INDEX0| 1 | 28751 | 1 |00:00:24.74 | 4038K| 38444 |
|* 3 | TABLE ACCESS BY INDEX ROWID| 고객 | 1000K| 1 | 1 |00:00:07.15 | 4000K| 0 |
|* 4 | INDEX RANGE SCAN | 고객_INDEX0| 1000K| 1 | 1000K|00:00:04.40 | 3000K| 0 |
---------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("청구년월"='200503')
filter(("청구년월"='200503' AND IS NOT NULL))
3 - filter("고객번호"=:B1)
4 - access("납입자"=1000)
|
INDEX FULL SCAN 다른 방법은 없는것인가?? 유니크 인덱스가 없어서 그런거 같다.. 생성
DROP INDEX 청구_INDEX01
CREATE UNIQUE INDEX 청구_INDEX02 ON 청구( 고객번호, 청구년월 );
SQL> UPDATE /*+ gather_plan_statistics LEADING( @SUB Y ) NO_UNNEST( @SUB ) INDEX( X 청구_INDEX02 ) */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ QB_NAME( SUB ) */ ( 고객번호 )
5 FROM 고객 Y
6 WHERE 납입자 = 1000);
1 행이 갱신되었습니다.
SQL> @XPLAN
-- 아니군요 ㅠ
---------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
---------------------------------------------------------------------------------------------------------------
| 1 | UPDATE | 청구 | 1 | | 0 |00:00:09.54 | 4036K| 47 |
|* 2 | INDEX FULL SCAN | 청구_INDEX0| 1 | 28751 | 1 |00:00:09.54 | 4036K| 47 |
|* 3 | TABLE ACCESS BY INDEX ROWID| 고객 | 1000K| 1 | 1 |00:00:06.99 | 4000K| 0 |
|* 4 | INDEX RANGE SCAN | 고객_INDEX0| 1000K| 1 | 1000K|00:00:04.28 | 3000K| 0 |
---------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("청구년월"='200503')
filter(("청구년월"='200503' AND IS NOT NULL))
3 - filter("고객번호"=:B1)
4 - access("납입자"=1000)
|
선두 컬럼이 알수 없으니 풀스캔으로 타는것 같다. 청구_INDEX03 ON 청구( 청구년월, 고객번호 ) 확인자( Early Filter 서브쿼리 )
CREATE INDEX 청구_INDEX03 ON 청구( 청구년월, 고객번호 );
SQL> UPDATE /*+ gather_plan_statistics LEADING( @SUB Y ) NO_UNNEST( @SUB ) INDEX( X 청구_INDEX03) */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ QB_NAME( SUB ) */ ( 고객번호 )
5 FROM 고객 Y
6 WHERE 납입자 = 1000);
1 행이 갱신되었습니다.
SQL> @XPLAN
Plan hash value: 483576752
---------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
---------------------------------------------------------------------------------------------------------------
| 1 | UPDATE | 청구 | 1 | | 0 |00:00:17.10 | 4003K| 3059 |
|* 2 | INDEX RANGE SCAN | 청구_INDEX0| 1 | 28751 | 1 |00:00:17.10 | 4003K| 3059 |
|* 3 | TABLE ACCESS BY INDEX ROWID| 고객 | 1000K| 1 | 1 |00:00:06.98 | 4000K| 0 |
|* 4 | INDEX RANGE SCAN | 고객_INDEX0| 1000K| 1 | 1000K|00:00:04.27 | 3000K| 0 |
---------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("청구년월"='200503')
filter( IS NOT NULL)
3 - filter("고객번호"=:B1)
4 - access("납입자"=1000)
|
나) Sort Merge형 세미조인
세미조인 : 조인형태가 가지는 고유한 장.단점이나 적용기준들도 거의 그대로 통용된다. 이 말은 곧 세미 조인에서도 상황에 따라 적절한 조인 형식을 적용하여야 함을 의미한다. 연결고리의 이상이 발생하거나 대량의 데이터를 연결해야 할 때는 세미 조인에서도 Sort Merge 형 조인이 적용 될 수 있다.
사원 테이블과 근태 테이블은 연결고리인 '부서코드'에서 볼때는 M:M 관계를 가지고 있지만 서브 쿼리 특성상 항상 메이쿼리의 집합은 보존되므로 물로 결과는 M:1 조인과 동일하다.
준비스크립트
DROP TABLE 사원 PURGE;
CREATE TABLE 사원 AS
SELECT 사번
, 성명
, 직급
, 입사일
, 부서코드
, DECODE( 부서코드, 1, 'DB1팀', 2, 'DB2팀', 3, 'DB3팀'
, 4, '시스템1팀', 5, '시스템2팀', 6, '시스템3팀'
, 7, '경리1과', 8, '경리2과', 9, '경리3과', 10, '경리과', 11, '경리4과'
, 12, '개발1팀', 13, '개발2팀', 14, '개발3팀', 15, '개발4팀', 16, '개발5팀', 17, '개발6팀'
, 18, 'MD1팀', 19, 'MD2팀', 20, 'MD3팀', 21, 'MD4팀'
, 22, '디자인1팀', 23, '디자인2팀', 24, '디자인3팀', 25, '디자인4팀'
, 26, '멀티미디어1팀', 27, '멀티미디어2팀', 28, '멀티미디어3팀'
, 29, '모바일1팀'
, 30, '웹모바일팀'
, 31, '마케팅팀'
, 32, '기획팀' ) 부서
FROM (SELECT LEVEL AS 사번
, '아무개'||LEVEL AS 성명
, TRUNC( dbms_random.value( 1,7 ) ) 직급
, TRUNC( SYSDATE ) - 100 + CEIL( LEVEL/ 10000 ) 입사일
, TRUNC( dbms_random.value( 1,32 ) ) 부서코드
FROM DUAL
CONNECT BY LEVEL <= 1000000
)
CREATE UNIQUE INDEX 사원_PK ON 사원 ( 사번 )
--CREATE INDEX 부서_INDEX_01 ON 사원 ( 부서코드, 직급 );
CREATE TABLE 근태 AS
SELECT A.사번
, B.부서코드
, A.근태유형
, A.일자
FROM (SELECT TRUNC( dbms_random.value( 1,1000000 ) ) 사번
, DECODE( A.JOIN_C, 1,'무단결근',2,'조퇴', 3,'지각', 4,'지각', 5,'지각', 6, '연차', 7, '연차', 8,'휴가', 9,'휴가',10, '지각') 근태유형
, A.일자
FROM (SELECT TRUNC( dbms_random.value( 1,10 ) ) JOIN_C --
, TO_CHAR( TO_DATE( '20050101', 'YYYYMMDD') + LEVEL -1, 'YYYYMMDD' ) 일자
FROM DUAL
CONNECT BY LEVEL <= 365--TO_DATE( '20050630', 'YYYYMMDD') - TO_DATE( '20050501', 'YYYYMMDD')
) A
, (SELECT LEVEL LV FROM DUAL CONNECT BY LEVEL <= 6 ) B
WHERE A.JOIN_C >= B.LV
) A
, 사원 B
WHERE A.사번 = B.사번
CREATE INDEX 유형_일자_INX ON 근태 ( 근태유형, 일자 )
SQL> SELECT COUNT(*) FROM 근태
2 ;
COUNT(*)
----------
1574
|
HASH UNIQUE ( SORT UNIQUE ) : '1' 집합을 만든다 ( 메인 쿼리 보존 )
Sort Merge형식으로 수행 될 때는 다른 집합에서 수해오딘 결과를 받아서 처리할 수 없으므로 독자적으로 처리범위를 충분히 줄일 수 있을 때 효과적이며
랜덤 액세스가 줄어들기 때문에 경우에 따라서는 매우 효과적일 수도 있다는 장점은 일반적인 조인 시와 동일하다.( ㅡㅡ )
예를 들어 앞서 예를 든 SQL의 서브쿼리에 GROUP BY가 있었다고 가정해보자. 그 결과는 이미 가공된 결과의 집합이므로 인덱스를 사용할 수 없다 그러나 GROUP BY에 의해 집합의 크기가 줄어 들었으므로 조인의 량은 감소한다. 이런 경우에는 Sort Merge형 세미 조인이 나름대로 가치가 있다. 물론 이런 경우에도 서브 쿼리를 인라인뷰로 만들어 일반 조인으로 연결하는 것도 내용적으로 거의 동일하다. 연결고리의 조건 조건에 'NOT'을 사용한 경우에도 이러한 형태의 연결이 나타날 수 있다. 여기에 대한 상세한 내용은 '부정형( ANTI ) 세미조인( Page 599 ~ 605 )에서 설명될 것이다.
1. 양쪽 집합이 Full Table Scan을 사용하면 조인순서에 상관없이 일량이 동일하므로 처리시간도 동일하다. 2. 조인순서에 상관없이 Sort량은 동일하다 3. 부분범위처리가 안된다. 4. Full Scan이 발생하면 인덱스를 사용할 수 없으므로 항상 Sort 작업을 동반한다. 5. Sort Merge Join 대신 Catesian Merge Join이 나오면 조인 조건이 빠진 악성 SQL이다. 6. 조인 컬럼 기준에 Sort되므로 Order by절과 조인 컬럼이 일치해야만 Sort가 발생하지 않는다.
왜 뜬꿈없이 오만과 편견이냐? 책에서 Sort Merge 조인으로 실행계획이 수립 되어서 테스트 중 결과가 도출되어 겸사겸사 설명까지 첨부.으흠.
2. 조인순서에 상관없이 처리시간과 Sort량 동일 할까? ( 맛베기.. 시간이 없어요 남음할게요.. ㅠ )
확인자 역할 : 먼전 수행하여 엑세스한 결과를 서브쿼리를 통해 체크하여 취할 것인지, 아니면 버려야 할 것인지를 결정하는 역할이다. 이 형식의 세미조인은 이러한 작업을 보다 효율적으로 수행하기 위해 버퍼( Buffer ) 내에 이전의 값을 저장해 두었다가 대응되는 집합을 엑세스하기 전에 먼저 저장된 값과 비교함으로써 액세스를 최소화하는 방법이다.
조인 : 연결된 로우들을 다음 단계의 처리를 위해 보관할 필요가 있지만 필터처리에서는 단지 선별을 위해서만 사용하므로 체크에 필요로 하지만 필터처리에서는 단지 선별을 위해서만 사용하므로 체크에 필요한 최소의 정보만 잠시 저장되어 있을 뿐이다.
SQL> SELECT /*+ gather_plan_statistics INDEX( X ORDDATE_INDEX ) */ COUNT(*)
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%'
4 AND EXISTS ( SELECT /*+ NO_UNNEST NO_PUSH_SUBQ */ 'O'
5 FROM DEPT Y
6 WHERE Y.DEPTNO = X.SALDEPTNO
7 AND Y.TYPE1 = 1 );
COUNT(*)
----------
15832
SQL> @XPLAN
------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
------------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.15 | 697 | 313 |
|* 2 | FILTER | | 1 | | 15832 |00:00:00.21 | 697 | 313 |
| 3 | TABLE ACCESS BY INDEX ROWID| ORDER | 1 | 96928 | 86400 |00:00:00.95 | 675 | 313 |
|* 4 | INDEX RANGE SCAN | ORDDATE_INDEX | 1 | 96928 | 86400 |00:00:00.26 | 316 | 313 |
|* 5 | TABLE ACCESS BY INDEX ROWID| DEPT | 11 | 1 | 2 |00:00:00.01 | 22 | 0 |
|* 6 | INDEX UNIQUE SCAN | DEPT_PK | 11 | 1 | 11 |00:00:00.01 | 11 | 0 |
------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter( IS NOT NULL)
4 - access("ORDDATE" LIKE '20110621%')
filter("ORDDATE" LIKE '20110621%')
5 - filter("Y"."TYPE1"=1)
6 - access("Y"."DEPTNO"=:B1)
|
1) 'ORDDATE_INDEX'를 86400 개까지 차례로 범위처리를 하면서 86400 건의 'ORDER' 테이블의 로우를 읽어 내려 간다.
2) 대응되는 DEPT 테이블을 연결한다.
3) 그렇다면 'ORDER' 테이블에서 엑세스한 86400 개의 각각의 로우에 대해서 메번 서브쿼리가 수행되어 EXISTS를 체크했다
4) 그러므로 'DEPT' 테이블을 액세스하는 서브쿼리도 86400 번 수행되어야 할 것이다.
5) 그런데 'DEPT' acess 5을 살펴보면 이 메인쿼리가 'DEPT' 테이블을 엑세스한것은 단 11회에 불과 하다.
NL SEMI ( ORDDATE_INDEX )
SQL> SELECT /*+ gather_plan_statistics INDEX( X ORDDATE_INDEX ) */ COUNT(*)
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%'
4 AND EXISTS ( SELECT /*+ NL_SJ */ 'O'
5 FROM DEPT Y
6 WHERE Y.DEPTNO = X.SALDEPTNO
7 AND Y.TYPE1 = 1 );
COUNT(*)
----------
15832
SQL> @xplan
---------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
---------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.12 | 688 |
| 2 | NESTED LOOPS SEMI | | 1 | 709 | 15832 |00:00:00.17 | 688 |
| 3 | TABLE ACCESS BY INDEX ROWID| ORDER | 1 | 3899 | 86400 |00:00:00.61 | 675 |
|* 4 | INDEX RANGE SCAN | ORDDATE_INDEX | 1 | 3899 | 86400 |00:00:00.26 | 316 |
|* 5 | TABLE ACCESS BY INDEX ROWID| DEPT | 11 | 1 | 2 |00:00:00.01 | 13 |
|* 6 | INDEX UNIQUE SCAN | DEPT_PK | 11 | 1 | 11 |00:00:00.01 | 2 |
---------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("ORDDATE" LIKE '20110621%')
filter("ORDDATE" LIKE '20110621%')
5 - filter("Y"."TYPE1"=1)
6 - access("Y"."DEPTNO"="X"."SALDEPTNO")
|
1) 먼저 ORDDATE_INDEX 에서 '20110621%'를 만족하는 첫 번재 로우를 읽고 그 ROWID로 ORDER 테이블의 해당 로우를 엑세스한다.
2) 그 로우가 가지고 있는 SALDEPTNO 와 버퍼에 있는 DEPT와 비교한 결과가 같지 않으므로 DEPT 테이블의 기본키를 이용해 액세스한 후 TYPE='1'을 체크한다. 체크를 하여 조건을 만족하면 운반단위에 태우고 아니면 버린다.
3) 액세스한 DEPT테이블의 비교 컬럼값들을 버퍼에 저장한다.
4) ORDDATE_INDEX의 두번재 로우에 대한 ORDER 테이블 로우를 액세스한 후 버퍼와 체크한다. 이때 ORDER테이블의 SALDEPTNO와 버퍼의 DEPT가 동일하면 버퍼와의 비교만 수행하며, DEPT 테이블은 액세스하지 않는다. 버퍼의 DEPT와 일치히지 않을 때는 DEPT 테이블을 액세스하여 체크 조건을 비교하고 그 값을 다시 버퍼에 저장한다. 버퍼는 하나의 값만 저장 할수 있으므로 앞서 저장된 값은 갱신된다. 그림에는 처음에 버퍼에 있던 '11,1'이 '22,2'로 바뀌어진 것을 표현하였다.
5) 이와 같은 방법으로 ORDDATE_INDEX의 처리범위가 완료될 때까지 수행한다.
메인 쿼리에서 엑세스되는 로우가 서브 쿼리에서 비교할 컬럼( 연결고리 )으로 정렬되어 있다면 서브 쿼리의 테이블 엑세스 양은 많이 감소시킬 수 있다.
즉, 선행처리되는 ORDER 테이블의 연결고리인 SALDEPTNO가 만약에 'ORDERDATE + SALDEPTNO'로 인덱스가 되어 있다면 , 그림에서 처럼 '11'이 모든 끝난 다음에 '22'가 나타날 것이므로 DEPT를 액세스하러 가는 경우가 최소화 될 것이다. ( 정말... ?? )
그러나 극단저긴 경우를 가정해보자. 만약 SALDEPT가 매번 바뀌었다고 한다면 항상 버퍼에 저장된 것과 일치하지 않을 것이므로 버퍼를 활용한 이득을 전혀 얻을 수 없다. 물론 이것은 최악의 경우를 가정한 것이므로 대부분의 경우는 최소한 유리하게 된다고 말 할 수 있다. ( 이처럼 서브쿼리를 확인자 역할로만 사용하고자 한다면 FILTER형 세미조인을 활용하는 것이 나쁠 것이 없다.
그러나 무조건 그렇게 생각하는 것도 약간의 문제는 잇다. 그림에서 볼 수 있듯이 이 처리방식은 개념적으로는 NL형시과 유사하므로 램덤액세스가 증가할 가능성이 있다. 물론 버퍼만 체크하는 경우가 많다면 걱정할 것이 없겠지만 그렇지 않을 때는 Sort Merge이나 해쉬 조인이 되도록 하는 것이 보다 유리할 것이다. 일반적으로 EXISTS 서브 쿼리를 사용할 경우는 대부분 Filter 처리 방식으로 실행계획이 수립되므로 필요하다면 인라인뷰를 활용한 조이문을 사용하느게 좋다. ( 정말..?? )
먼가 이상 하지 않나요?? ( 물론 제가 잘못 이해 할 수도 있습니다. )
현재 ORDER Table 데이타 상황은 책에서 말한 필터를 사용할 경우 최악의 상황인데.. 딱 11번만 서브쿼리르 엑세스 했습니다.
SQL> SELECT /*+ gather_plan_statistics INDEX( X ORDDATE_INDEX_01 ) */ COUNT(*)
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%'
4 AND EXISTS ( SELECT /*+ NO_UNNEST NO_PUSH_SUBQ */ 'O'
5 FROM DEPT Y
6 WHERE Y.DEPTNO = X.SALDEPTNO
7 AND Y.TYPE1 = 1 );
COUNT(*)
----------
15832
SQL> @XPLAN
---------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
---------------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.10 | 399 | 3 |
|* 2 | FILTER | | 1 | | 15832 |00:00:00.16 | 399 | 3 |
|* 3 | INDEX RANGE SCAN | ORDDATE_INDEX_01 | 1 | 3899 | 86400 |00:00:00.26 | 377 | 3 |
|* 4 | TABLE ACCESS BY INDEX ROWID| DEPT | 11 | 1 | 2 |00:00:00.01 | 22 | 0 |
|* 5 | INDEX UNIQUE SCAN | DEPT_PK | 11 | 1 | 11 |00:00:00.01 | 11 | 0 |
---------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter( IS NOT NULL)
3 - access("ORDDATE" LIKE '20110621%')
filter("ORDDATE" LIKE '20110621%')
4 - filter("Y"."TYPE1"=1)
5 - access("Y"."DEPTNO"=:B1)
|
NL SEMI ( ORDDATE_INDEX_01 )
SQL> SELECT /*+ gather_plan_statistics INDEX( X ORDDATE_INDEX_01 ) */ COUNT(*)
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%'
4 AND EXISTS ( SELECT /*+ NL_SJ */ 'O'
5 FROM DEPT Y
6 WHERE Y.DEPTNO = X.SALDEPTNO
7 AND Y.TYPE1 = 1 );
COUNT(*)
----------
15832
SQL> @XPLAN
------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.08 | 390 |
| 2 | NESTED LOOPS SEMI | | 1 | 709 | 15832 |00:00:00.13 | 390 |
|* 3 | INDEX RANGE SCAN | ORDDATE_INDEX_01 | 1 | 3899 | 86400 |00:00:00.26 | 377 |
|* 4 | TABLE ACCESS BY INDEX ROWID| DEPT | 11 | 1 | 2 |00:00:00.01 | 13 |
|* 5 | INDEX UNIQUE SCAN | DEPT_PK | 11 | 1 | 11 |00:00:00.01 | 2 |
------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access("ORDDATE" LIKE '20110621%')
filter("ORDDATE" LIKE '20110621%')
4 - filter("Y"."TYPE1"=1)
5 - access("Y"."DEPTNO"="X"."SALDEPTNO")
|
라) 해쉬( Hash )형 세미조인
필터 형식으로 처리되는 세미 조인은 랜덤 위주의 액세스가 발생하므로 만약 대량의 연결을 시도했을 때는 커다란 부담이 될 가능성이 충분히 있다.
물론 이런 문제를 해결하기 위해 Sort Merge형으로 실행을 유도할 수도 있겠지만, 일반적으로 해쉬 조인이 수행속도에 유리한 경우가 많기 때문에 이를 활용할 가치가 있다.
HASH JOIN SEMI
SQL> SELECT /*+ gather_plan_statistics INDEX( X ORDDATE_INDEX ) LEADING( A ) */ COUNT(*)
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%'
4 AND EXISTS ( SELECT /*+ HASH_SJ */ 'O'
5 FROM DEPT Y
6 WHERE Y.DEPTNO = X.SALDEPTNO
7 AND Y.TYPE1 = 1 );
COUNT(*)
----------
15832
SQL> @XPLAN
------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
------------------------------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.12 | 678 | | | |
|* 2 | HASH JOIN SEMI | | 1 | 709 | 15832 |00:00:00.15 | 678 | 3256K| 1861K| 6674K (0)| <--
| 3 | TABLE ACCESS BY INDEX ROWID| ORDER | 1 | 3899 | 86400 |00:00:00.60 | 675 | | | |
|* 4 | INDEX RANGE SCAN | ORDDATE_INDEX | 1 | 3899 | 86400 |00:00:00.26 | 316 | | | |
|* 5 | TABLE ACCESS FULL | DEPT | 1 | 2 | 2 |00:00:00.01 | 3 | | | |
------------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("Y"."DEPTNO"="X"."SALDEPTNO")
4 - access("ORDDATE" LIKE '20110621%')
filter("ORDDATE" LIKE '20110621%')
5 - filter("Y"."TYPE1"=1)
|
HASH JOIN RIGHT SEMI ( 적은 테이블을 선행 테이블로... )
SQL> SELECT /*+ gather_plan_statistics INDEX( X ORDDATE_INDEX ) */ COUNT(*)
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%'
4 AND EXISTS ( SELECT /*+ HASH_SJ */ 'O'
5 FROM DEPT Y
6 WHERE Y.DEPTNO = X.SALDEPTNO
7 AND Y.TYPE1 = 1 );
COUNT(*)
----------
15832
SQL> @XPLAN
------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
------------------------------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.10 | 678 | | | |
|* 2 | HASH JOIN RIGHT SEMI | | 1 | 709 | 15832 |00:00:00.14 | 678 | 1396K| 1396K| 474K (0)| <--
|* 3 | TABLE ACCESS FULL | DEPT | 1 | 2 | 2 |00:00:00.01 | 3 | | | |
| 4 | TABLE ACCESS BY INDEX ROWID| ORDER | 1 | 3899 | 86400 |00:00:00.60 | 675 | | | |
|* 5 | INDEX RANGE SCAN | ORDDATE_INDEX | 1 | 3899 | 86400 |00:00:00.26 | 316 | | | |
------------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("Y"."DEPTNO"="X"."SALDEPTNO")
3 - filter("Y"."TYPE1"=1)
5 - access("ORDDATE" LIKE '20110621%')
filter("ORDDATE" LIKE '20110621%')
26 개의 행이 선택되었습니다.
|
수행속도 향상의 요건 : 1) 대량의 연결을 해야 하고 메모리 내에 충분한 해쉬 영역
제약 조건 : 1) 서브쿼리에는 하나의 테이블만 존재해야만 한다 2) 서브쿼리 내에 또 다시 서브쿼리를 사용했을 때는 적용이 불가능하다 3) 연결고리의 연산자는 반드시 '='이 되어야 한다. 4) 서브쿼리 내에 GROUP BY, CONNECT BY, ROWNUM을 사용할 수 없다.
부분 범위 가능 요건 : 어느 한쪽 집합이 메모리 내의 해쉬 영역에 내포 될 수 있으면 부분범위 처리가 가능하다 ( ???? ) 이러한 경우는 온라인 애플리케이션의 경우 대량량 데이터라 하더라도 아주 빠른 수행 속도를 보장 받을 수가 있다.
마) 부정형( Anti ) 세미조인
'111'이 아닌 것을 찾겟다고 한다면 우리가 찾아야 할 대상이 어떤 값이 존재하는지 알 수 없으므로 연결을 위한 상수값을 제공하기가 여의치 않다.
그러나 약간만 생각을 바꾸어 보면 길이 없는 것도 아니다. 학창시절 수학시간에서 배웠던 상식적인 개념을 생각해 보자.
가령, '10-X'라는 수식이 있을때 'X'에 있는 마이너스를 플러스
로 바꾸는 방법이 있다. ( '10-(+X)' )
즉, 조인의 비교연산자는 'NOT'을 사용하지 않는 긍정형을 쓰고, 이것을 괄호로 묶는 것( 인라인뷰 )을 'NOT IN' 이나 'NOT EXISTS'로 비교하는 방법을 사용하면 된다.
다시 말해서 집합간의 연결은 기존의 세미조인으로 실시하고, 그 결과는 판정만 반대로 하는 방식을 적용하기 때문에 앞서 설명했던 세미조인과 매우 유사하다고 할 수 있다.
여기서 'IN' 이나 'EXISTS'와 같은 비교 연산자의 의미는 어떤 것을 찾고자 하는 것이 아니라 단지 기술한 서브쿼리의 결과가 'TRUE'인지 'FAULT'인지를 판정하는 불린 연산을 뜻한다.
항상 확인자의 역할을 담당 할 수 밖에 없다.
준비 스크립트 ( 위에 테이블 재사용 ( 편의상 ) )
DROP TABLE TAB1 PURGE;
DROP TABLE TAB2 PURGE;
CREATE TABLE TAB1 AS
SELECT LEVEL KEY1
, '상품'||LEVEL COL1
, TRUNC( dbms_random.value( 1,7 ) ) * 500 + TRUNC( dbms_random.value( 1,3 ) ) * 750 COL2
FROM DUAL
CONNECT BY LEVEL <= 10000;
CREATE TABLE TAB2 AS
SELECT LEVEL KEY1
, TRUNC(dbms_random.value( 1, 10000 ) ) KEY2
, TRUNC( SYSDATE ) - 100 + CEIL( LEVEL/ 1000 ) COL1
, 'A' COL2
FROM DUAL
CONNECT BY LEVEL <= 100000;
CREATE INDEX TAB1_COL1 ON TAB1( COL1 );
CREATE INDEX TAB2_COL1 ON TAB2( COL1 );
CREATE INDEX TAB2_KEY2 ON TAB2( KEY2, COL1 );
EXEC DBMS_STATS.GATHER_TABLE_STATS( USER, 'TAB1' );
EXEC DBMS_STATS.GATHER_TABLE_STATS( USER, 'TAB2' );
|
p. 600 실습 스크립트 ( 확인자 )
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND KEY1 NOT IN ( SELECT KEY2
5 FROM TAB2 B
6 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE ) );
COUNT(*)
----------
899
SQL> @XPLAN
------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:01.12 | 15487 |
|* 2 | FILTER | | 1 | | 899 |00:00:00.99 | 15487 |
| 3 | TABLE ACCESS BY INDEX ROWID | TAB1 | 1 | 1216 | 1112 |00:00:00.01 | 209 |
|* 4 | INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 |00:00:00.01 | 5 |
|* 5 | FILTER | | 1112 | | 213 |00:00:01.11 | 15278 |
|* 6 | TABLE ACCESS BY INDEX ROWID| TAB2 | 1112 | 1 | 213 |00:00:01.10 | 15278 |
|* 7 | INDEX RANGE SCAN | TAB2_COL1 | 1112 | 2000 | 2001K|00:00:04.01 | 8184 | ---- (a)
------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter( IS NULL)
4 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
5 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
6 - filter(LNNVL("KEY2"<>:B1))
7 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
|
서브쿼리가 확인자로 풀림
(a)서브쿼리의 처리주관 인덱스는 연결고리인 KTAB2_KEY2 인덱스가 아니라 TAB2_COL1 사용 되고 있다.
이거슨 서브쿼리가 계속해서 동일한 범위를 반복해서 치리하게 된다는것을 의미한다. ( 부정형에는 캐쉬가 먹지 않는가.?? 위경우는 TAB1.KEY1경우가 유니크하기때문에... )
만약 인덱스가 ( KEY2, COL1 )로 구성되었거나 COL1에 인덱스가 없다면 당연히 KEY2 인덱스를 사용하는 실행계획이 작성된다.
이것은 옵티마이저의 잘못이다. ( 데이터베이스 버전에따라 나타나지 않을 수도 있다. )
잘못된 실행 계획이 수립되는 것을 방지하는 바람직 SQL ( TAB2_COL1 풀림 )
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND NOT EXISTS ( SELECT/*+ */ 'X'
5 FROM TAB2 B
6 WHERE A.KEY1 = B.KEY2
7 AND COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE ) );
COUNT(*)
----------
899
SQL> @XPLAN
--------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
--------------------------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 224 | | | |
|* 2 | HASH JOIN ANTI | | 1 | 1 | 899 |00:00:00.01 | 224 | 1517K| 1517K| 1182K (0)|
| 3 | TABLE ACCESS BY INDEX ROWID| TAB1 | 1 | 1216 | 1112 |00:00:00.01 | 209 | | | |
|* 4 | INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 |00:00:00.01 | 5 | | | |
| 5 | TABLE ACCESS BY INDEX ROWID| TAB2 | 1 | 2000 | 2000 |00:00:00.02 | 15 | | | |
|* 6 | INDEX RANGE SCAN | TAB2_COL1 | 1 | 2000 | 2000 |00:00:00.01 | 8 | | | | <-- ㅡㅡ^
--------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("A"."KEY1"="B"."KEY2")
4 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
6 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
|
책에서는 필터형으로 설명이되었는데 옵티마이져는 해쉬 조인 안티를 선택하였다.
옵티마이져가 해쉬를 선택한 이유는 ? 서브쿼리를 한번만 액세스 하기 때문이다.( 주관 )
잘못된 실행 계획이 수립되는 것을 방지하는 바람직 SQL ( TAB2_KEY2 유도 )
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND NOT EXISTS ( SELECT/*+ INDEX( B TAB2_KEY2 ) */ 'X'
5 FROM TAB2 B
6 WHERE A.KEY1 = B.KEY2
7 AND COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE ) );
COUNT(*)
----------
899
SQL> @XPLAN
--------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
--------------------------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.08 | 531 | | | |
|* 2 | HASH JOIN ANTI | | 1 | 1 | 899 |00:00:00.08 | 531 | 1517K| 1517K| 1182K (0)|
| 3 | TABLE ACCESS BY INDEX ROWID| TAB1 | 1 | 1216 | 1112 |00:00:00.01 | 209 | | | |
|* 4 | INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 |00:00:00.01 | 5 | | | |
|* 5 | INDEX FULL SCAN | TAB2_KEY2 | 1 | 2000 | 2000 |00:00:00.08 | 322 | | | |
--------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("A"."KEY1"="B"."KEY2")
4 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
5 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
filter(("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!)))
|
왜 TAB2_COL1 인덱스를 선택하였나??
SQL> SELECT/*+ INDEX( B TAB2_KEY2 ) */ COUNT(*)
2 FROM TAB2 B
3 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE );
COUNT(*)
----------
2000
SQL> SELECT COUNT(*)
2 FROM TAB2
3 WHERE KEY2 IN (
4 SELECT/*+ INDEX( B TAB2_KEY2 ) */ DISTINCT KEY2
5 FROM TAB2 B
6 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE ) );
COUNT(*)
----------
19431
그럼 이제 책에서 말하는 필터형으로 풀어보자.!
보너스.. NL_AJ로 풀면 어떤가..??
p. 601 실행 스크립트
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND NOT EXISTS ( SELECT/*+ NO_UNNEST NO_PUSH_SUBQ */ 'X'
5 FROM TAB2 B
6 WHERE A.KEY1 = B.KEY2
7 AND COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE ) );
COUNT(*)
----------
899
SQL> @XPLAN
-----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.03 | 2434 |
|* 2 | FILTER | | 1 | | 899 |00:00:00.03 | 2434 |
| 3 | TABLE ACCESS BY INDEX ROWID| TAB1 | 1 | 1216 | 1112 |00:00:00.01 | 209 |
|* 4 | INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 |00:00:00.01 | 5 |
|* 5 | FILTER | | 1112 | | 213 |00:00:00.02 | 2225 |
|* 6 | INDEX RANGE SCAN | TAB2_KEY2 | 1112 | 1 | 213 |00:00:00.01 | 2225 | <-- 굿잡
-----------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter( IS NULL)
4 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
5 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
6 - access("B"."KEY2"=:B1 AND "COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
|
필터형으로 푸니 책에서 말씀하신데로 잘 풀리는구나... ( 버전에 따라 틀리수도 있다 )
선행 집한에서 상수값을 제공받아 처리한다.( 확인자 )
이런한 처리는 랜덤 액세스가 증가한다는 단점을 가지고 있기 때문에 상수값을 제공받았을 때 수행되는 처리량과 독자적으로 수행할 때의 처리량을 비교하여 판단해야 한다.
이 실행계획읜 장점은 부분범위 처리가 가능하다는 것이다. ( 운반단위 )
보너스.. NL_AJ로 풀면 어떤가..??
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND NOT EXISTS ( SELECT/*+ NL_AJ */ 'X'
5 FROM TAB2 B
6 WHERE A.KEY1 = B.KEY2
7 AND COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE ) );
COUNT(*)
----------
899
SQL> @XPLAN
-----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.02 | 1324 |
| 2 | NESTED LOOPS ANTI | | 1 | 1 | 899 |00:00:00.02 | 1324 |
| 3 | TABLE ACCESS BY INDEX ROWID| TAB1 | 1 | 1216 | 1112 |00:00:00.01 | 209 |
|* 4 | INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 |00:00:00.01 | 5 |
|* 5 | INDEX RANGE SCAN | TAB2_KEY2 | 1112 | 2000 | 213 |00:00:00.01 | 1115 |
-----------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
5 - access("A"."KEY1"="B"."KEY2" AND "COL1">=TRUNC(SYSDATE@!-1) AND
"COL1"<=TRUNC(SYSDATE@!))
|
부정형이 일반 조인이랑 같은 방식이라면 캐쉬도 되는건가??
SQL> SELECT /*+ gather_plan_statistics INDEX( X ORDDATE_INDEX ) */ COUNT(*)
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%'
4 AND NOT EXISTS ( SELECT /*+ NL_AJ */ 'O'
5 FROM DEPT Y
6 WHERE Y.DEPTNO = X.SALDEPTNO
7 AND Y.TYPE1 = 1 );
COUNT(*)
----------
70568
SQL> @XPLAN
---------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
---------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.14 | 688 |
| 2 | NESTED LOOPS ANTI | | 1 | 3190 | 70568 |00:00:00.35 | 688 |
| 3 | TABLE ACCESS BY INDEX ROWID| ORDER | 1 | 3899 | 86400 |00:00:00.78 | 675 |
|* 4 | INDEX RANGE SCAN | ORDDATE_INDEX | 1 | 3899 | 86400 |00:00:00.26 | 316 |
|* 5 | TABLE ACCESS BY INDEX ROWID| DEPT | 11 | 1 | 2 |00:00:00.01 | 13 |
|* 6 | INDEX UNIQUE SCAN | DEPT_PK | 11 | 1 | 11 |00:00:00.01 | 2 |
---------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("ORDDATE" LIKE '20110621%')
filter("ORDDATE" LIKE '20110621%')
5 - filter("Y"."TYPE1"=1)
6 - access("Y"."DEPTNO"="X"."SALDEPTNO")
연결 고리가 존재하지 않을 때는 어떻게 해야하는가.? ( 개인적으로 연결고리가 존재하더라도 부정형은 Hash가 월등한것으로 판단됨 ( 주관 ) )
p. 602 FILTER
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND KEY1 NOT IN ( SELECT KEY1
5 FROM TAB2 B
6 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE ) );
COUNT(*)
----------
1112
SQL> @XPLAN
------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:01.14 | 16889 |
|* 2 | FILTER | | 1 | | 1112 |00:00:01.15 | 16889 |
| 3 | TABLE ACCESS BY INDEX ROWID | TAB1 | 1 | 1216 | 1112 |00:00:00.01 | 209 |
|* 4 | INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 |00:00:00.01 | 5 |
|* 5 | FILTER | | 1112 | | 0 |00:00:01.13 | 16680 |
|* 6 | TABLE ACCESS BY INDEX ROWID| TAB2 | 1112 | 1 | 0 |00:00:01.12 | 16680 |
|* 7 | INDEX RANGE SCAN | TAB2_COL1 | 1112 | 2000 | 2224K|00:00:04.46 | 8896 |
------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter( IS NULL)
4 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
5 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
6 - filter(LNNVL("KEY1"<>:B1))
7 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
|
메인쿼리에서 추출한 범위가 매우 넓어서 서브쿼리가 랜덤으로 처리할 양이 매우 많아진다면 상당한 부담이 될 것이다.
이런한 경우에는 Sort Merge 조인처럼 각각의 집합을 별도로 한 번씩만 액세스하여 정렬시킨 다음 머지를 통해서 연결하는 것이 훨씬 유리하다.
부정형으로 연결되었지만 이런한 방식의 처리가 문제될 것은 없다. 머지 단계에서 일반적이 머지의 반대인 '머지에 실패한 것'을 추출하기만 하면 나머지는 동일한 방법이 되기 때문에이다.
그러나 대부분의 경우 옵티마이져는 필터형 처리로 실행계획을 수립하기 때문에 필요하다면 뭔가 특별한 조치를 해야 한다. ( Hash or Merge )
p.602 MERGE JOIN ANTI
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND KEY1 IS NOT NULL
5 AND KEY1 NOT IN ( SELECT /*+ MERGE_AJ */ KEY1
6 FROM TAB2 B
7 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
8 AND KEY1 IS NOT NULL );
COUNT(*)
----------
1112
SQL> @XPLAN
---------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
---------------------------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 224 | | | |
| 2 | MERGE JOIN ANTI | | 1 | 1 | 1112 |00:00:00.02 | 224 | | | |
| 3 | SORT JOIN | | 1 | 1216 | 1112 |00:00:00.01 | 209 | 24576 | 24576 |22528 (0)|
|* 4 | TABLE ACCESS BY INDEX ROWID| TAB1 | 1 | 1216 | 1112 |00:00:00.01 | 209 | | | |
|* 5 | INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 |00:00:00.01 | 5 | | | |
|* 6 | SORT UNIQUE | | 1112 | 2000 | 0 |00:00:00.01 | 15 | 83968 | 83968 |73728 (0)|
|* 7 | TABLE ACCESS BY INDEX ROWID| TAB2 | 1 | 2000 | 2000 |00:00:00.02 | 15 | | | |
|* 8 | INDEX RANGE SCAN | TAB2_COL1 | 1 | 2000 | 2000 |00:00:00.01 | 8 | | | |
---------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - filter("KEY1" IS NOT NULL)
5 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
6 - access("KEY1"="KEY1")
filter("KEY1"="KEY1")
7 - filter("KEY1" IS NOT NULL)
8 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
|{CODE:SQL}
* NOT IN 을 사용한 경우만 가능하며 ( MERGE JOIN ANTI )
* 비용기준 옵티마이져로 정의 되어야만 한다.
* 세미 조인이 'IN' 이나 'EXISTS' 모두 가능하듯이 부벙형 조인도 당연히 그래야 하지만 현재까지는 'NOT EXISTS'를 사용한 경우는 힌트가 적용되지 않는다 ( 현재도..?? )
|
|| p.603 HASH JOIN ANTI ||
|{CODE:SQL}
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND KEY1 IS NOT NULL
5 AND KEY1 NOT IN ( SELECT /*+ HASH_AJ */ KEY1
6 FROM TAB2 B
7 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
8 AND KEY1 IS NOT NULL );
COUNT(*)
----------
1112
SQL> @XPLAN
--------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
--------------------------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 224 | | | |
|* 2 | HASH JOIN ANTI | | 1 | 1 | 1112 |00:00:00.01 | 224 | 1517K| 1517K| 1180K (0)|
|* 3 | TABLE ACCESS BY INDEX ROWID| TAB1 | 1 | 1216 | 1112 |00:00:00.01 | 209 | | | |
|* 4 | INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 |00:00:00.01 | 5 | | | |
|* 5 | TABLE ACCESS BY INDEX ROWID| TAB2 | 1 | 2000 | 2000 |00:00:00.02 | 15 | | | |
|* 6 | INDEX RANGE SCAN | TAB2_COL1 | 1 | 2000 | 2000 |00:00:00.01 | 8 | | | |
--------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("KEY1"="KEY1")
3 - filter("KEY1" IS NOT NULL)
4 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
5 - filter("KEY1" IS NOT NULL)
6 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
|
MERGE_AJ과는 다르게 SORT UNIQUE 부분이 나타나지 않는 것은 해쉬 조인이 연결을 위해 사전에 준비해 두는 작업이 필요한 것은 머지 조인과 유사하지만 실제 연결단계에서 필터형식으로 처리되므로 굳이 유일한 집합을 만들어야 할 필요가 없기 때문이다.
항상 이런 방법이 유리한 것이 아니라는 것은 앞서 그 이유를 충분히 설명하였다. (막 쓰지말고 확인하고 )
특히 머지 부정형 조인이나 해쉬 부정형 조인은 전체범위 처리를 하는 경우가 많기 때문에 반드시 부분범위 처리를 하고자 할 때는 사용해서는 안 된다는 것을 분명이 기억하기 바란다.
세미 조인이 'IN' 이나 'EXISTS' 모두 가능하듯이 부벙형 조인도 당연히 그래야 하지만 현재까지는 'NOT EXISTS'를 사용한 경우는 힌트가 적용되지 않는다 ( 현재도..?? )
왜 IS NOT NULL을 추가해야하는가.? ( 없었을때는 안됨 )
긍정형 : 어차피 내가 찾고자 하는 데이터가 아니므로 빠지더라도 문제가 될 것이 없다. [ 1, 2, NULL ] 2를 액세스할 때 집합 내에 NULL의 존재유무는 결과에 아무런 문제도 일으키지 않는다.
부정형 : 2가 아닌 것은 [ 1, NULL ]이지만, 만약 NULL이 비교연산에서 무시된다면 '1'만 찾게 된다. 이것은 우리가 원하는 결과가 아니다. ( 부정형 서브쿼리에서도 그대로 적용 ) 예을 들어 메인쿼리에서 액세스한 값이 [ 1,2,3,NULL ]이라고 하고 부정형으로 확인자가 될 서브쿼리에는 [ 1,2,NULL ]이 있다고 가정해 보자. 메인쿼리의 결과 중에 서브쿼리에 없는 것을 찾이면 그 결과는 '3'과 'NULL'이다. 얼핏 생각하면 '3'만 찾아야 할 것처럼 보이지만 그것은 NULL값의 개념을 잘 모르는 사람들 생각 미지수 'X'와 미지수 'Y'는 같은 미지수라는 것일 뿐 이드을 '='이라고 할 수 없는 것과 같다. 그러므로 메인쿼리에 있는 NULL값이 서브쿼리에도 있다고 해서 존재한다는 조건(Exists)을 만족했다고 해서는 안된다.
결론 ?
부정형 해쉬 조인은 실제로는 긍정형과 같은 방법으로 조인을 시도해서 조인의 성공여부를 결정하는 방법만 반대로 할뿐이므로 연결되는 것을 찾는 작업을 하면서 그 결과만 반대로 처리한다. 만약 서브쿼리에 NULL값이 존재한다면 결과에 영향을 미칠 수 있으므로 NOT NULL 이라는 전제가 있을 때만 이런한 방식의 조인이 가능하다.
현재도 NOT EXISTS를 사용한 경우는 힌트가 적용되지 않는가.? ㅠ
보너스 MERGE JOIN ANTI ( NOT EXISTS )( 현재는 됨 )
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND KEY1 IS NOT NULL
5 AND NOT EXISTS ( SELECT /*+ MERGE_AJ */ 'X'
6 FROM TAB2 B
7 WHERE A.KEY1 = B.KEY1
8 AND COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
9 AND KEY1 IS NOT NULL );
COUNT(*)
----------
1112
SQL> @XPLAN
---------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
---------------------------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 224 | | | |
| 2 | MERGE JOIN ANTI | | 1 | 1 | 1112 |00:00:00.02 | 224 | | | |
| 3 | SORT JOIN | | 1 | 1216 | 1112 |00:00:00.01 | 209 | 24576 | 24576 |22528 (0)|
|* 4 | TABLE ACCESS BY INDEX ROWID| TAB1 | 1 | 1216 | 1112 |00:00:00.01 | 209 | | | |
|* 5 | INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 |00:00:00.01 | 5 | | | |
|* 6 | SORT UNIQUE | | 1112 | 2000 | 0 |00:00:00.01 | 15 | 83968 | 83968 |73728 (0)|
|* 7 | TABLE ACCESS BY INDEX ROWID| TAB2 | 1 | 2000 | 2000 |00:00:00.02 | 15 | | | |
|* 8 | INDEX RANGE SCAN | TAB2_COL1 | 1 | 2000 | 2000 |00:00:00.01 | 8 | | | |
---------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - filter("KEY1" IS NOT NULL)
5 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
6 - access("A"."KEY1"="B"."KEY1")
filter("A"."KEY1"="B"."KEY1")
7 - filter("KEY1" IS NOT NULL)
8 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
|
보너스 HASH JOIN ANTI ( NOT EXISTS )( 현재는 됨 )
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND KEY1 IS NOT NULL
5 AND NOT EXISTS ( SELECT /*+ HASH_AJ */ 'X'
6 FROM TAB2 B
7 WHERE A.KEY1 = B.KEY1
8 AND COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
9 AND KEY1 IS NOT NULL );
COUNT(*)
----------
1112
SQL> @XPLAN
--------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
--------------------------------------------------------------------------------------------------------------------------------
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 224 | | | |
|* 2 | HASH JOIN ANTI | | 1 | 1 | 1112 |00:00:00.01 | 224 | 1517K| 1517K| 1189K (0)|
|* 3 | TABLE ACCESS BY INDEX ROWID| TAB1 | 1 | 1216 | 1112 |00:00:00.01 | 209 | | | |
|* 4 | INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 |00:00:00.01 | 5 | | | |
|* 5 | TABLE ACCESS BY INDEX ROWID| TAB2 | 1 | 2000 | 2000 |00:00:00.02 | 15 | | | |
|* 6 | INDEX RANGE SCAN | TAB2_COL1 | 1 | 2000 | 2000 |00:00:00.01 | 8 | | | |
--------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("A"."KEY1"="B"."KEY1")
3 - filter("KEY1" IS NOT NULL)
4 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
5 - filter("KEY1" IS NOT NULL)
6 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
|
2.3.6. 스타 조인
스타조인이란?
어떤 의미에서 볼 때는 새로운 방식의 조인이 아니라고도 할 수 있다.
개념적으로만 생각해 본다면 기존의 조인방식들로 수행하면서 단지 조인의 실행계획이 특정한 처리절차로 수행되는 것일 뿐이라고 할 수 있다.
스타라는 말이 사용된 연유는 조인된 집합들 가의 관계들이 마치 별 모양처럼 생겼기 때문에 붙여진 것이다.
스타 스키마
위 그림의 문제점들...
1) 처리범위를 줄일 수 있는 조건들이 여러 테이블에 분산되어 있다는 점에 유의하자. ( 조인은 다이 다이 )
2) 각 테이블이 보유 하고 있는 조건들의 면면을 살펴보면 어느 것을 선택하더라도 처리 범위가 별로 줄어 들지 않는다는 것을 알 수 있다. ( SALES 테이블의 처림범위를 얼마나 효과적으로 줄일수 있는냐가 관건 )
3) SALES 테이블이 남의 도움을 받지 않고 수행할 수 있는 SALES_DATE는 3개월이나 되어 처리 범위가 만만치 않다. ( 스스로 힘들어 함 )
4) SALES_TYPE이나 SALES_DEPT의 도움을 받더라도 그렇게 많은 처리범위가 줄어들 것으로 보이지도 않는다.
5) 물론 이러한 경우를 대비해서 'SALES_TYPE + SALES_DATE' or 'SALES_DEPT + SALES_DATE, PRODUCT_CD + SALES_DATE'로 된 결합인덱스를 생성해 둔다면 얼마간 효과를 얻을 수 있다. ( 하지만 이런 인덱스 정책이라면 선택할 수 있는 해법은 아님 )
6) 물론 사용자 요구 사항이 적어 인덱스 개수가 그리 많지 않고, 효과 또한 크다면 적절한 인덱스 전략을 통해 해결이 가능하다.
7) 각 연결단계마다 조금씩 줄여 마지막 결과가 소량이 되었다면, 결과적으로 불필요한 처리가 많이 발생 했음을 의미한다.
8) 연결작업이 수행되고 난 결과 집합은 이미 기존의 테이블이 아니므로 이제 더 이상 인덱스를 가질 수 없다. 만약 NL 조인이라면 어쩔 수 없이 선행처리 집합이 되어서 다른 테이블을 랜덤으로 연결하게 되며, 해쉬조인이나 Sort Merge 조인이 되더라도 각 조인 단계의 결과 집합이 적지 않은 크리를 가지고 있으므로 부담이 된다.
위 문제점을 해결 할 수있는 논리지적으로 가장 이상적인 처리 방법은.?
1) 소량의 데이터를 가지고 있는 집합들이 힘을 모아 상수 집합을 만든다.
2) 만든 상수집합을 일거에 SALES 테이블에 제공할 수만 있다면 대량의 데이터를 가진 SALES 테이블이 한 번만에 연결작업을 수행하게 된다.
3) 만약 적절한 인덱스가 존재해 이러한 상수값들로 처적의 처리범위를 줄여줄 수만 있다면 바로 소량의 결과만 추출할 수 있게 되므로 아주 이상적인처리방법이 될 것이다.
위 이상적인 방법을 실현하기 위해서는 반드시 해결해야되는 몇 가지 문제점은.?
1) 디멘전테이블 가에는 릴레이쉽이 없기 때문에 이들 간의 연결을 먼저 시도할 수 없다는 점 ( 디멘전테이블들 간의 카티션곱으로 해결 할수 있음 )
2) 상수 집합값에 해당하는 팩터테이블에 적절한 인덱스를 보유해야함 ( 어떤 디멘전 테이블이 사용될지 종잡을 수 없어서 인덱스 정책 수립이 쉽지만은 않다. 최적에 인덱스는 '=' )
3) D1, D2, D3, D4들의 디멘전 테이블 = 15가지 경우의 수 ( 2의 4승 ), 디멘저 테이블이 10개면 2의 10승 <= 이런 인덱스 정책문제가 가장 근본적인 문제임
위 그림 설명...( 스타 조인 )
1) 디멘전 테이블 D1, D2, D3를 '무조건'( 카티션곱 ) 을 연결고리로 하여 조인하면 카티션 곱으로 만들어진 우측의 CARTESIAN_T와 같은 집합이 만들어 진다.
2) 쓸모없는 집합 같지만 3개의 디멘전 테이블들이 만들어 낼 수 있는 모든 경우의 수를 가지고 있다.
3) 그림의 좌측 하단에 있는 FACT_T는 데이터모델 상으로는 디멘전 테이블들과 릴레이션쉽을 맺고 있다. 그러나 논리적으로 보면 우측의 CARTESIAN_T와 릴레이션쉽을 맺고 있다는 것과 동일한 의미가 된다.
4) 일반적으로 디멘전 테이블은 아주 소량으로 구성된다. 개수가 많지않다면 이들의 곱 또한 크게 걱정할 것이 없다. ( 2 * 3 * 2 = 12 )
5) 문제의 초점을 좀더 명확히 하기 위해 위 그림에 있는 FACT_T를 백만 건이라고 가정해보자. 어떤 조인 방식을 도입하건 FACT_T와 D1의 조인은 100만건과 두건의 연결이 발생하고 그 결과는 다시 백만 건이 되어 D2와 연결을 수행하여야 한다. 그 결과는 또 다시 백만건이 된다 그 결과를 또 또 다시 D3와 연결을 해야한다.
스타 조인에 주의 할점
1) 디멘저의 카티션 곱이 지나치게 많은 집합을 만들지 않을 때만 사용해야 한다.
2) 지나치게 높은 카디널리티가 가진 디멘저 테이블이 존재한다면.. 스타조인에 배제하고 경우에 따라서 먼저 조인을 수행시키든지, 아니면 최종 결과와 마지막으로 조인을 하는 것이 바람직하다.
p. 606 준비 스크립트
SQL> SELECT * FROM V$VERSION WHERE ROWNUM <= 1;
BANNER
---------------------------------------------------------------------------------
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production
CREATE TABLE COUNTRY_JH TABLESPACE ORA03TS02 AS
SELECT TO_CHAR( LEVEL, 'FM000009' ) COUNTRY_CD
, 'NAME_'||LEVEL AS COUNTRY_NAME
FROM DUAL
CONNECT BY LEVEL <= 500
CREATE TABLE TYPES_JH TABLESPACE ORA03TS02 AS
SELECT CHR( 64 + LEVEL ) TYPE_CD
, 'NAME_'||CHR( 64 + LEVEL ) AS TYPE_NAME
FROM DUAL
CONNECT BY LEVEL <= 26
CREATE TABLE PRODUCTS_JH TABLESPACE ORA03TS02 AS
SELECT PRODCUT_CD2||PRODCUT_CD1||TO_CHAR( LV, 'FM009') AS PRODUCT_CD
, 'NAME_'||PRODCUT_CD2||PRODCUT_CD1||TO_CHAR( LV, 'FM009') AS PRODUCT_NAME
FROM (SELECT CHR( 64 + LEVEL ) PRODCUT_CD1 FROM DUAL CONNECT BY LEVEL <= 26 ) A
, (SELECT CHR( 64 + LEVEL ) PRODCUT_CD2 FROM DUAL CONNECT BY LEVEL <= 26 ) B
, (SELECT LEVEL LV FROM DUAL CONNECT BY LEVEL <= 101 ) C
--WHERE PRODCUT_CD2 = 'B'
CREATE TABLE DEPT_JH TABLESPACE ORA03TS02 AS
SELECT DISTINCT DEPT_NO, DEPT_NAME
FROM (
SELECT RPAD( LEVEL, 5, '0' ) AS DEPT_NO
, 'NAME_'|| RPAD( LEVEL, 5, '0' ) AS DEPT_NAME
FROM DUAL
CONNECT BY LEVEL <= 99999
)
CREATE TABLE SALES_JH TABLESPACE ORA03TS02 AS
SELECT LEVEL SALES_NO
, TO_CHAR( TRUNC( dbms_random.value( 1,501 ) ), 'FM000009' ) COUNTRY_CD
, CHR( TRUNC( 64+dbms_random.value( 1,27 ) ) ) || CHR( TRUNC( 64+dbms_random.value( 1,27 ) ) ) || TO_CHAR( TRUNC( dbms_random.value( 1,102 ) ), 'FM009' ) PRODUCT_CD
, RPAD( TRUNC( dbms_random.value( 1,99999 + 1 ) ), 5, '0' ) SALES_DEPT
, CHR( TRUNC( 64+dbms_random.value( 1,27 ) ) ) AS SALES_TYPE
, TO_CHAR( ( SYSDATE - 100 ) - 1 / 1440 / ( 60 / ( 2000001 - LEVEL )), 'YYYYMMDDHH24MISS' ) SALES_DATE
, TRUNC( dbms_random.value( 1,501 ) ) * 1000 AS SALES_AMOUNT
, 'list_price' AS LIST_PRICE
FROM DUAL
CONNECT BY LEVEL <= 2000000
ORA-30009: CONNECT BY 작업에 대한 메모리가 부족합니다. ㅡㅡ^
CREATE TABLE SALES_JH TABLESPACE ORA03TS02 AS
SELECT LEVEL SALES_NO
, TO_CHAR( TRUNC( dbms_random.value( 1,501 ) ), 'FM000009' ) COUNTRY_CD
, CHR( TRUNC( 64+dbms_random.value( 1,27 ) ) ) || CHR( TRUNC( 64+dbms_random.value( 1,27 ) ) ) || TO_CHAR( TRUNC( dbms_random.value( 1,102 ) ), 'FM009' ) PRODUCT_CD
, RPAD( TRUNC( dbms_random.value( 1,99999 + 1 ) ), 5, '0' ) SALES_DEPT
, CHR( TRUNC( 64+dbms_random.value( 1,27 ) ) ) AS SALES_TYPE
, TO_CHAR( ( SYSDATE - 100 ) - 1 / 1440 / ( 60 / ( 20000001 - LEVEL )), 'YYYYMMDDHH24MISS' ) SALES_DATE
, TRUNC( dbms_random.value( 1,501 ) ) * 1000 AS SALES_AMOUNT
, 'list_price' AS LIST_PRICE
FROM DUAL
CONNECT BY LEVEL <= 0
TRUNCATE TABLE SALES_JH;
BEGIN
FOR IDX IN 1 .. 50000000 LOOP
INSERT INTO SALES_JH
SELECT IDX SALES_NO
, TO_CHAR( TRUNC( dbms_random.value( 1,501 ) ), 'FM000009' ) COUNTRY_CD
, CHR( TRUNC( 64+dbms_random.value( 1,27 ) ) ) || CHR( TRUNC( 64+dbms_random.value( 1,27 ) ) ) || TO_CHAR( TRUNC( dbms_random.value( 1,102 ) ), 'FM009' ) PRODUCT_CD
, RPAD( TRUNC( dbms_random.value( 1,99999 + 1 ) ), 5, '0' ) SALES_DEPT
, CHR( TRUNC( 64+dbms_random.value( 1,27 ) ) ) AS SALES_TYPE
, TO_CHAR( ( SYSDATE - 100 ) - 1 / 1440 / ( 60 / ( 50000001 - IDX )), 'YYYYMMDDHH24MISS' ) SALES_DATE
, TRUNC( dbms_random.value( 1,501 ) ) * 1000 AS SALES_AMOUNT
, 'list_price' AS LIST_PRICE
FROM DUAL;
END LOOP;
COMMIT;
END;
CREATE UNIQUE INDEX COUNTRY_JH_PK ON COUNTRY_JH ( COUNTRY_CD ) TABLESPACE ORA03TS02
CREATE UNIQUE INDEX TYPES_JH_PK ON TYPES_JH ( TYPE_CD ) TABLESPACE ORA03TS02
CREATE UNIQUE INDEX PRODUCTS_JH_PK ON PRODUCTS_JH ( PRODUCT_CD ) TABLESPACE ORA03TS02
CREATE UNIQUE INDEX DEPT_JH_PK ON DEPT_JH ( DEPT_NO ) TABLESPACE ORA03TS02
CREATE INDEX SALES_JH_INDEX01 ON SALES_JH ( SALES_DEPT, PRODUCT_CD ) TABLESPACE ORA03TS02
-- 제약 조건 생성 후 통계정 보를 생성해야 STAR로 풀릴수 있다. ( 이거때무네.. ㅠ 4시간 삽질 )
ALTER TABLE DEPT_JH ADD (
CONSTRAINT DEPT_JH_PK
PRIMARY KEY
(DEPT_NO)
USING INDEX DEPT_JH_PK);
ALTER TABLE PRODUCTS_JH ADD (
CONSTRAINT PRODUCTS_JH_PK
PRIMARY KEY
(PRODUCT_CD)
USING INDEX PRODUCTS_JH_PK);
ALTER TABLE TYPES_JH ADD (
CONSTRAINT TYPES_JH_PK
PRIMARY KEY
(TYPE_CD)
USING INDEX TYPES_JH_PK);
ALTER TABLE COUNTRY_JH ADD (
CONSTRAINT COUNTRY_JH_PK
PRIMARY KEY
(COUNTRY_CD)
USING INDEX COUNTRY_JH_PK);
--ALTER TABLE TYPES_JH DROP CONSTRAINT SALES_JH_FK2;
ALTER TABLE SALES_JH ADD (
CONSTRAINT SALES_JH_FK4
FOREIGN KEY (SALES_DEPT) --SALES_DEPT 4 Y VARCHAR2 (20 Byte) Height Balanced 90144
REFERENCES DEPT_JH (DEPT_NO));
ALTER TABLE SALES_JH ADD (
CONSTRAINT SALES_JH_FK3
FOREIGN KEY (PRODUCT_CD) --SALES_TYPE 5 Y VARCHAR2 (4 Byte) Frequency 26
REFERENCES PRODUCTS_JH (PRODUCT_CD));
ALTER TABLE SALES_JH ADD (
CONSTRAINT SALES_JH_FK2
FOREIGN KEY (SALES_TYPE) --SALES_TYPE 5 Y VARCHAR2 (4 Byte) Frequency 26
REFERENCES TYPES_JH (TYPE_CD));
ALTER TABLE SALES_JH ADD (
CONSTRAINT SALES_JH_FK1
FOREIGN KEY (COUNTRY_CD)
REFERENCES COUNTRY_JH (COUNTRY_CD));
EXEC DBMS_STATS.GATHER_TABLE_STATS( USER, 'COUNTRY_JH' );
EXEC DBMS_STATS.GATHER_TABLE_STATS( USER, 'TYPES_JH' );
EXEC DBMS_STATS.GATHER_TABLE_STATS( USER, 'PRODUCTS_JH' );
EXEC DBMS_STATS.GATHER_TABLE_STATS( USER, 'DEPT_JH' );
EXEC DBMS_STATS.GATHER_TABLE_STATS( USER, 'SALES_JH' );
|
(a) 스타조인은 반드시 비용기준( Cost_based ) 옵티마이져 모드에서 수행되어야 한다. 또한 통계 정보를 생성해 주어야 작동 할 수 있다. ( 또한 PK AND FK 꼭필요하다 ( 주관 ) ) 원하는 실행계획이 생성되지 않을 때는 /*+ STAR */힌트를 적용한다.
(b) 디멘저 테이블들이 먼저 조인하여 카티션 곱을 만들어 내는 것을 확인할 수 있다. 이처럼 카티션 곱을 생성하는 대부분의 경우는 Sort Merge 조인 형식으로 나타난다.
(c) 카티션 곱을 좀더 효율적으로 생성하기 위해 정렬을 한 집합을 버퍼에 저장해 두는 것을 보여 주고 있다.
(d) 카티션 곱으로 생성된 집합과 팩트 테이블인 Sales 테이블이 해쉬 조이을 하고 있음을 확인할 수 있다, 그러나 이 단계가 항상 해쉬 조이이 되는 것은 아니다. 어쩌면 이 단계는이미 스타조인의 문제가 아니다. 단지 준비된 두 개의 집합이 가장 효율적인 조인형식을 선택하는 문제가 남아 있을 뿐이다.
(d)에서 제기된 문제가 바로 앞에서 우리가 해결하기 위해 남겨 두었던 바로 그 두 번재 문제이다. 카티션 곱의 집합은 이미 인덱스를 가질 수 없고, 팩트 테이블은 디멘전 테이블들의 수많은 조합을 모두 감당할 인덱스를 미리 구성하기가 어려우므로 인덱스를 사용하지 않는 해쉬 조인이나 Sort Merge 조인으로 수행하는 것이 바람직한 방법이다.
SALES_JH_INDEX01이 'DEPT_NO + PRODUCT_CD'( 순서는 무관 )로 구성 되어 있다면 문제가 없다.
만약 인덱스가 'DEPT_NO + SALES_TYPE + PRODUCT_CD'로 구성되어 있다면 상황은 달라진다. 중간에 있는 SALES_TYPE이 상수값을 제공받지 못한 상태에서 인덱스가 사용된다는 것은 결합 인덱스의 원리에 의해서 비효율이 발생하게 된다. 옵티마이젼느 이러한 인덱스를 구성일 때도 NL 조인으로 실행계획을 수립하는 경향이 많이 있다.
이처럼 전략적이지 못한 인덱스가 존재한다면 스타조인이 NL 형식으로 처리될 때 문제가 발생하는 경우가 많으므로 가장 우선적으로 해야할 일은 전략적인 인덱스를 구성하는 것이며, 그 다음은 옵티마이져가 수립한 실행계획을 확인할 필요가 있다는 점을 명심하길 바란다.
왜 디멘저 테이블을 FS 으로 풀었냐구요? 조건절 이행으로 인한 펙터 테이블이 선두 테이블로 되기 때문에...
(a) SQL이 비용기준으로 정의 되어 있지 않은 것을 보여주고 있다.. ( ㅡㅡ^ ) 위 실행계획은 스타조인에 의해 만드어진 것이 아님을 의미하고 있다.
(b) 인라인뷰를 사용하여 강제로 디메전 테이블이 먼저 조인되도록 하여 카티션이 나타났다.
(c) 'COUNT'는 사실 없어도 무관한 단계이기는 하지만 위의 SQL에 있는 인라인뷰의 'ROWNUM' 때문에 생겨난 것이다. ( NO_MERGE 힌트와 같은 효과 ) ROWNUM이 가시는 가상속성 때문에 병합이 방지하는 효과를 가져오게 된다. 물론 힌트만 제대로 작동된다는 보장만 있다면 'NO_MERGE'힌트를 사용하여 동일한 효과를 낼 수가 있다.
(d) 해쉬조인 힌트를 사용하여 카티션곱과 팩트 테이블의 최적의 조인을 하도록 유도하였다.
스타 조인은 단점 2가지.?
디멘전의 카티션 곱이 너무 클 때는 사용해서는 안된다
최종적으로 연결해야 할 팩트 테이블과의 조인 방식에 유의해야 한다. 해쉬 조인 : NL 조인처럼 선행집합의 처리 결과를 완벽하게 자기 집합의 처리범위를 줄일 수는 없다는데 한계가 있다. NL 조인 : 완벽한 결합 인덱스가 없다면 무용지밀이 된다.(이를 대비하기 위한 모든 사용형태에 대한 결합 인덱스를 만들어 줄수 없다면 아직 이러한 방법은 최적이 아니다.
2.3.7 스타 변형 조인.?
스타변형 조인은 스타조인의 일부 단점을 개선한 조인이다. ( 스타조인을 완전히 대체하는 개념이 아니라는 것을 먼저 밝혀둔다 )
스타조인을 필요없게 만드는 것이 아니라 스타 조인이 갖는 단점을 개선할 수 있다는 것에 의미를 두기 바란다.
비트맵 인덱스의 특성을 살린 것이다. 따라서 비트맵 인덱스의 장.단점을 그대로 승계하고 있다.
스타조인은 카티젼 곱을 만들지만 스타변형조인은 비트맵 인덱스를 활용한다.
비트맵 인덱스는 액세스 형태에 따라 중간에 사용되지 않는 컬럼이 발생하지 않도록 다양한 형태의 결합인덱스를 구성해야하는 B-Tree 인덱스와는 달리 각가의 독립적인 비트맵 인덱스가 액세스 형태에 따라 그때마다 머지하는 방식으로 처리되더라도 수행속다가 나빠지지 않는다.
인덱스 머지의 장점.?
1) 수많은 결합 인덱스를 가지지 않도록 할 수 있다.
2) 팩트 테이블을 연결하기전에 먼저 처리되어 팩트 테이블의 처리범위를 공동으로 줄여 줄 수 있다는 것이다.
p. 618 실습 스크립트
CREATE INDEX SALES_JH_INDEX02 ON SALES_JH ( PRODUCT_CD ) TABLESPACE ORA03TS02
SQL> SELECT /*+ INDEX_COMBINE(A A( PRODUCT_CD ) A( SALES_DEPT ) ) */ COUNT( * )
2 FROM SALES_JH A
3 WHERE PRODUCT_CD LIKE 'PA%'
4 AND SALES_DEPT LIKE '456%'
5 ;
COUNT(*)
----------
70
SQL> @XPLAN
---------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 180 | 179 |
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 180 | 179 |
|* 2 | INDEX RANGE SCAN| SALES_JH_INDEX01 | 1 | 1 | 70 |00:00:00.01 | 180 | 179 |
---------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("SALES_DEPT" LIKE '456%' AND "PRODUCT_CD" LIKE 'PA%')
filter(("PRODUCT_CD" LIKE 'PA%' AND "SALES_DEPT" LIKE '456%'))
SQL> @hidden_param _b_tree_bitmap_plans
Enter value for v_par: _b_tree_bitmap_plans
old 8: AND LOWER(A.KSPPINM) LIKE '%'|| TRIM(LOWER('&v_par')) || '%'
new 8: AND LOWER(A.KSPPINM) LIKE '%'|| TRIM(LOWER('_b_tree_bitmap_plans')) || '%'
NAME
--------------------------------------------------------------------------------
VALUE
--------------------------------------------------------------------------------
DEF_YN
---------
DESCRIPTION
--------------------------------------------------------------------------------
_b_tree_bitmap_plans
TRUE
TRUE
enable the use of bitmap plans for tables w. only B-tree indexes
DROP INDEX SALES_JH_INDEX01
CREATE INDEX SALES_JH_INDEX01 ON SALES_JH ( SALES_DEPT ) TABLESPACE ORA03TS02
SQL> SELECT /*+ INDEX_COMBINE(A A( PRODUCT_CD ) A( SALES_DEPT ) ) */ COUNT( * )
2 FROM SALES_JH A
3 WHERE PRODUCT_CD LIKE 'PA%'
4 AND SALES_DEPT LIKE '456%'
5 ;
COUNT(*)
----------
70
SQL> @XPLAN
--------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem |
--------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.16 | 313 | 309 | | | |
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.16 | 313 | 309 | | | |
|* 2 | VIEW | index$_join$_001 | 1 | 1 | 70 |00:00:00.16 | 313 | 309 | | | |
|* 3 | HASH JOIN | | 1 | | 70 |00:00:00.16 | 313 | 309 | 3755K| 1398K| 5578K (0)|
|* 4 | INDEX RANGE SCAN| SALES_JH_INDEX02 | 1 | 1 | 74378 |00:00:00.05 | 179 | 176 | | | |
|* 5 | INDEX RANGE SCAN| SALES_JH_INDEX01 | 1 | 1 | 55267 |00:00:00.02 | 134 | 133 | | | |
--------------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter(("PRODUCT_CD" LIKE 'PA%' AND "SALES_DEPT" LIKE '456%'))
3 - access(ROWID=ROWID)
4 - access("PRODUCT_CD" LIKE 'PA%')
5 - access("SALES_DEPT" LIKE '456%')
SQL> SELECT /*+ INDEX_COMBINE(A ) */ COUNT( * )
2 FROM SALES_JH A
3 WHERE PRODUCT_CD LIKE 'PA%'
4 AND SALES_DEPT LIKE '456%'
5 ;
COUNT(*)
----------
70
SQL> @XPLAN
------------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
------------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.21 | 313 | | | |
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.21 | 313 | | | |
| 2 | BITMAP CONVERSION COUNT | | 1 | 1 | 1 |00:00:00.21 | 313 | | | |
| 3 | BITMAP AND | | 1 | | 1 |00:00:00.20 | 313 | | | | <---
| 4 | BITMAP CONVERSION FROM ROWIDS| | 1 | | 7 |00:00:00.11 | 179 | | | |
| 5 | SORT ORDER BY | | 1 | | 74378 |00:00:00.09 | 179 | 2320K| 704K| 2062K (0)|
|* 6 | INDEX RANGE SCAN | SALES_JH_INDEX02 | 1 | | 74378 |00:00:00.04 | 179 | | | |
| 7 | BITMAP CONVERSION FROM ROWIDS| | 1 | | 5 |00:00:00.08 | 134 | | | |
| 8 | SORT ORDER BY | | 1 | | 55267 |00:00:00.07 | 134 | 1824K| 650K| 1621K (0)|
|* 9 | INDEX RANGE SCAN | SALES_JH_INDEX01 | 1 | | 55267 |00:00:00.03 | 134 | | | |
------------------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
6 - access("PRODUCT_CD" LIKE 'PA%')
filter(("PRODUCT_CD" LIKE 'PA%' AND "PRODUCT_CD" LIKE 'PA%'))
9 - access("SALES_DEPT" LIKE '456%')
filter(("SALES_DEPT" LIKE '456%' AND "SALES_DEPT" LIKE '456%'))
|
각각을 서로 결합( BITMAP AND )하여 테이블을 액세스한다. 우리가 너무나 잘 알고 있는 비트맵 인덱스의 기본형이다.
부여 받을 상수값이 다른 테이블에 존재할 경우 이를 서브쿼리를 통해서 제공할 수 있다고 생각하라는 것이다.
모든 서브쿼리가 먼저 수행되어 그 결과를 메인 쿼리의 비트맵 인덱스에게 제공하여 각각 비트맵을 액세스한 후에 이들을 'BITMAP AND' 연산의 가장 이상적인 실행계획
비트리로 구성된 SALES_JH_INDEX02가 BITMAP CONVERSION으로 변화하여 'BITMAP AND'를 수행한다는 것을 알 수 있다.
이 조인 방식은 디멘전 테이블에 주어진 조건들을 최대한 이용하였을 때 팩트 테이블의 처리범위가 현격하게 줄어든다면 비로소 빛을 발한다는 것을 의미한다.
만약 여러개의 디멘저 테이블들이 각각 서브쿼리를 통해 팩트 테이블의 비트맵을 액세스( BITMAP KEY ITERATION )하였으나 거의 감소하지 않았다고 하자 그렇다면 이들은 아무리 BITMAP AND 를 하더라도 그 결과는 매우 넓은 범위가 될 것이다. 더구나 이를 ROWID로 변환하여 일일이 팩트 테이블을 액세스 한다면 애써 많은 처리를 해 왔지만 정작 효과는 전혀 없다. 게다가 아직 조인해야 할 디멘저 테이블들이 그대로 남아 있으므로 우리가 얻은 것은 아무 것도 없다. 그 반대의 경우에는 최상의 실행 계획이라 할 수있다 ( 디멘전 처리 범위가 넓은 경우 = 스타 조인, 디멘전의 처리 범위가 작을 경우
그 결고 ROWID로 변환하여 팩트 테이블을 액세스 한다.
SALES_JH 검색 범위가 넓어 부하가 상당하다.
스타 조인과 스타 변형 조인은 어떤경우에 유용한가.? ( 주관 )
스타 조인 : 1) 디멘전 테이블들의 처리 범위 가 넓은 경우 2) 최소의 인덱스 정책 수립만으로 대응이 가능할 경우
스타 변형 조인 : 1) 디멘전 테이블들의 처리 범위가 작은 경우 2) 크리티걸한 인덱스 정책 수립이 필요한 경우
그러나 아니란다.;;
실전에서 적용되는 경우를 보면 스타 변형 조인이 훨씬 가치가 있고, 자주 사용되고 있다. ( DW 주로 사용 )
팩트 테이블에는 디멘전 컬럼과 계측값을 가진 메져( Measure ) 컬럼으로 구성되고 있다.
디멘전 컬럼은 디멘전 테이블들의 외부키로써 존재한다.
외부에서 주어지는 조건들은 디멘전 컬럼에 직접 주어지거나 디멘전 테이블의 컬럼에 주어진다.
실전에서 발생하는 대부분의 이러한 엑세스등른 주어진 조건들을 모두 적용하면 팩트 테이블의 처리범위가 크게 감소하는 경우가 많다.
그러므로 현질적으로는 스타변형 조인이 DW에서 일반적으로 훨씬 유리하다.
스타 변형 조인의 필수적인 전제 조건
1) 하나의 팩트 테이블과 최소한 2개 이상의 디멘전 테이블이 있어야 한다.
2) 팩트 테이블에 있는 디멘전 - 즉, 디멘전 테이블들의 외부키 - 에는 반드시 비트맵 인덱스가 존재해야 한다. 그러나 비트리 인덱스가 있어도 비트맵 컨버전이 일어나므로 스타변형조인이 발생할 수 있다. 그러나 복합( Composite ) 비트맵 인덱스 상에서 발생할 때는 사용상의 주의가 필요하다.
3) 팩트 테이블에 반드시 통계정보가 생성되어 있어야 한다. 그러나 ANALYZE 모드에는 영향을 받지 않는다.
4) STAR_TRANSFORMATION_ENABLED 파라미터가 FALSE가 아닌 TRUE 나 TEMP_DISABLE로 설정되어 있거나. 아니면 쿼리에 직접 STAR_TRANSFORMATION 힌트를 주어야 작동한다.
스타 변형 조인의 제약 조건
1) 비트맵을 사용할 수 없게 하는 힌트와는 서로 양립할 수 없다. 가령, FULL, ROWID, STAR와 같은 힌트는 논리적으로 서로 공존할 수 없기 때문이다.
2) 쿼리 내에 바인드 변수를 사용하지 않아야 한다. 어떤 경우의 바인드 변수의 사용도 스타 변형 조인을 발생시키지 않는다. 스타 변형 조인을 위한 WHERE절의 바인드 변수 뿐만 아니라, 이와 상관없는 WHERE 절에 바인드 변수가 있어도 스타 변형 조인은 일어 나지 않는다. 이러한 현상이 발생하는 이유에 대해서는 뒤에서 별도로 설명하기로 하겠다.
3) 원격( Remote ) 팩터 테이블인 경우에는 스타 변형 조인이 일어나지 않는다. 그 이유는 각 서브쿼리마다 원격에 있는 팩트 테이블을 비트맵 탐침( BITMAP KEY ITERATION )하고 최종적으로 합성 ( BITMAP AND) 된 결과로 다시 원격 테이블을 액세스 해야 하므로 오히려 부하가 크게 증가하기 때문이다.
4) 그러나 디멘저 테이블이 원격에 있으면 이 조인은 일어날 수 없다. 디멘전 테이블은 일반적으로 소량이며, 서브쿼리에서 먼저 수행하거나 마지막으로 해쉬조인 드응로 조인할 때 사용되므로 그리 큰 부담이 되지 않기 때문이다.
5) 부정형 조인으로 수행되는 경우에는 이 조인이 발생하지 않는다. 우리가 앞서 부정형 조인에서 그 특성을 알아보았듯이 부정형 조인은 제공자 역할이 아닌 확인자의 역할만을 담당하므로 스타변형 조인으로 수행되더라도 전혀 얻을 것이 없기 때문이다.
6) 인라인뷰나 뷰 쿼리 중에는 독립적으로 먼저 수행될 수 있는 경우와 액세스 쿼리와 머지 한 후에 수행될 수 있는 두 가지 종류가 있다. 후자의 경우는 이 조인이 일어날 수 없다. 그 이유는 논리적으로 보더라도 아직 머지가 일어나지 않은 쿼리르 대상으로 스타변형 조인을 위한 질의 재생성을 할수는 없기 때문이다.
7) 서브쿼리에서 이미 디멘전 테이블로 사용한 테이블에 대해서는 이런 방식의 조인을 위한 변형이 일어나지 않는다. 물론 여기서 말하는 서브쿼리는 변형에 의해 생성된 서브쿼리가 아니라 사용자 쿼리에 직접 기술한 것ㅇ르 말한다. 만약 변형이 일어난다면 이중으로 생기게 되므로 이러한 제약이 있는 것은 당연하다.
옵티마이져의 판단에 의해 스타변형 조인이 일어 나지 않는 경우.? ;;
1) 팩트 테이블이 가진 조건들 만으로도 충분히 처리범위가 주어든다고 판단하였거나 특정 디멘전 테이블이 선행해서 제공자 역하을 한 것만으로도 충분한 경우에는 스타변형조인은 일어나지않는다.
2) 팩트 테이블의 크기가 너무 작아서 굳이 스타변형 조인을 수행시킬 가치가 없다고 판단한 경우에도 이 조인은 일어나지 않는다.
3) 인덱스 머지에서도 나타났듯이 '머지'
바인드 변수는 상수값 아니므로 옵티마이져가 정확한 판단을 할 수 없어 효과를 보장 할 수 없기 때문에 아예 포기하는 것이다. ( 필자님 고찰 )
1) 실전에서 일어나는 대부분의 경우는 평균적ㅇ니 분포도를 기준으로 결정했을 때도 크나큰 문제가 없으며, 실전에서 사용되는 대다수의 쿼리에는 바인드 변수가 포함되어 있기 때문에 이러한 경우를 모두 제외해 보리면 이 조인을 활용할 기회가 너무 적어지기 때문에 바람직하지 않다.
2) 정 부담되면 변수를 사용한 조건이 포함된 부분만 변형에 포함시키지 않는다거나 퀄의 사용자가 힌트를 통해 반드시 적용하겠다고 의지를 보였다면 그 책임은 옵티마져에게 있는 것이 아니므로 이를 허용하는 것이 바람직하다고 믿는다. 이점은 점차 개선될 것이다 ( ㅡㅡ^ )
3) 비트맵 합성에서도 어느 한쪽이 지나치게 넓은 범위를 가지고 있다면 이를 합성 대상에서 제외 시킨다. 물론 항상 제외되는 것은 아니다 좁은 범위를 가지고 있는 다른디멘저들이 충분히 그 역할을 담당한다는 판단이 서면 제외시키지만 그렇지 않을 때는 대상에 포함시킨다. 실제로 테스트를 해보면 등장한 디멘전 테이블들의 수에 따라 선택된 디멘전이나 개수에 미묘한 차이를 보이고 있다. 그러나 여기에는 굳이 테이스트 결과를 공개하지 않겠다. 이것은 어차피 옵티마이져가 판단할 문제이므로 여러분이 너무 깊순한 부분까지 알면 오리혀 혼란만 가중될 수도 있을 거이라는 생각이기 때문이다 ( ㅡㅡ^ )
변칙적이지만 이런 생각 하시는 분이 있을것입니다. 굳이 서브쿼리들이 필요한가.? 네 필요합니다. 책에서는 디멘전 테이블이 중앙 테이블을 제어하기 때문입니다. 하지만 TEST ㄱㄱ
처리범위가 작다면 스타 변형 조인이 답인데.. 위에 처럼 처리 범위가 넓다면 정말 스타 변형 조인이 답인가.?
SQL> SELECT /*+ LEADING( P ) INDEX_JOIN( S ) */ D.DEPT_NAME, C.COUNTRY_CD, P.PRODUCT_NAME, T.TYPE_NAME
2 , SUM( S.SALES_AMOUNT )
3 FROM COUNTRY_JH C, PRODUCTS_JH P, DEPT_JH D, TYPES_JH T, SALES_JH S
4 WHERE C.COUNTRY_CD = S.COUNTRY_CD
5 AND P.PRODUCT_CD = S.PRODUCT_CD
6 AND D.DEPT_NO = S.SALES_DEPT
7 AND T.TYPE_CD = S.SALES_TYPE
8 AND S.SALES_DATE LIKE '201104%'
9 AND S.PRODUCT_CD IN ( SELECT /*+ UNNEST */ PRODUCT_CD FROM PRODUCTS_JH WHERE ( PRODUCT_CD ) IN ( 'AT080' , 'CP001' ))
10 AND ( S.SALES_TYPE ) IN (SELECT /*+ UNNEST */ TYPE_CD FROM TYPES_JH WHERE ( TYPE_CD ) BETWEEN 'A' AND 'Z' )
11 AND S.SALES_DEPT IN ( SELECT /*+ UNNEST */ DEPT_NO FROM DEPT_JH WHERE ( DEPT_NO ) LIKE '8%' )
12 -- AND ROWNUM <= 5
13 GROUP BY D.DEPT_NAME, C.COUNTRY_CD, P.PRODUCT_NAME, T.TYPE_NAME;
DEPT_NAME COUNTRY_CD PRODUCT_NAME TYPE_NAME SUM(S.SALES_AMOUNT)
-------------------------------------------------- -------------- ---------------------------------- ------------------ -------------------
NAME_80030 000490 NAME_AT080 NAME_F 476000
NAME_88348 000046 NAME_AT080 NAME_B 303000
...
21 개의 행이 선택되었습니다.
SQL> @XPLAN
-------------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
-------------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 21 |00:00:00.01 | 1547 | | | |
| 1 | HASH GROUP BY | | 1 | 2 | 21 |00:00:00.01 | 1547 | 813K| 813K| 1355K (0)|
| 2 | NESTED LOOPS | | 1 | | 21 |00:00:00.01 | 1547 | | | |
| 3 | NESTED LOOPS | | 1 | 2 | 21 |00:00:00.01 | 1526 | | | |
| 4 | NESTED LOOPS | | 1 | 2 | 21 |00:00:00.01 | 1522 | | | |
| 5 | NESTED LOOPS | | 1 | 2 | 21 |00:00:00.01 | 1478 | | | |
| 6 | INLIST ITERATOR | | 1 | | 2 |00:00:00.01 | 6 | | | |
| 7 | TABLE ACCESS BY INDEX ROWID| PRODUCTS_JH | 2 | 2 | 2 |00:00:00.01 | 6 | | | |
|* 8 | INDEX UNIQUE SCAN | PRODUCTS_JH_PK | 2 | 2 | 2 |00:00:00.01 | 4 | | | |
|* 9 | TABLE ACCESS BY INDEX ROWID | SALES_JH | 2 | 1 | 21 |00:00:00.01 | 1472 | | | |
|* 10 | INDEX RANGE SCAN | SALES_JH_INDEX02 | 2 | 1 | 1463 |00:00:00.01 | 10 | | | |
| 11 | TABLE ACCESS BY INDEX ROWID | DEPT_JH | 21 | 1 | 21 |00:00:00.01 | 44 | | | |
|* 12 | INDEX UNIQUE SCAN | DEPT_JH_PK | 21 | 1 | 21 |00:00:00.01 | 23 | | | |
|* 13 | INDEX UNIQUE SCAN | TYPES_JH_PK | 21 | 1 | 21 |00:00:00.01 | 4 | | | |
| 14 | TABLE ACCESS BY INDEX ROWID | TYPES_JH | 21 | 1 | 21 |00:00:00.01 | 21 | | | |
-------------------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
8 - access(("P"."PRODUCT_CD"='AT080' OR "P"."PRODUCT_CD"='CP001'))
9 - filter(("S"."SALES_DATE" LIKE '201104%' AND "S"."SALES_DEPT" LIKE '8%' AND "S"."COUNTRY_CD" IS NOT NULL AND
"S"."SALES_TYPE">='A' AND "S"."SALES_TYPE"<='Z' AND "S"."SALES_DEPT" IS NOT NULL))
10 - access("P"."PRODUCT_CD"="S"."PRODUCT_CD")
filter(("S"."PRODUCT_CD"='AT080' OR "S"."PRODUCT_CD"='CP001'))
12 - access("D"."DEPT_NO"="S"."SALES_DEPT")
filter("D"."DEPT_NO" LIKE '8%')
13 - access("T"."TYPE_CD"="S"."SALES_TYPE")
filter(("T"."TYPE_CD">='A' AND "T"."TYPE_CD"<='Z'))
|
한번 액세스한 디멘전의 결과를 재사용하기 위해서 생성하는 임시( TEMP ) 테이블에 대해 소개하겠다.
임시 테이블을 생성하는 이유 : 설사 내부적으로 테이블을 새롭게 만드는 부담이 있더라도 이를 재사용 하도록 하기 위한 것이다.
임시 테이블을 사용하는 기준 : 가령, '고객' 데이블과 같이 대량의 데이터를 가진 디멘전 테이블이 자신의 범위를 크게 줄여주는 조건을 가지고 있을 때 먼저 액세스하여 소량의 임시 테이블을 생성하고, 이를 활용하는 것은 분명히 효과가 있다. 그러나 소량의 데이터를 가진 디멘전 테이블이거나 조건이 처리범위를 별로 줄여주지 못한다면 거의 효과가 없다.
BYPASS_RECURSIVE_CHECK : 인서트 문에서 사용하는 WITH 구분과 같음 ( TEMP )
변형에 사용할 디멘전 테이블을 주어진 조건에 대한 추출하고 ( SYS_TEMP_OFD9D6615_BA951634 )
이것을 임시 테이블에 저장했다가 ( TEMP TABLE TRANSFORMATION )
비트맵 탐침에도 사용하고( BITMAP KEY ITERATION )
연결을 위한 해쉬조인에서도 사용 하였음을 알수 있다. ( 아래에서 두번째 TABLE ACCESS FULL )
스타변형 조인에서 디멘전 테이블이 항상 두번씩 액세스되는 것을 알고 있다 ( SELECT 절에 디멘전 테이블 컬럼을 사용 할 경우 ( 주관 ) )
옵티마이져가 대형( Big dimension )이라고 판단한 것에 대해서만 생성하며, 또한 대형이라고 해서 모두 생성되는 것이 아니라 조인에 참여하는 디멘전의 수에 따라 옵티마이져가 판단한다.
디멘전이 4개 이상 있을 때는 가장 큰 디멘전은 아예 제외되기 때문에 어차피 임시 테이블은 영향을 주지 못한다. 그러므로 사실 실전에서는 임시 테이블로 인한 수행속도 향상은 크게 기대할 만큼 나타나지 않는다.
만약 여러분드이 보유하고 있는 데이터 웨어하우스의 데이터 모델에서 대부분의 디멘전들이 소형이라면 'TEMP_DISABLE'을 적용해도 무리가 없다. 그러나 대형 디멘전들이 자주 사용되는 환경이라면 TRUE로 관리하는 것이 보다 좋은 결정이 될 것이다.
그럼 테스트 해봐야죠.?( 조건 범위가 큰 디멘저를 먼서 드라이빙해서 TEMP TABLE로 유도 하겠음 )
마치 팩트 테이블 컬럼처럼 인식하여 서브쿼를 만들어 비트랩 탐침도, 비트맵 머지도 하지 않고 바로 'BITMAP AND'에 참여하는 것을 확인하기 바란다.
아주 빈번하게 사용되는 디멘전 테이블의 컬럼들의 일부를 같은 방법으로 관리한다면 보다 향상된 수행속도를 얻을 수 있다.
비트맵 조인 인덱스의 개념에 대해 좀더 자세하게 알아보기로 하자.
개념을 가장 쉽게 이해하는 방법은 조인된 결과의 집합을 하나의 테이블이라고 생각하는 것이다. 바로 이 논리적 테이블에 있는 컬럼을 비트맵 인덱스로 생성하였다고 생각하면 아주 쉽다.
비트맨 조인 인덱스를 생성하는 문장을 살펴보자. 마치 SELECT 문의 조인처럼 FROM저로가 WHERE절을 가지고 있다. 서로 조인되는 테이블들은 동등한 관계를 가지고 있기 때문에 조인된 결과는 어느 특정 테이브의 소유로 볼 수는 없다. 이를 분명히 하기 위해 위의 생성문장에는 'ON SALES_JH'을 지정한 것이다.
조인을 하더라도 인덱스를 생성하는 집합이 그대로 보존되기 위해서는 다음의 사항을 준수해야한다. p. 633
인덱스를 생성하는 테이블에 조인( 참조 )되는 테이블은 반드시 기본키 컬럼이거나 유일성 제약 조건( Unique Constraints )을 가진 컬럼이 조인조건이 되어야 한다. 그것은 조인되는 집합의 유일성이 보장되지 않으면 그 결과는 보존될 수 없기 때문이다.
참조되는테이블의 조인컬럼은그 테이블의 기본키로 지정되어 있거나, 인텍스를 생성하는 테이블에서 외부키 제약조건이 선언되어야 한다.
조인 조건은 반드시 이퀄( = )이어야 하며,아우터 조인을 사용할 수 없다.
인덱스를 생성하는 조인 문에 집합을 처리하는 연산( Union, Minus 등 ), DISTINCT, SUM, AVG, COUNT 등의 집계 함수, 분석 함수, GROUP BY, ORDER BY, CONNECT BY, START WITH절의 상요해서는 안 된다.
인덱스 컬럼은 반드시 조인되는 테이블에 소속된 컬럼이어야 한다.
비트맵 조인인덱스는 비트맵 인덱스의 일종으므로 당연히 일반적인 비트맵 인덱스의 각종 규칙을 그대로 준수해야 한다. 가령, 유일성을 가진 컬럼은 비트맵 인덱스를 생성할 수 없다.
비트맵 조인인덱스 사용상제약 사항
병렬DML 처리는 비트맵조인 인덱스르 가지고 있는 테이블에서만 지원된다. 만약 관련된 참조 테이블에서 병렬 DML을 수행시키면 비트맵 조인인덱스는 'UNUSABLE'상태가 된다.
비트맵조인인덱스르 사용하게 되면, 언떤 트랜잭션에서 동시에 오직 하나의 테이블만처리해야 한다. 조인된 테이블이 COMMIT되지 않은상태에서 동시에 변경되면일관성을 보장할 수가 없기 때문이다.
조인 문장에서 동일한 테이블이 두 번 등장 할 수 없다.
동시에 여러 테이블을 참조하여 결합된 구조를 생성할 수도 있다.
CREATE BITMAP INDEX SALES_PROD_CTRY_NAME_BIX
ON SALES_JH( PRODUCTS_JH.PRODUCT_NAME, COUNTRY_JH.COUNTRY_NAME )
FROM SALES_JH
, PRODUCTS_JH
, COUNTRY_JH
WHERE SALES_JH.PRODUCT_CD = PRODUCTS_JH.PRODUCT_CD
AND SALES_JH.COUNTRY_CD = COUNTRY_JH.COUNTRY_CD
|
방법상 전혀 문제가 없지만, 바람직 하지 않다. ( 비트맵 인덱스의 특성상 임의의 비트맵이 독립적으로 사용되어 그때마다 비트맵 연사을 수행하여 해결하는 것이 여러 가지로 유리 하기 때문이다.)
결론 ( 이거 왜 필요한데.? )
결합 인덱스는 어떤 조건절에서 특정 컬럼이 사용되지 않거나 =로 사용되지 않을 때 결합된 인덱스 컬럼의 순서에 영향을 받는다. 그러므로 상황에 따라 적절한 인덱스 구조를 제공하려면 너무 많은 인덱스가 필요하게 되었다. 이러한 단점을 개선하기 위해 도입한 비트맵 인덱스르 굳이 결합된 컬럼으로 할 필요는 없다. 특히 비트맵인덱스는 보다 제한 사항이 많기 때문에 함부로 결합하지 않는 것이 좋다. 물론 아주 업무적으로 친밀도가 강력하여 언제나 같이 사용하는 경우라면 검토해볼 수도 있을 것이다. 그러나 역시 우리가 관심을 가져야 할 것은 여러 디멘전 테이븡레서 다양한 형태의 검색 조건을 받았을 때의 조인 최적화를 위한 스타변형 조이을 위해 이 개념을 활용하는 것이 바람직하다.