2014-02-28 3 views
5

집계 테이블에 변경 사항을 적용하는 문제를 고려하십시오. 새 행을 삽입해야하는 동안 존재하는 행을 업데이트해야합니다. 기본 테이블에 (한 번에 100K) 임시 테이블에MERGE 문의 SORT MERGE 조인 최적화

  • 삽입 모든 변경
  • MERGE 임시 테이블을

(결국 수백만 행의 100 단위에 도달) 다음과 같이 내 접근했다 (공상 아무것도) 다음 없기 때문에 (정렬 MERGE 힌트 사용) SQL 보인다 :

merge /*+ USE_MERGE(t s) */ 
into F_SCREEN_INSTANCE t 
using F_SCREEN_INSTANCE_BUF s 
    on (s.DAY_ID = t.DAY_ID and s.PARTIAL_ID = t.PARTIAL_ID) 
when matched then update set 
    t.ACTIVE_TIME_SUM = t.ACTIVE_TIME_SUM + s.ACTIVE_TIME_SUM, 
    t.IDLE_TIME_SUM = t.IDLE_TIME_SUM + s.IDLE_TIME_SUM 
when not matched then insert values (
    s.DAY_ID, s.PARTIAL_ID, s.ID, s.AGENT_USER_ID, s.COMPUTER_ID, s.RAW_APPLICATION_ID, s.APP_USER_ID, s.APPLICATION_ID, s.USER_ID, s.RAW_MODULE_ID, s.MODULE_ID, s.START_TIME, s.RAW_SCREEN_NAME, s.SCREEN_ID, s.SCREEN_TYPE, s.ACTIVE_TIME_SUM, s.IDLE_TIME_SUM) 

F_SCREEN_INSTANCE 테이블에 기본 키와 같은 (DAY_ID, PARTIAL_ID)있다 lso는 IOT (index organized table)입니다. 따라서 병합 조인을위한 이상적인 후보가됩니다. 행은 물리적으로 조회 키별로 정렬됩니다.

지금까지 그렇게 좋았습니다. 나는 벤치 마크를 시작했고 초기 시간은 한 병합으로 10 초를 보였다. 그러나 약 1 시간이 지난 후에는 병합 (tempdb) 사용량이 4 분이 걸렸습니다 (병합 당 4GB). 아래의 쿼리 계획은 표가 이상적으로 이미 정렬 되었더라도 병합 전에 F_SCREEN_INSTANCE이 다시 정렬되었음을 보여줍니다. 물론 테이블이 커짐에 따라 tempdb가 더 많이 필요하게되고 전체 접근 방식이 떨어져 버립니다.

Sort Merge Join

OK, 왜 재 분류 표? 병합 조인 구현의 한계로 변합니다 : the second table is always sorted.

색인이있는 경우 데이터베이스는 첫 번째 데이터를 으로 정렬하는 것을 피할 수 있습니다. 그러나 데이터베이스는 인덱스에 관계없이 항상 인 두 번째 데이터 세트를 정렬합니다.

O ... K, 그렇다면 기본 테이블을 먼저 만들고 버퍼를 두 번째로 만들 수 있습니까? 아니, 그럴 수도 없어. USE_MERGE 힌트에 테이블을 나열하는 방법에 관계없이 원본 테이블이 항상 첫 번째입니다.

마지막으로 여기 내 질문이 있습니다. 내가 놓친 것이 있습니까? 이 SORT MERGE 접근법을 작동시킬 수 있습니까? 좀 더 자세한 질문을 해결

현재 당신은 요청할 수 있습니다 :

  • 어떤 Oracle 버전을? 12c.
  • 해시 조인을 시도해 보셨습니까? 예, 예상대로 나쁩니다. 해시 테이블을 작성하려면 기본 테이블을 검사해야합니다. F_SCREEN_INSTANCE가 커짐에 따라 확장 할 수 없습니다.
  • LOOP JOIN을 사용해 보셨습니까? 그래, 나쁘다. 버퍼 테이블의 크기를 고려하면 F_SCREEN_INSTANCE에 대한 100K 조회가 부당하게 오래 걸립니다. 병합에는 약 3 분이 걸렸습니다.
  • 전체적으로 MERGE JOIN은 개념적으로 최상의 액세스 전략이지만 Oracle 구현은 대상 테이블을 다시 정렬하여 심각하게 장애가있는 것처럼 보입니다.
+0

별도의 '업데이트'와 '삽입'을 시도 했습니까? 내 경험에 의하면 그것은 때로는 병합 성명보다 훨씬 빠릅니다. –

+0

예. 그러나이 경우 배치 작업을 사용할 수 없습니다. 100K 삽입/업데이트 쌍이 MERGE보다 성능이 더 나쁩니다. 결국 나는 MERGE를위한 중첩 된 루프 힌트와 함께 갔다. –

답변

4

정렬 병합 외부 조인은 힌트에 관계없이 외부 조인 된 테이블을 항상 두 번째로 배치합니다. 추가 내부 조인을 추가하면 조인 순서를 제어 할 수 있으며 ROWID를 사용하여 다시 큰 테이블에 조인 할 수 있습니다. 두 개의 좋은 조인이 하나의 나쁜 조인보다 잘 작동되기를 바랍니다.

가정

이 답변 정렬 병합이 (가) 빠른 참여, 그리고 설명서는 두 번째 데이터 세트가 항상 정렬하는 것이 올바른지에 참여한다고 가정합니다. 데이터에 대한 더 많은 정보없이 이러한 가정을 테스트하는 것은 어려울 것입니다. 여기

샘플 스키마는 최적화가 500M 행과 100K 행이 생각하게하는 가짜 통계와 일부 유사한 테이블입니다. 문제점

원하는

create table F_SCREEN_INSTANCE(DAY_ID number, PARTIAL_ID number, ID number, AGENT_USER_ID number,COMPUTER_ID number, RAW_APPLICATION_ID number, APP_USER_ID number, APPLICATION_ID number, USER_ID number, RAW_MODULE_ID number,MODULE_ID number, START_TIME date, RAW_SCREEN_NAME varchar2(100), SCREEN_ID number, SCREEN_TYPE number, ACTIVE_TIME_SUM number, IDLE_TIME_SUM number, 
    constraint f_screen_instance_pk primary key (day_id, partial_id) 
) organization index; 

create table F_SCREEN_INSTANCE_BUF(DAY_ID number, PARTIAL_ID number, ID number, AGENT_USER_ID number,COMPUTER_ID number, RAW_APPLICATION_ID number, APP_USER_ID number,APPLICATION_ID number, USER_ID number, RAW_MODULE_ID number, MODULE_ID number, START_TIME date, RAW_SCREEN_NAME varchar2(100), SCREEN_ID number, SCREEN_TYPE number, ACTIVE_TIME_SUM number, IDLE_TIME_SUM number, 
    constraint f_screen_instance_buf_pk primary key (day_id, partial_id) 
); 

begin 
    dbms_stats.set_table_stats(user, 'F_SCREEN_INSTANCE', numrows => 500000000); 
    dbms_stats.set_table_stats(user, 'F_SCREEN_INSTANCE_BUF', numrows => 100000); 
end; 
/

가입 및 내부가 사용될 가입하면 순서가 선두 힌트 달성 될 수 조인. 더 작은 테이블 인 F_SCREEN_INSTANCE_BUF가 두 번째 테이블입니다.

explain plan for 
select /*+ use_merge(t s) leading(t s) */ * 
from f_screen_instance_buf s 
join f_screen_instance t 
    on (s.DAY_ID = t.DAY_ID and s.PARTIAL_ID = t.PARTIAL_ID); 

select * from table(dbms_xplan.display(format => '-predicate')); 

Plan hash value: 563239985 

----------------------------------------------------------------------------------------------------- 
| Id | Operation   | Name     | Rows | Bytes |TempSpc| Cost (%CPU)| Time  | 
----------------------------------------------------------------------------------------------------- 
| 0 | SELECT STATEMENT |      | 100K| 19M|  | 6898 (66)| 00:00:01 | 
| 1 | MERGE JOIN   |      | 100K| 19M|  | 6898 (66)| 00:00:01 | 
| 2 | INDEX FULL SCAN | F_SCREEN_INSTANCE_PK | 500M| 46G|  | 4504 (100)| 00:00:01 | 
| 3 | SORT JOIN   |      | 100K| 9765K| 26M| 2393 (1)| 00:00:01 | 
| 4 | TABLE ACCESS FULL| F_SCREEN_INSTANCE_BUF | 100K| 9765K|  | 34 (6)| 00:00:01 | 
----------------------------------------------------------------------------------------------------- 

왼쪽 결합으로 변경하면 LEADING 힌트가 작동하지 않습니다.

explain plan for 
select /*+ use_merge(t s) leading(t s) */ * 
from f_screen_instance_buf s 
left join f_screen_instance t 
    on (s.DAY_ID = t.DAY_ID and s.PARTIAL_ID = t.PARTIAL_ID); 

select * from table(dbms_xplan.display(format => '-predicate')); 

Plan hash value: 1472690071 

----------------------------------------------------------------------------------------------------------------- 
| Id | Operation     | Name      | Rows | Bytes |TempSpc| Cost (%CPU)| Time  | 
----------------------------------------------------------------------------------------------------------------- 
| 0 | SELECT STATEMENT    |       | 100K| 19M|  | 16M (1)| 00:10:34 | 
| 1 | MERGE JOIN OUTER   |       | 100K| 19M|  | 16M (1)| 00:10:34 | 
| 2 | TABLE ACCESS BY INDEX ROWID| F_SCREEN_INSTANCE_BUF | 100K| 9765K|  | 826 (0)| 00:00:01 | 
| 3 | INDEX FULL SCAN   | F_SCREEN_INSTANCE_BUF_PK | 100K|  |  | 26 (0)| 00:00:01 | 
| 4 | SORT JOIN     |       | 500M| 46G| 131G| 16M (1)| 00:10:34 | 
| 5 | INDEX FAST FULL SCAN  | F_SCREEN_INSTANCE_PK  | 500M| 46G|  | 2703 (100)| 00:00:01 | 
----------------------------------------------------------------------------------------------------------------- 

이 제한 사항은 내가 알 수있는 한 문서화되지 않았습니다. 나는 힌트의 전체 집합을보고 주위를 바꾼 DBMS_XPLAN+outline 설정을 사용해 보았습니다. 하지만 내가 한 일은 LEFT JOIN 버전의 조인 순서를 변경할 수 없습니다. 아마도 다른 사람이이 일을 할 수 있습니다.

select * from table(dbms_xplan.display(format => '-predicate +outline')); 

... 
Outline Data 
------------- 

    /*+ 
     BEGIN_OUTLINE_DATA 
     USE_MERGE(@"SEL$0E991E55" "T"@"SEL$1") 
     LEADING(@"SEL$0E991E55" "S"@"SEL$1" "T"@"SEL$1") 
     INDEX_FFS(@"SEL$0E991E55" "T"@"SEL$1" ("F_SCREEN_INSTANCE"."DAY_ID" "F_SCREEN_INSTANCE"."PARTIAL_ID")) 
     INDEX(@"SEL$0E991E55" "S"@"SEL$1" ("F_SCREEN_INSTANCE_BUF"."DAY_ID" 
       "F_SCREEN_INSTANCE_BUF"."PARTIAL_ID")) 
     OUTLINE(@"SEL$9EC647DD") 
     OUTLINE(@"SEL$2") 
     MERGE(@"SEL$9EC647DD") 
     OUTLINE_LEAF(@"SEL$0E991E55") 
     ALL_ROWS 
     DB_VERSION('12.1.0.1') 
     OPTIMIZER_FEATURES_ENABLE('12.1.0.1') 
     IGNORE_OPTIM_EMBEDDED_HINTS 
     END_OUTLINE_DATA 
    */ 

가능한 해결 방법

--#3: Join the large table to the smaller result set. This uses the largest table twice, 
--but the plan can use the ROWID for a very quick join. 
explain plan for 
merge into F_SCREEN_INSTANCE t 
using 
(
    --#2: Now get the missing rows with an outer join. Since the _BUF table is 
    --small I assume it does not make a big difference exactly how it it joind 
    --to the 100K result set. 
    --The hints NO_MERGE and NO_PUSH_PRED are required to keep the INNER_JOIN 
    --inline view intact. 
    select /*+ no_merge(inner_join) no_push_pred(inner_join) */ inner_join.* 
    from f_screen_instance_buf s 
    left join 
    (
     --#1: Get 100K rows efficiently with an inner join. 
     --Note that the ROWID is retrieved here. 
     select /*+ use_merge(t s) leading(t s) */ s.*, s.rowid s_rowid 
     from f_screen_instance_buf s 
     join f_screen_instance t 
      on (s.DAY_ID = t.DAY_ID and s.PARTIAL_ID = t.PARTIAL_ID) 
    ) inner_join 
     on (s.DAY_ID = inner_join.DAY_ID and s.PARTIAL_ID = inner_join.PARTIAL_ID) 
) s 
    on (s.s_rowid = t.rowid) 
when matched then update set 
    t.ACTIVE_TIME_SUM = t.ACTIVE_TIME_SUM + s.ACTIVE_TIME_SUM, 
    t.IDLE_TIME_SUM = t.IDLE_TIME_SUM + s.IDLE_TIME_SUM 
when not matched then insert values (
    s.DAY_ID, s.PARTIAL_ID, s.ID, s.AGENT_USER_ID, s.COMPUTER_ID, s.RAW_APPLICATION_ID, s.APP_USER_ID, s.APPLICATION_ID, s.USER_ID, s.RAW_MODULE_ID, s.MODULE_ID, s.START_TIME, s.RAW_SCREEN_NAME, s.SCREEN_ID, s.SCREEN_TYPE, s.ACTIVE_TIME_SUM, s.IDLE_TIME_SUM); 

그것은 꽤 아니지만, 적어도이 정렬 병합 조인을 먼저 큰 테이블 계획을 생성합니다.

select * from table(dbms_xplan.display); 

Plan hash value: 1086560566 

------------------------------------------------------------------------------------------------------------- 
| Id | Operation    | Name      | Rows | Bytes |TempSpc| Cost (%CPU)| Time  | 
------------------------------------------------------------------------------------------------------------- 
| 0 | MERGE STATEMENT   |       | 500G| 173T|  | 5355K (43)| 00:03:30 | 
| 1 | MERGE     | F_SCREEN_INSTANCE  |  |  |  |   |   | 
| 2 | VIEW     |       |  |  |  |   |   | 
|* 3 | HASH JOIN OUTER  |       | 500G| 179T| 29M| 5355K (43)| 00:03:30 | 
|* 4 |  HASH JOIN OUTER  |       | 100K| 28M| 3712K| 8663 (53)| 00:00:01 | 
| 5 |  INDEX FAST FULL SCAN| F_SCREEN_INSTANCE_BUF_PK | 100K| 2539K|  |  9 (0)| 00:00:01 | 
| 6 |  VIEW    |       | 100K| 25M|  | 6898 (66)| 00:00:01 | 
| 7 |  MERGE JOIN   |       | 100K| 12M|  | 6898 (66)| 00:00:01 | 
| 8 |  INDEX FULL SCAN | F_SCREEN_INSTANCE_PK  | 500M| 12G|  | 4504 (100)| 00:00:01 | 
|* 9 |  SORT JOIN   |       | 100K| 9765K| 26M| 2393 (1)| 00:00:01 | 
| 10 |   TABLE ACCESS FULL| F_SCREEN_INSTANCE_BUF | 100K| 9765K|  | 34 (6)| 00:00:01 | 
| 11 |  INDEX FAST FULL SCAN | F_SCREEN_INSTANCE_PK  | 500M| 46G|  | 2703 (100)| 00:00:01 | 
------------------------------------------------------------------------------------------------------------- 

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

    3 - access("INNER_JOIN"."S_ROWID"=("T".ROWID(+))) 
    4 - access("S"."PARTIAL_ID"="INNER_JOIN"."PARTIAL_ID"(+) AND 
       "S"."DAY_ID"="INNER_JOIN"."DAY_ID"(+)) 
    9 - access("S"."DAY_ID"="T"."DAY_ID" AND "S"."PARTIAL_ID"="T"."PARTIAL_ID") 
     filter("S"."PARTIAL_ID"="T"."PARTIAL_ID" AND "S"."DAY_ID"="T"."DAY_ID")