■ 개요


유니크 인덱스를 위주로 관리 편의를 위해 LOCAL 인덱스로 변경하거나 처음부터 LOCAL 인덱스로 설계할 때 고려해야할 점을 간단히 테스트해보고자 한다.

최근 고객사에서 이걸 고려하는 건이 있어 정리겸.


■ 사전 스크립트


CREATE TABLE sales
  ( prod_id       NUMBER
  , cust_id       NUMBER
  , time_id       DATE
  )
 PARTITION BY RANGE (time_id)
 ( PARTITION sales_2017_12 VALUES LESS THAN (TO_DATE('01-JAN-2018','dd-MON-yyyy'))
    TABLESPACE users
 , PARTITION sales_2018_01 VALUES LESS THAN (TO_DATE('01-FEB-2018','dd-MON-yyyy'))
    TABLESPACE users
 , PARTITION sales_2018_02 VALUES LESS THAN (TO_DATE('01-MAR-2018','dd-MON-yyyy'))
    TABLESPACE users
 , PARTITION sales_2018_03 VALUES LESS THAN (TO_DATE('01-APR-2018','dd-MON-yyyy'))
    TABLESPACE users
 , PARTITION sales_2018_04 VALUES LESS THAN (TO_DATE('01-MAY-2018','dd-MON-yyyy'))
    TABLESPACE users
 );
 
 create sequence sales_seq;
 
 insert into sales values(sales_seq.nextval,1121,to_date('20180101','yyyymmdd'));
 insert into sales values(sales_seq.nextval,1122,to_date('20180101','yyyymmdd'));
 insert into sales values(sales_seq.nextval,1120,to_date('20180101','yyyymmdd'));
 insert into sales values(sales_seq.nextval,1111,to_date('20180101','yyyymmdd'));
 insert into sales values(sales_seq.nextval,1123,to_date('20180201','yyyymmdd'));
 insert into sales values(sales_seq.nextval,1123,to_date('20180201','yyyymmdd'));
 insert into sales values(sales_seq.nextval,1234,to_date('20180201','yyyymmdd'));
 insert into sales values(sales_seq.nextval,1643,to_date('20180201','yyyymmdd'));
 insert into sales values(sales_seq.nextval,1111,to_date('20180301','yyyymmdd'));
 insert into sales values(sales_seq.nextval,2222,to_date('20180301','yyyymmdd'));
 insert into sales values(sales_seq.nextval,1321,to_date('20180301','yyyymmdd'));
 insert into sales values(sales_seq.nextval,1955,to_date('20180301','yyyymmdd'));
 insert into sales values(sales_seq.nextval,1754,to_date('20180401','yyyymmdd'));
 insert into sales values(sales_seq.nextval,1027,to_date('20180401','yyyymmdd'));
 insert into sales values(sales_seq.nextval,1013,to_date('20180401','yyyymmdd'));
 insert into sales values(sales_seq.nextval,1100,to_date('20180401','yyyymmdd'));
 commit;
create unique index sales_pk on sales(prod_id) local;
--유니크 인덱스는 파티션 키를 포함해야 local로 만들어진다.
                                *
--ERROR at line 1:
--ORA-14039: partitioning columns must form a subset of key columns of a UNIQUE index 
create unique index sales_pk on sales(prod_id,time_id) local;
create index sales_custid on sales(cust_id) local;
exec dbms_stats.gather_table_stats(user,'sales');
alter session set statistics_level=all;
alter session set nls_date_format='yyyymmdd hh24:mi:ss';
select /*+gather_plan_statistics*/ * from sales;
   PROD_ID    CUST_ID TIME_ID
---------- ---------- -----------------
        21       1121 20180101 00:00:00
        22       1122 20180101 00:00:00
        23       1120 20180101 00:00:00
        24       1111 20180101 00:00:00
        25       1123 20180201 00:00:00
        26       1123 20180201 00:00:00
        27       1234 20180201 00:00:00
        28       1643 20180201 00:00:00
        29       1111 20180301 00:00:00
        30       2222 20180301 00:00:00
        31       1321 20180301 00:00:00
        32       1955 20180301 00:00:00
        33       1754 20180401 00:00:00
        34       1027 20180401 00:00:00
        35       1013 20180401 00:00:00
        36       1100 20180401 00:00:00

■ 엑세스 테스트

이제 where prod_id=NN, where cust_id=NN 할 때 어떻게될까?
1. 파티션 하나의 인덱스만 액세스한다
2. 파티션 전부의 인덱스를 액세스한다?
select /*+gather_plan_statistics*/ * from sales where prod_id=28;
set long 5000 line 200 pages 1000
select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST -ROWS +PARTITION'));
 
----------------------------------------------------------------------------------------------------------------
| Id  | Operation                          | Name     | Starts | Pstart| Pstop | A-Rows |   A-Time   | Buffers |
----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |          |      1 |       |       |      1 |00:00:00.01 |       5 |
|   1 |  PARTITION RANGE ALL               |          |      1 |     1 |     5 |      1 |00:00:00.01 |       5 |
|   2 |   TABLE ACCESS BY LOCAL INDEX ROWID| SALES    |      5 |     1 |     5 |      1 |00:00:00.01 |       5 |
|*  3 |    INDEX RANGE SCAN                | SALES_PK |      4 |     1 |     5 |      1 |00:00:00.01 |       4 |
----------------------------------------------------------------------------------------------------------------
 
 
select /*+gather_plan_statistics*/ * from sales where cust_id=1643;
select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST -ROWS +PARTITION'));
 
--------------------------------------------------------------------------------------------------------------------
| Id  | Operation                          | Name         | Starts | Pstart| Pstop | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |              |      1 |       |       |      1 |00:00:00.01 |       5 |
|   1 |  PARTITION RANGE ALL               |              |      1 |     1 |     5 |      1 |00:00:00.01 |       5 |
|   2 |   TABLE ACCESS BY LOCAL INDEX ROWID| SALES        |      5 |     1 |     5 |      1 |00:00:00.01 |       5 |
|*  3 |    INDEX RANGE SCAN                | SALES_CUSTID |      4 |     1 |     5 |      1 |00:00:00.01 |       4 |
--------------------------------------------------------------------------------------------------------------------

파티션을 하나 추가한다면 버퍼가 하나 더 늘것이다. 데이터가 없어 인덱스 블럭이 하나이기 때문이다
alter session set deferred_segment_creation=false;
alter table sales add partition sales_2018_05 VALUES LESS THAN (TO_DATE('01-JUN-2018','dd-MON-yyyy'))
    TABLESPACE users;
exec dbms_stats.GATHER_TABLE_STATS(null,'sales',no_invalidate=>false);
 
select /*+gather_plan_statistics*/ * from sales where prod_id=28;
select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST -ROWS +PARTITION'));
 
----------------------------------------------------------------------------------------------------------------
| Id  | Operation                          | Name     | Starts | Pstart| Pstop | A-Rows |   A-Time   | Buffers |
----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |          |      1 |       |       |      1 |00:00:00.01 |       6 |
|   1 |  PARTITION RANGE ALL               |          |      1 |     1 |     6 |      1 |00:00:00.01 |       6 |
|   2 |   TABLE ACCESS BY LOCAL INDEX ROWID| SALES    |      6 |     1 |     6 |      1 |00:00:00.01 |       6 |
|*  3 |    INDEX RANGE SCAN                | SALES_PK |      5 |     1 |     6 |      1 |00:00:00.01 |       5 |
----------------------------------------------------------------------------------------------------------------
 
select /*+gather_plan_statistics*/ * from sales where cust_id=1643;
select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST -ROWS +PARTITION'));
--------------------------------------------------------------------------------------------------------------------
| Id  | Operation                          | Name         | Starts | Pstart| Pstop | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |              |      1 |       |       |      1 |00:00:00.01 |       6 |
|   1 |  PARTITION RANGE ALL               |              |      1 |     1 |     6 |      1 |00:00:00.01 |       6 |
|   2 |   TABLE ACCESS BY LOCAL INDEX ROWID| SALES        |      6 |     1 |     6 |      1 |00:00:00.01 |       6 |
|*  3 |    INDEX RANGE SCAN                | SALES_CUSTID |      5 |     1 |     6 |      1 |00:00:00.01 |       5 |
--------------------------------------------------------------------------------------------------------------------

 

예상대로 하나가 늘었다. 추가된 sales_2018_05 Local Index 블럭이 하나 늘었기 때문이다.

당연히 partition 조건도 같이 들어온다면 single partition만 스캔할것이다.

select /*+gather_plan_statistics*/ * from sales where prod_id=28 and time_id=to_date('20180201 00:00:00','yyyymmdd hh24:mi:ss');
select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST -ROWS +PARTITION'));
 
----------------------------------------------------------------------------------------------------------------
| Id  | Operation                          | Name     | Starts | Pstart| Pstop | A-Rows |   A-Time   | Buffers |
----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |          |      1 |       |       |      1 |00:00:00.01 |       2 |
|   1 |  PARTITION RANGE SINGLE            |          |      1 |     3 |     3 |      1 |00:00:00.01 |       2 |
|   2 |   TABLE ACCESS BY LOCAL INDEX ROWID| SALES    |      1 |     3 |     3 |      1 |00:00:00.01 |       2 |
|*  3 |    INDEX UNIQUE SCAN               | SALES_PK |      1 |     3 |     3 |      1 |00:00:00.01 |       1 |
----------------------------------------------------------------------------------------------------------------

 

마지막으로 prod_id는 시퀀스에 의해 right grow Index라서 Heavy하게 Insert될 경우 Index Split 현상으로 경합이 발생하기에
hash partition 인덱스를 만들어서 테스트해본다.

drop index sales_pk;
 
create index sales_pk on sales(prod_id) global
partition by hash(prod_id)
partitions 10
store in (users);
 
select /*+gather_plan_statistics*/ * from sales where prod_id=23;
select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST -ROWS +PARTITION'));
 
-----------------------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name     | Starts | Pstart| Pstop | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |          |      1 |       |       |      1 |00:00:00.01 |       3 |
|   1 |  PARTITION HASH SINGLE              |          |      1 |     7 |     7 |      1 |00:00:00.01 |       3 |
|   2 |   TABLE ACCESS BY GLOBAL INDEX ROWID| SALES    |      1 | ROWID | ROWID |      1 |00:00:00.01 |       3 |
|*  3 |    INDEX RANGE SCAN                 | SALES_PK |      1 |     7 |     7 |      1 |00:00:00.01 |       2 |
-----------------------------------------------------------------------------------------------------------------
 
실무에서 그럴일은 없겠지만 Hash 파티션된 대상에 부등호 조건으로 조회한다면 어찌될까
select /*+gather_plan_statistics*/ * from sales where prod_id<=22;
select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST -ROWS +PARTITION'));
 
-----------------------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name     | Starts | Pstart| Pstop | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |          |      1 |       |       |      2 |00:00:00.01 |      13 |
|   1 |  PARTITION HASH ALL                 |          |      1 |     1 |    10 |      2 |00:00:00.01 |      13 |
|   2 |   TABLE ACCESS BY GLOBAL INDEX ROWID| SALES    |     10 | ROWID | ROWID |      2 |00:00:00.01 |      13 |
|*  3 |    INDEX RANGE SCAN                 | SALES_PK |     10 |     1 |    10 |      2 |00:00:00.01 |      11 |
-----------------------------------------------------------------------------------------------------------------

 

비파티션은 테스트는 생략한다. 위 결과와 의미는 같다.


■결론


테이블을 날짜나 월 등으로 나누어 파티션하고 과거 파티션을 주기적으로 DROP하려고 한다면 LOCAL 인덱스가 관리에 유리하다.
단, 파티션 키가 조건에 같이 사용되지 않는다면 성능은 나쁠 것이다.
미래의 파티션을 만들어둘 경우 파티션의 갯수만큼 블럭 접근이 더 많아질 것이다만 한 파티션당 1 블럭 정도야 문제 없을 것이다.
문제는 과거의 파티션이다. 과거의 파티션은 RANSE SCAN을 위해 파티션 인덱스마다 root-branch를 거쳐 leaf 블럭을
하나는 접근하게 될 것이다.
데이터가 많으면 blevel이 높을 것이다. 추출 행이 적다면 포기할 성능 수준은 감안할 수 있지만추출행이
추출행이 많고 NL같은 조인이 일어난다면 그 차이는 심각해진다.
비파티션은 Leaf 블럭까지 접근은 한번만 일어날 것이다.

한가지 더 짚고 넘어가야 할 것은 중복 무결성 문제다.
UNIQUE 조건은 LOCAL 인덱스를 만들기 위해 파티션된 컬럼을 포함해줘야 한다.
만약 prod_id가 시퀀스를 통해 입력되지 않았다면? prod_id를 UPDATE하는 건이 있다면?

   PROD_ID    CUST_ID TIME_ID
---------- ---------- -----------------
        21       1121 20180101 00:00:00
        22       1122 20180101 00:00:00
        23       1120 20180101 00:00:00
        24       1111 20180101 00:00:00
        25       1123 20180201 00:00:00
        26       1123 20180201 00:00:00
        27       1234 20180201 00:00:00
        28       1643 20180201 00:00:00
        29       1111 20180301 00:00:00
        30       2222 20180301 00:00:00
        31       1321 20180301 00:00:00
        32       1955 20180301 00:00:00
        33       1754 20180401 00:00:00
        34       1027 20180401 00:00:00
        35       1013 20180401 00:00:00
        36       1100 20180401 00:00:00
 
Insert into sales values(361231 , to_date('20180401 00:00:01','yyyymmdd hh24:mi:ss'));


 

만약 global이나 non-partition으로 unique 인덱스를 만들었다면 에러 나겠지만,
local로 prod_id+time_id를 만들었다면 정상적으로 입력되어 버린다.

drop index sales_pk;
create unique index sales_pk on sales(prod_id,time_id) local;
 
SQL> Insert into sales values(361231 , to_date('20180401 00:00:01','yyyymmdd hh24:mi:ss'));
1 row created.
SQL> commit;
Commit complete.
 
select /*+gather_plan_statistics*/ * from sales where prod_id=36;
 
   PROD_ID    CUST_ID TIME_ID
---------- ---------- -----------------
        36       1100 20180401 00:00:00
        36       1231 20180401 00:00:01


회원정보 테이블의 중복을 허용하지 말아야 하는 유저닉네임 이라면 어찌될까? 잘 생각하자
이런 점들 때문에 OLTP는 일반적으로 GLOBAL이나 비파티션을 권장하게 된다.

 

그런데 주로 history성 테이블에 파티션을 range로 주로 적용하기에 문제가 안될 경우도 많다.

■정리 script


drop table sales purge;
drop sequence sales_seq;


'Knowledge > Oracle' 카테고리의 다른 글

Oracle DBA와 개발자의 필독서  (2) 2016.09.23
Data guard와 Active data guard license  (0) 2016.08.12
sqlplus 접속 hang시에 접속 방법  (0) 2016.05.27
impdp, imp 주의사항  (0) 2016.03.20
Oracle Create Table 내부 절차  (0) 2015.10.21
Posted by neo-orcl
,