# 요약
  ■ Row Chaining

■ Row Migration
analyze table hr.mig_table compute statistics;

■ B * tree

■ Bitmap Index

■ session_cached_cursors

 

 


 


■ Row Chaining
insert시에 한 블록 안에 한행의 값이 다 저장 못하고 여러 블록으로 걸쳐서 저장되는 현상
> 해결방법 : 모델링, 테이블 구조 재구성

1행 평균 바이트 from user_tables where AVG_ROW_LEN

 




■ Row Migration
- update시에 증가하려고 하는데 free 공간이 없어서 다른 블록으로 이전되는 현상

> 기존 헤더에 이전되었다는 정보 있음
> 리프 > 기존 헤더에서 이전되었다는 정보 > 이동한 정보 = i/o 2번, latch 2번 = 성능 저하
> rowid scan일 때 문제 발생 > 미리 방지? pctfree


HR@ora11g> create table hr.mig_table(id number, l_name varchar2(2000), f_name varchar2(2000));

Table created.

 


HR@ora11g> insert into hr.mig_table(id,l_name,f_name)
           select level,
                  decode(mod(level,3), 1, null, rpad('x',2000,'x')),
                  decode(mod(level,3), 1, null, rpad('x',1000,'x'))
           from dual
           connect by level <= 1000;

1000 rows created.

 


HR@ora11g> commit;

Commit complete.


HR@ora11g> select num_rows, blocks, chain_cnt from user_tables where table_name = 'MIG_TABLE';

  NUM_ROWS     BLOCKS  CHAIN_CNT
---------- ---------- ----------

 


HR@ora11g> execute dbms_stats.gather_table_stats('hr','mig_table')

PL/SQL procedure successfully completed.

 


HR@ora11g> select num_rows, blocks, chain_cnt from user_tables where table_name = 'MIG_TABLE';

  NUM_ROWS     BLOCKS  CHAIN_CNT
---------- ---------- ----------
      1000        370          0

- CHAIN_CNT : row migration + row chain


HR@ora11g> create index mig_table_idx on hr.mig_table(id);

Index created.

 


HR@ora11g> update hr.mig_table
           set l_name = rpad('x',2000,'x'),
               f_name = rpad('x',1000,'x')
           where mod(id,3) = 1;

334 rows updated.

 


HR@ora11g> commit;

Commit complete.

 


HR@ora11g> execute dbms_stats.gather_table_stats('hr','mig_table')

PL/SQL procedure successfully completed.

 


HR@ora11g> select num_rows, blocks, chain_cnt from user_tables where table_name = 'MIG_TABLE';

  NUM_ROWS     BLOCKS  CHAIN_CNT
---------- ---------- ----------
      1000        622          0

> row migration 발생했을텐데?


# system level 통계 정보 (전후 구간으로 검토)
SYS@ora11g> select * from v$sysstat where name = 'table fetch continued row';

STATISTIC# NAME                                               CLASS     VALUE    STAT_ID
---------- --------------------------------------------- ---------- --------- ----------
       417 table fetch continued row                             64      7895 1413702393

 

> 기존헤더에 액세스해서 이전된 block으로 찾아갈 때마다 value 값 증가


# session level 통계 정보 (전후 구간으로 검토하기 위해 시간 추가, 세션 종료 시 초기화)
SYS@ora11g> select to_char(sysdate,'yyyy-mm-dd hh24:ss:ss') day, a.sid, vss.username, a.name, a.value
            from (select vst.sid,vst.value,vsn.name,vsn.statistic#
                  from v$statname vsn, v$sesstat vst
                  where vsn.statistic# = vst.statistic#
                  order by vst.value desc) a, 
                 v$session vss
            where a.sid = vss.sid 
            and vss.username = 'HR' 
            and a.name =  'table fetch continued row';

DAY                     SID USERNAME   NAME                                              VALUE
------------------- ------- ---------- --------------------------------------------- ---------
2024-02-15 10:54:54     179 HR         table fetch continued row                           307


# 인덱스 스캔으로 유도
HR@ora11g> select /*+ index(t mig_table_idx) */ count(l_name)
           from hr.mig_table t where id > 0;

COUNT(L_NAME)
-------------
         1000


SYS@ora11g> select * from v$sysstat where name = 'table fetch continued row';

STATISTIC# NAME                                               CLASS     VALUE    STAT_ID
---------- --------------------------------------------- ---------- --------- ----------
       417 table fetch continued row                             64      8437 1413702393

 


SYS@ora11g> select to_char(sysdate,'yyyy-mm-dd hh24:ss:ss') day, a.sid, vss.username, a.name, a.value
    from (select vst.sid,vst.value,vsn.name,vsn.statistic#
          from v$statname vsn, v$sesstat vst
          where vsn.statistic# = vst.statistic#
          order by vst.value desc) a,
         v$session vss
    where a.sid = vss.sid
    and vss.username = 'HR'
    and a.name =  'table fetch continued row';

DAY                     SID USERNAME   NAME                                              VALUE
------------------- ------- ---------- --------------------------------------------- ---------
2024-02-15 11:15:15     179 HR         table fetch continued row                           641

>

system level value +542
session level value +334    -> 작업 행 수만큼 row migration 발생


SYS@ora11g> select s.prev_sql_id, s.prev_child_number, v.sql_text
            from v$session s, v$sql v
            where s.prev_sql_id = v.sql_id
            and s.prev_child_number = v.child_number
            and s.username = 'HR';


PREV_SQL_ID   PREV_CHILD_NUMBER SQL_TEXT
------------- ----------------- --------------------------------------------------
7bwscvpmwraq8                 0 select /*+ index(t mig_table_idx) */ count(l_name)
                                            from hr.mig_table t where id > 0

> row migration 발생한 sql문


SYS@ora11g> select * from table(dbms_xplan.display_cursor('7bwscvpmwraq8'));

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------
SQL_ID  7bwscvpmwraq8, child number 0
-------------------------------------
select /*+ index(t mig_table_idx) */ count(l_name)            from
hr.mig_table t where id > 0

Plan hash value: 2915741377

----------------------------------------------------------------------------------------------
| Id  | Operation                    | Name          | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |               |       |       |   337 (100)|          |
|   1 |  SORT AGGREGATE              |               |     1 |  2005 |            |          |
|   2 |   TABLE ACCESS BY INDEX ROWID| MIG_TABLE     |  1000 |  1958K|   337   (0)| 00:00:05 |
|*  3 |    INDEX RANGE SCAN          | MIG_TABLE_IDX |  1000 |       |     4   (0)| 00:00:01 |
----------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("ID">0)

- sort aggregate : 그룹함수 사용했구나, 정렬X

 


SYS@ora11g> select num_rows, blocks, chain_cnt from dba_tables where table_name = 'MIG_TABLE';

  NUM_ROWS     BLOCKS  CHAIN_CNT
---------- ---------- ----------
      1000        622          0


# row migration 통계수집
SYS@ora11g> analyze table hr.mig_table compute statistics;

Table analyzed.

 


SYS@ora11g> select num_rows, blocks, chain_cnt from dba_tables where table_name = 'MIG_TABLE';

  NUM_ROWS     BLOCKS  CHAIN_CNT
---------- ---------- ----------
      1000        622        334

> row migration 해결? > 재배치

 


SYS@ora11g> select object_id, data_object_id from dba_objects where object_name = 'MIG_TABLE';

 OBJECT_ID DATA_OBJECT_ID
---------- --------------
     88778          88778

 


# row migration이 발생한 테이블을 재구성하면 해결할 수 있다.
# 꼭 테이블 재구성하기 전에 테이블에 속한 테이블스페이스에 프리공간이 있는지 체크하고 수행한다.

SYS@ora11g> alter table hr.mig_table move;

Table altered.

> CTAS(복제) 임시공간 만들어 앞으로 이동(insert-delete-rename) dml 사용 - 리두 사용
> pctfree 제외하고 데이터를 앞으로 배치, rowid 변경
> 리오브 작업 전 대상 테이블스페이스에 free 공간 체크 후 진행하자.
> 다른 유저 DML wait

사용 블럭 수
전체 로우 수
한행 평균 바이트 값
/8k

시타스는 노로깅모드, 디스크만 사용, 메모리 미사용


SYS@ora11g> select object_id, data_object_id from dba_objects where object_name = 'MIG_TABLE';

 OBJECT_ID DATA_OBJECT_ID
---------- --------------
     88784          88786

- OBJECT_ID : 처음 생성 시 번호
- DATA_OBJECT_ID : 재구성 후 현재 번호 -> rowid에 사용

> rowid 변경 > index UNUSABLE 상태

 


SYS@ora11g> select index_name, status from dba_indexes where table_name = 'MIG_TABLE';

INDEX_NAME                     STATUS
------------------------------ -------------
MIG_TABLE_IDX                  UNUSABLE

> drop - create 방식은 정렬작업이 진행되기 때문에 보통 작은 데이터에서 사용

 


# index 재구성하는 방법
SYS@ora11g> alter index hr.mig_table_idx rebuild online;

Index altered.

- index 재구성 : UNUSABLE 상태 시, BLEVEL 높을 경우

 


SYS@ora11g> select index_name, status from dba_indexes where table_name = 'MIG_TABLE';

INDEX_NAME                     STATUS
------------------------------ -------------
MIG_TABLE_IDX                  VALID


SYS@ora11g> analyze table hr.mig_table compute statistics;

Table analyzed.

 


SYS@ora11g> select num_rows, blocks, chain_cnt from dba_tables where table_name = 'MIG_TABLE';

  NUM_ROWS     BLOCKS  CHAIN_CNT
---------- ---------- ----------
      1000        518          0

 


# 다른 테이블스페이스로 이관 작업 + 재구성

SYS@ora11g> alter table hr.mig_table move tablespace insa_tbs;

Table altered.


SYS@ora11g> select * from v$sysstat where name = 'table fetch continued row';

STATISTIC# NAME                                               CLASS     VALUE    STAT_ID
---------- --------------------------------------------- ---------- --------- ----------
       417 table fetch continued row                             64      9249 1413702393

 


SYS@ora11g> select to_char(sysdate,'yyyy-mm-dd hh24:ss:ss') day, a.sid, vss.username, a.name, a.value
    from (select vst.sid,vst.value,vsn.name,vsn.statistic#
          from v$statname vsn, v$sesstat vst
          where vsn.statistic# = vst.statistic#
          order by vst.value desc) a,
         v$session vss
    where a.sid = vss.sid
    and vss.username = 'HR'
    and a.name =  'table fetch continued row';

DAY                     SID USERNAME   NAME                                              VALUE
------------------- ------- ---------- --------------------------------------------- ---------
2024-02-15 11:25:25     179 HR         table fetch continued row                           641


HR@ora11g> select /*+ index(t mig_table_idx) */ count(l_name)
           from hr.mig_table t where id > 0;

COUNT(L_NAME)
-------------
         1000


SYS@ora11g> select * from v$sysstat where name = 'table fetch continued row';

STATISTIC# NAME                                               CLASS     VALUE    STAT_ID
---------- --------------------------------------------- ---------- --------- ----------
       417 table fetch continued row                             64      9249 1413702393

 


SYS@ora11g> select to_char(sysdate,'yyyy-mm-dd hh24:ss:ss') day, a.sid, vss.username, a.name, a.value
    from (select vst.sid,vst.value,vsn.name,vsn.statistic#
          from v$statname vsn, v$sesstat vst
          where vsn.statistic# = vst.statistic#
          order by vst.value desc) a,
         v$session vss
    where a.sid = vss.sid
    and vss.username = 'HR'
    and a.name =  'table fetch continued row';

DAY                     SID USERNAME   NAME                                              VALUE
------------------- ------- ---------- --------------------------------------------- ---------
2024-02-15 11:09:09     179 HR         table fetch continued row                           641

> 변화 없다.

- table fetch continued row : row migration + row chain



# 테이블 용량확인
SYS@ora11g> select owner, segment_name, segment_type, tablespace_name, bytes, blocks
from dba_segments where owner = 'HR' and segment_name in ('MIG_TABLE','MIG_TABLE_IDX');

OWNER      SEGMENT_NAME            SEGMENT_TYPE       TABLESPACE_NAME      BYTES     BLOCKS
---------- ----------------------- ------------------ --------------- ---------- ----------
HR         MIG_TABLE               TABLE              USERS              5242880        640
HR         MIG_TABLE_IDX           INDEX              USERS                65536          8


# 테이블스페이스 프리공간
SYS@ora11g> select round(sum(bytes)/1024/1024) mb 
            from dba_free_space where tablespace_name = 'EXAMPLE';

 

        MB
----------
        21


# 테이블을 다른 테이블스페이스로 이동
SYS@ora11g> alter table hr.mig_table move tablespace example;

Table altered.

> 운영중 하지말자


# 인덱스를 다른 테이블스페이스로 이동
SYS@ora11g> alter index hr.mig_table_idx rebuild online tablespace example;

Index altered.

> 인덱스가 UNUSABLE 상태라 속도 저하 중이니 운영중이라도 빨리 해결하자


SYS@ora11g> select owner, segment_name, segment_type, tablespace_name, bytes, blocks
from dba_segments where owner = 'HR' and segment_name in ('MIG_TABLE','MIG_TABLE_IDX');
OWNER      SEGMENT_NAME            SEGMENT_TYPE       TABLESPACE_NAME      BYTES     BLOCKS
---------- ----------------------- ------------------ --------------- ---------- ----------
HR         MIG_TABLE_IDX           INDEX              EXAMPLE              65536          8
HR         MIG_TABLE               TABLE              EXAMPLE            5242880        640

 


SYS@ora11g> select index_name, status, blevel from dba_indexes where table_name = 'MIG_TABLE';

INDEX_NAME                     STATUS            BLEVEL
------------------------------ ------------- ----------
MIG_TABLE_IDX                  VALID                  1


SYS@ora11g> select object_id, data_object_id from dba_objects where object_name = 'MIG_TABLE';

 OBJECT_ID DATA_OBJECT_ID
---------- --------------
     88784          88805

 


HR@ora11g> select /*+ index(t mig_table_idx) */ count(l_name)
           from hr.mig_table t where id > 0;

COUNT(L_NAME)
-------------
         1000

 


# 테이블 통계 수집
HR@ora11g> exec dbms_stats.gather_table_stats('hr','mig_table')

PL/SQL procedure successfully completed.

또는 


SYS@ora11g> analyze table hr.mig_table compute statistics;

 

- row chaining, row migration 정보 수집


SYS@ora11g> select tablespace_name, num_rows, blocks, avg_row_len, chain_cnt, 
                to_char(last_analyzed,'yyyy-mm-dd hh24:mi:ss') "LAST_ANALYZED"
            from dba_tables
            where table_name = 'MIG_TABLE';

TABLESPACE_NAME   NUM_ROWS     BLOCKS AVG_ROW_LEN  CHAIN_CNT LAST_ANALYZED
--------------- ---------- ---------- ----------- ---------- -------------------
EXAMPLE               1000        518        3006          0 2024-02-15 13:47:37





■ B * tree
- Balanced tree
- Binary tree
- Balanced Binary Search Tree (균형 이진 탐색 트리)

# B * tree 구조의 인덱스 생성 기준
- where 조건절에서 자주 사용되는 컬럼 중에 유일키값으로 구성되어 있는 컬럼
- 열에 null값은 제외하고 인덱스를 생성, null에서 인덱스 스캔을 하지 않는다.
- 테이블이 크고 대부분의 쿼리가 테이블에서 2 ~ 4%미만의 행을 검색할 때 유용하다.
- order by절에 자주 사용되는 컬럼 (이미 정렬이 되어있어 인덱스만 검색하면 됨, min,max도 포함)





■ Bitmap Index
- where 조건절에 자주 사용되는 컬럼들 중에 중복성이 많은 컬럼을 선정(성별, 결혼여부 등)
- 열에 null값을 포함해서 인덱스를 생성하기때문에 null에서 인덱스 스캔이 수행된다.
- index segment level lock : dml이 잦을 경우 단점

HR@ora11g> create index hr.emp_idx on hr.emp(employee_id);
HR@ora11g> create index emp_sal_idx on emp(salary);

Index created.

 


HR@ora11g> select * from user_ind_columns where table_name = 'EMP';

INDEX_NAME   TABLE_NAME           COLUMN_NAME          COLUMN_POSITION COLUMN_LENGTH CHAR_LENGTH DESCEND
------------ -------------------- -------------------- --------------- ------------- ----------- -------
EMP_SAL_IDX  EMP                  SALARY                             1            22          0  ASC
EMP_IDX      EMP                  EMPLOYEE_ID                        1            22          0  ASC


HR@ora11g> select /*+ gather_plan_statistics index_combine(e EMP_ID_PK EMP_SAL_IDX) */ employee_id, salary
           from emp e where employee_id = 100 and salary = 10000;

no rows selected


HR@ora11g> select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------
SQL_ID  7rv16wt9vzrhq, child number 1
-------------------------------------
select /*+ gather_plan_statistics index_combine(e EMP_ID_PK
EMP_SAL_IDX) */ employee_id, salary from emp e where employee_id = 100
and salary = 10000

Plan hash value: 3050648138

---------------------------------------------------------------------------------------------------------
| Id  | Operation                       | Name        | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
---------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                |             |      1 |        |      0 |00:00:00.01 |       2 |
|   1 |  BITMAP CONVERSION TO ROWIDS    |             |      1 |      1 |      0 |00:00:00.01 |       2 |
|   2 |   BITMAP AND                    |             |      1 |        |      0 |00:00:00.01 |       2 |
|   3 |    BITMAP CONVERSION FROM ROWIDS|             |      1 |        |      1 |00:00:00.01 |       1 |
|*  4 |     INDEX RANGE SCAN            | EMP_IDX     |      1 |        |      1 |00:00:00.01 |       1 |
|   5 |    BITMAP CONVERSION FROM ROWIDS|             |      1 |        |      1 |00:00:00.01 |       1 |
|*  6 |     INDEX RANGE SCAN            | EMP_SAL_IDX |      1 |        |      4 |00:00:00.01 |       1 |
---------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - access("EMPLOYEE_ID"=100)
   6 - access("SALARY"=10000)


# Bitmap Index 생성
create bitmap index t_table_idx on t_table(column);

비프리 구조 인덱스 : rowid
비트맵 구조 인덱스 : 0,1 조합...

값 시작rowid 끝rowid bitmap
     ㄴ데이터 범위     1,0로 행 개수만큼

데이터웨어하우징, exedata, ... 등에서 많이 사용

 

 

HR@ora11g> select /*+ gather_plan_statistics index_combine(e EMP_ID_PK EMP_SAL_IDX) */ employee_id, salary
           from emp e where employee_id = 100 or salary = 10000;

EMPLOYEE_ID     SALARY
----------- ----------
        204      10000
        100       2000
        150      10000
        156      10000
        169      10000

 

 

HR@ora11g> select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------
SQL_ID  57dwtr2ptsugt, child number 0
-------------------------------------
select /*+ gather_plan_statistics index_combine(e EMP_ID_PK
EMP_SAL_IDX) */ employee_id, salary            from emp e where
employee_id = 100 or salary = 10000

Plan hash value: 3344826655

----------------------------------------------------------------------------------------------------------
| Id  | Operation                        | Name        | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
----------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                 |             |      1 |        |      5 |00:00:00.01 |       4 |
|   1 |  TABLE ACCESS BY INDEX ROWID     | EMP         |      1 |      3 |      5 |00:00:00.01 |       4 |
|   2 |   BITMAP CONVERSION TO ROWIDS    |             |      1 |        |      5 |00:00:00.01 |       2 |
|   3 |    BITMAP OR                     |             |      1 |        |      1 |00:00:00.01 |       2 |
|   4 |     BITMAP CONVERSION FROM ROWIDS|             |      1 |        |      1 |00:00:00.01 |       1 |
|*  5 |      INDEX RANGE SCAN            | EMP_SAL_IDX |      1 |        |      4 |00:00:00.01 |       1 |
|   6 |     BITMAP CONVERSION FROM ROWIDS|             |      1 |        |      1 |00:00:00.01 |       1 |
|*  7 |      INDEX RANGE SCAN            | EMP_IDX     |      1 |        |      1 |00:00:00.01 |       1 |
----------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   5 - access("SALARY"=10000)
   7 - access("EMPLOYEE_ID"=100)


B * tree Bitmap Index
유일키값으로 데이터 있는 컬럼 중복성이 많은 컬럼
DML 비용이 저렴 (row level lock) DML 비용이 크다 (데이터는 row level lock,
                bitmap index는 segment level lock)
OR 연산자에서는 비효율적 OR 연산자에서도 효율적
null에 대해서 인덱스 스캔 안함 null에 대해 인덱스 스캔 한다
OLPT(Online Transaction Processing) DW(Data Warehouse),
DSS(Decision Support System, 의사결정시스템)

 

 




■ session_cached_cursors
오라클은 session 내에서 3번 이상 수행된 SQL문에 해당하는 Library Cache에 handle과 LCO에 대한 포인터 정보를
저장해놓는다.
Library Cache 영역의 탐색하는 시간이 줄어들어 "latch 보유 시간을 줄일 수 있다."
- 프로그램 레벨에서 효과적

SYS@ora11g> show parameter session_cached_cursors

NAME                                 TYPE    VALUE
------------------------------------ ------- ------------------------------
session_cached_cursors               integer 50 (개)


SYS@ora11g> alter system flush shared_pool;
SYS@ora11g> alter system flush shared_pool;
SYS@ora11g> alter system flush shared_pool;

System altered.

 


HR@ora11g> select last_name, salary, job_id from hr.employees where employee_id = 100;

LAST_NAME                     SALARY JOB_ID
------------------------- ---------- ----------
King                            2000 AD_PRES

 


SYS@ora11g> select parse_calls, executions, users_opening from v$sql
            where sql_text= 'select last_name, salary, job_id from hr.employees where employee_id = 100';

PARSE_CALLS EXECUTIONS USERS_OPENING
----------- ---------- -------------
          1          1             0

... select 문 3번 실행 시

 

PARSE_CALLS EXECUTIONS USERS_OPENING
----------- ---------- -------------
          3          3             1

- PARSE_CALLS : 파싱을 발생한 건수
- USERS_OPENING : cache에 저장되어있는 문장을 사용한 횟수


# v$mystat : 내 세션에서 발생한 통계 정보
HR@ora11g> select a.name, b.value
from v$statname a, v$mystat b
where b.statistic# = a.statistic#
and a.name in ('session cursor cache hits','parse count (total)');

NAME                                                                  VALUE
---------------------------------------------------------------- ----------
session cursor cache hits                                               853
parse count (total)                                                     210


HR@ora11g> select last_name, salary, job_id from hr.employees where employee_id = 100;

LAST_NAME                     SALARY JOB_ID
------------------------- ---------- ----------
King                            2000 AD_PRES


HR@ora11g> select a.name, b.value
from v$statname a, v$mystat b
where b.statistic# = a.statistic#
and a.name in ('session cursor cache hits','parse count (total)');

NAME                                                                  VALUE
---------------------------------------------------------------- ----------
session cursor cache hits                                               855
parse count (total)                                                     212

 


 

 

https://docs.oracle.com/en/database/oracle/oracle-database/19/cncpt/indexes-and-index-organized-tables.html#GUID-B15C4817-7748-456D-9740-8B9628AF9F47

 

Database Concepts

Indexes are schema objects that can speed access to table rows. Index-organized tables are tables stored in an index structure.

docs.oracle.com

 

https://docs.oracle.com/en/database/oracle/oracle-database/19/tgsql/optimizer-access-paths.html#GUID-E8F4A411-10BE-4D5E-9207-0D5F8E61A8E0

 

SQL Tuning Guide

An access path is a technique used by a query to retrieve rows from a row source.

docs.oracle.com

 

https://docs.oracle.com/en/database/oracle/oracle-database/19/dwhsg/data-warehouse-optimizations-techniques.html

 

Data Warehousing Guide

 

docs.oracle.com