[SQL] SQL Booster - JOIN과 성능



7. JOIN과 성능

7.1 조인의 내부적인 처리 방식

7.1.1 조인의 3가지 처리 방식

조인

  • 이너-조인
  • 아우터-조인
  • 카테시안-조인


조인의 내부적인 처리 방식

  • NESTED LOOPS JOIN
  • MERGE JOIN
  • HASH JOIN




7.1.2 NESTED LOOPS JOIN

: NL조인, 중첩된 반복문 형태

  • 선행 집합과 후행 집합의 정의가 매우 중요
  • 힌트 LEADING 사용
  • 필요한 부분에 인덱스를 정확히 만들어 주면, 가장 적은 비용으로 빠르게 조인 결과를 얻을 수 있음
  • 많은 양의 데이터를 조인하기에는 한계가 있음


실행계획 예시

"SQL_ID  0j6rqjda79y77, child number 0"
-------------------------------------
SELECT  /*+ GATHER_PLAN_STATISTICS LEADING(T1) USE_NL(T2) */    
"T1.RGN_ID ,T1.CUS_ID ,T1.CUS_NM    ,T2.ORD_DT ,T2.ORD_ST ,T2.ORD_AMT  "
"FROM    M_CUS T1    ,T_ORD T2  WHERE   T1.CUS_ID = T2.CUS_ID"
 
Plan hash value: 1392340618
 
-----------------------------------------------------------------------------------------------
| Id  | Operation          | Name  | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |       |      1 |        |    501 |00:00:00.06 |     357 |     27 |
|   1 |  NESTED LOOPS      |       |      1 |   3082 |    501 |00:00:00.06 |     357 |     27 |
|   2 |   TABLE ACCESS FULL| M_CUS |      1 |     90 |     15 |00:00:00.06 |       9 |      6 |
|*  3 |   TABLE ACCESS FULL| T_ORD |     15 |     34 |    501 |00:00:00.01 |     348 |     21 |
-----------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
"   3 - filter(""T1"".""CUS_ID""=""T2"".""CUS_ID"")"
 

  • Starts → NL 조인에서 후행 집합에 접근한 횟수




7.1.3 MERGE JOIN

: 두 데이터 집합을 연결 조건 값으로 정렬한 후 조인을 처리

  • USE_MERGE 힌트를 이용함
  • 머지 조인은 테이블에 반복해서 접근할 필요가 없음 (조인할 데이터를 먼저 정렬해서 한번만 읽음)
  • 소트 작업을 어떻게 줄이느냐가 성능 향상의 주요 포인트~


실행계획 예시

"SQL_ID  18a5gm8p95p4t, child number 0"
-------------------------------------
SELECT /*+ GATHER_PLAN_STATISTICS LEADING(T1) USE_MERGE(T2) */    
"T1.RGN_ID ,T1.CUS_ID ,T1.CUS_NM    ,T2.ORD_DT ,T2.ORD_ST ,T2.ORD_AMT  "
"FROM    M_CUS T1    ,T_ORD T2  WHERE   T1.CUS_ID = T2.CUS_ID"
 
Plan hash value: 954095496
 
---------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name     | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |  OMem |  1Mem | Used-Mem |
---------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |          |      1 |        |    501 |00:00:00.01 |      33 |      1 |       |       |          |
|   1 |  MERGE JOIN                  |          |      1 |   3082 |    501 |00:00:00.01 |      33 |      1 |       |       |          |
|   2 |   TABLE ACCESS BY INDEX ROWID| M_CUS    |      1 |     90 |     15 |00:00:00.01 |      10 |      1 |       |       |          |
|   3 |    INDEX FULL SCAN           | PK_M_CUS |      1 |     90 |     15 |00:00:00.01 |       5 |      1 |       |       |          |
|*  4 |   SORT JOIN                  |          |     15 |   3047 |    501 |00:00:00.01 |      23 |      0 |   178K|   178K|  158K (0)|
|   5 |    TABLE ACCESS FULL         | T_ORD    |      1 |   3047 |   3047 |00:00:00.01 |      23 |      0 |       |       |          |
---------------------------------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
"   4 - access(""T1"".""CUS_ID""=""T2"".""CUS_ID"")"
"       filter(""T1"".""CUS_ID""=""T2"".""CUS_ID"")"

  • MERGE JOIN 의 자식 단계
    • INDEX FULL SCAN → 인덱스 리프 블록을 처음부터 끝까지 차례대로 읽는 작업으로, PK_M_CUS를 CUS_ID를 정렬한 것과 같은 결과를 얻음
    • SORT JOIN → TABLE ACCESS FULL 결과를 위해 정렬하는 작업




7.1.4 HASH JOIN

  • 조인 성능 문제 대부분이 해시 조인으로 해결되는 경우가 많음. 하지만 다른 방식보다 많은 CPU와 메모리 자원을 사용하기에 무조건 해시 조인으로 해결하면 안된다.
  • 해시함수를 이용한 처리 방식


실행계획 예시

"SQL_ID  2vckg3w0twy29, child number 0"
-------------------------------------
SELECT /*+ GATHER_PLAN_STATISTICS LEADING(T1) USE_HASH(T2) */    
"T1.RGN_ID ,T1.CUS_ID ,T1.CUS_NM    ,T2.ORD_DT ,T2.ORD_ST ,T2.ORD_AMT  "
"FROM    M_CUS T1    ,T_ORD T2  WHERE   T1.CUS_ID = T2.CUS_ID"
 
Plan hash value: 3240201901
 
-----------------------------------------------------------------------------------------------------------------
| Id  | Operation          | Name  | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem |
-----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |       |      1 |        |    501 |00:00:00.01 |      18 |       |       |          |
|*  1 |  HASH JOIN         |       |      1 |   3082 |    501 |00:00:00.01 |      18 |   990K|   990K| 1269K (0)|
|   2 |   TABLE ACCESS FULL| M_CUS |      1 |     90 |     90 |00:00:00.01 |       7 |       |       |          |
|   3 |   TABLE ACCESS FULL| T_ORD |      1 |   3047 |    501 |00:00:00.01 |      11 |       |       |          |
-----------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
"   1 - access(""T1"".""CUS_ID""=""T2"".""CUS_ID"")"
 








7.2 NL 조인과 성능

7.2.2 후행 집합에 필요한 인덱스

  • 후행 테이블 쪽의 조인 조건 컬럼에 인덱스가 필수다.
"SQL_ID  64rutf4xk2sjd, child number 0"
-------------------------------------
"SELECT  /*+ GATHER_PLAN_STATISTICS */    T1.CUS_ID  ,MAX(T1.CUS_NM) "
"CUS_NM ,MAX(T1.CUS_GD) CUS_GD ,COUNT(*) ORD_CNT    ,SUM(T2.ORD_QTY * "
"T2.UNT_PRC) ORD_AMT  FROM    M_CUS T1    ,T_ORD_JOIN T2  WHERE   "
T1.CUS_ID = T2.CUS_ID  AND     T1.CUS_ID = 'CUS_0009'  AND     
T2.ORD_YMD = '20170218'  GROUP BY T1.CUS_ID
 
Plan hash value: 1756537872
 
------------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name       | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |            |      1 |        |      1 |00:00:00.20 |   26468 |
|   1 |  SORT GROUP BY NOSORT         |            |      1 |      1 |      1 |00:00:00.20 |   26468 |
|   2 |   NESTED LOOPS                |            |      1 |    363 |   2000 |00:00:00.04 |   26468 |
|   3 |    TABLE ACCESS BY INDEX ROWID| M_CUS      |      1 |      1 |      1 |00:00:00.01 |       2 |
|*  4 |     INDEX UNIQUE SCAN         | PK_M_CUS   |      1 |      1 |      1 |00:00:00.01 |       1 |
|*  5 |    TABLE ACCESS FULL          | T_ORD_JOIN |      1 |    363 |   2000 |00:00:00.04 |   26466 |
------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
"   4 - access(""T1"".""CUS_ID""='CUS_0009')"
"   5 - filter((""T2"".""CUS_ID""='CUS_0009' AND ""T2"".""ORD_YMD""='20170218'))"
 
Note
-----
   - dynamic sampling used for this statement (level=2)


후행 테이블에 CUS_ID 인덱스 걸고 실행 계획

"SQL_ID  2r1tcta4ru6j7, child number 0"
-------------------------------------
SELECT  /*+ GATHER_PLAN_STATISTICS LEADING(T1) USE_NL(T2) INDEX(T2 
"X_T_ORD_JOIN_1) */    T1.CUS_ID ,MAX(T1.CUS_NM) CUS_NM ,MAX(T1.CUS_GD) "
"CUS_GD ,COUNT(*) ORD_CNT    ,SUM(T2.ORD_QTY * T2.UNT_PRC) ORD_AMT  "
"FROM    M_CUS T1    ,T_ORD_JOIN T2  WHERE   T1.CUS_ID = T2.CUS_ID  "
AND     T1.CUS_ID = 'CUS_0009'  AND     T2.ORD_YMD = '20170218'  
GROUP BY T1.CUS_ID
 
Plan hash value: 1352034644
 
-------------------------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
-------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |                |      1 |        |      1 |00:00:00.12 |     646 |    155 |
|   1 |  SORT GROUP BY NOSORT         |                |      1 |      1 |      1 |00:00:00.12 |     646 |    155 |
|   2 |   NESTED LOOPS                |                |      1 |    363 |   2000 |00:00:00.03 |     646 |    155 |
|   3 |    TABLE ACCESS BY INDEX ROWID| M_CUS          |      1 |      1 |      1 |00:00:00.01 |       2 |      0 |
|*  4 |     INDEX UNIQUE SCAN         | PK_M_CUS       |      1 |      1 |      1 |00:00:00.01 |       1 |      0 |
|*  5 |    TABLE ACCESS BY INDEX ROWID| T_ORD_JOIN     |      1 |    363 |   2000 |00:00:00.03 |     644 |    155 |
|*  6 |     INDEX RANGE SCAN          | X_T_ORD_JOIN_1 |      1 |  51637 |  55000 |00:00:00.09 |     156 |    155 |
-------------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
"   4 - access(""T1"".""CUS_ID""='CUS_0009')"
"   5 - filter(""T2"".""ORD_YMD""='20170218')"
"   6 - access(""T2"".""CUS_ID""='CUS_0009')"
 
Note
-----
   - dynamic sampling used for this statement (level=2)
 

  • 오히려 더 느려짐
    • T_ORD_JOIN TABLE ACCESS 할때, INDEX RANGE SCAN 55000건 읽어와서 2000건만 사용해서 그럼
  • 해결방법
    • ORD_YMD까지 복합인덱스로 건다.


후행 테이블에 CUS_ID, ORD_YMD 인덱스 걸고 실행 계획

"SQL_ID  aw25899rc9ryw, child number 0"
-------------------------------------
SELECT  /*+ GATHER_PLAN_STATISTICS LEADING(T1) USE_NL(T2) INDEX(T2 
"X_T_ORD_JOIN_2) */    T1.CUS_ID ,MAX(T1.CUS_NM) CUS_NM ,MAX(T1.CUS_GD) "
"CUS_GD ,COUNT(*) ORD_CNT    ,SUM(T2.ORD_QTY * T2.UNT_PRC) ORD_AMT  "
"FROM    M_CUS T1      ,T_ORD_JOIN T2  WHERE   T1.CUS_ID = T2.CUS_ID  "
AND     T1.CUS_ID = 'CUS_0009'  AND     T2.ORD_YMD = '20170218'  
GROUP BY T1.CUS_ID
 
Plan hash value: 802225516
 
-------------------------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
-------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |                |      1 |        |      1 |00:00:00.05 |      29 |     10 |
|   1 |  SORT GROUP BY NOSORT         |                |      1 |      1 |      1 |00:00:00.05 |      29 |     10 |
|   2 |   NESTED LOOPS                |                |      1 |    363 |   2000 |00:00:00.05 |      29 |     10 |
|   3 |    TABLE ACCESS BY INDEX ROWID| M_CUS          |      1 |      1 |      1 |00:00:00.01 |       2 |      0 |
|*  4 |     INDEX UNIQUE SCAN         | PK_M_CUS       |      1 |      1 |      1 |00:00:00.01 |       1 |      0 |
|   5 |    TABLE ACCESS BY INDEX ROWID| T_ORD_JOIN     |      1 |    363 |   2000 |00:00:00.05 |      27 |     10 |
|*  6 |     INDEX RANGE SCAN          | X_T_ORD_JOIN_2 |      1 |     58 |   2000 |00:00:00.05 |      11 |     10 |
-------------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
"   4 - access(""T1"".""CUS_ID""='CUS_0009')"
"   6 - access(""T2"".""CUS_ID""='CUS_0009' AND ""T2"".""ORD_YMD""='20170218')"
 
Note
-----
   - dynamic sampling used for this statement (level=2)

  • Buffers 가 29로 개선된 것을 확인할 수 있다.

결론

  • 후행 집합의 조인 조건 컬럼에는 인덱스가 필수
  • 후행 집합에 사용된 조인 조건과 WHERE 조건 컬럼에 복합 인덱스를 고려해야 한다.




7.2.3 선행 집합 변경에 따른 쿼리 변형

선행 집합을 T_ORD_JOIN 으로 하고 실행계획

"SQL_ID  4jh305fymk6gj, child number 0"
-------------------------------------
SELECT  /*+ GATHER_PLAN_STATISTICS LEADING(T2) USE_NL(T1) INDEX(T2 
"X_T_ORD_JOIN_2) */    T1.CUS_ID ,MAX(T1.CUS_NM) CUS_NM ,MAX(T1.CUS_GD) "
"CUS_GD ,COUNT(*) ORD_CNT    ,SUM(T2.ORD_QTY * T2.UNT_PRC) ORD_AMT  "
"FROM    M_CUS T1      ,T_ORD_JOIN T2  WHERE   T1.CUS_ID = T2.CUS_ID  "
AND     T1.CUS_ID = 'CUS_0009'  AND     T2.ORD_YMD = '20170218'  
GROUP BY T1.CUS_ID
 
Plan hash value: 2519304977
 
--------------------------------------------------------------------------------------------------------------------
| Id  | Operation                      | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
--------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |                |      1 |        |      1 |00:00:00.04 |    2031 |     16 |
|   1 |  SORT GROUP BY NOSORT          |                |      1 |      1 |      1 |00:00:00.04 |    2031 |     16 |
|   2 |   NESTED LOOPS                 |                |      1 |        |   2000 |00:00:00.18 |    2031 |     16 |
|   3 |    NESTED LOOPS                |                |      1 |    170 |   2000 |00:00:00.03 |      31 |     16 |
|   4 |     TABLE ACCESS BY INDEX ROWID| T_ORD_JOIN     |      1 |    170 |   2000 |00:00:00.03 |      27 |     16 |
|*  5 |      INDEX RANGE SCAN          | X_T_ORD_JOIN_2 |      1 |    170 |   2000 |00:00:00.01 |      11 |      0 |
|*  6 |     INDEX UNIQUE SCAN          | PK_M_CUS       |   2000 |      1 |   2000 |00:00:00.01 |       4 |      0 |
|   7 |    TABLE ACCESS BY INDEX ROWID | M_CUS          |   2000 |      1 |   2000 |00:00:00.01 |    2000 |      0 |
--------------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
"   5 - access(""T2"".""CUS_ID""='CUS_0009' AND ""T2"".""ORD_YMD""='20170218')"
"   6 - access(""T1"".""CUS_ID""='CUS_0009')"
 

  • 쿼리에서 T_ORD_JOIN에는 CUS_ID 조건을 주지도 않았는데 알아서 T2.CUS_ID = ‘CUS_0009’ 를 가져옴 → 쿼리 변형 이라고 함
    • 변형된 이유 : 실행 결과에 전혀 영향이 없고, 변형이 성능에 더 좋음




7.2.4 조인 횟수를 줄이자#1

X_T_ORD_JOIN_2 → CUS_ID, ORD_YMD

"SQL_ID  55u3gdh55w3sg, child number 0"
-------------------------------------
SELECT  /*+ GATHER_PLAN_STATISTICS LEADING(T2) USE_NL(T1) INDEX(T2 
"X_T_ORD_JOIN_2) */    T1.CUS_ID ,MAX(T1.CUS_NM) CUS_NM ,MAX(T1.CUS_GD) "
"CUS_GD ,COUNT(*) ORD_CNT    ,SUM(T2.ORD_QTY * T2.UNT_PRC) ORD_AMT  "
"FROM    M_CUS T1    ,T_ORD_JOIN T2  
WHERE   T1.CUS_ID = T2.CUS_ID  "
AND     T2.ORD_YMD = '20170218'  
AND     T1.CUS_GD = 'A'  
GROUP BY 
T1.CUS_ID
 
Plan hash value: 4291467870
 
--------------------------------------------------------------------------------------------------------------------
| Id  | Operation                      | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
--------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |                |      1 |        |      6 |00:00:00.13 |   12384 |     88 |
|   1 |  SORT GROUP BY NOSORT          |                |      1 |     60 |      6 |00:00:00.13 |   12384 |     88 |
|   2 |   NESTED LOOPS                 |                |      1 |        |   9000 |00:00:00.02 |   12384 |     88 |
|   3 |    NESTED LOOPS                |                |      1 |   6149 |  12000 |00:00:00.02 |     384 |     88 |
|   4 |     TABLE ACCESS BY INDEX ROWID| T_ORD_JOIN     |      1 |   9121 |  12000 |00:00:00.01 |     380 |     88 |
|*  5 |      INDEX SKIP SCAN           | X_T_ORD_JOIN_2 |      1 |   9121 |  12000 |00:00:00.01 |     282 |      6 |
|*  6 |     INDEX UNIQUE SCAN          | PK_M_CUS       |  12000 |      1 |  12000 |00:00:00.01 |       4 |      0 |
|*  7 |    TABLE ACCESS BY INDEX ROWID | M_CUS          |  12000 |      1 |   9000 |00:00:00.01 |   12000 |      0 |
--------------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
"   5 - access(""T2"".""ORD_YMD""='20170218')"
"       filter(""T2"".""ORD_YMD""='20170218')"
"   6 - access(""T1"".""CUS_ID""=""T2"".""CUS_ID"")"
"   7 - filter(""T1"".""CUS_GD""='A')"

  • INDEX SKIP SCAN : 인덱스를 이용해 데이터를 검색하는 방법 중 하나


[참고] 인덱스 스캔 종류

  1. INDEX RANGE SCAN : INDEX 값이 중복을 허용할 때, 2번 이상 범위를 탐색하는 방법
  2. INDEX UNIQUE SCAN : INDEX 값이 하나만 존재할때 한번만 조회하는 방법
  3. INDEX SKIP SCAN : 인덱스의 첫 컬럼이 WHERE에 있지 않아도, 인덱스를 이용할 수 있는 방법
  4. INDEX FULL SCAN : SELECT 컬럼이 모두 인덱스로 있을 때
  5. INDEX FAST FULL SCAN : FULL SCAN에서 싱글I/O 방식이 아닌 멀티블록I/O 방식으로 조회
  6. INDEX MERGE SCAN : 여러 개의 인덱스 사용
  7. INDEX BITMAP MERGE SCAN : 여러 개의 인덱스를 비트맵 인덱스로 변환해서 스캔 (INDEX MERGE SCAN 보다 우수한 성능)


후행 집합의 접근 횟수

  • M_CUS - CUS_GD = ‘A’ 인 데이터 → 60건
  • T_ORD_JOIN - ORD_YMD = ‘20170218’ 인 데이터 → 12,000건

⇒ M_CUS를 선행으로 하면 T_ORD_JOIN에 60번만 접근 하는거임

"SQL_ID  d0mkxka3juy5t, child number 0"
-------------------------------------
SELECT  /*+ GATHER_PLAN_STATISTICS LEADING(T1) USE_NL(T2) INDEX(T2 
"X_T_ORD_JOIN_2) */    T1.CUS_ID ,MAX(T1.CUS_NM) CUS_NM ,MAX(T1.CUS_GD) "
"CUS_GD ,COUNT(*) ORD_CNT    ,SUM(T2.ORD_QTY * T2.UNT_PRC) ORD_AMT  "
"FROM    M_CUS T1    ,T_ORD_JOIN T2  WHERE   T1.CUS_ID = T2.CUS_ID  "
AND     T2.ORD_YMD = '20170218'  AND     T1.CUS_GD = 'A'  GROUP BY 
T1.CUS_ID
 
Plan hash value: 222718093
 
-----------------------------------------------------------------------------------------------------------
| Id  | Operation                      | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |                |      1 |        |      6 |00:00:00.01 |     235 |
|   1 |  SORT GROUP BY NOSORT          |                |      1 |     60 |      6 |00:00:00.01 |     235 |
|   2 |   NESTED LOOPS                 |                |      1 |        |   9000 |00:00:00.01 |     235 |
|   3 |    NESTED LOOPS                |                |      1 |   6149 |   9000 |00:00:00.01 |     161 |
|*  4 |     TABLE ACCESS BY INDEX ROWID| M_CUS          |      1 |     60 |     60 |00:00:00.01 |       3 |
|   5 |      INDEX FULL SCAN           | PK_M_CUS       |      1 |     90 |     90 |00:00:00.01 |       1 |
|*  6 |     INDEX RANGE SCAN           | X_T_ORD_JOIN_2 |     60 |    101 |   9000 |00:00:00.01 |     158 |
|   7 |    TABLE ACCESS BY INDEX ROWID | T_ORD_JOIN     |   9000 |    102 |   9000 |00:00:00.01 |      74 |
-----------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
"   4 - filter(""T1"".""CUS_GD""='A')"
"   6 - access(""T1"".""CUS_ID""=""T2"".""CUS_ID"" AND ""T2"".""ORD_YMD""='20170218')"
 

⇒ 6번에서 Starts가 60이 됨

결론 : 조인 횟수를 줄이고, 선행집합 선정에 카운트 하는 방식을 사용하면 좋다.

  • 선행집합을 데이터 적은걸 잡아야 한다.




7.3 MERGE 조인과 성능

7.3.1 대량의 데이터 처리

: 머지 조인은 대량의 데이터를 조인할 때 적합

T_ORD_BIG 테이블을 NL 조인

"SQL_ID  a319wbtvb7mzx, child number 0"
-------------------------------------
SELECT  /*+ GATHER_PLAN_STATISTICS LEADING(T2) USE_NL(T1) FULL(T2) */  
"  T1.CUS_ID ,MAX(T1.CUS_NM) CUS_NM ,MAX(T1.CUS_GD) CUS_GD ,COUNT(*) "
"ORD_CNT    ,SUM(T2.ORD_AMT) ORD_AMT    ,SUM(SUM(T2.ORD_AMT)) OVER() "
"TTL_ORD_AMT  FROM    M_CUS T1    ,T_ORD_BIG T2  WHERE   T1.CUS_ID = "
T2.CUS_ID  AND     T2.ORD_YMD LIKE '201702%'  GROUP BY T1.CUS_ID
 
Plan hash value: 4214578612
 
------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                      | Name      | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |  OMem |  1Mem | Used-Mem |
------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |           |      1 |        |     72 |00:00:18.71 |    2238K|    258K|       |       |          |
|   1 |  WINDOW BUFFER                 |           |      1 |     90 |     72 |00:00:18.71 |    2238K|    258K|  6144 |  6144 | 6144  (0)|
|   2 |   HASH GROUP BY                |           |      1 |     90 |     72 |00:00:18.71 |    2238K|    258K|   726K|   726K| 4980K (0)|
|   3 |    NESTED LOOPS                |           |      1 |        |   1980K|00:00:36.74 |    2238K|    258K|       |       |          |
|   4 |     NESTED LOOPS               |           |      1 |   1799K|   1980K|00:00:35.40 |     258K|    258K|       |       |          |
|*  5 |      TABLE ACCESS FULL         | T_ORD_BIG |      1 |   1799K|   1980K|00:00:34.01 |     258K|    258K|       |       |          |
|*  6 |      INDEX UNIQUE SCAN         | PK_M_CUS  |   1980K|      1 |   1980K|00:00:00.78 |       4 |      0 |       |       |          |
|   7 |     TABLE ACCESS BY INDEX ROWID| M_CUS     |   1980K|      1 |   1980K|00:00:00.82 |    1980K|      0 |       |       |          |
------------------------------------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
"   5 - filter(""T2"".""ORD_YMD"" LIKE '201702%')"
"   6 - access(""T1"".""CUS_ID""=""T2"".""CUS_ID"")"
 

  • Buffers가 2238K 나옴


T_ORD_BIG, 머지조인 사용

"SQL_ID  4h89vg74pgwh3, child number 0"
-------------------------------------
SELECT  /*+ GATHER_PLAN_STATISTICS LEADING(T1) USE_MERGE(T2) FULL(T2) 
"*/    T1.CUS_ID ,MAX(T1.CUS_NM) CUS_NM ,MAX(T1.CUS_GD) CUS_GD "
",COUNT(*) ORD_CNT    ,SUM(T2.ORD_AMT) ORD_AMT    "
",SUM(SUM(T2.ORD_AMT)) OVER() TTL_ORD_AMT  FROM    M_CUS T1    "
",T_ORD_BIG T2  WHERE   T1.CUS_ID = T2.CUS_ID  AND     T2.ORD_YMD LIKE "
'201702%'  GROUP BY T1.CUS_ID
 
Plan hash value: 3683264484
 
------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                      | Name      | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |  OMem |  1Mem | Used-Mem |
------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |           |      1 |        |     72 |00:00:19.77 |     258K|    258K|       |       |          |
|   1 |  WINDOW BUFFER                 |           |      1 |     90 |     72 |00:00:19.77 |     258K|    258K|  6144 |  6144 | 6144  (0)|
|   2 |   SORT GROUP BY NOSORT         |           |      1 |     90 |     72 |00:00:19.82 |     258K|    258K|       |       |          |
|   3 |    MERGE JOIN                  |           |      1 |   1799K|   1980K|00:00:19.76 |     258K|    258K|       |       |          |
|   4 |     TABLE ACCESS BY INDEX ROWID| M_CUS     |      1 |     90 |     90 |00:00:00.01 |       3 |      0 |       |       |          |
|   5 |      INDEX FULL SCAN           | PK_M_CUS  |      1 |     90 |     90 |00:00:00.01 |       1 |      0 |       |       |          |
|*  6 |     SORT JOIN                  |           |     90 |   1799K|   1980K|00:00:19.60 |     258K|    258K|    66M|  2819K|   58M (0)|
|*  7 |      TABLE ACCESS FULL         | T_ORD_BIG |      1 |   1799K|   1980K|00:00:37.05 |     258K|    258K|       |       |          |
------------------------------------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
"   6 - access(""T1"".""CUS_ID""=""T2"".""CUS_ID"")"
"       filter(""T1"".""CUS_ID""=""T2"".""CUS_ID"")"
"   7 - filter(""T2"".""ORD_YMD"" LIKE '201702%')"
 

  • Buffers가 258K로 개선됨
  • 처리순서
    • 5 : PK_M_CUS 를 INDEX FULL SCAN으로 읽음 (CUS_ID 순서대로)
    • 4 : ROWID를 이용해 M_CUS에 접근
    • 7 : T_ORD_BIG 을 FULL SCAN 하는데 filter 조건
      • 가져온걸 cus_id 순서로 정렬
    • M_CUS와 T_ORD_BIG을 머지
  • NL 조인과 차이점 : 후행 테이블을 반복해서 다시 읽지 않는다.




7.4 HASH 조인과 성능

7.4.1 대량의 데이터 처리

  • 머지 조인의 단점 : 데이터를 정렬해야하는 부담

    → 해쉬 조인은 이 단점을 해결함

  • 해쉬 조인의 단점 : CPU와 메모리에 추가적인 부하가 발생한다.

"SQL_ID  7tdby1j1tk4r0, child number 0"
-------------------------------------
SELECT  /*+ GATHER_PLAN_STATISTICS LEADING(T1) USE_MERGE(T2) */    
"T1.CUS_ID ,MAX(T1.CUS_NM) CUS_NM ,MAX(T1.CUS_GD) CUS_GD    ,COUNT(*) "
"ORD_CNT ,SUM(T2.ORD_AMT) ORD_AMT ,SUM(SUM(T2.ORD_AMT)) OVER() "
"TTL_ORD_AMT  FROM    M_CUS T1    ,T_ORD_BIG T2  WHERE   T1.CUS_ID = "
T2.CUS_ID  GROUP BY T1.CUS_ID
 
Plan hash value: 3683264484
 
-------------------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                      | Name      | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  | Writes |  OMem |  1Mem | Used-Mem | Used-Tmp|
-------------------------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |           |      1 |        |     90 |00:01:13.87 |     258K|    404K|    146K|       |       |          |         |
|   1 |  WINDOW BUFFER                 |           |      1 |     90 |     90 |00:01:13.87 |     258K|    404K|    146K|  6144 |  6144 | 6144  (0)|         |
|   2 |   SORT GROUP BY NOSORT         |           |      1 |     90 |     90 |00:01:15.23 |     258K|    404K|    146K|       |       |          |         |
|   3 |    MERGE JOIN                  |           |      1 |     30M|     30M|00:01:01.44 |     258K|    404K|    146K|       |       |          |         |
|   4 |     TABLE ACCESS BY INDEX ROWID| M_CUS     |      1 |     90 |     90 |00:00:00.01 |       3 |      0 |      0 |       |       |          |         |
|   5 |      INDEX FULL SCAN           | PK_M_CUS  |      1 |     90 |     90 |00:00:00.01 |       1 |      0 |      0 |       |       |          |         |
|*  6 |     SORT JOIN                  |           |     90 |     30M|     30M|00:00:57.98 |     258K|    404K|    146K|   644M|  8328K|  171M (1)|     576K|
|   7 |      TABLE ACCESS FULL         | T_ORD_BIG |      1 |     30M|     30M|00:00:14.15 |     258K|    258K|      0 |       |       |          |         |
-------------------------------------------------------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
"   6 - access(""T1"".""CUS_ID""=""T2"".""CUS_ID"")"
"       filter(""T1"".""CUS_ID""=""T2"".""CUS_ID"")"
 




7.4.2 빌드 입력 선택의 중요성

  • 해시 조인 성능 향상을 위해 선행 집합 선택이 중요함
  • 선행 집합 → 빌드 입력(Build-Input)으로 처리
    • 조인할 대상에 해시함수를 적용해 조인 준비
    • 빌드 입력 데이터가 적을수록 성능에 유리
  • 후행 집합 → 검증 입력(Probe-Input)으로 처리
    • 후행 집합에 해시 함수를 적용해 빌드 입력과 비교해 조인을 처리




7.4.4 어떤 조인을 사용할 것인가?

  • NL 조인 : 자주 실행되는 SQL은 NL 조인만으로 처리하는 것이 데이터베이스 전체 성능에 도움됨
  • 해시 조인 : 대량의 데이터를 조회해서 분석을 수행해야하는 상황
  • 머지 조인 : 잘 사용 안함

  • 어떤 조인을 할지는 옵티마이저가 결정함. 인덱스를 잘 구성하여 옵티마이저가 적절한 인덱스를 선택하도록 하자

다른 글