SQL 중 최종 추출 결과가 적은 경우에는, Function의 수행위치를 변경하여 과다 수행되는 성능 문제를 해결 할 수 있다.

예를들면,


select z.*

(select

empCnt (e.title) empCnt ,

deptCnt (d.title) deptCnt,

...

from emp e inner join dept d ON e.deptno = d.deptno

where ... ) z

order by rownum <= 3


만약 emp 와 dept 의 inner join rows가 4만건이라면 해당 함수도 4만횟수를 수행하여 쿼리 I/O 상에서는 문제가 없겟지만

함수 I/O 상에서 문제가 크게 발생된다 왜냐하면 함수를 굳이 4만번 수행하지 않고 rownum 3회만 수행하게 할 수 있기 때문이다.



select x.*

, empCnt(x.eTitle) empCnt

, deptCnt(x.dTitle) deptCnt

from (

select z.*

from (

select

e.title eTitle ,

d.title dTitle ,

...

from emp e inner join dept d ON e.deptno = d.deptno

where ... ) z

order by rownum <= 3

) x


위와 같이 쿼리를 수행하면 과다 수행 되지않고 3row를 뽑은뒤 함수가 3번만 수행 되게 되어진다.

하지만 SQL의 최종 추출 결과가 많은 경우에는, Function 의 수행 위치를 바꾸는 것만으로 성능을 개선하기는 힘들다.


Main SQL 에서 추출한 데이터 중 Function을 수행하는 입력값의 종류가 적은 경우 (Distinct rows가 적은경우) 스칼라 서브쿼리에서 Function을 수행하도록 변경하면, Multi Buffer 를 이용해 성능을 개선 시킬 수 있다.


select

jisaname( gr. jisacode1, '00', sysdate ) jisaname1 , 

jisaname( gr.jisacode1, gr.jisacode2, sysdate ) jisaname2,

groupname( gr.parent_groupno ) parent_groupname , 

groupname ( gr.groupno ) groupname ,

...

from t_member mem inner join t_group gr ON gr.groupno = mem.groupno

where ...


해당 쿼리의 함수별 추출 데이터 건수가 4,708 * 4 = 18,832 건이라 하면


select

(SELECT jisaname( gr. jisacode1, '00', sysdate ) FROM DUAL) jisaname1 , 

(SELECT jisaname( gr.jisacode1, gr.jisacode2, sysdate ) FROM DUAL) jisaname2,

(SELECT groupname( gr.parent_groupno ) FROM DUAL) parent_groupname , 

(SELECT groupname ( gr.groupno ) FROM DUAL) groupname ,

...

from t_member mem inner join t_group gr ON gr.groupno = mem.groupno

where ...


jisaname() 은 2번, groupname()은 총 2,213번으로 수행건이 줄어들었다.

gr.jisacode1, 과 gr.jisacode2가 모두 동일한 값을 가지고 있어 스칼라 서브쿼리에서 수행되도록 변경한 경우,

최초 1회씩만 수행하고 나머지는 Cache에서 결과 값을 가져 올 수 있었기 때문이다.



이와같이 입력 값에 대응하는 값의 종류가 다양하지 않아, 같은 입력 값에 대한 결과 값이 항상 동일하다면,

Function 은 스칼라 서브쿼리에서 수행하도록 변경하여 Function 수행에 의한 부하를 감소시켜야 한다.




Hash Join 방식

먼저 수행되는 테이블 (선행 테이블)에 대해 상수 조건으로 데이터 걸러낸 후 join key column 으로 Hash map을 만든다.

그 다음 Probe Table(후행 테이블)을 수행하는데 이 때 상수 조건이 있다면, 먼저 상수 조건으로 대상을 

줄인다. 그 이후 선행 테이블이 생성해 놓은 Hash Map 에 있는 데이터 중에서 조인에 성공한 데이터를 추출한다.


선행테이블의 상수 조건으로 데이터를 줄일 수 있더라도, 후행테이블의 상수 조건 없음으로 인해, 후행 테이블의 전체 데이터 건수만큼 Function 이 수행된다.

(I/O 처리량이 급격히 높아져 성능이 안좋게 된다.)



ex)

FN_C1_CODENM := c1 코드명을 가져오는 함수

FN_C2_CODENM := c2 코드명을 가져오는 함수


SELECT /*+ LEADING(T1) USE_HASH(T1 T2) */

T1.*,

T2.*

FROM FUNCTION_TABLE T1, C1_CODE_NM T2

WHERE

T1.C2 = 0

AND T1.C3 = 'A'

AND T1.C1 = T2.C1

AND T2.C3 = FN_C2_CODENM(T2.C4)


t1의 상수를 검색한 table 값을 hash로 만들어 후행테이블인 t2와 데이터를 추출한다

(t2의 total rows 가 10000만건이라면 성능이 그다지 좋지가 않다)


SELECT /*+ LEADING(T1) USE_HASH(T1 T2) */

T1.*,

T2.*

FROM FUNCTION_TABLE T1, C1_CODE_NM T2

WHERE

T1.C2 = 0

AND T1.C3 = 'A'

AND T1.C1 = T2.C1

AND T2.C4 IN (2, 4)

AND T2.C3 = FN_C2_CODENM(T2.C4)


하지만 위 쿼리와같이 t2의 where 상수 조건이 있다면 그만큼 줄여서 데이터를 추출한다

(t2의 c4값이 2와 4인 row가 4만건이라면 해당 수만큼 hash 조인한다)





Nested Loops Join 방식

선행 테이블을 액세스한 후 join 조건으로 후행 테이블을 반복 탐색하며 조인을 수행 한다.

이 때 선행 테이블에서 추출된 건수만큼 반복 수행하게 된다. 따라서 선행 테이블을 상수조건으로

추출한 건수만큼 조인을 시도하여 Function 이 수행된다.


(후행 테이블의 수가 많다 하더라도 선행테이블을 상수조건 만큼 줄여 반복 탐색 하기 때문에 I/O처리량이

hash join 방식 보다 낮아져 성능이 좋게 된다.)



ex)

SELECT /*+ LEADING(T1) USE_NL(T1 T2) */

T1.*,

T2.*

FROM FUNCTION_TABLE T1, C1_CODE_NM T2

WHERE

T1.C2 = 0

AND T1.C3 = 'A'

AND T1.C1 = T2.C1

AND T2.C3 = FN_C2_CODENM(T2.C4)


선행 테이블의 t1의 상수값을 검색한 row 에 반복적으로 t2 후행테이블 값을 조인 시킨다.

(t2가 10000만건이라도 t1의 상수검색한 row 수가 3800건이라면 3800번 조인한다)



SELECT /*+ LEADING(T1) USE_NL(T1 T2) */

T1.*,

T2.*

FROM FUNCTION_TABLE T1, C1_CODE_NM T2

WHERE

T1.C2 = 0

AND T1.C3 = 'A'

AND T1.C1 = T2.C1

AND T2.C4 IN (2, 4)

AND T2.C3 = FN_C2_CODENM(T2.C4)


후행테이블인 t2의 상수조건까지 들어간다면 t2의 상수검색된 row와 join 하기때문에 t2의 상수검색되지 않은 테이블보다

더 조인문이 줄어든다 (예 : 1800건) --> 왜 1800건일까


SELECT count(*)

FROM FUNCTION_TABLE T1, C1_CODE_NM T2

WHERE

T1.C2 = 0

AND T1.C3 = 'A'

AND T1.C1 = T2.C1

AND T2.C4 IN (2, 4)

--AND T2.C3 = FN_C2_CODENM(T2.C4)

위와같이 function으로 검색한 부분을 지우면 rows가 1800건이 나온다.

이러한 이유로, Nested Loops Join 이 성공하자 마자 Function이 수행되지 않는다.


조인 조건으로 조인 성공이 이루어지더라도 후행 테이블에 상수 조건이 잇다면, 조건을 수행하고 만족하는 데이터 건에 대해서만

Function 을 수행하여 데이터를 추출하게 된다는 것을 확인할 수 있다.



NL ( NASTED LOOPS JOIN )


요약 하자면

CASE    조인순서    조인 방법    T1 데이터건수    T2 데이터건수    조인후추출건수    Function수행 횟수

1        T1 -> T2    HASH JOIN    3,846            100,000            768            100,000

2        T1 -> T2    HASH JOIN    3,846            40,000             384            40,000

3        T1 -> T2    NL    JOIN    3,846            100,000            768              3,846

4        T1 -> T2    NL    JOIN    3,846            40,000              384            1,538

5        T2 -> T1    NL    JOIN    3,846            100,000            768            100,000

6        T2 -> T1    NL    JOIN    3,846            40,000              384            40,000



위 표를 참고하여 조인 방법 및 조인 순서를 고려해 Select 절 Function 수행횟수를 최적화 하여 SQL 성능 문제 해결에

도움이 되길 바란다.


Oracle 에 존재하는 Object 정보를 관리하는 테이블명은 USER_OBJECT 라고 한다.


- 테이블 구조

 OBJECT_NAME

 1

 VARCHAR2 (128 Byte)

 SUBOBJECT_NAME

 2

 VARCHAR2 (30 Byte)

 OBJECT_ID

 3

 NUMBER

 DATA_OBJECT_ID

 4

 NUMBER

 OBJECT_TYPE

 5

 VARCHAR2 (19 Byte)

 CREATED

 6

 DATE

 LAST_DDL_TIME

 7

 DATE

 TIMESTAMP

 8

 VARCHAR2 (19 Byte)

 STATUS

 9

 VARCHAR2 (7 Byte)

 TEMPORARY

 10

 VARCHAR2 (1 Byte)

 GENERATED

 11

 VARCHAR2 (1 Byte)

 SECONDARY

 12

 VARCHAR2 (1 Byte)

 NAMESPACE

 13

 NUMBER

 EDITION_NAME

 14

 VARCHAR2 (30 Byte)

 

▼ OBJECT_TYPE


TABLE SUBPARTITION

INDEX PARTITION

TABLE PARTITION

SEQUENCE

LOB PARTITION

PROCEDURE

LOB

INDEX SUBPARTITION

PACKAGE

PACKAGE BODY

TRIGGER

TABLE

FUNCTION

INDEX

VIEW

TYPE

CLUSTER




SELECT * FROM USER_OBJECTS

WHERE OBJECT_TYPE = 'TABLE'



SELECT * FROM USER_OBJECTS

WHERE OBJECT_TYPE = 'TYPE'


상위 Query 로 정보를 얻을 수 있다.




  • Return 받을 행을 받는 Object Type을 생성
CREATE OR REPLACE TYPE obj_type AS object
( c1 INT,
  c2 INT
);
/


  • Collection Type 생성
CREATE OR REPLACE TYPE table_type
AS TABLE OF obj_type;
/


  • Table Function을 생성 - 원하는 만큼의 Row를 출력하는 Function
CREATE OR REPLACE FUNCTION table_func (p_start int, p_end int)
  RETURN table_type
  IS
    v_type TABLE_TYPE := table_type();
  BEGIN
   
    FOR i IN p_start..p_end LOOP
      v_type.extend;
      v_type(i) := obj_type(i,i);
    END LOOP;
     
    RETURN v_type;
 END;
 /


  • FROM 절에 'TABLE'이라는 Keyword를 이용해 아래와 같은 결과 추출
SELECT * FROM TABLE(table_func(1,3));
 
 
   C1         C2
----- ----------
    1          1
    2          2
    3          3



Pipelined Table Function은 하나의 Row를 받아서 바로 처리하므로 수행 속도가 빠르다. 이에 비해 Table Function은 전체 Row가 처리된 이후에 동작되므로 Pipelined Table Function에 비해 이전 처리된 Row를 Cache할 Memory를 더 요구하게 된다.


Table Function은 전체 데이터 처리를 수행하지만 Pipelined Table Function은 부분 범위 처리를 수행한다

Oracle 10g부터 사용하는 dbms_xplan Package의 Function들도 Pipelined Table Function으로 구현되어 있다.





출처 : http://www.gurubee.net/lecture/2238

merge 문을 사용 할 때 

on절에 온 Unique 컬럼을 update 인자로 쓸 수 없다.


하지만 그 unique 컬럼을 merge 문으로 사용하기 위해 아래와 같이 

rowid 로 join을 걸어 해당 unique 컬럼을 update 로 사용 할 수 있다.




SELECT e.empno, e.ename

  FROM emp e INNER JOIN dept d ON e.deptno = d.deptno




SELECT a.empno, a.ename

  FROM emp a,

       (SELECT b.*, aa.ROWID rid

          FROM dept b, emp aa

         WHERE b.deptno = aa.deptno(+)) c

 WHERE a.ROWID = c.rid




아 참고로 Merge 문의 on 절에 오는 a.coulmn = b.column 들은

unique 값이 어야 한다.


unique 값이 아니라면 distinct 나 group by 하여 1:1 매칭이 되게 하여야 merge 문 에러가 나지 않는다.



여러 개의 Table이 Join된 View를 Update할 수 있다는 것을 잘 알고 있을 것이다. Oracle은 이것을 Updatable Join View라고 부른다.

View(단, Subquery까지 포함)를 Update할 수 있는 방법은 내가 알기로는 세 가지 정도가 있다. 

- Updatable Join View
- Merge Statement(정확하게 말하면 View는 아니지만 그런 효과가 있다는 의미)
- Instead of Trigger

역시 하나의 문제에 대해 여러 가지 솔루션이 있을 수 있다는 것을 아는 것이 중요하다는 것의 예가 되겠다.

간단한 예를 통해 각 방법들을 살펴 보면...

우선 다음과 같이 Parent/Child 관계를 가지는 Table을 만든다.

drop table t1 purge;
drop table t2 purge;

create table t1(c1 int, c2 int);
create table t2(c1 int, c2 int);

insert into t1
select level, level
from dual
connect by level <= 10
;

insert into t2
select mod(level, 10)+1, level
from dual
connect by level <= 100
;

commit;



Data만으로 보면 Parent(T1)/Child(T2) 관계이지만 Oracle이 이 관계를 파악할 수 있는 방법은 없다.

다음과 같이 두 개의 View를 만든다. View의 "Column"에 유의하자.

create or replace view v1 
as
select 
  t1.c1, t2.c2
from 
  t1, t2
where
  t1.c1 = t2.c1
  and t1.c1 between 1 and 2
;



이 View는 Updatable한가? user_updatable_columns 뷰를 보면 알 수 있다.

select 
  column_name, updatable, insertable, deletable
from 
  user_updatable_columns
where
  table_name = 'V1'
;

COLUMN_NAME          UPDATABLE INSERTABL DELETABLE 
-------------------- --------- --------- --------- 
C1                   NO        NO        NO        
C2                   NO        NO        NO   

     

전혀 Updatable하지 않다. Oracle은 Parent/Child 관계를 알 수 없기 때문이다. 
View의 특정 Column이 Updatable하려면 Key-Preserved해야 하는데(Oracle의 용어이다), Key-Preserved한지를 Oracle이 판단할 수 없기 때문이다.

같은 이유로 다음과 같은 View도 전혀 Updatable하지 않다. 

create or replace view v2
as
select 
  t2.c1, t2.c2
from 
  t1, t2
where
  t1.c1 = t2.c1
  and t1.c1 between 1 and 2
;

select 
  column_name, updatable, insertable, deletable
from 
  user_updatable_columns
where
  table_name = 'V2'
;
COLUMN_NAME          UPDATABLE INSERTABL DELETABLE 
-------------------- --------- --------- --------- 
C1                   NO        NO        NO        
C2                   NO        NO        NO    

    

Updatable하지 않기 때문에 다음과 같이 Subquery(View)에 대한 Update는 실패한다.

update (
  select 
    t1.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) x
set x.c2 = x.c2 + 1000
;

ERROR at line 10:
ORA-01779: cannot modify a column which maps to a non key-preserved table



update (
  select 
    t2.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) x
set x.c2 = x.c2 + 1000
;

ERROR at line 10:
ORA-01779: cannot modify a column which maps to a non key-preserved table



이 문제를 해결하는 한가지 안좋은 방법은 /*+ BYPASS_UJVC */ Hint를 사용하는 것이다. 말 그대로 Updatable Join View Constraint(제약 조건)을 Bypass한다는 것이다.

update /*+ bypass_ujvc */ (
  select 
    t1.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) x
set x.c2 = x.c2 + 1000
;

20 rows updated.




궁극적인 해결책은 Oracle에게 Parent/Child 관계를 알려 주는 것이다.

더 정확하게 이야기하면 t1:t2 = 1:N이며 t1.c1이 Unique하기 때문에 t2와 관련된 Column들은 Updatable하다는 것을 알려 주어야 한다.

다음과 같이 t1.c1에 primary key를 추가함으로써 이를 달성할 수 있다. 

alter table t1 add constraint t1_pk primary key (c1);



다음과 같이 일부 혹은 전체 Column들이 Updateable하다는 것을 확인할 수 있다. 

alter view v1 compile;
alter view v2 compile;

select 
  column_name, updatable, insertable, deletable
from 
  user_updatable_columns
where
  table_name = 'V1'
;
COLUMN_NAME          UPDATABLE INSERTABL DELETABLE 
-------------------- --------- --------- --------- 
C1                   NO        NO        NO        
C2                   YES       YES       YES       


select 
  column_name, updatable, insertable, deletable
from 
  user_updatable_columns
where
  table_name = 'V2'
;
COLUMN_NAME          UPDATABLE INSERTABL DELETABLE
-------------------- --------- --------- ---------
C1                   YES       YES       YES      
C2                   YES       YES       YES    

  

따라서 다음과 같이 Join View를 Update할 수 있다.
더 정확하게 말하면 user_updatable_columns 뷰에서 'YES'로 나온 Column에 대해서는 update/insert/delete가 가능하다.

update (
  select 
    t1.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) x
set x.c2 = x.c2 + 1000
;
20 rows updated.



다음과 같은 Insert 구문은 어떨까? 성공할까?

insert into (
    select 
    t1.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) 
values(1, 11)
;



Column t1.c1은 Updatable하지 않기 때문에 다음과 같이 에러가 난다.


ERROR at line 1:
ORA-01779: cannot modify a column which maps to a non key-preserved table



반면 다음과 같은 Insert는 성공한다. 
Key-Preserved인 Table에 대해서만 Insert가 이루어지기 때문이다.

insert into (
    select 
    t2.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) 
values(1, 11)
;

1 row created.




다음과 같이 Delete도 가능하다.

delete from (
  select 
    t1.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) x
;

20 rows deleted.



View에 대한 Update는 어떤 실행 계획을 보일까? 아래에 그 결과가 있다.

explain plan for
update (
  select
    t1.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) x
set x.c2 = x.c2 + 1000
;



매우 논리적인 실행 계획을 보인다.

-----------------------------------------------------------------------------
| Id  | Operation           | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | UPDATE STATEMENT    |       |     4 |   156 |     4   (0)| 00:00:01 |
|   1 |  UPDATE             | T2    |       |       |            |          |
|   2 |   NESTED LOOPS      |       |     4 |   156 |     4   (0)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL| T2    |    20 |   520 |     4   (0)| 00:00:01 |
|*  4 |    INDEX UNIQUE SCAN| T1_PK |     1 |    13 |     0   (0)| 00:00:01 |
-----------------------------------------------------------------------------

Predicate Information (identified by operation id):   
---------------------------------------------------   
                                                      
   3 - filter("T2"."C1">=1 AND "T2"."C1"<=2)          
   4 - access("T1"."C1"="T2"."C1")                    
       filter("T1"."C1">=1 AND "T1"."C1"<=2)         




논의를 더 확장해보자. 
이런 Query에서 Parallel Execution은 가능한가? 
안될 이유가 없다.

explain plan for
update (
  select /*+ parallel(t2) parallel(t1) no_index(t2) no_index(t1) */ 
    t1.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) x
set x.c2 = x.c2 + 1000
;



실행 계획은 다음과 같다.

-------------------------------------------------------------
| Id  | Operation                | Name     | Rows  | Bytes |
-------------------------------------------------------------
|   0 | UPDATE STATEMENT         |          |     4 |   156 |
|   1 |  UPDATE                  | T2       |       |       |
|   2 |   PX COORDINATOR         |          |       |       |
|   3 |    PX SEND QC (RANDOM)   | :TQ10001 |     4 |   156 |
|*  4 |     HASH JOIN            |          |     4 |   156 |
|   5 |      PX RECEIVE          |          |     2 |    26 |
|   6 |       PX SEND BROADCAST  | :TQ10000 |     2 |    26 |
|   7 |        PX BLOCK ITERATOR |          |     2 |    26 |
|*  8 |         TABLE ACCESS FULL| T1       |     2 |    26 |
|   9 |      PX BLOCK ITERATOR   |          |    20 |   520 |
|* 10 |       TABLE ACCESS FULL  | T2       |    20 |   520 |
-------------------------------------------------------------



View 부분(Select ...)은 명백하게 병렬 실행이 된다.
하지만 Update 부분은 병렬 실행이 되지 않는 것을 알 수 있다.

Parallel DML은 다음과 같이 명시적으로 활성화를 해야만 실행이 된다.

alter session enable parallel dml;

explain plan for
update (
  select /*+ parallel(t2) parallel(t1) no_index(t2) no_index(t1) */ 
    t1.c1, t2.c2
  from
    t1, t2
  where
    t1.c1 = t2.c1
    and t1.c1 between 1 and 2
  ) x
set x.c2 = x.c2 + 1000
;

---------------------------------------------------------------------
| Id  | Operation                        | Name     | Rows  | Bytes |
---------------------------------------------------------------------
|   0 | UPDATE STATEMENT                 |          |     4 |   156 |
|   1 |  PX COORDINATOR                  |          |       |       |
|   2 |   PX SEND QC (RANDOM)            | :TQ10002 |     4 |   156 |
|   3 |    UPDATE                        | T2       |       |       |
|   4 |     PX RECEIVE                   |          |     4 |   156 |
|   5 |      PX SEND HASH (BLOCK ADDRESS)| :TQ10001 |     4 |   156 |
|*  6 |       HASH JOIN                  |          |     4 |   156 |
|   7 |        PX RECEIVE                |          |     2 |    26 |
|   8 |         PX SEND BROADCAST        | :TQ10000 |     2 |    26 |
|   9 |          PX BLOCK ITERATOR       |          |     2 |    26 |
|* 10 |           TABLE ACCESS FULL      | T1       |     2 |    26 |
|  11 |        PX BLOCK ITERATOR         |          |    20 |   520 |
|* 12 |         TABLE ACCESS FULL        | T2       |    20 |   520 |
---------------------------------------------------------------------




Merge 구문이 등장함으로써 Updatable Join View의 기능을 어느 정도 대신할 수 있게 되었다. 
아래와 같이 Insert가 없는 Merge 구문을 사용하면 똑같은 효과를 얻을 수 있다.
(더 직관적이기도 하다)

merge into t2
using (select c1, c2 from t1 
          where c1 between 1 and 2) x
on (x.c1 = t2.c1)
when matched 
  then update set t2.c2 = t2.c2 + 1000
;

20 rows merged.



Updatable Join View나 Merge를 이용해서 하나의 Query로 View에 대해 DML이 불가능하다면?
개별 Table에 대한 여러 번의 DML로 나누어서 수행할 수 밖에 없는가?
만일 권한 관리 때문에 View에 대한 권한밖에 줄 수 없다면?

이런 상황을 해결할 수 있는 마지막 수단이 Instead of Trigger이다. 

가령 다음과 같은 View v1에 대한 Insert 문은 (당연하게도) 모두 실패한다.
(Column t1.c1은 Updatable하지 않기 때문이다)

insert into v1
values(1, 10)
;
ERROR at line 1:
ORA-01779: cannot modify a column which maps to a non key-preserved table

insert into v1
select values(100, 100);
ERROR at line 1:
ORA-01779: cannot modify a column which maps to a non key-preserved table


/*+ bypass_ujvc */ Hint를 사용하면 해결될까?

insert /*+ bypass_ujvc */ into v1 values(1, 10)
*
ERROR at line 1:
ORA-01776: cannot modify more than one base table through a join view


View v1은 Table t1, t2의 Column을 모두 참조하고 있기 때문에 두 개의 Table에 대해 Insert하는 것이 불가능하다.

이 문제를 해결할 수 있는 한가지 방법은 View에 대한 Trigger(Instead of Trigger)를 만드는 것이다.

create or replace trigger trg1
instead of insert on v1
for each row 
declare

begin
  insert into t1 values(:new.c1, 100);
  insert into t2 values(:new.c1, 100);
  
end;
/


다음과 같이 Insert가 모두 성공한다. 

insert into v1
values(1, 10)
;
ERROR at line 1:
ORA-00001: unique constraint (UKJA.T1_PK) violated
ORA-06512: at "UKJA.TRG1", line 4
ORA-04088: error during execution of trigger 'UKJA.TRG1'

insert into v1
values(100, 100);

1 row created.



역시 가장 중요한 것은 하나의 문제에 대해 여러 가지 솔루션이 있을 수 있다는 가정을 잊지 않는 것이다.



출처 : http://ukja.tistory.com/187

1. MUTATING ERROR란 무엇인가?


어느 TABLE에 DML(INSERT, UPDATE, DELETE 등)이 실행될 때마다 프로그램에

구애받지 않고 특정 작업을 수행하려할 때 database trigger를 사용한다.

예)EMP table에 data insert, update, delete 시 부서별 평균 급여 table에

updating 하는 경우.


이 경우 trigger를 사용하지 않고 같은 작업을 하려면 평균 급여를 구하는

PL/SQL program을 개발하여 EMP 테이블에 action이 발생 시마다 call하여 사용

하든가, 아니면 각 action 발생 후 동일한 routine을 반복 수행시켜야 한다.


이 때 만일 user가 EMP table에 update 시마다 EMP table에 어떤 처리를 수행

하는 trigger를 만든다면 원치 않는 결과를 일으킬 수 있고 OS memory가 소진

될 때까지 trigger가 trigger를 recursive하게 fire시켜 마치 looping

program과 같은 상황을 초래할 수도 있다.


이러한 trigger를 recursive trigger라 부르며 이런 불상사를 막기 위해

ORACLE은 EMP table에 row trigger를 만들어 원천적으로 trigger 내에서

EMP table을 아예 access 할 수 없도록 하고 있고, 이와 같은 원칙에 위배될

경우 발생되는 error를 mutating error 라고 부른다.


이 경우 user가 trigger를 만든 후 DML(insert, update, delete)을 수행 시

"ORA-4091:table SCOTT.EMP is mutating, trigger/function may not see

it." 와 같은 error를 만나게 된다.



2. ERROR가 발생하는 조건.


TRIGGER에는 다음과 같은 두 종류가 있다.

*row trigger - 프로그램에서 한 row 단위로 처리 시 처리할 때마다

fire되는 trigger.


*statement trigger - 프로그램 당 한번만 fire되는 trigger.


위와 같으므로 만일 application에서 한 row만 처리한다면 두 type에는 차이가

없고 여러 row를 처리할 경우 두 type 간의 차이가 발생한다.


Statement trigger는 일부 제한은 있으나 원칙적으로 mutating error를 발생

시키지 않는다.

Row trigger는 하나의 row 처리 후 해당 table에 대한 계속된 row 처리가 있을

수 있으므로 작업이 완료되기까지 해당 table을 access하는 것이 금지되지만

statement trigger는 일단 하나의 statement가 완료되었다는 보장을 할 수

있으므로 mutating의 기본 속성인 "현재 변화되고 있는 table" 이라는 범위에

들지 않는다.

따라서, mutating error는 row trigger에서의 제한 사항이라 해도 무리가 없다.



3. 해결 방법.


위에서 보았 듯 mutating error를 피해 나가려면 statement trigger를 사용하면

어 려움이 없으나 statement trigger에서는 row trigger에서와 같이 row 단위로

변경 전 후 column data를 handling할 수 없다는 단점이 있다.

즉 :new.column, :old.column을 사용하지 못한다.


이 와 같은 문제로 인하여 row trigger를 사용 시는 temp table 이나 PLSQL

table을 이용하여 피해갈 수가 있다.


다음은 row trigger를 사용 시 mutating error를 유발하는 case(A)와

이를 statement trigger로 전환하여 error를 피해가는 case(B)

에 대한 내용이다.


예) EMP table에 insert, update, delete 시 부서별 평균 급여를 계산하여

DEPT table에 load한다. (TABLE COLUMN은 다음과 같다.)


SQL> desc emp

Name Null? Type

------------------------------- -------- ----

EMPNO NOT NULL NUMBER(4)

ENAME VARCHAR2(10)

JOB VARCHAR2(9)

MGR NUMBER(4)

HIREDATE DATE

SAL NUMBER(7,2)

COMM NUMBER(7,2)

DEPTNO NUMBER(2)



SQL> desc dept

Name Null? Type

------------------------------- -------- ----

DEPTNO NUMBER(2)

DNAME VARCHAR2(14)

LOC VARCHAR2(13)

SAL NUMBER(7,2)




(CASE A) ROW Trigger 만을 사용 시 에러가 발생하는 case.


1) row trigger 생성


create or replace trigger emp_aft_row_trigger

after insert or update or delete on emp

for each row

declare

v_sal emp.sal%type;

begin

select avg(sal) into v_sal from emp

where deptno=:old.deptno;


update emp

set sal=v_sal

where deptno=:old.deptno;


if :old.deptno != :new.deptno then

select avg(sal) into v_sal from emp

where deptno=:new.deptno;


update emp

set sal=v_sal

where deptno=:new.deptno;

end if;

end;

/


2) DATA 1건을 UPDATE한다.


SQL)update emp

set sal=10000

where empno= 7934;

SQL)

update emp

*

ERROR at line 1:

ORA-04091: table JMKIM.EMP is mutating, trigger/function may not

see it

ORA-06512: at line 4

ORA-04088: error during execution of trigger 'JMKIM.EMP_AFT_ROW_TRIGGER'




(CASE B) 에러를 피해 가는 방법


1) PL/SQL table을 생성한다.


SQL) create or replace PACKAGE emp_pkg as

TYPE emp_tab_type is table of EMP.DEPTNO%TYPE

index by binary_integer;

emp_old emp_tab_type;

emp_new emp_tab_type;

emp_index binary_integer;

end emp_pkg;

/


Package created.



2) BEFORE STATEMENT trigger를 생성한다.


SQL)create or replace TRIGGER emp_bef_stm_all

before insert or update or delete on emp

begin

emp_pkg.emp_index :=0;

end;

/

SQL)

Trigger created.



3) AFTER ROW trigger를 생성한다.


SQL>create or replace TRIGGER emp_aft_row_all

after insert or update or delete on emp

for each row

begin

emp_pkg.emp_index := emp_pkg.emp_index + 1;

emp_pkg.emp_old(emp_pkg.emp_index) := :old.deptno;

emp_pkg.emp_new(emp_pkg.emp_index) := :new.deptno;

end;

/

SQL>Trigger created.



4) AFTER STATEMENT trigger를 생성한다.


SQL>create or replace TRIGGER emp_aft_stm_all

after insert or update or delete on emp

declare

v_sal emp.sal%type;

begin

for i in 1 .. emp_pkg.emp_index loop

select avg(sal) into v_sal from emp

where deptno=emp_pkg.emp_old(i);

update dept

set sal = v_sal

where deptno=emp_pkg.emp_old(i);

dbms_output.put_line('DEPTNO(old)=>'||to_char(emp_pkg.emp_old(i)));

if emp_pkg.emp_new(i) != emp_pkg.emp_old(i) then

select avg(sal) into v_sal from emp

where deptno=emp_pkg.emp_new(i);

update dept

set sal = v_sal

where deptno=emp_pkg.emp_new(i);

dbms_output.put_line('DEPTNO(new)=>'||to_char(emp_pkg.emp_new(i)));

end if;

end loop;

emp_pkg.emp_index :=0;

end;

/


SQL>

Package created.



5) data insert 및 확인


SQL> update emp

set sal = 9000

where empno=7902;


SQL>

DEPTNO(old)=>20

1 row updated.



where절에 있는 서브쿼리 결과가 1개 이상인 경우

filter 방식 / join 방식 옵티마이저 


filter 방식 - 메인 sql 데이터 만큼 서브쿼리가 돈다.

즉, 메인 sql 100만건 돈다면 일일이 서브쿼리에게 100만건 input 한다

(서브 쿼리의 결과가 1개라고 한다면 성능저하 되지는 않지만 그런 경우는 거의 없으므로

성능이 안좋은 쿼리라고 할 수 있다)


join 방식 - 메인 sql 양이 많고 서브 sql 의 양이 적을때

선 서브 sql 선 scan 하여 join 할수 있다.


하지만 nested loops join 이외 다른 join은 filter 방식에 대한 효과를 얻지 못한다.

(join 하고자하는 input 값이 적을땐 join 방식보다 filter 방식이 더 성능상 이점을 가질

수 있다)


스칼라 서브쿼리를 사용할 때 결과 추출값이 많을 때는

서브쿼리에서 조인으로 변경한 후 hash join 유도 하는게 좋다 (no index scan)


결과 추출값이 적을 때는 조인에서 부분 범위처리 (rownum >=10)를 선행 후

스칼라 서브쿼리로 바꾸는것이 I/O 처리량을 줄일 수 있다


관련 내용은 여기에 자세히 

http://oracle10g.tistory.com/871



오라클 9i 에서부터


Bind 변수를 가진 SQL 에 대해서 최초파싱할 때, 실제 Bind 값을 먼저 확인하고, 히스토그램을 참조하여

최적화 하는 것을 말한다. 

(Bind 변수를 사용하는 경우, Query가 최초로 실행되는 시점의 실제 Bind 값을 이용해서(Peeking해서) 실행 계획을 세우는 것을 의미한다.)


Bind 변수를 사용하는 경우 실제로 실행되는 시점에 어떤 값이 들어오는지 알 수 없기 때문에 컬럼의 평균적인 분포만을 가지고 비용을 계산할 수 밖에 없다. 대부분의 경우에는 큰 문제가 안되지만, 다음과 같은 경우에는 치명적인 단점을 가지고 있다. 


가령 status 컬럼의 분포가 다음과 같다고 하면...

 - status = 1  : 99%

 - status = 99 : 1%


이 경우

- Where status = '1' 이면 인덱스를 경유하지 않고 Full Table Scan을 하는 것이 유리하다

- Where status = '99' 이면 인덱스를 경유하는 것이 유리하다.


하지만, 

- Where status = :b1 과 같이 Bind 변수를 사용하면 어떤 값이 올지 모르기 때문에 평균적인 분포를 따르게 된다. 따라서 이 경우 반드시 Full Table Scan을 선택한다. 


Bind Peeking은 이러한 단점을 피하기 위해 쿼리가 실행되는 최초 시점에 Bind 변수에 들어온 값을 이용하게 된다. 즉, 쿼리가 최초로 실행되는 시점에 


exec :b1 := '1';

... Where status = :b1 


과 같이 실행되면 Full Table Scan을,


exec :b2 := '99';

... Where status = :b1


과 같이 실행되면 Index Range scan을 선택하게 된다. 

단, 위와 같이 작동하려면 컬럼 값이 "1" 인 경우와 "99"인 경우의 분포도의 차이를 Oracle이 명확하게 이해하고 있어야 되므로 Histogram이 반드시 존재해야 한다. 가령 


exec dbms_stats.gather_table_stats(user, 'TABLE_NAME', method_opt=>'for columns size 2 status');


와 같이 컬럼에 대해 적당한 크기의 Histogram이 생성되어 있어야 된다.


위의 설명을 이해했다면 Bind Peeking에는 기본적인 딜레마가 있음을 알 수 있다. 위의 예를 보면 쿼리가 최초로 실행되는 시점에 "1"의 값이 오느냐, "99"의 값이 오느냐에 따라 실행 계획이 결정된다. 어떤 쪽이든 한 쪽은 손해를 볼 수 밖에 없는 결정적인 구조적 한계를 가지고 있다.


이런 구조적 한계를 가진 기능을 제공한다는 것은 기본적인 설계의 결함이라고 할 수 있다. 덕분에 많은 시스템에서 Bind Peeking 기능을 비활성화시켜 버리고 있다. _optim_peek_user_binds 파라미터 값을 false로 지정해 버리는 것이다. 


이런 경향은 10g에서 특히 심한데, 그 이유는 10g에서 dbms_stats의 method_opt 파라미터의 기본값이 for all columns size auto로 바뀌는 바람에 Histogram의 생성 여부를 Oracle이 자동으로 판단해버리는데 있다. 이전 버전에서는 for all columns size 1 이 기본값으로 Histogram이 생성되지 않았다.

Histogram이 없으면 Bind Peeking에 의한 부작용은 거의 없다. 10g에서 Histogram의 생성 여부를 Oracle이 판단함으로써 이전 버전에서는 존재하지 않던 Histogram이 갑자기 생성되고 이로 인해 Bind Peeking의 결함이 더욱 두드러지게 나타나게 되어 버린 것이다. 

이 때문에 Oracle 10g에서는 _optim_peek_user_binds 파라미터를 False로 바꾸는 것이 기본 권고안처럼 되어 버릴 정도가 되었다.


하지만, Oracle 11g에서 아주 큰 희소식이 전달되었다. 이른바 Adaptive Cursor Sharing이라는 기능이 소개된 것인데, 이 기능으로 인해 Bind Peeking과 Histogram의 조합에 의한 부작용이 말끔히 해소될 수 있다. 


Adaptive Cursor Sharing이란 말 그대로 상황에 맞게 유연하게 Cursor를 Share하겠다는 의미이다. 이 개념을 구현하기 위해 Oracle은 Bind Sensitive Cursor, Bind Aware Cursor라는 새로운 개념을 도입했다. 

Bind Sensitive Cursor란, 말 그대로 Bind 값에 민감한 Cursor라는 의미이다. 즉, Bind 값이 바뀌면 그것을 민감하게 처리하겠다는 의미이다. 1) Equal 조건에서는 조건절에 쓰인 컬럼에 Histogram이 있고 2) Range 조건인 경우 Oracle은 이것을 Bind Senstive Cursor라고 부른다.

Bind Aware Cursor란, Bind Sensitive Cursor에 입력된 Bind 값에 따라 실행 계획이 분화된 Cursor를 의미한다. 즉, Bind Aware Cursor가 생성되었다는 것은 Bind 변수의 값에 따라 Oracle이 적절한 Child Cursor를 생성했다는 것을 의미한다.


Adaptive Cursor Sharing 기법을 사용하면 Bind Peeking에 의해 최초 실행 시점에 실행 계획이 결정되더라도 이후 새로운 Bind 변수 값이 사용되면 이에 맞게 실행 계획을 분화(새로운 Child Cursor 생성)시킨다. 따라서 Bind Peeking에 의한 부작용이 사실상 없어지게 된다.

단, 조건절에 쓰인 컬럼에 Histogram이 있고, Histogram의 분포도에 따라 실행 계획에 큰 차이가 있을 수 있다고 판단된다는 조건이 중요하다. 즉, 적절한 Histogram 없이는 의미가 없다는 것이다. Histogram은 이제 우리가 반드시 이해하고 활용해야 하는 존재가 되었다.

(dbms_stats의 method_opt 파라미터의 기본값이 for all columns size auto로 바뀌는 순간 이미 Histogram은 피할 수 없는 존재가 되어버렸다는 사실을 기억할 필요가 있겠다)


아래 샘플 코드를 이용하면 Oracle 11g의 Adaptive Cursor Sharing 기법을 이해할 수 있다. 


-------------------------------------

-- Oracle 11g Bind Aware Cursor

-- Author: 조동욱

--------------------------------------


-- create objects

drop table acs_table;


create table acs_table(id int, name char(10));


create index acs_table_idx on acs_table(id);


insert into acs_table select 1, 'name' from all_objects where rownum <= 100000


insert into acs_table values(99, 'name');


commit;


-- gather statistics with histogram

exec dbms_stats.gather_table_stats(user, 'ACS_TABLE', estimate_percent=>100, method_opt=>'FOR COLUMNS SIZE 2 ID', cascade=>true);


-- check histogram

select * from dba_tab_histograms where table_name = 'ACS_TABLE';


-- Bind query

alter system flush shared_pool


var id number;


-- id == 1

-- 각 단계마다 아래 쿼리 결과 확인

select sql_id, sql_text,is_bind_sensitive,is_bind_aware

from v$sql where sql_text like 'select count(name) from acs_table%';


exec :id := 1;


select count(name) from acs_table where id = :id;


-- id == 99

exec :id := 99;


select count(name) from acs_table where id = :id;


select count(name) from acs_table where id = :id;


-- id == 1 again

exec :id := 1;


select count(name) from acs_table where id = :id;


-- check mismatch

select * from v$sql_shared_cursor where sql_id = '<sql_id>';





출처 : http://ukja.tistory.com/entry/Bind-Peeking%EC%9D%98-%EB%94%9C%EB%A0%88%EB%A7%88%EC%99%80-Oracle-11g%EC%9D%98-Adaptive-Cursor-Sharing

옵티마이저를 이용한 SQL 튜닝


(1) 인덱스 정보를 이용하게 하려면


- Optimizer가 실행계획(Execution Plan)을 결정하는 중요한 요소로 테이블에 대한 정보가 큰 비중을 차지한다.

특히 Index 정보는 아주 중요한 역할을 하므로, 항상 현재의 Index 정보를 파악하고 있어야 한다.


SQL> SELECT DISTINCT B.CLASS, COUNT(B.CLASS)

2    FROM COL A, COM B

3    WHERE A.YEAR = B.YEAR

4        AND A.HOUSE = B.HOUSE

5        AND A.SERIAL_NO = B.SERIAL_NO

6        AND A.AMT >= 100

7        AND B.DATE < '950501'

8        AND B.CLASS IN ('C', 'Y', 'I', 'P')

9    GROUP BY B.CLASS ;


위의 SQL문은 테이블 COM의 컬럼 DATE가 950501 보다 적고 컬럼 CLASS가 'C', 'Y', 'I', 'P' 인 것 중 테이블 COL과 조인하여 각각의 컬럼 CLASS에 대하여 건수를 구하는 SQL 문이다.


INDEX 정보


테이블명        :        인덱스명        :        컬럼 구성

COL A           :        COL_PK1       :        YEAR + HOUSE + SERIAL_NO + HSNO

COM B          :        COM_IX        :         YEAR + HOUSE + SERIAL_NO

COM B          :        COM_IDX1     :        CLASS + DATE

실행계획


ROWS        EXECUTION PLAN

_______      _________________________________________

0            SELECT STATEMENT

  1564                SORT(GROUP BY)

  1564                    NESTED LOOP

  6361                        TABLE ACCESS (FULL) OF 'COM'

  2437                        TABLE ACCESS (BY ROWID) OF 'COL'

  4437                            INDEX (RANGE SCAN) OF 'COL_PK1'(UNIQUE)


COM 테이블에서 FULL SCAN이 발생했다. 무엇이 문제일까?


- SQL문을 보면 GROUP BY 를 사용했기 때문에 DISTINCT는 무의미하다.

- 물론 Execution Plan을 보아도 DISTINCT에 대해서 별도의 실행계획은 생성되지 않았음.

- DISTINCT를 제거한다 해도 실행계획과 실행시간에 영향을 미치지 않음.


- Optimizer Mode 가 ALL_ROWS인 상태에서 Optimizer는 COM 테이블에 CLASS+DATE로 구성된 인덱스가 있음에도 불구하고 전체 테이블 스캔을 하는 것이 더 효율적이라고 판단했기 때문.

- Optimizer는 우리가 생각할 때 아니다 싶은 방향으로도 실행계획을 생성하기도 함.

- 그러나, COM 테이블의 인덱스 COM_IDX1(CLASS+DAT)를 먼저 ACCESS하고 난 후에 COL테이블을 ACCESS하면 COM테이블을 FTS(Full Table Scan)하는 것보다 Access하는 횟수를 줄일 수 있을 것이다.


* 해결 방법


- SQL DISTINCT 제거

- Optimizer Mode를 FIRST_ROWS인 상태 또는 RULE로 변경하면 Optimizer는 COM테이블을 FTS(Full Table Scan)하지 않고 Index COM_IDX1을 제일 먼저 Access하게 될 것이다.


EXECUTION PLAN

________________________________________________

0                   SELECT STATEMENT

1    0                 SORT(GROUP BY)

2    1                     NESTED LOOP

3    2                          TABLE ACCESS (BY ROWID) OF 'COM'

4    3                            INDEX (RANGE SCAN) OF 'COM_IDX1'(NON-UNIQUE)

5    2                          TABLE ACCESS (BY ROWID) OF 'COL'

6    5                            INDEX (RANGE SCAN) OF 'COL_PK1'(UNIQUE)


Optimizer Mode를 FIRST_ROWS 또는 RULE로 변경하니, 실행계획이 변경되었다. ; COM 테이블이 COM_IDX1 인덱스를 사용한다.




(2) CHOOSE Mode 와 CBO


SQL 문에 포함된 테이블 중 어느 하나라도 이미 분석된 상태(ANALYZE)라면, 그 SQL문은 기본적으로 비용기반 최적화에 맞춰진다.


하나의 SQL 예를 봅시다.


- SQL


복잡해 보이지만, 빨간색 조건만 확인합니다. FZJ222T A 테이블의 RECIPE_GROUP 이 1이고 RECIPE_CE 가 1인 데이터를 다른 여러 테이블과 JOIN 시킨 SQL입니다. Optimizer Mode 는 CHOOSE 상태로 가정합니다.


- Index



FZJ222T 테이블의 인덱스 입니다. 나머지 테이블도 위와 같은 방식으로 인덱스가 구성되어 있습니다.


- 실행 계획



위 실행계획을 보면 Optimizer Mode는 CHOOSE 라는 것을 알 수 있고 CBO(COST BASED OPTIMIZER)으로 실행된 것을 알 수 있다.

(COST=8 CARD=1 BYTES=140 등 정보)

CHOOSE Mode에서 CBO 방식으로 실행계획이 작성된 것은 해당 테이블 중 ANALYZE 된 테이블이 존재한다는 것.


- 문제점은?


실행계획을 살펴보면 가장 먼저 Access 하는 테이블은 빨간색 부분인 FZJ221T 테이블이다. Index는 FZJ221T_IDX1 이다.

위에 SQL을 다시 살펴보면 WHERE 조건의 빨간색 부분을 기준이 되어 JOIN이 일어난다. 그러므로 FZJ222T_IDX2 인덱스로 인덱싱한 FZJ222T 테이블이 먼저 Access 되어야 한다.

Optimizer가 COST 비용이 제일 적은 FZJ221T 테이블을 먼저 Access 하는 실행계획을 세움으로써 전체 시간이 늘어나게 되었다.



- 해결 방법


앞에서 언급한대로 CHOOSE 에서 CBO 방식이 선택된 것은 SQL 에 있는 테이블 중 ANALYZE 된 테이블이 존재하기 때문입니다. DICTIONARY 정보를 참조하면 ANALYZE 된 테이블 정보를 확인할 수 있습니다.

따라서 실행 계획을 CBO(Cost Based Optimizer) 가 아닌 RBO(Rule Based Optimizer) 로 실행되도록 변경해야 합니다.


SQL 힌트를 사용해봅시다.



/*+RULE*/ 힌트를 사용하여 RBO 방식으로 실행계획이 작성되도록 변경하였습니다.


변경된 실행 계획을 확인해봅시다.



 힌트로 인하여 Optimizer 는 RULE로 변경되었습니다. 

실행계획은 FZJ222T 테이블과 FZJ222T_IDX2 로 Indexing을 하고 먼저 Access 하게 되었다.



출처 : http://hyeonstorage.tistory.com/

+ Recent posts