03. 다양한 인덱스 스캔

(1) Index Rang Scan

  • 인덱스 루트 블록에서 리프 블록까지 수직적으로 탐색한 후에 리프 블록을 필요한 범위만 스캔하는 방식.(p.40 그림 1-4)
  • INDEX (RANGE SCAN) OF 'TBL_IDX' (INDEX)
  • 인덱스를 스캔하는 범위를 얼마만큼 줄일 수 있느냐, 테이블로 액세스하는 횟수를 얼마만큼 줄일 수 있느냐가 인덱스 설계와
    sql 튜닝의 핵심 원리.
  • 인덱스를 구성한는 선두 컬럼이 조건절에 사용되어야 한다.
  • 생성된 결과집합은 인덱스 컬럼 순으로 정렬된 상태이므로 sort order by 연산을 생략하거나 min/max 값을 빠르게 추출할 수 있다.

(2) Index Full Scan

  • 수직적 탐색없이 인덱스 리프 블록을 처음부터 끝까지 수평적으로 탐색하는 방식. (p.41 그림 1-5)
    (실제로는 수직적 탐색이 먼저 일어난다. 가장 왼쪽의 리프 블록을 찾아가기 위해)
  • 데이터 검색을 위한 최적의 인덱스가 없을 때 차선으로 선택된다.
  • INDEX (FULL SCAN) OF 'TBL_IDX' (INDEX)

Index Full Scan의 효율성

  • 인덱스 선두 컬럼이 조건절에 없다면 옵티마이저는 우선적으로 Table Full Scan 고려
  • 테이블 전체를 스캔하기보다 인덱스 스캔 단계에서 대부분 레코드를 필터링 하고 일부에 대해서만 테이블 액세스가 발생하도록
    할 수 있다면 옵티마이저는 Index Full Scan 방식을 선택.(p.42 그림 1-6)
  • 적절한 인덱스가 없어 차선책으로 선택한 것이므로, 인덱스 구성을 조정해 주는 것이 좋다.

인덱스를 이용한 소트 연산 대체

  • 옵티마이저가 전략적으로 선택한 경우에 해당.

select /*+ first_rows */ * from emp
where sal > 1000
order by ename

Execution Plna
------------------------------------------------------------------------
.. 
INDEX (FULL SCAN) OF 'EMP_IDX' (INDEX)

  • 대부분의 사원의 연봉이 1,000을 초과하므로 Index Full Scan을 하면 거의 모든 레코드에 대해 테이블 엑세스가 발생해
    Table Full Scan보다 비 효율적. (p.43 그램 1-7)
  • first_rows 힌트를 이용해 옵티마이저 모드를 바꾸었기 때문
  • 옵티마이저는 소트 연산을 생략함으로서 처음 일부를 빠르게 리턴할 목적으로 Index Full Scan 방식 선택 (사용자 책임)

first_rows_n : 완전 비용기준 최적화 방법 (all_rows,first_rows_n), 처음 n건을 가장 빠르게 출력하기 위한 목표로 비용 계산.
(1, 10, 100, 1000)
first_rows : 비용기준과 경험적 방법(Heuristic method)을 혼합한 접근방법, first_rows 는 마치 과거에 'choose' 모드가
그러했던 것처럼 통계정보를 기반으로 한 완전한 비용기준 접근법과 어떤 규칙을 가지고 최적화를 선택해 가는 방법 중에서
주어진 상황에 따라 선택하여 최적화를 수행하는 방식.

(3) Index Unique Scan

  • 수직적 탐색만으로 데이터를 찾는 스캔방식. (p.44 그림 1-8)
  • Unique 인덱스를 통해 인덱스 키 컬럼 모두 '=' 조건으로 탐색하는 경우에 동작
  • INDEX (UNIQUE SCAN) OF 'TBL_IX' (UNIQUE)
  • 범위검색 조건, 일부 컬럼만으로 검색시에는 Index Range Scan으로 처리

(4) Index Skip Scan

  • 9i 버전부터 가능
  • 조건절에 빠진 인덱스 선두 컬럼의 Distinct Value 개수가 적고 후행 컬럼의 Distinct Value 개수가 많을 때 유용
  • 루트 또는 브랜치 블록에서 얽은 컬럼 값 정보를 이용해 조건에 부합하는 레코드를 포함할 "가능성이 있는" 리프 블록만
    골라서 액세스하는 방식
  • 첫번째 리프 블록, 마지막 리프블록은 항상 방문.

<p..45 그림 1-9 참고>

Range Scan
select * from 사원 where 성별 = '남' and 연봉 between 2000 and 4000
1. 성별이 '남'이면서 연봉이 2,000과 4,000 사이에 있는, 그 중 가장 작은 첫번째 레코드 찾는다.
2. 루트 블록의 세번째 레코드가 조건을 만족하는 레코드를 담당. 3번 리프블록을 찾아 간다.
3. 리브 블록을 차례로 스캔하다가 조건이 만족하지 않는 레코드를 만나면 스캔 중지.
Skip Scan
select /*_+ index_ss(사원 사원_IDX) */ *
from 사원
where 연봉 between 2000 and 4000
1. 첫 번째 레코드가 가리키는 리프 블록은 '남 & 800' 미만인 레코드를 담고 있지만 혹시 '남' 보다 작은 값이 존재 할 지 모르므로 방문.
2. 두 번째 레코드는 '남 & 800' 이상이면서 '남 & 1500' 미만인 레코드이므로 Skip.
3. 세 번째 레코드는 '남 & 1500' 이상이면서 '남 & 5000' 미만인 레코드이므로 방문.
4. 네 번째 레코드는 '남 & 5000' 이상이면서 '남 & 8000' 미만인 레코드이므로 Skip. 다섯 번째도 마찬가지.
5. 여섯 번째 레코드는 '남 & 10000' 이상이지만 '여' 중에 '연봉 < 3000' 이거나 '남', '여' 사이에 다른 값이 존재 할지 모르므로 방문.
6. 일곱 번째는 조건에 포함되므로 방문.
7. 여덟, 아홉 번째 레코드가 가리키는 리프블록은 Skip.
8. 마지막 열 번째레코드는 '여' 보다 큰 미지의 값이 존재할지 모르므로 방문.

버퍼 Pinning을 이용한 Skip 원리

  • p.47 그림 1-10
  • 첫 번째 리프블록을 방문한 후에 다른 리프 블록으로 찾아갈 방법이 없다???
  • 리프 블록에는 자신의 상위 브랜치 또는 루트 블록을 가리키는 주소 정보도 없다???
  • 브랜치 블록들 간에는 서로 연결할 수 있는 주소정보도 없다???
  • 버퍼 pinning 기법을 활용한다.
  • 브랜치 블록을 Pinning 한 채로 리프 블록을 방문했다가 다시 브랜치 블록으로 되돌아와 다음 방문할 리프 블록을 찾는 과정을 반복.
  • 하나의 브랜치 블록을 모두 처리하고 나면 다시 그 상위 노드를 재방문하는 식으로 진행.
☞ 버퍼 Pinning
- 버퍼를 읽고 나서 버퍼 Pin을 즉각 해제하지 않고 데이터베이스 Call이 진행되는 동안 유지하는 기능.
- 같은 블록을 반복적으로 읽을 때 버퍼 Pinning을 통해 래치 획득 과정을 생략한다면 논리적인 블록 읽기(Logical Read)
횟수를 획기적으로 줄일 수 있다.
- 모든 버퍼 블록을 이 방식으로 읽는 것은 아니며, 같은 블록을 재 방문할 가능성이 큰 몇몇 오퍼레이션을 수행할 때만 사용한다.
- v$sysstat, v$sesstat, v$mystat등을 조회해 보면, 래치 획득 과정을 통해 블록을 액세스 할 때는 session logical leads
항목이 증가하고, 래치 획득 과정 없이 버퍼 Pinning을 통해 블록을 곧바로 액세스할 때는 buffer is pinned count 항목의 수치가 증가한다.
- 버퍼 Pinning은 하나의 데이터베이스 Call(Parse Call, Execute Call, Fetch Call)내에서만 유효하다.
(Call이 끝나고 사용자에게 결과를 반환하고 나면 Pin은 해제되어야 한다. 따라서 첫 번째 Fetch Call에서 Pin된 블록은 두 번째
Fetch Call에서 다시 래치 획득 과정을 거쳐 Pin 되어야 한다.)
- 버퍼 Pinning이 적용되던 지점은 인덱스를 스캔하면서 테이블을 액세스할 때의 인덱스 리프 블록이다. Index Range Scan하면서
인덱스와 테이블 블록을 교차 방문할 때 블록 I/O를 체크해 보면, 테이블 블록에 대한 I/O만 계속 증가하는 이유가 여기에 있다.

Index Skip Scan이 작동하기 위한 조건


- 일별업종별거래_PK : 업종유형코드 + 업종코드 + 기준일자
1. 중간 컬럼에 대한 조건절이 누락된 경우
where 업종유형코드 = '01'
    and 기준일자 between '20080501' and '20080531'
2. Distinct Value가 적은 두 개의 선두컬럼이 모두 누락된 경우
where 기준일자 between '20080501' and '20080531'

- 일별업종별거래_X01 : 기준일자 + 업종유형코드
3. 선두 컬럼이 부등호, between, like 같은 범위검색 조건일 경우.
where 기준일자 between '20080501' and '20080531'
  and 업종유형코드 = '01'
- 선두 컬럼이 이처럼 범위검색 조건일 때 인덱스 스캔 단계에서 비효율이 발생하는데, 그럴 때 Index Skip Scan이 유용.

  • index_ss, no_index_ss 힌트 사용.

In-List Iterator와의 비교

  • 그림 1-9와 같은 인덱스 구조인 경우 성별 값을 In-List로 제공해 주면 어떨까?
  • INLISR ITERATOR 부분은 조건절 In-List에 제공된 값의 종류만큼 인덱스 탐색을 반복 수행함을 뜻한다.

select * from 사원
where 연봉 between 2000 and 4000
and 성별 in ('남', '여');

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=FIRST_ROWS
   1    0   INLIST ITERATOR
   2    1     TABLE ACCESS (BY INDEX ROWID) OF '사원' (TABLE)
   3    2       INDEX (RANGE SCAN) OF '사원_IDX' (INDEX (INDEX)) 
1. 인덱스 루트 블록을 읽어 세 번째 레코드가 가리키는 3번 리프 블록을 읽고 거기서 멈추거나 4번 블록 첫 번째 레코드까지 스캔. 
(남자사원 검색)
2. 다시 인덱스 루트 블록을 읽어 여섯 번째 레코드가 가리키는 6번 리프 블록을 시작으로 7번 또는 8번 블록까지 스캔. 
(여자사원 검색)

  • Index Skip Scan과 In-List의 동작 원리는 다르다.

(6) Index Fast Full Scan

  • 인덱스 트리 구조를 무시하고 인덱스 세그먼트 전체를 Multiblock Read 방식으로 스캔.
  • p.51 그림 1-12는 그림 1-11을 물리적 순서에 따라 재배치 한 것.
  • Index Full Scan에서는 인덱스의 논리적이 구조를 따라 블록을 읽는다
    (루트 -> 브랜치1 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10)
  • Index Fast Full Scan은 물리적으로 디스크에 저장된 순서대로 인덱스 블록을 읽는다.
    (Multiblock Read 방식 : 1 -> 2 -> 10 -> 3 -> 9, 8 -> 7 -> 4 ->. 5 -> 6, 루트, 블랜치 블록은 읽지만 필요 없으므로 버린다.)

create table big_table
nologging
as
select rownum id, a.*
from   all_objects a
where  1 = 0
/

set verify off

declare
  l_cnt    number;
  l_rows   number  := 1000000;
begin
  insert /*+ append */
  into big_table 
  select rownum, a.*
  from   all_objects a;
  
  l_cnt := sql%rowcount;

  commit;

  while(l_cnt < l_rows)
  loop
    insert /*+ append */ into big_table
    select rownum + l_cnt
         , owner, object_name, subobject_name
         , object_id, data_object_id
         , object_type, created, last_ddl_time
         , timestamp, status, temporary
         , generated, secondary
    from   big_table
    where  rownum <= l_rows - l_cnt;
    l_cnt := l_cnt + sql%rowcount;
    
    commit;
  end loop;
end;
/

alter table big_table add constraint big_table_pk primary key(id);
create index big_table_idx on big_table(object_name);
alter system flush buffer_cache;

show parameter multiblock

NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
db_file_multiblock_read_count        integer     16

- FULL SCAN
select /*+ index(b big_table_idx) */ count(object_name)
from big_table b

Call     Count CPU Time Elapsed Time       Disk      Query    Current       Rows
------- ------ -------- ------------ ---------- ---------- ---------- ----------
Parse        1    0.000        0.009          0         78          0          0
Execute      1    0.000        0.000          0          0          0          0
Fetch        2    0.453        0.914       4915       4900          0          1
------- ------ -------- ------------ ---------- ---------- ---------- ----------
Total        4    0.453        0.924       4915       4978          0          1

Misses in library cache during parse: 1
Optimizer goal: ALL_ROWS
Parsing user: KJWON (ID=62)

Rows     Row Source Operation
-------  ---------------------------------------------------
      0  STATEMENT
      1   SORT AGGREGATE (cr=4900 pr=4915 pw=0 time=914098 us)
1000000    INDEX FULL SCAN BIG_TABLE_IDX (cr=4900 pr=4915 pw=0 time=4020779 us)(Object ID 53818)

Wait Event Name                                      Count Wait(sec)  Max Wait
-------------------------------------------------- ------- ---------- --------
db file sequential read                                 625      0.182    0.007
SQL*Net message to client                                 2      0.000    0.000
SQL*Net message from client                               2      0.002    0.001
db file scattered read                                  627      0.311    0.013
--------------------------------------------------- ------- --------- --------
Total                                                  1256      0.49

- FAST FULL SCAN
select /*+ index_ffs(b big_table_idx) */ count(object_name)
from big_table b

Call     Count CPU Time Elapsed Time       Disk      Query    Current       Rows
------- ------ -------- ------------ ---------- ---------- ---------- ----------
Parse        1    0.016        0.009          0         78          0          0
Execute      1    0.000        0.000          0          0          0          0
Fetch        2    0.391        0.757       4921       4935          0          1
------- ------ -------- ------------ ---------- ---------- ---------- ----------
Total        4    0.406        0.766       4921       5013          0          1

Misses in library cache during parse: 1
Optimizer goal: ALL_ROWS
Parsing user: KJWON (ID=62)

Rows     Row Source Operation
-------  ---------------------------------------------------
      0  STATEMENT
      1   SORT AGGREGATE (cr=4935 pr=4921 pw=0 time=756600 us)
1000000    INDEX FAST FULL SCAN BIG_TABLE_IDX (cr=4935 pr=4921 pw=0 time=4048031 us)(Object ID 53818)

Wait Event Name                                      Count Wait(sec)  Max Wait
-------------------------------------------------- ------- ---------- --------
SQL*Net message to client                                 2      0.000    0.000
SQL*Net message from client                               2      0.003    0.002
db file scattered read                                  321      0.389    0.045
--------------------------------------------------- ------- --------- --------
Total                                                   325      0.39

  • 디스크 I/O량은 둘다 비슷하지만 Fast Full Scan 수행시간이 다 빠르다
  • Index Full Scan시 db file sequential read 대기 이벤트가 디스크 I/O 량과 동일하게 발생. (9i 실행시)
  • 10g부터는 Index Range Scan 또는 Index Full Scan 시 테이블 액세스가 없이 인덱스만 읽고 처리 한다면
    Multiblock I/O 방식으로 수행. (Prefetch 기능. 위의 경우 10.2.0.3 실행)

Index Fast Full Scan의 특징

  • 인덱스 리프 노드가 갖는 연결리스트 구조를 이용하지 않기 때문에 결과집합이 인덱스 키 순서대로 정렬되지 않는다.
  • index_ffs, no_index_ffs 힌트 사용.
  • 쿼리에 사용되는 모든 컬럼이 인덱스 컬럼에 포함돼 있을 때만 사용가능.
  • 인덱스가 파티션 돼 있지 않더라도 병렬 쿼리 가능.
Index Full ScanIndex Fast Full Scan
1. 인덱스 구조를 따라 스캔
2. 결과집합 순서 보장
3. Single Block I/O
4. 병렬스캔 불가(파티션 돼 있지 않으면)
5. 인덱스에 포함되지 않은 컬럼 조회 시에도 사용가능

1. 세그먼트 전체를 스캔
2. 결과집합 순서 보장 않됨
3. Multiblock I/O
4. 병렬스캔 가능
5. 인덱스에 포함된 컬럼으로만 조회할 때 사용 가능 |

Index Fast Full Scan을 활용한 튜닝 사례


select * from 공급업체
where 업체명 like '%네트웍스%'
- TABLE ACCESS Full
- 실행시간 5초 이상 소요
- 결과 건수 21
=>
select /*+ ordered use_nl(b) no_merge(b) rowid(b) */ b.*
from (select /*+ index_ffs(공급업체 공급업체_X01) */ rowid rid
       from 공급업체
      whjere instr(업체명, '네트웍스') > 0) a, 공급업체 b
where b.rowid = a.rid
- 업체명 컬럼의 인덱스를 Fast Full Scan 해서 얻은 rowid를 이용해 테이블을 다시 억세스.
- like 연산자보다 빠른 instr 함수 사용.
- 최종 결과 건수가 많아지더라도 부분범위 처리가 가능한 애플리케이션 환경이면 유리.
- 11g라면 결과 건수가 많더라도 인라인 뷰에 'order by rowid'를 추가함으로써 큰 효과를 얻을 수 있다(5절 (5)항에서 설명)
- 데이터 건수가 많다면 parallel_index 힌트를 이용해 병렬쿼리도 가능.

(6) Index Range Scan Descending

  • 인덱스를 뒤에서 앞으로 스캔하기 때문에 내림차순으로 정렬된 결과집합을 얻는다.
  • INDEX (RANGE SCAN DESCENDING) OF 'TBL_IDX' (INDEX (UNIQUE))
  • index_desc 힌트 사용

- max 값을 구할 때 해당 컬럼에 인덱스가 있으면 인덱스를 뒤쪽에서부터 한 건만 읽고 멈추는 실행 계획이 자동으로 수립 
- FIRST ROW
      INDEX (RANGE SCAN (MIN/MAX)) OF 'EMP_X02' (INDEX)
- first row(min/max) 알고리즘이 개발되기 전(오라클 7버전)에는 index_desc 힌트와 rownum <= 1 조건을 사용

(7) And-Equal, Index Combine, Index Join

  • 두 개의 인덱스를 함께 사용하는 방법

create index emp_deptno_idx on emp(deptno);
create index emp_job_idx on emp(job);

And-Equal

  • Index Combine 방식이 8i에서 도입됨으로 효용성이 거의 사라졌고, 10g부터는 아예 폐기(deprecated) 된 기능

select /*+ and_equal(e emp_deptno_idx emp_job_idx) */ *
from emp e
where deptno = 30
and job = 'SALESMAN';

----------------------------------------------------------------------------------------------
| Id  | Operation                   | Name           | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |                |     4 |   348 |     3   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS BY INDEX ROWID| EMP            |     4 |   348 |     3   (0)| 00:00:01 |
|   2 |   AND-EQUAL                 |                |       |       |            |          |
|*  3 |    INDEX RANGE SCAN         | EMP_JOB_IDX    |     4 |       |     1   (0)| 00:00:01 |
|*  4 |    INDEX RANGE SCAN         | EMP_DEPTNO_IDX |     6 |       |     1   (0)| 00:00:01 |
----------------------------------------------------------------------------------------------

  • 단일 컬럼의 Non-Unique 인덱스이며 동시에 인덱스 컬럼에 대한 조건절이 '='이어야 한다.
  • 인덱스 필터링을 거친 양쪽 집합은 rowid 순으로 정렬된 상태다. 양쪽을 번갈아 스캔하면서 rowid가 같은 레코드를 찾아
    테이블을 엑세스 하는 방식.
  • 인덱스 스캔량이 아무리 많더라도 두 인덱스를 결합하고 나서의 테이블 엑세스량이 소량일 때 효과적.

Index Combine


select /*+ index_combine(e emp_deptno_idx emp_job_idx) */ *
from emp e
where deptno = 30
and job = 'SALESMAN';

---------------------------------------------------------------------------------------------------
| Id  | Operation                        | Name           | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                 |                |     1 |    37 |     4   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID     | EMP            |     1 |    37 |     4   (0)| 00:00:01 |
|   2 |   BITMAP CONVERSION TO ROWIDS    |                |       |       |            |          |
|   3 |    BITMAP AND                    |                |       |       |            |          |
|   4 |     BITMAP CONVERSION FROM ROWIDS|                |       |       |            |          |
|*  5 |      INDEX RANGE SCAN            | EMP_JOB_IDX    |       |       |     1   (0)| 00:00:01 |
|   6 |     BITMAP CONVERSION FROM ROWIDS|                |       |       |            |          |
|*  7 |      INDEX RANGE SCAN            | EMP_DEPTNO_IDX |       |       |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------------

1. 일반 B*Tree 인덱스를 스캔하면서 각 조건을 만족하는 레코드의 rowid 목록을 얻는다.(5, 7)
2. 1단계에서 얻은 rowid 목록을 가지고 비트맵 인덱스 구조를 하나씩 만든다
(4, 6 - 조건을 만족하는 레코드의 비트를 '1'로 설정, 비트맵 인데스가 있으면 1~2 생략)
3. 비트맵 인덱스에 대한 Bit-Wise 오퍼레이션 수행.(3)
4. Bit-Wise 오퍼페이션을 수행한 결과가 '참(true)'인 비트 값들을 rowid 값으로 환산해 최종적으로 방문할 테이블의 
rowid 목록을 얻는다.(2)
5. rowid를 이용해 테이블을 액세스한다.(1)

select /*+ index_combine(e emp_deptno_idx emp_job_idx) */ *
from emp e
where deptno = 30
or job = 'SALESMAN';

---------------------------------------------------------------------------------------------------
| Id  | Operation                        | Name           | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                 |                |     8 |   296 |    14   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID     | EMP            |     8 |   296 |    14   (0)| 00:00:01 |
|   2 |   BITMAP CONVERSION TO ROWIDS    |                |       |       |            |          |
|   3 |    BITMAP OR                     |                |       |       |            |          |
|   4 |     BITMAP CONVERSION FROM ROWIDS|                |       |       |            |          |
|*  5 |      INDEX RANGE SCAN            | EMP_DEPTNO_IDX |       |       |     1   (0)| 00:00:01 |
|   6 |     BITMAP CONVERSION FROM ROWIDS|                |       |       |            |          |
|*  7 |      INDEX RANGE SCAN            | EMP_JOB_IDX    |       |       |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------------

  • 데이터 분표도가 좋지 않은 두 개 이상의 인덱스를 결합해 테이블 Random 액세스량을 줄이는데 목적.
  • 조건절이 '='일 필요 없고, Non-Unique 인덱스일 필요도 없다.
  • 조건절이 OR로 결합된 경우에도 유용
  • _b_tree_bitmap_plans 파라미터가 true일 때만 작동(9i 부터 디폴트로 true)

Index Join

  • 한 테이블에 속한 여러 인덱스를 이용해 테이블 액세스 없이 결과집합을 만들 때 사용하는 인덱스 스캔 방식
  • 해쉬 조인 매커니즘 사용
  • 쿼리에 사용된 컬럼들이 인덱스에 모두 포함될 때만 작동

select /*+ index_join(e emp_deptno_idx emp_job_idx) */ deptno, job
from emp e
where deptno = 30
and job = 'SALESMAN';

---------------------------------------------------------------------------------------
| Id  | Operation          | Name             | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |                  |     1 |    11 |     3  (34)| 00:00:01 |
|*  1 |  VIEW              | index$_join$_001 |     1 |    11 |     3  (34)| 00:00:01 |
|*  2 |   HASH JOIN        |                  |       |       |            |          |
|*  3 |    INDEX RANGE SCAN| EMP_JOB_IDX      |     1 |    11 |     1   (0)| 00:00:01 |
|*  4 |    INDEX RANGE SCAN| EMP_DEPTNO_IDX   |     1 |    11 |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------------------
1. 크기가 비교적 작은 쪽 인덱스에서 키 값과 rowid를 읽어 PGA 메모리에 해시 맵을 생성. 해쉬 키로는 rowid가 사용.
2. 다른 쪽 인덱를 스캔하면서 앞서 생성한 해시 맵에 같은 rowid 값을 갖는 레코드가 있는지 탐색.
3. rowid끼리 조인에 성공한 레코드만 결과집합에 포함.

문서에 대하여