조인 순서의 중요성

  • 조인 순서에 따라 쿼리 수행 성능이 달라질 수 있다.
  • 이 장에서는 NL조인에 있어서 어떤 테이블을 드라이빙하는것이 유리한지를 예제를 통해 확인한다.

준비

  • 10만 고개 테이블 생성
    {CODE:SQL}
    SQL> SELECT * FROM V$VERSION WHERE ROWNUM <= 1;

BANNER






















Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production

CREATE TABLE 고객
AS
SELECT LEVEL 고객번호
, MOD(LEVEL, 3) + 1 납입방법코드
, DECODE(MOD(LEVEL, 10)
, 1, '서울', 2, '대전', 3, '대구', 4, '부산', 5, '찍고'
, 6, '아하', 7, '경기', 8, '충청', 9, '전라', 0, '경상'
) 거주지역
FROM dual
CONNECT BY LEVEL <= 100000
;

ALTER TABLE 고객 ADD CONSTRAINT pk_고객 PRIMARY KEY(고객번호);

CREATE INDEX idx_1 ON 고객(거주지역);

CREATE INDEX idx_2 ON 고객(납입방법코드);

EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, '고객', CASCADE => TRUE);

{CODE}

  • 납입방법 코드 테이블생성
    {CODE:SQL}
    CREATE TABLE 납입방법
    AS
    SELECT 1 납입방법코드, '신용카드' 납입방법 FROM dual
    UNION ALL SELECT 2, '자동이체' FROM dual
    UNION ALL SELECT 3, '지로' FROM dual
    ;

ALTER TABLE 납입방법
ADD CONSTRAINT pk_납입방법 PRIMARY KEY(납입방법코드)

EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, '납입방법', CASCADE => TRUE);
;
{CODE}

  • 전체 테이블 조인 ( 옵티마이저 )
    {CODE:SQL}

SQL> SELECT COUNT()--a.납입방법명, b.
2 FROM 납입방법 a, 고객 b
3 WHERE b.납입방법코드 = a.납입방법코드
4 ;

COUNT()--A.납입방법명,B.







--
100000

SQL> @XPLAN






























---

IdOperationNameStartsE-RowsA-RowsA-TimeBuffersOMem1MemUsed-Mem






























---

0SELECT STATEMENT1100:00:00.09205
1SORT AGGREGATE11100:00:00.09205
  • 2
HASH JOIN1100K100K00:00:00.082051517K1517K821K (0)
3INDEX FULL SCANPK_납입13300:00:00.011
4INDEX FAST FULL SCANIDX_21100K100K00:00:00.02204






























---

Predicate Information (identified by operation id):













---

2 - access("B"."납입방법코드"="A"."납입방법코드")

{CODE}

필터조건이 없을 때

  • 고객테이블 드라이빙 - PK_납입 납입 10만번의 램덤엑세스 발생 후 납입방법테이블 램덤엑세스 10만번 ( 총 20 만번 )
    {CODE:SQL}

SQL> SELECT /*+ LEADING(b) USE_NL(a) INDEX( A ) */
2 a.납입방법, b.*
3 FROM 납입방법 a, 고객 b
4 WHERE b.납입방법코드 = a.납입방법코드
5 ;

...

SQL> @XPLAN



























---

IdOperationNameStartsE-RowsA-RowsA-TimeBuffersReads



























---

0SELECT STATEMENT1100K00:00:00.57113K251
1NESTED LOOPS1100K00:00:00.57113K251
2NESTED LOOPS1100K100K00:00:00.3813536250
3TABLE ACCESS FULL고객1100K100K00:00:00.206866249
  • 4
INDEX UNIQUE SCANPK_납입100K1100K00:00:00.1166701
5TABLE ACCESS BY INDEX ROWID납입방법100K1100K00:00:00.13100K1



























---

Predicate Information (identified by operation id):













---

4 - access("B"."납입방법코드"="A"."납입방법코드")

{CODE}

  • 납입방법 드라이빙 - 납입 테이블에서 고객 쪽 납입방법코드 인덱스로 조인을 시도할때 3번의 랜덤엑세스 발생후 고객 테이블 10만번 랜덤엑세스( 총 10만3번 )

{CODE:SQL}

SQL> SELECT /*+ LEADING(A) USE_NL(B) INDEX( B ) */
2 a.납입방법, b.*
3 FROM 납입방법 a, 고객 b
4 WHERE b.납입방법코드 = a.납입방법코드
5 ;

...

SQL> @XPLAN

Plan hash value: 1253683656



























-

IdOperationNameStartsE-RowsA-RowsA-TimeBuffersReads



























-

0SELECT STATEMENT1100K00:00:00.3514219200
1NESTED LOOPS1100K00:00:00.3514219200
2NESTED LOOPS1100K100K00:00:00.206860197
3TABLE ACCESS FULL납입방13300:00:00.0150
  • 4
INDEX RANGE SCANIDX_2333333100K00:00:00.176855197
5TABLE ACCESS BY INDEX ROWID고객100K33333100K00:00:00.0873593



























-

Predicate Information (identified by operation id):













---

4 - access("B"."납입방법코드"="A"."납입방법코드")

{CODE}

필터 조건이 있을 때 - 부산지역고객은 전체 중 10%에 해당하는 1만명이고 고객 테이블 거주지역 컬럼에 인덱스가 존재할때

  • 고객테이블 드라이빙 - 3만번
    {CODE:SQL}

SQL> SELECT /*+ LEADING(b) USE_NL(a) INDEX( A ) INDEX( B ) */
2 a.납입방법, b.*
3 FROM 납입방법 a, 고객 b
4 WHERE b.납입방법코드 = a.납입방법코드
5 AND b.거주지역 = '부산'

...

SQL> @XPLAN




























IdOperationNameStartsE-RowsA-RowsA-TimeBuffersReads




























0SELECT STATEMENT11000000:00:00.701226324
1NESTED LOOPS11000000:00:00.701226324
2NESTED LOOPS1100001000000:00:00.68226324
3TABLE ACCESS BY INDEX ROWID고객1100001000000:00:00.66159324
  • 4
INDEX RANGE SCANIDX_11100001000000:00:00.6668924
  • 5
INDEX UNIQUE SCANPK_납입1000011000000:00:00.016700
6TABLE ACCESS BY INDEX ROWID납입방법1000011000000:00:00.01100000




























Predicate Information (identified by operation id):













---

4 - access("B"."거주지역"='부산')
5 - access("B"."납입방법코드"="A"."납입방법코드")

{CODE}

  • 납입방법 드라이빙 - 10만 3번
    {CODE:SQL}
    SQL> SELECT /*+ LEADING(A) USE_NL(B) INDEX( B idx_2 ) */
    2 a.납입방법, b.*
    3 FROM 납입방법 a, 고객 b
    4 WHERE b.납입방법코드 = a.납입방법코드
    5 AND b.거주지역 = '부산'
    ;

...

SQL> @XPLAN

























IdOperationNameStartsE-RowsA-RowsA-TimeBuffers

























0SELECT STATEMENT11000000:00:00.152276
1NESTED LOOPS11000000:00:00.152276
2NESTED LOOPS110000100K00:00:00.05871
3TABLE ACCESS FULL납입방13300:00:00.015
  • 4
INDEX RANGE SCANIDX_2333333100K00:00:00.03866
  • 5
TABLE ACCESS BY INDEX ROWID고객100K33331000000:00:00.061405

























Predicate Information (identified by operation id):













---

4 - access("B"."납입방법코드"="A"."납입방법코드")
5 - filter("B"."거주지역"='부산')

{CODE}

  • 납입방법 드라이빙 - 거주지역 인덱스 컬럼에추가 ( 1만 3번 랜덤엑세스 발생 )
    {CODE:SQL}

SQL> CREATE INDEX idx_3 ON 고객(거주지역, 납입방법코드);

인덱스가 생성되었습니다.

SQL> SELECT /*+ LEADING(A) USE_NL(B) INDEX( B idx_3 ) */
2 a.납입방법, b.*
3 FROM 납입방법 a, 고객 b
4 WHERE b.납입방법코드 = a.납입방법코드
5 AND b.거주지역 = '부산'

...

SQL> @XPLAN



























-

IdOperationNameStartsE-RowsA-RowsA-TimeBuffersReads



























-

0SELECT STATEMENT11000000:00:00.03206327
1NESTED LOOPS11000000:00:00.03206327
2NESTED LOOPS1100001000000:00:00.0170327
3TABLE ACCESS FULL납입방13300:00:00.0150
  • 4
INDEX RANGE SCANIDX_3333331000000:00:00.0169827
5TABLE ACCESS BY INDEX ROWID고객1000033331000000:00:00.0113600



























-

Predicate Information (identified by operation id):













---

4 - access("B"."거주지역"='부산' AND "B"."납입방법코드"="A"."납입방법코드")

{CODE}

소트머지 조인

  • PGA상에 정렬된 집합을 통해 조인 액세스가 일어나기 때문에 랜덤 액세스 발생량 보다는 소트 부하에 의해 성능이 좌우 됨.
  • 디스크 소트가 발생할 정도의 큰 테이브을 포함할 때는 큰 테이블을 드라이빙하는 것이 더 빠르지만,
    메모리 소트 방식으로 조인할 때는 자근 쪽 테이블을 드라이빙하는 것이조금더 빠르다.

필터조건이 없을 때

  • 고객테이블 드라이빙
    {CODE:SQL}

SQL> SELECT /*+ LEADING(b) USE_MERGE(a) */
2 a.납입방법, b.*
3 FROM 납입방법 a, 고객 b
4 WHERE b.납입방법코드 = a.납입방법코드
5 ;

...

SQL> @XPLAN





























-

IdOperationNameStartsE-RowsA-RowsA-TimeBuffersOMem1MemUsed-Mem





























-

0SELECT STATEMENT1100K00:00:00.26255
1MERGE JOIN1100K100K00:00:00.26255
2SORT JOIN1100K100K00:00:00.102523738K834K3322K (0)
3TABLE ACCESS FULL고객1100K100K00:00:00.02252
  • 4
SORT JOIN100K3100K00:00:00.103204820482048 (0)
5TABLE ACCESS FULL납입13300:00:00.013





























-

Predicate Information (identified by operation id):













---

4 - access("B"."납입방법코드"="A"."납입방법코드")
filter("B"."납입방법코드"="A"."납입방법코드")

--INDEX
SELECT /*+ LEADING(b) INDEX( B PK_고객 ) USE_MERGE(a) */
a.납입방법, b.*
FROM 납입방법 a, 고객 b
WHERE b.납입방법코드 = a.납입방법코드;
































IdOperationNameStartsE-RowsA-RowsA-TimeBuffersOMem1MemUsed-Mem
































0SELECT STATEMENT1100K00:00:00.31460
1MERGE JOIN1100K100K00:00:00.31460
2SORT JOIN1100K100K00:00:00.164573738K834K3322K (0)
3TABLE ACCESS BY INDEX ROWID고객1100K100K00:00:00.06457
4INDEX FULL SCANPK_고1100K100K00:00:00.02209
  • 5
SORT JOIN100K3100K00:00:00.093204820482048 (0)
6TABLE ACCESS FULL납입방13300:00:00.013
































Predicate Information (identified by operation id):













---

5 - access("B"."납입방법코드"="A"."납입방법코드")
filter("B"."납입방법코드"="A"."납입방법코드")

{CODE}

  • 납입방법 드라이빙
    {CODE:SQL}

SQL> SELECT /*+ LEADING(A) USE_MERGE(B) */
2 a.납입방법, b.*
3 FROM 납입방법 a, 고객 b
4 WHERE b.납입방법코드 = a.납입방법코드
5 ;

...

SQL> @XPLAN





























-

IdOperationNameStartsE-RowsA-RowsA-TimeBuffersOMem1MemUsed-Mem





























-

0SELECT STATEMENT1100K00:00:00.11255
1MERGE JOIN1100K100K00:00:00.11255
2SORT JOIN13300:00:00.013204820482048 (0)
3TABLE ACCESS FULL납입13300:00:00.013
  • 4
SORT JOIN3100K100K00:00:00.092523738K834K3322K (0)
5TABLE ACCESS FULL고객1100K100K00:00:00.02252





























-

Predicate Information (identified by operation id):













---

4 - access("B"."납입방법코드"="A"."납입방법코드")
filter("B"."납입방법코드"="A"."납입방법코드")

--INDEX
SELECT /*+ LEADING(A) INDEX( A PK_납입방법 ) USE_MERGE(B) */
a.납입방법, b.*
FROM 납입방법 a, 고객 b
WHERE b.납입방법코드 = a.납입방법코드


































--

IdOperationNameStartsE-RowsA-RowsA-TimeBuffersReadsOMem1MemUsed-Mem


































--

0SELECT STATEMENT1100K00:00:00.132581
1MERGE JOIN1100K100K00:00:00.132581
2TABLE ACCESS BY INDEX ROWID납입방법13300:00:00.0261
3INDEX FULL SCANPK_납입13300:00:00.0231
  • 4
SORT JOIN3100K100K00:00:00.0825203738K834K3322K (0)
5TABLE ACCESS FULL고객1100K100K00:00:00.022520


































--

Predicate Information (identified by operation id):













---

4 - access("B"."납입방법코드"="A"."납입방법코드")
filter("B"."납입방법코드"="A"."납입방법코드")

{CODE}

해시 조인의 경우

  • Hash Area에 Build Input을 모두 채울 수 있느냐가 관건이므로 두말할 것도 없이 작은 쪽 테이블을 드라이빙하는 것이 유리하다.
    {CODE:SQL}

SQL> SELECT /*+ LEADING(b) USE_HASH(a) */
2 a.납입방법, b.*
3 FROM 납입방법 a, 고객 b
4 WHERE b.납입방법코드 = a.납입방법코드
5 ;
@
...

SQL> @XPLAN





























IdOperationNameStartsE-RowsA-RowsA-TimeBuffersOMem1MemUsed-Mem





























0SELECT STATEMENT1100K00:00:00.10257
  • 1
HASH JOIN1100K100K00:00:00.102574544K1485K8969K (0)
2TABLE ACCESS FULL고객1100K100K00:00:00.02252
3TABLE ACCESS FULL납입13300:00:00.015





























Predicate Information (identified by operation id):













---

1 - access("B"."납입방법코드"="A"."납입방법코드")

-- 카운터 방식
SELECT /*+ LEADING(b) USE_HASH(a) */
-- a.납입방법, b.*
COUNT(*)
FROM 납입방법 a, 고객 b
WHERE b.납입방법코드 = a.납입방법코드
;

































IdOperationNameStartsE-RowsA-RowsA-TimeBuffersReadsOMem1MemUsed-Mem

































0SELECT STATEMENT1100:00:00.27205198
1SORT AGGREGATE11100:00:00.27205198
  • 2
HASH JOIN1100K100K00:00:00.262051983411K2022K7766K (0)
3INDEX FAST FULL SCANIDX_21100K100K00:00:00.19204198
4INDEX FULL SCANPK_납입13300:00:00.0110

































Predicate Information (identified by operation id):













---

2 - access("B"."납입방법코드"="A"."납입방법코드")

{CODE}

  • 납입방법 드라이빙 ( INDEX : IDX_2( 납입방법코드 ) ON 고객 )
    {CODE:SQL}

SQL> SELECT /*+ LEADING(A) USE_HASH(B) */
2 a.납입방법, b.*
3 FROM 납입방법 a, 고객 b
4 WHERE b.납입방법코드 = a.납입방법코드
5 ;
@
...

SQL> @XPLAN





























IdOperationNameStartsE-RowsA-RowsA-TimeBuffersOMem1MemUsed-Mem





























0SELECT STATEMENT1100K00:00:00.116869
  • 1
HASH JOIN1100K100K00:00:00.1168691180K1180K608K (0)
2TABLE ACCESS FULL납입13300:00:00.013
3TABLE ACCESS FULL고객1100K100K00:00:00.046866





























Predicate Information (identified by operation id):













---

1 - access("B"."납입방법코드"="A"."납입방법코드")

--카운터방식
SELECT /*+ LEADING(A) USE_HASH(B) */
COUNT(*)
FROM 납입방법 a, 고객 b
WHERE b.납입방법코드 = a.납입방법코드
;






























---

IdOperationNameStartsE-RowsA-RowsA-TimeBuffersOMem1MemUsed-Mem






























---

0SELECT STATEMENT1100:00:00.09205
1SORT AGGREGATE11100:00:00.09205
  • 2
HASH JOIN1100K100K00:00:00.082051517K1517K870K (0)
3INDEX FULL SCANPK_납입13300:00:00.011
4INDEX FAST FULL SCANIDX_21100K100K00:00:00.02204






























---

Predicate Information (identified by operation id):













---

2 - access("B"."납입방법코드"="A"."납입방법코드")

--INDEX FFS
SELECT /*+ LEADING(A) USE_HASH(B) */
B.납입방법코드
FROM 납입방법 a, 고객 b
WHERE b.납입방법코드 = a.납입방법코드






























--

IdOperationNameStartsE-RowsA-RowsA-TimeBuffersOMem1MemUsed-Mem






























--

0SELECT STATEMENT1100K00:00:00.106859
  • 1
HASH JOIN1100K100K00:00:00.1068591517K1517K890K (0)
2INDEX FULL SCANPK_납입13300:00:00.011
3INDEX FAST FULL SCANIDX_21100K100K00:00:00.036858






























--

Predicate Information (identified by operation id):













---

1 - access("B"."납입방법코드"="A"."납입방법코드")

{CODE}