http://www.ssiso.net/cafe/club/club1/board1/content.php?board_code=oracle%7Cexora&idx=31520&club=oracle&cp=1&cb=1&search=&search_word
http://ukja.tistory.com/141
http://scidb.tistory.com/79
http://scidb.tistory.com/entry/SubQuery-Using-Method-1
http://scidb.tistory.com/entry/Hash-Join-Right-SemiAntiOuter-의-용도
http://scidb.tistory.com/search/Early%20Filter%20서브쿼리
분산질의 |
---|
{CODE:SQL} – Mgr : M 사이트 – Emp : E 사이트 |
SELECT mgr.name, emp.name
FROM mgr, emp
WHERE mgr.sal < emp.sal;
{CODE} 1 : M 사이트에서 SELECT sal FROM mgr 질의문을 수행한다 2 : 조인 속성만 추출( Projection) 하여 E 사이트로 전송하고 이를 mgrP라고 하자 3 : E 사이트에서 SELECT name, sal FROM emp WHERE sal > mgrP.sal를 수행한다. 4 : 3의 결과를 상대적으로 작은 양일 것이고 이를 M 사이트로 전송한다. |
그림 2-2-24 준비스크립트 |
---|
{CODE:SQL} |
11:45:32 SQL> SELECT * FROM V$VERSION WHERE ROWNUM <= 1;
BANNER
DROP TABLE TAB1 PURGE;
DROP TABLE TAB2 PURGE;
CREATE TABLE TAB1 AS
SELECT LEVEL KEY1
, '상품'||LEVEL COL1
, TRUNC( dbms_random.value( 1,7 ) ) * 500 + TRUNC( dbms_random.value( 1,3 ) ) * 750 COL2
FROM DUAL
CONNECT BY LEVEL <= 10000;
CREATE TABLE TAB2 AS
SELECT LEVEL KEY1
, TRUNC(dbms_random.value( 1, 10000 ) ) KEY2
, TRUNC( SYSDATE ) - 100 + CEIL( LEVEL/ 1000 ) COL1
, 'A' COL2
FROM DUAL
CONNECT BY LEVEL <= 100000;
CREATE INDEX KEY1_IDX ON TAB1( KEY1 );
CREATE INDEX COL1_IDX01 ON TAB2( COL1, COL2 );
EXEC DBMS_STATS.GATHER_TABLE_STATS( USER, 'TAB1' );
EXEC DBMS_STATS.GATHER_TABLE_STATS( USER, 'TAB2' );
SQL> DESC TAB2;
이름 널? 유형
SQL> SELECT /*+ gather_plan_statistics */ COUNT( DISTINCT KEY2 )
2 FROM TAB2 A
3 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
4 AND COL2 = 'A';
COUNT(DISTINCTKEY2)
경 과: 00:00:00.02
SQL> @xplan
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT GROUP BY | 1 | 1 | 1 | 00:00:00.01 | 15 | 83968 | 83968 | 73728 (0) | |
| FILTER | 1 | 2000 | 00:00:00.01 | 15 | |||||
3 | TABLE ACCESS BY INDEX ROWID | TAB2 | 1 | 2000 | 2000 | 00:00:00.01 | 15 | |||
| INDEX SKIP SCAN | COL1_IDX01 | 1 | 40 | 2000 | 00:00:00.01 | 8 |
Predicate Information (identified by operation id):
2 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
4 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL2"='A' AND "COL1"<=TRUNC(SYSDATE@!))
filter("COL2"='A')
{CODE} |
NO_HINT 옵티마이져가 서브쿼리를 조인으로 변형시킴 ( 그림 2-2-24 ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics */ COUNT( * )
2 FROM TAB1 A
3 WHERE KEY1 IN ( SELECT KEY2
4 FROM TAB2 B
5 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
6 AND COL2 = 'A' ) ;
COUNT(*)
경 과: 00:00:00.02
SQL> @xplan
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.01 | 41 | ||||
| FILTER | 1 | 1810 | 00:00:00.01 | 41 | |||||
| HASH JOIN RIGHT SEMI | 1 | 1830 | 1810 | 00:00:00.01 | 41 | 1517K | 1517K | 1277K (0) | |
4 | TABLE ACCESS BY INDEX ROWID | TAB2 | 1 | 2000 | 2000 | 00:00:00.01 | 15 | |||
| INDEX SKIP SCAN | COL1_IDX01 | 1 | 40 | 2000 | 00:00:00.01 | 8 | |||
6 | INDEX FAST FULL SCAN | KEY1_IDX | 1 | 10000 | 10000 | 00:00:00.01 | 26 |
Predicate Information (identified by operation id):
2 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
3 - access("KEY1"="KEY2")
5 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL2"='A' AND "COL1"<=TRUNC(SYSDATE@!))
filter("COL2"='A')
{CODE} * HASH JOIN SEMI ( Semi/Anti/Outer ) : Oracle 10g * Semi/Anti Join의 단점을 보안 * Semi/Anti Join의 단점.? Semi/Anti Join의 경우 서브쿼리는 항상 후행집합이 될수 밖에 없다. Hash Outer Join의 경우도 마찬가지로(+) 표시가 붙는 쪽의 집합은 항상 후행집합이 될 수 밖에 없다. 하지만 10g 부터 Hash join Right( Semi/Anti/Outer ) 기능이 나오게 되면서 서브쿼리 혹은 아우터 Join 되는쪽의 집합이 선행집합이 될 수 있다. * Right.? left 집합 대신에 right( 후행집합 )을 선행집합으로 하겠다는 뜻이다. |
HASH JOIN RIGHT SEMI을 힌트로 유도하는 법( 그림 2-2-24 ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics */ COUNT( * )
2 FROM TAB1 A
3 WHERE KEY1 IN ( SELECT /*+ HASH_SJ */ KEY2
4 FROM TAB2 B
5 WHERE B.COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
6 AND B.COL2 = 'A'
7 ) ;
COUNT(*)
경 과: 00:00:00.01
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.01 | 40 | ||||
| FILTER | 1 | 1810 | 00:00:00.01 | 40 | |||||
| HASH JOIN RIGHT SEMI | 1 | 1830 | 1810 | 00:00:00.01 | 40 | 1517K | 1517K | 1274K (0) | |
4 | TABLE ACCESS BY INDEX ROWID | TAB2 | 1 | 2000 | 2000 | 00:00:00.01 | 15 | |||
| INDEX SKIP SCAN | COL1_IDX01 | 1 | 40 | 2000 | 00:00:00.01 | 8 | |||
6 | INDEX FAST FULL SCAN | KEY1_IDX | 1 | 10000 | 10000 | 00:00:00.01 | 25 |
Predicate Information (identified by operation id):
2 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
3 - access("KEY1"="KEY2")
5 - access("B"."COL1">=TRUNC(SYSDATE@!-1) AND "B"."COL2"='A' AND "B"."COL1"<=TRUNC(SYSDATE@!))
filter("B"."COL2"='A')
{CODE} |
UNNEST 유도( 그림 2-2-24 ) : ㅠㅠ 쿼리 블럭명을 지정해야것군. ( 제공자 ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics */ COUNT( * )
2 FROM TAB1 A
3 WHERE KEY1 IN ( SELECT /*+ UNNEST */ KEY2
4 FROM TAB2 B
5 WHERE B.COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
6 AND B.COL2 = 'A'
7 ) ;
COUNT(*)
경 과: 00:00:00.01
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.01 | 41 | ||||
| FILTER | 1 | 1810 | 00:00:00.01 | 41 | |||||
| HASH JOIN RIGHT SEMI | 1 | 1830 | 1810 | 00:00:00.01 | 41 | 1517K | 1517K | 1552K (0) | |
4 | TABLE ACCESS BY INDEX ROWID | TAB2 | 1 | 2000 | 2000 | 00:00:00.01 | 15 | |||
| INDEX SKIP SCAN | COL1_IDX01 | 1 | 40 | 2000 | 00:00:00.01 | 8 | |||
6 | INDEX FAST FULL SCAN | KEY1_IDX | 1 | 10000 | 10000 | 00:00:00.01 | 26 |
Predicate Information (identified by operation id):
2 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
3 - access("KEY1"="KEY2")
5 - access("B"."COL1">=TRUNC(SYSDATE@!-1) AND "B"."COL2"='A' AND "B"."COL1"<=TRUNC(SYSDATE@!))
filter("B"."COL2"='A')
{CODE} |
UNNEST 유도( 그림 2-2-24 ) : 옵티마이져가 서브쿼리를 조인으로 변형시킴 ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics UNNEST( @SUB ) */ COUNT( * )
2 FROM TAB1 A
3 WHERE KEY1 IN ( SELECT /*+ QB_NAME( SUB )*/ KEY2
4 FROM TAB2 B
5 WHERE B.COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
6 AND B.COL2 = 'A'
7 ) ;
COUNT(*)
경 과: 00:00:00.01
SQL> @XPLAN
– KEY1_IDX 인덱스가 유니크인덱스가 아니라서 패스트 풀스켄으로 옵티마이져 선택해서 해쉬로 풀리는 듯.
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.01 | 41 | ||||
| FILTER | 1 | 1810 | 00:00:00.01 | 41 | |||||
| HASH JOIN | 1 | 2000 | 1810 | 00:00:00.01 | 41 | 1517K | 1517K | 1292K (0) | |
4 | SORT UNIQUE | 1 | 2000 | 1810 | 00:00:00.01 | 15 | 83968 | 83968 | 73728 (0) | |
5 | TABLE ACCESS BY INDEX ROWID | TAB2 | 1 | 2000 | 2000 | 00:00:00.01 | 15 | |||
| INDEX SKIP SCAN | COL1_IDX01 | 1 | 40 | 2000 | 00:00:00.01 | 8 | |||
7 | INDEX FAST FULL SCAN | KEY1_IDX | 1 | 10000 | 10000 | 00:00:00.01 | 26 |
Predicate Information (identified by operation id):
2 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
3 - access("KEY1"="KEY2")
6 - access("B"."COL1">=TRUNC(SYSDATE@!-1) AND "B"."COL2"='A' AND "B"."COL1"<=TRUNC(SYSDATE@!))
filter("B"."COL2"='A')
{CODE} * Unnesting 서브쿼리 : 옵티마이져가 서브 쿼리를 조인으로 변형시킴 * 서브쿼리를 조인으로 바꾸는 방식 ( 일반적으로 서브쿼리의 테이블이 Driving이된다. ) * 오라클은 서브쿼리를 인라뷰로 바꾸고 서브쿼리 집합이 DISTINCT하지 않을 경우 Sort Unique나 Hash Unique작업을 추가로 진행한다. * 힌트 : 유도 힌트 : /*+ unnest */ ( 서브쿼리에 사용하거나 메인 쿼리에서 쿼리 블럭 힌트( qb_name )를 사용하여야 한다. ) 방지 힌트 : /*+ no_unnest */ ( 서브쿼리에 사용 ) |
DROP INDEX KEY1_IDX ( KEY1_IDX 인덱스가 유니크인덱스가 아니라서 패스트 풀스켄으로 옵티마이져 선택해서 해쉬로 풀리는 듯.) |
---|
{CODE:SQL} |
DROP INDEX KEY1_IDX
CREATE UNIQUE INDEX KEY1_IDX ON TAB1( KEY1 )
{CODE} |
UNNEST 유도( 그림 2-2-24 ) ^^ |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics UNNEST( @SUB )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE KEY1 IN ( SELECT /*+ QB_NAME( SUB ) */ KEY2
4 FROM TAB2 B
5 WHERE B.COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
6 AND B.COL2 = 'A'
7 ) ;
COUNT(*)
경 과: 00:00:00.01
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.01 | 1827 | ||||
| FILTER | 1 | 1810 | 00:00:00.01 | 1827 | |||||
3 | NESTED LOOPS | 1 | 2000 | 1810 | 00:00:00.01 | 1827 | ||||
4 | SORT UNIQUE | 1 | 2000 | 1810 | 00:00:00.01 | 15 | 83968 | 83968 | 73728 (0) | |
5 | TABLE ACCESS BY INDEX ROWID | TAB2 | 1 | 2000 | 2000 | 00:00:00.01 | 15 | |||
| INDEX SKIP SCAN | COL1_IDX01 | 1 | 40 | 2000 | 00:00:00.01 | 8 | |||
| INDEX UNIQUE SCAN | KEY1_IDX | 1810 | 1 | 1810 | 00:00:00.01 | 1812 |
Predicate Information (identified by operation id):
2 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
6 - access("B"."COL1">=TRUNC(SYSDATE@!-1) AND "B"."COL2"='A' AND "B"."COL1"<=TRUNC(SYSDATE@!))
filter("B"."COL2"='A')
7 - access("KEY1"="KEY2")
{CODE} |
옵티마이져가 변형한쿼리 |
---|
{CODE:SQL} SQL> SELECT /*+ gather_plan_statistics LEADING( B A ) USE_NL( A B ) / COUNT() 2 FROM TAB1 A 3 , (SELECT DISTINCT KEY2 4 FROM TAB2 B 5 WHERE B.COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE ) 6 AND B.COL2 = 'A' ) B 7 WHERE A.KEY1 = B.KEY2; |
COUNT(*)
경 과: 00:00:00.01
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.01 | 1827 | |
2 | NESTED LOOPS | 1 | 1829 | 1810 | 00:00:00.01 | 1827 | |
3 | VIEW | 1 | 1829 | 1810 | 00:00:00.01 | 15 | |
4 | HASH UNIQUE | 1 | 1829 | 1810 | 00:00:00.01 | 15 | |
| FILTER | 1 | 2000 | 00:00:00.01 | 15 | ||
6 | TABLE ACCESS BY INDEX ROWID | TAB2 | 1 | 2000 | 2000 | 00:00:00.01 | 15 |
| INDEX SKIP SCAN | COL1_IDX01 | 1 | 40 | 2000 | 00:00:00.01 | 8 |
| INDEX UNIQUE SCAN | KEY1_IDX | 1810 | 1 | 1810 | 00:00:00.01 | 1812 |
Predicate Information (identified by operation id):
5 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
7 - access("B"."COL1">=TRUNC(SYSDATE@!-1) AND "B"."COL2"='A' AND "B"."COL1"<=TRUNC(SYSDATE@!))
filter("B"."COL2"='A')
8 - access("A"."KEY1"="B"."KEY2")
{CODE} | NO_UNNEST ( Filter 서브쿼리 : 쿼리 변형없음 ) |
---|---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics */ COUNT( * )
2 FROM TAB1 A
3 WHERE KEY1 IN ( SELECT /*+ NO_UNNEST */ KEY2
4 FROM TAB2 B
5 WHERE B.COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
6 AND B.COL2 = 'A'
7 ) ;
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:12.22 | 138K | |
| FILTER | 1 | 1801 | 00:00:13.86 | 138K | ||
3 | TABLE ACCESS FULL | TAB1 | 1 | 10000 | 10000 | 00:00:00.01 | 33 |
| FILTER | 10000 | 1801 | 00:00:12.21 | 138K | ||
| TABLE ACCESS BY INDEX ROWID | TAB2 | 10000 | 1 | 1801 | 00:00:12.18 | 138K |
| INDEX SKIP SCAN | COL1_IDX01 | 10000 | 46 | 18M | 00:00:00.09 | 74061 |
Predicate Information (identified by operation id):
2 - filter( IS NOT NULL)
4 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
5 - filter("KEY2"=:B1)
6 - access("B"."COL1">=TRUNC(SYSDATE@!-1) AND "B"."COL2"='A' AND
"B"."COL1"<=TRUNC(SYSDATE@!))
filter("B"."COL2"='A')
{CODE} * Filter 서브쿼리 : 쿼리변형이 없음 * 흔히 말하는 확인자 서브쿼리임.( 메인쿼리의 값을 제공받아 서브쿼리에서 체크하는 방식임 * 위의 1번 2번과 다르게 Plan에 메인 쿼리와 서브쿼리의 Join이 없고 Filter 로 나온다. * Filter SubQuery의 특징은 메인쿼리의 From 절에 있는 모든 테이블을 엑세스후에 가장 마지막에 서브쿼리가 실행된다. * 힌트: 특별한 힌트없음 다만 /*+ no_unnest */ 를 사용하여 SubQuery Flattening 을 방지하고 메인쿼리로부터 제공되는 서브쿼리의 조인컬럼에 인덱스가 생성되어 있으면됨. |
바로 위 쿼리 플렌 보충 쿼리들.. |
---|
{CODE:SQL} 바로 위 쿼리 플렌 보충 쿼리 1 SQL> SELECT /*+ gather_plan_statistics */ KEY1 2 FROM TAB1 A 3 WHERE KEY1 = 1 4 AND KEY1 IN ( SELECT /*+ NO_UNNEST */ KEY2 5 FROM TAB2 B 6 WHERE B.COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE ) 7 AND B.COL2 = 'A' 8 ) ; |
KEY1
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
| INDEX UNIQUE SCAN | KEY1_IDX | 1 | 1 | 1 | 00:00:00.01 | 15 |
| FILTER | 1 | 1 | 00:00:00.01 | 13 | ||
| TABLE ACCESS BY INDEX ROWID | TAB2 | 1 | 1 | 1 | 00:00:00.01 | 13 |
| INDEX SKIP SCAN | COL1_IDX01 | 1 | 46 | 1685 | 00:00:00.01 | 7 |
--바로 위 쿼리 플렌 보충 쿼리 2
SQL> SELECT /*+ gather_plan_statistics */ KEY1
2 FROM TAB1 A
3 WHERE KEY1 = 2
4 AND KEY1 IN ( SELECT /*+ NO_UNNEST */ KEY2
5 FROM TAB2 B
6 WHERE B.COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
7 AND B.COL2 = 'A'
8 ) ;
KEY1
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
| INDEX UNIQUE SCAN | KEY1_IDX | 1 | 1 | 1 | 00:00:00.01 | 15 |
| FILTER | 1 | 1 | 00:00:00.01 | 13 | ||
| TABLE ACCESS BY INDEX ROWID | TAB2 | 1 | 1 | 1 | 00:00:00.01 | 13 |
| INDEX SKIP SCAN | COL1_IDX01 | 1 | 46 | 1745 | 00:00:00.01 | 7 |
--바로 위 쿼리 플렌 보충 쿼리 3
SQL> SELECT /*+ gather_plan_statistics */ KEY1
2 FROM TAB1 A
3 WHERE KEY1 IN ( 1, 2 )
4 AND KEY1 IN ( SELECT /*+ NO_UNNEST */ KEY2
5 FROM TAB2 B
6 WHERE B.COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
7 AND B.COL2 = 'A'
8 ) ;
KEY1
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
1 | INLIST ITERATOR | 1 | 2 | 00:00:00.01 | 31 | ||
| INDEX RANGE SCAN | KEY1_IDX | 2 | 1 | 2 | 00:00:00.01 | 31 |
| FILTER | 2 | 2 | 00:00:00.01 | 26 | ||
| TABLE ACCESS BY INDEX ROWID | TAB2 | 2 | 1 | 2 | 00:00:00.01 | 26 |
| INDEX SKIP SCAN | COL1_IDX01 | 2 | 46 | 3430 | 00:00:00.01 | 14 |
{CODE} | 다른 방법은 없는가.? Driving Semi Join |
---|---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics SEMIJOIN_DRIVER(@SUB) */ COUNT( * )
2 FROM TAB1 A
3 WHERE KEY1 IN ( SELECT /*+ QB_NAME( SUB )* */ KEY2
4 FROM TAB2 B
5 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
6 AND COL2 = 'A' ) ;
COUNT(*)
경 과: 00:00:00.02
SQL> @xplan
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.02 | 2020 | ||||
2 | BITMAP CONVERSION COUNT | 1 | 2000 | 1 | 00:00:00.02 | 2020 | ||||
3 | BITMAP MERGE | 1 | 1 | 00:00:00.02 | 2020 | 1024K | 512K | 416K (0) | ||
4 | BITMAP KEY ITERATION | 1 | 2000 | 00:00:00.02 | 2020 | |||||
| FILTER | 1 | 2000 | 00:00:00.01 | 15 | |||||
6 | TABLE ACCESS BY INDEX ROWID | TAB2 | 1 | 2000 | 2000 | 00:00:00.01 | 15 | |||
| INDEX SKIP SCAN | COL1_IDX01 | 1 | 40 | 2000 | 00:00:00.01 | 8 | |||
8 | BITMAP CONVERSION FROM ROWIDS | 2000 | 2000 | 00:00:00.01 | 2005 | |||||
| INDEX RANGE SCAN | KEY1_IDX | 2000 | 2000 | 00:00:00.01 | 2005 |
Predicate Information (identified by operation id):
5 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
7 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL2"='A' AND "COL1"<=TRUNC(SYSDATE@!))
filter("COL2"='A')
9 - access("KEY1"="KEY2")
{CODE} * Semi Join의 변형된 형태로서 Hash Join의 장점( 조인건수의 최소화 )과 Nested Loop Join의 장점( 성행테이블의 상수화 되어 후행 테이블에서 인덱스를 효율적으로 사용)을 합친 개념이다. * Driving Semi Join이라는 Title 자체의 의미대로 Semi Join이 Driving집합이 되는것은 당연하다. * BITMAP KEY ITERATION : INLIST ITERATOR Operation 같다고 볼수있다. * _b_tree_bitmap_plans = true * Oracle 9i이상 * 오라클 정식 힌트가 아님( ㄷㄷ ) |
그림 2-2-25 준비스크립트 |
---|
{CODE:SQL} |
DROP TABLE 사원 PURGE;
DROP TABLE 가족 PURGE;
CREATE TABLE 사원 AS
SELECT LEVEL AS 사번
, '아무개'||LEVEL AS 성명
, TRUNC( dbms_random.value( 1,7 ) ) 직급
, TRUNC( SYSDATE ) - 100 + CEIL( LEVEL/ 10000 ) 입사일
, DECODE( TRUNC( dbms_random.value( 1,32 ) ), 1, 'DB1팀', 2, 'DB2팀', 3, 'DB3팀'
, 4, '시스템1팀', 5, '시스템2팀', 6, '시스템3팀'
, 7, '경리1과', 8, '경리2과', 9, '경리3과', 10, '경리과', 11, '경리4과'
, 12, '개발1팀', 13, '개발2팀', 14, '개발3팀', 15, '개발4팀', 16, '개발5팀', 17, '개발6팀'
, 18, 'MD1팀', 19, 'MD2팀', 20, 'MD3팀', 21, 'MD4팀'
, 22, '디자인1팀', 23, '디자인2팀', 24, '디자인3팀', 25, '디자인4팀'
, 26, '멀티미디어1팀', 27, '멀티미디어2팀', 28, '멀티미디어3팀'
, 29, '모바일1팀'
, 30, '웹모바일팀'
, 31, '마케팅팀'
, 32, '기획팀' ) 부서
FROM DUAL
CONNECT BY LEVEL <= 1000000
CREATE TABLE 가족 AS
SELECT 사번, LV 가족번호, '아무개'||LV 성명
, '19'|| TRUNC( dbms_random.value( 50,110 ) )|| TO_CHAR( TRUNC( dbms_random.value( 1,12 ) ), 'FM09' )|| TO_CHAR( TRUNC( dbms_random.value( 1,28 ) ), 'FM09' ) 생년월일
FROM 사원 A
, (SELECT LEVEL LV FROM DUAL CONNECT BY LEVEL <= 10 ) B
WHERE A.직급 >= B.LV
CREATE INDEX 부서_INDEX ON 사원 ( 부서, 사번 )
CREATE UNIQUE INDEX PK_INDEX ON 가족 ( 사번, 가족번호 )
SQL> SELECT 부서, COUNT(*)
2 FROM 사원
3 GROUP BY 부서
4 ;
부서 COUNT(*)
부서 COUNT(*)
부서 COUNT(*)
32 개의 행이 선택되었습니다.
경 과: 00:00:00.38
SQL>
{CODE} | 그림 2-2-25 실행 |
---|---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics / COUNT()
2 FROM 사원 X
3 WHERE 부서 = '경리과'
4 AND 사번 IN (SELECT 사번
5 FROM 가족 Y
6 WHERE Y.사번 = X.사번 --(a)
7 AND Y.생년월일 < '20051001' );
COUNT(*)
SQL> @xplan
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.69 | 14633 | 9632 | ||||
| HASH JOIN SEMI | 1 | 26199 | 30828 | 00:00:00.40 | 14633 | 9632 | 1935K | 1935K | 2195K (0) | |
| INDEX RANGE SCAN | 부서_INDE | 1 | 26199 | 31346 | 00:00:00.01 | 103 | 0 | |||
| TABLE ACCESS FULL | 가족 | 1 | 2901K | 3175K | 00:00:00.02 | 14530 | 9632 |
Predicate Information (identified by operation id):
2 - access("사번"="사번")
3 - access("부서"='경리과')
4 - filter("Y"."생년월일"<'20051001')
{CODE} |
너 왜 인덱스 안타냐?? 더 고비용이냐?? ㅠ( 음.. NL 더 고비용이군 ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics / COUNT()
2 FROM 사원 X
3 WHERE 부서 = '경리과'
4 AND 사번 IN (SELECT /*+ INDEX( Y PK_INDEX ) */ 사번
5 FROM 가족 Y
6 WHERE Y.사번 = X.사번 --(a)
7 AND Y.생년월일 < '20051001' );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.35 | 96727 | 17 | |
2 | NESTED LOOPS SEMI | 1 | 26199 | 30828 | 00:00:00.34 | 96727 | 17 | |
| INDEX RANGE SCAN | 부서_INDE | 1 | 26199 | 31346 | 00:00:00.01 | 103 | 0 |
| TABLE ACCESS BY INDEX ROWID | 가족 | 31346 | 2901K | 30828 | 00:00:00.31 | 96624 | 17 |
| INDEX RANGE SCAN | PK_INDEX | 31346 | 1 | 33915 | 00:00:00.15 | 62709 | 15 |
Predicate Information (identified by operation id):
3 - access("부서"='경리과')
4 - filter("Y"."생년월일"<'20051001')
5 - access("사번"="사번")
{CODE} * 서브쿼리 내에 메인 쿼리의 컬럼이 존재 한는것은 곧 종속성을 의미한다. ( 이 서브쿼리는 논리적으로 절대 먼저 수행 될 수 없도록 종속되었다는 것이다. * 위 쿼리는 잘못된 SQL이다. 왜? (a)라인을 삭제하면 연결이 안 될 것이라고 생각 |
음.. (a)을 배재함.. ( 으흠.. ) |
---|
{CODE:SQL} SQL> SELECT /*+ gather_plan_statistics LEADING( @SUB ) / COUNT() 2 FROM 사원 X 3 WHERE 부서 = '경리과' 4 AND 사번 IN (SELECT /*+ QB_NAME( SUB ) */ 사번 5 FROM 가족 Y 6 WHERE 1 = 1 --Y.사번 = X.사번 --(a) 7 AND Y.생년월일 < '20051001' ); |
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.61 | 14633 | ||||
| HASH JOIN SEMI | 1 | 26199 | 30828 | 00:00:00.35 | 14633 | 1935K | 1935K | 2187K (0) | |
| INDEX RANGE SCAN | 부서_INDE | 1 | 26199 | 31346 | 00:00:00.01 | 103 | |||
| TABLE ACCESS FULL | 가족 | 1 | 2901K | 3175K | 00:00:00.01 | 14530 |
Predicate Information (identified by operation id):
2 - access("사번"="사번")
3 - access("부서"='경리과')
4 - filter("Y"."생년월일"<'20051001')
{CODE} | 강제로 해쉬 조인 라이트 세미는 못푸는 것인가.?? |
---|---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics SWAP_JOIN_INPUTS(@SUB Y) / COUNT()
2 FROM 사원 X
3 WHERE 부서 = '경리과'
4 AND 사번 IN (SELECT /*+ QB_NAME( SUB ) */ 사번
5 FROM 가족 Y
6 WHERE 1 = 1 --Y.사번 = X.사번 --(a)
7 AND Y.생년월일 < '20051001' );
COUNT(*)
SQL> @xplan
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | Writes | OMem | 1Mem | Used-Mem | Used-Tmp |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:01:38.58 | 14633 | 12586 | 4371 | |||||
| HASH JOIN RIGHT SEMI | 1 | 26199 | 30828 | 00:01:38.20 | 14633 | 12586 | 4371 | 84M | 7739K | 111M (1) | 39936 | |
| TABLE ACCESS FULL | 가족 | 1 | 2901K | 3175K | 00:01:22.56 | 14530 | 8215 | 0 | ||||
| INDEX RANGE SCAN | 부서_INDE | 1 | 26199 | 31346 | 00:00:00.01 | 103 | 0 | 0 |
Predicate Information (identified by operation id):
2 - access("사번"="사번")
3 - filter("Y"."생년월일"<'20051001')
4 - access("부서"='경리과')
{CODE} |
( 참고 ) Early Filter 서브쿼리 제공자 쿼리로 보이지만 확인자 쿼리라고함. |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics / COUNT()
2 FROM 사원 X
3 WHERE 부서 = '경리과'
4 AND 사번 IN (SELECT /*+ NO_UNNEST PUSH_SUBQ */ 사번
5 FROM 가족 Y
6 WHERE Y.사번 = X.사번 --(a)
7 AND Y.생년월일 < '20051001' );
COUNT(*)
경 과: 00:00:00.37
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.35 | 128K | |
| INDEX RANGE SCAN | 부서_INDE | 1 | 1310 | 30828 | 00:00:00.34 | 128K |
| TABLE ACCESS BY INDEX ROWID | 가족 | 31346 | 29019 | 30828 | 00:00:00.30 | 127K |
| INDEX RANGE SCAN | PK_INDEX | 31346 | 12806 | 33915 | 00:00:00.14 | 94053 |
Predicate Information (identified by operation id):
2 - access("부서"='경리과')
filter( IS NOT NULL)
3 - filter("Y"."생년월일"<'20051001')
4 - access("사번"=:B1)
{CODE} * Early Filter 서브쿼리 : 쿼리변형 없음 * Filter SubQuery와 같은 방식이지만 서브 쿼리를 최대한먼저 실행하여 데이터를 걸러낸다. * 힌트 : 메인 쿼리에 push_subq 힌트사용 ( 10g 이후부터는 서브쿼리에 힌트사용해야함 ) * 주의사항 : 많은 튜닝책에서 "Push_subq 힌트를 사용하면 제공자 서브쿼리를 유도한다" 라고 되어 있으나 이는 잘못된 것이다. push_subq힌트를 사용하면 확인자 서브쿼리( Filter 서브쿼리)를 유도하지만 최대한 먼저 수행한다. * Early Filter 서브쿼리 효과는 최소 메인 절에 두가지 테이블이 존재 해야만 눈으로 확인 할수있다. * http://scidb.tistory.com/entry/SubQuery-Using-Method-1 * http://scidb.tistory.com/entry/Using-sub-query-method-Filter-Access-sub-Query |
그림 2-2-25 ( 위 쿼리가 정말 Early Filter 서브쿼리인가? ( 솔까 잘모르지만.. 테스트 결과 맞는거 같다. ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics / COUNT()
2 FROM 사원 X
3 WHERE 부서 = '경리과'
4 AND 사번 IN (SELECT /*+ NO_UNNEST NO_PUSH_SUBQ */ 사번
5 FROM 가족 Y
6 WHERE Y.사번 = X.사번 --(a)
7 AND Y.생년월일 < '20051001' );
COUNT(*)
경 과: 00:00:00.37
SQL> @XPLAN
Plan hash value: 2622244588
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.36 | 128K | |
| FILTER | 1 | 30828 | 00:00:00.37 | 128K | ||
| INDEX RANGE SCAN | 부서_INDE | 1 | 26199 | 31346 | 00:00:00.01 | 103 |
| TABLE ACCESS BY INDEX ROWID | 가족 | 31346 | 29019 | 30828 | 00:00:00.30 | 127K |
| INDEX RANGE SCAN | PK_INDEX | 31346 | 12806 | 33915 | 00:00:00.14 | 94053 |
Predicate Information (identified by operation id):
2 - filter( IS NOT NULL)
3 - access("부서"='경리과')
4 - filter("Y"."생년월일"<'20051001')
5 - access("사번"=:B1)
{CODE} |
준비 스크립트 |
---|
{CODE:SQL} |
CREATE TABLE 고객 AS
SELECT LEVEL 고객번호
, LEVEL 납입자
FROM DUAL
CONNECT BY LEVEL <= 1000000; --100만건
CREATE TABLE 청구 AS
SELECT B.고객번호
, A.청구년월
, 0 입금액
FROM (SELECT '2005'||TO_CHAR( LEVEL , 'FM09' ) 청구년월
FROM DUAL
CONNECT BY LEVEL <= 12 ) A
, ( SELECT * FROM 고객 ) B --1200만건
CREATE INDEX 고객_INDEX01 ON 고객( 납입자 );
CREATE INDEX 청구_INDEX01 ON 청구( 고객번호, 청구년월 );
CREATE INDEX 청구_INDEX02 ON 청구( 청구년월, 고객번호 );
SQL> SELECT *
2 FROM 고객
3 WHERE 납입자 = 1000;
고객번호 납입자
{CODE} |
오티마이져가 UNNEST 선택함 ( 책하곤 틀리게 잘풀린다. ㅠㅠ ) |
---|
{CODE:SQL} |
SQL> UPDATE /*+ gather_plan_statistics */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT 고객번호
5 FROM 고객 Y
6 WHERE 납입자 = 1000
7 AND Y.고객번호 = X.고객번호 );
1 행이 갱신되었습니다.
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem |
1 | UPDATE | 청구 | 1 | 0 | 00:00:00.04 | 10 | 3 | ||||
2 | NESTED LOOPS | 1 | 1 | 1 | 00:00:00.02 | 7 | 2 | ||||
3 | SORT UNIQUE | 1 | 1 | 1 | 00:00:00.01 | 4 | 0 | 9216 | 9216 | 8192 (0) | |
4 | TABLE ACCESS BY INDEX ROWID | 고객 | 1 | 1 | 1 | 00:00:00.01 | 4 | 0 | |||
| INDEX RANGE SCAN | 고객_INDEX0 | 1 | 1 | 1 | 00:00:00.01 | 3 | 0 | |||
| INDEX RANGE SCAN | 청구_INDEX0 | 1 | 1 | 1 | 00:00:00.02 | 3 | 2 |
Predicate Information (identified by operation id):
5 - access("납입자"=1000)
6 - access("고객번호"="고객번호" AND "청구년월"='200503')
{CODE} |
NO_UNNEST ( 청구_INDEX02 ) |
---|
{CODE:SQL} |
SQL> UPDATE /*+ gather_plan_statistics */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ NO_UNNEST */ 고객번호
5 FROM 고객 Y
6 WHERE 납입자 = 1000
7 AND Y.고객번호 = X.고객번호 );
1 행이 갱신되었습니다.
SQL> @xplan
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
1 | UPDATE | 청구 | 1 | 0 | 00:00:13.77 | 4003K | 3196 | |
| INDEX RANGE SCAN | 청구_INDEX0 | 1 | 67149 | 1 | 00:00:13.77 | 4003K | 3196 |
| TABLE ACCESS BY INDEX ROWID | 고객 | 1000K | 1 | 1 | 00:00:06.59 | 4000K | 0 |
| INDEX RANGE SCAN | 고객_INDEX0 | 1000K | 1 | 1000K | 00:00:04.15 | 3000K | 0 |
Predicate Information (identified by operation id):
2 - access("청구년월"='200503')
filter( IS NOT NULL)
3 - filter("고객번호"=:B1)
4 - access("납입자"=1000)
{CODE} |
NO_UNNEST ( 청구_INDEX01 ) |
---|
{CODE:SQL} |
SQL> UPDATE /*+ gather_plan_statistics */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ NO_UNNEST */ 고객번호
5 FROM 고객 Y
6 WHERE 납입자 = 1000
7 AND Y.고객번호 = X.고객번호 );
1 행이 갱신되었습니다.
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
1 | UPDATE | 청구 | 1 | 0 | 00:00:17.11 | 4031K | 31441 | |
| FILTER | 1 | 1 | 00:00:17.11 | 4031K | 31441 | ||
| TABLE ACCESS FULL | 청구 | 1 | 575K | 1000K | 00:00:01.30 | 31508 | 31441 |
| TABLE ACCESS BY INDEX ROWID | 고객 | 1000K | 1 | 1 | 00:00:07.22 | 4000K | 0 |
| INDEX RANGE SCAN | 고객_INDEX0 | 1000K | 1 | 1000K | 00:00:04.46 | 3000K | 0 |
Predicate Information (identified by operation id):
2 - filter( IS NOT NULL)
3 - filter("청구년월"='200503')
4 - filter("고객번호"=:B1)
5 - access("납입자"=1000)
{CODE} |
불필요한 연결고리 제거 |
---|
{CODE:SQL} |
SQL> UPDATE /*+ gather_plan_statistics */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ */ 고객번호
5 FROM 고객 Y
6 WHERE 납입자 = 1000);
1 행이 갱신되었습니다.
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | UPDATE | 청구 | 1 | 0 | 00:00:00.01 | 10 | ||||
2 | NESTED LOOPS | 1 | 1 | 1 | 00:00:00.01 | 7 | ||||
3 | SORT UNIQUE | 1 | 1 | 1 | 00:00:00.01 | 4 | 9216 | 9216 | 8192 (0) | |
4 | TABLE ACCESS BY INDEX ROWID | 고객 | 1 | 1 | 1 | 00:00:00.01 | 4 | |||
| INDEX RANGE SCAN | 고객_INDEX0 | 1 | 1 | 1 | 00:00:00.01 | 3 | |||
| INDEX RANGE SCAN | 청구_INDEX0 | 1 | 1 | 1 | 00:00:00.01 | 3 |
Predicate Information (identified by operation id):
5 - access("납입자"=1000)
6 - access("고객번호"="고객번호" AND "청구년월"='200503')
{CODE} |
NESTED LOOPS SEMI |
---|
{CODE:SQL} |
SQL> UPDATE /*+ gather_plan_statistics INDEX( X 청구_INDEX01 )*/ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ NL_SJ*/ 고객번호
5 FROM 고객 Y
6 WHERE 납입자 = 1000);
1 행이 갱신되었습니다.
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
1 | UPDATE | 청구 | 1 | 0 | 00:00:24.92 | 3031K | 31246 | |
2 | NESTED LOOPS SEMI | 1 | 1 | 1 | 00:00:24.92 | 3031K | 31246 | |
| TABLE ACCESS FULL | 청구 | 1 | 1342K | 1000K | 00:00:09.04 | 31508 | 31246 |
| TABLE ACCESS BY INDEX ROWID | 고객 | 1000K | 1 | 1 | 00:00:08.62 | 3000K | 0 |
| INDEX RANGE SCAN | 고객_INDEX0 | 1000K | 1 | 1000K | 00:00:03.62 | 2000K | 0 |
Predicate Information (identified by operation id):
3 - filter("청구년월"='200503')
4 - filter("고객번호"="고객번호")
5 - access("납입자"=1000)
-- 청구_INDEX02
SQL> UPDATE /*+ gather_plan_statistics INDEX( X 청구_INDEX02 )*/ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ NL_SJ*/ 고객번호
5 FROM 고객 Y
6 WHERE 납입자 = 1000);
1 행이 갱신되었습니다.
SQL>
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
1 | UPDATE | 청구 | 1 | 0 | 00:00:09.18 | 3003K | |
2 | NESTED LOOPS SEMI | 1 | 1 | 1 | 00:00:09.18 | 3003K | |
| INDEX RANGE SCAN | 청구_INDEX0 | 1 | 1342K | 1000K | 00:00:00.01 | 3207 |
| TABLE ACCESS BY INDEX ROWID | 고객 | 1000K | 1 | 1 | 00:00:08.27 | 3000K |
| INDEX RANGE SCAN | 고객_INDEX0 | 1000K | 1 | 1000K | 00:00:03.38 | 2000K |
Predicate Information (identified by operation id):
3 - access("청구년월"='200503')
4 - filter("고객번호"="고객번호")
5 - access("납입자"=1000)
{CODE} |
그럼 청구_INDEX02을 사용해서 HASH JOIN SEMI로 풀면.? ( HASH JOIN SEMI 를 원했는데 오티마이져가 HASH JOIN RIGHT SEMI로 풀었다 ) |
---|
{CODE:SQL} |
SQL> UPDATE /*+ gather_plan_statistics INDEX( X 청구_INDEX02 )*/ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ HASH_SJ*/ 고객번호
5 FROM 고객 Y
6 WHERE 납입자 = 1000);
1 행이 갱신되었습니다.
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | UPDATE | 청구 | 1 | 0 | 00:00:00.26 | 3214 | ||||||||||||||||
| HASH JOIN RIGHT SEMI | 1 | 1 | 1 | 00:00:00.26 | 3211 | 1035K | 1035K | 324K (0) | <-- | 3 | TABLE ACCESS BY INDEX ROWID | 고객 | 1 | 1 | 1 | 00:00:00.01 | 4 | ||||
| INDEX RANGE SCAN | 고객_INDEX0 | 1 | 1 | 1 | 00:00:00.01 | 3 | |||||||||||||||
| INDEX RANGE SCAN | 청구_INDEX0 | 1 | 1342K | 1000K | 00:00:00.01 | 3207 |
Predicate Information (identified by operation id):
2 - access("고객번호"="고객번호")
4 - access("납입자"=1000)
5 - access("청구년월"='200503')
{CODE} |
그럼 청구_INDEX02을 사용해서 HASH JOIN SEMI로 풀면.? |
---|
{CODE:SQL} |
SQL> UPDATE /*+ gather_plan_statistics INDEX( X 청구_INDEX02 ) LEADING( X) */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ HASH_SJ*/ 고객번호
5 FROM 고객 Y
6 WHERE 납입자 = 1000);
1 행이 갱신되었습니다.
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | Writes | OMem | 1Mem | Used-Mem | Used-Tmp |
1 | UPDATE | 청구 | 1 | 0 | 00:00:05.95 | 3212 | 3503 | 3503 | ||||||||||||||||||||
| HASH JOIN SEMI | 1 | 1 | 1 | 00:00:05.95 | 3211 | 3503 | 3503 | 50M | 5133K | 69M (0) | 36864 | <-- |
| INDEX RANGE SCAN | 청구_INDEX0 | 1 | 1342K | 1000K | 00:00:00.01 | 3207 | 0 | 0 | |||||
4 | TABLE ACCESS BY INDEX ROWID | 고객 | 1 | 1 | 1 | 00:00:00.01 | 4 | 0 | 0 | |||||||||||||||||||
| INDEX RANGE SCAN | 고객_INDEX0 | 1 | 1 | 1 | 00:00:00.01 | 3 | 0 | 0 |
Predicate Information (identified by operation id):
2 - access("고객번호"="고객번호")
3 - access("청구년월"='200503')
5 - access("납입자"=1000)
{CODE} |
그럼 청구_INDEX02을 사용해서 MERGE JOIN SEMI로 풀면.? |
---|
{CODE:SQL} |
SQL> UPDATE /*+ gather_plan_statistics INDEX( X 청구_INDEX02 )*/ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ MERGE_SJ*/ 고객번호
5 FROM 고객 Y
6 WHERE 납입자 = 1000);
1 행이 갱신되었습니다.
SQL> @XPLAN
Plan hash value: 2507689243
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | UPDATE | 청구 | 1 | 0 | 00:00:00.01 | 11 | ||||
2 | MERGE JOIN SEMI | 1 | 1 | 1 | 00:00:00.01 | 10 | ||||
| INDEX RANGE SCAN | 청구_INDEX0 | 1 | 1342K | 1001 | 00:00:00.01 | 6 | |||
| SORT UNIQUE | 1001 | 1 | 1 | 00:00:00.01 | 4 | 73728 | 73728 | ||
5 | TABLE ACCESS BY INDEX ROWID | 고객 | 1 | 1 | 1 | 00:00:00.01 | 4 | |||
| INDEX RANGE SCAN | 고객_INDEX0 | 1 | 1 | 1 | 00:00:00.01 | 3 |
Predicate Information (identified by operation id):
3 - access("청구년월"='200503')
4 - access("고객번호"="고객번호")
filter("고객번호"="고객번호")
6 - access("납입자"=1000)
{CODE} |
그럼 청구_INDEX02을 사용해서 고객 테이블을 선두테이블로 먼저 드라이빙 해서 MERGE JOIN SEMI로 풀면.?( 별짓 다해두 안됨 ) |
---|
{CODE:SQL} |
– 1
SQL> UPDATE /*+ gather_plan_statistics SWAP_JOIN_INPUTS(@SUB Y) INDEX( X@MAIN 청구_INDEX02 ) MERGE_SJ( @SUB ) QB_NAME( MAIN ) */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ QB_NAME( SUB )*/ 고객번호
5 FROM 고객 Y
6 WHERE 납입자 = 1000 );
1 행이 갱신되었습니다.
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | UPDATE | 청구 | 1 | 0 | 00:00:00.01 | 11 | ||||
2 | MERGE JOIN SEMI | 1 | 1 | 1 | 00:00:00.01 | 10 | ||||
| INDEX RANGE SCAN | 청구_INDEX0 | 1 | 1342K | 1001 | 00:00:00.01 | 6 | |||
| SORT UNIQUE | 1001 | 1 | 1 | 00:00:00.01 | 4 | 73728 | 73728 | ||
5 | TABLE ACCESS BY INDEX ROWID | 고객 | 1 | 1 | 1 | 00:00:00.01 | 4 | |||
| INDEX RANGE SCAN | 고객_INDEX0 | 1 | 1 | 1 | 00:00:00.01 | 3 |
Predicate Information (identified by operation id):
3 - access("청구년월"='200503')
4 - access("고객번호"="고객번호")
filter("고객번호"="고객번호")
6 - access("납입자"=1000)
– 2
SQL> UPDATE /*+ gather_plan_statistics SWAP_JOIN_INPUTS(@SUB Y) INDEX( X@MAIN 청구_INDEX02 ) HASH_SJ( @SUB ) QB_NAME( MAIN ) */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ QB_NAME( SUB )*/ 고객번호
5 FROM 고객 Y
6 WHERE 납입자 = 1000 );
1 행이 갱신되었습니다.
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem |
1 | UPDATE | 청구 | 1 | 0 | 00:00:01.43 | 3212 | 5 | ||||
| HASH JOIN RIGHT SEMI | 1 | 1 | 1 | 00:00:01.43 | 3211 | 5 | 1035K | 1035K | 310K (0) | |
3 | TABLE ACCESS BY INDEX ROWID | 고객 | 1 | 1 | 1 | 00:00:00.01 | 4 | 0 | |||
| INDEX RANGE SCAN | 고객_INDEX0 | 1 | 1 | 1 | 00:00:00.01 | 3 | 0 | |||
| INDEX RANGE SCAN | 청구_INDEX0 | 1 | 1342K | 1000K | 00:00:00.01 | 3207 | 5 |
Predicate Information (identified by operation id):
2 - access("고객번호"="고객번호")
4 - access("납입자"=1000)
5 - access("청구년월"='200503')
{CODE} |
번외편 SEMIJOIN_DRIVER( 안풀림 ) |
---|
{CODE:SQL} |
SQL> UPDATE /*+ gather_plan_statistics SEMIJOIN_DRIVER(@SUB ) */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ QB_NAME( SUB ) */ 고객번호
5 FROM 고객 Y
6 WHERE 납입자 = 1000);
1 행이 갱신되었습니다.
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | UPDATE | 청구 | 1 | 0 | 00:00:00.91 | 10 | ||||
2 | NESTED LOOPS | 1 | 1 | 1 | 00:00:00.90 | 7 | ||||
3 | SORT UNIQUE | 1 | 1 | 1 | 00:00:00.86 | 4 | 9216 | 9216 | 8192 (0) | |
4 | TABLE ACCESS BY INDEX ROWID | 고객 | 1 | 1 | 1 | 00:00:00.86 | 4 | |||
| INDEX RANGE SCAN | 고객_INDEX0 | 1 | 1 | 1 | 00:00:00.01 | 3 | |||
| INDEX RANGE SCAN | 청구_INDEX0 | 1 | 1 | 1 | 00:00:00.04 | 3 |
Predicate Information (identified by operation id):
5 - access("납입자"=1000)
6 - access("고객번호"="고객번호" AND "청구년월"='200503')
{CODE} |
번외편 SWAP_JOIN_INPUTS |
---|
{CODE:SQL} SQL> UPDATE /*+ gather_plan_statistics SWAP_JOIN_INPUTS(@SUB Y) QB_NAME( MAIN ) */ 청구 X 2 SET 입금액 = NVL( 입금액, 0 ) + 50000 3 WHERE 청구년월 = '200503' 4 AND 고객번호 IN (SELECT /*+ QB_NAME( SUB ) */ 고객번호 5 FROM 고객 Y 6 WHERE 납입자 = 1000); |
1 행이 갱신되었습니다.
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem |
1 | UPDATE | 청구 | 1 | 0 | 00:00:00.14 | 10 | 3 | ||||
2 | NESTED LOOPS | 1 | 1 | 1 | 00:00:00.12 | 7 | 2 | ||||
3 | SORT UNIQUE | 1 | 1 | 1 | 00:00:00.01 | 4 | 0 | 9216 | 9216 | 8192 (0) | |
4 | TABLE ACCESS BY INDEX ROWID | 고객 | 1 | 1 | 1 | 00:00:00.01 | 4 | 0 | |||
| INDEX RANGE SCAN | 고객_INDEX0 | 1 | 1 | 1 | 00:00:00.01 | 3 | 0 | |||
| INDEX RANGE SCAN | 청구_INDEX0 | 1 | 1 | 1 | 00:00:00.12 | 3 | 2 |
Predicate Information (identified by operation id):
5 - access("납입자"=1000)
6 - access("고객번호"="고객번호" AND "청구년월"='200503')
--음.. 좀더 공격적인 힌트
SQL> UPDATE /*+ gather_plan_statistics SWAP_JOIN_INPUTS( @SUB Y ) LEADING( @SUB Y ) HASH_SJ( @SUB ) INDEX( X 청구_INDEX02 ) */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ QB_NAME( SUB ) */ ( 고객번호 )
5 FROM 고객 Y
6 WHERE 납입자 = 1000);
1 행이 갱신되었습니다.
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | UPDATE | 청구 | 1 | 0 | 00:00:01.24 | 36800 | ||||
| HASH JOIN RIGHT SEMI | 1 | 1 | 1 | 00:00:01.24 | 36799 | 1035K | 1035K | 340K (0) | |
3 | TABLE ACCESS BY INDEX ROWID | 고객 | 1 | 1 | 1 | 00:00:00.01 | 4 | |||
| INDEX RANGE SCAN | 고객_INDEX0 | 1 | 1 | 1 | 00:00:00.01 | 3 | |||
| INDEX FULL SCAN | 청구_INDEX0 | 1 | 575K | 1000K | 00:00:01.00 | 36795 |
Predicate Information (identified by operation id):
2 - access("고객번호"="고객번호")
4 - access("납입자"=1000)
5 - access("청구년월"='200503')
filter("청구년월"='200503')
{CODE} |
Filter 서브쿼리 |
---|
{CODE:SQL} |
SQL> UPDATE /*+ gather_plan_statistics LEADING( @SUB Y ) NO_UNNEST( @SUB ) INDEX( X 청구_INDEX01 ) */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ QB_NAME( SUB ) */ 고객번호
5 FROM 고객 Y
6 WHERE 납입자 = 1000);
1 행이 갱신되었습니다.
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
1 | UPDATE | 청구 | 1 | 0 | 00:00:24.74 | 4038K | 38444 | |
| INDEX FULL SCAN | 청구_INDEX0 | 1 | 28751 | 1 | 00:00:24.74 | 4038K | 38444 |
| TABLE ACCESS BY INDEX ROWID | 고객 | 1000K | 1 | 1 | 00:00:07.15 | 4000K | 0 |
| INDEX RANGE SCAN | 고객_INDEX0 | 1000K | 1 | 1000K | 00:00:04.40 | 3000K | 0 |
Predicate Information (identified by operation id):
2 - access("청구년월"='200503')
filter(("청구년월"='200503' AND IS NOT NULL))
3 - filter("고객번호"=:B1)
4 - access("납입자"=1000)
{CODE} |
INDEX FULL SCAN 다른 방법은 없는것인가?? 유니크 인덱스가 없어서 그런거 같다.. 생성 |
---|
{CODE:SQL} |
DROP INDEX 청구_INDEX01
CREATE UNIQUE INDEX 청구_INDEX02 ON 청구( 고객번호, 청구년월 );
SQL> UPDATE /*+ gather_plan_statistics LEADING( @SUB Y ) NO_UNNEST( @SUB ) INDEX( X 청구_INDEX02 ) */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ QB_NAME( SUB ) */ ( 고객번호 )
5 FROM 고객 Y
6 WHERE 납입자 = 1000);
1 행이 갱신되었습니다.
SQL> @XPLAN
-- 아니군요 ㅠ
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
1 | UPDATE | 청구 | 1 | 0 | 00:00:09.54 | 4036K | 47 | |
| INDEX FULL SCAN | 청구_INDEX0 | 1 | 28751 | 1 | 00:00:09.54 | 4036K | 47 |
| TABLE ACCESS BY INDEX ROWID | 고객 | 1000K | 1 | 1 | 00:00:06.99 | 4000K | 0 |
| INDEX RANGE SCAN | 고객_INDEX0 | 1000K | 1 | 1000K | 00:00:04.28 | 3000K | 0 |
Predicate Information (identified by operation id):
2 - access("청구년월"='200503')
filter(("청구년월"='200503' AND IS NOT NULL))
3 - filter("고객번호"=:B1)
4 - access("납입자"=1000)
{CODE} | 선두 컬럼이 알수 없으니 풀스캔으로 타는것 같다. 청구_INDEX03 ON 청구( 청구년월, 고객번호 ) 확인자( Early Filter 서브쿼리 ) |
---|---|
{CODE:SQL} |
CREATE INDEX 청구_INDEX03 ON 청구( 청구년월, 고객번호 );
SQL> UPDATE /*+ gather_plan_statistics LEADING( @SUB Y ) NO_UNNEST( @SUB ) INDEX( X 청구_INDEX03) */ 청구 X
2 SET 입금액 = NVL( 입금액, 0 ) + 50000
3 WHERE 청구년월 = '200503'
4 AND 고객번호 IN (SELECT /*+ QB_NAME( SUB ) */ ( 고객번호 )
5 FROM 고객 Y
6 WHERE 납입자 = 1000);
1 행이 갱신되었습니다.
SQL> @XPLAN
Plan hash value: 483576752
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
1 | UPDATE | 청구 | 1 | 0 | 00:00:17.10 | 4003K | 3059 | |
| INDEX RANGE SCAN | 청구_INDEX0 | 1 | 28751 | 1 | 00:00:17.10 | 4003K | 3059 |
| TABLE ACCESS BY INDEX ROWID | 고객 | 1000K | 1 | 1 | 00:00:06.98 | 4000K | 0 |
| INDEX RANGE SCAN | 고객_INDEX0 | 1000K | 1 | 1000K | 00:00:04.27 | 3000K | 0 |
Predicate Information (identified by operation id):
2 - access("청구년월"='200503')
filter( IS NOT NULL)
3 - filter("고객번호"=:B1)
4 - access("납입자"=1000)
{CODE} |
준비스크립트 |
---|
{CODE:SQL} |
DROP TABLE 사원 PURGE;
CREATE TABLE 사원 AS
SELECT 사번
, 성명
, 직급
, 입사일
, 부서코드
, DECODE( 부서코드, 1, 'DB1팀', 2, 'DB2팀', 3, 'DB3팀'
, 4, '시스템1팀', 5, '시스템2팀', 6, '시스템3팀'
, 7, '경리1과', 8, '경리2과', 9, '경리3과', 10, '경리과', 11, '경리4과'
, 12, '개발1팀', 13, '개발2팀', 14, '개발3팀', 15, '개발4팀', 16, '개발5팀', 17, '개발6팀'
, 18, 'MD1팀', 19, 'MD2팀', 20, 'MD3팀', 21, 'MD4팀'
, 22, '디자인1팀', 23, '디자인2팀', 24, '디자인3팀', 25, '디자인4팀'
, 26, '멀티미디어1팀', 27, '멀티미디어2팀', 28, '멀티미디어3팀'
, 29, '모바일1팀'
, 30, '웹모바일팀'
, 31, '마케팅팀'
, 32, '기획팀' ) 부서
FROM (SELECT LEVEL AS 사번
, '아무개'||LEVEL AS 성명
, TRUNC( dbms_random.value( 1,7 ) ) 직급
, TRUNC( SYSDATE ) - 100 + CEIL( LEVEL/ 10000 ) 입사일
, TRUNC( dbms_random.value( 1,32 ) ) 부서코드
FROM DUAL
CONNECT BY LEVEL <= 1000000
)
CREATE UNIQUE INDEX 사원_PK ON 사원 ( 사번 )
--CREATE INDEX 부서_INDEX_01 ON 사원 ( 부서코드, 직급 );
CREATE TABLE 근태 AS
SELECT A.사번
, B.부서코드
, A.근태유형
, A.일자
FROM (SELECT TRUNC( dbms_random.value( 1,1000000 ) ) 사번
, DECODE( A.JOIN_C, 1,'무단결근',2,'조퇴', 3,'지각', 4,'지각', 5,'지각', 6, '연차', 7, '연차', 8,'휴가', 9,'휴가',10, '지각') 근태유형
, A.일자
FROM (SELECT TRUNC( dbms_random.value( 1,10 ) ) JOIN_C --
, TO_CHAR( TO_DATE( '20050101', 'YYYYMMDD') + LEVEL -1, 'YYYYMMDD' ) 일자
FROM DUAL
CONNECT BY LEVEL <= 365--TO_DATE( '20050630', 'YYYYMMDD') - TO_DATE( '20050501', 'YYYYMMDD')
) A
, (SELECT LEVEL LV FROM DUAL CONNECT BY LEVEL <= 6 ) B
WHERE A.JOIN_C >= B.LV
) A
, 사원 B
WHERE A.사번 = B.사번
CREATE INDEX 유형_일자_INX ON 근태 ( 근태유형, 일자 )
SQL> SELECT COUNT(*) FROM 근태
2 ;
COUNT(*)
{CODE} |
p. 591 테스트 |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics QB_NAME( MAIN ) LEADING( B@SUB ) UNNEST( @SUB ) USE_MERGE( B@SUB A@MAIN ) / COUNT()
2 FROM 사원 A
3 WHERE 부서코드 IN ( SELECT /*+ QB_NAME( SUB ) */ 부서코드
4 FROM 근태 B
5 WHERE 일자 BETWEEN '20050601' AND '20050612'
6 AND 근태유형 = '지각' )
7 AND 직급 >= 3;
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.43 | 6336 | ||||
2 | MERGE JOIN | 1 | 351K | 365K | 00:00:02.44 | 6336 | ||||
3 | SORT JOIN | 1 | 10 | 17 | 00:00:00.01 | 3 | 2048 | 2048 | 2048 (0) | |
4 | SORT UNIQUE | 1 | 19 | 17 | 00:00:00.01 | 3 | 9216 | 9216 | 8192 (0) | |
5 | TABLE ACCESS BY INDEX ROWID | 근태 | 1 | 19 | 19 | 00:00:00.01 | 3 | |||
| INDEX RANGE SCAN | 유형_일자_ | 1 | 19 | 19 | 00:00:00.01 | 2 | |||
| SORT JOIN | 17 | 573K | 365K | 00:00:01.03 | 6333 | 13M | 1381K | 11M (0) | |
| TABLE ACCESS FULL | 사원 | 1 | 573K | 666K | 00:00:01.33 | 6333 |
Predicate Information (identified by operation id):
6 - access("근태유형"='지각' AND "일자">='20050601' AND "일자"<='20050612')
7 - access("부서코드"="부서코드")
filter("부서코드"="부서코드")
8 - filter("직급">=3)
{CODE} |
p. 592 책처럼 실행계획으로 나오려면... |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics LEADING( A ) USE_MERGE( B ) / COUNT()
2 FROM 사원 A
3 , (SELECT DISTINCT 부서코드
4 FROM 근태 B
5 WHERE 일자 BETWEEN '20050601' AND '20050612'
6 AND 근태유형 = '지각' ) B
7 WHERE A.부서코드 = B.부서코드
8 AND 직급 >= 3;
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:06.96 | 6336 | ||||
2 | MERGE JOIN | 1 | 351K | 365K | 00:00:05.50 | 6336 | ||||
3 | SORT JOIN | 1 | 573K | 666K | 00:00:01.72 | 6333 | 13M | 1381K | 11M (0) | |
| TABLE ACCESS FULL | 사원 | 1 | 573K | 666K | 00:00:02.00 | 6333 | |||
| SORT JOIN | 666K | 19 | 365K | 00:00:04.28 | 3 | 73728 | 73728 | ||
6 | VIEW | 1 | 19 | 17 | 00:00:00.01 | 3 | ||||
7 | HASH UNIQUE | 1 | 19 | 17 | 00:00:00.01 | 3 | ||||
8 | TABLE ACCESS BY INDEX ROWID | 근태 | 1 | 19 | 19 | 00:00:00.01 | 3 | |||
| INDEX RANGE SCAN | 유형_일자_ | 1 | 19 | 19 | 00:00:00.01 | 2 |
Predicate Information (identified by operation id):
4 - filter("직급">=3)
5 - access("A"."부서코드"="B"."부서코드")
filter("A"."부서코드"="B"."부서코드")
9 - access("근태유형"='지각' AND "일자">='20050601' AND "일자"<='20050612')
{CODE} * HASH UNIQUE ( SORT UNIQUE ) : '1' 집합을 만든다 ( 메인 쿼리 보존 ) * Sort Merge형식으로 수행 될 때는 다른 집합에서 수해오딘 결과를 받아서 처리할 수 없으므로 독자적으로 처리범위를 충분히 줄일 수 있을 때 효과적이며 * 랜덤 액세스가 줄어들기 때문에 경우에 따라서는 매우 효과적일 수도 있다는 장점은 일반적인 조인 시와 동일하다.( ㅡㅡ ) * 예를 들어 앞서 예를 든 SQL의 서브쿼리에 GROUP BY가 있었다고 가정해보자. 그 결과는 이미 가공된 결과의 집합이므로 인덱스를 사용할 수 없다 그러나 GROUP BY에 의해 집합의 크기가 줄어 들었으므로 조인의 량은 감소한다. 이런 경우에는 Sort Merge형 세미 조인이 나름대로 가치가 있다. 물론 이런 경우에도 서브 쿼리를 인라인뷰로 만들어 일반 조인으로 연결하는 것도 내용적으로 거의 동일하다. 연결고리의 조건 조건에 'NOT'을 사용한 경우에도 이러한 형태의 연결이 나타날 수 있다. 여기에 대한 상세한 내용은 '부정형( ANTI ) 세미조인( Page 599 ~ 605 )에서 설명될 것이다. |
참고 사이트
http://scidb.tistory.com/search/SORT
1. 양쪽 집합이 Full Table Scan을 사용하면 조인순서에 상관없이 일량이 동일하므로 처리시간도 동일하다.
2. 조인순서에 상관없이 Sort량은 동일하다
3. 부분범위처리가 안된다.
4. Full Scan이 발생하면 인덱스를 사용할 수 없으므로 항상 Sort 작업을 동반한다.
5. Sort Merge Join 대신 Catesian Merge Join이 나오면 조인 조건이 빠진 악성 SQL이다.
6. 조인 컬럼 기준에 Sort되므로 Order by절과 조인 컬럼이 일치해야만 Sort가 발생하지 않는다.
p. 592 책처럼 실행계획으로..LEADING( 사원 ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics LEADING( A ) USE_MERGE( B ) / COUNT()
2 FROM 사원 A
3 , (SELECT DISTINCT 부서코드
4 FROM 근태 B
5 WHERE 일자 BETWEEN '20050601' AND '20050612'
6 AND 근태유형 = '지각' ) B
7 WHERE A.부서코드 = B.부서코드
8 AND 직급 >= 3;
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:06.92 | 6336 | ||||
2 | MERGE JOIN | 1 | 351K | 365K | 00:00:05.42 | 6336 | ||||
3 | SORT JOIN | 1 | 573K | 666K | 00:00:01.63 | 6333 | 13M | 1381K | 11M (0) | |
| TABLE ACCESS FULL | 사원 | 1 | 573K | 666K | 00:00:01.33 | 6333 | |||
| SORT JOIN | 666K | 19 | 365K | 00:00:04.33 | 3 | 73728 | 73728 | ||
6 | VIEW | 1 | 19 | 17 | 00:00:00.01 | 3 | ||||
7 | HASH UNIQUE | 1 | 19 | 17 | 00:00:00.01 | 3 | ||||
8 | TABLE ACCESS BY INDEX ROWID | 근태 | 1 | 19 | 19 | 00:00:00.01 | 3 | |||
| INDEX RANGE SCAN | 유형_일자_ | 1 | 19 | 19 | 00:00:00.01 | 2 |
Predicate Information (identified by operation id):
4 - filter("직급">=3)
5 - access("A"."부서코드"="B"."부서코드")
filter("A"."부서코드"="B"."부서코드")
9 - access("근태유형"='지각' AND "일자">='20050601' AND "일자"<='20050612')
{CODE} |
p. 592 책처럼 실행계획으로..LEADING( 근태 ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics LEADING( B ) USE_MERGE( A B ) / COUNT()
2 FROM 사원 A
3 , (SELECT DISTINCT 부서코드
4 FROM 근태 B
5 WHERE 일자 BETWEEN '20050601' AND '20050612'
6 AND 근태유형 = '지각' ) B
7 WHERE A.부서코드 = B.부서코드
8 AND 직급 >= 3;
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.41 | 6336 | ||||
2 | MERGE JOIN | 1 | 351K | 365K | 00:00:02.44 | 6336 | ||||
3 | SORT JOIN | 1 | 19 | 17 | 00:00:00.01 | 3 | 2048 | 2048 | 2048 (0) | |
4 | VIEW | 1 | 19 | 17 | 00:00:00.01 | 3 | ||||
5 | HASH UNIQUE | 1 | 19 | 17 | 00:00:00.01 | 3 | ||||
6 | TABLE ACCESS BY INDEX ROWID | 근태 | 1 | 19 | 19 | 00:00:00.01 | 3 | |||
| INDEX RANGE SCAN | 유형_일자_ | 1 | 19 | 19 | 00:00:00.01 | 2 | |||
| SORT JOIN | 17 | 573K | 365K | 00:00:01.02 | 6333 | 13M | 1381K | 11M (0) | |
| TABLE ACCESS FULL | 사원 | 1 | 573K | 666K | 00:00:01.33 | 6333 |
Predicate Information (identified by operation id):
7 - access("근태유형"='지각' AND "일자">='20050601' AND "일자"<='20050612')
8 - access("A"."부서코드"="B"."부서코드")
filter("A"."부서코드"="B"."부서코드")
9 - filter("직급">=3)
{CODE} |
준비 스크립티 |
---|
{CODE:SQL} |
DROP TABLE "ORDER" PURGE
DROP TABLE DEPT PURGE
CREATE TABLE "ORDER" AS
SELECT LEVEL SEQNO
, TO_CHAR( ( SYSDATE - 100 ) - 1 / 1440 / ( 60 / ( 1000001 - LEVEL )), 'YYYYMMDDHH24MISS' ) ORDDATE
, TO_CHAR( TRUNC( dbms_random.value( 1,12 ) ), 'FM0009' ) SALDEPTNO
FROM DUAL
CONNECT BY LEVEL <= 1000000
CREATE TABLE DEPT AS
SELECT TO_CHAR( LEVEL, 'FM0009' ) DEPTNO
, '부서'|| TO_CHAR( LEVEL, 'FM0009' ) AS DEPTNAME
, TRUNC( dbms_random.value( 1,4 ) ) TYPE1
FROM DUAL
CONNECT BY LEVEL <= 12
CREATE INDEX ORDDATE_INDEX ON "ORDER"( ORDDATE )
CREATE INDEX ORDDATE_INDEX_01 ON "ORDER"( ORDDATE, SALDEPTNO )
CREATE UNIQUE INDEX DEPT_PK ON DEPT ( DEPTNO )
SQL> SELECT *
2 FROM (SELECT *
3 FROM "ORDER"
4 ORDER BY SEQNO ASC
5 )
6 WHERE ROWNUM <= 10;
SEQNO ORDDATE SALDE
10 개의 행이 선택되었습니다.
SQL> SELECT SUBSTR( ORDDATE, 1,8 ), MAX(SEQNO), COUNT(*)
2 FROM "ORDER"
3 GROUP BY SUBSTR( ORDDATE, 1,8 )
4 ORDER BY MAX(SEQNO);
SUBSTR(ORDDATE,1 MAX(SEQNO) COUNT(*)
SUBSTR(ORDDATE,1 MAX(SEQNO) COUNT(*)
12 개의 행이 선택되었습니다.
SQL> SELECT * FROM DEPT
2 ;
DEPTN DEPTNAME TYPE1
DEPTN DEPTNAME TYPE1
12 개의 행이 선택되었습니다.
{CODE} |
p.593 그림 실행 스크립트( NO_HINT ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics / COUNT()
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%'
4 AND EXISTS ( SELECT 'O'
5 FROM DEPT Y
6 WHERE Y.DEPTNO = X.SALDEPTNO
7 AND Y.TYPE1 = 1 );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.11 | 757 | 374 | ||||
2 | NESTED LOOPS | 1 | 17623 | 15832 | 00:00:00.19 | 757 | 374 | ||||
3 | SORT UNIQUE | 1 | 2 | 2 | 00:00:00.01 | 3 | 0 | 9216 | 9216 | 8192 (0) | |
| TABLE ACCESS FULL | DEPT | 1 | 2 | 2 | 00:00:00.01 | 3 | 0 | |||
| INDEX RANGE SCAN | ORDDATE_INDEX_01 | 2 | 8812 | 15832 | 00:00:00.13 | 754 | 374 |
Predicate Information (identified by operation id):
4 - filter("Y"."TYPE1"=1)
5 - access("ORDDATE" LIKE '20110621%' AND "Y"."DEPTNO"="X"."SALDEPTNO")
filter(("ORDDATE" LIKE '20110621%' AND "Y"."DEPTNO"="X"."SALDEPTNO"))
{CODE} |
p. 594 그림 실행 스크립트 ( ORDDATE_INDEX( ORDDATE ) ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics INDEX( X ORDDATE_INDEX ) / COUNT()
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%'
4 AND EXISTS ( SELECT /*+ NO_UNNEST NO_PUSH_SUBQ */ 'O'
5 FROM DEPT Y
6 WHERE Y.DEPTNO = X.SALDEPTNO
7 AND Y.TYPE1 = 1 );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.15 | 697 | 313 | |
| FILTER | 1 | 15832 | 00:00:00.21 | 697 | 313 | ||
3 | TABLE ACCESS BY INDEX ROWID | ORDER | 1 | 96928 | 86400 | 00:00:00.95 | 675 | 313 |
| INDEX RANGE SCAN | ORDDATE_INDEX | 1 | 96928 | 86400 | 00:00:00.26 | 316 | 313 |
| TABLE ACCESS BY INDEX ROWID | DEPT | 11 | 1 | 2 | 00:00:00.01 | 22 | 0 |
| INDEX UNIQUE SCAN | DEPT_PK | 11 | 1 | 11 | 00:00:00.01 | 11 | 0 |
Predicate Information (identified by operation id):
2 - filter( IS NOT NULL)
4 - access("ORDDATE" LIKE '20110621%')
filter("ORDDATE" LIKE '20110621%')
5 - filter("Y"."TYPE1"=1)
6 - access("Y"."DEPTNO"=:B1)
{CODE} * 1) 'ORDDATE_INDEX'를 86400 개까지 차례로 범위처리를 하면서 86400 건의 'ORDER' 테이블의 로우를 읽어 내려 간다. * 2) 대응되는 DEPT 테이블을 연결한다. * 3) 그렇다면 'ORDER' 테이블에서 엑세스한 86400 개의 각각의 로우에 대해서 메번 서브쿼리가 수행되어 EXISTS를 체크했다 * 4) 그러므로 'DEPT' 테이블을 액세스하는 서브쿼리도 86400 번 수행되어야 할 것이다. * 5) 그런데 'DEPT' acess 5을 살펴보면 이 메인쿼리가 'DEPT' 테이블을 엑세스한것은 단 11회에 불과 하다. |
NL SEMI ( ORDDATE_INDEX ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics INDEX( X ORDDATE_INDEX ) / COUNT()
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%'
4 AND EXISTS ( SELECT /*+ NL_SJ */ 'O'
5 FROM DEPT Y
6 WHERE Y.DEPTNO = X.SALDEPTNO
7 AND Y.TYPE1 = 1 );
COUNT(*)
SQL> @xplan
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.12 | 688 | |
2 | NESTED LOOPS SEMI | 1 | 709 | 15832 | 00:00:00.17 | 688 | |
3 | TABLE ACCESS BY INDEX ROWID | ORDER | 1 | 3899 | 86400 | 00:00:00.61 | 675 |
| INDEX RANGE SCAN | ORDDATE_INDEX | 1 | 3899 | 86400 | 00:00:00.26 | 316 |
| TABLE ACCESS BY INDEX ROWID | DEPT | 11 | 1 | 2 | 00:00:00.01 | 13 |
| INDEX UNIQUE SCAN | DEPT_PK | 11 | 1 | 11 | 00:00:00.01 | 2 |
Predicate Information (identified by operation id):
4 - access("ORDDATE" LIKE '20110621%')
filter("ORDDATE" LIKE '20110621%')
5 - filter("Y"."TYPE1"=1)
6 - access("Y"."DEPTNO"="X"."SALDEPTNO")
{CODE} |
{CODE:SQL}
SQL> SELECT /*+ gather_plan_statistics INDEX( X ORDDATE_INDEX ) */ *
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%'
4 AND ROWNUM <= 50
5 ORDER BY ORDDATE ASC;
SEQNO ORDDATE SALDE
SEQNO ORDDATE SALDE
SEQNO ORDDATE SALDE
SEQNO ORDDATE SALDE
SEQNO ORDDATE SALDE
50 개의 행이 선택되었습니다.
SQL> SELECT /*+ gather_plan_statistics INDEX( X ORDDATE_INDEX ) */ DISTINCT SALDEPTNO
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%' ;
SALDE
11 개의 행이 선택되었습니다.
SQL> select ksppinm name,
2 ksppstvl value,
3 decode(bitand(ksppiflg/256,1),1,'true','false') ses_modifiable,
4 decode(bitand(ksppiflg/65536,3),1,'immediate',2,'deferred',3,'immediate','false') sys_modifiable,
5 ksppdesc description
6 from sys.x$ksppi i, sys.x$ksppcv v
7 where i.indx = v.indx
8 and i.ksppinm like '%_query_execution_cache_max_size%';
NAME
{CODE}
p. 594 그림 실행 스크립트 ( ORDDATE_INDEX_01( ORDDATE ) ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics INDEX( X ORDDATE_INDEX_01 ) / COUNT()
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%'
4 AND EXISTS ( SELECT /*+ NO_UNNEST NO_PUSH_SUBQ */ 'O'
5 FROM DEPT Y
6 WHERE Y.DEPTNO = X.SALDEPTNO
7 AND Y.TYPE1 = 1 );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.10 | 399 | 3 | |
| FILTER | 1 | 15832 | 00:00:00.16 | 399 | 3 | ||
| INDEX RANGE SCAN | ORDDATE_INDEX_01 | 1 | 3899 | 86400 | 00:00:00.26 | 377 | 3 |
| TABLE ACCESS BY INDEX ROWID | DEPT | 11 | 1 | 2 | 00:00:00.01 | 22 | 0 |
| INDEX UNIQUE SCAN | DEPT_PK | 11 | 1 | 11 | 00:00:00.01 | 11 | 0 |
Predicate Information (identified by operation id):
2 - filter( IS NOT NULL)
3 - access("ORDDATE" LIKE '20110621%')
filter("ORDDATE" LIKE '20110621%')
4 - filter("Y"."TYPE1"=1)
5 - access("Y"."DEPTNO"=:B1)
{CODE} |
NL SEMI ( ORDDATE_INDEX_01 ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics INDEX( X ORDDATE_INDEX_01 ) / COUNT()
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%'
4 AND EXISTS ( SELECT /*+ NL_SJ */ 'O'
5 FROM DEPT Y
6 WHERE Y.DEPTNO = X.SALDEPTNO
7 AND Y.TYPE1 = 1 );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.08 | 390 | |
2 | NESTED LOOPS SEMI | 1 | 709 | 15832 | 00:00:00.13 | 390 | |
| INDEX RANGE SCAN | ORDDATE_INDEX_01 | 1 | 3899 | 86400 | 00:00:00.26 | 377 |
| TABLE ACCESS BY INDEX ROWID | DEPT | 11 | 1 | 2 | 00:00:00.01 | 13 |
| INDEX UNIQUE SCAN | DEPT_PK | 11 | 1 | 11 | 00:00:00.01 | 2 |
Predicate Information (identified by operation id):
3 - access("ORDDATE" LIKE '20110621%')
filter("ORDDATE" LIKE '20110621%')
4 - filter("Y"."TYPE1"=1)
5 - access("Y"."DEPTNO"="X"."SALDEPTNO")
{CODE} |
HASH JOIN SEMI |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics INDEX( X ORDDATE_INDEX ) LEADING( A ) / COUNT()
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%'
4 AND EXISTS ( SELECT /*+ HASH_SJ */ 'O'
5 FROM DEPT Y
6 WHERE Y.DEPTNO = X.SALDEPTNO
7 AND Y.TYPE1 = 1 );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.12 | 678 | ||||||||||||||||
| HASH JOIN SEMI | 1 | 709 | 15832 | 00:00:00.15 | 678 | 3256K | 1861K | 6674K (0) | <-- | 3 | TABLE ACCESS BY INDEX ROWID | ORDER | 1 | 3899 | 86400 | 00:00:00.60 | 675 | ||||
| INDEX RANGE SCAN | ORDDATE_INDEX | 1 | 3899 | 86400 | 00:00:00.26 | 316 | |||||||||||||||
| TABLE ACCESS FULL | DEPT | 1 | 2 | 2 | 00:00:00.01 | 3 |
Predicate Information (identified by operation id):
2 - access("Y"."DEPTNO"="X"."SALDEPTNO")
4 - access("ORDDATE" LIKE '20110621%')
filter("ORDDATE" LIKE '20110621%')
5 - filter("Y"."TYPE1"=1)
{CODE} |
HASH JOIN RIGHT SEMI ( 적은 테이블을 선행 테이블로... ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics INDEX( X ORDDATE_INDEX ) / COUNT()
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%'
4 AND EXISTS ( SELECT /*+ HASH_SJ */ 'O'
5 FROM DEPT Y
6 WHERE Y.DEPTNO = X.SALDEPTNO
7 AND Y.TYPE1 = 1 );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.10 | 678 | ||||||||||||||||
| HASH JOIN RIGHT SEMI | 1 | 709 | 15832 | 00:00:00.14 | 678 | 1396K | 1396K | 474K (0) | <-- |
| TABLE ACCESS FULL | DEPT | 1 | 2 | 2 | 00:00:00.01 | 3 | ||||
4 | TABLE ACCESS BY INDEX ROWID | ORDER | 1 | 3899 | 86400 | 00:00:00.60 | 675 | |||||||||||||||
| INDEX RANGE SCAN | ORDDATE_INDEX | 1 | 3899 | 86400 | 00:00:00.26 | 316 |
Predicate Information (identified by operation id):
2 - access("Y"."DEPTNO"="X"."SALDEPTNO")
3 - filter("Y"."TYPE1"=1)
5 - access("ORDDATE" LIKE '20110621%')
filter("ORDDATE" LIKE '20110621%')
26 개의 행이 선택되었습니다.
{CODE} * 수행속도 향상의 요건 : 1) 대량의 연결을 해야 하고 메모리 내에 충분한 해쉬 영역 * 제약 조건 : 1) 서브쿼리에는 하나의 테이블만 존재해야만 한다 2) 서브쿼리 내에 또 다시 서브쿼리를 사용했을 때는 적용이 불가능하다 3) 연결고리의 연산자는 반드시 '='이 되어야 한다. 4) 서브쿼리 내에 GROUP BY, CONNECT BY, ROWNUM을 사용할 수 없다. * 부분 범위 가능 요건 : 어느 한쪽 집합이 메모리 내의 해쉬 영역에 내포 될 수 있으면 부분범위 처리가 가능하다 ( ???? ) 이러한 경우는 온라인 애플리케이션의 경우 대량량 데이터라 하더라도 아주 빠른 수행 속도를 보장 받을 수가 있다. |
준비 스크립트 ( 위에 테이블 재사용 ( 편의상 ) ) |
---|
{CODE:SQL} |
DROP TABLE TAB1 PURGE;
DROP TABLE TAB2 PURGE;
CREATE TABLE TAB1 AS
SELECT LEVEL KEY1
, '상품'||LEVEL COL1
, TRUNC( dbms_random.value( 1,7 ) ) * 500 + TRUNC( dbms_random.value( 1,3 ) ) * 750 COL2
FROM DUAL
CONNECT BY LEVEL <= 10000;
CREATE TABLE TAB2 AS
SELECT LEVEL KEY1
, TRUNC(dbms_random.value( 1, 10000 ) ) KEY2
, TRUNC( SYSDATE ) - 100 + CEIL( LEVEL/ 1000 ) COL1
, 'A' COL2
FROM DUAL
CONNECT BY LEVEL <= 100000;
CREATE INDEX TAB1_COL1 ON TAB1( COL1 );
CREATE INDEX TAB2_COL1 ON TAB2( COL1 );
CREATE INDEX TAB2_KEY2 ON TAB2( KEY2, COL1 );
EXEC DBMS_STATS.GATHER_TABLE_STATS( USER, 'TAB1' );
EXEC DBMS_STATS.GATHER_TABLE_STATS( USER, 'TAB2' );
{CODE} |
p. 600 실습 스크립트 ( 확인자 ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND KEY1 NOT IN ( SELECT KEY2
5 FROM TAB2 B
6 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE ) );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:01.12 | 15487 | ||
| FILTER | 1 | 899 | 00:00:00.99 | 15487 | |||
3 | TABLE ACCESS BY INDEX ROWID | TAB1 | 1 | 1216 | 1112 | 00:00:00.01 | 209 | |
| INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 | 00:00:00.01 | 5 | |
| FILTER | 1112 | 213 | 00:00:01.11 | 15278 | |||
| TABLE ACCESS BY INDEX ROWID | TAB2 | 1112 | 1 | 213 | 00:00:01.10 | 15278 | |
| INDEX RANGE SCAN | TAB2_COL1 | 1112 | 2000 | 2001K | 00:00:04.01 | 8184 | (a) -- |
Predicate Information (identified by operation id):
{CODE} * 서브쿼리가 확인자로 풀림 * (a)서브쿼리의 처리주관 인덱스는 연결고리인 KTAB2_KEY2 인덱스가 아니라 TAB2_COL1 사용 되고 있다. * 이거슨 서브쿼리가 계속해서 동일한 범위를 반복해서 치리하게 된다는것을 의미한다. ( 부정형에는 캐쉬가 먹지 않는가.?? 위경우는 TAB1.KEY1경우가 유니크하기때문에... ) * 만약 인덱스가 ( KEY2, COL1 )로 구성되었거나 COL1에 인덱스가 없다면 당연히 KEY2 인덱스를 사용하는 실행계획이 작성된다. * 이것은 옵티마이저의 잘못이다. ( 데이터베이스 버전에따라 나타나지 않을 수도 있다. ) |
잘못된 실행 계획이 수립되는 것을 방지하는 바람직 SQL ( TAB2_COL1 풀림 ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND NOT EXISTS ( SELECT/*+ */ 'X'
5 FROM TAB2 B
6 WHERE A.KEY1 = B.KEY2
7 AND COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE ) );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.01 | 224 | |||||
| HASH JOIN ANTI | 1 | 1 | 899 | 00:00:00.01 | 224 | 1517K | 1517K | 1182K (0) | ||
3 | TABLE ACCESS BY INDEX ROWID | TAB1 | 1 | 1216 | 1112 | 00:00:00.01 | 209 | ||||
| INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 | 00:00:00.01 | 5 | ||||
5 | TABLE ACCESS BY INDEX ROWID | TAB2 | 1 | 2000 | 2000 | 00:00:00.02 | 15 | ||||
| INDEX RANGE SCAN | TAB2_COL1 | 1 | 2000 | 2000 | 00:00:00.01 | 8 | <-- ㅡㅡ^ |
Predicate Information (identified by operation id):
2 - access("A"."KEY1"="B"."KEY2")
4 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
6 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
{CODE} |
잘못된 실행 계획이 수립되는 것을 방지하는 바람직 SQL ( TAB2_KEY2 유도 ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND NOT EXISTS ( SELECT/*+ INDEX( B TAB2_KEY2 ) */ 'X'
5 FROM TAB2 B
6 WHERE A.KEY1 = B.KEY2
7 AND COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE ) );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.08 | 531 | ||||
| HASH JOIN ANTI | 1 | 1 | 899 | 00:00:00.08 | 531 | 1517K | 1517K | 1182K (0) | |
3 | TABLE ACCESS BY INDEX ROWID | TAB1 | 1 | 1216 | 1112 | 00:00:00.01 | 209 | |||
| INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 | 00:00:00.01 | 5 | |||
| INDEX FULL SCAN | TAB2_KEY2 | 1 | 2000 | 2000 | 00:00:00.08 | 322 |
Predicate Information (identified by operation id):
2 - access("A"."KEY1"="B"."KEY2")
4 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
5 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
filter(("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!)))
{CODE} |
{CODE:SQL}
SQL> SELECT/*+ INDEX( B TAB2_KEY2 ) / COUNT()
2 FROM TAB2 B
3 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE );
COUNT(*)
SQL> SELECT COUNT(*)
2 FROM TAB2
3 WHERE KEY2 IN (
4 SELECT/*+ INDEX( B TAB2_KEY2 ) */ DISTINCT KEY2
5 FROM TAB2 B
6 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE ) );
COUNT(*)
{CODE}
p. 601 실행 스크립트 |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND NOT EXISTS ( SELECT/*+ NO_UNNEST NO_PUSH_SUBQ */ 'X'
5 FROM TAB2 B
6 WHERE A.KEY1 = B.KEY2
7 AND COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE ) );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.03 | 2434 | ||
| FILTER | 1 | 899 | 00:00:00.03 | 2434 | |||
3 | TABLE ACCESS BY INDEX ROWID | TAB1 | 1 | 1216 | 1112 | 00:00:00.01 | 209 | |
| INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 | 00:00:00.01 | 5 | |
| FILTER | 1112 | 213 | 00:00:00.02 | 2225 | |||
| INDEX RANGE SCAN | TAB2_KEY2 | 1112 | 1 | 213 | 00:00:00.01 | 2225 | <-- 굿잡 - |
Predicate Information (identified by operation id):
2 - filter( IS NULL)
4 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
5 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
6 - access("B"."KEY2"=:B1 AND "COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
{CODE} * 필터형으로 푸니 책에서 말씀하신데로 잘 풀리는구나... ( 버전에 따라 틀리수도 있다 ) * 선행 집한에서 상수값을 제공받아 처리한다.( 확인자 ) * 이런한 처리는 랜덤 액세스가 증가한다는 단점을 가지고 있기 때문에 상수값을 제공받았을 때 수행되는 처리량과 독자적으로 수행할 때의 처리량을 비교하여 판단해야 한다. * 이 실행계획읜 장점은 부분범위 처리가 가능하다는 것이다. ( 운반단위 ) |
보너스.. NL_AJ로 풀면 어떤가..?? |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND NOT EXISTS ( SELECT/*+ NL_AJ */ 'X'
5 FROM TAB2 B
6 WHERE A.KEY1 = B.KEY2
7 AND COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE ) );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.02 | 1324 | |
2 | NESTED LOOPS ANTI | 1 | 1 | 899 | 00:00:00.02 | 1324 | |
3 | TABLE ACCESS BY INDEX ROWID | TAB1 | 1 | 1216 | 1112 | 00:00:00.01 | 209 |
| INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 | 00:00:00.01 | 5 |
| INDEX RANGE SCAN | TAB2_KEY2 | 1112 | 2000 | 213 | 00:00:00.01 | 1115 |
Predicate Information (identified by operation id):
4 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
5 - access("A"."KEY1"="B"."KEY2" AND "COL1">=TRUNC(SYSDATE@!-1) AND
"COL1"<=TRUNC(SYSDATE@!))
{CODE} |
{CODE:SQL}
SQL> SELECT /*+ gather_plan_statistics INDEX( X ORDDATE_INDEX ) / COUNT()
2 FROM "ORDER" X
3 WHERE ORDDATE LIKE '20110621%'
4 AND NOT EXISTS ( SELECT /*+ NL_AJ */ 'O'
5 FROM DEPT Y
6 WHERE Y.DEPTNO = X.SALDEPTNO
7 AND Y.TYPE1 = 1 );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.14 | 688 | |
2 | NESTED LOOPS ANTI | 1 | 3190 | 70568 | 00:00:00.35 | 688 | |
3 | TABLE ACCESS BY INDEX ROWID | ORDER | 1 | 3899 | 86400 | 00:00:00.78 | 675 |
| INDEX RANGE SCAN | ORDDATE_INDEX | 1 | 3899 | 86400 | 00:00:00.26 | 316 |
| TABLE ACCESS BY INDEX ROWID | DEPT | 11 | 1 | 2 | 00:00:00.01 | 13 |
| INDEX UNIQUE SCAN | DEPT_PK | 11 | 1 | 11 | 00:00:00.01 | 2 |
Predicate Information (identified by operation id):
4 - access("ORDDATE" LIKE '20110621%')
filter("ORDDATE" LIKE '20110621%')
5 - filter("Y"."TYPE1"=1)
6 - access("Y"."DEPTNO"="X"."SALDEPTNO")
{CODE}
p. 602 FILTER |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND KEY1 NOT IN ( SELECT KEY1
5 FROM TAB2 B
6 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE ) );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:01.14 | 16889 | |
| FILTER | 1 | 1112 | 00:00:01.15 | 16889 | ||
3 | TABLE ACCESS BY INDEX ROWID | TAB1 | 1 | 1216 | 1112 | 00:00:00.01 | 209 |
| INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 | 00:00:00.01 | 5 |
| FILTER | 1112 | 0 | 00:00:01.13 | 16680 | ||
| TABLE ACCESS BY INDEX ROWID | TAB2 | 1112 | 1 | 0 | 00:00:01.12 | 16680 |
| INDEX RANGE SCAN | TAB2_COL1 | 1112 | 2000 | 2224K | 00:00:04.46 | 8896 |
Predicate Information (identified by operation id):
2 - filter( IS NULL)
4 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
5 - filter(TRUNC(SYSDATE@!-1)<=TRUNC(SYSDATE@!))
6 - filter(LNNVL("KEY1"<>:B1))
7 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
{CODE} * 메인쿼리에서 추출한 범위가 매우 넓어서 서브쿼리가 랜덤으로 처리할 양이 매우 많아진다면 상당한 부담이 될 것이다. * 이런한 경우에는 Sort Merge 조인처럼 각각의 집합을 별도로 한 번씩만 액세스하여 정렬시킨 다음 머지를 통해서 연결하는 것이 훨씬 유리하다. * 부정형으로 연결되었지만 이런한 방식의 처리가 문제될 것은 없다. 머지 단계에서 일반적이 머지의 반대인 '머지에 실패한 것'을 추출하기만 하면 나머지는 동일한 방법이 되기 때문에이다. * 그러나 대부분의 경우 옵티마이져는 필터형 처리로 실행계획을 수립하기 때문에 필요하다면 뭔가 특별한 조치를 해야 한다. ( Hash or Merge ) |
p.602 MERGE JOIN ANTI |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND KEY1 IS NOT NULL
5 AND KEY1 NOT IN ( SELECT /*+ MERGE_AJ */ KEY1
6 FROM TAB2 B
7 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
8 AND KEY1 IS NOT NULL );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.01 | 224 | ||||
2 | MERGE JOIN ANTI | 1 | 1 | 1112 | 00:00:00.02 | 224 | ||||
3 | SORT JOIN | 1 | 1216 | 1112 | 00:00:00.01 | 209 | 24576 | 24576 | 22528 (0) | |
| TABLE ACCESS BY INDEX ROWID | TAB1 | 1 | 1216 | 1112 | 00:00:00.01 | 209 | |||
| INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 | 00:00:00.01 | 5 | |||
| SORT UNIQUE | 1112 | 2000 | 0 | 00:00:00.01 | 15 | 83968 | 83968 | 73728 (0) | |
| TABLE ACCESS BY INDEX ROWID | TAB2 | 1 | 2000 | 2000 | 00:00:00.02 | 15 | |||
| INDEX RANGE SCAN | TAB2_COL1 | 1 | 2000 | 2000 | 00:00:00.01 | 8 |
Predicate Information (identified by operation id):
4 - filter("KEY1" IS NOT NULL)
5 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
6 - access("KEY1"="KEY1")
filter("KEY1"="KEY1")
7 - filter("KEY1" IS NOT NULL)
8 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
{CODE:SQL} * NOT IN 을 사용한 경우만 가능하며 ( MERGE JOIN ANTI ) * 비용기준 옵티마이져로 정의 되어야만 한다. * 세미 조인이 'IN' 이나 'EXISTS' 모두 가능하듯이 부벙형 조인도 당연히 그래야 하지만 현재까지는 'NOT EXISTS'를 사용한 경우는 힌트가 적용되지 않는다 ( 현재도..?? ) |
p.603 HASH JOIN ANTI |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND KEY1 IS NOT NULL
5 AND KEY1 NOT IN ( SELECT /*+ HASH_AJ */ KEY1
6 FROM TAB2 B
7 WHERE COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
8 AND KEY1 IS NOT NULL );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.01 | 224 | ||||
| HASH JOIN ANTI | 1 | 1 | 1112 | 00:00:00.01 | 224 | 1517K | 1517K | 1180K (0) | |
| TABLE ACCESS BY INDEX ROWID | TAB1 | 1 | 1216 | 1112 | 00:00:00.01 | 209 | |||
| INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 | 00:00:00.01 | 5 | |||
| TABLE ACCESS BY INDEX ROWID | TAB2 | 1 | 2000 | 2000 | 00:00:00.02 | 15 | |||
| INDEX RANGE SCAN | TAB2_COL1 | 1 | 2000 | 2000 | 00:00:00.01 | 8 |
Predicate Information (identified by operation id):
2 - access("KEY1"="KEY1")
3 - filter("KEY1" IS NOT NULL)
4 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
5 - filter("KEY1" IS NOT NULL)
6 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
{CODE} * MERGE_AJ과는 다르게 SORT UNIQUE 부분이 나타나지 않는 것은 해쉬 조인이 연결을 위해 사전에 준비해 두는 작업이 필요한 것은 머지 조인과 유사하지만 실제 연결단계에서 필터형식으로 처리되므로 굳이 유일한 집합을 만들어야 할 필요가 없기 때문이다. * 항상 이런 방법이 유리한 것이 아니라는 것은 앞서 그 이유를 충분히 설명하였다. (막 쓰지말고 확인하고 ) * 특히 머지 부정형 조인이나 해쉬 부정형 조인은 전체범위 처리를 하는 경우가 많기 때문에 반드시 부분범위 처리를 하고자 할 때는 사용해서는 안 된다는 것을 분명이 기억하기 바란다. * 세미 조인이 'IN' 이나 'EXISTS' 모두 가능하듯이 부벙형 조인도 당연히 그래야 하지만 현재까지는 'NOT EXISTS'를 사용한 경우는 힌트가 적용되지 않는다 ( 현재도..?? ) |
보너스 MERGE JOIN ANTI ( NOT EXISTS )( 현재는 됨 ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND KEY1 IS NOT NULL
5 AND NOT EXISTS ( SELECT /*+ MERGE_AJ */ 'X'
6 FROM TAB2 B
7 WHERE A.KEY1 = B.KEY1
8 AND COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
9 AND KEY1 IS NOT NULL );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.01 | 224 | ||||
2 | MERGE JOIN ANTI | 1 | 1 | 1112 | 00:00:00.02 | 224 | ||||
3 | SORT JOIN | 1 | 1216 | 1112 | 00:00:00.01 | 209 | 24576 | 24576 | 22528 (0) | |
| TABLE ACCESS BY INDEX ROWID | TAB1 | 1 | 1216 | 1112 | 00:00:00.01 | 209 | |||
| INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 | 00:00:00.01 | 5 | |||
| SORT UNIQUE | 1112 | 2000 | 0 | 00:00:00.01 | 15 | 83968 | 83968 | 73728 (0) | |
| TABLE ACCESS BY INDEX ROWID | TAB2 | 1 | 2000 | 2000 | 00:00:00.02 | 15 | |||
| INDEX RANGE SCAN | TAB2_COL1 | 1 | 2000 | 2000 | 00:00:00.01 | 8 |
Predicate Information (identified by operation id):
4 - filter("KEY1" IS NOT NULL)
5 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
6 - access("A"."KEY1"="B"."KEY1")
filter("A"."KEY1"="B"."KEY1")
7 - filter("KEY1" IS NOT NULL)
8 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
{CODE} |
보너스 HASH JOIN ANTI ( NOT EXISTS )( 현재는 됨 ) |
---|
{CODE:SQL} |
SQL> SELECT /*+ gather_plan_statistics INDEX( A TAB1_COL1 )*/ COUNT( * )
2 FROM TAB1 A
3 WHERE COL1 LIKE '상품1%'
4 AND KEY1 IS NOT NULL
5 AND NOT EXISTS ( SELECT /*+ HASH_AJ */ 'X'
6 FROM TAB2 B
7 WHERE A.KEY1 = B.KEY1
8 AND COL1 BETWEEN TRUNC( SYSDATE - 1 ) AND TRUNC( SYSDATE )
9 AND KEY1 IS NOT NULL );
COUNT(*)
SQL> @XPLAN
Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
1 | SORT AGGREGATE | 1 | 1 | 1 | 00:00:00.01 | 224 | ||||
| HASH JOIN ANTI | 1 | 1 | 1112 | 00:00:00.01 | 224 | 1517K | 1517K | 1189K (0) | |
| TABLE ACCESS BY INDEX ROWID | TAB1 | 1 | 1216 | 1112 | 00:00:00.01 | 209 | |||
| INDEX RANGE SCAN | TAB1_COL1 | 1 | 1216 | 1112 | 00:00:00.01 | 5 | |||
| TABLE ACCESS BY INDEX ROWID | TAB2 | 1 | 2000 | 2000 | 00:00:00.02 | 15 | |||
| INDEX RANGE SCAN | TAB2_COL1 | 1 | 2000 | 2000 | 00:00:00.01 | 8 |
Predicate Information (identified by operation id):
2 - access("A"."KEY1"="B"."KEY1")
3 - filter("KEY1" IS NOT NULL)
4 - access("COL1" LIKE '상품1%')
filter("COL1" LIKE '상품1%')
5 - filter("KEY1" IS NOT NULL)
6 - access("COL1">=TRUNC(SYSDATE@!-1) AND "COL1"<=TRUNC(SYSDATE@!))
{CODE} |